diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index f2a58e7b6a7ac..c474998e6fd3d 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/.ci/end2end.groovy b/.ci/end2end.groovy index 1615e5173849d..9da5335aa792b 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -12,7 +12,7 @@ pipeline { environment { BASE_DIR = 'src/github.com/elastic/kibana' HOME = "${env.WORKSPACE}" - E2E_DIR = 'x-pack/legacy/plugins/apm/e2e' + E2E_DIR = 'x-pack/plugins/apm/e2e' PIPELINE_LOG_LEVEL = 'DEBUG' } options { @@ -38,7 +38,7 @@ pipeline { shallow: false, reference: "/var/lib/jenkins/.git-references/kibana.git") script { dir("${BASE_DIR}"){ - def regexps =[ "^x-pack/legacy/plugins/apm/.*" ] + def regexps =[ "^x-pack/plugins/apm/.*" ] env.APM_UPDATED = isGitRegionMatch(patterns: regexps) } } diff --git a/.eslintignore b/.eslintignore index 1f22b6074e76e..4913192e81c1d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,46 +1,49 @@ -node_modules -bower_components -/data -/optimize -/build -/target +**/*.js.snap +**/graphql/types.ts /.es -/plugins +/build /built_assets +/data /html_docs -/src/plugins/data/common/es_query/kuery/ast/_generated_/** -/src/plugins/vis_type_timelion/public/_generated_/** -src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data -/src/legacy/ui/public/flot-charts +/optimize +/plugins /test/fixtures/scenarios -/src/legacy/core_plugins/console/public/webpackShims +/x-pack/build +node_modules +target + +!/.eslintrc.js + +# plugin overrides +/src/core/lib/kbn_internal_native_observable /src/legacy/core_plugins/console/public/tests/webpackShims +/src/legacy/core_plugins/console/public/webpackShims +/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken +/src/legacy/ui/public/flot-charts /src/legacy/ui/public/utils/decode_geo_hash.js +/src/plugins/data/common/es_query/kuery/ast/_generated_/** +/src/plugins/vis_type_timelion/public/_generated_/** /src/plugins/vis_type_timelion/public/webpackShims/jquery.flot.* -/src/core/lib/kbn_internal_native_observable -/packages/*/target -/packages/eslint-config-kibana -/packages/kbn-pm/dist -/packages/kbn-plugin-generator/sao_template/template -/packages/kbn-ui-framework/dist -/packages/kbn-ui-framework/doc_site/build -/packages/kbn-ui-framework/generator-kui/*/templates/ -/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ -/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ -/x-pack/legacy/plugins/maps/public/vendor/** -/x-pack/coverage -/x-pack/build /x-pack/legacy/plugins/**/__tests__/fixtures/** -/packages/kbn-interpreter/src/common/lib/grammar.js +/x-pack/legacy/plugins/apm/e2e/cypress/**/snapshots.js /x-pack/legacy/plugins/canvas/canvas_plugin +/x-pack/legacy/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/legacy/plugins/canvas/shareable_runtime/build /x-pack/legacy/plugins/canvas/storybook -/x-pack/legacy/plugins/canvas/canvas_plugin_src/lib/flot-charts +/x-pack/plugins/monitoring/public/lib/jquery_flot /x-pack/legacy/plugins/infra/common/graphql/types.ts /x-pack/legacy/plugins/infra/public/graphql/types.ts /x-pack/legacy/plugins/infra/server/graphql/types.ts -/x-pack/legacy/plugins/apm/e2e/cypress/**/snapshots.js -/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken -**/graphql/types.ts -**/*.js.snap -!/.eslintrc.js +/x-pack/legacy/plugins/maps/public/vendor/** + +# package overrides +/packages/eslint-config-kibana +/packages/kbn-interpreter/src/common/lib/grammar.js +/packages/kbn-plugin-generator/sao_template/template +/packages/kbn-pm/dist +/packages/kbn-test/src/functional_test_runner/__tests__/fixtures/ +/packages/kbn-test/src/functional_test_runner/lib/config/__tests__/fixtures/ +/packages/kbn-ui-framework/dist +/packages/kbn-ui-framework/doc_site/build +/packages/kbn-ui-framework/generator-kui/*/templates/ + diff --git a/.eslintrc.js b/.eslintrc.js index a2b8ae7622d0b..8b33ec83347a8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -96,7 +96,7 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], rules: { 'jsx-a11y/click-events-have-key-events': 'off', }, @@ -185,31 +185,43 @@ module.exports = { zones: [ { target: [ - 'src/legacy/**/*', - 'x-pack/**/*', - '!x-pack/**/*.test.*', - '!x-pack/test/**/*', + '(src|x-pack)/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', - 'src/core/(public|server)/**/*', 'examples/**/*', ], from: [ 'src/core/public/**/*', - '!src/core/public/index.ts', - '!src/core/public/mocks.ts', - '!src/core/public/*.test.mocks.ts', + '!src/core/public/index.ts', // relative import + '!src/core/public/mocks{,.ts}', + '!src/core/server/types{,.ts}', '!src/core/public/utils/**/*', + '!src/core/public/*.test.mocks{,.ts}', 'src/core/server/**/*', - '!src/core/server/index.ts', - '!src/core/server/mocks.ts', - '!src/core/server/types.ts', - '!src/core/server/test_utils.ts', + '!src/core/server/index.ts', // relative import + '!src/core/server/mocks{,.ts}', + '!src/core/server/types{,.ts}', + '!src/core/server/test_utils', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 - '!src/core/server/types', - '!src/core/server/*.test.mocks.ts', - + '!src/core/server/*.test.mocks{,.ts}', + ], + allowSameFolder: true, + errorMessage: + 'Plugins may only import from top-level public and server modules in core.', + }, + { + target: [ + '(src|x-pack)/legacy/**/*', + '(src|x-pack)/plugins/**/(public|server)/**/*', + 'examples/**/*', + '!(src|x-pack)/**/*.test.*', + '!(x-pack/)?test/**/*', + // next folder contains legacy browser tests which can't be migrated to jest + // which import np files + '!src/legacy/core_plugins/kibana/public/__tests__/**/*', + ], + from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', ], @@ -553,7 +565,7 @@ module.exports = { */ { // front end typescript and javascript files only - files: ['x-pack/legacy/plugins/siem/public/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/siem/public/**/*.{js,ts,tsx}'], rules: { 'import/no-nodejs-modules': 'error', 'no-restricted-imports': [ @@ -602,7 +614,7 @@ module.exports = { // { // // will introduced after the other warns are fixed // // typescript and javascript for front end react performance - // files: ['x-pack/legacy/plugins/siem/public/**/!(*.test).{js,ts,tsx}'], + // files: ['x-pack/plugins/siem/public/**/!(*.test).{js,ts,tsx}'], // plugins: ['react-perf'], // rules: { // // 'react-perf/jsx-no-new-object-as-prop': 'error', @@ -730,6 +742,114 @@ module.exports = { }, }, + /** + * Lists overrides + */ + { + // typescript and javascript for front and back end + files: ['x-pack/plugins/lists/**/*.{js,ts,tsx}'], + plugins: ['eslint-plugin-node'], + env: { + mocha: true, + jest: true, + }, + rules: { + 'accessor-pairs': 'error', + 'array-callback-return': 'error', + 'no-array-constructor': 'error', + complexity: 'error', + 'consistent-return': 'error', + 'func-style': ['error', 'expression'], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], + 'newlines-between': 'always', + }, + ], + 'sort-imports': [ + 'error', + { + ignoreDeclarationSort: true, + }, + ], + 'node/no-deprecated-api': 'error', + 'no-bitwise': 'error', + 'no-continue': 'error', + 'no-dupe-keys': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty-character-class': 'error', + 'no-empty-pattern': 'error', + 'no-ex-assign': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-boolean-cast': 'error', + 'no-extra-label': 'error', + 'no-func-assign': 'error', + 'no-implicit-globals': 'error', + 'no-implied-eval': 'error', + 'no-invalid-regexp': 'error', + 'no-inner-declarations': 'error', + 'no-lone-blocks': 'error', + 'no-multi-assign': 'error', + 'no-misleading-character-class': 'error', + 'no-new-symbol': 'error', + 'no-obj-calls': 'error', + 'no-param-reassign': ['error', { props: true }], + 'no-process-exit': 'error', + 'no-prototype-builtins': 'error', + 'no-return-await': 'error', + 'no-self-compare': 'error', + 'no-shadow-restricted-names': 'error', + 'no-sparse-arrays': 'error', + 'no-this-before-super': 'error', + 'no-undef': 'error', + 'no-unreachable': 'error', + 'no-unsafe-finally': 'error', + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-concat': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-escape': 'error', + 'no-useless-rename': 'error', + 'no-useless-return': 'error', + 'no-void': 'error', + 'one-var-declaration-per-line': 'error', + 'prefer-object-spread': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'error', + 'require-atomic-updates': 'error', + 'symbol-description': 'error', + 'vars-on-top': 'error', + '@typescript-eslint/explicit-member-accessibility': 'error', + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-useless-constructor': 'error', + '@typescript-eslint/unified-signatures': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-unused-vars': 'error', + 'no-template-curly-in-string': 'error', + 'sort-keys': 'error', + 'prefer-destructuring': 'error', + }, + }, + /** + * Alerting Services overrides + */ + { + // typescript only for front and back end + files: [ + 'x-pack/{,legacy/}plugins/{alerting,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', + ], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + /** * Lens overrides */ @@ -843,6 +963,12 @@ module.exports = { jquery: true, }, }, + { + files: ['x-pack/plugins/monitoring/public/lib/jquery_flot/**/*.js'], + env: { + jquery: true, + }, + }, /** * TSVB overrides diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ab05b32ab063e..3981a8e1e9afe 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,12 +11,14 @@ /src/legacy/core_plugins/kibana/public/discover/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/local_application_service/ @elastic/kibana-app /src/legacy/core_plugins/kibana/public/dev_tools/ @elastic/kibana-app -/src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app +/src/plugins/vis_type_vislib/ @elastic/kibana-app /src/plugins/vis_type_xy/ @elastic/kibana-app +/src/plugins/vis_type_table/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/vis_type_timelion/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app +/src/plugins/input_control_vis/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/vis_type_timeseries/ @elastic/kibana-app /src/plugins/vis_type_metric/ @elastic/kibana-app @@ -82,7 +84,7 @@ /x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/logs-metrics-ui @elastic/apm-ui @elastic/uptime @elastic/ingest-management /x-pack/legacy/plugins/monitoring/ @elastic/stack-monitoring-ui -/x-pack/legacy/plugins/uptime @elastic/uptime +/x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime # Machine Learning @@ -165,8 +167,6 @@ /x-pack/plugins/telemetry_collection_xpack/ @elastic/pulse # Kibana Alerting Services -/x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services -/x-pack/legacy/plugins/actions/ @elastic/kibana-alerting-services /x-pack/plugins/alerting/ @elastic/kibana-alerting-services /x-pack/plugins/actions/ @elastic/kibana-alerting-services /x-pack/plugins/event_log/ @elastic/kibana-alerting-services @@ -174,7 +174,6 @@ /x-pack/test/alerting_api_integration/ @elastic/kibana-alerting-services /x-pack/test/plugin_api_integration/plugins/task_manager/ @elastic/kibana-alerting-services /x-pack/test/plugin_api_integration/test_suites/task_manager/ @elastic/kibana-alerting-services -/x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/ @elastic/kibana-alerting-services /x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/ @elastic/kibana-alerting-services @@ -205,24 +204,24 @@ /x-pack/plugins/snapshot_restore/ @elastic/es-ui /x-pack/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/watcher/ @elastic/es-ui +/x-pack/plugins/ingest_pipelines/ @elastic/es-ui # Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team -/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team -/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team +/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/api_integration/apis/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional_endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional_endpoint_ingest_failure/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem # SIEM -/x-pack/legacy/plugins/siem/ @elastic/siem -/x-pack/plugins/siem/ @elastic/siem -/x-pack/test/detection_engine_api_integration @elastic/siem -/x-pack/test/api_integration/apis/siem @elastic/siem -/x-pack/plugins/case @elastic/siem +/x-pack/plugins/siem/ @elastic/siem @elastic/endpoint-app-team +/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team +/x-pack/test/api_integration/apis/siem @elastic/siem @elastic/endpoint-app-team +/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team +/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team # Security Intelligence And Analytics -/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics /x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 544dd577313df..89e0af270c54d 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -8,6 +8,9 @@ - "Feature:ExpressionLanguage": - "src/plugins/expressions/**/*.*" - "src/plugins/bfetch/**/*.*" + - "Team:apm" + - "x-pack/plugins/apm/**/*.*" + - "x-pack/legacy/plugins/apm/**/*.*" - "Team:uptime": - "x-pack/plugins/uptime/**/*.*" - "x-pack/legacy/plugins/uptime/**/*.*" diff --git a/.i18nrc.json b/.i18nrc.json index 4a516f23ebf05..b04c02f6b2265 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -14,7 +14,7 @@ "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", - "inputControl": "src/legacy/core_plugins/input_control_vis", + "inputControl": "src/plugins/input_control_vis", "inspector": "src/plugins/inspector", "inspectorViews": "src/legacy/core_plugins/inspector_views", "interpreter": "src/legacy/core_plugins/interpreter", @@ -48,12 +48,12 @@ "visDefaultEditor": "src/plugins/vis_default_editor", "visTypeMarkdown": "src/plugins/vis_type_markdown", "visTypeMetric": "src/plugins/vis_type_metric", - "visTypeTable": "src/legacy/core_plugins/vis_type_table", - "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", - "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], - "visTypeVega": "src/legacy/core_plugins/vis_type_vega", - "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", - "visTypeXy": "src/legacy/core_plugins/vis_type_xy", + "visTypeTable": "src/plugins/vis_type_table", + "visTypeTagCloud": "src/plugins/vis_type_tagcloud", + "visTypeTimeseries": "src/plugins/vis_type_timeseries", + "visTypeVega": "src/plugins/vis_type_vega", + "visTypeVislib": "src/plugins/vis_type_vislib", + "visTypeXy": "src/plugins/vis_type_xy", "visualizations": "src/plugins/visualizations", "visualize": "src/plugins/visualize" }, diff --git a/.sass-lint.yml b/.sass-lint.yml index 5c2c88a1dad5d..44b4d49384136 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -2,13 +2,13 @@ files: include: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - - 'src/legacy/core_plugins/vis_type_vislib/**/*.s+(a|c)ss' - - 'src/legacy/core_plugins/vis_type_xy/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' + - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' + - 'src/plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' - 'x-pack/plugins/lens/**/*.s+(a|c)ss' + - 'x-pack/plugins/cross_cluster_replication/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/maps/**/*.s+(a|c)ss' ignore: diff --git a/Jenkinsfile b/Jenkinsfile index 6646ee15ba1c2..958ced8c6195a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,54 +4,56 @@ library 'kibana-pipeline-library' kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 135, checkPrChanges: true) { - githubPr.withDefaultPrComments { - catchError { - retryable.enable() - parallel([ - 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), - 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - // 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), - 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), - 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), - 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), - 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), - 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), - 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), - 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), - 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), - 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), - 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), - 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), - 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), - 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), - // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), - ]), - 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - // 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), - 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), - 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), - 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), - 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), - 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), - 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), - 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), - 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), - 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), - 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), - 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), - 'xpack-siemCypress': { processNumber -> - whenChanged(['x-pack/plugins/siem/', 'x-pack/legacy/plugins/siem/', 'x-pack/test/siem_cypress/']) { - kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh')(processNumber) - } - }, + ciStats.trackBuild { + githubPr.withDefaultPrComments { + catchError { + retryable.enable() + parallel([ + 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), + 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), + 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ + 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), + 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), + 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), + 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4), + 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5), + 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6), + 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7), + 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8), + 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9), + 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10), + 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11), + 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12), + 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'), + // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), + ]), + 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ + 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), + 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), + 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), + 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4), + 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5), + 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6), + 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7), + 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8), + 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9), + 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10), + 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'), + 'xpack-siemCypress': { processNumber -> + whenChanged(['x-pack/plugins/siem/', 'x-pack/test/siem_cypress/']) { + kibanaPipeline.functionalTestProcess('xpack-siemCypress', './test/scripts/jenkins_siem_cypress.sh')(processNumber) + } + }, - // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), - ]), - ]) + // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'), + ]), + ]) + } } - } - retryable.printFlakyFailures() - kibanaPipeline.sendMail() + retryable.printFlakyFailures() + kibanaPipeline.sendMail() + } } diff --git a/docs/developer/core-development.asciidoc b/docs/developer/core-development.asciidoc index 447a4c6d2601b..8f356abd095f2 100644 --- a/docs/developer/core-development.asciidoc +++ b/docs/developer/core-development.asciidoc @@ -7,6 +7,7 @@ * <> * <> * <> +* <> include::core/development-basepath.asciidoc[] @@ -19,3 +20,5 @@ include::core/development-elasticsearch.asciidoc[] include::core/development-unit-tests.asciidoc[] include::core/development-functional-tests.asciidoc[] + +include::core/development-es-snapshots.asciidoc[] diff --git a/docs/developer/core/development-es-snapshots.asciidoc b/docs/developer/core/development-es-snapshots.asciidoc new file mode 100644 index 0000000000000..4cd4f31e582db --- /dev/null +++ b/docs/developer/core/development-es-snapshots.asciidoc @@ -0,0 +1,113 @@ +[[development-es-snapshots]] +=== Daily Elasticsearch Snapshots + +For local development and CI, Kibana, by default, uses Elasticsearch snapshots that are built daily when running tasks that require Elasticsearch (e.g. functional tests). + +A snapshot is just a group of tarballs, one for each supported distribution/architecture/os of Elasticsearch, and a JSON-based manifest file containing metadata about the distributions. + +https://ci.kibana.dev/es-snapshots[A dashboard] is available that shows the current status and compatibility of the latest Elasticsearch snapshots. + +==== Process Overview + +1. Elasticsearch snapshots are built for each current tracked branch of Kibana. +2. Each snapshot is uploaded to a public Google Cloud Storage bucket, `kibana-ci-es-snapshots-daily`. +** At this point, the snapshot is not automatically used in CI or local development. It needs to be tested/verified first. +3. Each snapshot is tested with the latest commit of the corresponding Kibana branch, using the full CI suite. +4. After CI +** If the snapshot passes, it is promoted and automatically used in CI and local development. +** If the snapshot fails, the issue must be investigated and resolved. A new incompatibility may exist between Elasticsearch and Kibana. + +==== Using the latest snapshot + +When developing locally, you may wish to use the most recent Elasticsearch snapshot, even if it's failing CI. To do so, prefix your commands with the follow environment variable: + +["source","bash"] +----------- +KBN_ES_SNAPSHOT_USE_UNVERIFIED=true +----------- + +You can use this flag with any command that downloads and runs Elasticsearch snapshots, such as `scripts/es` or the FTR. + +For example, to run functional tests with the latest snapshot: + +["source","bash"] +----------- +KBN_ES_SNAPSHOT_USE_UNVERIFIED=true node scripts/functional_tests_server +----------- + +===== For Pull Requests + +Currently, there is not a way to run your pull request with the latest unverified snapshot without a code change. You can, however, do it with a small code change. + +1. Edit `Jenkinsfile` in the root of the Kibana repo +2. Add `env.KBN_ES_SNAPSHOT_USE_UNVERIFIED = 'true'` at the top of the file. +3. Commit the change + +Your pull request should then use the latest snapshot the next time that it runs. Just don't merge the change to `Jenkinsfile`! + +==== Google Cloud Storage buckets + +===== kibana-ci-es-snapshots-daily + +This bucket stores snapshots that are created on a daily basis, and is the primary location used by `kbn-es` to download snapshots. + +Snapshots are automatically deleted after 10 days. + +The file structure for this bucket looks like this: + +* `/manifest-latest.json` +* `/manifest-latest-verified.json` +* `/archives//*.tar.gz` +* `/archives//*.tar.gz.sha512` +* `/archives//manifest.json` + +===== kibana-ci-es-snapshots-permanent + +This bucket stores only the most recently promoted snapshot for each version. Old snapshots are only deleted when new ones are uploaded. + +This bucket serves as permanent snapshot storage for old branches/versions that are no longer being built. `kbn-es` checks the daily bucket first, followed by this one if no snapshots were found. + +The file structure for this bucket looks like this: + +* `/*.tar.gz` +* `/*.tar.gz.sha512` +* `/manifest.json` + +==== How snapshots are built, tested, and promoted + +Each day, a https://kibana-ci.elastic.co/job/elasticsearch+snapshots+trigger/[Jenkins job] runs that triggers Elasticsearch builds for each currently tracked branch/version. This job is automatically updated with the correct branches whenever we release new versions of Kibana. + +===== Build + +https://kibana-ci.elastic.co/job/elasticsearch+snapshots+build/[This Jenkins job] builds the Elasticsearch snapshots and uploads them to GCS. + +The Jenkins job pipeline definition is https://github.com/elastic/kibana/blob/master/.ci/es-snapshots/Jenkinsfile_build_es[in the kibana repo]. + +1. Checkout Elasticsearch repo for the given branch/version. +2. Run `./gradlew -p distribution/archives assemble --parallel` to create all of the Elasticsearch distributions. +3. Create a tarball for each distribution. +4. Create a manifest JSON file containing info about the distribution, as well as its download URL. +5. Upload the tarballs and manifest to a unique location in the GCS bucket `kibana-ci-es-snapshots-daily`. +** e.g. `/archives/` +6. Replace `/manifest-latest.json` in GCS with this newest manifest. +** This allows the `KBN_ES_SNAPSHOT_USE_UNVERIFIED` flag to work. +7. Trigger the verification job, to run the full Kibana CI test suite with this snapshot. + +===== Verification and Promotion + +https://kibana-ci.elastic.co/job/elasticsearch+snapshots+verify/[This Jenkins job] tests the latest Elasticsearch snapshot with the full Kibana CI pipeline, and promotes if it there are no test failures. + +The Jenkins job pipeline definition is https://github.com/elastic/kibana/blob/master/.ci/es-snapshots/Jenkinsfile_verify_es[in the kibana repo]. + +1. Checkout Kibana and set up CI environment as normal. +2. Set the `ES_SNAPSHOT_MANIFEST` env var to point to the latest snapshot manifest. +3. Run CI (functional tests, integration tests, etc). +4. After CI +** If there was a test failure or other build error, send out an e-mail notification and stop. +** If there were no errors, promote the snapshot. + +Promotion is done as part of the same pipeline: + +1. Replace the manifest at `kibana-ci-es-snapshots-daily//manifest-latest-verified.json` with the manifest from the tested snapshot. +** At this point, the snapshot has been promoted and will automatically be used in CI and in local development. +2. Replace the snapshot at `kibana-ci-es-snapshots-permanent//` with the tested snapshot by copying all of the tarballs and the manifest file. \ No newline at end of file diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index 51b5273851ce7..2b091d9abb9fc 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -154,16 +154,16 @@ A test suite is a collection of tests defined by calling `describe()`, and then Use tags in `describe()` function to group functional tests. Tags include: * `ciGroup{id}` - Assigns test suite to a specific CI worker * `skipCloud` and `skipFirefox` - Excludes test suite from running on Cloud or Firefox -* `smoke` - Groups tests that run on Chrome and Firefox +* `includeFirefox` - Groups tests that run on Chrome and Firefox **Cross-browser testing**::: -On CI, all the functional tests are executed in Chrome by default. To also run a suite against Firefox, assign the `smoke` tag: +On CI, all the functional tests are executed in Chrome by default. To also run a suite against Firefox, assign the `includeFirefox` tag: ["source","js"] ----------- // on CI test suite will be run twice: in Chrome and Firefox describe('My Cross-browser Test Suite', function () { - this.tags('smoke'); + this.tags('includeFirefox'); it('My First Test'); } 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 0000000000000..51492756ef232 --- /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 b73785647f23c..7b624f12ac1df 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 cdf9171a46aed..3d8b5d115c8a2 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 1cc1a1194a537..a9fabb38df869 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 0c415ed1a7fad..1e0b890015993 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/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 5450e84417f89..a91a5bec988b7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -263,13 +263,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | | [SavedObjectMigrationFn](./kibana-plugin-core-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-core-server.savedobjectstype.md) used to migrate it to a given version | -| [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | | -| [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | +| [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a references root property. | +| [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | | [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global.Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md). | +| [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) | Describes Saved Object documents from Kibana < 7.0.0 which don't have a references root property defined. This type should only be used in migrations. | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | | [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). | | [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | | 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 a502c40db0cd8..a3294fb0a087a 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 47feb50e9a827..3f4090619edbf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md @@ -4,9 +4,10 @@ ## SavedObjectSanitizedDoc type +Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a `references` root property. 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.savedobjectsclientcontract.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md index c39df0655f1d4..610356a733126 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md @@ -30,10 +30,6 @@ At the time of writing we are in the process of transitioning away from the oper From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing. -\#\#\# 503s from missing index - -Unlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's `action.auto_create_index` setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated. - See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md index 96f1143630856..dbc7d0ca431ce 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md @@ -19,5 +19,6 @@ export interface SavedObjectsCoreFieldMapping | [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean | | | [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
};
} | | | [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean | | +| [null\_value](./kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md) | number | boolean | string | | | [type](./kibana-plugin-core-server.savedobjectscorefieldmapping.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md new file mode 100644 index 0000000000000..627ea3695383a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) > [null\_value](./kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md) + +## SavedObjectsCoreFieldMapping.null\_value property + +Signature: + +```typescript +null_value?: number | boolean | string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md deleted file mode 100644 index 6350afacee2ba..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createEsAutoCreateIndexError](./kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md) - -## SavedObjectsErrorHelpers.createEsAutoCreateIndexError() method - -Signature: - -```typescript -static createEsAutoCreateIndexError(): DecoratedError; -``` -Returns: - -`DecoratedError` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md deleted file mode 100644 index bdffff5c1365b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isEsAutoCreateIndexError](./kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md) - -## SavedObjectsErrorHelpers.isEsAutoCreateIndexError() method - -Signature: - -```typescript -static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| error | Error | DecoratedError | | - -Returns: - -`boolean` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 250b9d3899670..7874be311d52c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -17,7 +17,6 @@ export declare class SavedObjectsErrorHelpers | --- | --- | --- | | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | | [createConflictError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | -| [createEsAutoCreateIndexError()](./kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | @@ -31,7 +30,6 @@ export declare class SavedObjectsErrorHelpers | [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | static | | | [isBadRequestError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md) | static | | | [isConflictError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md) | static | | -| [isEsAutoCreateIndexError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md) | static | | | [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md new file mode 100644 index 0000000000000..8e2395ee6310d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) + +## SavedObjectUnsanitizedDoc type + +Describes Saved Object documents from Kibana < 7.0.0 which don't have a `references` root property defined. This type should only be used in migrations. + +Signature: + +```typescript +export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md deleted file mode 100644 index 2ef8c797f4054..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) - -## AggConfigOptions.enabled property - -Signature: - -```typescript -enabled?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md deleted file mode 100644 index 8939854ab19ca..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) - -## AggConfigOptions.id property - -Signature: - -```typescript -id?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md index b841d9b04d6a7..ff8055b8cf1b1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md @@ -2,21 +2,12 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) -## AggConfigOptions interface +## AggConfigOptions type Signature: ```typescript -export interface AggConfigOptions +export declare type AggConfigOptions = Assign; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) | boolean | | -| [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) | string | | -| [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) | Record<string, any> | | -| [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) | string | | -| [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) | IAggType | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md deleted file mode 100644 index 45219a837cc33..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) - -## AggConfigOptions.params property - -Signature: - -```typescript -params?: Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md deleted file mode 100644 index b2b42eb2e5b4d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) - -## AggConfigOptions.schema property - -Signature: - -```typescript -schema?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md deleted file mode 100644 index 866065ce52ba6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) - -## AggConfigOptions.type property - -Signature: - -```typescript -type: IAggType; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggrouplabels.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggrouplabels.md new file mode 100644 index 0000000000000..6684ba8546f85 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggrouplabels.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggGroupLabels](./kibana-plugin-plugins-data-public.agggrouplabels.md) + +## AggGroupLabels variable + +Signature: + +```typescript +AggGroupLabels: { + [AggGroupNames.Buckets]: string; + [AggGroupNames.Metrics]: string; + [AggGroupNames.None]: string; +} +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggroupname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggroupname.md new file mode 100644 index 0000000000000..d4476398680a8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.agggroupname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) + +## AggGroupName type + +Signature: + +```typescript +export declare type AggGroupName = $Values; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md index 43f30d73ca6df..a91db7e7aac8b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md @@ -7,5 +7,5 @@ Signature: ```typescript -makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; +makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md index b75065da91abd..f9733529a315d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md @@ -21,5 +21,5 @@ export declare class AggParamType ex | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [allowedAggs](./kibana-plugin-plugins-data-public.aggparamtype.allowedaggs.md) | | string[] | | -| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: any) => TAggConfig | | +| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md deleted file mode 100644 index c9d6772a13b8d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) > [addFilter](./kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md) - -## AggTypeFieldFilters.addFilter() method - -Register a new with this registry. This will be used by the . - -Signature: - -```typescript -addFilter(filter: AggTypeFieldFilter): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| filter | AggTypeFieldFilter | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md deleted file mode 100644 index 038c339bf6774..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) > [filter](./kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md) - -## AggTypeFieldFilters.filter() method - -Returns the filtered by all registered filters. - -Signature: - -```typescript -filter(fields: IndexPatternField[], aggConfig: IAggConfig): IndexPatternField[]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| fields | IndexPatternField[] | | -| aggConfig | IAggConfig | | - -Returns: - -`IndexPatternField[]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.md deleted file mode 100644 index c0b386efbf9c7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefieldfilters.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) - -## AggTypeFieldFilters class - -A registry to store which are used to filter down available fields for a specific visualization and . - -Signature: - -```typescript -declare class AggTypeFieldFilters -``` - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [addFilter(filter)](./kibana-plugin-plugins-data-public.aggtypefieldfilters.addfilter.md) | | Register a new with this registry. This will be used by the . | -| [filter(fields, aggConfig)](./kibana-plugin-plugins-data-public.aggtypefieldfilters.filter.md) | | Returns the filtered by all registered filters. | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md deleted file mode 100644 index 9df003377c4a1..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) > [addFilter](./kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md) - -## AggTypeFilters.addFilter() method - -Register a new with this registry. - -Signature: - -```typescript -addFilter(filter: AggTypeFilter): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| filter | AggTypeFilter | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.filter.md deleted file mode 100644 index 81e6e9b95d655..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.filter.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) > [filter](./kibana-plugin-plugins-data-public.aggtypefilters.filter.md) - -## AggTypeFilters.filter() method - -Returns the filtered by all registered filters. - -Signature: - -```typescript -filter(aggTypes: IAggType[], indexPattern: IndexPattern, aggConfig: IAggConfig, aggFilter: string[]): IAggType[]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| aggTypes | IAggType[] | | -| indexPattern | IndexPattern | | -| aggConfig | IAggConfig | | -| aggFilter | string[] | | - -Returns: - -`IAggType[]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.md deleted file mode 100644 index c5e24bc0a78a0..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggtypefilters.md +++ /dev/null @@ -1,21 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) - -## AggTypeFilters class - -A registry to store which are used to filter down available aggregations for a specific visualization and . - -Signature: - -```typescript -declare class AggTypeFilters -``` - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [addFilter(filter)](./kibana-plugin-plugins-data-public.aggtypefilters.addfilter.md) | | Register a new with this registry. | -| [filter(aggTypes, indexPattern, aggConfig, aggFilter)](./kibana-plugin-plugins-data-public.aggtypefilters.filter.md) | | Returns the filtered by all registered filters. | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md index 50e8f2409ac02..ddbf1a8459d1f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md @@ -7,5 +7,5 @@ Signature: ```typescript -baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[] +baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[] ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md deleted file mode 100644 index 5c5aa348eecdf..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.createsearchsource.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) - -## createSearchSource variable - -Deserializes a json string and a set of referenced objects to a `SearchSource` instance. Use this method to re-create the search source serialized using `searchSource.serialize`. - -This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service - -Signature: - -```typescript -createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md index 3e966caa30799..25ce6eaa688f8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md @@ -8,6 +8,7 @@ ```typescript actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md index a623e91388fd6..4f43f10ce089e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md @@ -14,7 +14,7 @@ export interface DataPublicPluginStart | Property | Type | Description | | --- | --- | --- | -| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromEvent: typeof createFiltersFromEvent;
} | | +| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction;
createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction;
} | | | [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | | | [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | | | [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.from.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.from.md deleted file mode 100644 index 245269af366bc..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.from.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) > [from](./kibana-plugin-plugins-data-public.daterangekey.from.md) - -## DateRangeKey.from property - -Signature: - -```typescript -from: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.md deleted file mode 100644 index 540d429dced48..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) - -## DateRangeKey interface - -Signature: - -```typescript -export interface DateRangeKey -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [from](./kibana-plugin-plugins-data-public.daterangekey.from.md) | number | | -| [to](./kibana-plugin-plugins-data-public.daterangekey.to.md) | number | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.to.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.to.md deleted file mode 100644 index 024a6c2105427..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.daterangekey.to.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) > [to](./kibana-plugin-plugins-data-public.daterangekey.to.md) - -## DateRangeKey.to property - -Signature: - -```typescript -to: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.__spec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.__spec.md new file mode 100644 index 0000000000000..43ff9a930b974 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.__spec.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [$$spec](./kibana-plugin-plugins-data-public.field.__spec.md) + +## Field.$$spec property + +Signature: + +```typescript +$$spec: FieldSpec; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field._constructor_.md new file mode 100644 index 0000000000000..c3b2ac8d30b5a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field._constructor_.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [(constructor)](./kibana-plugin-plugins-data-public.field._constructor_.md) + +## Field.(constructor) + +Constructs a new instance of the `Field` class + +Signature: + +```typescript +constructor(indexPattern: IndexPattern, spec: FieldSpec | Field, shortDotsEnable?: boolean); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| spec | FieldSpec | Field | | +| shortDotsEnable | boolean | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.aggregatable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.aggregatable.md new file mode 100644 index 0000000000000..fcfd7d73c8b0c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.aggregatable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [aggregatable](./kibana-plugin-plugins-data-public.field.aggregatable.md) + +## Field.aggregatable property + +Signature: + +```typescript +aggregatable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.conflictdescriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.conflictdescriptions.md new file mode 100644 index 0000000000000..21b6917c4aad4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.conflictdescriptions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [conflictDescriptions](./kibana-plugin-plugins-data-public.field.conflictdescriptions.md) + +## Field.conflictDescriptions property + +Signature: + +```typescript +conflictDescriptions?: Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.count.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.count.md new file mode 100644 index 0000000000000..4f51d88a3046e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.count.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [count](./kibana-plugin-plugins-data-public.field.count.md) + +## Field.count property + +Signature: + +```typescript +count?: number; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.displayname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.displayname.md new file mode 100644 index 0000000000000..0846a7595cf90 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.displayname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [displayName](./kibana-plugin-plugins-data-public.field.displayname.md) + +## Field.displayName property + +Signature: + +```typescript +displayName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.estypes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.estypes.md new file mode 100644 index 0000000000000..efe1bceb43361 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.estypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [esTypes](./kibana-plugin-plugins-data-public.field.estypes.md) + +## Field.esTypes property + +Signature: + +```typescript +esTypes?: string[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.filterable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.filterable.md new file mode 100644 index 0000000000000..fd7be589e87a7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.filterable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [filterable](./kibana-plugin-plugins-data-public.field.filterable.md) + +## Field.filterable property + +Signature: + +```typescript +filterable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.format.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.format.md new file mode 100644 index 0000000000000..431e043d1fecc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.format.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [format](./kibana-plugin-plugins-data-public.field.format.md) + +## Field.format property + +Signature: + +```typescript +format: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.indexpattern.md new file mode 100644 index 0000000000000..59420747e0958 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.indexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [indexPattern](./kibana-plugin-plugins-data-public.field.indexpattern.md) + +## Field.indexPattern property + +Signature: + +```typescript +indexPattern?: IndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.lang.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.lang.md new file mode 100644 index 0000000000000..d51857090356f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.lang.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [lang](./kibana-plugin-plugins-data-public.field.lang.md) + +## Field.lang property + +Signature: + +```typescript +lang?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.md new file mode 100644 index 0000000000000..86ff2b2c28ae9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.md @@ -0,0 +1,41 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) + +## Field class + +Signature: + +```typescript +export declare class Field implements IFieldType +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(indexPattern, spec, shortDotsEnable)](./kibana-plugin-plugins-data-public.field._constructor_.md) | | Constructs a new instance of the Field class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [$$spec](./kibana-plugin-plugins-data-public.field.__spec.md) | | FieldSpec | | +| [aggregatable](./kibana-plugin-plugins-data-public.field.aggregatable.md) | | boolean | | +| [conflictDescriptions](./kibana-plugin-plugins-data-public.field.conflictdescriptions.md) | | Record<string, string[]> | | +| [count](./kibana-plugin-plugins-data-public.field.count.md) | | number | | +| [displayName](./kibana-plugin-plugins-data-public.field.displayname.md) | | string | | +| [esTypes](./kibana-plugin-plugins-data-public.field.estypes.md) | | string[] | | +| [filterable](./kibana-plugin-plugins-data-public.field.filterable.md) | | boolean | | +| [format](./kibana-plugin-plugins-data-public.field.format.md) | | any | | +| [indexPattern](./kibana-plugin-plugins-data-public.field.indexpattern.md) | | IndexPattern | | +| [lang](./kibana-plugin-plugins-data-public.field.lang.md) | | string | | +| [name](./kibana-plugin-plugins-data-public.field.name.md) | | string | | +| [script](./kibana-plugin-plugins-data-public.field.script.md) | | string | | +| [scripted](./kibana-plugin-plugins-data-public.field.scripted.md) | | boolean | | +| [searchable](./kibana-plugin-plugins-data-public.field.searchable.md) | | boolean | | +| [sortable](./kibana-plugin-plugins-data-public.field.sortable.md) | | boolean | | +| [subType](./kibana-plugin-plugins-data-public.field.subtype.md) | | IFieldSubType | | +| [type](./kibana-plugin-plugins-data-public.field.type.md) | | string | | +| [visualizable](./kibana-plugin-plugins-data-public.field.visualizable.md) | | boolean | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.name.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.name.md new file mode 100644 index 0000000000000..d2a9b9b86aefc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [name](./kibana-plugin-plugins-data-public.field.name.md) + +## Field.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.script.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.script.md new file mode 100644 index 0000000000000..676ff9bdfc35a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.script.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [script](./kibana-plugin-plugins-data-public.field.script.md) + +## Field.script property + +Signature: + +```typescript +script?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.scripted.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.scripted.md new file mode 100644 index 0000000000000..1f6c8105e3f61 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.scripted.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [scripted](./kibana-plugin-plugins-data-public.field.scripted.md) + +## Field.scripted property + +Signature: + +```typescript +scripted?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.searchable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.searchable.md new file mode 100644 index 0000000000000..186d344f50378 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.searchable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [searchable](./kibana-plugin-plugins-data-public.field.searchable.md) + +## Field.searchable property + +Signature: + +```typescript +searchable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.sortable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.sortable.md new file mode 100644 index 0000000000000..0cd4b14d0e1e5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.sortable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [sortable](./kibana-plugin-plugins-data-public.field.sortable.md) + +## Field.sortable property + +Signature: + +```typescript +sortable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.subtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.subtype.md new file mode 100644 index 0000000000000..bef3b2131fa47 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.subtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [subType](./kibana-plugin-plugins-data-public.field.subtype.md) + +## Field.subType property + +Signature: + +```typescript +subType?: IFieldSubType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.type.md new file mode 100644 index 0000000000000..490615edcf097 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [type](./kibana-plugin-plugins-data-public.field.type.md) + +## Field.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.visualizable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.visualizable.md new file mode 100644 index 0000000000000..f32a5c456dc5d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.field.visualizable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [Field](./kibana-plugin-plugins-data-public.field.md) > [visualizable](./kibana-plugin-plugins-data-public.field.visualizable.md) + +## Field.visualizable property + +Signature: + +```typescript +visualizable?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._constructor_.md new file mode 100644 index 0000000000000..e38da6600696c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [(constructor)](./kibana-plugin-plugins-data-public.fieldformat._constructor_.md) + +## FieldFormat.(constructor) + +Constructs a new instance of the `FieldFormat` class + +Signature: + +```typescript +constructor(_params?: IFieldFormatMetaParams, getConfig?: FieldFormatsGetConfigFn); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| \_params | IFieldFormatMetaParams | | +| getConfig | FieldFormatsGetConfigFn | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._params.md new file mode 100644 index 0000000000000..ac3f256a9afc3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat._params.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [\_params](./kibana-plugin-plugins-data-public.fieldformat._params.md) + +## FieldFormat.\_params property + +Signature: + +```typescript +protected readonly _params: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convert.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convert.md new file mode 100644 index 0000000000000..0535585cb4718 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convert.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [convert](./kibana-plugin-plugins-data-public.fieldformat.convert.md) + +## FieldFormat.convert() method + +Convert a raw value to a formatted string + +Signature: + +```typescript +convert(value: any, contentType?: FieldFormatsContentType, options?: HtmlContextTypeOptions | TextContextTypeOptions): string; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| value | any | | +| contentType | FieldFormatsContentType | | +| options | HtmlContextTypeOptions | TextContextTypeOptions | | + +Returns: + +`string` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convertobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convertobject.md new file mode 100644 index 0000000000000..436124ac08387 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.convertobject.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [convertObject](./kibana-plugin-plugins-data-public.fieldformat.convertobject.md) + +## FieldFormat.convertObject property + + {FieldFormatConvert} have to remove the private because of https://github.com/Microsoft/TypeScript/issues/17293 + +Signature: + +```typescript +convertObject: FieldFormatConvert | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.fieldtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.fieldtype.md new file mode 100644 index 0000000000000..1d109a599d2d9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.fieldtype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [fieldType](./kibana-plugin-plugins-data-public.fieldformat.fieldtype.md) + +## FieldFormat.fieldType property + + {string} - Field Format Type + +Signature: + +```typescript +static fieldType: string | string[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.from.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.from.md new file mode 100644 index 0000000000000..ec497de59d236 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.from.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [from](./kibana-plugin-plugins-data-public.fieldformat.from.md) + +## FieldFormat.from() method + +Signature: + +```typescript +static from(convertFn: FieldFormatConvertFunction): FieldFormatInstanceType; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| convertFn | FieldFormatConvertFunction | | + +Returns: + +`FieldFormatInstanceType` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconfig.md new file mode 100644 index 0000000000000..446e0c237ce13 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconfig.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [getConfig](./kibana-plugin-plugins-data-public.fieldformat.getconfig.md) + +## FieldFormat.getConfig property + +Signature: + +```typescript +protected getConfig: FieldFormatsGetConfigFn | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md new file mode 100644 index 0000000000000..f4eeb5eed06a0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [getConverterFor](./kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md) + +## FieldFormat.getConverterFor() method + +Get a convert function that is bound to a specific contentType + +Signature: + +```typescript +getConverterFor(contentType?: FieldFormatsContentType): FieldFormatConvertFunction; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| contentType | FieldFormatsContentType | | + +Returns: + +`FieldFormatConvertFunction` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md new file mode 100644 index 0000000000000..59afdc25df350 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [getParamDefaults](./kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md) + +## FieldFormat.getParamDefaults() method + +Get parameter defaults {object} - parameter defaults + +Signature: + +```typescript +getParamDefaults(): Record; +``` +Returns: + +`Record` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md new file mode 100644 index 0000000000000..945ac7ededff6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [htmlConvert](./kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md) + +## FieldFormat.htmlConvert property + + {htmlConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 + +Signature: + +```typescript +htmlConvert: HtmlContextTypeConvert | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.id.md new file mode 100644 index 0000000000000..91c3ff4f2d9a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [id](./kibana-plugin-plugins-data-public.fieldformat.id.md) + +## FieldFormat.id property + + {string} - Field Format Id + +Signature: + +```typescript +static id: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md new file mode 100644 index 0000000000000..c6afa27fe5952 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [isInstanceOfFieldFormat](./kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md) + +## FieldFormat.isInstanceOfFieldFormat() method + +Signature: + +```typescript +static isInstanceOfFieldFormat(fieldFormat: any): fieldFormat is FieldFormat; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldFormat | any | | + +Returns: + +`fieldFormat is FieldFormat` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.md new file mode 100644 index 0000000000000..b53e301c46c1c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.md @@ -0,0 +1,46 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) + +## FieldFormat class + +Signature: + +```typescript +export declare abstract class FieldFormat +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(\_params, getConfig)](./kibana-plugin-plugins-data-public.fieldformat._constructor_.md) | | Constructs a new instance of the FieldFormat class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [\_params](./kibana-plugin-plugins-data-public.fieldformat._params.md) | | any | | +| [convertObject](./kibana-plugin-plugins-data-public.fieldformat.convertobject.md) | | FieldFormatConvert | undefined | {FieldFormatConvert} have to remove the private because of https://github.com/Microsoft/TypeScript/issues/17293 | +| [fieldType](./kibana-plugin-plugins-data-public.fieldformat.fieldtype.md) | static | string | string[] | {string} - Field Format Type | +| [getConfig](./kibana-plugin-plugins-data-public.fieldformat.getconfig.md) | | FieldFormatsGetConfigFn | undefined | | +| [htmlConvert](./kibana-plugin-plugins-data-public.fieldformat.htmlconvert.md) | | HtmlContextTypeConvert | undefined | {htmlConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 | +| [id](./kibana-plugin-plugins-data-public.fieldformat.id.md) | static | string | {string} - Field Format Id | +| [textConvert](./kibana-plugin-plugins-data-public.fieldformat.textconvert.md) | | TextContextTypeConvert | undefined | {textConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 | +| [title](./kibana-plugin-plugins-data-public.fieldformat.title.md) | static | string | {string} - Field Format Title | +| [type](./kibana-plugin-plugins-data-public.fieldformat.type.md) | | any | {Function} - ref to child class | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [convert(value, contentType, options)](./kibana-plugin-plugins-data-public.fieldformat.convert.md) | | Convert a raw value to a formatted string | +| [from(convertFn)](./kibana-plugin-plugins-data-public.fieldformat.from.md) | static | | +| [getConverterFor(contentType)](./kibana-plugin-plugins-data-public.fieldformat.getconverterfor.md) | | Get a convert function that is bound to a specific contentType | +| [getParamDefaults()](./kibana-plugin-plugins-data-public.fieldformat.getparamdefaults.md) | | Get parameter defaults {object} - parameter defaults | +| [isInstanceOfFieldFormat(fieldFormat)](./kibana-plugin-plugins-data-public.fieldformat.isinstanceoffieldformat.md) | static | | +| [param(name)](./kibana-plugin-plugins-data-public.fieldformat.param.md) | | Get the value of a param. This value may be a default value. | +| [params()](./kibana-plugin-plugins-data-public.fieldformat.params.md) | | Get all of the params in a single object {object} | +| [setupContentType()](./kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md) | | | +| [toJSON()](./kibana-plugin-plugins-data-public.fieldformat.tojson.md) | | Serialize this format to a simple POJO, with only the params that are not default {object} | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.param.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.param.md new file mode 100644 index 0000000000000..1e7fd9d161429 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.param.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [param](./kibana-plugin-plugins-data-public.fieldformat.param.md) + +## FieldFormat.param() method + +Get the value of a param. This value may be a default value. + +Signature: + +```typescript +param(name: string): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.params.md new file mode 100644 index 0000000000000..5825af4925d06 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.params.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [params](./kibana-plugin-plugins-data-public.fieldformat.params.md) + +## FieldFormat.params() method + +Get all of the params in a single object {object} + +Signature: + +```typescript +params(): Record; +``` +Returns: + +`Record` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md new file mode 100644 index 0000000000000..41f5f2446f22a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [setupContentType](./kibana-plugin-plugins-data-public.fieldformat.setupcontenttype.md) + +## FieldFormat.setupContentType() method + +Signature: + +```typescript +setupContentType(): FieldFormatConvert; +``` +Returns: + +`FieldFormatConvert` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.textconvert.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.textconvert.md new file mode 100644 index 0000000000000..57ccca9136081 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.textconvert.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [textConvert](./kibana-plugin-plugins-data-public.fieldformat.textconvert.md) + +## FieldFormat.textConvert property + + {textConvert} have to remove the protected because of https://github.com/Microsoft/TypeScript/issues/17293 + +Signature: + +```typescript +textConvert: TextContextTypeConvert | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.title.md new file mode 100644 index 0000000000000..b19246758f080 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.title.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [title](./kibana-plugin-plugins-data-public.fieldformat.title.md) + +## FieldFormat.title property + + {string} - Field Format Title + +Signature: + +```typescript +static title: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md new file mode 100644 index 0000000000000..5fa7d4841537b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.tojson.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [toJSON](./kibana-plugin-plugins-data-public.fieldformat.tojson.md) + +## FieldFormat.toJSON() method + +Serialize this format to a simple POJO, with only the params that are not default + + {object} + +Signature: + +```typescript +toJSON(): { + id: unknown; + params: _.Dictionary | undefined; + }; +``` +Returns: + +`{ + id: unknown; + params: _.Dictionary | undefined; + }` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.type.md new file mode 100644 index 0000000000000..394a2e3cc9afb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformat.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) > [type](./kibana-plugin-plugins-data-public.fieldformat.type.md) + +## FieldFormat.type property + + {Function} - ref to child class + +Signature: + +```typescript +type: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md index 244633c3c4c9e..d39871b99f744 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md @@ -10,7 +10,7 @@ fieldFormats: { FieldFormat: typeof FieldFormat; FieldFormatsRegistry: typeof FieldFormatsRegistry; - serialize: (agg: import("./search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; 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 04a0d871cab2d..3969a97fa7789 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.iagggroupnames.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iagggroupnames.md deleted file mode 100644 index 07310a4219359..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iagggroupnames.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IAggGroupNames](./kibana-plugin-plugins-data-public.iagggroupnames.md) - -## IAggGroupNames type - -Signature: - -```typescript -export declare type IAggGroupNames = $Values; -``` 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 0000000000000..c3998876c9712 --- /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 1bbd6cf67f0ce..1cb89822eb605 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.indexpatternfield.__spec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.__spec.md deleted file mode 100644 index f52a3324af36f..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.__spec.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [$$spec](./kibana-plugin-plugins-data-public.indexpatternfield.__spec.md) - -## IndexPatternField.$$spec property - -Signature: - -```typescript -$$spec: FieldSpec; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md deleted file mode 100644 index cf7470c035a53..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [(constructor)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) - -## IndexPatternField.(constructor) - -Constructs a new instance of the `Field` class - -Signature: - -```typescript -constructor(indexPattern: IndexPattern, spec: FieldSpec | Field, shortDotsEnable?: boolean); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| indexPattern | IndexPattern | | -| spec | FieldSpec | Field | | -| shortDotsEnable | boolean | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md deleted file mode 100644 index 267c8f786b5dd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) - -## IndexPatternField.aggregatable property - -Signature: - -```typescript -aggregatable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md deleted file mode 100644 index 8e848276f21c4..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) - -## IndexPatternField.count property - -Signature: - -```typescript -count?: number; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md deleted file mode 100644 index ed9630f92fc97..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.displayname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) - -## IndexPatternField.displayName property - -Signature: - -```typescript -displayName?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.estypes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.estypes.md deleted file mode 100644 index dec74df099d43..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.estypes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) - -## IndexPatternField.esTypes property - -Signature: - -```typescript -esTypes?: string[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.filterable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.filterable.md deleted file mode 100644 index 4290c4a2f86b3..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.filterable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) - -## IndexPatternField.filterable property - -Signature: - -```typescript -filterable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md deleted file mode 100644 index d5df8ed628cb0..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.format.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) - -## IndexPatternField.format property - -Signature: - -```typescript -format: any; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md deleted file mode 100644 index d1a1ee0905c6e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) - -## IndexPatternField.indexPattern property - -Signature: - -```typescript -indexPattern?: IndexPattern; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md deleted file mode 100644 index f731be8f613cf..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) - -## IndexPatternField.lang property - -Signature: - -```typescript -lang?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md deleted file mode 100644 index df0de6ce0e541..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ /dev/null @@ -1,40 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) - -## IndexPatternField class - -Signature: - -```typescript -export declare class Field implements IFieldType -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(indexPattern, spec, shortDotsEnable)](./kibana-plugin-plugins-data-public.indexpatternfield._constructor_.md) | | Constructs a new instance of the Field class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [$$spec](./kibana-plugin-plugins-data-public.indexpatternfield.__spec.md) | | FieldSpec | | -| [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | -| [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | | -| [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | -| [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | | -| [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | -| [format](./kibana-plugin-plugins-data-public.indexpatternfield.format.md) | | any | | -| [indexPattern](./kibana-plugin-plugins-data-public.indexpatternfield.indexpattern.md) | | IndexPattern | | -| [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | | -| [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | -| [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | | -| [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | -| [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | -| [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) | | boolean | | -| [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) | | IFieldSubType | | -| [type](./kibana-plugin-plugins-data-public.indexpatternfield.type.md) | | string | | -| [visualizable](./kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md) | | boolean | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.name.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.name.md deleted file mode 100644 index cb24621e73209..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) - -## IndexPatternField.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md deleted file mode 100644 index 132ba25a47637..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) - -## IndexPatternField.script property - -Signature: - -```typescript -script?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.scripted.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.scripted.md deleted file mode 100644 index 1dd6bc865a75d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.scripted.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) - -## IndexPatternField.scripted property - -Signature: - -```typescript -scripted?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.searchable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.searchable.md deleted file mode 100644 index 42f984d851435..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.searchable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) - -## IndexPatternField.searchable property - -Signature: - -```typescript -searchable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.sortable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.sortable.md deleted file mode 100644 index 72d225185140b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.sortable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) - -## IndexPatternField.sortable property - -Signature: - -```typescript -sortable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md deleted file mode 100644 index 2d807f8a5739c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.subtype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [subType](./kibana-plugin-plugins-data-public.indexpatternfield.subtype.md) - -## IndexPatternField.subType property - -Signature: - -```typescript -subType?: IFieldSubType; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.type.md deleted file mode 100644 index c8483c9b83c9a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [type](./kibana-plugin-plugins-data-public.indexpatternfield.type.md) - -## IndexPatternField.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md deleted file mode 100644 index dd661ae779c11..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) > [visualizable](./kibana-plugin-plugins-data-public.indexpatternfield.visualizable.md) - -## IndexPatternField.visualizable property - -Signature: - -```typescript -visualizable?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iprangekey.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iprangekey.md deleted file mode 100644 index 96903a5df9844..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iprangekey.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IpRangeKey](./kibana-plugin-plugins-data-public.iprangekey.md) - -## IpRangeKey type - -Signature: - -```typescript -export declare type IpRangeKey = { - type: 'mask'; - mask: string; -} | { - type: 'range'; - from: string; - to: string; -}; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md index a8154dff72a6a..4b9f6e3594dc5 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md @@ -4,6 +4,8 @@ ## ISearchSource type +\* + Signature: ```typescript 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 bf29c883e4eb9..13e38ba5e6e5d 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 @@ -9,11 +9,10 @@ | Class | Description | | --- | --- | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | -| [AggTypeFieldFilters](./kibana-plugin-plugins-data-public.aggtypefieldfilters.md) | A registry to store which are used to filter down available fields for a specific visualization and . | -| [AggTypeFilters](./kibana-plugin-plugins-data-public.aggtypefilters.md) | A registry to store which are used to filter down available aggregations for a specific visualization and . | +| [Field](./kibana-plugin-plugins-data-public.field.md) | | +| [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | | [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) | | -| [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) | | | [IndexPatternFieldList](./kibana-plugin-plugins-data-public.indexpatternfieldlist.md) | | | [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | | @@ -21,7 +20,6 @@ | [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | | -| [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | | | [TimeHistory](./kibana-plugin-plugins-data-public.timehistory.md) | | ## Enumerations @@ -43,18 +41,16 @@ | [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 | Interface | Description | | --- | --- | -| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | | | [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | | | [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | | -| [DateRangeKey](./kibana-plugin-plugins-data-public.daterangekey.md) | | | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FetchOptions](./kibana-plugin-plugins-data-public.fetchoptions.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | @@ -76,7 +72,6 @@ | [ISearchStrategy](./kibana-plugin-plugins-data-public.isearchstrategy.md) | Search strategy interface contains a search method that takes in a request and returns a promise that resolves to a response. | | [ISyncSearchRequest](./kibana-plugin-plugins-data-public.isyncsearchrequest.md) | | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | -| [OptionedParamEditorProps](./kibana-plugin-plugins-data-public.optionedparameditorprops.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | @@ -96,12 +91,12 @@ | Variable | Description | | --- | --- | +| [AggGroupLabels](./kibana-plugin-plugins-data-public.agggrouplabels.md) | | | [AggGroupNames](./kibana-plugin-plugins-data-public.agggroupnames.md) | | | [baseFormattersPublic](./kibana-plugin-plugins-data-public.baseformatterspublic.md) | | | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | -| [createSearchSource](./kibana-plugin-plugins-data-public.createsearchsource.md) | Deserializes a json string and a set of referenced objects to a SearchSource instance. Use this method to re-create the search source serialized using searchSource.serialize.This function is a factory function that returns the actual utility when calling it with the required service dependency (index patterns contract). A pre-wired version is also exposed in the start contract of the data plugin as part of the search service | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | @@ -120,6 +115,8 @@ | Type Alias | Description | | --- | --- | +| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | +| [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | @@ -128,7 +125,6 @@ | [FieldFormatsContentType](./kibana-plugin-plugins-data-public.fieldformatscontenttype.md) | \* | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-public.fieldformatsgetconfigfn.md) | | | [IAggConfig](./kibana-plugin-plugins-data-public.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | -| [IAggGroupNames](./kibana-plugin-plugins-data-public.iagggroupnames.md) | | | [IAggType](./kibana-plugin-plugins-data-public.iaggtype.md) | | | [IFieldFormat](./kibana-plugin-plugins-data-public.ifieldformat.md) | | | [IFieldFormatsRegistry](./kibana-plugin-plugins-data-public.ifieldformatsregistry.md) | | @@ -137,10 +133,9 @@ | [IndexPatternAggRestrictions](./kibana-plugin-plugins-data-public.indexpatternaggrestrictions.md) | | | [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | | | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | -| [IpRangeKey](./kibana-plugin-plugins-data-public.iprangekey.md) | | | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | -| [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | | +| [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | \* | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md deleted file mode 100644 index 68e4371acc2f3..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [OptionedParamEditorProps](./kibana-plugin-plugins-data-public.optionedparameditorprops.md) > [aggParam](./kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md) - -## OptionedParamEditorProps.aggParam property - -Signature: - -```typescript -aggParam: { - options: T[]; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.md deleted file mode 100644 index 00a440a0a775a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.optionedparameditorprops.md +++ /dev/null @@ -1,18 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [OptionedParamEditorProps](./kibana-plugin-plugins-data-public.optionedparameditorprops.md) - -## OptionedParamEditorProps interface - -Signature: - -```typescript -export interface OptionedParamEditorProps -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggParam](./kibana-plugin-plugins-data-public.optionedparameditorprops.aggparam.md) | {
options: T[];
} | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md index 78ac05b9fd386..67c4eac67a9e6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.search.md @@ -9,12 +9,7 @@ ```typescript search: { aggs: { - AggConfigs: typeof AggConfigs; - aggGroupNamesMap: () => Record<"metrics" | "buckets", string>; - aggTypeFilters: import("./search/aggs/filter/agg_type_filters").AggTypeFilters; CidrMask: typeof CidrMask; - convertDateRangeToString: typeof convertDateRangeToString; - convertIPRangeToString: (range: import("./search").IpRangeKey, format: (val: any) => string) => string; dateHistogramInterval: typeof dateHistogramInterval; intervalOptions: ({ display: string; @@ -27,8 +22,9 @@ search: { InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; + isNumberType: (agg: import("./search").AggConfig) => boolean; isStringType: (agg: import("./search").AggConfig) => boolean; - isType: (type: string) => (agg: import("./search").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("./search").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parentPipelineType: string; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md deleted file mode 100644 index e0c9e77b313a5..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [(constructor)](./kibana-plugin-plugins-data-public.searchsource._constructor_.md) - -## SearchSource.(constructor) - -Constructs a new instance of the `SearchSource` class - -Signature: - -```typescript -constructor(fields?: SearchSourceFields); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| fields | SearchSourceFields | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md deleted file mode 100644 index b0a0201680ca8..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [create](./kibana-plugin-plugins-data-public.searchsource.create.md) - -## SearchSource.create() method - -Signature: - -```typescript -create(): SearchSource; -``` -Returns: - -`SearchSource` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md deleted file mode 100644 index 3f17dc21cf514..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [createChild](./kibana-plugin-plugins-data-public.searchsource.createchild.md) - -## SearchSource.createChild() method - -Signature: - -```typescript -createChild(options?: {}): SearchSource; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| options | {} | | - -Returns: - -`SearchSource` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md deleted file mode 100644 index f503a3dfc3299..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [createCopy](./kibana-plugin-plugins-data-public.searchsource.createcopy.md) - -## SearchSource.createCopy() method - -Signature: - -```typescript -createCopy(): SearchSource; -``` -Returns: - -`SearchSource` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md deleted file mode 100644 index 8a7cc5ee75d11..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [destroy](./kibana-plugin-plugins-data-public.searchsource.destroy.md) - -## SearchSource.destroy() method - -Completely destroy the SearchSource. {undefined} - -Signature: - -```typescript -destroy(): void; -``` -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md deleted file mode 100644 index 208ce565fac13..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [fetch](./kibana-plugin-plugins-data-public.searchsource.fetch.md) - -## SearchSource.fetch() method - -Fetch this source and reject the returned Promise on error - - -Signature: - -```typescript -fetch(options?: FetchOptions): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| options | FetchOptions | | - -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md deleted file mode 100644 index 98ba815696cf6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getField](./kibana-plugin-plugins-data-public.searchsource.getfield.md) - -## SearchSource.getField() method - -Get fields from the fields - -Signature: - -```typescript -getField(field: K, recurse?: boolean): SearchSourceFields[K]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| field | K | | -| recurse | boolean | | - -Returns: - -`SearchSourceFields[K]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md deleted file mode 100644 index dce03e7e1a95c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md +++ /dev/null @@ -1,49 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getFields](./kibana-plugin-plugins-data-public.searchsource.getfields.md) - -## SearchSource.getFields() method - -Signature: - -```typescript -getFields(): { - type?: string | undefined; - query?: import("../..").Query | undefined; - filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record | Record[] | undefined; - highlight?: any; - highlightAll?: boolean | undefined; - aggs?: any; - from?: number | undefined; - size?: number | undefined; - source?: string | boolean | string[] | undefined; - version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; - index?: import("../..").IndexPattern | undefined; - searchAfter?: import("./types").EsQuerySearchAfter | undefined; - timeout?: string | undefined; - terminate_after?: number | undefined; - }; -``` -Returns: - -`{ - type?: string | undefined; - query?: import("../..").Query | undefined; - filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record | Record[] | undefined; - highlight?: any; - highlightAll?: boolean | undefined; - aggs?: any; - from?: number | undefined; - size?: number | undefined; - source?: string | boolean | string[] | undefined; - version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; - index?: import("../..").IndexPattern | undefined; - searchAfter?: import("./types").EsQuerySearchAfter | undefined; - timeout?: string | undefined; - terminate_after?: number | undefined; - }` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md deleted file mode 100644 index 55aaa26ca62f3..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getId](./kibana-plugin-plugins-data-public.searchsource.getid.md) - -## SearchSource.getId() method - -Signature: - -```typescript -getId(): string; -``` -Returns: - -`string` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md deleted file mode 100644 index d5a133772264e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getOwnField](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) - -## SearchSource.getOwnField() method - -Get the field from our own fields, don't traverse up the chain - -Signature: - -```typescript -getOwnField(field: K): SearchSourceFields[K]; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| field | K | | - -Returns: - -`SearchSourceFields[K]` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md deleted file mode 100644 index 14578f7949ba6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getParent](./kibana-plugin-plugins-data-public.searchsource.getparent.md) - -## SearchSource.getParent() method - -Get the parent of this SearchSource {undefined\|searchSource} - -Signature: - -```typescript -getParent(): SearchSource | undefined; -``` -Returns: - -`SearchSource | undefined` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md deleted file mode 100644 index f3451c9391074..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getSearchRequestBody](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) - -## SearchSource.getSearchRequestBody() method - -Signature: - -```typescript -getSearchRequestBody(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md deleted file mode 100644 index e77c9dac7239f..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [history](./kibana-plugin-plugins-data-public.searchsource.history.md) - -## SearchSource.history property - -Signature: - -```typescript -history: SearchRequest[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md deleted file mode 100644 index 5f2fc809a5590..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ /dev/null @@ -1,46 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) - -## SearchSource class - -Signature: - -```typescript -export declare class SearchSource -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(fields)](./kibana-plugin-plugins-data-public.searchsource._constructor_.md) | | Constructs a new instance of the SearchSource class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [history](./kibana-plugin-plugins-data-public.searchsource.history.md) | | SearchRequest[] | | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [create()](./kibana-plugin-plugins-data-public.searchsource.create.md) | | | -| [createChild(options)](./kibana-plugin-plugins-data-public.searchsource.createchild.md) | | | -| [createCopy()](./kibana-plugin-plugins-data-public.searchsource.createcopy.md) | | | -| [destroy()](./kibana-plugin-plugins-data-public.searchsource.destroy.md) | | Completely destroy the SearchSource. {undefined} | -| [fetch(options)](./kibana-plugin-plugins-data-public.searchsource.fetch.md) | | Fetch this source and reject the returned Promise on error | -| [getField(field, recurse)](./kibana-plugin-plugins-data-public.searchsource.getfield.md) | | Get fields from the fields | -| [getFields()](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | | -| [getId()](./kibana-plugin-plugins-data-public.searchsource.getid.md) | | | -| [getOwnField(field)](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) | | Get the field from our own fields, don't traverse up the chain | -| [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | -| [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | | -| [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start | -| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index.Using createSearchSource, the instance can be re-created. | -| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | | -| [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | | -| [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from | -| [setPreferredSearchStrategyId(searchStrategyId)](./kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md) | | \*\*\* PUBLIC API \*\*\* | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md deleted file mode 100644 index 092d057c69196..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [onRequestStart](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) - -## SearchSource.onRequestStart() method - -Add a handler that will be notified whenever requests start - -Signature: - -```typescript -onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| handler | (searchSource: ISearchSource, options?: FetchOptions) => Promise<unknown> | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md deleted file mode 100644 index 52d25dec01dfd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md +++ /dev/null @@ -1,27 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [serialize](./kibana-plugin-plugins-data-public.searchsource.serialize.md) - -## SearchSource.serialize() method - -Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object. - -The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. - -Using `createSearchSource`, the instance can be re-created. - -Signature: - -```typescript -serialize(): { - searchSourceJSON: string; - references: SavedObjectReference[]; - }; -``` -Returns: - -`{ - searchSourceJSON: string; - references: SavedObjectReference[]; - }` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md deleted file mode 100644 index 83b7c30281752..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md +++ /dev/null @@ -1,23 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setField](./kibana-plugin-plugins-data-public.searchsource.setfield.md) - -## SearchSource.setField() method - -Signature: - -```typescript -setField(field: K, value: SearchSourceFields[K]): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| field | K | | -| value | SearchSourceFields[K] | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md deleted file mode 100644 index fa9b265aa43b7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setFields](./kibana-plugin-plugins-data-public.searchsource.setfields.md) - -## SearchSource.setFields() method - -Signature: - -```typescript -setFields(newFields: SearchSourceFields): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| newFields | SearchSourceFields | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md deleted file mode 100644 index 19bf10bec210f..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setParent](./kibana-plugin-plugins-data-public.searchsource.setparent.md) - -## SearchSource.setParent() method - -Set a searchSource that this source should inherit from - -Signature: - -```typescript -setParent(parent?: ISearchSource, options?: SearchSourceOptions): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| parent | ISearchSource | | -| options | SearchSourceOptions | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md deleted file mode 100644 index 8d8dbce9e60f6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setPreferredSearchStrategyId](./kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md) - -## SearchSource.setPreferredSearchStrategyId() method - -\*\*\* PUBLIC API \*\*\* - -Signature: - -```typescript -setPreferredSearchStrategyId(searchStrategyId: string): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| searchStrategyId | string | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md index 2b986aee508e2..11f18a195d271 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md @@ -10,7 +10,7 @@ fieldFormats: { FieldFormatsRegistry: typeof FieldFormatsRegistry; FieldFormat: typeof FieldFormat; - serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index 5c2f542204079..bd617990a00a2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -9,7 +9,7 @@ ```typescript setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; @@ -26,7 +26,7 @@ setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { `{ fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; search: ISearchSetup; }` diff --git a/docs/images/clone_panel.gif b/docs/images/clone_panel.gif new file mode 100644 index 0000000000000..e521e438d051a Binary files /dev/null and b/docs/images/clone_panel.gif differ diff --git a/docs/images/report-automate-csv.png b/docs/images/report-automate-csv.png new file mode 100644 index 0000000000000..fba77821ae29f Binary files /dev/null and b/docs/images/report-automate-csv.png differ diff --git a/docs/images/report-automate-pdf.png b/docs/images/report-automate-pdf.png new file mode 100644 index 0000000000000..f96eebe6fe02d Binary files /dev/null and b/docs/images/report-automate-pdf.png differ diff --git a/docs/images/vega_lite_default.png b/docs/images/vega_lite_default.png new file mode 100644 index 0000000000000..1ce1592d0738c Binary files /dev/null and b/docs/images/vega_lite_default.png differ diff --git a/docs/logs/images/logs-action-menu.png b/docs/logs/images/logs-action-menu.png new file mode 100644 index 0000000000000..f1c79b6fa88d1 Binary files /dev/null and b/docs/logs/images/logs-action-menu.png differ diff --git a/docs/logs/images/logs-view-in-context.png b/docs/logs/images/logs-view-in-context.png new file mode 100644 index 0000000000000..09a9e89fc3042 Binary files /dev/null and b/docs/logs/images/logs-view-in-context.png differ diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index 96e7c5fa4d312..4f5992f945c7a 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -69,12 +69,19 @@ To highlight a word or phrase in the logs stream, click *Highlights* and enter y [float] [[logs-event-inspector]] === Inspect a log event -To inspect a log event, hover over it, then click the *View details* icon image:logs/images/logs-view-event.png[View event icon] beside the event. -This opens the *Log event document details* fly-out that shows the fields associated with the log event. +To inspect a log event, hover over it, then click the *View actions for line* icon image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View details*. This opens the *Log event document details* fly-out that shows the fields associated with the log event. To quickly filter the logs stream by one of the field values, in the log event details, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field. This automatically adds a search filter to the logs stream to filter the entries by this field and value. +[float] +[[log-view-in-context]] +=== View log line in context +To view a certain line in its context (for example, with other log lines from the same file, or the same cloud container), hover over it, then click the *View actions for line* image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View in context*. This opens the *View log in context* modal, that shows the log line in its context. + +[role="screenshot"] +image::logs/images/logs-view-in-context.png[View a log line in context] + [float] [[view-log-anomalies]] === View log anomalies diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index f62a4d28dfc0d..51910169e8673 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -142,7 +142,14 @@ This setting does not have an effect when loading a saved search. Highlighting slows requests when working on big documents. +[float] +[[kibana-ml-settings]] +==== Machine learning +[horizontal] +`ml:fileDataVisualizerMaxFileSize`:: Sets the file size limit when importing +data in the {data-viz}. The default value is `100MB`. The highest supported +value for this setting is `1GB`. [float] @@ -217,6 +224,8 @@ might increase the search time. This setting is off by default. Users must opt-i [horizontal] `siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. `siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. +`siem:ipReputationLinks`:: A JSON array containing links for verifying the reputation of an IP address. The links are displayed on +{siem-guide}/siem-ui-overview.html#network-ui[IP detail] pages. `siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* page. `siem:newsFeedUrl`:: The URL from which the security news feed content is diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 6528568e86890..36578c909f513 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -8,7 +8,6 @@ You do not need to configure any settings to use {kib} {ml-features}. They are enabled by default. -[float] [[general-ml-settings-kb]] ==== General {ml} settings @@ -19,3 +18,5 @@ If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, you can still use the {ml} APIs. To disable {ml} entirely, see the {ref}/ml-settings.html[{es} {ml} settings]. + + diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index ddabce3d5b842..12ee96b21b0c7 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -94,7 +94,7 @@ Some example translations are shown here: **Environment Variable**:: **Kibana Setting** `SERVER_NAME`:: `server.name` `KIBANA_DEFAULTAPPID`:: `kibana.defaultAppId` -`XPACK_MONITORING_ENABLED`:: `xpack.monitoring.enabled` +`MONITORING_ENABLED`:: `monitoring.enabled` In general, any setting listed in <> can be configured with this technique. @@ -125,9 +125,9 @@ images: `server.name`:: `kibana` `server.host`:: `"0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` -`xpack.monitoring.ui.container.elasticsearch.enabled`:: `true` +`monitoring.ui.container.elasticsearch.enabled`:: `true` -NOTE: The setting `xpack.monitoring.ui.container.elasticsearch.enabled` is not +NOTE: The setting `monitoring.ui.container.elasticsearch.enabled` is not defined in the `-oss` image. These settings are defined in the default `kibana.yml`. They can be overridden diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc index eef2b11e53d85..19f9d64d13623 100644 --- a/docs/setup/production.asciidoc +++ b/docs/setup/production.asciidoc @@ -133,7 +133,8 @@ server.port Settings that must be the same: -------- xpack.security.encryptionKey //decrypting session cookies -xpack.reporting.encryptionKey //decrypting reports stored in Elasticsearch +xpack.reporting.encryptionKey //decrypting reports +xpack.encryptedSavedObjects.encryptionKey // decrypting saved objects -------- Separate configuration files can be used from the command line by using the `-c` flag: diff --git a/docs/siem/images/cases-ui.png b/docs/siem/images/cases-ui.png new file mode 100644 index 0000000000000..b513efb664740 Binary files /dev/null and b/docs/siem/images/cases-ui.png differ diff --git a/docs/siem/siem-ui.asciidoc b/docs/siem/siem-ui.asciidoc index 85253daaf2933..985138756622d 100644 --- a/docs/siem/siem-ui.asciidoc +++ b/docs/siem/siem-ui.asciidoc @@ -35,7 +35,7 @@ image::siem/images/network-ui.png[] [float] [[detections-ui]] -=== Detections (Beta) +=== Detections (beta) The Detections feature automatically searches for threats and creates signals when they are detected. Signal detection rules define the conditions @@ -50,6 +50,22 @@ or the Detections API. [role="screenshot"] image::siem/images/detections-ui.png[] +[float] +[[cases-ui]] +=== Cases (beta) + +Cases are used to open and track security issues directly in SIEM. +Cases list the original reporter and all users who contribute to a case +(`participants`). Case comments support Markdown syntax, and allow linking to +saved Timelines. Additionally, you can send cases to external systems from +within SIEM (currently ServiceNow). + +For information about opening, updating, and closing cases, see +{siem-guide}/cases-overview.html[Cases] in the SIEM Guide. + +[role="screenshot"] +image::siem/images/cases-ui.png[] + [float] [[timelines-ui]] === Timeline diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 49e7bd1d77743..8794c389d72bc 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -41,12 +41,14 @@ see https://www.elastic.co/subscriptions[the subscription page]. [float] [[create-connectors]] -=== Connectors +=== Preconfigured connectors and action types You can create connectors for actions in <> or via the action API. For out-of-the-box and standardized connectors, you can <> before {kib} starts. +Action type with only preconfigured connectors could be specified as a <>. + include::action-types/email.asciidoc[] include::action-types/index.asciidoc[] include::action-types/pagerduty.asciidoc[] @@ -54,3 +56,4 @@ include::action-types/server-log.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::pre-configured-connectors.asciidoc[] +include::pre-configured-action-types.asciidoc[] diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index be3623dd9e59c..794fc14005f2f 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[email-action-type]] -== Email action type +=== Email action The email action type uses the SMTP protocol to send mail message, using an integration of https://nodemailer.com/[Nodemailer]. Email message text is sent as both plain text and html text. @@ -10,11 +10,11 @@ The email action type uses the SMTP protocol to send mail message, using an inte Email connectors have the following configuration properties: -Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. +Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Sender:: The from address for all emails sent with this connector, specified in `user@host-name` format. -Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. +Host:: Host name of the service provider. If you are using the <> setting, make sure this hostname is whitelisted. Port:: The port to connect to on the service provider. -Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. +Secure:: If true the connection will use TLS when connecting to the service provider. See https://nodemailer.com/smtp/#tls-options[nodemailer TLS documentation] for more information. Username:: username for 'login' type authentication. Password:: password for 'login' type authentication. @@ -26,4 +26,4 @@ Email actions have the following configuration properties: To, CC, BCC:: Each is a list of addresses. Addresses can be specified in `user@host-name` format, or in `name ` format. One of To, CC, or BCC must contain an entry. Subject:: The subject line of the email. -Message:: The message text of the email. Markdown format is supported. \ No newline at end of file +Message:: The message text of the email. Markdown format is supported. diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc index 75d9e57b1f212..625b8f704b7c6 100644 --- a/docs/user/alerting/action-types/index.asciidoc +++ b/docs/user/alerting/action-types/index.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[index-action-type]] -== Index action type +=== Index action The index action type will index a document into {es}. @@ -21,4 +21,4 @@ Execution time field:: This field will be automatically set to the time the ale Index actions have the following properties: -Document:: The document to index in json format. \ No newline at end of file +Document:: The document to index in json format. diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index 50a1f31e4a9ae..abdcc7d1ba524 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -1,9 +1,130 @@ [role="xpack"] [[pagerduty-action-type]] -== PagerDuty action type +=== PagerDuty action The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/events-api-v2[v2 Events API] to trigger, acknowledge, and resolve PagerDuty alerts. +* <> +* <> +* <> + +[float] +[[pagerduty-benefits]] +==== PagerDuty + Elastic integration benefits + +By integrating PagerDuty with alerts, you can: + +* Route your alerts to the right PagerDuty responder within your team, based on your structure, escalation policies, and workflows. +* Automatically generate incidents of different types and severity based on each alert’s context. +* Tailor the incident data to match your needs by easily passing the alerting context from Kibana to PagerDuty. + +[float] +[[pagerduty-how-it-works]] +===== How it works + +{kib} allows you to create alerts to notify you of a significant move +in your dataset. +You can create alerts for all your Observability, Security, and Elastic Stack use cases. +Alerts will trigger a new incident on the corresponding PagerDuty service. + +[float] +===== Requirements + +In the `kibana.yml` configuration file, you must add the <>. +This is required to encrypt parameters that must be secured, for example PagerDuty’s integration key. + +If you have security enabled: + +* You must have +application privileges to access Metrics, APM, Uptime, or SIEM. +* If you are using a self-managed deployment with security, you must have +Transport Security Layer (TLS) enabled for communication <>. +Alerts uses API keys to secure background alert checks and actions, +and API keys require {ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. + +Although not a requirement, to harden the integrations security you might want to +review the <> that are available to you. + +[float] +[[pagerduty-support]] +===== Support +If you need help with this integration, get in touch with the {kib} team by visiting +https://support.elastic.co[support.elastic.co] or by using the *Ask Elastic* option in the {kib} Help menu. +You can also select the {kib} category at https://discuss.elastic.co/[discuss.elastic.co]. + +[float] +[[pagerduty-integration-walkthrough]] +===== Integration with PagerDuty walkthrough + +[[pagerduty-in-pagerduty]] +*In PagerDuty* + +. From the *Configuration* menu, select *Services*. +. Add an integration to a service: ++ +* If you are adding your integration to an existing service, +click the name of the service you want to add the integration to. +Then, select the *Integrations* tab and click the *New Integration* button. +* If you are creating a new service for your integration, +go to +https://support.pagerduty.com/docs/services-and-integrations#section-configuring-services-and-integrations[Configuring Services and Integrations] +and follow the steps outlined in the *Create a New Service* section, selecting *Elastic* as the *Integration Type* in step 4. +Continue with the <> section once you have finished these steps. + +. Enter an *Integration Name* in the format Elastic-service-name (for example, Elastic-Alerting or Kibana-APM-Alerting) +and select Elastic from the *Integration Type* menu. +. Click *Add Integration* to save your new integration. ++ +You will be redirected to the *Integrations* tab for your service. An Integration Key is generated on this screen. ++ +[role="screenshot"] +image::user/alerting/images/pagerduty-integration.png[PagerDuty Integrations tab] + +. Save this key, as you will use it when you configure the integration with Elastic in the next section. + +[[pagerduty-in-elastic]] +*In Elastic* + +. Create a PagerDuty Connector in Kibana. You can: ++ +* Create a connector as part of creating an alert by selecting PagerDuty in the *Actions* +section of the alert configuration and selecting *Add new*. +* Alternatively, create a connector by navigating to *Management* from the {kib} navbar and selecting +*Alerts and Actions*. Then, select the *Connectors* tab, click the *Create connector* button, and select the PagerDuty option. + +. Configure the connector by giving it a name and optionally entering the API URL and Routing Key, or using the defaults. ++ +See <> for how to obtain the endpoint and key information from PagerDuty and +<> for more details. + +. Save the Connector. + +. Create an alert using *Management > Alerts and Actions* or the application of your choice. + +. Set up an action using your PagerDuty connector, by determining: ++ +* The action’s type: Trigger, Resolve, or Acknowledge. +* The event’s severity: Info, warning, error, or critical. +* An array of different fields, including the timestamp, group, class, component, and your dedup key. +Depending on your custom needs, assign them variables from the alerting context. +To see the available context variables, click on the *Add alert variable* icon next +to each corresponding field. For more details on these parameters, see the +<> and the PagerDuty +https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[API v2 documentation]. + + +[float] +[[pagerduty-uninstall]] +===== How to uninstall +To remove a PagerDuty connector from an alert, simply remove it +from the *Actions* section of that alert, using the remove (x) icon. +This will disable the integration for the particular alert. + +To delete the connector entirely, go to *Management > Alerts and Actions*. +Select the *Connectors* tab, and then click on the delete icon. +This is an irreversible action and impacts all alerts that use this connector. + + [float] [[pagerduty-connector-configuration]] ==== Connector configuration @@ -11,7 +132,7 @@ The PagerDuty action type uses the https://v2.developer.pagerduty.com/docs/event PagerDuty connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. +API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is whitelisted. Routing Key:: A 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. [float] @@ -26,8 +147,8 @@ Dedup Key:: All actions sharing this key will be associated with the same Pa Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. -Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. +Source:: An *optional* value indicating the affected system, preferably a hostname or fully qualified domain name. Defaults to the {kib} saved object id of the action. Summary:: An *optional* text summary of the event, defaults to `No summary provided`. The maximum length is 1024 characters. Class:: An *optional* value indicating the class/type of the event, for example `ping failure` or `cpu load`. -For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. \ No newline at end of file +For more details on these properties, see https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2[PagerDuty v2 event parameters]. diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc index 4efbdf3bea099..8f888785626c9 100644 --- a/docs/user/alerting/action-types/server-log.asciidoc +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[server-log-action-type]] -== Server log action type +=== Server log action This action type writes and entry to the {kib} server log. @@ -18,4 +18,4 @@ Name:: The name of the connector. The name is used to identify a connector Server log actions have the following properties: -Message:: The message to log. \ No newline at end of file +Message:: The message to log. diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc index a4bacbf162e46..c0965d65bfdbe 100644 --- a/docs/user/alerting/action-types/slack.asciidoc +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -1,8 +1,8 @@ [role="xpack"] [[slack-action-type]] -== Slack action type +=== Slack action -The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. +The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incoming Webhooks]. [float] [[slack-connector-configuration]] @@ -11,7 +11,7 @@ The Slack action type uses https://api.slack.com/incoming-webhooks[Slack Incomin Slack connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. +Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is whitelisted. [float] [[slack-action-configuration]] @@ -19,4 +19,4 @@ Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messa Slack actions have the following properties: -Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. \ No newline at end of file +Message:: The message text, converted to the `text` field in the Webhook JSON payload. Currently only the text field is supported. Markdown, images, and other advanced formatting are not yet supported. diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index 8c211aa83af89..64bfa6a1d6364 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -1,6 +1,6 @@ [role="xpack"] [[webhook-action-type]] -== Webhook action type +=== Webhook action The Webhook action type uses https://github.com/axios/axios[axios] to send a POST or PUT request to a web service. @@ -11,7 +11,7 @@ The Webhook action type uses https://github.com/axios/axios[axios] to send a POS Webhook connectors have the following configuration properties: Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. -URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. +URL:: The request URL. If you are using the <> setting, make sure the hostname is whitelisted. Method:: HTTP request method, either `post`(default) or `put`. Headers:: A set of key-value pairs sent as headers with the request User:: An optional username. If set, HTTP basic authentication is used. Currently only basic authentication is supported. @@ -23,4 +23,4 @@ Password:: An optional password. If set, HTTP basic authentication is used. Cur Webhook actions have the following properties: -Body:: A json payload sent to the request URL. \ No newline at end of file +Body:: A json payload sent to the request URL. diff --git a/docs/user/alerting/images/pagerduty-integration.png b/docs/user/alerting/images/pagerduty-integration.png new file mode 100755 index 0000000000000..0a270de866b36 Binary files /dev/null and b/docs/user/alerting/images/pagerduty-integration.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-alert-form.png b/docs/user/alerting/images/pre-configured-action-type-alert-form.png new file mode 100644 index 0000000000000..e12bad468009a Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-alert-form.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-managing.png b/docs/user/alerting/images/pre-configured-action-type-managing.png new file mode 100644 index 0000000000000..95fe1c6aa0958 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-managing.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-select-type.png b/docs/user/alerting/images/pre-configured-action-type-select-type.png new file mode 100644 index 0000000000000..5f555f851cd81 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-select-type.png differ diff --git a/docs/user/alerting/pre-configured-action-types.asciidoc b/docs/user/alerting/pre-configured-action-types.asciidoc new file mode 100644 index 0000000000000..780a2119037b1 --- /dev/null +++ b/docs/user/alerting/pre-configured-action-types.asciidoc @@ -0,0 +1,61 @@ +[role="xpack"] +[[pre-configured-action-types]] + +== Preconfigured action types + +A preconfigure an action type has all the information it needs prior to startup. +A preconfigured action type offers the following capabilities: + +- Requires no setup. Configuration and credentials needed to execute an +action are predefined. +- Has only <>. +- Connectors of the preconfigured action type cannot be edited or deleted. + +[float] +[[preconfigured-action-type-example]] +=== Creating a preconfigured action + +In the `kibana.yml` file: + +. Exclude the action type from `xpack.actions.enabledActionTypes`. +. Add all its connectors. + +The following example shows a valid configuration of preconfigured action type with one out-of-the box connector. + +```js + xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1> + xpack.actions.preconfigured: <2> + - id: 'my-server-log' + actionTypeId: .server-log + name: 'Server log #xyz' +``` + +<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors. +<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. + +[float] +[[pre-configured-action-type-alert-form]] +=== Attaching a preconfigured action to an alert + +To attach an action to an alert, +select from a list of available action types, and +then select the *Server log* type. This action type was configured previously. + +[role="screenshot"] +image::images/pre-configured-action-type-alert-form.png[Create alert with selected Server log action type] + +[float] +[[managing-pre-configured-action-types]] +=== Managing preconfigured actions + +Connectors with preconfigured actions appear in the connector list, regardless of which space the user is in. +They are tagged as “preconfigured” and cannot be deleted. + +[role="screenshot"] +image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured] + +Clicking *Create connector* shows the list of available action types. +Preconfigured action types are not included because you can't create a connector with a preconfigured action type. + +[role="screenshot"] +image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu] diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc index 3db13acfb423e..4c408da92f579 100644 --- a/docs/user/alerting/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/pre-configured-connectors.asciidoc @@ -20,8 +20,7 @@ action are predefined, including the connector name and ID. The following example shows a valid configuration 2 out-of-the box connector. -[source,console] ------------------------- +```js xpack.actions.preconfigured: - id: 'my-slack1' <1> actionTypeId: .slack <2> @@ -40,7 +39,7 @@ The following example shows a valid configuration 2 out-of-the box connector. secrets: <5> user: elastic password: changeme ------------------------- +``` <1> `id` is the action connector identifier. <2> `actionTypeId` is the action type identifier. diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index ab529a533d5e3..de714ae40086b 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -98,6 +98,24 @@ to the new dimensions. * To delete a panel, open the panel menu and select *Delete from dashboard.* Deleting a panel from a dashboard does *not* delete the saved visualization or search. +[float] +[[cloning-a-panel]] +=== Clone dashboard elements + +In *Edit* mode, you can clone any panel on a dashboard. + +To clone an existing panel, open the panel menu of the element you wish to clone, then select *Clone panel*. + +* Cloned panels appear beside the original, and will move other panels down to make room if necessary. + +* Clones support all of the original panel's functionality, including renaming, editing, and cloning. + +* All cloned visualizations will appear in the visualization list. + +[role="screenshot"] +image:images/clone_panel.gif[clone panel] + + [float] [[viewing-detailed-information]] === Inspect and edit elements diff --git a/docs/user/ml/images/ml-data-visualizer-sample.jpg b/docs/user/ml/images/ml-data-visualizer-sample.jpg index 6c2e018932717..ce2bb660d7da1 100644 Binary files a/docs/user/ml/images/ml-data-visualizer-sample.jpg and b/docs/user/ml/images/ml-data-visualizer-sample.jpg differ diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index cca0dc5e4530f..6483ddde07335 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -4,31 +4,31 @@ [partintro] -- -As datasets increase in size and complexity, the human effort required to +As data sets increase in size and complexity, the human effort required to inspect dashboards or maintain rules for spotting infrastructure problems, cyber attacks, or business issues becomes impractical. Elastic {ml-features} such as {anomaly-detect} and {oldetection} make it easier to notice suspicious activities with minimal human interference. -If you have a basic license, you can use the *Data Visualizer* to learn more -about your data. In particular, if your data is stored in {es} and contains a -time field, you can use the *Data Visualizer* to identify possible fields for -{anomaly-detect}: +{kib} includes a free *{data-viz}* to learn more about your data. In particular, +if your data is stored in {es} and contains a time field, you can use the +*{data-viz}* to identify possible fields for {anomaly-detect}: [role="screenshot"] -image::user/ml/images/ml-data-visualizer-sample.jpg[Data Visualizer for sample flight data] +image::user/ml/images/ml-data-visualizer-sample.jpg[{data-viz} for sample flight data] -experimental[] You can also upload a CSV, NDJSON, or log file (up to 100 MB in -size). The *Data Visualizer* identifies the file format and field mappings. You -can then optionally import that data into an {es} index. +experimental[] You can also upload a CSV, NDJSON, or log file. The *{data-viz}* +identifies the file format and field mappings. You can then optionally import +that data into an {es} index. To change the default file size limit, see +<>. -You need the following permissions to use the Data Visualizer with file upload: +You need the following permissions to use the {data-viz} with file upload: * cluster privileges: `monitor`, `manage_ingest_pipelines` * index privileges: `read`, `manage`, `index` For more information, see {ref}/security-privileges.html[Security privileges] -and {ref}/built-in-roles.html[Built-in roles]. +and {ml-docs}/setup.html[Set up {ml-features}]. -- diff --git a/docs/user/monitoring/cluster-alerts.asciidoc b/docs/user/monitoring/cluster-alerts.asciidoc index cfdc9e2037030..a58ccc7f7d68d 100644 --- a/docs/user/monitoring/cluster-alerts.asciidoc +++ b/docs/user/monitoring/cluster-alerts.asciidoc @@ -49,13 +49,13 @@ To receive email notifications for the Cluster Alerts: . Configure an email account as described in {ref}/actions-email.html#configuring-email[Configuring email accounts]. . Configure the -`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +`monitoring.cluster_alerts.email_notifications.email_address` setting in `kibana.yml` with your email address. + -- TIP: If you have separate production and monitoring clusters and separate {kib} instances for those clusters, you must put the -`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +`monitoring.cluster_alerts.email_notifications.email_address` setting in the {kib} instance that is associated with the production cluster. -- diff --git a/docs/user/monitoring/elasticsearch-details.asciidoc b/docs/user/monitoring/elasticsearch-details.asciidoc index c0e804672d298..93f809cfff650 100644 --- a/docs/user/monitoring/elasticsearch-details.asciidoc +++ b/docs/user/monitoring/elasticsearch-details.asciidoc @@ -164,4 +164,4 @@ image::user/monitoring/images/monitoring-elasticsearch-logs.jpg["Recent {es} log TIP: By default, up to 10 log entries are shown. You can show up to 50 log entries by changing the -<>. +<>. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index d0f2bd6acd901..9aa10289d299b 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -61,8 +61,8 @@ For more information, see {ref}/monitoring-settings.html[Monitoring settings in and {ref}/cluster-update-settings.html[Cluster update settings]. -- -. Verify that `xpack.monitoring.enabled` and -`xpack.monitoring.kibana.collection.enabled` are set to `true` in the +. Verify that `monitoring.enabled` and +`monitoring.kibana.collection.enabled` are set to `true` in the `kibana.yml` file. These are the default values. For more information, see <>. diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index f03a2ce1525a4..61aeaf21d3a4b 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -25,10 +25,10 @@ Add the following setting in the {kib} configuration file (`kibana.yml`): [source,yaml] ---------------------------------- -xpack.monitoring.kibana.collection.enabled: false +monitoring.kibana.collection.enabled: false ---------------------------------- -Leave the `xpack.monitoring.enabled` set to its default value (`true`). +Leave the `monitoring.enabled` set to its default value (`true`). // end::disable-kibana-collection[] For more information, see <>. diff --git a/docs/user/monitoring/monitoring-troubleshooting.asciidoc b/docs/user/monitoring/monitoring-troubleshooting.asciidoc index 7e1d6f94f15fa..bdaa10990c3aa 100644 --- a/docs/user/monitoring/monitoring-troubleshooting.asciidoc +++ b/docs/user/monitoring/monitoring-troubleshooting.asciidoc @@ -52,7 +52,7 @@ The *Stack Monitoring* page in {kib} is empty. . Confirm that {kib} is seeking monitoring data from the appropriate {es} URL. By default, data is retrieved from the cluster specified in the `elasticsearch.hosts` setting in the `kibana.yml` file. If you want to retrieve it -from a different monitoring cluster, set `xpack.monitoring.elasticsearch.hosts`. +from a different monitoring cluster, set `monitoring.ui.elasticsearch.hosts`. See <>. . Confirm that there is monitoring data available at that URL. It is stored in diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 11516e32400fb..0a5535e6e1a91 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -26,14 +26,14 @@ cluster and view them all through the same instance of {kib}. By default, data is retrieved from the cluster specified in the `elasticsearch.hosts` value in the `kibana.yml` file. If you want to retrieve it -from a different cluster, set `xpack.monitoring.elasticsearch.hosts`. +from a different cluster, set `monitoring.ui.elasticsearch.hosts`. To learn more about typical monitoring architectures, see {ref}/how-monitoring-works.html[How monitoring works] and {ref}/monitoring-production.html[Monitoring in a production environment]. -- -. Verify that `xpack.monitoring.ui.enabled` is set to `true`, which is the +. Verify that `monitoring.ui.enabled` is set to `true`, which is the default value, in the `kibana.yml` file. For more information, see <>. @@ -43,8 +43,8 @@ must provide a user ID and password so {kib} can retrieve the data. .. Create a user that has the `monitoring_user` {ref}/built-in-roles.html[built-in role] on the monitoring cluster. -.. Add the `xpack.monitoring.elasticsearch.username` and -`xpack.monitoring.elasticsearch.password` settings in the `kibana.yml` file. +.. Add the `monitoring.ui.elasticsearch.username` and +`monitoring.ui.elasticsearch.password` settings in the `kibana.yml` file. If these settings are omitted, {kib} uses the `elasticsearch.username` and `elasticsearch.password` setting values. For more information, see {kibana-ref}/using-kibana-with-security.html[Configuring security in {kib}]. diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 5d35f103ecee0..3e227229ddcc5 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -1,32 +1,42 @@ [role="xpack"] [[automating-report-generation]] == Automating report generation -You can automatically generate reports with {watcher}, or by submitting -HTTP `POST` requests from a script. +Automatically generate PDF and CSV reports by submitting HTTP `POST` requests using {watcher} or a script. include::report-intervals.asciidoc[] [float] -=== Get the POST URL +=== Create a POST URL -Generating a report either through {watcher} or a script requires capturing the **POST -URL**, which is the URL to queue a report for generation. +Create the POST +URL that triggers a report to generate. -To get the URL for triggering PDF report generation during a given time period: +To create the POST URL for PDF reports: -. Load the saved object in *Visualize* or *Dashboard*. -. To specify a relative or absolute time period, use the time filter. -. In the {kib} toolbar, click *Share*. -. Select *PDF Reports*. -. Click **Copy POST URL**. +. Go to *Visualize* or *Dashboard*, then open the visualization or dashboard. ++ +To specify a relative or absolute time period, use the time filter. -To get the URL for triggering CSV report generation during a given time period: +. From the {kib} toolbar, click *Share*, then select *PDF Reports*. + +. Click *Copy POST URL*. ++ +[role="screenshot"] +image::images/report-automate-pdf.png[Generate Visualize and Dashboard reports] + + +To create the POST URL for CSV reports: . Load the saved search in *Discover*. -. To specify a relative or absolute time period, use the time filter. -. In the {kib} toolbar, click *Share*. -. Select *CSV Reports*. -. Click **Copy POST URL**. ++ +To specify a relative or absolute time period, use the time filter. + +. From the {kib} toolbar, click *Share*, then select *CSV Reports*. + +. Click *Copy POST URL*. ++ +[role="screenshot"] +image::images/report-automate-csv.png[Generate Discover reports] [float] === Use Watcher diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc index 97313c19f44cb..3bdc59b90b3fd 100644 --- a/docs/user/security/securing-communications/index.asciidoc +++ b/docs/user/security/securing-communications/index.asciidoc @@ -188,5 +188,5 @@ verification. For more information about this setting, see <>. + +* *Pin filters to global state* — When selected, all filters created by interacting with the inputs are automatically pinned. + +. Click *Update*. + [float] [[markdown-widget]] === Markdown diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index b181763c0d0d0..422afbb201183 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -14,20 +14,6 @@ beta[] * Save your visualization for use in a dashboard. -[float] -[[lens-aggregation]] -=== Supported aggregations - -Lens supports the following aggregations: - -* <> - -* <> - -* <> - -* <> - [float] [[drag-drop]] === Drag and drop diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc index ba291e3cc6859..f716930e7e65c 100644 --- a/docs/visualize/most-frequent.asciidoc +++ b/docs/visualize/most-frequent.asciidoc @@ -13,20 +13,6 @@ The most frequently used visualizations include: [[metric-chart]] -[float] -[[frequently-used-viz-aggregation]] -=== Supported aggregations - -The most frequently used visualizations support the following aggregations: - -* <> - -* <> - -* <> - -* <> - [float] === Configure your visualization diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 51342847080e0..c889bd0bb6ca0 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -6,19 +6,6 @@ Display graphical representations of data where the individual values are repres [role="screenshot"] image::images/visualize_heat_map_example.png[] -[float] -[[build-heat-map]] -=== Build a heat map - -To display your data on the heat map, use the supported aggregations. - -Heat maps support the following aggregations: - -* <> -* <> -* <> -* <> - [float] [[navigate-heatmap]] === Change the color ranges diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc index 69d6985acd1e4..36709c2cc6437 100644 --- a/docs/visualize/tsvb.asciidoc +++ b/docs/visualize/tsvb.asciidoc @@ -43,18 +43,6 @@ Table:: Display data from multiple time series by defining the field group to sh [role="screenshot"] image:images/tsvb-table.png["Table visualization"] -[float] -[[tsvb-aggregation]] -=== Supported aggregations - -TSVB supports the following aggregations: - -* <> - -* <> - -* <> - [float] [[create-tsvb-visualization]] === Create TSVB visualizations diff --git a/docs/visualize/vega.asciidoc b/docs/visualize/vega.asciidoc index b8c0d1dbe3dda..e0d9955f0c3db 100644 --- a/docs/visualize/vega.asciidoc +++ b/docs/visualize/vega.asciidoc @@ -1,81 +1,80 @@ [[vega-graph]] -== Vega Graphs -experimental[] +== Vega -You can build https://vega.github.io/vega/examples/[Vega] and -https://vega.github.io/vega-lite/examples/[Vega-Lite] data visualizations -into Kibana, either standalone, or on top of a map. To see Vega in action, -watch this -https://www.youtube.com/watch?v=lQGCipY3th8[short introduction video]. +experimental[] -Currently Vega version 4.3 and Vega-Lite version 2.6 are supported. +Build custom visualizations from multiple data sources using Vega +and Vega-Lite. -NOTE: In Vega it is possible to load data dynamically, e.g. by setting signals as data urls. This is not supported in Kibana as all data is fetched at once prior to passing it to the Vega renderer. +* *Vega* — A declarative format to create visualizations using JSON. + Generate interactive displays using D3. +* *Vega-Lite* — An easier format to use than Vega that enables more rapid + data analysis. Compiles into Vega. -[[vega-quick-demo]] -=== Getting Started with Vega +For more information about Vega and Vega-Lite, refer to +<>. -* To experiment using sample data, first click the {kib} logo in the upper left hand corner -and then click the link next to *Sample Data*. -* Once you have data loaded, go to *Visualize*, click *+*, and select *Vega* to see an example graph. -*Note*: The default graph is written in Vega-Lite, but you can build visualizations -in either language. See <> for more information. -* Try changing `mark` from `line` to `point`, `area`, `bar`, `circle`, -or `square`. Check out the -https://vega.github.io/vega-lite/docs/mark.html#mark-def[Vega-Lite docs] for more information. -* Explore other available https://vega.github.io/vega/examples/[Vega] or -https://vega.github.io/vega-lite/examples/[Vega-Lite] visualizations. -*Note*: You might need to make URLs absolute, for example, replace -`"url": "data/world-110m.json"` with -`"url": "https://vega.github.io/editor/data/world-110m.json"`. -See <>. -* For more information on getting started, check out this https://www.elastic.co/blog/getting-started-with-vega-visualizations-in-kibana[blog post]. +[float] +[[create-vega-viz]] +=== Create Vega visualizations +You create Vega visualizations by using the text editor, which is +preconfigured with the options you need. -[[vega-vs-vegalite]] -=== Vega vs Vega-Lite +[role="screenshot"] +image::images/vega_lite_default.png[] -The Vega visualization in {kib} supports both Vega and Vega-Lite. You can use the -`schema` value to define which language you would like to use and its minimum -required version. +[float] +[[vega-schema]] +==== Change the Vega version -For example: +The default visualization uses Vega-Lite version 2. To use Vega version 4, edit +the `schema`. -* Vega-Lite v2: `$schema: https://vega.github.io/schema/vega-lite/v2.json` -* Vega v4: `$schema: https://vega.github.io/schema/vega/v4.json` +Go to `$schema`, enter `https://vega.github.io/schema/vega/v4.json`, then click +*Update*. -The `schema` URL is only used for identification, and does not need to be accessible by {kib}. +[float] +[[vega-type]] +==== Change the visualization type -Vega-Lite is a simplified version of Vega; it automates some constructions and has -much shorter specifications than Vega. Vega-Lite is automatically converted into -Vega before rendering, but it has some limitations, and there are some visualizations -that can be expressed in Vega that cannot be expressed in Vega-Lite. You can learn more -in the https://vega.github.io/vega-lite/[Vega-Lite documentation]. +The default visualization is a line chart. To change the visualization type, +change the `mark` value. The supported visualization types are listed in the +text editor. -You can use https://vega.github.io/editor/[this editor] to convert Vega-Lite into -Vega. +Go to `mark`, change the value to a different visualization type, then click +*Update*. -When you create a Vega visualization in {kib}, you can edit the `schema` -value in the dev tools to the left of the graph to define which of the two expression -languages you would like to use. +[float] +[[vega-sizing-and-positioning]] +==== Change the layout +By default, Vega visualizations use the `autosize = { type: 'fit', contains: 'padding' }` layout. +`fit` uses all available space, ignores `width` and `height` values, +and respects the padding values. To override this behavior, change the +`autosize` value. [[vega-querying-elasticsearch]] -=== Querying Elasticsearch +=== Query {es} -By default, Vega's https://vega.github.io/vega/docs/data/[data] element -can use embedded and external data with a `"url"` parameter. Kibana adds support for the direct Elasticsearch queries by overloading +experimental[] Vega https://vega.github.io/vega/docs/data/[data] elements +use embedded and external data with a `"url"` parameter. {kib} adds support for +direct {es} queries by overloading the `"url"` value. -Here is an example of an Elasticsearch query that counts the number of documents in all indexes. The query uses *@timestamp* field to filter the time range, and break it into histogram buckets. +NOTE: With Vega, you dynamically load your data by setting signals as data URLs. +Since {kib} is unable to support dynamically loaded data, all data is fetched +before it's passed to the Vega renderer. + +For example, count the number of documents in all indices: [source,yaml] ---- -// An object instead of a string for the url value +// An object instead of a string for the URL value // is treated as a context-aware Elasticsearch query. url: { - // Specify the time filter (upper right corner) with this field + // Specify the time filter. %timefield%: @timestamp // Apply dashboard context filters when set %context%: true @@ -88,8 +87,8 @@ url: { time_buckets: { date_histogram: { // Use date histogram aggregation on @timestamp field - field: @timestamp - // interval value will depend on the daterange picker + field: @timestamp <1> + // interval value will depend on the time filter // Use an integer to set approximate bucket count interval: { %autointerval%: true } // Make sure we get an entire range, even if it has no data @@ -109,7 +108,10 @@ url: { } ---- -The full result has this kind of structure: +<1> `@timestamp` — Filters the time range and breaks it into histogram +buckets. + +The full result includes the following structure: [source,yaml] ---- @@ -118,23 +120,24 @@ The full result has this kind of structure: "time_buckets": { "buckets": [{ "key_as_string": "2015-11-30T22:00:00.000Z", - "key": 1448920800000, + "key": 1448920800000,<1> "doc_count": 28 }, { "key_as_string": "2015-11-30T23:00:00.000Z", - "key": 1448924400000, + "key": 1448924400000, <1> "doc_count": 330 }, ... ---- -Note that `"key"` is a unix timestamp, and can be used without conversions by the +<1> `"key"` — The unix timestamp you can use without conversions by the Vega date expressions. -For most graphs we only need the list of the bucket values, so we use `format: {property: "aggregations.time_buckets.buckets"}` expression to focus on just the data we need. +For most visualizations, you only need the list of bucket values. To focus on +only the data you need, use `format: {property: "aggregations.time_buckets.buckets"}`. -Query may be specified with individual range and dashboard context as -well. This query is equivalent to `"%context%": true, "%timefield%": "@timestamp"`, -except that the timerange is shifted back by 10 minutes: +Specify a query with individual range and dashboard context. The query is +equivalent to `"%context%": true, "%timefield%": "@timestamp"`, +except that the time range is shifted back by 10 minutes: [source,yaml] ---- @@ -185,9 +188,9 @@ on the currently picked range: `"interval": {"%autointerval%": 10}` will try to get about 10-15 data points (buckets). [[vega-esmfiles]] -=== Elastic Map Files +=== Access Elastic Map Service files -It is possible to access Elastic Map Service's files via the same mechanism +experimental[] Access the Elastic Map Service files via the same mechanism: [source,yaml] ---- @@ -203,11 +206,8 @@ url: { format: {property: "features"} ---- -[[vega-with-a-map]] -=== Vega with a Map - -Kibana's default map can be used as a base of the Vega graph. To enable, -the graph must specify `type=map` in the host configuration: +To enable Elastic Maps, the graph must specify `type=map` in the host +configuration: [source,yaml] ---- @@ -247,42 +247,47 @@ the graph must specify `type=map` in the host configuration: } ---- -This visualization will automatically inject a projection called -`"projection"`. Use it to calculate positioning of all geo-aware marks. -Additionally, you may use `latitude`, `longitude`, and `zoom` signals. +The visualization automatically injects a `"projection"`, which you can use to +calculate the position of all geo-aware marks. +Additionally, you can use `latitude`, `longitude`, and `zoom` signals. These signals can be used in the graph, or can be updated to modify the -positioning of the map. +position of the map. + +Vega visualization ignore the `autosize`, `width`, `height`, and `padding` +values, using `fit` model with zero padding. [[vega-debugging]] -=== Debugging +=== Debugging Vega [[vega-browser-debugging-console]] -==== Browser Debugging console +==== Browser debugging console -Use browser debugging tools (e.g. F12 or Ctrl+Shift+J in Chrome) to +experimental[] Use browser debugging tools (for example, F12 or Ctrl+Shift+J in Chrome) to inspect the `VEGA_DEBUG` variable: -* `view` - access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] - on how to inspect data and signals at runtime. For Vega-Lite, `VEGA_DEBUG.view.data('source_0')` gets the main data set. - For Vega, it uses the data name as defined in your Vega spec. -* `vega_spec` - Vega JSON graph specification after some modifications by Kibana. In case ++ +* `view` — Access to the Vega View object. See https://vega.github.io/vega/docs/api/debugging/[Vega Debugging Guide] +on how to inspect data and signals at runtime. For Vega-Lite, `VEGA_DEBUG.view.data('source_0')` gets the main data set. +For Vega, it uses the data name as defined in your Vega spec. + +* `vega_spec` — Vega JSON graph specification after some modifications by {kib}. In case of Vega-Lite, this is the output of the Vega-Lite compiler. -* `vegalite_spec` - If this is a Vega-Lite graph, JSON specification of the graph before + +* `vegalite_spec` — If this is a Vega-Lite graph, JSON specification of the graph before Vega-Lite compilation. [[vega-data]] ==== Data -If you are using Elasticsearch query, make sure your resulting data is -what you expected. The easiest way to view it is by using "networking" -tab in the browser debugging tools (e.g. F12). Modify the graph slightly +experimental[] If you are using an {es} query, make sure your resulting data is +what you expected. The easiest way to view it is by using the "networking" +tab in the browser debugging tools (for example, F12). Modify the graph slightly so that it makes a search request, and view the response from the server. Another approach is to use -https://www.elastic.co/guide/en/kibana/current/console-kibana.html[Kibana -Dev Tools] tab - place the index name into the first line: -`GET /_search`, and add your query as the following lines -(just the value of the `"query"` field) +https://www.elastic.co/guide/en/kibana/current/console-kibana.html[Dev Tools]. Place the index name into the first line: +`GET /_search`, then add your query as the following lines +(just the value of the `"query"` field). -If you need to share your graph with someone, you may want to copy the +If you need to share your graph with someone, copy the raw data response to https://gist.github.com/[gist.github.com], possibly with a `.json` extension, use the `[raw]` button, and use that url directly in your graph. @@ -292,9 +297,11 @@ to your kibana.yml file. [[vega-notes]] [[vega-useful-links]] -=== Useful Links +=== Resources and examples + +experimental[] To learn more about Vega and Vega-List, refer to the resources and examples. -==== Vega Editor +==== Vega editor The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Vega-Lite, but does not support any {kib}-specific features like {es} requests and interactive base maps. @@ -308,28 +315,15 @@ The https://vega.github.io/editor/[Vega Editor] includes examples for Vega & Veg * https://vega.github.io/vega/docs/[Docs] * https://vega.github.io/vega/examples/[Examples] -==== Elastic blog posts -* https://www.elastic.co/blog/getting-started-with-vega-visualizations-in-kibana[Getting Started with Vega Visualizations in Kibana] -* https://www.elastic.co/blog/custom-vega-visualizations-in-kibana[Custom Vega Visualizations in Kibana] -* https://www.elastic.co/blog/sankey-visualization-with-vega-in-kibana[Sankey Visualization with Vega in Kibana] - - -[[vega-using-vega-and-vegalite-examples]] -==== Using Vega and Vega-Lite examples - -When using https://vega.github.io/vega/examples/[Vega] and -https://vega.github.io/vega-lite/examples/[VegaLite] examples, you may +TIP: When you use the examples, you may need to modify the "data" section to use absolute URL. For example, replace `"url": "data/world-110m.json"` with -`"url": "https://vega.github.io/editor/data/world-110m.json"`. Also, -regular Vega examples use `"autosize": "pad"` layout model, whereas -Kibana uses `fit`. Remove all `autosize`, `width`, and `height` -values. See link:#sizing-and-positioning[sizing and positioning]. +`"url": "https://vega.github.io/editor/data/world-110m.json"`. [[vega-additional-configuration-options]] ==== Additional configuration options -These options are specific to the Kibana. link:#vega-with-a-map[Map support] has +These options are specific to the {kib}. link:#vega-with-a-map[Map support] has additional configuration options. [source,yaml] @@ -351,21 +345,3 @@ additional configuration options. /* the rest of Vega code */ } ---- - -[[vega-sizing-and-positioning]] -==== Sizing and positioning - -[[vega-and-vegalite]] -===== Vega and Vega-Lite - -By default, Kibana Vega graphs will use -`autosize = { type: 'fit', contains: 'padding' }` layout model for Vega -and Vega-Lite graphs. The `fit` model uses all available space, ignores -`width` and `height` values, but respects the padding values. You may -override this behaviour by specifying a different `autosize` value. - -[[vega-on-a-map]] -===== Vega on a map - -All Vega graphs will ignore `autosize`, `width`, `height`, and `padding` -values, using `fit` model with zero padding. diff --git a/docs/visualize/visualize_rollup_data.asciidoc b/docs/visualize/visualize_rollup_data.asciidoc deleted file mode 100644 index 481cbc6e39418..0000000000000 --- a/docs/visualize/visualize_rollup_data.asciidoc +++ /dev/null @@ -1,43 +0,0 @@ -[role="xpack"] -[[visualize-rollup-data]] -== Use rolled up data in a visualization - -beta[] - -You can visualize your rolled up data in a variety of charts, tables, maps, and -more. Most visualizations support rolled up data, with the exception of -Timelion and Vega visualizations. - -To get started, go to *Management > Kibana > Index patterns.* -If a rollup index is detected in the cluster, *Create index pattern* -includes an item for creating a rollup index pattern. - -[role="screenshot"] -image::images/management_create_rollup_menu.png[Create index pattern menu] - -You can match an index pattern to only rolled up data, or mix both rolled up -and raw data to visualize all data together. An index pattern can match only one -rolled up index, not multiple. There is no restriction on the number of standard -indices that an index pattern can match. When matching multiple indices, -use a comma to separate the names, with no space after the comma. - -Keep the following in mind when creating a visualization from rolled up data: - -* The data in a rollup index only has summarized metrics for specific fields. -You can’t search any other field from the original raw data. -* Data is summarized into time buckets that might be split into sub buckets for -numeric field values or terms. You can ask for a time aggregation that takes -several time buckets and combines them to lower granularity. For example, -if the rollup job was aggregated by hours, you can ask for buckets of days. - -The following visualization of rolled up data shows the date histogram -interval multiple and the limited metrics aggregations. - -[role="screenshot"] -image::images/management_rollups_visualization.png[][Rollups in visualizations] - -Dashboards can have a mixture of rollup visualizations and regular visualizations, -as shown in the following figure. Note that not all queries and filters support rollups. - -[role="screenshot"] -image::images/management_rolled_dashboard.png[][Rollups in dashboards] diff --git a/examples/state_containers_examples/public/todo/app.tsx b/examples/state_containers_examples/public/todo/app.tsx index 319680d07f9bc..f2183613e4a12 100644 --- a/examples/state_containers_examples/public/todo/app.tsx +++ b/examples/state_containers_examples/public/todo/app.tsx @@ -20,7 +20,7 @@ import { AppMountParameters } from 'kibana/public'; import ReactDOM from 'react-dom'; import React from 'react'; -import { createHashHistory, createBrowserHistory } from 'history'; +import { createHashHistory } from 'history'; import { TodoAppPage } from './todo'; export interface AppOptions { @@ -35,13 +35,10 @@ export enum History { } export const renderApp = ( - { appBasePath, element }: AppMountParameters, + { appBasePath, element, history: platformHistory }: AppMountParameters, { appInstanceId, appTitle, historyType }: AppOptions ) => { - const history = - historyType === History.Browser - ? createBrowserHistory({ basename: appBasePath }) - : createHashHistory(); + const history = historyType === History.Browser ? platformHistory : createHashHistory(); ReactDOM.render( = ({ filter }) => { return ( <>
- + All - + Completed - + Not Completed @@ -121,6 +124,7 @@ const TodoApp: React.FC = ({ filter }) => { }); }} label={todo.text} + data-test-subj={`todoCheckbox-${todo.id}`} /> { - const history = createBrowserHistory({ basename: appBasePath }); const kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); ReactDOM.render( diff --git a/package.json b/package.json index 21e9f67e6206a..0ad304fdf2f69 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "data/optimize", "built_assets", ".eslintcache", - ".node_binaries" + ".node_binaries", + "src/plugins/*/target" ] } }, @@ -84,6 +85,7 @@ "**/@types/hapi": "^17.0.18", "**/@types/angular": "^1.6.56", "**/@types/hoist-non-react-statics": "^3.3.1", + "**/@types/chai": "^4.2.11", "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/hoist-non-react-statics": "^3.3.2", @@ -120,10 +122,10 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@elastic/apm-rum": "^5.1.1", - "@elastic/charts": "18.3.0", + "@elastic/charts": "18.4.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.4.0", @@ -185,7 +187,7 @@ "glob-all": "^3.2.1", "globby": "^8.0.1", "h2o2": "^8.1.2", - "handlebars": "4.5.3", + "handlebars": "4.7.6", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", "history": "^4.9.0", @@ -441,7 +443,7 @@ "gulp-babel": "^8.0.0", "gulp-sourcemaps": "2.6.5", "has-ansi": "^3.0.0", - "iedriver": "^3.14.1", + "iedriver": "^3.14.2", "intl-messageformat-parser": "^1.4.0", "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", @@ -450,13 +452,13 @@ "jest-raw-loader": "^1.0.1", "jimp": "^0.9.6", "json5": "^1.0.1", - "karma": "3.1.4", + "karma": "5.0.2", "karma-chrome-launcher": "2.2.0", "karma-coverage": "1.1.2", "karma-firefox-launcher": "1.1.0", "karma-ie-launcher": "1.0.0", "karma-junit-reporter": "1.2.0", - "karma-mocha": "1.3.0", + "karma-mocha": "2.0.0", "karma-safari-launcher": "1.0.0", "license-checker": "^16.0.0", "listr": "^0.14.1", diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index b82c8d0fac897..1a2f6941c2020 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -15,6 +15,7 @@ "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-filter-imports": "^3.0.0", "babel-plugin-styled-components": "^1.10.6", - "babel-plugin-transform-define": "^1.3.1" + "babel-plugin-transform-define": "^1.3.1", + "babel-plugin-transform-imports": "^2.0.0" } } diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index d76a3e9714838..2c1129f275bfe 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -42,5 +42,24 @@ module.exports = () => { }, ], ], + // NOTE: we can enable this by default for everything as soon as we only have one instance + // of lodash across the entire project. For now we are just enabling it for siem + // as they are extensively using the lodash v4 + overrides: [ + { + test: [/x-pack[\/\\]legacy[\/\\]plugins[\/\\]siem[\/\\]public/], + plugins: [ + [ + require.resolve('babel-plugin-transform-imports'), + { + 'lodash/?(((\\w*)?/?)*)': { + transform: 'lodash/${1}/${member}', + preventFullImport: false, + }, + }, + ], + ], + }, + ], }; }; diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 47a0f5f7a5491..5ab59d1c02077 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -18,6 +18,7 @@ */ import { schema } from '..'; +import { TypeOf } from './object_type'; test('returns value by default', () => { const type = schema.object({ @@ -350,3 +351,26 @@ test('unknowns = `ignore` affects only own keys', () => { }) ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); }); + +test('handles optional properties', () => { + const type = schema.object({ + required: schema.string(), + optional: schema.maybe(schema.string()), + }); + + type SchemaType = TypeOf; + + let foo: SchemaType = { + required: 'foo', + }; + foo = { + required: 'hello', + optional: undefined, + }; + foo = { + required: 'hello', + optional: 'bar', + }; + + expect(foo).toBeDefined(); +}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index 5a50e714a5931..fee2d02c1bfb9 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -26,9 +26,26 @@ export type Props = Record>; export type TypeOf> = RT['type']; +type OptionalProperties = Pick< + Base, + { + [Key in keyof Base]: undefined extends TypeOf ? Key : never; + }[keyof Base] +>; + +type RequiredProperties = Pick< + Base, + { + [Key in keyof Base]: undefined extends TypeOf ? never : Key; + }[keyof Base] +>; + // Because of https://github.com/Microsoft/TypeScript/issues/14041 // this might not have perfect _rendering_ output, but it will be typed. -export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeOf }>; +export type ObjectResultType

= Readonly< + { [K in keyof OptionalProperties

]?: TypeOf } & + { [K in keyof RequiredProperties

]: TypeOf } +>; interface UnknownOptions { /** @@ -40,9 +57,7 @@ interface UnknownOptions { unknowns?: 'allow' | 'ignore' | 'forbid'; } -export type ObjectTypeOptions

= TypeOptions< - { [K in keyof P]: TypeOf } -> & +export type ObjectTypeOptions

= TypeOptions> & UnknownOptions; export class ObjectType

extends Type> { diff --git a/packages/kbn-dev-utils/package.json b/packages/kbn-dev-utils/package.json index ee9f349f49051..08b2e5b226967 100644 --- a/packages/kbn-dev-utils/package.json +++ b/packages/kbn-dev-utils/package.json @@ -10,6 +10,7 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { + "axios": "^0.19.0", "chalk": "^2.4.2", "dedent": "^0.7.0", "execa": "^4.0.0", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md new file mode 100644 index 0000000000000..6133f9871699f --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md @@ -0,0 +1,23 @@ +# Kibana CI Stats reporter + +We're working on building a new service, the Kibana CI Stats service, which will collect information about CI runs and metrics produced while testing Kibana, and then provide tools for reporting on those metrics in specific PRs and overall as a way to spot trends. + +### `CiStatsReporter` + +This class integrates with the `ciStats.trackBuild {}` Jenkins Pipeline function, consuming the `KIBANA_CI_STATS_CONFIG` variable produced by that wrapper, and then allowing test code to report stats to the service. + +To create an instance of the reporter, import the class and call `CiStatsReporter.fromEnv(log)` (passing it a tooling log). + +#### `CiStatsReporter#metric(name: string, subName: string, value: number)` + +Use this method to record metrics in the Kibana CI Stats service. + +Example: + +```ts +import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; + +const log = new ToolingLog(...); +const reporter = CiStatsReporter.fromEnv(log) +reporter.metric('Build speed', specificBuildName, timeToRunBuild) +``` \ No newline at end of file diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts new file mode 100644 index 0000000000000..5fe1844a85563 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -0,0 +1,153 @@ +/* + * 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 { inspect } from 'util'; + +import Axios from 'axios'; + +import { ToolingLog } from '../tooling_log'; + +interface Config { + apiUrl: string; + apiToken: string; + buildId: string; +} + +function parseConfig(log: ToolingLog) { + const configJson = process.env.KIBANA_CI_STATS_CONFIG; + if (!configJson) { + log.debug('KIBANA_CI_STATS_CONFIG environment variable not found, disabling CiStatsReporter'); + return; + } + + let config: unknown; + try { + config = JSON.parse(configJson); + } catch (_) { + // handled below + } + + if (typeof config === 'object' && config !== null) { + return validateConfig(log, config as { [k in keyof Config]: unknown }); + } + + log.warning('KIBANA_CI_STATS_CONFIG is invalid, stats will not be reported'); + return; +} + +function validateConfig(log: ToolingLog, config: { [k in keyof Config]: unknown }) { + const validApiUrl = typeof config.apiUrl === 'string' && config.apiUrl.length !== 0; + if (!validApiUrl) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api url, stats will not be reported'); + return; + } + + const validApiToken = typeof config.apiToken === 'string' && config.apiToken.length !== 0; + if (!validApiToken) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api token, stats will not be reported'); + return; + } + + const validId = typeof config.buildId === 'string' && config.buildId.length !== 0; + if (!validId) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid build id, stats will not be reported'); + return; + } + + return config as Config; +} + +export class CiStatsReporter { + static fromEnv(log: ToolingLog) { + return new CiStatsReporter(parseConfig(log), log); + } + + constructor(private config: Config | undefined, private log: ToolingLog) {} + + isEnabled() { + return !!this.config; + } + + async metric(name: string, subName: string, value: number) { + if (!this.config) { + return; + } + + let attempt = 0; + const maxAttempts = 5; + + while (true) { + attempt += 1; + + try { + await Axios.request({ + method: 'POST', + url: '/metric', + baseURL: this.config.apiUrl, + params: { + buildId: this.config.buildId, + }, + headers: { + Authorization: `token ${this.config.apiToken}`, + }, + data: { + name, + subName, + value, + }, + }); + + return; + } catch (error) { + if (!error?.request) { + // not an axios error, must be a usage error that we should notify user about + throw error; + } + + if (error?.response && error.response.status !== 502) { + // error response from service was received so warn the user and move on + this.log.warning( + `error recording metric [status=${error.response.status}] [resp=${inspect( + error.response.data + )}] [${name}/${subName}=${value}]` + ); + return; + } + + if (attempt === maxAttempts) { + this.log.warning( + `failed to reach kibana-ci-stats service too many times, unable to record metric [${name}/${subName}=${value}]` + ); + return; + } + + // we failed to reach the backend and we have remaining attempts, lets retry after a short delay + const reason = error?.response?.status + ? `${error.response.status} response` + : 'no response'; + + this.log.warning( + `failed to reach kibana-ci-stats service [reason=${reason}], retrying in ${attempt} seconds` + ); + + await new Promise(resolve => setTimeout(resolve, attempt * 1000)); + } + } + } +} diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts new file mode 100644 index 0000000000000..3487de08f034d --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts @@ -0,0 +1,20 @@ +/* + * 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 * from './ci_stats_reporter'; diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 305e29a0e41df..40ee72d94729f 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -37,3 +37,4 @@ export { run, createFailError, createFlagError, combineErrors, isFailError, Flag export { REPO_ROOT } from './repo_root'; export { KbnClient } from './kbn_client'; export * from './axios'; +export * from './ci_stats_reporter'; diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index dcb4dcd35698d..e46075eff63a7 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -21,10 +21,11 @@ import 'source-map-support/register'; import Path from 'path'; -import { run, REPO_ROOT, createFlagError } from '@kbn/dev-utils'; +import { run, REPO_ROOT, createFlagError, createFailError, CiStatsReporter } from '@kbn/dev-utils'; import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; +import { reportOptimizerStats } from './report_optimizer_stats'; import { runOptimizer } from './run_optimizer'; run( @@ -44,6 +45,11 @@ run( throw createFlagError('expected --cache to have no value'); } + const includeCoreBundle = flags.core ?? true; + if (typeof includeCoreBundle !== 'boolean') { + throw createFlagError('expected --core to have no value'); + } + const dist = flags.dist ?? false; if (typeof dist !== 'boolean') { throw createFlagError('expected --dist to have no value'); @@ -76,6 +82,11 @@ run( throw createFlagError('expected --scan-dir to be a string'); } + const reportStatsName = flags['report-stats']; + if (reportStatsName !== undefined && typeof reportStatsName !== 'string') { + throw createFlagError('expected --report-stats to be a string'); + } + const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, watch, @@ -87,17 +98,29 @@ run( profileWebpack, extraPluginScanDirs, inspectWorkers, + includeCoreBundle, }); - await runOptimizer(config) - .pipe(logOptimizerState(log, config)) - .toPromise(); + let update$ = runOptimizer(config); + + if (reportStatsName) { + const reporter = CiStatsReporter.fromEnv(log); + + if (!reporter.isEnabled()) { + throw createFailError('Unable to initialize CiStatsReporter from env'); + } + + update$ = update$.pipe(reportOptimizerStats(reporter, reportStatsName)); + } + + await update$.pipe(logOptimizerState(log, config)).toPromise(); }, { flags: { - boolean: ['watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], - string: ['workers', 'scan-dir'], + boolean: ['core', 'watch', 'oss', 'examples', 'dist', 'cache', 'profile', 'inspect-workers'], + string: ['workers', 'scan-dir', 'report-stats'], default: { + core: true, examples: true, cache: true, 'inspect-workers': true, @@ -107,11 +130,13 @@ run( --workers max number of workers to use --oss only build oss plugins --profile profile the webpack builds and write stats.json files to build outputs + --no-core disable generating the core bundle --no-cache disable the cache --no-examples don't build the example plugins --dist create bundles that are suitable for inclusion in the Kibana distributable --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --no-inspect-workers when inspecting the parent process, don't inspect the workers + --report-stats=[name] attempt to report stats about this execution of the build to the kibana-ci-stats service using this name `, }, } diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts index f1bc0965a46cc..7581b90d60af2 100644 --- a/packages/kbn-optimizer/src/common/bundle.ts +++ b/packages/kbn-optimizer/src/common/bundle.ts @@ -23,7 +23,7 @@ import { BundleCache } from './bundle_cache'; import { UnknownVals } from './ts_helpers'; import { includes, ascending, entriesToObject } from './array_helpers'; -const VALID_BUNDLE_TYPES = ['plugin' as const]; +const VALID_BUNDLE_TYPES = ['plugin' as const, 'entry' as const]; export interface BundleSpec { readonly type: typeof VALID_BUNDLE_TYPES[0]; diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 8026cf39db73d..29922944e8817 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -21,3 +21,4 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; export * from './common/disallowed_syntax_plugin'; +export * from './report_optimizer_stats'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 4b4bb1282d939..fe0f75c05c646 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,6 @@ OptimizerConfig { } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i { - expect( - getBundles( - [ - { - directory: '/repo/plugins/foo', - id: 'foo', - isUiPlugin: true, - }, - { - directory: '/repo/plugins/bar', - id: 'bar', - isUiPlugin: false, - }, - { - directory: '/outside/of/repo/plugins/baz', - id: 'baz', - isUiPlugin: true, - }, - ], - '/repo' - ).map(b => b.toSpec()) - ).toMatchInlineSnapshot(` - Array [ - Object { - "contextDir": /plugins/foo, - "entry": "./public/index", - "id": "foo", - "outputDir": /plugins/foo/target/public, - "sourceRoot": , - "type": "plugin", - }, - Object { - "contextDir": "/outside/of/repo/plugins/baz", - "entry": "./public/index", - "id": "baz", - "outputDir": "/outside/of/repo/plugins/baz/target/public", - "sourceRoot": , - "type": "plugin", - }, - ] - `); -}); diff --git a/packages/kbn-optimizer/src/optimizer/get_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_bundles.ts deleted file mode 100644 index 7cd7bf15317e0..0000000000000 --- a/packages/kbn-optimizer/src/optimizer/get_bundles.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 Path from 'path'; - -import { Bundle } from '../common'; - -import { KibanaPlatformPlugin } from './kibana_platform_plugins'; - -export function getBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { - return plugins - .filter(p => p.isUiPlugin) - .map( - p => - new Bundle({ - type: 'plugin', - id: p.id, - entry: './public/index', - sourceRoot: repoRoot, - contextDir: p.directory, - outputDir: Path.resolve(p.directory, 'target/public'), - }) - ); -} diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts new file mode 100644 index 0000000000000..36dc0ca64c6ca --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { createAbsolutePathSerializer } from '@kbn/dev-utils'; + +import { getPluginBundles } from './get_plugin_bundles'; + +expect.addSnapshotSerializer(createAbsolutePathSerializer('/repo')); + +it('returns a bundle for core and each plugin', () => { + expect( + getPluginBundles( + [ + { + directory: '/repo/plugins/foo', + id: 'foo', + isUiPlugin: true, + }, + { + directory: '/repo/plugins/bar', + id: 'bar', + isUiPlugin: false, + }, + { + directory: '/outside/of/repo/plugins/baz', + id: 'baz', + isUiPlugin: true, + }, + ], + '/repo' + ).map(b => b.toSpec()) + ).toMatchInlineSnapshot(` + Array [ + Object { + "contextDir": /plugins/foo, + "entry": "./public/index", + "id": "foo", + "outputDir": /plugins/foo/target/public, + "sourceRoot": , + "type": "plugin", + }, + Object { + "contextDir": "/outside/of/repo/plugins/baz", + "entry": "./public/index", + "id": "baz", + "outputDir": "/outside/of/repo/plugins/baz/target/public", + "sourceRoot": , + "type": "plugin", + }, + ] + `); +}); diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts new file mode 100644 index 0000000000000..4741cc3c30af7 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts @@ -0,0 +1,40 @@ +/* + * 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 Path from 'path'; + +import { Bundle } from '../common'; + +import { KibanaPlatformPlugin } from './kibana_platform_plugins'; + +export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: string) { + return plugins + .filter(p => p.isUiPlugin) + .map( + p => + new Bundle({ + type: 'plugin', + id: p.id, + entry: './public/index', + sourceRoot: repoRoot, + contextDir: p.directory, + outputDir: Path.resolve(p.directory, 'target/public'), + }) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts index 5dd500cd7a9e4..b4b02649259a2 100644 --- a/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts +++ b/packages/kbn-optimizer/src/optimizer/handle_optimizer_completion.ts @@ -17,20 +17,20 @@ * under the License. */ -import * as Rx from 'rxjs'; import { tap } from 'rxjs/operators'; import { createFailError } from '@kbn/dev-utils'; -import { pipeClosure, Update } from '../common'; +import { pipeClosure } from '../common'; +import { OptimizerUpdate$ } from '../run_optimizer'; import { OptimizerState } from './optimizer_state'; import { OptimizerConfig } from './optimizer_config'; export function handleOptimizerCompletion(config: OptimizerConfig) { - return pipeClosure((source$: Rx.Observable>) => { + return pipeClosure((update$: OptimizerUpdate$) => { let prevState: OptimizerState | undefined; - return source$.pipe( + return update$.pipe( tap({ next: update => { prevState = update.state; diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index cc564dd4a8387..d4152133f289d 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -19,7 +19,7 @@ jest.mock('./assign_bundles_to_workers.ts'); jest.mock('./kibana_platform_plugins.ts'); -jest.mock('./get_bundles.ts'); +jest.mock('./get_plugin_bundles.ts'); import Path from 'path'; import Os from 'os'; @@ -90,6 +90,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -114,6 +115,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -138,6 +140,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -164,6 +167,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -187,6 +191,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 2, "pluginPaths": Array [], @@ -210,6 +215,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -230,6 +236,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -250,6 +257,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -271,6 +279,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": false, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -292,6 +301,7 @@ describe('OptimizerConfig::parseOptions()', () => { Object { "cache": true, "dist": false, + "includeCoreBundle": false, "inspectWorkers": false, "maxWorkerCount": 100, "pluginPaths": Array [], @@ -314,7 +324,7 @@ describe('OptimizerConfig::create()', () => { .assignBundlesToWorkers; const findKibanaPlatformPlugins: jest.Mock = jest.requireMock('./kibana_platform_plugins.ts') .findKibanaPlatformPlugins; - const getBundles: jest.Mock = jest.requireMock('./get_bundles.ts').getBundles; + const getPluginBundles: jest.Mock = jest.requireMock('./get_plugin_bundles.ts').getPluginBundles; beforeEach(() => { if ('mock' in OptimizerConfig.parseOptions) { @@ -326,7 +336,7 @@ describe('OptimizerConfig::create()', () => { { config: Symbol('worker config 2') }, ]); findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins')); - getBundles.mockReturnValue(Symbol('bundles')); + getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({ cache: Symbol('parsed cache'), @@ -348,7 +358,10 @@ describe('OptimizerConfig::create()', () => { expect(config).toMatchInlineSnapshot(` OptimizerConfig { - "bundles": Symbol(bundles), + "bundles": Array [ + Symbol(bundle1), + Symbol(bundle2), + ], "cache": Symbol(parsed cache), "dist": Symbol(parsed dist), "inspectWorkers": Symbol(parsed inspect workers), @@ -383,7 +396,7 @@ describe('OptimizerConfig::create()', () => { } `); - expect(getBundles.mock).toMatchInlineSnapshot(` + expect(getPluginBundles.mock).toMatchInlineSnapshot(` Object { "calls": Array [ Array [ @@ -400,7 +413,10 @@ describe('OptimizerConfig::create()', () => { "results": Array [ Object { "type": "return", - "value": Symbol(bundles), + "value": Array [ + Symbol(bundle1), + Symbol(bundle2), + ], }, ], } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index 7e1514058446b..d6336cf867470 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -23,7 +23,7 @@ import Os from 'os'; import { Bundle, WorkerConfig } from '../common'; import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; -import { getBundles } from './get_bundles'; +import { getPluginBundles } from './get_plugin_bundles'; function pickMaxWorkerCount(dist: boolean) { // don't break if cpus() returns nothing, or an empty array @@ -60,6 +60,9 @@ interface Options { pluginScanDirs?: string[]; /** absolute paths that should be added to the default scan dirs */ extraPluginScanDirs?: string[]; + + /** flag that causes the core bundle to be built along with plugins */ + includeCoreBundle?: boolean; } interface ParsedOptions { @@ -72,6 +75,7 @@ interface ParsedOptions { pluginPaths: string[]; pluginScanDirs: string[]; inspectWorkers: boolean; + includeCoreBundle: boolean; } export class OptimizerConfig { @@ -83,6 +87,7 @@ export class OptimizerConfig { const profileWebpack = !!options.profileWebpack; const inspectWorkers = !!options.inspectWorkers; const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE; + const includeCoreBundle = !!options.includeCoreBundle; const repoRoot = options.repoRoot; if (!Path.isAbsolute(repoRoot)) { @@ -134,13 +139,28 @@ export class OptimizerConfig { pluginScanDirs, pluginPaths, inspectWorkers, + includeCoreBundle, }; } static create(inputOptions: Options) { const options = OptimizerConfig.parseOptions(inputOptions); const plugins = findKibanaPlatformPlugins(options.pluginScanDirs, options.pluginPaths); - const bundles = getBundles(plugins, options.repoRoot); + const bundles = [ + ...(options.includeCoreBundle + ? [ + new Bundle({ + type: 'entry', + id: 'core', + entry: './public/entry_point', + sourceRoot: options.repoRoot, + contextDir: Path.resolve(options.repoRoot, 'src/core'), + outputDir: Path.resolve(options.repoRoot, 'src/core/target/public'), + }), + ] + : []), + ...getPluginBundles(plugins, options.repoRoot), + ]; return new OptimizerConfig( bundles, diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts new file mode 100644 index 0000000000000..375978b9b7944 --- /dev/null +++ b/packages/kbn-optimizer/src/report_optimizer_stats.ts @@ -0,0 +1,46 @@ +/* + * 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 { materialize, mergeMap, dematerialize } from 'rxjs/operators'; +import { CiStatsReporter } from '@kbn/dev-utils'; + +import { OptimizerUpdate$ } from './run_optimizer'; +import { OptimizerState } from './optimizer'; +import { pipeClosure } from './common'; + +export function reportOptimizerStats(reporter: CiStatsReporter, name: string) { + return pipeClosure((update$: OptimizerUpdate$) => { + let lastState: OptimizerState | undefined; + return update$.pipe( + materialize(), + mergeMap(async n => { + if (n.kind === 'N' && n.value?.state) { + lastState = n.value?.state; + } + + if (n.kind === 'C' && lastState) { + await reporter.metric('@kbn/optimizer build time', name, lastState.durSec); + } + + return n; + }), + dematerialize() + ); + }); +} diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 2f81d92ec923c..cc3fa8c2720de 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -34,11 +34,10 @@ import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../c const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); -const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const STATIC_BUNDLE_PLUGINS = [ - // { id: 'data', dirname: 'data' }, + { id: 'data', dirname: 'data' }, { id: 'kibanaReact', dirname: 'kibana_react' }, { id: 'kibanaUtils', dirname: 'kibana_utils' }, { id: 'esUiShared', dirname: 'es_ui_shared' }, @@ -61,13 +60,8 @@ function dynamicExternals(bundle: Bundle, context: string, request: string) { return; } - // don't allow any static bundle to rely on other static bundles - if (STATIC_BUNDLE_PLUGINS.some(p => bundle.id === p.id)) { - return; - } - - // ignore requests that don't include a /data/public, /kibana_react/public, or - // /kibana_utils/public segment as a cheap way to avoid doing path resolution + // ignore requests that don't include a /{dirname}/public for one of our + // "static" bundles as a cheap way to avoid doing path resolution // for paths that couldn't possibly resolve to what we're looking for const reqToStaticBundle = STATIC_BUNDLE_PLUGINS.some(p => request.includes(`/${p.dirname}/public`) @@ -104,8 +98,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { output: { path: bundle.outputDir, - filename: '[name].plugin.js', - publicPath: PUBLIC_PATH_PLACEHOLDER, + filename: `[name].${bundle.type}.js`, devtoolModuleFilenameTemplate: info => `/${bundle.type}:${bundle.id}/${Path.relative( bundle.sourceRoot, @@ -140,12 +133,22 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { module: { // no parse rules for a few known large packages which have no require() statements + // or which have require() statements that should be ignored because the file is + // already bundled with all its necessary depedencies noParse: [ /[\///]node_modules[\///]elasticsearch-browser[\///]/, - /[\///]node_modules[\///]lodash[\///]index\.js/, + /[\///]node_modules[\///]lodash[\///]index\.js$/, + /[\///]node_modules[\///]vega-lib[\///]build[\///]vega\.js$/, ], rules: [ + { + include: Path.join(bundle.contextDir, bundle.entry), + loader: UiSharedDeps.publicPathLoader, + options: { + key: bundle.id, + }, + }, { test: /\.css$/, include: /node_modules/, @@ -289,6 +292,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { resolve: { extensions: ['.js', '.ts', '.tsx', '.json'], + mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), }, diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 6a2d02ee778dd..28cf36dedba3f 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,21 +94,21 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(703); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(705); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(500); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(502); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjects", function() { return _utils_projects__WEBPACK_IMPORTED_MODULE_2__["getProjects"]; }); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(515); +/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(517); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Project", function() { return _utils_project__WEBPACK_IMPORTED_MODULE_3__["Project"]; }); -/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(576); +/* harmony import */ var _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(578); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return _utils_workspaces__WEBPACK_IMPORTED_MODULE_4__["copyWorkspacePackages"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(577); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(579); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -152,7 +152,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(687); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(689); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(34); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -2506,9 +2506,9 @@ module.exports = require("path"); __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(18); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(584); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(684); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(685); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(586); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(686); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(687); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -2549,10 +2549,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_link_project_executables__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(19); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(34); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(499); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(500); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(583); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(501); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(502); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(580); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(585); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -4517,6 +4517,7 @@ exports.REPO_ROOT = repo_root_1.REPO_ROOT; var kbn_client_1 = __webpack_require__(449); exports.KbnClient = kbn_client_1.KbnClient; tslib_1.__exportStar(__webpack_require__(492), exports); +tslib_1.__exportStar(__webpack_require__(499), exports); /***/ }), @@ -40169,9 +40170,9 @@ module.exports = __webpack_require__(454); var utils = __webpack_require__(455); var bind = __webpack_require__(456); -var Axios = __webpack_require__(458); +var Axios = __webpack_require__(457); var mergeConfig = __webpack_require__(488); -var defaults = __webpack_require__(464); +var defaults = __webpack_require__(463); /** * Create an instance of Axios @@ -40206,7 +40207,7 @@ axios.create = function create(instanceConfig) { // Expose Cancel & CancelToken axios.Cancel = __webpack_require__(489); axios.CancelToken = __webpack_require__(490); -axios.isCancel = __webpack_require__(463); +axios.isCancel = __webpack_require__(462); // Expose all/spread axios.all = function all(promises) { @@ -40228,7 +40229,6 @@ module.exports.default = axios; var bind = __webpack_require__(456); -var isBuffer = __webpack_require__(457); /*global toString:true*/ @@ -40246,6 +40246,27 @@ function isArray(val) { return toString.call(val) === '[object Array]'; } +/** + * Determine if a value is undefined + * + * @param {Object} val The value to test + * @returns {boolean} True if the value is undefined, otherwise false + */ +function isUndefined(val) { + return typeof val === 'undefined'; +} + +/** + * Determine if a value is a Buffer + * + * @param {Object} val The value to test + * @returns {boolean} True if value is a Buffer, otherwise false + */ +function isBuffer(val) { + return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor) + && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val); +} + /** * Determine if a value is an ArrayBuffer * @@ -40302,16 +40323,6 @@ function isNumber(val) { return typeof val === 'number'; } -/** - * Determine if a value is undefined - * - * @param {Object} val The value to test - * @returns {boolean} True if the value is undefined, otherwise false - */ -function isUndefined(val) { - return typeof val === 'undefined'; -} - /** * Determine if a value is an Object * @@ -40581,32 +40592,15 @@ module.exports = function bind(fn, thisArg) { /***/ }), /* 457 */ -/***/ (function(module, exports) { - -/*! - * Determine if an object is a Buffer - * - * @author Feross Aboukhadijeh - * @license MIT - */ - -module.exports = function isBuffer (obj) { - return obj != null && obj.constructor != null && - typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj) -} - - -/***/ }), -/* 458 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var buildURL = __webpack_require__(459); -var InterceptorManager = __webpack_require__(460); -var dispatchRequest = __webpack_require__(461); +var buildURL = __webpack_require__(458); +var InterceptorManager = __webpack_require__(459); +var dispatchRequest = __webpack_require__(460); var mergeConfig = __webpack_require__(488); /** @@ -40638,7 +40632,15 @@ Axios.prototype.request = function request(config) { } config = mergeConfig(this.defaults, config); - config.method = config.method ? config.method.toLowerCase() : 'get'; + + // Set config.method + if (config.method) { + config.method = config.method.toLowerCase(); + } else if (this.defaults.method) { + config.method = this.defaults.method.toLowerCase(); + } else { + config.method = 'get'; + } // Hook up interceptors middleware var chain = [dispatchRequest, undefined]; @@ -40690,7 +40692,7 @@ module.exports = Axios; /***/ }), -/* 459 */ +/* 458 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40768,7 +40770,7 @@ module.exports = function buildURL(url, params, paramsSerializer) { /***/ }), -/* 460 */ +/* 459 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40827,18 +40829,16 @@ module.exports = InterceptorManager; /***/ }), -/* 461 */ +/* 460 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var transformData = __webpack_require__(462); -var isCancel = __webpack_require__(463); -var defaults = __webpack_require__(464); -var isAbsoluteURL = __webpack_require__(486); -var combineURLs = __webpack_require__(487); +var transformData = __webpack_require__(461); +var isCancel = __webpack_require__(462); +var defaults = __webpack_require__(463); /** * Throws a `Cancel` if cancellation has been requested. @@ -40858,11 +40858,6 @@ function throwIfCancellationRequested(config) { module.exports = function dispatchRequest(config) { throwIfCancellationRequested(config); - // Support baseURL config - if (config.baseURL && !isAbsoluteURL(config.url)) { - config.url = combineURLs(config.baseURL, config.url); - } - // Ensure headers exist config.headers = config.headers || {}; @@ -40877,7 +40872,7 @@ module.exports = function dispatchRequest(config) { config.headers = utils.merge( config.headers.common || {}, config.headers[config.method] || {}, - config.headers || {} + config.headers ); utils.forEach( @@ -40920,7 +40915,7 @@ module.exports = function dispatchRequest(config) { /***/ }), -/* 462 */ +/* 461 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40947,7 +40942,7 @@ module.exports = function transformData(data, headers, fns) { /***/ }), -/* 463 */ +/* 462 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -40959,14 +40954,14 @@ module.exports = function isCancel(value) { /***/ }), -/* 464 */ +/* 463 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var normalizeHeaderName = __webpack_require__(465); +var normalizeHeaderName = __webpack_require__(464); var DEFAULT_CONTENT_TYPE = { 'Content-Type': 'application/x-www-form-urlencoded' @@ -40980,13 +40975,12 @@ function setContentTypeIfUnset(headers, value) { function getDefaultAdapter() { var adapter; - // Only Node.JS has a process variable that is of [[Class]] process - if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { - // For node use HTTP adapter - adapter = __webpack_require__(466); - } else if (typeof XMLHttpRequest !== 'undefined') { + if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter - adapter = __webpack_require__(482); + adapter = __webpack_require__(465); + } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { + // For node use HTTP adapter + adapter = __webpack_require__(475); } return adapter; } @@ -41064,7 +41058,7 @@ module.exports = defaults; /***/ }), -/* 465 */ +/* 464 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41083,295 +41077,200 @@ module.exports = function normalizeHeaderName(headers, normalizedName) { /***/ }), -/* 466 */ +/* 465 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var utils = __webpack_require__(455); -var settle = __webpack_require__(467); -var buildURL = __webpack_require__(459); -var http = __webpack_require__(470); -var https = __webpack_require__(471); -var httpFollow = __webpack_require__(472).http; -var httpsFollow = __webpack_require__(472).https; -var url = __webpack_require__(452); -var zlib = __webpack_require__(480); -var pkg = __webpack_require__(481); -var createError = __webpack_require__(468); -var enhanceError = __webpack_require__(469); - -var isHttps = /https:?/; +var settle = __webpack_require__(466); +var buildURL = __webpack_require__(458); +var buildFullPath = __webpack_require__(469); +var parseHeaders = __webpack_require__(472); +var isURLSameOrigin = __webpack_require__(473); +var createError = __webpack_require__(467); -/*eslint consistent-return:0*/ -module.exports = function httpAdapter(config) { - return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { - var timer; - var resolve = function resolve(value) { - clearTimeout(timer); - resolvePromise(value); - }; - var reject = function reject(value) { - clearTimeout(timer); - rejectPromise(value); - }; - var data = config.data; - var headers = config.headers; +module.exports = function xhrAdapter(config) { + return new Promise(function dispatchXhrRequest(resolve, reject) { + var requestData = config.data; + var requestHeaders = config.headers; - // Set User-Agent (required by some servers) - // Only set header if it hasn't been set in config - // See https://github.com/axios/axios/issues/69 - if (!headers['User-Agent'] && !headers['user-agent']) { - headers['User-Agent'] = 'axios/' + pkg.version; + if (utils.isFormData(requestData)) { + delete requestHeaders['Content-Type']; // Let the browser set it } - if (data && !utils.isStream(data)) { - if (Buffer.isBuffer(data)) { - // Nothing to do... - } else if (utils.isArrayBuffer(data)) { - data = Buffer.from(new Uint8Array(data)); - } else if (utils.isString(data)) { - data = Buffer.from(data, 'utf-8'); - } else { - return reject(createError( - 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', - config - )); - } - - // Add Content-Length header if data exists - headers['Content-Length'] = data.length; - } + var request = new XMLHttpRequest(); // HTTP basic authentication - var auth = undefined; if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; - auth = username + ':' + password; + requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); } - // Parse url - var parsed = url.parse(config.url); - var protocol = parsed.protocol || 'http:'; + var fullPath = buildFullPath(config.baseURL, config.url); + request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); - if (!auth && parsed.auth) { - var urlAuth = parsed.auth.split(':'); - var urlUsername = urlAuth[0] || ''; - var urlPassword = urlAuth[1] || ''; - auth = urlUsername + ':' + urlPassword; - } + // Set the request timeout in MS + request.timeout = config.timeout; - if (auth) { - delete headers.Authorization; - } + // Listen for ready state + request.onreadystatechange = function handleLoad() { + if (!request || request.readyState !== 4) { + return; + } - var isHttpsRequest = isHttps.test(protocol); - var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; + // The request errored out and we didn't get a response, this will be + // handled by onerror instead + // With one exception: request that using file: protocol, most browsers + // will return status as 0 even though it's a successful request + if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { + return; + } - var options = { - path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), - method: config.method.toUpperCase(), - headers: headers, - agent: agent, - auth: auth - }; + // Prepare the response + var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; + var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; + var response = { + data: responseData, + status: request.status, + statusText: request.statusText, + headers: responseHeaders, + config: config, + request: request + }; - if (config.socketPath) { - options.socketPath = config.socketPath; - } else { - options.hostname = parsed.hostname; - options.port = parsed.port; - } + settle(resolve, reject, response); - var proxy = config.proxy; - if (!proxy && proxy !== false) { - var proxyEnv = protocol.slice(0, -1) + '_proxy'; - var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()]; - if (proxyUrl) { - var parsedProxyUrl = url.parse(proxyUrl); - var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY; - var shouldProxy = true; + // Clean up request + request = null; + }; - if (noProxyEnv) { - var noProxy = noProxyEnv.split(',').map(function trim(s) { - return s.trim(); - }); + // Handle browser request cancellation (as opposed to a manual cancellation) + request.onabort = function handleAbort() { + if (!request) { + return; + } - shouldProxy = !noProxy.some(function proxyMatch(proxyElement) { - if (!proxyElement) { - return false; - } - if (proxyElement === '*') { - return true; - } - if (proxyElement[0] === '.' && - parsed.hostname.substr(parsed.hostname.length - proxyElement.length) === proxyElement && - proxyElement.match(/\./g).length === parsed.hostname.match(/\./g).length) { - return true; - } + reject(createError('Request aborted', config, 'ECONNABORTED', request)); - return parsed.hostname === proxyElement; - }); - } + // Clean up request + request = null; + }; + // Handle low level network errors + request.onerror = function handleError() { + // Real errors are hidden from us by the browser + // onerror should only fire if it's a network error + reject(createError('Network Error', config, null, request)); - if (shouldProxy) { - proxy = { - host: parsedProxyUrl.hostname, - port: parsedProxyUrl.port - }; + // Clean up request + request = null; + }; - if (parsedProxyUrl.auth) { - var proxyUrlAuth = parsedProxyUrl.auth.split(':'); - proxy.auth = { - username: proxyUrlAuth[0], - password: proxyUrlAuth[1] - }; - } - } + // Handle timeout + request.ontimeout = function handleTimeout() { + var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded'; + if (config.timeoutErrorMessage) { + timeoutErrorMessage = config.timeoutErrorMessage; } - } + reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', + request)); - if (proxy) { - options.hostname = proxy.host; - options.host = proxy.host; - options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : ''); - options.port = proxy.port; - options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path; + // Clean up request + request = null; + }; - // Basic proxy authorization - if (proxy.auth) { - var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64'); - options.headers['Proxy-Authorization'] = 'Basic ' + base64; - } - } + // Add xsrf header + // This is only done if running in a standard browser environment. + // Specifically not if we're in a web worker, or react-native. + if (utils.isStandardBrowserEnv()) { + var cookies = __webpack_require__(474); - var transport; - var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true); - if (config.transport) { - transport = config.transport; - } else if (config.maxRedirects === 0) { - transport = isHttpsProxy ? https : http; - } else { - if (config.maxRedirects) { - options.maxRedirects = config.maxRedirects; + // Add xsrf header + var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? + cookies.read(config.xsrfCookieName) : + undefined; + + if (xsrfValue) { + requestHeaders[config.xsrfHeaderName] = xsrfValue; } - transport = isHttpsProxy ? httpsFollow : httpFollow; } - if (config.maxContentLength && config.maxContentLength > -1) { - options.maxBodyLength = config.maxContentLength; + // Add headers to the request + if ('setRequestHeader' in request) { + utils.forEach(requestHeaders, function setRequestHeader(val, key) { + if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { + // Remove Content-Type if data is undefined + delete requestHeaders[key]; + } else { + // Otherwise add header to the request + request.setRequestHeader(key, val); + } + }); } - // Create the request - var req = transport.request(options, function handleResponse(res) { - if (req.aborted) return; - - // uncompress the response body transparently if required - var stream = res; - switch (res.headers['content-encoding']) { - /*eslint default-case:0*/ - case 'gzip': - case 'compress': - case 'deflate': - // add the unzipper to the body stream processing pipeline - stream = (res.statusCode === 204) ? stream : stream.pipe(zlib.createUnzip()); - - // remove the content-encoding in order to not confuse downstream operations - delete res.headers['content-encoding']; - break; - } - - // return the last request in case of redirects - var lastRequest = res.req || req; - - var response = { - status: res.statusCode, - statusText: res.statusMessage, - headers: res.headers, - config: config, - request: lastRequest - }; - - if (config.responseType === 'stream') { - response.data = stream; - settle(resolve, reject, response); - } else { - var responseBuffer = []; - stream.on('data', function handleStreamData(chunk) { - responseBuffer.push(chunk); - - // make sure the content length is not over the maxContentLength if specified - if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { - stream.destroy(); - reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', - config, null, lastRequest)); - } - }); - - stream.on('error', function handleStreamError(err) { - if (req.aborted) return; - reject(enhanceError(err, config, null, lastRequest)); - }); - - stream.on('end', function handleStreamEnd() { - var responseData = Buffer.concat(responseBuffer); - if (config.responseType !== 'arraybuffer') { - responseData = responseData.toString(config.responseEncoding); - } + // Add withCredentials to request if needed + if (!utils.isUndefined(config.withCredentials)) { + request.withCredentials = !!config.withCredentials; + } - response.data = responseData; - settle(resolve, reject, response); - }); + // Add responseType to request if needed + if (config.responseType) { + try { + request.responseType = config.responseType; + } catch (e) { + // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2. + // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function. + if (config.responseType !== 'json') { + throw e; + } } - }); + } - // Handle errors - req.on('error', function handleRequestError(err) { - if (req.aborted) return; - reject(enhanceError(err, config, null, req)); - }); + // Handle progress if needed + if (typeof config.onDownloadProgress === 'function') { + request.addEventListener('progress', config.onDownloadProgress); + } - // Handle request timeout - if (config.timeout) { - timer = setTimeout(function handleRequestTimeout() { - req.abort(); - reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); - }, config.timeout); + // Not all browsers support upload events + if (typeof config.onUploadProgress === 'function' && request.upload) { + request.upload.addEventListener('progress', config.onUploadProgress); } if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { - if (req.aborted) return; + if (!request) { + return; + } - req.abort(); + request.abort(); reject(cancel); + // Clean up request + request = null; }); } - // Send the request - if (utils.isStream(data)) { - data.on('error', function handleStreamError(err) { - reject(enhanceError(err, config, null, req)); - }).pipe(req); - } else { - req.end(data); + if (requestData === undefined) { + requestData = null; } + + // Send the request + request.send(requestData); }); }; /***/ }), -/* 467 */ +/* 466 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var createError = __webpack_require__(468); +var createError = __webpack_require__(467); /** * Resolve or reject a Promise based on response status. @@ -41397,13 +41296,13 @@ module.exports = function settle(resolve, reject, response) { /***/ }), -/* 468 */ +/* 467 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var enhanceError = __webpack_require__(469); +var enhanceError = __webpack_require__(468); /** * Create an Error with the specified message, config, error code, request and response. @@ -41422,7 +41321,7 @@ module.exports = function createError(message, config, code, request, response) /***/ }), -/* 469 */ +/* 468 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -41470,1091 +41369,1049 @@ module.exports = function enhanceError(error, config, code, request, response) { }; +/***/ }), +/* 469 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isAbsoluteURL = __webpack_require__(470); +var combineURLs = __webpack_require__(471); + +/** + * Creates a new URL by combining the baseURL with the requestedURL, + * only when the requestedURL is not already an absolute URL. + * If the requestURL is absolute, this function returns the requestedURL untouched. + * + * @param {string} baseURL The base URL + * @param {string} requestedURL Absolute or relative URL to combine + * @returns {string} The combined full path + */ +module.exports = function buildFullPath(baseURL, requestedURL) { + if (baseURL && !isAbsoluteURL(requestedURL)) { + return combineURLs(baseURL, requestedURL); + } + return requestedURL; +}; + + /***/ }), /* 470 */ -/***/ (function(module, exports) { +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/** + * Determines whether the specified URL is absolute + * + * @param {string} url The URL to test + * @returns {boolean} True if the specified URL is absolute, otherwise false + */ +module.exports = function isAbsoluteURL(url) { + // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). + // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed + // by any combination of letters, digits, plus, period, or hyphen. + return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); +}; -module.exports = require("http"); /***/ }), /* 471 */ -/***/ (function(module, exports) { +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +/** + * Creates a new URL by combining the specified URLs + * + * @param {string} baseURL The base URL + * @param {string} relativeURL The relative URL + * @returns {string} The combined URL + */ +module.exports = function combineURLs(baseURL, relativeURL) { + return relativeURL + ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') + : baseURL; +}; -module.exports = require("https"); /***/ }), /* 472 */ /***/ (function(module, exports, __webpack_require__) { -var url = __webpack_require__(452); -var http = __webpack_require__(470); -var https = __webpack_require__(471); -var assert = __webpack_require__(30); -var Writable = __webpack_require__(27).Writable; -var debug = __webpack_require__(473)("follow-redirects"); +"use strict"; -// RFC7231§4.2.1: Of the request methods defined by this specification, -// the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. -var SAFE_METHODS = { GET: true, HEAD: true, OPTIONS: true, TRACE: true }; -// Create handlers that pass events from native requests -var eventHandlers = Object.create(null); -["abort", "aborted", "error", "socket", "timeout"].forEach(function (event) { - eventHandlers[event] = function (arg) { - this._redirectable.emit(event, arg); - }; -}); +var utils = __webpack_require__(455); -// An HTTP(S) request that can be redirected -function RedirectableRequest(options, responseCallback) { - // Initialize the request - Writable.call(this); - options.headers = options.headers || {}; - this._options = options; - this._redirectCount = 0; - this._redirects = []; - this._requestBodyLength = 0; - this._requestBodyBuffers = []; +// Headers whose duplicates are ignored by node +// c.f. https://nodejs.org/api/http.html#http_message_headers +var ignoreDuplicateOf = [ + 'age', 'authorization', 'content-length', 'content-type', 'etag', + 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', + 'last-modified', 'location', 'max-forwards', 'proxy-authorization', + 'referer', 'retry-after', 'user-agent' +]; - // Since http.request treats host as an alias of hostname, - // but the url module interprets host as hostname plus port, - // eliminate the host property to avoid confusion. - if (options.host) { - // Use hostname if set, because it has precedence - if (!options.hostname) { - options.hostname = options.host; - } - delete options.host; - } +/** + * Parse headers into an object + * + * ``` + * Date: Wed, 27 Aug 2014 08:58:49 GMT + * Content-Type: application/json + * Connection: keep-alive + * Transfer-Encoding: chunked + * ``` + * + * @param {String} headers Headers needing to be parsed + * @returns {Object} Headers parsed into an object + */ +module.exports = function parseHeaders(headers) { + var parsed = {}; + var key; + var val; + var i; - // Attach a callback if passed - if (responseCallback) { - this.on("response", responseCallback); - } + if (!headers) { return parsed; } - // React to responses of native requests - var self = this; - this._onNativeResponse = function (response) { - self._processResponse(response); - }; + utils.forEach(headers.split('\n'), function parser(line) { + i = line.indexOf(':'); + key = utils.trim(line.substr(0, i)).toLowerCase(); + val = utils.trim(line.substr(i + 1)); - // Complete the URL object when necessary - if (!options.pathname && options.path) { - var searchPos = options.path.indexOf("?"); - if (searchPos < 0) { - options.pathname = options.path; - } - else { - options.pathname = options.path.substring(0, searchPos); - options.search = options.path.substring(searchPos); + if (key) { + if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) { + return; + } + if (key === 'set-cookie') { + parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]); + } else { + parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; + } } - } - - // Perform the first request - this._performRequest(); -} -RedirectableRequest.prototype = Object.create(Writable.prototype); - -// Writes buffered data to the current native request -RedirectableRequest.prototype.write = function (data, encoding, callback) { - // Validate input and shift parameters if necessary - if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) { - throw new Error("data should be a string, Buffer or Uint8Array"); - } - if (typeof encoding === "function") { - callback = encoding; - encoding = null; - } + }); - // Ignore empty buffers, since writing them doesn't invoke the callback - // https://github.com/nodejs/node/issues/22066 - if (data.length === 0) { - if (callback) { - callback(); - } - return; - } - // Only write when we don't exceed the maximum body length - if (this._requestBodyLength + data.length <= this._options.maxBodyLength) { - this._requestBodyLength += data.length; - this._requestBodyBuffers.push({ data: data, encoding: encoding }); - this._currentRequest.write(data, encoding, callback); - } - // Error when we exceed the maximum body length - else { - this.emit("error", new Error("Request body larger than maxBodyLength limit")); - this.abort(); - } + return parsed; }; -// Ends the current native request -RedirectableRequest.prototype.end = function (data, encoding, callback) { - // Shift parameters if necessary - if (typeof data === "function") { - callback = data; - data = encoding = null; - } - else if (typeof encoding === "function") { - callback = encoding; - encoding = null; - } - // Write data and end - var currentRequest = this._currentRequest; - this.write(data || "", encoding, function () { - currentRequest.end(null, null, callback); - }); -}; +/***/ }), +/* 473 */ +/***/ (function(module, exports, __webpack_require__) { -// Sets a header value on the current native request -RedirectableRequest.prototype.setHeader = function (name, value) { - this._options.headers[name] = value; - this._currentRequest.setHeader(name, value); -}; +"use strict"; -// Clears a header value on the current native request -RedirectableRequest.prototype.removeHeader = function (name) { - delete this._options.headers[name]; - this._currentRequest.removeHeader(name); -}; -// Proxy all other public ClientRequest methods -[ - "abort", "flushHeaders", "getHeader", - "setNoDelay", "setSocketKeepAlive", "setTimeout", -].forEach(function (method) { - RedirectableRequest.prototype[method] = function (a, b) { - return this._currentRequest[method](a, b); - }; -}); +var utils = __webpack_require__(455); -// Proxy all public ClientRequest properties -["aborted", "connection", "socket"].forEach(function (property) { - Object.defineProperty(RedirectableRequest.prototype, property, { - get: function () { return this._currentRequest[property]; }, - }); -}); +module.exports = ( + utils.isStandardBrowserEnv() ? -// Executes the next native request (initial or redirect) -RedirectableRequest.prototype._performRequest = function () { - // Load the native protocol - var protocol = this._options.protocol; - var nativeProtocol = this._options.nativeProtocols[protocol]; - if (!nativeProtocol) { - this.emit("error", new Error("Unsupported protocol " + protocol)); - return; - } + // Standard browser envs have full support of the APIs needed to test + // whether the request URL is of the same origin as current location. + (function standardBrowserEnv() { + var msie = /(msie|trident)/i.test(navigator.userAgent); + var urlParsingNode = document.createElement('a'); + var originURL; - // If specified, use the agent corresponding to the protocol - // (HTTP and HTTPS use different types of agents) - if (this._options.agents) { - var scheme = protocol.substr(0, protocol.length - 1); - this._options.agent = this._options.agents[scheme]; - } + /** + * Parse a URL to discover it's components + * + * @param {String} url The URL to be parsed + * @returns {Object} + */ + function resolveURL(url) { + var href = url; - // Create the native request - var request = this._currentRequest = - nativeProtocol.request(this._options, this._onNativeResponse); - this._currentUrl = url.format(this._options); + if (msie) { + // IE needs attribute set twice to normalize properties + urlParsingNode.setAttribute('href', href); + href = urlParsingNode.href; + } - // Set up event handlers - request._redirectable = this; - for (var event in eventHandlers) { - /* istanbul ignore else */ - if (event) { - request.on(event, eventHandlers[event]); - } - } + urlParsingNode.setAttribute('href', href); - // End a redirected request - // (The first request must be ended explicitly with RedirectableRequest#end) - if (this._isRedirect) { - // Write the request entity and end. - var i = 0; - var buffers = this._requestBodyBuffers; - (function writeNext() { - if (i < buffers.length) { - var buffer = buffers[i++]; - request.write(buffer.data, buffer.encoding, writeNext); - } - else { - request.end(); + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', + host: urlParsingNode.host, + search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', + hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', + hostname: urlParsingNode.hostname, + port: urlParsingNode.port, + pathname: (urlParsingNode.pathname.charAt(0) === '/') ? + urlParsingNode.pathname : + '/' + urlParsingNode.pathname + }; } - }()); - } -}; -// Processes a response from the current native request -RedirectableRequest.prototype._processResponse = function (response) { - // Store the redirected response - if (this._options.trackRedirects) { - this._redirects.push({ - url: this._currentUrl, - headers: response.headers, - statusCode: response.statusCode, - }); - } + originURL = resolveURL(window.location.href); - // RFC7231§6.4: The 3xx (Redirection) class of status code indicates - // that further action needs to be taken by the user agent in order to - // fulfill the request. If a Location header field is provided, - // the user agent MAY automatically redirect its request to the URI - // referenced by the Location field value, - // even if the specific status code is not understood. - var location = response.headers.location; - if (location && this._options.followRedirects !== false && - response.statusCode >= 300 && response.statusCode < 400) { - // RFC7231§6.4: A client SHOULD detect and intervene - // in cyclical redirections (i.e., "infinite" redirection loops). - if (++this._redirectCount > this._options.maxRedirects) { - this.emit("error", new Error("Max redirects exceeded.")); - return; - } + /** + * Determine if a URL shares the same origin as the current location + * + * @param {String} requestURL The URL to test + * @returns {boolean} True if URL shares the same origin, otherwise false + */ + return function isURLSameOrigin(requestURL) { + var parsed = (utils.isString(requestURL)) ? resolveURL(requestURL) : requestURL; + return (parsed.protocol === originURL.protocol && + parsed.host === originURL.host); + }; + })() : - // RFC7231§6.4: Automatic redirection needs to done with - // care for methods not known to be safe […], - // since the user might not wish to redirect an unsafe request. - // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates - // that the target resource resides temporarily under a different URI - // and the user agent MUST NOT change the request method - // if it performs an automatic redirection to that URI. - var header; - var headers = this._options.headers; - if (response.statusCode !== 307 && !(this._options.method in SAFE_METHODS)) { - this._options.method = "GET"; - // Drop a possible entity and headers related to it - this._requestBodyBuffers = []; - for (header in headers) { - if (/^content-/i.test(header)) { - delete headers[header]; - } - } - } + // Non standard browser envs (web workers, react-native) lack needed support. + (function nonStandardBrowserEnv() { + return function isURLSameOrigin() { + return true; + }; + })() +); - // Drop the Host header, as the redirect might lead to a different host - if (!this._isRedirect) { - for (header in headers) { - if (/^host$/i.test(header)) { - delete headers[header]; - } - } - } - // Perform the redirected request - var redirectUrl = url.resolve(this._currentUrl, location); - debug("redirecting to", redirectUrl); - Object.assign(this._options, url.parse(redirectUrl)); - this._isRedirect = true; - this._performRequest(); +/***/ }), +/* 474 */ +/***/ (function(module, exports, __webpack_require__) { - // Discard the remainder of the response to avoid waiting for data - response.destroy(); - } - else { - // The response is not a redirect; return it as-is - response.responseUrl = this._currentUrl; - response.redirects = this._redirects; - this.emit("response", response); +"use strict"; - // Clean up - this._requestBodyBuffers = []; - } -}; -// Wraps the key/value object of protocols with redirect functionality -function wrap(protocols) { - // Default settings - var exports = { - maxRedirects: 21, - maxBodyLength: 10 * 1024 * 1024, - }; +var utils = __webpack_require__(455); - // Wrap each protocol - var nativeProtocols = {}; - Object.keys(protocols).forEach(function (scheme) { - var protocol = scheme + ":"; - var nativeProtocol = nativeProtocols[protocol] = protocols[scheme]; - var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); +module.exports = ( + utils.isStandardBrowserEnv() ? - // Executes a request, following redirects - wrappedProtocol.request = function (options, callback) { - if (typeof options === "string") { - options = url.parse(options); - options.maxRedirects = exports.maxRedirects; - } - else { - options = Object.assign({ - protocol: protocol, - maxRedirects: exports.maxRedirects, - maxBodyLength: exports.maxBodyLength, - }, options); - } - options.nativeProtocols = nativeProtocols; - assert.equal(options.protocol, protocol, "protocol mismatch"); - debug("options", options); - return new RedirectableRequest(options, callback); - }; + // Standard browser envs support document.cookie + (function standardBrowserEnv() { + return { + write: function write(name, value, expires, path, domain, secure) { + var cookie = []; + cookie.push(name + '=' + encodeURIComponent(value)); - // Executes a GET request, following redirects - wrappedProtocol.get = function (options, callback) { - var request = wrappedProtocol.request(options, callback); - request.end(); - return request; - }; - }); - return exports; -} + if (utils.isNumber(expires)) { + cookie.push('expires=' + new Date(expires).toGMTString()); + } -// Exports -module.exports = wrap({ http: http, https: https }); -module.exports.wrap = wrap; + if (utils.isString(path)) { + cookie.push('path=' + path); + } + if (utils.isString(domain)) { + cookie.push('domain=' + domain); + } -/***/ }), -/* 473 */ -/***/ (function(module, exports, __webpack_require__) { + if (secure === true) { + cookie.push('secure'); + } -/** - * Detect Electron renderer process, which is node, but we should - * treat as a browser. - */ + document.cookie = cookie.join('; '); + }, -if (typeof process === 'undefined' || process.type === 'renderer') { - module.exports = __webpack_require__(474); -} else { - module.exports = __webpack_require__(477); -} + read: function read(name) { + var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); + return (match ? decodeURIComponent(match[3]) : null); + }, + + remove: function remove(name) { + this.write(name, '', Date.now() - 86400000); + } + }; + })() : + + // Non standard browser env (web workers, react-native) lack needed support. + (function nonStandardBrowserEnv() { + return { + write: function write() {}, + read: function read() { return null; }, + remove: function remove() {} + }; + })() +); /***/ }), -/* 474 */ +/* 475 */ /***/ (function(module, exports, __webpack_require__) { -/** - * This is the web browser implementation of `debug()`. - * - * Expose `debug()` as the module. - */ +"use strict"; -exports = module.exports = __webpack_require__(475); -exports.log = log; -exports.formatArgs = formatArgs; -exports.save = save; -exports.load = load; -exports.useColors = useColors; -exports.storage = 'undefined' != typeof chrome - && 'undefined' != typeof chrome.storage - ? chrome.storage.local - : localstorage(); -/** - * Colors. - */ +var utils = __webpack_require__(455); +var settle = __webpack_require__(466); +var buildFullPath = __webpack_require__(469); +var buildURL = __webpack_require__(458); +var http = __webpack_require__(476); +var https = __webpack_require__(477); +var httpFollow = __webpack_require__(478).http; +var httpsFollow = __webpack_require__(478).https; +var url = __webpack_require__(452); +var zlib = __webpack_require__(486); +var pkg = __webpack_require__(487); +var createError = __webpack_require__(467); +var enhanceError = __webpack_require__(468); -exports.colors = [ - '#0000CC', '#0000FF', '#0033CC', '#0033FF', '#0066CC', '#0066FF', '#0099CC', - '#0099FF', '#00CC00', '#00CC33', '#00CC66', '#00CC99', '#00CCCC', '#00CCFF', - '#3300CC', '#3300FF', '#3333CC', '#3333FF', '#3366CC', '#3366FF', '#3399CC', - '#3399FF', '#33CC00', '#33CC33', '#33CC66', '#33CC99', '#33CCCC', '#33CCFF', - '#6600CC', '#6600FF', '#6633CC', '#6633FF', '#66CC00', '#66CC33', '#9900CC', - '#9900FF', '#9933CC', '#9933FF', '#99CC00', '#99CC33', '#CC0000', '#CC0033', - '#CC0066', '#CC0099', '#CC00CC', '#CC00FF', '#CC3300', '#CC3333', '#CC3366', - '#CC3399', '#CC33CC', '#CC33FF', '#CC6600', '#CC6633', '#CC9900', '#CC9933', - '#CCCC00', '#CCCC33', '#FF0000', '#FF0033', '#FF0066', '#FF0099', '#FF00CC', - '#FF00FF', '#FF3300', '#FF3333', '#FF3366', '#FF3399', '#FF33CC', '#FF33FF', - '#FF6600', '#FF6633', '#FF9900', '#FF9933', '#FFCC00', '#FFCC33' -]; +var isHttps = /https:?/; -/** - * Currently only WebKit-based Web Inspectors, Firefox >= v31, - * and the Firebug extension (any Firefox version) are known - * to support "%c" CSS customizations. - * - * TODO: add a `localStorage` variable to explicitly enable/disable colors - */ +/*eslint consistent-return:0*/ +module.exports = function httpAdapter(config) { + return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { + var resolve = function resolve(value) { + resolvePromise(value); + }; + var reject = function reject(value) { + rejectPromise(value); + }; + var data = config.data; + var headers = config.headers; -function useColors() { - // NB: In an Electron preload script, document will be defined but not fully - // initialized. Since we know we're in Chrome, we'll just detect this case - // explicitly - if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { - return true; - } + // Set User-Agent (required by some servers) + // Only set header if it hasn't been set in config + // See https://github.com/axios/axios/issues/69 + if (!headers['User-Agent'] && !headers['user-agent']) { + headers['User-Agent'] = 'axios/' + pkg.version; + } - // Internet Explorer and Edge do not support colors. - if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { - return false; - } + if (data && !utils.isStream(data)) { + if (Buffer.isBuffer(data)) { + // Nothing to do... + } else if (utils.isArrayBuffer(data)) { + data = Buffer.from(new Uint8Array(data)); + } else if (utils.isString(data)) { + data = Buffer.from(data, 'utf-8'); + } else { + return reject(createError( + 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', + config + )); + } - // is webkit? http://stackoverflow.com/a/16459606/376773 - // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 - return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || - // is firebug? http://stackoverflow.com/a/398120/376773 - (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || - // is firefox >= v31? - // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || - // double check webkit in userAgent just in case we are in a worker - (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); -} + // Add Content-Length header if data exists + headers['Content-Length'] = data.length; + } -/** - * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. - */ + // HTTP basic authentication + var auth = undefined; + if (config.auth) { + var username = config.auth.username || ''; + var password = config.auth.password || ''; + auth = username + ':' + password; + } -exports.formatters.j = function(v) { - try { - return JSON.stringify(v); - } catch (err) { - return '[UnexpectedJSONParseError]: ' + err.message; - } -}; + // Parse url + var fullPath = buildFullPath(config.baseURL, config.url); + var parsed = url.parse(fullPath); + var protocol = parsed.protocol || 'http:'; + if (!auth && parsed.auth) { + var urlAuth = parsed.auth.split(':'); + var urlUsername = urlAuth[0] || ''; + var urlPassword = urlAuth[1] || ''; + auth = urlUsername + ':' + urlPassword; + } -/** - * Colorize log arguments if enabled. - * - * @api public - */ + if (auth) { + delete headers.Authorization; + } -function formatArgs(args) { - var useColors = this.useColors; + var isHttpsRequest = isHttps.test(protocol); + var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; - args[0] = (useColors ? '%c' : '') - + this.namespace - + (useColors ? ' %c' : ' ') - + args[0] - + (useColors ? '%c ' : ' ') - + '+' + exports.humanize(this.diff); + var options = { + path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), + method: config.method.toUpperCase(), + headers: headers, + agent: agent, + agents: { http: config.httpAgent, https: config.httpsAgent }, + auth: auth + }; - if (!useColors) return; + if (config.socketPath) { + options.socketPath = config.socketPath; + } else { + options.hostname = parsed.hostname; + options.port = parsed.port; + } - var c = 'color: ' + this.color; - args.splice(1, 0, c, 'color: inherit') + var proxy = config.proxy; + if (!proxy && proxy !== false) { + var proxyEnv = protocol.slice(0, -1) + '_proxy'; + var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()]; + if (proxyUrl) { + var parsedProxyUrl = url.parse(proxyUrl); + var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY; + var shouldProxy = true; - // the final "%c" is somewhat tricky, because there could be other - // arguments passed either before or after the %c, so we need to - // figure out the correct index to insert the CSS into - var index = 0; - var lastC = 0; - args[0].replace(/%[a-zA-Z%]/g, function(match) { - if ('%%' === match) return; - index++; - if ('%c' === match) { - // we only are interested in the *last* %c - // (the user may have provided their own) - lastC = index; - } - }); + if (noProxyEnv) { + var noProxy = noProxyEnv.split(',').map(function trim(s) { + return s.trim(); + }); - args.splice(lastC, 0, c); -} + shouldProxy = !noProxy.some(function proxyMatch(proxyElement) { + if (!proxyElement) { + return false; + } + if (proxyElement === '*') { + return true; + } + if (proxyElement[0] === '.' && + parsed.hostname.substr(parsed.hostname.length - proxyElement.length) === proxyElement) { + return true; + } -/** - * Invokes `console.log()` when available. - * No-op when `console.log` is not a "function". - * - * @api public - */ + return parsed.hostname === proxyElement; + }); + } -function log() { - // this hackery is required for IE8/9, where - // the `console.log` function doesn't have 'apply' - return 'object' === typeof console - && console.log - && Function.prototype.apply.call(console.log, console, arguments); -} -/** - * Save `namespaces`. - * - * @param {String} namespaces - * @api private - */ + if (shouldProxy) { + proxy = { + host: parsedProxyUrl.hostname, + port: parsedProxyUrl.port + }; -function save(namespaces) { - try { - if (null == namespaces) { - exports.storage.removeItem('debug'); - } else { - exports.storage.debug = namespaces; + if (parsedProxyUrl.auth) { + var proxyUrlAuth = parsedProxyUrl.auth.split(':'); + proxy.auth = { + username: proxyUrlAuth[0], + password: proxyUrlAuth[1] + }; + } + } + } } - } catch(e) {} -} -/** - * Load `namespaces`. - * - * @return {String} returns the previously persisted debug modes - * @api private - */ + if (proxy) { + options.hostname = proxy.host; + options.host = proxy.host; + options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : ''); + options.port = proxy.port; + options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path; -function load() { - var r; - try { - r = exports.storage.debug; - } catch(e) {} + // Basic proxy authorization + if (proxy.auth) { + var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64'); + options.headers['Proxy-Authorization'] = 'Basic ' + base64; + } + } - // If debug isn't set in LS, and we're in Electron, try to load $DEBUG - if (!r && typeof process !== 'undefined' && 'env' in process) { - r = process.env.DEBUG; - } + var transport; + var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true); + if (config.transport) { + transport = config.transport; + } else if (config.maxRedirects === 0) { + transport = isHttpsProxy ? https : http; + } else { + if (config.maxRedirects) { + options.maxRedirects = config.maxRedirects; + } + transport = isHttpsProxy ? httpsFollow : httpFollow; + } - return r; -} + if (config.maxContentLength && config.maxContentLength > -1) { + options.maxBodyLength = config.maxContentLength; + } -/** - * Enable namespaces listed in `localStorage.debug` initially. - */ + // Create the request + var req = transport.request(options, function handleResponse(res) { + if (req.aborted) return; -exports.enable(load()); + // uncompress the response body transparently if required + var stream = res; + switch (res.headers['content-encoding']) { + /*eslint default-case:0*/ + case 'gzip': + case 'compress': + case 'deflate': + // add the unzipper to the body stream processing pipeline + stream = (res.statusCode === 204) ? stream : stream.pipe(zlib.createUnzip()); -/** - * Localstorage attempts to return the localstorage. - * - * This is necessary because safari throws - * when a user disables cookies/localstorage - * and you attempt to access it. - * - * @return {LocalStorage} - * @api private - */ + // remove the content-encoding in order to not confuse downstream operations + delete res.headers['content-encoding']; + break; + } -function localstorage() { - try { - return window.localStorage; - } catch (e) {} -} + // return the last request in case of redirects + var lastRequest = res.req || req; + var response = { + status: res.statusCode, + statusText: res.statusMessage, + headers: res.headers, + config: config, + request: lastRequest + }; -/***/ }), -/* 475 */ -/***/ (function(module, exports, __webpack_require__) { + if (config.responseType === 'stream') { + response.data = stream; + settle(resolve, reject, response); + } else { + var responseBuffer = []; + stream.on('data', function handleStreamData(chunk) { + responseBuffer.push(chunk); + // make sure the content length is not over the maxContentLength if specified + if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { + stream.destroy(); + reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', + config, null, lastRequest)); + } + }); -/** - * This is the common logic for both the Node.js and web browser - * implementations of `debug()`. - * - * Expose `debug()` as the module. - */ + stream.on('error', function handleStreamError(err) { + if (req.aborted) return; + reject(enhanceError(err, config, null, lastRequest)); + }); -exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; -exports.coerce = coerce; -exports.disable = disable; -exports.enable = enable; -exports.enabled = enabled; -exports.humanize = __webpack_require__(476); + stream.on('end', function handleStreamEnd() { + var responseData = Buffer.concat(responseBuffer); + if (config.responseType !== 'arraybuffer') { + responseData = responseData.toString(config.responseEncoding); + } -/** - * Active `debug` instances. - */ -exports.instances = []; + response.data = responseData; + settle(resolve, reject, response); + }); + } + }); -/** - * The currently active debug mode names, and names to skip. - */ + // Handle errors + req.on('error', function handleRequestError(err) { + if (req.aborted) return; + reject(enhanceError(err, config, null, req)); + }); -exports.names = []; -exports.skips = []; + // Handle request timeout + if (config.timeout) { + // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system. + // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET. + // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up. + // And then these socket which be hang up will devoring CPU little by little. + // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect. + req.setTimeout(config.timeout, function handleRequestTimeout() { + req.abort(); + reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); + }); + } -/** - * Map of special "%n" handling functions, for the debug "format" argument. - * - * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". - */ + if (config.cancelToken) { + // Handle cancellation + config.cancelToken.promise.then(function onCanceled(cancel) { + if (req.aborted) return; -exports.formatters = {}; + req.abort(); + reject(cancel); + }); + } -/** - * Select a color. - * @param {String} namespace - * @return {Number} - * @api private - */ + // Send the request + if (utils.isStream(data)) { + data.on('error', function handleStreamError(err) { + reject(enhanceError(err, config, null, req)); + }).pipe(req); + } else { + req.end(data); + } + }); +}; -function selectColor(namespace) { - var hash = 0, i; - for (i in namespace) { - hash = ((hash << 5) - hash) + namespace.charCodeAt(i); - hash |= 0; // Convert to 32bit integer - } +/***/ }), +/* 476 */ +/***/ (function(module, exports) { - return exports.colors[Math.abs(hash) % exports.colors.length]; -} +module.exports = require("http"); -/** - * Create a debugger with the given `namespace`. - * - * @param {String} namespace - * @return {Function} - * @api public - */ +/***/ }), +/* 477 */ +/***/ (function(module, exports) { -function createDebug(namespace) { +module.exports = require("https"); - var prevTime; +/***/ }), +/* 478 */ +/***/ (function(module, exports, __webpack_require__) { - function debug() { - // disabled? - if (!debug.enabled) return; +var url = __webpack_require__(452); +var http = __webpack_require__(476); +var https = __webpack_require__(477); +var assert = __webpack_require__(30); +var Writable = __webpack_require__(27).Writable; +var debug = __webpack_require__(479)("follow-redirects"); - var self = debug; +// RFC7231§4.2.1: Of the request methods defined by this specification, +// the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe. +var SAFE_METHODS = { GET: true, HEAD: true, OPTIONS: true, TRACE: true }; - // set `diff` timestamp - var curr = +new Date(); - var ms = curr - (prevTime || curr); - self.diff = ms; - self.prev = prevTime; - self.curr = curr; - prevTime = curr; +// Create handlers that pass events from native requests +var eventHandlers = Object.create(null); +["abort", "aborted", "error", "socket", "timeout"].forEach(function (event) { + eventHandlers[event] = function (arg) { + this._redirectable.emit(event, arg); + }; +}); - // turn the `arguments` into a proper Array - var args = new Array(arguments.length); - for (var i = 0; i < args.length; i++) { - args[i] = arguments[i]; +// An HTTP(S) request that can be redirected +function RedirectableRequest(options, responseCallback) { + // Initialize the request + Writable.call(this); + options.headers = options.headers || {}; + this._options = options; + this._redirectCount = 0; + this._redirects = []; + this._requestBodyLength = 0; + this._requestBodyBuffers = []; + + // Since http.request treats host as an alias of hostname, + // but the url module interprets host as hostname plus port, + // eliminate the host property to avoid confusion. + if (options.host) { + // Use hostname if set, because it has precedence + if (!options.hostname) { + options.hostname = options.host; } + delete options.host; + } - args[0] = exports.coerce(args[0]); + // Attach a callback if passed + if (responseCallback) { + this.on("response", responseCallback); + } - if ('string' !== typeof args[0]) { - // anything else let's inspect with %O - args.unshift('%O'); + // React to responses of native requests + var self = this; + this._onNativeResponse = function (response) { + self._processResponse(response); + }; + + // Complete the URL object when necessary + if (!options.pathname && options.path) { + var searchPos = options.path.indexOf("?"); + if (searchPos < 0) { + options.pathname = options.path; + } + else { + options.pathname = options.path.substring(0, searchPos); + options.search = options.path.substring(searchPos); } + } - // apply any `formatters` transformations - var index = 0; - args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { - // if we encounter an escaped % then don't increase the array index - if (match === '%%') return match; - index++; - var formatter = exports.formatters[format]; - if ('function' === typeof formatter) { - var val = args[index]; - match = formatter.call(self, val); - - // now we need to remove `args[index]` since it's inlined in the `format` - args.splice(index, 1); - index--; - } - return match; - }); - - // apply env-specific formatting (colors, etc.) - exports.formatArgs.call(self, args); + // Perform the first request + this._performRequest(); +} +RedirectableRequest.prototype = Object.create(Writable.prototype); - var logFn = debug.log || exports.log || console.log.bind(console); - logFn.apply(self, args); +// Writes buffered data to the current native request +RedirectableRequest.prototype.write = function (data, encoding, callback) { + // Validate input and shift parameters if necessary + if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) { + throw new Error("data should be a string, Buffer or Uint8Array"); } - - debug.namespace = namespace; - debug.enabled = exports.enabled(namespace); - debug.useColors = exports.useColors(); - debug.color = selectColor(namespace); - debug.destroy = destroy; - - // env-specific initialization logic for debug instances - if ('function' === typeof exports.init) { - exports.init(debug); + if (typeof encoding === "function") { + callback = encoding; + encoding = null; } - exports.instances.push(debug); - - return debug; -} + // Ignore empty buffers, since writing them doesn't invoke the callback + // https://github.com/nodejs/node/issues/22066 + if (data.length === 0) { + if (callback) { + callback(); + } + return; + } + // Only write when we don't exceed the maximum body length + if (this._requestBodyLength + data.length <= this._options.maxBodyLength) { + this._requestBodyLength += data.length; + this._requestBodyBuffers.push({ data: data, encoding: encoding }); + this._currentRequest.write(data, encoding, callback); + } + // Error when we exceed the maximum body length + else { + this.emit("error", new Error("Request body larger than maxBodyLength limit")); + this.abort(); + } +}; -function destroy () { - var index = exports.instances.indexOf(this); - if (index !== -1) { - exports.instances.splice(index, 1); - return true; - } else { - return false; +// Ends the current native request +RedirectableRequest.prototype.end = function (data, encoding, callback) { + // Shift parameters if necessary + if (typeof data === "function") { + callback = data; + data = encoding = null; + } + else if (typeof encoding === "function") { + callback = encoding; + encoding = null; } -} -/** - * Enables a debug mode by namespaces. This can include modes - * separated by a colon and wildcards. - * - * @param {String} namespaces - * @api public - */ + // Write data and end + var currentRequest = this._currentRequest; + this.write(data || "", encoding, function () { + currentRequest.end(null, null, callback); + }); +}; -function enable(namespaces) { - exports.save(namespaces); +// Sets a header value on the current native request +RedirectableRequest.prototype.setHeader = function (name, value) { + this._options.headers[name] = value; + this._currentRequest.setHeader(name, value); +}; - exports.names = []; - exports.skips = []; +// Clears a header value on the current native request +RedirectableRequest.prototype.removeHeader = function (name) { + delete this._options.headers[name]; + this._currentRequest.removeHeader(name); +}; - var i; - var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); - var len = split.length; +// Proxy all other public ClientRequest methods +[ + "abort", "flushHeaders", "getHeader", + "setNoDelay", "setSocketKeepAlive", "setTimeout", +].forEach(function (method) { + RedirectableRequest.prototype[method] = function (a, b) { + return this._currentRequest[method](a, b); + }; +}); - for (i = 0; i < len; i++) { - if (!split[i]) continue; // ignore empty strings - namespaces = split[i].replace(/\*/g, '.*?'); - if (namespaces[0] === '-') { - exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); - } else { - exports.names.push(new RegExp('^' + namespaces + '$')); - } - } +// Proxy all public ClientRequest properties +["aborted", "connection", "socket"].forEach(function (property) { + Object.defineProperty(RedirectableRequest.prototype, property, { + get: function () { return this._currentRequest[property]; }, + }); +}); - for (i = 0; i < exports.instances.length; i++) { - var instance = exports.instances[i]; - instance.enabled = exports.enabled(instance.namespace); +// Executes the next native request (initial or redirect) +RedirectableRequest.prototype._performRequest = function () { + // Load the native protocol + var protocol = this._options.protocol; + var nativeProtocol = this._options.nativeProtocols[protocol]; + if (!nativeProtocol) { + this.emit("error", new Error("Unsupported protocol " + protocol)); + return; } -} - -/** - * Disable debug output. - * - * @api public - */ -function disable() { - exports.enable(''); -} + // If specified, use the agent corresponding to the protocol + // (HTTP and HTTPS use different types of agents) + if (this._options.agents) { + var scheme = protocol.substr(0, protocol.length - 1); + this._options.agent = this._options.agents[scheme]; + } -/** - * Returns true if the given mode name is enabled, false otherwise. - * - * @param {String} name - * @return {Boolean} - * @api public - */ + // Create the native request + var request = this._currentRequest = + nativeProtocol.request(this._options, this._onNativeResponse); + this._currentUrl = url.format(this._options); -function enabled(name) { - if (name[name.length - 1] === '*') { - return true; - } - var i, len; - for (i = 0, len = exports.skips.length; i < len; i++) { - if (exports.skips[i].test(name)) { - return false; - } - } - for (i = 0, len = exports.names.length; i < len; i++) { - if (exports.names[i].test(name)) { - return true; + // Set up event handlers + request._redirectable = this; + for (var event in eventHandlers) { + /* istanbul ignore else */ + if (event) { + request.on(event, eventHandlers[event]); } } - return false; -} -/** - * Coerce `val`. - * - * @param {Mixed} val - * @return {Mixed} - * @api private - */ + // End a redirected request + // (The first request must be ended explicitly with RedirectableRequest#end) + if (this._isRedirect) { + // Write the request entity and end. + var i = 0; + var buffers = this._requestBodyBuffers; + (function writeNext() { + if (i < buffers.length) { + var buffer = buffers[i++]; + request.write(buffer.data, buffer.encoding, writeNext); + } + else { + request.end(); + } + }()); + } +}; -function coerce(val) { - if (val instanceof Error) return val.stack || val.message; - return val; -} +// Processes a response from the current native request +RedirectableRequest.prototype._processResponse = function (response) { + // Store the redirected response + if (this._options.trackRedirects) { + this._redirects.push({ + url: this._currentUrl, + headers: response.headers, + statusCode: response.statusCode, + }); + } + // RFC7231§6.4: The 3xx (Redirection) class of status code indicates + // that further action needs to be taken by the user agent in order to + // fulfill the request. If a Location header field is provided, + // the user agent MAY automatically redirect its request to the URI + // referenced by the Location field value, + // even if the specific status code is not understood. + var location = response.headers.location; + if (location && this._options.followRedirects !== false && + response.statusCode >= 300 && response.statusCode < 400) { + // RFC7231§6.4: A client SHOULD detect and intervene + // in cyclical redirections (i.e., "infinite" redirection loops). + if (++this._redirectCount > this._options.maxRedirects) { + this.emit("error", new Error("Max redirects exceeded.")); + return; + } -/***/ }), -/* 476 */ -/***/ (function(module, exports) { + // RFC7231§6.4: Automatic redirection needs to done with + // care for methods not known to be safe […], + // since the user might not wish to redirect an unsafe request. + // RFC7231§6.4.7: The 307 (Temporary Redirect) status code indicates + // that the target resource resides temporarily under a different URI + // and the user agent MUST NOT change the request method + // if it performs an automatic redirection to that URI. + var header; + var headers = this._options.headers; + if (response.statusCode !== 307 && !(this._options.method in SAFE_METHODS)) { + this._options.method = "GET"; + // Drop a possible entity and headers related to it + this._requestBodyBuffers = []; + for (header in headers) { + if (/^content-/i.test(header)) { + delete headers[header]; + } + } + } -/** - * Helpers. - */ + // Drop the Host header, as the redirect might lead to a different host + if (!this._isRedirect) { + for (header in headers) { + if (/^host$/i.test(header)) { + delete headers[header]; + } + } + } -var s = 1000; -var m = s * 60; -var h = m * 60; -var d = h * 24; -var y = d * 365.25; + // Perform the redirected request + var redirectUrl = url.resolve(this._currentUrl, location); + debug("redirecting to", redirectUrl); + Object.assign(this._options, url.parse(redirectUrl)); + this._isRedirect = true; + this._performRequest(); -/** - * Parse or format the given `val`. - * - * Options: - * - * - `long` verbose formatting [false] - * - * @param {String|Number} val - * @param {Object} [options] - * @throws {Error} throw an error if val is not a non-empty string or a number - * @return {String|Number} - * @api public - */ + // Discard the remainder of the response to avoid waiting for data + response.destroy(); + } + else { + // The response is not a redirect; return it as-is + response.responseUrl = this._currentUrl; + response.redirects = this._redirects; + this.emit("response", response); -module.exports = function(val, options) { - options = options || {}; - var type = typeof val; - if (type === 'string' && val.length > 0) { - return parse(val); - } else if (type === 'number' && isNaN(val) === false) { - return options.long ? fmtLong(val) : fmtShort(val); + // Clean up + this._requestBodyBuffers = []; } - throw new Error( - 'val is not a non-empty string or a valid number. val=' + - JSON.stringify(val) - ); }; -/** - * Parse the given `str` and return milliseconds. - * - * @param {String} str - * @return {Number} - * @api private - */ +// Wraps the key/value object of protocols with redirect functionality +function wrap(protocols) { + // Default settings + var exports = { + maxRedirects: 21, + maxBodyLength: 10 * 1024 * 1024, + }; -function parse(str) { - str = String(str); - if (str.length > 100) { - return; - } - var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( - str - ); - if (!match) { - return; - } - var n = parseFloat(match[1]); - var type = (match[2] || 'ms').toLowerCase(); - switch (type) { - case 'years': - case 'year': - case 'yrs': - case 'yr': - case 'y': - return n * y; - case 'days': - case 'day': - case 'd': - return n * d; - case 'hours': - case 'hour': - case 'hrs': - case 'hr': - case 'h': - return n * h; - case 'minutes': - case 'minute': - case 'mins': - case 'min': - case 'm': - return n * m; - case 'seconds': - case 'second': - case 'secs': - case 'sec': - case 's': - return n * s; - case 'milliseconds': - case 'millisecond': - case 'msecs': - case 'msec': - case 'ms': - return n; - default: - return undefined; - } -} + // Wrap each protocol + var nativeProtocols = {}; + Object.keys(protocols).forEach(function (scheme) { + var protocol = scheme + ":"; + var nativeProtocol = nativeProtocols[protocol] = protocols[scheme]; + var wrappedProtocol = exports[scheme] = Object.create(nativeProtocol); -/** - * Short format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ + // Executes a request, following redirects + wrappedProtocol.request = function (options, callback) { + if (typeof options === "string") { + options = url.parse(options); + options.maxRedirects = exports.maxRedirects; + } + else { + options = Object.assign({ + protocol: protocol, + maxRedirects: exports.maxRedirects, + maxBodyLength: exports.maxBodyLength, + }, options); + } + options.nativeProtocols = nativeProtocols; + assert.equal(options.protocol, protocol, "protocol mismatch"); + debug("options", options); + return new RedirectableRequest(options, callback); + }; -function fmtShort(ms) { - if (ms >= d) { - return Math.round(ms / d) + 'd'; - } - if (ms >= h) { - return Math.round(ms / h) + 'h'; - } - if (ms >= m) { - return Math.round(ms / m) + 'm'; - } - if (ms >= s) { - return Math.round(ms / s) + 's'; - } - return ms + 'ms'; + // Executes a GET request, following redirects + wrappedProtocol.get = function (options, callback) { + var request = wrappedProtocol.request(options, callback); + request.end(); + return request; + }; + }); + return exports; } -/** - * Long format for `ms`. - * - * @param {Number} ms - * @return {String} - * @api private - */ +// Exports +module.exports = wrap({ http: http, https: https }); +module.exports.wrap = wrap; -function fmtLong(ms) { - return plural(ms, d, 'day') || - plural(ms, h, 'hour') || - plural(ms, m, 'minute') || - plural(ms, s, 'second') || - ms + ' ms'; -} + +/***/ }), +/* 479 */ +/***/ (function(module, exports, __webpack_require__) { /** - * Pluralization helper. + * Detect Electron renderer process, which is node, but we should + * treat as a browser. */ -function plural(ms, n, name) { - if (ms < n) { - return; - } - if (ms < n * 1.5) { - return Math.floor(ms / n) + ' ' + name; - } - return Math.ceil(ms / n) + ' ' + name + 's'; +if (typeof process === 'undefined' || process.type === 'renderer') { + module.exports = __webpack_require__(480); +} else { + module.exports = __webpack_require__(483); } /***/ }), -/* 477 */ +/* 480 */ /***/ (function(module, exports, __webpack_require__) { /** - * Module dependencies. - */ - -var tty = __webpack_require__(478); -var util = __webpack_require__(29); - -/** - * This is the Node.js implementation of `debug()`. + * This is the web browser implementation of `debug()`. * * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(475); -exports.init = init; +exports = module.exports = __webpack_require__(481); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; exports.load = load; exports.useColors = useColors; +exports.storage = 'undefined' != typeof chrome + && 'undefined' != typeof chrome.storage + ? chrome.storage.local + : localstorage(); /** * Colors. */ -exports.colors = [ 6, 2, 3, 4, 5, 1 ]; - -try { - var supportsColor = __webpack_require__(479); - if (supportsColor && supportsColor.level >= 2) { - exports.colors = [ - 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, - 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134, - 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, - 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201, 202, 203, 204, - 205, 206, 207, 208, 209, 214, 215, 220, 221 - ]; - } -} catch (err) { - // swallow - we only care if `supports-color` is available; it doesn't have to be. -} +exports.colors = [ + '#0000CC', '#0000FF', '#0033CC', '#0033FF', '#0066CC', '#0066FF', '#0099CC', + '#0099FF', '#00CC00', '#00CC33', '#00CC66', '#00CC99', '#00CCCC', '#00CCFF', + '#3300CC', '#3300FF', '#3333CC', '#3333FF', '#3366CC', '#3366FF', '#3399CC', + '#3399FF', '#33CC00', '#33CC33', '#33CC66', '#33CC99', '#33CCCC', '#33CCFF', + '#6600CC', '#6600FF', '#6633CC', '#6633FF', '#66CC00', '#66CC33', '#9900CC', + '#9900FF', '#9933CC', '#9933FF', '#99CC00', '#99CC33', '#CC0000', '#CC0033', + '#CC0066', '#CC0099', '#CC00CC', '#CC00FF', '#CC3300', '#CC3333', '#CC3366', + '#CC3399', '#CC33CC', '#CC33FF', '#CC6600', '#CC6633', '#CC9900', '#CC9933', + '#CCCC00', '#CCCC33', '#FF0000', '#FF0033', '#FF0066', '#FF0099', '#FF00CC', + '#FF00FF', '#FF3300', '#FF3333', '#FF3366', '#FF3399', '#FF33CC', '#FF33FF', + '#FF6600', '#FF6633', '#FF9900', '#FF9933', '#FFCC00', '#FFCC33' +]; /** - * Build up the default `inspectOpts` object from the environment variables. + * Currently only WebKit-based Web Inspectors, Firefox >= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. * - * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js + * TODO: add a `localStorage` variable to explicitly enable/disable colors */ -exports.inspectOpts = Object.keys(process.env).filter(function (key) { - return /^debug_/i.test(key); -}).reduce(function (obj, key) { - // camel-case - var prop = key - .substring(6) - .toLowerCase() - .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); - - // coerce string value into JS value - var val = process.env[key]; - if (/^(yes|on|true|enabled)$/i.test(val)) val = true; - else if (/^(no|off|false|disabled)$/i.test(val)) val = false; - else if (val === 'null') val = null; - else val = Number(val); - - obj[prop] = val; - return obj; -}, {}); +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { + return true; + } -/** - * Is stdout a TTY? Colored output is enabled when `true`. - */ + // Internet Explorer and Edge do not support colors. + if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) { + return false; + } -function useColors() { - return 'colors' in exports.inspectOpts - ? Boolean(exports.inspectOpts.colors) - : tty.isatty(process.stderr.fd); + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); } /** - * Map %o to `util.inspect()`, all on a single line. + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. */ -exports.formatters.o = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts) - .split('\n').map(function(str) { - return str.trim() - }).join(' '); +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } }; -/** - * Map %o to `util.inspect()`, allowing multiple lines if needed. - */ - -exports.formatters.O = function(v) { - this.inspectOpts.colors = this.useColors; - return util.inspect(v, this.inspectOpts); -}; /** - * Adds ANSI color escape codes if enabled. + * Colorize log arguments if enabled. * * @api public */ function formatArgs(args) { - var name = this.namespace; var useColors = this.useColors; - if (useColors) { - var c = this.color; - var colorCode = '\u001b[3' + (c < 8 ? c : '8;5;' + c); - var prefix = ' ' + colorCode + ';1m' + name + ' ' + '\u001b[0m'; + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); - args[0] = prefix + args[0].split('\n').join('\n' + prefix); - args.push(colorCode + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); - } else { - args[0] = getDate() + name + ' ' + args[0]; - } -} + if (!useColors) return; -function getDate() { - if (exports.inspectOpts.hideDate) { - return ''; - } else { - return new Date().toISOString() + ' '; - } + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); } /** - * Invokes `util.format()` with the specified arguments and writes to stderr. + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public */ function log() { - return process.stderr.write(util.format.apply(util, arguments) + '\n'); + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); } /** @@ -42565,13 +42422,13 @@ function log() { */ function save(namespaces) { - if (null == namespaces) { - // If you set a process.env field to null or undefined, it gets cast to the - // string 'null' or 'undefined'. Just delete instead. - delete process.env.DEBUG; - } else { - process.env.DEBUG = namespaces; - } + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} } /** @@ -42582,606 +42439,780 @@ function save(namespaces) { */ function load() { - return process.env.DEBUG; -} - -/** - * Init logic for `debug` instances. - * - * Create a new `inspectOpts` object in case `useColors` is set - * differently for a particular `debug` instance. - */ - -function init (debug) { - debug.inspectOpts = {}; + var r; + try { + r = exports.storage.debug; + } catch(e) {} - var keys = Object.keys(exports.inspectOpts); - for (var i = 0; i < keys.length; i++) { - debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; } + + return r; } /** - * Enable namespaces listed in `process.env.DEBUG` initially. + * Enable namespaces listed in `localStorage.debug` initially. */ exports.enable(load()); +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ -/***/ }), -/* 478 */ -/***/ (function(module, exports) { +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} -module.exports = require("tty"); /***/ }), -/* 479 */ +/* 481 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; - -const os = __webpack_require__(11); -const hasFlag = __webpack_require__(12); - -const env = process.env; - -let forceColor; -if (hasFlag('no-color') || - hasFlag('no-colors') || - hasFlag('color=false')) { - forceColor = false; -} else if (hasFlag('color') || - hasFlag('colors') || - hasFlag('color=true') || - hasFlag('color=always')) { - forceColor = true; -} -if ('FORCE_COLOR' in env) { - forceColor = env.FORCE_COLOR.length === 0 || parseInt(env.FORCE_COLOR, 10) !== 0; -} - -function translateLevel(level) { - if (level === 0) { - return false; - } - return { - level, - hasBasic: true, - has256: level >= 2, - has16m: level >= 3 - }; -} +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ -function supportsColor(stream) { - if (forceColor === false) { - return 0; - } +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = __webpack_require__(482); - if (hasFlag('color=16m') || - hasFlag('color=full') || - hasFlag('color=truecolor')) { - return 3; - } +/** + * Active `debug` instances. + */ +exports.instances = []; - if (hasFlag('color=256')) { - return 2; - } +/** + * The currently active debug mode names, and names to skip. + */ - if (stream && !stream.isTTY && forceColor !== true) { - return 0; - } +exports.names = []; +exports.skips = []; - const min = forceColor ? 1 : 0; +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ - if (process.platform === 'win32') { - // Node.js 7.5.0 is the first version of Node.js to include a patch to - // libuv that enables 256 color output on Windows. Anything earlier and it - // won't work. However, here we target Node.js 8 at minimum as it is an LTS - // release, and Node.js 7 is not. Windows 10 build 10586 is the first Windows - // release that supports 256 colors. Windows 10 build 14931 is the first release - // that supports 16m/TrueColor. - const osRelease = os.release().split('.'); - if ( - Number(process.versions.node.split('.')[0]) >= 8 && - Number(osRelease[0]) >= 10 && - Number(osRelease[2]) >= 10586 - ) { - return Number(osRelease[2]) >= 14931 ? 3 : 2; - } +exports.formatters = {}; - return 1; - } +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ - if ('CI' in env) { - if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI'].some(sign => sign in env) || env.CI_NAME === 'codeship') { - return 1; - } +function selectColor(namespace) { + var hash = 0, i; - return min; - } + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } - if ('TEAMCITY_VERSION' in env) { - return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; - } + return exports.colors[Math.abs(hash) % exports.colors.length]; +} - if (env.COLORTERM === 'truecolor') { - return 3; - } +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ - if ('TERM_PROGRAM' in env) { - const version = parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); +function createDebug(namespace) { - switch (env.TERM_PROGRAM) { - case 'iTerm.app': - return version >= 3 ? 3 : 2; - case 'Apple_Terminal': - return 2; - // No default - } - } + var prevTime; - if (/-256(color)?$/i.test(env.TERM)) { - return 2; - } + function debug() { + // disabled? + if (!debug.enabled) return; - if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { - return 1; - } + var self = debug; - if ('COLORTERM' in env) { - return 1; - } + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; - if (env.TERM === 'dumb') { - return min; - } + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } - return min; -} + args[0] = exports.coerce(args[0]); -function getSupportLevel(stream) { - const level = supportsColor(stream); - return translateLevel(level); -} + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } -module.exports = { - supportsColor: getSupportLevel, - stdout: getSupportLevel(process.stdout), - stderr: getSupportLevel(process.stderr) -}; + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); -/***/ }), -/* 480 */ -/***/ (function(module, exports) { + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); -module.exports = require("zlib"); + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } -/***/ }), -/* 481 */ -/***/ (function(module) { + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + debug.destroy = destroy; -module.exports = JSON.parse("{\"name\":\"axios\",\"version\":\"0.19.0\",\"description\":\"Promise based HTTP client for the browser and node.js\",\"main\":\"index.js\",\"scripts\":{\"test\":\"grunt test && bundlesize\",\"start\":\"node ./sandbox/server.js\",\"build\":\"NODE_ENV=production grunt build\",\"preversion\":\"npm test\",\"version\":\"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json\",\"postversion\":\"git push && git push --tags\",\"examples\":\"node ./examples/server.js\",\"coveralls\":\"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js\",\"fix\":\"eslint --fix lib/**/*.js\"},\"repository\":{\"type\":\"git\",\"url\":\"https://github.com/axios/axios.git\"},\"keywords\":[\"xhr\",\"http\",\"ajax\",\"promise\",\"node\"],\"author\":\"Matt Zabriskie\",\"license\":\"MIT\",\"bugs\":{\"url\":\"https://github.com/axios/axios/issues\"},\"homepage\":\"https://github.com/axios/axios\",\"devDependencies\":{\"bundlesize\":\"^0.17.0\",\"coveralls\":\"^3.0.0\",\"es6-promise\":\"^4.2.4\",\"grunt\":\"^1.0.2\",\"grunt-banner\":\"^0.6.0\",\"grunt-cli\":\"^1.2.0\",\"grunt-contrib-clean\":\"^1.1.0\",\"grunt-contrib-watch\":\"^1.0.0\",\"grunt-eslint\":\"^20.1.0\",\"grunt-karma\":\"^2.0.0\",\"grunt-mocha-test\":\"^0.13.3\",\"grunt-ts\":\"^6.0.0-beta.19\",\"grunt-webpack\":\"^1.0.18\",\"istanbul-instrumenter-loader\":\"^1.0.0\",\"jasmine-core\":\"^2.4.1\",\"karma\":\"^1.3.0\",\"karma-chrome-launcher\":\"^2.2.0\",\"karma-coverage\":\"^1.1.1\",\"karma-firefox-launcher\":\"^1.1.0\",\"karma-jasmine\":\"^1.1.1\",\"karma-jasmine-ajax\":\"^0.1.13\",\"karma-opera-launcher\":\"^1.0.0\",\"karma-safari-launcher\":\"^1.0.0\",\"karma-sauce-launcher\":\"^1.2.0\",\"karma-sinon\":\"^1.0.5\",\"karma-sourcemap-loader\":\"^0.3.7\",\"karma-webpack\":\"^1.7.0\",\"load-grunt-tasks\":\"^3.5.2\",\"minimist\":\"^1.2.0\",\"mocha\":\"^5.2.0\",\"sinon\":\"^4.5.0\",\"typescript\":\"^2.8.1\",\"url-search-params\":\"^0.10.0\",\"webpack\":\"^1.13.1\",\"webpack-dev-server\":\"^1.14.1\"},\"browser\":{\"./lib/adapters/http.js\":\"./lib/adapters/xhr.js\"},\"typings\":\"./index.d.ts\",\"dependencies\":{\"follow-redirects\":\"1.5.10\",\"is-buffer\":\"^2.0.2\"},\"bundlesize\":[{\"path\":\"./dist/axios.min.js\",\"threshold\":\"5kB\"}]}"); + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } -/***/ }), -/* 482 */ -/***/ (function(module, exports, __webpack_require__) { + exports.instances.push(debug); -"use strict"; + return debug; +} +function destroy () { + var index = exports.instances.indexOf(this); + if (index !== -1) { + exports.instances.splice(index, 1); + return true; + } else { + return false; + } +} -var utils = __webpack_require__(455); -var settle = __webpack_require__(467); -var buildURL = __webpack_require__(459); -var parseHeaders = __webpack_require__(483); -var isURLSameOrigin = __webpack_require__(484); -var createError = __webpack_require__(468); +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ -module.exports = function xhrAdapter(config) { - return new Promise(function dispatchXhrRequest(resolve, reject) { - var requestData = config.data; - var requestHeaders = config.headers; +function enable(namespaces) { + exports.save(namespaces); - if (utils.isFormData(requestData)) { - delete requestHeaders['Content-Type']; // Let the browser set it - } + exports.names = []; + exports.skips = []; - var request = new XMLHttpRequest(); + var i; + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; - // HTTP basic authentication - if (config.auth) { - var username = config.auth.username || ''; - var password = config.auth.password || ''; - requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); + for (i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); } + } - request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true); + for (i = 0; i < exports.instances.length; i++) { + var instance = exports.instances[i]; + instance.enabled = exports.enabled(instance.namespace); + } +} - // Set the request timeout in MS - request.timeout = config.timeout; +/** + * Disable debug output. + * + * @api public + */ - // Listen for ready state - request.onreadystatechange = function handleLoad() { - if (!request || request.readyState !== 4) { - return; - } +function disable() { + exports.enable(''); +} - // The request errored out and we didn't get a response, this will be - // handled by onerror instead - // With one exception: request that using file: protocol, most browsers - // will return status as 0 even though it's a successful request - if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { - return; - } +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ - // Prepare the response - var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; - var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; - var response = { - data: responseData, - status: request.status, - statusText: request.statusText, - headers: responseHeaders, - config: config, - request: request - }; - - settle(resolve, reject, response); - - // Clean up request - request = null; - }; - - // Handle browser request cancellation (as opposed to a manual cancellation) - request.onabort = function handleAbort() { - if (!request) { - return; - } - - reject(createError('Request aborted', config, 'ECONNABORTED', request)); - - // Clean up request - request = null; - }; +function enabled(name) { + if (name[name.length - 1] === '*') { + return true; + } + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} - // Handle low level network errors - request.onerror = function handleError() { - // Real errors are hidden from us by the browser - // onerror should only fire if it's a network error - reject(createError('Network Error', config, null, request)); +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ - // Clean up request - request = null; - }; +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} - // Handle timeout - request.ontimeout = function handleTimeout() { - reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', - request)); - // Clean up request - request = null; - }; +/***/ }), +/* 482 */ +/***/ (function(module, exports) { - // Add xsrf header - // This is only done if running in a standard browser environment. - // Specifically not if we're in a web worker, or react-native. - if (utils.isStandardBrowserEnv()) { - var cookies = __webpack_require__(485); +/** + * Helpers. + */ - // Add xsrf header - var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ? - cookies.read(config.xsrfCookieName) : - undefined; +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; - if (xsrfValue) { - requestHeaders[config.xsrfHeaderName] = xsrfValue; - } - } +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ - // Add headers to the request - if ('setRequestHeader' in request) { - utils.forEach(requestHeaders, function setRequestHeader(val, key) { - if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { - // Remove Content-Type if data is undefined - delete requestHeaders[key]; - } else { - // Otherwise add header to the request - request.setRequestHeader(key, val); - } - }); - } +module.exports = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isNaN(val) === false) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; - // Add withCredentials to request if needed - if (config.withCredentials) { - request.withCredentials = true; - } +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ - // Add responseType to request if needed - if (config.responseType) { - try { - request.responseType = config.responseType; - } catch (e) { - // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2. - // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function. - if (config.responseType !== 'json') { - throw e; - } - } - } +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} - // Handle progress if needed - if (typeof config.onDownloadProgress === 'function') { - request.addEventListener('progress', config.onDownloadProgress); - } +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ - // Not all browsers support upload events - if (typeof config.onUploadProgress === 'function' && request.upload) { - request.upload.addEventListener('progress', config.onUploadProgress); - } +function fmtShort(ms) { + if (ms >= d) { + return Math.round(ms / d) + 'd'; + } + if (ms >= h) { + return Math.round(ms / h) + 'h'; + } + if (ms >= m) { + return Math.round(ms / m) + 'm'; + } + if (ms >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} - if (config.cancelToken) { - // Handle cancellation - config.cancelToken.promise.then(function onCanceled(cancel) { - if (!request) { - return; - } +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ - request.abort(); - reject(cancel); - // Clean up request - request = null; - }); - } +function fmtLong(ms) { + return plural(ms, d, 'day') || + plural(ms, h, 'hour') || + plural(ms, m, 'minute') || + plural(ms, s, 'second') || + ms + ' ms'; +} - if (requestData === undefined) { - requestData = null; - } +/** + * Pluralization helper. + */ - // Send the request - request.send(requestData); - }); -}; +function plural(ms, n, name) { + if (ms < n) { + return; + } + if (ms < n * 1.5) { + return Math.floor(ms / n) + ' ' + name; + } + return Math.ceil(ms / n) + ' ' + name + 's'; +} /***/ }), /* 483 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; - - -var utils = __webpack_require__(455); +/** + * Module dependencies. + */ -// Headers whose duplicates are ignored by node -// c.f. https://nodejs.org/api/http.html#http_message_headers -var ignoreDuplicateOf = [ - 'age', 'authorization', 'content-length', 'content-type', 'etag', - 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', - 'last-modified', 'location', 'max-forwards', 'proxy-authorization', - 'referer', 'retry-after', 'user-agent' -]; +var tty = __webpack_require__(484); +var util = __webpack_require__(29); /** - * Parse headers into an object - * - * ``` - * Date: Wed, 27 Aug 2014 08:58:49 GMT - * Content-Type: application/json - * Connection: keep-alive - * Transfer-Encoding: chunked - * ``` + * This is the Node.js implementation of `debug()`. * - * @param {String} headers Headers needing to be parsed - * @returns {Object} Headers parsed into an object + * Expose `debug()` as the module. */ -module.exports = function parseHeaders(headers) { - var parsed = {}; - var key; - var val; - var i; - - if (!headers) { return parsed; } - utils.forEach(headers.split('\n'), function parser(line) { - i = line.indexOf(':'); - key = utils.trim(line.substr(0, i)).toLowerCase(); - val = utils.trim(line.substr(i + 1)); +exports = module.exports = __webpack_require__(481); +exports.init = init; +exports.log = log; +exports.formatArgs = formatArgs; +exports.save = save; +exports.load = load; +exports.useColors = useColors; - if (key) { - if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) { - return; - } - if (key === 'set-cookie') { - parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]); - } else { - parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; - } - } - }); +/** + * Colors. + */ - return parsed; -}; +exports.colors = [ 6, 2, 3, 4, 5, 1 ]; +try { + var supportsColor = __webpack_require__(485); + if (supportsColor && supportsColor.level >= 2) { + exports.colors = [ + 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 56, 57, 62, 63, 68, + 69, 74, 75, 76, 77, 78, 79, 80, 81, 92, 93, 98, 99, 112, 113, 128, 129, 134, + 135, 148, 149, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, + 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201, 202, 203, 204, + 205, 206, 207, 208, 209, 214, 215, 220, 221 + ]; + } +} catch (err) { + // swallow - we only care if `supports-color` is available; it doesn't have to be. +} -/***/ }), -/* 484 */ -/***/ (function(module, exports, __webpack_require__) { +/** + * Build up the default `inspectOpts` object from the environment variables. + * + * $ DEBUG_COLORS=no DEBUG_DEPTH=10 DEBUG_SHOW_HIDDEN=enabled node script.js + */ -"use strict"; +exports.inspectOpts = Object.keys(process.env).filter(function (key) { + return /^debug_/i.test(key); +}).reduce(function (obj, key) { + // camel-case + var prop = key + .substring(6) + .toLowerCase() + .replace(/_([a-z])/g, function (_, k) { return k.toUpperCase() }); + // coerce string value into JS value + var val = process.env[key]; + if (/^(yes|on|true|enabled)$/i.test(val)) val = true; + else if (/^(no|off|false|disabled)$/i.test(val)) val = false; + else if (val === 'null') val = null; + else val = Number(val); -var utils = __webpack_require__(455); + obj[prop] = val; + return obj; +}, {}); -module.exports = ( - utils.isStandardBrowserEnv() ? +/** + * Is stdout a TTY? Colored output is enabled when `true`. + */ - // Standard browser envs have full support of the APIs needed to test - // whether the request URL is of the same origin as current location. - (function standardBrowserEnv() { - var msie = /(msie|trident)/i.test(navigator.userAgent); - var urlParsingNode = document.createElement('a'); - var originURL; +function useColors() { + return 'colors' in exports.inspectOpts + ? Boolean(exports.inspectOpts.colors) + : tty.isatty(process.stderr.fd); +} - /** - * Parse a URL to discover it's components - * - * @param {String} url The URL to be parsed - * @returns {Object} - */ - function resolveURL(url) { - var href = url; +/** + * Map %o to `util.inspect()`, all on a single line. + */ - if (msie) { - // IE needs attribute set twice to normalize properties - urlParsingNode.setAttribute('href', href); - href = urlParsingNode.href; - } +exports.formatters.o = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts) + .split('\n').map(function(str) { + return str.trim() + }).join(' '); +}; - urlParsingNode.setAttribute('href', href); +/** + * Map %o to `util.inspect()`, allowing multiple lines if needed. + */ - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils - return { - href: urlParsingNode.href, - protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', - host: urlParsingNode.host, - search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', - hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', - hostname: urlParsingNode.hostname, - port: urlParsingNode.port, - pathname: (urlParsingNode.pathname.charAt(0) === '/') ? - urlParsingNode.pathname : - '/' + urlParsingNode.pathname - }; - } +exports.formatters.O = function(v) { + this.inspectOpts.colors = this.useColors; + return util.inspect(v, this.inspectOpts); +}; - originURL = resolveURL(window.location.href); +/** + * Adds ANSI color escape codes if enabled. + * + * @api public + */ - /** - * Determine if a URL shares the same origin as the current location - * - * @param {String} requestURL The URL to test - * @returns {boolean} True if URL shares the same origin, otherwise false - */ - return function isURLSameOrigin(requestURL) { - var parsed = (utils.isString(requestURL)) ? resolveURL(requestURL) : requestURL; - return (parsed.protocol === originURL.protocol && - parsed.host === originURL.host); - }; - })() : +function formatArgs(args) { + var name = this.namespace; + var useColors = this.useColors; - // Non standard browser envs (web workers, react-native) lack needed support. - (function nonStandardBrowserEnv() { - return function isURLSameOrigin() { - return true; - }; - })() -); + if (useColors) { + var c = this.color; + var colorCode = '\u001b[3' + (c < 8 ? c : '8;5;' + c); + var prefix = ' ' + colorCode + ';1m' + name + ' ' + '\u001b[0m'; + args[0] = prefix + args[0].split('\n').join('\n' + prefix); + args.push(colorCode + 'm+' + exports.humanize(this.diff) + '\u001b[0m'); + } else { + args[0] = getDate() + name + ' ' + args[0]; + } +} -/***/ }), -/* 485 */ -/***/ (function(module, exports, __webpack_require__) { +function getDate() { + if (exports.inspectOpts.hideDate) { + return ''; + } else { + return new Date().toISOString() + ' '; + } +} -"use strict"; +/** + * Invokes `util.format()` with the specified arguments and writes to stderr. + */ +function log() { + return process.stderr.write(util.format.apply(util, arguments) + '\n'); +} -var utils = __webpack_require__(455); +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ -module.exports = ( - utils.isStandardBrowserEnv() ? +function save(namespaces) { + if (null == namespaces) { + // If you set a process.env field to null or undefined, it gets cast to the + // string 'null' or 'undefined'. Just delete instead. + delete process.env.DEBUG; + } else { + process.env.DEBUG = namespaces; + } +} - // Standard browser envs support document.cookie - (function standardBrowserEnv() { - return { - write: function write(name, value, expires, path, domain, secure) { - var cookie = []; - cookie.push(name + '=' + encodeURIComponent(value)); +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ - if (utils.isNumber(expires)) { - cookie.push('expires=' + new Date(expires).toGMTString()); - } +function load() { + return process.env.DEBUG; +} - if (utils.isString(path)) { - cookie.push('path=' + path); - } +/** + * Init logic for `debug` instances. + * + * Create a new `inspectOpts` object in case `useColors` is set + * differently for a particular `debug` instance. + */ - if (utils.isString(domain)) { - cookie.push('domain=' + domain); - } +function init (debug) { + debug.inspectOpts = {}; - if (secure === true) { - cookie.push('secure'); - } + var keys = Object.keys(exports.inspectOpts); + for (var i = 0; i < keys.length; i++) { + debug.inspectOpts[keys[i]] = exports.inspectOpts[keys[i]]; + } +} - document.cookie = cookie.join('; '); - }, +/** + * Enable namespaces listed in `process.env.DEBUG` initially. + */ - read: function read(name) { - var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')); - return (match ? decodeURIComponent(match[3]) : null); - }, +exports.enable(load()); - remove: function remove(name) { - this.write(name, '', Date.now() - 86400000); - } - }; - })() : - // Non standard browser env (web workers, react-native) lack needed support. - (function nonStandardBrowserEnv() { - return { - write: function write() {}, - read: function read() { return null; }, - remove: function remove() {} - }; - })() -); +/***/ }), +/* 484 */ +/***/ (function(module, exports) { +module.exports = require("tty"); /***/ }), -/* 486 */ +/* 485 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const os = __webpack_require__(11); +const hasFlag = __webpack_require__(12); -/** - * Determines whether the specified URL is absolute - * - * @param {string} url The URL to test - * @returns {boolean} True if the specified URL is absolute, otherwise false - */ -module.exports = function isAbsoluteURL(url) { - // A URL is considered absolute if it begins with "://" or "//" (protocol-relative URL). - // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed - // by any combination of letters, digits, plus, period, or hyphen. - return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); -}; +const env = process.env; +let forceColor; +if (hasFlag('no-color') || + hasFlag('no-colors') || + hasFlag('color=false')) { + forceColor = false; +} else if (hasFlag('color') || + hasFlag('colors') || + hasFlag('color=true') || + hasFlag('color=always')) { + forceColor = true; +} +if ('FORCE_COLOR' in env) { + forceColor = env.FORCE_COLOR.length === 0 || parseInt(env.FORCE_COLOR, 10) !== 0; +} -/***/ }), -/* 487 */ -/***/ (function(module, exports, __webpack_require__) { +function translateLevel(level) { + if (level === 0) { + return false; + } -"use strict"; + return { + level, + hasBasic: true, + has256: level >= 2, + has16m: level >= 3 + }; +} +function supportsColor(stream) { + if (forceColor === false) { + return 0; + } -/** - * Creates a new URL by combining the specified URLs - * - * @param {string} baseURL The base URL - * @param {string} relativeURL The relative URL - * @returns {string} The combined URL - */ -module.exports = function combineURLs(baseURL, relativeURL) { - return relativeURL - ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') - : baseURL; + if (hasFlag('color=16m') || + hasFlag('color=full') || + hasFlag('color=truecolor')) { + return 3; + } + + if (hasFlag('color=256')) { + return 2; + } + + if (stream && !stream.isTTY && forceColor !== true) { + return 0; + } + + const min = forceColor ? 1 : 0; + + if (process.platform === 'win32') { + // Node.js 7.5.0 is the first version of Node.js to include a patch to + // libuv that enables 256 color output on Windows. Anything earlier and it + // won't work. However, here we target Node.js 8 at minimum as it is an LTS + // release, and Node.js 7 is not. Windows 10 build 10586 is the first Windows + // release that supports 256 colors. Windows 10 build 14931 is the first release + // that supports 16m/TrueColor. + const osRelease = os.release().split('.'); + if ( + Number(process.versions.node.split('.')[0]) >= 8 && + Number(osRelease[0]) >= 10 && + Number(osRelease[2]) >= 10586 + ) { + return Number(osRelease[2]) >= 14931 ? 3 : 2; + } + + return 1; + } + + if ('CI' in env) { + if (['TRAVIS', 'CIRCLECI', 'APPVEYOR', 'GITLAB_CI'].some(sign => sign in env) || env.CI_NAME === 'codeship') { + return 1; + } + + return min; + } + + if ('TEAMCITY_VERSION' in env) { + return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; + } + + if (env.COLORTERM === 'truecolor') { + return 3; + } + + if ('TERM_PROGRAM' in env) { + const version = parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10); + + switch (env.TERM_PROGRAM) { + case 'iTerm.app': + return version >= 3 ? 3 : 2; + case 'Apple_Terminal': + return 2; + // No default + } + } + + if (/-256(color)?$/i.test(env.TERM)) { + return 2; + } + + if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { + return 1; + } + + if ('COLORTERM' in env) { + return 1; + } + + if (env.TERM === 'dumb') { + return min; + } + + return min; +} + +function getSupportLevel(stream) { + const level = supportsColor(stream); + return translateLevel(level); +} + +module.exports = { + supportsColor: getSupportLevel, + stdout: getSupportLevel(process.stdout), + stderr: getSupportLevel(process.stderr) }; +/***/ }), +/* 486 */ +/***/ (function(module, exports) { + +module.exports = require("zlib"); + +/***/ }), +/* 487 */ +/***/ (function(module) { + +module.exports = JSON.parse("{\"name\":\"axios\",\"version\":\"0.19.2\",\"description\":\"Promise based HTTP client for the browser and node.js\",\"main\":\"index.js\",\"scripts\":{\"test\":\"grunt test && bundlesize\",\"start\":\"node ./sandbox/server.js\",\"build\":\"NODE_ENV=production grunt build\",\"preversion\":\"npm test\",\"version\":\"npm run build && grunt version && git add -A dist && git add CHANGELOG.md bower.json package.json\",\"postversion\":\"git push && git push --tags\",\"examples\":\"node ./examples/server.js\",\"coveralls\":\"cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js\",\"fix\":\"eslint --fix lib/**/*.js\"},\"repository\":{\"type\":\"git\",\"url\":\"https://github.com/axios/axios.git\"},\"keywords\":[\"xhr\",\"http\",\"ajax\",\"promise\",\"node\"],\"author\":\"Matt Zabriskie\",\"license\":\"MIT\",\"bugs\":{\"url\":\"https://github.com/axios/axios/issues\"},\"homepage\":\"https://github.com/axios/axios\",\"devDependencies\":{\"bundlesize\":\"^0.17.0\",\"coveralls\":\"^3.0.0\",\"es6-promise\":\"^4.2.4\",\"grunt\":\"^1.0.2\",\"grunt-banner\":\"^0.6.0\",\"grunt-cli\":\"^1.2.0\",\"grunt-contrib-clean\":\"^1.1.0\",\"grunt-contrib-watch\":\"^1.0.0\",\"grunt-eslint\":\"^20.1.0\",\"grunt-karma\":\"^2.0.0\",\"grunt-mocha-test\":\"^0.13.3\",\"grunt-ts\":\"^6.0.0-beta.19\",\"grunt-webpack\":\"^1.0.18\",\"istanbul-instrumenter-loader\":\"^1.0.0\",\"jasmine-core\":\"^2.4.1\",\"karma\":\"^1.3.0\",\"karma-chrome-launcher\":\"^2.2.0\",\"karma-coverage\":\"^1.1.1\",\"karma-firefox-launcher\":\"^1.1.0\",\"karma-jasmine\":\"^1.1.1\",\"karma-jasmine-ajax\":\"^0.1.13\",\"karma-opera-launcher\":\"^1.0.0\",\"karma-safari-launcher\":\"^1.0.0\",\"karma-sauce-launcher\":\"^1.2.0\",\"karma-sinon\":\"^1.0.5\",\"karma-sourcemap-loader\":\"^0.3.7\",\"karma-webpack\":\"^1.7.0\",\"load-grunt-tasks\":\"^3.5.2\",\"minimist\":\"^1.2.0\",\"mocha\":\"^5.2.0\",\"sinon\":\"^4.5.0\",\"typescript\":\"^2.8.1\",\"url-search-params\":\"^0.10.0\",\"webpack\":\"^1.13.1\",\"webpack-dev-server\":\"^1.14.1\"},\"browser\":{\"./lib/adapters/http.js\":\"./lib/adapters/xhr.js\"},\"typings\":\"./index.d.ts\",\"dependencies\":{\"follow-redirects\":\"1.5.10\"},\"bundlesize\":[{\"path\":\"./dist/axios.min.js\",\"threshold\":\"5kB\"}]}"); + /***/ }), /* 488 */ /***/ (function(module, exports, __webpack_require__) { @@ -43204,13 +43235,23 @@ module.exports = function mergeConfig(config1, config2) { config2 = config2 || {}; var config = {}; - utils.forEach(['url', 'method', 'params', 'data'], function valueFromConfig2(prop) { + var valueFromConfig2Keys = ['url', 'method', 'params', 'data']; + var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy']; + var defaultToConfig2Keys = [ + 'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer', + 'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', + 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', + 'maxContentLength', 'validateStatus', 'maxRedirects', 'httpAgent', + 'httpsAgent', 'cancelToken', 'socketPath' + ]; + + utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } }); - utils.forEach(['headers', 'auth', 'proxy'], function mergeDeepProperties(prop) { + utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) { if (utils.isObject(config2[prop])) { config[prop] = utils.deepMerge(config1[prop], config2[prop]); } else if (typeof config2[prop] !== 'undefined') { @@ -43222,13 +43263,25 @@ module.exports = function mergeConfig(config1, config2) { } }); - utils.forEach([ - 'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer', - 'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', - 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'maxContentLength', - 'validateStatus', 'maxRedirects', 'httpAgent', 'httpsAgent', 'cancelToken', - 'socketPath' - ], function defaultToConfig2(prop) { + utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { + if (typeof config2[prop] !== 'undefined') { + config[prop] = config2[prop]; + } else if (typeof config1[prop] !== 'undefined') { + config[prop] = config1[prop]; + } + }); + + var axiosKeys = valueFromConfig2Keys + .concat(mergeDeepPropertiesKeys) + .concat(defaultToConfig2Keys); + + var otherKeys = Object + .keys(config2) + .filter(function filterAxiosKeys(key) { + return axiosKeys.indexOf(key) === -1; + }); + + utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (typeof config1[prop] !== 'undefined') { @@ -43777,6 +43830,165 @@ exports.KbnClientUiSettings = KbnClientUiSettings; /***/ }), /* 499 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = __webpack_require__(36); +tslib_1.__exportStar(__webpack_require__(500), exports); + + +/***/ }), +/* 500 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = __webpack_require__(36); +const util_1 = __webpack_require__(29); +const axios_1 = tslib_1.__importDefault(__webpack_require__(453)); +function parseConfig(log) { + const configJson = process.env.KIBANA_CI_STATS_CONFIG; + if (!configJson) { + log.debug('KIBANA_CI_STATS_CONFIG environment variable not found, disabling CiStatsReporter'); + return; + } + let config; + try { + config = JSON.parse(configJson); + } + catch (_) { + // handled below + } + if (typeof config === 'object' && config !== null) { + return validateConfig(log, config); + } + log.warning('KIBANA_CI_STATS_CONFIG is invalid, stats will not be reported'); + return; +} +function validateConfig(log, config) { + const validApiUrl = typeof config.apiUrl === 'string' && config.apiUrl.length !== 0; + if (!validApiUrl) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api url, stats will not be reported'); + return; + } + const validApiToken = typeof config.apiToken === 'string' && config.apiToken.length !== 0; + if (!validApiToken) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid api token, stats will not be reported'); + return; + } + const validId = typeof config.buildId === 'string' && config.buildId.length !== 0; + if (!validId) { + log.warning('KIBANA_CI_STATS_CONFIG is missing a valid build id, stats will not be reported'); + return; + } + return config; +} +class CiStatsReporter { + constructor(config, log) { + this.config = config; + this.log = log; + } + static fromEnv(log) { + return new CiStatsReporter(parseConfig(log), log); + } + isEnabled() { + return !!this.config; + } + async metric(name, subName, value) { + var _a, _b, _c, _d; + if (!this.config) { + return; + } + let attempt = 0; + const maxAttempts = 5; + while (true) { + attempt += 1; + try { + await axios_1.default.request({ + method: 'POST', + url: '/metric', + baseURL: this.config.apiUrl, + params: { + buildId: this.config.buildId, + }, + headers: { + Authorization: `token ${this.config.apiToken}`, + }, + data: { + name, + subName, + value, + }, + }); + return; + } + catch (error) { + if (!((_a = error) === null || _a === void 0 ? void 0 : _a.request)) { + // not an axios error, must be a usage error that we should notify user about + throw error; + } + if (((_b = error) === null || _b === void 0 ? void 0 : _b.response) && error.response.status !== 502) { + // error response from service was received so warn the user and move on + this.log.warning(`error recording metric [status=${error.response.status}] [resp=${util_1.inspect(error.response.data)}] [${name}/${subName}=${value}]`); + return; + } + if (attempt === maxAttempts) { + this.log.warning(`failed to reach kibana-ci-stats service too many times, unable to record metric [${name}/${subName}=${value}]`); + return; + } + // we failed to reach the backend and we have remaining attempts, lets retry after a short delay + const reason = ((_d = (_c = error) === null || _c === void 0 ? void 0 : _c.response) === null || _d === void 0 ? void 0 : _d.status) ? `${error.response.status} response` + : 'no response'; + this.log.warning(`failed to reach kibana-ci-stats service [reason=${reason}], retrying in ${attempt} seconds`); + await new Promise(resolve => setTimeout(resolve, attempt * 1000)); + } + } + } +} +exports.CiStatsReporter = CiStatsReporter; + + +/***/ }), +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -43842,7 +44054,7 @@ async function parallelize(items, fn, concurrency = 4) { } /***/ }), -/* 500 */ +/* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -43851,15 +44063,15 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProjectGraph", function() { return buildProjectGraph; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "topologicallyBatchProjects", function() { return topologicallyBatchProjects; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "includeTransitiveProjects", function() { return includeTransitiveProjects; }); -/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(501); +/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(503); /* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(glob__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); -/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(515); -/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(576); +/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(516); +/* harmony import */ var _project__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(517); +/* harmony import */ var _workspaces__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(578); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -44058,7 +44270,7 @@ function includeTransitiveProjects(subsetOfProjects, allProjects, { } /***/ }), -/* 501 */ +/* 503 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -44104,21 +44316,21 @@ function includeTransitiveProjects(subsetOfProjects, allProjects, { module.exports = glob var fs = __webpack_require__(23) -var rp = __webpack_require__(502) -var minimatch = __webpack_require__(504) +var rp = __webpack_require__(504) +var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(508) +var inherits = __webpack_require__(510) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(510) -var globSync = __webpack_require__(511) -var common = __webpack_require__(512) +var isAbsolute = __webpack_require__(512) +var globSync = __webpack_require__(513) +var common = __webpack_require__(514) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp -var inflight = __webpack_require__(513) +var inflight = __webpack_require__(515) var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored @@ -44854,7 +45066,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 502 */ +/* 504 */ /***/ (function(module, exports, __webpack_require__) { module.exports = realpath @@ -44870,7 +45082,7 @@ var origRealpathSync = fs.realpathSync var version = process.version var ok = /^v[0-5]\./.test(version) -var old = __webpack_require__(503) +var old = __webpack_require__(505) function newError (er) { return er && er.syscall === 'realpath' && ( @@ -44926,7 +45138,7 @@ function unmonkeypatch () { /***/ }), -/* 503 */ +/* 505 */ /***/ (function(module, exports, __webpack_require__) { // Copyright Joyent, Inc. and other Node contributors. @@ -45235,7 +45447,7 @@ exports.realpath = function realpath(p, cache, cb) { /***/ }), -/* 504 */ +/* 506 */ /***/ (function(module, exports, __webpack_require__) { module.exports = minimatch @@ -45247,7 +45459,7 @@ try { } catch (er) {} var GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {} -var expand = __webpack_require__(505) +var expand = __webpack_require__(507) var plTypes = { '!': { open: '(?:(?!(?:', close: '))[^/]*?)'}, @@ -46164,11 +46376,11 @@ function regExpEscape (s) { /***/ }), -/* 505 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { -var concatMap = __webpack_require__(506); -var balanced = __webpack_require__(507); +var concatMap = __webpack_require__(508); +var balanced = __webpack_require__(509); module.exports = expandTop; @@ -46371,7 +46583,7 @@ function expand(str, isTop) { /***/ }), -/* 506 */ +/* 508 */ /***/ (function(module, exports) { module.exports = function (xs, fn) { @@ -46390,7 +46602,7 @@ var isArray = Array.isArray || function (xs) { /***/ }), -/* 507 */ +/* 509 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46456,7 +46668,7 @@ function range(a, b, str) { /***/ }), -/* 508 */ +/* 510 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -46466,12 +46678,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(509); + module.exports = __webpack_require__(511); } /***/ }), -/* 509 */ +/* 511 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -46504,7 +46716,7 @@ if (typeof Object.create === 'function') { /***/ }), -/* 510 */ +/* 512 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -46531,22 +46743,22 @@ module.exports.win32 = win32; /***/ }), -/* 511 */ +/* 513 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync globSync.GlobSync = GlobSync var fs = __webpack_require__(23) -var rp = __webpack_require__(502) -var minimatch = __webpack_require__(504) +var rp = __webpack_require__(504) +var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(501).Glob +var Glob = __webpack_require__(503).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(510) -var common = __webpack_require__(512) +var isAbsolute = __webpack_require__(512) +var common = __webpack_require__(514) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -47023,7 +47235,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 512 */ +/* 514 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -47041,8 +47253,8 @@ function ownProp (obj, field) { } var path = __webpack_require__(16) -var minimatch = __webpack_require__(504) -var isAbsolute = __webpack_require__(510) +var minimatch = __webpack_require__(506) +var isAbsolute = __webpack_require__(512) var Minimatch = minimatch.Minimatch function alphasorti (a, b) { @@ -47269,7 +47481,7 @@ function childrenIgnored (self, path) { /***/ }), -/* 513 */ +/* 515 */ /***/ (function(module, exports, __webpack_require__) { var wrappy = __webpack_require__(385) @@ -47329,7 +47541,7 @@ function slice (args) { /***/ }), -/* 514 */ +/* 516 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47362,7 +47574,7 @@ class CliError extends Error { } /***/ }), -/* 515 */ +/* 517 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47376,10 +47588,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(514); +/* harmony import */ var _errors__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(516); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); -/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); -/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(561); +/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(518); +/* harmony import */ var _scripts__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(563); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -47610,7 +47822,7 @@ function normalizePath(path) { } /***/ }), -/* 516 */ +/* 518 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47618,9 +47830,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readPackageJson", function() { return readPackageJson; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "writePackageJson", function() { return writePackageJson; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isLinkDependency", function() { return isLinkDependency; }); -/* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(517); +/* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(519); /* harmony import */ var read_pkg__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(read_pkg__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(543); +/* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(545); /* harmony import */ var write_pkg__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(write_pkg__WEBPACK_IMPORTED_MODULE_1__); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -47654,7 +47866,7 @@ function writePackageJson(path, json) { const isLinkDependency = depVersion => depVersion.startsWith('link:'); /***/ }), -/* 517 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47662,7 +47874,7 @@ const isLinkDependency = depVersion => depVersion.startsWith('link:'); const {promisify} = __webpack_require__(29); const fs = __webpack_require__(23); const path = __webpack_require__(16); -const parseJson = __webpack_require__(518); +const parseJson = __webpack_require__(520); const readFileAsync = promisify(fs.readFile); @@ -47677,7 +47889,7 @@ module.exports = async options => { const json = parseJson(await readFileAsync(filePath, 'utf8')); if (options.normalize) { - __webpack_require__(519)(json); + __webpack_require__(521)(json); } return json; @@ -47694,7 +47906,7 @@ module.exports.sync = options => { const json = parseJson(fs.readFileSync(filePath, 'utf8')); if (options.normalize) { - __webpack_require__(519)(json); + __webpack_require__(521)(json); } return json; @@ -47702,7 +47914,7 @@ module.exports.sync = options => { /***/ }), -/* 518 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -47759,15 +47971,15 @@ module.exports = (string, reviver, filename) => { /***/ }), -/* 519 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { module.exports = normalize -var fixer = __webpack_require__(520) +var fixer = __webpack_require__(522) normalize.fixer = fixer -var makeWarning = __webpack_require__(541) +var makeWarning = __webpack_require__(543) var fieldsToFix = ['name','version','description','repository','modules','scripts' ,'files','bin','man','bugs','keywords','readme','homepage','license'] @@ -47804,17 +48016,17 @@ function ucFirst (string) { /***/ }), -/* 520 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { -var semver = __webpack_require__(521) -var validateLicense = __webpack_require__(522); -var hostedGitInfo = __webpack_require__(527) -var isBuiltinModule = __webpack_require__(530).isCore +var semver = __webpack_require__(523) +var validateLicense = __webpack_require__(524); +var hostedGitInfo = __webpack_require__(529) +var isBuiltinModule = __webpack_require__(532).isCore var depTypes = ["dependencies","devDependencies","optionalDependencies"] -var extractDescription = __webpack_require__(539) +var extractDescription = __webpack_require__(541) var url = __webpack_require__(452) -var typos = __webpack_require__(540) +var typos = __webpack_require__(542) var fixer = module.exports = { // default warning function @@ -48228,7 +48440,7 @@ function bugsTypos(bugs, warn) { /***/ }), -/* 521 */ +/* 523 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -49717,11 +49929,11 @@ function coerce (version) { /***/ }), -/* 522 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(523); -var correct = __webpack_require__(525); +var parse = __webpack_require__(525); +var correct = __webpack_require__(527); var genericWarning = ( 'license should be ' + @@ -49807,10 +50019,10 @@ module.exports = function(argument) { /***/ }), -/* 523 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { -var parser = __webpack_require__(524).parser +var parser = __webpack_require__(526).parser module.exports = function (argument) { return parser.parse(argument) @@ -49818,7 +50030,7 @@ module.exports = function (argument) { /***/ }), -/* 524 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { /* WEBPACK VAR INJECTION */(function(module) {/* parser generated by jison 0.4.17 */ @@ -51182,10 +51394,10 @@ if ( true && __webpack_require__.c[__webpack_require__.s] === module) { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 525 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { -var licenseIDs = __webpack_require__(526); +var licenseIDs = __webpack_require__(528); function valid(string) { return licenseIDs.indexOf(string) > -1; @@ -51425,20 +51637,20 @@ module.exports = function(identifier) { /***/ }), -/* 526 */ +/* 528 */ /***/ (function(module) { module.exports = JSON.parse("[\"Glide\",\"Abstyles\",\"AFL-1.1\",\"AFL-1.2\",\"AFL-2.0\",\"AFL-2.1\",\"AFL-3.0\",\"AMPAS\",\"APL-1.0\",\"Adobe-Glyph\",\"APAFML\",\"Adobe-2006\",\"AGPL-1.0\",\"Afmparse\",\"Aladdin\",\"ADSL\",\"AMDPLPA\",\"ANTLR-PD\",\"Apache-1.0\",\"Apache-1.1\",\"Apache-2.0\",\"AML\",\"APSL-1.0\",\"APSL-1.1\",\"APSL-1.2\",\"APSL-2.0\",\"Artistic-1.0\",\"Artistic-1.0-Perl\",\"Artistic-1.0-cl8\",\"Artistic-2.0\",\"AAL\",\"Bahyph\",\"Barr\",\"Beerware\",\"BitTorrent-1.0\",\"BitTorrent-1.1\",\"BSL-1.0\",\"Borceux\",\"BSD-2-Clause\",\"BSD-2-Clause-FreeBSD\",\"BSD-2-Clause-NetBSD\",\"BSD-3-Clause\",\"BSD-3-Clause-Clear\",\"BSD-4-Clause\",\"BSD-Protection\",\"BSD-Source-Code\",\"BSD-3-Clause-Attribution\",\"0BSD\",\"BSD-4-Clause-UC\",\"bzip2-1.0.5\",\"bzip2-1.0.6\",\"Caldera\",\"CECILL-1.0\",\"CECILL-1.1\",\"CECILL-2.0\",\"CECILL-2.1\",\"CECILL-B\",\"CECILL-C\",\"ClArtistic\",\"MIT-CMU\",\"CNRI-Jython\",\"CNRI-Python\",\"CNRI-Python-GPL-Compatible\",\"CPOL-1.02\",\"CDDL-1.0\",\"CDDL-1.1\",\"CPAL-1.0\",\"CPL-1.0\",\"CATOSL-1.1\",\"Condor-1.1\",\"CC-BY-1.0\",\"CC-BY-2.0\",\"CC-BY-2.5\",\"CC-BY-3.0\",\"CC-BY-4.0\",\"CC-BY-ND-1.0\",\"CC-BY-ND-2.0\",\"CC-BY-ND-2.5\",\"CC-BY-ND-3.0\",\"CC-BY-ND-4.0\",\"CC-BY-NC-1.0\",\"CC-BY-NC-2.0\",\"CC-BY-NC-2.5\",\"CC-BY-NC-3.0\",\"CC-BY-NC-4.0\",\"CC-BY-NC-ND-1.0\",\"CC-BY-NC-ND-2.0\",\"CC-BY-NC-ND-2.5\",\"CC-BY-NC-ND-3.0\",\"CC-BY-NC-ND-4.0\",\"CC-BY-NC-SA-1.0\",\"CC-BY-NC-SA-2.0\",\"CC-BY-NC-SA-2.5\",\"CC-BY-NC-SA-3.0\",\"CC-BY-NC-SA-4.0\",\"CC-BY-SA-1.0\",\"CC-BY-SA-2.0\",\"CC-BY-SA-2.5\",\"CC-BY-SA-3.0\",\"CC-BY-SA-4.0\",\"CC0-1.0\",\"Crossword\",\"CrystalStacker\",\"CUA-OPL-1.0\",\"Cube\",\"curl\",\"D-FSL-1.0\",\"diffmark\",\"WTFPL\",\"DOC\",\"Dotseqn\",\"DSDP\",\"dvipdfm\",\"EPL-1.0\",\"ECL-1.0\",\"ECL-2.0\",\"eGenix\",\"EFL-1.0\",\"EFL-2.0\",\"MIT-advertising\",\"MIT-enna\",\"Entessa\",\"ErlPL-1.1\",\"EUDatagrid\",\"EUPL-1.0\",\"EUPL-1.1\",\"Eurosym\",\"Fair\",\"MIT-feh\",\"Frameworx-1.0\",\"FreeImage\",\"FTL\",\"FSFAP\",\"FSFUL\",\"FSFULLR\",\"Giftware\",\"GL2PS\",\"Glulxe\",\"AGPL-3.0\",\"GFDL-1.1\",\"GFDL-1.2\",\"GFDL-1.3\",\"GPL-1.0\",\"GPL-2.0\",\"GPL-3.0\",\"LGPL-2.1\",\"LGPL-3.0\",\"LGPL-2.0\",\"gnuplot\",\"gSOAP-1.3b\",\"HaskellReport\",\"HPND\",\"IBM-pibs\",\"IPL-1.0\",\"ICU\",\"ImageMagick\",\"iMatix\",\"Imlib2\",\"IJG\",\"Info-ZIP\",\"Intel-ACPI\",\"Intel\",\"Interbase-1.0\",\"IPA\",\"ISC\",\"JasPer-2.0\",\"JSON\",\"LPPL-1.0\",\"LPPL-1.1\",\"LPPL-1.2\",\"LPPL-1.3a\",\"LPPL-1.3c\",\"Latex2e\",\"BSD-3-Clause-LBNL\",\"Leptonica\",\"LGPLLR\",\"Libpng\",\"libtiff\",\"LAL-1.2\",\"LAL-1.3\",\"LiLiQ-P-1.1\",\"LiLiQ-Rplus-1.1\",\"LiLiQ-R-1.1\",\"LPL-1.02\",\"LPL-1.0\",\"MakeIndex\",\"MTLL\",\"MS-PL\",\"MS-RL\",\"MirOS\",\"MITNFA\",\"MIT\",\"Motosoto\",\"MPL-1.0\",\"MPL-1.1\",\"MPL-2.0\",\"MPL-2.0-no-copyleft-exception\",\"mpich2\",\"Multics\",\"Mup\",\"NASA-1.3\",\"Naumen\",\"NBPL-1.0\",\"NetCDF\",\"NGPL\",\"NOSL\",\"NPL-1.0\",\"NPL-1.1\",\"Newsletr\",\"NLPL\",\"Nokia\",\"NPOSL-3.0\",\"NLOD-1.0\",\"Noweb\",\"NRL\",\"NTP\",\"Nunit\",\"OCLC-2.0\",\"ODbL-1.0\",\"PDDL-1.0\",\"OCCT-PL\",\"OGTSL\",\"OLDAP-2.2.2\",\"OLDAP-1.1\",\"OLDAP-1.2\",\"OLDAP-1.3\",\"OLDAP-1.4\",\"OLDAP-2.0\",\"OLDAP-2.0.1\",\"OLDAP-2.1\",\"OLDAP-2.2\",\"OLDAP-2.2.1\",\"OLDAP-2.3\",\"OLDAP-2.4\",\"OLDAP-2.5\",\"OLDAP-2.6\",\"OLDAP-2.7\",\"OLDAP-2.8\",\"OML\",\"OPL-1.0\",\"OSL-1.0\",\"OSL-1.1\",\"OSL-2.0\",\"OSL-2.1\",\"OSL-3.0\",\"OpenSSL\",\"OSET-PL-2.1\",\"PHP-3.0\",\"PHP-3.01\",\"Plexus\",\"PostgreSQL\",\"psfrag\",\"psutils\",\"Python-2.0\",\"QPL-1.0\",\"Qhull\",\"Rdisc\",\"RPSL-1.0\",\"RPL-1.1\",\"RPL-1.5\",\"RHeCos-1.1\",\"RSCPL\",\"RSA-MD\",\"Ruby\",\"SAX-PD\",\"Saxpath\",\"SCEA\",\"SWL\",\"SMPPL\",\"Sendmail\",\"SGI-B-1.0\",\"SGI-B-1.1\",\"SGI-B-2.0\",\"OFL-1.0\",\"OFL-1.1\",\"SimPL-2.0\",\"Sleepycat\",\"SNIA\",\"Spencer-86\",\"Spencer-94\",\"Spencer-99\",\"SMLNJ\",\"SugarCRM-1.1.3\",\"SISSL\",\"SISSL-1.2\",\"SPL-1.0\",\"Watcom-1.0\",\"TCL\",\"Unlicense\",\"TMate\",\"TORQUE-1.1\",\"TOSL\",\"Unicode-TOU\",\"UPL-1.0\",\"NCSA\",\"Vim\",\"VOSTROM\",\"VSL-1.0\",\"W3C-19980720\",\"W3C\",\"Wsuipa\",\"Xnet\",\"X11\",\"Xerox\",\"XFree86-1.1\",\"xinetd\",\"xpp\",\"XSkat\",\"YPL-1.0\",\"YPL-1.1\",\"Zed\",\"Zend-2.0\",\"Zimbra-1.3\",\"Zimbra-1.4\",\"Zlib\",\"zlib-acknowledgement\",\"ZPL-1.1\",\"ZPL-2.0\",\"ZPL-2.1\",\"BSD-3-Clause-No-Nuclear-License\",\"BSD-3-Clause-No-Nuclear-Warranty\",\"BSD-3-Clause-No-Nuclear-License-2014\",\"eCos-2.0\",\"GPL-2.0-with-autoconf-exception\",\"GPL-2.0-with-bison-exception\",\"GPL-2.0-with-classpath-exception\",\"GPL-2.0-with-font-exception\",\"GPL-2.0-with-GCC-exception\",\"GPL-3.0-with-autoconf-exception\",\"GPL-3.0-with-GCC-exception\",\"StandardML-NJ\",\"WXwindows\"]"); /***/ }), -/* 527 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var url = __webpack_require__(452) -var gitHosts = __webpack_require__(528) -var GitHost = module.exports = __webpack_require__(529) +var gitHosts = __webpack_require__(530) +var GitHost = module.exports = __webpack_require__(531) var protocolToRepresentationMap = { 'git+ssh': 'sshurl', @@ -51559,7 +51771,7 @@ function parseGitUrl (giturl) { /***/ }), -/* 528 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51634,12 +51846,12 @@ Object.keys(gitHosts).forEach(function (name) { /***/ }), -/* 529 */ +/* 531 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var gitHosts = __webpack_require__(528) +var gitHosts = __webpack_require__(530) var extend = Object.assign || __webpack_require__(29)._extend var GitHost = module.exports = function (type, user, auth, project, committish, defaultRepresentation, opts) { @@ -51755,21 +51967,21 @@ GitHost.prototype.toString = function (opts) { /***/ }), -/* 530 */ +/* 532 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(531); -var async = __webpack_require__(533); +var core = __webpack_require__(533); +var async = __webpack_require__(535); async.core = core; async.isCore = function isCore(x) { return core[x]; }; -async.sync = __webpack_require__(538); +async.sync = __webpack_require__(540); exports = async; module.exports = async; /***/ }), -/* 531 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { var current = (process.versions && process.versions.node && process.versions.node.split('.')) || []; @@ -51816,7 +52028,7 @@ function versionIncluded(specifierValue) { return matchesRange(specifierValue); } -var data = __webpack_require__(532); +var data = __webpack_require__(534); var core = {}; for (var mod in data) { // eslint-disable-line no-restricted-syntax @@ -51828,21 +52040,21 @@ module.exports = core; /***/ }), -/* 532 */ +/* 534 */ /***/ (function(module) { module.exports = JSON.parse("{\"assert\":true,\"async_hooks\":\">= 8\",\"buffer_ieee754\":\"< 0.9.7\",\"buffer\":true,\"child_process\":true,\"cluster\":true,\"console\":true,\"constants\":true,\"crypto\":true,\"_debugger\":\"< 8\",\"dgram\":true,\"dns\":true,\"domain\":true,\"events\":true,\"freelist\":\"< 6\",\"fs\":true,\"fs/promises\":\">= 10 && < 10.1\",\"_http_agent\":\">= 0.11.1\",\"_http_client\":\">= 0.11.1\",\"_http_common\":\">= 0.11.1\",\"_http_incoming\":\">= 0.11.1\",\"_http_outgoing\":\">= 0.11.1\",\"_http_server\":\">= 0.11.1\",\"http\":true,\"http2\":\">= 8.8\",\"https\":true,\"inspector\":\">= 8.0.0\",\"_linklist\":\"< 8\",\"module\":true,\"net\":true,\"node-inspect/lib/_inspect\":\">= 7.6.0\",\"node-inspect/lib/internal/inspect_client\":\">= 7.6.0\",\"node-inspect/lib/internal/inspect_repl\":\">= 7.6.0\",\"os\":true,\"path\":true,\"perf_hooks\":\">= 8.5\",\"process\":\">= 1\",\"punycode\":true,\"querystring\":true,\"readline\":true,\"repl\":true,\"smalloc\":\">= 0.11.5 && < 3\",\"_stream_duplex\":\">= 0.9.4\",\"_stream_transform\":\">= 0.9.4\",\"_stream_wrap\":\">= 1.4.1\",\"_stream_passthrough\":\">= 0.9.4\",\"_stream_readable\":\">= 0.9.4\",\"_stream_writable\":\">= 0.9.4\",\"stream\":true,\"string_decoder\":true,\"sys\":true,\"timers\":true,\"_tls_common\":\">= 0.11.13\",\"_tls_legacy\":\">= 0.11.3 && < 10\",\"_tls_wrap\":\">= 0.11.3\",\"tls\":true,\"trace_events\":\">= 10\",\"tty\":true,\"url\":true,\"util\":true,\"v8/tools/arguments\":\">= 10\",\"v8/tools/codemap\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/consarray\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/csvparser\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/logreader\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/profile_view\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8/tools/splaytree\":[\">= 4.4.0 && < 5\",\">= 5.2.0\"],\"v8\":\">= 1\",\"vm\":true,\"worker_threads\":\">= 11.7\",\"zlib\":true}"); /***/ }), -/* 533 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(531); +var core = __webpack_require__(533); var fs = __webpack_require__(23); var path = __webpack_require__(16); -var caller = __webpack_require__(534); -var nodeModulesPaths = __webpack_require__(535); -var normalizeOptions = __webpack_require__(537); +var caller = __webpack_require__(536); +var nodeModulesPaths = __webpack_require__(537); +var normalizeOptions = __webpack_require__(539); var defaultIsFile = function isFile(file, cb) { fs.stat(file, function (err, stat) { @@ -52069,7 +52281,7 @@ module.exports = function resolve(x, options, callback) { /***/ }), -/* 534 */ +/* 536 */ /***/ (function(module, exports) { module.exports = function () { @@ -52083,11 +52295,11 @@ module.exports = function () { /***/ }), -/* 535 */ +/* 537 */ /***/ (function(module, exports, __webpack_require__) { var path = __webpack_require__(16); -var parse = path.parse || __webpack_require__(536); +var parse = path.parse || __webpack_require__(538); var getNodeModulesDirs = function getNodeModulesDirs(absoluteStart, modules) { var prefix = '/'; @@ -52131,7 +52343,7 @@ module.exports = function nodeModulesPaths(start, opts, request) { /***/ }), -/* 536 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52231,7 +52443,7 @@ module.exports.win32 = win32.parse; /***/ }), -/* 537 */ +/* 539 */ /***/ (function(module, exports) { module.exports = function (x, opts) { @@ -52247,15 +52459,15 @@ module.exports = function (x, opts) { /***/ }), -/* 538 */ +/* 540 */ /***/ (function(module, exports, __webpack_require__) { -var core = __webpack_require__(531); +var core = __webpack_require__(533); var fs = __webpack_require__(23); var path = __webpack_require__(16); -var caller = __webpack_require__(534); -var nodeModulesPaths = __webpack_require__(535); -var normalizeOptions = __webpack_require__(537); +var caller = __webpack_require__(536); +var nodeModulesPaths = __webpack_require__(537); +var normalizeOptions = __webpack_require__(539); var defaultIsFile = function isFile(file) { try { @@ -52407,7 +52619,7 @@ module.exports = function (x, options) { /***/ }), -/* 539 */ +/* 541 */ /***/ (function(module, exports) { module.exports = extractDescription @@ -52427,17 +52639,17 @@ function extractDescription (d) { /***/ }), -/* 540 */ +/* 542 */ /***/ (function(module) { module.exports = JSON.parse("{\"topLevel\":{\"dependancies\":\"dependencies\",\"dependecies\":\"dependencies\",\"depdenencies\":\"dependencies\",\"devEependencies\":\"devDependencies\",\"depends\":\"dependencies\",\"dev-dependencies\":\"devDependencies\",\"devDependences\":\"devDependencies\",\"devDepenencies\":\"devDependencies\",\"devdependencies\":\"devDependencies\",\"repostitory\":\"repository\",\"repo\":\"repository\",\"prefereGlobal\":\"preferGlobal\",\"hompage\":\"homepage\",\"hampage\":\"homepage\",\"autohr\":\"author\",\"autor\":\"author\",\"contributers\":\"contributors\",\"publicationConfig\":\"publishConfig\",\"script\":\"scripts\"},\"bugs\":{\"web\":\"url\",\"name\":\"url\"},\"script\":{\"server\":\"start\",\"tests\":\"test\"}}"); /***/ }), -/* 541 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { var util = __webpack_require__(29) -var messages = __webpack_require__(542) +var messages = __webpack_require__(544) module.exports = function() { var args = Array.prototype.slice.call(arguments, 0) @@ -52462,20 +52674,20 @@ function makeTypoWarning (providedName, probableName, field) { /***/ }), -/* 542 */ +/* 544 */ /***/ (function(module) { module.exports = JSON.parse("{\"repositories\":\"'repositories' (plural) Not supported. Please pick one as the 'repository' field\",\"missingRepository\":\"No repository field.\",\"brokenGitUrl\":\"Probably broken git url: %s\",\"nonObjectScripts\":\"scripts must be an object\",\"nonStringScript\":\"script values must be string commands\",\"nonArrayFiles\":\"Invalid 'files' member\",\"invalidFilename\":\"Invalid filename in 'files' list: %s\",\"nonArrayBundleDependencies\":\"Invalid 'bundleDependencies' list. Must be array of package names\",\"nonStringBundleDependency\":\"Invalid bundleDependencies member: %s\",\"nonDependencyBundleDependency\":\"Non-dependency in bundleDependencies: %s\",\"nonObjectDependencies\":\"%s field must be an object\",\"nonStringDependency\":\"Invalid dependency: %s %s\",\"deprecatedArrayDependencies\":\"specifying %s as array is deprecated\",\"deprecatedModules\":\"modules field is deprecated\",\"nonArrayKeywords\":\"keywords should be an array of strings\",\"nonStringKeyword\":\"keywords should be an array of strings\",\"conflictingName\":\"%s is also the name of a node core module.\",\"nonStringDescription\":\"'description' field should be a string\",\"missingDescription\":\"No description\",\"missingReadme\":\"No README data\",\"missingLicense\":\"No license field.\",\"nonEmailUrlBugsString\":\"Bug string field must be url, email, or {email,url}\",\"nonUrlBugsUrlField\":\"bugs.url field must be a string url. Deleted.\",\"nonEmailBugsEmailField\":\"bugs.email field must be a string email. Deleted.\",\"emptyNormalizedBugs\":\"Normalized value of bugs field is an empty object. Deleted.\",\"nonUrlHomepage\":\"homepage field must be a string url. Deleted.\",\"invalidLicense\":\"license should be a valid SPDX license expression\",\"typo\":\"%s should probably be %s.\"}"); /***/ }), -/* 543 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const writeJsonFile = __webpack_require__(544); -const sortKeys = __webpack_require__(556); +const writeJsonFile = __webpack_require__(546); +const sortKeys = __webpack_require__(558); const dependencyKeys = new Set([ 'dependencies', @@ -52540,18 +52752,18 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 544 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const fs = __webpack_require__(545); -const writeFileAtomic = __webpack_require__(549); -const sortKeys = __webpack_require__(556); -const makeDir = __webpack_require__(558); -const pify = __webpack_require__(559); -const detectIndent = __webpack_require__(560); +const fs = __webpack_require__(547); +const writeFileAtomic = __webpack_require__(551); +const sortKeys = __webpack_require__(558); +const makeDir = __webpack_require__(560); +const pify = __webpack_require__(561); +const detectIndent = __webpack_require__(562); const init = (fn, filePath, data, options) => { if (!filePath) { @@ -52623,13 +52835,13 @@ module.exports.sync = (filePath, data, options) => { /***/ }), -/* 545 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(23) -var polyfills = __webpack_require__(546) -var legacy = __webpack_require__(547) -var clone = __webpack_require__(548) +var polyfills = __webpack_require__(548) +var legacy = __webpack_require__(549) +var clone = __webpack_require__(550) var queue = [] @@ -52908,7 +53120,7 @@ function retry () { /***/ }), -/* 546 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { var constants = __webpack_require__(25) @@ -53243,7 +53455,7 @@ function patch (fs) { /***/ }), -/* 547 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27).Stream @@ -53367,7 +53579,7 @@ function legacy (fs) { /***/ }), -/* 548 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53393,7 +53605,7 @@ function clone (obj) { /***/ }), -/* 549 */ +/* 551 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -53403,8 +53615,8 @@ module.exports.sync = writeFileSync module.exports._getTmpname = getTmpname // for testing module.exports._cleanupOnExit = cleanupOnExit -var fs = __webpack_require__(550) -var MurmurHash3 = __webpack_require__(554) +var fs = __webpack_require__(552) +var MurmurHash3 = __webpack_require__(556) var onExit = __webpack_require__(377) var path = __webpack_require__(16) var activeFiles = {} @@ -53413,7 +53625,7 @@ var activeFiles = {} /* istanbul ignore next */ var threadId = (function getId () { try { - var workerThreads = __webpack_require__(555) + var workerThreads = __webpack_require__(557) /// if we are in main thread, this is set to `0` return workerThreads.threadId @@ -53638,12 +53850,12 @@ function writeFileSync (filename, data, options) { /***/ }), -/* 550 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { var fs = __webpack_require__(23) -var polyfills = __webpack_require__(551) -var legacy = __webpack_require__(553) +var polyfills = __webpack_require__(553) +var legacy = __webpack_require__(555) var queue = [] var util = __webpack_require__(29) @@ -53667,7 +53879,7 @@ if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || '')) { }) } -module.exports = patch(__webpack_require__(552)) +module.exports = patch(__webpack_require__(554)) if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH) { module.exports = patch(fs) } @@ -53906,10 +54118,10 @@ function retry () { /***/ }), -/* 551 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { -var fs = __webpack_require__(552) +var fs = __webpack_require__(554) var constants = __webpack_require__(25) var origCwd = process.cwd @@ -54242,7 +54454,7 @@ function chownErOk (er) { /***/ }), -/* 552 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54270,7 +54482,7 @@ function clone (obj) { /***/ }), -/* 553 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27).Stream @@ -54394,7 +54606,7 @@ function legacy (fs) { /***/ }), -/* 554 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -54536,18 +54748,18 @@ function legacy (fs) { /***/ }), -/* 555 */ +/* 557 */ /***/ (function(module, exports) { module.exports = require(undefined); /***/ }), -/* 556 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isPlainObj = __webpack_require__(557); +const isPlainObj = __webpack_require__(559); module.exports = (obj, opts) => { if (!isPlainObj(obj)) { @@ -54604,7 +54816,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 557 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54618,15 +54830,15 @@ module.exports = function (x) { /***/ }), -/* 558 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const pify = __webpack_require__(559); -const semver = __webpack_require__(521); +const pify = __webpack_require__(561); +const semver = __webpack_require__(523); const defaults = { mode: 0o777 & (~process.umask()), @@ -54764,7 +54976,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 559 */ +/* 561 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54839,7 +55051,7 @@ module.exports = (input, options) => { /***/ }), -/* 560 */ +/* 562 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -54968,7 +55180,7 @@ module.exports = str => { /***/ }), -/* 561 */ +/* 563 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54977,7 +55189,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackage", function() { return runScriptInPackage; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runScriptInPackageStreaming", function() { return runScriptInPackageStreaming; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "yarnWorkspacesInfo", function() { return yarnWorkspacesInfo; }); -/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(562); +/* harmony import */ var _child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(564); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -55047,7 +55259,7 @@ async function yarnWorkspacesInfo(directory) { } /***/ }), -/* 562 */ +/* 564 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55058,9 +55270,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(563); +/* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(565); /* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(log_symbols__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(568); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(570); /* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -55126,12 +55338,12 @@ function spawnStreaming(command, args, opts, { } /***/ }), -/* 563 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(564); +const chalk = __webpack_require__(566); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -55153,16 +55365,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 564 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(565); -const stdoutColor = __webpack_require__(566).stdout; +const ansiStyles = __webpack_require__(567); +const stdoutColor = __webpack_require__(568).stdout; -const template = __webpack_require__(567); +const template = __webpack_require__(569); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -55388,7 +55600,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 565 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55561,7 +55773,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 566 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55703,7 +55915,7 @@ module.exports = { /***/ }), -/* 567 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55838,7 +56050,7 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 568 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -55846,12 +56058,12 @@ module.exports = (chalk, tmp) => { // This file is licensed under the Apache License 2.0. // License text available at https://opensource.org/licenses/Apache-2.0 -module.exports = __webpack_require__(569); -module.exports.cli = __webpack_require__(573); +module.exports = __webpack_require__(571); +module.exports.cli = __webpack_require__(575); /***/ }), -/* 569 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -55866,9 +56078,9 @@ var stream = __webpack_require__(27); var util = __webpack_require__(29); var fs = __webpack_require__(23); -var through = __webpack_require__(570); -var duplexer = __webpack_require__(571); -var StringDecoder = __webpack_require__(572).StringDecoder; +var through = __webpack_require__(572); +var duplexer = __webpack_require__(573); +var StringDecoder = __webpack_require__(574).StringDecoder; module.exports = Logger; @@ -56057,7 +56269,7 @@ function lineMerger(host) { /***/ }), -/* 570 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56171,7 +56383,7 @@ function through (write, end, opts) { /***/ }), -/* 571 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(27) @@ -56264,13 +56476,13 @@ function duplex(writer, reader) { /***/ }), -/* 572 */ +/* 574 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 573 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -56281,11 +56493,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(574); +var minimist = __webpack_require__(576); var path = __webpack_require__(16); -var Logger = __webpack_require__(569); -var pkg = __webpack_require__(575); +var Logger = __webpack_require__(571); +var pkg = __webpack_require__(577); module.exports = cli; @@ -56339,7 +56551,7 @@ function usage($0, p) { /***/ }), -/* 574 */ +/* 576 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -56581,29 +56793,29 @@ function isNumber (x) { /***/ }), -/* 575 */ +/* 577 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"strong-log-transformer\",\"version\":\"2.1.0\",\"description\":\"Stream transformer that prefixes lines with timestamps and other things.\",\"author\":\"Ryan Graham \",\"license\":\"Apache-2.0\",\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/strongloop/strong-log-transformer\"},\"keywords\":[\"logging\",\"streams\"],\"bugs\":{\"url\":\"https://github.com/strongloop/strong-log-transformer/issues\"},\"homepage\":\"https://github.com/strongloop/strong-log-transformer\",\"directories\":{\"test\":\"test\"},\"bin\":{\"sl-log-transformer\":\"bin/sl-log-transformer.js\"},\"main\":\"index.js\",\"scripts\":{\"test\":\"tap --100 test/test-*\"},\"dependencies\":{\"duplexer\":\"^0.1.1\",\"minimist\":\"^1.2.0\",\"through\":\"^2.3.4\"},\"devDependencies\":{\"tap\":\"^12.0.1\"},\"engines\":{\"node\":\">=4\"}}"); /***/ }), -/* 576 */ +/* 578 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "workspacePackagePaths", function() { return workspacePackagePaths; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "copyWorkspacePackages", function() { return copyWorkspacePackages; }); -/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(501); +/* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(503); /* harmony import */ var glob__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(glob__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(577); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(579); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); -/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(516); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(500); +/* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(518); +/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(502); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -56695,7 +56907,7 @@ function packagesFromGlobPattern({ } /***/ }), -/* 577 */ +/* 579 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56765,7 +56977,7 @@ function getProjectPaths({ } /***/ }), -/* 578 */ +/* 580 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56773,13 +56985,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAllChecksums", function() { return getAllChecksums; }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(23); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(579); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(581); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(351); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(580); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(582); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -56813,7 +57025,7 @@ async function getChangesForProjects(projects, kbn, log) { log.verbose('getting changed files'); const { stdout - } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmt', '--', ...Array.from(projects.values()).filter(p => kbn.isPartOfRepo(p)).map(p => p.path)], { + } = await execa__WEBPACK_IMPORTED_MODULE_3___default()('git', ['ls-files', '-dmto', '--exclude-standard', '--', ...Array.from(projects.values()).filter(p => kbn.isPartOfRepo(p)).map(p => p.path)], { cwd: kbn.getAbsolute() }); const output = stdout.trim(); @@ -56840,10 +57052,13 @@ async function getChangesForProjects(projects, kbn, log) { unassignedChanges.set(path, 'deleted'); break; + case '?': + unassignedChanges.set(path, 'untracked'); + break; + case 'H': case 'S': case 'K': - case '?': default: log.warning(`unexpected modification status "${tag}" for ${path}, please report this!`); unassignedChanges.set(path, 'invalid'); @@ -57005,19 +57220,19 @@ async function getAllChecksums(kbn, log) { } /***/ }), -/* 579 */ +/* 581 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 580 */ +/* 582 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readYarnLock", function() { return readYarnLock; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(581); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(583); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(20); /* @@ -57061,7 +57276,7 @@ async function readYarnLock(kbn) { } /***/ }), -/* 581 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -58620,7 +58835,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(579); +module.exports = __webpack_require__(581); /***/ }), /* 10 */, @@ -60944,7 +61159,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(582); +module.exports = __webpack_require__(584); /***/ }), /* 64 */, @@ -61882,7 +62097,7 @@ module.exports.win32 = win32; /* 79 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(478); +module.exports = __webpack_require__(484); /***/ }), /* 80 */, @@ -67339,13 +67554,13 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 582 */ +/* 584 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 583 */ +/* 585 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67442,7 +67657,7 @@ class BootstrapCacheFile { } /***/ }), -/* 584 */ +/* 586 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -67450,9 +67665,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(587); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(673); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(675); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__); @@ -67551,21 +67766,21 @@ const CleanCommand = { }; /***/ }), -/* 585 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const path = __webpack_require__(16); -const globby = __webpack_require__(586); -const isGlob = __webpack_require__(603); -const slash = __webpack_require__(664); +const globby = __webpack_require__(588); +const isGlob = __webpack_require__(605); +const slash = __webpack_require__(666); const gracefulFs = __webpack_require__(22); -const isPathCwd = __webpack_require__(666); -const isPathInside = __webpack_require__(667); -const rimraf = __webpack_require__(668); -const pMap = __webpack_require__(669); +const isPathCwd = __webpack_require__(668); +const isPathInside = __webpack_require__(669); +const rimraf = __webpack_require__(670); +const pMap = __webpack_require__(671); const rimrafP = promisify(rimraf); @@ -67679,19 +67894,19 @@ module.exports.sync = (patterns, {force, dryRun, cwd = process.cwd(), ...options /***/ }), -/* 586 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(587); -const merge2 = __webpack_require__(588); -const glob = __webpack_require__(589); -const fastGlob = __webpack_require__(594); -const dirGlob = __webpack_require__(660); -const gitignore = __webpack_require__(662); -const {FilterStream, UniqueStream} = __webpack_require__(665); +const arrayUnion = __webpack_require__(589); +const merge2 = __webpack_require__(590); +const glob = __webpack_require__(591); +const fastGlob = __webpack_require__(596); +const dirGlob = __webpack_require__(662); +const gitignore = __webpack_require__(664); +const {FilterStream, UniqueStream} = __webpack_require__(667); const DEFAULT_FILTER = () => false; @@ -67864,7 +68079,7 @@ module.exports.gitignore = gitignore; /***/ }), -/* 587 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67876,7 +68091,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 588 */ +/* 590 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67990,7 +68205,7 @@ function pauseStreams (streams, options) { /***/ }), -/* 589 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -68036,21 +68251,21 @@ function pauseStreams (streams, options) { module.exports = glob var fs = __webpack_require__(23) -var rp = __webpack_require__(502) -var minimatch = __webpack_require__(504) +var rp = __webpack_require__(504) +var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(590) +var inherits = __webpack_require__(592) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(510) -var globSync = __webpack_require__(592) -var common = __webpack_require__(593) +var isAbsolute = __webpack_require__(512) +var globSync = __webpack_require__(594) +var common = __webpack_require__(595) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp -var inflight = __webpack_require__(513) +var inflight = __webpack_require__(515) var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored @@ -68786,7 +69001,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 590 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -68796,12 +69011,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(591); + module.exports = __webpack_require__(593); } /***/ }), -/* 591 */ +/* 593 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -68834,22 +69049,22 @@ if (typeof Object.create === 'function') { /***/ }), -/* 592 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync globSync.GlobSync = GlobSync var fs = __webpack_require__(23) -var rp = __webpack_require__(502) -var minimatch = __webpack_require__(504) +var rp = __webpack_require__(504) +var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(589).Glob +var Glob = __webpack_require__(591).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(510) -var common = __webpack_require__(593) +var isAbsolute = __webpack_require__(512) +var common = __webpack_require__(595) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -69326,7 +69541,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 593 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -69344,8 +69559,8 @@ function ownProp (obj, field) { } var path = __webpack_require__(16) -var minimatch = __webpack_require__(504) -var isAbsolute = __webpack_require__(510) +var minimatch = __webpack_require__(506) +var isAbsolute = __webpack_require__(512) var Minimatch = minimatch.Minimatch function alphasorti (a, b) { @@ -69572,17 +69787,17 @@ function childrenIgnored (self, path) { /***/ }), -/* 594 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(595); -const async_1 = __webpack_require__(623); -const stream_1 = __webpack_require__(656); -const sync_1 = __webpack_require__(657); -const settings_1 = __webpack_require__(659); -const utils = __webpack_require__(596); +const taskManager = __webpack_require__(597); +const async_1 = __webpack_require__(625); +const stream_1 = __webpack_require__(658); +const sync_1 = __webpack_require__(659); +const settings_1 = __webpack_require__(661); +const utils = __webpack_require__(598); function FastGlob(source, options) { try { assertPatternsInput(source); @@ -69640,13 +69855,13 @@ module.exports = FastGlob; /***/ }), -/* 595 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(596); +const utils = __webpack_require__(598); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -69714,28 +69929,28 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 596 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const array = __webpack_require__(597); +const array = __webpack_require__(599); exports.array = array; -const errno = __webpack_require__(598); +const errno = __webpack_require__(600); exports.errno = errno; -const fs = __webpack_require__(599); +const fs = __webpack_require__(601); exports.fs = fs; -const path = __webpack_require__(600); +const path = __webpack_require__(602); exports.path = path; -const pattern = __webpack_require__(601); +const pattern = __webpack_require__(603); exports.pattern = pattern; -const stream = __webpack_require__(622); +const stream = __webpack_require__(624); exports.stream = stream; /***/ }), -/* 597 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69748,7 +69963,7 @@ exports.flatten = flatten; /***/ }), -/* 598 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69761,7 +69976,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 599 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69786,7 +70001,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 600 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69807,16 +70022,16 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 601 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const globParent = __webpack_require__(602); -const isGlob = __webpack_require__(603); -const micromatch = __webpack_require__(605); +const globParent = __webpack_require__(604); +const isGlob = __webpack_require__(605); +const micromatch = __webpack_require__(607); const GLOBSTAR = '**'; function isStaticPattern(pattern) { return !isDynamicPattern(pattern); @@ -69905,13 +70120,13 @@ exports.matchAny = matchAny; /***/ }), -/* 602 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isGlob = __webpack_require__(603); +var isGlob = __webpack_require__(605); var pathPosixDirname = __webpack_require__(16).posix.dirname; var isWin32 = __webpack_require__(11).platform() === 'win32'; @@ -69946,7 +70161,7 @@ module.exports = function globParent(str) { /***/ }), -/* 603 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -69956,7 +70171,7 @@ module.exports = function globParent(str) { * Released under the MIT License. */ -var isExtglob = __webpack_require__(604); +var isExtglob = __webpack_require__(606); var chars = { '{': '}', '(': ')', '[': ']'}; var strictRegex = /\\(.)|(^!|\*|[\].+)]\?|\[[^\\\]]+\]|\{[^\\}]+\}|\(\?[:!=][^\\)]+\)|\([^|]+\|[^\\)]+\))/; var relaxedRegex = /\\(.)|(^!|[*?{}()[\]]|\(\?)/; @@ -70000,7 +70215,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 604 */ +/* 606 */ /***/ (function(module, exports) { /*! @@ -70026,16 +70241,16 @@ module.exports = function isExtglob(str) { /***/ }), -/* 605 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(29); -const braces = __webpack_require__(606); -const picomatch = __webpack_require__(616); -const utils = __webpack_require__(619); +const braces = __webpack_require__(608); +const picomatch = __webpack_require__(618); +const utils = __webpack_require__(621); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); /** @@ -70500,16 +70715,16 @@ module.exports = micromatch; /***/ }), -/* 606 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(607); -const compile = __webpack_require__(609); -const expand = __webpack_require__(613); -const parse = __webpack_require__(614); +const stringify = __webpack_require__(609); +const compile = __webpack_require__(611); +const expand = __webpack_require__(615); +const parse = __webpack_require__(616); /** * Expand the given pattern or create a regex-compatible string. @@ -70677,13 +70892,13 @@ module.exports = braces; /***/ }), -/* 607 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(608); +const utils = __webpack_require__(610); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -70716,7 +70931,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 608 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70835,14 +71050,14 @@ exports.flatten = (...args) => { /***/ }), -/* 609 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(610); -const utils = __webpack_require__(608); +const fill = __webpack_require__(612); +const utils = __webpack_require__(610); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -70899,7 +71114,7 @@ module.exports = compile; /***/ }), -/* 610 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70913,7 +71128,7 @@ module.exports = compile; const util = __webpack_require__(29); -const toRegexRange = __webpack_require__(611); +const toRegexRange = __webpack_require__(613); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -71155,7 +71370,7 @@ module.exports = fill; /***/ }), -/* 611 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71168,7 +71383,7 @@ module.exports = fill; -const isNumber = __webpack_require__(612); +const isNumber = __webpack_require__(614); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -71450,7 +71665,7 @@ module.exports = toRegexRange; /***/ }), -/* 612 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71475,15 +71690,15 @@ module.exports = function(num) { /***/ }), -/* 613 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(610); -const stringify = __webpack_require__(607); -const utils = __webpack_require__(608); +const fill = __webpack_require__(612); +const stringify = __webpack_require__(609); +const utils = __webpack_require__(610); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -71595,13 +71810,13 @@ module.exports = expand; /***/ }), -/* 614 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(607); +const stringify = __webpack_require__(609); /** * Constants @@ -71623,7 +71838,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(615); +} = __webpack_require__(617); /** * parse @@ -71935,7 +72150,7 @@ module.exports = parse; /***/ }), -/* 615 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71999,26 +72214,26 @@ module.exports = { /***/ }), -/* 616 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(617); +module.exports = __webpack_require__(619); /***/ }), -/* 617 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const scan = __webpack_require__(618); -const parse = __webpack_require__(621); -const utils = __webpack_require__(619); +const scan = __webpack_require__(620); +const parse = __webpack_require__(623); +const utils = __webpack_require__(621); /** * Creates a matcher function from one or more glob patterns. The @@ -72321,7 +72536,7 @@ picomatch.toRegex = (source, options) => { * @return {Object} */ -picomatch.constants = __webpack_require__(620); +picomatch.constants = __webpack_require__(622); /** * Expose "picomatch" @@ -72331,13 +72546,13 @@ module.exports = picomatch; /***/ }), -/* 618 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(619); +const utils = __webpack_require__(621); const { CHAR_ASTERISK, /* * */ @@ -72355,7 +72570,7 @@ const { CHAR_RIGHT_CURLY_BRACE, /* } */ CHAR_RIGHT_PARENTHESES, /* ) */ CHAR_RIGHT_SQUARE_BRACKET /* ] */ -} = __webpack_require__(620); +} = __webpack_require__(622); const isPathSeparator = code => { return code === CHAR_FORWARD_SLASH || code === CHAR_BACKWARD_SLASH; @@ -72557,7 +72772,7 @@ module.exports = (input, options) => { /***/ }), -/* 619 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72569,7 +72784,7 @@ const { REGEX_SPECIAL_CHARS, REGEX_SPECIAL_CHARS_GLOBAL, REGEX_REMOVE_BACKSLASH -} = __webpack_require__(620); +} = __webpack_require__(622); exports.isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); exports.hasRegexChars = str => REGEX_SPECIAL_CHARS.test(str); @@ -72607,7 +72822,7 @@ exports.escapeLast = (input, char, lastIdx) => { /***/ }), -/* 620 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72793,14 +73008,14 @@ module.exports = { /***/ }), -/* 621 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(619); -const constants = __webpack_require__(620); +const utils = __webpack_require__(621); +const constants = __webpack_require__(622); /** * Constants @@ -73811,13 +74026,13 @@ module.exports = parse; /***/ }), -/* 622 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const merge2 = __webpack_require__(588); +const merge2 = __webpack_require__(590); function merge(streams) { const mergedStream = merge2(streams); streams.forEach((stream) => { @@ -73829,14 +74044,14 @@ exports.merge = merge; /***/ }), -/* 623 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(624); -const provider_1 = __webpack_require__(651); +const stream_1 = __webpack_require__(626); +const provider_1 = __webpack_require__(653); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -73864,16 +74079,16 @@ exports.default = ProviderAsync; /***/ }), -/* 624 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const fsStat = __webpack_require__(625); -const fsWalk = __webpack_require__(630); -const reader_1 = __webpack_require__(650); +const fsStat = __webpack_require__(627); +const fsWalk = __webpack_require__(632); +const reader_1 = __webpack_require__(652); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -73926,15 +74141,15 @@ exports.default = ReaderStream; /***/ }), -/* 625 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(626); -const sync = __webpack_require__(627); -const settings_1 = __webpack_require__(628); +const async = __webpack_require__(628); +const sync = __webpack_require__(629); +const settings_1 = __webpack_require__(630); exports.Settings = settings_1.default; function stat(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -73957,7 +74172,7 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 626 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -73995,7 +74210,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 627 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74024,13 +74239,13 @@ exports.read = read; /***/ }), -/* 628 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(629); +const fs = __webpack_require__(631); class Settings { constructor(_options = {}) { this._options = _options; @@ -74047,7 +74262,7 @@ exports.default = Settings; /***/ }), -/* 629 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74070,16 +74285,16 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 630 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(631); -const stream_1 = __webpack_require__(646); -const sync_1 = __webpack_require__(647); -const settings_1 = __webpack_require__(649); +const async_1 = __webpack_require__(633); +const stream_1 = __webpack_require__(648); +const sync_1 = __webpack_require__(649); +const settings_1 = __webpack_require__(651); exports.Settings = settings_1.default; function walk(dir, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74109,13 +74324,13 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 631 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async_1 = __webpack_require__(632); +const async_1 = __webpack_require__(634); class AsyncProvider { constructor(_root, _settings) { this._root = _root; @@ -74146,17 +74361,17 @@ function callSuccessCallback(callback, entries) { /***/ }), -/* 632 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __webpack_require__(379); -const fsScandir = __webpack_require__(633); -const fastq = __webpack_require__(642); -const common = __webpack_require__(644); -const reader_1 = __webpack_require__(645); +const fsScandir = __webpack_require__(635); +const fastq = __webpack_require__(644); +const common = __webpack_require__(646); +const reader_1 = __webpack_require__(647); class AsyncReader extends reader_1.default { constructor(_root, _settings) { super(_root, _settings); @@ -74246,15 +74461,15 @@ exports.default = AsyncReader; /***/ }), -/* 633 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const async = __webpack_require__(634); -const sync = __webpack_require__(639); -const settings_1 = __webpack_require__(640); +const async = __webpack_require__(636); +const sync = __webpack_require__(641); +const settings_1 = __webpack_require__(642); exports.Settings = settings_1.default; function scandir(path, optionsOrSettingsOrCallback, callback) { if (typeof optionsOrSettingsOrCallback === 'function') { @@ -74277,16 +74492,16 @@ function getSettings(settingsOrOptions = {}) { /***/ }), -/* 634 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(625); -const rpl = __webpack_require__(635); -const constants_1 = __webpack_require__(636); -const utils = __webpack_require__(637); +const fsStat = __webpack_require__(627); +const rpl = __webpack_require__(637); +const constants_1 = __webpack_require__(638); +const utils = __webpack_require__(639); function read(dir, settings, callback) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings, callback); @@ -74375,7 +74590,7 @@ function callSuccessCallback(callback, result) { /***/ }), -/* 635 */ +/* 637 */ /***/ (function(module, exports) { module.exports = runParallel @@ -74429,7 +74644,7 @@ function runParallel (tasks, cb) { /***/ }), -/* 636 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74445,18 +74660,18 @@ exports.IS_SUPPORT_READDIR_WITH_FILE_TYPES = MAJOR_VERSION > 10 || (MAJOR_VERSIO /***/ }), -/* 637 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __webpack_require__(638); +const fs = __webpack_require__(640); exports.fs = fs; /***/ }), -/* 638 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74481,15 +74696,15 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 639 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(625); -const constants_1 = __webpack_require__(636); -const utils = __webpack_require__(637); +const fsStat = __webpack_require__(627); +const constants_1 = __webpack_require__(638); +const utils = __webpack_require__(639); function read(dir, settings) { if (!settings.stats && constants_1.IS_SUPPORT_READDIR_WITH_FILE_TYPES) { return readdirWithFileTypes(dir, settings); @@ -74540,15 +74755,15 @@ exports.readdir = readdir; /***/ }), -/* 640 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(625); -const fs = __webpack_require__(641); +const fsStat = __webpack_require__(627); +const fs = __webpack_require__(643); class Settings { constructor(_options = {}) { this._options = _options; @@ -74571,7 +74786,7 @@ exports.default = Settings; /***/ }), -/* 641 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74596,13 +74811,13 @@ exports.createFileSystemAdapter = createFileSystemAdapter; /***/ }), -/* 642 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var reusify = __webpack_require__(643) +var reusify = __webpack_require__(645) function fastqueue (context, worker, concurrency) { if (typeof context === 'function') { @@ -74776,7 +74991,7 @@ module.exports = fastqueue /***/ }), -/* 643 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74816,7 +75031,7 @@ module.exports = reusify /***/ }), -/* 644 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -74847,13 +75062,13 @@ exports.joinPathSegments = joinPathSegments; /***/ }), -/* 645 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const common = __webpack_require__(644); +const common = __webpack_require__(646); class Reader { constructor(_root, _settings) { this._root = _root; @@ -74865,14 +75080,14 @@ exports.default = Reader; /***/ }), -/* 646 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const async_1 = __webpack_require__(632); +const async_1 = __webpack_require__(634); class StreamProvider { constructor(_root, _settings) { this._root = _root; @@ -74902,13 +75117,13 @@ exports.default = StreamProvider; /***/ }), -/* 647 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(648); +const sync_1 = __webpack_require__(650); class SyncProvider { constructor(_root, _settings) { this._root = _root; @@ -74923,15 +75138,15 @@ exports.default = SyncProvider; /***/ }), -/* 648 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsScandir = __webpack_require__(633); -const common = __webpack_require__(644); -const reader_1 = __webpack_require__(645); +const fsScandir = __webpack_require__(635); +const common = __webpack_require__(646); +const reader_1 = __webpack_require__(647); class SyncReader extends reader_1.default { constructor() { super(...arguments); @@ -74989,14 +75204,14 @@ exports.default = SyncReader; /***/ }), -/* 649 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsScandir = __webpack_require__(633); +const fsScandir = __webpack_require__(635); class Settings { constructor(_options = {}) { this._options = _options; @@ -75022,15 +75237,15 @@ exports.default = Settings; /***/ }), -/* 650 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const fsStat = __webpack_require__(625); -const utils = __webpack_require__(596); +const fsStat = __webpack_require__(627); +const utils = __webpack_require__(598); class Reader { constructor(_settings) { this._settings = _settings; @@ -75062,17 +75277,17 @@ exports.default = Reader; /***/ }), -/* 651 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(16); -const deep_1 = __webpack_require__(652); -const entry_1 = __webpack_require__(653); -const error_1 = __webpack_require__(654); -const entry_2 = __webpack_require__(655); +const deep_1 = __webpack_require__(654); +const entry_1 = __webpack_require__(655); +const error_1 = __webpack_require__(656); +const entry_2 = __webpack_require__(657); class Provider { constructor(_settings) { this._settings = _settings; @@ -75117,13 +75332,13 @@ exports.default = Provider; /***/ }), -/* 652 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(596); +const utils = __webpack_require__(598); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75183,13 +75398,13 @@ exports.default = DeepFilter; /***/ }), -/* 653 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(596); +const utils = __webpack_require__(598); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -75244,13 +75459,13 @@ exports.default = EntryFilter; /***/ }), -/* 654 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(596); +const utils = __webpack_require__(598); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -75266,13 +75481,13 @@ exports.default = ErrorFilter; /***/ }), -/* 655 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(596); +const utils = __webpack_require__(598); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -75299,15 +75514,15 @@ exports.default = EntryTransformer; /***/ }), -/* 656 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(27); -const stream_2 = __webpack_require__(624); -const provider_1 = __webpack_require__(651); +const stream_2 = __webpack_require__(626); +const provider_1 = __webpack_require__(653); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -75335,14 +75550,14 @@ exports.default = ProviderStream; /***/ }), -/* 657 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(658); -const provider_1 = __webpack_require__(651); +const sync_1 = __webpack_require__(660); +const provider_1 = __webpack_require__(653); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -75365,15 +75580,15 @@ exports.default = ProviderSync; /***/ }), -/* 658 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsStat = __webpack_require__(625); -const fsWalk = __webpack_require__(630); -const reader_1 = __webpack_require__(650); +const fsStat = __webpack_require__(627); +const fsWalk = __webpack_require__(632); +const reader_1 = __webpack_require__(652); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -75415,7 +75630,7 @@ exports.default = ReaderSync; /***/ }), -/* 659 */ +/* 661 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75475,13 +75690,13 @@ exports.default = Settings; /***/ }), -/* 660 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(661); +const pathType = __webpack_require__(663); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -75557,7 +75772,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 661 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75607,7 +75822,7 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 662 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75615,9 +75830,9 @@ exports.isSymlinkSync = isTypeSync.bind(null, 'lstatSync', 'isSymbolicLink'); const {promisify} = __webpack_require__(29); const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(594); -const gitIgnore = __webpack_require__(663); -const slash = __webpack_require__(664); +const fastGlob = __webpack_require__(596); +const gitIgnore = __webpack_require__(665); +const slash = __webpack_require__(666); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -75731,7 +75946,7 @@ module.exports.sync = options => { /***/ }), -/* 663 */ +/* 665 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -76322,7 +76537,7 @@ if ( /***/ }), -/* 664 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76340,7 +76555,7 @@ module.exports = path => { /***/ }), -/* 665 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76393,7 +76608,7 @@ module.exports = { /***/ }), -/* 666 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76415,7 +76630,7 @@ module.exports = path_ => { /***/ }), -/* 667 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76443,7 +76658,7 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 668 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { const assert = __webpack_require__(30) @@ -76451,7 +76666,7 @@ const path = __webpack_require__(16) const fs = __webpack_require__(23) let glob = undefined try { - glob = __webpack_require__(589) + glob = __webpack_require__(591) } catch (_err) { // treat glob as optional. } @@ -76817,12 +77032,12 @@ rimraf.sync = rimrafSync /***/ }), -/* 669 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const AggregateError = __webpack_require__(670); +const AggregateError = __webpack_require__(672); module.exports = async ( iterable, @@ -76905,13 +77120,13 @@ module.exports = async ( /***/ }), -/* 670 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const indentString = __webpack_require__(671); -const cleanStack = __webpack_require__(672); +const indentString = __webpack_require__(673); +const cleanStack = __webpack_require__(674); const cleanInternalStack = stack => stack.replace(/\s+at .*aggregate-error\/index.js:\d+:\d+\)?/g, ''); @@ -76959,7 +77174,7 @@ module.exports = AggregateError; /***/ }), -/* 671 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77001,7 +77216,7 @@ module.exports = (string, count = 1, options) => { /***/ }), -/* 672 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77048,15 +77263,15 @@ module.exports = (stack, options) => { /***/ }), -/* 673 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(674); -const cliCursor = __webpack_require__(678); -const cliSpinners = __webpack_require__(682); -const logSymbols = __webpack_require__(563); +const chalk = __webpack_require__(676); +const cliCursor = __webpack_require__(680); +const cliSpinners = __webpack_require__(684); +const logSymbols = __webpack_require__(565); class Ora { constructor(options) { @@ -77203,16 +77418,16 @@ module.exports.promise = (action, options) => { /***/ }), -/* 674 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(3); -const ansiStyles = __webpack_require__(675); -const stdoutColor = __webpack_require__(676).stdout; +const ansiStyles = __webpack_require__(677); +const stdoutColor = __webpack_require__(678).stdout; -const template = __webpack_require__(677); +const template = __webpack_require__(679); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -77438,7 +77653,7 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 675 */ +/* 677 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77611,7 +77826,7 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(5)(module))) /***/ }), -/* 676 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77753,7 +77968,7 @@ module.exports = { /***/ }), -/* 677 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77888,12 +78103,12 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 678 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(679); +const restoreCursor = __webpack_require__(681); let hidden = false; @@ -77934,12 +78149,12 @@ exports.toggle = (force, stream) => { /***/ }), -/* 679 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(680); +const onetime = __webpack_require__(682); const signalExit = __webpack_require__(377); module.exports = onetime(() => { @@ -77950,12 +78165,12 @@ module.exports = onetime(() => { /***/ }), -/* 680 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const mimicFn = __webpack_require__(681); +const mimicFn = __webpack_require__(683); module.exports = (fn, opts) => { // TODO: Remove this in v3 @@ -77996,7 +78211,7 @@ module.exports = (fn, opts) => { /***/ }), -/* 681 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78012,22 +78227,22 @@ module.exports = (to, from) => { /***/ }), -/* 682 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = __webpack_require__(683); +module.exports = __webpack_require__(685); /***/ }), -/* 683 */ +/* 685 */ /***/ (function(module) { module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]}}"); /***/ }), -/* 684 */ +/* 686 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78036,8 +78251,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(34); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(499); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(501); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(502); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -78087,7 +78302,7 @@ const RunCommand = { }; /***/ }), -/* 685 */ +/* 687 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78096,9 +78311,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(34); -/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(499); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(686); +/* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(501); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(502); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(688); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -78182,7 +78397,7 @@ const WatchCommand = { }; /***/ }), -/* 686 */ +/* 688 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78256,7 +78471,7 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 687 */ +/* 689 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78264,15 +78479,15 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "runCommand", function() { return runCommand; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(688); +/* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(690); /* harmony import */ var indent_string__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(indent_string__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(689); +/* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(691); /* harmony import */ var wrap_ansi__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(wrap_ansi__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(514); +/* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(516); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(34); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(500); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(696); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(697); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(502); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(698); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(699); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -78360,7 +78575,7 @@ function toArray(value) { } /***/ }), -/* 688 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78394,13 +78609,13 @@ module.exports = (str, count, opts) => { /***/ }), -/* 689 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringWidth = __webpack_require__(690); -const stripAnsi = __webpack_require__(694); +const stringWidth = __webpack_require__(692); +const stripAnsi = __webpack_require__(696); const ESCAPES = new Set([ '\u001B', @@ -78594,13 +78809,13 @@ module.exports = (str, cols, opts) => { /***/ }), -/* 690 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stripAnsi = __webpack_require__(691); -const isFullwidthCodePoint = __webpack_require__(693); +const stripAnsi = __webpack_require__(693); +const isFullwidthCodePoint = __webpack_require__(695); module.exports = str => { if (typeof str !== 'string' || str.length === 0) { @@ -78637,18 +78852,18 @@ module.exports = str => { /***/ }), -/* 691 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(692); +const ansiRegex = __webpack_require__(694); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 692 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78665,7 +78880,7 @@ module.exports = () => { /***/ }), -/* 693 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78718,18 +78933,18 @@ module.exports = x => { /***/ }), -/* 694 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(695); +const ansiRegex = __webpack_require__(697); module.exports = input => typeof input === 'string' ? input.replace(ansiRegex(), '') : input; /***/ }), -/* 695 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78746,7 +78961,7 @@ module.exports = () => { /***/ }), -/* 696 */ +/* 698 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78899,7 +79114,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 697 */ +/* 699 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -78907,12 +79122,12 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(698); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(700); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); -/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(702); +/* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(704); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(500); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(577); +/* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(502); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(579); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -79053,15 +79268,15 @@ class Kibana { } /***/ }), -/* 698 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const minimatch = __webpack_require__(504); -const arrayUnion = __webpack_require__(699); -const arrayDiffer = __webpack_require__(700); -const arrify = __webpack_require__(701); +const minimatch = __webpack_require__(506); +const arrayUnion = __webpack_require__(701); +const arrayDiffer = __webpack_require__(702); +const arrify = __webpack_require__(703); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -79085,7 +79300,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 699 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79097,7 +79312,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 700 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79112,7 +79327,7 @@ module.exports = arrayDiffer; /***/ }), -/* 701 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79142,7 +79357,7 @@ module.exports = arrify; /***/ }), -/* 702 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79170,15 +79385,15 @@ module.exports = (childPath, parentPath) => { /***/ }), -/* 703 */ +/* 705 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(704); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(706); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(927); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(941); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79203,23 +79418,23 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 704 */ +/* 706 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(705); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(707); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(585); +/* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(587); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(577); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(579); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(20); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(34); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(516); -/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(500); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(518); +/* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(502); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -79351,7 +79566,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 705 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79359,13 +79574,13 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(379); const path = __webpack_require__(16); const os = __webpack_require__(11); -const pAll = __webpack_require__(706); -const arrify = __webpack_require__(708); -const globby = __webpack_require__(709); -const isGlob = __webpack_require__(603); -const cpFile = __webpack_require__(912); -const junk = __webpack_require__(924); -const CpyError = __webpack_require__(925); +const pAll = __webpack_require__(708); +const arrify = __webpack_require__(710); +const globby = __webpack_require__(711); +const isGlob = __webpack_require__(605); +const cpFile = __webpack_require__(926); +const junk = __webpack_require__(938); +const CpyError = __webpack_require__(939); const defaultOptions = { ignoreJunk: true @@ -79484,12 +79699,12 @@ module.exports = (source, destination, { /***/ }), -/* 706 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(707); +const pMap = __webpack_require__(709); module.exports = (iterable, options) => pMap(iterable, element => element(), options); // TODO: Remove this for the next major release @@ -79497,7 +79712,7 @@ module.exports.default = module.exports; /***/ }), -/* 707 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79576,7 +79791,7 @@ module.exports.default = pMap; /***/ }), -/* 708 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79606,17 +79821,17 @@ module.exports = arrify; /***/ }), -/* 709 */ +/* 711 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const arrayUnion = __webpack_require__(710); -const glob = __webpack_require__(712); -const fastGlob = __webpack_require__(717); -const dirGlob = __webpack_require__(905); -const gitignore = __webpack_require__(908); +const arrayUnion = __webpack_require__(712); +const glob = __webpack_require__(714); +const fastGlob = __webpack_require__(719); +const dirGlob = __webpack_require__(919); +const gitignore = __webpack_require__(922); const DEFAULT_FILTER = () => false; @@ -79761,12 +79976,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 710 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(711); +var arrayUniq = __webpack_require__(713); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -79774,7 +79989,7 @@ module.exports = function () { /***/ }), -/* 711 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79843,7 +80058,7 @@ if ('Set' in global) { /***/ }), -/* 712 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { // Approach: @@ -79889,21 +80104,21 @@ if ('Set' in global) { module.exports = glob var fs = __webpack_require__(23) -var rp = __webpack_require__(502) -var minimatch = __webpack_require__(504) +var rp = __webpack_require__(504) +var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch -var inherits = __webpack_require__(713) +var inherits = __webpack_require__(715) var EE = __webpack_require__(379).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(510) -var globSync = __webpack_require__(715) -var common = __webpack_require__(716) +var isAbsolute = __webpack_require__(512) +var globSync = __webpack_require__(717) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts var ownProp = common.ownProp -var inflight = __webpack_require__(513) +var inflight = __webpack_require__(515) var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored @@ -80639,7 +80854,7 @@ Glob.prototype._stat2 = function (f, abs, er, stat, cb) { /***/ }), -/* 713 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -80649,12 +80864,12 @@ try { module.exports = util.inherits; } catch (e) { /* istanbul ignore next */ - module.exports = __webpack_require__(714); + module.exports = __webpack_require__(716); } /***/ }), -/* 714 */ +/* 716 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -80687,22 +80902,22 @@ if (typeof Object.create === 'function') { /***/ }), -/* 715 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { module.exports = globSync globSync.GlobSync = GlobSync var fs = __webpack_require__(23) -var rp = __webpack_require__(502) -var minimatch = __webpack_require__(504) +var rp = __webpack_require__(504) +var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch -var Glob = __webpack_require__(712).Glob +var Glob = __webpack_require__(714).Glob var util = __webpack_require__(29) var path = __webpack_require__(16) var assert = __webpack_require__(30) -var isAbsolute = __webpack_require__(510) -var common = __webpack_require__(716) +var isAbsolute = __webpack_require__(512) +var common = __webpack_require__(718) var alphasort = common.alphasort var alphasorti = common.alphasorti var setopts = common.setopts @@ -81179,7 +81394,7 @@ GlobSync.prototype._makeAbs = function (f) { /***/ }), -/* 716 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { exports.alphasort = alphasort @@ -81197,8 +81412,8 @@ function ownProp (obj, field) { } var path = __webpack_require__(16) -var minimatch = __webpack_require__(504) -var isAbsolute = __webpack_require__(510) +var minimatch = __webpack_require__(506) +var isAbsolute = __webpack_require__(512) var Minimatch = minimatch.Minimatch function alphasorti (a, b) { @@ -81425,10 +81640,10 @@ function childrenIgnored (self, path) { /***/ }), -/* 717 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(718); +const pkg = __webpack_require__(720); module.exports = pkg.async; module.exports.default = pkg.async; @@ -81441,19 +81656,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 718 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(719); -var taskManager = __webpack_require__(720); -var reader_async_1 = __webpack_require__(876); -var reader_stream_1 = __webpack_require__(900); -var reader_sync_1 = __webpack_require__(901); -var arrayUtils = __webpack_require__(903); -var streamUtils = __webpack_require__(904); +var optionsManager = __webpack_require__(721); +var taskManager = __webpack_require__(722); +var reader_async_1 = __webpack_require__(890); +var reader_stream_1 = __webpack_require__(914); +var reader_sync_1 = __webpack_require__(915); +var arrayUtils = __webpack_require__(917); +var streamUtils = __webpack_require__(918); /** * Synchronous API. */ @@ -81519,7 +81734,7 @@ function isString(source) { /***/ }), -/* 719 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81557,13 +81772,13 @@ exports.prepare = prepare; /***/ }), -/* 720 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(721); +var patternUtils = __webpack_require__(723); /** * Generate tasks based on parent directory of each pattern. */ @@ -81654,16 +81869,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 721 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var globParent = __webpack_require__(722); -var isGlob = __webpack_require__(725); -var micromatch = __webpack_require__(726); +var globParent = __webpack_require__(724); +var isGlob = __webpack_require__(727); +var micromatch = __webpack_require__(728); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -81809,15 +82024,15 @@ exports.matchAny = matchAny; /***/ }), -/* 722 */ +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(16); -var isglob = __webpack_require__(723); -var pathDirname = __webpack_require__(724); +var isglob = __webpack_require__(725); +var pathDirname = __webpack_require__(726); var isWin32 = __webpack_require__(11).platform() === 'win32'; module.exports = function globParent(str) { @@ -81840,7 +82055,7 @@ module.exports = function globParent(str) { /***/ }), -/* 723 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -81850,7 +82065,7 @@ module.exports = function globParent(str) { * Licensed under the MIT License. */ -var isExtglob = __webpack_require__(604); +var isExtglob = __webpack_require__(606); module.exports = function isGlob(str) { if (typeof str !== 'string' || str === '') { @@ -81871,7 +82086,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 724 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82021,7 +82236,7 @@ module.exports.win32 = win32; /***/ }), -/* 725 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -82031,7 +82246,7 @@ module.exports.win32 = win32; * Released under the MIT License. */ -var isExtglob = __webpack_require__(604); +var isExtglob = __webpack_require__(606); var chars = { '{': '}', '(': ')', '[': ']'}; module.exports = function isGlob(str, options) { @@ -82073,7 +82288,7 @@ module.exports = function isGlob(str, options) { /***/ }), -/* 726 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82084,18 +82299,18 @@ module.exports = function isGlob(str, options) { */ var util = __webpack_require__(29); -var braces = __webpack_require__(727); -var toRegex = __webpack_require__(829); -var extend = __webpack_require__(837); +var braces = __webpack_require__(729); +var toRegex = __webpack_require__(842); +var extend = __webpack_require__(850); /** * Local dependencies */ -var compilers = __webpack_require__(840); -var parsers = __webpack_require__(872); -var cache = __webpack_require__(873); -var utils = __webpack_require__(874); +var compilers = __webpack_require__(853); +var parsers = __webpack_require__(886); +var cache = __webpack_require__(887); +var utils = __webpack_require__(888); var MAX_LENGTH = 1024 * 64; /** @@ -82957,7 +83172,7 @@ module.exports = micromatch; /***/ }), -/* 727 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82967,18 +83182,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(728); -var unique = __webpack_require__(740); -var extend = __webpack_require__(737); +var toRegex = __webpack_require__(730); +var unique = __webpack_require__(744); +var extend = __webpack_require__(739); /** * Local dependencies */ -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var Braces = __webpack_require__(766); -var utils = __webpack_require__(742); +var compilers = __webpack_require__(745); +var parsers = __webpack_require__(762); +var Braces = __webpack_require__(772); +var utils = __webpack_require__(746); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -83282,15 +83497,15 @@ module.exports = braces; /***/ }), -/* 728 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(729); -var extend = __webpack_require__(737); -var not = __webpack_require__(739); +var define = __webpack_require__(731); +var extend = __webpack_require__(739); +var not = __webpack_require__(741); var MAX_LENGTH = 1024 * 64; /** @@ -83437,7 +83652,7 @@ module.exports.makeRe = makeRe; /***/ }), -/* 729 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83450,7 +83665,7 @@ module.exports.makeRe = makeRe; -var isDescriptor = __webpack_require__(730); +var isDescriptor = __webpack_require__(732); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -83475,7 +83690,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 730 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83488,9 +83703,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(731); -var isAccessor = __webpack_require__(732); -var isData = __webpack_require__(735); +var typeOf = __webpack_require__(733); +var isAccessor = __webpack_require__(734); +var isData = __webpack_require__(737); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -83504,7 +83719,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 731 */ +/* 733 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -83657,7 +83872,7 @@ function isBuffer(val) { /***/ }), -/* 732 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83670,7 +83885,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(733); +var typeOf = __webpack_require__(735); // accessor descriptor properties var accessor = { @@ -83733,10 +83948,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 733 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -83855,7 +84070,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 734 */ +/* 736 */ /***/ (function(module, exports) { /*! @@ -83882,7 +84097,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 735 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83895,7 +84110,7 @@ function isSlowBuffer (obj) { -var typeOf = __webpack_require__(736); +var typeOf = __webpack_require__(738); // data descriptor properties var data = { @@ -83944,10 +84159,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 736 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -84066,13 +84281,13 @@ module.exports = function kindOf(val) { /***/ }), -/* 737 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(738); +var isObject = __webpack_require__(740); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -84106,7 +84321,7 @@ function hasOwn(obj, key) { /***/ }), -/* 738 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84126,13 +84341,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 739 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(737); +var extend = __webpack_require__(742); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84199,7 +84414,67 @@ module.exports = toRegex; /***/ }), -/* 740 */ +/* 742 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isObject = __webpack_require__(743); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 743 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84249,13 +84524,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 741 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(742); +var utils = __webpack_require__(746); module.exports = function(braces, options) { braces.compiler @@ -84538,25 +84813,25 @@ function hasQueue(node) { /***/ }), -/* 742 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(743); +var splitString = __webpack_require__(747); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(737); -utils.flatten = __webpack_require__(749); -utils.isObject = __webpack_require__(747); -utils.fillRange = __webpack_require__(750); -utils.repeat = __webpack_require__(755); -utils.unique = __webpack_require__(740); +utils.extend = __webpack_require__(739); +utils.flatten = __webpack_require__(753); +utils.isObject = __webpack_require__(751); +utils.fillRange = __webpack_require__(754); +utils.repeat = __webpack_require__(761); +utils.unique = __webpack_require__(744); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -84888,7 +85163,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 743 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84901,7 +85176,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(744); +var extend = __webpack_require__(748); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85066,14 +85341,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 744 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(745); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(749); +var assignSymbols = __webpack_require__(752); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85133,7 +85408,7 @@ function isEnum(obj, key) { /***/ }), -/* 745 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85146,7 +85421,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(750); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85154,7 +85429,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 746 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85167,7 +85442,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(751); function isObjectObject(o) { return isObject(o) === true @@ -85198,7 +85473,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 747 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85217,7 +85492,7 @@ module.exports = function isObject(val) { /***/ }), -/* 748 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85264,7 +85539,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 749 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85293,7 +85568,7 @@ function flat(arr, res) { /***/ }), -/* 750 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85307,10 +85582,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(751); -var extend = __webpack_require__(737); -var repeat = __webpack_require__(753); -var toRegex = __webpack_require__(754); +var isNumber = __webpack_require__(755); +var extend = __webpack_require__(757); +var repeat = __webpack_require__(759); +var toRegex = __webpack_require__(760); /** * Return a range of numbers or letters. @@ -85508,7 +85783,7 @@ module.exports = fillRange; /***/ }), -/* 751 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85521,7 +85796,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(756); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85537,10 +85812,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 752 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(734); +var isBuffer = __webpack_require__(736); var toString = Object.prototype.toString; /** @@ -85659,7 +85934,67 @@ module.exports = function kindOf(val) { /***/ }), -/* 753 */ +/* 757 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isObject = __webpack_require__(758); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 758 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85736,7 +86071,7 @@ function repeat(str, num) { /***/ }), -/* 754 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85749,8 +86084,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(753); -var isNumber = __webpack_require__(751); +var repeat = __webpack_require__(759); +var isNumber = __webpack_require__(755); var cache = {}; function toRegexRange(min, max, options) { @@ -86037,7 +86372,7 @@ module.exports = toRegexRange; /***/ }), -/* 755 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86062,14 +86397,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 756 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(757); -var utils = __webpack_require__(742); +var Node = __webpack_require__(763); +var utils = __webpack_require__(746); /** * Braces parsers @@ -86429,15 +86764,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 757 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(747); -var define = __webpack_require__(758); -var utils = __webpack_require__(765); +var isObject = __webpack_require__(751); +var define = __webpack_require__(764); +var utils = __webpack_require__(771); var ownNames; /** @@ -86928,7 +87263,7 @@ exports = module.exports = Node; /***/ }), -/* 758 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86941,7 +87276,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(765); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -86966,7 +87301,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 759 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86979,9 +87314,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(760); -var isAccessor = __webpack_require__(761); -var isData = __webpack_require__(763); +var typeOf = __webpack_require__(766); +var isAccessor = __webpack_require__(767); +var isData = __webpack_require__(769); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -86995,7 +87330,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 760 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87130,7 +87465,7 @@ function isBuffer(val) { /***/ }), -/* 761 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87143,7 +87478,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(762); +var typeOf = __webpack_require__(768); // accessor descriptor properties var accessor = { @@ -87206,7 +87541,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 762 */ +/* 768 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87341,7 +87676,7 @@ function isBuffer(val) { /***/ }), -/* 763 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87354,7 +87689,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(764); +var typeOf = __webpack_require__(770); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87397,7 +87732,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 764 */ +/* 770 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87532,13 +87867,13 @@ function isBuffer(val) { /***/ }), -/* 765 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(756); var utils = module.exports; /** @@ -88514,1845 +88849,2578 @@ function isObject(val) { } /** - * Return true if val is a string + * Return true if val is a string + */ + +function isString(val) { + return typeof val === 'string'; +} + +/** + * Return true if val is a function + */ + +function isFunction(val) { + return typeof val === 'function'; +} + +/** + * Return true if val is an array + */ + +function isArray(val) { + return Array.isArray(val); +} + +/** + * Shim to ensure the `.append` methods work with any version of snapdragon + */ + +function append(compiler, val, node) { + if (typeof compiler.append !== 'function') { + return compiler.emit(val, node); + } + return compiler.append(val, node); +} + +/** + * Simplified assertion. Throws an error is `val` is falsey. + */ + +function assert(val, message) { + if (!val) throw new Error(message); +} + + +/***/ }), +/* 772 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var extend = __webpack_require__(739); +var Snapdragon = __webpack_require__(773); +var compilers = __webpack_require__(745); +var parsers = __webpack_require__(762); +var utils = __webpack_require__(746); + +/** + * Customize Snapdragon parser and renderer + */ + +function Braces(options) { + this.options = extend({}, options); +} + +/** + * Initialize braces + */ + +Braces.prototype.init = function(options) { + if (this.isInitialized) return; + this.isInitialized = true; + var opts = utils.createOptions({}, this.options, options); + this.snapdragon = this.options.snapdragon || new Snapdragon(opts); + this.compiler = this.snapdragon.compiler; + this.parser = this.snapdragon.parser; + + compilers(this.snapdragon, opts); + parsers(this.snapdragon, opts); + + /** + * Call Snapdragon `.parse` method. When AST is returned, we check to + * see if any unclosed braces are left on the stack and, if so, we iterate + * over the stack and correct the AST so that compilers are called in the correct + * order and unbalance braces are properly escaped. + */ + + utils.define(this.snapdragon, 'parse', function(pattern, options) { + var parsed = Snapdragon.prototype.parse.apply(this, arguments); + this.parser.ast.input = pattern; + + var stack = this.parser.stack; + while (stack.length) { + addParent({type: 'brace.close', val: ''}, stack.pop()); + } + + function addParent(node, parent) { + utils.define(node, 'parent', parent); + parent.nodes.push(node); + } + + // add non-enumerable parser reference + utils.define(parsed, 'parser', this.parser); + return parsed; + }); +}; + +/** + * Decorate `.parse` method + */ + +Braces.prototype.parse = function(ast, options) { + if (ast && typeof ast === 'object' && ast.nodes) return ast; + this.init(options); + return this.snapdragon.parse(ast, options); +}; + +/** + * Decorate `.compile` method + */ + +Braces.prototype.compile = function(ast, options) { + if (typeof ast === 'string') { + ast = this.parse(ast, options); + } else { + this.init(options); + } + return this.snapdragon.compile(ast, options); +}; + +/** + * Expand + */ + +Braces.prototype.expand = function(pattern) { + var ast = this.parse(pattern, {expand: true}); + return this.compile(ast, {expand: true}); +}; + +/** + * Optimize + */ + +Braces.prototype.optimize = function(pattern) { + var ast = this.parse(pattern, {optimize: true}); + return this.compile(ast, {optimize: true}); +}; + +/** + * Expose `Braces` + */ + +module.exports = Braces; + + +/***/ }), +/* 773 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var Base = __webpack_require__(774); +var define = __webpack_require__(800); +var Compiler = __webpack_require__(810); +var Parser = __webpack_require__(839); +var utils = __webpack_require__(819); +var regexCache = {}; +var cache = {}; + +/** + * Create a new instance of `Snapdragon` with the given `options`. + * + * ```js + * var snapdragon = new Snapdragon(); + * ``` + * + * @param {Object} `options` + * @api public + */ + +function Snapdragon(options) { + Base.call(this, null, options); + this.options = utils.extend({source: 'string'}, this.options); + this.compiler = new Compiler(this.options); + this.parser = new Parser(this.options); + + Object.defineProperty(this, 'compilers', { + get: function() { + return this.compiler.compilers; + } + }); + + Object.defineProperty(this, 'parsers', { + get: function() { + return this.parser.parsers; + } + }); + + Object.defineProperty(this, 'regex', { + get: function() { + return this.parser.regex; + } + }); +} + +/** + * Inherit Base + */ + +Base.extend(Snapdragon); + +/** + * Add a parser to `snapdragon.parsers` for capturing the given `type` using + * the specified regex or parser function. A function is useful if you need + * to customize how the token is created and/or have access to the parser + * instance to check options, etc. + * + * ```js + * snapdragon + * .capture('slash', /^\//) + * .capture('dot', function() { + * var pos = this.position(); + * var m = this.match(/^\./); + * if (!m) return; + * return pos({ + * type: 'dot', + * val: m[0] + * }); + * }); + * ``` + * @param {String} `type` + * @param {RegExp|Function} `regex` + * @return {Object} Returns the parser instance for chaining + * @api public + */ + +Snapdragon.prototype.capture = function() { + return this.parser.capture.apply(this.parser, arguments); +}; + +/** + * Register a plugin `fn`. + * + * ```js + * var snapdragon = new Snapdgragon([options]); + * snapdragon.use(function() { + * console.log(this); //<= snapdragon instance + * console.log(this.parser); //<= parser instance + * console.log(this.compiler); //<= compiler instance + * }); + * ``` + * @param {Object} `fn` + * @api public + */ + +Snapdragon.prototype.use = function(fn) { + fn.call(this, this); + return this; +}; + +/** + * Parse the given `str`. + * + * ```js + * var snapdragon = new Snapdgragon([options]); + * // register parsers + * snapdragon.parser.use(function() {}); + * + * // parse + * var ast = snapdragon.parse('foo/bar'); + * console.log(ast); + * ``` + * @param {String} `str` + * @param {Object} `options` Set `options.sourcemap` to true to enable source maps. + * @return {Object} Returns an AST. + * @api public + */ + +Snapdragon.prototype.parse = function(str, options) { + this.options = utils.extend({}, this.options, options); + var parsed = this.parser.parse(str, this.options); + + // add non-enumerable parser reference + define(parsed, 'parser', this.parser); + return parsed; +}; + +/** + * Compile the given `AST`. + * + * ```js + * var snapdragon = new Snapdgragon([options]); + * // register plugins + * snapdragon.use(function() {}); + * // register parser plugins + * snapdragon.parser.use(function() {}); + * // register compiler plugins + * snapdragon.compiler.use(function() {}); + * + * // parse + * var ast = snapdragon.parse('foo/bar'); + * + * // compile + * var res = snapdragon.compile(ast); + * console.log(res.output); + * ``` + * @param {Object} `ast` + * @param {Object} `options` + * @return {Object} Returns an object with an `output` property with the rendered string. + * @api public + */ + +Snapdragon.prototype.compile = function(ast, options) { + this.options = utils.extend({}, this.options, options); + var compiled = this.compiler.compile(ast, this.options); + + // add non-enumerable compiler reference + define(compiled, 'compiler', this.compiler); + return compiled; +}; + +/** + * Expose `Snapdragon` + */ + +module.exports = Snapdragon; + +/** + * Expose `Parser` and `Compiler` + */ + +module.exports.Compiler = Compiler; +module.exports.Parser = Parser; + + +/***/ }), +/* 774 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var util = __webpack_require__(29); +var define = __webpack_require__(775); +var CacheBase = __webpack_require__(776); +var Emitter = __webpack_require__(777); +var isObject = __webpack_require__(751); +var merge = __webpack_require__(794); +var pascal = __webpack_require__(797); +var cu = __webpack_require__(798); + +/** + * Optionally define a custom `cache` namespace to use. + */ + +function namespace(name) { + var Cache = name ? CacheBase.namespace(name) : CacheBase; + var fns = []; + + /** + * Create an instance of `Base` with the given `config` and `options`. + * + * ```js + * // initialize with `config` and `options` + * var app = new Base({isApp: true}, {abc: true}); + * app.set('foo', 'bar'); + * + * // values defined with the given `config` object will be on the root of the instance + * console.log(app.baz); //=> undefined + * console.log(app.foo); //=> 'bar' + * // or use `.get` + * console.log(app.get('isApp')); //=> true + * console.log(app.get('foo')); //=> 'bar' + * + * // values defined with the given `options` object will be on `app.options + * console.log(app.options.abc); //=> true + * ``` + * + * @param {Object} `config` If supplied, this object is passed to [cache-base][] to merge onto the the instance upon instantiation. + * @param {Object} `options` If supplied, this object is used to initialize the `base.options` object. + * @api public + */ + + function Base(config, options) { + if (!(this instanceof Base)) { + return new Base(config, options); + } + Cache.call(this, config); + this.is('base'); + this.initBase(config, options); + } + + /** + * Inherit cache-base + */ + + util.inherits(Base, Cache); + + /** + * Add static emitter methods + */ + + Emitter(Base); + + /** + * Initialize `Base` defaults with the given `config` object + */ + + Base.prototype.initBase = function(config, options) { + this.options = merge({}, this.options, options); + this.cache = this.cache || {}; + this.define('registered', {}); + if (name) this[name] = {}; + + // make `app._callbacks` non-enumerable + this.define('_callbacks', this._callbacks); + if (isObject(config)) { + this.visit('set', config); + } + Base.run(this, 'use', fns); + }; + + /** + * Set the given `name` on `app._name` and `app.is*` properties. Used for doing + * lookups in plugins. + * + * ```js + * app.is('foo'); + * console.log(app._name); + * //=> 'foo' + * console.log(app.isFoo); + * //=> true + * app.is('bar'); + * console.log(app.isFoo); + * //=> true + * console.log(app.isBar); + * //=> true + * console.log(app._name); + * //=> 'bar' + * ``` + * @name .is + * @param {String} `name` + * @return {Boolean} + * @api public + */ + + Base.prototype.is = function(name) { + if (typeof name !== 'string') { + throw new TypeError('expected name to be a string'); + } + this.define('is' + pascal(name), true); + this.define('_name', name); + this.define('_appname', name); + return this; + }; + + /** + * Returns true if a plugin has already been registered on an instance. + * + * Plugin implementors are encouraged to use this first thing in a plugin + * to prevent the plugin from being called more than once on the same + * instance. + * + * ```js + * var base = new Base(); + * base.use(function(app) { + * if (app.isRegistered('myPlugin')) return; + * // do stuff to `app` + * }); + * + * // to also record the plugin as being registered + * base.use(function(app) { + * if (app.isRegistered('myPlugin', true)) return; + * // do stuff to `app` + * }); + * ``` + * @name .isRegistered + * @emits `plugin` Emits the name of the plugin being registered. Useful for unit tests, to ensure plugins are only registered once. + * @param {String} `name` The plugin name. + * @param {Boolean} `register` If the plugin if not already registered, to record it as being registered pass `true` as the second argument. + * @return {Boolean} Returns true if a plugin is already registered. + * @api public + */ + + Base.prototype.isRegistered = function(name, register) { + if (this.registered.hasOwnProperty(name)) { + return true; + } + if (register !== false) { + this.registered[name] = true; + this.emit('plugin', name); + } + return false; + }; + + /** + * Define a plugin function to be called immediately upon init. Plugins are chainable + * and expose the following arguments to the plugin function: + * + * - `app`: the current instance of `Base` + * - `base`: the [first ancestor instance](#base) of `Base` + * + * ```js + * var app = new Base() + * .use(foo) + * .use(bar) + * .use(baz) + * ``` + * @name .use + * @param {Function} `fn` plugin function to call + * @return {Object} Returns the item instance for chaining. + * @api public + */ + + Base.prototype.use = function(fn) { + fn.call(this, this); + return this; + }; + + /** + * The `.define` method is used for adding non-enumerable property on the instance. + * Dot-notation is **not supported** with `define`. + * + * ```js + * // arbitrary `render` function using lodash `template` + * app.define('render', function(str, locals) { + * return _.template(str)(locals); + * }); + * ``` + * @name .define + * @param {String} `key` The name of the property to define. + * @param {any} `value` + * @return {Object} Returns the instance for chaining. + * @api public + */ + + Base.prototype.define = function(key, val) { + if (isObject(key)) { + return this.visit('define', key); + } + define(this, key, val); + return this; + }; + + /** + * Mix property `key` onto the Base prototype. If base is inherited using + * `Base.extend` this method will be overridden by a new `mixin` method that will + * only add properties to the prototype of the inheriting application. + * + * ```js + * app.mixin('foo', function() { + * // do stuff + * }); + * ``` + * @name .mixin + * @param {String} `key` + * @param {Object|Array} `val` + * @return {Object} Returns the `base` instance for chaining. + * @api public + */ + + Base.prototype.mixin = function(key, val) { + Base.prototype[key] = val; + return this; + }; + + /** + * Non-enumberable mixin array, used by the static [Base.mixin]() method. + */ + + Base.prototype.mixins = Base.prototype.mixins || []; + + /** + * Getter/setter used when creating nested instances of `Base`, for storing a reference + * to the first ancestor instance. This works by setting an instance of `Base` on the `parent` + * property of a "child" instance. The `base` property defaults to the current instance if + * no `parent` property is defined. + * + * ```js + * // create an instance of `Base`, this is our first ("base") instance + * var first = new Base(); + * first.foo = 'bar'; // arbitrary property, to make it easier to see what's happening later + * + * // create another instance + * var second = new Base(); + * // create a reference to the first instance (`first`) + * second.parent = first; + * + * // create another instance + * var third = new Base(); + * // create a reference to the previous instance (`second`) + * // repeat this pattern every time a "child" instance is created + * third.parent = second; + * + * // we can always access the first instance using the `base` property + * console.log(first.base.foo); + * //=> 'bar' + * console.log(second.base.foo); + * //=> 'bar' + * console.log(third.base.foo); + * //=> 'bar' + * // and now you know how to get to third base ;) + * ``` + * @name .base + * @api public + */ + + Object.defineProperty(Base.prototype, 'base', { + configurable: true, + get: function() { + return this.parent ? this.parent.base : this; + } + }); + + /** + * Static method for adding global plugin functions that will + * be added to an instance when created. + * + * ```js + * Base.use(function(app) { + * app.foo = 'bar'; + * }); + * var app = new Base(); + * console.log(app.foo); + * //=> 'bar' + * ``` + * @name #use + * @param {Function} `fn` Plugin function to use on each instance. + * @return {Object} Returns the `Base` constructor for chaining + * @api public + */ + + define(Base, 'use', function(fn) { + fns.push(fn); + return Base; + }); + + /** + * Run an array of functions by passing each function + * to a method on the given object specified by the given property. + * + * @param {Object} `obj` Object containing method to use. + * @param {String} `prop` Name of the method on the object to use. + * @param {Array} `arr` Array of functions to pass to the method. + */ + + define(Base, 'run', function(obj, prop, arr) { + var len = arr.length, i = 0; + while (len--) { + obj[prop](arr[i++]); + } + return Base; + }); + + /** + * Static method for inheriting the prototype and static methods of the `Base` class. + * This method greatly simplifies the process of creating inheritance-based applications. + * See [static-extend][] for more details. + * + * ```js + * var extend = cu.extend(Parent); + * Parent.extend(Child); + * + * // optional methods + * Parent.extend(Child, { + * foo: function() {}, + * bar: function() {} + * }); + * ``` + * @name #extend + * @param {Function} `Ctor` constructor to extend + * @param {Object} `methods` Optional prototype properties to mix in. + * @return {Object} Returns the `Base` constructor for chaining + * @api public + */ + + define(Base, 'extend', cu.extend(Base, function(Ctor, Parent) { + Ctor.prototype.mixins = Ctor.prototype.mixins || []; + + define(Ctor, 'mixin', function(fn) { + var mixin = fn(Ctor.prototype, Ctor); + if (typeof mixin === 'function') { + Ctor.prototype.mixins.push(mixin); + } + return Ctor; + }); + + define(Ctor, 'mixins', function(Child) { + Base.run(Child, 'mixin', Ctor.prototype.mixins); + return Ctor; + }); + + Ctor.prototype.mixin = function(key, value) { + Ctor.prototype[key] = value; + return this; + }; + return Base; + })); + + /** + * Used for adding methods to the `Base` prototype, and/or to the prototype of child instances. + * When a mixin function returns a function, the returned function is pushed onto the `.mixins` + * array, making it available to be used on inheriting classes whenever `Base.mixins()` is + * called (e.g. `Base.mixins(Child)`). + * + * ```js + * Base.mixin(function(proto) { + * proto.foo = function(msg) { + * return 'foo ' + msg; + * }; + * }); + * ``` + * @name #mixin + * @param {Function} `fn` Function to call + * @return {Object} Returns the `Base` constructor for chaining + * @api public + */ + + define(Base, 'mixin', function(fn) { + var mixin = fn(Base.prototype, Base); + if (typeof mixin === 'function') { + Base.prototype.mixins.push(mixin); + } + return Base; + }); + + /** + * Static method for running global mixin functions against a child constructor. + * Mixins must be registered before calling this method. + * + * ```js + * Base.extend(Child); + * Base.mixins(Child); + * ``` + * @name #mixins + * @param {Function} `Child` Constructor function of a child class + * @return {Object} Returns the `Base` constructor for chaining + * @api public + */ + + define(Base, 'mixins', function(Child) { + Base.run(Child, 'mixin', Base.prototype.mixins); + return Base; + }); + + /** + * Similar to `util.inherit`, but copies all static properties, prototype properties, and + * getters/setters from `Provider` to `Receiver`. See [class-utils][]{#inherit} for more details. + * + * ```js + * Base.inherit(Foo, Bar); + * ``` + * @name #inherit + * @param {Function} `Receiver` Receiving (child) constructor + * @param {Function} `Provider` Providing (parent) constructor + * @return {Object} Returns the `Base` constructor for chaining + * @api public + */ + + define(Base, 'inherit', cu.inherit); + define(Base, 'bubble', cu.bubble); + return Base; +} + +/** + * Expose `Base` with default settings */ -function isString(val) { - return typeof val === 'string'; -} +module.exports = namespace(); /** - * Return true if val is a function + * Allow users to define a namespace */ -function isFunction(val) { - return typeof val === 'function'; -} +module.exports.namespace = namespace; -/** - * Return true if val is an array - */ -function isArray(val) { - return Array.isArray(val); -} +/***/ }), +/* 775 */ +/***/ (function(module, exports, __webpack_require__) { -/** - * Shim to ensure the `.append` methods work with any version of snapdragon +"use strict"; +/*! + * define-property + * + * Copyright (c) 2015, 2017, Jon Schlinkert. + * Released under the MIT License. */ -function append(compiler, val, node) { - if (typeof compiler.append !== 'function') { - return compiler.emit(val, node); + + +var isDescriptor = __webpack_require__(765); + +module.exports = function defineProperty(obj, prop, val) { + if (typeof obj !== 'object' && typeof obj !== 'function') { + throw new TypeError('expected an object or function.'); } - return compiler.append(val, node); -} -/** - * Simplified assertion. Throws an error is `val` is falsey. - */ + if (typeof prop !== 'string') { + throw new TypeError('expected `prop` to be a string.'); + } -function assert(val, message) { - if (!val) throw new Error(message); -} + if (isDescriptor(val) && ('set' in val || 'get' in val)) { + return Object.defineProperty(obj, prop, val); + } + + return Object.defineProperty(obj, prop, { + configurable: true, + enumerable: false, + writable: true, + value: val + }); +}; /***/ }), -/* 766 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var utils = __webpack_require__(742); +var isObject = __webpack_require__(751); +var Emitter = __webpack_require__(777); +var visit = __webpack_require__(778); +var toPath = __webpack_require__(781); +var union = __webpack_require__(782); +var del = __webpack_require__(786); +var get = __webpack_require__(784); +var has = __webpack_require__(791); +var set = __webpack_require__(785); /** - * Customize Snapdragon parser and renderer + * Create a `Cache` constructor that when instantiated will + * store values on the given `prop`. + * + * ```js + * var Cache = require('cache-base').namespace('data'); + * var cache = new Cache(); + * + * cache.set('foo', 'bar'); + * //=> {data: {foo: 'bar'}} + * ``` + * @param {String} `prop` The property name to use for storing values. + * @return {Function} Returns a custom `Cache` constructor + * @api public */ -function Braces(options) { - this.options = extend({}, options); -} +function namespace(prop) { -/** - * Initialize braces - */ + /** + * Create a new `Cache`. Internally the `Cache` constructor is created using + * the `namespace` function, with `cache` defined as the storage object. + * + * ```js + * var app = new Cache(); + * ``` + * @param {Object} `cache` Optionally pass an object to initialize with. + * @constructor + * @api public + */ -Braces.prototype.init = function(options) { - if (this.isInitialized) return; - this.isInitialized = true; - var opts = utils.createOptions({}, this.options, options); - this.snapdragon = this.options.snapdragon || new Snapdragon(opts); - this.compiler = this.snapdragon.compiler; - this.parser = this.snapdragon.parser; + function Cache(cache) { + if (prop) { + this[prop] = {}; + } + if (cache) { + this.set(cache); + } + } - compilers(this.snapdragon, opts); - parsers(this.snapdragon, opts); + /** + * Inherit Emitter + */ + + Emitter(Cache.prototype); /** - * Call Snapdragon `.parse` method. When AST is returned, we check to - * see if any unclosed braces are left on the stack and, if so, we iterate - * over the stack and correct the AST so that compilers are called in the correct - * order and unbalance braces are properly escaped. + * Assign `value` to `key`. Also emits `set` with + * the key and value. + * + * ```js + * app.on('set', function(key, val) { + * // do something when `set` is emitted + * }); + * + * app.set(key, value); + * + * // also takes an object or array + * app.set({name: 'Halle'}); + * app.set([{foo: 'bar'}, {baz: 'quux'}]); + * console.log(app); + * //=> {name: 'Halle', foo: 'bar', baz: 'quux'} + * ``` + * + * @name .set + * @emits `set` with `key` and `value` as arguments. + * @param {String} `key` + * @param {any} `value` + * @return {Object} Returns the instance for chaining. + * @api public */ - utils.define(this.snapdragon, 'parse', function(pattern, options) { - var parsed = Snapdragon.prototype.parse.apply(this, arguments); - this.parser.ast.input = pattern; + Cache.prototype.set = function(key, val) { + if (Array.isArray(key) && arguments.length === 2) { + key = toPath(key); + } + if (isObject(key) || Array.isArray(key)) { + this.visit('set', key); + } else { + set(prop ? this[prop] : this, key, val); + this.emit('set', key, val); + } + return this; + }; - var stack = this.parser.stack; - while (stack.length) { - addParent({type: 'brace.close', val: ''}, stack.pop()); + /** + * Union `array` to `key`. Also emits `set` with + * the key and value. + * + * ```js + * app.union('a.b', ['foo']); + * app.union('a.b', ['bar']); + * console.log(app.get('a')); + * //=> {b: ['foo', 'bar']} + * ``` + * @name .union + * @param {String} `key` + * @param {any} `value` + * @return {Object} Returns the instance for chaining. + * @api public + */ + + Cache.prototype.union = function(key, val) { + if (Array.isArray(key) && arguments.length === 2) { + key = toPath(key); } + var ctx = prop ? this[prop] : this; + union(ctx, key, arrayify(val)); + this.emit('union', val); + return this; + }; - function addParent(node, parent) { - utils.define(node, 'parent', parent); - parent.nodes.push(node); + /** + * Return the value of `key`. Dot notation may be used + * to get [nested property values][get-value]. + * + * ```js + * app.set('a.b.c', 'd'); + * app.get('a.b'); + * //=> {c: 'd'} + * + * app.get(['a', 'b']); + * //=> {c: 'd'} + * ``` + * + * @name .get + * @emits `get` with `key` and `value` as arguments. + * @param {String} `key` The name of the property to get. Dot-notation may be used. + * @return {any} Returns the value of `key` + * @api public + */ + + Cache.prototype.get = function(key) { + key = toPath(arguments); + + var ctx = prop ? this[prop] : this; + var val = get(ctx, key); + + this.emit('get', key, val); + return val; + }; + + /** + * Return true if app has a stored value for `key`, + * false only if value is `undefined`. + * + * ```js + * app.set('foo', 'bar'); + * app.has('foo'); + * //=> true + * ``` + * + * @name .has + * @emits `has` with `key` and true or false as arguments. + * @param {String} `key` + * @return {Boolean} + * @api public + */ + + Cache.prototype.has = function(key) { + key = toPath(arguments); + + var ctx = prop ? this[prop] : this; + var val = get(ctx, key); + + var has = typeof val !== 'undefined'; + this.emit('has', key, has); + return has; + }; + + /** + * Delete one or more properties from the instance. + * + * ```js + * app.del(); // delete all + * // or + * app.del('foo'); + * // or + * app.del(['foo', 'bar']); + * ``` + * @name .del + * @emits `del` with the `key` as the only argument. + * @param {String|Array} `key` Property name or array of property names. + * @return {Object} Returns the instance for chaining. + * @api public + */ + + Cache.prototype.del = function(key) { + if (Array.isArray(key)) { + this.visit('del', key); + } else { + del(prop ? this[prop] : this, key); + this.emit('del', key); } + return this; + }; - // add non-enumerable parser reference - utils.define(parsed, 'parser', this.parser); - return parsed; - }); -}; + /** + * Reset the entire cache to an empty object. + * + * ```js + * app.clear(); + * ``` + * @api public + */ -/** - * Decorate `.parse` method - */ + Cache.prototype.clear = function() { + if (prop) { + this[prop] = {}; + } + }; -Braces.prototype.parse = function(ast, options) { - if (ast && typeof ast === 'object' && ast.nodes) return ast; - this.init(options); - return this.snapdragon.parse(ast, options); -}; + /** + * Visit `method` over the properties in the given object, or map + * visit over the object-elements in an array. + * + * @name .visit + * @param {String} `method` The name of the `base` method to call. + * @param {Object|Array} `val` The object or array to iterate over. + * @return {Object} Returns the instance for chaining. + * @api public + */ -/** - * Decorate `.compile` method - */ + Cache.prototype.visit = function(method, val) { + visit(this, method, val); + return this; + }; -Braces.prototype.compile = function(ast, options) { - if (typeof ast === 'string') { - ast = this.parse(ast, options); - } else { - this.init(options); - } - return this.snapdragon.compile(ast, options); -}; + return Cache; +} /** - * Expand + * Cast val to an array */ -Braces.prototype.expand = function(pattern) { - var ast = this.parse(pattern, {expand: true}); - return this.compile(ast, {expand: true}); -}; +function arrayify(val) { + return val ? (Array.isArray(val) ? val : [val]) : []; +} /** - * Optimize + * Expose `Cache` */ -Braces.prototype.optimize = function(pattern) { - var ast = this.parse(pattern, {optimize: true}); - return this.compile(ast, {optimize: true}); -}; +module.exports = namespace(); /** - * Expose `Braces` + * Expose `Cache.namespace` */ -module.exports = Braces; +module.exports.namespace = namespace; + + +/***/ }), +/* 777 */ +/***/ (function(module, exports, __webpack_require__) { + + +/** + * Expose `Emitter`. + */ + +if (true) { + module.exports = Emitter; +} + +/** + * Initialize a new `Emitter`. + * + * @api public + */ + +function Emitter(obj) { + if (obj) return mixin(obj); +}; + +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +function mixin(obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key]; + } + return obj; +} + +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.on = +Emitter.prototype.addEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks['$' + event] = this._callbacks['$' + event] || []) + .push(fn); + return this; +}; + +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.once = function(event, fn){ + function on() { + this.off(event, on); + fn.apply(this, arguments); + } + + on.fn = fn; + this.on(event, on); + return this; +}; + +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + +Emitter.prototype.off = +Emitter.prototype.removeListener = +Emitter.prototype.removeAllListeners = +Emitter.prototype.removeEventListener = function(event, fn){ + this._callbacks = this._callbacks || {}; + + // all + if (0 == arguments.length) { + this._callbacks = {}; + return this; + } + + // specific event + var callbacks = this._callbacks['$' + event]; + if (!callbacks) return this; + + // remove all handlers + if (1 == arguments.length) { + delete this._callbacks['$' + event]; + return this; + } + + // remove specific handler + var cb; + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i]; + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1); + break; + } + } + return this; +}; + +/** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ + +Emitter.prototype.emit = function(event){ + this._callbacks = this._callbacks || {}; + var args = [].slice.call(arguments, 1) + , callbacks = this._callbacks['$' + event]; + + if (callbacks) { + callbacks = callbacks.slice(0); + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args); + } + } + + return this; +}; + +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ + +Emitter.prototype.listeners = function(event){ + this._callbacks = this._callbacks || {}; + return this._callbacks['$' + event] || []; +}; + +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + +Emitter.prototype.hasListeners = function(event){ + return !! this.listeners(event).length; +}; /***/ }), -/* 767 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; - - -var Base = __webpack_require__(768); -var define = __webpack_require__(729); -var Compiler = __webpack_require__(797); -var Parser = __webpack_require__(826); -var utils = __webpack_require__(806); -var regexCache = {}; -var cache = {}; - -/** - * Create a new instance of `Snapdragon` with the given `options`. - * - * ```js - * var snapdragon = new Snapdragon(); - * ``` +/*! + * collection-visit * - * @param {Object} `options` - * @api public + * Copyright (c) 2015, 2017, Jon Schlinkert. + * Released under the MIT License. */ -function Snapdragon(options) { - Base.call(this, null, options); - this.options = utils.extend({source: 'string'}, this.options); - this.compiler = new Compiler(this.options); - this.parser = new Parser(this.options); - - Object.defineProperty(this, 'compilers', { - get: function() { - return this.compiler.compilers; - } - }); - Object.defineProperty(this, 'parsers', { - get: function() { - return this.parser.parsers; - } - }); - Object.defineProperty(this, 'regex', { - get: function() { - return this.parser.regex; - } - }); -} +var visit = __webpack_require__(779); +var mapVisit = __webpack_require__(780); -/** - * Inherit Base - */ +module.exports = function(collection, method, val) { + var result; -Base.extend(Snapdragon); + if (typeof val === 'string' && (method in collection)) { + var args = [].slice.call(arguments, 2); + result = collection[method].apply(collection, args); + } else if (Array.isArray(val)) { + result = mapVisit.apply(null, arguments); + } else { + result = visit.apply(null, arguments); + } -/** - * Add a parser to `snapdragon.parsers` for capturing the given `type` using - * the specified regex or parser function. A function is useful if you need - * to customize how the token is created and/or have access to the parser - * instance to check options, etc. - * - * ```js - * snapdragon - * .capture('slash', /^\//) - * .capture('dot', function() { - * var pos = this.position(); - * var m = this.match(/^\./); - * if (!m) return; - * return pos({ - * type: 'dot', - * val: m[0] - * }); - * }); - * ``` - * @param {String} `type` - * @param {RegExp|Function} `regex` - * @return {Object} Returns the parser instance for chaining - * @api public - */ + if (typeof result !== 'undefined') { + return result; + } -Snapdragon.prototype.capture = function() { - return this.parser.capture.apply(this.parser, arguments); + return collection; }; -/** - * Register a plugin `fn`. - * - * ```js - * var snapdragon = new Snapdgragon([options]); - * snapdragon.use(function() { - * console.log(this); //<= snapdragon instance - * console.log(this.parser); //<= parser instance - * console.log(this.compiler); //<= compiler instance - * }); - * ``` - * @param {Object} `fn` - * @api public - */ -Snapdragon.prototype.use = function(fn) { - fn.call(this, this); - return this; -}; +/***/ }), +/* 779 */ +/***/ (function(module, exports, __webpack_require__) { -/** - * Parse the given `str`. - * - * ```js - * var snapdragon = new Snapdgragon([options]); - * // register parsers - * snapdragon.parser.use(function() {}); +"use strict"; +/*! + * object-visit * - * // parse - * var ast = snapdragon.parse('foo/bar'); - * console.log(ast); - * ``` - * @param {String} `str` - * @param {Object} `options` Set `options.sourcemap` to true to enable source maps. - * @return {Object} Returns an AST. - * @api public + * Copyright (c) 2015, 2017, Jon Schlinkert. + * Released under the MIT License. */ -Snapdragon.prototype.parse = function(str, options) { - this.options = utils.extend({}, this.options, options); - var parsed = this.parser.parse(str, this.options); - - // add non-enumerable parser reference - define(parsed, 'parser', this.parser); - return parsed; -}; -/** - * Compile the given `AST`. - * - * ```js - * var snapdragon = new Snapdgragon([options]); - * // register plugins - * snapdragon.use(function() {}); - * // register parser plugins - * snapdragon.parser.use(function() {}); - * // register compiler plugins - * snapdragon.compiler.use(function() {}); - * - * // parse - * var ast = snapdragon.parse('foo/bar'); - * - * // compile - * var res = snapdragon.compile(ast); - * console.log(res.output); - * ``` - * @param {Object} `ast` - * @param {Object} `options` - * @return {Object} Returns an object with an `output` property with the rendered string. - * @api public - */ -Snapdragon.prototype.compile = function(ast, options) { - this.options = utils.extend({}, this.options, options); - var compiled = this.compiler.compile(ast, this.options); +var isObject = __webpack_require__(751); - // add non-enumerable compiler reference - define(compiled, 'compiler', this.compiler); - return compiled; -}; +module.exports = function visit(thisArg, method, target, val) { + if (!isObject(thisArg) && typeof thisArg !== 'function') { + throw new Error('object-visit expects `thisArg` to be an object.'); + } -/** - * Expose `Snapdragon` - */ + if (typeof method !== 'string') { + throw new Error('object-visit expects `method` name to be a string'); + } -module.exports = Snapdragon; + if (typeof thisArg[method] !== 'function') { + return thisArg; + } -/** - * Expose `Parser` and `Compiler` - */ + var args = [].slice.call(arguments, 3); + target = target || {}; -module.exports.Compiler = Compiler; -module.exports.Parser = Parser; + for (var key in target) { + var arr = [key, target[key]].concat(args); + thisArg[method].apply(thisArg, arr); + } + return thisArg; +}; /***/ }), -/* 768 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(769); -var CacheBase = __webpack_require__(770); -var Emitter = __webpack_require__(771); -var isObject = __webpack_require__(747); -var merge = __webpack_require__(788); -var pascal = __webpack_require__(791); -var cu = __webpack_require__(792); +var visit = __webpack_require__(779); /** - * Optionally define a custom `cache` namespace to use. + * Map `visit` over an array of objects. + * + * @param {Object} `collection` The context in which to invoke `method` + * @param {String} `method` Name of the method to call on `collection` + * @param {Object} `arr` Array of objects. */ -function namespace(name) { - var Cache = name ? CacheBase.namespace(name) : CacheBase; - var fns = []; - - /** - * Create an instance of `Base` with the given `config` and `options`. - * - * ```js - * // initialize with `config` and `options` - * var app = new Base({isApp: true}, {abc: true}); - * app.set('foo', 'bar'); - * - * // values defined with the given `config` object will be on the root of the instance - * console.log(app.baz); //=> undefined - * console.log(app.foo); //=> 'bar' - * // or use `.get` - * console.log(app.get('isApp')); //=> true - * console.log(app.get('foo')); //=> 'bar' - * - * // values defined with the given `options` object will be on `app.options - * console.log(app.options.abc); //=> true - * ``` - * - * @param {Object} `config` If supplied, this object is passed to [cache-base][] to merge onto the the instance upon instantiation. - * @param {Object} `options` If supplied, this object is used to initialize the `base.options` object. - * @api public - */ - - function Base(config, options) { - if (!(this instanceof Base)) { - return new Base(config, options); - } - Cache.call(this, config); - this.is('base'); - this.initBase(config, options); +module.exports = function mapVisit(collection, method, val) { + if (isObject(val)) { + return visit.apply(null, arguments); } - /** - * Inherit cache-base - */ - - util.inherits(Base, Cache); - - /** - * Add static emitter methods - */ + if (!Array.isArray(val)) { + throw new TypeError('expected an array: ' + util.inspect(val)); + } - Emitter(Base); + var args = [].slice.call(arguments, 3); - /** - * Initialize `Base` defaults with the given `config` object - */ + for (var i = 0; i < val.length; i++) { + var ele = val[i]; + if (isObject(ele)) { + visit.apply(null, [collection, method, ele].concat(args)); + } else { + collection[method].apply(collection, [ele].concat(args)); + } + } +}; - Base.prototype.initBase = function(config, options) { - this.options = merge({}, this.options, options); - this.cache = this.cache || {}; - this.define('registered', {}); - if (name) this[name] = {}; +function isObject(val) { + return val && (typeof val === 'function' || (!Array.isArray(val) && typeof val === 'object')); +} - // make `app._callbacks` non-enumerable - this.define('_callbacks', this._callbacks); - if (isObject(config)) { - this.visit('set', config); - } - Base.run(this, 'use', fns); - }; - /** - * Set the given `name` on `app._name` and `app.is*` properties. Used for doing - * lookups in plugins. - * - * ```js - * app.is('foo'); - * console.log(app._name); - * //=> 'foo' - * console.log(app.isFoo); - * //=> true - * app.is('bar'); - * console.log(app.isFoo); - * //=> true - * console.log(app.isBar); - * //=> true - * console.log(app._name); - * //=> 'bar' - * ``` - * @name .is - * @param {String} `name` - * @return {Boolean} - * @api public - */ +/***/ }), +/* 781 */ +/***/ (function(module, exports, __webpack_require__) { - Base.prototype.is = function(name) { - if (typeof name !== 'string') { - throw new TypeError('expected name to be a string'); - } - this.define('is' + pascal(name), true); - this.define('_name', name); - this.define('_appname', name); - return this; - }; +"use strict"; +/*! + * to-object-path + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ - /** - * Returns true if a plugin has already been registered on an instance. - * - * Plugin implementors are encouraged to use this first thing in a plugin - * to prevent the plugin from being called more than once on the same - * instance. - * - * ```js - * var base = new Base(); - * base.use(function(app) { - * if (app.isRegistered('myPlugin')) return; - * // do stuff to `app` - * }); - * - * // to also record the plugin as being registered - * base.use(function(app) { - * if (app.isRegistered('myPlugin', true)) return; - * // do stuff to `app` - * }); - * ``` - * @name .isRegistered - * @emits `plugin` Emits the name of the plugin being registered. Useful for unit tests, to ensure plugins are only registered once. - * @param {String} `name` The plugin name. - * @param {Boolean} `register` If the plugin if not already registered, to record it as being registered pass `true` as the second argument. - * @return {Boolean} Returns true if a plugin is already registered. - * @api public - */ - Base.prototype.isRegistered = function(name, register) { - if (this.registered.hasOwnProperty(name)) { - return true; - } - if (register !== false) { - this.registered[name] = true; - this.emit('plugin', name); - } - return false; - }; - /** - * Define a plugin function to be called immediately upon init. Plugins are chainable - * and expose the following arguments to the plugin function: - * - * - `app`: the current instance of `Base` - * - `base`: the [first ancestor instance](#base) of `Base` - * - * ```js - * var app = new Base() - * .use(foo) - * .use(bar) - * .use(baz) - * ``` - * @name .use - * @param {Function} `fn` plugin function to call - * @return {Object} Returns the item instance for chaining. - * @api public - */ +var typeOf = __webpack_require__(756); - Base.prototype.use = function(fn) { - fn.call(this, this); - return this; - }; +module.exports = function toPath(args) { + if (typeOf(args) !== 'arguments') { + args = arguments; + } + return filter(args).join('.'); +}; - /** - * The `.define` method is used for adding non-enumerable property on the instance. - * Dot-notation is **not supported** with `define`. - * - * ```js - * // arbitrary `render` function using lodash `template` - * app.define('render', function(str, locals) { - * return _.template(str)(locals); - * }); - * ``` - * @name .define - * @param {String} `key` The name of the property to define. - * @param {any} `value` - * @return {Object} Returns the instance for chaining. - * @api public - */ +function filter(arr) { + var len = arr.length; + var idx = -1; + var res = []; - Base.prototype.define = function(key, val) { - if (isObject(key)) { - return this.visit('define', key); + while (++idx < len) { + var ele = arr[idx]; + if (typeOf(ele) === 'arguments' || Array.isArray(ele)) { + res.push.apply(res, filter(ele)); + } else if (typeof ele === 'string') { + res.push(ele); } - define(this, key, val); - return this; - }; + } + return res; +} - /** - * Mix property `key` onto the Base prototype. If base is inherited using - * `Base.extend` this method will be overridden by a new `mixin` method that will - * only add properties to the prototype of the inheriting application. - * - * ```js - * app.mixin('foo', function() { - * // do stuff - * }); - * ``` - * @name .mixin - * @param {String} `key` - * @param {Object|Array} `val` - * @return {Object} Returns the `base` instance for chaining. - * @api public - */ - Base.prototype.mixin = function(key, val) { - Base.prototype[key] = val; - return this; - }; +/***/ }), +/* 782 */ +/***/ (function(module, exports, __webpack_require__) { - /** - * Non-enumberable mixin array, used by the static [Base.mixin]() method. - */ +"use strict"; - Base.prototype.mixins = Base.prototype.mixins || []; - /** - * Getter/setter used when creating nested instances of `Base`, for storing a reference - * to the first ancestor instance. This works by setting an instance of `Base` on the `parent` - * property of a "child" instance. The `base` property defaults to the current instance if - * no `parent` property is defined. - * - * ```js - * // create an instance of `Base`, this is our first ("base") instance - * var first = new Base(); - * first.foo = 'bar'; // arbitrary property, to make it easier to see what's happening later - * - * // create another instance - * var second = new Base(); - * // create a reference to the first instance (`first`) - * second.parent = first; - * - * // create another instance - * var third = new Base(); - * // create a reference to the previous instance (`second`) - * // repeat this pattern every time a "child" instance is created - * third.parent = second; - * - * // we can always access the first instance using the `base` property - * console.log(first.base.foo); - * //=> 'bar' - * console.log(second.base.foo); - * //=> 'bar' - * console.log(third.base.foo); - * //=> 'bar' - * // and now you know how to get to third base ;) - * ``` - * @name .base - * @api public - */ +var isObject = __webpack_require__(743); +var union = __webpack_require__(783); +var get = __webpack_require__(784); +var set = __webpack_require__(785); - Object.defineProperty(Base.prototype, 'base', { - configurable: true, - get: function() { - return this.parent ? this.parent.base : this; - } - }); +module.exports = function unionValue(obj, prop, value) { + if (!isObject(obj)) { + throw new TypeError('union-value expects the first argument to be an object.'); + } - /** - * Static method for adding global plugin functions that will - * be added to an instance when created. - * - * ```js - * Base.use(function(app) { - * app.foo = 'bar'; - * }); - * var app = new Base(); - * console.log(app.foo); - * //=> 'bar' - * ``` - * @name #use - * @param {Function} `fn` Plugin function to use on each instance. - * @return {Object} Returns the `Base` constructor for chaining - * @api public - */ + if (typeof prop !== 'string') { + throw new TypeError('union-value expects `prop` to be a string.'); + } - define(Base, 'use', function(fn) { - fns.push(fn); - return Base; - }); + var arr = arrayify(get(obj, prop)); + set(obj, prop, union(arr, arrayify(value))); + return obj; +}; - /** - * Run an array of functions by passing each function - * to a method on the given object specified by the given property. - * - * @param {Object} `obj` Object containing method to use. - * @param {String} `prop` Name of the method on the object to use. - * @param {Array} `arr` Array of functions to pass to the method. - */ +function arrayify(val) { + if (val === null || typeof val === 'undefined') { + return []; + } + if (Array.isArray(val)) { + return val; + } + return [val]; +} - define(Base, 'run', function(obj, prop, arr) { - var len = arr.length, i = 0; - while (len--) { - obj[prop](arr[i++]); - } - return Base; - }); - /** - * Static method for inheriting the prototype and static methods of the `Base` class. - * This method greatly simplifies the process of creating inheritance-based applications. - * See [static-extend][] for more details. - * - * ```js - * var extend = cu.extend(Parent); - * Parent.extend(Child); - * - * // optional methods - * Parent.extend(Child, { - * foo: function() {}, - * bar: function() {} - * }); - * ``` - * @name #extend - * @param {Function} `Ctor` constructor to extend - * @param {Object} `methods` Optional prototype properties to mix in. - * @return {Object} Returns the `Base` constructor for chaining - * @api public - */ +/***/ }), +/* 783 */ +/***/ (function(module, exports, __webpack_require__) { - define(Base, 'extend', cu.extend(Base, function(Ctor, Parent) { - Ctor.prototype.mixins = Ctor.prototype.mixins || []; +"use strict"; - define(Ctor, 'mixin', function(fn) { - var mixin = fn(Ctor.prototype, Ctor); - if (typeof mixin === 'function') { - Ctor.prototype.mixins.push(mixin); - } - return Ctor; - }); - define(Ctor, 'mixins', function(Child) { - Base.run(Child, 'mixin', Ctor.prototype.mixins); - return Ctor; - }); +module.exports = function union(init) { + if (!Array.isArray(init)) { + throw new TypeError('arr-union expects the first argument to be an array.'); + } - Ctor.prototype.mixin = function(key, value) { - Ctor.prototype[key] = value; - return this; - }; - return Base; - })); + var len = arguments.length; + var i = 0; - /** - * Used for adding methods to the `Base` prototype, and/or to the prototype of child instances. - * When a mixin function returns a function, the returned function is pushed onto the `.mixins` - * array, making it available to be used on inheriting classes whenever `Base.mixins()` is - * called (e.g. `Base.mixins(Child)`). - * - * ```js - * Base.mixin(function(proto) { - * proto.foo = function(msg) { - * return 'foo ' + msg; - * }; - * }); - * ``` - * @name #mixin - * @param {Function} `fn` Function to call - * @return {Object} Returns the `Base` constructor for chaining - * @api public - */ + while (++i < len) { + var arg = arguments[i]; + if (!arg) continue; - define(Base, 'mixin', function(fn) { - var mixin = fn(Base.prototype, Base); - if (typeof mixin === 'function') { - Base.prototype.mixins.push(mixin); + if (!Array.isArray(arg)) { + arg = [arg]; } - return Base; - }); - /** - * Static method for running global mixin functions against a child constructor. - * Mixins must be registered before calling this method. - * - * ```js - * Base.extend(Child); - * Base.mixins(Child); - * ``` - * @name #mixins - * @param {Function} `Child` Constructor function of a child class - * @return {Object} Returns the `Base` constructor for chaining - * @api public - */ + for (var j = 0; j < arg.length; j++) { + var ele = arg[j]; - define(Base, 'mixins', function(Child) { - Base.run(Child, 'mixin', Base.prototype.mixins); - return Base; - }); + if (init.indexOf(ele) >= 0) { + continue; + } + init.push(ele); + } + } + return init; +}; - /** - * Similar to `util.inherit`, but copies all static properties, prototype properties, and - * getters/setters from `Provider` to `Receiver`. See [class-utils][]{#inherit} for more details. - * - * ```js - * Base.inherit(Foo, Bar); - * ``` - * @name #inherit - * @param {Function} `Receiver` Receiving (child) constructor - * @param {Function} `Provider` Providing (parent) constructor - * @return {Object} Returns the `Base` constructor for chaining - * @api public - */ - define(Base, 'inherit', cu.inherit); - define(Base, 'bubble', cu.bubble); - return Base; -} +/***/ }), +/* 784 */ +/***/ (function(module, exports) { -/** - * Expose `Base` with default settings +/*! + * get-value + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. */ -module.exports = namespace(); +module.exports = function(obj, prop, a, b, c) { + if (!isObject(obj) || !prop) { + return obj; + } -/** - * Allow users to define a namespace - */ + prop = toString(prop); -module.exports.namespace = namespace; + // allowing for multiple properties to be passed as + // a string or array, but much faster (3-4x) than doing + // `[].slice.call(arguments)` + if (a) prop += '.' + toString(a); + if (b) prop += '.' + toString(b); + if (c) prop += '.' + toString(c); + + if (prop in obj) { + return obj[prop]; + } + + var segs = prop.split('.'); + var len = segs.length; + var i = -1; + + while (obj && (++i < len)) { + var key = segs[i]; + while (key[key.length - 1] === '\\') { + key = key.slice(0, -1) + '.' + segs[++i]; + } + obj = obj[key]; + } + return obj; +}; + +function isObject(val) { + return val !== null && (typeof val === 'object' || typeof val === 'function'); +} + +function toString(val) { + if (!val) return ''; + if (Array.isArray(val)) { + return val.join('.'); + } + return val; +} /***/ }), -/* 769 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * define-property + * set-value * - * Copyright (c) 2015, 2017, Jon Schlinkert. + * Copyright (c) 2014-2015, 2017, Jon Schlinkert. * Released under the MIT License. */ -var isDescriptor = __webpack_require__(759); +var split = __webpack_require__(747); +var extend = __webpack_require__(742); +var isPlainObject = __webpack_require__(750); +var isObject = __webpack_require__(743); -module.exports = function defineProperty(obj, prop, val) { - if (typeof obj !== 'object' && typeof obj !== 'function') { - throw new TypeError('expected an object or function.'); +module.exports = function(obj, prop, val) { + if (!isObject(obj)) { + return obj; + } + + if (Array.isArray(prop)) { + prop = [].concat.apply([], prop).join('.'); } if (typeof prop !== 'string') { - throw new TypeError('expected `prop` to be a string.'); + return obj; } - if (isDescriptor(val) && ('set' in val || 'get' in val)) { - return Object.defineProperty(obj, prop, val); + var keys = split(prop, {sep: '.', brackets: true}).filter(isValidKey); + var len = keys.length; + var idx = -1; + var current = obj; + + while (++idx < len) { + var key = keys[idx]; + if (idx !== len - 1) { + if (!isObject(current[key])) { + current[key] = {}; + } + current = current[key]; + continue; + } + + if (isPlainObject(current[key]) && isPlainObject(val)) { + current[key] = extend({}, current[key], val); + } else { + current[key] = val; + } } - return Object.defineProperty(obj, prop, { - configurable: true, - enumerable: false, - writable: true, - value: val - }); + return obj; }; +function isValidKey(key) { + return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; +} + /***/ }), -/* 770 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; - - -var isObject = __webpack_require__(747); -var Emitter = __webpack_require__(771); -var visit = __webpack_require__(772); -var toPath = __webpack_require__(775); -var union = __webpack_require__(776); -var del = __webpack_require__(780); -var get = __webpack_require__(778); -var has = __webpack_require__(785); -var set = __webpack_require__(779); - -/** - * Create a `Cache` constructor that when instantiated will - * store values on the given `prop`. - * - * ```js - * var Cache = require('cache-base').namespace('data'); - * var cache = new Cache(); +/*! + * unset-value * - * cache.set('foo', 'bar'); - * //=> {data: {foo: 'bar'}} - * ``` - * @param {String} `prop` The property name to use for storing values. - * @return {Function} Returns a custom `Cache` constructor - * @api public + * Copyright (c) 2015, 2017, Jon Schlinkert. + * Released under the MIT License. */ -function namespace(prop) { - /** - * Create a new `Cache`. Internally the `Cache` constructor is created using - * the `namespace` function, with `cache` defined as the storage object. - * - * ```js - * var app = new Cache(); - * ``` - * @param {Object} `cache` Optionally pass an object to initialize with. - * @constructor - * @api public - */ - function Cache(cache) { - if (prop) { - this[prop] = {}; - } - if (cache) { - this.set(cache); - } +var isObject = __webpack_require__(751); +var has = __webpack_require__(787); + +module.exports = function unset(obj, prop) { + if (!isObject(obj)) { + throw new TypeError('expected an object.'); + } + if (obj.hasOwnProperty(prop)) { + delete obj[prop]; + return true; } - /** - * Inherit Emitter - */ + if (has(obj, prop)) { + var segs = prop.split('.'); + var last = segs.pop(); + while (segs.length && segs[segs.length - 1].slice(-1) === '\\') { + last = segs.pop().slice(0, -1) + '.' + last; + } + while (segs.length) obj = obj[prop = segs.shift()]; + return (delete obj[last]); + } + return true; +}; - Emitter(Cache.prototype); - /** - * Assign `value` to `key`. Also emits `set` with - * the key and value. - * - * ```js - * app.on('set', function(key, val) { - * // do something when `set` is emitted - * }); - * - * app.set(key, value); - * - * // also takes an object or array - * app.set({name: 'Halle'}); - * app.set([{foo: 'bar'}, {baz: 'quux'}]); - * console.log(app); - * //=> {name: 'Halle', foo: 'bar', baz: 'quux'} - * ``` - * - * @name .set - * @emits `set` with `key` and `value` as arguments. - * @param {String} `key` - * @param {any} `value` - * @return {Object} Returns the instance for chaining. - * @api public - */ +/***/ }), +/* 787 */ +/***/ (function(module, exports, __webpack_require__) { - Cache.prototype.set = function(key, val) { - if (Array.isArray(key) && arguments.length === 2) { - key = toPath(key); - } - if (isObject(key) || Array.isArray(key)) { - this.visit('set', key); - } else { - set(prop ? this[prop] : this, key, val); - this.emit('set', key, val); - } - return this; - }; +"use strict"; +/*! + * has-value + * + * Copyright (c) 2014-2016, Jon Schlinkert. + * Licensed under the MIT License. + */ - /** - * Union `array` to `key`. Also emits `set` with - * the key and value. - * - * ```js - * app.union('a.b', ['foo']); - * app.union('a.b', ['bar']); - * console.log(app.get('a')); - * //=> {b: ['foo', 'bar']} - * ``` - * @name .union - * @param {String} `key` - * @param {any} `value` - * @return {Object} Returns the instance for chaining. - * @api public - */ - Cache.prototype.union = function(key, val) { - if (Array.isArray(key) && arguments.length === 2) { - key = toPath(key); - } - var ctx = prop ? this[prop] : this; - union(ctx, key, arrayify(val)); - this.emit('union', val); - return this; - }; - /** - * Return the value of `key`. Dot notation may be used - * to get [nested property values][get-value]. - * - * ```js - * app.set('a.b.c', 'd'); - * app.get('a.b'); - * //=> {c: 'd'} - * - * app.get(['a', 'b']); - * //=> {c: 'd'} - * ``` - * - * @name .get - * @emits `get` with `key` and `value` as arguments. - * @param {String} `key` The name of the property to get. Dot-notation may be used. - * @return {any} Returns the value of `key` - * @api public - */ +var isObject = __webpack_require__(788); +var hasValues = __webpack_require__(790); +var get = __webpack_require__(784); - Cache.prototype.get = function(key) { - key = toPath(arguments); +module.exports = function(obj, prop, noZero) { + if (isObject(obj)) { + return hasValues(get(obj, prop), noZero); + } + return hasValues(obj, prop); +}; - var ctx = prop ? this[prop] : this; - var val = get(ctx, key); - this.emit('get', key, val); - return val; - }; +/***/ }), +/* 788 */ +/***/ (function(module, exports, __webpack_require__) { - /** - * Return true if app has a stored value for `key`, - * false only if value is `undefined`. - * - * ```js - * app.set('foo', 'bar'); - * app.has('foo'); - * //=> true - * ``` - * - * @name .has - * @emits `has` with `key` and true or false as arguments. - * @param {String} `key` - * @return {Boolean} - * @api public - */ +"use strict"; +/*! + * isobject + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. + */ - Cache.prototype.has = function(key) { - key = toPath(arguments); - var ctx = prop ? this[prop] : this; - var val = get(ctx, key); - var has = typeof val !== 'undefined'; - this.emit('has', key, has); - return has; - }; +var isArray = __webpack_require__(789); - /** - * Delete one or more properties from the instance. - * - * ```js - * app.del(); // delete all - * // or - * app.del('foo'); - * // or - * app.del(['foo', 'bar']); - * ``` - * @name .del - * @emits `del` with the `key` as the only argument. - * @param {String|Array} `key` Property name or array of property names. - * @return {Object} Returns the instance for chaining. - * @api public - */ +module.exports = function isObject(val) { + return val != null && typeof val === 'object' && isArray(val) === false; +}; - Cache.prototype.del = function(key) { - if (Array.isArray(key)) { - this.visit('del', key); - } else { - del(prop ? this[prop] : this, key); - this.emit('del', key); - } - return this; - }; - /** - * Reset the entire cache to an empty object. - * - * ```js - * app.clear(); - * ``` - * @api public - */ +/***/ }), +/* 789 */ +/***/ (function(module, exports) { - Cache.prototype.clear = function() { - if (prop) { - this[prop] = {}; - } - }; +var toString = {}.toString; - /** - * Visit `method` over the properties in the given object, or map - * visit over the object-elements in an array. - * - * @name .visit - * @param {String} `method` The name of the `base` method to call. - * @param {Object|Array} `val` The object or array to iterate over. - * @return {Object} Returns the instance for chaining. - * @api public - */ +module.exports = Array.isArray || function (arr) { + return toString.call(arr) == '[object Array]'; +}; - Cache.prototype.visit = function(method, val) { - visit(this, method, val); - return this; - }; - return Cache; -} +/***/ }), +/* 790 */ +/***/ (function(module, exports, __webpack_require__) { -/** - * Cast val to an array +"use strict"; +/*! + * has-values + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. */ -function arrayify(val) { - return val ? (Array.isArray(val) ? val : [val]) : []; -} -/** - * Expose `Cache` - */ -module.exports = namespace(); +module.exports = function hasValue(o, noZero) { + if (o === null || o === undefined) { + return false; + } -/** - * Expose `Cache.namespace` - */ + if (typeof o === 'boolean') { + return true; + } -module.exports.namespace = namespace; + if (typeof o === 'number') { + if (o === 0 && noZero === true) { + return false; + } + return true; + } + + if (o.length !== undefined) { + return o.length !== 0; + } + + for (var key in o) { + if (o.hasOwnProperty(key)) { + return true; + } + } + return false; +}; /***/ }), -/* 771 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { - -/** - * Expose `Emitter`. - */ - -if (true) { - module.exports = Emitter; -} - -/** - * Initialize a new `Emitter`. - * - * @api public - */ - -function Emitter(obj) { - if (obj) return mixin(obj); -}; - -/** - * Mixin the emitter properties. - * - * @param {Object} obj - * @return {Object} - * @api private - */ - -function mixin(obj) { - for (var key in Emitter.prototype) { - obj[key] = Emitter.prototype[key]; - } - return obj; -} - -/** - * Listen on the given `event` with `fn`. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.on = -Emitter.prototype.addEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - (this._callbacks['$' + event] = this._callbacks['$' + event] || []) - .push(fn); - return this; -}; - -/** - * Adds an `event` listener that will be invoked a single - * time then automatically removed. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.once = function(event, fn){ - function on() { - this.off(event, on); - fn.apply(this, arguments); - } - - on.fn = fn; - this.on(event, on); - return this; -}; - -/** - * Remove the given callback for `event` or all - * registered callbacks. - * - * @param {String} event - * @param {Function} fn - * @return {Emitter} - * @api public - */ - -Emitter.prototype.off = -Emitter.prototype.removeListener = -Emitter.prototype.removeAllListeners = -Emitter.prototype.removeEventListener = function(event, fn){ - this._callbacks = this._callbacks || {}; - - // all - if (0 == arguments.length) { - this._callbacks = {}; - return this; - } - - // specific event - var callbacks = this._callbacks['$' + event]; - if (!callbacks) return this; - - // remove all handlers - if (1 == arguments.length) { - delete this._callbacks['$' + event]; - return this; - } - - // remove specific handler - var cb; - for (var i = 0; i < callbacks.length; i++) { - cb = callbacks[i]; - if (cb === fn || cb.fn === fn) { - callbacks.splice(i, 1); - break; - } - } - return this; -}; - -/** - * Emit `event` with the given args. - * - * @param {String} event - * @param {Mixed} ... - * @return {Emitter} - */ - -Emitter.prototype.emit = function(event){ - this._callbacks = this._callbacks || {}; - var args = [].slice.call(arguments, 1) - , callbacks = this._callbacks['$' + event]; - - if (callbacks) { - callbacks = callbacks.slice(0); - for (var i = 0, len = callbacks.length; i < len; ++i) { - callbacks[i].apply(this, args); - } - } - - return this; -}; - -/** - * Return array of callbacks for `event`. - * - * @param {String} event - * @return {Array} - * @api public - */ - -Emitter.prototype.listeners = function(event){ - this._callbacks = this._callbacks || {}; - return this._callbacks['$' + event] || []; -}; - -/** - * Check if this emitter has `event` handlers. - * - * @param {String} event - * @return {Boolean} - * @api public - */ - -Emitter.prototype.hasListeners = function(event){ - return !! this.listeners(event).length; -}; +"use strict"; +/*! + * has-value + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +var isObject = __webpack_require__(751); +var hasValues = __webpack_require__(792); +var get = __webpack_require__(784); + +module.exports = function(val, prop) { + return hasValues(isObject(val) && prop ? get(val, prop) : val); +}; /***/ }), -/* 772 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * collection-visit + * has-values * - * Copyright (c) 2015, 2017, Jon Schlinkert. + * Copyright (c) 2014-2015, 2017, Jon Schlinkert. * Released under the MIT License. */ -var visit = __webpack_require__(773); -var mapVisit = __webpack_require__(774); - -module.exports = function(collection, method, val) { - var result; +var typeOf = __webpack_require__(793); +var isNumber = __webpack_require__(755); - if (typeof val === 'string' && (method in collection)) { - var args = [].slice.call(arguments, 2); - result = collection[method].apply(collection, args); - } else if (Array.isArray(val)) { - result = mapVisit.apply(null, arguments); - } else { - result = visit.apply(null, arguments); +module.exports = function hasValue(val) { + // is-number checks for NaN and other edge cases + if (isNumber(val)) { + return true; } - if (typeof result !== 'undefined') { - return result; + switch (typeOf(val)) { + case 'null': + case 'boolean': + case 'function': + return true; + case 'string': + case 'arguments': + return val.length !== 0; + case 'error': + return val.message !== ''; + case 'array': + var len = val.length; + if (len === 0) { + return false; + } + for (var i = 0; i < len; i++) { + if (hasValue(val[i])) { + return true; + } + } + return false; + case 'file': + case 'map': + case 'set': + return val.size !== 0; + case 'object': + var keys = Object.keys(val); + if (keys.length === 0) { + return false; + } + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (hasValue(val[key])) { + return true; + } + } + return false; + default: { + return false; + } } - - return collection; }; /***/ }), -/* 773 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; -/*! - * object-visit +var isBuffer = __webpack_require__(736); +var toString = Object.prototype.toString; + +/** + * Get the native `typeof` a value. * - * Copyright (c) 2015, 2017, Jon Schlinkert. - * Released under the MIT License. + * @param {*} `val` + * @return {*} Native javascript type */ +module.exports = function kindOf(val) { + // primitivies + if (typeof val === 'undefined') { + return 'undefined'; + } + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (typeof val === 'string' || val instanceof String) { + return 'string'; + } + if (typeof val === 'number' || val instanceof Number) { + return 'number'; + } + // functions + if (typeof val === 'function' || val instanceof Function) { + return 'function'; + } -var isObject = __webpack_require__(747); + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; + } -module.exports = function visit(thisArg, method, target, val) { - if (!isObject(thisArg) && typeof thisArg !== 'function') { - throw new Error('object-visit expects `thisArg` to be an object.'); + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; + } + if (val instanceof Date) { + return 'date'; } - if (typeof method !== 'string') { - throw new Error('object-visit expects `method` name to be a string'); + // other objects + var type = toString.call(val); + + if (type === '[object RegExp]') { + return 'regexp'; + } + if (type === '[object Date]') { + return 'date'; + } + if (type === '[object Arguments]') { + return 'arguments'; + } + if (type === '[object Error]') { + return 'error'; + } + if (type === '[object Promise]') { + return 'promise'; } - if (typeof thisArg[method] !== 'function') { - return thisArg; + // buffer + if (isBuffer(val)) { + return 'buffer'; } - var args = [].slice.call(arguments, 3); - target = target || {}; + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } - for (var key in target) { - var arr = [key, target[key]].concat(args); - thisArg[method].apply(thisArg, arr); + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; } - return thisArg; + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; + } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; }; /***/ }), -/* 774 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var util = __webpack_require__(29); -var visit = __webpack_require__(773); +var isExtendable = __webpack_require__(795); +var forIn = __webpack_require__(796); + +function mixinDeep(target, objects) { + var len = arguments.length, i = 0; + while (++i < len) { + var obj = arguments[i]; + if (isObject(obj)) { + forIn(obj, copy, target); + } + } + return target; +} /** - * Map `visit` over an array of objects. + * Copy properties from the source object to the + * target object. * - * @param {Object} `collection` The context in which to invoke `method` - * @param {String} `method` Name of the method to call on `collection` - * @param {Object} `arr` Array of objects. + * @param {*} `val` + * @param {String} `key` */ -module.exports = function mapVisit(collection, method, val) { - if (isObject(val)) { - return visit.apply(null, arguments); +function copy(val, key) { + if (!isValidKey(key)) { + return; } - if (!Array.isArray(val)) { - throw new TypeError('expected an array: ' + util.inspect(val)); + var obj = this[key]; + if (isObject(val) && isObject(obj)) { + mixinDeep(obj, val); + } else { + this[key] = val; } +} - var args = [].slice.call(arguments, 3); - - for (var i = 0; i < val.length; i++) { - var ele = val[i]; - if (isObject(ele)) { - visit.apply(null, [collection, method, ele].concat(args)); - } else { - collection[method].apply(collection, [ele].concat(args)); - } - } -}; +/** + * Returns true if `val` is an object or function. + * + * @param {any} val + * @return {Boolean} + */ function isObject(val) { - return val && (typeof val === 'function' || (!Array.isArray(val) && typeof val === 'object')); + return isExtendable(val) && !Array.isArray(val); } +/** + * Returns true if `key` is a valid key to use when extending objects. + * + * @param {String} `key` + * @return {Boolean} + */ + +function isValidKey(key) { + return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; +}; + +/** + * Expose `mixinDeep` + */ + +module.exports = mixinDeep; + /***/ }), -/* 775 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * to-object-path + * is-extendable * - * Copyright (c) 2015, Jon Schlinkert. - * Licensed under the MIT License. + * Copyright (c) 2015-2017, Jon Schlinkert. + * Released under the MIT License. */ -var typeOf = __webpack_require__(752); +var isPlainObject = __webpack_require__(750); -module.exports = function toPath(args) { - if (typeOf(args) !== 'arguments') { - args = arguments; - } - return filter(args).join('.'); +module.exports = function isExtendable(val) { + return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); }; -function filter(arr) { - var len = arr.length; - var idx = -1; - var res = []; - - while (++idx < len) { - var ele = arr[idx]; - if (typeOf(ele) === 'arguments' || Array.isArray(ele)) { - res.push.apply(res, filter(ele)); - } else if (typeof ele === 'string') { - res.push(ele); - } - } - return res; -} - /***/ }), -/* 776 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +/*! + * for-in + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ -var isObject = __webpack_require__(738); -var union = __webpack_require__(777); -var get = __webpack_require__(778); -var set = __webpack_require__(779); -module.exports = function unionValue(obj, prop, value) { - if (!isObject(obj)) { - throw new TypeError('union-value expects the first argument to be an object.'); +module.exports = function forIn(obj, fn, thisArg) { + for (var key in obj) { + if (fn.call(thisArg, obj[key], key, obj) === false) { + break; + } } +}; - if (typeof prop !== 'string') { - throw new TypeError('union-value expects `prop` to be a string.'); - } - var arr = arrayify(get(obj, prop)); - set(obj, prop, union(arr, arrayify(value))); - return obj; -}; +/***/ }), +/* 797 */ +/***/ (function(module, exports) { -function arrayify(val) { - if (val === null || typeof val === 'undefined') { - return []; - } - if (Array.isArray(val)) { - return val; +/*! + * pascalcase + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + +function pascalcase(str) { + if (typeof str !== 'string') { + throw new TypeError('expected a string.'); } - return [val]; + str = str.replace(/([A-Z])/g, ' $1'); + if (str.length === 1) { return str.toUpperCase(); } + str = str.replace(/^[\W_]+|[\W_]+$/g, '').toLowerCase(); + str = str.charAt(0).toUpperCase() + str.slice(1); + return str.replace(/[\W_]+(\w|$)/g, function (_, ch) { + return ch.toUpperCase(); + }); } +module.exports = pascalcase; + /***/ }), -/* 777 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -module.exports = function union(init) { - if (!Array.isArray(init)) { - throw new TypeError('arr-union expects the first argument to be an array.'); - } +var util = __webpack_require__(29); +var utils = __webpack_require__(799); - var len = arguments.length; - var i = 0; +/** + * Expose class utils + */ - while (++i < len) { - var arg = arguments[i]; - if (!arg) continue; +var cu = module.exports; - if (!Array.isArray(arg)) { - arg = [arg]; - } +/** + * Expose class utils: `cu` + */ - for (var j = 0; j < arg.length; j++) { - var ele = arg[j]; +cu.isObject = function isObject(val) { + return utils.isObj(val) || typeof val === 'function'; +}; - if (init.indexOf(ele) >= 0) { - continue; +/** + * Returns true if an array has any of the given elements, or an + * object has any of the give keys. + * + * ```js + * cu.has(['a', 'b', 'c'], 'c'); + * //=> true + * + * cu.has(['a', 'b', 'c'], ['c', 'z']); + * //=> true + * + * cu.has({a: 'b', c: 'd'}, ['c', 'z']); + * //=> true + * ``` + * @param {Object} `obj` + * @param {String|Array} `val` + * @return {Boolean} + * @api public + */ + +cu.has = function has(obj, val) { + val = cu.arrayify(val); + var len = val.length; + + if (cu.isObject(obj)) { + for (var key in obj) { + if (val.indexOf(key) > -1) { + return true; } - init.push(ele); } + + var keys = cu.nativeKeys(obj); + return cu.has(keys, val); } - return init; -}; + if (Array.isArray(obj)) { + var arr = obj; + while (len--) { + if (arr.indexOf(val[len]) > -1) { + return true; + } + } + return false; + } -/***/ }), -/* 778 */ -/***/ (function(module, exports) { + throw new TypeError('expected an array or object.'); +}; -/*! - * get-value +/** + * Returns true if an array or object has all of the given values. * - * Copyright (c) 2014-2015, Jon Schlinkert. - * Licensed under the MIT License. + * ```js + * cu.hasAll(['a', 'b', 'c'], 'c'); + * //=> true + * + * cu.hasAll(['a', 'b', 'c'], ['c', 'z']); + * //=> false + * + * cu.hasAll({a: 'b', c: 'd'}, ['c', 'z']); + * //=> false + * ``` + * @param {Object|Array} `val` + * @param {String|Array} `values` + * @return {Boolean} + * @api public */ -module.exports = function(obj, prop, a, b, c) { - if (!isObject(obj) || !prop) { - return obj; +cu.hasAll = function hasAll(val, values) { + values = cu.arrayify(values); + var len = values.length; + while (len--) { + if (!cu.has(val, values[len])) { + return false; + } } + return true; +}; - prop = toString(prop); - - // allowing for multiple properties to be passed as - // a string or array, but much faster (3-4x) than doing - // `[].slice.call(arguments)` - if (a) prop += '.' + toString(a); - if (b) prop += '.' + toString(b); - if (c) prop += '.' + toString(c); +/** + * Cast the given value to an array. + * + * ```js + * cu.arrayify('foo'); + * //=> ['foo'] + * + * cu.arrayify(['foo']); + * //=> ['foo'] + * ``` + * + * @param {String|Array} `val` + * @return {Array} + * @api public + */ - if (prop in obj) { - return obj[prop]; - } +cu.arrayify = function arrayify(val) { + return val ? (Array.isArray(val) ? val : [val]) : []; +}; - var segs = prop.split('.'); - var len = segs.length; - var i = -1; +/** + * Noop + */ - while (obj && (++i < len)) { - var key = segs[i]; - while (key[key.length - 1] === '\\') { - key = key.slice(0, -1) + '.' + segs[++i]; - } - obj = obj[key]; - } - return obj; +cu.noop = function noop() { + return; }; -function isObject(val) { - return val !== null && (typeof val === 'object' || typeof val === 'function'); -} +/** + * Returns the first argument passed to the function. + */ -function toString(val) { - if (!val) return ''; - if (Array.isArray(val)) { - return val.join('.'); - } +cu.identity = function identity(val) { return val; -} - - -/***/ }), -/* 779 */ -/***/ (function(module, exports, __webpack_require__) { +}; -"use strict"; -/*! - * set-value +/** + * Returns true if a value has a `contructor` * - * Copyright (c) 2014-2015, 2017, Jon Schlinkert. - * Released under the MIT License. + * ```js + * cu.hasConstructor({}); + * //=> true + * + * cu.hasConstructor(Object.create(null)); + * //=> false + * ``` + * @param {Object} `value` + * @return {Boolean} + * @api public */ +cu.hasConstructor = function hasConstructor(val) { + return cu.isObject(val) && typeof val.constructor !== 'undefined'; +}; +/** + * Get the native `ownPropertyNames` from the constructor of the + * given `object`. An empty array is returned if the object does + * not have a constructor. + * + * ```js + * cu.nativeKeys({a: 'b', b: 'c', c: 'd'}) + * //=> ['a', 'b', 'c'] + * + * cu.nativeKeys(function(){}) + * //=> ['length', 'caller'] + * ``` + * + * @param {Object} `obj` Object that has a `constructor`. + * @return {Array} Array of keys. + * @api public + */ -var split = __webpack_require__(743); -var extend = __webpack_require__(737); -var isPlainObject = __webpack_require__(746); -var isObject = __webpack_require__(738); +cu.nativeKeys = function nativeKeys(val) { + if (!cu.hasConstructor(val)) return []; + return Object.getOwnPropertyNames(val); +}; -module.exports = function(obj, prop, val) { - if (!isObject(obj)) { - return obj; - } +/** + * Returns property descriptor `key` if it's an "own" property + * of the given object. + * + * ```js + * function App() {} + * Object.defineProperty(App.prototype, 'count', { + * get: function() { + * return Object.keys(this).length; + * } + * }); + * cu.getDescriptor(App.prototype, 'count'); + * // returns: + * // { + * // get: [Function], + * // set: undefined, + * // enumerable: false, + * // configurable: false + * // } + * ``` + * + * @param {Object} `obj` + * @param {String} `key` + * @return {Object} Returns descriptor `key` + * @api public + */ - if (Array.isArray(prop)) { - prop = [].concat.apply([], prop).join('.'); +cu.getDescriptor = function getDescriptor(obj, key) { + if (!cu.isObject(obj)) { + throw new TypeError('expected an object.'); } - - if (typeof prop !== 'string') { - return obj; + if (typeof key !== 'string') { + throw new TypeError('expected key to be a string.'); } + return Object.getOwnPropertyDescriptor(obj, key); +}; - var keys = split(prop, {sep: '.', brackets: true}).filter(isValidKey); - var len = keys.length; - var idx = -1; - var current = obj; - - while (++idx < len) { - var key = keys[idx]; - if (idx !== len - 1) { - if (!isObject(current[key])) { - current[key] = {}; - } - current = current[key]; - continue; - } +/** + * Copy a descriptor from one object to another. + * + * ```js + * function App() {} + * Object.defineProperty(App.prototype, 'count', { + * get: function() { + * return Object.keys(this).length; + * } + * }); + * var obj = {}; + * cu.copyDescriptor(obj, App.prototype, 'count'); + * ``` + * @param {Object} `receiver` + * @param {Object} `provider` + * @param {String} `name` + * @return {Object} + * @api public + */ - if (isPlainObject(current[key]) && isPlainObject(val)) { - current[key] = extend({}, current[key], val); - } else { - current[key] = val; - } +cu.copyDescriptor = function copyDescriptor(receiver, provider, name) { + if (!cu.isObject(receiver)) { + throw new TypeError('expected receiving object to be an object.'); + } + if (!cu.isObject(provider)) { + throw new TypeError('expected providing object to be an object.'); + } + if (typeof name !== 'string') { + throw new TypeError('expected name to be a string.'); } - return obj; + var val = cu.getDescriptor(provider, name); + if (val) Object.defineProperty(receiver, name, val); }; -function isValidKey(key) { - return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; -} - - -/***/ }), -/* 780 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * unset-value +/** + * Copy static properties, prototype properties, and descriptors + * from one object to another. * - * Copyright (c) 2015, 2017, Jon Schlinkert. - * Released under the MIT License. + * @param {Object} `receiver` + * @param {Object} `provider` + * @param {String|Array} `omit` One or more properties to omit + * @return {Object} + * @api public */ +cu.copy = function copy(receiver, provider, omit) { + if (!cu.isObject(receiver)) { + throw new TypeError('expected receiving object to be an object.'); + } + if (!cu.isObject(provider)) { + throw new TypeError('expected providing object to be an object.'); + } + var props = Object.getOwnPropertyNames(provider); + var keys = Object.keys(provider); + var len = props.length, + key; + omit = cu.arrayify(omit); + + while (len--) { + key = props[len]; + if (cu.has(keys, key)) { + utils.define(receiver, key, provider[key]); + } else if (!(key in receiver) && !cu.has(omit, key)) { + cu.copyDescriptor(receiver, provider, key); + } + } +}; -var isObject = __webpack_require__(747); -var has = __webpack_require__(781); +/** + * Inherit the static properties, prototype properties, and descriptors + * from of an object. + * + * @param {Object} `receiver` + * @param {Object} `provider` + * @param {String|Array} `omit` One or more properties to omit + * @return {Object} + * @api public + */ -module.exports = function unset(obj, prop) { - if (!isObject(obj)) { - throw new TypeError('expected an object.'); +cu.inherit = function inherit(receiver, provider, omit) { + if (!cu.isObject(receiver)) { + throw new TypeError('expected receiving object to be an object.'); } - if (obj.hasOwnProperty(prop)) { - delete obj[prop]; - return true; + if (!cu.isObject(provider)) { + throw new TypeError('expected providing object to be an object.'); } - if (has(obj, prop)) { - var segs = prop.split('.'); - var last = segs.pop(); - while (segs.length && segs[segs.length - 1].slice(-1) === '\\') { - last = segs.pop().slice(0, -1) + '.' + last; - } - while (segs.length) obj = obj[prop = segs.shift()]; - return (delete obj[last]); + var keys = []; + for (var key in provider) { + keys.push(key); + receiver[key] = provider[key]; } - return true; -}; + keys = keys.concat(cu.arrayify(omit)); -/***/ }), -/* 781 */ -/***/ (function(module, exports, __webpack_require__) { + var a = provider.prototype || provider; + var b = receiver.prototype || receiver; + cu.copy(b, a, keys); +}; -"use strict"; -/*! - * has-value +/** + * Returns a function for extending the static properties, + * prototype properties, and descriptors from the `Parent` + * constructor onto `Child` constructors. * - * Copyright (c) 2014-2016, Jon Schlinkert. - * Licensed under the MIT License. + * ```js + * var extend = cu.extend(Parent); + * Parent.extend(Child); + * + * // optional methods + * Parent.extend(Child, { + * foo: function() {}, + * bar: function() {} + * }); + * ``` + * @param {Function} `Parent` Parent ctor + * @param {Function} `extend` Optional extend function to handle custom extensions. Useful when updating methods that require a specific prototype. + * @param {Function} `Child` Child ctor + * @param {Object} `proto` Optionally pass additional prototype properties to inherit. + * @return {Object} + * @api public */ +cu.extend = function() { + // keep it lazy, instead of assigning to `cu.extend` + return utils.staticExtend.apply(null, arguments); +}; +/** + * Bubble up events emitted from static methods on the Parent ctor. + * + * @param {Object} `Parent` + * @param {Array} `events` Event names to bubble up + * @api public + */ -var isObject = __webpack_require__(782); -var hasValues = __webpack_require__(784); -var get = __webpack_require__(778); - -module.exports = function(obj, prop, noZero) { - if (isObject(obj)) { - return hasValues(get(obj, prop), noZero); - } - return hasValues(obj, prop); +cu.bubble = function(Parent, events) { + events = events || []; + Parent.bubble = function(Child, arr) { + if (Array.isArray(arr)) { + events = utils.union([], events, arr); + } + var len = events.length; + var idx = -1; + while (++idx < len) { + var name = events[idx]; + Parent.on(name, Child.emit.bind(Child, name)); + } + cu.bubble(Child, events); + }; }; /***/ }), -/* 782 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -/*! - * isobject - * - * Copyright (c) 2014-2015, Jon Schlinkert. - * Licensed under the MIT License. - */ +var utils = {}; -var isArray = __webpack_require__(783); -module.exports = function isObject(val) { - return val != null && typeof val === 'object' && isArray(val) === false; -}; +/** + * Lazily required module dependencies + */ -/***/ }), -/* 783 */ -/***/ (function(module, exports) { +utils.union = __webpack_require__(783); +utils.define = __webpack_require__(800); +utils.isObj = __webpack_require__(751); +utils.staticExtend = __webpack_require__(807); -var toString = {}.toString; -module.exports = Array.isArray || function (arr) { - return toString.call(arr) == '[object Array]'; -}; +/** + * Expose `utils` + */ + +module.exports = utils; /***/ }), -/* 784 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * has-values + * define-property * - * Copyright (c) 2014-2015, Jon Schlinkert. + * Copyright (c) 2015, Jon Schlinkert. * Licensed under the MIT License. */ -module.exports = function hasValue(o, noZero) { - if (o === null || o === undefined) { - return false; - } - - if (typeof o === 'boolean') { - return true; - } +var isDescriptor = __webpack_require__(801); - if (typeof o === 'number') { - if (o === 0 && noZero === true) { - return false; - } - return true; +module.exports = function defineProperty(obj, prop, val) { + if (typeof obj !== 'object' && typeof obj !== 'function') { + throw new TypeError('expected an object or function.'); } - if (o.length !== undefined) { - return o.length !== 0; + if (typeof prop !== 'string') { + throw new TypeError('expected `prop` to be a string.'); } - for (var key in o) { - if (o.hasOwnProperty(key)) { - return true; - } + if (isDescriptor(val) && ('set' in val || 'get' in val)) { + return Object.defineProperty(obj, prop, val); } - return false; -}; - - -/***/ }), -/* 785 */ -/***/ (function(module, exports, __webpack_require__) { -"use strict"; -/*! - * has-value - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Licensed under the MIT License. - */ - - - -var isObject = __webpack_require__(747); -var hasValues = __webpack_require__(786); -var get = __webpack_require__(778); - -module.exports = function(val, prop) { - return hasValues(isObject(val) && prop ? get(val, prop) : val); + return Object.defineProperty(obj, prop, { + configurable: true, + enumerable: false, + writable: true, + value: val + }); }; /***/ }), -/* 786 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * has-values + * is-descriptor * - * Copyright (c) 2014-2015, 2017, Jon Schlinkert. + * Copyright (c) 2015-2017, Jon Schlinkert. * Released under the MIT License. */ -var typeOf = __webpack_require__(787); -var isNumber = __webpack_require__(751); +var typeOf = __webpack_require__(802); +var isAccessor = __webpack_require__(803); +var isData = __webpack_require__(805); -module.exports = function hasValue(val) { - // is-number checks for NaN and other edge cases - if (isNumber(val)) { - return true; +module.exports = function isDescriptor(obj, key) { + if (typeOf(obj) !== 'object') { + return false; } - - switch (typeOf(val)) { - case 'null': - case 'boolean': - case 'function': - return true; - case 'string': - case 'arguments': - return val.length !== 0; - case 'error': - return val.message !== ''; - case 'array': - var len = val.length; - if (len === 0) { - return false; - } - for (var i = 0; i < len; i++) { - if (hasValue(val[i])) { - return true; - } - } - return false; - case 'file': - case 'map': - case 'set': - return val.size !== 0; - case 'object': - var keys = Object.keys(val); - if (keys.length === 0) { - return false; - } - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - if (hasValue(val[key])) { - return true; - } - } - return false; - default: { - return false; - } + if ('get' in obj) { + return isAccessor(obj, key); } + return isData(obj, key); }; /***/ }), -/* 787 */ -/***/ (function(module, exports, __webpack_require__) { +/* 802 */ +/***/ (function(module, exports) { -var isBuffer = __webpack_require__(734); var toString = Object.prototype.toString; /** @@ -90363,8 +91431,10 @@ var toString = Object.prototype.toString; */ module.exports = function kindOf(val) { + var type = typeof val; + // primitivies - if (typeof val === 'undefined') { + if (type === 'undefined') { return 'undefined'; } if (val === null) { @@ -90373,15 +91443,18 @@ module.exports = function kindOf(val) { if (val === true || val === false || val instanceof Boolean) { return 'boolean'; } - if (typeof val === 'string' || val instanceof String) { + if (type === 'string' || val instanceof String) { return 'string'; } - if (typeof val === 'number' || val instanceof Number) { + if (type === 'number' || val instanceof Number) { return 'number'; } // functions - if (typeof val === 'function' || val instanceof Function) { + if (type === 'function' || val instanceof Function) { + if (typeof val.constructor.name !== 'undefined' && val.constructor.name.slice(0, 9) === 'Generator') { + return 'generatorfunction'; + } return 'function'; } @@ -90399,7 +91472,7 @@ module.exports = function kindOf(val) { } // other objects - var type = toString.call(val); + type = toString.call(val); if (type === '[object RegExp]') { return 'regexp'; @@ -90438,7 +91511,20 @@ module.exports = function kindOf(val) { if (type === '[object Symbol]') { return 'symbol'; } - + + if (type === '[object Map Iterator]') { + return 'mapiterator'; + } + if (type === '[object Set Iterator]') { + return 'setiterator'; + } + if (type === '[object String Iterator]') { + return 'stringiterator'; + } + if (type === '[object Array Iterator]') { + return 'arrayiterator'; + } + // typed arrays if (type === '[object Int8Array]') { return 'int8array'; @@ -90472,551 +91558,402 @@ module.exports = function kindOf(val) { return 'object'; }; - -/***/ }), -/* 788 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -var isExtendable = __webpack_require__(789); -var forIn = __webpack_require__(790); - -function mixinDeep(target, objects) { - var len = arguments.length, i = 0; - while (++i < len) { - var obj = arguments[i]; - if (isObject(obj)) { - forIn(obj, copy, target); - } - } - return target; -} - -/** - * Copy properties from the source object to the - * target object. - * - * @param {*} `val` - * @param {String} `key` - */ - -function copy(val, key) { - if (!isValidKey(key)) { - return; - } - - var obj = this[key]; - if (isObject(val) && isObject(obj)) { - mixinDeep(obj, val); - } else { - this[key] = val; - } -} - /** - * Returns true if `val` is an object or function. - * - * @param {any} val - * @return {Boolean} + * If you need to support Safari 5-7 (8-10 yr-old browser), + * take a look at https://github.com/feross/is-buffer */ -function isObject(val) { - return isExtendable(val) && !Array.isArray(val); +function isBuffer(val) { + return val.constructor + && typeof val.constructor.isBuffer === 'function' + && val.constructor.isBuffer(val); } -/** - * Returns true if `key` is a valid key to use when extending objects. - * - * @param {String} `key` - * @return {Boolean} - */ - -function isValidKey(key) { - return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; -}; - -/** - * Expose `mixinDeep` - */ - -module.exports = mixinDeep; - - -/***/ }), -/* 789 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/*! - * is-extendable - * - * Copyright (c) 2015-2017, Jon Schlinkert. - * Released under the MIT License. - */ - - - -var isPlainObject = __webpack_require__(746); - -module.exports = function isExtendable(val) { - return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); -}; - /***/ }), -/* 790 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * for-in - * - * Copyright (c) 2014-2017, Jon Schlinkert. - * Released under the MIT License. - */ - - - -module.exports = function forIn(obj, fn, thisArg) { - for (var key in obj) { - if (fn.call(thisArg, obj[key], key, obj) === false) { - break; - } - } -}; - - -/***/ }), -/* 791 */ -/***/ (function(module, exports) { - -/*! - * pascalcase + * is-accessor-descriptor * * Copyright (c) 2015, Jon Schlinkert. * Licensed under the MIT License. */ -function pascalcase(str) { - if (typeof str !== 'string') { - throw new TypeError('expected a string.'); - } - str = str.replace(/([A-Z])/g, ' $1'); - if (str.length === 1) { return str.toUpperCase(); } - str = str.replace(/^[\W_]+|[\W_]+$/g, '').toLowerCase(); - str = str.charAt(0).toUpperCase() + str.slice(1); - return str.replace(/[\W_]+(\w|$)/g, function (_, ch) { - return ch.toUpperCase(); - }); -} - -module.exports = pascalcase; - - -/***/ }), -/* 792 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -var util = __webpack_require__(29); -var utils = __webpack_require__(793); - -/** - * Expose class utils - */ - -var cu = module.exports; -/** - * Expose class utils: `cu` - */ +var typeOf = __webpack_require__(804); -cu.isObject = function isObject(val) { - return utils.isObj(val) || typeof val === 'function'; +// accessor descriptor properties +var accessor = { + get: 'function', + set: 'function', + configurable: 'boolean', + enumerable: 'boolean' }; -/** - * Returns true if an array has any of the given elements, or an - * object has any of the give keys. - * - * ```js - * cu.has(['a', 'b', 'c'], 'c'); - * //=> true - * - * cu.has(['a', 'b', 'c'], ['c', 'z']); - * //=> true - * - * cu.has({a: 'b', c: 'd'}, ['c', 'z']); - * //=> true - * ``` - * @param {Object} `obj` - * @param {String|Array} `val` - * @return {Boolean} - * @api public - */ +function isAccessorDescriptor(obj, prop) { + if (typeof prop === 'string') { + var val = Object.getOwnPropertyDescriptor(obj, prop); + return typeof val !== 'undefined'; + } -cu.has = function has(obj, val) { - val = cu.arrayify(val); - var len = val.length; + if (typeOf(obj) !== 'object') { + return false; + } - if (cu.isObject(obj)) { - for (var key in obj) { - if (val.indexOf(key) > -1) { - return true; - } - } + if (has(obj, 'value') || has(obj, 'writable')) { + return false; + } - var keys = cu.nativeKeys(obj); - return cu.has(keys, val); + if (!has(obj, 'get') || typeof obj.get !== 'function') { + return false; } - if (Array.isArray(obj)) { - var arr = obj; - while (len--) { - if (arr.indexOf(val[len]) > -1) { - return true; - } - } + // tldr: it's valid to have "set" be undefined + // "set" might be undefined if `Object.getOwnPropertyDescriptor` + // was used to get the value, and only `get` was defined by the user + if (has(obj, 'set') && typeof obj[key] !== 'function' && typeof obj[key] !== 'undefined') { return false; } - throw new TypeError('expected an array or object.'); -}; + for (var key in obj) { + if (!accessor.hasOwnProperty(key)) { + continue; + } -/** - * Returns true if an array or object has all of the given values. - * - * ```js - * cu.hasAll(['a', 'b', 'c'], 'c'); - * //=> true - * - * cu.hasAll(['a', 'b', 'c'], ['c', 'z']); - * //=> false - * - * cu.hasAll({a: 'b', c: 'd'}, ['c', 'z']); - * //=> false - * ``` - * @param {Object|Array} `val` - * @param {String|Array} `values` - * @return {Boolean} - * @api public - */ + if (typeOf(obj[key]) === accessor[key]) { + continue; + } -cu.hasAll = function hasAll(val, values) { - values = cu.arrayify(values); - var len = values.length; - while (len--) { - if (!cu.has(val, values[len])) { + if (typeof obj[key] !== 'undefined') { return false; } } return true; -}; - -/** - * Cast the given value to an array. - * - * ```js - * cu.arrayify('foo'); - * //=> ['foo'] - * - * cu.arrayify(['foo']); - * //=> ['foo'] - * ``` - * - * @param {String|Array} `val` - * @return {Array} - * @api public - */ - -cu.arrayify = function arrayify(val) { - return val ? (Array.isArray(val) ? val : [val]) : []; -}; - -/** - * Noop - */ - -cu.noop = function noop() { - return; -}; - -/** - * Returns the first argument passed to the function. - */ +} -cu.identity = function identity(val) { - return val; -}; +function has(obj, key) { + return {}.hasOwnProperty.call(obj, key); +} /** - * Returns true if a value has a `contructor` - * - * ```js - * cu.hasConstructor({}); - * //=> true - * - * cu.hasConstructor(Object.create(null)); - * //=> false - * ``` - * @param {Object} `value` - * @return {Boolean} - * @api public + * Expose `isAccessorDescriptor` */ -cu.hasConstructor = function hasConstructor(val) { - return cu.isObject(val) && typeof val.constructor !== 'undefined'; -}; +module.exports = isAccessorDescriptor; -/** - * Get the native `ownPropertyNames` from the constructor of the - * given `object`. An empty array is returned if the object does - * not have a constructor. - * - * ```js - * cu.nativeKeys({a: 'b', b: 'c', c: 'd'}) - * //=> ['a', 'b', 'c'] - * - * cu.nativeKeys(function(){}) - * //=> ['length', 'caller'] - * ``` - * - * @param {Object} `obj` Object that has a `constructor`. - * @return {Array} Array of keys. - * @api public - */ -cu.nativeKeys = function nativeKeys(val) { - if (!cu.hasConstructor(val)) return []; - return Object.getOwnPropertyNames(val); -}; +/***/ }), +/* 804 */ +/***/ (function(module, exports, __webpack_require__) { + +var isBuffer = __webpack_require__(736); +var toString = Object.prototype.toString; /** - * Returns property descriptor `key` if it's an "own" property - * of the given object. - * - * ```js - * function App() {} - * Object.defineProperty(App.prototype, 'count', { - * get: function() { - * return Object.keys(this).length; - * } - * }); - * cu.getDescriptor(App.prototype, 'count'); - * // returns: - * // { - * // get: [Function], - * // set: undefined, - * // enumerable: false, - * // configurable: false - * // } - * ``` + * Get the native `typeof` a value. * - * @param {Object} `obj` - * @param {String} `key` - * @return {Object} Returns descriptor `key` - * @api public + * @param {*} `val` + * @return {*} Native javascript type */ -cu.getDescriptor = function getDescriptor(obj, key) { - if (!cu.isObject(obj)) { - throw new TypeError('expected an object.'); +module.exports = function kindOf(val) { + // primitivies + if (typeof val === 'undefined') { + return 'undefined'; } - if (typeof key !== 'string') { - throw new TypeError('expected key to be a string.'); + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (typeof val === 'string' || val instanceof String) { + return 'string'; + } + if (typeof val === 'number' || val instanceof Number) { + return 'number'; } - return Object.getOwnPropertyDescriptor(obj, key); -}; -/** - * Copy a descriptor from one object to another. - * - * ```js - * function App() {} - * Object.defineProperty(App.prototype, 'count', { - * get: function() { - * return Object.keys(this).length; - * } - * }); - * var obj = {}; - * cu.copyDescriptor(obj, App.prototype, 'count'); - * ``` - * @param {Object} `receiver` - * @param {Object} `provider` - * @param {String} `name` - * @return {Object} - * @api public - */ + // functions + if (typeof val === 'function' || val instanceof Function) { + return 'function'; + } -cu.copyDescriptor = function copyDescriptor(receiver, provider, name) { - if (!cu.isObject(receiver)) { - throw new TypeError('expected receiving object to be an object.'); + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; } - if (!cu.isObject(provider)) { - throw new TypeError('expected providing object to be an object.'); + + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; } - if (typeof name !== 'string') { - throw new TypeError('expected name to be a string.'); + if (val instanceof Date) { + return 'date'; } - var val = cu.getDescriptor(provider, name); - if (val) Object.defineProperty(receiver, name, val); -}; - -/** - * Copy static properties, prototype properties, and descriptors - * from one object to another. - * - * @param {Object} `receiver` - * @param {Object} `provider` - * @param {String|Array} `omit` One or more properties to omit - * @return {Object} - * @api public - */ + // other objects + var type = toString.call(val); -cu.copy = function copy(receiver, provider, omit) { - if (!cu.isObject(receiver)) { - throw new TypeError('expected receiving object to be an object.'); + if (type === '[object RegExp]') { + return 'regexp'; } - if (!cu.isObject(provider)) { - throw new TypeError('expected providing object to be an object.'); + if (type === '[object Date]') { + return 'date'; + } + if (type === '[object Arguments]') { + return 'arguments'; + } + if (type === '[object Error]') { + return 'error'; } - var props = Object.getOwnPropertyNames(provider); - var keys = Object.keys(provider); - var len = props.length, - key; - omit = cu.arrayify(omit); - while (len--) { - key = props[len]; + // buffer + if (isBuffer(val)) { + return 'buffer'; + } - if (cu.has(keys, key)) { - utils.define(receiver, key, provider[key]); - } else if (!(key in receiver) && !cu.has(omit, key)) { - cu.copyDescriptor(receiver, provider, key); - } + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } + + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; + } + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; }; -/** - * Inherit the static properties, prototype properties, and descriptors - * from of an object. + +/***/ }), +/* 805 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-data-descriptor * - * @param {Object} `receiver` - * @param {Object} `provider` - * @param {String|Array} `omit` One or more properties to omit - * @return {Object} - * @api public + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. */ -cu.inherit = function inherit(receiver, provider, omit) { - if (!cu.isObject(receiver)) { - throw new TypeError('expected receiving object to be an object.'); + + +var typeOf = __webpack_require__(806); + +// data descriptor properties +var data = { + configurable: 'boolean', + enumerable: 'boolean', + writable: 'boolean' +}; + +function isDataDescriptor(obj, prop) { + if (typeOf(obj) !== 'object') { + return false; } - if (!cu.isObject(provider)) { - throw new TypeError('expected providing object to be an object.'); + + if (typeof prop === 'string') { + var val = Object.getOwnPropertyDescriptor(obj, prop); + return typeof val !== 'undefined'; } - var keys = []; - for (var key in provider) { - keys.push(key); - receiver[key] = provider[key]; + if (!('value' in obj) && !('writable' in obj)) { + return false; } - keys = keys.concat(cu.arrayify(omit)); + for (var key in obj) { + if (key === 'value') continue; - var a = provider.prototype || provider; - var b = receiver.prototype || receiver; - cu.copy(b, a, keys); -}; + if (!data.hasOwnProperty(key)) { + continue; + } -/** - * Returns a function for extending the static properties, - * prototype properties, and descriptors from the `Parent` - * constructor onto `Child` constructors. - * - * ```js - * var extend = cu.extend(Parent); - * Parent.extend(Child); - * - * // optional methods - * Parent.extend(Child, { - * foo: function() {}, - * bar: function() {} - * }); - * ``` - * @param {Function} `Parent` Parent ctor - * @param {Function} `extend` Optional extend function to handle custom extensions. Useful when updating methods that require a specific prototype. - * @param {Function} `Child` Child ctor - * @param {Object} `proto` Optionally pass additional prototype properties to inherit. - * @return {Object} - * @api public - */ + if (typeOf(obj[key]) === data[key]) { + continue; + } -cu.extend = function() { - // keep it lazy, instead of assigning to `cu.extend` - return utils.staticExtend.apply(null, arguments); -}; + if (typeof obj[key] !== 'undefined') { + return false; + } + } + return true; +} /** - * Bubble up events emitted from static methods on the Parent ctor. - * - * @param {Object} `Parent` - * @param {Array} `events` Event names to bubble up - * @api public + * Expose `isDataDescriptor` */ -cu.bubble = function(Parent, events) { - events = events || []; - Parent.bubble = function(Child, arr) { - if (Array.isArray(arr)) { - events = utils.union([], events, arr); - } - var len = events.length; - var idx = -1; - while (++idx < len) { - var name = events[idx]; - Parent.on(name, Child.emit.bind(Child, name)); - } - cu.bubble(Child, events); - }; -}; +module.exports = isDataDescriptor; /***/ }), -/* 793 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; +var isBuffer = __webpack_require__(736); +var toString = Object.prototype.toString; +/** + * Get the native `typeof` a value. + * + * @param {*} `val` + * @return {*} Native javascript type + */ -var utils = {}; +module.exports = function kindOf(val) { + // primitivies + if (typeof val === 'undefined') { + return 'undefined'; + } + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (typeof val === 'string' || val instanceof String) { + return 'string'; + } + if (typeof val === 'number' || val instanceof Number) { + return 'number'; + } + // functions + if (typeof val === 'function' || val instanceof Function) { + return 'function'; + } + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; + } -/** - * Lazily required module dependencies - */ + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; + } + if (val instanceof Date) { + return 'date'; + } -utils.union = __webpack_require__(777); -utils.define = __webpack_require__(729); -utils.isObj = __webpack_require__(747); -utils.staticExtend = __webpack_require__(794); + // other objects + var type = toString.call(val); + + if (type === '[object RegExp]') { + return 'regexp'; + } + if (type === '[object Date]') { + return 'date'; + } + if (type === '[object Arguments]') { + return 'arguments'; + } + if (type === '[object Error]') { + return 'error'; + } + // buffer + if (isBuffer(val)) { + return 'buffer'; + } -/** - * Expose `utils` - */ + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } -module.exports = utils; + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; + } + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; + } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; +}; /***/ }), -/* 794 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91029,8 +91966,8 @@ module.exports = utils; -var copy = __webpack_require__(795); -var define = __webpack_require__(729); +var copy = __webpack_require__(808); +var define = __webpack_require__(800); var util = __webpack_require__(29); /** @@ -91113,15 +92050,15 @@ module.exports = extend; /***/ }), -/* 795 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); -var copyDescriptor = __webpack_require__(796); -var define = __webpack_require__(729); +var typeOf = __webpack_require__(756); +var copyDescriptor = __webpack_require__(809); +var define = __webpack_require__(800); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91294,7 +92231,7 @@ module.exports.has = has; /***/ }), -/* 796 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91382,16 +92319,16 @@ function isObject(val) { /***/ }), -/* 797 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:compiler'); -var utils = __webpack_require__(806); +var use = __webpack_require__(811); +var define = __webpack_require__(800); +var debug = __webpack_require__(813)('snapdragon:compiler'); +var utils = __webpack_require__(819); /** * Create a new `Compiler` with the given `options`. @@ -91545,7 +92482,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(825); + var sourcemaps = __webpack_require__(838); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91566,7 +92503,7 @@ module.exports = Compiler; /***/ }), -/* 798 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91579,7 +92516,7 @@ module.exports = Compiler; -var utils = __webpack_require__(799); +var utils = __webpack_require__(812); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91694,7 +92631,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 799 */ +/* 812 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91708,8 +92645,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(729); -utils.isObject = __webpack_require__(747); +utils.define = __webpack_require__(800); +utils.isObject = __webpack_require__(751); utils.isString = function(val) { @@ -91724,7 +92661,7 @@ module.exports = utils; /***/ }), -/* 800 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91733,14 +92670,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(801); + module.exports = __webpack_require__(814); } else { - module.exports = __webpack_require__(804); + module.exports = __webpack_require__(817); } /***/ }), -/* 801 */ +/* 814 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91749,7 +92686,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(815); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -91931,7 +92868,7 @@ function localstorage() { /***/ }), -/* 802 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { @@ -91947,7 +92884,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(803); +exports.humanize = __webpack_require__(816); /** * The currently active debug mode names, and names to skip. @@ -92139,7 +93076,7 @@ function coerce(val) { /***/ }), -/* 803 */ +/* 816 */ /***/ (function(module, exports) { /** @@ -92297,14 +93234,14 @@ function plural(ms, n, name) { /***/ }), -/* 804 */ +/* 817 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(478); +var tty = __webpack_require__(484); var util = __webpack_require__(29); /** @@ -92313,7 +93250,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(815); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92492,7 +93429,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(818); stream = new net.Socket({ fd: fd, readable: false, @@ -92551,13 +93488,13 @@ exports.enable(load()); /***/ }), -/* 805 */ +/* 818 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 806 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92567,9 +93504,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(737); -exports.SourceMap = __webpack_require__(807); -exports.sourceMapResolve = __webpack_require__(818); +exports.extend = __webpack_require__(742); +exports.SourceMap = __webpack_require__(820); +exports.sourceMapResolve = __webpack_require__(831); /** * Convert backslash in the given string to forward slashes @@ -92612,7 +93549,7 @@ exports.last = function(arr, n) { /***/ }), -/* 807 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92620,13 +93557,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(814).SourceMapConsumer; -exports.SourceNode = __webpack_require__(817).SourceNode; +exports.SourceMapGenerator = __webpack_require__(821).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(827).SourceMapConsumer; +exports.SourceNode = __webpack_require__(830).SourceNode; /***/ }), -/* 808 */ +/* 821 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92636,10 +93573,10 @@ exports.SourceNode = __webpack_require__(817).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(809); -var util = __webpack_require__(811); -var ArraySet = __webpack_require__(812).ArraySet; -var MappingList = __webpack_require__(813).MappingList; +var base64VLQ = __webpack_require__(822); +var util = __webpack_require__(824); +var ArraySet = __webpack_require__(825).ArraySet; +var MappingList = __webpack_require__(826).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93048,7 +93985,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 809 */ +/* 822 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93088,7 +94025,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(810); +var base64 = __webpack_require__(823); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -93194,7 +94131,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 810 */ +/* 823 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93267,7 +94204,7 @@ exports.decode = function (charCode) { /***/ }), -/* 811 */ +/* 824 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93690,7 +94627,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 812 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93700,7 +94637,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(824); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -93817,7 +94754,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 813 */ +/* 826 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93827,7 +94764,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(824); /** * Determine whether mappingB is after mappingA with respect to generated @@ -93902,7 +94839,7 @@ exports.MappingList = MappingList; /***/ }), -/* 814 */ +/* 827 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93912,11 +94849,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); -var binarySearch = __webpack_require__(815); -var ArraySet = __webpack_require__(812).ArraySet; -var base64VLQ = __webpack_require__(809); -var quickSort = __webpack_require__(816).quickSort; +var util = __webpack_require__(824); +var binarySearch = __webpack_require__(828); +var ArraySet = __webpack_require__(825).ArraySet; +var base64VLQ = __webpack_require__(822); +var quickSort = __webpack_require__(829).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -94990,7 +95927,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 815 */ +/* 828 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95107,7 +96044,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 816 */ +/* 829 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95227,7 +96164,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 817 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95237,8 +96174,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -var util = __webpack_require__(811); +var SourceMapGenerator = __webpack_require__(821).SourceMapGenerator; +var util = __webpack_require__(824); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95646,17 +96583,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 818 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(819) -var resolveUrl = __webpack_require__(820) -var decodeUriComponent = __webpack_require__(821) -var urix = __webpack_require__(823) -var atob = __webpack_require__(824) +var sourceMappingURL = __webpack_require__(832) +var resolveUrl = __webpack_require__(833) +var decodeUriComponent = __webpack_require__(834) +var urix = __webpack_require__(836) +var atob = __webpack_require__(837) @@ -95954,7 +96891,7 @@ module.exports = { /***/ }), -/* 819 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96017,7 +96954,7 @@ void (function(root, factory) { /***/ }), -/* 820 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96035,13 +96972,13 @@ module.exports = resolveUrl /***/ }), -/* 821 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(835) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96052,7 +96989,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 822 */ +/* 835 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96153,7 +97090,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 823 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96176,7 +97113,7 @@ module.exports = urix /***/ }), -/* 824 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96190,7 +97127,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 825 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96198,8 +97135,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(729); -var utils = __webpack_require__(806); +var define = __webpack_require__(800); +var utils = __webpack_require__(819); /** * Expose `mixin()`. @@ -96342,19 +97279,19 @@ exports.comment = function(node) { /***/ }), -/* 826 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); +var use = __webpack_require__(811); var util = __webpack_require__(29); -var Cache = __webpack_require__(827); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:parser'); -var Position = __webpack_require__(828); -var utils = __webpack_require__(806); +var Cache = __webpack_require__(840); +var define = __webpack_require__(800); +var debug = __webpack_require__(813)('snapdragon:parser'); +var Position = __webpack_require__(841); +var utils = __webpack_require__(819); /** * Create a new `Parser` with the given `input` and `options`. @@ -96882,7 +97819,7 @@ module.exports = Parser; /***/ }), -/* 827 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96989,13 +97926,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 828 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(729); +var define = __webpack_require__(800); /** * Store position for a node @@ -97010,16 +97947,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 829 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(830); -var define = __webpack_require__(836); -var extend = __webpack_require__(837); -var not = __webpack_require__(839); +var safe = __webpack_require__(843); +var define = __webpack_require__(849); +var extend = __webpack_require__(850); +var not = __webpack_require__(852); var MAX_LENGTH = 1024 * 64; /** @@ -97172,10 +98109,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 830 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(831); +var parse = __webpack_require__(844); var types = parse.types; module.exports = function (re, opts) { @@ -97221,13 +98158,13 @@ function isRegExp (x) { /***/ }), -/* 831 */ +/* 844 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(832); -var types = __webpack_require__(833); -var sets = __webpack_require__(834); -var positions = __webpack_require__(835); +var util = __webpack_require__(845); +var types = __webpack_require__(846); +var sets = __webpack_require__(847); +var positions = __webpack_require__(848); module.exports = function(regexpStr) { @@ -97509,11 +98446,11 @@ module.exports.types = types; /***/ }), -/* 832 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); -var sets = __webpack_require__(834); +var types = __webpack_require__(846); +var sets = __webpack_require__(847); // All of these are private and only used by randexp. @@ -97626,7 +98563,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 833 */ +/* 846 */ /***/ (function(module, exports) { module.exports = { @@ -97642,10 +98579,10 @@ module.exports = { /***/ }), -/* 834 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(846); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97730,10 +98667,10 @@ exports.anyChar = function() { /***/ }), -/* 835 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(846); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -97753,7 +98690,7 @@ exports.end = function() { /***/ }), -/* 836 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97766,8 +98703,8 @@ exports.end = function() { -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(751); +var isDescriptor = __webpack_require__(765); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -97798,14 +98735,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 837 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(838); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(851); +var assignSymbols = __webpack_require__(752); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -97865,7 +98802,7 @@ function isEnum(obj, key) { /***/ }), -/* 838 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97878,7 +98815,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(750); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -97886,14 +98823,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 839 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(837); -var safe = __webpack_require__(830); +var extend = __webpack_require__(850); +var safe = __webpack_require__(843); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -97965,14 +98902,14 @@ module.exports = toRegex; /***/ }), -/* 840 */ +/* 853 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(841); -var extglob = __webpack_require__(856); +var nanomatch = __webpack_require__(854); +var extglob = __webpack_require__(870); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98049,7 +98986,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 841 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98060,17 +98997,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(728); -var extend = __webpack_require__(842); +var toRegex = __webpack_require__(855); +var extend = __webpack_require__(856); /** * Local dependencies */ -var compilers = __webpack_require__(844); -var parsers = __webpack_require__(845); -var cache = __webpack_require__(848); -var utils = __webpack_require__(850); +var compilers = __webpack_require__(858); +var parsers = __webpack_require__(859); +var cache = __webpack_require__(862); +var utils = __webpack_require__(864); var MAX_LENGTH = 1024 * 64; /** @@ -98894,14 +99831,169 @@ module.exports = nanomatch; /***/ }), -/* 842 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(843); -var assignSymbols = __webpack_require__(748); +var define = __webpack_require__(800); +var extend = __webpack_require__(742); +var not = __webpack_require__(741); +var MAX_LENGTH = 1024 * 64; + +/** + * Session cache + */ + +var cache = {}; + +/** + * Create a regular expression from the given `pattern` string. + * + * @param {String|RegExp} `pattern` Pattern can be a string or regular expression. + * @param {Object} `options` + * @return {RegExp} + * @api public + */ + +module.exports = function(patterns, options) { + if (!Array.isArray(patterns)) { + return makeRe(patterns, options); + } + return makeRe(patterns.join('|'), options); +}; + +/** + * Create a regular expression from the given `pattern` string. + * + * @param {String|RegExp} `pattern` Pattern can be a string or regular expression. + * @param {Object} `options` + * @return {RegExp} + * @api public + */ + +function makeRe(pattern, options) { + if (pattern instanceof RegExp) { + return pattern; + } + + if (typeof pattern !== 'string') { + throw new TypeError('expected a string'); + } + + if (pattern.length > MAX_LENGTH) { + throw new Error('expected pattern to be less than ' + MAX_LENGTH + ' characters'); + } + + var key = pattern; + // do this before shallow cloning options, it's a lot faster + if (!options || (options && options.cache !== false)) { + key = createKey(pattern, options); + + if (cache.hasOwnProperty(key)) { + return cache[key]; + } + } + + var opts = extend({}, options); + if (opts.contains === true) { + if (opts.negate === true) { + opts.strictNegate = false; + } else { + opts.strict = false; + } + } + + if (opts.strict === false) { + opts.strictOpen = false; + opts.strictClose = false; + } + + var open = opts.strictOpen !== false ? '^' : ''; + var close = opts.strictClose !== false ? '$' : ''; + var flags = opts.flags || ''; + var regex; + + if (opts.nocase === true && !/i/.test(flags)) { + flags += 'i'; + } + + try { + if (opts.negate || typeof opts.strictNegate === 'boolean') { + pattern = not.create(pattern, opts); + } + var str = open + '(?:' + pattern + ')' + close; + regex = new RegExp(str, flags); + } catch (err) { + if (opts.strictErrors === true) { + err.key = key; + err.pattern = pattern; + err.originalOptions = options; + err.createdOptions = opts; + throw err; + } + + try { + regex = new RegExp('^' + pattern.replace(/(\W)/g, '\\$1') + '$'); + } catch (err) { + regex = /.^/; //<= match nothing + } + } + + if (opts.cache !== false) { + cacheRegex(regex, key, pattern, opts); + } + return regex; +} + +/** + * Cache generated regex. This can result in dramatic speed improvements + * and simplify debugging by adding options and pattern to the regex. It can be + * disabled by passing setting `options.cache` to false. + */ + +function cacheRegex(regex, key, pattern, options) { + define(regex, 'cached', true); + define(regex, 'pattern', pattern); + define(regex, 'options', options); + define(regex, 'key', key); + cache[key] = regex; +} + +/** + * Create the key to use for memoization. The key is generated + * by iterating over the options and concatenating key-value pairs + * to the pattern string. + */ + +function createKey(pattern, options) { + if (!options) return pattern; + var key = pattern; + for (var prop in options) { + if (options.hasOwnProperty(prop)) { + key += ';' + prop + '=' + String(options[prop]); + } + } + return key; +} + +/** + * Expose `makeRe` + */ + +module.exports.makeRe = makeRe; + + +/***/ }), +/* 856 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isExtendable = __webpack_require__(857); +var assignSymbols = __webpack_require__(752); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -98961,7 +100053,7 @@ function isEnum(obj, key) { /***/ }), -/* 843 */ +/* 857 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98974,7 +100066,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(750); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -98982,7 +100074,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 844 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99328,15 +100420,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 845 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(728); -var isOdd = __webpack_require__(846); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(855); +var isOdd = __webpack_require__(860); /** * Characters to use in negation regex (we want to "not" match @@ -99722,7 +100814,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 846 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99735,7 +100827,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(847); +var isNumber = __webpack_require__(861); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -99749,7 +100841,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 847 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99777,14 +100869,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 848 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(863))(); /***/ }), -/* 849 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99797,7 +100889,7 @@ module.exports = new (__webpack_require__(849))(); -var MapCache = __webpack_require__(827); +var MapCache = __webpack_require__(840); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -99919,7 +101011,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 850 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99932,14 +101024,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(851)(); -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(852); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(842); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(855); -utils.unique = __webpack_require__(740); +var isWindows = __webpack_require__(865)(); +var Snapdragon = __webpack_require__(773); +utils.define = __webpack_require__(866); +utils.diff = __webpack_require__(867); +utils.extend = __webpack_require__(856); +utils.pick = __webpack_require__(868); +utils.typeOf = __webpack_require__(869); +utils.unique = __webpack_require__(744); /** * Returns true if the given value is effectively an empty string @@ -100305,7 +101397,7 @@ utils.unixify = function(options) { /***/ }), -/* 851 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100333,7 +101425,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 852 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100346,8 +101438,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(751); +var isDescriptor = __webpack_require__(765); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100378,7 +101470,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 853 */ +/* 867 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100432,7 +101524,7 @@ function diffArray(one, two) { /***/ }), -/* 854 */ +/* 868 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100445,7 +101537,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(751); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100474,7 +101566,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 855 */ +/* 869 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100609,7 +101701,7 @@ function isBuffer(val) { /***/ }), -/* 856 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100619,18 +101711,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(737); -var unique = __webpack_require__(740); -var toRegex = __webpack_require__(728); +var extend = __webpack_require__(742); +var unique = __webpack_require__(744); +var toRegex = __webpack_require__(855); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); -var Extglob = __webpack_require__(871); -var utils = __webpack_require__(870); +var compilers = __webpack_require__(871); +var parsers = __webpack_require__(882); +var Extglob = __webpack_require__(885); +var utils = __webpack_require__(884); var MAX_LENGTH = 1024 * 64; /** @@ -100947,13 +102039,13 @@ module.exports = extglob; /***/ }), -/* 857 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); +var brackets = __webpack_require__(872); /** * Extglob compilers @@ -101123,7 +102215,7 @@ module.exports = function(extglob) { /***/ }), -/* 858 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101133,17 +102225,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(859); -var parsers = __webpack_require__(861); +var compilers = __webpack_require__(873); +var parsers = __webpack_require__(875); /** * Module dependencies */ -var debug = __webpack_require__(863)('expand-brackets'); -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var toRegex = __webpack_require__(728); +var debug = __webpack_require__(877)('expand-brackets'); +var extend = __webpack_require__(742); +var Snapdragon = __webpack_require__(773); +var toRegex = __webpack_require__(855); /** * Parses the given POSIX character class `pattern` and returns a @@ -101341,13 +102433,13 @@ module.exports = brackets; /***/ }), -/* 859 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(860); +var posix = __webpack_require__(874); module.exports = function(brackets) { brackets.compiler @@ -101435,7 +102527,7 @@ module.exports = function(brackets) { /***/ }), -/* 860 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101464,14 +102556,14 @@ module.exports = { /***/ }), -/* 861 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(862); -var define = __webpack_require__(729); +var utils = __webpack_require__(876); +var define = __webpack_require__(800); /** * Text regex @@ -101690,14 +102782,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 862 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(728); -var regexNot = __webpack_require__(739); +var toRegex = __webpack_require__(855); +var regexNot = __webpack_require__(741); var cached; /** @@ -101731,7 +102823,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 863 */ +/* 877 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101740,14 +102832,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(864); + module.exports = __webpack_require__(878); } else { - module.exports = __webpack_require__(867); + module.exports = __webpack_require__(881); } /***/ }), -/* 864 */ +/* 878 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101756,7 +102848,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(879); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -101938,7 +103030,7 @@ function localstorage() { /***/ }), -/* 865 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { @@ -101954,7 +103046,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(866); +exports.humanize = __webpack_require__(880); /** * The currently active debug mode names, and names to skip. @@ -102146,7 +103238,7 @@ function coerce(val) { /***/ }), -/* 866 */ +/* 880 */ /***/ (function(module, exports) { /** @@ -102304,14 +103396,14 @@ function plural(ms, n, name) { /***/ }), -/* 867 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { /** * Module dependencies. */ -var tty = __webpack_require__(478); +var tty = __webpack_require__(484); var util = __webpack_require__(29); /** @@ -102320,7 +103412,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(879); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -102499,7 +103591,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(818); stream = new net.Socket({ fd: fd, readable: false, @@ -102558,15 +103650,15 @@ exports.enable(load()); /***/ }), -/* 868 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); -var define = __webpack_require__(869); -var utils = __webpack_require__(870); +var brackets = __webpack_require__(872); +var define = __webpack_require__(883); +var utils = __webpack_require__(884); /** * Characters to use in text regex (we want to "not" match @@ -102721,7 +103813,7 @@ module.exports = parsers; /***/ }), -/* 869 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102734,7 +103826,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(765); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -102759,14 +103851,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 870 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(739); -var Cache = __webpack_require__(849); +var regex = __webpack_require__(741); +var Cache = __webpack_require__(863); /** * Utils @@ -102835,7 +103927,7 @@ utils.createRegex = function(str) { /***/ }), -/* 871 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102845,16 +103937,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(767); -var define = __webpack_require__(869); -var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(773); +var define = __webpack_require__(883); +var extend = __webpack_require__(742); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); +var compilers = __webpack_require__(871); +var parsers = __webpack_require__(882); /** * Customize Snapdragon parser and renderer @@ -102920,16 +104012,16 @@ module.exports = Extglob; /***/ }), -/* 872 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(856); -var nanomatch = __webpack_require__(841); -var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(829); +var extglob = __webpack_require__(870); +var nanomatch = __webpack_require__(854); +var regexNot = __webpack_require__(741); +var toRegex = __webpack_require__(842); var not; /** @@ -103010,14 +104102,14 @@ function textRegex(pattern) { /***/ }), -/* 873 */ +/* 887 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(863))(); /***/ }), -/* 874 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103030,13 +104122,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(836); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(837); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(875); -utils.unique = __webpack_require__(740); +var Snapdragon = __webpack_require__(773); +utils.define = __webpack_require__(849); +utils.diff = __webpack_require__(867); +utils.extend = __webpack_require__(850); +utils.pick = __webpack_require__(868); +utils.typeOf = __webpack_require__(889); +utils.unique = __webpack_require__(744); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -103333,7 +104425,7 @@ utils.unixify = function(options) { /***/ }), -/* 875 */ +/* 889 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103468,7 +104560,7 @@ function isBuffer(val) { /***/ }), -/* 876 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103487,9 +104579,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(891); +var reader_1 = __webpack_require__(904); +var fs_stream_1 = __webpack_require__(908); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103550,15 +104642,15 @@ exports.default = ReaderAsync; /***/ }), -/* 877 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(878); -const readdirAsync = __webpack_require__(886); -const readdirStream = __webpack_require__(889); +const readdirSync = __webpack_require__(892); +const readdirAsync = __webpack_require__(900); +const readdirStream = __webpack_require__(903); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103642,7 +104734,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 878 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103650,11 +104742,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(893); let syncFacade = { - fs: __webpack_require__(884), - forEach: __webpack_require__(885), + fs: __webpack_require__(898), + forEach: __webpack_require__(899), sync: true }; @@ -103683,7 +104775,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 879 */ +/* 893 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103692,9 +104784,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(880); -const stat = __webpack_require__(882); -const call = __webpack_require__(883); +const normalizeOptions = __webpack_require__(894); +const stat = __webpack_require__(896); +const call = __webpack_require__(897); /** * Asynchronously reads the contents of a directory and streams the results @@ -104070,14 +105162,14 @@ module.exports = DirectoryReader; /***/ }), -/* 880 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(881); +const globToRegExp = __webpack_require__(895); module.exports = normalizeOptions; @@ -104254,7 +105346,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 881 */ +/* 895 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104391,13 +105483,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 882 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(883); +const call = __webpack_require__(897); module.exports = stat; @@ -104472,7 +105564,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 883 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104533,14 +105625,14 @@ function callOnce (fn) { /***/ }), -/* 884 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(883); +const call = __webpack_require__(897); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104604,7 +105696,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 885 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104633,7 +105725,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 886 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104641,12 +105733,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(887); -const DirectoryReader = __webpack_require__(879); +const maybe = __webpack_require__(901); +const DirectoryReader = __webpack_require__(893); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(902), async: true }; @@ -104688,7 +105780,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 887 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104715,7 +105807,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 888 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104751,7 +105843,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 889 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104759,11 +105851,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(893); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(902), async: true }; @@ -104783,16 +105875,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 890 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(891); -var entry_1 = __webpack_require__(893); -var pathUtil = __webpack_require__(892); +var deep_1 = __webpack_require__(905); +var entry_1 = __webpack_require__(907); +var pathUtil = __webpack_require__(906); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -104858,14 +105950,14 @@ exports.default = Reader; /***/ }), -/* 891 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); -var patternUtils = __webpack_require__(721); +var pathUtils = __webpack_require__(906); +var patternUtils = __webpack_require__(723); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -104948,7 +106040,7 @@ exports.default = DeepFilter; /***/ }), -/* 892 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104979,14 +106071,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 893 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); -var patternUtils = __webpack_require__(721); +var pathUtils = __webpack_require__(906); +var patternUtils = __webpack_require__(723); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -105071,7 +106163,7 @@ exports.default = EntryFilter; /***/ }), -/* 894 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105091,8 +106183,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(909); +var fs_1 = __webpack_require__(913); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105142,14 +106234,14 @@ exports.default = FileSystemStream; /***/ }), -/* 895 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(896); -const statProvider = __webpack_require__(898); +const optionsManager = __webpack_require__(910); +const statProvider = __webpack_require__(912); /** * Asynchronous API. */ @@ -105180,13 +106272,13 @@ exports.statSync = statSync; /***/ }), -/* 896 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(897); +const fsAdapter = __webpack_require__(911); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105199,7 +106291,7 @@ exports.prepare = prepare; /***/ }), -/* 897 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105222,7 +106314,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 898 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105274,7 +106366,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 899 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105305,7 +106397,7 @@ exports.default = FileSystem; /***/ }), -/* 900 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105325,9 +106417,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(891); +var reader_1 = __webpack_require__(904); +var fs_stream_1 = __webpack_require__(908); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105395,7 +106487,7 @@ exports.default = ReaderStream; /***/ }), -/* 901 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105414,9 +106506,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_sync_1 = __webpack_require__(902); +var readdir = __webpack_require__(891); +var reader_1 = __webpack_require__(904); +var fs_sync_1 = __webpack_require__(916); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105476,7 +106568,7 @@ exports.default = ReaderSync; /***/ }), -/* 902 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105495,8 +106587,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(909); +var fs_1 = __webpack_require__(913); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105542,7 +106634,7 @@ exports.default = FileSystemSync; /***/ }), -/* 903 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105558,13 +106650,13 @@ exports.flatten = flatten; /***/ }), -/* 904 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var merge2 = __webpack_require__(588); +var merge2 = __webpack_require__(590); /** * Merge multiple streams and propagate their errors into one stream in parallel. */ @@ -105579,13 +106671,13 @@ exports.merge = merge; /***/ }), -/* 905 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(906); +const pathType = __webpack_require__(920); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105651,13 +106743,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 906 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(907); +const pify = __webpack_require__(921); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105700,7 +106792,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 907 */ +/* 921 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105791,17 +106883,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 908 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); const path = __webpack_require__(16); -const fastGlob = __webpack_require__(717); -const gitIgnore = __webpack_require__(909); -const pify = __webpack_require__(910); -const slash = __webpack_require__(911); +const fastGlob = __webpack_require__(719); +const gitIgnore = __webpack_require__(923); +const pify = __webpack_require__(924); +const slash = __webpack_require__(925); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -105899,7 +106991,7 @@ module.exports.sync = options => { /***/ }), -/* 909 */ +/* 923 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106368,7 +107460,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 910 */ +/* 924 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106443,7 +107535,7 @@ module.exports = (input, options) => { /***/ }), -/* 911 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106461,17 +107553,17 @@ module.exports = input => { /***/ }), -/* 912 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(913); -const CpFileError = __webpack_require__(916); -const fs = __webpack_require__(920); -const ProgressEmitter = __webpack_require__(923); +const pEvent = __webpack_require__(927); +const CpFileError = __webpack_require__(930); +const fs = __webpack_require__(934); +const ProgressEmitter = __webpack_require__(937); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106585,12 +107677,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 913 */ +/* 927 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(914); +const pTimeout = __webpack_require__(928); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -106881,12 +107973,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 914 */ +/* 928 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(915); +const pFinally = __webpack_require__(929); class TimeoutError extends Error { constructor(message) { @@ -106932,7 +108024,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 915 */ +/* 929 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106954,12 +108046,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 916 */ +/* 930 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(917); +const NestedError = __webpack_require__(931); class CpFileError extends NestedError { constructor(message, nested) { @@ -106973,10 +108065,10 @@ module.exports = CpFileError; /***/ }), -/* 917 */ +/* 931 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(918); +var inherits = __webpack_require__(932); var NestedError = function (message, nested) { this.nested = nested; @@ -107027,7 +108119,7 @@ module.exports = NestedError; /***/ }), -/* 918 */ +/* 932 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107035,12 +108127,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(919); + module.exports = __webpack_require__(933); } /***/ }), -/* 919 */ +/* 933 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107069,16 +108161,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 920 */ +/* 934 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(921); -const pEvent = __webpack_require__(913); -const CpFileError = __webpack_require__(916); +const makeDir = __webpack_require__(935); +const pEvent = __webpack_require__(927); +const CpFileError = __webpack_require__(930); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107175,7 +108267,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 921 */ +/* 935 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107183,7 +108275,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(922); +const semver = __webpack_require__(936); const defaults = { mode: 0o777 & (~process.umask()), @@ -107332,7 +108424,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 922 */ +/* 936 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -108934,7 +110026,7 @@ function coerce (version, options) { /***/ }), -/* 923 */ +/* 937 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -108975,7 +110067,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 924 */ +/* 938 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109021,12 +110113,12 @@ exports.default = module.exports; /***/ }), -/* 925 */ +/* 939 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(926); +const NestedError = __webpack_require__(940); class CpyError extends NestedError { constructor(message, nested) { @@ -109040,7 +110132,7 @@ module.exports = CpyError; /***/ }), -/* 926 */ +/* 940 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109096,14 +110188,14 @@ module.exports = NestedError; /***/ }), -/* 927 */ +/* 941 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return prepareExternalProjectDependencies; }); -/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(516); -/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(515); +/* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(518); +/* harmony import */ var _utils_project__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(517); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with diff --git a/packages/kbn-pm/src/utils/project_checksums.ts b/packages/kbn-pm/src/utils/project_checksums.ts index 572f2adb19bd9..7d939e715d411 100644 --- a/packages/kbn-pm/src/utils/project_checksums.ts +++ b/packages/kbn-pm/src/utils/project_checksums.ts @@ -32,7 +32,7 @@ import { Kibana } from '../utils/kibana'; export type ChecksumMap = Map; /** map of [repo relative path to changed file, type of change] */ -type Changes = Map; +type Changes = Map; const statAsync = promisify(Fs.stat); const projectBySpecificitySorter = (a: Project, b: Project) => b.path.length - a.path.length; @@ -45,7 +45,8 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too 'git', [ 'ls-files', - '-dmt', + '-dmto', + '--exclude-standard', '--', ...Array.from(projects.values()) .filter(p => kbn.isPartOfRepo(p)) @@ -78,10 +79,13 @@ async function getChangesForProjects(projects: ProjectMap, kbn: Kibana, log: Too unassignedChanges.set(path, 'deleted'); break; + case '?': + unassignedChanges.set(path, 'untracked'); + break; + case 'H': case 'S': case 'K': - case '?': default: log.warning(`unexpected modification status "${tag}" for ${path}, please report this!`); unassignedChanges.set(path, 'invalid'); diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 1531c1d22b01b..779d8a4153644 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -49,6 +49,13 @@ module.exports = async ({ config }) => { }, }); + config.module.rules.push({ + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', + }, + }); + // Handle Typescript files config.module.rules.push({ test: /\.tsx?$/, diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index 03075dd4081fd..3a66ba22ccf3d 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -30,6 +30,7 @@ import { setupMocha, runTests, Config, + SuiteTracker, } from './lib'; export class FunctionalTestRunner { @@ -52,6 +53,8 @@ export class FunctionalTestRunner { async run() { return await this._run(async (config, coreProviders) => { + SuiteTracker.startTracking(this.lifecycle, this.configFile); + const providers = new ProviderCollection(this.log, [ ...coreProviders, ...readProviderSpec('Service', config.get('services')), diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index 8940eccad503a..2e534974e1d76 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -23,3 +23,4 @@ export { readConfigFile, Config } from './config'; export { readProviderSpec, ProviderCollection, Provider } from './providers'; export { runTests, setupMocha } from './mocha'; export { FailureMetadata } from './failure_metadata'; +export { SuiteTracker } from './suite_tracker'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts new file mode 100644 index 0000000000000..b6c2c0a6d511d --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.test.ts @@ -0,0 +1,197 @@ +/* + * 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 fs from 'fs'; +import { join, resolve } from 'path'; + +jest.mock('fs'); +jest.mock('@kbn/dev-utils', () => { + return { REPO_ROOT: '/dev/null/root' }; +}); + +import { REPO_ROOT } from '@kbn/dev-utils'; +import { Lifecycle } from './lifecycle'; +import { SuiteTracker } from './suite_tracker'; + +const DEFAULT_TEST_METADATA_PATH = join(REPO_ROOT, 'target', 'test_metadata.json'); +const MOCK_CONFIG_PATH = join('test', 'config.js'); +const MOCK_TEST_PATH = join('test', 'apps', 'test.js'); +const ENVS_TO_RESET = ['TEST_METADATA_PATH']; + +describe('SuiteTracker', () => { + const originalEnvs: Record = {}; + + beforeEach(() => { + for (const env of ENVS_TO_RESET) { + if (env in process.env) { + originalEnvs[env] = process.env[env] || ''; + delete process.env[env]; + } + } + }); + + afterEach(() => { + for (const env of ENVS_TO_RESET) { + delete process.env[env]; + } + + for (const env of Object.keys(originalEnvs)) { + process.env[env] = originalEnvs[env]; + } + + jest.resetAllMocks(); + }); + + let MOCKS: Record; + + const createMock = (overrides = {}) => { + return { + file: resolve(REPO_ROOT, MOCK_TEST_PATH), + title: 'A Test', + suiteTag: MOCK_TEST_PATH, + ...overrides, + }; + }; + + const runLifecycleWithMocks = async (mocks: object[], fn: (objs: any) => any = () => {}) => { + const lifecycle = new Lifecycle(); + const suiteTracker = SuiteTracker.startTracking( + lifecycle, + resolve(REPO_ROOT, MOCK_CONFIG_PATH) + ); + + const ret = { lifecycle, suiteTracker }; + + for (const mock of mocks) { + await lifecycle.beforeTestSuite.trigger(mock); + } + + if (fn) { + fn(ret); + } + + for (const mock of mocks.reverse()) { + await lifecycle.afterTestSuite.trigger(mock); + } + + return ret; + }; + + beforeEach(() => { + MOCKS = { + WITH_TESTS: createMock({ tests: [{}] }), // i.e. a describe with tests in it + WITHOUT_TESTS: createMock(), // i.e. a describe with only other describes in it + }; + }); + + it('collects metadata for a single suite with multiple describe()s', async () => { + const { suiteTracker } = await runLifecycleWithMocks([MOCKS.WITHOUT_TESTS, MOCKS.WITH_TESTS]); + + const suites = suiteTracker.getAllFinishedSuites(); + expect(suites.length).toBe(1); + const suite = suites[0]; + + expect(suite).toMatchObject({ + config: MOCK_CONFIG_PATH, + file: MOCK_TEST_PATH, + tag: MOCK_TEST_PATH, + hasTests: true, + success: true, + }); + }); + + it('writes metadata to a file when cleanup is triggered', async () => { + const { lifecycle, suiteTracker } = await runLifecycleWithMocks([MOCKS.WITH_TESTS]); + await lifecycle.cleanup.trigger(); + + const suites = suiteTracker.getAllFinishedSuites(); + + const call = (fs.writeFileSync as jest.Mock).mock.calls[0]; + expect(call[0]).toEqual(DEFAULT_TEST_METADATA_PATH); + expect(call[1]).toEqual(JSON.stringify(suites, null, 2)); + }); + + it('respects TEST_METADATA_PATH env var for metadata target override', async () => { + process.env.TEST_METADATA_PATH = resolve(REPO_ROOT, '../fake-test-path'); + const { lifecycle } = await runLifecycleWithMocks([MOCKS.WITH_TESTS]); + await lifecycle.cleanup.trigger(); + + expect((fs.writeFileSync as jest.Mock).mock.calls[0][0]).toEqual( + process.env.TEST_METADATA_PATH + ); + }); + + it('identifies suites with tests as leaf suites', async () => { + const root = createMock({ title: 'root', file: join(REPO_ROOT, 'root.js') }); + const parent = createMock({ parent: root }); + const withTests = createMock({ parent, tests: [{}] }); + + const { suiteTracker } = await runLifecycleWithMocks([root, parent, withTests]); + const suites = suiteTracker.getAllFinishedSuites(); + + const finishedRoot = suites.find(s => s.title === 'root'); + const finishedWithTests = suites.find(s => s.title !== 'root'); + + expect(finishedRoot).toBeTruthy(); + expect(finishedRoot?.hasTests).toBeFalsy(); + expect(finishedWithTests?.hasTests).toBe(true); + }); + + describe('with a failing suite', () => { + let root: any; + let parent: any; + let failed: any; + + beforeEach(() => { + root = createMock({ file: join(REPO_ROOT, 'root.js') }); + parent = createMock({ parent: root }); + failed = createMock({ parent, tests: [{}] }); + }); + + it('marks parent suites as not successful when a test fails', async () => { + const { suiteTracker } = await runLifecycleWithMocks( + [root, parent, failed], + async ({ lifecycle }) => { + await lifecycle.testFailure.trigger(Error('test'), { parent: failed }); + } + ); + + const suites = suiteTracker.getAllFinishedSuites(); + expect(suites.length).toBe(2); + for (const suite of suites) { + expect(suite.success).toBeFalsy(); + } + }); + + it('marks parent suites as not successful when a test hook fails', async () => { + const { suiteTracker } = await runLifecycleWithMocks( + [root, parent, failed], + async ({ lifecycle }) => { + await lifecycle.testHookFailure.trigger(Error('test'), { parent: failed }); + } + ); + + const suites = suiteTracker.getAllFinishedSuites(); + expect(suites.length).toBe(2); + for (const suite of suites) { + expect(suite.success).toBeFalsy(); + } + }); + }); +}); diff --git a/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.ts b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.ts new file mode 100644 index 0000000000000..8967251ea78de --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/suite_tracker.ts @@ -0,0 +1,147 @@ +/* + * 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 fs from 'fs'; +import { dirname, relative, resolve } from 'path'; + +import { REPO_ROOT } from '@kbn/dev-utils'; + +import { Lifecycle } from './lifecycle'; + +export interface SuiteInProgress { + startTime?: Date; + endTime?: Date; + success?: boolean; +} + +export interface SuiteWithMetadata { + config: string; + file: string; + tag: string; + title: string; + startTime: Date; + endTime: Date; + duration: number; + success: boolean; + hasTests: boolean; +} + +const getTestMetadataPath = () => { + return process.env.TEST_METADATA_PATH || resolve(REPO_ROOT, 'target', 'test_metadata.json'); +}; + +export class SuiteTracker { + finishedSuitesByConfig: Record> = {}; + inProgressSuites: Map = new Map(); + + static startTracking(lifecycle: Lifecycle, configPath: string): SuiteTracker { + const suiteTracker = new SuiteTracker(lifecycle, configPath); + return suiteTracker; + } + + getTracked(suite: object): SuiteInProgress { + if (!this.inProgressSuites.has(suite)) { + this.inProgressSuites.set(suite, { success: undefined } as SuiteInProgress); + } + return this.inProgressSuites.get(suite)!; + } + + constructor(lifecycle: Lifecycle, configPathAbsolute: string) { + if (fs.existsSync(getTestMetadataPath())) { + fs.unlinkSync(getTestMetadataPath()); + } else { + fs.mkdirSync(dirname(getTestMetadataPath()), { recursive: true }); + } + + const config = relative(REPO_ROOT, configPathAbsolute); + + lifecycle.beforeTestSuite.add(suite => { + const tracked = this.getTracked(suite); + tracked.startTime = new Date(); + }); + + // If a test fails, we want to make sure all of the ancestors, all the way up to the root, get marked as failed + // This information is not available on the mocha objects without traversing all descendants of a given node + const handleFailure = (_: any, test: any) => { + let parent = test.parent; + + // Infinite loop protection, just in case + for (let i = 0; i < 500 && parent; i++) { + if (this.inProgressSuites.has(parent)) { + this.getTracked(parent).success = false; + } + parent = parent.parent; + } + }; + + lifecycle.testFailure.add(handleFailure); + lifecycle.testHookFailure.add(handleFailure); + + lifecycle.afterTestSuite.add(suite => { + const tracked = this.getTracked(suite); + tracked.endTime = new Date(); + + // The suite ended without any children failing, so we can mark it as successful + if (typeof tracked.success === 'undefined') { + tracked.success = true; + } + + let duration = tracked.endTime.getTime() - (tracked.startTime || new Date()).getTime(); + duration = Math.floor(duration / 1000); + + const file = relative(REPO_ROOT, suite.file); + + this.finishedSuitesByConfig[config] = this.finishedSuitesByConfig[config] || {}; + + // This will get called multiple times for a test file that has multiple describes in it or similar + // This is okay, because the last one that fires is always the root of the file, which is is the one we ultimately want + this.finishedSuitesByConfig[config][file] = { + ...tracked, + duration, + config, + file, + tag: suite.suiteTag, + title: suite.title, + hasTests: !!( + (suite.tests && suite.tests.length) || + // The below statement is so that `hasTests` will bubble up nested describes in the same file + (this.finishedSuitesByConfig[config][file] && + this.finishedSuitesByConfig[config][file].hasTests) + ), + } as SuiteWithMetadata; + }); + + lifecycle.cleanup.add(() => { + const suites = this.getAllFinishedSuites(); + + fs.writeFileSync(getTestMetadataPath(), JSON.stringify(suites, null, 2)); + }); + } + + getAllFinishedSuites() { + const flattened: SuiteWithMetadata[] = []; + for (const byFile of Object.values(this.finishedSuitesByConfig)) { + for (const suite of Object.values(byFile)) { + flattened.push(suite); + } + } + + flattened.sort((a, b) => b.duration - a.duration); + return flattened; + } +} diff --git a/packages/kbn-ui-shared-deps/index.d.ts b/packages/kbn-ui-shared-deps/index.d.ts index dec519da69641..b829c87d91c4a 100644 --- a/packages/kbn-ui-shared-deps/index.d.ts +++ b/packages/kbn-ui-shared-deps/index.d.ts @@ -53,3 +53,8 @@ export const lightCssDistFilename: string; export const externals: { [key: string]: string; }; + +/** + * Webpack loader for configuring the public path lookup from `window.__kbnPublicPath__`. + */ +export const publicPathLoader: string; diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 666ec7a46ff06..42ed08259ac8f 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -64,3 +64,4 @@ exports.externals = { 'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser', 'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser', }; +exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index c8614b1df9d5d..a60e2b0449d95 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,8 +9,8 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "18.3.0", - "@elastic/eui": "21.0.1", + "@elastic/charts": "18.4.2", + "@elastic/eui": "22.3.0", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", "angular": "^1.7.9", diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js new file mode 100644 index 0000000000000..6b7a27c9ca52b --- /dev/null +++ b/packages/kbn-ui-shared-deps/public_path_loader.js @@ -0,0 +1,23 @@ +/* + * 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. + */ + +module.exports = function(source) { + const options = this.query; + return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`; +}; diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index a875274544905..bf63c57765859 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -46,7 +46,6 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ path: UiSharedDeps.distDir, filename: '[name].js', sourceMapFilename: '[file].map', - publicPath: '__REPLACE_WITH_PUBLIC_PATH__', devtoolModuleFilenameTemplate: info => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', @@ -55,6 +54,17 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ module: { noParse: [MOMENT_SRC], rules: [ + { + include: [require.resolve('./entry.js')], + use: [ + { + loader: UiSharedDeps.publicPathLoader, + options: { + key: 'kbn-ui-shared-deps', + }, + }, + ], + }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], diff --git a/renovate.json5 b/renovate.json5 index ffa006264873d..c0ddcaf4f23c8 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/scripts/prettier_on_changed.js b/scripts/prettier_on_changed.js new file mode 100644 index 0000000000000..f9598110f91fd --- /dev/null +++ b/scripts/prettier_on_changed.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +require('../src/setup_node_env/babel_register'); +require('../src/dev/run_prettier_on_changed'); diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index a87e2aa11f2c0..97dec3eead303 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -258,14 +258,16 @@ export class ClusterManager { ); const ignorePaths = [ - /[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/, - /\.test\.(js|ts)$/, + /[\\\/](\..*|node_modules|bower_components|target|public|__[a-z0-9_]+__|coverage)([\\\/]|$)/, + /\.test\.(js|tsx?)$/, + /\.md$/, + /debug\.log$/, ...pluginInternalDirsIgnore, fromRoot('src/legacy/server/sass/__tmp__'), fromRoot('x-pack/legacy/plugins/reporting/.chromium'), fromRoot('x-pack/plugins/siem/cypress'), - fromRoot('x-pack/legacy/plugins/apm/e2e'), - fromRoot('x-pack/legacy/plugins/apm/scripts'), + fromRoot('x-pack/plugins/apm/e2e'), + fromRoot('x-pack/plugins/apm/scripts'), fromRoot('x-pack/legacy/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, 'plugins/java_languageserver', ]; diff --git a/src/cli/cluster/run_kbn_optimizer.ts b/src/cli/cluster/run_kbn_optimizer.ts index 7752d4a45ab65..b811fc1f6b294 100644 --- a/src/cli/cluster/run_kbn_optimizer.ts +++ b/src/cli/cluster/run_kbn_optimizer.ts @@ -34,6 +34,7 @@ export function runKbnOptimizer(opts: Record, config: LegacyConfig) const optimizerConfig = OptimizerConfig.create({ repoRoot: REPO_ROOT, watch: true, + includeCoreBundle: true, oss: !!opts.oss, examples: !!opts.runExamples, pluginPaths: config.get('plugins.paths'), diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 8c5fe4875aaea..c91c00bc1aa02 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/public/core.css b/src/core/public/_core.scss similarity index 100% rename from src/core/public/core.css rename to src/core/public/_core.scss diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index c25918c6b7328..e29837aecb125 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 1c9492d81c7f6..bafa1932e5e92 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 318afb652999e..0734e178033e2 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/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 61c8bc3cadae5..4c135c5769067 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -21,7 +21,7 @@ import React, { FunctionComponent, useMemo } from 'react'; import { Route, RouteComponentProps, Router, Switch } from 'react-router-dom'; import { History } from 'history'; import { Observable } from 'rxjs'; -import { useObservable } from 'react-use'; +import useObservable from 'react-use/lib/useObservable'; import { AppLeaveHandler, AppStatus, Mounter } from '../types'; import { AppContainer } from './app_container'; diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils.test.ts new file mode 100644 index 0000000000000..7ed0919f88c61 --- /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 0000000000000..048f195fe1223 --- /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 d0ef2aeb265fe..fb2972735c2b7 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 23fdabe0f3430..4c319873af804 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 18e4b7b26b6ba..f79b1df77f8e1 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 52b59c53b658c..d97ef477c2ee0 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/core_system.test.ts b/src/core/public/core_system.test.ts index 94fa74f4bd861..a42719417a2b1 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -59,7 +59,6 @@ const defaultCoreSystemParams = { warnLegacyBrowsers: true, }, } as any, - requireLegacyFiles: jest.fn(), }; beforeEach(() => { @@ -104,19 +103,22 @@ describe('constructor', () => { }); }); - it('passes requireLegacyFiles, useLegacyTestHarness, and a dom element to LegacyPlatformService', () => { + it('passes required params to LegacyPlatformService', () => { const requireLegacyFiles = { requireLegacyFiles: true }; - const useLegacyTestHarness = { useLegacyTestHarness: true }; + const requireLegacyBootstrapModule = { requireLegacyBootstrapModule: true }; + const requireNewPlatformShimModule = { requireNewPlatformShimModule: true }; createCoreSystem({ requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, }); expect(LegacyPlatformServiceConstructor).toHaveBeenCalledTimes(1); expect(LegacyPlatformServiceConstructor).toHaveBeenCalledWith({ requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 5c10d89459128..e58114b69dcc1 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -17,8 +17,6 @@ * under the License. */ -import './core.css'; - import { CoreId } from '../server'; import { PackageInfo, EnvironmentMode } from '../server/types'; import { CoreSetup, CoreStart } from '.'; @@ -50,8 +48,9 @@ interface Params { rootDomElement: HTMLElement; browserSupportsCsp: boolean; injectedMetadata: InjectedMetadataParams['injectedMetadata']; - requireLegacyFiles: LegacyPlatformParams['requireLegacyFiles']; - useLegacyTestHarness?: LegacyPlatformParams['useLegacyTestHarness']; + requireLegacyFiles?: LegacyPlatformParams['requireLegacyFiles']; + requireLegacyBootstrapModule?: LegacyPlatformParams['requireLegacyBootstrapModule']; + requireNewPlatformShimModule?: LegacyPlatformParams['requireNewPlatformShimModule']; } /** @internal */ @@ -111,7 +110,8 @@ export class CoreSystem { browserSupportsCsp, injectedMetadata, requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, } = params; this.rootDomElement = rootDomElement; @@ -145,7 +145,8 @@ export class CoreSystem { this.legacy = new LegacyPlatformService({ requireLegacyFiles, - useLegacyTestHarness, + requireLegacyBootstrapModule, + requireNewPlatformShimModule, }); } diff --git a/src/core/public/entry_point.ts b/src/core/public/entry_point.ts new file mode 100644 index 0000000000000..9461acccf30b9 --- /dev/null +++ b/src/core/public/entry_point.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. + */ + +/** + * This is the entry point used to boot the frontend when serving a application + * that lives in the Kibana Platform. + * + * Any changes to this file should be kept in sync with + * src/legacy/ui/ui_bundles/app_entry_template.js + */ + +import './index.scss'; +import { i18n } from '@kbn/i18n'; +import { CoreSystem } from './core_system'; + +const injectedMetadata = JSON.parse( + document.querySelector('kbn-injected-metadata')!.getAttribute('data')! +); + +if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && process.env.ELASTIC_APM_ACTIVE === 'true') { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { init } = require('@elastic/apm-rum'); + init(injectedMetadata.vars.apmConfig); +} + +i18n + .load(injectedMetadata.i18n.translationsUrl) + .catch(e => e) + .then(async i18nError => { + const coreSystem = new CoreSystem({ + injectedMetadata, + rootDomElement: document.body, + browserSupportsCsp: !(window as any).__kbnCspNotEnforced__, + }); + + const setup = await coreSystem.setup(); + if (i18nError && setup) { + setup.fatalErrors.add(i18nError); + } + + await coreSystem.start(); + }); diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts index d4aced6894526..3cd8ef6169090 100644 --- a/src/core/public/http/index.ts +++ b/src/core/public/http/index.ts @@ -18,4 +18,5 @@ */ export { HttpService } from './http_service'; +export { HttpFetchError } from './http_fetch_error'; export * from './types'; diff --git a/src/core/public/index.scss b/src/core/public/index.scss index 86f2efdff7702..4be46899cff67 100644 --- a/src/core/public/index.scss +++ b/src/core/public/index.scss @@ -1,11 +1,11 @@ -// Functions need to be first, since we use them in our variables and mixin definitions -@import '@elastic/eui/src/global_styling/functions/index'; - -// Variables come next, and are used in some mixins -@import '@elastic/eui/src/global_styling/variables/index'; - -// Mixins provide generic code expansion through helpers -@import '@elastic/eui/src/global_styling/mixins/index'; +// This file is built by both the legacy and KP build systems so we need to +// import this explicitly +@import '../../legacy/ui/public/styles/_styling_constants'; +@import './core'; @import './chrome/index'; @import './overlays/index'; +@import './rendering/index'; + +// Global styles need to be migrated +@import '../../legacy/ui/public/styles/_legacy/_index'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 254cac3495599..b4f64125a03ef 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -143,6 +143,7 @@ export { export { HttpHeadersInit, HttpRequestInit, + HttpFetchError, HttpFetchOptions, HttpFetchOptionsWithPath, HttpFetchQuery, diff --git a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap deleted file mode 100644 index 97629fdd1add5..0000000000000 --- a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#start() load order useLegacyTestHarness = false loads ui/modules before ui/chrome, and both before legacy files 1`] = ` -Array [ - "ui/new_platform", - "ui/chrome", - "legacy files", -] -`; - -exports[`#start() load order useLegacyTestHarness = true loads ui/modules before ui/test_harness, and both before legacy files 1`] = ` -Array [ - "ui/new_platform", - "ui/test_harness", - "legacy files", -] -`; - -exports[`#stop() destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement 1`] = ` -

-`; - -exports[`#stop() does nothing if angular was not bootstrapped to targetDomElement 1`] = ` -
- - -

- this should not be removed -

- - -
-`; diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index c3de645c6b17e..fa29320aab4e6 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -19,34 +19,6 @@ import angular from 'angular'; -const mockLoadOrder: string[] = []; - -const mockUiNewPlatformSetup = jest.fn(); -const mockUiNewPlatformStart = jest.fn(); -jest.mock('ui/new_platform', () => { - mockLoadOrder.push('ui/new_platform'); - return { - __setup__: mockUiNewPlatformSetup, - __start__: mockUiNewPlatformStart, - }; -}); - -const mockUiChromeBootstrap = jest.fn(); -jest.mock('ui/chrome', () => { - mockLoadOrder.push('ui/chrome'); - return { - bootstrap: mockUiChromeBootstrap, - }; -}); - -const mockUiTestHarnessBootstrap = jest.fn(); -jest.mock('ui/test_harness', () => { - mockLoadOrder.push('ui/test_harness'); - return { - bootstrap: mockUiTestHarnessBootstrap, - }; -}); - import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -69,10 +41,24 @@ const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); const notificationsSetup = notificationServiceMock.createSetupContract(); const uiSettingsSetup = uiSettingsServiceMock.createSetupContract(); +const mockLoadOrder: string[] = []; +const mockUiNewPlatformSetup = jest.fn(); +const mockUiNewPlatformStart = jest.fn(); +const mockUiChromeBootstrap = jest.fn(); const defaultParams = { requireLegacyFiles: jest.fn(() => { mockLoadOrder.push('legacy files'); }), + requireLegacyBootstrapModule: jest.fn(() => { + mockLoadOrder.push('ui/chrome'); + return { + bootstrap: mockUiChromeBootstrap, + }; + }), + requireNewPlatformShimModule: jest.fn(() => ({ + __setup__: mockUiNewPlatformSetup, + __start__: mockUiNewPlatformStart, + })), }; const defaultSetupDeps = { @@ -128,7 +114,7 @@ afterEach(() => { describe('#setup()', () => { describe('default', () => { - it('initializes ui/new_platform with core APIs', () => { + it('initializes new platform shim module with core APIs', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, }); @@ -138,6 +124,21 @@ describe('#setup()', () => { expect(mockUiNewPlatformSetup).toHaveBeenCalledTimes(1); expect(mockUiNewPlatformSetup).toHaveBeenCalledWith(expect.any(Object), {}); }); + + it('throws error if requireNewPlatformShimModule is undefined', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + requireNewPlatformShimModule: undefined, + }); + + expect(() => { + legacyPlatform.setup(defaultSetupDeps); + }).toThrowErrorMatchingInlineSnapshot( + `"requireNewPlatformShimModule must be specified when rendering a legacy application"` + ); + + expect(mockUiNewPlatformSetup).not.toHaveBeenCalled(); + }); }); }); @@ -171,6 +172,21 @@ describe('#start()', () => { expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); }); + it('throws error if requireNewPlatformShimeModule is undefined', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + requireNewPlatformShimModule: undefined, + }); + + expect(() => { + legacyPlatform.start(defaultStartDeps); + }).toThrowErrorMatchingInlineSnapshot( + `"requireNewPlatformShimModule must be specified when rendering a legacy application"` + ); + + expect(mockUiNewPlatformStart).not.toHaveBeenCalled(); + }); + it('resolves getStartServices with core and plugin APIs', async () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, @@ -185,67 +201,35 @@ describe('#start()', () => { expect(pluginsStart).toBe(defaultStartDeps.plugins); }); - describe('useLegacyTestHarness = false', () => { - it('passes the targetDomElement to ui/chrome', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); + it('passes the targetDomElement to legacy bootstrap module', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); - expect(mockUiTestHarnessBootstrap).not.toHaveBeenCalled(); - expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); - }); + expect(mockUiChromeBootstrap).toHaveBeenCalledTimes(1); + expect(mockUiChromeBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); }); - describe('useLegacyTestHarness = true', () => { - it('passes the targetDomElement to ui/test_harness', () => { + describe('load order', () => { + it('loads ui/modules before ui/chrome, and both before legacy files', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, - useLegacyTestHarness: true, }); + expect(mockLoadOrder).toEqual([]); + legacyPlatform.setup(defaultSetupDeps); legacyPlatform.start(defaultStartDeps); - expect(mockUiChromeBootstrap).not.toHaveBeenCalled(); - expect(mockUiTestHarnessBootstrap).toHaveBeenCalledTimes(1); - expect(mockUiTestHarnessBootstrap).toHaveBeenCalledWith(defaultStartDeps.targetDomElement); - }); - }); - - describe('load order', () => { - describe('useLegacyTestHarness = false', () => { - it('loads ui/modules before ui/chrome, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockLoadOrder).toMatchSnapshot(); - }); - }); - - describe('useLegacyTestHarness = true', () => { - it('loads ui/modules before ui/test_harness, and both before legacy files', () => { - const legacyPlatform = new LegacyPlatformService({ - ...defaultParams, - useLegacyTestHarness: true, - }); - - expect(mockLoadOrder).toEqual([]); - - legacyPlatform.setup(defaultSetupDeps); - legacyPlatform.start(defaultStartDeps); - - expect(mockLoadOrder).toMatchSnapshot(); - }); + expect(mockLoadOrder).toMatchInlineSnapshot(` + Array [ + "ui/chrome", + "legacy files", + ] + `); }); }); }); @@ -262,7 +246,17 @@ describe('#stop()', () => { }); legacyPlatform.stop(); - expect(targetDomElement).toMatchSnapshot(); + expect(targetDomElement).toMatchInlineSnapshot(` +
+ + +

+ this should not be removed +

+ + +
+ `); }); it('destroys the angular scope and empties the targetDomElement if angular is bootstrapped to targetDomElement', async () => { @@ -291,7 +285,11 @@ describe('#stop()', () => { legacyPlatform.start({ ...defaultStartDeps, targetDomElement }); legacyPlatform.stop(); - expect(targetDomElement).toMatchSnapshot(); + expect(targetDomElement).toMatchInlineSnapshot(` +
+ `); expect(scopeDestroySpy).toHaveBeenCalledTimes(1); }); }); diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 39ca7bdf54b7c..01837ba6f5940 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -25,8 +25,12 @@ import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../'; /** @internal */ export interface LegacyPlatformParams { - requireLegacyFiles: () => void; - useLegacyTestHarness?: boolean; + requireLegacyFiles?: () => void; + requireLegacyBootstrapModule?: () => BootstrapModule; + requireNewPlatformShimModule?: () => { + __setup__: (legacyCore: LegacyCoreSetup, plugins: Record) => void; + __start__: (legacyCore: LegacyCoreStart, plugins: Record) => void; + }; } interface SetupDeps { @@ -92,7 +96,13 @@ export class LegacyPlatformService { // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts if (core.injectedMetadata.getLegacyMode()) { - require('ui/new_platform').__setup__(legacyCore, plugins); + if (!this.params.requireNewPlatformShimModule) { + throw new Error( + `requireNewPlatformShimModule must be specified when rendering a legacy application` + ); + } + + this.params.requireNewPlatformShimModule().__setup__(legacyCore, plugins); } } @@ -131,16 +141,29 @@ export class LegacyPlatformService { this.startDependencies$.next([legacyCore, plugins, {}]); + if (!this.params.requireNewPlatformShimModule) { + throw new Error( + `requireNewPlatformShimModule must be specified when rendering a legacy application` + ); + } + if (!this.params.requireLegacyBootstrapModule) { + throw new Error( + `requireLegacyBootstrapModule must be specified when rendering a legacy application` + ); + } + // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts - require('ui/new_platform').__start__(legacyCore, plugins); + this.params.requireNewPlatformShimModule().__start__(legacyCore, plugins); // Load the bootstrap module before loading the legacy platform files so that // the bootstrap module can modify the environment a bit first - this.bootstrapModule = this.loadBootstrapModule(); + this.bootstrapModule = this.params.requireLegacyBootstrapModule(); // require the files that will tie into the legacy platform - this.params.requireLegacyFiles(); + if (this.params.requireLegacyFiles) { + this.params.requireLegacyFiles(); + } if (!this.bootstrapModule) { throw new Error('Bootstrap module must be loaded before `start`'); @@ -172,20 +195,6 @@ export class LegacyPlatformService { // clear the inner html of the root angular element this.targetDomElement.textContent = ''; } - - private loadBootstrapModule(): BootstrapModule { - if (this.params.useLegacyTestHarness) { - // wrapped in NODE_ENV check so the `ui/test_harness` module - // is not included in the distributable - if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true') { - return require('ui/test_harness'); - } - - throw new Error('tests bundle is not available in the distributable'); - } - - return require('ui/chrome'); - } } const notSupported = (methodName: string) => (...args: any[]) => { diff --git a/src/core/public/plugins/plugin.test.mocks.ts b/src/core/public/plugins/plugin.test.mocks.ts index b877847aaa90e..422442c9ca4d2 100644 --- a/src/core/public/plugins/plugin.test.mocks.ts +++ b/src/core/public/plugins/plugin.test.mocks.ts @@ -24,8 +24,8 @@ export const mockPlugin = { }; export const mockInitializer = jest.fn(() => mockPlugin); -export const mockPluginLoader = jest.fn().mockResolvedValue(mockInitializer); +export const mockPluginReader = jest.fn(() => mockInitializer); -jest.mock('./plugin_loader', () => ({ - loadPluginBundle: mockPluginLoader, +jest.mock('./plugin_reader', () => ({ + read: mockPluginReader, })); diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index 39330711f7980..8fe745db9554d 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { mockInitializer, mockPlugin, mockPluginLoader } from './plugin.test.mocks'; +import { mockInitializer, mockPlugin, mockPluginReader } from './plugin.test.mocks'; import { DiscoveredPlugin } from '../../server'; import { coreMock } from '../mocks'; @@ -38,10 +38,9 @@ function createManifest( let plugin: PluginWrapper>; const opaqueId = Symbol(); const initializerContext = coreMock.createPluginInitializerContext(); -const addBasePath = (path: string) => path; beforeEach(() => { - mockPluginLoader.mockClear(); + mockPluginReader.mockClear(); mockPlugin.setup.mockClear(); mockPlugin.start.mockClear(); mockPlugin.stop.mockClear(); @@ -49,20 +48,8 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`load` calls loadPluginBundle', () => { - plugin.load(addBasePath); - expect(mockPluginLoader).toHaveBeenCalledWith(addBasePath, 'plugin-a'); - }); - - test('`setup` fails if load is not called first', async () => { - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Plugin \\"plugin-a\\" can't be setup since its bundle isn't loaded."` - ); - }); - test('`setup` fails if plugin.setup is not a function', async () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); - await plugin.load(addBasePath); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"setup\\" function."` ); @@ -70,20 +57,17 @@ describe('PluginWrapper', () => { test('`setup` fails if plugin.start is not a function', async () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn() } as any); - await plugin.load(addBasePath); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Instance of plugin \\"plugin-a\\" does not define \\"start\\" function."` ); }); test('`setup` calls initializer with initializer context', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); expect(mockInitializer).toHaveBeenCalledWith(initializerContext); }); test('`setup` calls plugin.setup with context and dependencies', async () => { - await plugin.load(addBasePath); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; await plugin.setup(context, deps); @@ -91,14 +75,12 @@ describe('PluginWrapper', () => { }); test('`start` fails if setup is not called first', async () => { - await plugin.load(addBasePath); await expect(plugin.start({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be started since it isn't set up."` ); }); test('`start` calls plugin.start with context and dependencies', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; @@ -114,20 +96,21 @@ describe('PluginWrapper', () => { }; let startDependenciesResolved = false; - mockPluginLoader.mockResolvedValueOnce(() => ({ - setup: jest.fn(), - start: async () => { - // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. - await new Promise(resolve => setTimeout(resolve, 10)); - expect(startDependenciesResolved).toBe(false); - return pluginStartContract; - }, - })); - await plugin.load(addBasePath); + mockPluginReader.mockReturnValueOnce( + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(async () => { + // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + return pluginStartContract; + }), + stop: jest.fn(), + })) + ); await plugin.setup({} as any, {} as any); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; - // Add promise callback prior to calling `start` to ensure calls in `setup` will not resolve before `start` is // called. const startDependenciesCheck = plugin.startDependencies.then(res => { @@ -145,7 +128,6 @@ describe('PluginWrapper', () => { }); test('`stop` calls plugin.stop', async () => { - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); await plugin.stop(); expect(mockPlugin.stop).toHaveBeenCalled(); @@ -153,7 +135,6 @@ describe('PluginWrapper', () => { test('`stop` does not fail if plugin.stop does not exist', async () => { mockInitializer.mockReturnValueOnce({ setup: jest.fn(), start: jest.fn() } as any); - await plugin.load(addBasePath); await plugin.setup({} as any, {} as any); expect(() => plugin.stop()).not.toThrow(); }); diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index e51c45040c452..591165fcd2839 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -21,7 +21,7 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; -import { loadPluginBundle } from './plugin_loader'; +import { read } from './plugin_reader'; import { CoreStart, CoreSetup } from '..'; /** @@ -69,7 +69,6 @@ export class PluginWrapper< public readonly configPath: DiscoveredPlugin['configPath']; public readonly requiredPlugins: DiscoveredPlugin['requiredPlugins']; public readonly optionalPlugins: DiscoveredPlugin['optionalPlugins']; - private initializer?: PluginInitializer; private instance?: Plugin; private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart, TStart]>(); @@ -86,18 +85,6 @@ export class PluginWrapper< this.optionalPlugins = discoveredPlugin.optionalPlugins; } - /** - * Loads the plugin's bundle into the browser. Should be called in parallel with all plugins - * using `Promise.all`. Must be called before `setup`. - * @param addBasePath Function that adds the base path to a string for plugin bundle path. - */ - public async load(addBasePath: (path: string) => string) { - this.initializer = await loadPluginBundle( - addBasePath, - this.name - ); - } - /** * Instantiates plugin and calls `setup` function exposed by the plugin initializer. * @param setupContext Context that consists of various core services tailored specifically @@ -146,11 +133,14 @@ export class PluginWrapper< } private async createPluginInstance() { - if (this.initializer === undefined) { - throw new Error(`Plugin "${this.name}" can't be setup since its bundle isn't loaded.`); - } - - const instance = this.initializer(this.initializerContext); + const initializer = read(this.name) as PluginInitializer< + TSetup, + TStart, + TPluginsSetup, + TPluginsStart + >; + + const instance = initializer(this.initializerContext); if (typeof instance.setup !== 'function') { throw new Error(`Instance of plugin "${this.name}" does not define "setup" function.`); diff --git a/src/core/public/plugins/plugin_loader.mock.ts b/src/core/public/plugins/plugin_loader.mock.ts deleted file mode 100644 index abdd9d4ddce2a..0000000000000 --- a/src/core/public/plugins/plugin_loader.mock.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { PluginName } from 'src/core/server'; -import { LoadPluginBundle, UnknownPluginInitializer } from './plugin_loader'; - -/** - * @param initializerProvider A function provided by the test to resolve initializers. - */ -const createLoadPluginBundleMock = ( - initializerProvider: (name: PluginName) => UnknownPluginInitializer -): jest.Mock, Parameters> => - jest.fn((addBasePath, pluginName, _ = {}) => { - return Promise.resolve(initializerProvider(pluginName)) as any; - }); - -export const loadPluginBundleMock = { create: createLoadPluginBundleMock }; diff --git a/src/core/public/plugins/plugin_loader.test.ts b/src/core/public/plugins/plugin_loader.test.ts deleted file mode 100644 index 18cc2d7a6f182..0000000000000 --- a/src/core/public/plugins/plugin_loader.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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 { CoreWindow, loadPluginBundle } from './plugin_loader'; - -let createdScriptTags = [] as any[]; -let appendChildSpy: jest.SpyInstance; -let createElementSpy: jest.SpyInstance< - HTMLElement, - [string, (ElementCreationOptions | undefined)?] ->; - -const coreWindow = (window as unknown) as CoreWindow; - -beforeEach(() => { - // Mock document.createElement to return fake tags we can use to inspect what - // loadPluginBundles does. - createdScriptTags = []; - createElementSpy = jest.spyOn(document, 'createElement').mockImplementation(() => { - const scriptTag = { setAttribute: jest.fn() } as any; - createdScriptTags.push(scriptTag); - return scriptTag; - }); - - // Mock document.body.appendChild to avoid errors about appending objects that aren't `Node`'s - // and so we can verify that the script tags were added to the page. - appendChildSpy = jest.spyOn(document.body, 'appendChild').mockReturnValue({} as any); - - // Mock global fields needed for loading modules. - coreWindow.__kbnBundles__ = {}; -}); - -afterEach(() => { - appendChildSpy.mockRestore(); - createElementSpy.mockRestore(); - delete coreWindow.__kbnBundles__; -}); - -const addBasePath = (path: string) => path; - -test('`loadPluginBundles` creates a script tag and loads initializer', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - - // Verify it sets up the script tag correctly and adds it to document.body - expect(createdScriptTags).toHaveLength(1); - const fakeScriptTag = createdScriptTags[0]; - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( - 'src', - '/bundles/plugin:plugin-a/plugin-a.plugin.js' - ); - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith('id', 'kbn-plugin-plugin-a'); - expect(fakeScriptTag.onload).toBeInstanceOf(Function); - expect(fakeScriptTag.onerror).toBeInstanceOf(Function); - expect(appendChildSpy).toHaveBeenCalledWith(fakeScriptTag); - - // Setup a fake initializer as if a plugin bundle had actually been loaded. - const fakeInitializer = jest.fn(); - coreWindow.__kbnBundles__['plugin/plugin-a'] = { plugin: fakeInitializer }; - // Call the onload callback - fakeScriptTag.onload(); - await expect(loadPromise).resolves.toEqual(fakeInitializer); -}); - -test('`loadPluginBundles` includes the basePath', async () => { - loadPluginBundle((path: string) => `/mybasepath${path}`, 'plugin-a'); - - // Verify it sets up the script tag correctly and adds it to document.body - expect(createdScriptTags).toHaveLength(1); - const fakeScriptTag = createdScriptTags[0]; - expect(fakeScriptTag.setAttribute).toHaveBeenCalledWith( - 'src', - '/mybasepath/bundles/plugin:plugin-a/plugin-a.plugin.js' - ); -}); - -test('`loadPluginBundles` rejects if script.onerror is called', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - const fakeScriptTag1 = createdScriptTags[0]; - // Call the error on the second script - fakeScriptTag1.onerror(new Error('Whoa there!')); - - await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to load \\"plugin-a\\" bundle (/bundles/plugin:plugin-a/plugin-a.plugin.js)"` - ); -}); - -test('`loadPluginBundles` rejects if timeout is reached', async () => { - await expect( - // Override the timeout to 1 ms for testi. - loadPluginBundle(addBasePath, 'plugin-a', { timeoutMs: 1 }) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Timeout reached when loading \\"plugin-a\\" bundle (/bundles/plugin:plugin-a/plugin-a.plugin.js)"` - ); -}); - -test('`loadPluginBundles` rejects if bundle does attach an initializer to window.__kbnBundles__', async () => { - const loadPromise = loadPluginBundle(addBasePath, 'plugin-a'); - - const fakeScriptTag1 = createdScriptTags[0]; - - // Setup a fake initializer as if a plugin bundle had actually been loaded. - coreWindow.__kbnBundles__['plugin:plugin-a'] = undefined; - // Call the onload callback - fakeScriptTag1.onload(); - - await expect(loadPromise).rejects.toThrowErrorMatchingInlineSnapshot( - `"Definition of plugin \\"plugin-a\\" should be a function (/bundles/plugin:plugin-a/plugin-a.plugin.js)."` - ); -}); diff --git a/src/core/public/plugins/plugin_loader.ts b/src/core/public/plugins/plugin_loader.ts deleted file mode 100644 index 9b35588dfe726..0000000000000 --- a/src/core/public/plugins/plugin_loader.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 { PluginName } from '../../server'; -import { PluginInitializer } from './plugin'; - -/** - * Unknown variant for internal use only for when plugins are not known. - * @internal - */ -export type UnknownPluginInitializer = PluginInitializer>; - -/** - * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. - * @internal - */ -export interface CoreWindow { - __kbnBundles__: { - [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; - }; -} - -/** - * Timeout for loading a single script in milliseconds. - * @internal - */ -export const LOAD_TIMEOUT = 120 * 1000; // 2 minutes - -/** - * Loads the bundle for a plugin onto the page and returns their PluginInitializer. This should - * be called for all plugins (once per plugin) in parallel using Promise.all. - * - * If this is slowing down browser load time, there are some ways we could make this faster: - * - Add these bundles in the generated bootstrap.js file so they're loaded immediately - * - Concatenate all the bundles files on the backend and serve them in single request. - * - Use HTTP/2 to load these bundles without having to open new connections for each. - * - * This may not be much of an issue since these should be cached by the browser after the first - * page load. - * - * @param basePath - * @param plugins - * @internal - */ -export const loadPluginBundle: LoadPluginBundle = < - TSetup, - TStart, - TPluginsSetup extends object, - TPluginsStart extends object ->( - addBasePath: (path: string) => string, - pluginName: PluginName, - { timeoutMs = LOAD_TIMEOUT }: { timeoutMs?: number } = {} -) => - new Promise>( - (resolve, reject) => { - const coreWindow = (window as unknown) as CoreWindow; - const exportId = `plugin/${pluginName}`; - - const readPluginExport = () => { - const PluginExport: any = coreWindow.__kbnBundles__[exportId]; - if (typeof PluginExport?.plugin !== 'function') { - reject( - new Error(`Definition of plugin "${pluginName}" should be a function (${bundlePath}).`) - ); - } else { - resolve( - PluginExport.plugin as PluginInitializer - ); - } - }; - - if (coreWindow.__kbnBundles__[exportId]) { - readPluginExport(); - return; - } - - const script = document.createElement('script'); - // Assumes that all plugin bundles get put into the bundles/plugins subdirectory - const bundlePath = addBasePath(`/bundles/plugin:${pluginName}/${pluginName}.plugin.js`); - script.setAttribute('src', bundlePath); - script.setAttribute('id', `kbn-plugin-${pluginName}`); - script.setAttribute('async', ''); - - const cleanupTag = () => { - clearTimeout(timeout); - // Set to null for IE memory leak issue. Webpack does the same thing. - // @ts-ignore - script.onload = script.onerror = null; - }; - - // Wire up resolve and reject - script.onload = () => { - cleanupTag(); - readPluginExport(); - }; - - script.onerror = () => { - cleanupTag(); - reject(new Error(`Failed to load "${pluginName}" bundle (${bundlePath})`)); - }; - - const timeout = setTimeout(() => { - cleanupTag(); - reject(new Error(`Timeout reached when loading "${pluginName}" bundle (${bundlePath})`)); - }, timeoutMs); - - // Add the script tag to the end of the body to start downloading - document.body.appendChild(script); - } - ); - -/** - * @internal - */ -export type LoadPluginBundle = < - TSetup, - TStart, - TPluginsSetup extends object, - TPluginsStart extends object ->( - addBasePath: (path: string) => string, - pluginName: PluginName, - options?: { timeoutMs?: number } -) => Promise>; diff --git a/src/core/public/plugins/plugin_reader.test.ts b/src/core/public/plugins/plugin_reader.test.ts new file mode 100644 index 0000000000000..d4324f81de8e6 --- /dev/null +++ b/src/core/public/plugins/plugin_reader.test.ts @@ -0,0 +1,53 @@ +/* + * 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 { CoreWindow, read, UnknownPluginInitializer } from './plugin_reader'; + +const coreWindow: CoreWindow = window as any; +beforeEach(() => { + coreWindow.__kbnBundles__ = {}; +}); + +it('handles undefined plugin exports', () => { + coreWindow.__kbnBundles__['plugin/foo'] = undefined; + + expect(() => { + read('foo'); + }).toThrowError(`Definition of plugin "foo" not found and may have failed to load.`); +}); + +it('handles plugin exports with a "plugin" export that is not a function', () => { + coreWindow.__kbnBundles__['plugin/foo'] = { + plugin: 1234, + } as any; + + expect(() => { + read('foo'); + }).toThrowError(`Definition of plugin "foo" should be a function.`); +}); + +it('returns the plugin initializer when the "plugin" named export is a function', () => { + const plugin: UnknownPluginInitializer = () => { + return undefined as any; + }; + + coreWindow.__kbnBundles__['plugin/foo'] = { plugin }; + + expect(read('foo')).toBe(plugin); +}); diff --git a/src/core/public/plugins/plugin_reader.ts b/src/core/public/plugins/plugin_reader.ts new file mode 100644 index 0000000000000..1907dfa6a3e99 --- /dev/null +++ b/src/core/public/plugins/plugin_reader.ts @@ -0,0 +1,52 @@ +/* + * 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 { PluginInitializer } from './plugin'; + +/** + * Unknown variant for internal use only for when plugins are not known. + * @internal + */ +export type UnknownPluginInitializer = PluginInitializer>; + +/** + * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. + * @internal + */ +export interface CoreWindow { + __kbnBundles__: { + [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; + }; +} + +/** + * Reads the plugin's bundle declared in the global context. + */ +export function read(name: string) { + const coreWindow = (window as unknown) as CoreWindow; + const exportId = `plugin/${name}`; + const pluginExport = coreWindow.__kbnBundles__[exportId]; + if (!pluginExport) { + throw new Error(`Definition of plugin "${name}" not found and may have failed to load.`); + } else if (typeof pluginExport.plugin !== 'function') { + throw new Error(`Definition of plugin "${name}" should be a function.`); + } else { + return pluginExport.plugin; + } +} diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index a76078932518f..85b84e561056a 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -19,16 +19,16 @@ import { PluginName } from 'kibana/server'; import { Plugin } from './plugin'; -import { loadPluginBundleMock } from './plugin_loader.mock'; export type MockedPluginInitializer = jest.Mock>, any>; export const mockPluginInitializerProvider: jest.Mock< MockedPluginInitializer, [PluginName] -> = jest.fn().mockRejectedValue(new Error('No provider specified')); +> = jest.fn().mockImplementation(() => () => { + throw new Error('No provider specified'); +}); -export const mockLoadPluginBundle = loadPluginBundleMock.create(mockPluginInitializerProvider); -jest.mock('./plugin_loader', () => ({ - loadPluginBundle: mockLoadPluginBundle, +jest.mock('./plugin_reader', () => ({ + read: mockPluginInitializerProvider, })); diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 688eaf4f2bfc7..6d71844bc19c8 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -21,7 +21,6 @@ import { omit, pick } from 'lodash'; import { MockedPluginInitializer, - mockLoadPluginBundle, mockPluginInitializerProvider, } from './plugins_service.test.mocks'; @@ -32,6 +31,7 @@ import { PluginsServiceStartDeps, PluginsServiceSetupDeps, } from './plugins_service'; + import { InjectedPluginMetadata } from '../injected_metadata'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { applicationServiceMock } from '../application/application_service.mock'; @@ -152,10 +152,6 @@ describe('PluginsService', () => { ] as unknown) as [[PluginName, any]]); }); - afterEach(() => { - mockLoadPluginBundle.mockClear(); - }); - describe('#getOpaqueIds()', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -174,15 +170,6 @@ describe('PluginsService', () => { }); describe('#setup()', () => { - it('fails if any bundle cannot be loaded', async () => { - mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Could not load bundle"` - ); - }); - it('fails if any plugin instance does not have a setup function', async () => { mockPluginInitializers.set('pluginA', (() => ({})) as any); const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -191,25 +178,6 @@ describe('PluginsService', () => { ); }); - it('calls loadPluginBundles with http and plugins', async () => { - const pluginsService = new PluginsService(mockCoreContext, plugins); - await pluginsService.setup(mockSetupDeps); - - expect(mockLoadPluginBundle).toHaveBeenCalledTimes(3); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginA' - ); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginB' - ); - expect(mockLoadPluginBundle).toHaveBeenCalledWith( - mockSetupDeps.http.basePath.prepend, - 'pluginC' - ); - }); - it('initializes plugins with PluginInitializerContext', async () => { const pluginsService = new PluginsService(mockCoreContext, plugins); await pluginsService.setup(mockSetupDeps); @@ -302,7 +270,6 @@ describe('PluginsService', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); const promise = pluginsService.setup(mockSetupDeps); - jest.runAllTimers(); // load plugin bundles await flushPromises(); jest.runAllTimers(); // setup plugins diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index e698af689036d..862aa5043ad4b 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -93,9 +93,6 @@ export class PluginsService implements CoreService { - // Load plugin bundles - await this.loadPluginBundles(deps.http.basePath.prepend); - // Setup each plugin with required and optional plugin contracts const contracts = new Map(); for (const [pluginName, plugin] of this.plugins.entries()) { @@ -167,9 +164,4 @@ export class PluginsService implements CoreService string) { - // Load all bundles in parallel - return Promise.all([...this.plugins.values()].map(plugin => plugin.load(addBasePath))); - } } diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6d95d1bc7069c..af06b207889c2 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; } @@ -593,6 +593,23 @@ export type HandlerFunction = (context: T, ...args: any[]) => // @public export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; +// @internal (undocumented) +export class HttpFetchError extends Error implements IHttpFetchError { + constructor(message: string, name: string, request: Request, response?: Response | undefined, body?: any); + // (undocumented) + readonly body?: any; + // (undocumented) + readonly name: string; + // (undocumented) + readonly req: Request; + // (undocumented) + readonly request: Request; + // (undocumented) + readonly res?: Response; + // (undocumented) + readonly response?: Response | undefined; +} + // @public export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss new file mode 100644 index 0000000000000..ff28fc75e367d --- /dev/null +++ b/src/core/public/rendering/_base.scss @@ -0,0 +1,46 @@ +/** + * stretch the root element of the Kibana application to set the base-size that + * flexed children should keep. Only works when paired with root styles applied + * by core service from new platform + */ +// SASSTODO: Naming here is too embedded and high up that changing them could cause major breaks +#kibana-body { + overflow-x: hidden; + min-height: 100%; +} + +.app-wrapper { + display: flex; + flex-flow: column nowrap; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: 5; + margin: 0 auto; + + /** + * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state + * when we're looking at the log-in screen. + */ + &.hidden-chrome { + left: 0 !important; /* 1 */ + } + + .navbar-right { + margin-right: 0; + } +} + +.app-wrapper-panel { + display: flex; + flex-grow: 1; + flex-shrink: 0; + flex-basis: auto; + flex-direction: column; + + > * { + flex-shrink: 0; + } +} diff --git a/src/core/public/rendering/_index.scss b/src/core/public/rendering/_index.scss new file mode 100644 index 0000000000000..c8567498b42ec --- /dev/null +++ b/src/core/public/rendering/_index.scss @@ -0,0 +1 @@ +@import './base'; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index afc77806afb91..7958a4f8134d3 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -28,12 +28,6 @@ import { SavedObjectsMigrationVersion, } from '../../server'; -// TODO: Migrate to an error modal powered by the NP? -import { - isAutoCreateIndexError, - showAutoCreateIndexErrorPage, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index'; import { SimpleSavedObject } from './simple_saved_object'; import { HttpFetchOptions, HttpSetup } from '../http'; @@ -222,15 +216,7 @@ export class SavedObjectsClient { }), }); - return createRequest - .then(resp => this.createSavedObject(resp)) - .catch((error: object) => { - if (isAutoCreateIndexError(error)) { - showAutoCreateIndexErrorPage(); - } - - throw error; - }); + return createRequest.then(resp => this.createSavedObject(resp)); }; /** diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index f0b21adf62ff7..9e098c06ba155 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -128,56 +128,6 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), - // Monitoring renames - // TODO: Remove these from here once the monitoring plugin is migrated to NP - renameFromRoot('xpack.monitoring.enabled', 'monitoring.enabled'), - renameFromRoot('xpack.monitoring.ui.enabled', 'monitoring.ui.enabled'), - renameFromRoot( - 'xpack.monitoring.kibana.collection.enabled', - 'monitoring.kibana.collection.enabled' - ), - renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), - renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), - renameFromRoot( - 'xpack.monitoring.show_license_expiration', - 'monitoring.ui.show_license_expiration' - ), - renameFromRoot( - 'xpack.monitoring.ui.container.elasticsearch.enabled', - 'monitoring.ui.container.elasticsearch.enabled' - ), - renameFromRoot( - 'xpack.monitoring.ui.container.logstash.enabled', - 'monitoring.ui.container.logstash.enabled' - ), - renameFromRoot( - 'xpack.monitoring.tests.cloud_detector.enabled', - 'monitoring.tests.cloud_detector.enabled' - ), - renameFromRoot( - 'xpack.monitoring.kibana.collection.interval', - 'monitoring.kibana.collection.interval' - ), - renameFromRoot('xpack.monitoring.elasticsearch.hosts', 'monitoring.ui.elasticsearch.hosts'), - renameFromRoot('xpack.monitoring.elasticsearch.username', 'monitoring.ui.elasticsearch.username'), - renameFromRoot('xpack.monitoring.elasticsearch.password', 'monitoring.ui.elasticsearch.password'), - renameFromRoot( - 'xpack.monitoring.xpack_api_polling_frequency_millis', - 'monitoring.xpack_api_polling_frequency_millis' - ), - renameFromRoot( - 'xpack.monitoring.cluster_alerts.email_notifications.enabled', - 'monitoring.cluster_alerts.email_notifications.enabled' - ), - renameFromRoot( - 'xpack.monitoring.cluster_alerts.email_notifications.email_address', - 'monitoring.cluster_alerts.email_notifications.email_address' - ), - renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), - renameFromRoot( - 'xpack.monitoring.elasticsearch.logFetchCount', - 'monitoring.ui.elasticsearch.logFetchCount' - ), configPathDeprecation, dataPathDeprecation, rewriteBasePathDeprecation, diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index 27db79bb94d25..4fb433b5c77ba 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -1068,6 +1068,14 @@ describe('setup contract', () => { await create(); expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created'); }); + + it('does not throw if called after stop', async () => { + const { createCookieSessionStorageFactory } = await server.setup(config); + await server.stop(); + expect(() => { + createCookieSessionStorageFactory(cookieOptions); + }).not.toThrow(); + }); }); describe('#isTlsEnabled', () => { @@ -1113,4 +1121,54 @@ describe('setup contract', () => { expect(getServerInfo().protocol).toEqual('https'); }); }); + + describe('#registerStaticDir', () => { + it('does not throw if called after stop', async () => { + const { registerStaticDir } = await server.setup(config); + await server.stop(); + expect(() => { + registerStaticDir('/path1/{path*}', '/path/to/resource'); + }).not.toThrow(); + }); + }); + + describe('#registerOnPreAuth', () => { + test('does not throw if called after stop', async () => { + const { registerOnPreAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPreAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); + + describe('#registerOnPostAuth', () => { + test('does not throw if called after stop', async () => { + const { registerOnPostAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPostAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); + + describe('#registerOnPreResponse', () => { + test('does not throw if called after stop', async () => { + const { registerOnPreResponse } = await server.setup(config); + await server.stop(); + expect(() => { + registerOnPreResponse((req, res, t) => t.next()); + }).not.toThrow(); + }); + }); + + describe('#registerAuth', () => { + test('does not throw if called after stop', async () => { + const { registerAuth } = await server.setup(config); + await server.stop(); + expect(() => { + registerAuth((req, res) => res.unauthorized()); + }).not.toThrow(); + }); + }); }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 77d3d99fb48cb..92ac5220735a1 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -74,6 +74,7 @@ export class HttpServer { private registeredRouters = new Set(); private authRegistered = false; private cookieSessionStorageCreated = false; + private stopped = false; private readonly log: Logger; private readonly authState: AuthStateStorage; @@ -144,6 +145,10 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Http server is not setup up yet'); } + if (this.stopped) { + this.log.warn(`start called after stop`); + return; + } this.log.debug('starting http server'); for (const router of this.registeredRouters) { @@ -189,13 +194,13 @@ export class HttpServer { } public async stop() { + this.stopped = true; if (this.server === undefined) { return; } this.log.debug('stopping http server'); await this.server.stop(); - this.server = undefined; } private getAuthOption( @@ -234,6 +239,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`setupConditionalCompression called after stop`); + } const { enabled, referrerWhitelist: list } = config.compression; if (!enabled) { @@ -261,6 +269,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPostAuth called after stop`); + } this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn, this.log)); } @@ -269,6 +280,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPreAuth called after stop`); + } this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log)); } @@ -277,6 +291,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerOnPreResponse called after stop`); + } this.server.ext('onPreResponse', adoptToHapiOnPreResponseFormat(fn, this.log)); } @@ -288,6 +305,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`createCookieSessionStorageFactory called after stop`); + } if (this.cookieSessionStorageCreated) { throw new Error('A cookieSessionStorageFactory was already created'); } @@ -305,6 +325,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Server is not created yet'); } + if (this.stopped) { + this.log.warn(`registerAuth called after stop`); + } if (this.authRegistered) { throw new Error('Auth interceptor was already registered'); } @@ -348,6 +371,9 @@ export class HttpServer { if (this.server === undefined) { throw new Error('Http server is not setup up yet'); } + if (this.stopped) { + this.log.warn(`registerStaticDir called after stop`); + } this.server.route({ path, diff --git a/src/core/server/http/router/validator/validator.ts b/src/core/server/http/router/validator/validator.ts index 6c766e69f0f37..a2299b47ae253 100644 --- a/src/core/server/http/router/validator/validator.ts +++ b/src/core/server/http/router/validator/validator.ts @@ -17,7 +17,14 @@ * under the License. */ -import { ValidationError, Type, schema, ObjectType, isConfigSchema } from '@kbn/config-schema'; +import { + ValidationError, + Type, + schema, + ObjectType, + TypeOf, + isConfigSchema, +} from '@kbn/config-schema'; import { Stream } from 'stream'; import { RouteValidationError } from './validator_error'; @@ -85,7 +92,7 @@ type RouteValidationResultType | undefined> = T extends RouteValidationFunction ? ReturnType['value'] : T extends Type - ? ReturnType + ? TypeOf : undefined >; @@ -170,7 +177,7 @@ export class RouteValidator

{ * @internal */ public getParams(data: unknown, namespace?: string): Readonly

{ - return this.validate(this.config.params, this.options.unsafe?.params, data, namespace); + return this.validate(this.config.params, this.options.unsafe?.params, data, namespace) as P; } /** @@ -178,7 +185,7 @@ export class RouteValidator

{ * @internal */ public getQuery(data: unknown, namespace?: string): Readonly { - return this.validate(this.config.query, this.options.unsafe?.query, data, namespace); + return this.validate(this.config.query, this.options.unsafe?.query, data, namespace) as Q; } /** @@ -186,7 +193,7 @@ export class RouteValidator

{ * @internal */ public getBody(data: unknown, namespace?: string): Readonly { - return this.validate(this.config.body, this.options.unsafe?.body, data, namespace); + return this.validate(this.config.body, this.options.unsafe?.body, data, namespace) as B; } /** diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ef57fae159d7e..86192245bd2d1 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -230,6 +230,7 @@ export { SavedObjectsMigrationLogger, SavedObjectsRawDoc, SavedObjectSanitizedDoc, + SavedObjectUnsanitizedDoc, SavedObjectsRepositoryFactory, SavedObjectsResolveImportErrorsOptions, SavedObjectsSchema, diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts index 1790b096a71ae..dfa2396d5904b 100644 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts @@ -48,7 +48,7 @@ describe('logLegacyThirdPartyPluginDeprecationWarning', () => { expect(log.warn).toHaveBeenCalledTimes(1); expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", ] `); }); @@ -65,7 +65,7 @@ describe('logLegacyThirdPartyPluginDeprecationWarning', () => { expect(log.warn).toHaveBeenCalledTimes(1); expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` Array [ - "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html for a list of breaking changes and https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md for documentation on how to migrate legacy plugins.", + "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", ] `); }); diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts index f9c3dcbf554cb..df86f5a2b4031 100644 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts +++ b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts @@ -22,9 +22,10 @@ import { LegacyPluginSpec } from '../types'; const internalPaths = ['/src/legacy/core_plugins', '/x-pack']; -const breakingChangesUrl = - 'https://www.elastic.co/guide/en/kibana/master/breaking-changes-8.0.html'; -const migrationGuideUrl = 'https://github.com/elastic/kibana/blob/master/src/core/MIGRATION.md'; +// Use shortened URLs so destinations can be updated if/when documentation moves +// All platform team members have access to edit these +const breakingChangesUrl = 'https://ela.st/kibana-breaking-changes-8-0'; +const migrationGuideUrl = 'https://ela.st/kibana-platform-migration'; export const logLegacyThirdPartyPluginDeprecationWarning = ({ specs, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 3b9a39db72278..c707fa2b479e4 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -18,7 +18,7 @@ */ import { of } from 'rxjs'; import { duration } from 'moment'; -import { PluginInitializerContext, CoreSetup, CoreStart } from '.'; +import { PluginInitializerContext, CoreSetup, CoreStart, StartServicesAccessor } from '.'; import { CspConfig } from './csp'; import { loggingServiceMock } from './logging/logging_service.mock'; import { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service.mock'; @@ -45,6 +45,7 @@ export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service. export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; +export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; @@ -99,7 +100,9 @@ function pluginInitializerContextMock(config: T = {} as T) { return mock; } -type CoreSetupMockType = MockedKeys & jest.Mocked>; +type CoreSetupMockType = MockedKeys & { + getStartServices: jest.MockedFunction>; +}; function createCoreSetupMock({ pluginStartDeps = {}, diff --git a/src/core/server/rendering/views/template.tsx b/src/core/server/rendering/views/template.tsx index b38259a84cb9c..73e119a5a97e7 100644 --- a/src/core/server/rendering/views/template.tsx +++ b/src/core/server/rendering/views/template.tsx @@ -104,6 +104,10 @@ export const Template: FunctionComponent = ({ + + {/* Inject stylesheets into the before scripts so that KP plugins with bundled styles will override them */} + + {createElement('kbn-csp', { diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index 47fc29f8cf7d2..c1b65763949bb 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -131,6 +131,7 @@ export interface IndexMappingMeta { */ export interface SavedObjectsCoreFieldMapping { type: string; + null_value?: number | boolean | string; index?: boolean; enabled?: boolean; fields?: { diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 64270c677ff20..3ec478e3ca28d 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -149,13 +149,13 @@ describe('DocumentMigrator', () => { expect(_.get(migratedDoc, 'attributes.name')).toBe('Mike'); }); - it('migrates meta properties', () => { + it('migrates root properties', () => { const migrator = new DocumentMigrator({ ...testOpts(), typeRegistry: createRegistry({ name: 'acl', migrations: { - '2.3.5': setAttr('acl', 'admins-only,sucka!'), + '2.3.5': setAttr('acl', 'admins-only, sucka!'), }, }), }); @@ -165,13 +165,13 @@ describe('DocumentMigrator', () => { attributes: { name: 'Tyler' }, acl: 'anyone', migrationVersion: {}, - }); + } as SavedObjectUnsanitizedDoc); expect(actual).toEqual({ id: 'me', type: 'user', attributes: { name: 'Tyler' }, migrationVersion: { acl: '2.3.5' }, - acl: 'admins-only,sucka!', + acl: 'admins-only, sucka!', }); }); @@ -241,7 +241,7 @@ describe('DocumentMigrator', () => { type: 'user', attributes: { name: 'Tyler' }, bbb: 'Shazm', - }); + } as SavedObjectUnsanitizedDoc); expect(actual).toEqual({ id: 'me', type: 'user', @@ -405,7 +405,7 @@ describe('DocumentMigrator', () => { attributes: { name: 'Callie' }, dawg: 'Yo', migrationVersion: {}, - }); + } as SavedObjectUnsanitizedDoc); expect(actual).toEqual({ id: 'smelly', type: 'foo', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 0284f513a361c..4ddb2b070d3ac 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -279,7 +279,7 @@ function props(doc: SavedObjectUnsanitizedDoc) { */ function propVersion(doc: SavedObjectUnsanitizedDoc | ActiveMigrations, prop: string) { return ( - (doc[prop] && doc[prop].latestVersion) || + ((doc as any)[prop] && (doc as any)[prop].latestVersion) || (doc.migrationVersion && (doc as any).migrationVersion[prop]) ); } diff --git a/src/core/server/saved_objects/migrations/core/index.ts b/src/core/server/saved_objects/migrations/core/index.ts index 466d399f653cd..f7274740ea5fe 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 0000000000000..76a890d26bfa0 --- /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 6bc085dde872e..85f15b4c18b66 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 7ba4613c857d7..4e1f5981d6a41 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 7ea61f67e9496..acd2c7b5284aa 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -45,13 +45,10 @@ export interface SavedObjectsRawDocSource { } /** - * A saved object type definition that allows for miscellaneous, unknown - * properties, as current discussions around security, ACLs, etc indicate - * that future props are likely to be added. Migrations support this - * scenario out of the box. + * 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; @@ -59,8 +56,6 @@ interface SavedObjectDoc { migrationVersion?: SavedObjectsMigrationVersion; version?: string; updated_at?: string; - - [rootProp: string]: any; } interface Referencable { @@ -68,14 +63,18 @@ interface Referencable { } /** - * We want to have two types, one that guarantees a "references" attribute - * will exist and one that allows it to be null. Since we're not migrating - * all the saved objects to have a "references" array, we need to support - * the scenarios where it may be missing (ex migrations). + * Describes Saved Object documents from Kibana < 7.0.0 which don't have a + * `references` root property defined. This type should only be used in + * migrations. * * @public */ -export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; -/** @public */ -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +/** + * Describes Saved Object documents that have passed through the migration + * framework and are guaranteed to have a `references` root property. + * + * @public + */ +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts index 4a43835d795d1..324d19e279212 100644 --- a/src/core/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -403,43 +403,4 @@ describe('savedObjectsClient/errorTypes', () => { }); }); }); - - describe('EsAutoCreateIndex error', () => { - describe('createEsAutoCreateIndexError', () => { - it('does not take an error argument', () => { - const error = new Error(); - // @ts-ignore - expect(SavedObjectsErrorHelpers.createEsAutoCreateIndexError(error)).not.toBe(error); - }); - - it('returns a new Error', () => { - expect(SavedObjectsErrorHelpers.createEsAutoCreateIndexError()).toBeInstanceOf(Error); - }); - - it('makes errors identifiable as EsAutoCreateIndex errors', () => { - expect( - SavedObjectsErrorHelpers.isEsAutoCreateIndexError( - SavedObjectsErrorHelpers.createEsAutoCreateIndexError() - ) - ).toBe(true); - }); - - it('returns a boom error', () => { - const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error).toHaveProperty('isBoom', true); - }); - - describe('error.output', () => { - it('uses "Automatic index creation failed" message', () => { - const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed'); - }); - - it('sets statusCode to 503', () => { - const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error.output).toHaveProperty('statusCode', 503); - }); - }); - }); - }); }); diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 478c6b6d26d53..9614d692741e0 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -37,8 +37,6 @@ const CODE_CONFLICT = 'SavedObjectsClient/conflict'; const CODE_ES_CANNOT_EXECUTE_SCRIPT = 'SavedObjectsClient/esCannotExecuteScript'; // 503 - Es Unavailable const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; -// 503 - Unable to automatically create index because of action.auto_create_index setting -const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex'; // 500 - General Error const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError'; @@ -180,18 +178,6 @@ export class SavedObjectsErrorHelpers { return isSavedObjectsClientError(error) && error[code] === CODE_ES_UNAVAILABLE; } - public static createEsAutoCreateIndexError() { - const error = Boom.serverUnavailable('Automatic index creation failed'); - error.output.payload.attributes = error.output.payload.attributes || {}; - error.output.payload.attributes.code = 'ES_AUTO_CREATE_INDEX_ERROR'; - - return decorate(error, CODE_ES_AUTO_CREATE_INDEX_ERROR, 503); - } - - public static isEsAutoCreateIndexError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR; - } - public static decorateGeneralError(error: Error, reason?: string) { return decorate(error, CODE_GENERAL_ERROR, 500, reason); } diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 5f17c11792763..bc8ad2cdb0058 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -239,40 +239,31 @@ export class SavedObjectsRepository { } } - try { - const migrated = this._migrator.migrateDocument({ - id, - type, - ...(savedObjectNamespace && { namespace: savedObjectNamespace }), - ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes, - migrationVersion, - updated_at: time, - ...(Array.isArray(references) && { references }), - }); - - const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); + const migrated = this._migrator.migrateDocument({ + id, + type, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + attributes, + migrationVersion, + updated_at: time, + ...(Array.isArray(references) && { references }), + }); - const method = id && overwrite ? 'index' : 'create'; - const response = await this._writeToCluster(method, { - id: raw._id, - index: this.getIndexForType(type), - refresh, - body: raw._source, - }); + const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - return this._rawToSavedObject({ - ...raw, - ...response, - }); - } catch (error) { - if (SavedObjectsErrorHelpers.isNotFoundError(error)) { - // See "503s from missing index" above - throw SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - } + const method = id && overwrite ? 'index' : 'create'; + const response = await this._writeToCluster(method, { + id: raw._id, + index: this.getIndexForType(type), + refresh, + body: raw._source, + }); - throw error; - } + return this._rawToSavedObject({ + ...raw, + ...response, + }); } /** diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index b50c6dc9a1abf..43b7663491711 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -156,15 +156,6 @@ export type MutatingOperationRefreshSetting = boolean | 'wait_for'; * takes special care to ensure that 404 errors are generic and don't distinguish * between index missing or document missing. * - * ### 503s from missing index - * - * Unlike all other methods, create requests are supposed to succeed even when - * the Kibana index does not exist because it will be automatically created by - * elasticsearch. When that is not the case it is because Elasticsearch's - * `action.auto_create_index` setting prevents it from being created automatically - * so we throw a special 503 with the intention of informing the user that their - * Elasticsearch settings need to be updated. - * * See {@link SavedObjectsClient} * See {@link SavedObjectsErrorHelpers} * diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7ca5c75f19e8f..e8b77a8570291 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1679,10 +1679,8 @@ export interface SavedObjectMigrationContext { log: SavedObjectsMigrationLogger; } -// Warning: (ae-forgotten-export) The symbol "SavedObjectUnsanitizedDoc" needs to be exported by the entry point index.d.ts -// // @public -export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; +export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; // @public export interface SavedObjectMigrationMap { @@ -1709,8 +1707,8 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // -// @public (undocumented) -export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; +// @public +export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; // @public (undocumented) export interface SavedObjectsBaseOptions { @@ -1842,6 +1840,8 @@ export interface SavedObjectsCoreFieldMapping { // (undocumented) index?: boolean; // (undocumented) + null_value?: number | boolean | string; + // (undocumented) type: string; } @@ -1877,8 +1877,6 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createConflictError(type: string, id: string): DecoratedError; // (undocumented) - static createEsAutoCreateIndexError(): DecoratedError; - // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; @@ -1905,8 +1903,6 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isConflictError(error: Error | DecoratedError): boolean; // (undocumented) - static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; - // (undocumented) static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean; // (undocumented) static isEsUnavailableError(error: Error | DecoratedError): boolean; @@ -2314,6 +2310,9 @@ export class SavedObjectTypeRegistry { registerType(type: SavedObjectsType): void; } +// @public +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; + // @public export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; diff --git a/src/dev/build/tasks/build_kibana_platform_plugins.js b/src/dev/build/tasks/build_kibana_platform_plugins.js index 101d6bd15fc10..28d6b49f9e89a 100644 --- a/src/dev/build/tasks/build_kibana_platform_plugins.js +++ b/src/dev/build/tasks/build_kibana_platform_plugins.js @@ -17,7 +17,13 @@ * under the License. */ -import { runOptimizer, OptimizerConfig, logOptimizerState } from '@kbn/optimizer'; +import { CiStatsReporter } from '@kbn/dev-utils'; +import { + runOptimizer, + OptimizerConfig, + logOptimizerState, + reportOptimizerStats, +} from '@kbn/optimizer'; export const BuildKibanaPlatformPluginsTask = { description: 'Building distributable versions of Kibana platform plugins', @@ -29,10 +35,17 @@ export const BuildKibanaPlatformPluginsTask = { examples: false, watch: false, dist: true, + includeCoreBundle: true, }); + const reporter = CiStatsReporter.fromEnv(log); + const reportStatsName = build.isOss() ? 'oss distributable' : 'default distributable'; + await runOptimizer(optimizerConfig) - .pipe(logOptimizerState(log, optimizerConfig)) + .pipe( + reportOptimizerStats(reporter, reportStatsName), + logOptimizerState(log, optimizerConfig) + ) .toPromise(); }, }; diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 52928d6e47fc4..8e8d69a4dfefa 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -48,12 +48,12 @@ export const CleanClientModulesOnDLLTask = { ]; const discoveredLegacyCorePluginEntries = await globby([ `${baseDir}/src/legacy/core_plugins/*/index.js`, - // Small exception to load dynamically discovered functions for timelion plugin - `${baseDir}/src/legacy/core_plugins/timelion/server/*_functions/**/*.js`, `!${baseDir}/src/legacy/core_plugins/**/public`, ]); const discoveredPluginEntries = await globby([ `${baseDir}/src/plugins/*/server/index.js`, + // Small exception to load dynamically discovered functions for timelion plugin + `${baseDir}/src/plugins/vis_type_timelion/server/*_functions/**/*.js`, `!${baseDir}/src/plugins/**/public`, ]); const discoveredNewPlatformXpackPlugins = await globby([ diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index d4d2e86e1e96b..d1fb544de733c 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -72,6 +72,22 @@ kibana_vars=( map.tilemap.options.minZoom map.tilemap.options.subdomains map.tilemap.url + monitoring.cluster_alerts.email_notifications.email_address + monitoring.enabled + monitoring.kibana.collection.enabled + monitoring.kibana.collection.interval + monitoring.ui.container.elasticsearch.enabled + monitoring.ui.container.logstash.enabled + monitoring.ui.elasticsearch.password + monitoring.ui.elasticsearch.pingTimeout + monitoring.ui.elasticsearch.hosts + monitoring.ui.elasticsearch.username + monitoring.ui.elasticsearch.logFetchCount + monitoring.ui.elasticsearch.ssl.certificateAuthorities + monitoring.ui.elasticsearch.ssl.verificationMode + monitoring.ui.enabled + monitoring.ui.max_bucket_size + monitoring.ui.min_interval_seconds newsfeed.enabled ops.interval path.data @@ -160,25 +176,6 @@ kibana_vars=( xpack.infra.sources.default.metricAlias xpack.license_management.enabled xpack.ml.enabled - xpack.monitoring.cluster_alerts.email_notifications.email_address - xpack.monitoring.elasticsearch.password - xpack.monitoring.elasticsearch.pingTimeout - xpack.monitoring.elasticsearch.hosts - xpack.monitoring.elasticsearch.username - xpack.monitoring.elasticsearch.logFetchCount - xpack.monitoring.elasticsearch.ssl.certificateAuthorities - xpack.monitoring.elasticsearch.ssl.verificationMode - xpack.monitoring.enabled - xpack.monitoring.kibana.collection.enabled - xpack.monitoring.kibana.collection.interval - xpack.monitoring.max_bucket_size - xpack.monitoring.min_interval_seconds - xpack.monitoring.node_resolver - xpack.monitoring.report_stats - xpack.monitoring.elasticsearch.pingTimeout - xpack.monitoring.ui.container.elasticsearch.enabled - xpack.monitoring.ui.container.logstash.enabled - xpack.monitoring.ui.enabled xpack.reporting.capture.browser.autoDownload xpack.reporting.capture.browser.chromium.disableSandbox xpack.reporting.capture.browser.chromium.inspect @@ -195,11 +192,16 @@ kibana_vars=( xpack.reporting.capture.viewport.width xpack.reporting.capture.zoom xpack.reporting.csv.checkForFormulas + xpack.reporting.csv.escapeFormulaValues xpack.reporting.csv.enablePanelActionDownload + xpack.reporting.csv.useByteOrderMarkEncoding xpack.reporting.csv.maxSizeBytes xpack.reporting.csv.scroll.duration xpack.reporting.csv.scroll.size xpack.reporting.capture.maxAttempts + xpack.reporting.capture.timeouts.openUrl + xpack.reporting.capture.timeouts.waitForElements + xpack.reporting.capture.timeouts.renderComplete xpack.reporting.enabled xpack.reporting.encryptionKey xpack.reporting.index @@ -232,6 +234,7 @@ kibana_vars=( xpack.security.session.idleTimeout xpack.security.session.lifespan xpack.security.loginAssistanceMessage + xpack.security.loginHelp telemetry.allowChangingOptInStatus telemetry.enabled telemetry.optIn diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js index 69df33f7f2e11..c80f9334cfaeb 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.js @@ -24,12 +24,12 @@ function generator({ imageFlavor }) { # # ** THIS IS AN AUTO-GENERATED FILE ** # - + # Default Kibana configuration for docker target server.name: kibana server.host: "0" elasticsearch.hosts: [ "http://elasticsearch:9200" ] - ${!imageFlavor ? 'xpack.monitoring.ui.container.elasticsearch.enabled: true' : ''} + ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} `); } diff --git a/src/dev/eslint/index.js b/src/dev/eslint/index.ts similarity index 100% rename from src/dev/eslint/index.js rename to src/dev/eslint/index.ts diff --git a/src/dev/eslint/lint_files.js b/src/dev/eslint/lint_files.js deleted file mode 100644 index a76edeb2eb865..0000000000000 --- a/src/dev/eslint/lint_files.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 { CLIEngine } from 'eslint'; - -import { createFailError } from '@kbn/dev-utils'; -import { REPO_ROOT } from '../constants'; - -/** - * Lints a list of files with eslint. eslint reports are written to the log - * and a FailError is thrown when linting errors occur. - * - * @param {ToolingLog} log - * @param {Array} files - * @return {undefined} - */ -export function lintFiles(log, files, { fix } = {}) { - const cli = new CLIEngine({ - cache: true, - cwd: REPO_ROOT, - fix, - }); - - const paths = files.map(file => file.getRelativePath()); - const report = cli.executeOnFiles(paths); - - if (fix) { - CLIEngine.outputFixes(report); - } - - const failTypes = []; - if (report.errorCount > 0) failTypes.push('errors'); - if (report.warningCount > 0) failTypes.push('warning'); - - if (!failTypes.length) { - log.success('[eslint] %d files linted successfully', files.length); - return; - } - - log.error(cli.getFormatter()(report.results)); - throw createFailError(`[eslint] ${failTypes.join(' & ')}`); -} diff --git a/src/dev/eslint/lint_files.ts b/src/dev/eslint/lint_files.ts new file mode 100644 index 0000000000000..80c493233f39a --- /dev/null +++ b/src/dev/eslint/lint_files.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 { CLIEngine } from 'eslint'; + +import { createFailError, ToolingLog } from '@kbn/dev-utils'; +import { File } from '../file'; +import { REPO_ROOT } from '../constants'; + +/** + * Lints a list of files with eslint. eslint reports are written to the log + * and a FailError is thrown when linting errors occur. + * + * @param {ToolingLog} log + * @param {Array} files + * @return {undefined} + */ +export function lintFiles(log: ToolingLog, files: File[], { fix }: { fix?: boolean } = {}) { + const cli = new CLIEngine({ + cache: true, + cwd: REPO_ROOT, + fix, + }); + + const paths = files.map(file => file.getRelativePath()); + const report = cli.executeOnFiles(paths); + + if (fix) { + CLIEngine.outputFixes(report); + } + + const failTypes = []; + if (report.errorCount > 0) failTypes.push('errors'); + if (report.warningCount > 0) failTypes.push('warning'); + + if (!failTypes.length) { + log.success('[eslint] %d files linted successfully', files.length); + return; + } + + log.error(cli.getFormatter()(report.results)); + throw createFailError(`[eslint] ${failTypes.join(' & ')}`); +} diff --git a/src/dev/eslint/pick_files_to_lint.js b/src/dev/eslint/pick_files_to_lint.js deleted file mode 100644 index e3212c00d9e0d..0000000000000 --- a/src/dev/eslint/pick_files_to_lint.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { CLIEngine } from 'eslint'; - -/** - * Filters a list of files to only include lintable files. - * - * @param {ToolingLog} log - * @param {Array} files - * @return {Array} - */ -export function pickFilesToLint(log, files) { - const cli = new CLIEngine(); - - return files.filter(file => { - if (!file.isJs() && !file.isTypescript()) { - return; - } - - const path = file.getRelativePath(); - - if (cli.isPathIgnored(path)) { - log.warning(`[eslint] %j ignored by .eslintignore`, file); - return false; - } - - log.debug('[eslint] linting %j', file); - return true; - }); -} diff --git a/src/dev/eslint/pick_files_to_lint.ts b/src/dev/eslint/pick_files_to_lint.ts new file mode 100644 index 0000000000000..b96781fc3a611 --- /dev/null +++ b/src/dev/eslint/pick_files_to_lint.ts @@ -0,0 +1,49 @@ +/* + * 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 { CLIEngine } from 'eslint'; + +import { ToolingLog } from '@kbn/dev-utils'; +import { File } from '../file'; + +/** + * Filters a list of files to only include lintable files. + * + * @param {ToolingLog} log + * @param {Array} files + * @return {Array} + */ +export function pickFilesToLint(log: ToolingLog, files: File[]) { + const cli = new CLIEngine({}); + + return files.filter(file => { + if (!file.isJs() && !file.isTypescript()) { + return; + } + + const path = file.getRelativePath(); + + if (cli.isPathIgnored(path)) { + log.warning(`[eslint] %j ignored by .eslintignore`, file); + return false; + } + + log.debug('[eslint] linting %j', file); + return true; + }); +} diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 7da14e0dfe51b..c5387590fcf66 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', @@ -63,6 +64,7 @@ export default { '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js', '\\.ace\\.worker.js$': '/src/dev/jest/mocks/ace_worker_module_mock.js', + '^(!!)?file-loader!': '/src/dev/jest/mocks/file_mock.js', }, setupFiles: [ '/src/dev/jest/setup/babel_polyfill.js', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 1b5110a61cbc4..8630221b3e94f 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -35,9 +35,9 @@ export const IGNORE_FILE_GLOBS = [ '**/Gruntfile.js', 'tasks/config/**/*', '**/{Dockerfile,docker-compose.yml}', - 'x-pack/legacy/plugins/apm/**/*', 'x-pack/legacy/plugins/canvas/tasks/**/*', 'x-pack/legacy/plugins/canvas/canvas_plugin_src/**/*', + 'x-pack/plugins/monitoring/public/lib/jquery_flot/**/*', '**/.*', '**/{webpackShims,__mocks__}/**/*', 'x-pack/docs/**/*', @@ -58,6 +58,11 @@ export const IGNORE_FILE_GLOBS = [ // filename required by api-extractor 'api-documenter.json', + + // TODO fix file names in APM to remove these + 'x-pack/plugins/apm/public/**/*', + 'x-pack/plugins/apm/scripts/**/*', + 'x-pack/plugins/apm/e2e/**/*', ]; /** @@ -116,13 +121,7 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', - 'src/legacy/core_plugins/timelion/server/lib/asSorted.js', - 'src/legacy/core_plugins/timelion/server/lib/unzipPairs.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/bucketList.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/seriesList.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/tlConfig.js', 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', - 'src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', 'src/core/server/core_app/assets/favicons/android-chrome-256x256.png', 'src/core/server/core_app/assets/favicons/android-chrome-512x512.png', @@ -164,16 +163,13 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'webpackShims/elasticsearch-browser.js', 'webpackShims/moment-timezone.js', 'webpackShims/ui-bootstrap.js', - 'x-pack/legacy/plugins/graph/public/graphClientWorkspace.js', - 'x-pack/legacy/plugins/graph/public/angular-venn-simple.js', 'x-pack/legacy/plugins/index_management/public/lib/editSettings.js', 'x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js', - 'x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', - 'x-pack/legacy/plugins/monitoring/public/icons/alert-blue.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-green.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-red.svg', - 'x-pack/legacy/plugins/monitoring/public/icons/health-yellow.svg', + 'x-pack/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', + 'x-pack/plugins/monitoring/public/icons/health-gray.svg', + 'x-pack/plugins/monitoring/public/icons/health-green.svg', + 'x-pack/plugins/monitoring/public/icons/health-red.svg', + 'x-pack/plugins/monitoring/public/icons/health-yellow.svg', 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Medium.ttf', 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/noto/NotoSansCJKtc-Regular.ttf', 'x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/pdf/assets/fonts/roboto/Roboto-Italic.ttf', diff --git a/src/dev/run_check_lockfile_symlinks.js b/src/dev/run_check_lockfile_symlinks.js index 6c6fc54638ee8..b912ea9ddb87e 100644 --- a/src/dev/run_check_lockfile_symlinks.js +++ b/src/dev/run_check_lockfile_symlinks.js @@ -35,9 +35,9 @@ const IGNORE_FILE_GLOBS = [ // fixtures aren't used in production, ignore them '**/*fixtures*/**/*', // cypress isn't used in production, ignore it - 'x-pack/legacy/plugins/apm/e2e/*', + 'x-pack/plugins/apm/e2e/*', // apm scripts aren't used in production, ignore them - 'x-pack/legacy/plugins/apm/scripts/*', + 'x-pack/plugins/apm/scripts/*', ]; run(async ({ log }) => { diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index ba3cd1280f34b..6d5fa04a93951 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -250,7 +250,7 @@ async function run( Options: --accept {dim Accepts all changes by updating the API Review files and documentation} --docs {dim Updates the Core API documentation} - --only {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} + --filter {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} --help {dim Show this message} `) ); diff --git a/src/dev/run_prettier_on_changed.ts b/src/dev/run_prettier_on_changed.ts new file mode 100644 index 0000000000000..deca4fa1be3ce --- /dev/null +++ b/src/dev/run_prettier_on_changed.ts @@ -0,0 +1,87 @@ +/* + * 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 execa from 'execa'; +// @ts-ignore +import SimpleGit from 'simple-git'; +import { run } from '@kbn/dev-utils'; +import dedent from 'dedent'; +import Util from 'util'; + +import pkg from '../../package.json'; +import { REPO_ROOT } from './constants'; +import { File } from './file'; +import * as Eslint from './eslint'; + +run(async function getChangedFiles({ log }) { + const simpleGit = new SimpleGit(REPO_ROOT); + + const getStatus = Util.promisify(simpleGit.status.bind(simpleGit)); + const gitStatus = await getStatus(); + + if (gitStatus.files.length > 0) { + throw new Error( + dedent(`You should run prettier formatter on a clean branch. + Found not committed changes to: + ${gitStatus.files.map((f: { path: string }) => f.path).join('\n')}`) + ); + } + + const revParse = Util.promisify(simpleGit.revparse.bind(simpleGit)); + const currentBranch = await revParse(['--abbrev-ref', 'HEAD']); + const headBranch = pkg.branch; + + const diff = Util.promisify(simpleGit.diff.bind(simpleGit)); + + const changedFileStatuses: string = await diff([ + '--name-status', + `${headBranch}...${currentBranch}`, + ]); + + const changedFiles = changedFileStatuses + .split('\n') + // Ignore blank lines + .filter(line => line.trim().length > 0) + // git diff --name-status outputs lines with two OR three parts + // separated by a tab character + .map(line => line.trim().split('\t')) + .map(([status, ...paths]) => { + // ignore deleted files + if (status === 'D') { + return undefined; + } + + // the status is always in the first column + // .. If the file is edited the line will only have two columns + // .. If the file is renamed it will have three columns + // .. In any case, the last column is the CURRENT path to the file + return new File(paths[paths.length - 1]); + }) + .filter((file): file is File => Boolean(file)); + + const pathsToLint = Eslint.pickFilesToLint(log, changedFiles).map(f => f.getAbsolutePath()); + + if (pathsToLint.length > 0) { + log.debug('[prettier] run on %j files: ', pathsToLint.length, pathsToLint); + } + + while (pathsToLint.length > 0) { + await execa('npx', ['prettier@2.0.4', '--write', ...pathsToLint.splice(0, 100)]); + } +}); diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 43114b2edccfc..4dc930dae3e25 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -18,7 +18,7 @@ */ export const storybookAliases = { - apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js', + apm: 'x-pack/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 01d8a30b598c1..a13f61af60173 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -30,7 +30,7 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'x-pack/plugins/siem/cypress/tsconfig.json'), { name: 'siem/cypress', }), - new Project(resolve(REPO_ROOT, 'x-pack/legacy/plugins/apm/e2e/tsconfig.json'), { + new Project(resolve(REPO_ROOT, 'x-pack/plugins/apm/e2e/tsconfig.json'), { name: 'apm/cypress', disableTypeCheck: true, }), diff --git a/src/es_archiver/actions/rebuild_all.ts b/src/es_archiver/actions/rebuild_all.ts index 1467a1d0430b7..f35b2ca49c666 100644 --- a/src/es_archiver/actions/rebuild_all.ts +++ b/src/es_archiver/actions/rebuild_all.ts @@ -18,7 +18,7 @@ */ import { resolve, dirname, relative } from 'path'; -import { stat, rename, createReadStream, createWriteStream } from 'fs'; +import { stat, Stats, rename, createReadStream, createWriteStream } from 'fs'; import { Readable, Writable } from 'stream'; import { fromNode } from 'bluebird'; import { ToolingLog } from '@kbn/dev-utils'; @@ -33,7 +33,7 @@ import { } from '../lib'; async function isDirectory(path: string): Promise { - const stats = await fromNode(cb => stat(path, cb)); + const stats: Stats = await fromNode(cb => stat(path, cb)); return stats.isDirectory(); } diff --git a/src/legacy/core_plugins/input_control_vis/index.ts b/src/legacy/core_plugins/input_control_vis/index.ts deleted file mode 100644 index 0529aa24dffd7..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const inputControlVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'input_control_vis', - require: ['kibana', 'elasticsearch', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default inputControlVisPluginInitializer; diff --git a/src/legacy/core_plugins/input_control_vis/package.json b/src/legacy/core_plugins/input_control_vis/package.json deleted file mode 100644 index 0d52be412f2fd..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "input_control_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts deleted file mode 100644 index e6426e5a4c69d..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 { listControlFactory, ListControl } from './list_control_factory'; -import { ControlParams, CONTROL_TYPES } from '../editor_utils'; -import { getDepsMock, getSearchSourceMock } from '../test_utils'; - -const MockSearchSource = getSearchSourceMock(); -const deps = getDepsMock(); - -jest.doMock('./create_search_source.ts', () => ({ - createSearchSource: MockSearchSource, -})); - -describe('hasValue', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - const useTimeFilter = false; - - let listControl: ListControl; - beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); - }); - - test('should be false when control has no value', () => { - expect(listControl.hasValue()).toBe(false); - }); - - test('should be true when control has value', () => { - listControl.set([{ value: 'selected option', label: 'selection option' }]); - expect(listControl.hasValue()).toBe(true); - }); - - test('should be true when control has value that is the string "false"', () => { - listControl.set([{ value: 'false', label: 'selection option' }]); - expect(listControl.hasValue()).toBe(true); - }); -}); - -describe('fetch', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - const useTimeFilter = false; - - let listControl: ListControl; - beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); - }); - - test('should pass in timeout parameters from injected vars', async () => { - await listControl.fetch(); - expect(MockSearchSource).toHaveBeenCalledWith({ - timeout: `1000ms`, - terminate_after: 100000, - }); - }); - - test('should set selectOptions to results of terms aggregation', async () => { - await listControl.fetch(); - expect(listControl.selectOptions).toEqual([ - 'Zurich Airport', - 'Xi an Xianyang International Airport', - ]); - }); -}); - -describe('fetch with ancestors', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - const useTimeFilter = false; - - let listControl: ListControl; - let parentControl; - beforeEach(async () => { - listControl = await listControlFactory(controlParams, useTimeFilter, MockSearchSource, deps); - - const parentControlParams: ControlParams = { - id: 'parent', - fieldName: 'myField', - options: {} as any, - type: CONTROL_TYPES.LIST, - label: 'test', - indexPattern: {} as any, - parent: 'parent', - }; - parentControl = await listControlFactory( - parentControlParams, - useTimeFilter, - MockSearchSource, - deps - ); - parentControl.clear(); - listControl.setAncestors([parentControl]); - }); - - describe('ancestor does not have value', () => { - test('should disable control', async () => { - await listControl.fetch(); - expect(listControl.isEnabled()).toBe(false); - }); - - test('should reset lastAncestorValues', async () => { - listControl.lastAncestorValues = 'last ancestor value'; - await listControl.fetch(); - expect(listControl.lastAncestorValues).toBeUndefined(); - }); - }); -}); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts deleted file mode 100644 index 32df9de8ac983..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 { rangeControlFactory } from './range_control_factory'; -import { ControlParams, CONTROL_TYPES } from '../editor_utils'; -import { getDepsMock, getSearchSourceMock } from '../test_utils'; - -const deps = getDepsMock(); - -describe('fetch', () => { - const controlParams: ControlParams = { - id: '1', - fieldName: 'myNumberField', - options: {}, - type: CONTROL_TYPES.RANGE, - label: 'test', - indexPattern: {} as any, - parent: {} as any, - }; - const useTimeFilter = false; - - test('should set min and max from aggregation results', async () => { - const esSearchResponse = { - aggregations: { - maxAgg: { value: 100 }, - minAgg: { value: 10 }, - }, - }; - const rangeControl = await rangeControlFactory( - controlParams, - useTimeFilter, - getSearchSourceMock(esSearchResponse), - deps - ); - await rangeControl.fetch(); - - expect(rangeControl.isEnabled()).toBe(true); - expect(rangeControl.min).toBe(10); - expect(rangeControl.max).toBe(100); - }); - - test('should disable control when there are 0 hits', async () => { - // ES response when the query does not match any documents - const esSearchResponse = { - aggregations: { - maxAgg: { value: null }, - minAgg: { value: null }, - }, - }; - const rangeControl = await rangeControlFactory( - controlParams, - useTimeFilter, - getSearchSourceMock(esSearchResponse), - deps - ); - await rangeControl.fetch(); - - expect(rangeControl.isEnabled()).toBe(false); - }); - - test('should disable control when response is empty', async () => { - // ES response for dashboardonly user who does not have read permissions on index is 200 (which is weird) - // and there is not aggregations key - const esSearchResponse = {}; - const rangeControl = await rangeControlFactory( - controlParams, - useTimeFilter, - getSearchSourceMock(esSearchResponse), - deps - ); - await rangeControl.fetch(); - - expect(rangeControl.isEnabled()).toBe(false); - }); -}); diff --git a/src/legacy/core_plugins/input_control_vis/public/index.scss b/src/legacy/core_plugins/input_control_vis/public/index.scss deleted file mode 100644 index ac4692494b923..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/index.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "icv" to avoid conflicts. -// Examples -// icvChart -// icvChart__legend -// icvChart__legend--small -// icvChart__legend-isLoading - -@import './components/editor/index'; -@import './components/vis/index'; diff --git a/src/legacy/core_plugins/input_control_vis/public/index.ts b/src/legacy/core_plugins/input_control_vis/public/index.ts deleted file mode 100644 index e14c2cc4b69b6..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { PluginInitializerContext } from '../../../../core/public'; -import { InputControlVisPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy.ts b/src/legacy/core_plugins/input_control_vis/public/legacy.ts deleted file mode 100644 index 67299068819e8..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/legacy.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from '.'; - -import { - InputControlVisPluginSetupDependencies, - InputControlVisPluginStartDependencies, -} from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - data: npSetup.plugins.data, - visualizations: npSetup.plugins.visualizations, -}; - -const startPlugins: Readonly = { - expressions: npStart.plugins.expressions, - data: npStart.plugins.data, - visualizations: npStart.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts b/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts deleted file mode 100644 index 8c58ac2386da4..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/legacy_imports.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { Class } from '@kbn/utility-types'; -import { SearchSource as SearchSourceClass, ISearchSource } from '../../../../plugins/data/public'; - -export { SearchSourceFields } from '../../../../plugins/data/public'; - -export type SearchSource = Class; -export const SearchSource = SearchSourceClass; diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/legacy/core_plugins/input_control_vis/public/plugin.ts deleted file mode 100644 index b743468065430..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/plugin.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; - -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; -import { createInputControlVisFn } from './input_control_fn'; -import { createInputControlVisTypeDefinition } from './input_control_vis_type'; - -type InputControlVisCoreSetup = CoreSetup; - -export interface InputControlVisDependencies { - core: InputControlVisCoreSetup; - data: DataPublicPluginSetup; -} - -/** @internal */ -export interface InputControlVisPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - data: DataPublicPluginSetup; -} - -/** @internal */ -export interface InputControlVisPluginStartDependencies { - expressions: ReturnType; - visualizations: VisualizationsStart; - data: DataPublicPluginStart; -} - -/** @internal */ -export class InputControlVisPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - - public async setup( - core: InputControlVisCoreSetup, - { expressions, visualizations, data }: InputControlVisPluginSetupDependencies - ) { - const visualizationDependencies: Readonly = { - core, - data, - }; - - expressions.registerFunction(createInputControlVisFn); - visualizations.createBaseVisualization( - createInputControlVisTypeDefinition(visualizationDependencies) - ); - } - - public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) { - // nothing to do here - } -} diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx deleted file mode 100644 index c4a7d286850e3..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/* - * 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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { I18nStart } from 'kibana/public'; -import { SearchSource } from './legacy_imports'; - -import { InputControlVis } from './components/vis/input_control_vis'; -import { getControlFactory } from './control/control_factory'; -import { getLineageMap } from './lineage'; -import { ControlParams } from './editor_utils'; -import { RangeControl } from './control/range_control_factory'; -import { ListControl } from './control/list_control_factory'; -import { InputControlVisDependencies } from './plugin'; -import { FilterManager, Filter } from '../../../../plugins/data/public'; -import { VisParams, Vis } from '../../../../plugins/visualizations/public'; - -export const createInputControlVisController = (deps: InputControlVisDependencies) => { - return class InputControlVisController { - private I18nContext?: I18nStart['Context']; - - controls: Array; - queryBarUpdateHandler: () => void; - filterManager: FilterManager; - updateSubsciption: any; - visParams?: VisParams; - - constructor(public el: Element, public vis: Vis) { - this.controls = []; - - this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); - - this.filterManager = deps.data.query.filterManager; - this.updateSubsciption = this.filterManager - .getUpdates$() - .subscribe(this.queryBarUpdateHandler); - } - - async render(visData: any, visParams: VisParams) { - this.visParams = visParams; - this.controls = []; - this.controls = await this.initControls(); - const [{ i18n }] = await deps.core.getStartServices(); - this.I18nContext = i18n.Context; - this.drawVis(); - } - - destroy() { - this.updateSubsciption.unsubscribe(); - unmountComponentAtNode(this.el); - this.controls.forEach(control => control.destroy()); - } - - drawVis = () => { - if (!this.I18nContext) { - throw new Error('no i18n context found'); - } - - render( - - - , - this.el - ); - }; - - async initControls() { - const controlParamsList = (this.visParams?.controls as ControlParams[])?.filter( - controlParams => { - // ignore controls that do not have indexPattern or field - return controlParams.indexPattern && controlParams.fieldName; - } - ); - - const controlFactoryPromises = controlParamsList.map(controlParams => { - const factory = getControlFactory(controlParams); - return factory(controlParams, this.visParams?.useTimeFilter, SearchSource, deps); - }); - const controls = await Promise.all(controlFactoryPromises); - - const getControl = (controlId: string) => { - return controls.find(({ id }) => id === controlId); - }; - - const controlInitPromises: Array> = []; - getLineageMap(controlParamsList).forEach((lineage, controlId) => { - // first lineage item is the control. remove it - lineage.shift(); - const ancestors: Array = []; - lineage.forEach(ancestorId => { - const control = getControl(ancestorId); - - if (control) { - ancestors.push(control); - } - }); - const control = getControl(controlId); - - if (control) { - control.setAncestors(ancestors); - controlInitPromises.push(control.fetch()); - } - }); - - await Promise.all(controlInitPromises); - return controls; - } - - stageFilter = async (controlIndex: number, newValue: any) => { - this.controls[controlIndex].set(newValue); - if (this.visParams?.updateFiltersOnChange) { - // submit filters on each control change - this.submitFilters(); - } else { - // Do not submit filters, just update vis so controls are updated with latest value - await this.updateNestedControls(); - this.drawVis(); - } - }; - - submitFilters = () => { - const stagedControls = this.controls.filter(control => { - return control.hasChanged(); - }); - - const newFilters = stagedControls - .map(control => control.getKbnFilter()) - .filter((filter): filter is Filter => { - return filter !== null; - }); - - stagedControls.forEach(control => { - // to avoid duplicate filters, remove any old filters for control - control.filterManager.findFilters().forEach(existingFilter => { - this.filterManager.removeFilter(existingFilter); - }); - }); - - // Clean up filter pills for nested controls that are now disabled because ancestors are not set. - // This has to be done after looking up the staged controls because otherwise removing a filter - // will re-sync the controls of all other filters. - this.controls.map(control => { - if (control.hasAncestors() && control.hasUnsetAncestor()) { - control.filterManager.findFilters().forEach(existingFilter => { - this.filterManager.removeFilter(existingFilter); - }); - } - }); - - this.filterManager.addFilters(newFilters, this.visParams?.pinFilters); - }; - - clearControls = async () => { - this.controls.forEach(control => { - control.clear(); - }); - await this.updateNestedControls(); - this.drawVis(); - }; - - updateControlsFromKbn = async () => { - this.controls.forEach(control => { - control.reset(); - }); - await this.updateNestedControls(); - this.drawVis(); - }; - - async updateNestedControls() { - const fetchPromises = this.controls.map(async control => { - if (control.hasAncestors()) { - await control.fetch(); - } - }); - return await Promise.all(fetchPromises); - } - - hasChanges = () => { - return this.controls.map(control => control.hasChanged()).some(control => control); - }; - - hasValues = () => { - return this.controls - .map(control => { - return control.hasValue(); - }) - .reduce((a, b) => { - return a || b; - }); - }; - - refreshControl = async (controlIndex: number, query: any) => { - await this.controls[controlIndex].fetch(query); - this.drawVis(); - }; - }; -}; diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 989583742acd0..48d86e3628e49 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -21,7 +21,6 @@ import Fs from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; -import { migrations } from './migrations'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import mappings from './mappings.json'; @@ -119,42 +118,6 @@ export default function(kibana) { }, ], - savedObjectsManagement: { - dashboard: { - icon: 'dashboardApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'dashboard.show', - }; - }, - }, - url: { - defaultSearchField: 'url', - isImportableAndExportable: true, - getTitle(obj) { - return `/goto/${encodeURIComponent(obj.id)}`; - }, - }, - }, - - savedObjectSchemas: { - 'sample-data-telemetry': { - isNamespaceAgnostic: true, - }, - 'kql-telemetry': { - isNamespaceAgnostic: true, - }, - }, - injectDefaultVars(server, options) { const mapConfig = server.config().get('map'); const tilemap = mapConfig.tilemap; @@ -177,63 +140,6 @@ export default function(kibana) { mappings, uiSettingDefaults: getUiSettingDefaults(), - - migrations, - }, - - uiCapabilities: async function() { - return { - discover: { - show: true, - createShortUrl: true, - save: true, - saveQuery: true, - }, - visualize: { - show: true, - createShortUrl: true, - delete: true, - save: true, - saveQuery: true, - }, - dashboard: { - createNew: true, - show: true, - showWriteControls: true, - saveQuery: true, - }, - catalogue: { - discover: true, - dashboard: true, - visualize: true, - console: true, - advanced_settings: true, - index_patterns: true, - }, - advancedSettings: { - show: true, - save: true, - }, - indexPatterns: { - save: true, - }, - savedObjectsManagement: { - delete: true, - edit: true, - read: true, - }, - management: { - /* - * Management settings correspond to management section/link ids, and should not be changed - * without also updating those definitions. - */ - kibana: { - settings: true, - index_patterns: true, - objects: true, - }, - }, - }; }, preInit: async function(server) { diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json index af3f79588552b..e2cbd584dbe1f 100644 --- a/src/legacy/core_plugins/kibana/mappings.json +++ b/src/legacy/core_plugins/kibana/mappings.json @@ -1,105 +1,9 @@ { - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, "server": { "properties": { "uuid": { "type": "keyword" } } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } - }, - "sample-data-telemetry": { - "properties": { - "installCount": { - "type": "long" - }, - "unInstallCount": { - "type": "long" - } - } } } diff --git a/src/legacy/core_plugins/kibana/migrations/index.ts b/src/legacy/core_plugins/kibana/migrations/index.ts deleted file mode 100644 index 68c843d2343c8..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ - -// @ts-ignore -export { migrations } from './migrations'; diff --git a/src/legacy/core_plugins/kibana/migrations/is_doc.ts b/src/legacy/core_plugins/kibana/migrations/is_doc.ts deleted file mode 100644 index cc50dfa3b2d26..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/is_doc.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 { Doc } from './types'; - -export function isDoc(doc: { [key: string]: unknown } | Doc): doc is Doc { - return ( - typeof doc.id === 'string' && - typeof doc.type === 'string' && - doc.attributes !== null && - typeof doc.attributes === 'object' && - doc.references !== null && - typeof doc.references === 'object' - ); -} diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js deleted file mode 100644 index 029dbde555a4b..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/migrations.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { get } from 'lodash'; -import { - migrateMatchAllQuery, - migrations730 as dashboardMigrations730, -} from '../public/dashboard/migrations'; - -function migrateIndexPattern(doc) { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); - if (typeof searchSourceJSON !== 'string') { - return; - } - let searchSource; - try { - searchSource = JSON.parse(searchSourceJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return; - } - if (searchSource.index) { - searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - doc.references.push({ - name: searchSource.indexRefName, - type: 'index-pattern', - id: searchSource.index, - }); - delete searchSource.index; - } - if (searchSource.filter) { - searchSource.filter.forEach((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return; - } - filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - doc.references.push({ - name: filterRow.meta.indexRefName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - delete filterRow.meta.index; - }); - } - doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); -} - -export const migrations = { - dashboard: { - '6.7.2': migrateMatchAllQuery, - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - - // Migrate index pattern - migrateIndexPattern(doc); - // Migrate panels - const panelsJSON = get(doc, 'attributes.panelsJSON'); - if (typeof panelsJSON !== 'string') { - return doc; - } - let panels; - try { - panels = JSON.parse(panelsJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc; - } - if (!Array.isArray(panels)) { - return doc; - } - panels.forEach((panel, i) => { - if (!panel.type || !panel.id) { - return; - } - panel.panelRefName = `panel_${i}`; - doc.references.push({ - name: `panel_${i}`, - type: panel.type, - id: panel.id, - }); - delete panel.type; - delete panel.id; - }); - doc.attributes.panelsJSON = JSON.stringify(panels); - return doc; - }, - '7.3.0': dashboardMigrations730, - }, -}; diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.test.js b/src/legacy/core_plugins/kibana/migrations/migrations.test.js deleted file mode 100644 index b02081128c858..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/migrations.test.js +++ /dev/null @@ -1,447 +0,0 @@ -/* - * 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 { migrations } from './migrations'; - -describe('dashboard', () => { - describe('7.0.0', () => { - const migration = migrations.dashboard['7.0.0']; - - test('skips error on empty object', () => { - expect(migration({})).toMatchInlineSnapshot(` -Object { - "references": Array [], -} -`); - }); - - test('skips errors when searchSourceJSON is null', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: null, - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips errors when searchSourceJSON is undefined', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: undefined, - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when searchSourceJSON is not a string', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: 123, - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when searchSourceJSON is invalid json', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: '{abc123}', - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true }), - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('extracts "index" attribute from doc', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('extracts index patterns from filter', () => { - const doc = { - id: '1', - type: 'dashboard', - attributes: { - kibanaSavedObjectMeta: { - searchSourceJSON: JSON.stringify({ - bar: true, - filter: [ - { - meta: { - foo: true, - index: 'my-index', - }, - }, - ], - }), - }, - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], - "type": "dashboard", -} -`); - }); - - test('skips error when panelsJSON is not a string', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: 123, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": 123, - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips error when panelsJSON is not valid JSON', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '{123abc}', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "{123abc}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips panelsJSON when its not an array', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '{}', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "{}", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips error when a panel is missing "type" attribute', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '[{"id":"123"}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"id\\":\\"123\\"}]", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('skips error when a panel is missing "id" attribute', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: '[{"type":"visualization"}]', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('extract panel references from doc', () => { - const doc = { - id: '1', - attributes: { - panelsJSON: - '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", - }, - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "panel_0", - "type": "visualization", - }, - Object { - "id": "2", - "name": "panel_1", - "type": "visualization", - }, - ], -} -`); - }); - }); -}); diff --git a/src/legacy/core_plugins/kibana/migrations/types.ts b/src/legacy/core_plugins/kibana/migrations/types.ts deleted file mode 100644 index 839f753670b20..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectReference } from '../../../../core/server'; - -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} - -export interface Doc { - references: SavedObjectReference[]; - attributes: Attributes; - id: string; - type: string; -} - -export interface DocPre700 { - attributes: Attributes; - id: string; - type: string; -} diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js new file mode 100644 index 0000000000000..b212ecf578dd1 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table.js @@ -0,0 +1,490 @@ +/* + * 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 $ from 'jquery'; +import moment from 'moment'; +import ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; +import './legacy'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { npStart } from 'ui/new_platform'; +import { round } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInnerAngular } from '../../../../../../plugins/vis_type_table/public/get_inner_angular'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { initTableVisLegacyModule } from '../../../../../../plugins/vis_type_table/public/table_vis_legacy_module'; +import { tabifiedData } from './tabified_data'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { configureAppAngularModule } from '../../../../../../plugins/kibana_legacy/public/angular'; + +describe('Table Vis - AggTable Directive', function() { + let $rootScope; + let $compile; + let settings; + + const initLocalAngular = () => { + const tableVisModule = getInnerAngular('kibana/table_vis', npStart.core); + configureAppAngularModule(tableVisModule, npStart.core, true); + initTableVisLegacyModule(tableVisModule); + }; + + beforeEach(initLocalAngular); + + beforeEach(ngMock.module('kibana/table_vis')); + beforeEach( + ngMock.inject(function($injector, config) { + settings = config; + + $rootScope = $injector.get('$rootScope'); + $compile = $injector.get('$compile'); + }) + ); + + let $scope; + beforeEach(function() { + $scope = $rootScope.$new(); + }); + afterEach(function() { + $scope.$destroy(); + }); + + it('renders a simple response properly', function() { + $scope.dimensions = { + metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], + buckets: [], + }; + $scope.table = tabifiedData.metricOnly.tables[0]; + + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + expect($el.find('tbody').length).to.be(1); + expect($el.find('td').length).to.be(1); + expect($el.find('td').text()).to.eql('1,000'); + }); + + it('renders nothing if the table is empty', function() { + $scope.dimensions = {}; + $scope.table = null; + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + expect($el.find('tbody').length).to.be(0); + }); + + it('renders a complex response properly', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], + }; + $scope.table = tabifiedData.threeTermBuckets.tables[0]; + const $el = $(''); + $compile($el)($scope); + $scope.$digest(); + + expect($el.find('tbody').length).to.be(1); + + const $rows = $el.find('tbody tr'); + expect($rows.length).to.be.greaterThan(0); + + function validBytes(str) { + const num = str.replace(/,/g, ''); + if (num !== '-') { + expect(num).to.match(/^\d+$/); + } + } + + $rows.each(function() { + // 6 cells in every row + const $cells = $(this).find('td'); + expect($cells.length).to.be(6); + + const txts = $cells.map(function() { + return $(this) + .text() + .trim(); + }); + + // two character country code + expect(txts[0]).to.match(/^(png|jpg|gif|html|css)$/); + validBytes(txts[1]); + + // country + expect(txts[2]).to.match(/^\w\w$/); + validBytes(txts[3]); + + // os + expect(txts[4]).to.match(/^(win|mac|linux)$/); + validBytes(txts[5]); + }); + }); + + describe('renders totals row', function() { + async function totalsRowTest(totalFunc, expected) { + function setDefaultTimezone() { + moment.tz.setDefault(settings.get('dateFormat:tz')); + } + + const off = $scope.$on('change:config.dateFormat:tz', setDefaultTimezone); + const oldTimezoneSetting = settings.get('dateFormat:tz'); + settings.set('dateFormat:tz', 'UTC'); + + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, + ], + metrics: [ + { accessor: 2, format: { id: 'number' } }, + { accessor: 3, format: { id: 'date' } }, + { accessor: 4, format: { id: 'number' } }, + { accessor: 5, format: { id: 'number' } }, + ], + }; + $scope.table = + tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; + $scope.showTotal = true; + $scope.totalFunc = totalFunc; + const $el = $(``); + $compile($el)($scope); + $scope.$digest(); + + expect($el.find('tfoot').length).to.be(1); + + const $rows = $el.find('tfoot tr'); + expect($rows.length).to.be(1); + + const $cells = $($rows[0]).find('th'); + expect($cells.length).to.be(6); + + for (let i = 0; i < 6; i++) { + expect( + $($cells[i]) + .text() + .trim() + ).to.be(expected[i]); + } + settings.set('dateFormat:tz', oldTimezoneSetting); + off(); + } + it('as count', async function() { + await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); + }); + it('as min', async function() { + await totalsRowTest('min', [ + '', + '2014-09-28', + '9,283', + 'Sep 28, 2014 @ 00:00:00.000', + '1', + '11', + ]); + }); + it('as max', async function() { + await totalsRowTest('max', [ + '', + '2014-10-03', + '220,943', + 'Oct 3, 2014 @ 00:00:00.000', + '239', + '837', + ]); + }); + it('as avg', async function() { + await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); + }); + it('as sum', async function() { + await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); + }); + }); + + describe('aggTable.toCsv()', function() { + it('escapes rows and columns properly', function() { + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = { + columns: [ + { id: 'a', name: 'one' }, + { id: 'b', name: 'two' }, + { id: 'c', name: 'with double-quotes(")' }, + ], + rows: [{ a: 1, b: 2, c: '"foobar"' }], + }; + + expect(aggTable.toCsv()).to.be( + 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' + ); + }); + + it('exports rows and columns properly', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], + }; + $scope.table = tabifiedData.threeTermBuckets.tables[0]; + + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = $scope.table; + + const raw = aggTable.toCsv(false); + expect(raw).to.be( + '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + + '\r\n' + + 'png,IT,win,412032,9299,0' + + '\r\n' + + 'png,IT,mac,412032,9299,9299' + + '\r\n' + + 'png,US,linux,412032,8293,3992' + + '\r\n' + + 'png,US,mac,412032,8293,3029' + + '\r\n' + + 'css,MX,win,412032,9299,4992' + + '\r\n' + + 'css,MX,mac,412032,9299,5892' + + '\r\n' + + 'css,US,linux,412032,8293,3992' + + '\r\n' + + 'css,US,mac,412032,8293,3029' + + '\r\n' + + 'html,CN,win,412032,9299,4992' + + '\r\n' + + 'html,CN,mac,412032,9299,5892' + + '\r\n' + + 'html,FR,win,412032,8293,3992' + + '\r\n' + + 'html,FR,mac,412032,8293,3029' + + '\r\n' + ); + }); + + it('exports formatted rows and columns properly', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 2, params: {} }, + { accessor: 4, params: {} }, + ], + metrics: [ + { accessor: 1, params: {} }, + { accessor: 3, params: {} }, + { accessor: 5, params: {} }, + ], + }; + $scope.table = tabifiedData.threeTermBuckets.tables[0]; + + const $el = $compile('')( + $scope + ); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = $scope.table; + + // Create our own converter since the ones we use for tests don't actually transform the provided value + $tableScope.formattedColumns[0].formatter.convert = v => `${v}_formatted`; + + const formatted = aggTable.toCsv(true); + expect(formatted).to.be( + '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + + '\r\n' + + '"png_formatted",IT,win,412032,9299,0' + + '\r\n' + + '"png_formatted",IT,mac,412032,9299,9299' + + '\r\n' + + '"png_formatted",US,linux,412032,8293,3992' + + '\r\n' + + '"png_formatted",US,mac,412032,8293,3029' + + '\r\n' + + '"css_formatted",MX,win,412032,9299,4992' + + '\r\n' + + '"css_formatted",MX,mac,412032,9299,5892' + + '\r\n' + + '"css_formatted",US,linux,412032,8293,3992' + + '\r\n' + + '"css_formatted",US,mac,412032,8293,3029' + + '\r\n' + + '"html_formatted",CN,win,412032,9299,4992' + + '\r\n' + + '"html_formatted",CN,mac,412032,9299,5892' + + '\r\n' + + '"html_formatted",FR,win,412032,8293,3992' + + '\r\n' + + '"html_formatted",FR,mac,412032,8293,3029' + + '\r\n' + ); + }); + }); + + it('renders percentage columns', async function() { + $scope.dimensions = { + buckets: [ + { accessor: 0, params: {} }, + { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, + ], + metrics: [ + { accessor: 2, format: { id: 'number' } }, + { accessor: 3, format: { id: 'date' } }, + { accessor: 4, format: { id: 'number' } }, + { accessor: 5, format: { id: 'number' } }, + ], + }; + $scope.table = + tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; + $scope.percentageCol = 'Average bytes'; + + const $el = $(``); + + $compile($el)($scope); + $scope.$digest(); + + const $headings = $el.find('th'); + expect($headings.length).to.be(7); + expect( + $headings + .eq(3) + .text() + .trim() + ).to.be('Average bytes percentages'); + + const countColId = $scope.table.columns.find(col => col.name === $scope.percentageCol).id; + const counts = $scope.table.rows.map(row => row[countColId]); + const total = counts.reduce((sum, curr) => sum + curr, 0); + const $percentageColValues = $el.find('tbody tr').map((i, el) => + $(el) + .find('td') + .eq(3) + .text() + ); + + $percentageColValues.each((i, value) => { + const percentage = `${round((counts[i] / total) * 100, 1)}%`; + expect(value).to.be(percentage); + }); + }); + + describe('aggTable.exportAsCsv()', function() { + let origBlob; + function FakeBlob(slices, opts) { + this.slices = slices; + this.opts = opts; + } + + beforeEach(function() { + origBlob = window.Blob; + window.Blob = FakeBlob; + }); + + afterEach(function() { + window.Blob = origBlob; + }); + + it('calls _saveAs properly', function() { + const $el = $compile('')($scope); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + + const saveAs = sinon.stub(aggTable, '_saveAs'); + $tableScope.table = { + columns: [ + { id: 'a', name: 'one' }, + { id: 'b', name: 'two' }, + { id: 'c', name: 'with double-quotes(")' }, + ], + rows: [{ a: 1, b: 2, c: '"foobar"' }], + }; + + aggTable.csv.filename = 'somefilename.csv'; + aggTable.exportAsCsv(); + + expect(saveAs.callCount).to.be(1); + const call = saveAs.getCall(0); + expect(call.args[0]).to.be.a(FakeBlob); + expect(call.args[0].slices).to.eql([ + 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', + ]); + expect(call.args[0].opts).to.eql({ + type: 'text/plain;charset=utf-8', + }); + expect(call.args[1]).to.be('somefilename.csv'); + }); + + it('should use the export-title attribute', function() { + const expected = 'export file name'; + const $el = $compile( + `` + )($scope); + $scope.$digest(); + + const $tableScope = $el.isolateScope(); + const aggTable = $tableScope.aggTable; + $tableScope.table = { + columns: [], + rows: [], + }; + $tableScope.exportTitle = expected; + $scope.$digest(); + + expect(aggTable.csv.filename).to.equal(`${expected}.csv`); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js similarity index 81% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js index 40a0993ccb017..3cd7de393d66a 100644 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table_group.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/agg_table_group.js @@ -20,17 +20,24 @@ import $ from 'jquery'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { npStart } from '../../legacy_imports'; -import { getAngularModule } from '../../get_inner_angular'; -import { initTableVisLegacyModule } from '../../table_vis_legacy_module'; +import './legacy'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInnerAngular } from '../../../../../../plugins/vis_type_table/public/get_inner_angular'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { initTableVisLegacyModule } from '../../../../../../plugins/vis_type_table/public/table_vis_legacy_module'; import { tabifiedData } from './tabified_data'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { npStart } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { configureAppAngularModule } from '../../../../../../plugins/kibana_legacy/public/angular'; describe('Table Vis - AggTableGroup Directive', function() { let $rootScope; let $compile; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', npStart.core); + const tableVisModule = getInnerAngular('kibana/table_vis', npStart.core); + configureAppAngularModule(tableVisModule, npStart.core, true); initTableVisLegacyModule(tableVisModule); }; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts new file mode 100644 index 0000000000000..c6467a5beae68 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/legacy.ts @@ -0,0 +1,38 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { npStart, npSetup } from 'ui/new_platform'; +import { + TableVisPlugin, + TablePluginSetupDependencies, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/vis_type_table/public/plugin'; + +const plugins: Readonly = { + expressions: npSetup.plugins.expressions, + visualizations: npSetup.plugins.visualizations, +}; + +const pluginInstance = new TableVisPlugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, plugins); +export const start = pluginInstance.start(npStart.core, { + data: npStart.plugins.data, +}); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/tabified_data.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/tabified_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/tabified_data.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_table/tabified_data.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js similarity index 98% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js index 152efe5667f18..8f08f6a1f37e6 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import _ from 'lodash'; import d3 from 'd3'; -import { TagCloud } from '../tag_cloud'; import { fromNode, delay } from 'bluebird'; import { ImageComparator } from 'test_utils/image_comparator'; import simpleloadPng from './simpleload.png'; @@ -29,6 +28,9 @@ import simpleloadPng from './simpleload.png'; // Replace with mock when converting to jest tests // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TagCloud } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud'; describe('tag cloud tests', function() { const minValue = 1; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js similarity index 89% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js index 9e611861417cd..040ee18916fa2 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js @@ -20,7 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { ImageComparator } from 'test_utils/image_comparator'; -import { createTagCloudVisualization } from '../tag_cloud_visualization'; import basicdrawPng from './basicdraw.png'; import afterresizePng from './afterresize.png'; import afterparamChange from './afterparamchange.png'; @@ -32,7 +31,14 @@ import { ExprVis } from '../../../../../../plugins/visualizations/public/express import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; -import { createTagCloudVisTypeDefinition } from '../../tag_cloud_type'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createTagCloudVisTypeDefinition } from '../../../../../../plugins/vis_type_tagcloud/public/tag_cloud_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createTagCloudVisualization } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud_visualization'; +import { npStart } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setFormatService } from '../../../../../../plugins/vis_type_tagcloud/public/services'; const THRESHOLD = 0.65; const PIXEL_DIFF = 64; @@ -66,6 +72,8 @@ describe('TagCloudVisualizationTest', function() { }, }); + before(() => setFormatService(npStart.plugins.data.fieldFormats)); + beforeEach(ngMock.module('kibana')); describe('TagCloudVisualization - basics', function() { diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js new file mode 100644 index 0000000000000..9f5f4b764f9b0 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -0,0 +1,335 @@ +/* + * 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 Bluebird from 'bluebird'; +import expect from '@kbn/expect'; +import ngMock from 'ng_mock'; +import $ from 'jquery'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaVisualization } from '../../../../../../plugins/vis_type_vega/public/vega_visualization'; +import { ImageComparator } from 'test_utils/image_comparator'; + +import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson'; +import vegaliteImage256 from './vegalite_image_256.png'; +import vegaliteImage512 from './vegalite_image_512.png'; + +import vegaGraph from '!!raw-loader!./vega_graph.hjson'; +import vegaImage512 from './vega_image_512.png'; + +import vegaTooltipGraph from '!!raw-loader!./vega_tooltip_test.hjson'; + +import vegaMapGraph from '!!raw-loader!./vega_map_test.hjson'; +import vegaMapImage256 from './vega_map_image_256.png'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { VegaParser } from '../../../../../../plugins/vis_type_vega/public/data_model/vega_parser'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SearchCache } from '../../../../../../plugins/vis_type_vega/public/data_model/search_cache'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaTypeDefinition } from '../../../../../../plugins/vis_type_vega/public/vega_type'; +// TODO This is an integration test and thus requires a running platform. When moving to the new platform, +// this test has to be migrated to the newly created integration test environment. +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { npStart } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ExprVis } from '../../../../../../plugins/visualizations/public/expressions/vis'; + +import { + setInjectedVars, + setData, + setSavedObjects, + setNotifications, + setKibanaMapFactory, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/vis_type_vega/public/services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setInjectedVarFunc } from '../../../../../../plugins/maps_legacy/public/kibana_services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; +import { getKibanaMapFactoryProvider } from '../../../../../../plugins/maps_legacy/public'; + +const THRESHOLD = 0.1; +const PIXEL_DIFF = 30; + +describe('VegaVisualizations', () => { + let domNode; + let VegaVisualization; + let vis; + let imageComparator; + let vegaVisualizationDependencies; + let vegaVisType; + + const coreSetupMock = { + notifications: { + toasts: {}, + }, + uiSettings: { + get: () => {}, + }, + injectedMetadata: { + getInjectedVar: () => {}, + }, + }; + setKibanaMapFactory(getKibanaMapFactoryProvider(coreSetupMock)); + setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + esShardTimeout: 10000, + }); + setData(npStart.plugins.data); + setSavedObjects(npStart.core.savedObjects); + setNotifications(npStart.core.notifications); + + beforeEach(ngMock.module('kibana')); + beforeEach( + ngMock.inject(() => { + setInjectedVarFunc(injectedVar => { + switch (injectedVar) { + case 'mapConfig': + return { + emsFileApiUrl: '', + emsTileApiUrl: '', + emsLandingPageUrl: '', + }; + case 'tilemapsConfig': + return { + deprecated: { + config: { + options: { + attribution: '123', + }, + }, + }, + }; + case 'version': + return '123'; + default: + return 'not found'; + } + }); + const serviceSettings = new ServiceSettings(); + vegaVisualizationDependencies = { + serviceSettings, + core: { + uiSettings: npStart.core.uiSettings, + }, + plugins: { + data: { + query: { + timefilter: { + timefilter: {}, + }, + }, + }, + }, + }; + + vegaVisType = new BaseVisType(createVegaTypeDefinition(vegaVisualizationDependencies)); + VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); + }) + ); + + describe('VegaVisualization - basics', () => { + beforeEach(async function() { + setupDOM('512px', '512px'); + imageComparator = new ImageComparator(); + + vis = new ExprVis({ + type: vegaVisType, + }); + }); + + afterEach(function() { + teardownDOM(); + imageComparator.destroy(); + }); + + it('should show vegalite graph and update on resize (may fail in dev env)', async function() { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + + const vegaParser = new VegaParser(vegaliteGraph, new SearchCache()); + await vegaParser.parseAsync(); + + await vegaVis.render(vegaParser, vis.params, { data: true }); + const mismatchedPixels1 = await compareImage(vegaliteImage512); + expect(mismatchedPixels1).to.be.lessThan(PIXEL_DIFF); + + domNode.style.width = '256px'; + domNode.style.height = '256px'; + + await vegaVis.render(vegaParser, vis.params, { resize: true }); + const mismatchedPixels2 = await compareImage(vegaliteImage256); + expect(mismatchedPixels2).to.be.lessThan(PIXEL_DIFF); + } finally { + vegaVis.destroy(); + } + }); + + it('should show vega graph (may fail in dev env)', async function() { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + const vegaParser = new VegaParser(vegaGraph, new SearchCache()); + await vegaParser.parseAsync(); + + await vegaVis.render(vegaParser, vis.params, { data: true }); + const mismatchedPixels = await compareImage(vegaImage512); + + expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); + } finally { + vegaVis.destroy(); + } + }); + + it('should show vegatooltip on mouseover over a vega graph (may fail in dev env)', async () => { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + const vegaParser = new VegaParser(vegaTooltipGraph, new SearchCache()); + await vegaParser.parseAsync(); + await vegaVis.render(vegaParser, vis.params, { data: true }); + + const $el = $(domNode); + const offset = $el.offset(); + + const event = new MouseEvent('mousemove', { + view: window, + bubbles: true, + cancelable: true, + clientX: offset.left + 10, + clientY: offset.top + 10, + }); + + $el.find('canvas')[0].dispatchEvent(event); + + await Bluebird.delay(10); + + let tooltip = document.getElementById('vega-kibana-tooltip'); + expect(tooltip).to.be.ok(); + expect(tooltip.innerHTML).to.be( + '

This is a long title

' + + '' + + '' + + '' + + '
fieldA:value of fld1
fld2:42
' + ); + + vegaVis.destroy(); + + tooltip = document.getElementById('vega-kibana-tooltip'); + expect(tooltip).to.not.be.ok(); + } finally { + vegaVis.destroy(); + } + }); + + it('should show vega blank rectangle on top of a map (vegamap)', async () => { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + const vegaParser = new VegaParser(vegaMapGraph, new SearchCache()); + await vegaParser.parseAsync(); + + domNode.style.width = '256px'; + domNode.style.height = '256px'; + + await vegaVis.render(vegaParser, vis.params, { data: true }); + const mismatchedPixels = await compareImage(vegaMapImage256); + expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); + } finally { + vegaVis.destroy(); + } + }); + + it('should add a small subpixel value to the height of the canvas to avoid getting it set to 0', async () => { + let vegaVis; + try { + vegaVis = new VegaVisualization(domNode, vis); + const vegaParser = new VegaParser( + `{ + "$schema": "https://vega.github.io/schema/vega/v3.json", + "marks": [ + { + "type": "text", + "encode": { + "update": { + "text": { + "value": "Test" + }, + "align": {"value": "center"}, + "baseline": {"value": "middle"}, + "xc": {"signal": "width/2"}, + "yc": {"signal": "height/2"} + fontSize: {value: "14"} + } + } + } + ] + }`, + new SearchCache() + ); + await vegaParser.parseAsync(); + + domNode.style.width = '256px'; + domNode.style.height = '256px'; + + await vegaVis.render(vegaParser, vis.params, { data: true }); + const vegaView = vegaVis._vegaView._view; + expect(vegaView.height()).to.be(250.00000001); + + vegaView.height(250); + await vegaView.runAsync(); + // as soon as this test fails, the workaround with the subpixel value can be removed. + expect(vegaView.height()).to.be(0); + } finally { + vegaVis.destroy(); + } + }); + }); + + async function compareImage(expectedImageSource) { + const elementList = domNode.querySelectorAll('canvas'); + expect(elementList.length).to.equal(1); + const firstCanvasOnMap = elementList[0]; + return imageComparator.compareImage(firstCanvasOnMap, expectedImageSource, THRESHOLD); + } + + function setupDOM(width, height) { + domNode = document.createElement('div'); + domNode.style.top = '0'; + domNode.style.left = '0'; + domNode.style.width = width; + domNode.style.height = height; + domNode.style.position = 'fixed'; + domNode.style.border = '1px solid blue'; + domNode.style['pointer-events'] = 'none'; + document.body.appendChild(domNode); + } + + function teardownDOM() { + domNode.innerHTML = ''; + document.body.removeChild(domNode); + } +}); diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js similarity index 82% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js index 05cea7addf560..8a542fec0639c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/_vis_fixture.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js @@ -20,13 +20,10 @@ import _ from 'lodash'; import $ from 'jquery'; -import { Vis } from '../../../vis'; +import { Vis } from '../../../../../../plugins/vis_type_vislib/public/vislib/vis'; // TODO: Remove when converted to jest mocks -import { - ColorsService, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/charts/public/services'; +import { ColorsService } from '../../../../../../plugins/charts/public/services'; const $visCanvas = $('
') .attr('id', 'vislib-vis-fixtures') @@ -72,17 +69,6 @@ const getDeps = () => { }; }; -export const getMockUiState = () => { - const map = new Map(); - - return (() => ({ - get: (...args) => map.get(...args), - set: (...args) => map.set(...args), - setSilent: (...args) => map.set(...args), - on: () => undefined, - }))(); -}; - export function getVis(visLibParams, element) { return new Vis( element || $visCanvas.new(), diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js similarity index 91% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js index b65571becd83c..81fef155daf57 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/chart_title.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js @@ -21,9 +21,9 @@ import d3 from 'd3'; import _ from 'lodash'; import expect from '@kbn/expect'; -import { ChartTitle } from '../../lib/chart_title'; -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; +import { ChartTitle } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/chart_title'; +import { VisConfig } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; describe('Vislib ChartTitle Class Test Suite', function() { let mockUiState; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js index a5d8eb80419a1..eb4e109690c37 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js @@ -22,9 +22,10 @@ import d3 from 'd3'; import expect from '@kbn/expect'; // Data -import data from './fixtures/mock_data/date_histogram/_series'; +import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; -import { getVis, getMockUiState } from './fixtures/_vis_fixture'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; describe('Vislib Dispatch Class Test Suite', function() { function destroyVis(vis) { diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js new file mode 100644 index 0000000000000..27f7f4ed3e073 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js @@ -0,0 +1,151 @@ +/* + * 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 'jquery'; + +// Data +import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; +import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../../_vis_fixture'; + +const dateHistogramArray = [series, columns, rows, stackedSeries]; +const names = ['series', 'columns', 'rows', 'stackedSeries']; + +dateHistogramArray.forEach(function(data, i) { + describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function() { + const events = ['click', 'brush']; + let vis; + + beforeEach(() => { + vis = getVis(); + vis.render(data, getMockUiState()); + }); + + afterEach(function() { + vis.destroy(); + }); + + describe('render Method', function() { + it('should render charts', function() { + expect(vis.handler.charts.length).to.be.greaterThan(0); + vis.handler.charts.forEach(function(chart) { + expect($(chart.chartEl).find('svg').length).to.be(1); + }); + }); + }); + + describe('enable Method', function() { + let charts; + + beforeEach(function() { + charts = vis.handler.charts; + + charts.forEach(function(chart) { + events.forEach(function(event) { + vis.handler.enable(event, chart); + }); + }); + }); + + it('should add events to chart and emit to the Events class', function() { + charts.forEach(function(chart) { + events.forEach(function(event) { + expect(chart.events.listenerCount(event)).to.be.above(0); + }); + }); + }); + }); + + describe('disable Method', function() { + let charts; + + beforeEach(function() { + charts = vis.handler.charts; + + charts.forEach(function(chart) { + events.forEach(function(event) { + vis.handler.disable(event, chart); + }); + }); + }); + + it('should remove events from the chart', function() { + charts.forEach(function(chart) { + events.forEach(function(event) { + expect(chart.events.listenerCount(event)).to.be(0); + }); + }); + }); + }); + + describe('removeAll Method', function() { + beforeEach(function() { + vis.handler.removeAll(vis.element); + }); + + it('should remove all DOM elements from the el', function() { + expect($(vis.element).children().length).to.be(0); + }); + }); + + describe('error Method', function() { + beforeEach(function() { + vis.handler.error('This is an error!'); + }); + + it('should return an error classed DOM element with a text message', function() { + expect($(vis.element).find('.error').length).to.be(1); + expect($('.error h4').html()).to.be('This is an error!'); + }); + }); + + describe('destroy Method', function() { + beforeEach(function() { + vis.handler.destroy(); + }); + + it('should destroy all the charts in the visualization', function() { + expect(vis.handler.charts.length).to.be(0); + }); + }); + + describe('event proxying', function() { + it('should only pass the original event object to downstream handlers', function(done) { + const event = {}; + const chart = vis.handler.charts[0]; + + const mockEmitter = function() { + const args = Array.from(arguments); + expect(args.length).to.be(2); + expect(args[0]).to.be('click'); + expect(args[1]).to.be(event); + done(); + }; + + vis.emit = mockEmitter; + vis.handler.enable('click', chart); + chart.events.emit('click', event); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js similarity index 85% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js index f72794e27e834..505b0a04c6183 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js @@ -22,14 +22,14 @@ import expect from '@kbn/expect'; import $ from 'jquery'; // Data -import series from '../fixtures/mock_data/date_histogram/_series'; -import columns from '../fixtures/mock_data/date_histogram/_columns'; -import rows from '../fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series'; - -import { Layout } from '../../../lib/layout/layout'; -import { getVis, getMockUiState } from '../fixtures/_vis_fixture'; -import { VisConfig } from '../../../lib/vis_config'; +import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; +import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { Layout } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/layout/layout'; +import { VisConfig } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config'; +import { getVis } from '../../_vis_fixture'; const dateHistogramArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js similarity index 92% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js index 4852f71d8c45b..67f29ee96a336 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/vis.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js @@ -21,11 +21,12 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import series from './lib/fixtures/mock_data/date_histogram/_series'; -import columns from './lib/fixtures/mock_data/date_histogram/_columns'; -import rows from './lib/fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from './lib/fixtures/mock_data/date_histogram/_stacked_series'; -import { getVis, getMockUiState } from './lib/fixtures/_vis_fixture'; +import series from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import columns from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns'; +import rows from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import stackedSeries from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from './_vis_fixture'; const dataArray = [series, columns, rows, stackedSeries]; const names = ['series', 'columns', 'rows', 'stackedSeries']; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js similarity index 89% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js index c3f5859eb454c..eb529c380cdda 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/area_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js @@ -22,15 +22,16 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; const dataTypesArray = { - 'series pos': require('../lib/fixtures/mock_data/date_histogram/_series'), - 'series pos neg': require('../lib/fixtures/mock_data/date_histogram/_series_pos_neg'), - 'series neg': require('../lib/fixtures/mock_data/date_histogram/_series_neg'), - 'term columns': require('../lib/fixtures/mock_data/terms/_columns'), - 'range rows': require('../lib/fixtures/mock_data/range/_rows'), - stackedSeries: require('../lib/fixtures/mock_data/date_histogram/_stacked_series'), + 'series pos': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'), + 'series pos neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'), + 'series neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'), + 'term columns': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'), + 'range rows': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'), + stackedSeries: require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'), }; const visLibParams = { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js similarity index 92% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js index 9653f9abab6fb..4c5e3db316243 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js @@ -20,8 +20,9 @@ import d3 from 'd3'; import expect from '@kbn/expect'; -import { Chart } from '../../visualizations/_chart'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import { Chart } from '../../../../../../../plugins/vis_type_vislib/public/vislib/visualizations/_chart'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; describe('Vislib _chart Test Suite', function() { let vis; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js similarity index 89% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js index 2216294fcbac1..5cbd5948bc477 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/column_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js @@ -23,17 +23,18 @@ import $ from 'jquery'; import expect from '@kbn/expect'; // Data -import series from '../lib/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; -import termsColumns from '../lib/fixtures/mock_data/terms/_columns'; -import histogramRows from '../lib/fixtures/mock_data/histogram/_rows'; -import stackedSeries from '../lib/fixtures/mock_data/date_histogram/_stacked_series'; - -import { seriesMonthlyInterval } from '../lib/fixtures/mock_data/date_histogram/_series_monthly_interval'; -import { rowsSeriesWithHoles } from '../lib/fixtures/mock_data/date_histogram/_rows_series_with_holes'; -import rowsWithZeros from '../lib/fixtures/mock_data/date_histogram/_rows'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; +import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'; +import histogramRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows'; +import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; + +import { seriesMonthlyInterval } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval'; +import { rowsSeriesWithHoles } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes'; +import rowsWithZeros from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; // tuple, with the format [description, mode, data] const dataTypesArray = [ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js index fe25734fcbfde..d8ce8f1f5f44b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/gauge_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js @@ -21,8 +21,9 @@ import $ from 'jquery'; import _ from 'lodash'; import expect from '@kbn/expect'; -import data from '../lib/fixtures/mock_data/terms/_seriesMultiple'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; describe('Vislib Gauge Chart Test Suite', function() { let vis; diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js new file mode 100644 index 0000000000000..765b9118e6844 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js @@ -0,0 +1,194 @@ +/* + * 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 _ from 'lodash'; +import $ from 'jquery'; +import d3 from 'd3'; +import expect from '@kbn/expect'; + +// Data +import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; +import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'; +import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; + +// tuple, with the format [description, mode, data] +const dataTypesArray = [ + ['series', series], + ['series with positive and negative values', seriesPosNeg], + ['series with negative values', seriesNeg], + ['terms columns', termsColumns], + ['stackedSeries', stackedSeries], +]; + +describe('Vislib Heatmap Chart Test Suite', function() { + dataTypesArray.forEach(function(dataType) { + const name = dataType[0]; + const data = dataType[1]; + + describe('for ' + name + ' Data', function() { + let vis; + let mockUiState; + const visLibParams = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + invertColors: false, + colorsRange: [], + }; + + function generateVis(opts = {}) { + const config = _.defaultsDeep({}, opts, visLibParams); + vis = getVis(config); + mockUiState = getMockUiState(); + vis.on('brush', _.noop); + vis.render(data, mockUiState); + } + + beforeEach(() => { + generateVis(); + }); + + afterEach(function() { + vis.destroy(); + }); + + it('category axes should be rendered in reverse order', () => { + const renderedCategoryAxes = vis.handler.renderArray.filter(item => { + return ( + item.constructor && + item.constructor.name === 'Axis' && + item.axisConfig.get('type') === 'category' + ); + }); + expect(vis.handler.categoryAxes.length).to.equal(renderedCategoryAxes.length); + expect(vis.handler.categoryAxes[0].axisConfig.get('id')).to.equal( + renderedCategoryAxes[1].axisConfig.get('id') + ); + expect(vis.handler.categoryAxes[1].axisConfig.get('id')).to.equal( + renderedCategoryAxes[0].axisConfig.get('id') + ); + }); + + describe('addSquares method', function() { + it('should append rects', function() { + vis.handler.charts.forEach(function(chart) { + const numOfRects = chart.chartData.series.reduce((result, series) => { + return result + series.values.length; + }, 0); + expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects); + }); + }); + }); + + describe('addBarEvents method', function() { + function checkChart(chart) { + const rect = $(chart.chartEl) + .find('.series rect') + .get(0); + + return { + click: !!rect.__onclick, + mouseOver: !!rect.__onmouseover, + // D3 brushing requires that a g element is appended that + // listens for mousedown events. This g element includes + // listeners, however, I was not able to test for the listener + // function being present. I will need to update this test + // in the future. + brush: !!d3.select('.brush')[0][0], + }; + } + + it('should attach the brush if data is a set of ordered dates', function() { + vis.handler.charts.forEach(function(chart) { + const has = checkChart(chart); + const ordered = vis.handler.data.get('ordered'); + const date = Boolean(ordered && ordered.date); + expect(has.brush).to.be(date); + }); + }); + + it('should attach a click event', function() { + vis.handler.charts.forEach(function(chart) { + const has = checkChart(chart); + expect(has.click).to.be(true); + }); + }); + + it('should attach a hover event', function() { + vis.handler.charts.forEach(function(chart) { + const has = checkChart(chart); + expect(has.mouseOver).to.be(true); + }); + }); + }); + + describe('draw method', function() { + it('should return a function', function() { + vis.handler.charts.forEach(function(chart) { + expect(_.isFunction(chart.draw())).to.be(true); + }); + }); + + it('should return a yMin and yMax', function() { + vis.handler.charts.forEach(function(chart) { + const yAxis = chart.handler.valueAxes[0]; + const domain = yAxis.getScale().domain(); + + expect(domain[0]).to.not.be(undefined); + expect(domain[1]).to.not.be(undefined); + }); + }); + }); + + it('should define default colors', function() { + expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined); + }); + + it('should set custom range', function() { + vis.destroy(); + generateVis({ + setColorRange: true, + colorsRange: [ + { from: 0, to: 200 }, + { from: 200, to: 400 }, + { from: 400, to: 500 }, + { from: 500, to: Infinity }, + ], + }); + const labels = vis.getLegendLabels(); + expect(labels[0]).to.be('0 - 200'); + expect(labels[1]).to.be('200 - 400'); + expect(labels[2]).to.be('400 - 500'); + expect(labels[3]).to.be('500 - Infinity'); + }); + + it('should show correct Y axis title', function() { + expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).to.equal(''); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js similarity index 89% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js index 1269fe7bcf62e..691417e968eed 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/line_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js @@ -23,14 +23,14 @@ import $ from 'jquery'; import _ from 'lodash'; // Data -import seriesPos from '../lib/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; -import histogramColumns from '../lib/fixtures/mock_data/histogram/_columns'; -import rangeRows from '../lib/fixtures/mock_data/range/_rows'; -import termSeries from '../lib/fixtures/mock_data/terms/_series'; - -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import seriesPos from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'; +import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'; +import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'; +import histogramColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns'; +import rangeRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'; +import termSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; const dataTypes = [ ['series pos', seriesPos], diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js index caafb2c636271..506ad2af85c34 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js @@ -22,7 +22,8 @@ import _ from 'lodash'; import $ from 'jquery'; import expect from '@kbn/expect'; -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; +import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks'; +import { getVis } from '../_vis_fixture'; import { pieChartMockData } from './pie_chart_mock_data'; const names = ['rows', 'columns', 'slices']; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart_mock_data.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/pie_chart_mock_data.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts deleted file mode 100644 index f333ce97d120f..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { migrations730 } from './migrations_730'; -export { migrateMatchAllQuery } from './migrate_match_all_query'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts deleted file mode 100644 index d8f8882a218dd..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 { DashboardDoc730ToLatest } from '../../../../../../plugins/dashboard/public'; -import { isDoc } from '../../../migrations/is_doc'; - -export function isDashboardDoc( - doc: { [key: string]: unknown } | DashboardDoc730ToLatest -): doc is DashboardDoc730ToLatest { - if (!isDoc(doc)) { - return false; - } - - if (typeof (doc as DashboardDoc730ToLatest).attributes.panelsJSON !== 'string') { - return false; - } - - return true; -} diff --git a/src/legacy/core_plugins/kibana/public/discover/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/build_services.ts index a3a99a0ded523..b987129a9a7ed 100644 --- a/src/legacy/core_plugins/kibana/public/discover/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/build_services.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { createHashHistory, History } from 'history'; +import { History } from 'history'; import { Capabilities, @@ -42,6 +42,7 @@ import { DocViewerComponent, SavedSearch, } from '../../../../../plugins/discover/public'; +import { SavedObjectKibanaServices } from '../../../../../plugins/saved_objects/public'; export interface DiscoverServices { addBasePath: (path: string) => string; @@ -51,7 +52,7 @@ export interface DiscoverServices { data: DataPublicPluginStart; docLinks: DocLinksStart; DocViewer: DocViewerComponent; - history: History; + history: () => History; theme: ChartsPluginStart['theme']; filterManager: FilterManager; indexPatterns: IndexPatternsContract; @@ -65,11 +66,13 @@ export interface DiscoverServices { uiSettings: IUiSettingsClient; visualizations: VisualizationsStart; } + export async function buildServices( core: CoreStart, - plugins: DiscoverStartPlugins + plugins: DiscoverStartPlugins, + getHistory: () => History ): Promise { - const services = { + const services: SavedObjectKibanaServices = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, search: plugins.data.search, @@ -77,6 +80,7 @@ export async function buildServices( overlays: core.overlays, }; const savedObjectService = createSavedSearchesLoader(services); + return { addBasePath: core.http.basePath.prepend, capabilities: core.application.capabilities, @@ -85,11 +89,11 @@ export async function buildServices( data: plugins.data, docLinks: core.docLinks, DocViewer: plugins.discover.docViews.DocViewer, - history: createHashHistory(), theme: plugins.charts.theme, filterManager: plugins.data.query.filterManager, getSavedSearchById: async (id: string) => savedObjectService.get(id), getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), + history: getHistory, indexPatterns: plugins.data.indexPatterns, inspector: plugins.inspector, // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts index 6ccbc13aeeb57..6366466103652 100644 --- a/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts +++ b/src/legacy/core_plugins/kibana/public/discover/get_inner_angular.ts @@ -21,6 +21,8 @@ // these are necessary to bootstrap the local angular. // They can stay even after NP cutover import angular from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { EuiIcon } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, LegacyCoreStart } from 'kibana/public'; diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 0a81ca0222b0a..77664e87a3279 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { createHashHistory } from 'history'; import { DiscoverServices } from './build_services'; import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; import { search } from '../../../../../plugins/data/public'; @@ -52,12 +53,13 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ setTrackedUrl: (url: string) => void; }>('urlTracker'); +/** + * Makes sure discover and context are using one instance of history + */ +export const getHistory = _.once(() => createHashHistory()); + export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; -export { - unhashUrl, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../../../plugins/kibana_utils/public'; +export { unhashUrl, redirectWhenMissing } from '../../../../../plugins/kibana_utils/public'; export { formatMsg, formatStack, @@ -71,7 +73,6 @@ export { IndexPattern, indexPatterns, IFieldType, - SearchSource, ISearchSource, EsQuerySortValue, SortDirection, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js index 5b03b313e4e3e..032ec7af09a30 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context.js @@ -81,6 +81,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { defaultStepSize: getServices().uiSettings.get('context:defaultSize'), timeFieldName: indexPattern.timeFieldName, storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), + history: getServices().history(), }); this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js index f6ed570be2c37..acd609df203e3 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/_stubs.js @@ -19,7 +19,6 @@ import sinon from 'sinon'; import moment from 'moment'; -import { SearchSource } from '../../../../../../../../../plugins/data/public'; export function createIndexPatternsStub() { return { @@ -46,17 +45,15 @@ export function createSearchSourceStub(hits, timeField) { }), }; - searchSourceStub.setParent = sinon - .stub(SearchSource.prototype, 'setParent') - .returns(searchSourceStub); - searchSourceStub.setField = sinon - .stub(SearchSource.prototype, 'setField') - .returns(searchSourceStub); - searchSourceStub.getField = sinon.stub(SearchSource.prototype, 'getField').callsFake(key => { + searchSourceStub.setParent = sinon.spy(() => searchSourceStub); + searchSourceStub.setField = sinon.spy(() => searchSourceStub); + + searchSourceStub.getField = sinon.spy(key => { const previousSetCall = searchSourceStub.setField.withArgs(key).lastCall; return previousSetCall ? previousSetCall.args[1] : null; }); - searchSourceStub.fetch = sinon.stub(SearchSource.prototype, 'fetch').callsFake(() => + + searchSourceStub.fetch = sinon.spy(() => Promise.resolve({ hits: { hits: searchSourceStub._stubHits, @@ -65,13 +62,6 @@ export function createSearchSourceStub(hits, timeField) { }) ); - searchSourceStub._restore = () => { - searchSourceStub.setParent.restore(); - searchSourceStub.setField.restore(); - searchSourceStub.getField.restore(); - searchSourceStub.fetch.restore(); - }; - return searchSourceStub; } @@ -81,8 +71,7 @@ export function createSearchSourceStub(hits, timeField) { export function createContextSearchSourceStub(hits, timeField = '@timestamp') { const searchSourceStub = createSearchSourceStub(hits, timeField); - searchSourceStub.fetch.restore(); - searchSourceStub.fetch = sinon.stub(SearchSource.prototype, 'fetch').callsFake(() => { + searchSourceStub.fetch = sinon.spy(() => { const timeField = searchSourceStub._stubTimeField; const lastQuery = searchSourceStub.setField.withArgs('query').lastCall.args[1]; const timeRange = lastQuery.query.constant_score.filter.range[timeField]; @@ -99,6 +88,7 @@ export function createContextSearchSourceStub(hits, timeField = '@timestamp') { moment(hit[timeField]).isSameOrBefore(timeRange.lte) ) .sort(sortFunction); + return Promise.resolve({ hits: { hits: filteredHits, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js index 0bc2cbacc1eee..757e74589555a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/anchor.test.js @@ -31,10 +31,6 @@ describe('context app', function() { fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); }); - afterEach(() => { - searchSourceStub._restore(); - }); - it('should use the `fetch` method of the SearchSource', function() { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ { '@timestamp': 'desc' }, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js index d6e91e57b22a8..ebd4af536aabd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.predecessors.test.js @@ -21,6 +21,7 @@ import moment from 'moment'; import * as _ from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { fetchContextProvider } from './context'; +import { setServices } from '../../../../kibana_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -31,10 +32,21 @@ const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); describe('context app', function() { describe('function fetchPredecessors', function() { let fetchPredecessors; - let searchSourceStub; + let mockSearchSource; beforeEach(() => { - searchSourceStub = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + mockSearchSource = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + + setServices({ + data: { + search: { + searchSource: { + create: jest.fn().mockImplementation(() => mockSearchSource), + }, + }, + }, + }); + fetchPredecessors = ( indexPatternId, timeField, @@ -65,17 +77,13 @@ describe('context app', function() { }; }); - afterEach(() => { - searchSourceStub._restore(); - }); - it('should perform exactly one query when enough hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3000 + 2), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 + 1), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 2000), - searchSourceStub._createStubHit(MS_PER_DAY * 1000), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 2), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 2000), + mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; return fetchPredecessors( @@ -89,18 +97,18 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(searchSourceStub.fetch.calledOnce).toBe(true); - expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 3)); + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); }); }); it('should perform multiple queries with the last being unrestricted when too few hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3010), - searchSourceStub._createStubHit(MS_PER_DAY * 3002), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 2998), - searchSourceStub._createStubHit(MS_PER_DAY * 2990), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3010), + mockSearchSource._createStubHit(MS_PER_DAY * 3002), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 2998), + mockSearchSource._createStubHit(MS_PER_DAY * 2990), ]; return fetchPredecessors( @@ -114,7 +122,7 @@ describe('context app', function() { 6, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -129,16 +137,16 @@ describe('context app', function() { expect(Object.keys(_.last(intervals))).toEqual(['format', 'gte']); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 3)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); }); }); it('should perform multiple queries until the expected hit count is returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 1700), - searchSourceStub._createStubHit(MS_PER_DAY * 1200), - searchSourceStub._createStubHit(MS_PER_DAY * 1100), - searchSourceStub._createStubHit(MS_PER_DAY * 1000), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 1700), + mockSearchSource._createStubHit(MS_PER_DAY * 1200), + mockSearchSource._createStubHit(MS_PER_DAY * 1100), + mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; return fetchPredecessors( @@ -152,7 +160,7 @@ describe('context app', function() { 3, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -163,7 +171,7 @@ describe('context app', function() { // should have stopped before reaching MS_PER_DAY * 1700 expect(moment(_.last(intervals).lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); }); @@ -195,7 +203,7 @@ describe('context app', function() { 3, [] ).then(() => { - const setParentSpy = searchSourceStub.setParent; + const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); expect(setParentSpy.called).toBe(true); }); @@ -214,7 +222,7 @@ describe('context app', function() { [] ).then(() => { expect( - searchSourceStub.setField.calledWith('sort', [{ '@timestamp': 'asc' }, { _doc: 'asc' }]) + mockSearchSource.setField.calledWith('sort', [{ '@timestamp': 'asc' }, { _doc: 'asc' }]) ).toBe(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js index cc2b6d31cb43b..452d0cc9fd1a0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.successors.test.js @@ -21,6 +21,7 @@ import moment from 'moment'; import * as _ from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; +import { setServices } from '../../../../kibana_services'; import { fetchContextProvider } from './context'; @@ -32,10 +33,20 @@ const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); describe('context app', function() { describe('function fetchSuccessors', function() { let fetchSuccessors; - let searchSourceStub; + let mockSearchSource; beforeEach(() => { - searchSourceStub = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + + setServices({ + data: { + search: { + searchSource: { + create: jest.fn().mockImplementation(() => mockSearchSource), + }, + }, + }, + }); fetchSuccessors = ( indexPatternId, @@ -67,17 +78,13 @@ describe('context app', function() { }; }); - afterEach(() => { - searchSourceStub._restore(); - }); - it('should perform exactly one query when enough hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 5000), - searchSourceStub._createStubHit(MS_PER_DAY * 4000), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 1), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 2), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 5000), + mockSearchSource._createStubHit(MS_PER_DAY * 4000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), ]; return fetchSuccessors( @@ -91,18 +98,18 @@ describe('context app', function() { 3, [] ).then(hits => { - expect(searchSourceStub.fetch.calledOnce).toBe(true); - expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); }); it('should perform multiple queries with the last being unrestricted when too few hits are returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3010), - searchSourceStub._createStubHit(MS_PER_DAY * 3002), - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 2998), - searchSourceStub._createStubHit(MS_PER_DAY * 2990), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3010), + mockSearchSource._createStubHit(MS_PER_DAY * 3002), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 2998), + mockSearchSource._createStubHit(MS_PER_DAY * 2990), ]; return fetchSuccessors( @@ -116,7 +123,7 @@ describe('context app', function() { 6, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -131,18 +138,18 @@ describe('context app', function() { expect(Object.keys(_.last(intervals))).toEqual(['format', 'lte']); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(-3)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); }); it('should perform multiple queries until the expected hit count is returned', function() { - searchSourceStub._stubHits = [ - searchSourceStub._createStubHit(MS_PER_DAY * 3000), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 1), - searchSourceStub._createStubHit(MS_PER_DAY * 3000 - 2), - searchSourceStub._createStubHit(MS_PER_DAY * 2800), - searchSourceStub._createStubHit(MS_PER_DAY * 2200), - searchSourceStub._createStubHit(MS_PER_DAY * 1000), + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), + mockSearchSource._createStubHit(MS_PER_DAY * 2800), + mockSearchSource._createStubHit(MS_PER_DAY * 2200), + mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; return fetchSuccessors( @@ -156,7 +163,7 @@ describe('context app', function() { 4, [] ).then(hits => { - const intervals = searchSourceStub.setField.args + const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') .map(([, { query }]) => _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) @@ -168,7 +175,7 @@ describe('context app', function() { expect(moment(_.last(intervals).gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(searchSourceStub._stubHits.slice(0, 4)); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 4)); }); }); @@ -200,7 +207,7 @@ describe('context app', function() { 3, [] ).then(() => { - const setParentSpy = searchSourceStub.setParent; + const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); expect(setParentSpy.called).toBe(true); }); @@ -219,7 +226,7 @@ describe('context app', function() { [] ).then(() => { expect( - searchSourceStub.setField.calledWith('sort', [{ '@timestamp': 'desc' }, { _doc: 'desc' }]) + mockSearchSource.setField.calledWith('sort', [{ '@timestamp': 'desc' }, { _doc: 'desc' }]) ).toBe(true); }); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts index 507f927c608e1..2760eec38755e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/context.ts @@ -27,8 +27,8 @@ import { Filter, IndexPatternsContract, IndexPattern, - SearchSource, } from '../../../../../../../../../plugins/data/public'; +import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { @@ -115,7 +115,10 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { } async function createSearchSource(indexPattern: IndexPattern, filters: Filter[]) { - return new SearchSource() + const { data } = getServices(); + + return data.search.searchSource + .create() .setParent(undefined) .setField('index', indexPattern) .setField('filter', filters); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js index 9efddc5275069..efc230d2cd4ae 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/query/actions.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { getServices, SearchSource } from '../../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; import { fetchAnchorProvider } from '../api/anchor'; import { fetchContextProvider } from '../api/context'; @@ -29,8 +29,8 @@ import { FAILURE_REASONS, LOADING_STATUS } from './constants'; import { MarkdownSimple } from '../../../../../../../../../plugins/kibana_react/public'; export function QueryActionsProvider(Promise) { - const { filterManager, indexPatterns } = getServices(); - const fetchAnchor = fetchAnchorProvider(indexPatterns, new SearchSource()); + const { filterManager, indexPatterns, data } = getServices(); + const fetchAnchor = fetchAnchorProvider(indexPatterns, data.search.searchSource.create()); const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns); const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions( filterManager, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts index ed59143b163f6..b46995d44d826 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context_state.ts @@ -17,7 +17,7 @@ * under the License. */ import _ from 'lodash'; -import { createBrowserHistory, History } from 'history'; +import { History } from 'history'; import { createStateContainer, createKbnUrlStateStorage, @@ -71,9 +71,9 @@ interface GetStateParams { */ storeInSessionStorage?: boolean; /** - * Browser history used for testing + * History instance to use */ - history?: History; + history: History; } interface GetStateReturn { @@ -126,7 +126,7 @@ export function getState({ }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, - history: history ? history : createBrowserHistory(), + history, }); const globalStateInitial = stateStorage.get(GLOBAL_STATE_URL_KEY) as GlobalState; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 72276a38f6ac2..c1de704d1c00a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -49,7 +49,6 @@ import { subscribeWithScope, tabifyAggResponse, getAngularModule, - ensureDefaultIndexPattern, redirectWhenMissing, } from '../../kibana_services'; @@ -57,8 +56,7 @@ const { core, chrome, data, - docTitle, - history, + history: getHistory, indexPatterns, filterManager, share, @@ -117,8 +115,9 @@ app.config($routeProvider => { reloadOnSearch: false, resolve: { savedObjects: function($route, Promise) { + const history = getHistory(); const savedSearchId = $route.current.params.id; - return ensureDefaultIndexPattern(core, data, history).then(() => { + return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ @@ -205,6 +204,8 @@ function discoverController( return isDefaultType($scope.indexPattern) ? $scope.indexPattern.timeFieldName : undefined; }; + const history = getHistory(); + const { appStateContainer, startSync: startStateSync, @@ -214,6 +215,7 @@ function discoverController( isAppStateDirty, kbnUrlStateStorage, getPreviousAppState, + resetInitialAppState, } = getState({ defaultAppState: getStateDefaults(), storeInSessionStorage: config.get('state:storeInSessionStorage'), @@ -373,6 +375,8 @@ function discoverController( // If the save wasn't successful, put the original values back. if (!response.id || response.error) { savedSearch.title = currentTitle; + } else { + resetInitialAppState(); } return response; }); @@ -758,7 +762,7 @@ function discoverController( } else { // Update defaults so that "reload saved query" functions correctly setAppState(getStateDefaults()); - docTitle.change(savedSearch.lastSavedTitle); + chrome.docTitle.change(savedSearch.lastSavedTitle); } } }); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx index 9a6bd65813d18..fdae2c0c16c9f 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_field.test.tsx @@ -31,11 +31,11 @@ import { IndexPatternField } from '../../../../../../../../plugins/data/public'; jest.mock('../../../kibana_services', () => ({ getServices: () => ({ - history: { + history: () => ({ location: { search: '', }, - }, + }), capabilities: { visualize: { show: true, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx index 0df14515adc6d..29451c075bcad 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/discover_sidebar.test.tsx @@ -36,11 +36,11 @@ import { SavedObject } from '../../../../../../../../core/types'; jest.mock('../../../kibana_services', () => ({ getServices: () => ({ - history: { + history: () => ({ location: { search: '', }, - }, + }), capabilities: { visualize: { show: true, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts index d146d212055b7..968ceeeab73a5 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/sidebar/lib/visualize_url_utils.ts @@ -125,7 +125,7 @@ export function getVisualizeUrl( services: DiscoverServices ) { const aggsTermSize = services.uiSettings.get('discover:aggs:terms:size'); - const urlParams = parse(services.history.location.search) as Record; + const urlParams = parse(services.history().location.search) as Record; if ( (field.type === KBN_FIELD_TYPES.GEO_POINT || field.type === KBN_FIELD_TYPES.GEO_SHAPE) && diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index d05e96ccaaf0b..702331529b879 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -31,7 +31,7 @@ import { registerFeature } from './np_ready/register_feature'; import './kibana_services'; import { EmbeddableStart, EmbeddableSetup } from '../../../../../plugins/embeddable/public'; import { getInnerAngularModule, getInnerAngularModuleEmbeddable } from './get_inner_angular'; -import { setAngularModule, setServices, setUrlTracker } from './kibana_services'; +import { getHistory, setAngularModule, setServices, setUrlTracker } from './kibana_services'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../../plugins/navigation/public'; import { ChartsPluginStart } from '../../../../../plugins/charts/public'; import { buildServices } from './build_services'; @@ -98,6 +98,10 @@ export class DiscoverPlugin implements Plugin { stop: stopUrlTracker, setActiveUrl: setTrackedUrl, } = createKbnUrlTracker({ + // we pass getter here instead of plain `history`, + // so history is lazily created (when app is mounted) + // this prevents redundant `#` when not in discover app + getHistory, baseUrl: core.http.basePath.prepend('/app/kibana'), defaultSubUrl: '#/discover', storageKey: `lastUrl:${core.http.basePath.get()}:discover`, @@ -143,6 +147,9 @@ export class DiscoverPlugin implements Plugin { await this.initializeServices(); await this.initializeInnerAngular(); + // make sure the index pattern list is up to date + const [, { data: dataStart }] = await core.getStartServices(); + await dataStart.indexPatterns.clearCache(); const { renderApp } = await import('./np_ready/application'); const unmount = await renderApp(innerAngularName, params.element); return () => { @@ -174,7 +181,7 @@ export class DiscoverPlugin implements Plugin { if (this.servicesInitialized) { return { core, plugins }; } - const services = await buildServices(core, plugins); + const services = await buildServices(core, plugins, getHistory); setServices(services); this.servicesInitialized = true; diff --git a/src/legacy/core_plugins/kibana/public/index.scss b/src/legacy/core_plugins/kibana/public/index.scss index d49c59970f521..26805554370b9 100644 --- a/src/legacy/core_plugins/kibana/public/index.scss +++ b/src/legacy/core_plugins/kibana/public/index.scss @@ -7,16 +7,9 @@ // Public UI styles @import 'src/legacy/ui/public/index'; -// vis_type_vislib UI styles -@import 'src/legacy/core_plugins/vis_type_vislib/public/index'; - // Discover styles @import 'discover/index'; -// Visualization styles are imported here for running karma Browser tests -// should be somehow included through the "visualizations" plugin initialization -@import '../../../../plugins/visualizations/public/index'; - // Has to come after visualize because of some // bad cascading in the Editor layout @import '../../../../plugins/maps_legacy/public/index'; diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 20c46765dcb30..ea0d5ad3790b1 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -49,8 +49,9 @@ import { showAppRedirectNotification } from '../../../../plugins/kibana_legacy/p import 'leaflet'; import { localApplicationService } from './local_application_service'; -npSetup.plugins.kibanaLegacy.forwardApp('doc', 'discover', { keepPrefix: true }); -npSetup.plugins.kibanaLegacy.forwardApp('context', 'discover', { keepPrefix: true }); +npSetup.plugins.kibanaLegacy.registerLegacyAppAlias('doc', 'discover', { keepPrefix: true }); +npSetup.plugins.kibanaLegacy.registerLegacyAppAlias('context', 'discover', { keepPrefix: true }); + localApplicationService.attachToAngular(routes); routes.enable(); diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index 14564cfd9ee78..f38c410e6832f 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -98,14 +98,29 @@ export class LocalApplicationService { } }); - npStart.plugins.kibanaLegacy.getForwards().forEach(({ legacyAppId, newAppId, keepPrefix }) => { - angularRouteManager.when(matchAllWithPrefix(legacyAppId), { - resolveRedirectTo: ($location: ILocationService) => { - const url = $location.url(); - return `/${newAppId}${keepPrefix ? url : url.replace(legacyAppId, '')}`; + npStart.plugins.kibanaLegacy.getForwards().forEach(forwardDefinition => { + angularRouteManager.when(matchAllWithPrefix(forwardDefinition.legacyAppId), { + outerAngularWrapperRoute: true, + reloadOnSearch: false, + reloadOnUrl: false, + template: '', + controller($location: ILocationService) { + const newPath = forwardDefinition.rewritePath($location.url()); + npStart.core.application.navigateToApp(forwardDefinition.newAppId, { path: newPath }); }, }); }); + + npStart.plugins.kibanaLegacy + .getLegacyAppAliases() + .forEach(({ legacyAppId, newAppId, keepPrefix }) => { + angularRouteManager.when(matchAllWithPrefix(legacyAppId), { + resolveRedirectTo: ($location: ILocationService) => { + const url = $location.url(); + return `/${newAppId}${keepPrefix ? url : url.replace(legacyAppId, '')}`; + }, + }); + }); } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 09a06bd8827ce..ed65db10e0acb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -149,6 +149,7 @@ exports[`CreateIndexPatternWizard renders time field step when step is set to 2 indexPatternsService={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/constants.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/constants.ts new file mode 100644 index 0000000000000..56da031eb4ee8 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/constants.ts @@ -0,0 +1,22 @@ +/* + * 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 const TAB_INDEXED_FIELDS = 'indexedFields'; +export const TAB_SCRIPTED_FIELDS = 'scriptedFields'; +export const TAB_SOURCE_FILTERS = 'sourceFilters'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field.html similarity index 100% rename from src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.html rename to src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field.html diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js deleted file mode 100644 index 95d6cb6878e53..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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 { IndexPatternField } from '../../../../../../../../../plugins/data/public'; -import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; -import { docTitle } from 'ui/doc_title'; -import { KbnUrlProvider } from 'ui/url'; -import uiRoutes from 'ui/routes'; -import { toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; - -import template from './create_edit_field.html'; -import { getEditFieldBreadcrumbs, getCreateFieldBreadcrumbs } from '../../breadcrumbs'; - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { FieldEditor } from 'ui/field_editor'; -import { I18nContext } from 'ui/i18n'; -import { i18n } from '@kbn/i18n'; - -import { IndexHeader } from '../index_header'; - -const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; -const renderFieldEditor = ( - $scope, - indexPattern, - field, - { getConfig, $http, fieldFormatEditors, redirectAway } -) => { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - if (!node) { - return; - } - - render( - - - - , - node - ); - }); -}; - -const destroyFieldEditor = () => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - node && unmountComponentAtNode(node); -}; - -uiRoutes - .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { - mode: 'edit', - k7Breadcrumbs: getEditFieldBreadcrumbs, - }) - .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { - mode: 'create', - k7Breadcrumbs: getCreateFieldBreadcrumbs, - }) - .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { - template, - mapBreadcrumbs($route, breadcrumbs) { - const { indexPattern } = $route.current.locals; - return breadcrumbs.map(crumb => { - if (crumb.id !== indexPattern.id) { - return crumb; - } - - return { - ...crumb, - display: indexPattern.title, - }; - }); - }, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, - controllerAs: 'fieldSettings', - controller: function FieldEditorPageController( - $scope, - $route, - $timeout, - $http, - Private, - config - ) { - const getConfig = (...args) => config.get(...args); - const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); - const kbnUrl = Private(KbnUrlProvider); - - this.mode = $route.current.mode; - this.indexPattern = $route.current.locals.indexPattern; - - if (this.mode === 'edit') { - const fieldName = $route.current.params.fieldName; - this.field = this.indexPattern.fields.getByName(fieldName); - - if (!this.field) { - const message = i18n.translate('kbn.management.editIndexPattern.scripted.noFieldLabel', { - defaultMessage: - "'{indexPatternTitle}' index pattern doesn't have a scripted field called '{fieldName}'", - values: { indexPatternTitle: this.indexPattern.title, fieldName }, - }); - toastNotifications.add(message); - - kbnUrl.redirectToRoute(this.indexPattern, 'edit'); - return; - } - } else if (this.mode === 'create') { - this.field = new IndexPatternField(this.indexPattern, { - scripted: true, - type: 'number', - }); - } else { - const errorMessage = i18n.translate( - 'kbn.management.editIndexPattern.scripted.unknownModeErrorMessage', - { - defaultMessage: 'unknown fieldSettings mode {mode}', - values: { mode: this.mode }, - } - ); - throw new Error(errorMessage); - } - - const fieldName = - this.field.name || - i18n.translate('kbn.management.editIndexPattern.scripted.newFieldPlaceholder', { - defaultMessage: 'New Scripted Field', - }); - docTitle.change([fieldName, this.indexPattern.title]); - - renderFieldEditor($scope, this.indexPattern, this.field, { - getConfig, - $http, - fieldFormatEditors, - redirectAway: () => { - $timeout(() => { - kbnUrl.changeToRoute( - this.indexPattern, - this.field.scripted ? 'scriptedFields' : 'indexedFields' - ); - }); - }, - }); - - $scope.$on('$destroy', () => { - destroyFieldEditor(); - }); - }, - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.tsx new file mode 100644 index 0000000000000..564f115cf2c48 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -0,0 +1,116 @@ +/* + * 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 React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +// @ts-ignore +import { FieldEditor } from 'ui/field_editor'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { HttpStart, DocLinksStart } from 'src/core/public'; +import { IndexHeader } from '../index_header'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public'; +import { ChromeDocTitle, NotificationsStart } from '../../../../../../../../../core/public'; +import { TAB_SCRIPTED_FIELDS, TAB_INDEXED_FIELDS } from '../constants'; + +interface CreateEditFieldProps extends RouteComponentProps { + indexPattern: IndexPattern; + mode?: string; + fieldName?: string; + fieldFormatEditors: any; + getConfig: (name: string) => any; + services: { + notifications: NotificationsStart; + docTitle: ChromeDocTitle; + getHttpStart: () => HttpStart; + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; + }; +} + +const newFieldPlaceholder = i18n.translate( + 'kbn.management.editIndexPattern.scripted.newFieldPlaceholder', + { + defaultMessage: 'New Scripted Field', + } +); + +export const CreateEditField = withRouter( + ({ + indexPattern, + mode, + fieldName, + fieldFormatEditors, + getConfig, + services, + history, + }: CreateEditFieldProps) => { + const field = + mode === 'edit' && fieldName + ? indexPattern.fields.getByName(fieldName) + : new IndexPatternField(indexPattern, { + scripted: true, + type: 'number', + }); + + const url = `/management/kibana/index_patterns/${indexPattern.id}`; + + if (mode === 'edit' && !field) { + const message = i18n.translate('kbn.management.editIndexPattern.scripted.noFieldLabel', { + defaultMessage: + "'{indexPatternTitle}' index pattern doesn't have a scripted field called '{fieldName}'", + values: { indexPatternTitle: indexPattern.title, fieldName }, + }); + services.notifications.toasts.addWarning(message); + history.push(url); + } + + const docFieldName = field?.name || newFieldPlaceholder; + + services.docTitle.change([docFieldName, indexPattern.title]); + + const redirectAway = () => { + history.push(`${url}?_a=(tab:${field?.scripted ? TAB_SCRIPTED_FIELDS : TAB_INDEXED_FIELDS})`); + }; + + if (field) { + return ( + + + + + + + + + ); + } else { + return <>; + } + } +); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.js deleted file mode 100644 index 890a3b2622577..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 './create_edit_field'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.ts new file mode 100644 index 0000000000000..473a8f5b57c82 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { CreateEditField } from './create_edit_field'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 0376df6bbdc58..0bf7c7f0bdfbe 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -6,156 +6,7 @@ role="region" aria-label="{{::'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}" > - -
- -
-

- - - - - - - - - - - - - - - {{tag.name}} - - - - -

- -
- -
-

- - - - - -

-
- -
- - -
-
- - -
- -
-
- -
-
-
- - -
- -
- - -
-
-
- - -
-
- -
- -
- -
- -
-
- - -
-
- -
- -
-
+
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js deleted file mode 100644 index 69184a513f53a..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ /dev/null @@ -1,427 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { IndexHeader } from './index_header'; -import './create_edit_field'; -import { docTitle } from 'ui/doc_title'; -import { KbnUrlProvider } from 'ui/url'; -import { IndicesEditSectionsProvider } from './edit_sections'; -import { fatalError, toastNotifications } from 'ui/notify'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import template from './edit_index_pattern.html'; -import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public'; -import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { SourceFiltersTable } from './source_filters_table'; -import { IndexedFieldsTable } from './indexed_fields_table'; -import { ScriptedFieldsTable } from './scripted_fields_table'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; - -import { getEditBreadcrumbs } from '../breadcrumbs'; -import { createEditIndexPatternPageStateContainer } from './edit_index_pattern_state_container'; - -const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable'; -const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable'; -const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable'; -const REACT_INDEX_HEADER_DOM_ELEMENT_ID = 'reactIndexHeader'; - -const TAB_INDEXED_FIELDS = 'indexedFields'; -const TAB_SCRIPTED_FIELDS = 'scriptedFields'; -const TAB_SOURCE_FILTERS = 'sourceFilters'; - -const EDIT_FIELD_PATH = '/management/kibana/index_patterns/{{indexPattern.id}}/field/{{name}}'; - -function updateSourceFiltersTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroySourceFiltersTable() { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateScriptedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route), - }} - onRemoveField={() => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroyScriptedFieldsTable() { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateIndexedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getFieldInfo: $scope.getFieldInfo, - }} - /> - , - node - ); - }); -} - -function destroyIndexedFieldsTable() { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function destroyIndexHeader() { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function renderIndexHeader($scope, config) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); -} - -function handleTabChange($scope, newTab) { - destroyIndexedFieldsTable(); - destroySourceFiltersTable(); - destroyScriptedFieldsTable(); - updateTables($scope, newTab); -} - -function updateTables($scope, currentTab) { - switch (currentTab) { - case TAB_SCRIPTED_FIELDS: - return updateScriptedFieldsTable($scope); - case TAB_INDEXED_FIELDS: - return updateIndexedFieldsTable($scope); - case TAB_SOURCE_FILTERS: - return updateSourceFiltersTable($scope); - } -} - -uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { - template, - k7Breadcrumbs: getEditBreadcrumbs, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, -}); - -uiModules - .get('apps/management') - .controller('managementIndexPatternsEdit', function( - $scope, - $location, - $route, - Promise, - config, - Private - ) { - const { - startSyncingState, - stopSyncingState, - setCurrentTab, - getCurrentTab, - state$, - } = createEditIndexPatternPageStateContainer({ - useHashedUrl: config.get('state:storeInSessionStorage'), - defaultTab: TAB_INDEXED_FIELDS, - }); - - $scope.getCurrentTab = getCurrentTab; - $scope.setCurrentTab = setCurrentTab; - - const stateChangedSub = subscribeWithScope( - $scope, - state$, - { - next: ({ tab }) => { - handleTabChange($scope, tab); - }, - }, - fatalError - ); - - handleTabChange($scope, getCurrentTab()); // setup initial tab depending on initial tab state - - startSyncingState(); // starts syncing state between state container and url - - const destroyState = () => { - stateChangedSub.unsubscribe(); - stopSyncingState(); - }; - - $scope.fieldWildcardMatcher = (...args) => - fieldWildcardMatcher(...args, config.get('metaFields')); - $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); - $scope.kbnUrl = Private(KbnUrlProvider); - $scope.indexPattern = $route.current.locals.indexPattern; - $scope.indexPatternListProvider = npStart.plugins.indexPatternManagement.list; - $scope.indexPattern.tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( - $scope.indexPattern, - $scope.indexPattern.id === config.get('defaultIndex') - ); - $scope.getFieldInfo = npStart.plugins.indexPatternManagement.list.getFieldInfo; - docTitle.change($scope.indexPattern.title); - - const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => { - return pattern.id !== $scope.indexPattern.id; - }); - - $scope.$watch('indexPattern.fields', function() { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - $scope.refreshFilters(); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - }); - - $scope.refreshFilters = function() { - const indexedFieldTypes = []; - const scriptedFieldLanguages = []; - $scope.indexPattern.fields.forEach(field => { - if (field.scripted) { - scriptedFieldLanguages.push(field.lang); - } else { - indexedFieldTypes.push(field.type); - } - }); - - $scope.indexedFieldTypes = _.unique(indexedFieldTypes); - $scope.scriptedFieldLanguages = _.unique(scriptedFieldLanguages); - }; - - $scope.changeFilter = function(filter, val) { - $scope[filter] = val || ''; // null causes filter to check for null explicitly - }; - - $scope.$watchCollection('indexPattern.fields', function() { - $scope.conflictFields = $scope.indexPattern.fields.filter(field => field.type === 'conflict'); - }); - - $scope.refreshFields = function() { - const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { - defaultMessage: 'This action resets the popularity counter of each field.', - }); - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { - defaultMessage: 'Refresh', - }), - title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { - defaultMessage: 'Refresh field list?', - }), - }; - - npStart.core.overlays - .openConfirm(confirmMessage, confirmModalOptions) - .then(async isConfirmed => { - if (isConfirmed) { - await $scope.indexPattern.init(true); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - } - }); - }; - - $scope.removePattern = function() { - function doRemove() { - if ($scope.indexPattern.id === config.get('defaultIndex')) { - config.remove('defaultIndex'); - - if (otherPatterns.length) { - config.set('defaultIndex', otherPatterns[0].id); - } - } - - Promise.resolve($scope.indexPattern.destroy()) - .then(function() { - $location.url('/management/kibana/index_patterns'); - }) - .catch(fatalError); - } - - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { - defaultMessage: 'Delete', - }), - title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { - defaultMessage: 'Delete index pattern?', - }), - }; - - npStart.core.overlays.openConfirm('', confirmModalOptions).then(isConfirmed => { - if (isConfirmed) { - doRemove(); - } - }); - }; - - $scope.setDefaultPattern = function() { - config.set('defaultIndex', $scope.indexPattern.id); - }; - - $scope.setIndexPatternsTimeField = function(field) { - if (field.type !== 'date') { - const errorMessage = i18n.translate('kbn.management.editIndexPattern.notDateErrorMessage', { - defaultMessage: 'That field is a {fieldType} not a date.', - values: { fieldType: field.type }, - }); - toastNotifications.addDanger(errorMessage); - return; - } - $scope.indexPattern.timeFieldName = field.name; - return $scope.indexPattern.save(); - }; - - $scope.$watch('fieldFilter', () => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - - if ($scope.fieldFilter === undefined) { - return; - } - - updateTables($scope, getCurrentTab()); - }); - - $scope.$watch('indexedFieldTypeFilter', () => { - if ($scope.indexedFieldTypeFilter !== undefined && getCurrentTab() === TAB_INDEXED_FIELDS) { - updateIndexedFieldsTable($scope); - } - }); - - $scope.$watch('scriptedFieldLanguageFilter', () => { - if ( - $scope.scriptedFieldLanguageFilter !== undefined && - getCurrentTab() === TAB_SCRIPTED_FIELDS - ) { - updateScriptedFieldsTable($scope); - } - }); - - $scope.$on('$destroy', () => { - destroyIndexedFieldsTable(); - destroyScriptedFieldsTable(); - destroySourceFiltersTable(); - destroyIndexHeader(); - destroyState(); - }); - - renderIndexHeader($scope, config); - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx new file mode 100644 index 0000000000000..e869ac84c2db2 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx @@ -0,0 +1,238 @@ +/* + * 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 { filter } from 'lodash'; +import React, { useEffect, useState, useCallback } from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiText, + EuiLink, + EuiIcon, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../plugins/data/public'; +import { + ChromeDocTitle, + NotificationsStart, + OverlayStart, +} from '../../../../../../../../core/public'; +import { IndexPatternManagementStart } from '../../../../../../../../plugins/index_pattern_management/public'; +import { Tabs } from './tabs'; +import { IndexHeader } from './index_header'; + +interface EditIndexPatternProps extends RouteComponentProps { + indexPattern: IndexPattern; + indexPatterns: IndexPattern[]; + config: Record; + services: { + notifications: NotificationsStart; + docTitle: ChromeDocTitle; + overlays: OverlayStart; + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const mappingAPILink = i18n.translate( + 'kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink', + { + defaultMessage: 'Mapping API', + } +); + +const mappingConflictHeader = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictHeader', + { + defaultMessage: 'Mapping conflict', + } +); + +const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { + defaultMessage: 'This action resets the popularity counter of each field.', +}); + +const confirmModalOptionsRefresh = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { + defaultMessage: 'Refresh', + }), + title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { + defaultMessage: 'Refresh field list?', + }), +}; + +const confirmModalOptionsDelete = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { + defaultMessage: 'Delete', + }), + title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { + defaultMessage: 'Delete index pattern?', + }), +}; + +export const EditIndexPattern = withRouter( + ({ indexPattern, indexPatterns, config, services, history, location }: EditIndexPatternProps) => { + const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); + const [conflictedFields, setConflictedFields] = useState( + indexPattern.fields.filter(field => field.type === 'conflict') + ); + const [defaultIndex, setDefaultIndex] = useState(config.get('defaultIndex')); + const [tags, setTags] = useState([]); + + useEffect(() => { + setFields(indexPattern.getNonScriptedFields()); + setConflictedFields(indexPattern.fields.filter(field => field.type === 'conflict')); + }, [indexPattern, indexPattern.fields]); + + useEffect(() => { + const indexPatternTags = + services.indexPatternManagement.list.getIndexPatternTags( + indexPattern, + indexPattern.id === defaultIndex + ) || []; + setTags(indexPatternTags); + }, [defaultIndex, indexPattern, services.indexPatternManagement.list]); + + const setDefaultPattern = useCallback(() => { + config.set('defaultIndex', indexPattern.id); + setDefaultIndex(indexPattern.id || ''); + }, [config, indexPattern.id]); + + const refreshFields = () => { + services.overlays + .openConfirm(confirmMessage, confirmModalOptionsRefresh) + .then(async isConfirmed => { + if (isConfirmed) { + await indexPattern.init(true); + setFields(indexPattern.getNonScriptedFields()); + } + }); + }; + + const removePattern = () => { + function doRemove() { + if (indexPattern.id === defaultIndex) { + config.remove('defaultIndex'); + const otherPatterns = filter(indexPatterns, pattern => { + return pattern.id !== indexPattern.id; + }); + + if (otherPatterns.length) { + config.set('defaultIndex', otherPatterns[0].id); + } + } + + Promise.resolve(indexPattern.destroy()).then(function() { + history.push('/management/kibana/index_patterns'); + }); + } + + services.overlays.openConfirm('', confirmModalOptionsDelete).then(isConfirmed => { + if (isConfirmed) { + doRemove(); + } + }); + }; + + const timeFilterHeader = i18n.translate('kbn.management.editIndexPattern.timeFilterHeader', { + defaultMessage: "Time Filter field name: '{timeFieldName}'", + values: { timeFieldName: indexPattern.timeFieldName }, + }); + + const mappingConflictLabel = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictLabel', + { + defaultMessage: + '{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.', + values: { conflictFieldsLength: conflictedFields.length }, + } + ); + + services.docTitle.change(indexPattern.title); + + const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0)); + + return ( + + + + + {showTagsSection && ( + + {Boolean(indexPattern.timeFieldName) && ( + + {timeFilterHeader} + + )} + {tags.map((tag: any) => ( + + {tag.name} + + ))} + + )} + + +

+ {indexPattern.title} }} + />{' '} + + {mappingAPILink} + + +

+
+ {conflictedFields.length > 0 && ( + +

{mappingConflictLabel}

+
+ )} +
+ + + +
+ ); + } +); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts index 473417a7aabd6..5723a596f95d5 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts @@ -25,7 +25,7 @@ import { } from '../../../../../../../../plugins/kibana_utils/public'; interface IEditIndexPatternState { - tab: string; // TODO: type those 3 tabs with enum, when edit_index_pattern.js migrated to ts + tab: string; } /** @@ -38,7 +38,6 @@ export function createEditIndexPatternPageStateContainer({ defaultTab: string; useHashedUrl: boolean; }) { - // until angular is used as shell - use hash history const history = createHashHistory(); // query param to store app state at const stateStorageKey = '_a'; @@ -78,12 +77,10 @@ export function createEditIndexPatternPageStateContainer({ // makes sure initial url is the same as initial state (this is not really required) kbnUrlStateStorage.set(stateStorageKey, stateContainer.getState(), { replace: true }); - // expose api needed for Controller return { startSyncingState: start, stopSyncingState: stop, setCurrentTab: (newTab: string) => stateContainer.transitions.setTab(newTab), getCurrentTab: () => stateContainer.selectors.tab(), - state$: stateContainer.state$, }; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js deleted file mode 100644 index f0220b2f798e5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -function filterBy(items, key, filter) { - const lowercaseFilter = (filter || '').toLowerCase(); - return items.filter(item => item[key].toLowerCase().includes(lowercaseFilter)); -} - -function getCounts(fields, sourceFilters, fieldFilter = '') { - const fieldCount = _.countBy(filterBy(fields, 'name', fieldFilter), function(field) { - return field.scripted ? 'scripted' : 'indexed'; - }); - - _.defaults(fieldCount, { - indexed: 0, - scripted: 0, - sourceFilters: sourceFilters ? filterBy(sourceFilters, 'value', fieldFilter).length : 0, - }); - - return fieldCount; -} - -export function IndicesEditSectionsProvider() { - return function(indexPattern, fieldFilter, indexPatternListProvider) { - const totalCount = getCounts(indexPattern.fields, indexPattern.sourceFilters); - const filteredCount = getCounts(indexPattern.fields, indexPattern.sourceFilters, fieldFilter); - - const editSections = []; - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { - defaultMessage: 'Fields', - }), - index: 'indexedFields', - count: filteredCount.indexed, - totalCount: totalCount.indexed, - }); - - if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { - defaultMessage: 'Scripted fields', - }), - index: 'scriptedFields', - count: filteredCount.scripted, - totalCount: totalCount.scripted, - }); - } - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { - defaultMessage: 'Source filters', - }), - index: 'sourceFilters', - count: filteredCount.sourceFilters, - totalCount: totalCount.sourceFilters, - }); - - return editSections; - }; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html deleted file mode 100644 index c14dcd3f3a8d5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - - - -
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js index 6beaee60b3788..e2f387c0291a7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js @@ -17,4 +17,160 @@ * under the License. */ -import './edit_index_pattern'; +import React from 'react'; +import { HashRouter } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; +import uiRoutes from 'ui/routes'; +import { uiModules } from 'ui/modules'; +import { I18nContext } from 'ui/i18n'; +import { npStart } from 'ui/new_platform'; +import template from './edit_index_pattern.html'; +import createEditFieldtemplate from './create_edit_field.html'; +import { + getEditBreadcrumbs, + getEditFieldBreadcrumbs, + getCreateFieldBreadcrumbs, +} from '../breadcrumbs'; +import { EditIndexPattern } from './edit_index_pattern'; +import { CreateEditField } from './create_edit_field'; + +const REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID = 'reactEditIndexPattern'; + +function destroyEditIndexPattern() { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + +function renderEditIndexPattern($scope, config, $route) { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +} + +uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { + template, + k7Breadcrumbs: getEditBreadcrumbs, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, +}); + +uiModules + .get('apps/management') + .controller('managementIndexPatternsEdit', function($scope, $route, config) { + $scope.$on('$destroy', () => { + destroyEditIndexPattern(); + }); + + renderEditIndexPattern($scope, config, $route); + }); + +// routes for create edit field. Will be removed after migartion all component to react. +const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; +const renderCreateEditField = ($scope, $route, getConfig, fieldFormatEditors) => { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + if (!node) { + return; + } + + render( + + + npStart.core.http, + notifications: npStart.core.notifications, + docTitle: npStart.core.chrome.docTitle, + docLinksScriptedFields: npStart.core.docLinks.links.scriptedFields, + }} + /> + + , + node + ); + }); +}; + +const destroyCreateEditField = () => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + node && unmountComponentAtNode(node); +}; + +uiRoutes + .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { + mode: 'edit', + k7Breadcrumbs: getEditFieldBreadcrumbs, + }) + .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { + mode: 'create', + k7Breadcrumbs: getCreateFieldBreadcrumbs, + }) + .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { + template: createEditFieldtemplate, + mapBreadcrumbs($route, breadcrumbs) { + const { indexPattern } = $route.current.locals; + return breadcrumbs.map(crumb => { + if (crumb.id !== indexPattern.id) { + return crumb; + } + + return { + ...crumb, + display: indexPattern.title, + }; + }); + }, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, + controllerAs: 'fieldSettings', + controller: function FieldEditorPageController($scope, $route, Private, config) { + const getConfig = (...args) => config.get(...args); + const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); + + renderCreateEditField($scope, $route, getConfig, fieldFormatEditors); + + $scope.$on('$destroy', () => { + destroyCreateEditField(); + }); + }, + }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx index 866d10ecb0e19..a06671ef6a470 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx @@ -30,8 +30,8 @@ import { import { IIndexPattern } from '../../../../../../../../../plugins/data/public'; interface IndexHeaderProps { - defaultIndex: string; indexPattern: IIndexPattern; + defaultIndex?: string; setDefault?: () => void; refreshFields?: () => void; deleteIndexPattern?: () => void; @@ -77,7 +77,7 @@ export function IndexHeader({ )} - +

{indexPattern.title}

@@ -92,7 +92,7 @@ export function IndexHeader({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 7c2bb565615d7..c69063967b1e2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -19,7 +19,11 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; -import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { + IndexPatternField, + IIndexPattern, + IFieldType, +} from '../../../../../../../../../plugins/data/public'; import { Table } from './components/table'; import { getFieldFormat } from './lib'; import { IndexedFieldItem } from './types'; @@ -31,7 +35,7 @@ interface IndexedFieldsTableProps { indexedFieldTypeFilter?: string; helpers: { redirectToRoute: (obj: any) => void; - getFieldInfo: (indexPattern: IIndexPattern, field: string) => string[]; + getFieldInfo: (indexPattern: IIndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; } @@ -76,7 +80,7 @@ export class IndexedFieldsTable extends Component< indexPattern: field.indexPattern, format: getFieldFormat(indexPattern, field.name), excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, - info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name), + info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), }; })) || [] diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap index 569b75c848c52..202b09ddc6066 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
void; } @@ -136,14 +136,19 @@ export class ScriptedFieldsTable extends Component< }; render() { - const { helpers, indexPattern } = this.props; + const { indexPattern } = this.props; const { fieldToDelete, deprecatedLangsInUse } = this.state; const items = this.getFilteredItems(); return ( <> -
+
{ + indexPattern: IndexPattern; + config: Record; + fields: IndexPatternField[]; + services: { + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const filterAriaLabel = i18n.translate('kbn.management.editIndexPattern.fields.filterAria', { + defaultMessage: 'Filter', +}); + +const filterPlaceholder = i18n.translate( + 'kbn.management.editIndexPattern.fields.filterPlaceholder', + { + defaultMessage: 'Filter', + } +); + +export function Tabs({ config, indexPattern, fields, services, history, location }: TabsProps) { + const [fieldFilter, setFieldFilter] = useState(''); + const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); + const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState(''); + const [indexedFieldTypes, setIndexedFieldType] = useState([]); + const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState([]); + const [syncingStateFunc, setSyncingStateFunc] = useState({ + getCurrentTab: () => TAB_INDEXED_FIELDS, + }); + + const refreshFilters = useCallback(() => { + const tempIndexedFieldTypes: string[] = []; + const tempScriptedFieldLanguages: string[] = []; + indexPattern.fields.forEach(field => { + if (field.scripted) { + if (field.lang) { + tempScriptedFieldLanguages.push(field.lang); + } + } else { + tempIndexedFieldTypes.push(field.type); + } + }); + + setIndexedFieldType(convertToEuiSelectOption(tempIndexedFieldTypes, 'indexedFiledTypes')); + setScriptedFieldLanguages( + convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages') + ); + }, [indexPattern]); + + useEffect(() => { + refreshFilters(); + }, [indexPattern, indexPattern.fields, refreshFilters]); + + const fieldWildcardMatcherDecorated = useCallback( + (filters: string[]) => fieldWildcardMatcher(filters, config.get('metaFields')), + [config] + ); + + const getFilterSection = useCallback( + (type: string) => { + return ( + + + setFieldFilter(e.target.value)} + data-test-subj="indexPatternFieldFilter" + aria-label={filterAriaLabel} + /> + + {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( + + setIndexedFieldTypeFilter(e.target.value)} + data-test-subj="indexedFieldTypeFilterDropdown" + /> + + )} + {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( + + setScriptedFieldLanguageFilter(e.target.value)} + data-test-subj="scriptedFieldLanguageFilterDropdown" + /> + + )} + + ); + }, + [ + fieldFilter, + indexedFieldTypeFilter, + indexedFieldTypes, + scriptedFieldLanguageFilter, + scriptedFieldLanguages, + ] + ); + + const getContent = useCallback( + (type: string) => { + switch (type) { + case TAB_INDEXED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + getFieldInfo: services.indexPatternManagement.list.getFieldInfo, + }} + /> + + ); + case TAB_SCRIPTED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + }} + onRemoveField={refreshFilters} + /> + + ); + case TAB_SOURCE_FILTERS: + return ( + + + {getFilterSection(type)} + + + + ); + } + }, + [ + fieldFilter, + fieldWildcardMatcherDecorated, + fields, + getFilterSection, + history, + indexPattern, + indexedFieldTypeFilter, + refreshFilters, + scriptedFieldLanguageFilter, + services.indexPatternManagement.list.getFieldInfo, + ] + ); + + const euiTabs: EuiTabbedContentTab[] = useMemo( + () => + getTabs(indexPattern, fieldFilter, services.indexPatternManagement.list).map( + (tab: Pick) => { + return { + ...tab, + content: getContent(tab.id), + }; + } + ), + [fieldFilter, getContent, indexPattern, services.indexPatternManagement.list] + ); + + const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id); + + useEffect(() => { + const { + startSyncingState, + stopSyncingState, + setCurrentTab, + getCurrentTab, + } = createEditIndexPatternPageStateContainer({ + useHashedUrl: config.get('state:storeInSessionStorage'), + defaultTab: TAB_INDEXED_FIELDS, + }); + + startSyncingState(); + setSyncingStateFunc({ + setCurrentTab, + getCurrentTab, + }); + setSelectedTabId(getCurrentTab()); + + return () => { + stopSyncingState(); + }; + }, [config]); + + return ( + tab.id === selectedTabId)} + onTabClick={tab => { + setSelectedTabId(tab.id); + syncingStateFunc.setCurrentTab(tab.id); + }} + /> + ); +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts new file mode 100644 index 0000000000000..bdb1436c37efb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts @@ -0,0 +1,146 @@ +/* + * 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 { Dictionary, countBy, defaults, unique } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public'; +import { IndexPatternManagementStart } from '../../../../../../../../../plugins/index_pattern_management/public'; +import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants'; + +function filterByName(items: IndexPatternField[], filter: string) { + const lowercaseFilter = (filter || '').toLowerCase(); + return items.filter(item => item.name.toLowerCase().includes(lowercaseFilter)); +} + +function getCounts( + fields: IndexPatternField[], + sourceFilters: { + excludes: string[]; + }, + fieldFilter = '' +) { + const fieldCount = countBy(filterByName(fields, fieldFilter), function(field) { + return field.scripted ? 'scripted' : 'indexed'; + }); + + defaults(fieldCount, { + indexed: 0, + scripted: 0, + sourceFilters: sourceFilters.excludes + ? sourceFilters.excludes.filter(value => + value.toLowerCase().includes(fieldFilter.toLowerCase()) + ).length + : 0, + }); + + return fieldCount; +} + +function getTitle(type: string, filteredCount: Dictionary, totalCount: Dictionary) { + let title = ''; + switch (type) { + case 'indexed': + title = i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { + defaultMessage: 'Fields', + }); + break; + case 'scripted': + title = i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { + defaultMessage: 'Scripted fields', + }); + break; + case 'sourceFilters': + title = i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { + defaultMessage: 'Source filters', + }); + break; + } + const count = ` (${ + filteredCount[type] === totalCount[type] + ? filteredCount[type] + : filteredCount[type] + ' / ' + totalCount[type] + })`; + return title + count; +} + +export function getTabs( + indexPattern: IndexPattern, + fieldFilter: string, + indexPatternListProvider: IndexPatternManagementStart['list'] +) { + const totalCount = getCounts(indexPattern.fields, indexPattern.getSourceFiltering()); + const filteredCount = getCounts( + indexPattern.fields, + indexPattern.getSourceFiltering(), + fieldFilter + ); + + const tabs = []; + + tabs.push({ + name: getTitle('indexed', filteredCount, totalCount), + id: TAB_INDEXED_FIELDS, + }); + + if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { + tabs.push({ + name: getTitle('scripted', filteredCount, totalCount), + id: TAB_SCRIPTED_FIELDS, + }); + } + + tabs.push({ + name: getTitle('sourceFilters', filteredCount, totalCount), + id: TAB_SOURCE_FILTERS, + }); + + return tabs; +} + +export function getPath(field: IndexPatternField) { + return `/management/kibana/index_patterns/${field.indexPattern?.id}/field/${field.name}`; +} + +const allTypesDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allTypesDropDown', { + defaultMessage: 'All field types', +}); + +const allLangsDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allLangsDropDown', { + defaultMessage: 'All languages', +}); + +export function convertToEuiSelectOption(options: string[], type: string) { + const euiOptions = + options.length > 0 + ? [ + { + value: '', + text: type === 'scriptedFieldLanguages' ? allLangsDropDown : allTypesDropDown, + }, + ] + : []; + return euiOptions.concat( + unique(options).map(option => { + return { + value: option, + text: option, + }; + }) + ); +} diff --git a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js index 6e1b0b7160941..87592cf4e750e 100644 --- a/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/__tests__/region_map_visualization.js @@ -54,6 +54,7 @@ import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_ty import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; +import { getBaseMapsVis } from '../../../../../plugins/maps_legacy/public'; const THRESHOLD = 0.45; const PIXEL_DIFF = 96; @@ -101,7 +102,7 @@ describe('RegionMapsVisualizationTests', function() { let getManifestStub; beforeEach( - ngMock.inject((Private, $injector) => { + ngMock.inject(() => { setInjectedVarFunc(injectedVar => { switch (injectedVar) { case 'mapConfig': @@ -127,17 +128,28 @@ describe('RegionMapsVisualizationTests', function() { } }); const serviceSettings = new ServiceSettings(); - const uiSettings = $injector.get('config'); const regionmapsConfig = { includeElasticMapsService: true, layers: [], }; + const coreSetupMock = { + notifications: { + toasts: {}, + }, + uiSettings: { + get: () => {}, + }, + injectedMetadata: { + getInjectedVar: () => {}, + }, + }; + const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); dependencies = { serviceSettings, - $injector, regionmapsConfig, - uiSettings, + uiSettings: coreSetupMock.uiSettings, + BaseMapsVisualization, }; regionMapVisType = new BaseVisType(createRegionMapTypeDefinition(dependencies)); diff --git a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx index 31a27c4da7fcf..5604067433f13 100644 --- a/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx +++ b/src/legacy/core_plugins/region_map/public/components/region_map_options.tsx @@ -32,8 +32,7 @@ import { SelectOption, SwitchOption, } from '../../../../../plugins/charts/public'; -import { WmsOptions } from '../../../tile_map/public/components/wms_options'; -import { RegionMapVisParams } from '../types'; +import { RegionMapVisParams, WmsOptions } from '../../../../../plugins/maps_legacy/public'; const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ text: name, diff --git a/src/legacy/core_plugins/region_map/public/legacy.ts b/src/legacy/core_plugins/region_map/public/legacy.ts index b0cc767a044e8..4bbd839331e56 100644 --- a/src/legacy/core_plugins/region_map/public/legacy.ts +++ b/src/legacy/core_plugins/region_map/public/legacy.ts @@ -21,17 +21,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; import { RegionMapPluginSetupDependencies } from './plugin'; -import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const plugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, mapsLegacy: npSetup.plugins.mapsLegacy, - - // Temporary solution - // It will be removed when all dependent services are migrated to the new platform. - __LEGACY: new LegacyDependenciesPlugin(), }; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/region_map/public/plugin.ts b/src/legacy/core_plugins/region_map/public/plugin.ts index 1453c2155e2d6..08a73517dc13b 100644 --- a/src/legacy/core_plugins/region_map/public/plugin.ts +++ b/src/legacy/core_plugins/region_map/public/plugin.ts @@ -25,28 +25,28 @@ import { } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; - // @ts-ignore import { createRegionMapFn } from './region_map_fn'; // @ts-ignore import { createRegionMapTypeDefinition } from './region_map_type'; -import { IServiceSettings, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; +import { + getBaseMapsVis, + IServiceSettings, + MapsLegacyPluginSetup, +} from '../../../../plugins/maps_legacy/public'; /** @private */ -interface RegionMapVisualizationDependencies extends LegacyDependenciesPluginSetup { +interface RegionMapVisualizationDependencies { uiSettings: IUiSettingsClient; regionmapsConfig: RegionMapsConfig; serviceSettings: IServiceSettings; - notificationService: any; + BaseMapsVisualization: any; } /** @internal */ export interface RegionMapPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; - __LEGACY: LegacyDependenciesPlugin; mapsLegacy: MapsLegacyPluginSetup; } @@ -66,14 +66,13 @@ export class RegionMapPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, mapsLegacy, __LEGACY }: RegionMapPluginSetupDependencies + { expressions, visualizations, mapsLegacy }: RegionMapPluginSetupDependencies ) { const visualizationDependencies: Readonly = { uiSettings: core.uiSettings, regionmapsConfig: core.injectedMetadata.getInjectedVar('regionmap') as RegionMapsConfig, serviceSettings: mapsLegacy.serviceSettings, - notificationService: core.notifications.toasts, - ...(await __LEGACY.setup()), + BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), }; expressions.registerFunction(createRegionMapFn); diff --git a/src/legacy/core_plugins/region_map/public/region_map_type.js b/src/legacy/core_plugins/region_map/public/region_map_type.js index 9174b03cf843c..b7ed14ed3706e 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_type.js +++ b/src/legacy/core_plugins/region_map/public/region_map_type.js @@ -23,9 +23,7 @@ import { createRegionMapVisualization } from './region_map_visualization'; import { RegionMapOptions } from './components/region_map_options'; import { truncatedColorSchemas } from '../../../../plugins/charts/public'; import { Schemas } from '../../../../plugins/vis_default_editor/public'; - -// TODO: reference to TILE_MAP plugin should be removed -import { ORIGIN } from '../../tile_map/common/origin'; +import { ORIGIN } from '../../../../plugins/maps_legacy/public'; export function createRegionMapTypeDefinition(dependencies) { const { uiSettings, regionmapsConfig, serviceSettings } = dependencies; diff --git a/src/legacy/core_plugins/region_map/public/region_map_visualization.js b/src/legacy/core_plugins/region_map/public/region_map_visualization.js index f08d53ee35c8d..5dbc1ecad277f 100644 --- a/src/legacy/core_plugins/region_map/public/region_map_visualization.js +++ b/src/legacy/core_plugins/region_map/public/region_map_visualization.js @@ -21,30 +21,21 @@ import { i18n } from '@kbn/i18n'; import ChoroplethLayer from './choropleth_layer'; import { getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { toastNotifications } from 'ui/notify'; - -import { TileMapTooltipFormatter } from './tooltip_formatter'; import { truncatedColorMaps } from '../../../../plugins/charts/public'; - -// TODO: reference to TILE_MAP plugin should be removed -import { BaseMapsVisualizationProvider } from '../../tile_map/public/base_maps_visualization'; +import { tooltipFormatter } from './tooltip_formatter'; +import { mapTooltipProvider } from '../../../../plugins/maps_legacy/public'; export function createRegionMapVisualization({ serviceSettings, - $injector, uiSettings, - notificationService, + BaseMapsVisualization, }) { - const BaseMapsVisualization = new BaseMapsVisualizationProvider( - serviceSettings, - notificationService - ); - const tooltipFormatter = new TileMapTooltipFormatter($injector); - return class RegionMapsVisualization extends BaseMapsVisualization { constructor(container, vis) { super(container, vis); this._vis = this.vis; this._choroplethLayer = null; + this._tooltipFormatter = mapTooltipProvider(container, tooltipFormatter); } async render(esResponse, visParams) { @@ -89,7 +80,7 @@ export function createRegionMapVisualization({ this._choroplethLayer.setMetrics(results, metricFieldFormatter, valueColumn.name); if (termColumn && valueColumn) { this._choroplethLayer.setTooltipFormatter( - tooltipFormatter, + this._tooltipFormatter, metricFieldFormatter, termColumn.name, valueColumn.name @@ -123,7 +114,7 @@ export function createRegionMapVisualization({ this._choroplethLayer.setColorRamp(truncatedColorMaps[visParams.colorSchema].value); this._choroplethLayer.setLineWeight(visParams.outlineWeight); this._choroplethLayer.setTooltipFormatter( - tooltipFormatter, + this._tooltipFormatter, metricFieldFormatter, this._metricLabel ); diff --git a/src/legacy/core_plugins/region_map/public/shim/index.ts b/src/legacy/core_plugins/region_map/public/shim/index.ts deleted file mode 100644 index cfc7b62ff4f86..0000000000000 --- a/src/legacy/core_plugins/region_map/public/shim/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 * from './legacy_dependencies_plugin'; diff --git a/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts deleted file mode 100644 index 3a7615e83f281..0000000000000 --- a/src/legacy/core_plugins/region_map/public/shim/legacy_dependencies_plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 chrome from 'ui/chrome'; -import { CoreStart, Plugin } from 'kibana/public'; - -/** @internal */ -export interface LegacyDependenciesPluginSetup { - $injector: any; - serviceSettings: any; -} - -export class LegacyDependenciesPlugin - implements Plugin, void> { - public async setup() { - const $injector = await chrome.dangerouslyGetActiveInjector(); - - return { - $injector, - } as LegacyDependenciesPluginSetup; - } - - public start(core: CoreStart) { - // nothing to do here yet - } -} diff --git a/src/legacy/core_plugins/region_map/public/tooltip.html b/src/legacy/core_plugins/region_map/public/tooltip.html deleted file mode 100644 index 0d57120c80a98..0000000000000 --- a/src/legacy/core_plugins/region_map/public/tooltip.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - -
{{detail.label}}{{detail.value}}
diff --git a/src/legacy/core_plugins/region_map/public/tooltip_formatter.js b/src/legacy/core_plugins/region_map/public/tooltip_formatter.js index 6df08aea0baa6..8d38095ac25e0 100644 --- a/src/legacy/core_plugins/region_map/public/tooltip_formatter.js +++ b/src/legacy/core_plugins/region_map/public/tooltip_formatter.js @@ -17,39 +17,24 @@ * under the License. */ -import $ from 'jquery'; -import template from './tooltip.html'; - -export const TileMapTooltipFormatter = $injector => { - const $rootScope = $injector.get('$rootScope'); - const $compile = $injector.get('$compile'); - - const $tooltipScope = $rootScope.$new(); - const $el = $('
').html(template); - - $compile($el)($tooltipScope); - - return function tooltipFormatter(metric, fieldFormatter, fieldName, metricName) { - if (!metric) { - return ''; - } - - $tooltipScope.details = []; - if (fieldName && metric) { - $tooltipScope.details.push({ - label: fieldName, - value: metric.term, - }); - } - - if (metric) { - $tooltipScope.details.push({ - label: metricName, - value: fieldFormatter ? fieldFormatter.convert(metric.value, 'text') : metric.value, - }); - } - - $tooltipScope.$apply(); - return $el.html(); - }; -}; +export function tooltipFormatter(metric, fieldFormatter, fieldName, metricName) { + if (!metric) { + return ''; + } + + const details = []; + if (fieldName && metric) { + details.push({ + label: fieldName, + value: metric.term, + }); + } + + if (metric) { + details.push({ + label: metricName, + value: fieldFormatter ? fieldFormatter.convert(metric.value, 'text') : metric.value, + }); + } + return details; +} diff --git a/src/legacy/core_plugins/region_map/public/types.ts b/src/legacy/core_plugins/region_map/public/types.ts deleted file mode 100644 index 8585bf720e0cf..0000000000000 --- a/src/legacy/core_plugins/region_map/public/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 { VectorLayer, FileLayerField } from '../../../../plugins/maps_legacy/public'; -import { WMSOptions } from '../../tile_map/public/types'; - -export interface RegionMapVisParams { - readonly addTooltip: true; - readonly legendPosition: 'bottomright'; - colorSchema: string; - emsHotLink?: string | null; - mapCenter: [number, number]; - mapZoom: number; - outlineWeight: number | ''; - isDisplayWarning: boolean; - showAllShapes: boolean; - selectedLayer?: VectorLayer; - selectedJoinField?: FileLayerField; - wms: WMSOptions; -} diff --git a/src/legacy/core_plugins/region_map/public/util.ts b/src/legacy/core_plugins/region_map/public/util.ts index 24c721da1f31a..b4e0dcd5f3510 100644 --- a/src/legacy/core_plugins/region_map/public/util.ts +++ b/src/legacy/core_plugins/region_map/public/util.ts @@ -18,8 +18,7 @@ */ import { FileLayer, VectorLayer } from '../../../../plugins/maps_legacy/public'; -// TODO: reference to TILE_MAP plugin should be removed -import { ORIGIN } from '../../../../legacy/core_plugins/tile_map/common/origin'; +import { ORIGIN } from '../../../../plugins/maps_legacy/public'; export const mapToLayerWithId = (prefix: string, layer: FileLayer): VectorLayer => ({ ...layer, diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js index 5e78047088d2a..3348096c0e2f1 100644 --- a/src/legacy/core_plugins/tests_bundle/index.js +++ b/src/legacy/core_plugins/tests_bundle/index.js @@ -18,6 +18,7 @@ */ import { createReadStream } from 'fs'; +import { resolve } from 'path'; import globby from 'globby'; import MultiStream from 'multistream'; @@ -40,6 +41,7 @@ export default kibana => { }, uiExports: { + styleSheetPaths: resolve(__dirname, 'public/index.scss'), async __bundleProvider__(kbnServer) { const modules = new Set(); @@ -148,6 +150,19 @@ export default kibana => { .type('text/css'); }, }); + + // Sets global variables normally set by the bootstrap.js script + kbnServer.server.route({ + path: '/test_bundle/karma/globals.js', + method: 'GET', + async handler(req, h) { + const basePath = config.get('server.basePath'); + + const file = `window.__kbnPublicPath__ = { 'kbn-ui-shared-deps': "${basePath}/bundles/kbn-ui-shared-deps/" };`; + + return h.response(file).header('content-type', 'application/json'); + }, + }); }, __globalImportAliases__: { diff --git a/src/legacy/core_plugins/tests_bundle/public/index.scss b/src/legacy/core_plugins/tests_bundle/public/index.scss new file mode 100644 index 0000000000000..8020cef8d8492 --- /dev/null +++ b/src/legacy/core_plugins/tests_bundle/public/index.scss @@ -0,0 +1,6 @@ +@import 'src/legacy/ui/public/styles/styling_constants'; + +// This file pulls some styles of NP plugins into the legacy test stylesheet +// so they are available for karma browser tests. +@import '../../../../plugins/vis_type_vislib/public/index'; +@import '../../../../plugins/visualizations/public/index'; diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 3e3dc284671da..f075d8365c299 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -135,7 +135,16 @@ const coreSystem = new CoreSystem({ }, }, rootDomElement, - useLegacyTestHarness: true, + requireLegacyBootstrapModule: () => { + // wrapped in NODE_ENV check so the 'ui/test_harness' module + // is not included in the distributable + if (process.env.IS_KIBANA_DISTRIBUTABLE !== 'true') { + return require('ui/test_harness'); + } + + throw new Error('tests bundle is not available in the distributable'); + }, + requireNewPlatformShimModule: () => require('ui/new_platform'), requireLegacyFiles: () => { ${bundle.getRequires().join('\n ')} } diff --git a/src/legacy/core_plugins/tile_map/common/origin.ts b/src/legacy/core_plugins/tile_map/common/origin.ts deleted file mode 100644 index 7fcf1c659bdf3..0000000000000 --- a/src/legacy/core_plugins/tile_map/common/origin.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 enum ORIGIN { - EMS = 'elastic_maps_service', - KIBANA_YML = 'self_hosted', -} diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js index 3904c43707906..bce2e157ebbc8 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/coordinate_maps_visualization.js @@ -53,6 +53,7 @@ import { import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +import { getBaseMapsVis } from '../../../../../plugins/maps_legacy/public'; function mockRawData() { const stack = [dummyESResponse]; @@ -114,15 +115,26 @@ describe('CoordinateMapsVisualizationTest', function() { return 'not found'; } }); + + const coreSetupMock = { + notifications: { + toasts: {}, + }, + uiSettings: {}, + injectedMetadata: { + getInjectedVar: () => {}, + }, + }; const serviceSettings = new ServiceSettings(); + const BaseMapsVisualization = getBaseMapsVis(coreSetupMock, serviceSettings); const uiSettings = $injector.get('config'); dependencies = { - serviceSettings, - uiSettings, - $injector, - getPrecision, getZoomPrecision, + getPrecision, + BaseMapsVisualization, + uiSettings, + serviceSettings, }; visType = new BaseVisType(createTileMapTypeDefinition(dependencies)); diff --git a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js b/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js index fc029d6bccb6e..bdf9cd806eb8b 100644 --- a/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js +++ b/src/legacy/core_plugins/tile_map/public/__tests__/geohash_layer.js @@ -24,7 +24,8 @@ import scaledCircleMarkersPng from './scaledCircleMarkers.png'; // import shadedCircleMarkersPng from './shadedCircleMarkers.png'; import { ImageComparator } from 'test_utils/image_comparator'; import GeoHashSampleData from './dummy_es_response.json'; -import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaMap } from '../../../../../plugins/maps_legacy/public/map/kibana_map'; describe('geohash_layer', function() { let domNode; diff --git a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx index 9ca42fe3e4074..1efb0b2f884f8 100644 --- a/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/legacy/core_plugins/tile_map/public/components/tile_map_options.tsx @@ -28,9 +28,7 @@ import { SelectOption, SwitchOption, } from '../../../../../plugins/charts/public'; -import { WmsOptions } from './wms_options'; -import { TileMapVisParams } from '../types'; -import { MapTypes } from '../map_types'; +import { WmsOptions, TileMapVisParams, MapTypes } from '../../../../../plugins/maps_legacy/public'; export type TileMapOptionsProps = VisOptionsProps; diff --git a/src/legacy/core_plugins/tile_map/public/editors/_tooltip.html b/src/legacy/core_plugins/tile_map/public/editors/_tooltip.html deleted file mode 100644 index 9df5b94a21eda..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/editors/_tooltip.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - -
{{detail.label}}{{detail.value}}
diff --git a/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js b/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js deleted file mode 100644 index eec90e512b462..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/editors/_tooltip_formatter.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 $ from 'jquery'; -import { i18n } from '@kbn/i18n'; - -import template from './_tooltip.html'; - -export function TileMapTooltipFormatterProvider($injector) { - const $rootScope = $injector.get('$rootScope'); - const $compile = $injector.get('$compile'); - - const $tooltipScope = $rootScope.$new(); - const $el = $('
').html(template); - - $compile($el)($tooltipScope); - - return function tooltipFormatter(metricTitle, metricFormat, feature) { - if (!feature) { - return ''; - } - - $tooltipScope.details = [ - { - label: metricTitle, - value: metricFormat(feature.properties.value), - }, - { - label: i18n.translate('tileMap.tooltipFormatter.latitudeLabel', { - defaultMessage: 'Latitude', - }), - value: feature.geometry.coordinates[1], - }, - { - label: i18n.translate('tileMap.tooltipFormatter.longitudeLabel', { - defaultMessage: 'Longitude', - }), - value: feature.geometry.coordinates[0], - }, - ]; - - $tooltipScope.$apply(); - - return $el.html(); - }; -} diff --git a/src/legacy/core_plugins/tile_map/public/geohash_layer.js b/src/legacy/core_plugins/tile_map/public/geohash_layer.js index b9acf1a15208f..f0261483d302d 100644 --- a/src/legacy/core_plugins/tile_map/public/geohash_layer.js +++ b/src/legacy/core_plugins/tile_map/public/geohash_layer.js @@ -20,12 +20,11 @@ import L from 'leaflet'; import { min, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaMapLayer } from '../../../../plugins/maps_legacy/public'; +import { KibanaMapLayer, MapTypes } from '../../../../plugins/maps_legacy/public'; import { HeatmapMarkers } from './markers/heatmap'; import { ScaledCirclesMarkers } from './markers/scaled_circles'; import { ShadedCirclesMarkers } from './markers/shaded_circles'; import { GeohashGridMarkers } from './markers/geohash_grid'; -import { MapTypes } from './map_types'; export class GeohashLayer extends KibanaMapLayer { constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap) { diff --git a/src/legacy/core_plugins/tile_map/public/legacy.ts b/src/legacy/core_plugins/tile_map/public/legacy.ts index 741e118750f32..dd8d4c6e9311e 100644 --- a/src/legacy/core_plugins/tile_map/public/legacy.ts +++ b/src/legacy/core_plugins/tile_map/public/legacy.ts @@ -21,17 +21,12 @@ import { PluginInitializerContext } from 'kibana/public'; import { npSetup, npStart } from 'ui/new_platform'; import { TileMapPluginSetupDependencies } from './plugin'; -import { LegacyDependenciesPlugin } from './shim'; import { plugin } from '.'; const plugins: Readonly = { expressions: npSetup.plugins.expressions, visualizations: npSetup.plugins.visualizations, mapsLegacy: npSetup.plugins.mapsLegacy, - - // Temporary solution - // It will be removed when all dependent services are migrated to the new platform. - __LEGACY: new LegacyDependenciesPlugin(), }; const pluginInstance = plugin({} as PluginInitializerContext); diff --git a/src/legacy/core_plugins/tile_map/public/plugin.ts b/src/legacy/core_plugins/tile_map/public/plugin.ts index 2b97407b17b38..aa1460a7e2890 100644 --- a/src/legacy/core_plugins/tile_map/public/plugin.ts +++ b/src/legacy/core_plugins/tile_map/public/plugin.ts @@ -25,22 +25,21 @@ import { } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim'; +// TODO: Determine why visualizations don't populate without this +import 'angular-sanitize'; // @ts-ignore import { createTileMapFn } from './tile_map_fn'; // @ts-ignore import { createTileMapTypeDefinition } from './tile_map_type'; -import { IServiceSettings, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; +import { getBaseMapsVis, MapsLegacyPluginSetup } from '../../../../plugins/maps_legacy/public'; /** @private */ -interface TileMapVisualizationDependencies extends LegacyDependenciesPluginSetup { - serviceSettings: IServiceSettings; +interface TileMapVisualizationDependencies { uiSettings: IUiSettingsClient; getZoomPrecision: any; getPrecision: any; - notificationService: any; + BaseMapsVisualization: any; } /** @internal */ @@ -48,7 +47,6 @@ export interface TileMapPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; mapsLegacy: MapsLegacyPluginSetup; - __LEGACY: LegacyDependenciesPlugin; } /** @internal */ @@ -61,16 +59,14 @@ export class TileMapPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, mapsLegacy, __LEGACY }: TileMapPluginSetupDependencies + { expressions, visualizations, mapsLegacy }: TileMapPluginSetupDependencies ) { - const { getZoomPrecision, getPrecision, serviceSettings } = mapsLegacy; + const { getZoomPrecision, getPrecision } = mapsLegacy; const visualizationDependencies: Readonly = { - serviceSettings, getZoomPrecision, getPrecision, - notificationService: core.notifications.toasts, + BaseMapsVisualization: getBaseMapsVis(core, mapsLegacy.serviceSettings), uiSettings: core.uiSettings, - ...(await __LEGACY.setup()), }; expressions.registerFunction(() => createTileMapFn(visualizationDependencies)); diff --git a/src/legacy/core_plugins/tile_map/public/shim/index.ts b/src/legacy/core_plugins/tile_map/public/shim/index.ts deleted file mode 100644 index cfc7b62ff4f86..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/shim/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 * from './legacy_dependencies_plugin'; diff --git a/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts b/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts deleted file mode 100644 index 5296e98b09efe..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/shim/legacy_dependencies_plugin.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 chrome from 'ui/chrome'; -import { CoreStart, Plugin } from 'kibana/public'; -// TODO: Determine why visualizations don't populate without this -import 'angular-sanitize'; - -/** @internal */ -export interface LegacyDependenciesPluginSetup { - $injector: any; -} - -export class LegacyDependenciesPlugin - implements Plugin, void> { - public async setup() { - const $injector = await chrome.dangerouslyGetActiveInjector(); - - return { - $injector, - } as LegacyDependenciesPluginSetup; - } - - public start(core: CoreStart) { - // nothing to do here yet - } -} diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_type.js b/src/legacy/core_plugins/tile_map/public/tile_map_type.js index ae3a839b600e9..ca6a586d22008 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_type.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_type.js @@ -19,11 +19,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { convertToGeoJson } from '../../../../plugins/maps_legacy/public'; +import { convertToGeoJson, MapTypes } from '../../../../plugins/maps_legacy/public'; import { Schemas } from '../../../../plugins/vis_default_editor/public'; import { createTileMapVisualization } from './tile_map_visualization'; import { TileMapOptions } from './components/tile_map_options'; -import { MapTypes } from './map_types'; import { supportsCssFilters } from './css_filters'; import { truncatedColorSchemas } from '../../../../plugins/charts/public'; diff --git a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js index fdce8bc51fe86..6a7bda5e18883 100644 --- a/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js +++ b/src/legacy/core_plugins/tile_map/public/tile_map_visualization.js @@ -19,30 +19,24 @@ import { get } from 'lodash'; import { GeohashLayer } from './geohash_layer'; -import { BaseMapsVisualizationProvider } from './base_maps_visualization'; -import { TileMapTooltipFormatterProvider } from './editors/_tooltip_formatter'; import { npStart } from 'ui/new_platform'; import { getFormat } from '../../../ui/public/visualize/loader/pipeline_helpers/utilities'; -import { scaleBounds, geoContains } from '../../../../plugins/maps_legacy/public'; - -export const createTileMapVisualization = ({ - serviceSettings, - $injector, - getZoomPrecision, - getPrecision, - notificationService, -}) => { - const BaseMapsVisualization = new BaseMapsVisualizationProvider( - serviceSettings, - notificationService - ); - const tooltipFormatter = new TileMapTooltipFormatterProvider($injector); +import { + scaleBounds, + geoContains, + mapTooltipProvider, +} from '../../../../plugins/maps_legacy/public'; +import { tooltipFormatter } from './tooltip_formatter'; + +export const createTileMapVisualization = dependencies => { + const { getZoomPrecision, getPrecision, BaseMapsVisualization } = dependencies; return class CoordinateMapsVisualization extends BaseMapsVisualization { constructor(element, vis) { super(element, vis); this._geohashLayer = null; + this._tooltipFormatter = mapTooltipProvider(element, tooltipFormatter); } updateGeohashAgg = () => { @@ -190,18 +184,15 @@ export const createTileMapVisualization = ({ const metricDimension = this._params.dimensions.metric; const metricLabel = metricDimension ? metricDimension.label : ''; const metricFormat = getFormat(metricDimension && metricDimension.format); - const boundTooltipFormatter = tooltipFormatter.bind( - null, - metricLabel, - metricFormat.getConverterFor('text') - ); return { label: metricLabel, valueFormatter: this._geoJsonFeatureCollectionAndMeta ? metricFormat.getConverterFor('text') : null, - tooltipFormatter: this._geoJsonFeatureCollectionAndMeta ? boundTooltipFormatter : null, + tooltipFormatter: this._geoJsonFeatureCollectionAndMeta + ? this._tooltipFormatter.bind(null, metricLabel, metricFormat.getConverterFor('text')) + : null, mapType: newParams.mapType, isFilteredByCollar: this._isFilteredByCollar(), colorRamp: newParams.colorSchema, diff --git a/src/legacy/core_plugins/tile_map/public/tooltip_formatter.js b/src/legacy/core_plugins/tile_map/public/tooltip_formatter.js new file mode 100644 index 0000000000000..1c87d4dcca2b5 --- /dev/null +++ b/src/legacy/core_plugins/tile_map/public/tooltip_formatter.js @@ -0,0 +1,45 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export function tooltipFormatter(metricTitle, metricFormat, feature) { + if (!feature) { + return ''; + } + + return [ + { + label: metricTitle, + value: metricFormat(feature.properties.value), + }, + { + label: i18n.translate('tileMap.tooltipFormatter.latitudeLabel', { + defaultMessage: 'Latitude', + }), + value: feature.geometry.coordinates[1], + }, + { + label: i18n.translate('tileMap.tooltipFormatter.longitudeLabel', { + defaultMessage: 'Longitude', + }), + value: feature.geometry.coordinates[0], + }, + ]; +} diff --git a/src/legacy/core_plugins/tile_map/public/types.ts b/src/legacy/core_plugins/tile_map/public/types.ts deleted file mode 100644 index e1b4c27319123..0000000000000 --- a/src/legacy/core_plugins/tile_map/public/types.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { TmsLayer } from '../../../../plugins/maps_legacy/public'; -import { MapTypes } from './map_types'; - -export interface WMSOptions { - selectedTmsLayer?: TmsLayer; - enabled: boolean; - url?: string; - options: { - version?: string; - layers?: string; - format: string; - transparent: boolean; - attribution?: string; - styles?: string; - }; -} - -export interface TileMapVisParams { - colorSchema: string; - mapType: MapTypes; - isDesaturated: boolean; - addTooltip: boolean; - heatClusterSize: number; - legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft'; - mapZoom: number; - mapCenter: [number, number]; - wms: WMSOptions; -} diff --git a/src/legacy/core_plugins/timelion/public/app.js b/src/legacy/core_plugins/timelion/public/app.js index 7f5c7d4664af8..80ffa440e7285 100644 --- a/src/legacy/core_plugins/timelion/public/app.js +++ b/src/legacy/core_plugins/timelion/public/app.js @@ -18,6 +18,8 @@ */ import _ from 'lodash'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18n } from '@kbn/i18n'; diff --git a/src/legacy/core_plugins/vis_type_table/index.ts b/src/legacy/core_plugins/vis_type_table/index.ts deleted file mode 100644 index 04ca9da7de32b..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const tableVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'table_vis', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default tableVisPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_table/package.json b/src/legacy/core_plugins/vis_type_table/package.json deleted file mode 100644 index 2809b0e047836..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "table_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js deleted file mode 100644 index a23407a599ae2..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/__tests__/agg_table.js +++ /dev/null @@ -1,482 +0,0 @@ -/* - * 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 $ from 'jquery'; -import moment from 'moment'; -import ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { npStart } from '../../legacy_imports'; -import { round } from 'lodash'; -import { getAngularModule } from '../../get_inner_angular'; -import { initTableVisLegacyModule } from '../../table_vis_legacy_module'; -import { tabifiedData } from './tabified_data'; - -describe('Table Vis - AggTable Directive', function() { - let $rootScope; - let $compile; - let settings; - - const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', npStart.core); - initTableVisLegacyModule(tableVisModule); - }; - - beforeEach(initLocalAngular); - - beforeEach(ngMock.module('kibana/table_vis')); - beforeEach( - ngMock.inject(function($injector, config) { - settings = config; - - $rootScope = $injector.get('$rootScope'); - $compile = $injector.get('$compile'); - }) - ); - - let $scope; - beforeEach(function() { - $scope = $rootScope.$new(); - }); - afterEach(function() { - $scope.$destroy(); - }); - - it('renders a simple response properly', function() { - $scope.dimensions = { - metrics: [{ accessor: 0, format: { id: 'number' }, params: {} }], - buckets: [], - }; - $scope.table = tabifiedData.metricOnly.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).to.be(1); - expect($el.find('td').length).to.be(1); - expect($el.find('td').text()).to.eql('1,000'); - }); - - it('renders nothing if the table is empty', function() { - $scope.dimensions = {}; - $scope.table = null; - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - expect($el.find('tbody').length).to.be(0); - }); - - it('renders a complex response properly', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - const $el = $(''); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tbody').length).to.be(1); - - const $rows = $el.find('tbody tr'); - expect($rows.length).to.be.greaterThan(0); - - function validBytes(str) { - const num = str.replace(/,/g, ''); - if (num !== '-') { - expect(num).to.match(/^\d+$/); - } - } - - $rows.each(function() { - // 6 cells in every row - const $cells = $(this).find('td'); - expect($cells.length).to.be(6); - - const txts = $cells.map(function() { - return $(this) - .text() - .trim(); - }); - - // two character country code - expect(txts[0]).to.match(/^(png|jpg|gif|html|css)$/); - validBytes(txts[1]); - - // country - expect(txts[2]).to.match(/^\w\w$/); - validBytes(txts[3]); - - // os - expect(txts[4]).to.match(/^(win|mac|linux)$/); - validBytes(txts[5]); - }); - }); - - describe('renders totals row', function() { - async function totalsRowTest(totalFunc, expected) { - function setDefaultTimezone() { - moment.tz.setDefault(settings.get('dateFormat:tz')); - } - - const off = $scope.$on('change:config.dateFormat:tz', setDefaultTimezone); - const oldTimezoneSetting = settings.get('dateFormat:tz'); - settings.set('dateFormat:tz', 'UTC'); - - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.showTotal = true; - $scope.totalFunc = totalFunc; - const $el = $(``); - $compile($el)($scope); - $scope.$digest(); - - expect($el.find('tfoot').length).to.be(1); - - const $rows = $el.find('tfoot tr'); - expect($rows.length).to.be(1); - - const $cells = $($rows[0]).find('th'); - expect($cells.length).to.be(6); - - for (let i = 0; i < 6; i++) { - expect( - $($cells[i]) - .text() - .trim() - ).to.be(expected[i]); - } - settings.set('dateFormat:tz', oldTimezoneSetting); - off(); - } - it('as count', async function() { - await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']); - }); - it('as min', async function() { - await totalsRowTest('min', [ - '', - '2014-09-28', - '9,283', - 'Sep 28, 2014 @ 00:00:00.000', - '1', - '11', - ]); - }); - it('as max', async function() { - await totalsRowTest('max', [ - '', - '2014-10-03', - '220,943', - 'Oct 3, 2014 @ 00:00:00.000', - '239', - '837', - ]); - }); - it('as avg', async function() { - await totalsRowTest('avg', ['', '', '87,221.5', '', '64.667', '206.833']); - }); - it('as sum', async function() { - await totalsRowTest('sum', ['', '', '1,569,987', '', '1,164', '3,723']); - }); - }); - - describe('aggTable.toCsv()', function() { - it('escapes rows and columns properly', function() { - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - expect(aggTable.toCsv()).to.be( - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n' - ); - }); - - it('exports rows and columns properly', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - const raw = aggTable.toCsv(false); - expect(raw).to.be( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - 'png,IT,win,412032,9299,0' + - '\r\n' + - 'png,IT,mac,412032,9299,9299' + - '\r\n' + - 'png,US,linux,412032,8293,3992' + - '\r\n' + - 'png,US,mac,412032,8293,3029' + - '\r\n' + - 'css,MX,win,412032,9299,4992' + - '\r\n' + - 'css,MX,mac,412032,9299,5892' + - '\r\n' + - 'css,US,linux,412032,8293,3992' + - '\r\n' + - 'css,US,mac,412032,8293,3029' + - '\r\n' + - 'html,CN,win,412032,9299,4992' + - '\r\n' + - 'html,CN,mac,412032,9299,5892' + - '\r\n' + - 'html,FR,win,412032,8293,3992' + - '\r\n' + - 'html,FR,mac,412032,8293,3029' + - '\r\n' - ); - }); - - it('exports formatted rows and columns properly', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 2, params: {} }, - { accessor: 4, params: {} }, - ], - metrics: [ - { accessor: 1, params: {} }, - { accessor: 3, params: {} }, - { accessor: 5, params: {} }, - ], - }; - $scope.table = tabifiedData.threeTermBuckets.tables[0]; - - const $el = $compile('')( - $scope - ); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = $scope.table; - - // Create our own converter since the ones we use for tests don't actually transform the provided value - $tableScope.formattedColumns[0].formatter.convert = v => `${v}_formatted`; - - const formatted = aggTable.toCsv(true); - expect(formatted).to.be( - '"extension: Descending","Average bytes","geo.src: Descending","Average bytes","machine.os: Descending","Average bytes"' + - '\r\n' + - '"png_formatted",IT,win,412032,9299,0' + - '\r\n' + - '"png_formatted",IT,mac,412032,9299,9299' + - '\r\n' + - '"png_formatted",US,linux,412032,8293,3992' + - '\r\n' + - '"png_formatted",US,mac,412032,8293,3029' + - '\r\n' + - '"css_formatted",MX,win,412032,9299,4992' + - '\r\n' + - '"css_formatted",MX,mac,412032,9299,5892' + - '\r\n' + - '"css_formatted",US,linux,412032,8293,3992' + - '\r\n' + - '"css_formatted",US,mac,412032,8293,3029' + - '\r\n' + - '"html_formatted",CN,win,412032,9299,4992' + - '\r\n' + - '"html_formatted",CN,mac,412032,9299,5892' + - '\r\n' + - '"html_formatted",FR,win,412032,8293,3992' + - '\r\n' + - '"html_formatted",FR,mac,412032,8293,3029' + - '\r\n' - ); - }); - }); - - it('renders percentage columns', async function() { - $scope.dimensions = { - buckets: [ - { accessor: 0, params: {} }, - { accessor: 1, format: { id: 'date', params: { pattern: 'YYYY-MM-DD' } } }, - ], - metrics: [ - { accessor: 2, format: { id: 'number' } }, - { accessor: 3, format: { id: 'date' } }, - { accessor: 4, format: { id: 'number' } }, - { accessor: 5, format: { id: 'number' } }, - ], - }; - $scope.table = - tabifiedData.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative.tables[0]; - $scope.percentageCol = 'Average bytes'; - - const $el = $(``); - - $compile($el)($scope); - $scope.$digest(); - - const $headings = $el.find('th'); - expect($headings.length).to.be(7); - expect( - $headings - .eq(3) - .text() - .trim() - ).to.be('Average bytes percentages'); - - const countColId = $scope.table.columns.find(col => col.name === $scope.percentageCol).id; - const counts = $scope.table.rows.map(row => row[countColId]); - const total = counts.reduce((sum, curr) => sum + curr, 0); - const $percentageColValues = $el.find('tbody tr').map((i, el) => - $(el) - .find('td') - .eq(3) - .text() - ); - - $percentageColValues.each((i, value) => { - const percentage = `${round((counts[i] / total) * 100, 1)}%`; - expect(value).to.be(percentage); - }); - }); - - describe('aggTable.exportAsCsv()', function() { - let origBlob; - function FakeBlob(slices, opts) { - this.slices = slices; - this.opts = opts; - } - - beforeEach(function() { - origBlob = window.Blob; - window.Blob = FakeBlob; - }); - - afterEach(function() { - window.Blob = origBlob; - }); - - it('calls _saveAs properly', function() { - const $el = $compile('')($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - - const saveAs = sinon.stub(aggTable, '_saveAs'); - $tableScope.table = { - columns: [ - { id: 'a', name: 'one' }, - { id: 'b', name: 'two' }, - { id: 'c', name: 'with double-quotes(")' }, - ], - rows: [{ a: 1, b: 2, c: '"foobar"' }], - }; - - aggTable.csv.filename = 'somefilename.csv'; - aggTable.exportAsCsv(); - - expect(saveAs.callCount).to.be(1); - const call = saveAs.getCall(0); - expect(call.args[0]).to.be.a(FakeBlob); - expect(call.args[0].slices).to.eql([ - 'one,two,"with double-quotes("")"' + '\r\n' + '1,2,"""foobar"""' + '\r\n', - ]); - expect(call.args[0].opts).to.eql({ - type: 'text/plain;charset=utf-8', - }); - expect(call.args[1]).to.be('somefilename.csv'); - }); - - it('should use the export-title attribute', function() { - const expected = 'export file name'; - const $el = $compile( - `` - )($scope); - $scope.$digest(); - - const $tableScope = $el.isolateScope(); - const aggTable = $tableScope.aggTable; - $tableScope.table = { - columns: [], - rows: [], - }; - $tableScope.exportTitle = expected; - $scope.$digest(); - - expect(aggTable.csv.filename).to.equal(`${expected}.csv`); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/_index.scss b/src/legacy/core_plugins/vis_type_table/public/agg_table/_index.scss deleted file mode 100644 index b19d4a887a7f3..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'agg_table'; diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js b/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js deleted file mode 100644 index b9e79f96e4fc1..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.js +++ /dev/null @@ -1,272 +0,0 @@ -/* - * 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 _ from 'lodash'; -import aggTableTemplate from './agg_table.html'; -import { getFormatService } from '../services'; -import { i18n } from '@kbn/i18n'; - -export function KbnAggTable(config, RecursionHelper) { - return { - restrict: 'E', - template: aggTableTemplate, - scope: { - table: '=', - dimensions: '=', - perPage: '=?', - sort: '=?', - exportTitle: '=?', - showTotal: '=', - totalFunc: '=', - percentageCol: '=', - filter: '=', - }, - controllerAs: 'aggTable', - compile: function($el) { - // Use the compile function from the RecursionHelper, - // And return the linking function(s) which it returns - return RecursionHelper.compile($el); - }, - controller: function($scope) { - const self = this; - - self._saveAs = require('@elastic/filesaver').saveAs; - self.csv = { - separator: config.get('csv:separator'), - quoteValues: config.get('csv:quoteValues'), - }; - - self.exportAsCsv = function(formatted) { - const csv = new Blob([self.toCsv(formatted)], { type: 'text/plain;charset=utf-8' }); - self._saveAs(csv, self.csv.filename); - }; - - self.toCsv = function(formatted) { - const rows = $scope.table.rows; - const columns = formatted ? $scope.formattedColumns : $scope.table.columns; - const nonAlphaNumRE = /[^a-zA-Z0-9]/; - const allDoubleQuoteRE = /"/g; - - function escape(val) { - if (!formatted && _.isObject(val)) val = val.valueOf(); - val = String(val); - if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { - val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; - } - return val; - } - - // escape each cell in each row - const csvRows = rows.map(function(row) { - return Object.entries(row).map(([k, v]) => { - const column = columns.find(c => c.id === k); - if (formatted && column) { - return escape(column.formatter.convert(v)); - } - return escape(v); - }); - }); - - // add the columns to the rows - csvRows.unshift( - columns.map(function(col) { - return escape(formatted ? col.title : col.name); - }) - ); - - return csvRows - .map(function(row) { - return row.join(self.csv.separator) + '\r\n'; - }) - .join(''); - }; - - $scope.$watchMulti( - ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], - function() { - const { table, exportTitle, percentageCol } = $scope; - const showPercentage = percentageCol !== ''; - - if (!table) { - $scope.rows = null; - $scope.formattedColumns = null; - return; - } - - self.csv.filename = (exportTitle || table.title || 'table') + '.csv'; - $scope.rows = table.rows; - $scope.formattedColumns = []; - - if (typeof $scope.dimensions === 'undefined') return; - - const { buckets, metrics, splitColumn } = $scope.dimensions; - - $scope.formattedColumns = table.columns - .map(function(col, i) { - const isBucket = buckets.find(bucket => bucket.accessor === i); - const isSplitColumn = splitColumn - ? splitColumn.find(splitColumn => splitColumn.accessor === i) - : undefined; - const dimension = - isBucket || isSplitColumn || metrics.find(metric => metric.accessor === i); - - if (!dimension) return; - - const formatter = getFormatService().deserialize(dimension.format); - - const formattedColumn = { - id: col.id, - title: col.name, - formatter: formatter, - filterable: !!isBucket, - }; - - const last = i === table.columns.length - 1; - - if (last || !isBucket) { - formattedColumn.class = 'visualize-table-right'; - } - - const isDate = - dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; - const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; - - let { totalFunc } = $scope; - if (typeof totalFunc === 'undefined' && showPercentage) { - totalFunc = 'sum'; - } - - if (allowsNumericalAggregations || isDate || totalFunc === 'count') { - const sum = tableRows => { - return _.reduce( - tableRows, - function(prev, curr) { - // some metrics return undefined for some of the values - // derivative is an example of this as it returns undefined in the first row - if (curr[col.id] === undefined) return prev; - return prev + curr[col.id]; - }, - 0 - ); - }; - - formattedColumn.sumTotal = sum(table.rows); - switch (totalFunc) { - case 'sum': { - if (!isDate) { - const total = formattedColumn.sumTotal; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = formattedColumn.sumTotal; - } - break; - } - case 'avg': { - if (!isDate) { - const total = sum(table.rows) / table.rows.length; - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - } - break; - } - case 'min': { - const total = _.chain(table.rows) - .map(col.id) - .min() - .value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'max': { - const total = _.chain(table.rows) - .map(col.id) - .max() - .value(); - formattedColumn.formattedTotal = formatter.convert(total); - formattedColumn.total = total; - break; - } - case 'count': { - const total = table.rows.length; - formattedColumn.formattedTotal = total; - formattedColumn.total = total; - break; - } - default: - break; - } - } - - return formattedColumn; - }) - .filter(column => column); - - if (showPercentage) { - const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); - - // column to show percentage for was removed - if (insertAtIndex < 0) return; - - const { cols, rows } = addPercentageCol( - $scope.formattedColumns, - percentageCol, - table.rows, - insertAtIndex - ); - $scope.rows = rows; - $scope.formattedColumns = cols; - } - } - ); - }, - }; -} - -/** - * @param {[]Object} columns - the formatted columns that will be displayed - * @param {String} title - the title of the column to add to - * @param {[]Object} rows - the row data for the columns - * @param {Number} insertAtIndex - the index to insert the percentage column at - * @returns {Object} - cols and rows for the table to render now included percentage column(s) - */ -function addPercentageCol(columns, title, rows, insertAtIndex) { - const { id, sumTotal } = columns[insertAtIndex]; - const newId = `${id}-percents`; - const formatter = getFormatService().deserialize({ id: 'percent' }); - const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { - defaultMessage: '{title} percentages', - values: { title }, - }); - const newCols = insert(columns, insertAtIndex, { - title: i18nTitle, - id: newId, - formatter, - }); - const newRows = rows.map(row => ({ - [newId]: formatter.convert(row[id] / sumTotal / 100), - ...row, - })); - - return { cols: newCols, rows: newRows }; -} - -function insert(arr, index, ...items) { - const newArray = [...arr]; - newArray.splice(index + 1, 0, ...items); - return newArray; -} diff --git a/src/legacy/core_plugins/vis_type_table/public/index.scss b/src/legacy/core_plugins/vis_type_table/public/index.scss deleted file mode 100644 index 54124ebc42620..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/index.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "tbv" to avoid conflicts. -// Examples -// tbvChart -// tbvChart__legend -// tbvChart__legend--small -// tbvChart__legend-isLoading - -@import 'agg_table/index'; -@import 'paginated_table/index'; -@import './table_vis'; diff --git a/src/legacy/core_plugins/vis_type_table/public/index.ts b/src/legacy/core_plugins/vis_type_table/public/index.ts deleted file mode 100644 index efbaf69659ea2..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { PluginInitializerContext } from '../../../../core/public'; -import { TableVisPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy.ts b/src/legacy/core_plugins/vis_type_table/public/legacy.ts deleted file mode 100644 index 3d5f8c1b3efe9..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/legacy.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from './legacy_imports'; -import { plugin } from '.'; - -import { TablePluginSetupDependencies } from './plugin'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, { data: npStart.plugins.data }); diff --git a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts deleted file mode 100644 index 1030e971d6450..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/legacy_imports.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { npSetup, npStart } from 'ui/new_platform'; diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/_index.scss b/src/legacy/core_plugins/vis_type_table/public/paginated_table/_index.scss deleted file mode 100644 index 9473b847d3c2b..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/paginated_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './table_cell_filter'; diff --git a/src/legacy/core_plugins/vis_type_table/public/plugin.ts b/src/legacy/core_plugins/vis_type_table/public/plugin.ts deleted file mode 100644 index ea12a5320a14d..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/plugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; - -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; - -import { createTableVisFn } from './table_vis_fn'; -import { tableVisTypeDefinition } from './table_vis_type'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { setFormatService } from './services'; - -/** @internal */ -export interface TablePluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; -} - -/** @internal */ -export interface TablePluginStartDependencies { - data: DataPublicPluginStart; -} - -/** @internal */ -export class TableVisPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup( - core: CoreSetup, - { expressions, visualizations }: TablePluginSetupDependencies - ) { - expressions.registerFunction(createTableVisFn); - - visualizations.createBaseVisualization(tableVisTypeDefinition); - } - - public start(core: CoreStart, { data }: TablePluginStartDependencies) { - setFormatService(data.fieldFormats); - } -} diff --git a/src/legacy/core_plugins/vis_type_table/public/services.ts b/src/legacy/core_plugins/vis_type_table/public/services.ts deleted file mode 100644 index b4b491ac7a555..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/services.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('table data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts b/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts deleted file mode 100644 index 43816121bc23b..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_type.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; -import { Vis } from '../../../../plugins/visualizations/public'; -import { tableVisResponseHandler } from './table_vis_response_handler'; -// @ts-ignore -import tableVisTemplate from './table_vis.html'; -import { TableOptions } from './components/table_vis_options'; -import { TableVisualizationController } from './vis_controller'; - -export const tableVisTypeDefinition = { - type: 'table', - name: 'table', - title: i18n.translate('visTypeTable.tableVisTitle', { - defaultMessage: 'Data Table', - }), - icon: 'visTable', - description: i18n.translate('visTypeTable.tableVisDescription', { - defaultMessage: 'Display values in a table', - }), - visualization: TableVisualizationController, - visConfig: { - defaults: { - perPage: 10, - showPartialRows: false, - showMetricsAtAllLevels: false, - sort: { - columnIndex: null, - direction: null, - }, - showTotal: false, - totalFunc: 'sum', - percentageCol: '', - }, - template: tableVisTemplate, - }, - editorConfig: { - optionsTemplate: TableOptions, - schemas: new Schemas([ - { - group: AggGroupNames.Metrics, - name: 'metric', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { - defaultMessage: 'Metric', - }), - aggFilter: ['!geo_centroid', '!geo_bounds'], - aggSettings: { - top_hits: { - allowStrings: true, - }, - }, - min: 1, - defaults: [{ type: 'count', schema: 'metric' }], - }, - { - group: AggGroupNames.Buckets, - name: 'bucket', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { - defaultMessage: 'Split rows', - }), - aggFilter: ['!filter'], - }, - { - group: AggGroupNames.Buckets, - name: 'split', - title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { - defaultMessage: 'Split table', - }), - min: 0, - max: 1, - aggFilter: ['!filter'], - }, - ]), - }, - responseHandler: tableVisResponseHandler, - hierarchicalData: (vis: Vis) => { - return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); - }, -}; diff --git a/src/legacy/core_plugins/vis_type_table/public/types.ts b/src/legacy/core_plugins/vis_type_table/public/types.ts deleted file mode 100644 index c6de14b9f050c..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 { SchemaConfig } from '../../../../plugins/visualizations/public'; - -export enum AggTypes { - SUM = 'sum', - AVG = 'avg', - MIN = 'min', - MAX = 'max', - COUNT = 'count', -} - -export interface Dimensions { - buckets: SchemaConfig[]; - metrics: SchemaConfig[]; -} - -export interface TableVisParams { - type: 'table'; - perPage: number | ''; - showPartialRows: boolean; - showMetricsAtAllLevels: boolean; - sort: { - columnIndex: number | null; - direction: string | null; - }; - showTotal: boolean; - totalFunc: AggTypes; - percentageCol: string; - dimensions: Dimensions; -} diff --git a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts b/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts deleted file mode 100644 index 5bb730d2f9b10..0000000000000 --- a/src/legacy/core_plugins/vis_type_table/public/vis_controller.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; -import $ from 'jquery'; - -import { VisParams, ExprVis } from '../../../../plugins/visualizations/public'; -import { npStart } from './legacy_imports'; -import { getAngularModule } from './get_inner_angular'; -import { initTableVisLegacyModule } from './table_vis_legacy_module'; - -const innerAngularName = 'kibana/table_vis'; - -export class TableVisualizationController { - private tableVisModule: IModule | undefined; - private injector: auto.IInjectorService | undefined; - el: JQuery; - vis: ExprVis; - $rootScope: IRootScopeService | null = null; - $scope: (IScope & { [key: string]: any }) | undefined; - $compile: ICompileService | undefined; - - constructor(domeElement: Element, vis: ExprVis) { - this.el = $(domeElement); - this.vis = vis; - } - - getInjector() { - if (!this.injector) { - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); - this.injector = angular.bootstrap(mountpoint, [innerAngularName]); - this.el.append(mountpoint); - } - - return this.injector; - } - - initLocalAngular() { - if (!this.tableVisModule) { - this.tableVisModule = getAngularModule(innerAngularName, npStart.core); - initTableVisLegacyModule(this.tableVisModule); - } - } - - async render(esResponse: object, visParams: VisParams) { - this.initLocalAngular(); - - return new Promise(async (resolve, reject) => { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - const updateScope = () => { - if (!this.$scope) { - return; - } - this.$scope.vis = this.vis; - this.$scope.visState = { params: visParams }; - this.$scope.esResponse = esResponse; - - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = this.vis.getUiState(); - updateScope(); - this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); - } - }); - } - - destroy() { - if (this.$rootScope) { - this.$rootScope.$destroy(); - this.$rootScope = null; - } - } -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/index.ts b/src/legacy/core_plugins/vis_type_tagcloud/index.ts deleted file mode 100644 index 6f768131f2190..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const tagCloudPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'tagcloud', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default tagCloudPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/package.json b/src/legacy/core_plugins/vis_type_tagcloud/package.json deleted file mode 100644 index 4200ef264fece..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "tagcloud", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss b/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss deleted file mode 100644 index a4fcf8418ce1c..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "tgc" to avoid conflicts. -// Examples -// tgcChart -// tgcChart__legend -// tgcChart__legend--small -// tgcChart__legend-isLoading - -@import './tag_cloud'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts deleted file mode 100644 index 90e6305262caa..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { PluginInitializerContext } from '../../../../core/public'; -import { TagCloudPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts deleted file mode 100644 index f70789edc66ba..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { TagCloudPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; - -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, { data: npStart.plugins.data }); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts deleted file mode 100644 index 1061271aa315b..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; - -import { createTagCloudFn } from './tag_cloud_fn'; -import { createTagCloudVisTypeDefinition } from './tag_cloud_type'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { setFormatService } from './services'; - -/** @internal */ -export interface TagCloudPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - charts: ChartsPluginSetup; -} - -/** @internal */ -export interface TagCloudVisDependencies { - colors: ChartsPluginSetup['colors']; -} - -/** @internal */ -export interface TagCloudVisPluginStartDependencies { - data: DataPublicPluginStart; -} - -/** @internal */ -export class TagCloudPlugin implements Plugin { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public setup( - core: CoreSetup, - { expressions, visualizations, charts }: TagCloudPluginSetupDependencies - ) { - const visualizationDependencies: TagCloudVisDependencies = { - colors: charts.colors, - }; - expressions.registerFunction(createTagCloudFn); - visualizations.createBaseVisualization( - createTagCloudVisTypeDefinition(visualizationDependencies) - ); - } - - public start(core: CoreStart, { data }: TagCloudVisPluginStartDependencies) { - setFormatService(data.fieldFormats); - } -} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts b/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts deleted file mode 100644 index 272bed3e91a08..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('data.fieldFormats'); - -export { npStart } from 'ui/new_platform'; diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts deleted file mode 100644 index ac7e407ca9e4d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const vegaPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_type_vega', - deprecations: ({ rename }: { rename: any }) => [ - rename('vega.enabled', 'vis_type_vega.enabled'), - ], - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - enableExternalUrls: Joi.boolean().default(false), - }).default(); - }, - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => { - const serverConfig = server.config(); - const mapConfig: Record = serverConfig.get('map'); - - return { - emsTileLayerId: mapConfig.emsTileLayerId, - }; - }, - }, - init: (server: Legacy.Server) => ({}), - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default vegaPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_vega/package.json b/src/legacy/core_plugins/vis_type_vega/package.json deleted file mode 100644 index acd6f3da128ab..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "author": "Yuri Astrakhan", - "name": "vega", - "version": "kibana" -} - diff --git a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts b/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts deleted file mode 100644 index b2f3e5b2241e6..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../plugins/data/public'; -import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; -import { coreMock } from '../../../../../core/public/mocks'; - -export const [getData, setData] = createGetterSetter('Data'); -setData(dataPluginMock.createStartContract()); - -export const [getNotifications, setNotifications] = createGetterSetter( - 'Notifications' -); -setNotifications(coreMock.createStart().notifications); - -export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); -setUISettings(coreMock.createStart().uiSettings); - -export const [getSavedObjects, setSavedObjects] = createGetterSetter( - 'SavedObjects' -); -setSavedObjects(coreMock.createStart().savedObjects); - -export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ - esShardTimeout: number; - enableExternalUrls: boolean; - emsTileLayerId: unknown; -}>('InjectedVars'); -setInjectedVars({ - emsTileLayerId: {}, - enableExternalUrls: true, - esShardTimeout: 10000, -}); - -export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; -export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; -export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js deleted file mode 100644 index 6412d8a569b2a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ /dev/null @@ -1,309 +0,0 @@ -/* - * 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 Bluebird from 'bluebird'; -import expect from '@kbn/expect'; -import ngMock from 'ng_mock'; -import $ from 'jquery'; -import { createVegaVisualization } from '../vega_visualization'; -import { ImageComparator } from 'test_utils/image_comparator'; - -import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson'; -import vegaliteImage256 from './vegalite_image_256.png'; -import vegaliteImage512 from './vegalite_image_512.png'; - -import vegaGraph from '!!raw-loader!./vega_graph.hjson'; -import vegaImage512 from './vega_image_512.png'; - -import vegaTooltipGraph from '!!raw-loader!./vega_tooltip_test.hjson'; - -import vegaMapGraph from '!!raw-loader!./vega_map_test.hjson'; -import vegaMapImage256 from './vega_map_image_256.png'; - -import { VegaParser } from '../data_model/vega_parser'; -import { SearchCache } from '../data_model/search_cache'; - -import { createVegaTypeDefinition } from '../vega_type'; -// TODO This is an integration test and thus requires a running platform. When moving to the new platform, -// this test has to be migrated to the newly created integration test environment. -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { npStart } from 'ui/new_platform'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_types/base_vis_type'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; -import { setInjectedVars } from '../services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; - -const THRESHOLD = 0.1; -const PIXEL_DIFF = 30; - -describe('VegaVisualizations', () => { - let domNode; - let VegaVisualization; - let vis; - let imageComparator; - let vegaVisualizationDependencies; - let vegaVisType; - - setInjectedVars({ - emsTileLayerId: {}, - enableExternalUrls: true, - esShardTimeout: 10000, - }); - - beforeEach(ngMock.module('kibana')); - beforeEach( - ngMock.inject(() => { - setInjectedVarFunc(injectedVar => { - switch (injectedVar) { - case 'mapConfig': - return { - emsFileApiUrl: '', - emsTileApiUrl: '', - emsLandingPageUrl: '', - }; - case 'tilemapsConfig': - return { - deprecated: { - config: { - options: { - attribution: '123', - }, - }, - }, - }; - case 'version': - return '123'; - default: - return 'not found'; - } - }); - const serviceSettings = new ServiceSettings(); - vegaVisualizationDependencies = { - serviceSettings, - core: { - uiSettings: npStart.core.uiSettings, - }, - plugins: { - data: { - query: { - timefilter: { - timefilter: {}, - }, - }, - __LEGACY: { - esClient: npStart.plugins.data.search.__LEGACY.esClient, - }, - }, - }, - }; - - vegaVisType = new BaseVisType(createVegaTypeDefinition(vegaVisualizationDependencies)); - VegaVisualization = createVegaVisualization(vegaVisualizationDependencies); - }) - ); - - describe('VegaVisualization - basics', () => { - beforeEach(async function() { - setupDOM('512px', '512px'); - imageComparator = new ImageComparator(); - - vis = new ExprVis({ - type: vegaVisType, - }); - }); - - afterEach(function() { - teardownDOM(); - imageComparator.destroy(); - }); - - it('should show vegalite graph and update on resize (may fail in dev env)', async function() { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - - const vegaParser = new VegaParser(vegaliteGraph, new SearchCache()); - await vegaParser.parseAsync(); - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const mismatchedPixels1 = await compareImage(vegaliteImage512); - expect(mismatchedPixels1).to.be.lessThan(PIXEL_DIFF); - - domNode.style.width = '256px'; - domNode.style.height = '256px'; - - await vegaVis.render(vegaParser, vis.params, { resize: true }); - const mismatchedPixels2 = await compareImage(vegaliteImage256); - expect(mismatchedPixels2).to.be.lessThan(PIXEL_DIFF); - } finally { - vegaVis.destroy(); - } - }); - - it('should show vega graph (may fail in dev env)', async function() { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser(vegaGraph, new SearchCache()); - await vegaParser.parseAsync(); - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const mismatchedPixels = await compareImage(vegaImage512); - - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - } finally { - vegaVis.destroy(); - } - }); - - it('should show vegatooltip on mouseover over a vega graph (may fail in dev env)', async () => { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser(vegaTooltipGraph, new SearchCache()); - await vegaParser.parseAsync(); - await vegaVis.render(vegaParser, vis.params, { data: true }); - - const $el = $(domNode); - const offset = $el.offset(); - - const event = new MouseEvent('mousemove', { - view: window, - bubbles: true, - cancelable: true, - clientX: offset.left + 10, - clientY: offset.top + 10, - }); - - $el.find('canvas')[0].dispatchEvent(event); - - await Bluebird.delay(10); - - let tooltip = document.getElementById('vega-kibana-tooltip'); - expect(tooltip).to.be.ok(); - expect(tooltip.innerHTML).to.be( - '

This is a long title

' + - '' + - '' + - '' + - '
fieldA:value of fld1
fld2:42
' - ); - - vegaVis.destroy(); - - tooltip = document.getElementById('vega-kibana-tooltip'); - expect(tooltip).to.not.be.ok(); - } finally { - vegaVis.destroy(); - } - }); - - it('should show vega blank rectangle on top of a map (vegamap)', async () => { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser(vegaMapGraph, new SearchCache()); - await vegaParser.parseAsync(); - - domNode.style.width = '256px'; - domNode.style.height = '256px'; - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const mismatchedPixels = await compareImage(vegaMapImage256); - expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF); - } finally { - vegaVis.destroy(); - } - }); - - it('should add a small subpixel value to the height of the canvas to avoid getting it set to 0', async () => { - let vegaVis; - try { - vegaVis = new VegaVisualization(domNode, vis); - const vegaParser = new VegaParser( - `{ - "$schema": "https://vega.github.io/schema/vega/v3.json", - "marks": [ - { - "type": "text", - "encode": { - "update": { - "text": { - "value": "Test" - }, - "align": {"value": "center"}, - "baseline": {"value": "middle"}, - "xc": {"signal": "width/2"}, - "yc": {"signal": "height/2"} - fontSize: {value: "14"} - } - } - } - ] - }`, - new SearchCache() - ); - await vegaParser.parseAsync(); - - domNode.style.width = '256px'; - domNode.style.height = '256px'; - - await vegaVis.render(vegaParser, vis.params, { data: true }); - const vegaView = vegaVis._vegaView._view; - expect(vegaView.height()).to.be(250.00000001); - - vegaView.height(250); - await vegaView.runAsync(); - // as soon as this test fails, the workaround with the subpixel value can be removed. - expect(vegaView.height()).to.be(0); - } finally { - vegaVis.destroy(); - } - }); - }); - - async function compareImage(expectedImageSource) { - const elementList = domNode.querySelectorAll('canvas'); - expect(elementList.length).to.equal(1); - const firstCanvasOnMap = elementList[0]; - return imageComparator.compareImage(firstCanvasOnMap, expectedImageSource, THRESHOLD); - } - - function setupDOM(width, height) { - domNode = document.createElement('div'); - domNode.style.top = '0'; - domNode.style.left = '0'; - domNode.style.width = width; - domNode.style.height = height; - domNode.style.position = 'fixed'; - domNode.style.border = '1px solid blue'; - domNode.style['pointer-events'] = 'none'; - document.body.appendChild(domNode); - } - - function teardownDOM() { - domNode.innerHTML = ''; - document.body.removeChild(domNode); - } -}); diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.scss b/src/legacy/core_plugins/vis_type_vega/public/index.scss deleted file mode 100644 index 1ab2119d481a0..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/index.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - -// Prefix all styles with "vga" to avoid conflicts. -// Examples -// vgaChart -// vgaChart__legend -// vgaChart__legend--small -// vgaChart__legend-isLoading - -@import './vega_vis'; -@import './vega_editor'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.ts b/src/legacy/core_plugins/vis_type_vega/public/index.ts deleted file mode 100644 index 34ca0e72190e4..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { PluginInitializerContext } from '../../../../core/public'; -import { VegaPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts deleted file mode 100644 index 450af4a6f253e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { VegaPluginSetupDependencies, VegaPluginStartDependencies } from './plugin'; -import { plugin } from '.'; - -const setupPlugins: Readonly = { - ...npSetup.plugins, - visualizations: npSetup.plugins.visualizations, - mapsLegacy: npSetup.plugins.mapsLegacy, -}; - -const startPlugins: Readonly = { - ...npStart.plugins, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/legacy/core_plugins/vis_type_vega/public/plugin.ts deleted file mode 100644 index 9fa77d28fbbfa..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; -import { - setNotifications, - setData, - setSavedObjects, - setInjectedVars, - setUISettings, -} from './services'; - -import { createVegaFn } from './vega_fn'; -import { createVegaTypeDefinition } from './vega_type'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; -import { IServiceSettings } from '../../../../plugins/maps_legacy/public'; - -/** @internal */ -export interface VegaVisualizationDependencies { - core: CoreSetup; - plugins: { - data: ReturnType; - }; - serviceSettings: IServiceSettings; -} - -/** @internal */ -export interface VegaPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - data: ReturnType; - visTypeVega: VisTypeVegaSetup; - mapsLegacy: any; -} - -/** @internal */ -export interface VegaPluginStartDependencies { - data: ReturnType; -} - -/** @internal */ -export class VegaPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; - - constructor(initializerContext: PluginInitializerContext) { - this.initializerContext = initializerContext; - } - - public async setup( - core: CoreSetup, - { data, expressions, visualizations, visTypeVega, mapsLegacy }: VegaPluginSetupDependencies - ) { - setInjectedVars({ - enableExternalUrls: visTypeVega.config.enableExternalUrls, - esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, - emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), - }); - setUISettings(core.uiSettings); - - const visualizationDependencies: Readonly = { - core, - plugins: { - data, - }, - serviceSettings: mapsLegacy.serviceSettings, - }; - - expressions.registerFunction(() => createVegaFn(visualizationDependencies)); - - visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); - } - - public start(core: CoreStart, { data }: VegaPluginStartDependencies) { - setNotifications(core.notifications); - setSavedObjects(core.savedObjects); - setData(data); - } -} diff --git a/src/legacy/core_plugins/vis_type_vega/public/services.ts b/src/legacy/core_plugins/vis_type_vega/public/services.ts deleted file mode 100644 index 88e0e0098bf8c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/services.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { SavedObjectsStart } from 'kibana/public'; -import { NotificationsStart } from 'src/core/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { IUiSettingsClient } from '../../../../core/public'; - -export const [getData, setData] = createGetterSetter('Data'); - -export const [getNotifications, setNotifications] = createGetterSetter( - 'Notifications' -); - -export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); - -export const [getSavedObjects, setSavedObjects] = createGetterSetter( - 'SavedObjects' -); - -export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ - esShardTimeout: number; - enableExternalUrls: boolean; - emsTileLayerId: unknown; -}>('InjectedVars'); - -export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; -export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; -export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js b/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js deleted file mode 100644 index a6e911de7f0cb..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { VegaView } from './vega_view/vega_view'; -import { VegaMapView } from './vega_view/vega_map_view'; -import { getNotifications, getData, getSavedObjects } from './services'; - -export const createVegaVisualization = ({ serviceSettings }) => - class VegaVisualization { - constructor(el, vis) { - this._el = el; - this._vis = vis; - - this.savedObjectsClient = getSavedObjects(); - this.dataPlugin = getData(); - } - - /** - * Find index pattern by its title, of if not given, gets default - * @param {string} [index] - * @returns {Promise} index id - */ - async findIndex(index) { - const { indexPatterns } = this.dataPlugin; - let idxObj; - - if (index) { - idxObj = indexPatterns.findByTitle(this.savedObjectsClient, index); - if (!idxObj) { - throw new Error( - i18n.translate('visTypeVega.visualization.indexNotFoundErrorMessage', { - defaultMessage: 'Index {index} not found', - values: { index: `"${index}"` }, - }) - ); - } - } else { - idxObj = await indexPatterns.getDefault(); - if (!idxObj) { - throw new Error( - i18n.translate('visTypeVega.visualization.unableToFindDefaultIndexErrorMessage', { - defaultMessage: 'Unable to find default index', - }) - ); - } - } - return idxObj.id; - } - - /** - * - * @param {VegaParser} visData - * @param {*} status - * @returns {Promise} - */ - async render(visData) { - const { toasts } = getNotifications(); - - if (!visData && !this._vegaView) { - toasts.addWarning( - i18n.translate('visTypeVega.visualization.unableToRenderWithoutDataWarningMessage', { - defaultMessage: 'Unable to render without data', - }) - ); - return; - } - - try { - await this._render(visData); - } catch (error) { - if (this._vegaView) { - this._vegaView.onError(error); - } else { - toasts.addError(error, { - title: i18n.translate('visTypeVega.visualization.renderErrorTitle', { - defaultMessage: 'Vega error', - }), - }); - } - } - } - - async _render(vegaParser) { - if (vegaParser) { - // New data received, rebuild the graph - if (this._vegaView) { - await this._vegaView.destroy(); - this._vegaView = null; - } - - const { filterManager } = this.dataPlugin.query; - const { timefilter } = this.dataPlugin.query.timefilter; - const vegaViewParams = { - parentEl: this._el, - vegaParser, - serviceSettings, - filterManager, - timefilter, - findIndex: this.findIndex.bind(this), - }; - - if (vegaParser.useMap) { - const services = { toastService: getNotifications().toasts }; - this._vegaView = new VegaMapView(vegaViewParams, services); - } else { - this._vegaView = new VegaView(vegaViewParams); - } - await this._vegaView.init(); - } - } - - destroy() { - return this._vegaView && this._vegaView.destroy(); - } - }; diff --git a/src/legacy/core_plugins/vis_type_vislib/index.ts b/src/legacy/core_plugins/vis_type_vislib/index.ts deleted file mode 100644 index da9476285a9b2..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../types'; - -const visTypeVislibPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_type_vislib', - require: ['kibana', 'elasticsearch', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - uiExports: { - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default visTypeVislibPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_vislib/package.json b/src/legacy/core_plugins/vis_type_vislib/package.json deleted file mode 100644 index e30a9e2b35834..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "vis_type_vislib", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx deleted file mode 100644 index 3fca9dc8adc08..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 React from 'react'; -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { SwitchOption, TextInputOption } from '../../../../../../../plugins/charts/public'; -import { GaugeOptionsInternalProps } from '.'; - -function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInternalProps) { - return ( - - -

- -

-
- - - - setGaugeValue('labels', { ...stateParams.gauge.labels, [paramName]: value }) - } - /> - - - - setGaugeValue('style', { ...stateParams.gauge.style, [paramName]: value }) - } - /> - - -
- ); -} - -export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx deleted file mode 100644 index dc207ad89286f..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/index.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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 React, { useCallback, useEffect, useState } from 'react'; - -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { - BasicOptions, - ColorRanges, - ColorSchemaOptions, - NumberInputOption, - SelectOption, - SwitchOption, - SetColorSchemaOptionsValue, - SetColorRangeValue, -} from '../../../../../../../plugins/charts/public'; -import { HeatmapVisParams } from '../../../heatmap'; -import { ValueAxis } from '../../../types'; -import { LabelsPanel } from './labels_panel'; - -function HeatmapOptions(props: VisOptionsProps) { - const { stateParams, vis, uiState, setValue, setValidity, setTouched } = props; - const [valueAxis] = stateParams.valueAxes; - const isColorsNumberInvalid = stateParams.colorsNumber < 2 || stateParams.colorsNumber > 10; - const [isColorRangesValid, setIsColorRangesValid] = useState(false); - - const setValueAxisScale = useCallback( - (paramName: T, value: ValueAxis['scale'][T]) => - setValue('valueAxes', [ - { - ...valueAxis, - scale: { - ...valueAxis.scale, - [paramName]: value, - }, - }, - ]), - [valueAxis, setValue] - ); - - useEffect(() => { - setValidity(stateParams.setColorRange ? isColorRangesValid : !isColorsNumberInvalid); - }, [stateParams.setColorRange, isColorRangesValid, isColorsNumberInvalid, setValidity]); - - return ( - <> - - -

- -

-
- - - - - -
- - - - - -

- -

-
- - - - - - - - - - - - - - - - - {stateParams.setColorRange && ( - - )} -
- - - - - - ); -} - -export { HeatmapOptions }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx deleted file mode 100644 index 3d1629740df2c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 React, { useCallback } from 'react'; - -import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValueAxis } from '../../../types'; -import { HeatmapVisParams } from '../../../heatmap'; -import { SwitchOption } from '../../../../../../../plugins/charts/public'; - -const VERTICAL_ROTATION = 270; - -interface LabelsPanelProps { - valueAxis: ValueAxis; - setValue: VisOptionsProps['setValue']; -} - -function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) { - const rotateLabels = valueAxis.labels.rotate === VERTICAL_ROTATION; - - const setValueAxisLabels = useCallback( - (paramName: T, value: ValueAxis['labels'][T]) => - setValue('valueAxes', [ - { - ...valueAxis, - labels: { - ...valueAxis.labels, - [paramName]: value, - }, - }, - ]), - [valueAxis, setValue] - ); - - const setRotateLabels = useCallback( - (paramName: 'rotate', value: boolean) => - setValueAxisLabels(paramName, value ? VERTICAL_ROTATION : 0), - [setValueAxisLabels] - ); - - const setColor = useCallback(value => setValueAxisLabels('color', value), [setValueAxisLabels]); - - return ( - - -

- -

-
- - - - - - - - - - - -
- ); -} - -export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx deleted file mode 100644 index 89aab3a19c589..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * 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 React, { useMemo, useCallback } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; - -import { Vis } from '../../../../../../../plugins/visualizations/public'; -import { SeriesParam, ValueAxis } from '../../../types'; -import { ChartTypes } from '../../../utils/collections'; -import { SelectOption } from '../../../../../../../plugins/charts/public'; -import { LineOptions } from './line_options'; -import { SetParamByIndex, ChangeValueAxis } from './'; - -export type SetChart = (paramName: T, value: SeriesParam[T]) => void; - -export interface ChartOptionsParams { - chart: SeriesParam; - index: number; - changeValueAxis: ChangeValueAxis; - setParamByIndex: SetParamByIndex; - valueAxes: ValueAxis[]; - vis: Vis; -} - -function ChartOptions({ - chart, - index, - valueAxes, - vis, - changeValueAxis, - setParamByIndex, -}: ChartOptionsParams) { - const setChart: SetChart = useCallback( - (paramName, value) => { - setParamByIndex('seriesParams', index, paramName, value); - }, - [setParamByIndex, index] - ); - - const setValueAxis = useCallback( - (paramName, value) => { - changeValueAxis(index, paramName, value); - }, - [changeValueAxis, index] - ); - - const valueAxesOptions = useMemo( - () => [ - ...valueAxes.map(({ id, name }: ValueAxis) => ({ - text: name, - value: id, - })), - { - text: i18n.translate('visTypeVislib.controls.pointSeries.series.newAxisLabel', { - defaultMessage: 'New axis…', - }), - value: 'new', - }, - ], - [valueAxes] - ); - - return ( - <> - - - - - - - - - - - - - - {chart.type === ChartTypes.AREA && ( - <> - - - - - )} - - {chart.type === ChartTypes.LINE && ( - - )} - - ); -} - -export { ChartOptions }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts b/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts deleted file mode 100644 index a296281375dac..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { Vis } from '../../../../../../../plugins/visualizations/public'; -import { Axis, ValueAxis, SeriesParam } from '../../../types'; -import { - ChartTypes, - ChartModes, - InterpolationModes, - ScaleTypes, - Positions, - AxisTypes, - getScaleTypes, - getAxisModes, - getPositions, - getInterpolationModes, -} from '../../../utils/collections'; -import { Style } from '../../../../../../../plugins/charts/public'; - -const defaultValueAxisId = 'ValueAxis-1'; - -const axis = { - show: true, - style: {} as Style, - title: { - text: '', - }, - labels: { - show: true, - filter: true, - truncate: 0, - color: 'black', - }, -}; - -const categoryAxis: Axis = { - ...axis, - id: 'CategoryAxis-1', - type: AxisTypes.CATEGORY, - position: Positions.BOTTOM, - scale: { - type: ScaleTypes.LINEAR, - }, -}; - -const valueAxis: ValueAxis = { - ...axis, - id: defaultValueAxisId, - name: 'ValueAxis-1', - type: AxisTypes.VALUE, - position: Positions.LEFT, - scale: { - type: ScaleTypes.LINEAR, - boundsMargin: 1, - defaultYExtents: true, - min: 1, - max: 2, - setYExtents: true, - }, -}; - -const seriesParam: SeriesParam = { - show: true, - type: ChartTypes.HISTOGRAM, - mode: ChartModes.STACKED, - data: { - label: 'Count', - id: '1', - }, - drawLinesBetweenPoints: true, - lineWidth: 2, - showCircles: true, - interpolate: InterpolationModes.LINEAR, - valueAxis: defaultValueAxisId, -}; - -const positions = getPositions(); -const axisModes = getAxisModes(); -const scaleTypes = getScaleTypes(); -const interpolationModes = getInterpolationModes(); - -const vis = ({ - type: { - editorConfig: { - collections: { scaleTypes, axisModes, positions, interpolationModes }, - }, - }, -} as any) as Vis; - -export { defaultValueAxisId, categoryAxis, valueAxis, seriesParam, vis }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/index.ts b/src/legacy/core_plugins/vis_type_vislib/public/index.ts deleted file mode 100644 index 4d7091ffb204b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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 { PluginInitializerContext } from '../../../../core/public'; -import { VisTypeVislibPlugin as Plugin } from './plugin'; - -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} - -export * from './types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts deleted file mode 100644 index 579caa1cb88f6..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { npSetup, npStart } from 'ui/new_platform'; -import { PluginInitializerContext } from 'kibana/public'; - -import { plugin } from '.'; -import { - VisTypeVislibPluginSetupDependencies, - VisTypeVislibPluginStartDependencies, -} from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, - visTypeXy: npSetup.plugins.visTypeXy, -}; - -const startPlugins: Readonly = { - data: npStart.plugins.data, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts b/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts deleted file mode 100644 index c04ffa506eb04..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/legacy_imports.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { search } from '../../../../plugins/data/public'; -export const { tabifyAggResponse, tabifyGetColumns } = search; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts deleted file mode 100644 index ef3f664252856..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 { - CoreSetup, - CoreStart, - Plugin, - IUiSettingsClient, - PluginInitializerContext, -} from 'kibana/public'; - -import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; -import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; -import { createPieVisFn } from './pie_fn'; -import { - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, -} from './vis_type_vislib_vis_types'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { setFormatService, setDataActions } from './services'; - -export interface VisTypeVislibDependencies { - uiSettings: IUiSettingsClient; - charts: ChartsPluginSetup; -} - -/** @internal */ -export interface VisTypeVislibPluginSetupDependencies { - expressions: ReturnType; - visualizations: VisualizationsSetup; - charts: ChartsPluginSetup; - visTypeXy?: VisTypeXyPluginSetup; -} - -/** @internal */ -export interface VisTypeVislibPluginStartDependencies { - data: DataPublicPluginStart; -} - -type VisTypeVislibCoreSetup = CoreSetup; - -/** @internal */ -export class VisTypeVislibPlugin implements Plugin { - constructor(public initializerContext: PluginInitializerContext) {} - - public async setup( - core: VisTypeVislibCoreSetup, - { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies - ) { - const visualizationDependencies: Readonly = { - uiSettings: core.uiSettings, - charts, - }; - const vislibTypes = [ - createHistogramVisTypeDefinition, - createLineVisTypeDefinition, - createPieVisTypeDefinition, - createAreaVisTypeDefinition, - createHeatmapVisTypeDefinition, - createHorizontalBarVisTypeDefinition, - createGaugeVisTypeDefinition, - createGoalVisTypeDefinition, - ]; - const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; - - // if visTypeXy plugin is disabled it's config will be undefined - if (!visTypeXy) { - const convertedTypes: any[] = []; - const convertedFns: any[] = []; - - // Register legacy vislib types that have been converted - convertedFns.forEach(expressions.registerFunction); - convertedTypes.forEach(vis => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); - } - - // Register non-converted types - vislibFns.forEach(expressions.registerFunction); - vislibTypes.forEach(vis => - visualizations.createBaseVisualization(vis(visualizationDependencies)) - ); - } - - public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { - setFormatService(data.fieldFormats); - setDataActions({ createFiltersFromEvent: data.actions.createFiltersFromEvent }); - } -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/services.ts b/src/legacy/core_plugins/vis_type_vislib/public/services.ts deleted file mode 100644 index 0d6b1b5e8de58..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/services.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; - -export const [getDataActions, setDataActions] = createGetterSetter< - DataPublicPluginStart['actions'] ->('vislib data.actions'); - -export const [getFormatService, setFormatService] = createGetterSetter< - DataPublicPluginStart['fieldFormats'] ->('vislib data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/types.ts b/src/legacy/core_plugins/vis_type_vislib/public/types.ts deleted file mode 100644 index 25c6ae5439fe8..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/types.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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 { TimeMarker } from './vislib/visualizations/time_marker'; -import { - Positions, - ChartModes, - ChartTypes, - AxisModes, - AxisTypes, - InterpolationModes, - ScaleTypes, - ThresholdLineStyles, -} from './utils/collections'; -import { Labels, Style } from '../../../../plugins/charts/public'; - -export interface CommonVislibParams { - addTooltip: boolean; - legendPosition: Positions; -} - -export interface Scale { - boundsMargin?: number | ''; - defaultYExtents?: boolean; - max?: number | null; - min?: number | null; - mode?: AxisModes; - setYExtents?: boolean; - type: ScaleTypes; -} - -interface ThresholdLine { - show: boolean; - value: number | null; - width: number | null; - style: ThresholdLineStyles; - color: string; -} - -export interface Axis { - id: string; - labels: Labels; - position: Positions; - scale: Scale; - show: boolean; - style: Style; - title: { text: string }; - type: AxisTypes; -} - -export interface ValueAxis extends Axis { - name: string; -} - -export interface SeriesParam { - data: { label: string; id: string }; - drawLinesBetweenPoints: boolean; - interpolate: InterpolationModes; - lineWidth?: number; - mode: ChartModes; - show: boolean; - showCircles: boolean; - type: ChartTypes; - valueAxis: string; -} - -export interface BasicVislibParams extends CommonVislibParams { - addTimeMarker: boolean; - categoryAxes: Axis[]; - orderBucketsBySum?: boolean; - labels: Labels; - thresholdLine: ThresholdLine; - valueAxes: ValueAxis[]; - grid: { - categoryLines: boolean; - valueAxis?: string; - }; - seriesParams: SeriesParam[]; - times: TimeMarker[]; -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx deleted file mode 100644 index ec091e5d29cfd..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vis_controller.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 $ from 'jquery'; -import React, { RefObject } from 'react'; - -// @ts-ignore -import { Vis as Vislib } from './vislib/vis'; -import { Positions } from './utils/collections'; -import { VisTypeVislibDependencies } from './plugin'; -import { mountReactNode } from '../../../../core/public/utils'; -import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; -import { VisParams, ExprVis } from '../../../../plugins/visualizations/public'; - -const legendClassName = { - top: 'visLib--legend-top', - bottom: 'visLib--legend-bottom', - left: 'visLib--legend-left', - right: 'visLib--legend-right', -}; - -export const createVislibVisController = (deps: VisTypeVislibDependencies) => { - return class VislibVisController { - unmount: (() => void) | null = null; - visParams?: VisParams; - legendRef: RefObject; - container: HTMLDivElement; - chartEl: HTMLDivElement; - legendEl: HTMLDivElement; - vislibVis: any; - - constructor(public el: Element, public vis: ExprVis) { - this.el = el; - this.vis = vis; - this.unmount = null; - this.legendRef = React.createRef(); - - // vis mount point - this.container = document.createElement('div'); - this.container.className = 'visLib'; - this.el.appendChild(this.container); - - // chart mount point - this.chartEl = document.createElement('div'); - this.chartEl.className = 'visLib__chart'; - this.container.appendChild(this.chartEl); - - // legend mount point - this.legendEl = document.createElement('div'); - this.legendEl.className = 'visLib__legend'; - this.container.appendChild(this.legendEl); - } - - render(esResponse: any, visParams: VisParams) { - if (this.vislibVis) { - this.destroy(); - } - - return new Promise(async resolve => { - if (this.el.clientWidth === 0 || this.el.clientHeight === 0) { - return resolve(); - } - - this.vislibVis = new Vislib(this.chartEl, visParams, deps); - this.vislibVis.on('brush', this.vis.API.events.brush); - this.vislibVis.on('click', this.vis.API.events.filter); - this.vislibVis.on('renderComplete', resolve); - - this.vislibVis.initVisConfig(esResponse, this.vis.getUiState()); - - if (visParams.addLegend) { - $(this.container) - .attr('class', (i, cls) => { - return cls.replace(/visLib--legend-\S+/g, ''); - }) - .addClass((legendClassName as any)[visParams.legendPosition]); - - this.mountLegend(esResponse, visParams.legendPosition); - } - - this.vislibVis.render(esResponse, this.vis.getUiState()); - - // refreshing the legend after the chart is rendered. - // this is necessary because some visualizations - // provide data necessary for the legend only after a render cycle. - if ( - visParams.addLegend && - CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type) - ) { - this.unmountLegend(); - this.mountLegend(esResponse, visParams.legendPosition); - this.vislibVis.render(esResponse, this.vis.getUiState()); - } - }); - } - - mountLegend(visData: any, position: Positions) { - this.unmount = mountReactNode( - - )(this.legendEl); - } - - unmountLegend() { - if (this.unmount) { - this.unmount(); - } - } - - destroy() { - if (this.unmount) { - this.unmount(); - } - - if (this.vislibVis) { - this.vislibVis.off('brush', this.vis.API.events.brush); - this.vislibVis.off('click', this.vis.API.events.filter); - this.vislibVis.destroy(); - delete this.vislibVis; - } - } - }; -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js deleted file mode 100644 index a3aabcb90be62..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/_tooltip_formatter.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 _ from 'lodash'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import { pointSeriesTooltipFormatter } from '../../components/tooltip'; - -describe('tooltipFormatter', function() { - const tooltipFormatter = pointSeriesTooltipFormatter(); - - function cell($row, i) { - return $row - .eq(i) - .text() - .trim(); - } - - const baseEvent = { - data: { - xAxisLabel: 'inner', - xAxisFormatter: _.identity, - yAxisLabel: 'middle', - yAxisFormatter: _.identity, - zAxisLabel: 'top', - zAxisFormatter: _.identity, - series: [ - { - rawId: '1', - label: 'middle', - zLabel: 'top', - yAxisFormatter: _.identity, - zAxisFormatter: _.identity, - }, - ], - }, - datum: { - x: 3, - y: 2, - z: 1, - extraMetrics: [], - seriesId: '1', - }, - }; - - it('returns html based on the mouse event', function() { - const event = _.cloneDeep(baseEvent); - const $el = $(tooltipFormatter(event)); - const $rows = $el.find('tr'); - expect($rows.length).to.be(3); - - const $row1 = $rows.eq(0).find('td'); - expect(cell($row1, 0)).to.be('inner'); - expect(cell($row1, 1)).to.be('3'); - - const $row2 = $rows.eq(1).find('td'); - expect(cell($row2, 0)).to.be('middle'); - expect(cell($row2, 1)).to.be('2'); - - const $row3 = $rows.eq(2).find('td'); - expect(cell($row3, 0)).to.be('top'); - expect(cell($row3, 1)).to.be('1'); - }); - - it('renders correctly on missing extraMetrics in datum', function() { - const event = _.cloneDeep(baseEvent); - delete event.datum.extraMetrics; - const $el = $(tooltipFormatter(event)); - const $rows = $el.find('tr'); - expect($rows.length).to.be(3); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js deleted file mode 100644 index db99b881a6e38..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/labels.js +++ /dev/null @@ -1,470 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; - -import { labels } from '../../components/labels/labels'; -import { dataArray } from '../../components/labels/data_array'; -import { uniqLabels } from '../../components/labels/uniq_labels'; -import { flattenSeries as getSeries } from '../../components/labels/flatten_series'; - -let seriesLabels; -let rowsLabels; -let seriesArr; -let rowsArr; - -const seriesData = { - label: '', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], -}; - -const rowsData = { - rows: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -const columnsData = { - columns: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -describe('Vislib Labels Module Test Suite', function() { - let uniqSeriesLabels; - describe('Labels (main)', function() { - beforeEach(() => { - seriesLabels = labels(seriesData); - rowsLabels = labels(rowsData); - seriesArr = Array.isArray(seriesLabels); - rowsArr = Array.isArray(rowsLabels); - uniqSeriesLabels = _.chain(rowsData.rows) - .pluck('series') - .flattenDeep() - .pluck('label') - .uniq() - .value(); - }); - - it('should be a function', function() { - expect(typeof labels).to.be('function'); - }); - - it('should return an array if input is data.series', function() { - expect(seriesArr).to.be(true); - }); - - it('should return an array if input is data.rows', function() { - expect(rowsArr).to.be(true); - }); - - it('should throw an error if input is not an object', function() { - expect(function() { - labels('string not object'); - }).to.throwError(); - }); - - it('should return unique label values', function() { - expect(rowsLabels[0]).to.equal(uniqSeriesLabels[0]); - expect(rowsLabels[1]).to.equal(uniqSeriesLabels[1]); - expect(rowsLabels[2]).to.equal(uniqSeriesLabels[2]); - }); - }); - - describe('Data array', function() { - const childrenObject = { - children: [], - }; - const seriesObject = { - series: [], - }; - const rowsObject = { - rows: [], - }; - const columnsObject = { - columns: [], - }; - const string = 'string'; - const number = 23; - const boolean = false; - const emptyArray = []; - const nullValue = null; - let notAValue; - let testSeries; - let testRows; - - beforeEach(() => { - seriesLabels = dataArray(seriesData); - rowsLabels = dataArray(rowsData); - testSeries = Array.isArray(seriesLabels); - testRows = Array.isArray(rowsLabels); - }); - - it('should throw an error if the input is not an object', function() { - expect(function() { - dataArray(string); - }).to.throwError(); - - expect(function() { - dataArray(number); - }).to.throwError(); - - expect(function() { - dataArray(boolean); - }).to.throwError(); - - expect(function() { - dataArray(emptyArray); - }).to.throwError(); - - expect(function() { - dataArray(nullValue); - }).to.throwError(); - - expect(function() { - dataArray(notAValue); - }).to.throwError(); - }); - - it( - 'should throw an error if property series, rows, or ' + 'columns is not present', - function() { - expect(function() { - dataArray(childrenObject); - }).to.throwError(); - } - ); - - it( - 'should not throw an error if object has property series, rows, or ' + 'columns', - function() { - expect(function() { - dataArray(seriesObject); - }).to.not.throwError(); - - expect(function() { - dataArray(rowsObject); - }).to.not.throwError(); - - expect(function() { - dataArray(columnsObject); - }).to.not.throwError(); - } - ); - - it('should be a function', function() { - expect(typeof dataArray).to.equal('function'); - }); - - it('should return an array of objects if input is data.series', function() { - expect(testSeries).to.equal(true); - }); - - it('should return an array of objects if input is data.rows', function() { - expect(testRows).to.equal(true); - }); - - it('should return an array of same length as input data.series', function() { - expect(seriesLabels.length).to.equal(seriesData.series.length); - }); - - it('should return an array of same length as input data.rows', function() { - expect(rowsLabels.length).to.equal(rowsData.rows.length); - }); - - it('should return an array of objects with obj.labels and obj.values', function() { - expect(seriesLabels[0].label).to.equal('100'); - expect(seriesLabels[0].values[0].x).to.equal(0); - expect(seriesLabels[0].values[0].y).to.equal(1); - }); - }); - - describe('Unique labels', function() { - const arrObj = [ - { label: 'a' }, - { label: 'b' }, - { label: 'b' }, - { label: 'c' }, - { label: 'c' }, - { label: 'd' }, - { label: 'f' }, - ]; - const string = 'string'; - const number = 24; - const boolean = false; - const nullValue = null; - const emptyObject = {}; - const emptyArray = []; - let notAValue; - let uniq; - let testArr; - - beforeEach(() => { - uniq = uniqLabels(arrObj, function(d) { - return d; - }); - testArr = Array.isArray(uniq); - }); - - it('should throw an error if input is not an array', function() { - expect(function() { - uniqLabels(string); - }).to.throwError(); - - expect(function() { - uniqLabels(number); - }).to.throwError(); - - expect(function() { - uniqLabels(boolean); - }).to.throwError(); - - expect(function() { - uniqLabels(nullValue); - }).to.throwError(); - - expect(function() { - uniqLabels(emptyObject); - }).to.throwError(); - - expect(function() { - uniqLabels(notAValue); - }).to.throwError(); - }); - - it('should not throw an error if the input is an array', function() { - expect(function() { - uniqLabels(emptyArray); - }).to.not.throwError(); - }); - - it('should be a function', function() { - expect(typeof uniqLabels).to.be('function'); - }); - - it('should return an array', function() { - expect(testArr).to.be(true); - }); - - it('should return array of 5 unique values', function() { - expect(uniq.length).to.be(5); - }); - }); - - describe('Get series', function() { - const string = 'string'; - const number = 24; - const boolean = false; - const nullValue = null; - const rowsObject = { - rows: [], - }; - const columnsObject = { - columns: [], - }; - const emptyObject = {}; - const emptyArray = []; - let notAValue; - let columnsLabels; - let rowsLabels; - let columnsArr; - let rowsArr; - - beforeEach(() => { - columnsLabels = getSeries(columnsData); - rowsLabels = getSeries(rowsData); - columnsArr = Array.isArray(columnsLabels); - rowsArr = Array.isArray(rowsLabels); - }); - - it('should throw an error if input is not an object', function() { - expect(function() { - getSeries(string); - }).to.throwError(); - - expect(function() { - getSeries(number); - }).to.throwError(); - - expect(function() { - getSeries(boolean); - }).to.throwError(); - - expect(function() { - getSeries(nullValue); - }).to.throwError(); - - expect(function() { - getSeries(emptyArray); - }).to.throwError(); - - expect(function() { - getSeries(notAValue); - }).to.throwError(); - }); - - it('should throw an if property rows or columns is not set on the object', function() { - expect(function() { - getSeries(emptyObject); - }).to.throwError(); - }); - - it('should not throw an error if rows or columns set on object', function() { - expect(function() { - getSeries(rowsObject); - }).to.not.throwError(); - - expect(function() { - getSeries(columnsObject); - }).to.not.throwError(); - }); - - it('should be a function', function() { - expect(typeof getSeries).to.be('function'); - }); - - it('should return an array if input is data.columns', function() { - expect(columnsArr).to.be(true); - }); - - it('should return an array if input is data.rows', function() { - expect(rowsArr).to.be(true); - }); - - it('should return an array of the same length as as input data.columns', function() { - expect(columnsLabels.length).to.be(columnsData.columns.length); - }); - - it('should return an array of the same length as as input data.rows', function() { - expect(rowsLabels.length).to.be(rowsData.rows.length); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js deleted file mode 100644 index f1c80c9981020..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/components/positioning.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 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 'jquery'; -import _ from 'lodash'; -import sinon from 'sinon'; - -import { positionTooltip } from '../../components/tooltip/position_tooltip'; - -describe('Tooltip Positioning', function() { - const sandbox = sinon.createSandbox(); - const positions = ['north', 'south', 'east', 'west']; - const bounds = ['top', 'left', 'bottom', 'right', 'area']; - let $window; - let $chart; - let $tooltip; - let $sizer; - - function testEl(width, height, $children) { - const $el = $('
'); - - const size = { - width: _.random(width[0], width[1]), - height: _.random(height[0], height[1]), - }; - - $el - .css({ - width: size.width, - height: size.height, - visibility: 'hidden', - }) - .appendTo('body'); - - if ($children) { - $el.append($children); - } - - $el.testSize = size; - - return $el; - } - - beforeEach(function() { - $window = testEl( - [500, 1000], - [600, 800], - ($chart = testEl([600, 750], [350, 550], ($tooltip = testEl([50, 100], [35, 75])))) - ); - - $sizer = $tooltip.clone().appendTo($window); - }); - - afterEach(function() { - $window.remove(); - $window = $chart = $tooltip = $sizer = null; - positionTooltip.removeClone(); - sandbox.restore(); - }); - - function makeEvent(xPercent, yPercent) { - xPercent = xPercent || 0.5; - yPercent = yPercent || 0.5; - - const base = $chart.offset(); - - return { - clientX: base.left + $chart.testSize.width * xPercent, - clientY: base.top + $chart.testSize.height * yPercent, - }; - } - - describe('getTtSize()', function() { - it('should measure the outer-size of the tooltip using an un-obstructed clone', function() { - const w = sandbox.spy($.fn, 'outerWidth'); - const h = sandbox.spy($.fn, 'outerHeight'); - - positionTooltip.getTtSize($tooltip.html(), $sizer); - - [w, h].forEach(function(spy) { - expect(spy).to.have.property('callCount', 1); - const matchHtml = w.thisValues.filter(function($t) { - return !$t.is($tooltip) && $t.html() === $tooltip.html(); - }); - expect(matchHtml).to.have.length(1); - }); - }); - }); - - describe('getBasePosition()', function() { - it('calculates the offset values for the four positions', function() { - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - const pos = positionTooltip.getBasePosition(size, makeEvent()); - - positions.forEach(function(p) { - expect(pos).to.have.property(p); - }); - - expect(pos.north).to.be.lessThan(pos.south); - expect(pos.east).to.be.greaterThan(pos.west); - }); - }); - - describe('getBounds()', function() { - it('returns the offsets for the tlrb of the element', function() { - const cbounds = positionTooltip.getBounds($chart); - - bounds.forEach(function(b) { - expect(cbounds).to.have.property(b); - }); - - expect(cbounds.top).to.be.lessThan(cbounds.bottom); - expect(cbounds.left).to.be.lessThan(cbounds.right); - }); - }); - - describe('getOverflow()', function() { - it('determines how much the base placement overflows the containing bounds in each direction', function() { - // size the tooltip very small so it won't collide with the edges - $tooltip.css({ width: 15, height: 15 }); - $sizer.css({ width: 15, height: 15 }); - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - expect(size).to.have.property('width', 15); - expect(size).to.have.property('height', 15); - - // position the element based on a mouse that is in the middle of the chart - const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); - - const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); - positions.forEach(function(p) { - expect(overflow).to.have.property(p); - - // all positions should be less than 0 because the tooltip is so much smaller than the chart - expect(overflow[p]).to.be.lessThan(0); - }); - }); - - it('identifies an overflow with a positive value in that direction', function() { - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - - // position the element based on a mouse that is in the bottom right hand corner of the chart - const pos = positionTooltip.getBasePosition(size, makeEvent(0.99, 0.99)); - const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); - - positions.forEach(function(p) { - expect(overflow).to.have.property(p); - - if (p === 'south' || p === 'east') { - expect(overflow[p]).to.be.greaterThan(0); - } else { - expect(overflow[p]).to.be.lessThan(0); - } - }); - }); - - it('identifies only right overflow when tooltip overflows both sides of inner container but outer contains tooltip', function() { - // Size $tooltip larger than chart - const largeWidth = $chart.width() + 10; - $tooltip.css({ width: largeWidth }); - $sizer.css({ width: largeWidth }); - const size = positionTooltip.getTtSize($tooltip.html(), $sizer); - expect(size).to.have.property('width', largeWidth); - - // $chart is flush with the $window on the left side - expect(positionTooltip.getBounds($chart).left).to.be(0); - - // Size $window large enough for tooltip on right side - $window.css({ width: $chart.width() * 3 }); - - // Position click event in center of $chart so $tooltip overflows both sides of chart - const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); - - const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); - - // no overflow on left (east) - expect(overflow.east).to.be.lessThan(0); - // overflow on right (west) - expect(overflow.west).to.be.greaterThan(0); - }); - }); - - describe('positionTooltip() integration', function() { - it('returns nothing if the $chart or $tooltip are not passed in', function() { - expect(positionTooltip() === void 0).to.be(true); - expect(positionTooltip(null, null, null) === void 0).to.be(true); - expect(positionTooltip(null, $(), $()) === void 0).to.be(true); - }); - - function check(xPercent, yPercent /*, prev, directions... */) { - const directions = _.drop(arguments, 2); - const event = makeEvent(xPercent, yPercent); - const placement = positionTooltip({ - $window: $window, - $chart: $chart, - $sizer: $sizer, - event: event, - $el: $tooltip, - prev: _.isObject(directions[0]) ? directions.shift() : null, - }); - - expect(placement) - .to.have.property('top') - .and.property('left'); - - directions.forEach(function(dir) { - switch (dir) { - case 'top': - expect(placement.top).to.be.lessThan(event.clientY); - return; - case 'bottom': - expect(placement.top).to.be.greaterThan(event.clientY); - return; - case 'right': - expect(placement.left).to.be.greaterThan(event.clientX); - return; - case 'left': - expect(placement.left).to.be.lessThan(event.clientX); - return; - } - }); - - return placement; - } - - describe('calculates placement of the tooltip properly', function() { - it('mouse is in the middle', function() { - check(0.5, 0.5, 'bottom', 'right'); - }); - - it('mouse is in the top left', function() { - check(0.1, 0.1, 'bottom', 'right'); - }); - - it('mouse is in the top right', function() { - check(0.99, 0.1, 'bottom', 'left'); - }); - - it('mouse is in the bottom right', function() { - check(0.99, 0.99, 'top', 'left'); - }); - - it('mouse is in the bottom left', function() { - check(0.1, 0.99, 'top', 'right'); - }); - }); - - describe('maintain the direction of the tooltip on reposition', function() { - it('mouse moves from the top right to the middle', function() { - const pos = check(0.99, 0.1, 'bottom', 'left'); - check(0.5, 0.5, pos, 'bottom', 'left'); - }); - - it('mouse moves from the bottom left to the middle', function() { - const pos = check(0.1, 0.99, 'top', 'right'); - check(0.5, 0.5, pos, 'top', 'right'); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js deleted file mode 100644 index 734c6d003278f..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/index.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; - -import VislibProvider from '..'; - -describe('Vislib Index Test Suite', function() { - let vislib; - - beforeEach(() => { - vislib = new VislibProvider(); - }); - - it('should return an object', function() { - expect(_.isObject(vislib)).to.be(true); - }); - - it('should return a Vis function', function() { - expect(_.isFunction(vislib.Vis)).to.be(true); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js deleted file mode 100644 index bc4a4f9925513..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis/axis.js +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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 d3 from 'd3'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { Axis } from '../../../lib/axis'; -import { VisConfig } from '../../../lib/vis_config'; -import { getMockUiState } from '../fixtures/_vis_fixture'; - -describe('Vislib Axis Class Test Suite', function() { - let mockUiState; - let yAxis; - let el; - let fixture; - let seriesData; - - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734130000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - ], - }, - { - label: 'Count2', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734140000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - ], - }, - ], - xAxisFormatter: function(thing) { - return new Date(thing); - }, - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - mockUiState = getMockUiState(); - el = d3 - .select('body') - .append('div') - .attr('class', 'visAxis--x') - .style('height', '40px'); - - fixture = el.append('div').attr('class', 'x-axis-div'); - - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - mockUiState, - $('.x-axis-div')[0], - () => undefined - ); - yAxis = new Axis(visConfig, { - type: 'value', - id: 'ValueAxis-1', - }); - - seriesData = data.series.map(series => { - return series.values; - }); - }); - - afterEach(function() { - fixture.remove(); - el.remove(); - }); - - describe('_stackNegAndPosVals Method', function() { - it('should correctly stack positive values', function() { - const expectedResult = [ - { - x: 1408734060000, - y: 8, - y0: 8, - }, - { - x: 1408734090000, - y: 23, - y0: 23, - }, - { - x: 1408734120000, - y: 30, - y0: 30, - }, - { - x: 1408734140000, - y: 30, - y0: 0, - }, - { - x: 1408734150000, - y: 28, - y0: 28, - }, - ]; - const stackedData = yAxis._stackNegAndPosVals(seriesData); - expect(stackedData[1]).to.eql(expectedResult); - }); - - it('should correctly stack pos and neg values', function() { - const expectedResult = [ - { - x: 1408734060000, - y: 8, - y0: 0, - }, - { - x: 1408734090000, - y: 23, - y0: 0, - }, - { - x: 1408734120000, - y: 30, - y0: 0, - }, - { - x: 1408734140000, - y: 30, - y0: 0, - }, - { - x: 1408734150000, - y: 28, - y0: 0, - }, - ]; - const dataClone = _.cloneDeep(seriesData); - dataClone[0].forEach(value => { - value.y = -value.y; - }); - const stackedData = yAxis._stackNegAndPosVals(dataClone); - expect(stackedData[1]).to.eql(expectedResult); - }); - - it('should correctly stack mixed pos and neg values', function() { - const expectedResult = [ - { - x: 1408734060000, - y: 8, - y0: 8, - }, - { - x: 1408734090000, - y: 23, - y0: 0, - }, - { - x: 1408734120000, - y: 30, - y0: 30, - }, - { - x: 1408734140000, - y: 30, - y0: 0, - }, - { - x: 1408734150000, - y: 28, - y0: 28, - }, - ]; - const dataClone = _.cloneDeep(seriesData); - dataClone[0].forEach((value, i) => { - if (i % 2 === 1) value.y = -value.y; - }); - const stackedData = yAxis._stackNegAndPosVals(dataClone); - expect(stackedData[1]).to.eql(expectedResult); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js deleted file mode 100644 index fd25335dd2cd4..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/axis_title.js +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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 d3 from 'd3'; -import _ from 'lodash'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import { AxisTitle } from '../../lib/axis/axis_title'; -import { AxisConfig } from '../../lib/axis/axis_config'; -import { VisConfig } from '../../lib/vis_config'; -import { Data } from '../../lib/data'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -describe('Vislib AxisTitle Class Test Suite', function() { - let el; - let dataObj; - let xTitle; - let yTitle; - let visConfig; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - el = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper'); - - el.append('div') - .attr('class', 'visAxis__column--bottom') - .append('div') - .attr('class', 'axis-title y-axis-title') - .style('height', '20px') - .style('width', '20px'); - - el.append('div') - .attr('class', 'visAxis__column--left') - .append('div') - .attr('class', 'axis-title x-axis-title') - .style('height', '20px') - .style('width', '20px'); - - const uiState = getMockUiState(); - uiState.set('vis.colors', []); - dataObj = new Data(data, getMockUiState(), () => undefined); - visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - getMockUiState(), - el.node(), - () => undefined - ); - const xAxisConfig = new AxisConfig(visConfig, { - position: 'bottom', - title: { - text: dataObj.get('xAxisLabel'), - }, - }); - const yAxisConfig = new AxisConfig(visConfig, { - position: 'left', - title: { - text: dataObj.get('yAxisLabel'), - }, - }); - xTitle = new AxisTitle(xAxisConfig); - yTitle = new AxisTitle(yAxisConfig); - }); - - afterEach(function() { - el.remove(); - }); - - it('should not do anything if title.show is set to false', function() { - const xAxisConfig = new AxisConfig(visConfig, { - position: 'bottom', - show: false, - title: { - text: dataObj.get('xAxisLabel'), - }, - }); - xTitle = new AxisTitle(xAxisConfig); - xTitle.render(); - expect( - $(el.node()) - .find('.x-axis-title') - .find('svg').length - ).to.be(0); - }); - - describe('render Method', function() { - beforeEach(function() { - xTitle.render(); - yTitle.render(); - }); - - it('should append an svg to div', function() { - expect(el.select('.x-axis-title').selectAll('svg').length).to.be(1); - expect(el.select('.y-axis-title').selectAll('svg').length).to.be(1); - }); - - it('should append a g element to the svg', function() { - expect( - el - .select('.x-axis-title') - .selectAll('svg') - .select('g').length - ).to.be(1); - expect( - el - .select('.y-axis-title') - .selectAll('svg') - .select('g').length - ).to.be(1); - }); - - it('should append text', function() { - expect( - !!el - .select('.x-axis-title') - .selectAll('svg') - .selectAll('text') - ).to.be(true); - expect( - !!el - .select('.y-axis-title') - .selectAll('svg') - .selectAll('text') - ).to.be(true); - }); - }); - - describe('draw Method', function() { - it('should be a function', function() { - expect(_.isFunction(xTitle.draw())).to.be(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js deleted file mode 100644 index d4ec6f363a75b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/data.js +++ /dev/null @@ -1,310 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; - -import { Data } from '../../lib/data'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -const seriesData = { - label: '', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], -}; - -const rowsData = { - rows: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -const colsData = { - columns: [ - { - label: 'a', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'b', - series: [ - { - label: '300', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'c', - series: [ - { - label: '100', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - { - label: 'd', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: 2 }, - { x: 2, y: 3 }, - ], - }, - ], - }, - ], -}; - -describe('Vislib Data Class Test Suite', function() { - let mockUiState; - - beforeEach(() => { - mockUiState = getMockUiState(); - }); - - describe('Data Class (main)', function() { - it('should be a function', function() { - expect(_.isFunction(Data)).to.be(true); - }); - - it('should return an object', function() { - const rowIn = new Data(rowsData, mockUiState, () => undefined); - expect(_.isObject(rowIn)).to.be(true); - }); - }); - - describe('_removeZeroSlices', function() { - let data; - const pieData = { - slices: { - children: [{ size: 30 }, { size: 20 }, { size: 0 }], - }, - }; - - beforeEach(function() { - data = new Data(pieData, mockUiState, () => undefined); - }); - - it('should remove zero values', function() { - const slices = data._removeZeroSlices(data.data.slices); - expect(slices.children.length).to.be(2); - }); - }); - - describe('Data.flatten', function() { - let serIn; - let serOut; - - beforeEach(function() { - serIn = new Data(seriesData, mockUiState, () => undefined); - serOut = serIn.flatten(); - }); - - it('should return an array of value objects from every series', function() { - expect(serOut.every(_.isObject)).to.be(true); - }); - - it('should return all points from every series', testLength(seriesData)); - it('should return all points from every series in the rows', testLength(rowsData)); - it('should return all points from every series in the columns', testLength(colsData)); - - function testLength(inputData) { - return function() { - const data = new Data(inputData, mockUiState, () => undefined); - const len = _.reduce( - data.chartData(), - function(sum, chart) { - return ( - sum + - chart.series.reduce(function(sum, series) { - return sum + series.values.length; - }, 0) - ); - }, - 0 - ); - - expect(data.flatten()).to.have.length(len); - }; - } - }); - - describe('geohashGrid methods', function() { - let data; - const geohashGridData = { - hits: 3954, - rows: [ - { - title: 'Top 5 _type: apache', - label: 'Top 5 _type: apache', - geoJson: { - type: 'FeatureCollection', - features: [], - properties: { - min: 2, - max: 331, - zoom: 3, - center: [47.517200697839414, -112.06054687499999], - }, - }, - }, - { - title: 'Top 5 _type: nginx', - label: 'Top 5 _type: nginx', - geoJson: { - type: 'FeatureCollection', - features: [], - properties: { - min: 1, - max: 88, - zoom: 3, - center: [47.517200697839414, -112.06054687499999], - }, - }, - }, - ], - }; - - beforeEach(function() { - data = new Data(geohashGridData, mockUiState, () => undefined); - }); - - describe('getVisData', function() { - it('should return the rows property', function() { - const visData = data.getVisData(); - expect(visData[0].title).to.eql(geohashGridData.rows[0].title); - }); - }); - - describe('getGeoExtents', function() { - it('should return the min and max geoJson properties', function() { - const minMax = data.getGeoExtents(); - expect(minMax.min).to.be(1); - expect(minMax.max).to.be(331); - }); - }); - }); - - describe('null value check', function() { - it('should return false', function() { - const data = new Data(rowsData, mockUiState, () => undefined); - expect(data.hasNullValues()).to.be(false); - }); - - it('should return true', function() { - const nullRowData = { rows: rowsData.rows.slice(0) }; - nullRowData.rows.push({ - label: 'e', - series: [ - { - label: '200', - values: [ - { x: 0, y: 1 }, - { x: 1, y: null }, - { x: 2, y: 3 }, - ], - }, - ], - }); - - const data = new Data(nullRowData, mockUiState, () => undefined); - expect(data.hasNullValues()).to.be(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_vertical_bar_chart.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_vertical_bar_chart.test.js deleted file mode 100644 index 8fe9ac24db77b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_vertical_bar_chart.test.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 mockDispatchDataD3 from './fixtures/dispatch_bar_chart_d3.json'; -import { Dispatch } from '../../lib/dispatch'; -import mockdataPoint from './fixtures/dispatch_bar_chart_data_point.json'; -import mockConfigPercentage from './fixtures/dispatch_bar_chart_config_percentage.json'; -import mockConfigNormal from './fixtures/dispatch_bar_chart_config_normal.json'; - -jest.mock('ui/new_platform'); -jest.mock('d3', () => ({ - event: { - target: { - nearestViewportElement: { - __data__: mockDispatchDataD3, - }, - }, - }, -})); -jest.mock('../../../legacy_imports.ts', () => ({ - ...jest.requireActual('../../../legacy_imports.ts'), - chrome: { - getUiSettingsClient: () => ({ - get: () => '', - }), - addBasePath: () => {}, - }, -})); - -function getHandlerMock(config = {}, data = {}) { - return { - visConfig: { get: (id, fallback) => config[id] || fallback }, - data, - }; -} - -describe('Vislib event responses dispatcher', () => { - test('return data for a vertical bars popover in percentage mode', () => { - const dataPoint = mockdataPoint; - const handlerMock = getHandlerMock(mockConfigPercentage); - const dispatch = new Dispatch(handlerMock); - const actual = dispatch.eventResponse(dataPoint, 0); - expect(actual.isPercentageMode).toBeTruthy(); - }); - - test('return data for a vertical bars popover in normal mode', () => { - const dataPoint = mockdataPoint; - const handlerMock = getHandlerMock(mockConfigNormal); - const dispatch = new Dispatch(handlerMock); - const actual = dispatch.eventResponse(dataPoint, 0); - expect(actual.isPercentageMode).toBeFalsy(); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js deleted file mode 100644 index 4523e70ccbb4c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/error_handler.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { ErrorHandler } from '../../lib/_error_handler'; - -describe('Vislib ErrorHandler Test Suite', function() { - let errorHandler; - - beforeEach(() => { - errorHandler = new ErrorHandler(); - }); - - describe('validateWidthandHeight Method', function() { - it('should throw an error when width and/or height is 0', function() { - expect(function() { - errorHandler.validateWidthandHeight(0, 200); - }).to.throwError(); - expect(function() { - errorHandler.validateWidthandHeight(200, 0); - }).to.throwError(); - }); - - it('should throw an error when width and/or height is NaN', function() { - expect(function() { - errorHandler.validateWidthandHeight(null, 200); - }).to.throwError(); - expect(function() { - errorHandler.validateWidthandHeight(200, null); - }).to.throwError(); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_columns.js deleted file mode 100644 index b5b14c279b40e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_columns.js +++ /dev/null @@ -1,300 +0,0 @@ -import moment from 'moment'; - -export default { - 'columns': [ - { - 'label': '200: response', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826608440, - 'max': 1415827508440 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1415826600000, - 'y': 4 - }, - { - 'x': 1415826630000, - 'y': 8 - }, - { - 'x': 1415826660000, - 'y': 7 - }, - { - 'x': 1415826690000, - 'y': 5 - }, - { - 'x': 1415826720000, - 'y': 5 - }, - { - 'x': 1415826750000, - 'y': 4 - }, - { - 'x': 1415826780000, - 'y': 10 - }, - { - 'x': 1415826810000, - 'y': 7 - }, - { - 'x': 1415826840000, - 'y': 9 - }, - { - 'x': 1415826870000, - 'y': 8 - }, - { - 'x': 1415826900000, - 'y': 9 - }, - { - 'x': 1415826930000, - 'y': 8 - }, - { - 'x': 1415826960000, - 'y': 3 - }, - { - 'x': 1415826990000, - 'y': 9 - }, - { - 'x': 1415827020000, - 'y': 6 - }, - { - 'x': 1415827050000, - 'y': 8 - }, - { - 'x': 1415827080000, - 'y': 7 - }, - { - 'x': 1415827110000, - 'y': 4 - }, - { - 'x': 1415827140000, - 'y': 6 - }, - { - 'x': 1415827170000, - 'y': 10 - }, - { - 'x': 1415827200000, - 'y': 2 - }, - { - 'x': 1415827230000, - 'y': 8 - }, - { - 'x': 1415827260000, - 'y': 5 - }, - { - 'x': 1415827290000, - 'y': 6 - }, - { - 'x': 1415827320000, - 'y': 6 - }, - { - 'x': 1415827350000, - 'y': 10 - }, - { - 'x': 1415827380000, - 'y': 6 - }, - { - 'x': 1415827410000, - 'y': 6 - }, - { - 'x': 1415827440000, - 'y': 12 - }, - { - 'x': 1415827470000, - 'y': 9 - }, - { - 'x': 1415827500000, - 'y': 1 - } - ] - } - ] - }, - { - 'label': '503: response', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826608440, - 'max': 1415827508440 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1415826630000, - 'y': 1 - }, - { - 'x': 1415826660000, - 'y': 1 - }, - { - 'x': 1415826720000, - 'y': 1 - }, - { - 'x': 1415826780000, - 'y': 1 - }, - { - 'x': 1415826900000, - 'y': 1 - }, - { - 'x': 1415827020000, - 'y': 1 - }, - { - 'x': 1415827080000, - 'y': 1 - }, - { - 'x': 1415827110000, - 'y': 2 - } - ] - } - ] - }, - { - 'label': '404: response', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826608440, - 'max': 1415827508440 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1415826660000, - 'y': 1 - }, - { - 'x': 1415826720000, - 'y': 1 - }, - { - 'x': 1415826810000, - 'y': 1 - }, - { - 'x': 1415826960000, - 'y': 1 - }, - { - 'x': 1415827050000, - 'y': 1 - }, - { - 'x': 1415827260000, - 'y': 1 - }, - { - 'x': 1415827380000, - 'y': 1 - }, - { - 'x': 1415827410000, - 'y': 1 - } - ] - } - ] - } - ], - 'xAxisOrderedValues': [ - 1415826600000, - 1415826630000, - 1415826660000, - 1415826690000, - 1415826720000, - 1415826750000, - 1415826780000, - 1415826810000, - 1415826840000, - 1415826870000, - 1415826900000, - 1415826930000, - 1415826960000, - 1415826990000, - 1415827020000, - 1415827050000, - 1415827080000, - 1415827110000, - 1415827140000, - 1415827170000, - 1415827200000, - 1415827230000, - 1415827260000, - 1415827290000, - 1415827320000, - 1415827350000, - 1415827380000, - 1415827410000, - 1415827440000, - 1415827470000, - 1415827500000, - ], - 'hits': 225 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows.js deleted file mode 100644 index 98609d8ffbcd3..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows.js +++ /dev/null @@ -1,1678 +0,0 @@ -import moment from 'moment'; - -export default { - 'rows': [ - { - 'label': '0.0-1000.0: bytes', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826260456, - 'max': 1415827160456 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 3, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826660000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 1, - 'y0': 1 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 1, - 'y0': 1 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 2 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - } - ] - }, - { - 'label': '1000.0-2000.0: bytes', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1415826260457, - 'max': 1415827160457 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - }, - 'series': [ - { - 'label': 'jpg', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 2, - 'y0': 0 - }, - { - 'x': 1415826840000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1415826240000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826270000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826300000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826330000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826360000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826390000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826420000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826450000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826480000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826510000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826540000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826570000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826600000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826630000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826660000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826690000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826720000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826750000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826780000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826810000, - 'y': 0, - 'y0': 2 - }, - { - 'x': 1415826840000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826870000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826900000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415826930000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826960000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415826990000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827020000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827050000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827080000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 1415827110000, - 'y': 0, - 'y0': 1 - }, - { - 'x': 1415827140000, - 'y': 0, - 'y0': 0 - } - ] - } - ] - } - ], - 'xAxisOrderedValues': [ - 1415826240000, - 1415826270000, - 1415826300000, - 1415826330000, - 1415826360000, - 1415826390000, - 1415826420000, - 1415826450000, - 1415826480000, - 1415826510000, - 1415826540000, - 1415826570000, - 1415826600000, - 1415826630000, - 1415826660000, - 1415826690000, - 1415826720000, - 1415826750000, - 1415826780000, - 1415826810000, - 1415826840000, - 1415826870000, - 1415826900000, - 1415826930000, - 1415826960000, - 1415826990000, - 1415827020000, - 1415827050000, - 1415827080000, - 1415827110000, - 1415827140000, - ], - 'hits': 236 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows_series_with_holes.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows_series_with_holes.js deleted file mode 100644 index 4ca631c7fc497..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_rows_series_with_holes.js +++ /dev/null @@ -1,123 +0,0 @@ -import moment from 'moment'; - -export const rowsSeriesWithHoles = { - rows: [ - { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': 41 - }, - { - 'x': 1411761510000, - 'y': 22 - }, - { - 'x': 1411761540000, - 'y': 17 - }, - { - 'x': 1411761840000, - 'y': 20 - }, - { - 'x': 1411761870000, - 'y': 20 - }, - { - 'x': 1411761900000, - 'y': 21 - }, - { - 'x': 1411761930000, - 'y': 17 - }, - { - 'x': 1411761960000, - 'y': 20 - }, - { - 'x': 1411761990000, - 'y': 13 - }, - { - 'x': 1411762020000, - 'y': 14 - }, - { - 'x': 1411762050000, - 'y': 25 - }, - { - 'x': 1411762080000, - 'y': 17 - }, - { - 'x': 1411762110000, - 'y': 14 - }, - { - 'x': 1411762140000, - 'y': 22 - }, - { - 'x': 1411762170000, - 'y': 14 - }, - { - 'x': 1411762200000, - 'y': 19 - }, - { - 'x': 1411762320000, - 'y': 15 - }, - { - 'x': 1411762350000, - 'y': 4 - } - ] - } - ], - 'hits': 533, - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': [ - 1411761450000, - 1411761510000, - 1411761540000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762320000, - 1411762350000, - ], -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series.js deleted file mode 100644 index 13e2ab7b7fb1a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series.js +++ /dev/null @@ -1,184 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': 41 - }, - { - 'x': 1411761480000, - 'y': 18 - }, - { - 'x': 1411761510000, - 'y': 22 - }, - { - 'x': 1411761540000, - 'y': 17 - }, - { - 'x': 1411761570000, - 'y': 17 - }, - { - 'x': 1411761600000, - 'y': 21 - }, - { - 'x': 1411761630000, - 'y': 16 - }, - { - 'x': 1411761660000, - 'y': 17 - }, - { - 'x': 1411761690000, - 'y': 15 - }, - { - 'x': 1411761720000, - 'y': 19 - }, - { - 'x': 1411761750000, - 'y': 11 - }, - { - 'x': 1411761780000, - 'y': 13 - }, - { - 'x': 1411761810000, - 'y': 24 - }, - { - 'x': 1411761840000, - 'y': 20 - }, - { - 'x': 1411761870000, - 'y': 20 - }, - { - 'x': 1411761900000, - 'y': 21 - }, - { - 'x': 1411761930000, - 'y': 17 - }, - { - 'x': 1411761960000, - 'y': 20 - }, - { - 'x': 1411761990000, - 'y': 13 - }, - { - 'x': 1411762020000, - 'y': 14 - }, - { - 'x': 1411762050000, - 'y': 25 - }, - { - 'x': 1411762080000, - 'y': 17 - }, - { - 'x': 1411762110000, - 'y': 14 - }, - { - 'x': 1411762140000, - 'y': 22 - }, - { - 'x': 1411762170000, - 'y': 14 - }, - { - 'x': 1411762200000, - 'y': 19 - }, - { - 'x': 1411762230000, - 'y': 22 - }, - { - 'x': 1411762260000, - 'y': 17 - }, - { - 'x': 1411762290000, - 'y': 8 - }, - { - 'x': 1411762320000, - 'y': 15 - }, - { - 'x': 1411762350000, - 'y': 4 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1411761450000, - 1411761480000, - 1411761510000, - 1411761540000, - 1411761570000, - 1411761600000, - 1411761630000, - 1411761660000, - 1411761690000, - 1411761720000, - 1411761750000, - 1411761780000, - 1411761810000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762230000, - 1411762260000, - 1411762290000, - 1411762320000, - 1411762350000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_monthly_interval.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_monthly_interval.js deleted file mode 100644 index 6b7c574ab5551..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_monthly_interval.js +++ /dev/null @@ -1,89 +0,0 @@ -import moment from 'moment'; - -export const seriesMonthlyInterval = { - 'label': '', - 'xAxisLabel': '@timestamp per month', - 'ordered': { - 'date': true, - 'min': 1451631600000, - 'max': 1483254000000, - 'interval': 2678000000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1451631600000, - 'y': 10220 - }, - { - 'x': 1454310000000, - 'y': 9997, - }, - { - 'x': 1456815600000, - 'y': 10792, - }, - { - 'x': 1459490400000, - 'y': 10262 - }, - { - 'x': 1462082400000, - 'y': 10080 - }, - { - 'x': 1464760800000, - 'y': 11161 - }, - { - 'x': 1467352800000, - 'y': 9933 - }, - { - 'x': 1470031200000, - 'y': 10342 - }, - { - 'x': 1472709600000, - 'y': 10887 - }, - { - 'x': 1475301600000, - 'y': 9666 - }, - { - 'x': 1477980000000, - 'y': 9556 - }, - { - 'x': 1480575600000, - 'y': 11644 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1451631600000, - 1454310000000, - 1456815600000, - 1459490400000, - 1462082400000, - 1464760800000, - 1467352800000, - 1470031200000, - 1472709600000, - 1475301600000, - 1477980000000, - 1480575600000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_neg.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_neg.js deleted file mode 100644 index ff5cd05b2f2d4..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_neg.js +++ /dev/null @@ -1,184 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': -41 - }, - { - 'x': 1411761480000, - 'y': -18 - }, - { - 'x': 1411761510000, - 'y': -22 - }, - { - 'x': 1411761540000, - 'y': -17 - }, - { - 'x': 1411761570000, - 'y': -17 - }, - { - 'x': 1411761600000, - 'y': -21 - }, - { - 'x': 1411761630000, - 'y': -16 - }, - { - 'x': 1411761660000, - 'y': -17 - }, - { - 'x': 1411761690000, - 'y': -15 - }, - { - 'x': 1411761720000, - 'y': -19 - }, - { - 'x': 1411761750000, - 'y': -11 - }, - { - 'x': 1411761780000, - 'y': -13 - }, - { - 'x': 1411761810000, - 'y': -24 - }, - { - 'x': 1411761840000, - 'y': -20 - }, - { - 'x': 1411761870000, - 'y': -20 - }, - { - 'x': 1411761900000, - 'y': -21 - }, - { - 'x': 1411761930000, - 'y': -17 - }, - { - 'x': 1411761960000, - 'y': -20 - }, - { - 'x': 1411761990000, - 'y': -13 - }, - { - 'x': 1411762020000, - 'y': -14 - }, - { - 'x': 1411762050000, - 'y': -25 - }, - { - 'x': 1411762080000, - 'y': -17 - }, - { - 'x': 1411762110000, - 'y': -14 - }, - { - 'x': 1411762140000, - 'y': -22 - }, - { - 'x': 1411762170000, - 'y': -14 - }, - { - 'x': 1411762200000, - 'y': -19 - }, - { - 'x': 1411762230000, - 'y': -22 - }, - { - 'x': 1411762260000, - 'y': -17 - }, - { - 'x': 1411762290000, - 'y': -8 - }, - { - 'x': 1411762320000, - 'y': -15 - }, - { - 'x': 1411762350000, - 'y': -4 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1411761450000, - 1411761480000, - 1411761510000, - 1411761540000, - 1411761570000, - 1411761600000, - 1411761630000, - 1411761660000, - 1411761690000, - 1411761720000, - 1411761750000, - 1411761780000, - 1411761810000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762230000, - 1411762260000, - 1411762290000, - 1411762320000, - 1411762350000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_pos_neg.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_pos_neg.js deleted file mode 100644 index 06d9b31dc6b57..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_series_pos_neg.js +++ /dev/null @@ -1,184 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'min': 1411761457636, - 'max': 1411762357636, - 'interval': 30000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 1411761450000, - 'y': 41 - }, - { - 'x': 1411761480000, - 'y': 18 - }, - { - 'x': 1411761510000, - 'y': -22 - }, - { - 'x': 1411761540000, - 'y': -17 - }, - { - 'x': 1411761570000, - 'y': -17 - }, - { - 'x': 1411761600000, - 'y': -21 - }, - { - 'x': 1411761630000, - 'y': -16 - }, - { - 'x': 1411761660000, - 'y': 17 - }, - { - 'x': 1411761690000, - 'y': 15 - }, - { - 'x': 1411761720000, - 'y': 19 - }, - { - 'x': 1411761750000, - 'y': 11 - }, - { - 'x': 1411761780000, - 'y': -13 - }, - { - 'x': 1411761810000, - 'y': -24 - }, - { - 'x': 1411761840000, - 'y': -20 - }, - { - 'x': 1411761870000, - 'y': -20 - }, - { - 'x': 1411761900000, - 'y': -21 - }, - { - 'x': 1411761930000, - 'y': 17 - }, - { - 'x': 1411761960000, - 'y': 20 - }, - { - 'x': 1411761990000, - 'y': -13 - }, - { - 'x': 1411762020000, - 'y': -14 - }, - { - 'x': 1411762050000, - 'y': 25 - }, - { - 'x': 1411762080000, - 'y': -17 - }, - { - 'x': 1411762110000, - 'y': -14 - }, - { - 'x': 1411762140000, - 'y': -22 - }, - { - 'x': 1411762170000, - 'y': -14 - }, - { - 'x': 1411762200000, - 'y': 19 - }, - { - 'x': 1411762230000, - 'y': 22 - }, - { - 'x': 1411762260000, - 'y': 17 - }, - { - 'x': 1411762290000, - 'y': 8 - }, - { - 'x': 1411762320000, - 'y': -15 - }, - { - 'x': 1411762350000, - 'y': -4 - } - ] - } - ], - 'hits': 533, - 'xAxisOrderedValues': [ - 1411761450000, - 1411761480000, - 1411761510000, - 1411761540000, - 1411761570000, - 1411761600000, - 1411761630000, - 1411761660000, - 1411761690000, - 1411761720000, - 1411761750000, - 1411761780000, - 1411761810000, - 1411761840000, - 1411761870000, - 1411761900000, - 1411761930000, - 1411761960000, - 1411761990000, - 1411762020000, - 1411762050000, - 1411762080000, - 1411762110000, - 1411762140000, - 1411762170000, - 1411762200000, - 1411762230000, - 1411762260000, - 1411762290000, - 1411762320000, - 1411762350000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series.js deleted file mode 100644 index 5208c7e996cd8..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series.js +++ /dev/null @@ -1,1557 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 10 min', - 'ordered': { - 'date': true, - 'min': 1413544140087, - 'max': 1413587340087, - 'interval': 600000 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'html', - 'values': [ - { - 'x': 1413543600000, - 'y': 140 - }, - { - 'x': 1413544200000, - 'y': 1388 - }, - { - 'x': 1413544800000, - 'y': 1308 - }, - { - 'x': 1413545400000, - 'y': 1356 - }, - { - 'x': 1413546000000, - 'y': 1314 - }, - { - 'x': 1413546600000, - 'y': 1343 - }, - { - 'x': 1413547200000, - 'y': 1353 - }, - { - 'x': 1413547800000, - 'y': 1353 - }, - { - 'x': 1413548400000, - 'y': 1334 - }, - { - 'x': 1413549000000, - 'y': 1433 - }, - { - 'x': 1413549600000, - 'y': 1331 - }, - { - 'x': 1413550200000, - 'y': 1349 - }, - { - 'x': 1413550800000, - 'y': 1323 - }, - { - 'x': 1413551400000, - 'y': 1203 - }, - { - 'x': 1413552000000, - 'y': 1231 - }, - { - 'x': 1413552600000, - 'y': 1227 - }, - { - 'x': 1413553200000, - 'y': 1187 - }, - { - 'x': 1413553800000, - 'y': 1119 - }, - { - 'x': 1413554400000, - 'y': 1159 - }, - { - 'x': 1413555000000, - 'y': 1117 - }, - { - 'x': 1413555600000, - 'y': 1152 - }, - { - 'x': 1413556200000, - 'y': 1057 - }, - { - 'x': 1413556800000, - 'y': 1009 - }, - { - 'x': 1413557400000, - 'y': 979 - }, - { - 'x': 1413558000000, - 'y': 975 - }, - { - 'x': 1413558600000, - 'y': 848 - }, - { - 'x': 1413559200000, - 'y': 873 - }, - { - 'x': 1413559800000, - 'y': 808 - }, - { - 'x': 1413560400000, - 'y': 784 - }, - { - 'x': 1413561000000, - 'y': 799 - }, - { - 'x': 1413561600000, - 'y': 684 - }, - { - 'x': 1413562200000, - 'y': 727 - }, - { - 'x': 1413562800000, - 'y': 621 - }, - { - 'x': 1413563400000, - 'y': 615 - }, - { - 'x': 1413564000000, - 'y': 569 - }, - { - 'x': 1413564600000, - 'y': 523 - }, - { - 'x': 1413565200000, - 'y': 474 - }, - { - 'x': 1413565800000, - 'y': 470 - }, - { - 'x': 1413566400000, - 'y': 466 - }, - { - 'x': 1413567000000, - 'y': 394 - }, - { - 'x': 1413567600000, - 'y': 404 - }, - { - 'x': 1413568200000, - 'y': 389 - }, - { - 'x': 1413568800000, - 'y': 312 - }, - { - 'x': 1413569400000, - 'y': 274 - }, - { - 'x': 1413570000000, - 'y': 285 - }, - { - 'x': 1413570600000, - 'y': 299 - }, - { - 'x': 1413571200000, - 'y': 207 - }, - { - 'x': 1413571800000, - 'y': 213 - }, - { - 'x': 1413572400000, - 'y': 119 - }, - { - 'x': 1413573600000, - 'y': 122 - }, - { - 'x': 1413574200000, - 'y': 169 - }, - { - 'x': 1413574800000, - 'y': 151 - }, - { - 'x': 1413575400000, - 'y': 152 - }, - { - 'x': 1413576000000, - 'y': 115 - }, - { - 'x': 1413576600000, - 'y': 117 - }, - { - 'x': 1413577200000, - 'y': 108 - }, - { - 'x': 1413577800000, - 'y': 100 - }, - { - 'x': 1413578400000, - 'y': 78 - }, - { - 'x': 1413579000000, - 'y': 88 - }, - { - 'x': 1413579600000, - 'y': 63 - }, - { - 'x': 1413580200000, - 'y': 58 - }, - { - 'x': 1413580800000, - 'y': 45 - }, - { - 'x': 1413581400000, - 'y': 57 - }, - { - 'x': 1413582000000, - 'y': 34 - }, - { - 'x': 1413582600000, - 'y': 41 - }, - { - 'x': 1413583200000, - 'y': 24 - }, - { - 'x': 1413583800000, - 'y': 27 - }, - { - 'x': 1413584400000, - 'y': 19 - }, - { - 'x': 1413585000000, - 'y': 24 - }, - { - 'x': 1413585600000, - 'y': 18 - }, - { - 'x': 1413586200000, - 'y': 17 - }, - { - 'x': 1413586800000, - 'y': 14 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1413543600000, - 'y': 90 - }, - { - 'x': 1413544200000, - 'y': 949 - }, - { - 'x': 1413544800000, - 'y': 1012 - }, - { - 'x': 1413545400000, - 'y': 1027 - }, - { - 'x': 1413546000000, - 'y': 1073 - }, - { - 'x': 1413546600000, - 'y': 992 - }, - { - 'x': 1413547200000, - 'y': 1005 - }, - { - 'x': 1413547800000, - 'y': 1014 - }, - { - 'x': 1413548400000, - 'y': 987 - }, - { - 'x': 1413549000000, - 'y': 982 - }, - { - 'x': 1413549600000, - 'y': 1086 - }, - { - 'x': 1413550200000, - 'y': 998 - }, - { - 'x': 1413550800000, - 'y': 935 - }, - { - 'x': 1413551400000, - 'y': 995 - }, - { - 'x': 1413552000000, - 'y': 926 - }, - { - 'x': 1413552600000, - 'y': 897 - }, - { - 'x': 1413553200000, - 'y': 873 - }, - { - 'x': 1413553800000, - 'y': 885 - }, - { - 'x': 1413554400000, - 'y': 859 - }, - { - 'x': 1413555000000, - 'y': 852 - }, - { - 'x': 1413555600000, - 'y': 779 - }, - { - 'x': 1413556200000, - 'y': 739 - }, - { - 'x': 1413556800000, - 'y': 783 - }, - { - 'x': 1413557400000, - 'y': 784 - }, - { - 'x': 1413558000000, - 'y': 687 - }, - { - 'x': 1413558600000, - 'y': 660 - }, - { - 'x': 1413559200000, - 'y': 672 - }, - { - 'x': 1413559800000, - 'y': 600 - }, - { - 'x': 1413560400000, - 'y': 659 - }, - { - 'x': 1413561000000, - 'y': 540 - }, - { - 'x': 1413561600000, - 'y': 539 - }, - { - 'x': 1413562200000, - 'y': 481 - }, - { - 'x': 1413562800000, - 'y': 498 - }, - { - 'x': 1413563400000, - 'y': 444 - }, - { - 'x': 1413564000000, - 'y': 452 - }, - { - 'x': 1413564600000, - 'y': 408 - }, - { - 'x': 1413565200000, - 'y': 358 - }, - { - 'x': 1413565800000, - 'y': 321 - }, - { - 'x': 1413566400000, - 'y': 305 - }, - { - 'x': 1413567000000, - 'y': 292 - }, - { - 'x': 1413567600000, - 'y': 289 - }, - { - 'x': 1413568200000, - 'y': 239 - }, - { - 'x': 1413568800000, - 'y': 256 - }, - { - 'x': 1413569400000, - 'y': 220 - }, - { - 'x': 1413570000000, - 'y': 205 - }, - { - 'x': 1413570600000, - 'y': 201 - }, - { - 'x': 1413571200000, - 'y': 183 - }, - { - 'x': 1413571800000, - 'y': 172 - }, - { - 'x': 1413572400000, - 'y': 73 - }, - { - 'x': 1413573600000, - 'y': 90 - }, - { - 'x': 1413574200000, - 'y': 130 - }, - { - 'x': 1413574800000, - 'y': 104 - }, - { - 'x': 1413575400000, - 'y': 108 - }, - { - 'x': 1413576000000, - 'y': 92 - }, - { - 'x': 1413576600000, - 'y': 79 - }, - { - 'x': 1413577200000, - 'y': 90 - }, - { - 'x': 1413577800000, - 'y': 72 - }, - { - 'x': 1413578400000, - 'y': 68 - }, - { - 'x': 1413579000000, - 'y': 52 - }, - { - 'x': 1413579600000, - 'y': 60 - }, - { - 'x': 1413580200000, - 'y': 51 - }, - { - 'x': 1413580800000, - 'y': 32 - }, - { - 'x': 1413581400000, - 'y': 37 - }, - { - 'x': 1413582000000, - 'y': 30 - }, - { - 'x': 1413582600000, - 'y': 29 - }, - { - 'x': 1413583200000, - 'y': 24 - }, - { - 'x': 1413583800000, - 'y': 16 - }, - { - 'x': 1413584400000, - 'y': 15 - }, - { - 'x': 1413585000000, - 'y': 15 - }, - { - 'x': 1413585600000, - 'y': 10 - }, - { - 'x': 1413586200000, - 'y': 9 - }, - { - 'x': 1413586800000, - 'y': 9 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1413543600000, - 'y': 44 - }, - { - 'x': 1413544200000, - 'y': 495 - }, - { - 'x': 1413544800000, - 'y': 489 - }, - { - 'x': 1413545400000, - 'y': 492 - }, - { - 'x': 1413546000000, - 'y': 556 - }, - { - 'x': 1413546600000, - 'y': 536 - }, - { - 'x': 1413547200000, - 'y': 511 - }, - { - 'x': 1413547800000, - 'y': 479 - }, - { - 'x': 1413548400000, - 'y': 544 - }, - { - 'x': 1413549000000, - 'y': 513 - }, - { - 'x': 1413549600000, - 'y': 501 - }, - { - 'x': 1413550200000, - 'y': 532 - }, - { - 'x': 1413550800000, - 'y': 440 - }, - { - 'x': 1413551400000, - 'y': 455 - }, - { - 'x': 1413552000000, - 'y': 455 - }, - { - 'x': 1413552600000, - 'y': 471 - }, - { - 'x': 1413553200000, - 'y': 428 - }, - { - 'x': 1413553800000, - 'y': 457 - }, - { - 'x': 1413554400000, - 'y': 450 - }, - { - 'x': 1413555000000, - 'y': 418 - }, - { - 'x': 1413555600000, - 'y': 398 - }, - { - 'x': 1413556200000, - 'y': 397 - }, - { - 'x': 1413556800000, - 'y': 359 - }, - { - 'x': 1413557400000, - 'y': 398 - }, - { - 'x': 1413558000000, - 'y': 339 - }, - { - 'x': 1413558600000, - 'y': 363 - }, - { - 'x': 1413559200000, - 'y': 297 - }, - { - 'x': 1413559800000, - 'y': 323 - }, - { - 'x': 1413560400000, - 'y': 302 - }, - { - 'x': 1413561000000, - 'y': 260 - }, - { - 'x': 1413561600000, - 'y': 276 - }, - { - 'x': 1413562200000, - 'y': 249 - }, - { - 'x': 1413562800000, - 'y': 248 - }, - { - 'x': 1413563400000, - 'y': 235 - }, - { - 'x': 1413564000000, - 'y': 234 - }, - { - 'x': 1413564600000, - 'y': 188 - }, - { - 'x': 1413565200000, - 'y': 192 - }, - { - 'x': 1413565800000, - 'y': 173 - }, - { - 'x': 1413566400000, - 'y': 160 - }, - { - 'x': 1413567000000, - 'y': 137 - }, - { - 'x': 1413567600000, - 'y': 158 - }, - { - 'x': 1413568200000, - 'y': 111 - }, - { - 'x': 1413568800000, - 'y': 145 - }, - { - 'x': 1413569400000, - 'y': 118 - }, - { - 'x': 1413570000000, - 'y': 104 - }, - { - 'x': 1413570600000, - 'y': 80 - }, - { - 'x': 1413571200000, - 'y': 79 - }, - { - 'x': 1413571800000, - 'y': 86 - }, - { - 'x': 1413572400000, - 'y': 47 - }, - { - 'x': 1413573600000, - 'y': 49 - }, - { - 'x': 1413574200000, - 'y': 68 - }, - { - 'x': 1413574800000, - 'y': 78 - }, - { - 'x': 1413575400000, - 'y': 77 - }, - { - 'x': 1413576000000, - 'y': 50 - }, - { - 'x': 1413576600000, - 'y': 51 - }, - { - 'x': 1413577200000, - 'y': 40 - }, - { - 'x': 1413577800000, - 'y': 42 - }, - { - 'x': 1413578400000, - 'y': 29 - }, - { - 'x': 1413579000000, - 'y': 24 - }, - { - 'x': 1413579600000, - 'y': 30 - }, - { - 'x': 1413580200000, - 'y': 18 - }, - { - 'x': 1413580800000, - 'y': 15 - }, - { - 'x': 1413581400000, - 'y': 19 - }, - { - 'x': 1413582000000, - 'y': 18 - }, - { - 'x': 1413582600000, - 'y': 13 - }, - { - 'x': 1413583200000, - 'y': 11 - }, - { - 'x': 1413583800000, - 'y': 11 - }, - { - 'x': 1413584400000, - 'y': 13 - }, - { - 'x': 1413585000000, - 'y': 9 - }, - { - 'x': 1413585600000, - 'y': 9 - }, - { - 'x': 1413586200000, - 'y': 9 - }, - { - 'x': 1413586800000, - 'y': 3 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1413543600000, - 'y': 35 - }, - { - 'x': 1413544200000, - 'y': 360 - }, - { - 'x': 1413544800000, - 'y': 343 - }, - { - 'x': 1413545400000, - 'y': 329 - }, - { - 'x': 1413546000000, - 'y': 345 - }, - { - 'x': 1413546600000, - 'y': 336 - }, - { - 'x': 1413547200000, - 'y': 330 - }, - { - 'x': 1413547800000, - 'y': 334 - }, - { - 'x': 1413548400000, - 'y': 326 - }, - { - 'x': 1413549000000, - 'y': 351 - }, - { - 'x': 1413549600000, - 'y': 334 - }, - { - 'x': 1413550200000, - 'y': 351 - }, - { - 'x': 1413550800000, - 'y': 337 - }, - { - 'x': 1413551400000, - 'y': 306 - }, - { - 'x': 1413552000000, - 'y': 346 - }, - { - 'x': 1413552600000, - 'y': 317 - }, - { - 'x': 1413553200000, - 'y': 298 - }, - { - 'x': 1413553800000, - 'y': 288 - }, - { - 'x': 1413554400000, - 'y': 283 - }, - { - 'x': 1413555000000, - 'y': 262 - }, - { - 'x': 1413555600000, - 'y': 245 - }, - { - 'x': 1413556200000, - 'y': 259 - }, - { - 'x': 1413556800000, - 'y': 267 - }, - { - 'x': 1413557400000, - 'y': 230 - }, - { - 'x': 1413558000000, - 'y': 218 - }, - { - 'x': 1413558600000, - 'y': 241 - }, - { - 'x': 1413559200000, - 'y': 213 - }, - { - 'x': 1413559800000, - 'y': 239 - }, - { - 'x': 1413560400000, - 'y': 208 - }, - { - 'x': 1413561000000, - 'y': 187 - }, - { - 'x': 1413561600000, - 'y': 166 - }, - { - 'x': 1413562200000, - 'y': 154 - }, - { - 'x': 1413562800000, - 'y': 184 - }, - { - 'x': 1413563400000, - 'y': 148 - }, - { - 'x': 1413564000000, - 'y': 153 - }, - { - 'x': 1413564600000, - 'y': 149 - }, - { - 'x': 1413565200000, - 'y': 102 - }, - { - 'x': 1413565800000, - 'y': 110 - }, - { - 'x': 1413566400000, - 'y': 121 - }, - { - 'x': 1413567000000, - 'y': 120 - }, - { - 'x': 1413567600000, - 'y': 86 - }, - { - 'x': 1413568200000, - 'y': 96 - }, - { - 'x': 1413568800000, - 'y': 71 - }, - { - 'x': 1413569400000, - 'y': 92 - }, - { - 'x': 1413570000000, - 'y': 65 - }, - { - 'x': 1413570600000, - 'y': 54 - }, - { - 'x': 1413571200000, - 'y': 68 - }, - { - 'x': 1413571800000, - 'y': 57 - }, - { - 'x': 1413572400000, - 'y': 33 - }, - { - 'x': 1413573600000, - 'y': 47 - }, - { - 'x': 1413574200000, - 'y': 42 - }, - { - 'x': 1413574800000, - 'y': 39 - }, - { - 'x': 1413575400000, - 'y': 25 - }, - { - 'x': 1413576000000, - 'y': 31 - }, - { - 'x': 1413576600000, - 'y': 37 - }, - { - 'x': 1413577200000, - 'y': 35 - }, - { - 'x': 1413577800000, - 'y': 19 - }, - { - 'x': 1413578400000, - 'y': 15 - }, - { - 'x': 1413579000000, - 'y': 21 - }, - { - 'x': 1413579600000, - 'y': 16 - }, - { - 'x': 1413580200000, - 'y': 18 - }, - { - 'x': 1413580800000, - 'y': 10 - }, - { - 'x': 1413581400000, - 'y': 13 - }, - { - 'x': 1413582000000, - 'y': 14 - }, - { - 'x': 1413582600000, - 'y': 11 - }, - { - 'x': 1413583200000, - 'y': 4 - }, - { - 'x': 1413583800000, - 'y': 6 - }, - { - 'x': 1413584400000, - 'y': 3 - }, - { - 'x': 1413585000000, - 'y': 6 - }, - { - 'x': 1413585600000, - 'y': 6 - }, - { - 'x': 1413586200000, - 'y': 2 - }, - { - 'x': 1413586800000, - 'y': 3 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1413543600000, - 'y': 21 - }, - { - 'x': 1413544200000, - 'y': 191 - }, - { - 'x': 1413544800000, - 'y': 176 - }, - { - 'x': 1413545400000, - 'y': 166 - }, - { - 'x': 1413546000000, - 'y': 183 - }, - { - 'x': 1413546600000, - 'y': 170 - }, - { - 'x': 1413547200000, - 'y': 153 - }, - { - 'x': 1413547800000, - 'y': 202 - }, - { - 'x': 1413548400000, - 'y': 175 - }, - { - 'x': 1413549000000, - 'y': 161 - }, - { - 'x': 1413549600000, - 'y': 174 - }, - { - 'x': 1413550200000, - 'y': 167 - }, - { - 'x': 1413550800000, - 'y': 171 - }, - { - 'x': 1413551400000, - 'y': 176 - }, - { - 'x': 1413552000000, - 'y': 139 - }, - { - 'x': 1413552600000, - 'y': 145 - }, - { - 'x': 1413553200000, - 'y': 157 - }, - { - 'x': 1413553800000, - 'y': 148 - }, - { - 'x': 1413554400000, - 'y': 149 - }, - { - 'x': 1413555000000, - 'y': 135 - }, - { - 'x': 1413555600000, - 'y': 118 - }, - { - 'x': 1413556200000, - 'y': 142 - }, - { - 'x': 1413556800000, - 'y': 141 - }, - { - 'x': 1413557400000, - 'y': 146 - }, - { - 'x': 1413558000000, - 'y': 114 - }, - { - 'x': 1413558600000, - 'y': 115 - }, - { - 'x': 1413559200000, - 'y': 136 - }, - { - 'x': 1413559800000, - 'y': 106 - }, - { - 'x': 1413560400000, - 'y': 92 - }, - { - 'x': 1413561000000, - 'y': 97 - }, - { - 'x': 1413561600000, - 'y': 90 - }, - { - 'x': 1413562200000, - 'y': 69 - }, - { - 'x': 1413562800000, - 'y': 66 - }, - { - 'x': 1413563400000, - 'y': 93 - }, - { - 'x': 1413564000000, - 'y': 75 - }, - { - 'x': 1413564600000, - 'y': 68 - }, - { - 'x': 1413565200000, - 'y': 55 - }, - { - 'x': 1413565800000, - 'y': 73 - }, - { - 'x': 1413566400000, - 'y': 57 - }, - { - 'x': 1413567000000, - 'y': 48 - }, - { - 'x': 1413567600000, - 'y': 41 - }, - { - 'x': 1413568200000, - 'y': 39 - }, - { - 'x': 1413568800000, - 'y': 32 - }, - { - 'x': 1413569400000, - 'y': 33 - }, - { - 'x': 1413570000000, - 'y': 39 - }, - { - 'x': 1413570600000, - 'y': 35 - }, - { - 'x': 1413571200000, - 'y': 25 - }, - { - 'x': 1413571800000, - 'y': 28 - }, - { - 'x': 1413572400000, - 'y': 8 - }, - { - 'x': 1413573600000, - 'y': 13 - }, - { - 'x': 1413574200000, - 'y': 23 - }, - { - 'x': 1413574800000, - 'y': 19 - }, - { - 'x': 1413575400000, - 'y': 16 - }, - { - 'x': 1413576000000, - 'y': 22 - }, - { - 'x': 1413576600000, - 'y': 13 - }, - { - 'x': 1413577200000, - 'y': 21 - }, - { - 'x': 1413577800000, - 'y': 11 - }, - { - 'x': 1413578400000, - 'y': 12 - }, - { - 'x': 1413579000000, - 'y': 10 - }, - { - 'x': 1413579600000, - 'y': 7 - }, - { - 'x': 1413580200000, - 'y': 4 - }, - { - 'x': 1413580800000, - 'y': 5 - }, - { - 'x': 1413581400000, - 'y': 7 - }, - { - 'x': 1413582000000, - 'y': 9 - }, - { - 'x': 1413582600000, - 'y': 2 - }, - { - 'x': 1413583200000, - 'y': 2 - }, - { - 'x': 1413583800000, - 'y': 4 - }, - { - 'x': 1413584400000, - 'y': 6 - }, - { - 'x': 1413585600000, - 'y': 2 - }, - { - 'x': 1413586200000, - 'y': 4 - }, - { - 'x': 1413586800000, - 'y': 4 - } - ] - } - ], - 'hits': 108970, - 'xAxisOrderedValues': [ - 1413543600000, - 1413544200000, - 1413544800000, - 1413545400000, - 1413546000000, - 1413546600000, - 1413547200000, - 1413547800000, - 1413548400000, - 1413549000000, - 1413549600000, - 1413550200000, - 1413550800000, - 1413551400000, - 1413552000000, - 1413552600000, - 1413553200000, - 1413553800000, - 1413554400000, - 1413555000000, - 1413555600000, - 1413556200000, - 1413556800000, - 1413557400000, - 1413558000000, - 1413558600000, - 1413559200000, - 1413559800000, - 1413560400000, - 1413561000000, - 1413561600000, - 1413562200000, - 1413562800000, - 1413563400000, - 1413564000000, - 1413564600000, - 1413565200000, - 1413565800000, - 1413566400000, - 1413567000000, - 1413567600000, - 1413568200000, - 1413568800000, - 1413569400000, - 1413570000000, - 1413570600000, - 1413571200000, - 1413571800000, - 1413572400000, - 1413573600000, - 1413574200000, - 1413574800000, - 1413575400000, - 1413576000000, - 1413576600000, - 1413577200000, - 1413577800000, - 1413578400000, - 1413579000000, - 1413579600000, - 1413580200000, - 1413580800000, - 1413581400000, - 1413582000000, - 1413582600000, - 1413583200000, - 1413583800000, - 1413584400000, - 1413585000000, - 1413585600000, - 1413586200000, - 1413586800000, - ], - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_columns.js deleted file mode 100644 index 041fad2ed15b9..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_columns.js +++ /dev/null @@ -1,112 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 10379 - }, - { - 'x': 'png', - 'y': 6395 - } - ] - } - ], - 'xAxisOrderedValues': ['css', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 9253 - }, - { - 'x': 'png', - 'y': 5571 - } - ] - } - ], - 'xAxisOrderedValues': ['css', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 7740 - }, - { - 'x': 'png', - 'y': 4697 - } - ] - } - ], - 'xAxisOrderedValues': ['css', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171443 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_rows.js deleted file mode 100644 index cc4f598c7b1b7..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_rows.js +++ /dev/null @@ -1,109 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': '200: response', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 25260 - }, - { - 'x': 'png', - 'y': 15311 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '404: response', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 1352 - }, - { - 'x': 'png', - 'y': 826 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '503: response', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 761 - }, - { - 'x': 'png', - 'y': 527 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171443 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_series.js deleted file mode 100644 index 2a2d14d59b67e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/filters/_series.js +++ /dev/null @@ -1,42 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'filters', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'css', - 'y': 27374 - }, - { - 'x': 'html', - 'y': 0 - }, - { - 'x': 'png', - 'y': 16663 - } - ] - } - ], - 'hits': 171454, - 'xAxisOrderedValues': ['css', 'html', 'png'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_columns.js deleted file mode 100644 index d283d79315177..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_columns.js +++ /dev/null @@ -1,3745 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'title': 'Top 2 geo.dest: CN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 42, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 42, - 'value': 42, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 25, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 25, - 'value': 25, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 22, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 22, - 'value': 22, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 22, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 22, - 'value': 22, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 21, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 21, - 'value': 21, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 19, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 19, - 'value': 19, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 18, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 18, - 'value': 18, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - 0 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '5', - 'center': [ - -22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '5', - 'value': '5', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -90 - ], - [ - 0, - -90 - ], - [ - 0, - -45 - ], - [ - -45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '3', - 'center': [ - -112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '3', - 'value': '3', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - -45 - ], - [ - -90, - -45 - ], - [ - -90, - 0 - ], - [ - -135, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 42 - } - } - }, - { - 'label': 'Top 2 geo.dest: IN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 32, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 32, - 'value': 32, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 28, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 28, - 'value': 28, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 27, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 27, - 'value': 27, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 24, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 24, - 'value': 24, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 23, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 23, - 'value': 23, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 17, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 17, - 'value': 17, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 16, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 16, - 'value': 16, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 14, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 14, - 'value': 14, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 13, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 13, - 'value': 13, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 5, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 5, - 'value': 5, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'p', - 'center': [ - 157.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'p', - 'value': 'p', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -90 - ], - [ - 180, - -90 - ], - [ - 180, - -45 - ], - [ - 135, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - 0 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': false - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 32 - } - } - } - ] -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_geo_json.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_geo_json.js deleted file mode 100644 index 4e65502f8d278..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_geo_json.js +++ /dev/null @@ -1,1847 +0,0 @@ -import _ from 'lodash'; - -export default { - 'valueFormatter': _.identity, - 'geohashGridAgg': { 'vis': { 'params': {} } }, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 608, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 608, - 'value': 608, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 522, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 522, - 'value': 522, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 90 - ], - [ - 0, - 135 - ], - [ - 45, - 135 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 517, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 517, - 'value': 517, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 446, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 446, - 'value': 446, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -90 - ], - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 426, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 426, - 'value': 426, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 413, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 413, - 'value': 413, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - 45, - 90 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 362, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 362, - 'value': 362, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 352, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 352, - 'value': 352, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -135 - ], - [ - 0, - -90 - ], - [ - 45, - -90 - ], - [ - 45, - -135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 216, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 216, - 'value': 216, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 183, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 183, - 'value': 183, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 90, - 90 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 158, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 158, - 'value': 158, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 135 - ], - [ - -45, - 180 - ], - [ - 0, - 180 - ], - [ - 0, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 139, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 139, - 'value': 139, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 90 - ], - [ - 45, - 135 - ], - [ - 90, - 135 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 110, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 110, - 'value': 110, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -135 - ], - [ - 45, - -90 - ], - [ - 90, - -90 - ], - [ - 90, - -135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 101, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 101, - 'value': 101, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 90 - ], - [ - -45, - 135 - ], - [ - 0, - 135 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 101, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 101, - 'value': 101, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 92, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 92, - 'value': 92, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -90 - ], - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 75, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 75, - 'value': 75, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -180 - ], - [ - 45, - -135 - ], - [ - 90, - -135 - ], - [ - 90, - -180 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 64, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 64, - 'value': 64, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 36, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 36, - 'value': 36, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 135 - ], - [ - 45, - 180 - ], - [ - 90, - 180 - ], - [ - 90, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 34, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 34, - 'value': 34, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 135 - ], - [ - 0, - 180 - ], - [ - 45, - 180 - ], - [ - 45, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - -90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 16, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 16, - 'value': 16, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - 0, - 90 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -67.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': '5', - 'center': [ - -22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '5', - 'value': '5', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'p', - 'center': [ - 157.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'p', - 'value': 'p', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 135 - ], - [ - -90, - 180 - ], - [ - -45, - 180 - ], - [ - -45, - 135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - -22.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': '2', - 'center': [ - -157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '2', - 'value': '2', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -180 - ], - [ - -45, - -135 - ], - [ - 0, - -135 - ], - [ - 0, - -180 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -67.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'h', - 'center': [ - 22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'h', - 'value': 'h', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'n', - 'center': [ - 112.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'n', - 'value': 'n', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 90 - ], - [ - -90, - 135 - ], - [ - -45, - 135 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'j', - 'center': [ - 67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': 'j', - 'value': 'j', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -45, - 90 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '3', - 'center': [ - -112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '3', - 'value': '3', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -135 - ], - [ - -45, - -90 - ], - [ - 0, - -90 - ], - [ - 0, - -135 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '1', - 'center': [ - -112.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - 'key': '1', - 'value': '1', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -135 - ], - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -135 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 608, - 'zoom': 2, - 'center': [5, 15] - } - }, -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_rows.js deleted file mode 100644 index 64deea0e391a6..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/geohash/_rows.js +++ /dev/null @@ -1,3667 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'title': 'Top 2 geo.dest: CN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 39, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 39, - 'value': 39, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 25, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 25, - 'value': 25, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 23, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 23, - 'value': 23, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 23, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 23, - 'value': 23, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 22, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 22, - 'value': 22, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 20, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 20, - 'value': 20, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 18, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 18, - 'value': 18, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 10, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 10, - 'value': 10, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 8, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 8, - 'value': 8, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '5', - 'center': [ - -22.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '5', - 'value': '5', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -90 - ], - [ - 0, - -90 - ], - [ - 0, - -45 - ], - [ - -45, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '3', - 'center': [ - -112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'CN', - 'value': 'CN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '3', - 'value': '3', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - -45 - ], - [ - -90, - -45 - ], - [ - -90, - 0 - ], - [ - -135, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 39 - } - } - }, - { - 'label': 'Top 2 geo.dest: IN', - 'valueFormatter': _.identity, - 'geoJson': { - 'type': 'FeatureCollection', - 'features': [ - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -22.5 - ] - }, - 'properties': { - 'value': 31, - 'geohash': '6', - 'center': [ - -67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '6', - 'value': '6', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 31, - 'value': 31, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -45 - ], - [ - -45, - -45 - ], - [ - -45, - 0 - ], - [ - -90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 22.5 - ] - }, - 'properties': { - 'value': 30, - 'geohash': 's', - 'center': [ - 22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 's', - 'value': 's', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 30, - 'value': 30, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 0 - ], - [ - 45, - 0 - ], - [ - 45, - 45 - ], - [ - 0, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 22.5 - ] - }, - 'properties': { - 'value': 29, - 'geohash': 'w', - 'center': [ - 112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'w', - 'value': 'w', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 29, - 'value': 29, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 0 - ], - [ - 135, - 0 - ], - [ - 135, - 45 - ], - [ - 90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 22.5 - ] - }, - 'properties': { - 'value': 28, - 'geohash': 'd', - 'center': [ - -67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'd', - 'value': 'd', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 28, - 'value': 28, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 0 - ], - [ - -45, - 0 - ], - [ - -45, - 45 - ], - [ - -90, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 22.5 - ] - }, - 'properties': { - 'value': 25, - 'geohash': 't', - 'center': [ - 67.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 't', - 'value': 't', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 25, - 'value': 25, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 0 - ], - [ - 90, - 0 - ], - [ - 90, - 45 - ], - [ - 45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - -22.5 - ] - }, - 'properties': { - 'value': 24, - 'geohash': 'k', - 'center': [ - 22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'k', - 'value': 'k', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 24, - 'value': 24, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - -45 - ], - [ - 45, - -45 - ], - [ - 45, - 0 - ], - [ - 0, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 22.5, - 67.5 - ] - }, - 'properties': { - 'value': 20, - 'geohash': 'u', - 'center': [ - 22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'u', - 'value': 'u', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 20, - 'value': 20, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 0, - 45 - ], - [ - 45, - 45 - ], - [ - 45, - 90 - ], - [ - 0, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 22.5 - ] - }, - 'properties': { - 'value': 18, - 'geohash': '9', - 'center': [ - -112.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '9', - 'value': '9', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 18, - 'value': 18, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 0 - ], - [ - -90, - 0 - ], - [ - -90, - 45 - ], - [ - -135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - 67.5 - ] - }, - 'properties': { - 'value': 14, - 'geohash': 'v', - 'center': [ - 67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'v', - 'value': 'v', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 14, - 'value': 14, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - 45 - ], - [ - 90, - 45 - ], - [ - 90, - 90 - ], - [ - 45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 22.5 - ] - }, - 'properties': { - 'value': 11, - 'geohash': 'e', - 'center': [ - -22.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'e', - 'value': 'e', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 11, - 'value': 11, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 0 - ], - [ - 0, - 0 - ], - [ - 0, - 45 - ], - [ - -45, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -22.5 - ] - }, - 'properties': { - 'value': 9, - 'geohash': 'r', - 'center': [ - 157.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'r', - 'value': 'r', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 9, - 'value': 9, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -45 - ], - [ - 180, - -45 - ], - [ - 180, - 0 - ], - [ - 135, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'y', - 'center': [ - 112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'y', - 'value': 'y', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - 45 - ], - [ - 135, - 45 - ], - [ - 135, - 90 - ], - [ - 90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - 67.5 - ] - }, - 'properties': { - 'value': 6, - 'geohash': 'f', - 'center': [ - -67.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'f', - 'value': 'f', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 6, - 'value': 6, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - 45 - ], - [ - -45, - 45 - ], - [ - -45, - 90 - ], - [ - -90, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - 67.5 - ] - }, - 'properties': { - 'value': 5, - 'geohash': 'g', - 'center': [ - -22.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'g', - 'value': 'g', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 5, - 'value': 5, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - 45 - ], - [ - 0, - 45 - ], - [ - 0, - 90 - ], - [ - -45, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -112.5, - 67.5 - ] - }, - 'properties': { - 'value': 5, - 'geohash': 'c', - 'center': [ - -112.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'c', - 'value': 'c', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 5, - 'value': 5, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -135, - 45 - ], - [ - -90, - 45 - ], - [ - -90, - 90 - ], - [ - -135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -157.5, - 67.5 - ] - }, - 'properties': { - 'value': 4, - 'geohash': 'b', - 'center': [ - -157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'b', - 'value': 'b', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 4, - 'value': 4, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -180, - 45 - ], - [ - -135, - 45 - ], - [ - -135, - 90 - ], - [ - -180, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 112.5, - -22.5 - ] - }, - 'properties': { - 'value': 3, - 'geohash': 'q', - 'center': [ - 112.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'q', - 'value': 'q', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 3, - 'value': 3, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 90, - -45 - ], - [ - 135, - -45 - ], - [ - 135, - 0 - ], - [ - 90, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -67.5, - -67.5 - ] - }, - 'properties': { - 'value': 2, - 'geohash': '4', - 'center': [ - -67.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '4', - 'value': '4', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 2, - 'value': 2, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -90, - -90 - ], - [ - -45, - -90 - ], - [ - -45, - -45 - ], - [ - -90, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'z', - 'center': [ - 157.5, - 67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'z', - 'value': 'z', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 45 - ], - [ - 180, - 45 - ], - [ - 180, - 90 - ], - [ - 135, - 90 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - 22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'x', - 'center': [ - 157.5, - 22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'x', - 'value': 'x', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - 0 - ], - [ - 180, - 0 - ], - [ - 180, - 45 - ], - [ - 135, - 45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 157.5, - -67.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'p', - 'center': [ - 157.5, - -67.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'p', - 'value': 'p', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 135, - -90 - ], - [ - 180, - -90 - ], - [ - 180, - -45 - ], - [ - 135, - -45 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - 67.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': 'm', - 'center': [ - 67.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': 'm', - 'value': 'm', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - 45, - -45 - ], - [ - 90, - -45 - ], - [ - 90, - 0 - ], - [ - 45, - 0 - ] - ] - } - }, - { - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': [ - -22.5, - -22.5 - ] - }, - 'properties': { - 'value': 1, - 'geohash': '7', - 'center': [ - -22.5, - -22.5 - ], - 'aggConfigResult': { - '$parent': { - '$parent': { - '$parent': null, - 'key': 'IN', - 'value': 'IN', - 'aggConfig': { - 'id': '3', - 'type': 'terms', - 'schema': 'split', - 'params': { - 'field': 'geo.dest', - 'size': 2, - 'order': 'desc', - 'orderBy': '1', - 'row': true - } - }, - 'type': 'bucket' - }, - 'key': '7', - 'value': '7', - 'aggConfig': { - 'id': '2', - 'type': 'geohash_grid', - 'schema': 'segment', - 'params': { - 'field': 'geo.coordinates', - 'precision': 1 - } - }, - 'type': 'bucket' - }, - 'key': 1, - 'value': 1, - 'aggConfig': { - 'id': '1', - 'type': 'count', - 'schema': 'metric', - 'params': {} - }, - 'type': 'metric' - }, - 'rectangle': [ - [ - -45, - -45 - ], - [ - 0, - -45 - ], - [ - 0, - 0 - ], - [ - -45, - 0 - ] - ] - } - } - ], - 'properties': { - 'min': 1, - 'max': 31 - } - } - } - ], - 'hits': 1639 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_columns.js deleted file mode 100644 index 96d2cfd174579..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_columns.js +++ /dev/null @@ -1,368 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': '404: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 1, - 'y0': 0 - }, - { - 'x': 3221225400, - 'y': 0, - 'y0': 0 - }, - { - 'x': 4294967200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 5368709100, - 'y': 0, - 'y0': 0 - }, - { - 'x': 6442450900, - 'y': 0, - 'y0': 0 - }, - { - 'x': 7516192700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 8589934500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 10737418200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 11811160000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 12884901800, - 'y': 1, - 'y0': 0 - }, - { - 'x': 13958643700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 15032385500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 16106127300, - 'y': 0, - 'y0': 0 - }, - { - 'x': 18253611000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 19327352800, - 'y': 0, - 'y0': 0 - }, - { - 'x': 20401094600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 21474836400, - 'y': 0, - 'y0': 0 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '200: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 3221225400, - 'y': 2, - 'y0': 0 - }, - { - 'x': 4294967200, - 'y': 3, - 'y0': 0 - }, - { - 'x': 5368709100, - 'y': 3, - 'y0': 0 - }, - { - 'x': 6442450900, - 'y': 1, - 'y0': 0 - }, - { - 'x': 7516192700, - 'y': 1, - 'y0': 0 - }, - { - 'x': 8589934500, - 'y': 4, - 'y0': 0 - }, - { - 'x': 10737418200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 11811160000, - 'y': 1, - 'y0': 0 - }, - { - 'x': 12884901800, - 'y': 1, - 'y0': 0 - }, - { - 'x': 13958643700, - 'y': 1, - 'y0': 0 - }, - { - 'x': 15032385500, - 'y': 2, - 'y0': 0 - }, - { - 'x': 16106127300, - 'y': 3, - 'y0': 0 - }, - { - 'x': 18253611000, - 'y': 4, - 'y0': 0 - }, - { - 'x': 19327352800, - 'y': 5, - 'y0': 0 - }, - { - 'x': 20401094600, - 'y': 2, - 'y0': 0 - }, - { - 'x': 21474836400, - 'y': 2, - 'y0': 0 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '503: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 3221225400, - 'y': 0, - 'y0': 0 - }, - { - 'x': 4294967200, - 'y': 0, - 'y0': 0 - }, - { - 'x': 5368709100, - 'y': 0, - 'y0': 0 - }, - { - 'x': 6442450900, - 'y': 0, - 'y0': 0 - }, - { - 'x': 7516192700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 8589934500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 10737418200, - 'y': 1, - 'y0': 0 - }, - { - 'x': 11811160000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 12884901800, - 'y': 0, - 'y0': 0 - }, - { - 'x': 13958643700, - 'y': 0, - 'y0': 0 - }, - { - 'x': 15032385500, - 'y': 0, - 'y0': 0 - }, - { - 'x': 16106127300, - 'y': 0, - 'y0': 0 - }, - { - 'x': 18253611000, - 'y': 0, - 'y0': 0 - }, - { - 'x': 19327352800, - 'y': 0, - 'y0': 0 - }, - { - 'x': 20401094600, - 'y': 0, - 'y0': 0 - }, - { - 'x': 21474836400, - 'y': 0, - 'y0': 0 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': [ - 2147483600, - 3221225400, - 4294967200, - 5368709100, - 6442450900, - 7516192700, - 8589934500, - 10737418200, - 11811160000, - 12884901800, - 13958643700, - 15032385500, - 16106127300, - 18253611000, - 19327352800, - 20401094600, - 21474836400, - ], - 'hits': 40 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_rows.js deleted file mode 100644 index 27050030ebdfd..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_rows.js +++ /dev/null @@ -1,212 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': '404: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 2147483600, - 'y': 1 - }, - { - 'x': 10737418200, - 'y': 1 - }, - { - 'x': 15032385500, - 'y': 2 - }, - { - 'x': 19327352800, - 'y': 1 - }, - { - 'x': 32212254700, - 'y': 1 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '200: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 3221225400, - 'y': 4 - }, - { - 'x': 4294967200, - 'y': 3 - }, - { - 'x': 5368709100, - 'y': 3 - }, - { - 'x': 6442450900, - 'y': 2 - }, - { - 'x': 7516192700, - 'y': 2 - }, - { - 'x': 8589934500, - 'y': 2 - }, - { - 'x': 9663676400, - 'y': 3 - }, - { - 'x': 11811160000, - 'y': 3 - }, - { - 'x': 12884901800, - 'y': 2 - }, - { - 'x': 13958643700, - 'y': 1 - }, - { - 'x': 15032385500, - 'y': 2 - }, - { - 'x': 16106127300, - 'y': 3 - }, - { - 'x': 17179869100, - 'y': 1 - }, - { - 'x': 18253611000, - 'y': 4 - }, - { - 'x': 19327352800, - 'y': 1 - }, - { - 'x': 20401094600, - 'y': 1 - }, - { - 'x': 21474836400, - 'y': 4 - }, - { - 'x': 32212254700, - 'y': 3 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '503: response', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 10737418200, - 'y': 1 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': [ - 2147483600, - 3221225400, - 4294967200, - 5368709100, - 6442450900, - 7516192700, - 8589934500, - 9663676400, - 10737418200, - 11811160000, - 12884901800, - 13958643700, - 15032385500, - 16106127300, - 17179869100, - 18253611000, - 19327352800, - 20401094600, - 21474836400, - 32212254700, - ], - 'hits': 51 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_series.js deleted file mode 100644 index 5c7554db2060d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_series.js +++ /dev/null @@ -1,124 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'machine.ram', - 'ordered': { - 'interval': 100 - }, - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 3221225400, - 'y': 5 - }, - { - 'x': 4294967200, - 'y': 2 - }, - { - 'x': 5368709100, - 'y': 5 - }, - { - 'x': 6442450900, - 'y': 4 - }, - { - 'x': 7516192700, - 'y': 1 - }, - { - 'x': 9663676400, - 'y': 9 - }, - { - 'x': 10737418200, - 'y': 5 - }, - { - 'x': 11811160000, - 'y': 5 - }, - { - 'x': 12884901800, - 'y': 2 - }, - { - 'x': 13958643700, - 'y': 3 - }, - { - 'x': 15032385500, - 'y': 3 - }, - { - 'x': 16106127300, - 'y': 3 - }, - { - 'x': 17179869100, - 'y': 1 - }, - { - 'x': 18253611000, - 'y': 6 - }, - { - 'x': 19327352800, - 'y': 3 - }, - { - 'x': 20401094600, - 'y': 3 - }, - { - 'x': 21474836400, - 'y': 7 - }, - { - 'x': 32212254700, - 'y': 4 - } - ] - } - ], - 'hits': 71, - 'xAxisOrderedValues': [ - 3221225400, - 4294967200, - 5368709100, - 6442450900, - 7516192700, - 9663676400, - 10737418200, - 11811160000, - 12884901800, - 13958643700, - 15032385500, - 16106127300, - 17179869100, - 18253611000, - 19327352800, - 20401094600, - 21474836400, - 32212254700, - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_slices.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_slices.js deleted file mode 100644 index c47155840cec5..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/histogram/_slices.js +++ /dev/null @@ -1,309 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'slices': { - 'children': [ - { - 'name': 0, - 'size': 378611, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 1000, - 'size': 205997, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 2000, - 'size': 397189, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 3000, - 'size': 397195, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 4000, - 'size': 398429, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 5000, - 'size': 397843, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 6000, - 'size': 398140, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 7000, - 'size': 398076, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 8000, - 'size': 396746, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 9000, - 'size': 397418, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 10000, - 'size': 20222, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 11000, - 'size': 20173, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 12000, - 'size': 20026, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 13000, - 'size': 19986, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 14000, - 'size': 20091, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 15000, - 'size': 20052, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 16000, - 'size': 20349, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 17000, - 'size': 20290, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 18000, - 'size': 20399, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 19000, - 'size': 20133, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - }, - { - 'name': 20000, - 'size': 9, - 'aggConfig': { - 'type': 'histogram', - 'schema': 'segment', - 'fieldFormatter': _.constant(String), - 'params': { - 'interval': 1000, - 'extended_bounds': {} - } - } - } - ] - }, - 'names': [ - 0, - 1000, - 2000, - 3000, - 4000, - 5000, - 6000, - 7000, - 8000, - 9000, - 10000, - 11000, - 12000, - 13000, - 14000, - 15000, - 16000, - 17000, - 18000, - 19000, - 20000 - ], - 'hits': 3967374, - 'tooltipFormatter': function (event) { - return event.point; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/not_enough_data/_one_point.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/not_enough_data/_one_point.js deleted file mode 100644 index b05e258133963..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/not_enough_data/_one_point.js +++ /dev/null @@ -1,34 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': '', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '_all', - 'y': 274 - } - ] - } - ], - 'hits': 274, - 'xAxisOrderedValues': ['_all'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_columns.js deleted file mode 100644 index d6f3fc4361f32..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_columns.js +++ /dev/null @@ -1,62 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'apache: _type', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 13309 - }, - { - 'x': '1000.0-2000.0', - 'y': 7196 - } - ] - } - ] - }, - { - 'label': 'nginx: _type', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 3278 - }, - { - 'x': '1000.0-2000.0', - 'y': 1804 - } - ] - } - ] - } - ], - 'hits': 171499, - 'xAxisOrderedValues': ['0.0-1000.0', '1000.0-2000.0'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_rows.js deleted file mode 100644 index b420565b1c96b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_rows.js +++ /dev/null @@ -1,88 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 6422, - 'y0': 0 - }, - { - 'x': '1000.0-2000.0', - 'y': 3446, - 'y0': 0 - } - ] - } - ] - }, - { - 'label': 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 5430, - 'y0': 0 - }, - { - 'x': '1000.0-2000.0', - 'y': 3010, - 'y0': 0 - } - ] - } - ] - }, - { - 'label': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 4735, - 'y0': 0 - }, - { - 'x': '1000.0-2000.0', - 'y': 2542, - 'y0': 0 - } - ] - } - ] - } - ], - 'hits': 171501, - 'xAxisOrderedValues': ['0.0-1000.0', '1000.0-2000.0'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_series.js deleted file mode 100644 index 2ac35efadc8f2..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/range/_series.js +++ /dev/null @@ -1,38 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'bytes ranges', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': '0.0-1000.0', - 'y': 16576 - }, - { - 'x': '1000.0-2000.0', - 'y': 9005 - } - ] - } - ], - 'hits': 171500, - 'xAxisOrderedValues': ['0.0-1000.0', '1000.0-2000.0'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_columns.js deleted file mode 100644 index 5b1e29ac0d54a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_columns.js +++ /dev/null @@ -1,242 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'http: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128237 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10258 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'info: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 108148 - }, - { - 'x': 'info', - 'y': 96242 - }, - { - 'x': 'security', - 'y': 25889 - }, - { - 'x': 'error', - 'y': 7673 - }, - { - 'x': 'warning', - 'y': 12842 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'www.slate.com: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 98056 - }, - { - 'x': 'info', - 'y': 87344 - }, - { - 'x': 'security', - 'y': 23577 - }, - { - 'x': 'error', - 'y': 7004 - }, - { - 'x': 'warning', - 'y': 11759 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'twitter.com: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 74154 - }, - { - 'x': 'info', - 'y': 65963 - }, - { - 'x': 'security', - 'y': 17832 - }, - { - 'x': 'error', - 'y': 5258 - }, - { - 'x': 'warning', - 'y': 8906 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'www.www.slate.com: links', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 62591 - }, - { - 'x': 'info', - 'y': 55822 - }, - { - 'x': 'security', - 'y': 15100 - }, - { - 'x': 'error', - 'y': 4564 - }, - { - 'x': 'warning', - 'y': 7498 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171446 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_rows.js deleted file mode 100644 index 147eb691eb67b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_rows.js +++ /dev/null @@ -1,242 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': 'h3: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128235 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10257 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'h5: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128235 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10257 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'http: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 144000 - }, - { - 'x': 'info', - 'y': 128235 - }, - { - 'x': 'security', - 'y': 34518 - }, - { - 'x': 'error', - 'y': 10257 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'success: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 120689 - }, - { - 'x': 'info', - 'y': 107621 - }, - { - 'x': 'security', - 'y': 28916 - }, - { - 'x': 'error', - 'y': 8590 - }, - { - 'x': 'warning', - 'y': 14548 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': 'www.slate.com: headings', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 62292 - }, - { - 'x': 'info', - 'y': 55646 - }, - { - 'x': 'security', - 'y': 14823 - }, - { - 'x': 'error', - 'y': 4441 - }, - { - 'x': 'warning', - 'y': 7539 - } - ] - } - ], - 'xAxisOrderedValues': ['success', 'info', 'security', 'error', 'warning'], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'hits': 171445 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_series.js deleted file mode 100644 index 3691d854c6c2a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/significant_terms/_series.js +++ /dev/null @@ -1,49 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'Top 5 unusual terms in @tags', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'success', - 'y': 143995 - }, - { - 'x': 'info', - 'y': 128233 - }, - { - 'x': 'security', - 'y': 34515 - }, - { - 'x': 'error', - 'y': 10256 - }, - { - 'x': 'warning', - 'y': 17188 - } - ] - } - ], - 'hits': 171439, - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/stacked/_stacked.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/stacked/_stacked.js deleted file mode 100644 index 228a22ed534d5..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/stacked/_stacked.js +++ /dev/null @@ -1,1635 +0,0 @@ -import moment from 'moment'; - -export default { - 'label': '', - 'xAxisLabel': '@timestamp per 30 sec', - 'ordered': { - 'date': true, - 'interval': 30000, - 'min': 1416850340336, - 'max': 1416852140336 - }, - 'yAxisLabel': 'Count of documents', - 'xAxisOrderedValues': [ - 1416850320000, - 1416850350000, - 1416850380000, - 1416850410000, - 1416850440000, - 1416850470000, - 1416850500000, - 1416850530000, - 1416850560000, - 1416850590000, - 1416850620000, - 1416850650000, - 1416850680000, - 1416850710000, - 1416850740000, - 1416850770000, - 1416850800000, - 1416850830000, - 1416850860000, - 1416850890000, - 1416850920000, - 1416850950000, - 1416850980000, - 1416851010000, - 1416851040000, - 1416851070000, - 1416851100000, - 1416851130000, - 1416851160000, - 1416851190000, - 1416851220000, - 1416851250000, - 1416851280000, - 1416851310000, - 1416851340000, - 1416851370000, - 1416851400000, - 1416851430000, - 1416851460000, - 1416851490000, - 1416851520000, - 1416851550000, - 1416851580000, - 1416851610000, - 1416851640000, - 1416851670000, - 1416851700000, - 1416851730000, - 1416851760000, - 1416851790000, - 1416851820000, - 1416851850000, - 1416851880000, - 1416851910000, - 1416851940000, - 1416851970000, - 1416852000000, - 1416852030000, - 1416852060000, - 1416852090000, - 1416852120000, - ], - 'series': [ - { - 'label': 'jpg', - 'values': [ - { - 'x': 1416850320000, - 'y': 110, - 'y0': 0 - }, - { - 'x': 1416850350000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850380000, - 'y': 34, - 'y0': 0 - }, - { - 'x': 1416850410000, - 'y': 21, - 'y0': 0 - }, - { - 'x': 1416850440000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416850470000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850500000, - 'y': 16, - 'y0': 0 - }, - { - 'x': 1416850530000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416850560000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850590000, - 'y': 38, - 'y0': 0 - }, - { - 'x': 1416850620000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416850650000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416850680000, - 'y': 31, - 'y0': 0 - }, - { - 'x': 1416850710000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850740000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416850770000, - 'y': 38, - 'y0': 0 - }, - { - 'x': 1416850800000, - 'y': 34, - 'y0': 0 - }, - { - 'x': 1416850830000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416850860000, - 'y': 38, - 'y0': 0 - }, - { - 'x': 1416850890000, - 'y': 19, - 'y0': 0 - }, - { - 'x': 1416850920000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416850950000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416850980000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851010000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416851040000, - 'y': 22, - 'y0': 0 - }, - { - 'x': 1416851070000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851100000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416851130000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851160000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851190000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416851220000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851250000, - 'y': 36, - 'y0': 0 - }, - { - 'x': 1416851280000, - 'y': 32, - 'y0': 0 - }, - { - 'x': 1416851310000, - 'y': 29, - 'y0': 0 - }, - { - 'x': 1416851340000, - 'y': 22, - 'y0': 0 - }, - { - 'x': 1416851370000, - 'y': 29, - 'y0': 0 - }, - { - 'x': 1416851400000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416851430000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851460000, - 'y': 39, - 'y0': 0 - }, - { - 'x': 1416851490000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851520000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851550000, - 'y': 28, - 'y0': 0 - }, - { - 'x': 1416851580000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416851610000, - 'y': 29, - 'y0': 0 - }, - { - 'x': 1416851640000, - 'y': 30, - 'y0': 0 - }, - { - 'x': 1416851670000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416851700000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416851730000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416851760000, - 'y': 21, - 'y0': 0 - }, - { - 'x': 1416851790000, - 'y': 24, - 'y0': 0 - }, - { - 'x': 1416851820000, - 'y': 26, - 'y0': 0 - }, - { - 'x': 1416851850000, - 'y': 26, - 'y0': 0 - }, - { - 'x': 1416851880000, - 'y': 21, - 'y0': 0 - }, - { - 'x': 1416851910000, - 'y': 33, - 'y0': 0 - }, - { - 'x': 1416851940000, - 'y': 23, - 'y0': 0 - }, - { - 'x': 1416851970000, - 'y': 46, - 'y0': 0 - }, - { - 'x': 1416852000000, - 'y': 27, - 'y0': 0 - }, - { - 'x': 1416852030000, - 'y': 20, - 'y0': 0 - }, - { - 'x': 1416852060000, - 'y': 34, - 'y0': 0 - }, - { - 'x': 1416852090000, - 'y': 15, - 'y0': 0 - }, - { - 'x': 1416852120000, - 'y': 18, - 'y0': 0 - } - ] - }, - { - 'label': 'css', - 'values': [ - { - 'x': 1416850320000, - 'y': 3, - 'y0': 11 - }, - { - 'x': 1416850350000, - 'y': 13, - 'y0': 24 - }, - { - 'x': 1416850380000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416850410000, - 'y': 12, - 'y0': 21 - }, - { - 'x': 1416850440000, - 'y': 9, - 'y0': 32 - }, - { - 'x': 1416850470000, - 'y': 12, - 'y0': 24 - }, - { - 'x': 1416850500000, - 'y': 6, - 'y0': 16 - }, - { - 'x': 1416850530000, - 'y': 6, - 'y0': 27 - }, - { - 'x': 1416850560000, - 'y': 11, - 'y0': 24 - }, - { - 'x': 1416850590000, - 'y': 11, - 'y0': 38 - }, - { - 'x': 1416850620000, - 'y': 6, - 'y0': 33 - }, - { - 'x': 1416850650000, - 'y': 8, - 'y0': 33 - }, - { - 'x': 1416850680000, - 'y': 6, - 'y0': 31 - }, - { - 'x': 1416850710000, - 'y': 4, - 'y0': 24 - }, - { - 'x': 1416850740000, - 'y': 9, - 'y0': 24 - }, - { - 'x': 1416850770000, - 'y': 3, - 'y0': 38 - }, - { - 'x': 1416850800000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416850830000, - 'y': 6, - 'y0': 30 - }, - { - 'x': 1416850860000, - 'y': 9, - 'y0': 38 - }, - { - 'x': 1416850890000, - 'y': 5, - 'y0': 19 - }, - { - 'x': 1416850920000, - 'y': 8, - 'y0': 23 - }, - { - 'x': 1416850950000, - 'y': 9, - 'y0': 33 - }, - { - 'x': 1416850980000, - 'y': 5, - 'y0': 28 - }, - { - 'x': 1416851010000, - 'y': 6, - 'y0': 24 - }, - { - 'x': 1416851040000, - 'y': 9, - 'y0': 22 - }, - { - 'x': 1416851070000, - 'y': 9, - 'y0': 28 - }, - { - 'x': 1416851100000, - 'y': 11, - 'y0': 27 - }, - { - 'x': 1416851130000, - 'y': 5, - 'y0': 32 - }, - { - 'x': 1416851160000, - 'y': 8, - 'y0': 32 - }, - { - 'x': 1416851190000, - 'y': 6, - 'y0': 30 - }, - { - 'x': 1416851220000, - 'y': 10, - 'y0': 32 - }, - { - 'x': 1416851250000, - 'y': 5, - 'y0': 36 - }, - { - 'x': 1416851280000, - 'y': 6, - 'y0': 32 - }, - { - 'x': 1416851310000, - 'y': 4, - 'y0': 29 - }, - { - 'x': 1416851340000, - 'y': 8, - 'y0': 22 - }, - { - 'x': 1416851370000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416851400000, - 'y': 8, - 'y0': 33 - }, - { - 'x': 1416851430000, - 'y': 10, - 'y0': 28 - }, - { - 'x': 1416851460000, - 'y': 5, - 'y0': 39 - }, - { - 'x': 1416851490000, - 'y': 7, - 'y0': 28 - }, - { - 'x': 1416851520000, - 'y': 6, - 'y0': 28 - }, - { - 'x': 1416851550000, - 'y': 4, - 'y0': 28 - }, - { - 'x': 1416851580000, - 'y': 9, - 'y0': 30 - }, - { - 'x': 1416851610000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416851640000, - 'y': 9, - 'y0': 30 - }, - { - 'x': 1416851670000, - 'y': 6, - 'y0': 23 - }, - { - 'x': 1416851700000, - 'y': 11, - 'y0': 23 - }, - { - 'x': 1416851730000, - 'y': 4, - 'y0': 27 - }, - { - 'x': 1416851760000, - 'y': 8, - 'y0': 21 - }, - { - 'x': 1416851790000, - 'y': 5, - 'y0': 24 - }, - { - 'x': 1416851820000, - 'y': 7, - 'y0': 26 - }, - { - 'x': 1416851850000, - 'y': 7, - 'y0': 26 - }, - { - 'x': 1416851880000, - 'y': 4, - 'y0': 21 - }, - { - 'x': 1416851910000, - 'y': 8, - 'y0': 33 - }, - { - 'x': 1416851940000, - 'y': 6, - 'y0': 23 - }, - { - 'x': 1416851970000, - 'y': 6, - 'y0': 46 - }, - { - 'x': 1416852000000, - 'y': 3, - 'y0': 27 - }, - { - 'x': 1416852030000, - 'y': 6, - 'y0': 20 - }, - { - 'x': 1416852060000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416852090000, - 'y': 5, - 'y0': 15 - }, - { - 'x': 1416852120000, - 'y': 1, - 'y0': 18 - } - ] - }, - { - 'label': 'gif', - 'values': [ - { - 'x': 1416850320000, - 'y': 1, - 'y0': 14 - }, - { - 'x': 1416850350000, - 'y': 2, - 'y0': 37 - }, - { - 'x': 1416850380000, - 'y': 4, - 'y0': 39 - }, - { - 'x': 1416850410000, - 'y': 2, - 'y0': 33 - }, - { - 'x': 1416850440000, - 'y': 3, - 'y0': 41 - }, - { - 'x': 1416850470000, - 'y': 1, - 'y0': 36 - }, - { - 'x': 1416850500000, - 'y': 1, - 'y0': 22 - }, - { - 'x': 1416850530000, - 'y': 1, - 'y0': 33 - }, - { - 'x': 1416850560000, - 'y': 2, - 'y0': 35 - }, - { - 'x': 1416850590000, - 'y': 5, - 'y0': 49 - }, - { - 'x': 1416850620000, - 'y': 1, - 'y0': 39 - }, - { - 'x': 1416850650000, - 'y': 1, - 'y0': 41 - }, - { - 'x': 1416850680000, - 'y': 4, - 'y0': 37 - }, - { - 'x': 1416850710000, - 'y': 1, - 'y0': 28 - }, - { - 'x': 1416850740000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416850770000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416850800000, - 'y': 2, - 'y0': 39 - }, - { - 'x': 1416850830000, - 'y': 5, - 'y0': 36 - }, - { - 'x': 1416850860000, - 'y': 3, - 'y0': 47 - }, - { - 'x': 1416850890000, - 'y': 1, - 'y0': 24 - }, - { - 'x': 1416850920000, - 'y': 3, - 'y0': 31 - }, - { - 'x': 1416850950000, - 'y': 4, - 'y0': 42 - }, - { - 'x': 1416850980000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416851010000, - 'y': 5, - 'y0': 30 - }, - { - 'x': 1416851040000, - 'y': 2, - 'y0': 31 - }, - { - 'x': 1416851070000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851100000, - 'y': 5, - 'y0': 38 - }, - { - 'x': 1416851130000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851160000, - 'y': 4, - 'y0': 40 - }, - { - 'x': 1416851190000, - 'y': 9, - 'y0': 36 - }, - { - 'x': 1416851220000, - 'y': 7, - 'y0': 42 - }, - { - 'x': 1416851250000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416851280000, - 'y': 1, - 'y0': 38 - }, - { - 'x': 1416851310000, - 'y': 2, - 'y0': 33 - }, - { - 'x': 1416851340000, - 'y': 5, - 'y0': 30 - }, - { - 'x': 1416851370000, - 'y': 3, - 'y0': 32 - }, - { - 'x': 1416851400000, - 'y': 5, - 'y0': 41 - }, - { - 'x': 1416851430000, - 'y': 4, - 'y0': 38 - }, - { - 'x': 1416851460000, - 'y': 5, - 'y0': 44 - }, - { - 'x': 1416851490000, - 'y': 2, - 'y0': 35 - }, - { - 'x': 1416851520000, - 'y': 2, - 'y0': 34 - }, - { - 'x': 1416851550000, - 'y': 4, - 'y0': 32 - }, - { - 'x': 1416851580000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851610000, - 'y': 4, - 'y0': 32 - }, - { - 'x': 1416851640000, - 'y': 0, - 'y0': 39 - }, - { - 'x': 1416851670000, - 'y': 2, - 'y0': 29 - }, - { - 'x': 1416851700000, - 'y': 1, - 'y0': 34 - }, - { - 'x': 1416851730000, - 'y': 3, - 'y0': 31 - }, - { - 'x': 1416851760000, - 'y': 0, - 'y0': 29 - }, - { - 'x': 1416851790000, - 'y': 4, - 'y0': 29 - }, - { - 'x': 1416851820000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416851850000, - 'y': 3, - 'y0': 33 - }, - { - 'x': 1416851880000, - 'y': 0, - 'y0': 25 - }, - { - 'x': 1416851910000, - 'y': 0, - 'y0': 41 - }, - { - 'x': 1416851940000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416851970000, - 'y': 3, - 'y0': 52 - }, - { - 'x': 1416852000000, - 'y': 1, - 'y0': 30 - }, - { - 'x': 1416852030000, - 'y': 5, - 'y0': 26 - }, - { - 'x': 1416852060000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416852090000, - 'y': 1, - 'y0': 20 - }, - { - 'x': 1416852120000, - 'y': 2, - 'y0': 19 - } - ] - }, - { - 'label': 'png', - 'values': [ - { - 'x': 1416850320000, - 'y': 1, - 'y0': 15 - }, - { - 'x': 1416850350000, - 'y': 6, - 'y0': 39 - }, - { - 'x': 1416850380000, - 'y': 6, - 'y0': 43 - }, - { - 'x': 1416850410000, - 'y': 5, - 'y0': 35 - }, - { - 'x': 1416850440000, - 'y': 3, - 'y0': 44 - }, - { - 'x': 1416850470000, - 'y': 5, - 'y0': 37 - }, - { - 'x': 1416850500000, - 'y': 6, - 'y0': 23 - }, - { - 'x': 1416850530000, - 'y': 1, - 'y0': 34 - }, - { - 'x': 1416850560000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416850590000, - 'y': 2, - 'y0': 54 - }, - { - 'x': 1416850620000, - 'y': 1, - 'y0': 40 - }, - { - 'x': 1416850650000, - 'y': 1, - 'y0': 42 - }, - { - 'x': 1416850680000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416850710000, - 'y': 5, - 'y0': 29 - }, - { - 'x': 1416850740000, - 'y': 7, - 'y0': 36 - }, - { - 'x': 1416850770000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416850800000, - 'y': 3, - 'y0': 41 - }, - { - 'x': 1416850830000, - 'y': 6, - 'y0': 41 - }, - { - 'x': 1416850860000, - 'y': 2, - 'y0': 50 - }, - { - 'x': 1416850890000, - 'y': 4, - 'y0': 25 - }, - { - 'x': 1416850920000, - 'y': 2, - 'y0': 34 - }, - { - 'x': 1416850950000, - 'y': 3, - 'y0': 46 - }, - { - 'x': 1416850980000, - 'y': 8, - 'y0': 36 - }, - { - 'x': 1416851010000, - 'y': 4, - 'y0': 35 - }, - { - 'x': 1416851040000, - 'y': 4, - 'y0': 33 - }, - { - 'x': 1416851070000, - 'y': 1, - 'y0': 40 - }, - { - 'x': 1416851100000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416851130000, - 'y': 4, - 'y0': 40 - }, - { - 'x': 1416851160000, - 'y': 3, - 'y0': 44 - }, - { - 'x': 1416851190000, - 'y': 4, - 'y0': 45 - }, - { - 'x': 1416851220000, - 'y': 2, - 'y0': 49 - }, - { - 'x': 1416851250000, - 'y': 4, - 'y0': 43 - }, - { - 'x': 1416851280000, - 'y': 8, - 'y0': 39 - }, - { - 'x': 1416851310000, - 'y': 4, - 'y0': 35 - }, - { - 'x': 1416851340000, - 'y': 4, - 'y0': 35 - }, - { - 'x': 1416851370000, - 'y': 7, - 'y0': 35 - }, - { - 'x': 1416851400000, - 'y': 2, - 'y0': 46 - }, - { - 'x': 1416851430000, - 'y': 3, - 'y0': 42 - }, - { - 'x': 1416851460000, - 'y': 3, - 'y0': 49 - }, - { - 'x': 1416851490000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851520000, - 'y': 4, - 'y0': 36 - }, - { - 'x': 1416851550000, - 'y': 3, - 'y0': 36 - }, - { - 'x': 1416851580000, - 'y': 4, - 'y0': 42 - }, - { - 'x': 1416851610000, - 'y': 5, - 'y0': 36 - }, - { - 'x': 1416851640000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851670000, - 'y': 3, - 'y0': 31 - }, - { - 'x': 1416851700000, - 'y': 2, - 'y0': 35 - }, - { - 'x': 1416851730000, - 'y': 5, - 'y0': 34 - }, - { - 'x': 1416851760000, - 'y': 4, - 'y0': 29 - }, - { - 'x': 1416851790000, - 'y': 5, - 'y0': 33 - }, - { - 'x': 1416851820000, - 'y': 1, - 'y0': 36 - }, - { - 'x': 1416851850000, - 'y': 3, - 'y0': 36 - }, - { - 'x': 1416851880000, - 'y': 6, - 'y0': 25 - }, - { - 'x': 1416851910000, - 'y': 4, - 'y0': 41 - }, - { - 'x': 1416851940000, - 'y': 7, - 'y0': 32 - }, - { - 'x': 1416851970000, - 'y': 5, - 'y0': 55 - }, - { - 'x': 1416852000000, - 'y': 2, - 'y0': 31 - }, - { - 'x': 1416852030000, - 'y': 2, - 'y0': 31 - }, - { - 'x': 1416852060000, - 'y': 4, - 'y0': 42 - }, - { - 'x': 1416852090000, - 'y': 6, - 'y0': 21 - }, - { - 'x': 1416852120000, - 'y': 2, - 'y0': 21 - } - ] - }, - { - 'label': 'php', - 'values': [ - { - 'x': 1416850320000, - 'y': 0, - 'y0': 16 - }, - { - 'x': 1416850350000, - 'y': 1, - 'y0': 45 - }, - { - 'x': 1416850380000, - 'y': 0, - 'y0': 49 - }, - { - 'x': 1416850410000, - 'y': 2, - 'y0': 40 - }, - { - 'x': 1416850440000, - 'y': 0, - 'y0': 47 - }, - { - 'x': 1416850470000, - 'y': 0, - 'y0': 42 - }, - { - 'x': 1416850500000, - 'y': 3, - 'y0': 29 - }, - { - 'x': 1416850530000, - 'y': 1, - 'y0': 35 - }, - { - 'x': 1416850560000, - 'y': 3, - 'y0': 40 - }, - { - 'x': 1416850590000, - 'y': 2, - 'y0': 56 - }, - { - 'x': 1416850620000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416850650000, - 'y': 5, - 'y0': 43 - }, - { - 'x': 1416850680000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416850710000, - 'y': 1, - 'y0': 34 - }, - { - 'x': 1416850740000, - 'y': 2, - 'y0': 43 - }, - { - 'x': 1416850770000, - 'y': 2, - 'y0': 45 - }, - { - 'x': 1416850800000, - 'y': 1, - 'y0': 44 - }, - { - 'x': 1416850830000, - 'y': 1, - 'y0': 47 - }, - { - 'x': 1416850860000, - 'y': 1, - 'y0': 52 - }, - { - 'x': 1416850890000, - 'y': 1, - 'y0': 29 - }, - { - 'x': 1416850920000, - 'y': 2, - 'y0': 36 - }, - { - 'x': 1416850950000, - 'y': 2, - 'y0': 49 - }, - { - 'x': 1416850980000, - 'y': 0, - 'y0': 44 - }, - { - 'x': 1416851010000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851040000, - 'y': 2, - 'y0': 37 - }, - { - 'x': 1416851070000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416851100000, - 'y': 2, - 'y0': 45 - }, - { - 'x': 1416851130000, - 'y': 0, - 'y0': 44 - }, - { - 'x': 1416851160000, - 'y': 1, - 'y0': 47 - }, - { - 'x': 1416851190000, - 'y': 2, - 'y0': 49 - }, - { - 'x': 1416851220000, - 'y': 4, - 'y0': 51 - }, - { - 'x': 1416851250000, - 'y': 0, - 'y0': 47 - }, - { - 'x': 1416851280000, - 'y': 3, - 'y0': 47 - }, - { - 'x': 1416851310000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851340000, - 'y': 2, - 'y0': 39 - }, - { - 'x': 1416851370000, - 'y': 2, - 'y0': 42 - }, - { - 'x': 1416851400000, - 'y': 3, - 'y0': 48 - }, - { - 'x': 1416851430000, - 'y': 1, - 'y0': 45 - }, - { - 'x': 1416851460000, - 'y': 0, - 'y0': 52 - }, - { - 'x': 1416851490000, - 'y': 2, - 'y0': 40 - }, - { - 'x': 1416851520000, - 'y': 1, - 'y0': 40 - }, - { - 'x': 1416851550000, - 'y': 3, - 'y0': 39 - }, - { - 'x': 1416851580000, - 'y': 1, - 'y0': 46 - }, - { - 'x': 1416851610000, - 'y': 2, - 'y0': 41 - }, - { - 'x': 1416851640000, - 'y': 1, - 'y0': 42 - }, - { - 'x': 1416851670000, - 'y': 2, - 'y0': 34 - }, - { - 'x': 1416851700000, - 'y': 3, - 'y0': 37 - }, - { - 'x': 1416851730000, - 'y': 1, - 'y0': 39 - }, - { - 'x': 1416851760000, - 'y': 1, - 'y0': 33 - }, - { - 'x': 1416851790000, - 'y': 1, - 'y0': 38 - }, - { - 'x': 1416851820000, - 'y': 1, - 'y0': 37 - }, - { - 'x': 1416851850000, - 'y': 1, - 'y0': 39 - }, - { - 'x': 1416851880000, - 'y': 1, - 'y0': 31 - }, - { - 'x': 1416851910000, - 'y': 2, - 'y0': 45 - }, - { - 'x': 1416851940000, - 'y': 0, - 'y0': 39 - }, - { - 'x': 1416851970000, - 'y': 0, - 'y0': 60 - }, - { - 'x': 1416852000000, - 'y': 1, - 'y0': 33 - }, - { - 'x': 1416852030000, - 'y': 2, - 'y0': 33 - }, - { - 'x': 1416852060000, - 'y': 1, - 'y0': 46 - }, - { - 'x': 1416852090000, - 'y': 1, - 'y0': 27 - }, - { - 'x': 1416852120000, - 'y': 0, - 'y0': 23 - } - ] - } - ], - 'hits': 2595, - 'xAxisFormatter': function (thing) { - return moment(thing); - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_columns.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_columns.js deleted file mode 100644 index 4683640725f2a..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_columns.js +++ /dev/null @@ -1,146 +0,0 @@ -import _ from 'lodash'; - -export default { - 'columns': [ - { - 'label': 'logstash: index', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 110710 - }, - { - 'x': 'css', - 'y': 27376 - }, - { - 'x': 'png', - 'y': 16664 - }, - { - 'x': 'gif', - 'y': 11264 - }, - { - 'x': 'php', - 'y': 5448 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '2014.11.12: index', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 110643 - }, - { - 'x': 'css', - 'y': 27350 - }, - { - 'x': 'png', - 'y': 16648 - }, - { - 'x': 'gif', - 'y': 11257 - }, - { - 'x': 'php', - 'y': 5440 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '2014.11.11: index', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 67 - }, - { - 'x': 'css', - 'y': 26 - }, - { - 'x': 'png', - 'y': 16 - }, - { - 'x': 'gif', - 'y': 7 - }, - { - 'x': 'php', - 'y': 8 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': ['jpg', 'css', 'png', 'gif', 'php'], - 'hits': 171462 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_rows.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_rows.js deleted file mode 100644 index 2b4ee83eca44c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_rows.js +++ /dev/null @@ -1,100 +0,0 @@ -import _ from 'lodash'; - -export default { - 'rows': [ - { - 'label': '0.0-1000.0: bytes', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 3378 - }, - { - 'x': 'css', - 'y': 762 - }, - { - 'x': 'png', - 'y': 527 - }, - { - 'x': 'gif', - 'y': 11258 - }, - { - 'x': 'php', - 'y': 653 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - }, - { - 'label': '1000.0-2000.0: bytes', - 'xAxisLabel': 'Top 5 extension', - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 6422 - }, - { - 'x': 'css', - 'y': 1591 - }, - { - 'x': 'png', - 'y': 430 - }, - { - 'x': 'gif', - 'y': 8 - }, - { - 'x': 'php', - 'y': 561 - } - ] - } - ], - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } - } - ], - 'xAxisOrderedValues': ['jpg', 'css', 'png', 'gif', 'php'], - 'hits': 171458 -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_series.js deleted file mode 100644 index f717012d430cf..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_series.js +++ /dev/null @@ -1,50 +0,0 @@ -import _ from 'lodash'; - -export default { - 'label': '', - 'xAxisLabel': 'Top 5 extension', - 'xAxisOrderedValues': ['jpg', 'css', 'png', 'gif', 'php'], - 'yAxisLabel': 'Count of documents', - 'series': [ - { - 'label': 'Count', - 'values': [ - { - 'x': 'jpg', - 'y': 110710 - }, - { - 'x': 'css', - 'y': 27389 - }, - { - 'x': 'png', - 'y': 16661 - }, - { - 'x': 'gif', - 'y': 11269 - }, - { - 'x': 'php', - 'y': 5447 - } - ] - } - ], - 'hits': 171476, - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js deleted file mode 100644 index 2b86663ab4673..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js +++ /dev/null @@ -1,72 +0,0 @@ -import _ from 'lodash'; - -export default { - 'xAxisOrderedValues': ['_all'], - 'yAxisLabel': 'Count', - 'zAxisLabel': 'machine.os.raw: Descending', - 'yScale': null, - 'series': [{ - 'label': 'ios', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 2820, - 'series': 'ios' - }] - }, { - 'label': 'win 7', - 'aggId': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 2319, - 'series': 'win 7' - }] - }, { - 'label': 'win 8', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 1835, - 'series': 'win 8' - }] - }, { - 'label': 'windows xp service pack 2 version 20123452', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 734, - 'series': 'win xp' - }] - }, { - 'label': 'osx', - 'id': '1', - 'yAxisFormatter': _.identity, - 'values': [{ - 'x': '_all', - 'y': 1352, - 'series': 'osx' - }] - }], - 'hits': 14005, - 'xAxisFormatter': function (val) { - if (_.isObject(val)) { - return JSON.stringify(val); - } - else if (val == null) { - return ''; - } - else { - return '' + val; - } - }, - 'yAxisFormatter': function (val) { - return val; - }, - 'tooltipFormatter': function (d) { - return d; - } -}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js deleted file mode 100644 index 8e25015c10186..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/handler/handler.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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 'jquery'; - -// Data -import series from '../fixtures/mock_data/date_histogram/_series'; -import columns from '../fixtures/mock_data/date_histogram/_columns'; -import rows from '../fixtures/mock_data/date_histogram/_rows'; -import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series'; - -import { getVis, getMockUiState } from '../fixtures/_vis_fixture'; - -const dateHistogramArray = [series, columns, rows, stackedSeries]; -const names = ['series', 'columns', 'rows', 'stackedSeries']; - -dateHistogramArray.forEach(function(data, i) { - describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function() { - const events = ['click', 'brush']; - let vis; - - beforeEach(() => { - vis = getVis(); - vis.render(data, getMockUiState()); - }); - - afterEach(function() { - vis.destroy(); - }); - - describe('render Method', function() { - it('should render charts', function() { - expect(vis.handler.charts.length).to.be.greaterThan(0); - vis.handler.charts.forEach(function(chart) { - expect($(chart.chartEl).find('svg').length).to.be(1); - }); - }); - }); - - describe('enable Method', function() { - let charts; - - beforeEach(function() { - charts = vis.handler.charts; - - charts.forEach(function(chart) { - events.forEach(function(event) { - vis.handler.enable(event, chart); - }); - }); - }); - - it('should add events to chart and emit to the Events class', function() { - charts.forEach(function(chart) { - events.forEach(function(event) { - expect(chart.events.listenerCount(event)).to.be.above(0); - }); - }); - }); - }); - - describe('disable Method', function() { - let charts; - - beforeEach(function() { - charts = vis.handler.charts; - - charts.forEach(function(chart) { - events.forEach(function(event) { - vis.handler.disable(event, chart); - }); - }); - }); - - it('should remove events from the chart', function() { - charts.forEach(function(chart) { - events.forEach(function(event) { - expect(chart.events.listenerCount(event)).to.be(0); - }); - }); - }); - }); - - describe('removeAll Method', function() { - beforeEach(function() { - vis.handler.removeAll(vis.element); - }); - - it('should remove all DOM elements from the el', function() { - expect($(vis.element).children().length).to.be(0); - }); - }); - - describe('error Method', function() { - beforeEach(function() { - vis.handler.error('This is an error!'); - }); - - it('should return an error classed DOM element with a text message', function() { - expect($(vis.element).find('.error').length).to.be(1); - expect($('.error h4').html()).to.be('This is an error!'); - }); - }); - - describe('destroy Method', function() { - beforeEach(function() { - vis.handler.destroy(); - }); - - it('should destroy all the charts in the visualization', function() { - expect(vis.handler.charts.length).to.be(0); - }); - }); - - describe('event proxying', function() { - it('should only pass the original event object to downstream handlers', function(done) { - const event = {}; - const chart = vis.handler.charts[0]; - - const mockEmitter = function() { - const args = Array.from(arguments); - expect(args.length).to.be(2); - expect(args[0]).to.be('click'); - expect(args[1]).to.be(event); - done(); - }; - - vis.emit = mockEmitter; - vis.handler.enable('click', chart); - chart.events.emit('click', event); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js deleted file mode 100644 index cc6d33a2d98da..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/layout_types.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; - -import { layoutTypes as layoutType } from '../../../lib/layout/layout_types'; - -describe('Vislib Layout Types Test Suite', function() { - let layoutFunc; - - beforeEach(() => { - layoutFunc = layoutType.point_series; - }); - - it('should be an object', function() { - expect(_.isObject(layoutType)).to.be(true); - }); - - it('should return a function', function() { - expect(typeof layoutFunc).to.be('function'); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js deleted file mode 100644 index 3942aa18891b8..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/column_chart/splits.js +++ /dev/null @@ -1,279 +0,0 @@ -/* - * 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 d3 from 'd3'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { chartSplit } from '../../../../../lib/layout/splits/column_chart/chart_split'; -import { chartTitleSplit } from '../../../../../lib/layout/splits/column_chart/chart_title_split'; -import { xAxisSplit } from '../../../../../lib/layout/splits/column_chart/x_axis_split'; -import { yAxisSplit } from '../../../../../lib/layout/splits/column_chart/y_axis_split'; - -describe('Vislib Split Function Test Suite', function() { - describe('Column Chart', function() { - let el; - const data = { - rows: [ - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - ], - }; - - beforeEach(() => { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization') - .datum(data); - }); - - afterEach(function() { - el.remove(); - }); - - describe('chart split function', function() { - let fixture; - - beforeEach(function() { - fixture = d3.select('.visualization').call(chartSplit); - }); - - afterEach(function() { - fixture.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.chart').length).to.be(2); - }); - - it('should add the correct class name', function() { - expect(!!$('.visWrapper__splitCharts--row').length).to.be(true); - }); - }); - - describe('chart title split function', function() { - let visEl; - let newEl; - let fixture; - - beforeEach(function() { - visEl = el.append('div').attr('class', 'visWrapper'); - visEl.append('div').attr('class', 'visAxis__splitTitles--x'); - visEl.append('div').attr('class', 'visAxis__splitTitles--y'); - visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - - newEl = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper') - .datum({ series: [] }); - - newEl.append('div').attr('class', 'visAxis__splitTitles--x'); - newEl.append('div').attr('class', 'visAxis__splitTitles--y'); - newEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - newEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - - fixture = newEl.selectAll(this.childNodes)[0].length; - }); - - afterEach(function() { - newEl.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.chart-title').length).to.be(2); - }); - - it('should remove the correct div', function() { - expect($('.visAxis__splitTitles--y').length).to.be(1); - expect($('.visAxis__splitTitles--x').length).to.be(0); - }); - - it('should remove all chart title divs when only one chart is rendered', function() { - expect(fixture).to.be(0); - }); - }); - - describe('x axis split function', function() { - let fixture; - let divs; - - beforeEach(function() { - fixture = d3 - .select('body') - .append('div') - .attr('class', 'columns') - .datum({ columns: [{}, {}] }); - d3.select('.columns').call(xAxisSplit); - divs = d3.selectAll('.x-axis-div')[0]; - }); - - afterEach(function() { - fixture.remove(); - $(divs).remove(); - }); - - it('should append the correct number of divs', function() { - expect(divs.length).to.be(2); - }); - }); - - describe('y axis split function', function() { - let fixture; - let divs; - - beforeEach(function() { - fixture = d3 - .select('body') - .append('div') - .attr('class', 'rows') - .datum({ rows: [{}, {}] }); - - d3.select('.rows').call(yAxisSplit); - - divs = d3.selectAll('.y-axis-div')[0]; - }); - - afterEach(function() { - fixture.remove(); - $(divs).remove(); - }); - - it('should append the correct number of divs', function() { - expect(divs.length).to.be(2); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js deleted file mode 100644 index 8978f80f58dde..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/splits/gauge_chart/splits.js +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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 d3 from 'd3'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { chartSplit } from '../../../../../lib/layout/splits/gauge_chart/chart_split'; -import { chartTitleSplit } from '../../../../../lib/layout/splits/gauge_chart/chart_title_split'; - -describe('Vislib Gauge Split Function Test Suite', function() { - describe('Column Chart', function() { - let el; - const data = { - rows: [ - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - { - hits: 621, - label: '', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }, - ], - }; - - beforeEach(function() { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization') - .datum(data); - }); - - afterEach(function() { - el.remove(); - }); - - describe('chart split function', function() { - let fixture; - - beforeEach(function() { - fixture = d3.select('.visualization').call(chartSplit); - }); - - afterEach(function() { - fixture.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.chart').length).to.be(2); - }); - - it('should add the correct class name', function() { - expect(!!$('.visWrapper__splitCharts--row').length).to.be(true); - }); - }); - - describe('chart title split function', function() { - let visEl; - - beforeEach(function() { - visEl = el.append('div').attr('class', 'visWrapper'); - visEl.append('div').attr('class', 'visAxis__splitTitles--x'); - visEl.append('div').attr('class', 'visAxis__splitTitles--y'); - visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); - visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); - }); - - afterEach(function() { - visEl.remove(); - }); - - it('should append the correct number of divs', function() { - expect($('.visAxis__splitTitles--x .chart-title').length).to.be(2); - expect($('.visAxis__splitTitles--y .chart-title').length).to.be(2); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js deleted file mode 100644 index e9c2ff0d2fa07..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/layout/types/column_layout.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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 d3 from 'd3'; -import _ from 'lodash'; -import expect from '@kbn/expect'; - -import { layoutTypes } from '../../../../lib/layout/layout_types'; - -describe('Vislib Column Layout Test Suite', function() { - let columnLayout; - let el; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(function() { - el = d3 - .select('body') - .append('div') - .attr('class', 'visualization'); - columnLayout = layoutTypes.point_series(el, data); - }); - - afterEach(function() { - el.remove(); - }); - - it('should return an array of objects', function() { - expect(Array.isArray(columnLayout)).to.be(true); - expect(_.isObject(columnLayout[0])).to.be(true); - }); - - it('should throw an error when the wrong number or no arguments provided', function() { - expect(function() { - layoutTypes.point_series(el); - }).to.throwError(); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js deleted file mode 100644 index 03646d08298dd..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/point_series.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 percentileTestdata from './testdata_linechart_percentile.json'; -import percentileTestdataResult from './testdata_linechart_percentile_result.json'; - -import { vislibPointSeriesTypes as pointSeriesConfig } from '../../../lib/types/point_series'; - -describe('Point Series Config Type Class Test Suite', function() { - let parsedConfig; - const histogramConfig = { - type: 'histogram', - addLegend: true, - tooltip: { - show: true, - }, - categoryAxes: [ - { - id: 'CategoryAxis-1', - type: 'category', - title: {}, - }, - ], - valueAxes: [ - { - id: 'ValueAxis-1', - type: 'value', - labels: {}, - title: {}, - }, - ], - }; - - const data = { - get: prop => { - return data[prop] || data.data[prop] || null; - }, - getLabels: () => [], - data: { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { label: 's1', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's2', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's3', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's4', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's5', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's6', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's7', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's8', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's9', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's10', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's11', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's12', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's13', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's14', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's15', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's16', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's17', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's18', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's19', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's20', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's21', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's22', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's23', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's24', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's25', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's26', values: [{ x: 1408734060000, y: 8 }] }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'series', - yAxisFormatter: () => 'test', - }, - }; - - describe('histogram chart', function() { - beforeEach(function() { - parsedConfig = pointSeriesConfig.column(histogramConfig, data); - }); - it('should not throw an error when more than 25 series are provided', function() { - expect(parsedConfig.error).to.be.undefined; - }); - - it('should set axis title and formatter from data', () => { - expect(parsedConfig.categoryAxes[0].title.text).to.equal(data.data.xAxisLabel); - expect(parsedConfig.valueAxes[0].labels.axisFormatter).to.not.be.undefined; - }); - }); - - describe('line chart', function() { - beforeEach(function() { - const percentileDataObj = { - get: prop => { - return data[prop] || data.data[prop] || null; - }, - getLabels: () => [], - data: percentileTestdata.data, - }; - parsedConfig = pointSeriesConfig.line(percentileTestdata.cfg, percentileDataObj); - }); - it('should render a percentile line chart', function() { - expect(JSON.stringify(parsedConfig)).to.eql(JSON.stringify(percentileTestdataResult)); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js deleted file mode 100644 index 7dfd2ded36a66..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/vis_config.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 d3 from 'd3'; -import expect from '@kbn/expect'; - -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -describe('Vislib VisConfig Class Test Suite', function() { - let el; - let visConfig; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - el = d3 - .select('body') - .append('div') - .attr('class', 'visWrapper') - .node(); - - visConfig = new VisConfig( - { - type: 'point_series', - }, - data, - getMockUiState(), - el, - () => undefined - ); - }); - - afterEach(() => { - el.remove(); - }); - - describe('get Method', function() { - it('should be a function', function() { - expect(typeof visConfig.get).to.be('function'); - }); - - it('should get the property', function() { - expect(visConfig.get('el')).to.be(el); - expect(visConfig.get('type')).to.be('point_series'); - }); - - it('should return defaults if property does not exist', function() { - expect(visConfig.get('this.does.not.exist', 'defaults')).to.be('defaults'); - }); - - it('should throw an error if property does not exist and defaults were not provided', function() { - expect(function() { - visConfig.get('this.does.not.exist'); - }).to.throwError(); - }); - }); - - describe('set Method', function() { - it('should be a function', function() { - expect(typeof visConfig.set).to.be('function'); - }); - - it('should set a property', function() { - visConfig.set('this.does.not.exist', 'it.does.now'); - expect(visConfig.get('this.does.not.exist')).to.be('it.does.now'); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js deleted file mode 100644 index d42562a87b825..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/x_axis.js +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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 d3 from 'd3'; -import _ from 'lodash'; -import expect from '@kbn/expect'; -import $ from 'jquery'; - -import { Axis } from '../../lib/axis'; -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -describe('Vislib xAxis Class Test Suite', function() { - let mockUiState; - let xAxis; - let el; - let fixture; - const data = { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - xAxisOrderedValues: [ - 1408734060000, - 1408734090000, - 1408734120000, - 1408734150000, - 1408734180000, - 1408734210000, - 1408734240000, - 1408734270000, - 1408734300000, - 1408734330000, - ], - series: [ - { - label: 'Count', - values: [ - { - x: 1408734060000, - y: 8, - }, - { - x: 1408734090000, - y: 23, - }, - { - x: 1408734120000, - y: 30, - }, - { - x: 1408734150000, - y: 28, - }, - { - x: 1408734180000, - y: 36, - }, - { - x: 1408734210000, - y: 30, - }, - { - x: 1408734240000, - y: 26, - }, - { - x: 1408734270000, - y: 22, - }, - { - x: 1408734300000, - y: 29, - }, - { - x: 1408734330000, - y: 24, - }, - ], - }, - ], - xAxisFormatter: function(thing) { - return new Date(thing); - }, - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - beforeEach(() => { - mockUiState = getMockUiState(); - el = d3 - .select('body') - .append('div') - .attr('class', 'visAxis--x') - .style('height', '40px'); - - fixture = el.append('div').attr('class', 'x-axis-div'); - - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - mockUiState, - $('.x-axis-div')[0], - () => undefined - ); - xAxis = new Axis(visConfig, { - type: 'category', - id: 'CategoryAxis-1', - }); - }); - - afterEach(function() { - fixture.remove(); - el.remove(); - }); - - describe('render Method', function() { - beforeEach(function() { - xAxis.render(); - }); - - it('should append an svg to div', function() { - expect(el.selectAll('svg').length).to.be(1); - }); - - it('should append a g element to the svg', function() { - expect(el.selectAll('svg').select('g').length).to.be(1); - }); - - it('should append ticks with text', function() { - expect(!!el.selectAll('svg').selectAll('.tick text')).to.be(true); - }); - }); - - describe('getScale, getDomain, getTimeDomain, and getRange Methods', function() { - let timeScale; - let width; - let range; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.getAxis(width); - timeScale = xAxis.getScale(); - range = xAxis.axisScale.getRange(width); - }); - - it('should return a function', function() { - expect(_.isFunction(timeScale)).to.be(true); - }); - - it('should return the correct domain', function() { - expect(_.isDate(timeScale.domain()[0])).to.be(true); - expect(_.isDate(timeScale.domain()[1])).to.be(true); - }); - - it('should return the min and max dates', function() { - expect(timeScale.domain()[0].toDateString()).to.be(new Date(1408734060000).toDateString()); - expect(timeScale.domain()[1].toDateString()).to.be(new Date(1408734330000).toDateString()); - }); - - it('should return the correct range', function() { - expect(range[0]).to.be(0); - expect(range[1]).to.be(width); - }); - }); - - describe('getOrdinalDomain Method', function() { - let ordinalScale; - let ordinalDomain; - let width; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.ordered = null; - xAxis.axisConfig.ordered = null; - xAxis.getAxis(width); - ordinalScale = xAxis.getScale(); - ordinalDomain = ordinalScale.domain(['this', 'should', 'be', 'an', 'array']); - }); - - it('should return an ordinal scale', function() { - expect(ordinalDomain.domain()[0]).to.be('this'); - expect(ordinalDomain.domain()[4]).to.be('array'); - }); - - it('should return an array of values', function() { - expect(Array.isArray(ordinalDomain.domain())).to.be(true); - }); - }); - - describe('getXScale Method', function() { - let width; - let xScale; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.getAxis(width); - xScale = xAxis.getScale(); - }); - - it('should return a function', function() { - expect(_.isFunction(xScale)).to.be(true); - }); - - it('should return a domain', function() { - expect(_.isDate(xScale.domain()[0])).to.be(true); - expect(_.isDate(xScale.domain()[1])).to.be(true); - }); - - it('should return a range', function() { - expect(xScale.range()[0]).to.be(0); - expect(xScale.range()[1]).to.be(width); - }); - }); - - describe('getXAxis Method', function() { - let width; - - beforeEach(function() { - width = $('.x-axis-div').width(); - xAxis.getAxis(width); - }); - - it('should create an getScale function on the xAxis class', function() { - expect(_.isFunction(xAxis.getScale())).to.be(true); - }); - }); - - describe('draw Method', function() { - it('should be a function', function() { - expect(_.isFunction(xAxis.draw())).to.be(true); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js deleted file mode 100644 index f73011d661645..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/y_axis.js +++ /dev/null @@ -1,382 +0,0 @@ -/* - * 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 _ from 'lodash'; -import d3 from 'd3'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import { Axis } from '../../lib/axis'; -import { VisConfig } from '../../lib/vis_config'; -import { getMockUiState } from './fixtures/_vis_fixture'; - -const YAxis = Axis; -let mockUiState; -let el; -let buildYAxis; -let yAxis; -let yAxisDiv; - -const timeSeries = [ - 1408734060000, - 1408734090000, - 1408734120000, - 1408734150000, - 1408734180000, - 1408734210000, - 1408734240000, - 1408734270000, - 1408734300000, - 1408734330000, -]; - -const defaultGraphData = [ - [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], - [2, 13, 20, 18, 26, 20, 16, 12, 19, 14], -]; - -function makeSeriesData(data) { - return timeSeries.map(function(timestamp, i) { - return { - x: timestamp, - y: data[i] || 0, - }; - }); -} - -function createData(seriesData) { - const data = { - hits: 621, - label: 'test', - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: seriesData.map(function(series) { - return { values: makeSeriesData(series) }; - }), - xAxisLabel: 'Date Histogram', - yAxisLabel: 'Count', - }; - - const node = $('
') - .css({ - height: 40, - width: 40, - }) - .appendTo('body') - .addClass('y-axis-wrapper') - .get(0); - - el = d3.select(node).datum(data); - - yAxisDiv = el.append('div').attr('class', 'y-axis-div'); - - buildYAxis = function(params) { - const visConfig = new VisConfig( - { - type: 'histogram', - }, - data, - mockUiState, - node, - () => undefined - ); - return new YAxis( - visConfig, - _.merge( - {}, - { - id: 'ValueAxis-1', - type: 'value', - scale: { - defaultYMin: true, - setYExtents: false, - }, - }, - params - ) - ); - }; - - yAxis = buildYAxis(); -} - -describe('Vislib yAxis Class Test Suite', function() { - beforeEach(() => { - mockUiState = getMockUiState(); - expect($('.y-axis-wrapper')).to.have.length(0); - }); - - afterEach(function() { - if (el) { - el.remove(); - yAxisDiv.remove(); - } - }); - - describe('render Method', function() { - beforeEach(function() { - createData(defaultGraphData); - yAxis.render(); - }); - - it('should append an svg to div', function() { - expect(el.selectAll('svg').length).to.be(1); - }); - - it('should append a g element to the svg', function() { - expect(el.selectAll('svg').select('g').length).to.be(1); - }); - - it('should append ticks with text', function() { - expect(!!el.selectAll('svg').selectAll('.tick text')).to.be(true); - }); - }); - - describe('getYScale Method', function() { - let yScale; - let graphData; - let domain; - const height = 50; - - function checkDomain(min, max) { - const domain = yScale.domain(); - expect(domain[0]).to.be.lessThan(min + 1); - expect(domain[1]).to.be.greaterThan(max - 1); - return domain; - } - - function checkRange() { - expect(yScale.range()[0]).to.be(height); - expect(yScale.range()[1]).to.be(0); - } - - describe('API', function() { - beforeEach(function() { - createData(defaultGraphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should return a function', function() { - expect(_.isFunction(yScale)).to.be(true); - }); - }); - - describe('positive values', function() { - beforeEach(function() { - graphData = defaultGraphData; - createData(graphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should have domain between 0 and max value', function() { - const min = 0; - const max = _.max(_.flattenDeep(graphData)); - const domain = checkDomain(min, max); - expect(domain[1]).to.be.greaterThan(0); - checkRange(); - }); - }); - - describe('negative values', function() { - beforeEach(function() { - graphData = [ - [-8, -23, -30, -28, -36, -30, -26, -22, -29, -24], - [-22, -8, -30, -4, 0, 0, -3, -22, -14, -24], - ]; - createData(graphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should have domain between min value and 0', function() { - const min = _.min(_.flattenDeep(graphData)); - const max = 0; - const domain = checkDomain(min, max); - expect(domain[0]).to.be.lessThan(0); - checkRange(); - }); - }); - - describe('positive and negative values', function() { - beforeEach(function() { - graphData = [ - [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], - [22, 8, -30, -4, 0, 0, 3, -22, 14, 24], - ]; - createData(graphData); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should have domain between min and max values', function() { - const min = _.min(_.flattenDeep(graphData)); - const max = _.max(_.flattenDeep(graphData)); - const domain = checkDomain(min, max); - expect(domain[0]).to.be.lessThan(0); - expect(domain[1]).to.be.greaterThan(0); - checkRange(); - }); - }); - - describe('validate user defined values', function() { - beforeEach(function() { - createData(defaultGraphData); - yAxis.axisConfig.set('scale.stacked', true); - yAxis.axisConfig.set('scale.setYExtents', false); - yAxis.getAxis(height); - yScale = yAxis.getScale(); - }); - - it('should throw a NaN error', function() { - const min = 'Not a number'; - const max = 12; - - expect(function() { - yAxis.axisScale.validateUserExtents(min, max); - }).to.throwError(); - }); - - it('should return a decimal value', function() { - yAxis.axisConfig.set('scale.mode', 'percentage'); - yAxis.axisConfig.set('scale.setYExtents', true); - yAxis.getAxis(height); - domain = []; - domain[0] = 20; - domain[1] = 80; - const newDomain = yAxis.axisScale.validateUserExtents(domain); - - expect(newDomain[0]).to.be(domain[0] / 100); - expect(newDomain[1]).to.be(domain[1] / 100); - }); - - it('should return the user defined value', function() { - domain = [20, 50]; - const newDomain = yAxis.axisScale.validateUserExtents(domain); - - expect(newDomain[0]).to.be(domain[0]); - expect(newDomain[1]).to.be(domain[1]); - }); - }); - - describe('should throw an error when', function() { - it('min === max', function() { - const min = 12; - const max = 12; - - expect(function() { - yAxis.axisScale.validateAxisExtents(min, max); - }).to.throwError(); - }); - - it('min > max', function() { - const min = 30; - const max = 10; - - expect(function() { - yAxis.axisScale.validateAxisExtents(min, max); - }).to.throwError(); - }); - }); - }); - - describe('getScaleType method', function() { - const fnNames = ['linear', 'log', 'square root']; - - it('should return a function', function() { - fnNames.forEach(function(fnName) { - expect(yAxis.axisScale.getD3Scale(fnName)).to.be.a(Function); - }); - - // if no value is provided to the function, scale should default to a linear scale - expect(yAxis.axisScale.getD3Scale()).to.be.a(Function); - }); - - it('should throw an error if function name is undefined', function() { - expect(function() { - yAxis.axisScale.getD3Scale('square'); - }).to.throwError(); - }); - }); - - describe('_logDomain method', function() { - it('should throw an error', function() { - expect(function() { - yAxis.axisScale.logDomain(-10, -5); - }).to.throwError(); - expect(function() { - yAxis.axisScale.logDomain(-10, 5); - }).to.throwError(); - expect(function() { - yAxis.axisScale.logDomain(0, -5); - }).to.throwError(); - }); - - it('should return a yMin value of 1', function() { - const yMin = yAxis.axisScale.logDomain(0, 200)[0]; - expect(yMin).to.be(1); - }); - }); - - describe('getYAxis method', function() { - let yMax; - beforeEach(function() { - createData(defaultGraphData); - yMax = yAxis.yMax; - }); - - afterEach(function() { - yAxis.yMax = yMax; - yAxis = buildYAxis(); - }); - - it('should use decimal format for small values', function() { - yAxis.yMax = 1; - const tickFormat = yAxis.getAxis().tickFormat(); - expect(tickFormat(0.8)).to.be('0.8'); - }); - }); - - describe('draw Method', function() { - beforeEach(function() { - createData(defaultGraphData); - }); - - it('should be a function', function() { - expect(_.isFunction(yAxis.draw())).to.be(true); - }); - }); - - describe('tickScale Method', function() { - beforeEach(function() { - createData(defaultGraphData); - }); - - it('should return the correct number of ticks', function() { - expect(yAxis.tickScale(1000)).to.be(11); - expect(yAxis.tickScale(40)).to.be(3); - expect(yAxis.tickScale(20)).to.be(0); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js deleted file mode 100644 index f4c952be191de..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/heatmap_chart.js +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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 _ from 'lodash'; -import $ from 'jquery'; -import d3 from 'd3'; -import expect from '@kbn/expect'; - -// Data -import series from '../lib/fixtures/mock_data/date_histogram/_series'; -import seriesPosNeg from '../lib/fixtures/mock_data/date_histogram/_series_pos_neg'; -import seriesNeg from '../lib/fixtures/mock_data/date_histogram/_series_neg'; -import termsColumns from '../lib/fixtures/mock_data/terms/_columns'; -import stackedSeries from '../lib/fixtures/mock_data/date_histogram/_stacked_series'; - -import { getVis, getMockUiState } from '../lib/fixtures/_vis_fixture'; - -// tuple, with the format [description, mode, data] -const dataTypesArray = [ - ['series', series], - ['series with positive and negative values', seriesPosNeg], - ['series with negative values', seriesNeg], - ['terms columns', termsColumns], - ['stackedSeries', stackedSeries], -]; - -describe('Vislib Heatmap Chart Test Suite', function() { - dataTypesArray.forEach(function(dataType) { - const name = dataType[0]; - const data = dataType[1]; - - describe('for ' + name + ' Data', function() { - let vis; - let mockUiState; - const visLibParams = { - type: 'heatmap', - addLegend: true, - addTooltip: true, - colorsNumber: 4, - colorSchema: 'Greens', - setColorRange: false, - percentageMode: true, - invertColors: false, - colorsRange: [], - }; - - function generateVis(opts = {}) { - const config = _.defaultsDeep({}, opts, visLibParams); - vis = getVis(config); - mockUiState = getMockUiState(); - vis.on('brush', _.noop); - vis.render(data, mockUiState); - } - - beforeEach(() => { - generateVis(); - }); - - afterEach(function() { - vis.destroy(); - }); - - it('category axes should be rendered in reverse order', () => { - const renderedCategoryAxes = vis.handler.renderArray.filter(item => { - return ( - item.constructor && - item.constructor.name === 'Axis' && - item.axisConfig.get('type') === 'category' - ); - }); - expect(vis.handler.categoryAxes.length).to.equal(renderedCategoryAxes.length); - expect(vis.handler.categoryAxes[0].axisConfig.get('id')).to.equal( - renderedCategoryAxes[1].axisConfig.get('id') - ); - expect(vis.handler.categoryAxes[1].axisConfig.get('id')).to.equal( - renderedCategoryAxes[0].axisConfig.get('id') - ); - }); - - describe('addSquares method', function() { - it('should append rects', function() { - vis.handler.charts.forEach(function(chart) { - const numOfRects = chart.chartData.series.reduce((result, series) => { - return result + series.values.length; - }, 0); - expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects); - }); - }); - }); - - describe('addBarEvents method', function() { - function checkChart(chart) { - const rect = $(chart.chartEl) - .find('.series rect') - .get(0); - - return { - click: !!rect.__onclick, - mouseOver: !!rect.__onmouseover, - // D3 brushing requires that a g element is appended that - // listens for mousedown events. This g element includes - // listeners, however, I was not able to test for the listener - // function being present. I will need to update this test - // in the future. - brush: !!d3.select('.brush')[0][0], - }; - } - - it('should attach the brush if data is a set of ordered dates', function() { - vis.handler.charts.forEach(function(chart) { - const has = checkChart(chart); - const ordered = vis.handler.data.get('ordered'); - const date = Boolean(ordered && ordered.date); - expect(has.brush).to.be(date); - }); - }); - - it('should attach a click event', function() { - vis.handler.charts.forEach(function(chart) { - const has = checkChart(chart); - expect(has.click).to.be(true); - }); - }); - - it('should attach a hover event', function() { - vis.handler.charts.forEach(function(chart) { - const has = checkChart(chart); - expect(has.mouseOver).to.be(true); - }); - }); - }); - - describe('draw method', function() { - it('should return a function', function() { - vis.handler.charts.forEach(function(chart) { - expect(_.isFunction(chart.draw())).to.be(true); - }); - }); - - it('should return a yMin and yMax', function() { - vis.handler.charts.forEach(function(chart) { - const yAxis = chart.handler.valueAxes[0]; - const domain = yAxis.getScale().domain(); - - expect(domain[0]).to.not.be(undefined); - expect(domain[1]).to.not.be(undefined); - }); - }); - }); - - it('should define default colors', function() { - expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined); - }); - - it('should set custom range', function() { - vis.destroy(); - generateVis({ - setColorRange: true, - colorsRange: [ - { from: 0, to: 200 }, - { from: 200, to: 400 }, - { from: 400, to: 500 }, - { from: 500, to: Infinity }, - ], - }); - const labels = vis.getLegendLabels(); - expect(labels[0]).to.be('0 - 200'); - expect(labels[1]).to.be('200 - 400'); - expect(labels[2]).to.be('400 - 500'); - expect(labels[3]).to.be('500 - Infinity'); - }); - - it('should show correct Y axis title', function() { - expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).to.equal(''); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js deleted file mode 100644 index d69f952325ed0..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/time_marker.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 d3 from 'd3'; -import $ from 'jquery'; -import expect from '@kbn/expect'; - -import series from '../lib/fixtures/mock_data/date_histogram/_series'; -import terms from '../lib/fixtures/mock_data/terms/_columns'; -import { TimeMarker } from '../../visualizations/time_marker'; - -describe('Vislib Time Marker Test Suite', function() { - const height = 50; - const color = '#ff0000'; - const opacity = 0.5; - const width = 3; - const customClass = 'custom-time-marker'; - const dateMathTimes = ['now-1m', 'now-5m', 'now-15m']; - const myTimes = dateMathTimes.map(function(dateMathString) { - return { - time: dateMathString, - class: customClass, - color: color, - opacity: opacity, - width: width, - }; - }); - const getExtent = function(dataArray, func) { - return func(dataArray, function(obj) { - return func(obj.values, function(d) { - return d.x; - }); - }); - }; - const times = []; - let defaultMarker; - let customMarker; - let selection; - let xScale; - let minDomain; - let maxDomain; - let domain; - - beforeEach(function() { - minDomain = getExtent(series.series, d3.min); - maxDomain = getExtent(series.series, d3.max); - domain = [minDomain, maxDomain]; - xScale = d3.time - .scale() - .domain(domain) - .range([0, 500]); - defaultMarker = new TimeMarker(times, xScale, height); - customMarker = new TimeMarker(myTimes, xScale, height); - - selection = d3 - .select('body') - .append('div') - .attr('class', 'marker'); - selection.datum(series); - }); - - afterEach(function() { - selection.remove('*'); - selection = null; - defaultMarker = null; - }); - - describe('_isTimeBaseChart method', function() { - let boolean; - let newSelection; - - it('should return true when data is time based', function() { - boolean = defaultMarker._isTimeBasedChart(selection); - expect(boolean).to.be(true); - }); - - it('should return false when data is not time based', function() { - newSelection = selection.datum(terms); - boolean = defaultMarker._isTimeBasedChart(newSelection); - expect(boolean).to.be(false); - }); - }); - - describe('render method', function() { - let lineArray; - - beforeEach(function() { - defaultMarker.render(selection); - customMarker.render(selection); - lineArray = document.getElementsByClassName('custom-time-marker'); - }); - - it('should render the default line', function() { - expect(!!$('line.time-marker').length).to.be(true); - }); - - it('should render the custom (user defined) lines', function() { - expect($('line.custom-time-marker').length).to.be(myTimes.length); - }); - - it('should set the class', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(line.getAttribute('class')).to.be(customClass); - }); - }); - - it('should set the stroke', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(line.getAttribute('stroke')).to.be(color); - }); - }); - - it('should set the stroke-opacity', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(+line.getAttribute('stroke-opacity')).to.be(opacity); - }); - }); - - it('should set the stroke-width', function() { - Array.prototype.forEach.call(lineArray, function(line) { - expect(+line.getAttribute('stroke-width')).to.be(width); - }); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js deleted file mode 100644 index c8f0faf8dcca5..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/visualizations/vis_types.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; - -import { visTypes } from '../../visualizations/vis_types'; - -describe('Vislib Vis Types Test Suite', function() { - let visFunc; - - beforeEach(function() { - visFunc = visTypes.point_series; - }); - - it('should be an object', function() { - expect(_.isObject(visTypes)).to.be(true); - }); - - it('should return a function', function() { - expect(typeof visFunc).to.be('function'); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx deleted file mode 100644 index 2fe16bbfeb625..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ /dev/null @@ -1,279 +0,0 @@ -/* - * 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 React, { BaseSyntheticEvent, KeyboardEvent, PureComponent } from 'react'; -import classNames from 'classnames'; -import { compact, uniq, map, every, isUndefined } from 'lodash'; - -import { i18n } from '@kbn/i18n'; -import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; - -import { getDataActions } from '../../../services'; -import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; -import { VisLegendItem } from './legend_item'; -import { getPieNames } from './pie_utils'; - -export interface VisLegendProps { - vis: any; - vislibVis: any; - visData: any; - uiState: any; - position: 'top' | 'bottom' | 'left' | 'right'; -} - -export interface VisLegendState { - open: boolean; - labels: any[]; - filterableLabels: Set; - selectedLabel: string | null; -} - -export class VisLegend extends PureComponent { - legendId = htmlIdGenerator()('legend'); - getColor: (label: string) => string = () => ''; - - constructor(props: VisLegendProps) { - super(props); - const open = props.uiState.get('vis.legendOpen', true); - - this.state = { - open, - labels: [], - filterableLabels: new Set(), - selectedLabel: null, - }; - } - - componentDidMount() { - this.refresh(); - } - - toggleLegend = () => { - const bwcAddLegend = this.props.vis.params.addLegend; - const bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend; - const newOpen = !this.props.uiState.get('vis.legendOpen', bwcLegendStateDefault); - this.setState({ open: newOpen }); - // open should be applied on template before we update uiState - setTimeout(() => { - this.props.uiState.set('vis.legendOpen', newOpen); - }); - }; - - setColor = (label: string, color: string) => (event: BaseSyntheticEvent) => { - if ((event as KeyboardEvent).keyCode && (event as KeyboardEvent).keyCode !== keyCodes.ENTER) { - return; - } - - const colors = this.props.uiState.get('vis.colors') || {}; - if (colors[label] === color) delete colors[label]; - else colors[label] = color; - this.props.uiState.setSilent('vis.colors', null); - this.props.uiState.set('vis.colors', colors); - this.props.uiState.emit('colorChanged'); - this.refresh(); - }; - - filter = ({ values: data }: LegendItem, negate: boolean) => { - this.props.vis.API.events.filter({ data, negate }); - }; - - canFilter = async (item: LegendItem): Promise => { - if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { - return false; - } - - if (item.values && every(item.values, isUndefined)) { - return false; - } - - const filters = await getDataActions().createFiltersFromEvent(item.values); - return Boolean(filters.length); - }; - - toggleDetails = (label: string | null) => (event?: BaseSyntheticEvent) => { - if ( - event && - (event as KeyboardEvent).keyCode && - (event as KeyboardEvent).keyCode !== keyCodes.ENTER - ) { - return; - } - this.setState({ selectedLabel: this.state.selectedLabel === label ? null : label }); - }; - - getSeriesLabels = (data: any[]) => { - const values = data.map(chart => chart.series).reduce((a, b) => a.concat(b), []); - - return compact(uniq(values, 'label')).map((label: any) => ({ - ...label, - values: [label.values[0].seriesRaw], - })); - }; - - setFilterableLabels = (items: LegendItem[]): Promise => - new Promise(async resolve => { - const filterableLabels = new Set(); - items.forEach(async item => { - const canFilter = await this.canFilter(item); - if (canFilter) { - filterableLabels.add(item.label); - } - }); - - this.setState({ filterableLabels }, resolve); - }); - - setLabels = (data: any, type: string) => { - let labels = []; - if (CUSTOM_LEGEND_VIS_TYPES.includes(type)) { - const legendLabels = this.props.vislibVis.getLegendLabels(); - if (legendLabels) { - labels = map(legendLabels, label => { - return { label }; - }); - } - } else { - if (!data) return []; - data = data.columns || data.rows || [data]; - - labels = type === 'pie' ? getPieNames(data) : this.getSeriesLabels(data); - } - - this.setFilterableLabels(labels); - - this.setState({ - labels, - }); - }; - - refresh = () => { - const vislibVis = this.props.vislibVis; - if (!vislibVis || !vislibVis.visConfig) { - this.setState({ - labels: [ - { - label: i18n.translate('visTypeVislib.vislib.legend.loadingLabel', { - defaultMessage: 'loading…', - }), - }, - ], - }); - return; - } // make sure vislib is defined at this point - - if ( - this.props.uiState.get('vis.legendOpen') == null && - this.props.vis.params.addLegend != null - ) { - this.setState({ open: this.props.vis.params.addLegend }); - } - - if (vislibVis.visConfig) { - this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); - } - - this.setLabels(this.props.visData, vislibVis.visConfigArgs.type); - }; - - highlight = (event: BaseSyntheticEvent) => { - const el = event.currentTarget; - const handler = this.props.vislibVis && this.props.vislibVis.handler; - - // there is no guarantee that a Chart will set the highlight-function on its handler - if (!handler || typeof handler.highlight !== 'function') { - return; - } - handler.highlight.call(el, handler.el); - }; - - unhighlight = (event: BaseSyntheticEvent) => { - const el = event.currentTarget; - const handler = this.props.vislibVis && this.props.vislibVis.handler; - - // there is no guarantee that a Chart will set the unhighlight-function on its handler - if (!handler || typeof handler.unHighlight !== 'function') { - return; - } - handler.unHighlight.call(el, handler.el); - }; - - getAnchorPosition = () => { - const { position } = this.props; - - switch (position) { - case 'bottom': - return 'upCenter'; - case 'left': - return 'rightUp'; - case 'right': - return 'leftUp'; - default: - return 'downCenter'; - } - }; - - renderLegend = (anchorPosition: EuiPopoverProps['anchorPosition']) => ( -
    - {this.state.labels.map(item => ( - - ))} -
- ); - - render() { - const { open } = this.state; - const anchorPosition = this.getAnchorPosition(); - - return ( -
- - {open && this.renderLegend(anchorPosition)} -
- ); - } -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js deleted file mode 100644 index 129006cdf0ca3..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js +++ /dev/null @@ -1,510 +0,0 @@ -/* - * 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 _ from 'lodash'; -import expect from '@kbn/expect'; -import { injectZeros } from './inject_zeros'; -import { orderXValues } from './ordered_x_keys'; -import { getUniqKeys } from './uniq_keys'; -import { flattenData } from './flatten_data'; -import { createZeroFilledArray } from './zero_filled_array'; -import { zeroFillDataArray } from './zero_fill_data_array'; - -describe('Vislib Zero Injection Module Test Suite', function() { - const dateHistogramRowsObj = { - xAxisOrderedValues: [ - 1418410560000, - 1418410620000, - 1418410680000, - 1418410740000, - 1418410800000, - 1418410860000, - 1418410920000, - ], - series: [ - { - label: 'html', - values: [ - { x: 1418410560000, y: 2 }, - { x: 1418410620000, y: 4 }, - { x: 1418410680000, y: 1 }, - { x: 1418410740000, y: 5 }, - { x: 1418410800000, y: 2 }, - { x: 1418410860000, y: 3 }, - { x: 1418410920000, y: 2 }, - ], - }, - { - label: 'css', - values: [ - { x: 1418410560000, y: 1 }, - { x: 1418410620000, y: 3 }, - { x: 1418410680000, y: 1 }, - { x: 1418410740000, y: 4 }, - { x: 1418410800000, y: 2 }, - ], - }, - ], - }; - const dateHistogramRows = dateHistogramRowsObj.series; - - const seriesDataObj = { - xAxisOrderedValues: ['v1', 'v2', 'v3', 'v4', 'v5'], - series: [ - { - label: '200', - values: [ - { x: 'v1', y: 234 }, - { x: 'v2', y: 34 }, - { x: 'v3', y: 834 }, - { x: 'v4', y: 1234 }, - { x: 'v5', y: 4 }, - ], - }, - ], - }; - const seriesData = seriesDataObj.series; - - const multiSeriesDataObj = { - xAxisOrderedValues: ['1', '2', '3', '4', '5'], - series: [ - { - label: '200', - values: [ - { x: '1', y: 234 }, - { x: '2', y: 34 }, - { x: '3', y: 834 }, - { x: '4', y: 1234 }, - { x: '5', y: 4 }, - ], - }, - { - label: '404', - values: [ - { x: '1', y: 1234 }, - { x: '3', y: 234 }, - { x: '5', y: 34 }, - ], - }, - { - label: '503', - values: [{ x: '3', y: 834 }], - }, - ], - }; - const multiSeriesData = multiSeriesDataObj.series; - - const multiSeriesNumberedDataObj = { - xAxisOrderedValues: [1, 2, 3, 4, 5], - series: [ - { - label: '200', - values: [ - { x: 1, y: 234 }, - { x: 2, y: 34 }, - { x: 3, y: 834 }, - { x: 4, y: 1234 }, - { x: 5, y: 4 }, - ], - }, - { - label: '404', - values: [ - { x: 1, y: 1234 }, - { x: 3, y: 234 }, - { x: 5, y: 34 }, - ], - }, - { - label: '503', - values: [{ x: 3, y: 834 }], - }, - ], - }; - const multiSeriesNumberedData = multiSeriesNumberedDataObj.series; - - const emptyObject = {}; - const str = 'string'; - const number = 24; - const boolean = false; - const nullValue = null; - const emptyArray = []; - let notAValue; - - describe('Zero Injection (main)', function() { - let sample1; - let sample2; - let sample3; - - beforeEach(() => { - sample1 = injectZeros(seriesData, seriesDataObj); - sample2 = injectZeros(multiSeriesData, multiSeriesDataObj); - sample3 = injectZeros(multiSeriesNumberedData, multiSeriesNumberedDataObj); - }); - - it('should be a function', function() { - expect(_.isFunction(injectZeros)).to.be(true); - }); - - it('should return an object with series[0].values', function() { - expect(_.isObject(sample1)).to.be(true); - expect(_.isObject(sample1[0].values)).to.be(true); - }); - - it('should return the same array of objects when the length of the series array is 1', function() { - expect(sample1[0].values[0].x).to.be(seriesData[0].values[0].x); - expect(sample1[0].values[1].x).to.be(seriesData[0].values[1].x); - expect(sample1[0].values[2].x).to.be(seriesData[0].values[2].x); - expect(sample1[0].values[3].x).to.be(seriesData[0].values[3].x); - expect(sample1[0].values[4].x).to.be(seriesData[0].values[4].x); - }); - - it('should inject zeros in the input array', function() { - expect(sample2[1].values[1].y).to.be(0); - expect(sample2[2].values[0].y).to.be(0); - expect(sample2[2].values[1].y).to.be(0); - expect(sample2[2].values[4].y).to.be(0); - expect(sample3[1].values[1].y).to.be(0); - expect(sample3[2].values[0].y).to.be(0); - expect(sample3[2].values[1].y).to.be(0); - expect(sample3[2].values[4].y).to.be(0); - }); - - it('should return values arrays with the same x values', function() { - expect(sample2[1].values[0].x).to.be(sample2[2].values[0].x); - expect(sample2[1].values[1].x).to.be(sample2[2].values[1].x); - expect(sample2[1].values[2].x).to.be(sample2[2].values[2].x); - expect(sample2[1].values[3].x).to.be(sample2[2].values[3].x); - expect(sample2[1].values[4].x).to.be(sample2[2].values[4].x); - }); - - it('should return values arrays of the same length', function() { - expect(sample2[0].values.length).to.be(sample2[1].values.length); - expect(sample2[0].values.length).to.be(sample2[2].values.length); - expect(sample2[1].values.length).to.be(sample2[2].values.length); - }); - }); - - describe('Order X Values', function() { - let results; - let numberedResults; - - beforeEach(() => { - results = orderXValues(multiSeriesDataObj); - numberedResults = orderXValues(multiSeriesNumberedDataObj); - }); - - it('should return a function', function() { - expect(_.isFunction(orderXValues)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results)).to.be(true); - }); - - it('should return an array of values ordered by their index by default', function() { - expect(results[0]).to.be('1'); - expect(results[1]).to.be('2'); - expect(results[2]).to.be('3'); - expect(results[3]).to.be('4'); - expect(results[4]).to.be('5'); - expect(numberedResults[0]).to.be(1); - expect(numberedResults[1]).to.be(2); - expect(numberedResults[2]).to.be(3); - expect(numberedResults[3]).to.be(4); - expect(numberedResults[4]).to.be(5); - }); - - it('should return an array of values that preserve the index from xAxisOrderedValues', function() { - const data = { - xAxisOrderedValues: ['1', '2', '3', '4', '5'], - series: [ - { - label: '200', - values: [ - { x: '2', y: 34 }, - { x: '4', y: 1234 }, - ], - }, - { - label: '404', - values: [ - { x: '1', y: 1234 }, - { x: '3', y: 234 }, - { x: '5', y: 34 }, - ], - }, - { - label: '503', - values: [{ x: '3', y: 834 }], - }, - ], - }; - const result = orderXValues(data); - expect(result).to.eql(['1', '2', '3', '4', '5']); - }); - - it('should return an array of values ordered by their sum when orderBucketsBySum is true', function() { - const orderBucketsBySum = true; - results = orderXValues(multiSeriesDataObj, orderBucketsBySum); - numberedResults = orderXValues(multiSeriesNumberedDataObj, orderBucketsBySum); - - expect(results[0]).to.be('3'); - expect(results[1]).to.be('1'); - expect(results[2]).to.be('4'); - expect(results[3]).to.be('5'); - expect(results[4]).to.be('2'); - expect(numberedResults[0]).to.be(3); - expect(numberedResults[1]).to.be(1); - expect(numberedResults[2]).to.be(4); - expect(numberedResults[3]).to.be(5); - expect(numberedResults[4]).to.be(2); - }); - }); - - describe('Unique Keys', function() { - let results; - - beforeEach(() => { - results = getUniqKeys(multiSeriesDataObj); - }); - - it('should throw an error if input is not an object', function() { - expect(function() { - getUniqKeys(str); - }).to.throwError(); - - expect(function() { - getUniqKeys(number); - }).to.throwError(); - - expect(function() { - getUniqKeys(boolean); - }).to.throwError(); - - expect(function() { - getUniqKeys(nullValue); - }).to.throwError(); - - expect(function() { - getUniqKeys(emptyArray); - }).to.throwError(); - - expect(function() { - getUniqKeys(notAValue); - }).to.throwError(); - }); - - it('should return a function', function() { - expect(_.isFunction(getUniqKeys)).to.be(true); - }); - - it('should return an object', function() { - expect(_.isObject(results)).to.be(true); - }); - - it('should return an object of unique keys', function() { - expect(_.uniq(_.keys(results)).length).to.be(_.keys(results).length); - }); - }); - - describe('Flatten Data', function() { - let results; - - beforeEach(() => { - results = flattenData(multiSeriesDataObj); - }); - - it('should return a function', function() { - expect(_.isFunction(flattenData)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results)).to.be(true); - }); - - it('should return an array of objects', function() { - expect(_.isObject(results[0])).to.be(true); - expect(_.isObject(results[1])).to.be(true); - expect(_.isObject(results[2])).to.be(true); - }); - }); - - describe('Zero Filled Array', function() { - const arr1 = [1, 2, 3, 4, 5]; - const arr2 = ['1', '2', '3', '4', '5']; - let results1; - let results2; - - beforeEach(() => { - results1 = createZeroFilledArray(arr1); - results2 = createZeroFilledArray(arr2); - }); - - it('should throw an error if input is not an array', function() { - expect(function() { - createZeroFilledArray(str); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(number); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(boolean); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(nullValue); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(emptyObject); - }).to.throwError(); - - expect(function() { - createZeroFilledArray(notAValue); - }).to.throwError(); - }); - - it('should return a function', function() { - expect(_.isFunction(createZeroFilledArray)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results1)).to.be(true); - }); - - it('should return an array of objects', function() { - expect(_.isObject(results1[0])).to.be(true); - expect(_.isObject(results1[1])).to.be(true); - expect(_.isObject(results1[2])).to.be(true); - expect(_.isObject(results1[3])).to.be(true); - expect(_.isObject(results1[4])).to.be(true); - }); - - it('should return an array of objects where each y value is 0', function() { - expect(results1[0].y).to.be(0); - expect(results1[1].y).to.be(0); - expect(results1[2].y).to.be(0); - expect(results1[3].y).to.be(0); - expect(results1[4].y).to.be(0); - }); - - it('should return an array of objects where each x values are numbers', function() { - expect(_.isNumber(results1[0].x)).to.be(true); - expect(_.isNumber(results1[1].x)).to.be(true); - expect(_.isNumber(results1[2].x)).to.be(true); - expect(_.isNumber(results1[3].x)).to.be(true); - expect(_.isNumber(results1[4].x)).to.be(true); - }); - - it('should return an array of objects where each x values are strings', function() { - expect(_.isString(results2[0].x)).to.be(true); - expect(_.isString(results2[1].x)).to.be(true); - expect(_.isString(results2[2].x)).to.be(true); - expect(_.isString(results2[3].x)).to.be(true); - expect(_.isString(results2[4].x)).to.be(true); - }); - }); - - describe('Zero Filled Data Array', function() { - const xValueArr = [1, 2, 3, 4, 5]; - let arr1; - const arr2 = [{ x: 3, y: 834 }]; - let results; - - beforeEach(() => { - arr1 = createZeroFilledArray(xValueArr); - // Takes zero array as 1st arg and data array as 2nd arg - results = zeroFillDataArray(arr1, arr2); - }); - - it('should throw an error if input are not arrays', function() { - expect(function() { - zeroFillDataArray(str, str); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(number, number); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(boolean, boolean); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(nullValue, nullValue); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(emptyObject, emptyObject); - }).to.throwError(); - - expect(function() { - zeroFillDataArray(notAValue, notAValue); - }).to.throwError(); - }); - - it('should return a function', function() { - expect(_.isFunction(zeroFillDataArray)).to.be(true); - }); - - it('should return an array', function() { - expect(Array.isArray(results)).to.be(true); - }); - - it('should return an array of objects', function() { - expect(_.isObject(results[0])).to.be(true); - expect(_.isObject(results[1])).to.be(true); - expect(_.isObject(results[2])).to.be(true); - }); - - it('should return an array with zeros injected in the appropriate objects as y values', function() { - expect(results[0].y).to.be(0); - expect(results[1].y).to.be(0); - expect(results[3].y).to.be(0); - expect(results[4].y).to.be(0); - }); - }); - - describe('Injected Zero values return in the correct order', function() { - let results; - - beforeEach(() => { - results = injectZeros(dateHistogramRows, dateHistogramRowsObj); - }); - - it('should return an array of objects', function() { - results.forEach(function(row) { - expect(Array.isArray(row.values)).to.be(true); - }); - }); - - it('should return ordered x values', function() { - const values = results[0].values; - expect(values[0].x).to.be.lessThan(values[1].x); - expect(values[1].x).to.be.lessThan(values[2].x); - expect(values[2].x).to.be.lessThan(values[3].x); - expect(values[3].x).to.be.lessThan(values[4].x); - expect(values[4].x).to.be.lessThan(values[5].x); - expect(values[5].x).to.be.lessThan(values[6].x); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/index.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/index.js deleted file mode 100644 index b2559c085aeab..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 { VislibProvider } from './vislib'; - -// eslint-disable-next-line import/no-default-export -export default VislibProvider; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js deleted file mode 100644 index ecf67ee3e017c..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js +++ /dev/null @@ -1,245 +0,0 @@ -/* - * 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 d3 from 'd3'; -import _ from 'lodash'; -import MarkdownIt from 'markdown-it'; - -import { NoResults } from '../errors'; -import { Layout } from './layout/layout'; -import { ChartTitle } from './chart_title'; -import { Alerts } from './alerts'; -import { Axis } from './axis/axis'; -import { ChartGrid as Grid } from './chart_grid'; -import { visTypes as chartTypes } from '../visualizations/vis_types'; -import { Binder } from './binder'; -import { dispatchRenderComplete } from '../../../../../../plugins/kibana_utils/public'; - -const markdownIt = new MarkdownIt({ - html: false, - linkify: true, -}); - -/** - * Handles building all the components of the visualization - * - * @class Handler - * @constructor - * @param vis {Object} Reference to the Vis Class Constructor - * @param opts {Object} Reference to Visualization constructors needed to - * create the visualization - */ -export class Handler { - constructor(vis, visConfig, deps) { - this.el = visConfig.get('el'); - this.ChartClass = chartTypes[visConfig.get('type')]; - this.deps = deps; - this.charts = []; - - this.vis = vis; - this.visConfig = visConfig; - this.data = visConfig.data; - - this.categoryAxes = visConfig - .get('categoryAxes') - .map(axisArgs => new Axis(visConfig, axisArgs)); - this.valueAxes = visConfig.get('valueAxes').map(axisArgs => new Axis(visConfig, axisArgs)); - this.chartTitle = new ChartTitle(visConfig); - this.alerts = new Alerts(this, visConfig.get('alerts')); - this.grid = new Grid(this, visConfig.get('grid')); - - if (visConfig.get('type') === 'point_series') { - this.data.stackData(this); - } - - if (visConfig.get('resize', false)) { - this.resize = visConfig.get('resize'); - } - - this.layout = new Layout(visConfig); - this.binder = new Binder(); - this.renderArray = _.filter([this.layout, this.chartTitle, this.alerts], Boolean); - - this.renderArray = this.renderArray - .concat(this.valueAxes) - // category axes need to render in reverse order https://github.com/elastic/kibana/issues/13551 - .concat(this.categoryAxes.slice().reverse()); - - // memoize so that the same function is returned every time, - // allowing us to remove/re-add the same function - this.getProxyHandler = _.memoize(function(event) { - const self = this; - return function(e) { - self.vis.emit(event, e); - }; - }); - - /** - * Enables events, i.e. binds specific events to the chart - * object(s) `on` method. For example, `click` or `mousedown` events. - * - * @method enable - * @param event {String} Event type - * @param chart {Object} Chart - * @returns {*} - */ - this.enable = this.chartEventProxyToggle('on'); - - /** - * Disables events for all charts - * - * @method disable - * @param event {String} Event type - * @param chart {Object} Chart - * @returns {*} - */ - this.disable = this.chartEventProxyToggle('off'); - } - /** - * Validates whether data is actually present in the data object - * used to render the Vis. Throws a no results error if data is not - * present. - * - * @private - */ - _validateData() { - const dataType = this.data.type; - - if (!dataType) { - throw new NoResults(); - } - } - - /** - * Renders the constructors that create the visualization, - * including the chart constructor - * - * @method render - * @returns {HTMLElement} With the visualization child element - */ - render() { - if (this.visConfig.get('error', null)) return this.error(this.visConfig.get('error')); - - const self = this; - const { binder, charts = [] } = this; - const selection = d3.select(this.el); - - selection.selectAll('*').remove(); - - this._validateData(); - this.renderArray.forEach(function(property) { - if (typeof property.render === 'function') { - property.render(); - } - }); - - // render the chart(s) - let loadedCount = 0; - const chartSelection = selection.selectAll('.chart'); - chartSelection.each(function(chartData) { - const chart = new self.ChartClass(self, this, chartData, self.deps); - - self.vis.eventNames().forEach(function(event) { - self.enable(event, chart); - }); - - binder.on(chart.events, 'rendered', () => { - loadedCount++; - if (loadedCount === chartSelection.length) { - // events from all charts are propagated to vis, we only need to fire renderComplete once they all finish - self.vis.emit('renderComplete'); - } - }); - - charts.push(chart); - chart.render(); - }); - } - - chartEventProxyToggle(method) { - return function(event, chart) { - const proxyHandler = this.getProxyHandler(event); - - _.each(chart ? [chart] : this.charts, function(chart) { - chart.events[method](event, proxyHandler); - }); - }; - } - - /** - * Removes all DOM elements from the HTML element provided - * - * @method removeAll - * @param el {HTMLElement} Reference to the HTML Element that - * contains the chart - * @returns {D3.Selection|D3.Transition.Transition} With the chart - * child element removed - */ - removeAll(el) { - return d3 - .select(el) - .selectAll('*') - .remove(); - } - - /** - * Displays an error message in the DOM - * - * @method error - * @param message {String} Error message to display - * @returns {HTMLElement} Displays the input message - */ - error(message) { - this.removeAll(this.el); - - const div = d3 - .select(this.el) - .append('div') - // class name needs `chart` in it for the polling checkSize function - // to continuously call render on resize - .attr('class', 'visError chart error') - .attr('data-test-subj', 'visLibVisualizeError'); - - div.append('h4').text(markdownIt.renderInline(message)); - - dispatchRenderComplete(this.el); - return div; - } - - /** - * Destroys all the charts in the visualization - * - * @method destroy - */ - destroy() { - this.binder.destroy(); - - this.renderArray.forEach(function(renderable) { - if (_.isFunction(renderable.destroy)) { - renderable.destroy(); - } - }); - - this.charts.splice(0).forEach(function(chart) { - if (_.isFunction(chart.destroy)) { - chart.destroy(); - } - }); - } -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js deleted file mode 100644 index 38a6be548594f..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * 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 stackedSeries from '../../__tests__/lib/fixtures/mock_data/date_histogram/_stacked_series'; -import { vislibPointSeriesTypes } from './point_series'; - -describe('vislibPointSeriesTypes', () => { - const heatmapConfig = { - type: 'heatmap', - addLegend: true, - addTooltip: true, - colorsNumber: 4, - colorSchema: 'Greens', - setColorRange: false, - percentageMode: true, - invertColors: false, - colorsRange: [], - heatmapMaxBuckets: 20, - }; - - const stackedData = { - get: prop => { - return stackedSeries[prop] || null; - }, - getLabels: () => [], - data: stackedSeries, - }; - - const maxBucketData = { - get: prop => { - return maxBucketData[prop] || maxBucketData.data[prop] || null; - }, - getLabels: () => [], - data: { - hits: 621, - ordered: { - date: true, - interval: 30000, - max: 1408734982458, - min: 1408734082458, - }, - series: [ - { label: 's1', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's2', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's3', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's4', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's5', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's6', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's7', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's8', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's9', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's10', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's11', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's12', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's13', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's14', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's15', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's16', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's17', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's18', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's19', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's20', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's21', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's22', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's23', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's24', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's25', values: [{ x: 1408734060000, y: 8 }] }, - { label: 's26', values: [{ x: 1408734060000, y: 8 }] }, - ], - xAxisLabel: 'Date Histogram', - yAxisLabel: 'series', - yAxisFormatter: () => 'test', - }, - }; - - describe('axis formatters', () => { - it('should create a value axis config with the default y axis formatter', () => { - const parsedConfig = vislibPointSeriesTypes.line({}, maxBucketData); - expect(parsedConfig.valueAxes.length).toEqual(1); - expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe( - maxBucketData.data.yAxisFormatter - ); - }); - - it('should use the formatter of the first series matching the axis if there is a descriptor', () => { - const axisFormatter1 = jest.fn(); - const axisFormatter2 = jest.fn(); - const axisFormatter3 = jest.fn(); - const parsedConfig = vislibPointSeriesTypes.line( - { - valueAxes: [ - { - id: 'ValueAxis-1', - labels: {}, - }, - { - id: 'ValueAxis-2', - labels: {}, - }, - ], - seriesParams: [ - { - valueAxis: 'ValueAxis-1', - data: { - id: '2', - }, - }, - { - valueAxis: 'ValueAxis-2', - data: { - id: '3', - }, - }, - { - valueAxis: 'ValueAxis-2', - data: { - id: '4', - }, - }, - ], - }, - { - ...maxBucketData, - data: { - ...maxBucketData.data, - series: [ - { id: '2.1', label: 's1', values: [], yAxisFormatter: axisFormatter1 }, - { id: '2.2', label: 's2', values: [], yAxisFormatter: axisFormatter1 }, - { id: '3.1', label: 's3', values: [], yAxisFormatter: axisFormatter2 }, - { id: '3.2', label: 's4', values: [], yAxisFormatter: axisFormatter2 }, - { id: '4.1', label: 's5', values: [], yAxisFormatter: axisFormatter3 }, - { id: '4.2', label: 's6', values: [], yAxisFormatter: axisFormatter3 }, - ], - }, - } - ); - expect(parsedConfig.valueAxes.length).toEqual(2); - expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe(axisFormatter1); - expect(parsedConfig.valueAxes[1].labels.axisFormatter).toBe(axisFormatter2); - }); - }); - - describe('heatmap()', () => { - it('should return an error when more than 20 series are provided', () => { - const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, maxBucketData); - expect(parsedConfig.error).toMatchInlineSnapshot( - `"There are too many series defined (26). The configured maximum is 20."` - ); - }); - - it('should return valid config when less than 20 series are provided', () => { - const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, stackedData); - expect(parsedConfig.error).toBeUndefined(); - expect(parsedConfig.valueAxes[0].show).toBeFalsy(); - expect(parsedConfig.categoryAxes.length).toBe(2); - expect(parsedConfig.error).toBeUndefined(); - }); - }); -}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/vislib.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/vislib.js deleted file mode 100644 index 024dee60ef2bf..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/vislib.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 './lib/types/pie'; -import './lib/types/point_series'; -import './lib/types'; -import './lib/layout/layout_types'; -import './lib/data'; -import './visualizations/vis_types'; -import { Vis } from './vis'; - -// prefetched for faster optimization runs -// end prefetching - -/** - * Provides the Kibana4 Visualization Library - * - * @module vislib - * @main vislib - * @return {Object} Contains the version number and the Vis Class for creating visualizations - */ -export function VislibProvider() { - return { - version: '0.0.0', - Vis, - }; -} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js deleted file mode 100644 index 9822932c6071b..0000000000000 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js +++ /dev/null @@ -1,326 +0,0 @@ -/* - * 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 _ from 'lodash'; -import moment from 'moment'; - -import { isColorDark } from '@elastic/eui'; - -import { PointSeries } from './_point_series'; -import { getHeatmapColors } from '../../.../../../../../../../plugins/charts/public'; - -const defaults = { - color: undefined, // todo - fillColor: undefined, // todo -}; -/** - * Line Chart Visualization - * - * @class HeatmapChart - * @constructor - * @extends Chart - * @param handler {Object} Reference to the Handler Class Constructor - * @param el {HTMLElement} HTML element to which the chart will be appended - * @param chartData {Object} Elasticsearch query results for this specific chart - */ -export class HeatmapChart extends PointSeries { - constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { - super(handler, chartEl, chartData, seriesConfigArgs, deps); - - this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); - - this.handler.visConfig.set('legend', { - labels: this.getHeatmapLabels(this.handler.visConfig), - colors: this.getHeatmapColors(this.handler.visConfig), - }); - - const colors = this.handler.visConfig.get('legend.colors', null); - if (colors) { - this.handler.vis.uiState.setSilent('vis.defaultColors', null); - this.handler.vis.uiState.setSilent('vis.defaultColors', colors); - } - } - - getHeatmapLabels(cfg) { - const percentageMode = cfg.get('percentageMode'); - const colorsNumber = cfg.get('colorsNumber'); - const colorsRange = cfg.get('colorsRange'); - const zAxisConfig = this.getValueAxis().axisConfig; - const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); - const zScale = this.getValueAxis().getScale(); - const [min, max] = zScale.domain(); - const labels = []; - const maxColorCnt = 10; - if (cfg.get('setColorRange')) { - colorsRange.forEach(range => { - const from = isFinite(range.from) ? zAxisFormatter(range.from) : range.from; - const to = isFinite(range.to) ? zAxisFormatter(range.to) : range.to; - labels.push(`${from} - ${to}`); - }); - } else { - if (max === min) { - return [min.toString()]; - } - for (let i = 0; i < colorsNumber; i++) { - let label; - let val = i / colorsNumber; - let nextVal = (i + 1) / colorsNumber; - if (percentageMode) { - val = Math.ceil(val * 100); - nextVal = Math.ceil(nextVal * 100); - label = `${val}% - ${nextVal}%`; - } else { - val = val * (max - min) + min; - nextVal = nextVal * (max - min) + min; - if (max - min > maxColorCnt) { - const valInt = Math.ceil(val); - if (i === 0) { - val = valInt === val ? val : valInt - 1; - } else { - val = valInt; - } - nextVal = Math.ceil(nextVal); - } - if (isFinite(val)) val = zAxisFormatter(val); - if (isFinite(nextVal)) nextVal = zAxisFormatter(nextVal); - label = `${val} - ${nextVal}`; - } - - labels.push(label); - } - } - - return labels; - } - - getHeatmapColors(cfg) { - const invertColors = cfg.get('invertColors'); - const colorSchema = cfg.get('colorSchema'); - const labels = this.getHeatmapLabels(cfg); - const colors = {}; - for (const i in labels) { - if (labels[i]) { - const val = invertColors ? 1 - i / labels.length : i / labels.length; - colors[labels[i]] = getHeatmapColors(val, colorSchema); - } - } - return colors; - } - - addSquares(svg, data) { - const xScale = this.getCategoryAxis().getScale(); - const yScale = this.handler.categoryAxes[1].getScale(); - const zScale = this.getValueAxis().getScale(); - const tooltip = this.baseChart.tooltip; - const isTooltip = this.handler.visConfig.get('tooltip.show'); - const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); - const colorsNumber = this.handler.visConfig.get('colorsNumber'); - const setColorRange = this.handler.visConfig.get('setColorRange'); - const colorsRange = this.handler.visConfig.get('colorsRange'); - const color = this.handler.data.getColorFunc(); - const labels = this.handler.visConfig.get('legend.labels'); - const zAxisConfig = this.getValueAxis().axisConfig; - const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); - const showLabels = zAxisConfig.get('labels.show'); - const overwriteLabelColor = zAxisConfig.get('labels.overwriteColor', false); - - const layer = svg.append('g').attr('class', 'series'); - - const squares = layer.selectAll('g.square').data(data.values); - - squares.exit().remove(); - - let barWidth; - if (this.getCategoryAxis().axisConfig.isTimeDomain()) { - const { min, interval } = this.handler.data.get('ordered'); - const start = min; - const end = moment(min) - .add(interval) - .valueOf(); - - barWidth = xScale(end) - xScale(start); - if (!isHorizontal) barWidth *= -1; - } - - function x(d) { - return xScale(d.x); - } - - function y(d) { - return yScale(d.series); - } - - const [min, max] = zScale.domain(); - function getColorBucket(d) { - let val = 0; - if (setColorRange && colorsRange.length) { - const bucket = _.find(colorsRange, range => { - return range.from <= d.y && range.to > d.y; - }); - return bucket ? colorsRange.indexOf(bucket) : -1; - } else { - if (isNaN(min) || isNaN(max)) { - val = colorsNumber - 1; - } else if (min === max) { - val = 0; - } else { - val = (d.y - min) / (max - min); /* get val from 0 - 1 */ - val = Math.min(colorsNumber - 1, Math.floor(val * colorsNumber)); - } - } - if (d.y == null) { - return -1; - } - return !isNaN(val) ? val : -1; - } - - function label(d) { - const colorBucket = getColorBucket(d); - // colorBucket id should always GTE 0 - if (colorBucket < 0) d.hide = true; - return labels[colorBucket]; - } - - function z(d) { - if (label(d) === '') return 'transparent'; - return color(label(d)); - } - - const squareWidth = barWidth || xScale.rangeBand(); - const squareHeight = yScale.rangeBand(); - - squares - .enter() - .append('g') - .attr('class', 'square'); - - squares - .append('rect') - .attr('x', x) - .attr('width', squareWidth) - .attr('y', y) - .attr('height', squareHeight) - .attr('data-label', label) - .attr('fill', z) - .attr('style', 'cursor: pointer; stroke: black; stroke-width: 0.1px') - .style('display', d => { - return d.hide ? 'none' : 'initial'; - }); - - // todo: verify that longest label is not longer than the barwidth - // or barwidth is not smaller than textheight (and vice versa) - // - if (showLabels) { - const rotate = zAxisConfig.get('labels.rotate'); - const rotateRad = (rotate * Math.PI) / 180; - const cellPadding = 5; - const maxLength = - Math.min( - Math.abs(squareWidth / Math.cos(rotateRad)), - Math.abs(squareHeight / Math.sin(rotateRad)) - ) - cellPadding; - const maxHeight = - Math.min( - Math.abs(squareWidth / Math.sin(rotateRad)), - Math.abs(squareHeight / Math.cos(rotateRad)) - ) - cellPadding; - - let labelColor; - if (overwriteLabelColor) { - // If overwriteLabelColor is true, use the manual specified color - labelColor = zAxisConfig.get('labels.color'); - } else { - // Otherwise provide a function that will calculate a light or dark color - labelColor = d => { - const bgColor = z(d); - const color = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); - return color && isColorDark(parseInt(color[1]), parseInt(color[2]), parseInt(color[3])) - ? '#FFF' - : '#222'; - }; - } - - let hiddenLabels = false; - squares - .append('text') - .text(d => zAxisFormatter(d.y)) - .style('display', function(d) { - const textLength = this.getBBox().width; - const textHeight = this.getBBox().height; - const textTooLong = textLength > maxLength; - const textTooWide = textHeight > maxHeight; - if (!d.hide && (textTooLong || textTooWide)) { - hiddenLabels = true; - } - return d.hide || textTooLong || textTooWide ? 'none' : 'initial'; - }) - .style('dominant-baseline', 'central') - .style('text-anchor', 'middle') - .style('fill', labelColor) - .attr('x', function(d) { - const center = x(d) + squareWidth / 2; - return center; - }) - .attr('y', function(d) { - const center = y(d) + squareHeight / 2; - return center; - }) - .attr('transform', function(d) { - const horizontalCenter = x(d) + squareWidth / 2; - const verticalCenter = y(d) + squareHeight / 2; - return `rotate(${rotate},${horizontalCenter},${verticalCenter})`; - }); - if (hiddenLabels) { - this.baseChart.handler.alerts.show('Some labels were hidden due to size constraints'); - } - } - - if (isTooltip) { - squares.call(tooltip.render()); - } - - return squares.selectAll('rect'); - } - - /** - * Renders d3 visualization - * - * @method draw - * @returns {Function} Creates the line chart - */ - draw() { - const self = this; - - return function(selection) { - selection.each(function() { - const svg = self.chartEl.append('g'); - svg.data([self.chartData]); - - const squares = self.addSquares(svg, self.chartData); - self.addCircleEvents(squares); - - self.events.emit('rendered', { - chart: self.chartData, - }); - - return svg; - }); - }; - } -} diff --git a/src/legacy/server/config/config.js b/src/legacy/server/config/config.js index c31ded608dd31..b186071edeaf7 100644 --- a/src/legacy/server/config/config.js +++ b/src/legacy/server/config/config.js @@ -19,7 +19,7 @@ import Joi from 'joi'; import _ from 'lodash'; -import override from './override'; +import { override } from './override'; import createDefaultSchema from './schema'; import { unset, deepCloneWithBuffers as clone, IS_KIBANA_DISTRIBUTABLE } from '../../utils'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/src/legacy/server/config/explode_by.js b/src/legacy/server/config/explode_by.js deleted file mode 100644 index 46347feca550d..0000000000000 --- a/src/legacy/server/config/explode_by.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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 _ from 'lodash'; - -export default function(dot, flatObject) { - const fullObject = {}; - _.each(flatObject, function(value, key) { - const keys = key.split(dot); - (function walk(memo, keys, value) { - const _key = keys.shift(); - if (keys.length === 0) { - memo[_key] = value; - } else { - if (!memo[_key]) memo[_key] = {}; - walk(memo[_key], keys, value); - } - })(fullObject, keys, value); - }); - return fullObject; -} diff --git a/src/legacy/server/config/explode_by.test.js b/src/legacy/server/config/explode_by.test.js deleted file mode 100644 index 741edba27d325..0000000000000 --- a/src/legacy/server/config/explode_by.test.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 explodeBy from './explode_by'; - -describe('explode_by(dot, flatObject)', function() { - it('should explode a flatten object with dots', function() { - const flatObject = { - 'test.enable': true, - 'test.hosts': ['host-01', 'host-02'], - }; - expect(explodeBy('.', flatObject)).toEqual({ - test: { - enable: true, - hosts: ['host-01', 'host-02'], - }, - }); - }); - - it('should explode a flatten object with slashes', function() { - const flatObject = { - 'test/enable': true, - 'test/hosts': ['host-01', 'host-02'], - }; - expect(explodeBy('/', flatObject)).toEqual({ - test: { - enable: true, - hosts: ['host-01', 'host-02'], - }, - }); - }); -}); diff --git a/src/legacy/server/config/override.js b/src/legacy/server/config/override.js deleted file mode 100644 index bab9387ac006f..0000000000000 --- a/src/legacy/server/config/override.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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 _ from 'lodash'; -import explodeBy from './explode_by'; -import { getFlattenedObject } from '../../../core/utils'; - -export default function(target, source) { - const _target = getFlattenedObject(target); - const _source = getFlattenedObject(source); - return explodeBy('.', _.defaults(_source, _target)); -} diff --git a/src/legacy/server/config/override.test.js b/src/legacy/server/config/override.test.js deleted file mode 100644 index 331c586e28a87..0000000000000 --- a/src/legacy/server/config/override.test.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 override from './override'; - -describe('override(target, source)', function() { - it('should override the values form source to target', function() { - const target = { - test: { - enable: true, - host: ['host-01', 'host-02'], - client: { - type: 'sql', - }, - }, - }; - const source = { test: { client: { type: 'nosql' } } }; - expect(override(target, source)).toEqual({ - test: { - enable: true, - host: ['host-01', 'host-02'], - client: { - type: 'nosql', - }, - }, - }); - }); -}); diff --git a/src/legacy/server/config/override.test.ts b/src/legacy/server/config/override.test.ts new file mode 100644 index 0000000000000..4e21a88e79e61 --- /dev/null +++ b/src/legacy/server/config/override.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { override } from './override'; + +describe('override(target, source)', function() { + it('should override the values form source to target', function() { + const target = { + test: { + enable: true, + host: ['something else'], + client: { + type: 'sql', + }, + }, + }; + + const source = { + test: { + host: ['host-01', 'host-02'], + client: { + type: 'nosql', + }, + foo: { + bar: { + baz: 1, + }, + }, + }, + }; + + expect(override(target, source)).toMatchInlineSnapshot(` + Object { + "test": Object { + "client": Object { + "type": "nosql", + }, + "enable": true, + "foo": Object { + "bar": Object { + "baz": 1, + }, + }, + "host": Array [ + "host-01", + "host-02", + ], + }, + } + `); + }); + + it('does not mutate arguments', () => { + const target = { + foo: { + bar: 1, + baz: 1, + }, + }; + + const source = { + foo: { + bar: 2, + }, + box: 2, + }; + + expect(override(target, source)).toMatchInlineSnapshot(` + Object { + "box": 2, + "foo": Object { + "bar": 2, + "baz": 1, + }, + } + `); + expect(target).not.toHaveProperty('box'); + expect(source.foo).not.toHaveProperty('baz'); + }); + + it('explodes keys with dots in them', () => { + const target = { + foo: { + bar: 1, + }, + 'baz.box.boot.bar.bar': 20, + }; + + const source = { + 'foo.bar': 2, + 'baz.box.boot': { + 'bar.foo': 10, + }, + }; + + expect(override(target, source)).toMatchInlineSnapshot(` + Object { + "baz": Object { + "box": Object { + "boot": Object { + "bar": Object { + "bar": 20, + "foo": 10, + }, + }, + }, + }, + "foo": Object { + "bar": 2, + }, + } + `); + }); +}); diff --git a/src/legacy/server/config/override.ts b/src/legacy/server/config/override.ts new file mode 100644 index 0000000000000..3dd7d62016004 --- /dev/null +++ b/src/legacy/server/config/override.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +const isObject = (v: any): v is Record => + typeof v === 'object' && v !== null && !Array.isArray(v); + +const assignDeep = (target: Record, source: Record) => { + for (let [key, value] of Object.entries(source)) { + // unwrap dot-separated keys + if (key.includes('.')) { + const [first, ...others] = key.split('.'); + key = first; + value = { [others.join('.')]: value }; + } + + if (isObject(value)) { + if (!target.hasOwnProperty(key)) { + target[key] = {}; + } + + assignDeep(target[key], value); + } else { + target[key] = value; + } + } +}; + +export const override = (...sources: Array>): Record => { + const result = {}; + + for (const object of sources) { + assignDeep(result, object); + } + + return result; +}; diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 0d2f3528c9019..02491ff872981 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -153,6 +153,7 @@ export default class KbnServer { public server: Server; public inject: Server['inject']; public pluginSpecs: any[]; + public uiBundles: any; constructor( settings: Record, diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index aaed52f8b120a..f10718ba58c2c 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -9,9 +9,7 @@ // kbnChart__legend-isLoading @import './accessibility/index'; -@import './chrome/index'; @import './directives/index'; -@import './error_auto_create_index/index'; @import './error_url_overflow/index'; @import './exit_full_screen/index'; @import './field_editor/index'; diff --git a/src/legacy/ui/public/angular_ui_select.js b/src/legacy/ui/public/angular_ui_select.js index 92b1bf53520ce..99f92587507c9 100644 --- a/src/legacy/ui/public/angular_ui_select.js +++ b/src/legacy/ui/public/angular_ui_select.js @@ -19,6 +19,7 @@ import 'jquery'; import 'angular'; +// required for `ngSanitize` angular module import 'angular-sanitize'; import 'ui-select/dist/select'; diff --git a/src/legacy/ui/public/chrome/_index.scss b/src/legacy/ui/public/chrome/_index.scss deleted file mode 100644 index 7e6c3ebaccc5c..0000000000000 --- a/src/legacy/ui/public/chrome/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './variables'; - -@import './directives/index'; diff --git a/src/legacy/ui/public/chrome/_variables.scss b/src/legacy/ui/public/chrome/_variables.scss deleted file mode 100644 index 5097fe4c9bfae..0000000000000 --- a/src/legacy/ui/public/chrome/_variables.scss +++ /dev/null @@ -1,4 +0,0 @@ -$kbnGlobalNavClosedWidth: 53px; -$kbnGlobalNavOpenWidth: 180px; -$kbnGlobalNavLogoHeight: 70px; -$kbnGlobalNavAppIconHeight: $euiSizeXXL + $euiSizeXS; diff --git a/src/legacy/ui/public/chrome/directives/_index.scss b/src/legacy/ui/public/chrome/directives/_index.scss deleted file mode 100644 index 4d00b02279116..0000000000000 --- a/src/legacy/ui/public/chrome/directives/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './kbn_chrome'; diff --git a/src/legacy/ui/public/chrome/directives/_kbn_chrome.scss b/src/legacy/ui/public/chrome/directives/_kbn_chrome.scss deleted file mode 100644 index b29a83848d291..0000000000000 --- a/src/legacy/ui/public/chrome/directives/_kbn_chrome.scss +++ /dev/null @@ -1,46 +0,0 @@ -/** - * stretch the root element of the Kibana application to set the base-size that - * flexed children should keep. Only works when paired with root styles applied - * by core service from new platform - */ -// SASSTODO: Naming here is too embedded and high up that changing them could cause major breaks -#kibana-body { - overflow-x: hidden; - min-height: 100%; -} - -.app-wrapper { - display: flex; - flex-flow: column nowrap; - position: absolute; - left: $kbnGlobalNavClosedWidth; - top: 0; - right: 0; - bottom: 0; - z-index: 5; - margin: 0 auto; - - /** - * 1. Dirty, but we need to override the .kbnGlobalNav-isOpen state - * when we're looking at the log-in screen. - */ - &.hidden-chrome { - left: 0 !important; /* 1 */ - } - - .navbar-right { - margin-right: 0; - } -} - -.app-wrapper-panel { - display: flex; - flex-grow: 1; - flex-shrink: 0; - flex-basis: auto; - flex-direction: column; - - > * { - flex-shrink: 0; - } -} diff --git a/src/legacy/ui/public/error_auto_create_index/_error_auto_create_index.scss b/src/legacy/ui/public/error_auto_create_index/_error_auto_create_index.scss deleted file mode 100644 index ad31aabfc66cd..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/_error_auto_create_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -.kbnError--auto-create-index { - padding: $euiSizeL; -} diff --git a/src/legacy/ui/public/error_auto_create_index/_index.scss b/src/legacy/ui/public/error_auto_create_index/_index.scss deleted file mode 100644 index 42e672ab322dc..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './error_auto_create_index' diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.html b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.html deleted file mode 100644 index 2af31dda6c345..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.html +++ /dev/null @@ -1,69 +0,0 @@ -
-

- - -

- -

- -

- -

-
    -
  1. -
  2. -
  3. -
- -
-
- - -
- -
-
-
-
-
diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js deleted file mode 100644 index a8f6318090b1d..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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. - */ - -// @ts-ignore -import './error_auto_create_index.test.mocks'; -import fetchMock from 'fetch-mock/es5/client'; -import { kfetch } from '../kfetch'; - -import { isAutoCreateIndexError } from './error_auto_create_index'; - -describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch', () => { - describe('404', () => { - beforeEach(() => { - fetchMock.post({ - matcher: '*', - response: { - status: 404, - }, - }); - }); - afterEach(() => fetchMock.restore()); - - test('should return false', async () => { - expect.assertions(1); - try { - await kfetch({ method: 'POST', pathname: '/my/path' }); - } catch (kfetchError) { - expect(isAutoCreateIndexError(kfetchError)).toBe(false); - } - }); - }); - - describe('503 error that is not ES_AUTO_CREATE_INDEX_ERROR', () => { - beforeEach(() => { - fetchMock.post({ - matcher: '*', - response: { - status: 503, - }, - }); - }); - afterEach(() => fetchMock.restore()); - - test('should return false', async () => { - expect.assertions(1); - try { - await kfetch({ method: 'POST', pathname: '/my/path' }); - } catch (kfetchError) { - expect(isAutoCreateIndexError(kfetchError)).toBe(false); - } - }); - }); - - describe('503 error that is ES_AUTO_CREATE_INDEX_ERROR', () => { - beforeEach(() => { - fetchMock.post({ - matcher: '*', - response: { - body: { - attributes: { - code: 'ES_AUTO_CREATE_INDEX_ERROR', - }, - }, - status: 503, - }, - }); - }); - afterEach(() => fetchMock.restore()); - - test('should return true', async () => { - expect.assertions(1); - try { - await kfetch({ method: 'POST', pathname: '/my/path' }); - } catch (kfetchError) { - expect(isAutoCreateIndexError(kfetchError)).toBe(true); - } - }); - }); -}); diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js deleted file mode 100644 index 1ac30b85c5a85..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 { setup } from '../../../../test_utils/public/http_test_setup'; - -jest.doMock('ui/new_platform', () => ({ npSetup: { core: setup() } })); diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts deleted file mode 100644 index 09c6bfd93148f..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; - -import uiRoutes from '../routes'; - -import template from './error_auto_create_index.html'; - -uiRoutes.when('/error/action.auto_create_index', { - template, - k7Breadcrumbs: () => [ - { - text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { - defaultMessage: 'Error', - }), - }, - ], -}); - -export function isAutoCreateIndexError(error: object) { - return ( - get(error, 'res.status') === 503 && - get(error, 'body.attributes.code') === 'ES_AUTO_CREATE_INDEX_ERROR' - ); -} - -export function showAutoCreateIndexErrorPage() { - window.location.hash = '/error/action.auto_create_index'; -} diff --git a/src/legacy/ui/public/error_auto_create_index/index.ts b/src/legacy/ui/public/error_auto_create_index/index.ts deleted file mode 100644 index d290e0334b3d6..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { isAutoCreateIndexError, showAutoCreateIndexErrorPage } from './error_auto_create_index'; diff --git a/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap b/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap deleted file mode 100644 index 19d12f4bbbd4c..0000000000000 --- a/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.js.snap +++ /dev/null @@ -1,1450 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldEditor should render create new scripted field correctly 1`] = ` -
- -

- -

-
- - - - - - - - - - - - - - - - } - label={ - - Test format - , - } - } - /> - } - > - - - - - - - } - fullWidth={true} - isInvalid={true} - label="Script" - > - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should render edit scripted field correctly 1`] = ` -
- -

- -

-
- - - - - - - - - - - - - } - label={ - - Test format - , - } - } - /> - } - > - - - - - - - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should show conflict field warning 1`] = ` -
- -

- -

-
- - - - - - - -   - - foobar - , - "mappingConflict": - - , - } - } - /> - - } - isInvalid={false} - label="Name" - > - - - - - - - - - - } - label={ - - Test format - , - } - } - /> - } - > - - - - - - - } - fullWidth={true} - isInvalid={true} - label="Script" - > - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should show deprecated lang warning 1`] = ` -
- -

- -

-
- - - - - - - -   - - - -   - - testlang - , - "painlessLink": - - , - } - } - /> - - } - label="Language" - > - - - - - - - } - label={ - - Test format - , - } - } - /> - } - > - - - - - - - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - -
- -
-`; - -exports[`FieldEditor should show multiple type field warning with a table containing indices 1`] = ` -
- -

- -

-
- - - - - - - -   - - foobar - , - "mappingConflict": - - , - } - } - /> - - } - isInvalid={false} - label="Name" - > - - - - - - - - -
- - - } - > - - - - - -
- - } - label={ - - Test format - , - } - } - /> - } - > - - - - - - - } - fullWidth={true} - isInvalid={true} - label="Script" - > - - - - - - doc['some_field'].value - , - } - } - /> - -
- - - -
- - - - - - - - - - - - - - -
- -
-`; diff --git a/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.tsx.snap b/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.tsx.snap new file mode 100644 index 0000000000000..dc11bdfefa46b --- /dev/null +++ b/src/legacy/ui/public/field_editor/__snapshots__/field_editor.test.tsx.snap @@ -0,0 +1,1525 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldEditor should render create new scripted field correctly 1`] = ` +
+ +

+ +

+
+ + + + + + + + + + + + + + + + } + label={ + + } + > + + + + + + + } + fullWidth={true} + isInvalid={true} + label="Script" + > + + + + + + doc['some_field'].value + , + } + } + /> + +
+ + + +
+ + + + + + + + + + + + + + +
+ +
+`; + +exports[`FieldEditor should render edit scripted field correctly 1`] = ` +
+ +

+ +

+
+ + + + + + + + + + + + + } + label={ + + } + > + + + + + + + + + + + + doc['some_field'].value + , + } + } + /> + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+`; + +exports[`FieldEditor should show conflict field warning 1`] = ` +
+ +

+ +

+
+ + + + + + + +   + + foobar + , + "mappingConflict": + + , + } + } + /> + + } + isInvalid={false} + label="Name" + > + + + + + + + + + + } + label={ + + } + > + + + + + + + } + fullWidth={true} + isInvalid={true} + label="Script" + > + + + + + + doc['some_field'].value + , + } + } + /> + +
+ + + +
+ + + + + + + + + + + + + + +
+ +
+`; + +exports[`FieldEditor should show deprecated lang warning 1`] = ` +
+ +

+ +

+
+ + + + + + + +   + + + +   + + testlang + , + "painlessLink": + + , + } + } + /> + + } + label="Language" + > + + + + + + + } + label={ + + } + > + + + + + + + + + + + + doc['some_field'].value + , + } + } + /> + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+`; + +exports[`FieldEditor should show multiple type field warning with a table containing indices 1`] = ` +
+ +

+ +

+
+ + + + + + + +   + + foobar + , + "mappingConflict": + + , + } + } + /> + + } + isInvalid={false} + label="Name" + > + + + + + + + + +
+ + + } + > + + + + + +
+ + } + label={ + + } + > + + + + + + + } + fullWidth={true} + isInvalid={true} + label="Script" + > + + + + + + doc['some_field'].value + , + } + } + /> + +
+ + + +
+ + + + + + + + + + + + + + +
+ +
+`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.js.snap deleted file mode 100644 index b83ae8a901ecc..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FieldFormatEditor should render normally 1`] = ` - - - -`; - -exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = ``; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.tsx.snap new file mode 100644 index 0000000000000..82d21eb5d30ad --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/__snapshots__/field_format_editor.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldFormatEditor should render normally 1`] = ` + + + +`; + +exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = ` + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.js.snap deleted file mode 100644 index 1f77660c9784c..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.js.snap +++ /dev/null @@ -1,76 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BytesFormatEditor should render normally 1`] = ` - - - - -   - - - - } - isInvalid={false} - label={ - - 0,0.[000]b - , - } - } - /> - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap new file mode 100644 index 0000000000000..bf1682faf9a9d --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -0,0 +1,76 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BytesFormatEditor should render normally 1`] = ` + + + + +   + + + + } + isInvalid={false} + label={ + + 0,0.[000]b + , + } + } + /> + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.js deleted file mode 100644 index cb5d0758efcdb..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { NumberFormatEditor } from '../number'; - -export class BytesFormatEditor extends NumberFormatEditor { - static formatId = 'bytes'; - - constructor(props) { - super(props); - - this.state = { - ...this.state, - sampleInputs: [256, 1024, 5150000, 1990000000], - }; - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.test.js deleted file mode 100644 index fea42dee6b690..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { BytesFormatEditor } from './bytes'; - -const fieldType = 'number'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => input * 2), - getParamDefaults: jest.fn().mockImplementation(() => { - return { pattern: '0,0.[000]b' }; - }), -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('BytesFormatEditor', () => { - it('should have a formatId', () => { - expect(BytesFormatEditor.formatId).toEqual('bytes'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx new file mode 100644 index 0000000000000..40443ec262182 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { BytesFormatEditor } from './bytes'; +import { FieldFormat } from 'src/plugins/data/public'; + +const fieldType = 'number'; +const format = { + getConverterFor: jest.fn().mockImplementation(() => (input: number) => input * 2), + getParamDefaults: jest.fn().mockImplementation(() => { + return { pattern: '0,0.[000]b' }; + }), +}; +const formatParams = { + pattern: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('BytesFormatEditor', () => { + it('should have a formatId', () => { + expect(BytesFormatEditor.formatId).toEqual('bytes'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.ts b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.ts new file mode 100644 index 0000000000000..aa0f9ebb6567a --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/bytes.ts @@ -0,0 +1,29 @@ +/* + * 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 { NumberFormatEditor } from '../number'; +import { defaultState } from '../default'; + +export class BytesFormatEditor extends NumberFormatEditor { + static formatId = 'bytes'; + state = { + ...defaultState, + sampleInputs: [256, 1024, 5150000, 1990000000], + }; +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/bytes/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.js.snap deleted file mode 100644 index 7e49e93e4cc4f..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.js.snap +++ /dev/null @@ -1,278 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ColorFormatEditor should render multiple colors 1`] = ` - - , - "render": [Function], - }, - Object { - "field": "text", - "name": , - "render": [Function], - }, - Object { - "field": "background", - "name": , - "render": [Function], - }, - Object { - "name": , - "render": [Function], - }, - Object { - "actions": Array [ - Object { - "available": [Function], - "color": "danger", - "description": "Delete color format", - "icon": "trash", - "name": "Delete", - "onClick": [Function], - "type": "icon", - }, - ], - }, - ] - } - items={ - Array [ - Object { - "background": "#ffffff", - "index": 0, - "range": "-Infinity:Infinity", - "regex": "", - "text": "#000000", - }, - Object { - "background": "#ffffff", - "index": 1, - "range": "-Infinity:Infinity", - "regex": "", - "text": "#000000", - }, - ] - } - noItemsMessage="No items found" - responsive={true} - tableLayout="fixed" - /> - - - - - - -`; - -exports[`ColorFormatEditor should render other type normally (range field) 1`] = ` - - , - "render": [Function], - }, - Object { - "field": "text", - "name": , - "render": [Function], - }, - Object { - "field": "background", - "name": , - "render": [Function], - }, - Object { - "name": , - "render": [Function], - }, - Object { - "actions": Array [ - Object { - "available": [Function], - "color": "danger", - "description": "Delete color format", - "icon": "trash", - "name": "Delete", - "onClick": [Function], - "type": "icon", - }, - ], - }, - ] - } - items={ - Array [ - Object { - "background": "#ffffff", - "index": 0, - "range": "-Infinity:Infinity", - "regex": "", - "text": "#000000", - }, - ] - } - noItemsMessage="No items found" - responsive={true} - tableLayout="fixed" - /> - - - - - - -`; - -exports[`ColorFormatEditor should render string type normally (regex field) 1`] = ` - - , - "render": [Function], - }, - Object { - "field": "text", - "name": , - "render": [Function], - }, - Object { - "field": "background", - "name": , - "render": [Function], - }, - Object { - "name": , - "render": [Function], - }, - Object { - "actions": Array [ - Object { - "available": [Function], - "color": "danger", - "description": "Delete color format", - "icon": "trash", - "name": "Delete", - "onClick": [Function], - "type": "icon", - }, - ], - }, - ] - } - items={ - Array [ - Object { - "background": "#ffffff", - "index": 0, - "range": "-Infinity:Infinity", - "regex": "", - "text": "#000000", - }, - ] - } - noItemsMessage="No items found" - responsive={true} - tableLayout="fixed" - /> - - - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap new file mode 100644 index 0000000000000..e93c3c0661c16 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap @@ -0,0 +1,284 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ColorFormatEditor should render multiple colors 1`] = ` + + , + "render": [Function], + }, + Object { + "field": "text", + "name": , + "render": [Function], + }, + Object { + "field": "background", + "name": , + "render": [Function], + }, + Object { + "name": , + "render": [Function], + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "color": "danger", + "description": "Delete color format", + "icon": "trash", + "name": "Delete", + "onClick": [Function], + "type": "icon", + }, + ], + "field": "actions", + "name": "Actions", + }, + ] + } + items={ + Array [ + Object { + "background": "#ffffff", + "index": 0, + "range": "-Infinity:Infinity", + "regex": "", + "text": "#000000", + }, + Object { + "background": "#ffffff", + "index": 1, + "range": "-Infinity:Infinity", + "regex": "", + "text": "#000000", + }, + ] + } + noItemsMessage="No items found" + responsive={true} + tableLayout="fixed" + /> + + + + + + +`; + +exports[`ColorFormatEditor should render other type normally (range field) 1`] = ` + + , + "render": [Function], + }, + Object { + "field": "text", + "name": , + "render": [Function], + }, + Object { + "field": "background", + "name": , + "render": [Function], + }, + Object { + "name": , + "render": [Function], + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "color": "danger", + "description": "Delete color format", + "icon": "trash", + "name": "Delete", + "onClick": [Function], + "type": "icon", + }, + ], + "field": "actions", + "name": "Actions", + }, + ] + } + items={ + Array [ + Object { + "background": "#ffffff", + "index": 0, + "range": "-Infinity:Infinity", + "regex": "", + "text": "#000000", + }, + ] + } + noItemsMessage="No items found" + responsive={true} + tableLayout="fixed" + /> + + + + + + +`; + +exports[`ColorFormatEditor should render string type normally (regex field) 1`] = ` + + , + "render": [Function], + }, + Object { + "field": "text", + "name": , + "render": [Function], + }, + Object { + "field": "background", + "name": , + "render": [Function], + }, + Object { + "name": , + "render": [Function], + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "color": "danger", + "description": "Delete color format", + "icon": "trash", + "name": "Delete", + "onClick": [Function], + "type": "icon", + }, + ], + "field": "actions", + "name": "Actions", + }, + ] + } + items={ + Array [ + Object { + "background": "#ffffff", + "index": 0, + "range": "-Infinity:Infinity", + "regex": "", + "text": "#000000", + }, + ] + } + noItemsMessage="No items found" + responsive={true} + tableLayout="fixed" + /> + + + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.js deleted file mode 100644 index 4ad04f08915e7..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.js +++ /dev/null @@ -1,234 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { EuiBasicTable, EuiButton, EuiColorPicker, EuiFieldText, EuiSpacer } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { fieldFormats } from '../../../../../../../../plugins/data/public'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class ColorFormatEditor extends DefaultFormatEditor { - constructor(props) { - super(props); - this.onChange({ - fieldType: props.fieldType, - }); - } - - onColorChange = (newColorParams, index) => { - const colors = [...this.props.formatParams.colors]; - colors[index] = { - ...colors[index], - ...newColorParams, - }; - this.onChange({ - colors, - }); - }; - - addColor = () => { - const colors = [...this.props.formatParams.colors]; - this.onChange({ - colors: [...colors, { ...fieldFormats.DEFAULT_CONVERTER_COLOR }], - }); - }; - - removeColor = index => { - const colors = [...this.props.formatParams.colors]; - colors.splice(index, 1); - this.onChange({ - colors, - }); - }; - - render() { - const { formatParams, fieldType } = this.props; - - const items = - (formatParams.colors && - formatParams.colors.length && - formatParams.colors.map((color, index) => { - return { - ...color, - index, - }; - })) || - []; - - const columns = [ - fieldType === 'string' - ? { - field: 'regex', - name: ( - - ), - render: (value, item) => { - return ( - { - this.onColorChange( - { - regex: e.target.value, - }, - item.index - ); - }} - /> - ); - }, - } - : { - field: 'range', - name: ( - - ), - render: (value, item) => { - return ( - { - this.onColorChange( - { - range: e.target.value, - }, - item.index - ); - }} - /> - ); - }, - }, - { - field: 'text', - name: ( - - ), - render: (color, item) => { - return ( - { - this.onColorChange( - { - text: newColor, - }, - item.index - ); - }} - /> - ); - }, - }, - { - field: 'background', - name: ( - - ), - render: (color, item) => { - return ( - { - this.onColorChange( - { - background: newColor, - }, - item.index - ); - }} - /> - ); - }, - }, - { - name: ( - - ), - render: item => { - return ( -
- 123456 -
- ); - }, - }, - { - actions: [ - { - name: i18n.translate('common.ui.fieldEditor.color.deleteAria', { - defaultMessage: 'Delete', - }), - description: i18n.translate('common.ui.fieldEditor.color.deleteTitle', { - defaultMessage: 'Delete color format', - }), - onClick: item => { - this.removeColor(item.index); - }, - type: 'icon', - icon: 'trash', - color: 'danger', - available: () => items.length > 1, - }, - ], - }, - ]; - - return ( - - - - - - - - - ); - } -} - -ColorFormatEditor.formatId = 'color'; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.test.js deleted file mode 100644 index 486b1e34dcade..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.test.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { ColorFormatEditor } from './color'; -import { fieldFormats } from '../../../../../../../../plugins/data/public'; - -const fieldType = 'string'; -const format = { - getConverterFor: jest.fn(), -}; -const formatParams = { - colors: [{ ...fieldFormats.DEFAULT_CONVERTER_COLOR }], -}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('ColorFormatEditor', () => { - it('should have a formatId', () => { - expect(ColorFormatEditor.formatId).toEqual('color'); - }); - - it('should render string type normally (regex field)', async () => { - const component = shallowWithI18nProvider( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render other type normally (range field)', async () => { - const component = shallowWithI18nProvider( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render multiple colors', async () => { - const component = shallowWithI18nProvider( - - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.test.tsx new file mode 100644 index 0000000000000..549831e9c3fb2 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 React from 'react'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { FieldFormat } from 'src/plugins/data/public'; + +import { ColorFormatEditor } from './color'; +import { fieldFormats } from '../../../../../../../../plugins/data/public'; + +const fieldType = 'string'; +const format = { + getConverterFor: jest.fn(), +}; +const formatParams = { + colors: [{ ...fieldFormats.DEFAULT_CONVERTER_COLOR }], +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('ColorFormatEditor', () => { + it('should have a formatId', () => { + expect(ColorFormatEditor.formatId).toEqual('color'); + }); + + it('should render string type normally (regex field)', async () => { + const component = shallowWithI18nProvider( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render other type normally (range field)', async () => { + const component = shallowWithI18nProvider( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render multiple colors', async () => { + const component = shallowWithI18nProvider( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.tsx new file mode 100644 index 0000000000000..1fdc36d4e8549 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/color.tsx @@ -0,0 +1,251 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { EuiBasicTable, EuiButton, EuiColorPicker, EuiFieldText, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor, FormatEditorProps } from '../default'; + +import { fieldFormats } from '../../../../../../../../plugins/data/public'; + +interface Color { + range?: string; + regex?: string; + text: string; + background: string; +} + +interface IndexedColor extends Color { + index: number; +} + +interface ColorFormatEditorFormatParams { + colors: Color[]; +} + +export class ColorFormatEditor extends DefaultFormatEditor { + static formatId = 'color'; + constructor(props: FormatEditorProps) { + super(props); + this.onChange({ + fieldType: props.fieldType, + }); + } + + onColorChange = (newColorParams: Partial, index: number) => { + const colors = [...this.props.formatParams.colors]; + colors[index] = { + ...colors[index], + ...newColorParams, + }; + this.onChange({ + colors, + }); + }; + + addColor = () => { + const colors = [...this.props.formatParams.colors]; + this.onChange({ + colors: [...colors, { ...fieldFormats.DEFAULT_CONVERTER_COLOR }], + }); + }; + + removeColor = (index: number) => { + const colors = [...this.props.formatParams.colors]; + colors.splice(index, 1); + this.onChange({ + colors, + }); + }; + + render() { + const { formatParams, fieldType } = this.props; + + const items = + (formatParams.colors && + formatParams.colors.length && + formatParams.colors.map((color, index) => { + return { + ...color, + index, + }; + })) || + []; + + const columns = [ + fieldType === 'string' + ? { + field: 'regex', + name: ( + + ), + render: (value: string, item: IndexedColor) => { + return ( + { + this.onColorChange( + { + regex: e.target.value, + }, + item.index + ); + }} + /> + ); + }, + } + : { + field: 'range', + name: ( + + ), + render: (value: string, item: IndexedColor) => { + return ( + { + this.onColorChange( + { + range: e.target.value, + }, + item.index + ); + }} + /> + ); + }, + }, + { + field: 'text', + name: ( + + ), + render: (color: string, item: IndexedColor) => { + return ( + { + this.onColorChange( + { + text: newColor, + }, + item.index + ); + }} + /> + ); + }, + }, + { + field: 'background', + name: ( + + ), + render: (color: string, item: IndexedColor) => { + return ( + { + this.onColorChange( + { + background: newColor, + }, + item.index + ); + }} + /> + ); + }, + }, + { + name: ( + + ), + render: (item: IndexedColor) => { + return ( +
+ 123456 +
+ ); + }, + }, + { + field: 'actions', + name: i18n.translate('common.ui.fieldEditor.color.actions', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('common.ui.fieldEditor.color.deleteAria', { + defaultMessage: 'Delete', + }), + description: i18n.translate('common.ui.fieldEditor.color.deleteTitle', { + defaultMessage: 'Delete color format', + }), + onClick: (item: IndexedColor) => { + this.removeColor(item.index); + }, + type: 'icon', + icon: 'trash', + color: 'danger', + available: () => items.length > 1, + }, + ], + }, + ]; + + return ( + + + + + + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/color/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.js.snap deleted file mode 100644 index e33f0d6ee9c61..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DateFormatEditor should render normally 1`] = ` - - - - -   - - - - } - isInvalid={false} - label={ - - MMMM Do YYYY, HH:mm:ss.SSS - , - } - } - /> - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap new file mode 100644 index 0000000000000..2d73f775e316c --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DateFormatEditor should render normally 1`] = ` + + + + +   + + + + } + isInvalid={false} + label={ + + MMMM Do YYYY, HH:mm:ss.SSS + , + } + } + /> + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.js deleted file mode 100644 index 69cdb159dd4aa..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; -import moment from 'moment'; - -import { EuiCode, EuiFieldText, EuiFormRow, EuiIcon, EuiLink } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { FormatEditorSamples } from '../../samples'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export class DateFormatEditor extends DefaultFormatEditor { - static formatId = 'date'; - - constructor(props) { - super(props); - this.state.sampleInputs = [ - Date.now(), - moment() - .startOf('year') - .valueOf(), - moment() - .endOf('year') - .valueOf(), - ]; - } - - render() { - const { format, formatParams } = this.props; - const { error, samples } = this.state; - const defaultPattern = format.getParamDefaults().pattern; - - return ( - - {defaultPattern}, - }} - /> - } - isInvalid={!!error} - error={error} - helpText={ - - - -   - - - - } - > - { - this.onChange({ pattern: e.target.value }); - }} - isInvalid={!!error} - /> - - - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.test.js deleted file mode 100644 index 3aa60aa29fcb8..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.test.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { DateFormatEditor } from './date'; - -const fieldType = 'date'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => `converted date for ${input}`), - getParamDefaults: jest.fn().mockImplementation(() => { - return { pattern: 'MMMM Do YYYY, HH:mm:ss.SSS' }; - }), -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('DateFormatEditor', () => { - it('should have a formatId', () => { - expect(DateFormatEditor.formatId).toEqual('date'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - - // Date editor samples uses changing values - Date.now() - so we - // hardcode samples to avoid ever-changing snapshots - component.setState({ - sampleInputs: [1529097045190, 1514793600000, 1546329599999], - }); - - component.instance().forceUpdate(); - component.update(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.test.tsx new file mode 100644 index 0000000000000..746cb7c7fe302 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.test.tsx @@ -0,0 +1,66 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { FieldFormat } from 'src/plugins/data/public'; + +import { DateFormatEditor } from './date'; + +const fieldType = 'date'; +const format = { + getConverterFor: jest + .fn() + .mockImplementation(() => (input: string) => `converted date for ${input}`), + getParamDefaults: jest.fn().mockImplementation(() => { + return { pattern: 'MMMM Do YYYY, HH:mm:ss.SSS' }; + }), +}; +const formatParams = { pattern: '' }; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('DateFormatEditor', () => { + it('should have a formatId', () => { + expect(DateFormatEditor.formatId).toEqual('date'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + + // Date editor samples uses changing values - Date.now() - so we + // hardcode samples to avoid ever-changing snapshots + component.setState({ + sampleInputs: [1529097045190, 1514793600000, 1546329599999], + }); + + component.instance().forceUpdate(); + component.update(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.tsx new file mode 100644 index 0000000000000..8bbb379f5df5d --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/date.tsx @@ -0,0 +1,95 @@ +/* + * 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 React, { Fragment } from 'react'; +import moment from 'moment'; + +import { EuiCode, EuiFieldText, EuiFormRow, EuiIcon, EuiLink } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor, defaultState } from '../default'; + +import { FormatEditorSamples } from '../../samples'; + +interface DateFormatEditorFormatParams { + pattern: string; +} + +export class DateFormatEditor extends DefaultFormatEditor { + static formatId = 'date'; + state = { + ...defaultState, + sampleInputs: [ + Date.now(), + moment() + .startOf('year') + .valueOf(), + moment() + .endOf('year') + .valueOf(), + ], + }; + + render() { + const { format, formatParams } = this.props; + const { error, samples } = this.state; + const defaultPattern = format.getParamDefaults().pattern; + + return ( + + {defaultPattern}, + }} + /> + } + isInvalid={!!error} + error={error} + helpText={ + + + +   + + + + } + > + { + this.onChange({ pattern: e.target.value }); + }} + isInvalid={!!error} + /> + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/date/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.js.snap deleted file mode 100644 index cb570144fcee3..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.js.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DateFormatEditor should render normally 1`] = ` - - - - -   - - - - } - isInvalid={false} - label={ - - MMM D, YYYY @ HH:mm:ss.SSSSSSSSS - , - } - } - /> - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap new file mode 100644 index 0000000000000..1456deaa13e47 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DateFormatEditor should render normally 1`] = ` + + + + +   + + + + } + isInvalid={false} + label={ + + MMM D, YYYY @ HH:mm:ss.SSSSSSSSS + , + } + } + /> + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.js deleted file mode 100644 index 0660d06dda9df..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { EuiCode, EuiFieldText, EuiFormRow, EuiIcon, EuiLink } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { FormatEditorSamples } from '../../samples'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export class DateNanosFormatEditor extends DefaultFormatEditor { - static formatId = 'date_nanos'; - - constructor(props) { - super(props); - this.state.sampleInputs = [ - '2015-01-01T12:10:30.123456789Z', - '2019-05-08T06:55:21.567891234Z', - '2019-08-06T17:22:30.987654321Z', - ]; - } - - render() { - const { format, formatParams } = this.props; - const { error, samples } = this.state; - const defaultPattern = format.getParamDefaults().pattern; - - return ( - - {defaultPattern}, - }} - /> - } - isInvalid={!!error} - error={error} - helpText={ - - - -   - - - - } - > - { - this.onChange({ pattern: e.target.value }); - }} - isInvalid={!!error} - /> - - - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.js deleted file mode 100644 index 32cc55c304e5b..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { DateNanosFormatEditor } from './date_nanos'; - -const fieldType = 'date_nanos'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => `converted date for ${input}`), - getParamDefaults: jest.fn().mockImplementation(() => { - return { pattern: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS' }; - }), -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('DateFormatEditor', () => { - it('should have a formatId', () => { - expect(DateNanosFormatEditor.formatId).toEqual('date_nanos'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx new file mode 100644 index 0000000000000..e6b15c741af4e --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx @@ -0,0 +1,60 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { FieldFormat } from '../../../../../../../../plugins/data/public'; + +import { DateNanosFormatEditor } from './date_nanos'; + +const fieldType = 'date_nanos'; +const format = { + getConverterFor: jest + .fn() + .mockImplementation(() => (input: string) => `converted date for ${input}`), + getParamDefaults: jest.fn().mockImplementation(() => { + return { pattern: 'MMM D, YYYY @ HH:mm:ss.SSSSSSSSS' }; + }), +}; +const formatParams = { + pattern: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('DateFormatEditor', () => { + it('should have a formatId', () => { + expect(DateNanosFormatEditor.formatId).toEqual('date_nanos'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx new file mode 100644 index 0000000000000..bfce3c973a1fa --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx @@ -0,0 +1,90 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { EuiCode, EuiFieldText, EuiFormRow, EuiIcon, EuiLink } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor, defaultState } from '../default'; + +import { FormatEditorSamples } from '../../samples'; + +interface DateNanosFormatEditorFormatParams { + pattern: string; +} + +export class DateNanosFormatEditor extends DefaultFormatEditor { + static formatId = 'date_nanos'; + state = { + ...defaultState, + sampleInputs: [ + '2015-01-01T12:10:30.123456789Z', + '2019-05-08T06:55:21.567891234Z', + '2019-08-06T17:22:30.987654321Z', + ], + }; + + render() { + const { format, formatParams } = this.props; + const { error, samples } = this.state; + const defaultPattern = format.getParamDefaults().pattern; + + return ( + + {defaultPattern}, + }} + /> + } + isInvalid={!!error} + error={error} + helpText={ + + + +   + + + + } + > + { + this.onChange({ pattern: e.target.value }); + }} + isInvalid={!!error} + /> + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/date_nanos/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.js.snap deleted file mode 100644 index 0f9eef6f922c8..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DefaultFormatEditor should render nothing 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap new file mode 100644 index 0000000000000..74468ea1451f4 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DefaultFormatEditor should render nothing 1`] = ``; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.js deleted file mode 100644 index 6c4cfc9eb2106..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 { PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { i18n } from '@kbn/i18n'; - -export const convertSampleInput = (converter, inputs) => { - let error = null; - let samples = []; - - try { - samples = inputs.map(input => { - return { - input, - output: converter(input), - }; - }); - } catch (e) { - error = i18n.translate('common.ui.fieldEditor.defaultErrorMessage', { - defaultMessage: 'An error occurred while trying to use this format configuration: {message}', - values: { message: e.message }, - }); - } - - return { - error, - samples, - }; -}; - -export class DefaultFormatEditor extends PureComponent { - static propTypes = { - fieldType: PropTypes.string.isRequired, - format: PropTypes.object.isRequired, - formatParams: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - onError: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - sampleInputs: [], - sampleConverterType: 'text', - error: null, - samples: [], - }; - } - - static getDerivedStateFromProps(nextProps, state) { - const { format, formatParams, onError } = nextProps; - const { sampleInputsByType, sampleInputs, sampleConverterType } = state; - - const converter = format.getConverterFor(sampleConverterType); - const type = typeof sampleInputsByType === 'object' && formatParams.type; - const inputs = type ? sampleInputsByType[formatParams.type] || [] : sampleInputs; - const output = convertSampleInput(converter, inputs); - onError(output.error); - return output; - } - - onChange = (newParams = {}) => { - const { onChange, formatParams } = this.props; - onChange({ - ...formatParams, - ...newParams, - }); - }; - - render() { - return null; - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.test.js deleted file mode 100644 index cf87d4a886024..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.test.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { DefaultFormatEditor, convertSampleInput } from './default'; - -const fieldType = 'number'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => () => {}), -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('DefaultFormatEditor', () => { - describe('convertSampleInput', () => { - const converter = input => { - if (isNaN(input)) { - throw { - message: 'Input is not a number', - }; - } else { - return input * 2; - } - }; - - it('should convert a set of inputs', () => { - const inputs = [1, 10, 15]; - const output = convertSampleInput(converter, inputs); - - expect(output.error).toEqual(null); - expect(JSON.stringify(output.samples)).toEqual( - JSON.stringify([ - { input: 1, output: 2 }, - { input: 10, output: 20 }, - { input: 15, output: 30 }, - ]) - ); - }); - - it('should return error if converter throws one', () => { - const inputs = [1, 10, 15, 'invalid']; - const output = convertSampleInput(converter, inputs); - - expect(output.error).toEqual( - 'An error occurred while trying to use this format configuration: Input is not a number' - ); - expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([])); - }); - }); - - it('should render nothing', async () => { - const component = shallow( - - ); - - expect(format.getConverterFor).toBeCalled(); - expect(onError).toBeCalled(); - expect(component).toMatchSnapshot(); - }); - - it('should call prop onChange()', async () => { - const component = shallow( - - ); - - component.instance().onChange(); - expect(onChange).toBeCalled(); - }); - - it('should call prop onError() if converter throws an error', async () => { - const newFormat = { - getConverterFor: jest.fn().mockImplementation(() => () => { - throw { message: 'Test error message' }; - }), - }; - - shallow( - - ); - - expect(onError).toBeCalled(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.test.tsx new file mode 100644 index 0000000000000..3f30af97dc063 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.test.tsx @@ -0,0 +1,122 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { FieldFormat } from 'src/plugins/data/public'; + +import { DefaultFormatEditor, convertSampleInput, ConverterParams } from './default'; + +const fieldType = 'number'; +const format = { + getConverterFor: jest.fn().mockImplementation(() => () => {}), +}; +const formatParams = {}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('DefaultFormatEditor', () => { + describe('convertSampleInput', () => { + const converter = (input: ConverterParams) => { + if (typeof input !== 'number') { + throw new Error('Input is not a number'); + } else { + return (input * 2).toString(); + } + }; + + it('should convert a set of inputs', () => { + const inputs = [1, 10, 15]; + const output = convertSampleInput(converter, inputs); + + expect(output.error).toBeUndefined(); + expect(JSON.stringify(output.samples)).toEqual( + JSON.stringify([ + { input: 1, output: '2' }, + { input: 10, output: '20' }, + { input: 15, output: '30' }, + ]) + ); + }); + + it('should return error if converter throws one', () => { + const inputs = [1, 10, 15, 'invalid']; + const output = convertSampleInput(converter, inputs); + + expect(output.error).toEqual( + 'An error occurred while trying to use this format configuration: Input is not a number' + ); + expect(JSON.stringify(output.samples)).toEqual(JSON.stringify([])); + }); + }); + + it('should render nothing', async () => { + const component = shallow( + + ); + + expect(format.getConverterFor).toBeCalled(); + expect(onError).toBeCalled(); + expect(component).toMatchSnapshot(); + }); + + it('should call prop onChange()', async () => { + const component = shallow( + + ); + + (component.instance() as DefaultFormatEditor).onChange(); + expect(onChange).toBeCalled(); + }); + + it('should call prop onError() if converter throws an error', async () => { + const newFormat = { + getConverterFor: jest.fn().mockImplementation(() => () => { + throw new Error('Test error message'); + }), + }; + + shallow( + + ); + + expect(onError).toBeCalled(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.tsx new file mode 100644 index 0000000000000..8a98e57966558 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/default.tsx @@ -0,0 +1,115 @@ +/* + * 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 React, { PureComponent, ReactText } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { FieldFormat, FieldFormatsContentType } from 'src/plugins/data/public'; +import { Sample } from '../../../../types'; +import { FieldFormatEditorProps } from '../../field_format_editor'; + +export type ConverterParams = string | number | Array; + +export const convertSampleInput = ( + converter: (input: ConverterParams) => string, + inputs: ConverterParams[] +) => { + let error; + let samples: Sample[] = []; + + try { + samples = inputs.map(input => { + return { + input, + output: converter(input), + }; + }); + } catch (e) { + error = i18n.translate('common.ui.fieldEditor.defaultErrorMessage', { + defaultMessage: 'An error occurred while trying to use this format configuration: {message}', + values: { message: e.message }, + }); + } + + return { + error, + samples, + }; +}; + +interface SampleInputs { + [key: string]: Array; +} + +export interface FormatEditorProps

{ + fieldType: string; + format: FieldFormat; + formatParams: { type?: string } & P; + onChange: (newParams: Record) => void; + onError: FieldFormatEditorProps['onError']; + basePath: string; +} + +export interface FormatEditorState { + sampleInputs: ReactText[]; + sampleConverterType: FieldFormatsContentType; + error?: string; + samples: Sample[]; + sampleInputsByType: SampleInputs; +} + +export const defaultState = { + sampleInputs: [] as ReactText[], + sampleConverterType: 'text' as FieldFormatsContentType, + error: undefined, + samples: [] as Sample[], + sampleInputsByType: {}, +}; + +export class DefaultFormatEditor

extends PureComponent< + FormatEditorProps

, + FormatEditorState & S +> { + state = defaultState as FormatEditorState & S; + + static getDerivedStateFromProps(nextProps: FormatEditorProps<{}>, state: FormatEditorState) { + const { format, formatParams, onError } = nextProps; + const { sampleInputsByType, sampleInputs, sampleConverterType } = state; + + const converter = format.getConverterFor(sampleConverterType); + const type = typeof sampleInputsByType === 'object' && formatParams.type; + const inputs = type ? sampleInputsByType[formatParams.type as string] || [] : sampleInputs; + const output = convertSampleInput(converter, inputs); + onError(output.error); + return output; + } + + onChange = (newParams = {}) => { + const { onChange, formatParams } = this.props; + + onChange({ + ...formatParams, + ...newParams, + }); + }; + + render() { + return <>; + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/index.js deleted file mode 100644 index 506002df4fd07..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { DefaultFormatEditor } from './default'; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/index.ts b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/index.ts new file mode 100644 index 0000000000000..a6575f296864d --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/default/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { DefaultFormatEditor, defaultState, FormatEditorProps, FormatEditorState } from './default'; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.js.snap deleted file mode 100644 index ef11d70926ad7..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.js.snap +++ /dev/null @@ -1,247 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DurationFormatEditor should render human readable output normally 1`] = ` - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - -`; - -exports[`DurationFormatEditor should render non-human readable output normally 1`] = ` - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap new file mode 100644 index 0000000000000..dbebd324b16b6 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap @@ -0,0 +1,250 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DurationFormatEditor should render human readable output normally 1`] = ` + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + +`; + +exports[`DurationFormatEditor should render non-human readable output normally 1`] = ` + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.js deleted file mode 100644 index 2b2ae7c8fabb8..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { FormatEditorSamples } from '../../samples'; - -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -export class DurationFormatEditor extends DefaultFormatEditor { - static formatId = 'duration'; - - constructor(props) { - super(props); - this.state.sampleInputs = [-123, 1, 12, 123, 658, 1988, 3857, 123292, 923528271]; - this.state.hasDecimalError = false; - } - - static getDerivedStateFromProps(nextProps, state) { - const output = super.getDerivedStateFromProps(nextProps, state); - let error = null; - - if (!nextProps.format.isHuman() && nextProps.formatParams.outputPrecision > 20) { - error = i18n.translate('common.ui.fieldEditor.durationErrorMessage', { - defaultMessage: 'Decimal places must be between 0 and 20', - }); - nextProps.onError(error); - return { - ...output, - error, - hasDecimalError: true, - }; - } - - return { - ...output, - hasDecimalError: false, - }; - } - - render() { - const { format, formatParams } = this.props; - const { error, samples, hasDecimalError } = this.state; - - return ( - - - } - isInvalid={!!error} - error={hasDecimalError ? null : error} - > - { - return { - value: format.kind, - text: format.text, - }; - })} - onChange={e => { - this.onChange({ inputFormat: e.target.value }); - }} - isInvalid={!!error} - /> - - - } - isInvalid={!!error} - > - { - return { - value: format.method, - text: format.text, - }; - })} - onChange={e => { - this.onChange({ outputFormat: e.target.value }); - }} - isInvalid={!!error} - /> - - {!format.isHuman() ? ( - - } - isInvalid={!!error} - error={hasDecimalError ? error : null} - > - { - this.onChange({ outputPrecision: e.target.value ? Number(e.target.value) : null }); - }} - isInvalid={!!error} - /> - - ) : null} - - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.test.js deleted file mode 100644 index 8ee130655766f..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.test.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { DurationFormatEditor } from './duration'; - -const fieldType = 'number'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => `converted duration for ${input}`), - getParamDefaults: jest.fn().mockImplementation(() => { - return { - inputFormat: 'seconds', - outputFormat: 'humanize', - outputPrecision: 10, - }; - }), - isHuman: () => true, - type: { - inputFormats: [ - { - text: 'Seconds', - kind: 'seconds', - }, - ], - outputFormats: [ - { - text: 'Human Readable', - method: 'humanize', - }, - { - text: 'Minutes', - method: 'asMinutes', - }, - ], - }, -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('DurationFormatEditor', () => { - it('should have a formatId', () => { - expect(DurationFormatEditor.formatId).toEqual('duration'); - }); - - it('should render human readable output normally', async () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); - - it('should render non-human readable output normally', async () => { - const newFormat = { - ...format, - getParamDefaults: jest.fn().mockImplementation(() => { - return { - inputFormat: 'seconds', - outputFormat: 'asMinutes', - outputPrecision: 10, - }; - }), - isHuman: () => false, - }; - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.test.tsx new file mode 100644 index 0000000000000..3ab69d12d8c0e --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.test.tsx @@ -0,0 +1,109 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { DurationFormatEditor } from './duration'; +import { FieldFormat } from 'src/plugins/data/public'; + +const fieldType = 'number'; +const format = { + getConverterFor: jest + .fn() + .mockImplementation(() => (input: string) => `converted duration for ${input}`), + getParamDefaults: jest.fn().mockImplementation(() => { + return { + inputFormat: 'seconds', + outputFormat: 'humanize', + outputPrecision: 10, + }; + }), + isHuman: () => true, + type: { + inputFormats: [ + { + text: 'Seconds', + kind: 'seconds', + }, + ], + outputFormats: [ + { + text: 'Human Readable', + method: 'humanize', + }, + { + text: 'Minutes', + method: 'asMinutes', + }, + ], + }, +}; +const formatParams = { + outputPrecision: 2, + inputFormat: '', + outputFormat: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('DurationFormatEditor', () => { + it('should have a formatId', () => { + expect(DurationFormatEditor.formatId).toEqual('duration'); + }); + + it('should render human readable output normally', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('should render non-human readable output normally', async () => { + const newFormat = { + ...format, + getParamDefaults: jest.fn().mockImplementation(() => { + return { + inputFormat: 'seconds', + outputFormat: 'asMinutes', + outputPrecision: 10, + }; + }), + isHuman: () => false, + }; + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.tsx new file mode 100644 index 0000000000000..aed3264bad440 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/duration.tsx @@ -0,0 +1,174 @@ +/* + * 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 React, { Fragment } from 'react'; +import { DurationFormat } from 'src/plugins/data/common'; + +import { EuiFieldNumber, EuiFormRow, EuiSelect } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + DefaultFormatEditor, + defaultState, + FormatEditorProps, + FormatEditorState, +} from '../default'; + +import { FormatEditorSamples } from '../../samples'; + +interface DurationFormatEditorState { + hasDecimalError: boolean; +} + +interface InputFormat { + kind: string; + text: string; +} + +interface OutputFormat { + method: string; + text: string; +} + +interface DurationFormatEditorFormatParams { + outputPrecision: number; + inputFormat: string; + outputFormat: string; +} + +export class DurationFormatEditor extends DefaultFormatEditor< + DurationFormatEditorFormatParams, + DurationFormatEditorState +> { + static formatId = 'duration'; + state = { + ...defaultState, + sampleInputs: [-123, 1, 12, 123, 658, 1988, 3857, 123292, 923528271], + hasDecimalError: false, + }; + + static getDerivedStateFromProps( + nextProps: FormatEditorProps, + state: FormatEditorState & DurationFormatEditorState + ) { + const output = super.getDerivedStateFromProps(nextProps, state); + let error = null; + + if ( + !(nextProps.format as DurationFormat).isHuman() && + nextProps.formatParams.outputPrecision > 20 + ) { + error = i18n.translate('common.ui.fieldEditor.durationErrorMessage', { + defaultMessage: 'Decimal places must be between 0 and 20', + }); + nextProps.onError(error); + return { + ...output, + error, + hasDecimalError: true, + }; + } + + return { + ...output, + hasDecimalError: false, + }; + } + + render() { + const { format, formatParams } = this.props; + const { error, samples, hasDecimalError } = this.state; + + return ( + + + } + isInvalid={!!error} + error={hasDecimalError ? null : error} + > + { + return { + value: fmt.kind, + text: fmt.text, + }; + })} + onChange={e => { + this.onChange({ inputFormat: e.target.value }); + }} + isInvalid={!!error} + /> + + + } + isInvalid={!!error} + > + { + return { + value: fmt.method, + text: fmt.text, + }; + })} + onChange={e => { + this.onChange({ outputFormat: e.target.value }); + }} + isInvalid={!!error} + /> + + {!(format as DurationFormat).isHuman() ? ( + + } + isInvalid={!!error} + error={hasDecimalError ? error : null} + > + { + this.onChange({ outputPrecision: e.target.value ? Number(e.target.value) : null }); + }} + isInvalid={!!error} + /> + + ) : null} + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/index.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/duration/index.tsx diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.js.snap deleted file mode 100644 index f0c331d808a00..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.js.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NumberFormatEditor should render normally 1`] = ` - - - - -   - - - - } - isInvalid={false} - label={ - - 0,0.[000] - , - } - } - /> - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap new file mode 100644 index 0000000000000..cf04dd19428e5 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NumberFormatEditor should render normally 1`] = ` + + + + +   + + + + } + isInvalid={false} + label={ + + 0,0.[000] + , + } + } + /> + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.js deleted file mode 100644 index 509508590780f..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { EuiCode, EuiFieldText, EuiFormRow, EuiIcon, EuiLink } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { FormatEditorSamples } from '../../samples'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export class NumberFormatEditor extends DefaultFormatEditor { - static formatId = 'number'; - - constructor(props) { - super(props); - this.state.sampleInputs = [10000, 12.345678, -1, -999, 0.52]; - } - - render() { - const { format, formatParams } = this.props; - const { error, samples } = this.state; - const defaultPattern = format.getParamDefaults().pattern; - - return ( - - {defaultPattern} }} - /> - } - helpText={ - - - -   - - - - } - isInvalid={!!error} - error={error} - > - { - this.onChange({ pattern: e.target.value }); - }} - isInvalid={!!error} - /> - - - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.test.js deleted file mode 100644 index 9d97feaa75a6a..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { NumberFormatEditor } from './number'; - -const fieldType = 'number'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => input * 2), - getParamDefaults: jest.fn().mockImplementation(() => { - return { pattern: '0,0.[000]' }; - }), -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('NumberFormatEditor', () => { - it('should have a formatId', () => { - expect(NumberFormatEditor.formatId).toEqual('number'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.test.tsx new file mode 100644 index 0000000000000..c07c866359305 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { FieldFormat } from 'src/plugins/data/public'; + +import { NumberFormatEditor } from './number'; + +const fieldType = 'number'; +const format = { + getConverterFor: jest.fn().mockImplementation(() => (input: number) => input * 2), + getParamDefaults: jest.fn().mockImplementation(() => { + return { pattern: '0,0.[000]' }; + }), +}; +const formatParams = { + pattern: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('NumberFormatEditor', () => { + it('should have a formatId', () => { + expect(NumberFormatEditor.formatId).toEqual('number'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.tsx new file mode 100644 index 0000000000000..9279eef7aedeb --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/number/number.tsx @@ -0,0 +1,83 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { EuiCode, EuiFieldText, EuiFormRow, EuiIcon, EuiLink } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor, defaultState } from '../default'; + +import { FormatEditorSamples } from '../../samples'; + +export interface NumberFormatEditorParams { + pattern: string; +} + +export class NumberFormatEditor extends DefaultFormatEditor { + static formatId = 'number'; + state = { + ...defaultState, + sampleInputs: [10000, 12.345678, -1, -999, 0.52], + }; + + render() { + const { format, formatParams } = this.props; + const { error, samples } = this.state; + const defaultPattern = format.getParamDefaults().pattern; + + return ( + + {defaultPattern} }} + /> + } + helpText={ + + + +   + + + + } + isInvalid={!!error} + error={error} + > + { + this.onChange({ pattern: e.target.value }); + }} + isInvalid={!!error} + /> + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.js.snap deleted file mode 100644 index 30d1de270522e..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.js.snap +++ /dev/null @@ -1,80 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PercentFormatEditor should render normally 1`] = ` - - - - -   - - - - } - isInvalid={false} - label={ - - 0,0.[000]% - , - } - } - /> - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap new file mode 100644 index 0000000000000..0784a3f5e407d --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PercentFormatEditor should render normally 1`] = ` + + + + +   + + + + } + isInvalid={false} + label={ + + 0,0.[000]% + , + } + } + /> + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.js deleted file mode 100644 index 26effb8d80095..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { NumberFormatEditor } from '../number'; - -export class PercentFormatEditor extends NumberFormatEditor { - static formatId = 'percent'; - - constructor(props) { - super(props); - - this.state = { - ...this.state, - sampleInputs: [0.1, 0.99999, 1, 100, 1000], - }; - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.test.js deleted file mode 100644 index 853e155814ba6..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.test.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { PercentFormatEditor } from './percent'; - -const fieldType = 'number'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => input * 2), - getParamDefaults: jest.fn().mockImplementation(() => { - return { pattern: '0,0.[000]%' }; - }), -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('PercentFormatEditor', () => { - it('should have a formatId', () => { - expect(PercentFormatEditor.formatId).toEqual('percent'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.test.tsx new file mode 100644 index 0000000000000..ddeb79538cda1 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { FieldFormat } from '../../../../../../../../plugins/data/public'; + +import { PercentFormatEditor } from './percent'; + +const fieldType = 'number'; +const format = { + getConverterFor: jest.fn().mockImplementation(() => (input: number) => input * 2), + getParamDefaults: jest.fn().mockImplementation(() => { + return { pattern: '0,0.[000]%' }; + }), +}; +const formatParams = { + pattern: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('PercentFormatEditor', () => { + it('should have a formatId', () => { + expect(PercentFormatEditor.formatId).toEqual('percent'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.tsx new file mode 100644 index 0000000000000..050c7a8a6cf0a --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/percent/percent.tsx @@ -0,0 +1,29 @@ +/* + * 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 { NumberFormatEditor } from '../number'; +import { defaultState } from '../default'; + +export class PercentFormatEditor extends NumberFormatEditor { + static formatId = 'percent'; + state = { + ...defaultState, + sampleInputs: [0.1, 0.99999, 1, 100, 1000], + }; +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.js.snap deleted file mode 100644 index 2bfb0bbd15013..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.js.snap +++ /dev/null @@ -1,205 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StaticLookupFormatEditor should render multiple lookup entries and unknown key value 1`] = ` - - , - "render": [Function], - }, - Object { - "field": "value", - "name": , - "render": [Function], - }, - Object { - "actions": Array [ - Object { - "available": [Function], - "color": "danger", - "description": "Delete entry", - "icon": "trash", - "name": "Delete", - "onClick": [Function], - "type": "icon", - }, - ], - "width": "30px", - }, - ] - } - items={ - Array [ - Object { - "index": 0, - }, - Object { - "index": 1, - }, - Object { - "index": 2, - }, - ] - } - noItemsMessage="No items found" - responsive={true} - style={ - Object { - "maxWidth": "400px", - } - } - tableLayout="fixed" - /> - - - - - - - } - labelType="label" - > - - - - -`; - -exports[`StaticLookupFormatEditor should render normally 1`] = ` - - , - "render": [Function], - }, - Object { - "field": "value", - "name": , - "render": [Function], - }, - Object { - "actions": Array [ - Object { - "available": [Function], - "color": "danger", - "description": "Delete entry", - "icon": "trash", - "name": "Delete", - "onClick": [Function], - "type": "icon", - }, - ], - "width": "30px", - }, - ] - } - items={ - Array [ - Object { - "index": 0, - }, - ] - } - noItemsMessage="No items found" - responsive={true} - style={ - Object { - "maxWidth": "400px", - } - } - tableLayout="fixed" - /> - - - - - - - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap new file mode 100644 index 0000000000000..2e3c801881f51 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap @@ -0,0 +1,209 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StaticLookupFormatEditor should render multiple lookup entries and unknown key value 1`] = ` + + , + "render": [Function], + }, + Object { + "field": "value", + "name": , + "render": [Function], + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "color": "danger", + "description": "Delete entry", + "icon": "trash", + "name": "Delete", + "onClick": [Function], + "type": "icon", + }, + ], + "field": "actions", + "name": "actions", + "width": "30px", + }, + ] + } + items={ + Array [ + Object { + "index": 0, + }, + Object { + "index": 1, + }, + Object { + "index": 2, + }, + ] + } + noItemsMessage="No items found" + responsive={true} + style={ + Object { + "maxWidth": "400px", + } + } + tableLayout="fixed" + /> + + + + + + + } + labelType="label" + > + + + + +`; + +exports[`StaticLookupFormatEditor should render normally 1`] = ` + + , + "render": [Function], + }, + Object { + "field": "value", + "name": , + "render": [Function], + }, + Object { + "actions": Array [ + Object { + "available": [Function], + "color": "danger", + "description": "Delete entry", + "icon": "trash", + "name": "Delete", + "onClick": [Function], + "type": "icon", + }, + ], + "field": "actions", + "name": "actions", + "width": "30px", + }, + ] + } + items={ + Array [ + Object { + "index": 0, + }, + ] + } + noItemsMessage="No items found" + responsive={true} + style={ + Object { + "maxWidth": "400px", + } + } + tableLayout="fixed" + /> + + + + + + + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.js deleted file mode 100644 index 31ff99696da01..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { EuiBasicTable, EuiButton, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class StaticLookupFormatEditor extends DefaultFormatEditor { - onLookupChange = (newLookupParams, index) => { - const lookupEntries = [...this.props.formatParams.lookupEntries]; - lookupEntries[index] = { - ...lookupEntries[index], - ...newLookupParams, - }; - this.onChange({ - lookupEntries, - }); - }; - - addLookup = () => { - const lookupEntries = [...this.props.formatParams.lookupEntries]; - this.onChange({ - lookupEntries: [...lookupEntries, {}], - }); - }; - - removeLookup = index => { - const lookupEntries = [...this.props.formatParams.lookupEntries]; - lookupEntries.splice(index, 1); - this.onChange({ - lookupEntries, - }); - }; - - render() { - const { formatParams } = this.props; - - const items = - (formatParams.lookupEntries && - formatParams.lookupEntries.length && - formatParams.lookupEntries.map((lookup, index) => { - return { - ...lookup, - index, - }; - })) || - []; - - const columns = [ - { - field: 'key', - name: ( - - ), - render: (value, item) => { - return ( - { - this.onLookupChange( - { - key: e.target.value, - }, - item.index - ); - }} - /> - ); - }, - }, - { - field: 'value', - name: ( - - ), - render: (value, item) => { - return ( - { - this.onLookupChange( - { - value: e.target.value, - }, - item.index - ); - }} - /> - ); - }, - }, - { - actions: [ - { - name: i18n.translate('common.ui.fieldEditor.staticLookup.deleteAria', { - defaultMessage: 'Delete', - }), - description: i18n.translate('common.ui.fieldEditor.staticLookup.deleteTitle', { - defaultMessage: 'Delete entry', - }), - onClick: item => { - this.removeLookup(item.index); - }, - type: 'icon', - icon: 'trash', - color: 'danger', - available: () => items.length > 1, - }, - ], - width: '30px', - }, - ]; - - return ( - - - - - - - - - } - > - { - this.onChange({ unknownKeyValue: e.target.value }); - }} - /> - - - - ); - } -} - -StaticLookupFormatEditor.formatId = 'static_lookup'; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.js deleted file mode 100644 index c9d153e1b32b0..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { StaticLookupFormatEditor } from './static_lookup'; - -const fieldType = 'string'; -const format = { - getConverterFor: jest.fn(), -}; -const formatParams = { - lookupEntries: [{}], - unknownKeyValue: null, -}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('StaticLookupFormatEditor', () => { - it('should have a formatId', () => { - expect(StaticLookupFormatEditor.formatId).toEqual('static_lookup'); - }); - - it('should render normally', async () => { - const component = shallowWithI18nProvider( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render multiple lookup entries and unknown key value', async () => { - const component = shallowWithI18nProvider( - - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx new file mode 100644 index 0000000000000..2e2b1c3ae2357 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 React from 'react'; +import { shallowWithI18nProvider } from '../../../../../../../../test_utils/public/enzyme_helpers'; +import { StaticLookupFormatEditorFormatParams } from './static_lookup'; +import { FieldFormat } from '../../../../../../../../plugins/data/public'; + +import { StaticLookupFormatEditor } from './static_lookup'; + +const fieldType = 'string'; +const format = { + getConverterFor: jest.fn(), +}; +const formatParams = { + lookupEntries: [{}] as StaticLookupFormatEditorFormatParams['lookupEntries'], + unknownKeyValue: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('StaticLookupFormatEditor', () => { + it('should have a formatId', () => { + expect(StaticLookupFormatEditor.formatId).toEqual('static_lookup'); + }); + + it('should render normally', async () => { + const component = shallowWithI18nProvider( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render multiple lookup entries and unknown key value', async () => { + const component = shallowWithI18nProvider( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx new file mode 100644 index 0000000000000..f998e271b6c99 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx @@ -0,0 +1,191 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { EuiBasicTable, EuiButton, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor } from '../default'; + +export interface StaticLookupFormatEditorFormatParams { + lookupEntries: Array<{ key: string; value: string }>; + unknownKeyValue: string; +} + +interface StaticLookupItem { + key: string; + value: string; + index: number; +} + +export class StaticLookupFormatEditor extends DefaultFormatEditor< + StaticLookupFormatEditorFormatParams +> { + static formatId = 'static_lookup'; + onLookupChange = (newLookupParams: { value?: string; key?: string }, index: number) => { + const lookupEntries = [...this.props.formatParams.lookupEntries]; + lookupEntries[index] = { + ...lookupEntries[index], + ...newLookupParams, + }; + this.onChange({ + lookupEntries, + }); + }; + + addLookup = () => { + const lookupEntries = [...this.props.formatParams.lookupEntries]; + this.onChange({ + lookupEntries: [...lookupEntries, {}], + }); + }; + + removeLookup = (index: number) => { + const lookupEntries = [...this.props.formatParams.lookupEntries]; + lookupEntries.splice(index, 1); + this.onChange({ + lookupEntries, + }); + }; + + render() { + const { formatParams } = this.props; + + const items = + (formatParams.lookupEntries && + formatParams.lookupEntries.length && + formatParams.lookupEntries.map((lookup, index) => { + return { + ...lookup, + index, + }; + })) || + []; + + const columns = [ + { + field: 'key', + name: ( + + ), + render: (value: number, item: StaticLookupItem) => { + return ( + { + this.onLookupChange( + { + key: e.target.value, + }, + item.index + ); + }} + /> + ); + }, + }, + { + field: 'value', + name: ( + + ), + render: (value: number, item: StaticLookupItem) => { + return ( + { + this.onLookupChange( + { + value: e.target.value, + }, + item.index + ); + }} + /> + ); + }, + }, + { + field: 'actions', + name: i18n.translate('common.ui.fieldEditor.staticLookup.actions', { + defaultMessage: 'actions', + }), + actions: [ + { + name: i18n.translate('common.ui.fieldEditor.staticLookup.deleteAria', { + defaultMessage: 'Delete', + }), + description: i18n.translate('common.ui.fieldEditor.staticLookup.deleteTitle', { + defaultMessage: 'Delete entry', + }), + onClick: (item: StaticLookupItem) => { + this.removeLookup(item.index); + }, + type: 'icon', + icon: 'trash', + color: 'danger', + available: () => items.length > 1, + }, + ], + width: '30px', + }, + ]; + + return ( + + + + + + + + + } + > + { + this.onChange({ unknownKeyValue: e.target.value }); + }} + /> + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.js.snap deleted file mode 100644 index 270ff844fd086..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.js.snap +++ /dev/null @@ -1,68 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StringFormatEditor should render normally 1`] = ` - - - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.tsx.snap new file mode 100644 index 0000000000000..cde081ff10d14 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/__snapshots__/string.test.tsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StringFormatEditor should render normally 1`] = ` + + + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.js deleted file mode 100644 index d3de10e3e532c..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { EuiFormRow, EuiSelect } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { FormatEditorSamples } from '../../samples'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export class StringFormatEditor extends DefaultFormatEditor { - static formatId = 'string'; - - constructor(props) { - super(props); - this.state.sampleInputs = [ - 'A Quick Brown Fox.', - 'STAY CALM!', - 'com.organizations.project.ClassName', - 'hostname.net', - 'SGVsbG8gd29ybGQ=', - '%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98', - ]; - } - - render() { - const { format, formatParams } = this.props; - const { error, samples } = this.state; - - return ( - - - } - isInvalid={!!error} - error={error} - > - { - return { - value: option.kind, - text: option.text, - }; - })} - onChange={e => { - this.onChange({ transform: e.target.value }); - }} - isInvalid={!!error} - /> - - - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.test.js deleted file mode 100644 index 80334e3801e8b..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.test.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { StringFormatEditor } from './string'; - -const fieldType = 'string'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => input.toUpperCase()), - getParamDefaults: jest.fn().mockImplementation(() => { - return { transform: 'upper' }; - }), - type: { - transformOptions: [ - { - kind: 'upper', - text: 'Upper Case', - }, - ], - }, -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('StringFormatEditor', () => { - it('should have a formatId', () => { - expect(StringFormatEditor.formatId).toEqual('string'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.test.tsx new file mode 100644 index 0000000000000..d0fa0935b2664 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.test.tsx @@ -0,0 +1,65 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { FieldFormat } from 'src/plugins/data/public'; + +import { StringFormatEditor } from './string'; + +const fieldType = 'string'; +const format = { + getConverterFor: jest.fn().mockImplementation(() => (input: string) => input.toUpperCase()), + getParamDefaults: jest.fn().mockImplementation(() => { + return { transform: 'upper' }; + }), + type: { + transformOptions: [ + { + kind: 'upper', + text: 'Upper Case', + }, + ], + }, +}; +const formatParams = { + transform: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('StringFormatEditor', () => { + it('should have a formatId', () => { + expect(StringFormatEditor.formatId).toEqual('string'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.tsx new file mode 100644 index 0000000000000..f13fe44ee4280 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/string/string.tsx @@ -0,0 +1,87 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { EuiFormRow, EuiSelect } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor, defaultState } from '../default'; + +import { FormatEditorSamples } from '../../samples'; + +interface StringFormatEditorFormatParams { + transform: string; +} + +interface TransformOptions { + kind: string; + text: string; +} + +export class StringFormatEditor extends DefaultFormatEditor { + static formatId = 'string'; + state = { + ...defaultState, + sampleInputs: [ + 'A Quick Brown Fox.', + 'STAY CALM!', + 'com.organizations.project.ClassName', + 'hostname.net', + 'SGVsbG8gd29ybGQ=', + '%EC%95%88%EB%85%95%20%ED%82%A4%EB%B0%94%EB%82%98', + ], + }; + + render() { + const { format, formatParams } = this.props; + const { error, samples } = this.state; + + return ( + + + } + isInvalid={!!error} + error={error} + > + { + return { + value: option.kind, + text: option.text, + }; + })} + onChange={e => { + this.onChange({ transform: e.target.value }); + }} + isInvalid={!!error} + /> + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.js.snap deleted file mode 100644 index 729487dfae5d7..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.js.snap +++ /dev/null @@ -1,40 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TruncateFormatEditor should render normally 1`] = ` - - - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap new file mode 100644 index 0000000000000..f646d5b4afca8 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TruncateFormatEditor should render normally 1`] = ` + + + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/sample.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/sample.js deleted file mode 100644 index 5009ba5844eda..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/sample.js +++ /dev/null @@ -1 +0,0 @@ -export const sample = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vitae sem consequat, sollicitudin enim a, feugiat mi. Curabitur congue laoreet elit, eu dictum nisi commodo ut. Nullam congue sem a blandit commodo. Suspendisse eleifend sodales leo ac hendrerit. Nam fringilla tempor fermentum. Ut tristique pharetra sapien sit amet pharetra. Ut turpis massa, viverra id erat quis, fringilla vehicula risus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus tincidunt gravida gravida. Praesent et ligula viverra, semper lacus in, tristique elit. Cras ac eleifend diam. Nulla facilisi. Morbi id sagittis magna. Sed fringilla, magna in suscipit aliquet."; // eslint-disable-line diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/sample.ts b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/sample.ts new file mode 100644 index 0000000000000..3d7bba0fce907 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/sample.ts @@ -0,0 +1,20 @@ +/* + * 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 const sample = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vitae sem consequat, sollicitudin enim a, feugiat mi. Curabitur congue laoreet elit, eu dictum nisi commodo ut. Nullam congue sem a blandit commodo. Suspendisse eleifend sodales leo ac hendrerit. Nam fringilla tempor fermentum. Ut tristique pharetra sapien sit amet pharetra. Ut turpis massa, viverra id erat quis, fringilla vehicula risus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus tincidunt gravida gravida. Praesent et ligula viverra, semper lacus in, tristique elit. Cras ac eleifend diam. Nulla facilisi. Morbi id sagittis magna. Sed fringilla, magna in suscipit aliquet."; // eslint-disable-line diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.js deleted file mode 100644 index 9a9b6c954b78d..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { FormatEditorSamples } from '../../samples'; - -import { sample } from './sample'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export class TruncateFormatEditor extends DefaultFormatEditor { - static formatId = 'truncate'; - - constructor(props) { - super(props); - this.state.sampleInputs = [sample]; - } - - render() { - const { formatParams, onError } = this.props; - const { error, samples } = this.state; - - return ( - - - } - isInvalid={!!error} - error={error} - > - { - if (e.target.checkValidity()) { - this.onChange({ - fieldLength: e.target.value ? Number(e.target.value) : null, - }); - } else { - onError(e.target.validationMessage); - } - }} - isInvalid={!!error} - /> - - - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.test.js deleted file mode 100644 index 7ab6f2a9cbeb0..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.test.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; -import { EuiFieldNumber } from '@elastic/eui'; - -import { TruncateFormatEditor } from './truncate'; - -const fieldType = 'string'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => input.substring(0, 10)), - getParamDefaults: jest.fn().mockImplementation(() => { - return { fieldLength: 10 }; - }), -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -describe('TruncateFormatEditor', () => { - beforeEach(() => { - onChange.mockClear(); - onError.mockClear(); - }); - - it('should have a formatId', () => { - expect(TruncateFormatEditor.formatId).toEqual('truncate'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); - - it('should fire error, when input is invalid', async () => { - const component = shallow( - - ); - const input = component.find(EuiFieldNumber); - - const changeEvent = { - target: { - value: '123.3', - checkValidity: () => false, - validationMessage: 'Error!', - }, - }; - await input.invoke('onChange')(changeEvent); - - expect(onError).toBeCalledWith(changeEvent.target.validationMessage); - expect(onChange).not.toBeCalled(); - }); - - it('should fire change, when input changed and is valid', async () => { - const component = shallow( - - ); - const input = component.find(EuiFieldNumber); - - const changeEvent = { - target: { - value: '123', - checkValidity: () => true, - validationMessage: null, - }, - }; - onError.mockClear(); - await input.invoke('onChange')(changeEvent); - expect(onError).not.toBeCalled(); - expect(onChange).toBeCalledWith({ fieldLength: 123 }); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx new file mode 100644 index 0000000000000..bb723386ff777 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx @@ -0,0 +1,116 @@ +/* + * 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 React, { ChangeEvent } from 'react'; +import { shallow } from 'enzyme'; +import { EuiFieldNumber } from '@elastic/eui'; +import { FieldFormat } from 'src/plugins/data/public'; + +import { TruncateFormatEditor } from './truncate'; + +const fieldType = 'string'; +const format = { + getConverterFor: jest.fn().mockImplementation(() => (input: string) => input.substring(0, 10)), + getParamDefaults: jest.fn().mockImplementation(() => { + return { fieldLength: 10 }; + }), +}; +const formatParams = { + fieldLength: 5, +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +describe('TruncateFormatEditor', () => { + beforeEach(() => { + onChange.mockClear(); + onError.mockClear(); + }); + + it('should have a formatId', () => { + expect(TruncateFormatEditor.formatId).toEqual('truncate'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + + it('should fire error, when input is invalid', async () => { + const component = shallow( + + ); + const input = component.find(EuiFieldNumber); + + const changeEvent = { + target: { + value: '123.3', + checkValidity: () => false, + validationMessage: 'Error!', + }, + }; + + await input!.invoke('onChange')!((changeEvent as unknown) as ChangeEvent); + + expect(onError).toBeCalledWith(changeEvent.target.validationMessage); + expect(onChange).not.toBeCalled(); + }); + + it('should fire change, when input changed and is valid', async () => { + const component = shallow( + + ); + const input = component.find(EuiFieldNumber); + + const changeEvent = { + target: { + value: '123', + checkValidity: () => true, + validationMessage: null, + }, + }; + onError.mockClear(); + await input!.invoke('onChange')!((changeEvent as unknown) as ChangeEvent); + expect(onError).not.toBeCalled(); + expect(onChange).toBeCalledWith({ fieldLength: 123 }); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.tsx new file mode 100644 index 0000000000000..9fd44c5f9655d --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/truncate/truncate.tsx @@ -0,0 +1,77 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor, defaultState } from '../default'; + +import { FormatEditorSamples } from '../../samples'; + +import { sample } from './sample'; + +interface TruncateFormatEditorFormatParams { + fieldLength: number; +} + +export class TruncateFormatEditor extends DefaultFormatEditor { + static formatId = 'truncate'; + state = { + ...defaultState, + sampleInputs: [sample], + }; + + render() { + const { formatParams, onError } = this.props; + const { error, samples } = this.state; + + return ( + + + } + isInvalid={!!error} + error={error} + > + { + if (e.target.checkValidity()) { + this.onChange({ + fieldLength: e.target.value ? Number(e.target.value) : null, + }); + } else { + onError(e.target.validationMessage); + } + }} + isInvalid={!!error} + /> + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.js.snap rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.js.snap deleted file mode 100644 index c727f54874db4..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.js.snap +++ /dev/null @@ -1,552 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UrlFormatEditor should render label template help 1`] = ` - - - - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - -`; - -exports[`UrlFormatEditor should render normally 1`] = ` - - - - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - -`; - -exports[`UrlFormatEditor should render url template help 1`] = ` - - - - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - -`; - -exports[`UrlFormatEditor should render width and height fields if image 1`] = ` - - - - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - - - } - isInvalid={false} - label={ - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - } - labelType="label" - > - - - - -`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap new file mode 100644 index 0000000000000..a3418077ad258 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -0,0 +1,544 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UrlFormatEditor should render label template help 1`] = ` + + + + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + +`; + +exports[`UrlFormatEditor should render normally 1`] = ` + + + + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + +`; + +exports[`UrlFormatEditor should render url template help 1`] = ` + + + + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + +`; + +exports[`UrlFormatEditor should render width and height fields if image 1`] = ` + + + + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + + + } + isInvalid={false} + label={ + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + +`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.js.snap rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.js deleted file mode 100644 index 3f91f6f4253ad..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.js +++ /dev/null @@ -1,148 +0,0 @@ -/* - * 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 React from 'react'; - -import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { - return isVisible ? ( - - - -

- -

-

- {'{{ }}'} }} - /> -

-
    -
  • - value —  - -
  • -
  • - url —  - -
  • -
-

- -

- ' + - i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.idLabel', { - defaultMessage: 'User', - }) + - ' #1234', - }, - { - input: '/assets/main.css', - urlTemplate: 'http://site.com{{rawValue}}', - labelTemplate: i18n.translate( - 'common.ui.fieldEditor.labelTemplate.example.pathLabel', - { defaultMessage: 'View Asset' } - ), - output: - '' + - i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.pathLabel', { - defaultMessage: 'View Asset', - }) + - '', - }, - ]} - columns={[ - { - field: 'input', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.inputHeader', { - defaultMessage: 'Input', - }), - width: '160px', - }, - { - field: 'urlTemplate', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.urlHeader', { - defaultMessage: 'URL Template', - }), - }, - { - field: 'labelTemplate', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.labelHeader', { - defaultMessage: 'Label Template', - }), - }, - { - field: 'output', - name: i18n.translate('common.ui.fieldEditor.labelTemplate.outputHeader', { - defaultMessage: 'Output', - }), - render: value => { - return ( - - ); - }, - }, - ]} - /> - - - - ) : null; -}; - -LabelTemplateFlyout.displayName = 'LabelTemplateFlyout'; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx new file mode 100644 index 0000000000000..1ce7bec579e16 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx @@ -0,0 +1,153 @@ +/* + * 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 React from 'react'; + +import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface LabelTemplateExampleItem { + input: string | number; + urlTemplate: string; + labelTemplate: string; + output: string; +} + +const items: LabelTemplateExampleItem[] = [ + { + input: 1234, + urlTemplate: 'http://company.net/profiles?user_id={{value}}', + labelTemplate: i18n.translate('common.ui.fieldEditor.labelTemplate.example.idLabel', { + defaultMessage: 'User #{value}', + values: { value: '{{value}}' }, + }), + output: + '' + + i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.idLabel', { + defaultMessage: 'User', + }) + + ' #1234', + }, + { + input: '/assets/main.css', + urlTemplate: 'http://site.com{{rawValue}}', + labelTemplate: i18n.translate('common.ui.fieldEditor.labelTemplate.example.pathLabel', { + defaultMessage: 'View Asset', + }), + output: + '' + + i18n.translate('common.ui.fieldEditor.labelTemplate.example.output.pathLabel', { + defaultMessage: 'View Asset', + }) + + '', + }, +]; + +export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { + return isVisible ? ( + + + +

+ +

+

+ {'{{ }}'} }} + /> +

+
    +
  • + value —  + +
  • +
  • + url —  + +
  • +
+

+ +

+ + items={items} + columns={[ + { + field: 'input', + name: i18n.translate('common.ui.fieldEditor.labelTemplate.inputHeader', { + defaultMessage: 'Input', + }), + width: '160px', + }, + { + field: 'urlTemplate', + name: i18n.translate('common.ui.fieldEditor.labelTemplate.urlHeader', { + defaultMessage: 'URL Template', + }), + }, + { + field: 'labelTemplate', + name: i18n.translate('common.ui.fieldEditor.labelTemplate.labelHeader', { + defaultMessage: 'Label Template', + }), + }, + { + field: 'output', + name: i18n.translate('common.ui.fieldEditor.labelTemplate.outputHeader', { + defaultMessage: 'Output', + }), + render: (value: LabelTemplateExampleItem['output']) => { + return ( + + ); + }, + }, + ]} + /> +
+
+
+ ) : null; +}; + +LabelTemplateFlyout.displayName = 'LabelTemplateFlyout'; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.js deleted file mode 100644 index d4a4293b45669..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.js +++ /dev/null @@ -1,269 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; - -import { - EuiFieldText, - EuiFormRow, - EuiLink, - EuiSelect, - EuiSwitch, - EuiFieldNumber, -} from '@elastic/eui'; - -import { DefaultFormatEditor } from '../default'; - -import { FormatEditorSamples } from '../../samples'; - -import { LabelTemplateFlyout } from './label_template_flyout'; - -import { UrlTemplateFlyout } from './url_template_flyout'; - -import chrome from 'ui/chrome'; -import './icons'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export class UrlFormatEditor extends DefaultFormatEditor { - static formatId = 'url'; - - constructor(props) { - super(props); - const bp = chrome.getBasePath(); - this.iconPattern = `${bp}/bundles/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/icons/{{value}}.png`; - this.state = { - ...this.state, - sampleInputsByType: { - a: ['john', '/some/pathname/asset.png', 1234], - img: ['go', 'stop', ['de', 'ne', 'us', 'ni'], 'cv'], - audio: ['hello.mp3'], - }, - sampleConverterType: 'html', - showUrlTemplateHelp: false, - showLabelTemplateHelp: false, - }; - } - - sanitizeNumericValue = val => { - const sanitizedValue = parseInt(val); - if (isNaN(sanitizedValue)) { - return ''; - } - return sanitizedValue; - }; - - onTypeChange = newType => { - const { urlTemplate, width, height } = this.props.formatParams; - const params = { - type: newType, - }; - if (newType === 'img') { - params.width = width; - params.height = height; - if (!urlTemplate) { - params.urlTemplate = this.iconPattern; - } - } else if (newType !== 'img' && urlTemplate === this.iconPattern) { - params.urlTemplate = null; - } - this.onChange(params); - }; - - showUrlTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - showUrlTemplateHelp: true, - }); - }; - - hideUrlTemplateHelp = () => { - this.setState({ - showUrlTemplateHelp: false, - }); - }; - - showLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: true, - showUrlTemplateHelp: false, - }); - }; - - hideLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - }); - }; - - renderWidthHeightParameters = () => { - const width = this.sanitizeNumericValue(this.props.formatParams.width); - const height = this.sanitizeNumericValue(this.props.formatParams.height); - return ( - - - } - > - { - this.onChange({ width: e.target.value }); - }} - /> - - - } - > - { - this.onChange({ height: e.target.value }); - }} - /> - - - ); - }; - - render() { - const { format, formatParams } = this.props; - const { error, samples, sampleConverterType } = this.state; - - return ( - - - - - } - > - { - return { - value: type.kind, - text: type.text, - }; - })} - onChange={e => { - this.onTypeChange(e.target.value); - }} - /> - - - {formatParams.type === 'a' ? ( - - } - > - - ) : ( - - ) - } - checked={!formatParams.openLinkInCurrentTab} - onChange={e => { - this.onChange({ openLinkInCurrentTab: !e.target.checked }); - }} - /> - - ) : null} - - - } - helpText={ - - - - } - isInvalid={!!error} - error={error} - > - { - this.onChange({ urlTemplate: e.target.value }); - }} - /> - - - - } - helpText={ - - - - } - isInvalid={!!error} - error={error} - > - { - this.onChange({ labelTemplate: e.target.value }); - }} - /> - - - {formatParams.type === 'img' && this.renderWidthHeightParameters()} - - - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.test.js deleted file mode 100644 index 1d732b50db3d0..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.test.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { UrlFormatEditor } from './url'; - -const fieldType = 'string'; -const format = { - getConverterFor: jest.fn().mockImplementation(() => input => `converted url for ${input}`), - type: { - urlTypes: [ - { kind: 'a', text: 'Link' }, - { kind: 'img', text: 'Image' }, - { kind: 'audio', text: 'Audio' }, - ], - }, -}; -const formatParams = {}; -const onChange = jest.fn(); -const onError = jest.fn(); - -jest.mock('ui/chrome', () => ({ - getBasePath: () => 'http://localhost/', -})); - -describe('UrlFormatEditor', () => { - it('should have a formatId', () => { - expect(UrlFormatEditor.formatId).toEqual('url'); - }); - - it('should render normally', async () => { - const component = shallow( - - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render url template help', async () => { - const component = shallow( - - ); - - component.instance().showUrlTemplateHelp(); - component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should render label template help', async () => { - const component = shallow( - - ); - - component.instance().showLabelTemplateHelp(); - component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should render width and height fields if image', async () => { - const component = shallow( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.test.tsx new file mode 100644 index 0000000000000..4d09da84edfb6 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.test.tsx @@ -0,0 +1,120 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { FieldFormat } from 'src/plugins/data/public'; + +import { UrlFormatEditor } from './url'; + +const fieldType = 'string'; +const format = { + getConverterFor: jest + .fn() + .mockImplementation(() => (input: string) => `converted url for ${input}`), + type: { + urlTypes: [ + { kind: 'a', text: 'Link' }, + { kind: 'img', text: 'Image' }, + { kind: 'audio', text: 'Audio' }, + ], + }, +}; +const formatParams = { + openLinkInCurrentTab: true, + urlTemplate: '', + labelTemplate: '', + width: '', + height: '', +}; +const onChange = jest.fn(); +const onError = jest.fn(); + +jest.mock('ui/chrome', () => ({ + getBasePath: () => 'http://localhost/', +})); + +describe('UrlFormatEditor', () => { + it('should have a formatId', () => { + expect(UrlFormatEditor.formatId).toEqual('url'); + }); + + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render url template help', async () => { + const component = shallow( + + ); + + (component.instance() as UrlFormatEditor).showUrlTemplateHelp(); + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should render label template help', async () => { + const component = shallow( + + ); + + (component.instance() as UrlFormatEditor).showLabelTemplateHelp(); + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should render width and height fields if image', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.tsx new file mode 100644 index 0000000000000..73a130d442eb0 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url.tsx @@ -0,0 +1,296 @@ +/* + * 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 React, { Fragment } from 'react'; + +import { + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSelect, + EuiSwitch, + EuiFieldNumber, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { DefaultFormatEditor, FormatEditorProps } from '../default'; + +import { FormatEditorSamples } from '../../samples'; + +import { LabelTemplateFlyout } from './label_template_flyout'; + +import { UrlTemplateFlyout } from './url_template_flyout'; + +import './icons'; + +interface OnChangeParam { + type: string; + width?: string; + height?: string; + urlTemplate?: string; +} + +interface UrlFormatEditorFormatParams { + openLinkInCurrentTab: boolean; + urlTemplate: string; + labelTemplate: string; + width: string; + height: string; +} + +interface UrlFormatEditorFormatState { + showLabelTemplateHelp: boolean; + showUrlTemplateHelp: boolean; +} + +interface UrlType { + kind: string; + text: string; +} + +export class UrlFormatEditor extends DefaultFormatEditor< + UrlFormatEditorFormatParams, + UrlFormatEditorFormatState +> { + static formatId = 'url'; + iconPattern: string; + + constructor(props: FormatEditorProps) { + super(props); + + this.iconPattern = `${props.basePath}/bundles/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/icons/{{value}}.png`; + this.state = { + ...this.state, + sampleInputsByType: { + a: ['john', '/some/pathname/asset.png', 1234], + img: ['go', 'stop', ['de', 'ne', 'us', 'ni'], 'cv'], + audio: ['hello.mp3'], + }, + sampleConverterType: 'html', + showUrlTemplateHelp: false, + showLabelTemplateHelp: false, + }; + } + + sanitizeNumericValue = (val: string) => { + const sanitizedValue = parseInt(val, 10); + if (isNaN(sanitizedValue)) { + return ''; + } + return sanitizedValue; + }; + + onTypeChange = (newType: string) => { + const { urlTemplate, width, height } = this.props.formatParams; + const params: OnChangeParam = { + type: newType, + }; + if (newType === 'img') { + params.width = width; + params.height = height; + if (!urlTemplate) { + params.urlTemplate = this.iconPattern; + } + } else if (newType !== 'img' && urlTemplate === this.iconPattern) { + params.urlTemplate = undefined; + } + this.onChange(params); + }; + + showUrlTemplateHelp = () => { + this.setState({ + showLabelTemplateHelp: false, + showUrlTemplateHelp: true, + }); + }; + + hideUrlTemplateHelp = () => { + this.setState({ + showUrlTemplateHelp: false, + }); + }; + + showLabelTemplateHelp = () => { + this.setState({ + showLabelTemplateHelp: true, + showUrlTemplateHelp: false, + }); + }; + + hideLabelTemplateHelp = () => { + this.setState({ + showLabelTemplateHelp: false, + }); + }; + + renderWidthHeightParameters = () => { + const width = this.sanitizeNumericValue(this.props.formatParams.width); + const height = this.sanitizeNumericValue(this.props.formatParams.height); + return ( + + + } + > + { + this.onChange({ width: e.target.value }); + }} + /> + + + } + > + { + this.onChange({ height: e.target.value }); + }} + /> + + + ); + }; + + render() { + const { format, formatParams } = this.props; + const { error, samples, sampleConverterType } = this.state; + + return ( + + + + + } + > + { + return { + value: type.kind, + text: type.text, + }; + })} + onChange={e => { + this.onTypeChange(e.target.value); + }} + /> + + + {formatParams.type === 'a' ? ( + + } + > + + ) : ( + + ) + } + checked={!formatParams.openLinkInCurrentTab} + onChange={e => { + this.onChange({ openLinkInCurrentTab: !e.target.checked }); + }} + /> + + ) : null} + + + } + helpText={ + + + + } + isInvalid={!!error} + error={error} + > + { + this.onChange({ urlTemplate: e.target.value }); + }} + /> + + + + } + helpText={ + + + + } + isInvalid={!!error} + error={error} + > + { + this.onChange({ labelTemplate: e.target.value }); + }} + /> + + + {formatParams.type === 'img' && this.renderWidthHeightParameters()} + + + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.js b/src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.js b/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.js deleted file mode 100644 index 204554ad94644..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; - -export class FieldFormatEditor extends PureComponent { - static propTypes = { - fieldType: PropTypes.string.isRequired, - fieldFormat: PropTypes.object.isRequired, - fieldFormatId: PropTypes.string.isRequired, - fieldFormatParams: PropTypes.object.isRequired, - fieldFormatEditors: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, - onError: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - this.state = { - EditorComponent: null, - }; - } - - static getDerivedStateFromProps(nextProps) { - return { - EditorComponent: nextProps.fieldFormatEditors.getEditor(nextProps.fieldFormatId) || null, - }; - } - - render() { - const { EditorComponent } = this.state; - const { fieldType, fieldFormat, fieldFormatParams, onChange, onError } = this.props; - - return ( - - {EditorComponent ? ( - - ) : null} - - ); - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.test.js deleted file mode 100644 index 9e03841397872..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.test.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 React, { PureComponent } from 'react'; -import { shallow } from 'enzyme'; - -import { FieldFormatEditor } from './field_format_editor'; - -class TestEditor extends PureComponent { - render() { - if (this.props) { - return null; - } - return
Test editor
; - } -} - -describe('FieldFormatEditor', () => { - it('should render normally', async () => { - const component = shallow( - { - return TestEditor; - }, - }} - onChange={() => {}} - onError={() => {}} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render nothing if there is no editor for the format', async () => { - const component = shallow( - { - return null; - }, - }} - onChange={() => {}} - onError={() => {}} - /> - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.test.tsx new file mode 100644 index 0000000000000..f6e631c8b7ac0 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 React, { PureComponent } from 'react'; +import { shallow } from 'enzyme'; + +import { FieldFormatEditor } from './field_format_editor'; +import { DefaultFormatEditor } from './editors/default'; + +class TestEditor extends PureComponent { + render() { + if (this.props) { + return null; + } + return
Test editor
; + } +} + +const formatEditors = { + byFormatId: { + ip: TestEditor, + number: TestEditor, + }, +}; + +describe('FieldFormatEditor', () => { + it('should render normally', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if there is no editor for the format', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.tsx new file mode 100644 index 0000000000000..2de6dff5d251a --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/field_format_editor.tsx @@ -0,0 +1,80 @@ +/* + * 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 React, { PureComponent, Fragment } from 'react'; +import { DefaultFormatEditor } from '../../components/field_format_editor/editors/default'; + +export interface FieldFormatEditorProps { + fieldType: string; + fieldFormat: DefaultFormatEditor; + fieldFormatId: string; + fieldFormatParams: { [key: string]: unknown }; + fieldFormatEditors: any; + onChange: (change: { fieldType: string; [key: string]: any }) => void; + onError: (error?: string) => void; +} + +interface EditorComponentProps { + fieldType: FieldFormatEditorProps['fieldType']; + format: FieldFormatEditorProps['fieldFormat']; + formatParams: FieldFormatEditorProps['fieldFormatParams']; + onChange: FieldFormatEditorProps['onChange']; + onError: FieldFormatEditorProps['onError']; +} + +interface FieldFormatEditorState { + EditorComponent: React.FC; +} + +export class FieldFormatEditor extends PureComponent< + FieldFormatEditorProps, + FieldFormatEditorState +> { + constructor(props: FieldFormatEditorProps) { + super(props); + this.state = { + EditorComponent: props.fieldFormatEditors.byFormatId[props.fieldFormatId], + }; + } + + static getDerivedStateFromProps(nextProps: FieldFormatEditorProps) { + return { + EditorComponent: nextProps.fieldFormatEditors.byFormatId[nextProps.fieldFormatId] || null, + }; + } + + render() { + const { EditorComponent } = this.state; + const { fieldType, fieldFormat, fieldFormatParams, onChange, onError } = this.props; + + return ( + + {EditorComponent ? ( + + ) : null} + + ); + } +} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/register.js b/src/legacy/ui/public/field_editor/components/field_format_editor/register.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/register.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/register.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.js.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.js.snap deleted file mode 100644 index 73a7c1141c601..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.js.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FormatEditorSamples should render normally 1`] = ` - - } - labelType="label" -> - foo
, bar", - }, - ] - } - noItemsMessage="No items found" - responsive={true} - tableLayout="fixed" - /> - -`; - -exports[`FormatEditorSamples should render nothing if there are no samples 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap new file mode 100644 index 0000000000000..2883ffb6bc8a1 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormatEditorSamples should render normally 1`] = ` + + } + labelType="label" +> + foo, bar", + }, + ] + } + noItemsMessage="No items found" + responsive={true} + tableLayout="fixed" + /> + +`; + +exports[`FormatEditorSamples should render nothing if there are no samples 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/index.js b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/field_format_editor/samples/index.js rename to src/legacy/ui/public/field_editor/components/field_format_editor/samples/index.ts diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.js b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.js deleted file mode 100644 index b3345f085882c..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import { EuiBasicTable, EuiFormRow } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class FormatEditorSamples extends PureComponent { - static defaultProps = { - sampleType: 'text', - }; - static propTypes = { - samples: PropTypes.arrayOf( - PropTypes.shape({ - input: PropTypes.any.isRequired, - output: PropTypes.any.isRequired, - }) - ).isRequired, - sampleType: PropTypes.oneOf(['html', 'text']), - }; - - render() { - const { samples, sampleType } = this.props; - - const columns = [ - { - field: 'input', - name: i18n.translate('common.ui.fieldEditor.samples.inputHeader', { - defaultMessage: 'Input', - }), - render: input => { - return typeof input === 'object' ? JSON.stringify(input) : input; - }, - }, - { - field: 'output', - name: i18n.translate('common.ui.fieldEditor.samples.outputHeader', { - defaultMessage: 'Output', - }), - render: output => { - return sampleType === 'html' ? ( -
- ) : ( -
{output}
- ); - }, - }, - ]; - - return samples.length ? ( - - } - > - - - ) : null; - } -} diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.test.js b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.test.js deleted file mode 100644 index 8e18f1f5f4de8..0000000000000 --- a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.test.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 React from 'react'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -import { FormatEditorSamples } from './samples'; - -describe('FormatEditorSamples', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider( - foo, bar' }, - ]} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render nothing if there are no samples', async () => { - const component = shallowWithI18nProvider(); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.test.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.test.tsx new file mode 100644 index 0000000000000..01f405e9aff1f --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.test.tsx @@ -0,0 +1,45 @@ +/* + * 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 React from 'react'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; + +import { FormatEditorSamples } from './samples'; + +describe('FormatEditorSamples', () => { + it('should render normally', async () => { + const component = shallowWithI18nProvider( + foo, bar' }, + ]} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if there are no samples', async () => { + const component = shallowWithI18nProvider(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.tsx b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.tsx new file mode 100644 index 0000000000000..d63674bf4d205 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/field_format_editor/samples/samples.tsx @@ -0,0 +1,87 @@ +/* + * 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 React, { PureComponent } from 'react'; + +import { EuiBasicTable, EuiFormRow } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Sample } from '../../../types'; + +interface FormatEditorSamplesProps { + samples: Sample[]; + sampleType: string; +} + +export class FormatEditorSamples extends PureComponent { + static defaultProps = { + sampleType: 'text', + }; + + render() { + const { samples, sampleType } = this.props; + + const columns = [ + { + field: 'input', + name: i18n.translate('common.ui.fieldEditor.samples.inputHeader', { + defaultMessage: 'Input', + }), + render: (input: {} | string) => { + return typeof input === 'object' ? JSON.stringify(input) : input; + }, + }, + { + field: 'output', + name: i18n.translate('common.ui.fieldEditor.samples.outputHeader', { + defaultMessage: 'Output', + }), + render: (output: string) => { + return sampleType === 'html' ? ( +
+ ) : ( +
{output}
+ ); + }, + }, + ]; + + return samples.length ? ( + + } + > + + className="kbnFieldFormatEditor__samples" + compressed={true} + items={samples} + columns={columns} + /> + + ) : null; + } +} diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/disabled_call_out.test.js.snap b/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/disabled_call_out.test.tsx.snap similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/disabled_call_out.test.js.snap rename to src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/disabled_call_out.test.tsx.snap diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.js.snap b/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.js.snap deleted file mode 100644 index f09ea05f6711a..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.js.snap +++ /dev/null @@ -1,68 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptingWarningCallOut should render normally 1`] = ` - - - } - > -

- - -   - - , - "scriptsInAggregation": - -   - - , - } - } - /> -

-

- -

-
- -
-`; - -exports[`ScriptingWarningCallOut should render nothing if not visible 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap b/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap new file mode 100644 index 0000000000000..b331c1e38eb34 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_call_outs/__snapshots__/warning_call_out.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScriptingWarningCallOut should render normally 1`] = ` + + + } + > +

+ + +   + + , + "scriptsInAggregation": + +   + + , + } + } + /> +

+

+ +

+
+ +
+`; + +exports[`ScriptingWarningCallOut should render nothing if not visible 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.js rename to src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.test.tsx diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.tsx similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.js rename to src/legacy/ui/public/field_editor/components/scripting_call_outs/disabled_call_out.tsx diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/index.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_call_outs/index.js rename to src/legacy/ui/public/field_editor/components/scripting_call_outs/index.ts diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.js deleted file mode 100644 index b810541d72697..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; -import { getDocLink } from 'ui/documentation_links'; - -import { EuiCallOut, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ScriptingWarningCallOut = ({ isVisible = false }) => { - return isVisible ? ( - - - } - color="warning" - iconType="alert" - > -

- - -   - - - ), - scriptsInAggregation: ( - - -   - - - ), - }} - /> -

-

- -

-
- -
- ) : null; -}; - -ScriptingWarningCallOut.displayName = 'ScriptingWarningCallOut'; diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.js b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.js deleted file mode 100644 index 48094f63f8fe8..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { ScriptingWarningCallOut } from './warning_call_out'; - -jest.mock('ui/documentation_links', () => ({ - getDocLink: doc => `(docLink for ${doc})`, -})); - -describe('ScriptingWarningCallOut', () => { - it('should render normally', async () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); - - it('should render nothing if not visible', async () => { - const component = shallow(); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.tsx b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.tsx new file mode 100644 index 0000000000000..8568c2c79816b --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { ScriptingWarningCallOut } from './warning_call_out'; +// eslint-disable-next-line +import { docLinksServiceMock } from '../../../../../../core/public/doc_links/doc_links_service.mock'; + +describe('ScriptingWarningCallOut', () => { + const docLinksScriptedFields = docLinksServiceMock.createStartContract().links.scriptedFields; + it('should render normally', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if not visible', async () => { + const component = shallow( + + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.tsx b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.tsx new file mode 100644 index 0000000000000..7dac6681fa1ea --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_call_outs/warning_call_out.tsx @@ -0,0 +1,90 @@ +/* + * 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 React, { Fragment } from 'react'; +import { DocLinksStart } from 'src/core/public'; + +import { EuiCallOut, EuiIcon, EuiLink, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ScriptingWarningCallOutProps { + isVisible: boolean; + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; +} + +export const ScriptingWarningCallOut = ({ + isVisible = false, + docLinksScriptedFields, +}: ScriptingWarningCallOutProps) => { + return isVisible ? ( + + + } + color="warning" + iconType="alert" + > +

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

+

+ +

+
+ +
+ ) : null; +}; + +ScriptingWarningCallOut.displayName = 'ScriptingWarningCallOut'; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap deleted file mode 100644 index ca252b6d0147b..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.js.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ScriptingHelpFlyout should render normally 1`] = ` - - - , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - } - } - tabs={ - Array [ - Object { - "content": , - "data-test-subj": "syntaxTab", - "id": "syntax", - "name": "Syntax", - }, - Object { - "content": , - "data-test-subj": "testTab", - "id": "test", - "name": "Preview results", - }, - ] - } - /> - - -`; - -exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = `""`; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap new file mode 100644 index 0000000000000..282e8e311d984 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/__snapshots__/help_flyout.test.tsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ScriptingHelpFlyout should render normally 1`] = ` + + + , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + } + } + tabs={ + Array [ + Object { + "content": , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + }, + Object { + "content": , + "data-test-subj": "testTab", + "id": "test", + "name": "Preview results", + }, + ] + } + /> + + +`; + +exports[`ScriptingHelpFlyout should render nothing if not visible 1`] = ` + + + , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + } + } + tabs={ + Array [ + Object { + "content": , + "data-test-subj": "syntaxTab", + "id": "syntax", + "name": "Syntax", + }, + Object { + "content": , + "data-test-subj": "testTab", + "id": "test", + "name": "Preview results", + }, + ] + } + /> + + +`; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.js b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.js deleted file mode 100644 index c512b5f5f2019..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 React from 'react'; -import PropTypes from 'prop-types'; - -import { EuiFlyout, EuiFlyoutBody, EuiTabbedContent } from '@elastic/eui'; - -import { ScriptingSyntax } from './scripting_syntax'; -import { TestScript } from './test_script'; - -export const ScriptingHelpFlyout = ({ - isVisible = false, - onClose = () => {}, - indexPattern, - lang, - name, - script, - executeScript, -}) => { - const tabs = [ - { - id: 'syntax', - name: 'Syntax', - ['data-test-subj']: 'syntaxTab', - content: , - }, - { - id: 'test', - name: 'Preview results', - ['data-test-subj']: 'testTab', - content: ( - - ), - }, - ]; - - return isVisible ? ( - - - - - - ) : null; -}; - -ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout'; - -ScriptingHelpFlyout.propTypes = { - indexPattern: PropTypes.object.isRequired, - lang: PropTypes.string.isRequired, - name: PropTypes.string, - script: PropTypes.string, - executeScript: PropTypes.func.isRequired, -}; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js deleted file mode 100644 index 2fac8c7641ddb..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; - -import { ScriptingHelpFlyout } from './help_flyout'; - -jest.mock('ui/documentation_links', () => ({ - getDocLink: doc => `(docLink for ${doc})`, -})); - -jest.mock('./test_script', () => ({ - TestScript: () => { - return `
mockTestScript
`; - }, -})); - -const indexPatternMock = {}; - -describe('ScriptingHelpFlyout', () => { - it('should render normally', async () => { - const component = shallow( - {}} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - it('should render nothing if not visible', async () => { - const component = shallow( - {}} - /> - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.tsx new file mode 100644 index 0000000000000..4106eb7b283ee --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { HttpStart } from 'src/core/public'; +// eslint-disable-next-line +import { docLinksServiceMock } from '../../../../../../core/public/doc_links/doc_links_service.mock'; + +import { ScriptingHelpFlyout } from './help_flyout'; + +import { IndexPattern } from '../../../../../../plugins/data/public'; + +import { ExecuteScript } from '../../types'; + +jest.mock('./test_script', () => ({ + TestScript: () => { + return `
mockTestScript
`; + }, +})); + +const indexPatternMock = {} as IndexPattern; + +describe('ScriptingHelpFlyout', () => { + const docLinksScriptedFields = docLinksServiceMock.createStartContract().links.scriptedFields; + it('should render normally', async () => { + const component = shallow( + {}) as unknown) as ExecuteScript} + onClose={() => {}} + getHttpStart={() => (({} as unknown) as HttpStart)} + // docLinksScriptedFields={docLinksScriptedFields} + docLinksScriptedFields={{} as typeof docLinksScriptedFields} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if not visible', async () => { + const component = shallow( + {}) as unknown) as ExecuteScript} + onClose={() => {}} + getHttpStart={() => (({} as unknown) as HttpStart)} + docLinksScriptedFields={{} as typeof docLinksScriptedFields} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.tsx new file mode 100644 index 0000000000000..6f51379c796d4 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/help_flyout.tsx @@ -0,0 +1,87 @@ +/* + * 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 React from 'react'; +import { HttpStart, DocLinksStart } from 'src/core/public'; + +import { EuiFlyout, EuiFlyoutBody, EuiTabbedContent } from '@elastic/eui'; + +import { ScriptingSyntax } from './scripting_syntax'; +import { TestScript } from './test_script'; + +import { IndexPattern } from '../../../../../../plugins/data/public'; +import { ExecuteScript } from '../../types'; + +interface ScriptingHelpFlyoutProps { + indexPattern: IndexPattern; + lang: string; + name?: string; + script?: string; + executeScript: ExecuteScript; + isVisible: boolean; + onClose: () => void; + getHttpStart: () => HttpStart; + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; +} + +export const ScriptingHelpFlyout: React.FC = ({ + isVisible = false, + onClose = () => {}, + indexPattern, + lang, + name, + script, + executeScript, + getHttpStart, + docLinksScriptedFields, +}) => { + const tabs = [ + { + id: 'syntax', + name: 'Syntax', + ['data-test-subj']: 'syntaxTab', + content: , + }, + { + id: 'test', + name: 'Preview results', + ['data-test-subj']: 'testTab', + content: ( + + ), + }, + ]; + + return isVisible ? ( + + + + + + ) : null; +}; + +ScriptingHelpFlyout.displayName = 'ScriptingHelpFlyout'; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/index.js b/src/legacy/ui/public/field_editor/components/scripting_help/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/components/scripting_help/index.js rename to src/legacy/ui/public/field_editor/components/scripting_help/index.ts diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.js b/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.js deleted file mode 100644 index ba47b94aaea0c..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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 React, { Fragment } from 'react'; -import { getDocLink } from 'ui/documentation_links'; - -import { EuiCode, EuiIcon, EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ScriptingSyntax = () => ( - - - -

- -

-

- - {' '} - - - ), - }} - /> -

-

- - - -

-

- - -   - - - ), - syntax: ( - - -   - - - ), - }} - /> -

-

- -

-

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

-

- -

-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-

- -

-
    -
  • - + - * / % }} - /> -
  • -
  • - | & ^ ~ << >> >>>, - }} - /> -
  • -
  • - && || ! ?: }} - /> -
  • -
  • - < <= == >= > }} - /> -
  • -
  • - abs ceil exp floor ln log10 logn max min sqrt pow }} - /> -
  • -
  • - acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan - ), - }} - /> -
  • -
  • - haversin }} - /> -
  • -
  • - min, max }} - /> -
  • -
-
-
-); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.tsx new file mode 100644 index 0000000000000..8158c6881acf9 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/scripting_syntax.tsx @@ -0,0 +1,218 @@ +/* + * 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 React, { Fragment } from 'react'; +import { DocLinksStart } from 'src/core/public'; + +import { EuiCode, EuiIcon, EuiLink, EuiText, EuiSpacer } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ScriptingSyntaxProps { + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; +} + +export const ScriptingSyntax = ({ docLinksScriptedFields }: ScriptingSyntaxProps) => ( + + + +

+ +

+

+ + {' '} + + + ), + }} + /> +

+

+ + + +

+

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

+

+ +

+

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

+

+ +

+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+

+ +

+
    +
  • + + - * / % }} + /> +
  • +
  • + | & ^ ~ << >> >>>, + }} + /> +
  • +
  • + && || ! ?: }} + /> +
  • +
  • + < <= == >= > }} + /> +
  • +
  • + abs ceil exp floor ln log10 logn max min sqrt pow }} + /> +
  • +
  • + acosh acos asinh asin atanh atan atan2 cosh cos sinh sin tanh tan + ), + }} + /> +
  • +
  • + haversin }} + /> +
  • +
  • + min, max }} + /> +
  • +
+
+
+); diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js deleted file mode 100644 index 12bf5c1cce004..0000000000000 --- a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.js +++ /dev/null @@ -1,280 +0,0 @@ -/* - * 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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; - -import { - EuiButton, - EuiCodeBlock, - EuiComboBox, - EuiFormRow, - EuiText, - EuiSpacer, - EuiTitle, - EuiCallOut, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { npStart } from 'ui/new_platform'; -const { SearchBar } = npStart.plugins.data.ui; - -const { uiSettings } = npStart.core; - -import { esQuery } from '../../../../../../plugins/data/public'; - -export class TestScript extends Component { - state = { - isLoading: false, - additionalFields: [], - }; - - componentDidMount() { - if (this.props.script) { - this.previewScript(); - } - } - - previewScript = async searchContext => { - const { indexPattern, lang, name, script, executeScript } = this.props; - - if (!script || script.length === 0) { - return; - } - - this.setState({ - isLoading: true, - }); - - let query; - if (searchContext) { - const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); - query = esQuery.buildEsQuery( - this.props.indexPattern, - searchContext.query, - null, - esQueryConfigs - ); - } - - const scriptResponse = await executeScript({ - name, - lang, - script, - indexPatternTitle: indexPattern.title, - query, - additionalFields: this.state.additionalFields.map(option => { - return option.value; - }), - }); - - if (scriptResponse.status !== 200) { - this.setState({ - isLoading: false, - previewData: scriptResponse, - }); - return; - } - - this.setState({ - isLoading: false, - previewData: scriptResponse.hits.hits.map(hit => ({ - _id: hit._id, - ...hit._source, - ...hit.fields, - })), - }); - }; - - onAdditionalFieldsChange = selectedOptions => { - this.setState({ - additionalFields: selectedOptions, - }); - }; - - renderPreview() { - const { previewData } = this.state; - - if (!previewData) { - return null; - } - - if (previewData.error) { - return ( - - - {JSON.stringify(previewData.error, null, ' ')} - - - ); - } - - return ( - - -

- -

-
- - - {JSON.stringify(previewData, null, ' ')} - -
- ); - } - - renderToolbar() { - const fieldsByTypeMap = new Map(); - const fields = []; - - this.props.indexPattern.fields - .filter(field => { - const isMultiField = field.subType && field.subType.multi; - return !field.name.startsWith('_') && !isMultiField && !field.scripted; - }) - .forEach(field => { - if (fieldsByTypeMap.has(field.type)) { - const fieldsList = fieldsByTypeMap.get(field.type); - fieldsList.push(field.name); - fieldsByTypeMap.set(field.type, fieldsList); - } else { - fieldsByTypeMap.set(field.type, [field.name]); - } - }); - - fieldsByTypeMap.forEach((fieldsList, fieldType) => { - fields.push({ - label: fieldType, - options: fieldsList.sort().map(fieldName => { - return { value: fieldName, label: fieldName }; - }), - }); - }); - - fields.sort((a, b) => { - if (a.label < b.label) return -1; - if (a.label > b.label) return 1; - return 0; - }); - - return ( - - - - - -
- - - - } - /> -
-
- ); - } - - render() { - return ( - - - -

- -

-

- -

-
- - {this.renderToolbar()} - - {this.renderPreview()} -
- ); - } -} - -TestScript.propTypes = { - indexPattern: PropTypes.object.isRequired, - lang: PropTypes.string.isRequired, - name: PropTypes.string, - script: PropTypes.string, - executeScript: PropTypes.func.isRequired, -}; - -TestScript.defaultProps = { - name: 'myScriptedField', -}; diff --git a/src/legacy/ui/public/field_editor/components/scripting_help/test_script.tsx b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.tsx new file mode 100644 index 0000000000000..8d3a83a155553 --- /dev/null +++ b/src/legacy/ui/public/field_editor/components/scripting_help/test_script.tsx @@ -0,0 +1,293 @@ +/* + * 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 React, { Component, Fragment } from 'react'; +import { HttpStart } from 'src/core/public'; + +import { + EuiButton, + EuiCodeBlock, + EuiComboBox, + EuiFormRow, + EuiText, + EuiSpacer, + EuiTitle, + EuiCallOut, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { npStart } from 'ui/new_platform'; +const { SearchBar } = npStart.plugins.data.ui; + +const { uiSettings } = npStart.core; + +import { esQuery, IndexPattern, Query } from '../../../../../../plugins/data/public'; +import { ExecuteScript } from '../../types'; + +interface TestScriptProps { + indexPattern: IndexPattern; + lang: string; + name?: string; + script?: string; + executeScript: ExecuteScript; + getHttpStart: () => HttpStart; +} + +interface AdditionalField { + value: string; + label: string; +} + +interface TestScriptState { + isLoading: boolean; + additionalFields: AdditionalField[]; + previewData?: Record; +} + +export class TestScript extends Component { + defaultProps = { + name: 'myScriptedField', + }; + + state = { + isLoading: false, + additionalFields: [], + previewData: undefined, + }; + + componentDidMount() { + if (this.props.script) { + this.previewScript(); + } + } + + previewScript = async (searchContext?: { query?: Query | undefined }) => { + const { indexPattern, lang, name, script, executeScript, getHttpStart } = this.props; + + if (!script || script.length === 0) { + return; + } + + this.setState({ + isLoading: true, + }); + + let query; + if (searchContext) { + const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); + query = esQuery.buildEsQuery( + this.props.indexPattern, + searchContext.query || [], + [], + esQueryConfigs + ); + } + + const scriptResponse = await executeScript({ + name: name as string, + lang, + script, + indexPatternTitle: indexPattern.title, + query, + additionalFields: this.state.additionalFields.map((option: AdditionalField) => option.value), + getHttpStart, + }); + + if (scriptResponse.status !== 200) { + this.setState({ + isLoading: false, + previewData: scriptResponse, + }); + return; + } + + this.setState({ + isLoading: false, + previewData: scriptResponse.hits.hits.map(hit => ({ + _id: hit._id, + ...hit._source, + ...hit.fields, + })), + }); + }; + + onAdditionalFieldsChange = (selectedOptions: AdditionalField[]) => { + this.setState({ + additionalFields: selectedOptions, + }); + }; + + renderPreview(previewData: { error: any } | undefined) { + if (!previewData) { + return null; + } + + if (previewData.error) { + return ( + + + {JSON.stringify(previewData.error, null, ' ')} + + + ); + } + + return ( + + +

+ +

+
+ + + {JSON.stringify(previewData, null, ' ')} + +
+ ); + } + + renderToolbar() { + const fieldsByTypeMap = new Map(); + const fields: EuiComboBoxOptionOption[] = []; + + this.props.indexPattern.fields + .filter(field => { + const isMultiField = field.subType && field.subType.multi; + return !field.name.startsWith('_') && !isMultiField && !field.scripted; + }) + .forEach(field => { + if (fieldsByTypeMap.has(field.type)) { + const fieldsList = fieldsByTypeMap.get(field.type); + fieldsList.push(field.name); + fieldsByTypeMap.set(field.type, fieldsList); + } else { + fieldsByTypeMap.set(field.type, [field.name]); + } + }); + + fieldsByTypeMap.forEach((fieldsList, fieldType) => { + fields.push({ + label: fieldType, + options: fieldsList.sort().map((fieldName: string) => { + return { value: fieldName, label: fieldName }; + }), + }); + }); + + fields.sort((a, b) => { + if (a.label < b.label) return -1; + if (a.label > b.label) return 1; + return 0; + }); + + return ( + + + this.onAdditionalFieldsChange(selected as AdditionalField[])} + data-test-subj="additionalFieldsSelect" + fullWidth + /> + + +
+ + + + } + /> +
+
+ ); + } + + render() { + return ( + + + +

+ +

+

+ +

+
+ + {this.renderToolbar()} + + {this.renderPreview(this.state.previewData)} +
+ ); + } +} diff --git a/src/legacy/ui/public/field_editor/constants/index.js b/src/legacy/ui/public/field_editor/constants/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/constants/index.js rename to src/legacy/ui/public/field_editor/constants/index.ts diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js deleted file mode 100644 index e90cb110ac304..0000000000000 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ /dev/null @@ -1,832 +0,0 @@ -/* - * 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 React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { intersection, union, get } from 'lodash'; - -import { - GetEnabledScriptingLanguagesProvider, - getDeprecatedScriptingLanguages, - getSupportedScriptingLanguages, -} from 'ui/scripting_languages'; - -import { getDocLink } from 'ui/documentation_links'; - -import { toastNotifications } from 'ui/notify'; - -import { npStart } from 'ui/new_platform'; - -import { - EuiBasicTable, - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiCode, - EuiCodeEditor, - EuiConfirmModal, - EuiFieldNumber, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiIcon, - EuiLink, - EuiOverlayMask, - EuiSelect, - EuiSpacer, - EuiText, - EUI_MODAL_CONFIRM_BUTTON, -} from '@elastic/eui'; - -import { - ScriptingDisabledCallOut, - ScriptingWarningCallOut, -} from './components/scripting_call_outs'; - -import { ScriptingHelpFlyout } from './components/scripting_help'; - -import { FieldFormatEditor } from './components/field_format_editor'; - -import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants'; -import { executeScript, isScriptValid } from './lib'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -// This loads Ace editor's "groovy" mode, used below to highlight the script. -import 'brace/mode/groovy'; - -const getFieldFormats = () => npStart.plugins.data.fieldFormats; - -const getFieldTypeFormatsList = (field, defaultFieldFormat) => { - const fieldFormats = getFieldFormats(); - const formatsByType = fieldFormats.getByFieldType(field.type).map(({ id, title }) => ({ - id, - title, - })); - - return [ - { - id: '', - defaultFieldFormat, - title: i18n.translate('common.ui.fieldEditor.defaultFormatDropDown', { - defaultMessage: '- Default -', - }), - }, - ...formatsByType, - ]; -}; - -export class FieldEditor extends PureComponent { - static propTypes = { - indexPattern: PropTypes.object.isRequired, - field: PropTypes.object.isRequired, - helpers: PropTypes.shape({ - getConfig: PropTypes.func.isRequired, - $http: PropTypes.func.isRequired, - fieldFormatEditors: PropTypes.object.isRequired, - redirectAway: PropTypes.func.isRequired, - }), - }; - - constructor(props) { - super(props); - - const { field, indexPattern } = props; - - this.state = { - isReady: false, - isCreating: false, - isDeprecatedLang: false, - scriptingLangs: [], - fieldTypes: [], - fieldTypeFormats: [], - existingFieldNames: indexPattern.fields.map(f => f.name), - field: { ...field, format: field.format }, - fieldFormatId: undefined, - fieldFormatParams: {}, - showScriptingHelp: false, - showDeleteModal: false, - hasFormatError: false, - hasScriptError: false, - isSaving: false, - }; - this.supportedLangs = getSupportedScriptingLanguages(); - this.deprecatedLangs = getDeprecatedScriptingLanguages(); - this.init(); - } - - async init() { - const { $http } = this.props.helpers; - const { field } = this.state; - const { indexPattern } = this.props; - - const getEnabledScriptingLanguages = new GetEnabledScriptingLanguagesProvider($http); - const enabledLangs = await getEnabledScriptingLanguages(); - const scriptingLangs = intersection( - enabledLangs, - union(this.supportedLangs, this.deprecatedLangs) - ); - field.lang = scriptingLangs.includes(field.lang) ? field.lang : undefined; - - const fieldTypes = get(FIELD_TYPES_BY_LANG, field.lang, DEFAULT_FIELD_TYPES); - field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0]; - - const fieldFormats = getFieldFormats(); - const DefaultFieldFormat = fieldFormats.getDefaultType(field.type, field.esTypes); - - this.setState({ - isReady: true, - isCreating: !indexPattern.fields.getByName(field.name), - isDeprecatedLang: this.deprecatedLangs.includes(field.lang), - errors: [], - scriptingLangs, - fieldTypes, - fieldTypeFormats: getFieldTypeFormatsList(field, DefaultFieldFormat), - fieldFormatId: get(indexPattern, ['fieldFormatMap', field.name, 'type', 'id']), - fieldFormatParams: field.format.params(), - }); - } - - onFieldChange = (fieldName, value) => { - const { field } = this.state; - field[fieldName] = value; - this.forceUpdate(); - }; - - onTypeChange = type => { - const { getConfig } = this.props.helpers; - const { field } = this.state; - const fieldFormats = getFieldFormats(); - const DefaultFieldFormat = fieldFormats.getDefaultType(type); - - field.type = type; - field.format = new DefaultFieldFormat(null, getConfig); - - this.setState({ - fieldTypeFormats: getFieldTypeFormatsList(field, DefaultFieldFormat), - fieldFormatId: DefaultFieldFormat.id, - fieldFormatParams: field.format.params(), - }); - }; - - onLangChange = lang => { - const { field } = this.state; - const fieldTypes = get(FIELD_TYPES_BY_LANG, lang, DEFAULT_FIELD_TYPES); - field.lang = lang; - field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0]; - - this.setState({ - fieldTypes, - }); - }; - - onFormatChange = (formatId, params) => { - const fieldFormats = getFieldFormats(); - const { field, fieldTypeFormats } = this.state; - const FieldFormat = fieldFormats.getType( - formatId || fieldTypeFormats[0]?.defaultFieldFormat.id - ); - - field.format = new FieldFormat(params, this.props.helpers.getConfig); - - this.setState({ - fieldFormatId: FieldFormat.id, - fieldFormatParams: field.format.params(), - }); - }; - - onFormatParamsChange = newParams => { - const { fieldFormatId } = this.state; - this.onFormatChange(fieldFormatId, newParams); - }; - - onFormatParamsError = error => { - this.setState({ - hasFormatError: !!error, - }); - }; - - isDuplicateName() { - const { isCreating, field, existingFieldNames } = this.state; - return isCreating && existingFieldNames.includes(field.name); - } - - renderName() { - const { isCreating, field } = this.state; - const isInvalid = !field.name || !field.name.trim(); - - return isCreating ? ( - - -   - - - - ), - fieldName: {field.name}, - }} - /> - - ) : null - } - isInvalid={isInvalid} - error={ - isInvalid - ? i18n.translate('common.ui.fieldEditor.nameErrorMessage', { - defaultMessage: 'Name is required', - }) - : null - } - > - { - this.onFieldChange('name', e.target.value); - }} - isInvalid={isInvalid} - /> - - ) : null; - } - - renderLanguage() { - const { field, scriptingLangs, isDeprecatedLang } = this.state; - - return field.scripted ? ( - - -   - - - -   - {field.lang}, - painlessLink: ( - - - - ), - }} - /> - - ) : null - } - > - { - return { value: lang, text: lang }; - })} - data-test-subj="editorFieldLang" - onChange={e => { - this.onLangChange(e.target.value); - }} - /> - - ) : null; - } - - renderType() { - const { field, fieldTypes } = this.state; - - return ( - - { - return { value: type, text: type }; - })} - data-test-subj="editorFieldType" - onChange={e => { - this.onTypeChange(e.target.value); - }} - /> - - ); - } - - /** - * renders a warning and a table of conflicting indices - * in case there are indices with different types - */ - renderTypeConflict() { - const { field = {} } = this.state; - if (!field.conflictDescriptions || typeof field.conflictDescriptions !== 'object') { - return null; - } - - const columns = [ - { - field: 'type', - name: i18n.translate('common.ui.fieldEditor.typeLabel', { defaultMessage: 'Type' }), - width: '100px', - }, - { - field: 'indices', - name: i18n.translate('common.ui.fieldEditor.indexNameLabel', { - defaultMessage: 'Index names', - }), - }, - ]; - - const items = Object.entries(field.conflictDescriptions).map(([type, indices]) => ({ - type, - indices: Array.isArray(indices) ? indices.join(', ') : 'Index names unavailable', - })); - - return ( -
- - - } - size="s" - > - - - - - -
- ); - } - - renderFormat() { - const { field, fieldTypeFormats, fieldFormatId, fieldFormatParams } = this.state; - const { fieldFormatEditors } = this.props.helpers; - const defaultFormat = fieldTypeFormats[0]?.defaultFieldFormat.title; - - const label = defaultFormat ? ( - {defaultFormat}, - }} - /> - ) : ( - - ); - - return ( - - - } - > - { - return { value: format.id || '', text: format.title }; - })} - data-test-subj="editorSelectedFormatId" - onChange={e => { - this.onFormatChange(e.target.value); - }} - /> - - {fieldFormatId ? ( - - ) : null} - - ); - } - - renderPopularity() { - const { field } = this.state; - - return ( - - { - this.onFieldChange('count', e.target.value ? Number(e.target.value) : ''); - }} - /> - - ); - } - - onScriptChange = value => { - this.setState({ - hasScriptError: false, - }); - this.onFieldChange('script', value); - }; - - renderScript() { - const { field, hasScriptError } = this.state; - const isInvalid = !field.script || !field.script.trim() || hasScriptError; - const errorMsg = hasScriptError ? ( - - - - ) : ( - - ); - - return field.scripted ? ( - - - - - - - - - {`doc['some_field'].value`} }} - /> - -
- - - -
-
-
- ) : null; - } - - showScriptingHelp = () => { - this.setState({ - showScriptingHelp: true, - }); - }; - - hideScriptingHelp = () => { - this.setState({ - showScriptingHelp: false, - }); - }; - - renderDeleteModal = () => { - const { field } = this.state; - - return this.state.showDeleteModal ? ( - - { - this.hideDeleteModal(); - this.deleteField(); - }} - cancelButtonText={i18n.translate('common.ui.fieldEditor.deleteField.cancelButton', { - defaultMessage: 'Cancel', - })} - confirmButtonText={i18n.translate('common.ui.fieldEditor.deleteField.deleteButton', { - defaultMessage: 'Delete', - })} - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

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

-
-
- ) : null; - }; - - showDeleteModal = () => { - this.setState({ - showDeleteModal: true, - }); - }; - - hideDeleteModal = () => { - this.setState({ - showDeleteModal: false, - }); - }; - - renderActions() { - const { isCreating, field, isSaving } = this.state; - const { redirectAway } = this.props.helpers; - - return ( - - - - - {isCreating ? ( - - ) : ( - - )} - - - - - - - - {!isCreating && field.scripted ? ( - - - - - - - - - - ) : null} - - - ); - } - - renderScriptingPanels = () => { - const { scriptingLangs, field, showScriptingHelp } = this.state; - - if (!field.scripted) { - return; - } - - return ( - - - - - - ); - }; - - deleteField = () => { - const { redirectAway } = this.props.helpers; - const { indexPattern } = this.props; - const { field } = this.state; - const remove = indexPattern.removeScriptedField(field); - - if (remove) { - remove.then(() => { - const message = i18n.translate('common.ui.fieldEditor.deleteField.deletedHeader', { - defaultMessage: "Deleted '{fieldName}'", - values: { fieldName: field.name }, - }); - toastNotifications.addSuccess(message); - redirectAway(); - }); - } else { - redirectAway(); - } - }; - - saveField = async () => { - const field = this.state.field; - const { indexPattern } = this.props; - const { fieldFormatId } = this.state; - - if (field.scripted) { - this.setState({ - isSaving: true, - }); - - const isValid = await isScriptValid({ - name: field.name, - lang: field.lang, - script: field.script, - indexPatternTitle: indexPattern.title, - }); - - if (!isValid) { - this.setState({ - hasScriptError: true, - isSaving: false, - }); - return; - } - } - - const { redirectAway } = this.props.helpers; - const index = indexPattern.fields.findIndex(f => f.name === field.name); - - if (index > -1) { - indexPattern.fields.update(field); - } else { - indexPattern.fields.add(field); - } - - if (!fieldFormatId) { - indexPattern.fieldFormatMap[field.name] = undefined; - } else { - indexPattern.fieldFormatMap[field.name] = field.format; - } - - return indexPattern.save().then(function() { - const message = i18n.translate('common.ui.fieldEditor.deleteField.savedHeader', { - defaultMessage: "Saved '{fieldName}'", - values: { fieldName: field.name }, - }); - toastNotifications.addSuccess(message); - redirectAway(); - }); - }; - - isSavingDisabled() { - const { field, hasFormatError, hasScriptError } = this.state; - - if ( - hasFormatError || - hasScriptError || - !field.name || - !field.name.trim() || - (field.scripted && (!field.script || !field.script.trim())) - ) { - return true; - } - - return false; - } - - render() { - const { isReady, isCreating, field } = this.state; - - return isReady ? ( -
- -

- {isCreating ? ( - - ) : ( - - )} -

-
- - - {this.renderScriptingPanels()} - {this.renderName()} - {this.renderLanguage()} - {this.renderType()} - {this.renderTypeConflict()} - {this.renderFormat()} - {this.renderPopularity()} - {this.renderScript()} - {this.renderActions()} - {this.renderDeleteModal()} - - -
- ) : null; - } -} diff --git a/src/legacy/ui/public/field_editor/field_editor.test.js b/src/legacy/ui/public/field_editor/field_editor.test.js deleted file mode 100644 index cf61b6140f42c..0000000000000 --- a/src/legacy/ui/public/field_editor/field_editor.test.js +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 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. - */ - -jest.mock('ui/kfetch', () => ({})); - -import React from 'react'; - -import { npStart } from 'ui/new_platform'; -import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; - -jest.mock('brace/mode/groovy', () => ({})); -jest.mock('ui/new_platform'); - -import { FieldEditor } from './field_editor'; - -jest.mock('@elastic/eui', () => ({ - EuiBasicTable: 'eui-basic-table', - EuiButton: 'eui-button', - EuiButtonEmpty: 'eui-button-empty', - EuiCallOut: 'eui-call-out', - EuiCode: 'eui-code', - EuiConfirmModal: 'eui-confirm-modal', - EuiFieldNumber: 'eui-field-number', - EuiFieldText: 'eui-field-text', - EuiFlexGroup: 'eui-flex-group', - EuiFlexItem: 'eui-flex-item', - EuiForm: 'eui-form', - EuiFormRow: 'eui-form-row', - EuiIcon: 'eui-icon', - EuiLink: 'eui-link', - EuiOverlayMask: 'eui-overlay-mask', - EuiSelect: 'eui-select', - EuiSpacer: 'eui-spacer', - EuiText: 'eui-text', - EuiTextArea: 'eui-textArea', - htmlIdGenerator: () => 42, - euiPaletteColorBlind: () => ['red'], -})); - -jest.mock('ui/scripting_languages', () => ({ - GetEnabledScriptingLanguagesProvider: jest - .fn() - .mockImplementation(() => () => ['painless', 'testlang']), - getSupportedScriptingLanguages: () => ['painless'], - getDeprecatedScriptingLanguages: () => ['testlang'], -})); - -jest.mock('ui/documentation_links', () => ({ - getDocLink: doc => `(docLink for ${doc})`, -})); - -jest.mock('ui/notify', () => ({ - toastNotifications: { - addSuccess: jest.fn(), - }, -})); - -jest.mock('./components/scripting_call_outs', () => ({ - ScriptingDisabledCallOut: 'scripting-disabled-callOut', - ScriptingWarningCallOut: 'scripting-warning-callOut', - ScriptingHelpFlyout: 'scripting-help-flyout', -})); - -jest.mock('./components/field_format_editor', () => ({ - FieldFormatEditor: 'field-format-editor', -})); - -const fields = [ - { - name: 'foobar', - }, -]; -fields.getByName = name => { - const fields = { - foobar: { - name: 'foobar', - }, - }; - return fields[name]; -}; - -class Format { - static id = 'test_format'; - static title = 'Test format'; - params() {} -} - -const field = { - scripted: true, - type: 'number', - lang: 'painless', - format: new Format(), -}; - -const helpers = { - Field: () => {}, - getConfig: () => {}, - $http: () => {}, - fieldFormatEditors: {}, - redirectAway: () => {}, -}; - -describe('FieldEditor', () => { - let indexPattern; - - beforeEach(() => { - indexPattern = { - fields, - }; - - npStart.plugins.data.fieldFormats.getDefaultType = jest.fn(() => Format); - npStart.plugins.data.fieldFormats.getByFieldType = jest.fn(fieldType => { - if (fieldType === 'number') { - return [Format]; - } - }); - }); - - it('should render create new scripted field correctly', async () => { - const component = shallowWithI18nProvider( - - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should render edit scripted field correctly', async () => { - const testField = { - ...field, - name: 'test', - script: 'doc.test.value', - }; - indexPattern.fields.push(testField); - indexPattern.fields.getByName = name => { - const fields = { - [testField.name]: testField, - }; - return fields[name]; - }; - - const component = shallowWithI18nProvider( - - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should show deprecated lang warning', async () => { - const testField = { - ...field, - name: 'test', - script: 'doc.test.value', - lang: 'testlang', - }; - indexPattern.fields.push(testField); - indexPattern.fields.getByName = name => { - const fields = { - [testField.name]: testField, - }; - return fields[name]; - }; - - const component = shallowWithI18nProvider( - - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should show conflict field warning', async () => { - const testField = { ...field }; - const component = shallowWithI18nProvider( - - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.instance().onFieldChange('name', 'foobar'); - component.update(); - expect(component).toMatchSnapshot(); - }); - - it('should show multiple type field warning with a table containing indices', async () => { - const testField = { - ...field, - name: 'test-conflict', - conflictDescriptions: { - long: ['index_name_1', 'index_name_2'], - text: ['index_name_3'], - }, - }; - const component = shallowWithI18nProvider( - - ); - - await new Promise(resolve => process.nextTick(resolve)); - component.instance().onFieldChange('name', 'foobar'); - component.update(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/legacy/ui/public/field_editor/field_editor.test.tsx b/src/legacy/ui/public/field_editor/field_editor.test.tsx new file mode 100644 index 0000000000000..5716305b51483 --- /dev/null +++ b/src/legacy/ui/public/field_editor/field_editor.test.tsx @@ -0,0 +1,239 @@ +/* + * 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 React from 'react'; + +import { npStart } from 'ui/new_platform'; +import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; +import { + Field, + IndexPattern, + IndexPatternFieldList, + FieldFormatInstanceType, +} from 'src/plugins/data/public'; +import { HttpStart } from '../../../../core/public'; +// eslint-disable-next-line +import { docLinksServiceMock } from '../../../../core/public/doc_links/doc_links_service.mock'; + +jest.mock('brace/mode/groovy', () => ({})); +jest.mock('ui/new_platform'); + +import { FieldEditor } from './field_editor'; + +jest.mock('@elastic/eui', () => ({ + EuiBasicTable: 'eui-basic-table', + EuiButton: 'eui-button', + EuiButtonEmpty: 'eui-button-empty', + EuiCallOut: 'eui-call-out', + EuiCode: 'eui-code', + EuiConfirmModal: 'eui-confirm-modal', + EuiFieldNumber: 'eui-field-number', + EuiFieldText: 'eui-field-text', + EuiFlexGroup: 'eui-flex-group', + EuiFlexItem: 'eui-flex-item', + EuiForm: 'eui-form', + EuiFormRow: 'eui-form-row', + EuiIcon: 'eui-icon', + EuiLink: 'eui-link', + EuiOverlayMask: 'eui-overlay-mask', + EuiSelect: 'eui-select', + EuiSpacer: 'eui-spacer', + EuiText: 'eui-text', + EuiTextArea: 'eui-textArea', + htmlIdGenerator: () => 42, + euiPaletteColorBlind: () => ['red'], +})); + +jest.mock('ui/scripting_languages', () => ({ + getEnabledScriptingLanguages: () => ['painless', 'testlang'], + getSupportedScriptingLanguages: () => ['painless'], + getDeprecatedScriptingLanguages: () => ['testlang'], +})); + +jest.mock('./components/scripting_call_outs', () => ({ + ScriptingDisabledCallOut: 'scripting-disabled-callOut', + ScriptingWarningCallOut: 'scripting-warning-callOut', + ScriptingHelpFlyout: 'scripting-help-flyout', +})); + +jest.mock('./components/field_format_editor', () => ({ + FieldFormatEditor: 'field-format-editor', +})); + +const fields: Field[] = [ + { + name: 'foobar', + } as Field, +]; + +// @ts-ignore +fields.getByName = (name: string) => { + return fields.find(field => field.name === name); +}; + +class Format { + static id = 'test_format'; + static title = 'Test format'; + params() {} +} + +const field = { + scripted: true, + type: 'number', + lang: 'painless', + format: new Format(), +}; + +const helpers = { + Field: () => {}, + getConfig: () => {}, + getHttpStart: () => (({} as unknown) as HttpStart), + fieldFormatEditors: [], + redirectAway: () => {}, + docLinksScriptedFields: docLinksServiceMock.createStartContract().links.scriptedFields, +}; + +describe('FieldEditor', () => { + let indexPattern: IndexPattern; + + beforeEach(() => { + indexPattern = ({ + fields: fields as IndexPatternFieldList, + } as unknown) as IndexPattern; + + npStart.plugins.data.fieldFormats.getDefaultType = jest.fn( + () => (({} as unknown) as FieldFormatInstanceType) + ); + npStart.plugins.data.fieldFormats.getByFieldType = jest.fn(fieldType => { + if (fieldType === 'number') { + return [({} as unknown) as FieldFormatInstanceType]; + } else { + return []; + } + }); + }); + + it('should render create new scripted field correctly', async () => { + const component = shallowWithI18nProvider( + + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should render edit scripted field correctly', async () => { + const testField = { + ...field, + name: 'test', + script: 'doc.test.value', + }; + indexPattern.fields.push(testField as Field); + indexPattern.fields.getByName = name => { + const flds = { + [testField.name]: testField, + }; + return flds[name] as Field; + }; + + const component = shallowWithI18nProvider( + + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should show deprecated lang warning', async () => { + const testField = { + ...field, + name: 'test', + script: 'doc.test.value', + lang: 'testlang', + }; + indexPattern.fields.push((testField as unknown) as Field); + indexPattern.fields.getByName = name => { + const flds = { + [testField.name]: testField, + }; + return flds[name] as Field; + }; + + const component = shallowWithI18nProvider( + + ); + + await new Promise(resolve => process.nextTick(resolve)); + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should show conflict field warning', async () => { + const testField = { ...field }; + const component = shallowWithI18nProvider( + + ); + + await new Promise(resolve => process.nextTick(resolve)); + (component.instance() as FieldEditor).onFieldChange('name', 'foobar'); + component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should show multiple type field warning with a table containing indices', async () => { + const testField = { + ...field, + name: 'test-conflict', + conflictDescriptions: { + long: ['index_name_1', 'index_name_2'], + text: ['index_name_3'], + }, + }; + const component = shallowWithI18nProvider( + + ); + + await new Promise(resolve => process.nextTick(resolve)); + (component.instance() as FieldEditor).onFieldChange('name', 'foobar'); + component.update(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/legacy/ui/public/field_editor/field_editor.tsx b/src/legacy/ui/public/field_editor/field_editor.tsx new file mode 100644 index 0000000000000..aa62a53f2c32a --- /dev/null +++ b/src/legacy/ui/public/field_editor/field_editor.tsx @@ -0,0 +1,891 @@ +/* + * 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 React, { PureComponent, Fragment } from 'react'; +import { intersection, union, get } from 'lodash'; +import { HttpStart, DocLinksStart } from 'src/core/public'; + +import { + getEnabledScriptingLanguages, + getDeprecatedScriptingLanguages, + getSupportedScriptingLanguages, +} from 'ui/scripting_languages'; + +import { npStart } from 'ui/new_platform'; + +import { + EuiBasicTable, + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCode, + EuiCodeEditor, + EuiConfirmModal, + EuiFieldNumber, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiIcon, + EuiLink, + EuiOverlayMask, + EuiSelect, + EuiSpacer, + EuiText, + EUI_MODAL_CONFIRM_BUTTON, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + IndexPattern, + IFieldType, + KBN_FIELD_TYPES, + ES_FIELD_TYPES, +} from '../../../../plugins/data/public'; +import { FieldFormatInstanceType } from '../../../../plugins/data/common'; +import { Field } from '../../../../plugins/data/public'; +import { + ScriptingDisabledCallOut, + ScriptingWarningCallOut, +} from './components/scripting_call_outs'; + +import { ScriptingHelpFlyout } from './components/scripting_help'; + +import { FieldFormatEditor } from './components/field_format_editor'; + +import { DefaultFormatEditor } from './components/field_format_editor/editors/default'; + +import { FIELD_TYPES_BY_LANG, DEFAULT_FIELD_TYPES } from './constants'; +import { executeScript, isScriptValid } from './lib'; + +// This loads Ace editor's "groovy" mode, used below to highlight the script. +import 'brace/mode/groovy'; + +const getFieldFormats = () => npStart.plugins.data.fieldFormats; + +const getFieldTypeFormatsList = ( + field: IFieldType, + defaultFieldFormat: FieldFormatInstanceType +) => { + const fieldFormats = getFieldFormats(); + const formatsByType = fieldFormats + .getByFieldType(field.type as KBN_FIELD_TYPES) + .map(({ id, title }) => ({ + id, + title, + })); + + return [ + { + id: '', + defaultFieldFormat, + title: i18n.translate('common.ui.fieldEditor.defaultFormatDropDown', { + defaultMessage: '- Default -', + }), + }, + ...formatsByType, + ]; +}; + +interface FieldTypeFormat { + id: string; + title: string; +} + +interface InitialFieldTypeFormat extends FieldTypeFormat { + defaultFieldFormat: FieldFormatInstanceType; +} + +interface FieldClone extends Field { + format: any; +} + +export interface FieldEditorState { + isReady: boolean; + isCreating: boolean; + isDeprecatedLang: boolean; + scriptingLangs: string[]; + fieldTypes: string[]; + fieldTypeFormats: FieldTypeFormat[]; + existingFieldNames: string[]; + field: FieldClone; + fieldFormatId?: string; + fieldFormatParams: { [key: string]: unknown }; + showScriptingHelp: boolean; + showDeleteModal: boolean; + hasFormatError: boolean; + hasScriptError: boolean; + isSaving: boolean; + errors?: string[]; +} + +export interface FieldEdiorProps { + indexPattern: IndexPattern; + field: Field; + helpers: { + getConfig: (key: string) => any; + getHttpStart: () => HttpStart; + fieldFormatEditors: DefaultFormatEditor[]; + redirectAway: () => void; + docLinksScriptedFields: DocLinksStart['links']['scriptedFields']; + }; +} + +export class FieldEditor extends PureComponent { + supportedLangs: string[] = []; + deprecatedLangs: string[] = []; + constructor(props: FieldEdiorProps) { + super(props); + + const { field, indexPattern } = props; + + this.state = { + isReady: false, + isCreating: false, + isDeprecatedLang: false, + scriptingLangs: [], + fieldTypes: [], + fieldTypeFormats: [], + existingFieldNames: indexPattern.fields.map((f: IFieldType) => f.name), + field: { ...field, format: field.format }, + fieldFormatId: undefined, + fieldFormatParams: {}, + showScriptingHelp: false, + showDeleteModal: false, + hasFormatError: false, + hasScriptError: false, + isSaving: false, + }; + this.supportedLangs = getSupportedScriptingLanguages(); + this.deprecatedLangs = getDeprecatedScriptingLanguages(); + this.init(); + } + + async init() { + const { getHttpStart } = this.props.helpers; + const { field } = this.state; + const { indexPattern } = this.props; + + const enabledLangs = await getEnabledScriptingLanguages(await getHttpStart()); + const scriptingLangs = intersection( + enabledLangs, + union(this.supportedLangs, this.deprecatedLangs) + ); + field.lang = field.lang && scriptingLangs.includes(field.lang) ? field.lang : undefined; + + const fieldTypes = get(FIELD_TYPES_BY_LANG, field.lang || '', DEFAULT_FIELD_TYPES); + field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0]; + + const fieldFormats = getFieldFormats(); + const DefaultFieldFormat = fieldFormats.getDefaultType( + field.type as KBN_FIELD_TYPES, + field.esTypes as ES_FIELD_TYPES[] + ); + + this.setState({ + isReady: true, + isCreating: !indexPattern.fields.find(f => f.name === field.name), + isDeprecatedLang: this.deprecatedLangs.includes(field.lang || ''), + errors: [], + scriptingLangs, + fieldTypes, + fieldTypeFormats: getFieldTypeFormatsList( + field, + DefaultFieldFormat as FieldFormatInstanceType + ), + fieldFormatId: get(indexPattern, ['fieldFormatMap', field.name, 'type', 'id']), + fieldFormatParams: field.format.params(), + }); + } + + onFieldChange = (fieldName: string, value: string | number) => { + const { field } = this.state; + (field as any)[fieldName] = value; + this.forceUpdate(); + }; + + onTypeChange = (type: KBN_FIELD_TYPES) => { + const { getConfig } = this.props.helpers; + const { field } = this.state; + const fieldFormats = getFieldFormats(); + const DefaultFieldFormat = fieldFormats.getDefaultType(type) as FieldFormatInstanceType; + + field.type = type; + field.format = new DefaultFieldFormat(null, getConfig); + + this.setState({ + fieldTypeFormats: getFieldTypeFormatsList(field, DefaultFieldFormat), + fieldFormatId: DefaultFieldFormat.id, + fieldFormatParams: field.format.params(), + }); + }; + + onLangChange = (lang: string) => { + const { field } = this.state; + const fieldTypes = get(FIELD_TYPES_BY_LANG, lang, DEFAULT_FIELD_TYPES); + field.lang = lang; + field.type = fieldTypes.includes(field.type) ? field.type : fieldTypes[0]; + + this.setState({ + fieldTypes, + }); + }; + + onFormatChange = (formatId: string, params?: any) => { + const fieldFormats = getFieldFormats(); + const { field, fieldTypeFormats } = this.state; + const FieldFormat = fieldFormats.getType( + formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id + ) as FieldFormatInstanceType; + + field.format = new FieldFormat(params, this.props.helpers.getConfig); + + this.setState({ + fieldFormatId: FieldFormat.id, + fieldFormatParams: field.format.params(), + }); + }; + + onFormatParamsChange = (newParams: { fieldType: string; [key: string]: any }) => { + const { fieldFormatId } = this.state; + this.onFormatChange(fieldFormatId as string, newParams); + }; + + onFormatParamsError = (error?: string) => { + this.setState({ + hasFormatError: !!error, + }); + }; + + isDuplicateName() { + const { isCreating, field, existingFieldNames } = this.state; + return isCreating && existingFieldNames.includes(field.name); + } + + renderName() { + const { isCreating, field } = this.state; + const isInvalid = !field.name || !field.name.trim(); + + return isCreating ? ( + + +   + + + + ), + fieldName: {field.name}, + }} + /> + + ) : null + } + isInvalid={isInvalid} + error={ + isInvalid + ? i18n.translate('common.ui.fieldEditor.nameErrorMessage', { + defaultMessage: 'Name is required', + }) + : null + } + > + { + this.onFieldChange('name', e.target.value); + }} + isInvalid={isInvalid} + /> + + ) : null; + } + + renderLanguage() { + const { field, scriptingLangs, isDeprecatedLang } = this.state; + + return field.scripted ? ( + + +   + + + +   + {field.lang}, + painlessLink: ( + + + + ), + }} + /> + + ) : null + } + > + { + return { value: lang, text: lang }; + })} + data-test-subj="editorFieldLang" + onChange={e => { + this.onLangChange(e.target.value); + }} + /> + + ) : null; + } + + renderType() { + const { field, fieldTypes } = this.state; + + return ( + + { + return { value: type, text: type }; + })} + data-test-subj="editorFieldType" + onChange={e => { + this.onTypeChange(e.target.value as KBN_FIELD_TYPES); + }} + /> + + ); + } + + /** + * renders a warning and a table of conflicting indices + * in case there are indices with different types + */ + renderTypeConflict() { + const { field } = this.state; + if (!field.conflictDescriptions || typeof field.conflictDescriptions !== 'object') { + return null; + } + + const columns = [ + { + field: 'type', + name: i18n.translate('common.ui.fieldEditor.typeLabel', { defaultMessage: 'Type' }), + width: '100px', + }, + { + field: 'indices', + name: i18n.translate('common.ui.fieldEditor.indexNameLabel', { + defaultMessage: 'Index names', + }), + }, + ]; + + const items = Object.entries(field.conflictDescriptions).map(([type, indices]) => ({ + type, + indices: Array.isArray(indices) ? indices.join(', ') : 'Index names unavailable', + })); + + return ( +
+ + + } + size="s" + > + + + + + +
+ ); + } + + renderFormat() { + const { field, fieldTypeFormats, fieldFormatId, fieldFormatParams } = this.state; + const { fieldFormatEditors } = this.props.helpers; + const defaultFormat = (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.title; + + const label = defaultFormat ? ( + {defaultFormat}, + }} + /> + ) : ( + + ); + + return ( + + + } + > + { + return { value: format.id || '', text: format.title }; + })} + data-test-subj="editorSelectedFormatId" + onChange={e => { + this.onFormatChange(e.target.value); + }} + /> + + {fieldFormatId ? ( + + ) : null} + + ); + } + + renderPopularity() { + const { field } = this.state; + + return ( + + { + this.onFieldChange('count', e.target.value ? Number(e.target.value) : ''); + }} + /> + + ); + } + + onScriptChange = (value: string) => { + this.setState({ + hasScriptError: false, + }); + this.onFieldChange('script', value); + }; + + renderScript() { + const { field, hasScriptError } = this.state; + const isInvalid = !field.script || !field.script.trim() || hasScriptError; + const errorMsg = hasScriptError ? ( + + + + ) : ( + + ); + + return field.scripted ? ( + + + + + + + + + {`doc['some_field'].value`} }} + /> + +
+ + + +
+
+
+ ) : null; + } + + showScriptingHelp = () => { + this.setState({ + showScriptingHelp: true, + }); + }; + + hideScriptingHelp = () => { + this.setState({ + showScriptingHelp: false, + }); + }; + + renderDeleteModal = () => { + const { field } = this.state; + + return this.state.showDeleteModal ? ( + + { + this.hideDeleteModal(); + this.deleteField(); + }} + cancelButtonText={i18n.translate('common.ui.fieldEditor.deleteField.cancelButton', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('common.ui.fieldEditor.deleteField.deleteButton', { + defaultMessage: 'Delete', + })} + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + > +

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

+
+
+ ) : null; + }; + + showDeleteModal = () => { + this.setState({ + showDeleteModal: true, + }); + }; + + hideDeleteModal = () => { + this.setState({ + showDeleteModal: false, + }); + }; + + renderActions() { + const { isCreating, field, isSaving } = this.state; + const { redirectAway } = this.props.helpers; + + return ( + + + + + {isCreating ? ( + + ) : ( + + )} + + + + + + + + {!isCreating && field.scripted ? ( + + + + + + + + + + ) : null} + + + ); + } + + renderScriptingPanels = () => { + const { scriptingLangs, field, showScriptingHelp } = this.state; + + if (!field.scripted) { + return; + } + + return ( + + + + + + ); + }; + + deleteField = () => { + const { redirectAway } = this.props.helpers; + const { indexPattern } = this.props; + const { field } = this.state; + const remove = indexPattern.removeScriptedField(field); + + if (remove) { + remove.then(() => { + const message = i18n.translate('common.ui.fieldEditor.deleteField.deletedHeader', { + defaultMessage: "Deleted '{fieldName}'", + values: { fieldName: field.name }, + }); + npStart.core.notifications.toasts.addSuccess(message); + redirectAway(); + }); + } else { + redirectAway(); + } + }; + + saveField = async () => { + const field = this.state.field; + const { indexPattern } = this.props; + const { fieldFormatId } = this.state; + + if (field.scripted) { + this.setState({ + isSaving: true, + }); + + const isValid = await isScriptValid({ + name: field.name, + lang: field.lang as string, + script: field.script as string, + indexPatternTitle: indexPattern.title, + getHttpStart: this.props.helpers.getHttpStart, + }); + + if (!isValid) { + this.setState({ + hasScriptError: true, + isSaving: false, + }); + return; + } + } + + const { redirectAway } = this.props.helpers; + const index = indexPattern.fields.findIndex((f: IFieldType) => f.name === field.name); + + if (index > -1) { + indexPattern.fields.update(field); + } else { + indexPattern.fields.add(field); + } + + if (!fieldFormatId) { + indexPattern.fieldFormatMap[field.name] = undefined; + } else { + indexPattern.fieldFormatMap[field.name] = field.format; + } + + return indexPattern.save().then(function() { + const message = i18n.translate('common.ui.fieldEditor.deleteField.savedHeader', { + defaultMessage: "Saved '{fieldName}'", + values: { fieldName: field.name }, + }); + npStart.core.notifications.toasts.addSuccess(message); + redirectAway(); + }); + }; + + isSavingDisabled() { + const { field, hasFormatError, hasScriptError } = this.state; + + if ( + hasFormatError || + hasScriptError || + !field.name || + !field.name.trim() || + (field.scripted && (!field.script || !field.script.trim())) + ) { + return true; + } + + return false; + } + + render() { + const { isReady, isCreating, field } = this.state; + + return isReady ? ( +
+ +

+ {isCreating ? ( + + ) : ( + + )} +

+
+ + + {this.renderScriptingPanels()} + {this.renderName()} + {this.renderLanguage()} + {this.renderType()} + {this.renderTypeConflict()} + {this.renderFormat()} + {this.renderPopularity()} + {this.renderScript()} + {this.renderActions()} + {this.renderDeleteModal()} + + +
+ ) : null; + } +} diff --git a/src/legacy/ui/public/field_editor/index.js b/src/legacy/ui/public/field_editor/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/index.js rename to src/legacy/ui/public/field_editor/index.ts diff --git a/src/legacy/ui/public/field_editor/lib/index.js b/src/legacy/ui/public/field_editor/lib/index.ts similarity index 100% rename from src/legacy/ui/public/field_editor/lib/index.js rename to src/legacy/ui/public/field_editor/lib/index.ts diff --git a/src/legacy/ui/public/field_editor/lib/validate_script.js b/src/legacy/ui/public/field_editor/lib/validate_script.js deleted file mode 100644 index 47e2091565c30..0000000000000 --- a/src/legacy/ui/public/field_editor/lib/validate_script.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { kfetch } from 'ui/kfetch'; - -export const executeScript = async ({ - name, - lang, - script, - indexPatternTitle, - query, - additionalFields = [], -}) => { - // Using _msearch because _search with index name in path dorks everything up - const header = { - index: indexPatternTitle, - ignore_unavailable: true, - }; - - const search = { - query: { - match_all: {}, - }, - script_fields: { - [name]: { - script: { - lang, - source: script, - }, - }, - }, - size: 10, - timeout: '30s', - }; - - if (additionalFields.length > 0) { - search._source = additionalFields; - } - - if (query) { - search.query = query; - } - - const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; - const esResp = await kfetch({ method: 'POST', pathname: '/elasticsearch/_msearch', body }); - // unwrap _msearch response - return esResp.responses[0]; -}; - -export const isScriptValid = async ({ name, lang, script, indexPatternTitle }) => { - const scriptResponse = await executeScript({ name, lang, script, indexPatternTitle }); - - if (scriptResponse.status !== 200) { - return false; - } - - return true; -}; diff --git a/src/legacy/ui/public/field_editor/lib/validate_script.ts b/src/legacy/ui/public/field_editor/lib/validate_script.ts new file mode 100644 index 0000000000000..4db827a4229fd --- /dev/null +++ b/src/legacy/ui/public/field_editor/lib/validate_script.ts @@ -0,0 +1,97 @@ +/* + * 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 { Query } from 'src/plugins/data/public'; +import { HttpStart } from 'src/core/public'; +import { ExecuteScriptParams, ExecuteScriptResult } from '../types'; + +export const executeScript = async ({ + name, + lang, + script, + indexPatternTitle, + query, + additionalFields = [], + getHttpStart, +}: ExecuteScriptParams): Promise => { + // Using _msearch because _search with index name in path dorks everything up + const header = { + index: indexPatternTitle, + ignore_unavailable: true, + }; + + const search = { + query: { + match_all: {}, + } as Query['query'], + script_fields: { + [name]: { + script: { + lang, + source: script, + }, + }, + }, + _source: undefined as string[] | undefined, + size: 10, + timeout: '30s', + }; + + if (additionalFields.length > 0) { + search._source = additionalFields; + } + + if (query) { + search.query = query; + } + + const body = `${JSON.stringify(header)}\n${JSON.stringify(search)}\n`; + const http = await getHttpStart(); + const esResp = await http.fetch('/elasticsearch/_msearch', { method: 'POST', body }); + // unwrap _msearch response + return esResp.responses[0]; +}; + +export const isScriptValid = async ({ + name, + lang, + script, + indexPatternTitle, + getHttpStart, +}: { + name: string; + lang: string; + script: string; + indexPatternTitle: string; + getHttpStart: () => HttpStart; +}) => { + const scriptResponse = await executeScript({ + name, + lang, + script, + indexPatternTitle, + getHttpStart, + }); + + if (scriptResponse.status !== 200) { + return false; + } + + return true; +}; diff --git a/src/legacy/ui/public/field_editor/types.ts b/src/legacy/ui/public/field_editor/types.ts new file mode 100644 index 0000000000000..174cb7da73ceb --- /dev/null +++ b/src/legacy/ui/public/field_editor/types.ts @@ -0,0 +1,45 @@ +/* + * 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 { ReactText } from 'react'; +import { Query } from 'src/plugins/data/public'; +import { HttpStart } from 'src/core/public'; + +export interface Sample { + input: ReactText | ReactText[]; + output: string; +} + +export interface ExecuteScriptParams { + name: string; + lang: string; + script: string; + indexPatternTitle: string; + query?: Query['query']; + additionalFields?: string[]; + getHttpStart: () => HttpStart; +} + +export interface ExecuteScriptResult { + status: number; + hits: { hits: any[] }; + error?: any; +} + +export type ExecuteScript = (params: ExecuteScriptParams) => Promise; diff --git a/src/legacy/ui/public/i18n/index.test.tsx b/src/legacy/ui/public/i18n/index.test.tsx index c7a778ac18bd3..be8ab4cf8d696 100644 --- a/src/legacy/ui/public/i18n/index.test.tsx +++ b/src/legacy/ui/public/i18n/index.test.tsx @@ -21,6 +21,7 @@ import { render } from 'enzyme'; import PropTypes from 'prop-types'; import React from 'react'; +jest.mock('angular-sanitize', () => {}); jest.mock('ui/new_platform', () => ({ npStart: { core: { diff --git a/src/legacy/ui/public/i18n/index.tsx b/src/legacy/ui/public/i18n/index.tsx index 4d0f5d3a5bd56..6f1120dce0c7c 100644 --- a/src/legacy/ui/public/i18n/index.tsx +++ b/src/legacy/ui/public/i18n/index.tsx @@ -18,6 +18,8 @@ */ import React from 'react'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; // @ts-ignore @@ -44,7 +46,7 @@ export function wrapInI18nContext

(ComponentToWrap: React.ComponentType

) { } uiModules - .get('i18n') + .get('i18n', ['ngSanitize']) .provider('i18n', I18nProvider) .filter('i18n', i18nFilter) .directive('i18nId', i18nDirective); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f577a29ce90b9..3caba24748bfa 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -242,6 +242,7 @@ export const npSetup = { }, kibanaLegacy: { registerLegacyApp: () => {}, + registerLegacyAppAlias: () => {}, forwardApp: () => {}, config: { defaultAppId: 'home', @@ -362,6 +363,7 @@ export const npStart = { kibanaLegacy: { getApps: () => [], getForwards: () => [], + getLegacyAppAliases: () => [], config: { defaultAppId: 'home', }, @@ -375,7 +377,8 @@ export const npStart = { }, data: { actions: { - createFiltersFromEvent: Promise.resolve(['yes']), + createFiltersFromValueClickAction: Promise.resolve(['yes']), + createFiltersFromRangeSelectAction: sinon.fake(), }, autocomplete: { getProvider: sinon.fake(), @@ -459,16 +462,6 @@ export const npStart = { types: aggTypesRegistry.start(), }, __LEGACY: { - AggConfig: sinon.fake(), - AggType: sinon.fake(), - aggTypeFieldFilters: { - addFilter: sinon.fake(), - filter: sinon.fake(), - }, - FieldParamType: sinon.fake(), - MetricAggType: sinon.fake(), - parentPipelineAggHelper: sinon.fake(), - siblingPipelineAggHelper: sinon.fake(), esClient: { search: sinon.fake(), msearch: sinon.fake(), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 5ae2e2348aaa1..a15c7cce5511d 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -59,7 +59,6 @@ import { NavigationPublicPluginSetup, NavigationPublicPluginStart, } from '../../../../plugins/navigation/public'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; import { DiscoverSetup, DiscoverStart } from '../../../../plugins/discover/public'; import { SavedObjectsManagementPluginSetup, @@ -88,7 +87,6 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; - visTypeVega: VisTypeVegaSetup; discover: DiscoverSetup; visualizations: VisualizationsSetup; telemetry?: TelemetryPluginSetup; diff --git a/src/legacy/ui/public/registry/field_format_editors.js b/src/legacy/ui/public/registry/field_format_editors.js deleted file mode 100644 index 85850e56fdb86..0000000000000 --- a/src/legacy/ui/public/registry/field_format_editors.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { uiRegistry } from './_registry'; - -export const RegistryFieldFormatEditorsProvider = uiRegistry({ - name: 'fieldFormatEditors', - index: ['formatId'], - constructor: function() { - this.getEditor = function(formatId) { - return this.byFormatId[formatId]; - }; - }, -}); diff --git a/src/legacy/ui/public/registry/field_format_editors.ts b/src/legacy/ui/public/registry/field_format_editors.ts new file mode 100644 index 0000000000000..5489ce9250e04 --- /dev/null +++ b/src/legacy/ui/public/registry/field_format_editors.ts @@ -0,0 +1,25 @@ +/* + * 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 { uiRegistry } from './_registry'; + +export const RegistryFieldFormatEditorsProvider = uiRegistry({ + name: 'fieldFormatEditors', + index: ['formatId'], +}); diff --git a/src/legacy/ui/public/scripting_languages/index.ts b/src/legacy/ui/public/scripting_languages/index.ts index 283a3273a2a5d..459e72c0c67c1 100644 --- a/src/legacy/ui/public/scripting_languages/index.ts +++ b/src/legacy/ui/public/scripting_languages/index.ts @@ -17,10 +17,8 @@ * under the License. */ -import { IHttpService } from 'angular'; import { i18n } from '@kbn/i18n'; - -import chrome from '../chrome'; +import { HttpStart } from 'src/core/public'; import { toastNotifications } from '../notify'; export function getSupportedScriptingLanguages(): string[] { @@ -31,18 +29,12 @@ export function getDeprecatedScriptingLanguages(): string[] { return []; } -export function GetEnabledScriptingLanguagesProvider($http: IHttpService) { - return () => { - return $http - .get(chrome.addBasePath('/api/kibana/scripts/languages')) - .then((res: any) => res.data) - .catch(() => { - toastNotifications.addDanger( - i18n.translate('common.ui.scriptingLanguages.errorFetchingToastDescription', { - defaultMessage: 'Error getting available scripting languages from Elasticsearch', - }) - ); - return []; - }); - }; -} +export const getEnabledScriptingLanguages = (http: HttpStart) => + http.get('/api/kibana/scripts/languages').catch(() => { + toastNotifications.addDanger( + i18n.translate('common.ui.scriptingLanguages.errorFetchingToastDescription', { + defaultMessage: 'Error getting available scripting languages from Elasticsearch', + }) + ); + return []; + }); diff --git a/src/legacy/ui/ui_bundles/app_entry_template.js b/src/legacy/ui/ui_bundles/app_entry_template.js index a1c3a153a196c..683fedd34316f 100644 --- a/src/legacy/ui/ui_bundles/app_entry_template.js +++ b/src/legacy/ui/ui_bundles/app_entry_template.js @@ -25,6 +25,9 @@ export const appEntryTemplate = bundle => ` * * This is programmatically created and updated, do not modify * + * Any changes to this file should be kept in sync with + * src/core/public/entry_point.ts + * * context: ${bundle.getContext()} */ @@ -45,7 +48,9 @@ i18n.load(injectedMetadata.i18n.translationsUrl) browserSupportsCsp: !window.__kbnCspNotEnforced__, requireLegacyFiles: () => { ${bundle.getRequires().join('\n ')} - } + }, + requireLegacyBootstrapModule: () => require('ui/chrome'), + requireNewPlatformShimModule: () => require('ui/new_platform'), }); coreSystem diff --git a/src/legacy/ui/ui_bundles/ui_bundles_controller.js b/src/legacy/ui/ui_bundles/ui_bundles_controller.js index 7afa283af83e0..79112fd687e84 100644 --- a/src/legacy/ui/ui_bundles/ui_bundles_controller.js +++ b/src/legacy/ui/ui_bundles/ui_bundles_controller.js @@ -99,13 +99,6 @@ export class UiBundlesController { this._postLoaders = []; this._bundles = []; - // create a bundle for core-only with no modules - this.add({ - id: 'core', - modules: [], - template: appEntryTemplate, - }); - // create a bundle for each uiApp for (const uiApp of uiApps) { this.add({ diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index bb246d97bfe4e..35e1f8b7d2127 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -24,6 +24,7 @@ export const UI_EXPORT_DEFAULTS = { webpackNoParseRules: [ /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, /node_modules[\/\\](mocha|moment)[\/\\]/, + /node_modules[\/\\]vega-lib[\/\\]build[\/\\]vega\.js$/, ], webpackAliases: { diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 4557d911620a2..b60442690af3c 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,6 +1,7 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; window.__kbnDarkMode__ = {{darkMode}}; +window.__kbnPublicPath__ = {{publicPathMap}}; if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); @@ -28,6 +29,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { document.body.innerHTML = err.outerHTML; } + var stylesheetTarget = document.querySelector('head meta[name="add-styles-here"]') function loadStyleSheet(url, cb) { var dom = document.createElement('link'); dom.rel = 'stylesheet'; @@ -35,9 +37,10 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { dom.href = url; dom.addEventListener('error', failure); dom.addEventListener('load', cb); - document.head.appendChild(dom); + document.head.insertBefore(dom, stylesheetTarget); } + var scriptsTarget = document.querySelector('head meta[name="add-scripts-here"]') function loadScript(url, cb) { var dom = document.createElement('script'); {{!-- NOTE: async = false is used to trigger async-download/ordered-execution as outlined here: https://www.html5rocks.com/en/tutorials/speed/script-loading/ --}} @@ -45,7 +48,7 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { dom.src = url; dom.addEventListener('error', failure); dom.addEventListener('load', cb); - document.head.appendChild(dom); + document.head.insertBefore(dom, scriptsTarget); } function load(urls, cb) { @@ -69,26 +72,16 @@ if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { } load([ - {{#each sharedJsDepFilenames}} - '{{../regularBundlePath}}/kbn-ui-shared-deps/{{this}}', - {{/each}} - '{{regularBundlePath}}/kbn-ui-shared-deps/{{sharedJsFilename}}', - '{{dllBundlePath}}/vendors_runtime.bundle.dll.js', - {{#each dllJsChunks}} + {{#each jsDependencyPaths}} '{{this}}', {{/each}} - '{{regularBundlePath}}/commons.bundle.js', - {{!-- '{{regularBundlePath}}/plugin:data/data.plugin.js', --}} - '{{regularBundlePath}}/plugin:kibanaUtils/kibanaUtils.plugin.js', - '{{regularBundlePath}}/plugin:esUiShared/esUiShared.plugin.js', - '{{regularBundlePath}}/plugin:kibanaReact/kibanaReact.plugin.js' ], function () { load([ - '{{regularBundlePath}}/{{appId}}.bundle.js', + '{{entryBundlePath}}', {{#each styleSheetPaths}} '{{this}}', {{/each}} - ]) + ]); }); - }; + } } diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 0912d8683fc48..9b44395fa9c68 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -19,13 +19,15 @@ import { createHash } from 'crypto'; import Boom from 'boom'; -import { resolve } from 'path'; +import Path from 'path'; import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { AppBootstrap } from './bootstrap'; import { getApmConfig } from '../apm'; import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; +const uniq = (...items) => Array.from(new Set(items)); + /** * @typedef {import('../../server/kbn_server').default} KbnServer * @typedef {import('../../server/kbn_server').ResponseToolkit} ResponseToolkit @@ -39,7 +41,7 @@ import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; */ export function uiRenderMixin(kbnServer, server, config) { // render all views from ./views - server.setupViews(resolve(__dirname, 'views')); + server.setupViews(Path.resolve(__dirname, 'views')); const translationsCache = { translations: null, hash: null }; server.route({ @@ -94,50 +96,102 @@ export function uiRenderMixin(kbnServer, server, config) { ? await uiSettings.get('theme:darkMode') : false; + const buildHash = server.newPlatform.env.packageInfo.buildNum; const basePath = config.get('server.basePath'); - const regularBundlePath = `${basePath}/bundles`; - const dllBundlePath = `${basePath}/built_assets/dlls`; + + const regularBundlePath = `${basePath}/${buildHash}/bundles`; + const dllBundlePath = `${basePath}/${buildHash}/built_assets/dlls`; + const dllStyleChunks = DllCompiler.getRawDllConfig().chunks.map( chunk => `${dllBundlePath}/vendors${chunk}.style.dll.css` ); const dllJsChunks = DllCompiler.getRawDllConfig().chunks.map( chunk => `${dllBundlePath}/vendors${chunk}.bundle.dll.js` ); + const styleSheetPaths = [ - ...dllStyleChunks, - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + ...(isCore ? [] : dllStyleChunks), + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, ...(darkMode ? [ - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, + `${regularBundlePath}/dark_theme.style.css`, ] : [ - `${basePath}/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, + `${regularBundlePath}/light_theme.style.css`, ]), - `${regularBundlePath}/${darkMode ? 'dark' : 'light'}_theme.style.css`, `${regularBundlePath}/commons.style.css`, - ...(!isCore ? [`${regularBundlePath}/${app.getId()}.style.css`] : []), - ...kbnServer.uiExports.styleSheetPaths - .filter(path => path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light')) - .map(path => - path.localPath.endsWith('.scss') - ? `${basePath}/built_assets/css/${path.publicPath}` - : `${basePath}/${path.publicPath}` - ) - .reverse(), + ...(isCore + ? [] + : [ + `${regularBundlePath}/${app.getId()}.style.css`, + ...kbnServer.uiExports.styleSheetPaths + .filter( + path => path.theme === '*' || path.theme === (darkMode ? 'dark' : 'light') + ) + .map(path => + path.localPath.endsWith('.scss') + ? `${basePath}/${buildHash}/built_assets/css/${path.publicPath}` + : `${basePath}/${path.publicPath}` + ) + .reverse(), + ]), ]; + const kpPluginIds = uniq( + // load these plugins first, they are "shared" and other bundles access their + // public/index exports without considering topographic sorting by plugin deps (for now) + 'kibanaUtils', + 'kibanaReact', + 'data', + 'esUiShared', + ...kbnServer.newPlatform.__internals.uiPlugins.public.keys() + ); + + const jsDependencyPaths = [ + ...UiSharedDeps.jsDepFilenames.map( + filename => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` + ), + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + ...(isCore + ? [] + : [ + `${dllBundlePath}/vendors_runtime.bundle.dll.js`, + ...dllJsChunks, + `${regularBundlePath}/commons.bundle.js`, + ]), + + ...kpPluginIds.map( + pluginId => `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js` + ), + ]; + + // These paths should align with the bundle routes configured in + // src/optimize/bundles_route/bundles_route.ts + const publicPathMap = JSON.stringify({ + core: `${regularBundlePath}/core/`, + 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, + ...kpPluginIds.reduce( + (acc, pluginId) => ({ + ...acc, + [pluginId]: `${regularBundlePath}/plugin/${pluginId}/`, + }), + {} + ), + }); + const bootstrap = new AppBootstrap({ templateData: { - appId: isCore ? 'core' : app.getId(), - regularBundlePath, - dllBundlePath, - dllJsChunks, - styleSheetPaths, - sharedJsFilename: UiSharedDeps.jsFilename, - sharedJsDepFilenames: UiSharedDeps.jsDepFilenames, darkMode, + jsDependencyPaths, + styleSheetPaths, + publicPathMap, + entryBundlePath: isCore + ? `${regularBundlePath}/core/core.entry.js` + : `${regularBundlePath}/${app.getId()}.bundle.js`, }, }); diff --git a/src/optimize/bundles_route/__tests__/bundles_route.js b/src/optimize/bundles_route/__tests__/bundles_route.js index 0b2aeda11fb0e..902fa59b20569 100644 --- a/src/optimize/bundles_route/__tests__/bundles_route.js +++ b/src/optimize/bundles_route/__tests__/bundles_route.js @@ -32,6 +32,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from '../../public_path_placeholder'; const chance = new Chance(); const outputFixture = resolve(__dirname, './fixtures/output'); +const pluginNoPlaceholderFixture = resolve(__dirname, './fixtures/plugin/no_placeholder'); const randomWordsCache = new Set(); const uniqueRandomWord = () => { @@ -58,6 +59,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath = outputFixture, basePublicPath = '', builtCssPath = outputFixture, + npUiPluginPublicDirs = [], + buildHash = '1234', + isDist = false, } = options; const server = new Hapi.Server(); @@ -69,6 +73,9 @@ describe('optimizer/bundle route', () => { dllBundlesPath, basePublicPath, builtCssPath, + npUiPluginPublicDirs, + buildHash, + isDist, }) ); @@ -158,7 +165,7 @@ describe('optimizer/bundle route', () => { it('responds with exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/image.png', + url: '/1234/bundles/image.png', }); expect(response.statusCode).to.be(200); @@ -173,7 +180,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -187,12 +194,12 @@ describe('optimizer/bundle route', () => { }); describe('js file with placeholder', () => { - it('responds with no content-length and modified file data', async () => { + it('responds with no content-length and modifiedfile data ', async () => { const basePublicPath = `/${uniqueRandomWord()}`; const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(200); @@ -204,7 +211,7 @@ describe('optimizer/bundle route', () => { ); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -213,7 +220,7 @@ describe('optimizer/bundle route', () => { it('responds with no content-length and exact file data', async () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/no_placeholder.css', + url: '/1234/bundles/no_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -231,7 +238,7 @@ describe('optimizer/bundle route', () => { const server = createServer({ basePublicPath }); const response = await server.inject({ - url: '/bundles/with_placeholder.css', + url: '/1234/bundles/with_placeholder.css', }); expect(response.statusCode).to.be(200); @@ -240,7 +247,7 @@ describe('optimizer/bundle route', () => { expect(response.headers).to.have.property('content-type', 'text/css; charset=utf-8'); expect(response.result.indexOf(source)).to.be(-1); expect(response.result).to.be( - replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/bundles/`) + replaceAll(source, PUBLIC_PATH_PLACEHOLDER, `${basePublicPath}/1234/bundles/`) ); }); }); @@ -250,7 +257,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/../outside_output.js', + url: '/1234/bundles/../outside_output.js', }); expect(response.statusCode).to.be(404); @@ -267,7 +274,7 @@ describe('optimizer/bundle route', () => { const server = createServer(); const response = await server.inject({ - url: '/bundles/non_existent.js', + url: '/1234/bundles/non_existent.js', }); expect(response.statusCode).to.be(404); @@ -286,7 +293,7 @@ describe('optimizer/bundle route', () => { }); const response = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(response.statusCode).to.be(404); @@ -306,7 +313,7 @@ describe('optimizer/bundle route', () => { sinon.assert.notCalled(createHash); const resp1 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.calledOnce(createHash); @@ -314,23 +321,23 @@ describe('optimizer/bundle route', () => { expect(resp1.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }); sinon.assert.notCalled(createHash); expect(resp2.statusCode).to.be(200); }); - it('is unique per basePublicPath although content is the same', async () => { + it('is unique per basePublicPath although content is the same (by default)', async () => { const basePublicPath1 = `/${uniqueRandomWord()}`; const basePublicPath2 = `/${uniqueRandomWord()}`; const [resp1, resp2] = await Promise.all([ createServer({ basePublicPath: basePublicPath1 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), createServer({ basePublicPath: basePublicPath2 }).inject({ - url: '/bundles/no_placeholder.js', + url: '/1234/bundles/no_placeholder.js', }), ]); @@ -349,13 +356,13 @@ describe('optimizer/bundle route', () => { it('responds with 304 when etag and last modified are sent back', async () => { const server = createServer(); const resp = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', }); expect(resp.statusCode).to.be(200); const resp2 = await server.inject({ - url: '/bundles/with_placeholder.js', + url: '/1234/bundles/with_placeholder.js', headers: { 'if-modified-since': resp.headers['last-modified'], 'if-none-match': resp.headers.etag, @@ -366,4 +373,80 @@ describe('optimizer/bundle route', () => { expect(resp2.result).to.have.length(0); }); }); + + describe('kibana platform assets', () => { + describe('caching', () => { + describe('for non-distributable mode', () => { + it('uses "etag" header to invalidate cache', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ basePublicPath, npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be.a('string'); + expect(responce.headers['cache-control']).to.be('must-revalidate'); + }); + + it('creates the same "etag" header for the same content with the same basePath', async () => { + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const [resp1, resp2] = await Promise.all([ + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + createServer({ basePublicPath: '', npUiPluginPublicDirs }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }), + ]); + + expect(resp1.statusCode).to.be(200); + expect(resp2.statusCode).to.be(200); + + expect(resp1.rawPayload).to.eql(resp2.rawPayload); + + expect(resp1.headers.etag).to.be.a('string'); + expect(resp2.headers.etag).to.be.a('string'); + expect(resp1.headers.etag).to.eql(resp2.headers.etag); + }); + }); + + describe('for distributable mode', () => { + it('commands to cache assets for each release for a year', async () => { + const basePublicPath = `/${uniqueRandomWord()}`; + + const npUiPluginPublicDirs = [ + { + id: 'no_placeholder', + path: pluginNoPlaceholderFixture, + }, + ]; + const responce = await createServer({ + basePublicPath, + npUiPluginPublicDirs, + isDist: true, + }).inject({ + url: '/1234/bundles/plugin/no_placeholder/no_placeholder.plugin.js', + }); + + expect(responce.statusCode).to.be(200); + + expect(responce.headers.etag).to.be(undefined); + expect(responce.headers['cache-control']).to.be('max-age=31536000'); + }); + }); + }); + }); }); diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js deleted file mode 100644 index f4e3108f80a3b..0000000000000 --- a/src/optimize/bundles_route/bundles_route.js +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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 { isAbsolute, extname } from 'path'; -import LruCache from 'lru-cache'; -import * as UiSharedDeps from '@kbn/ui-shared-deps'; -import { createDynamicAssetResponse } from './dynamic_asset_response'; -import { assertIsNpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; - -/** - * Creates the routes that serves files from `bundlesPath` or from - * `dllBundlesPath` (if they are dll bundle's related files). If the - * file is js or css then it is searched for instances of - * PUBLIC_PATH_PLACEHOLDER and replaces them with `publicPath`. - * - * @param {Object} options - * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins - * @property {string} options.regularBundlesPath - * @property {string} options.dllBundlesPath - * @property {string} options.basePublicPath - * - * @return Array.of({Hapi.Route}) - */ -export function createBundlesRoute({ - regularBundlesPath, - dllBundlesPath, - basePublicPath, - builtCssPath, - npUiPluginPublicDirs = [], -}) { - // rather than calculate the fileHash on every request, we - // provide a cache object to `resolveDynamicAssetResponse()` that - // will store the 100 most recently used hashes. - const fileHashCache = new LruCache(100); - assertIsNpUiPluginPublicDirs(npUiPluginPublicDirs); - - if (typeof regularBundlesPath !== 'string' || !isAbsolute(regularBundlesPath)) { - throw new TypeError( - 'regularBundlesPath must be an absolute path to the directory containing the regular bundles' - ); - } - - if (typeof dllBundlesPath !== 'string' || !isAbsolute(dllBundlesPath)) { - throw new TypeError( - 'dllBundlesPath must be an absolute path to the directory containing the dll bundles' - ); - } - - if (typeof basePublicPath !== 'string') { - throw new TypeError('basePublicPath must be a string'); - } - - if (!basePublicPath.match(/(^$|^\/.*[^\/]$)/)) { - throw new TypeError('basePublicPath must be empty OR start and not end with a /'); - } - - return [ - buildRouteForBundles( - `${basePublicPath}/bundles/kbn-ui-shared-deps/`, - '/bundles/kbn-ui-shared-deps/', - UiSharedDeps.distDir, - fileHashCache - ), - ...npUiPluginPublicDirs.map(({ id, path }) => - buildRouteForBundles( - `${basePublicPath}/bundles/plugin:${id}/`, - `/bundles/plugin:${id}/`, - path, - fileHashCache - ) - ), - buildRouteForBundles( - `${basePublicPath}/bundles/`, - '/bundles/', - regularBundlesPath, - fileHashCache - ), - buildRouteForBundles( - `${basePublicPath}/built_assets/dlls/`, - '/built_assets/dlls/', - dllBundlesPath, - fileHashCache - ), - buildRouteForBundles(`${basePublicPath}/`, '/built_assets/css/', builtCssPath, fileHashCache), - ]; -} - -function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) { - return { - method: 'GET', - path: `${routePath}{path*}`, - config: { - auth: false, - ext: { - onPreHandler: { - method(request, h) { - const ext = extname(request.params.path); - - if (ext !== '.js' && ext !== '.css') { - return h.continue; - } - - return createDynamicAssetResponse({ - request, - h, - bundlesPath, - fileHashCache, - publicPath, - }); - }, - }, - }, - }, - handler: { - directory: { - path: bundlesPath, - listing: false, - lookupCompressed: true, - }, - }, - }; -} diff --git a/src/optimize/bundles_route/bundles_route.ts b/src/optimize/bundles_route/bundles_route.ts new file mode 100644 index 0000000000000..e9cfba0130d95 --- /dev/null +++ b/src/optimize/bundles_route/bundles_route.ts @@ -0,0 +1,188 @@ +/* + * 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 { isAbsolute, extname, join } from 'path'; + +import Hapi from 'hapi'; +import * as UiSharedDeps from '@kbn/ui-shared-deps'; + +import { createDynamicAssetResponse } from './dynamic_asset_response'; +import { FileHashCache } from './file_hash_cache'; +import { assertIsNpUiPluginPublicDirs, NpUiPluginPublicDirs } from '../np_ui_plugin_public_dirs'; +import { fromRoot } from '../../core/server/utils'; + +/** + * Creates the routes that serves files from `bundlesPath` or from + * `dllBundlesPath` (if they are dll bundle's related files). If the + * file is js or css then it is searched for instances of + * PUBLIC_PATH_PLACEHOLDER and replaces them with `publicPath`. + * + * @param {Object} options + * @property {Array<{id,path}>} options.npUiPluginPublicDirs array of ids and paths that should be served for new platform plugins + * @property {string} options.regularBundlesPath + * @property {string} options.dllBundlesPath + * @property {string} options.basePublicPath + * + * @return Array.of({Hapi.Route}) + */ +export function createBundlesRoute({ + regularBundlesPath, + dllBundlesPath, + basePublicPath, + builtCssPath, + npUiPluginPublicDirs = [], + buildHash, + isDist = false, +}: { + regularBundlesPath: string; + dllBundlesPath: string; + basePublicPath: string; + builtCssPath: string; + npUiPluginPublicDirs?: NpUiPluginPublicDirs; + buildHash: string; + isDist?: boolean; +}) { + // rather than calculate the fileHash on every request, we + // provide a cache object to `resolveDynamicAssetResponse()` that + // will store the 100 most recently used hashes. + const fileHashCache = new FileHashCache(); + assertIsNpUiPluginPublicDirs(npUiPluginPublicDirs); + + if (typeof regularBundlesPath !== 'string' || !isAbsolute(regularBundlesPath)) { + throw new TypeError( + 'regularBundlesPath must be an absolute path to the directory containing the regular bundles' + ); + } + + if (typeof dllBundlesPath !== 'string' || !isAbsolute(dllBundlesPath)) { + throw new TypeError( + 'dllBundlesPath must be an absolute path to the directory containing the dll bundles' + ); + } + + if (typeof basePublicPath !== 'string') { + throw new TypeError('basePublicPath must be a string'); + } + + if (!basePublicPath.match(/(^$|^\/.*[^\/]$)/)) { + throw new TypeError('basePublicPath must be empty OR start and not end with a /'); + } + + return [ + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/kbn-ui-shared-deps/`, + routePath: `/${buildHash}/bundles/kbn-ui-shared-deps/`, + bundlesPath: UiSharedDeps.distDir, + fileHashCache, + replacePublicPath: false, + isDist, + }), + ...npUiPluginPublicDirs.map(({ id, path }) => + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/plugin/${id}/`, + routePath: `/${buildHash}/bundles/plugin/${id}/`, + bundlesPath: path, + fileHashCache, + replacePublicPath: false, + isDist, + }) + ), + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/core/`, + routePath: `/${buildHash}/bundles/core/`, + bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), + fileHashCache, + replacePublicPath: false, + isDist, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/bundles/`, + routePath: `/${buildHash}/bundles/`, + bundlesPath: regularBundlesPath, + fileHashCache, + isDist, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/${buildHash}/built_assets/dlls/`, + routePath: `/${buildHash}/built_assets/dlls/`, + bundlesPath: dllBundlesPath, + fileHashCache, + isDist, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/`, + routePath: `/${buildHash}/built_assets/css/`, + bundlesPath: builtCssPath, + fileHashCache, + isDist, + }), + ]; +} + +function buildRouteForBundles({ + publicPath, + routePath, + bundlesPath, + fileHashCache, + replacePublicPath = true, + isDist, +}: { + publicPath: string; + routePath: string; + bundlesPath: string; + fileHashCache: FileHashCache; + replacePublicPath?: boolean; + isDist: boolean; +}) { + return { + method: 'GET', + path: `${routePath}{path*}`, + config: { + auth: false, + ext: { + onPreHandler: { + method(request: Hapi.Request, h: Hapi.ResponseToolkit) { + const ext = extname(request.params.path); + + if (ext !== '.js' && ext !== '.css') { + return h.continue; + } + + return createDynamicAssetResponse({ + request, + h, + bundlesPath, + fileHashCache, + publicPath, + replacePublicPath, + isDist, + }); + }, + }, + }, + }, + handler: { + directory: { + path: bundlesPath, + listing: false, + lookupCompressed: true, + }, + }, + }; +} diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.js deleted file mode 100644 index 7af780a79e430..0000000000000 --- a/src/optimize/bundles_route/dynamic_asset_response.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 { resolve } from 'path'; -import { open, fstat, createReadStream, close } from 'fs'; - -import Boom from 'boom'; -import { fromNode as fcb } from 'bluebird'; - -import { getFileHash } from './file_hash'; -import { replacePlaceholder } from '../public_path_placeholder'; - -/** - * Create a Hapi response for the requested path. This is designed - * to replicate a subset of the features provided by Hapi's Inert - * plugin including: - * - ensure path is not traversing out of the bundle directory - * - manage use file descriptors for file access to efficiently - * interact with the file multiple times in each request - * - generate and cache etag for the file - * - write correct headers to response for client-side caching - * and invalidation - * - stream file to response - * - * It differs from Inert in some important ways: - * - the PUBLIC_PATH_PLACEHOLDER is replaced with the correct - * public path as the response is streamed - * - cached hash/etag is based on the file on disk, but modified - * by the public path so that individual public paths have - * different etags, but can share a cache - * - * @param {Object} options - * @property {Hapi.Request} options.request - * @property {string} options.bundlesPath - * @property {string} options.publicPath - * @property {LruCache} options.fileHashCache - */ -export async function createDynamicAssetResponse(options) { - const { request, h, bundlesPath, publicPath, fileHashCache } = options; - - let fd; - try { - const path = resolve(bundlesPath, request.params.path); - - // prevent path traversal, only process paths that resolve within bundlesPath - if (!path.startsWith(bundlesPath)) { - throw Boom.forbidden(null, 'EACCES'); - } - - // we use and manage a file descriptor mostly because - // that's what Inert does, and since we are accessing - // the file 2 or 3 times per request it seems logical - fd = await fcb(cb => open(path, 'r', cb)); - - const stat = await fcb(cb => fstat(fd, cb)); - const hash = await getFileHash(fileHashCache, path, stat, fd); - - const read = createReadStream(null, { - fd, - start: 0, - autoClose: true, - }); - fd = null; // read stream is now responsible for fd - - return h - .response(replacePlaceholder(read, publicPath)) - .takeover() - .code(200) - .etag(`${hash}-${publicPath}`) - .header('cache-control', 'must-revalidate') - .type(request.server.mime.path(path).type); - } catch (error) { - if (fd) { - try { - await fcb(cb => close(fd, cb)); - } catch (error) { - // ignore errors from close, we already have one to report - // and it's very likely they are the same - } - } - - if (error.code === 'ENOENT') { - throw Boom.notFound(); - } - - throw Boom.boomify(error); - } -} diff --git a/src/optimize/bundles_route/dynamic_asset_response.ts b/src/optimize/bundles_route/dynamic_asset_response.ts new file mode 100644 index 0000000000000..a020c6935eeec --- /dev/null +++ b/src/optimize/bundles_route/dynamic_asset_response.ts @@ -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 Fs from 'fs'; +import { resolve } from 'path'; +import { promisify } from 'util'; + +import Boom from 'boom'; +import Hapi from 'hapi'; + +import { FileHashCache } from './file_hash_cache'; +import { getFileHash } from './file_hash'; +// @ts-ignore +import { replacePlaceholder } from '../public_path_placeholder'; + +const MINUTE = 60; +const HOUR = 60 * MINUTE; +const DAY = 24 * HOUR; + +const asyncOpen = promisify(Fs.open); +const asyncClose = promisify(Fs.close); +const asyncFstat = promisify(Fs.fstat); + +/** + * Create a Hapi response for the requested path. This is designed + * to replicate a subset of the features provided by Hapi's Inert + * plugin including: + * - ensure path is not traversing out of the bundle directory + * - manage use file descriptors for file access to efficiently + * interact with the file multiple times in each request + * - generate and cache etag for the file + * - write correct headers to response for client-side caching + * and invalidation + * - stream file to response + * + * It differs from Inert in some important ways: + * - the PUBLIC_PATH_PLACEHOLDER is replaced with the correct + * public path as the response is streamed + * - cached hash/etag is based on the file on disk, but modified + * by the public path so that individual public paths have + * different etags, but can share a cache + */ +export async function createDynamicAssetResponse({ + request, + h, + bundlesPath, + publicPath, + fileHashCache, + replacePublicPath, + isDist, +}: { + request: Hapi.Request; + h: Hapi.ResponseToolkit; + bundlesPath: string; + publicPath: string; + fileHashCache: FileHashCache; + replacePublicPath: boolean; + isDist: boolean; +}) { + let fd: number | undefined; + + try { + const path = resolve(bundlesPath, request.params.path); + + // prevent path traversal, only process paths that resolve within bundlesPath + if (!path.startsWith(bundlesPath)) { + throw Boom.forbidden(undefined, 'EACCES'); + } + + // we use and manage a file descriptor mostly because + // that's what Inert does, and since we are accessing + // the file 2 or 3 times per request it seems logical + fd = await asyncOpen(path, 'r'); + + const stat = await asyncFstat(fd); + const hash = isDist ? undefined : await getFileHash(fileHashCache, path, stat, fd); + + const read = Fs.createReadStream(null as any, { + fd, + start: 0, + autoClose: true, + }); + fd = undefined; // read stream is now responsible for fd + + const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; + + const response = h + .response(content) + .takeover() + .code(200) + .type(request.server.mime.path(path).type); + + if (isDist) { + response.header('cache-control', `max-age=${365 * DAY}`); + } else { + response.etag(`${hash}-${publicPath}`); + response.header('cache-control', 'must-revalidate'); + } + + return response; + } catch (error) { + if (fd) { + try { + await asyncClose(fd); + } catch (_) { + // ignore errors from close, we already have one to report + // and it's very likely they are the same + } + } + + if (error.code === 'ENOENT') { + throw Boom.notFound(); + } + + throw Boom.boomify(error); + } +} diff --git a/src/optimize/bundles_route/file_hash.js b/src/optimize/bundles_route/file_hash.js deleted file mode 100644 index d9464cf05eca1..0000000000000 --- a/src/optimize/bundles_route/file_hash.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { createHash } from 'crypto'; -import { createReadStream } from 'fs'; - -import * as Rx from 'rxjs'; -import { merge, mergeMap, takeUntil } from 'rxjs/operators'; - -/** - * Get the hash of a file via a file descriptor - * @param {LruCache} cache - * @param {string} path - * @param {Fs.Stat} stat - * @param {Fs.FileDescriptor} fd - * @return {Promise} - */ -export async function getFileHash(cache, path, stat, fd) { - const key = `${path}:${stat.ino}:${stat.size}:${stat.mtime.getTime()}`; - - const cached = cache.get(key); - if (cached) { - return await cached; - } - - const hash = createHash('sha1'); - const read = createReadStream(null, { - fd, - start: 0, - autoClose: false, - }); - - const promise = Rx.fromEvent(read, 'data') - .pipe( - merge(Rx.fromEvent(read, 'error').pipe(mergeMap(Rx.throwError))), - takeUntil(Rx.fromEvent(read, 'end')) - ) - .forEach(chunk => hash.update(chunk)) - .then(() => hash.digest('hex')) - .catch(error => { - // don't cache failed attempts - cache.del(key); - throw error; - }); - - cache.set(key, promise); - return await promise; -} diff --git a/src/optimize/bundles_route/file_hash.ts b/src/optimize/bundles_route/file_hash.ts new file mode 100644 index 0000000000000..7b0801098ed10 --- /dev/null +++ b/src/optimize/bundles_route/file_hash.ts @@ -0,0 +1,65 @@ +/* + * 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 { createHash } from 'crypto'; +import Fs from 'fs'; + +import * as Rx from 'rxjs'; +import { takeUntil, map } from 'rxjs/operators'; + +import { FileHashCache } from './file_hash_cache'; + +/** + * Get the hash of a file via a file descriptor + */ +export async function getFileHash(cache: FileHashCache, path: string, stat: Fs.Stats, fd: number) { + const key = `${path}:${stat.ino}:${stat.size}:${stat.mtime.getTime()}`; + + const cached = cache.get(key); + if (cached) { + return await cached; + } + + const hash = createHash('sha1'); + const read = Fs.createReadStream(null as any, { + fd, + start: 0, + autoClose: false, + }); + + const promise = Rx.merge( + Rx.fromEvent(read, 'data'), + Rx.fromEvent(read, 'error').pipe( + map(error => { + throw error; + }) + ) + ) + .pipe(takeUntil(Rx.fromEvent(read, 'end'))) + .forEach(chunk => hash.update(chunk)) + .then(() => hash.digest('hex')) + .catch(error => { + // don't cache failed attempts + cache.del(key); + throw error; + }); + + cache.set(key, promise); + return await promise; +} diff --git a/src/optimize/bundles_route/file_hash_cache.ts b/src/optimize/bundles_route/file_hash_cache.ts new file mode 100644 index 0000000000000..a7cdabbff13a7 --- /dev/null +++ b/src/optimize/bundles_route/file_hash_cache.ts @@ -0,0 +1,36 @@ +/* + * 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 LruCache from 'lru-cache'; + +export class FileHashCache { + private lru = new LruCache>(100); + + get(key: string) { + return this.lru.get(key); + } + + set(key: string, value: Promise) { + this.lru.set(key, value); + } + + del(key: string) { + this.lru.del(key); + } +} diff --git a/src/optimize/bundles_route/index.js b/src/optimize/bundles_route/index.ts similarity index 100% rename from src/optimize/bundles_route/index.js rename to src/optimize/bundles_route/index.ts diff --git a/src/optimize/bundles_route/proxy_bundles_route.js b/src/optimize/bundles_route/proxy_bundles_route.js deleted file mode 100644 index fff0ec444d95b..0000000000000 --- a/src/optimize/bundles_route/proxy_bundles_route.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 function createProxyBundlesRoute({ host, port }) { - return [ - buildProxyRouteForBundles('/bundles/', host, port), - buildProxyRouteForBundles('/built_assets/dlls/', host, port), - buildProxyRouteForBundles('/built_assets/css/', host, port), - ]; -} - -function buildProxyRouteForBundles(routePath, host, port) { - return { - path: `${routePath}{path*}`, - method: 'GET', - handler: { - proxy: { - host, - port, - passThrough: true, - xforward: true, - }, - }, - config: { auth: false }, - }; -} diff --git a/src/optimize/bundles_route/proxy_bundles_route.ts b/src/optimize/bundles_route/proxy_bundles_route.ts new file mode 100644 index 0000000000000..1d189054324a1 --- /dev/null +++ b/src/optimize/bundles_route/proxy_bundles_route.ts @@ -0,0 +1,50 @@ +/* + * 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 function createProxyBundlesRoute({ + host, + port, + buildHash, +}: { + host: string; + port: number; + buildHash: string; +}) { + return [ + buildProxyRouteForBundles(`/${buildHash}/bundles/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/dlls/`, host, port), + buildProxyRouteForBundles(`/${buildHash}/built_assets/css/`, host, port), + ]; +} + +function buildProxyRouteForBundles(routePath: string, host: string, port: number) { + return { + path: `${routePath}{path*}`, + method: 'GET', + handler: { + proxy: { + host, + port, + passThrough: true, + xforward: true, + }, + }, + config: { auth: false }, + }; +} diff --git a/src/optimize/index.js b/src/optimize/index.js index b7b9f7712358a..363f81a6a3a96 100644 --- a/src/optimize/index.js +++ b/src/optimize/index.js @@ -17,72 +17,5 @@ * under the License. */ -import FsOptimizer from './fs_optimizer'; -import { createBundlesRoute } from './bundles_route'; -import { DllCompiler } from './dynamic_dll_plugin'; -import { fromRoot } from '../core/server/utils'; -import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; - -export default async (kbnServer, server, config) => { - if (!config.get('optimize.enabled')) return; - - // the watch optimizer sets up two threads, one is the server listening - // on 5601 and the other is a server listening on 5602 that builds the - // bundles in a "middleware" style. - // - // the server listening on 5601 may be restarted a number of times, depending - // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/* - // requests to the other server. The server on 5602 is long running, in order - // to prevent complete rebuilds of the optimize content. - const watch = config.get('optimize.watch'); - if (watch) { - return await kbnServer.mixin(require('./watch/watch')); - } - - const { uiBundles } = kbnServer; - server.route( - createBundlesRoute({ - regularBundlesPath: uiBundles.getWorkingDir(), - dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, - basePublicPath: config.get('server.basePath'), - builtCssPath: fromRoot('built_assets/css'), - npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), - }) - ); - - // in prod, only bundle when something is missing or invalid - const reuseCache = config.get('optimize.useBundleCache') - ? await uiBundles.areAllBundleCachesValid() - : false; - - // we might not have any work to do - if (reuseCache) { - server.log(['debug', 'optimize'], `All bundles are cached and ready to go!`); - return; - } - - await uiBundles.resetBundleDir(); - - // only require the FsOptimizer when we need to - const optimizer = new FsOptimizer({ - logWithMetadata: (tags, message, metadata) => server.logWithMetadata(tags, message, metadata), - uiBundles, - profile: config.get('optimize.profile'), - sourceMaps: config.get('optimize.sourceMaps'), - workers: config.get('optimize.workers'), - }); - - server.log( - ['info', 'optimize'], - `Optimizing and caching ${uiBundles.getDescription()}. This may take a few minutes` - ); - - const start = Date.now(); - await optimizer.run(); - const seconds = ((Date.now() - start) / 1000).toFixed(2); - - server.log( - ['info', 'optimize'], - `Optimization of ${uiBundles.getDescription()} complete in ${seconds} seconds` - ); -}; +import { optimizeMixin } from './optimize_mixin'; +export default optimizeMixin; diff --git a/src/optimize/np_ui_plugin_public_dirs.js b/src/optimize/np_ui_plugin_public_dirs.js deleted file mode 100644 index de05fd2b863b8..0000000000000 --- a/src/optimize/np_ui_plugin_public_dirs.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 function getNpUiPluginPublicDirs(kbnServer) { - return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( - ([id, { publicTargetDir }]) => ({ - id, - path: publicTargetDir, - }) - ); -} - -export function isNpUiPluginPublicDirs(something) { - return ( - Array.isArray(something) && - something.every( - s => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' - ) - ); -} - -export function assertIsNpUiPluginPublicDirs(something) { - if (!isNpUiPluginPublicDirs(something)) { - throw new TypeError( - 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' - ); - } -} diff --git a/src/optimize/np_ui_plugin_public_dirs.ts b/src/optimize/np_ui_plugin_public_dirs.ts new file mode 100644 index 0000000000000..e7c3207948f6a --- /dev/null +++ b/src/optimize/np_ui_plugin_public_dirs.ts @@ -0,0 +1,50 @@ +/* + * 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 KbnServer from '../legacy/server/kbn_server'; + +export type NpUiPluginPublicDirs = Array<{ + id: string; + path: string; +}>; + +export function getNpUiPluginPublicDirs(kbnServer: KbnServer): NpUiPluginPublicDirs { + return Array.from(kbnServer.newPlatform.__internals.uiPlugins.internal.entries()).map( + ([id, { publicTargetDir }]) => ({ + id, + path: publicTargetDir, + }) + ); +} + +export function isNpUiPluginPublicDirs(x: any): x is NpUiPluginPublicDirs { + return ( + Array.isArray(x) && + x.every( + s => typeof s === 'object' && s && typeof s.id === 'string' && typeof s.path === 'string' + ) + ); +} + +export function assertIsNpUiPluginPublicDirs(x: any): asserts x is NpUiPluginPublicDirs { + if (!isNpUiPluginPublicDirs(x)) { + throw new TypeError( + 'npUiPluginPublicDirs must be an array of objects with string `id` and `path` properties' + ); + } +} diff --git a/src/optimize/optimize_mixin.ts b/src/optimize/optimize_mixin.ts new file mode 100644 index 0000000000000..9a3f08e2f667e --- /dev/null +++ b/src/optimize/optimize_mixin.ts @@ -0,0 +1,100 @@ +/* + * 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 Hapi from 'hapi'; + +// @ts-ignore not TS yet +import FsOptimizer from './fs_optimizer'; +import { createBundlesRoute } from './bundles_route'; +// @ts-ignore not TS yet +import { DllCompiler } from './dynamic_dll_plugin'; +import { fromRoot } from '../core/server/utils'; +import { getNpUiPluginPublicDirs } from './np_ui_plugin_public_dirs'; +import KbnServer, { KibanaConfig } from '../legacy/server/kbn_server'; + +export const optimizeMixin = async ( + kbnServer: KbnServer, + server: Hapi.Server, + config: KibanaConfig +) => { + if (!config.get('optimize.enabled')) return; + + // the watch optimizer sets up two threads, one is the server listening + // on 5601 and the other is a server listening on 5602 that builds the + // bundles in a "middleware" style. + // + // the server listening on 5601 may be restarted a number of times, depending + // on the watch setup managed by the cli. It proxies all bundles/* and built_assets/dlls/* + // requests to the other server. The server on 5602 is long running, in order + // to prevent complete rebuilds of the optimize content. + const watch = config.get('optimize.watch'); + if (watch) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return await kbnServer.mixin(require('./watch/watch')); + } + + const { uiBundles } = kbnServer; + server.route( + createBundlesRoute({ + regularBundlesPath: uiBundles.getWorkingDir(), + dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, + basePublicPath: config.get('server.basePath'), + builtCssPath: fromRoot('built_assets/css'), + npUiPluginPublicDirs: getNpUiPluginPublicDirs(kbnServer), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), + isDist: kbnServer.newPlatform.env.packageInfo.dist, + }) + ); + + // in prod, only bundle when something is missing or invalid + const reuseCache = config.get('optimize.useBundleCache') + ? await uiBundles.areAllBundleCachesValid() + : false; + + // we might not have any work to do + if (reuseCache) { + server.log(['debug', 'optimize'], `All bundles are cached and ready to go!`); + return; + } + + await uiBundles.resetBundleDir(); + + // only require the FsOptimizer when we need to + const optimizer = new FsOptimizer({ + logWithMetadata: server.logWithMetadata, + uiBundles, + profile: config.get('optimize.profile'), + sourceMaps: config.get('optimize.sourceMaps'), + workers: config.get('optimize.workers'), + }); + + server.log( + ['info', 'optimize'], + `Optimizing and caching ${uiBundles.getDescription()}. This may take a few minutes` + ); + + const start = Date.now(); + await optimizer.run(); + const seconds = ((Date.now() - start) / 1000).toFixed(2); + + server.log( + ['info', 'optimize'], + `Optimization of ${uiBundles.getDescription()} complete in ${seconds} seconds` + ); +}; diff --git a/src/optimize/public_path_placeholder.js b/src/optimize/public_path_placeholder.js deleted file mode 100644 index ef05d9e5ae704..0000000000000 --- a/src/optimize/public_path_placeholder.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 { createReplaceStream } from '../legacy/utils'; - -import * as Rx from 'rxjs'; -import { take, takeUntil } from 'rxjs/operators'; - -export const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; - -export function replacePlaceholder(read, replacement) { - const replace = createReplaceStream(PUBLIC_PATH_PLACEHOLDER, replacement); - - // handle errors on the read stream by proxying them - // to the replace stream so that the consumer can - // choose what to do with them. - Rx.fromEvent(read, 'error') - .pipe(take(1), takeUntil(Rx.fromEvent(read, 'end'))) - .forEach(error => { - replace.emit('error', error); - replace.end(); - }); - - replace.close = () => { - read.unpipe(); - - if (read.close) { - read.close(); - } - }; - - return read.pipe(replace); -} diff --git a/src/optimize/public_path_placeholder.ts b/src/optimize/public_path_placeholder.ts new file mode 100644 index 0000000000000..1ec2b4a431aa6 --- /dev/null +++ b/src/optimize/public_path_placeholder.ts @@ -0,0 +1,57 @@ +/* + * 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 Stream from 'stream'; +import Fs from 'fs'; + +import * as Rx from 'rxjs'; +import { take, takeUntil } from 'rxjs/operators'; +import { createReplaceStream } from '../legacy/utils'; + +export const PUBLIC_PATH_PLACEHOLDER = '__REPLACE_WITH_PUBLIC_PATH__'; + +interface ClosableTransform extends Stream.Transform { + close(): void; +} + +export function replacePlaceholder(read: Stream.Readable, replacement: string) { + const replace = createReplaceStream(PUBLIC_PATH_PLACEHOLDER, replacement); + + // handle errors on the read stream by proxying them + // to the replace stream so that the consumer can + // choose what to do with them. + Rx.fromEvent(read, 'error') + .pipe(take(1), takeUntil(Rx.fromEvent(read, 'end'))) + .forEach(error => { + replace.emit('error', error); + replace.end(); + }); + + const closableReplace: ClosableTransform = Object.assign(replace, { + close: () => { + read.unpipe(); + + if ('close' in read) { + (read as Fs.ReadStream).close(); + } + }, + }); + + return read.pipe(closableReplace); +} diff --git a/src/optimize/watch/optmzr_role.js b/src/optimize/watch/optmzr_role.js index a31ef7229e5da..1f6107996277c 100644 --- a/src/optimize/watch/optmzr_role.js +++ b/src/optimize/watch/optmzr_role.js @@ -49,7 +49,8 @@ export default async (kbnServer, kibanaHapiServer, config) => { config.get('optimize.watchPort'), config.get('server.basePath'), watchOptimizer, - getNpUiPluginPublicDirs(kbnServer) + getNpUiPluginPublicDirs(kbnServer), + kbnServer.newPlatform.env.packageInfo.buildNum.toString() ); watchOptimizer.status$.subscribe({ diff --git a/src/optimize/watch/proxy_role.js b/src/optimize/watch/proxy_role.js index 6093658ae1a2d..0f6f3b2d4b622 100644 --- a/src/optimize/watch/proxy_role.js +++ b/src/optimize/watch/proxy_role.js @@ -26,6 +26,7 @@ export default (kbnServer, server, config) => { createProxyBundlesRoute({ host: config.get('optimize.watchHost'), port: config.get('optimize.watchPort'), + buildHash: kbnServer.newPlatform.env.packageInfo.buildNum.toString(), }) ); diff --git a/src/optimize/watch/watch_optimizer.js b/src/optimize/watch/watch_optimizer.js index 6c20f21c7768e..cdff57a00c2e0 100644 --- a/src/optimize/watch/watch_optimizer.js +++ b/src/optimize/watch/watch_optimizer.js @@ -106,7 +106,7 @@ export default class WatchOptimizer extends BaseOptimizer { }); } - bindToServer(server, basePath, npUiPluginPublicDirs) { + bindToServer(server, basePath, npUiPluginPublicDirs, buildHash) { // pause all requests received while the compiler is running // and continue once an outcome is reached (aborting the request // with an error if it was a failure). @@ -118,6 +118,7 @@ export default class WatchOptimizer extends BaseOptimizer { server.route( createBundlesRoute({ npUiPluginPublicDirs: npUiPluginPublicDirs, + buildHash, regularBundlesPath: this.compiler.outputPath, dllBundlesPath: DllCompiler.getRawDllConfig().outputPath, basePublicPath: basePath, diff --git a/src/optimize/watch/watch_server.js b/src/optimize/watch/watch_server.js index 74a96dc8aea6e..81e04a5b83956 100644 --- a/src/optimize/watch/watch_server.js +++ b/src/optimize/watch/watch_server.js @@ -21,10 +21,11 @@ import { Server } from 'hapi'; import { registerHapiPlugins } from '../../legacy/server/http/register_hapi_plugins'; export default class WatchServer { - constructor(host, port, basePath, optimizer, npUiPluginPublicDirs) { + constructor(host, port, basePath, optimizer, npUiPluginPublicDirs, buildHash) { this.basePath = basePath; this.optimizer = optimizer; this.npUiPluginPublicDirs = npUiPluginPublicDirs; + this.buildHash = buildHash; this.server = new Server({ host: host, port: port, @@ -35,7 +36,12 @@ export default class WatchServer { async init() { await this.optimizer.init(); - this.optimizer.bindToServer(this.server, this.basePath, this.npUiPluginPublicDirs); + this.optimizer.bindToServer( + this.server, + this.basePath, + this.npUiPluginPublicDirs, + this.buildHash + ); await this.server.start(); } } diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index cac9a6daa8df8..e6ca6e797ba45 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -1,7 +1,7 @@ { "id": "advancedSettings", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["management"] } diff --git a/src/plugins/advanced_settings/server/capabilities_provider.ts b/src/plugins/advanced_settings/server/capabilities_provider.ts new file mode 100644 index 0000000000000..083d5f3ffced4 --- /dev/null +++ b/src/plugins/advanced_settings/server/capabilities_provider.ts @@ -0,0 +1,25 @@ +/* + * 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 const capabilitiesProvider = () => ({ + advancedSettings: { + show: true, + save: true, + }, +}); diff --git a/src/plugins/advanced_settings/server/index.ts b/src/plugins/advanced_settings/server/index.ts new file mode 100644 index 0000000000000..ffcf7cd49a8c3 --- /dev/null +++ b/src/plugins/advanced_settings/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { AdvancedSettingsServerPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new AdvancedSettingsServerPlugin(initContext); diff --git a/src/plugins/advanced_settings/server/plugin.ts b/src/plugins/advanced_settings/server/plugin.ts new file mode 100644 index 0000000000000..4d7bd34259819 --- /dev/null +++ b/src/plugins/advanced_settings/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; + +export class AdvancedSettingsServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('advancedSettings: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('advancedSettings: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/dashboard/common/bwc/types.ts b/src/plugins/dashboard/common/bwc/types.ts new file mode 100644 index 0000000000000..2427799345463 --- /dev/null +++ b/src/plugins/dashboard/common/bwc/types.ts @@ -0,0 +1,149 @@ +/* + * 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 { SavedObjectReference } from 'kibana/public'; + +import { GridData } from '../'; + +interface SavedObjectAttributes { + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; +} + +interface Doc { + references: SavedObjectReference[]; + attributes: Attributes; + id: string; + type: string; +} + +interface DocPre700 { + attributes: Attributes; + id: string; + type: string; +} + +interface DashboardAttributes extends SavedObjectAttributes { + panelsJSON: string; + description: string; + version: number; + timeRestore: boolean; + useMargins?: boolean; + title: string; + optionsJSON?: string; +} + +interface DashboardAttributesTo720 extends SavedObjectAttributes { + panelsJSON: string; + description: string; + uiStateJSON: string; + version: number; + timeRestore: boolean; + useMargins?: boolean; + title: string; + optionsJSON?: string; +} + +export type DashboardDoc730ToLatest = Doc; + +export type DashboardDoc700To720 = Doc; + +export type DashboardDocPre700 = DocPre700; + +// Note that these types are prefixed with `Raw` because there are some post processing steps +// that happen before the saved objects even reach the client. Namely, injecting type and id +// parameters back into the panels, where the raw saved objects actually have them stored elsewhere. +// +// Ideally, everywhere in the dashboard code would use references at the top level instead of +// embedded in the panels. The reason this is stored at the top level is so the references can be uniformly +// updated across all saved object types that have references. + +// Starting in 7.3 we introduced the possibility of embeddables existing without an id +// parameter. If there was no id, then type remains on the panel. So it either will have a name, +// or a type property. +export type RawSavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + // Should be either type, and not name (not backed by a saved object), or name but not type (backed by a + // saved object and type and id are stored on references). Had trouble with oring the two types + // because of optional properties being marked as required: https://github.com/microsoft/TypeScript/issues/20722 + readonly type?: string; + readonly name?: string; + + panelIndex: string; +}; + +// NOTE!! +// All of these types can actually exist in 7.2! The names are pretty confusing because we did +// in place migrations for so long. For example, `RawSavedDashboardPanelTo60` is what a panel +// created in 6.0 will look like after it's been migrated up to 7.2, *not* what it would look like in 6.0. +// That's why it actually doesn't have id or type, but has a name property, because that was a migration +// added in 7.0. + +// Hopefully since we finally have a formal saved object migration system and we can do less in place +// migrations, this will be easier to understand moving forward. + +// Starting in 6.4 we added an in-place edit on panels to remove columns and sort properties and put them +// inside the embeddable config (https://github.com/elastic/kibana/pull/17446). +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in +// this shape in v 7.2. +export type RawSavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel630, + Exclude +>; + +// In 6.3.0 we expanded the number of grid columns and rows: https://github.com/elastic/kibana/pull/16763 +// We added in-place migrations to multiply older x,y,h,w numbers. Note the typescript shape here is the same +// because it's just multiplying existing fields. +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in 7.2 +// that need to be modified. +export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620; + +// In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property. +// Source: https://github.com/elastic/kibana/pull/14949 +export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & { + embeddableConfig: { [key: string]: unknown }; + version: string; +}; + +// In 6.1 we switched from an angular grid to react grid layout (https://github.com/elastic/kibana/pull/13853) +// This used gridData instead of size_x, size_y, row and col. We also started tracking the version this panel +// was created in to make future migrations easier. +// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in +// this shape in v 7.2. +export type RawSavedDashboardPanel610 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { gridData: GridData; version: string }; + +export interface RawSavedDashboardPanelTo60 { + readonly columns?: string[]; + readonly sort?: string; + readonly size_x?: number; + readonly size_y?: number; + readonly row: number; + readonly col: number; + panelIndex?: number | string; // earlier versions allowed this to be number or string. Some very early versions seem to be missing this entirely + readonly name: string; + + // This is where custom panel titles are stored prior to Embeddable API v2 + title?: string; +} diff --git a/src/plugins/dashboard/common/embeddable/types.ts b/src/plugins/dashboard/common/embeddable/types.ts new file mode 100644 index 0000000000000..eb76d73af7a58 --- /dev/null +++ b/src/plugins/dashboard/common/embeddable/types.ts @@ -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 interface GridData { + w: number; + h: number; + x: number; + y: number; + i: string; +} diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts new file mode 100644 index 0000000000000..e3f3f629ae5d0 --- /dev/null +++ b/src/plugins/dashboard/common/index.ts @@ -0,0 +1,36 @@ +/* + * 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 { GridData } from './embeddable/types'; +export { + RawSavedDashboardPanel730ToLatest, + DashboardDoc730ToLatest, + DashboardDoc700To720, + DashboardDocPre700, +} from './bwc/types'; +export { + SavedDashboardPanelTo60, + SavedDashboardPanel610, + SavedDashboardPanel620, + SavedDashboardPanel630, + SavedDashboardPanel640To720, + SavedDashboardPanel730ToLatest, +} from './types'; + +export { migratePanelsTo730 } from './migrate_to_730_panels'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts b/src/plugins/dashboard/common/migrate_to_730_panels.test.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts rename to src/plugins/dashboard/common/migrate_to_730_panels.test.ts index 4dd71fd8ee5f4..0867909225ddb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts +++ b/src/plugins/dashboard/common/migrate_to_730_panels.test.ts @@ -19,15 +19,12 @@ import { migratePanelsTo730 } from './migrate_to_730_panels'; import { RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, RawSavedDashboardPanel630, RawSavedDashboardPanel640To720, - DEFAULT_PANEL_WIDTH, - DEFAULT_PANEL_HEIGHT, - SavedDashboardPanelTo60, - SavedDashboardPanel730ToLatest, -} from '../../../../../../plugins/dashboard/public'; + RawSavedDashboardPanel610, + RawSavedDashboardPanel620, +} from './bwc/types'; +import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from './types'; test('6.0 migrates uiState, sort, scales, and gridData', async () => { const uiState = { @@ -96,8 +93,8 @@ test('6.0 migration gives default width and height when missing', () => { }, ]; const newPanels = migratePanelsTo730(panels, '8.0.0', true); - expect(newPanels[0].gridData.w).toBe(DEFAULT_PANEL_WIDTH); - expect(newPanels[0].gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(newPanels[0].gridData.w).toBe(24); + expect(newPanels[0].gridData.h).toBe(15); expect(newPanels[0].version).toBe('8.0.0'); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts b/src/plugins/dashboard/common/migrate_to_730_panels.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts rename to src/plugins/dashboard/common/migrate_to_730_panels.ts index a19c861f092d5..b89345f0a872c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts +++ b/src/plugins/dashboard/common/migrate_to_730_panels.ts @@ -21,17 +21,19 @@ import semver from 'semver'; import uuid from 'uuid'; import { GridData, + SavedDashboardPanelTo60, + SavedDashboardPanel620, + SavedDashboardPanel630, + SavedDashboardPanel610, +} from './'; +import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel630, RawSavedDashboardPanel640To720, RawSavedDashboardPanel730ToLatest, RawSavedDashboardPanel610, RawSavedDashboardPanel620, - SavedDashboardPanelTo60, - SavedDashboardPanel620, - SavedDashboardPanel630, - SavedDashboardPanel610, -} from '../../../../../../plugins/dashboard/public'; +} from './bwc/types'; const PANEL_HEIGHT_SCALE_FACTOR = 5; const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4; @@ -92,7 +94,7 @@ function migratePre61PanelToLatest( ): RawSavedDashboardPanel730ToLatest { if (panel.col === undefined || panel.row === undefined) { throw new Error( - i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', { + i18n.translate('dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', { defaultMessage: 'Unable to migrate panel data for "6.1.0" backwards compatibility, panel does not contain expected col and/or row fields', }) @@ -151,7 +153,7 @@ function migrate610PanelToLatest( (['w', 'x', 'h', 'y'] as Array).forEach(key => { if (panel.gridData[key] === undefined) { throw new Error( - i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', { + i18n.translate('dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', { defaultMessage: 'Unable to migrate panel data for "6.3.0" backwards compatibility, panel does not contain expected field: {key}', values: { key }, diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts new file mode 100644 index 0000000000000..7cc82a9173976 --- /dev/null +++ b/src/plugins/dashboard/common/types.ts @@ -0,0 +1,76 @@ +/* + * 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 { + RawSavedDashboardPanelTo60, + RawSavedDashboardPanel610, + RawSavedDashboardPanel620, + RawSavedDashboardPanel630, + RawSavedDashboardPanel640To720, + RawSavedDashboardPanel730ToLatest, +} from './bwc/types'; + +export type SavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel630 = Pick< + RawSavedDashboardPanel630, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel620 = Pick< + RawSavedDashboardPanel620, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel610 = Pick< + RawSavedDashboardPanel610, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanelTo60 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +// id becomes optional starting in 7.3.0 +export type SavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel730ToLatest, + Exclude +> & { + readonly id?: string; + readonly type: string; +}; diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 9bcd999c2dcc0..4cd8f3c7d981f 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -11,6 +11,6 @@ "savedObjects" ], "optionalPlugins": ["home", "share", "usageCollection"], - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap index 7210879c5eacc..1bc85fa110ca0 100644 --- a/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -304,13 +304,13 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` url="/plugins/kibana/home/assets/welcome_graphic_light_2x.png" >

@@ -998,13 +998,13 @@ exports[`DashboardEmptyScreen renders correctly without visualize paragraph 1`] url="/plugins/kibana/home/assets/welcome_graphic_light_2x.png" >
diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index 3134a5bfe2c67..37f014d836075 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -21,6 +21,8 @@ import './index.scss'; import { EuiIcon } from '@elastic/eui'; import angular, { IModule } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext, @@ -38,12 +40,7 @@ import { EmbeddableStart } from '../../../embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../navigation/public'; import { DataPublicPluginStart } from '../../../data/public'; import { SharePluginStart } from '../../../share/public'; -import { - KibanaLegacyStart, - configureAppAngularModule, - createTopNavDirective, - createTopNavHelper, -} from '../../../kibana_legacy/public'; +import { KibanaLegacyStart, configureAppAngularModule } from '../../../kibana_legacy/public'; import { SavedObjectLoader } from '../../../saved_objects/public'; export interface RenderDeps { @@ -114,13 +111,11 @@ function mountDashboardApp(appBasePath: string, element: HTMLElement) { function createLocalAngularModule(core: AppMountContext['core'], navigation: NavigationStart) { createLocalI18nModule(); - createLocalTopNavModule(navigation); createLocalIconModule(); const dashboardAngularModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, 'app/dashboard/I18n', - 'app/dashboard/TopNav', 'app/dashboard/icon', ]); return dashboardAngularModule; @@ -132,13 +127,6 @@ function createLocalIconModule() { .directive('icon', reactDirective => reactDirective(EuiIcon)); } -function createLocalTopNavModule(navigation: NavigationStart) { - angular - .module('app/dashboard/TopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); -} - function createLocalI18nModule() { angular .module('app/dashboard/I18n', []) diff --git a/src/plugins/dashboard/public/application/dashboard_app.html b/src/plugins/dashboard/public/application/dashboard_app.html index 3cf8932958b6d..87a5728ac2059 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.html +++ b/src/plugins/dashboard/public/application/dashboard_app.html @@ -2,52 +2,7 @@ class="app-container dshAppContainer" ng-class="{'dshAppContainer--withMargins': model.useMargins}" > - - - - - - - - +

{{screenTitle}}

diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 150cd8f8fcbb5..f101935b9288d 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -33,7 +33,6 @@ import { SavedObjectDashboard } from '../saved_dashboards'; export interface DashboardAppScope extends ng.IScope { dash: SavedObjectDashboard; appState: DashboardAppState; - screenTitle: string; model: { query: Query; filters: Filter[]; @@ -54,21 +53,7 @@ export interface DashboardAppScope extends ng.IScope { getShouldShowEditHelp: () => boolean; getShouldShowViewHelp: () => boolean; updateQueryAndFetch: ({ query, dateRange }: { query: Query; dateRange?: TimeRange }) => void; - onRefreshChange: ({ - isPaused, - refreshInterval, - }: { - isPaused: boolean; - refreshInterval: any; - }) => void; - onFiltersUpdated: (filters: Filter[]) => void; - onCancelApplyFilters: () => void; - onApplyFilters: (filters: Filter[]) => void; - onQuerySaved: (savedQuery: SavedQuery) => void; - onSavedQueryUpdated: (savedQuery: SavedQuery) => void; - onClearSavedQuery: () => void; topNavMenu: any; - showFilterBar: () => boolean; showAddPanel: any; showSaveQuery: boolean; kbnTopNav: any; diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 283fe9f0a83a4..fa2f06bfcdcdd 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -21,12 +21,15 @@ import _, { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import React from 'react'; +import ReactDOM from 'react-dom'; import angular from 'angular'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { History } from 'history'; import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; +import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; +import { TimeRange } from 'src/plugins/data/public'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; import { @@ -87,6 +90,7 @@ export interface DashboardAppControllerDependencies extends RenderDeps { dashboardConfig: KibanaLegacyStart['dashboardConfig']; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; + navigation: NavigationStart; } export class DashboardAppController { @@ -123,10 +127,13 @@ export class DashboardAppController { history, kbnUrlStateStorage, usageCollection, + navigation, }: DashboardAppControllerDependencies) { const filterManager = queryService.filterManager; const queryFilter = filterManager; const timefilter = queryService.timefilter.timefilter; + let showSearchBar = true; + let showQueryBar = true; let lastReloadRequestTime = 0; const dash = ($scope.dash = $route.current.locals.dash); @@ -243,6 +250,9 @@ export class DashboardAppController { } }; + const showFilterBar = () => + $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); + const getEmptyScreenProps = ( shouldShowEditHelp: boolean, isEmptyInReadOnlyMode: boolean @@ -310,7 +320,6 @@ export class DashboardAppController { refreshInterval: timefilter.getRefreshInterval(), }; $scope.panels = dashboardStateManager.getPanels(); - $scope.screenTitle = dashboardStateManager.getTitle(); }; updateState(); @@ -515,49 +524,8 @@ export class DashboardAppController { } }; - $scope.onRefreshChange = function({ isPaused, refreshInterval }) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value, - }); - }; - - $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); - }; - - $scope.onQuerySaved = savedQuery => { - $scope.savedQuery = savedQuery; - }; - - $scope.onSavedQueryUpdated = savedQuery => { - $scope.savedQuery = { ...savedQuery }; - }; - - $scope.onClearSavedQuery = () => { - delete $scope.savedQuery; - dashboardStateManager.setSavedQueryId(undefined); - dashboardStateManager.applyFilters( - { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }, - queryFilter.getGlobalFilters() - ); - // Making this method sync broke the updates. - // Temporary fix, until we fix the complex state in this file. - setTimeout(() => { - queryFilter.setFilters(queryFilter.getGlobalFilters()); - }, 0); - }; - const updateStateFromSavedQuery = (savedQuery: SavedQuery) => { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = queryFilter.getGlobalFilters(); - const allFilters = [...globalFilters, ...savedQueryFilters]; - + const allFilters = filterManager.getFilters(); dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters); if (savedQuery.attributes.timefilter) { timefilter.setTime({ @@ -616,6 +584,48 @@ export class DashboardAppController { } ); + const onSavedQueryIdChange = (savedQueryId?: string) => { + dashboardStateManager.setSavedQueryId(savedQueryId); + }; + + const getNavBarProps = () => { + const isFullScreenMode = dashboardStateManager.getFullScreenMode(); + const screenTitle = dashboardStateManager.getTitle(); + return { + appName: 'dashboard', + config: $scope.isVisible ? $scope.topNavMenu : undefined, + className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined, + screenTitle, + showSearchBar, + showQueryBar, + showFilterBar: showFilterBar(), + indexPatterns: $scope.indexPatterns, + showSaveQuery: $scope.showSaveQuery, + query: $scope.model.query, + savedQuery: $scope.savedQuery, + onSavedQueryIdChange, + savedQueryId: dashboardStateManager.getSavedQueryId(), + useDefaultBehaviors: true, + onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }): void => { + if (!payload.query) { + $scope.updateQueryAndFetch({ query: $scope.model.query, dateRange: payload.dateRange }); + } else { + $scope.updateQueryAndFetch({ query: payload.query, dateRange: payload.dateRange }); + } + }, + }; + }; + const dashboardNavBar = document.getElementById('dashboardChrome'); + const updateNavBar = () => { + ReactDOM.render(, dashboardNavBar); + }; + + const unmountNavBar = () => { + if (dashboardNavBar) { + ReactDOM.unmountComponentAtNode(dashboardNavBar); + } + }; + $scope.timefilterSubscriptions$ = new Subscription(); $scope.timefilterSubscriptions$.add( @@ -707,6 +717,8 @@ export class DashboardAppController { revertChangesAndExitEditMode(); } }); + + updateNavBar(); }; /** @@ -761,9 +773,6 @@ export class DashboardAppController { }); } - $scope.showFilterBar = () => - $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); - $scope.showAddPanel = () => { dashboardStateManager.setFullScreenMode(false); /* @@ -785,7 +794,11 @@ export class DashboardAppController { const navActions: { [key: string]: NavAction; } = {}; - navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true); + navActions[TopNavIds.FULL_SCREEN] = () => { + dashboardStateManager.setFullScreenMode(true); + showQueryBar = false; + updateNavBar(); + }; navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW); navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(ViewMode.EDIT); navActions[TopNavIds.SAVE] = () => { @@ -858,6 +871,7 @@ export class DashboardAppController { if ((response as { error: Error }).error) { dashboardStateManager.setTitle(currentTitle); } + updateNavBar(); return response; }); }; @@ -939,6 +953,9 @@ export class DashboardAppController { const visibleSubscription = chrome.getIsVisible$().subscribe(isVisible => { $scope.$evalAsync(() => { $scope.isVisible = isVisible; + showSearchBar = isVisible || showFilterBar(); + showQueryBar = !dashboardStateManager.getFullScreenMode() && isVisible; + updateNavBar(); }); }); @@ -949,9 +966,17 @@ export class DashboardAppController { navActions, dashboardConfig.getHideWriteControls() ); + updateNavBar(); + }); + + $scope.$watch('indexPatterns', () => { + updateNavBar(); }); $scope.$on('$destroy', () => { + // we have to unmount nav bar manually to make sure all internal subscriptions are unsubscribed + unmountNavBar(); + updateSubscription.unsubscribe(); stopSyncingQueryServiceStateWithUrl(); stopSyncingAppFilters(); diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index b15a813aff903..f8632011002d0 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -29,9 +29,10 @@ import _ from 'lodash'; import React from 'react'; import { Subscription } from 'rxjs'; import ReactGridLayout, { Layout } from 'react-grid-layout'; +import { GridData } from '../../../../common'; import { ViewMode, EmbeddableChildPanel } from '../../../embeddable_plugin'; import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; -import { DashboardPanelState, GridData } from '../types'; +import { DashboardPanelState } from '../types'; import { withKibana } from '../../../../../kibana_react/public'; import { DashboardContainerInput } from '../dashboard_container'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; @@ -274,6 +275,7 @@ class DashboardGridUi extends React.Component { getEmbeddableFactory={this.props.kibana.services.embeddable.getEmbeddableFactory} getAllEmbeddableFactories={this.props.kibana.services.embeddable.getEmbeddableFactories} overlays={this.props.kibana.services.overlays} + application={this.props.kibana.services.application} notifications={this.props.kibana.services.notifications} inspector={this.props.kibana.services.inspector} SavedObjectFinder={this.props.kibana.services.SavedObjectFinder} diff --git a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts index 70a6c83418587..b95b7f394a27d 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts @@ -18,7 +18,8 @@ */ import { PanelNotFoundError } from '../../../embeddable_plugin'; -import { DashboardPanelState, GridData, DASHBOARD_GRID_COLUMN_COUNT } from '..'; +import { GridData } from '../../../../common'; +import { DashboardPanelState, DASHBOARD_GRID_COLUMN_COUNT } from '..'; export type PanelPlacementMethod = ( args: PlacementArgs diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts index 30a93989649a7..b3ce2f1e57d5f 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts @@ -33,6 +33,10 @@ export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition return false; } + public canCreateNew() { + return false; + } + public async create(initialInput: EmbeddableInput, parent?: IContainer) { return new PlaceholderEmbeddable(initialInput, parent); } diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 6d0221cb10e8b..66cdd22ed6bd4 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -17,18 +17,11 @@ * under the License. */ import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; +import { GridData } from '../../../common'; import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; export type PanelId = string; export type SavedObjectId = string; -export interface GridData { - w: number; - h: number; - x: number; - y: number; - i: string; -} - export interface DashboardPanelState< TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput > extends PanelState { diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 10243dbf2f979..31225530b10b9 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -28,7 +28,6 @@ import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants'; import { createKbnUrlStateStorage, - ensureDefaultIndexPattern, redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, @@ -138,7 +137,7 @@ export function initDashboardApp(app, deps) { }, resolve: { dash: function($route, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, history).then(() => { + return deps.data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; if (title) { @@ -173,7 +172,8 @@ export function initDashboardApp(app, deps) { requireUICapability: 'dashboard.createNew', resolve: { dash: history => - ensureDefaultIndexPattern(deps.core, deps.data, history) + deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get()) .catch( redirectWhenMissing({ @@ -194,7 +194,8 @@ export function initDashboardApp(app, deps) { dash: function($route, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.data, history) + return deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get(id)) .then(savedDashboard => { deps.chrome.recentlyAccessed.add( diff --git a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts index 8f8de3663518a..f4d97578adebf 100644 --- a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts +++ b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts @@ -22,18 +22,16 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { DashboardAppState, SavedDashboardPanel } from '../../types'; import { - DashboardAppState, + migratePanelsTo730, SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest, SavedDashboardPanel610, SavedDashboardPanel630, SavedDashboardPanel640To720, SavedDashboardPanel620, - SavedDashboardPanel, -} from '../../types'; -// should be moved in src/plugins/dashboard/common right after https://github.com/elastic/kibana/pull/61895 is merged -import { migratePanelsTo730 } from '../../../../../legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels'; +} from '../../../common'; /** * Attempts to migrate the state stored in the URL into the latest version of it. diff --git a/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts b/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts index 57c147ffe3588..ee59c68cce451 100644 --- a/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts +++ b/src/plugins/dashboard/public/application/test_helpers/get_saved_dashboard_mock.ts @@ -17,17 +17,19 @@ * under the License. */ -import { searchSourceMock } from '../../../../data/public/mocks'; +import { dataPluginMock } from '../../../../data/public/mocks'; import { SavedObjectDashboard } from '../../saved_dashboards'; export function getSavedDashboardMock( config?: Partial ): SavedObjectDashboard { + const searchSource = dataPluginMock.createStartContract(); + return { id: '123', title: 'my dashboard', panelsJSON: '[]', - searchSource: searchSourceMock, + searchSource: searchSource.search.searchSource.create(), copyOnSave: false, timeRestore: false, timeTo: 'now', diff --git a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx index 836cea298f035..5dab21ff671b4 100644 --- a/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/tests/dashboard_container.test.tsx @@ -84,6 +84,7 @@ test('DashboardContainer in edit mode shows edit mode actions', async () => { getAllEmbeddableFactories={(() => []) as any} getEmbeddableFactory={(() => null) as any} notifications={{} as any} + application={{} as any} overlays={{} as any} inspector={inspector} SavedObjectFinder={() => null} diff --git a/src/plugins/dashboard/public/bwc/index.ts b/src/plugins/dashboard/public/bwc/index.ts deleted file mode 100644 index d8f7b5091eb8f..0000000000000 --- a/src/plugins/dashboard/public/bwc/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 * from './types'; diff --git a/src/plugins/dashboard/public/bwc/types.ts b/src/plugins/dashboard/public/bwc/types.ts deleted file mode 100644 index d5655e525e9bd..0000000000000 --- a/src/plugins/dashboard/public/bwc/types.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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 { SavedObjectReference } from 'kibana/public'; -import { GridData } from '../application'; - -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} - -export interface Doc { - references: SavedObjectReference[]; - attributes: Attributes; - id: string; - type: string; -} - -export interface DocPre700 { - attributes: Attributes; - id: string; - type: string; -} - -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} - -interface DashboardAttributes extends SavedObjectAttributes { - panelsJSON: string; - description: string; - version: number; - timeRestore: boolean; - useMargins?: boolean; - title: string; - optionsJSON?: string; -} - -export type DashboardAttributes730ToLatest = DashboardAttributes; - -interface DashboardAttributesTo720 extends SavedObjectAttributes { - panelsJSON: string; - description: string; - uiStateJSON: string; - version: number; - timeRestore: boolean; - useMargins?: boolean; - title: string; - optionsJSON?: string; -} - -export type DashboardDoc730ToLatest = Doc; - -export type DashboardDoc700To720 = Doc; - -export type DashboardDocPre700 = DocPre700; - -// Note that these types are prefixed with `Raw` because there are some post processing steps -// that happen before the saved objects even reach the client. Namely, injecting type and id -// parameters back into the panels, where the raw saved objects actually have them stored elsewhere. -// -// Ideally, everywhere in the dashboard code would use references at the top level instead of -// embedded in the panels. The reason this is stored at the top level is so the references can be uniformly -// updated across all saved object types that have references. - -// Starting in 7.3 we introduced the possibility of embeddables existing without an id -// parameter. If there was no id, then type remains on the panel. So it either will have a name, -// or a type property. -export type RawSavedDashboardPanel730ToLatest = Pick< - RawSavedDashboardPanel640To720, - Exclude -> & { - // Should be either type, and not name (not backed by a saved object), or name but not type (backed by a - // saved object and type and id are stored on references). Had trouble with oring the two types - // because of optional properties being marked as required: https://github.com/microsoft/TypeScript/issues/20722 - readonly type?: string; - readonly name?: string; - - panelIndex: string; -}; - -// NOTE!! -// All of these types can actually exist in 7.2! The names are pretty confusing because we did -// in place migrations for so long. For example, `RawSavedDashboardPanelTo60` is what a panel -// created in 6.0 will look like after it's been migrated up to 7.2, *not* what it would look like in 6.0. -// That's why it actually doesn't have id or type, but has a name property, because that was a migration -// added in 7.0. - -// Hopefully since we finally have a formal saved object migration system and we can do less in place -// migrations, this will be easier to understand moving forward. - -// Starting in 6.4 we added an in-place edit on panels to remove columns and sort properties and put them -// inside the embeddable config (https://github.com/elastic/kibana/pull/17446). -// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in -// this shape in v 7.2. -export type RawSavedDashboardPanel640To720 = Pick< - RawSavedDashboardPanel630, - Exclude ->; - -// In 6.3.0 we expanded the number of grid columns and rows: https://github.com/elastic/kibana/pull/16763 -// We added in-place migrations to multiply older x,y,h,w numbers. Note the typescript shape here is the same -// because it's just multiplying existing fields. -// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in 7.2 -// that need to be modified. -export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620; - -// In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property. -// Source: https://github.com/elastic/kibana/pull/14949 -export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & { - embeddableConfig: { [key: string]: unknown }; - version: string; -}; - -// In 6.1 we switched from an angular grid to react grid layout (https://github.com/elastic/kibana/pull/13853) -// This used gridData instead of size_x, size_y, row and col. We also started tracking the version this panel -// was created in to make future migrations easier. -// Note that this was not added as a saved object migration until 7.3, so there can still exist panels in -// this shape in v 7.2. -export type RawSavedDashboardPanel610 = Pick< - RawSavedDashboardPanelTo60, - Exclude -> & { gridData: GridData; version: string }; - -export interface RawSavedDashboardPanelTo60 { - readonly columns?: string[]; - readonly sort?: string; - readonly size_x?: number; - readonly size_y?: number; - readonly row: number; - readonly col: number; - panelIndex?: number | string; // earlier versions allowed this to be number or string. Some very early versions seem to be missing this entirely - readonly name: string; - - // This is where custom panel titles are stored prior to Embeddable API v2 - title?: string; -} diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index ca0ea0293b07c..44733499cdcba 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -20,29 +20,6 @@ import { PluginInitializerContext } from '../../../core/public'; import { DashboardPlugin } from './plugin'; -/** - * These types can probably be internal once all of dashboard app is migrated into this plugin. Right - * now, migrations are still in legacy land. - */ -export { - DashboardDoc730ToLatest, - DashboardDoc700To720, - RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, - RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, - DashboardDocPre700, -} from './bwc'; -export { - SavedDashboardPanelTo60, - SavedDashboardPanel610, - SavedDashboardPanel620, - SavedDashboardPanel630, - SavedDashboardPanel730ToLatest, -} from './types'; - export { DashboardContainer, DashboardContainerInput, @@ -51,7 +28,6 @@ export { // Types below here can likely be made private when dashboard app moved into this NP plugin. DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT, - GridData, } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 203c784d9df4e..7de054f2eaa9c 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(), + }; + }) ); } @@ -251,6 +255,8 @@ export class DashboardPlugin localStorage: new Storage(localStorage), usageCollection, }; + // make sure the index pattern list is up to date + await dataStart.indexPatterns.clearCache(); const { renderApp } = await import('./application/application'); const unmount = renderApp(params.element, params.appBasePath, deps); return () => { diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index d96d2cdf75626..21c6bbc1bfc51 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,14 +19,7 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { - RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, - RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, -} from './bwc'; +import { SavedDashboardPanel730ToLatest } from '../common'; import { ViewMode } from './embeddable_plugin'; export interface DashboardCapabilities { @@ -83,55 +76,6 @@ export type NavAction = (anchorElement?: any) => void; */ export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; -// id becomes optional starting in 7.3.0 -export type SavedDashboardPanel730ToLatest = Pick< - RawSavedDashboardPanel730ToLatest, - Exclude -> & { - readonly id?: string; - readonly type: string; -}; - -export type SavedDashboardPanel640To720 = Pick< - RawSavedDashboardPanel640To720, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel630 = Pick< - RawSavedDashboardPanel630, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel620 = Pick< - RawSavedDashboardPanel620, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel610 = Pick< - RawSavedDashboardPanel610, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanelTo60 = Pick< - RawSavedDashboardPanelTo60, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index d48aacc1d8c1e..248a3f991d6cb 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 0fdf395e75bca..6f121ceb2d373 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 }, diff --git a/src/plugins/dashboard/server/capabilities_provider.ts b/src/plugins/dashboard/server/capabilities_provider.ts new file mode 100644 index 0000000000000..0bb53d60c38a5 --- /dev/null +++ b/src/plugins/dashboard/server/capabilities_provider.ts @@ -0,0 +1,27 @@ +/* + * 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 const capabilitiesProvider = () => ({ + dashboard: { + createNew: true, + show: true, + showWriteControls: true, + saveQuery: true, + }, +}); diff --git a/src/plugins/dashboard/server/index.ts b/src/plugins/dashboard/server/index.ts new file mode 100644 index 0000000000000..9719586001c59 --- /dev/null +++ b/src/plugins/dashboard/server/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/server'; +import { DashboardPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new DashboardPlugin(initializerContext); +} + +export { DashboardPluginSetup, DashboardPluginStart } from './types'; diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts new file mode 100644 index 0000000000000..ba7bdeeda0133 --- /dev/null +++ b/src/plugins/dashboard/server/plugin.ts @@ -0,0 +1,55 @@ +/* + * 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 { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { dashboardSavedObjectType } from './saved_objects'; +import { capabilitiesProvider } from './capabilities_provider'; + +import { DashboardPluginSetup, DashboardPluginStart } from './types'; + +export class DashboardPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('dashboard: Setup'); + + core.savedObjects.registerType(dashboardSavedObjectType); + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('dashboard: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts new file mode 100644 index 0000000000000..65d5a4021f962 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -0,0 +1,67 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; + +export const dashboardSavedObjectType: SavedObjectsType = { + name: 'dashboard', + hidden: false, + namespaceType: 'single', + management: { + icon: 'dashboardApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'dashboard.show', + }; + }, + }, + mappings: { + properties: { + description: { type: 'text' }, + hits: { type: 'integer' }, + kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, + optionsJSON: { type: 'text' }, + panelsJSON: { type: 'text' }, + refreshInterval: { + properties: { + display: { type: 'keyword' }, + pause: { type: 'boolean' }, + section: { type: 'integer' }, + value: { type: 'integer' }, + }, + }, + timeFrom: { type: 'keyword' }, + timeRestore: { type: 'boolean' }, + timeTo: { type: 'keyword' }, + title: { type: 'text' }, + version: { type: 'integer' }, + }, + }, + migrations: dashboardSavedObjectTypeMigrations, +}; diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts new file mode 100644 index 0000000000000..22ed18f75c652 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -0,0 +1,451 @@ +/* + * 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 { 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, contextMock)).toMatchInlineSnapshot(` +Object { + "references": Array [], +} +`); + }); + + test('skips errors when searchSourceJSON is null', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: null, + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc, contextMock); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips errors when searchSourceJSON is undefined', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: undefined, + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc, contextMock); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when searchSourceJSON is not a string', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: 123, + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when searchSourceJSON is invalid json', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: '{abc123}', + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when "index" and "filter" is missing from searchSourceJSON', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true }), + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc, contextMock); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('extracts "index" attribute from doc', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ bar: true, index: 'pattern*' }), + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc, contextMock); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('extracts index patterns from filter', () => { + const doc = { + id: '1', + type: 'dashboard', + attributes: { + kibanaSavedObjectMeta: { + searchSourceJSON: JSON.stringify({ + bar: true, + filter: [ + { + meta: { + foo: true, + index: 'my-index', + }, + }, + ], + }), + }, + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + }; + const migratedDoc = migration(doc, contextMock); + + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], + "type": "dashboard", +} +`); + }); + + test('skips error when panelsJSON is not a string', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: 123, + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": 123, + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips error when panelsJSON is not valid JSON', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '{123abc}', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "{123abc}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips panelsJSON when its not an array', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '{}', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "{}", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips error when a panel is missing "type" attribute', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '[{"id":"123"}]', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "[{\\"id\\":\\"123\\"}]", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('skips error when a panel is missing "id" attribute', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: '[{"type":"visualization"}]', + }, + } as SavedObjectUnsanitizedDoc; + expect(migration(doc, contextMock)).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "[{\\"type\\":\\"visualization\\"}]", + }, + "id": "1", + "references": Array [], +} +`); + }); + + test('extract panel references from doc', () => { + const doc = { + id: '1', + attributes: { + panelsJSON: + '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', + }, + } as SavedObjectUnsanitizedDoc; + const migratedDoc = migration(doc, contextMock); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "panelsJSON": "[{\\"foo\\":true,\\"panelRefName\\":\\"panel_0\\"},{\\"bar\\":true,\\"panelRefName\\":\\"panel_1\\"}]", + }, + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "panel_0", + "type": "visualization", + }, + Object { + "id": "2", + "name": "panel_1", + "type": "visualization", + }, + ], +} +`); + }); + }); +}); diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts new file mode 100644 index 0000000000000..4f7945d6dd601 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -0,0 +1,117 @@ +/* + * 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 { get, flow } from 'lodash'; + +import { SavedObjectMigrationFn } from 'kibana/server'; +import { migrations730 } from './migrations_730'; +import { migrateMatchAllQuery } from './migrate_match_all_query'; +import { DashboardDoc700To720 } from '../../common'; + +function migrateIndexPattern(doc: DashboardDoc700To720) { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return; + } + if (searchSource.index) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: 'index-pattern', + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); +} + +const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { + // Set new "references" attribute + doc.references = doc.references || []; + + // Migrate index pattern + migrateIndexPattern(doc as DashboardDoc700To720); + // Migrate panels + const panelsJSON = get(doc, 'attributes.panelsJSON'); + if (typeof panelsJSON !== 'string') { + return doc as DashboardDoc700To720; + } + let panels; + try { + panels = JSON.parse(panelsJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc as DashboardDoc700To720; + } + if (!Array.isArray(panels)) { + return doc as DashboardDoc700To720; + } + panels.forEach((panel, i) => { + if (!panel.type || !panel.id) { + return; + } + panel.panelRefName = `panel_${i}`; + doc.references!.push({ + name: `panel_${i}`, + type: panel.type, + id: panel.id, + }); + delete panel.type; + delete panel.id; + }); + doc.attributes.panelsJSON = JSON.stringify(panels); + return doc as DashboardDoc700To720; +}; + +export const dashboardSavedObjectTypeMigrations = { + /** + * We need to have this migration twice, once with a version prior to 7.0.0 once with a version + * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already + * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, + * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we + * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects + * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced + * 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>(migrations700), + '7.3.0': flow>(migrations730), +}; diff --git a/src/plugins/dashboard/server/saved_objects/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts new file mode 100644 index 0000000000000..ca97b9d2a6b70 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { dashboardSavedObjectType } from './dashboard'; diff --git a/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts b/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts new file mode 100644 index 0000000000000..c9b35263a549f --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts @@ -0,0 +1,48 @@ +/* + * 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 { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { DashboardDoc730ToLatest } from '../../common'; + +function isDoc( + doc: { [key: string]: unknown } | SavedObjectUnsanitizedDoc +): doc is SavedObjectUnsanitizedDoc { + return ( + typeof doc.id === 'string' && + typeof doc.type === 'string' && + doc.attributes !== null && + typeof doc.attributes === 'object' && + doc.references !== null && + typeof doc.references === 'object' + ); +} + +export function isDashboardDoc( + doc: { [key: string]: unknown } | DashboardDoc730ToLatest +): doc is DashboardDoc730ToLatest { + if (!isDoc(doc)) { + return false; + } + + if (typeof (doc as DashboardDoc730ToLatest).attributes.panelsJSON !== 'string') { + return false; + } + + return true; +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.test.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.test.ts rename to src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts rename to src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts index 707aae9e5d4ac..db2fbeb278802 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts @@ -19,9 +19,9 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { get } from 'lodash'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../../../../plugins/data/common'; +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/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index 5a4970897098d..a58df547fa522 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -17,21 +17,13 @@ * under the License. */ -import { migrations } from '../../../migrations'; +import { savedObjectsServiceMock } from '../../../../core/server/mocks'; +import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; -import { - DashboardDoc700To720, - DashboardDoc730ToLatest, - RawSavedDashboardPanel730ToLatest, - DashboardDocPre700, -} from '../../../../../../plugins/dashboard/public'; - -const mockLogger = { - warning: () => {}, - warn: () => {}, - debug: () => {}, - info: () => {}, -}; +import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; +import { RawSavedDashboardPanel730ToLatest } from '../../common'; + +const mockContext = savedObjectsServiceMock.createMigrationContext(); test('dashboard migration 7.3.0 migrates filters to query on search source', () => { const doc: DashboardDoc700To720 = { @@ -53,7 +45,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source', () '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const newDoc = migrations730(doc, mockLogger); + const newDoc = migrations730(doc, mockContext); expect(newDoc).toMatchInlineSnapshot(` Object { @@ -97,8 +89,8 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when }, }; - const doc700: DashboardDoc700To720 = migrations.dashboard['7.0.0'](doc, mockLogger); - const newDoc = migrations.dashboard['7.3.0'](doc700, mockLogger); + const doc700 = migrations['7.0.0'](doc, mockContext); + const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); expect(parsedSearchSource.filter.length).toBe(0); @@ -129,8 +121,8 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => { }, }; - const doc700: DashboardDoc700To720 = migrations.dashboard['7.0.0'](doc, mockLogger); - const newDoc = migrations.dashboard['7.3.0'](doc700, mockLogger); + const doc700 = migrations['7.0.0'](doc, mockContext); + const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); expect(parsedSearchSource.filter.length).toBe(0); @@ -159,7 +151,7 @@ test('dashboard migration 7.3.0 migrates panels', () => { }, }; - const newDoc = migrations730(doc, mockLogger) as DashboardDoc730ToLatest; + const newDoc = migrations730(doc, mockContext) as DashboardDoc730ToLatest; const newPanels = JSON.parse(newDoc.attributes.panelsJSON) as RawSavedDashboardPanel730ToLatest[]; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.ts similarity index 79% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts rename to src/plugins/dashboard/server/saved_objects/migrations_730.ts index 56856f7b21303..e9d483f68a5da 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.ts @@ -16,26 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -// This file should be moved to dashboard/server/ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsMigrationLogger } from 'src/core/server'; + import { inspect } from 'util'; -import { - DashboardDoc730ToLatest, - DashboardDoc700To720, -} from '../../../../../../plugins/dashboard/public'; +import { SavedObjectMigrationContext } from 'kibana/server'; +import { DashboardDoc730ToLatest } from '../../common'; import { isDashboardDoc } from './is_dashboard_doc'; import { moveFiltersToQuery } from './move_filters_to_query'; -import { migratePanelsTo730 } from './migrate_to_730_panels'; +import { migratePanelsTo730, DashboardDoc700To720 } from '../../common'; -export function migrations730( - doc: - | { - [key: string]: unknown; - } - | DashboardDoc700To720, - logger: SavedObjectsMigrationLogger -): DashboardDoc730ToLatest | { [key: string]: unknown } { +export const migrations730 = (doc: DashboardDoc700To720, { log }: SavedObjectMigrationContext) => { if (!isDashboardDoc(doc)) { // NOTE: we should probably throw an error here... but for now following suit and in the // case of errors, just returning the same document. @@ -48,7 +37,7 @@ export function migrations730( moveFiltersToQuery(searchSource) ); } catch (e) { - logger.warning( + log.warning( `Exception @ migrations730 while trying to migrate dashboard query filters!\n` + `${e.stack}\n` + `dashboard: ${inspect(doc, false, null)}` @@ -75,7 +64,7 @@ export function migrations730( delete doc.attributes.uiStateJSON; } catch (e) { - logger.warning( + log.warning( `Exception @ migrations730 while trying to migrate dashboard panels!\n` + `Error: ${e.stack}\n` + `dashboard: ${inspect(doc, false, null)}` @@ -84,4 +73,4 @@ export function migrations730( } return doc as DashboardDoc730ToLatest; -} +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts similarity index 96% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts rename to src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts index 621983b1ca8a5..a06f64e0f0c40 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts +++ b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts @@ -17,8 +17,8 @@ * under the License. */ +import { esFilters, Filter } from 'src/plugins/data/public'; import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; -import { esFilters, Filter } from '../../../../../../plugins/data/public'; const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts rename to src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts diff --git a/src/plugins/dashboard/server/types.ts b/src/plugins/dashboard/server/types.ts new file mode 100644 index 0000000000000..1151b06dbdab7 --- /dev/null +++ b/src/plugins/dashboard/server/types.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DashboardPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DashboardPluginStart {} diff --git a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js index f745f01873bae..fc6b706f6e01e 100644 --- a/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js +++ b/src/plugins/data/common/es_query/kuery/ast/_generated_/kuery.js @@ -222,9 +222,8 @@ module.exports = (function() { if (sequence === 'true') return buildLiteralNode(true); if (sequence === 'false') return buildLiteralNode(false); if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - const number = Number(sequence); - const value = isNaN(number) ? sequence : number; - return buildLiteralNode(value); + const isNumberPattern = /^(-?[1-9]+\d*([.]\d+)?)$|^(-?0[.]\d*[1-9]+)$|^0$|^0.0$|^[.]\d{1,}$/ + return buildLiteralNode(isNumberPattern.test(sequence) ? Number(sequence) : sequence); }, peg$c50 = { type: "any", description: "any character" }, peg$c51 = "*", @@ -3164,4 +3163,4 @@ module.exports = (function() { SyntaxError: peg$SyntaxError, parse: peg$parse }; -})(); \ No newline at end of file +})(); diff --git a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts index e441420760475..6a69d52d72134 100644 --- a/src/plugins/data/common/es_query/kuery/ast/ast.test.ts +++ b/src/plugins/data/common/es_query/kuery/ast/ast.test.ts @@ -278,6 +278,33 @@ describe('kuery AST API', () => { expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); expect(fromLiteralExpression('42')).toEqual(numberLiteral); + + expect(fromLiteralExpression('.3').value).toEqual(0.3); + expect(fromLiteralExpression('.36').value).toEqual(0.36); + expect(fromLiteralExpression('.00001').value).toEqual(0.00001); + expect(fromLiteralExpression('3').value).toEqual(3); + expect(fromLiteralExpression('-4').value).toEqual(-4); + expect(fromLiteralExpression('0').value).toEqual(0); + expect(fromLiteralExpression('0.0').value).toEqual(0); + expect(fromLiteralExpression('2.0').value).toEqual(2.0); + expect(fromLiteralExpression('0.8').value).toEqual(0.8); + expect(fromLiteralExpression('790.9').value).toEqual(790.9); + expect(fromLiteralExpression('0.0001').value).toEqual(0.0001); + expect(fromLiteralExpression('96565646732345').value).toEqual(96565646732345); + + expect(fromLiteralExpression('..4').value).toEqual('..4'); + expect(fromLiteralExpression('.3text').value).toEqual('.3text'); + expect(fromLiteralExpression('text').value).toEqual('text'); + expect(fromLiteralExpression('.').value).toEqual('.'); + expect(fromLiteralExpression('-').value).toEqual('-'); + expect(fromLiteralExpression('001').value).toEqual('001'); + expect(fromLiteralExpression('00.2').value).toEqual('00.2'); + expect(fromLiteralExpression('0.0.1').value).toEqual('0.0.1'); + expect(fromLiteralExpression('3.').value).toEqual('3.'); + expect(fromLiteralExpression('--4').value).toEqual('--4'); + expect(fromLiteralExpression('-.4').value).toEqual('-.4'); + expect(fromLiteralExpression('-0').value).toEqual('-0'); + expect(fromLiteralExpression('00949').value).toEqual('00949'); }); test('should allow escaping of special characters with a backslash', () => { diff --git a/src/plugins/data/common/es_query/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg index 389b9a82d2c76..625c5069f936a 100644 --- a/src/plugins/data/common/es_query/kuery/ast/kuery.peg +++ b/src/plugins/data/common/es_query/kuery/ast/kuery.peg @@ -247,9 +247,8 @@ UnquotedLiteral if (sequence === 'true') return buildLiteralNode(true); if (sequence === 'false') return buildLiteralNode(false); if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - const number = Number(sequence); - const value = isNaN(number) ? sequence : number; - return buildLiteralNode(value); + const isNumberPattern = /^(-?[1-9]+\d*([.]\d+)?)$|^(-?0[.]\d*[1-9]+)$|^0$|^0.0$|^[.]\d{1,}$/ + return buildLiteralNode(isNumberPattern.test(sequence) ? Number(sequence) : sequence); } UnquotedCharacter diff --git a/src/plugins/data/common/field_formats/constants/base_formatters.ts b/src/plugins/data/common/field_formats/constants/base_formatters.ts index 6befe8cea71f5..921c50571f727 100644 --- a/src/plugins/data/common/field_formats/constants/base_formatters.ts +++ b/src/plugins/data/common/field_formats/constants/base_formatters.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IFieldFormatType } from '../types'; +import { FieldFormatInstanceType } from '../types'; import { BoolFormat, @@ -36,7 +36,7 @@ import { UrlFormat, } from '../converters'; -export const baseFormatters: IFieldFormatType[] = [ +export const baseFormatters: FieldFormatInstanceType[] = [ BoolFormat, BytesFormat, ColorFormat, diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index a1ce0cf3e7b54..4dd011a7feff3 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -18,9 +18,9 @@ */ import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, FIELD_FORMAT_IDS, IFieldFormatType } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS, FieldFormatInstanceType } from '../types'; -export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): FieldFormatInstanceType => class CustomFieldFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.CUSTOM; diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 49baa8c074da8..26f07a12067ce 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -22,7 +22,7 @@ import { createCustomFieldFormat } from './converters/custom'; import { FieldFormatsGetConfigFn, FieldFormatsContentType, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatConvert, FieldFormatConvertFunction, HtmlContextTypeOptions, @@ -127,12 +127,12 @@ export abstract class FieldFormat { */ getConverterFor( contentType: FieldFormatsContentType = DEFAULT_CONTEXT_TYPE - ): FieldFormatConvertFunction | null { + ): FieldFormatConvertFunction { if (!this.convertObject) { this.convertObject = this.setupContentType(); } - return this.convertObject[contentType] || null; + return this.convertObject[contentType]; } /** @@ -199,7 +199,7 @@ export abstract class FieldFormat { }; } - static from(convertFn: FieldFormatConvertFunction): IFieldFormatType { + static from(convertFn: FieldFormatConvertFunction): FieldFormatInstanceType { return createCustomFieldFormat(convertFn); } diff --git a/src/plugins/data/common/field_formats/field_formats_registry.test.ts b/src/plugins/data/common/field_formats/field_formats_registry.test.ts index 0b32a62744fb1..f04524505a711 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.test.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.test.ts @@ -18,7 +18,7 @@ */ import { FieldFormatsRegistry } from './field_formats_registry'; import { BoolFormat, PercentFormat, StringFormat } from './converters'; -import { FieldFormatsGetConfigFn, IFieldFormatType } from './types'; +import { FieldFormatsGetConfigFn, FieldFormatInstanceType } from './types'; import { KBN_FIELD_TYPES } from '../../common'; const getValueOfPrivateField = (instance: any, field: string) => instance[field]; @@ -75,10 +75,10 @@ describe('FieldFormatsRegistry', () => { test('should register field formats', () => { fieldFormatsRegistry.register([StringFormat, BoolFormat]); - const registeredFieldFormatters: Map = getValueOfPrivateField( - fieldFormatsRegistry, - 'fieldFormats' - ); + const registeredFieldFormatters: Map< + string, + FieldFormatInstanceType + > = getValueOfPrivateField(fieldFormatsRegistry, 'fieldFormats'); expect(registeredFieldFormatters.size).toBe(2); diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 15b1687e22312..2eb9a3e593d1a 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -24,7 +24,7 @@ import { FieldFormatsGetConfigFn, FieldFormatConfig, FIELD_FORMAT_IDS, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatId, IFieldFormatMetaParams, IFieldFormat, @@ -35,7 +35,7 @@ import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types'; export class FieldFormatsRegistry { - protected fieldFormats: Map = new Map(); + protected fieldFormats: Map = new Map(); protected defaultMap: Record = {}; protected metaParamsOptions: Record = {}; protected getConfig?: FieldFormatsGetConfigFn; @@ -47,7 +47,7 @@ export class FieldFormatsRegistry { init( getConfig: FieldFormatsGetConfigFn, metaParamsOptions: Record = {}, - defaultFieldConverters: IFieldFormatType[] = baseFormatters + defaultFieldConverters: FieldFormatInstanceType[] = baseFormatters ) { const defaultTypeMap = getConfig('format:defaultTypeMap'); this.register(defaultFieldConverters); @@ -79,23 +79,23 @@ export class FieldFormatsRegistry { * Get a derived FieldFormat class by its id. * * @param {FieldFormatId} formatId - the format id - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ - getType = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getType = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { const fieldFormat = this.fieldFormats.get(formatId); if (fieldFormat) { const decoratedFieldFormat: any = this.fieldFormatMetaParamsDecorator(fieldFormat); if (decoratedFieldFormat) { - return decoratedFieldFormat as IFieldFormatType; + return decoratedFieldFormat as FieldFormatInstanceType; } } return undefined; }; - getTypeWithoutMetaParams = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getTypeWithoutMetaParams = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { return this.fieldFormats.get(formatId); }; @@ -106,12 +106,12 @@ export class FieldFormatsRegistry { * * @param {KBN_FIELD_TYPES} fieldType * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ getDefaultType = ( fieldType: KBN_FIELD_TYPES, - esTypes: ES_FIELD_TYPES[] - ): IFieldFormatType | undefined => { + esTypes?: ES_FIELD_TYPES[] + ): FieldFormatInstanceType | undefined => { const config = this.getDefaultConfig(fieldType, esTypes); return this.getType(config.id); @@ -206,14 +206,16 @@ export class FieldFormatsRegistry { * Get filtered list of field formats by format type * * @param {KBN_FIELD_TYPES} fieldType - * @return {IFieldFormatType[]} + * @return {FieldFormatInstanceType[]} */ - getByFieldType(fieldType: KBN_FIELD_TYPES): IFieldFormatType[] { + getByFieldType(fieldType: KBN_FIELD_TYPES): FieldFormatInstanceType[] { return [...this.fieldFormats.values()] - .filter((format: IFieldFormatType) => format && format.fieldType.indexOf(fieldType) !== -1) + .filter( + (format: FieldFormatInstanceType) => format && format.fieldType.indexOf(fieldType) !== -1 + ) .map( - (format: IFieldFormatType) => - this.fieldFormatMetaParamsDecorator(format) as IFieldFormatType + (format: FieldFormatInstanceType) => + this.fieldFormatMetaParamsDecorator(format) as FieldFormatInstanceType ); } @@ -238,7 +240,7 @@ export class FieldFormatsRegistry { }); } - register(fieldFormats: IFieldFormatType[]) { + register(fieldFormats: FieldFormatInstanceType[]) { fieldFormats.forEach(fieldFormat => this.fieldFormats.set(fieldFormat.id, fieldFormat)); } @@ -246,12 +248,12 @@ export class FieldFormatsRegistry { * FieldFormat decorator - provide a one way to add meta-params for all field formatters * * @private - * @param {IFieldFormatType} fieldFormat - field format type - * @return {IFieldFormatType | undefined} + * @param {FieldFormatInstanceType} fieldFormat - field format type + * @return {FieldFormatInstanceType | undefined} */ private fieldFormatMetaParamsDecorator = ( - fieldFormat: IFieldFormatType - ): IFieldFormatType | undefined => { + fieldFormat: FieldFormatInstanceType + ): FieldFormatInstanceType | undefined => { const getMetaParams = (customParams: Record) => this.buildMetaParams(customParams); if (fieldFormat) { diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index 13d3d9d73d43a..b64e115fd55ff 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -52,6 +52,6 @@ export { FieldFormatConfig, FieldFormatId, // Used in data plugin only - IFieldFormatType, + FieldFormatInstanceType, IFieldFormat, } from './types'; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 7c1d6a8522e52..5f11c7fe094bc 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - import { FieldFormat } from './field_format'; -export { FieldFormat }; /** @public **/ export type FieldFormatsContentType = 'html' | 'text'; @@ -82,10 +80,12 @@ export type IFieldFormat = PublicMethodsOf; */ export type FieldFormatId = FIELD_FORMAT_IDS | string; -export type IFieldFormatType = (new ( +/** @internal **/ +export type FieldFormatInstanceType = (new ( params?: any, getConfig?: FieldFormatsGetConfigFn ) => FieldFormat) & { + // Static properties: id: FieldFormatId; title: string; fieldType: string | string[]; diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 698edbf9cd6a8..e21d27a70e02a 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/common/utils/abort_utils.ts b/src/plugins/data/common/utils/abort_utils.ts index 5051515f3a826..9aec787170840 100644 --- a/src/plugins/data/common/utils/abort_utils.ts +++ b/src/plugins/data/common/utils/abort_utils.ts @@ -33,19 +33,31 @@ export class AbortError extends Error { * Returns a `Promise` corresponding with when the given `AbortSignal` is aborted. Useful for * situations when you might need to `Promise.race` multiple `AbortSignal`s, or an `AbortSignal` * with any other expected errors (or completions). + * * @param signal The `AbortSignal` to generate the `Promise` from * @param shouldReject If `false`, the promise will be resolved, otherwise it will be rejected */ -export function toPromise(signal: AbortSignal, shouldReject = false) { - return new Promise((resolve, reject) => { +export function toPromise(signal: AbortSignal, shouldReject?: false): Promise; +export function toPromise(signal: AbortSignal, shouldReject?: true): Promise; +export function toPromise(signal: AbortSignal, shouldReject: boolean = false) { + const promise = new Promise((resolve, reject) => { const action = shouldReject ? reject : resolve; if (signal.aborted) action(); signal.addEventListener('abort', action); }); + + /** + * Below is to make sure we don't have unhandled promise rejections. Otherwise + * Jest tests fail. + */ + promise.catch(() => {}); + + return promise; } /** * Returns an `AbortSignal` that will be aborted when the first of the given signals aborts. + * * @param signals */ export function getCombinedSignal(signals: AbortSignal[]) { diff --git a/src/plugins/data/public/actions/filters/brush_event.test.ts b/src/plugins/data/public/actions/filters/brush_event.test.ts deleted file mode 100644 index 60244354f06e4..0000000000000 --- a/src/plugins/data/public/actions/filters/brush_event.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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 moment from 'moment'; - -import { onBrushEvent, BrushEvent } from './brush_event'; - -import { IndexPatternsContract } from '../../../public'; -import { dataPluginMock } from '../../../public/mocks'; -import { setIndexPatterns } from '../../../public/services'; -import { mockDataServices } from '../../../public/search/aggs/test_helpers'; - -describe('brushEvent', () => { - const DAY_IN_MS = 24 * 60 * 60 * 1000; - const JAN_01_2014 = 1388559600000; - let baseEvent: BrushEvent; - - const aggConfigs = [ - { - params: { - field: {}, - }, - getIndexPattern: () => ({ - timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, - }), - }, - ]; - - beforeEach(() => { - mockDataServices(); - setIndexPatterns(({ - ...dataPluginMock.createStartContract().indexPatterns, - get: async () => ({ - id: 'indexPatternId', - timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, - }), - } as unknown) as IndexPatternsContract); - - baseEvent = { - data: { - ordered: { - date: false, - }, - series: [ - { - values: [ - { - xRaw: { - column: 0, - table: { - columns: [ - { - id: '1', - meta: { - type: 'histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: aggConfigs[0].params, - }, - }, - ], - }, - }, - }, - ], - }, - ], - }, - range: [], - }; - }); - - test('should be a function', () => { - expect(typeof onBrushEvent).toBe('function'); - }); - - test('ignores event when data.xAxisField not provided', async () => { - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); - }); - - describe('handles an event when the x-axis field is a date field', () => { - describe('date field is index pattern timefield', () => { - beforeEach(() => { - aggConfigs[0].params.field = { - name: 'time', - type: 'date', - }; - baseEvent.data.ordered = { date: true }; - }); - - afterAll(() => { - baseEvent.range = []; - baseEvent.data.ordered = { date: false }; - }); - - test('by ignoring the event when range spans zero time', async () => { - baseEvent.range = [JAN_01_2014, JAN_01_2014]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); - }); - - test('by updating the timefilter', async () => { - baseEvent.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeDefined(); - - if (filter) { - expect(filter.range.time.gte).toBe(new Date(JAN_01_2014).toISOString()); - // Set to a baseline timezone for comparison. - expect(filter.range.time.lt).toBe(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); - } - }); - }); - - describe('date field is not index pattern timefield', () => { - beforeEach(() => { - aggConfigs[0].params.field = { - name: 'anotherTimeField', - type: 'date', - }; - baseEvent.data.ordered = { date: true }; - }); - - afterAll(() => { - baseEvent.range = []; - baseEvent.data.ordered = { date: false }; - }); - - test('creates a new range filter', async () => { - const rangeBegin = JAN_01_2014; - const rangeEnd = rangeBegin + DAY_IN_MS; - baseEvent.range = [rangeBegin, rangeEnd]; - const filter = await onBrushEvent(baseEvent); - - expect(filter).toBeDefined(); - - if (filter) { - expect(filter.range.anotherTimeField.gte).toBe(moment(rangeBegin).toISOString()); - expect(filter.range.anotherTimeField.lt).toBe(moment(rangeEnd).toISOString()); - expect(filter.range.anotherTimeField).toHaveProperty( - 'format', - 'strict_date_optional_time' - ); - } - }); - }); - }); - - describe('handles an event when the x-axis field is a number', () => { - beforeAll(() => { - aggConfigs[0].params.field = { - name: 'numberField', - type: 'number', - }; - }); - - afterAll(() => { - baseEvent.range = []; - }); - - test('by ignoring the event when range does not span at least 2 values', async () => { - baseEvent.range = [1]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); - }); - - test('by creating a new filter', async () => { - baseEvent.range = [1, 2, 3, 4]; - const filter = await onBrushEvent(baseEvent); - - expect(filter).toBeDefined(); - - if (filter) { - expect(filter.range.numberField.gte).toBe(1); - expect(filter.range.numberField.lt).toBe(4); - expect(filter.range.numberField).not.toHaveProperty('format'); - } - }); - }); -}); diff --git a/src/plugins/data/public/actions/filters/brush_event.ts b/src/plugins/data/public/actions/filters/brush_event.ts deleted file mode 100644 index 714f005fbeb6d..0000000000000 --- a/src/plugins/data/public/actions/filters/brush_event.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 { get, last } from 'lodash'; -import moment from 'moment'; -import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; -import { deserializeAggConfig } from '../../search/expressions/utils'; - -export interface BrushEvent { - data: { - ordered: { - date: boolean; - }; - series: Array>; - }; - range: number[]; -} - -export async function onBrushEvent(event: BrushEvent) { - const isDate = get(event.data, 'ordered.date'); - const xRaw: Record = get(event.data, 'series[0].values[0].xRaw'); - - if (!xRaw) { - return; - } - - const column: Record = xRaw.table.columns[xRaw.column]; - - if (!column || !column.meta) { - return; - } - - const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); - const aggConfig = deserializeAggConfig({ - ...column.meta, - indexPattern, - }); - const field: IFieldType = aggConfig.params.field; - - if (!field || event.range.length <= 1) { - return; - } - - const min = event.range[0]; - const max = last(event.range); - - if (min === max) { - return; - } - - const range: RangeFilterParams = { - gte: isDate ? moment(min).toISOString() : min, - lt: isDate ? moment(max).toISOString() : max, - }; - - if (isDate) { - range.format = 'strict_date_optional_time'; - } - - return esFilters.buildRangeFilter(field, range, indexPattern); -} diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts deleted file mode 100644 index 1ed09002816d1..0000000000000 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * 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 { - fieldFormats, - FieldFormatsGetConfigFn, - esFilters, - IndexPatternsContract, -} from '../../../public'; -import { dataPluginMock } from '../../../public/mocks'; -import { setIndexPatterns } from '../../../public/services'; -import { mockDataServices } from '../../../public/search/aggs/test_helpers'; -import { createFiltersFromEvent, EventData } from './create_filters_from_event'; - -const mockField = { - name: 'bytes', - indexPattern: { - id: 'logstash-*', - }, - filterable: true, - format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), -}; - -describe('createFiltersFromEvent', () => { - let dataPoints: EventData[]; - - beforeEach(() => { - dataPoints = [ - { - table: { - columns: [ - { - name: 'test', - id: '1-1', - meta: { - type: 'histogram', - indexPatternId: 'logstash-*', - aggConfigParams: { - field: 'bytes', - interval: 30, - otherBucket: true, - }, - }, - }, - ], - rows: [ - { - '1-1': '2048', - }, - ], - }, - column: 0, - row: 0, - value: 'test', - }, - ]; - - mockDataServices(); - setIndexPatterns(({ - ...dataPluginMock.createStartContract().indexPatterns, - get: async () => ({ - id: 'logstash-*', - fields: { - getByName: () => mockField, - filter: () => [mockField], - }, - }), - } as unknown) as IndexPatternsContract); - }); - - test('ignores event when value for rows is not provided', async () => { - dataPoints[0].table.rows[0]['1-1'] = null; - const filters = await createFiltersFromEvent(dataPoints); - - expect(filters.length).toEqual(0); - }); - - test('handles an event when aggregations type is a terms', async () => { - if (dataPoints[0].table.columns[0].meta) { - dataPoints[0].table.columns[0].meta.type = 'terms'; - } - const filters = await createFiltersFromEvent(dataPoints); - - expect(filters.length).toEqual(1); - expect(filters[0].query.match_phrase.bytes).toEqual('2048'); - }); - - test('handles an event when aggregations type is not terms', async () => { - const filters = await createFiltersFromEvent(dataPoints); - - expect(filters.length).toEqual(1); - - const [rangeFilter] = filters; - - if (esFilters.isRangeFilter(rangeFilter)) { - expect(rangeFilter.range.bytes.gte).toEqual(2048); - expect(rangeFilter.range.bytes.lt).toEqual(2078); - } - }); -}); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.ts b/src/plugins/data/public/actions/filters/create_filters_from_event.ts deleted file mode 100644 index e62945a592072..0000000000000 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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 { KibanaDatatable } from '../../../../../plugins/expressions/public'; -import { deserializeAggConfig } from '../../search/expressions'; -import { esFilters, Filter } from '../../../public'; -import { getIndexPatterns } from '../../../public/services'; - -export interface EventData { - table: Pick; - column: number; - row: number; - value: any; -} - -/** - * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter - * terms based on a specific cell in the tabified data. - * - * @param {EventData['table']} table - tabified table data - * @param {number} columnIndex - current column index - * @param {number} rowIndex - current row index - * @return {array} - array of terms to filter against - */ -const getOtherBucketFilterTerms = ( - table: EventData['table'], - columnIndex: number, - rowIndex: number -) => { - if (rowIndex === -1) { - return []; - } - - // get only rows where cell value matches current row for all the fields before columnIndex - const rows = table.rows.filter(row => { - return table.columns.every((column, i) => { - return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; - }); - }); - const terms: any[] = rows.map(row => row[table.columns[columnIndex].id]); - - return [ - ...new Set( - terms.filter(term => { - const notOther = term !== '__other__'; - const notMissing = term !== '__missing__'; - return notOther && notMissing; - }) - ), - ]; -}; - -/** - * Assembles the filters needed to apply filtering against a specific cell value, while accounting - * for cases like if the value is a terms agg in an `__other__` or `__missing__` bucket. - * - * @param {EventData['table']} table - tabified table data - * @param {number} columnIndex - current column index - * @param {number} rowIndex - current row index - * @param {string} cellValue - value of the current cell - * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() - */ -const createFilter = async (table: EventData['table'], columnIndex: number, rowIndex: number) => { - if (!table || !table.columns || !table.columns[columnIndex]) { - return; - } - const column = table.columns[columnIndex]; - if (!column.meta || !column.meta.indexPatternId) { - return; - } - const aggConfig = deserializeAggConfig({ - type: column.meta.type, - aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, - indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), - }); - let filter: Filter[] = []; - const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; - if (value === null || value === undefined || !aggConfig.isFilterable()) { - return; - } - if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { - const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); - filter = aggConfig.createFilter(value, { terms }); - } else { - filter = aggConfig.createFilter(value); - } - - if (!filter) { - return; - } - - if (!Array.isArray(filter)) { - filter = [filter]; - } - - return filter; -}; - -/** @public */ -export const createFiltersFromEvent = async (dataPoints: EventData[], negate?: boolean) => { - const filters: Filter[] = []; - - await Promise.all( - dataPoints - .filter(point => point) - .map(async val => { - const { table, column, row } = val; - const filter: Filter[] = (await createFilter(table, column, row)) || []; - if (filter) { - filter.forEach(f => { - if (negate) { - f = esFilters.toggleFilterNegated(f); - } - filters.push(f); - }); - } - }) - ); - - return filters; -}; diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts new file mode 100644 index 0000000000000..5d21b395b994f --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -0,0 +1,190 @@ +/* + * 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 moment from 'moment'; + +import { createFiltersFromRangeSelectAction } from './create_filters_from_range_select'; + +import { IndexPatternsContract, RangeFilter } from '../../../public'; +import { dataPluginMock } from '../../../public/mocks'; +import { setIndexPatterns } from '../../../public/services'; +import { mockDataServices } from '../../../public/search/aggs/test_helpers'; +import { TriggerContextMapping } from '../../../../ui_actions/public'; + +describe('brushEvent', () => { + const DAY_IN_MS = 24 * 60 * 60 * 1000; + const JAN_01_2014 = 1388559600000; + let baseEvent: TriggerContextMapping['SELECT_RANGE_TRIGGER']['data']; + + const indexPattern = { + id: 'indexPatternId', + timeFieldName: 'time', + fields: { + getByName: () => undefined, + filter: () => [], + }, + }; + + const aggConfigs = [ + { + params: { + field: {}, + }, + getIndexPattern: () => indexPattern, + }, + ]; + + beforeEach(() => { + mockDataServices(); + setIndexPatterns(({ + ...dataPluginMock.createStartContract().indexPatterns, + get: async () => indexPattern, + } as unknown) as IndexPatternsContract); + + baseEvent = { + column: 0, + table: { + type: 'kibana_datatable', + columns: [ + { + id: '1', + name: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, + }, + ], + rows: [], + }, + range: [], + }; + }); + + test('should be a function', () => { + expect(typeof createFiltersFromRangeSelectAction).toBe('function'); + }); + + test('ignores event when data.xAxisField not provided', async () => { + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); + }); + + describe('handles an event when the x-axis field is a date field', () => { + describe('date field is index pattern timefield', () => { + beforeEach(() => { + aggConfigs[0].params.field = { + name: 'time', + type: 'date', + }; + }); + + afterAll(() => { + baseEvent.range = []; + aggConfigs[0].params.field = {}; + }); + + test('by ignoring the event when range spans zero time', async () => { + baseEvent.range = [JAN_01_2014, JAN_01_2014]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); + }); + + test('by updating the timefilter', async () => { + baseEvent.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toBeDefined(); + + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.time.gte).toBe(new Date(JAN_01_2014).toISOString()); + // Set to a baseline timezone for comparison. + expect(rangeFilter.range.time.lt).toBe(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); + } + }); + }); + + describe('date field is not index pattern timefield', () => { + beforeEach(() => { + aggConfigs[0].params.field = { + name: 'anotherTimeField', + type: 'date', + }; + }); + + afterAll(() => { + baseEvent.range = []; + aggConfigs[0].params.field = {}; + }); + + test('creates a new range filter', async () => { + const rangeBegin = JAN_01_2014; + const rangeEnd = rangeBegin + DAY_IN_MS; + baseEvent.range = [rangeBegin, rangeEnd]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + + expect(filter).toBeDefined(); + + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.anotherTimeField.gte).toBe(moment(rangeBegin).toISOString()); + expect(rangeFilter.range.anotherTimeField.lt).toBe(moment(rangeEnd).toISOString()); + expect(rangeFilter.range.anotherTimeField).toHaveProperty( + 'format', + 'strict_date_optional_time' + ); + } + }); + }); + }); + + describe('handles an event when the x-axis field is a number', () => { + beforeAll(() => { + aggConfigs[0].params.field = { + name: 'numberField', + type: 'number', + }; + }); + + afterAll(() => { + baseEvent.range = []; + }); + + test('by ignoring the event when range does not span at least 2 values', async () => { + baseEvent.range = [1]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); + }); + + test('by creating a new filter', async () => { + baseEvent.range = [1, 2, 3, 4]; + const filter = await createFiltersFromRangeSelectAction(baseEvent); + + expect(filter).toBeDefined(); + + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.numberField.gte).toBe(1); + expect(rangeFilter.range.numberField.lt).toBe(4); + expect(rangeFilter.range.numberField).not.toHaveProperty('format'); + } + }); + }); +}); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts new file mode 100644 index 0000000000000..409614ca9c380 --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts @@ -0,0 +1,64 @@ +/* + * 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 { last } from 'lodash'; +import moment from 'moment'; +import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; +import { getIndexPatterns } from '../../../public/services'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +import { RangeSelectTriggerContext } from '../../../../embeddable/public'; + +export async function createFiltersFromRangeSelectAction(event: RangeSelectTriggerContext['data']) { + const column: Record = event.table.columns[event.column]; + + if (!column || !column.meta) { + return []; + } + + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); + const field: IFieldType = aggConfig.params.field; + + if (!field || event.range.length <= 1) { + return []; + } + + const min = event.range[0]; + const max = last(event.range); + + if (min === max) { + return []; + } + + const isDate = field.type === 'date'; + + const range: RangeFilterParams = { + gte: isDate ? moment(min).toISOString() : min, + lt: isDate ? moment(max).toISOString() : max, + }; + + if (isDate) { + range.format = 'strict_date_optional_time'; + } + + return esFilters.mapAndFlattenFilters([esFilters.buildRangeFilter(field, range, indexPattern)]); +} diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts new file mode 100644 index 0000000000000..a0e285c20d776 --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -0,0 +1,117 @@ +/* + * 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 { + fieldFormats, + FieldFormatsGetConfigFn, + esFilters, + IndexPatternsContract, +} from '../../../public'; +import { dataPluginMock } from '../../../public/mocks'; +import { setIndexPatterns } from '../../../public/services'; +import { mockDataServices } from '../../../public/search/aggs/test_helpers'; +import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; +import { ValueClickTriggerContext } from '../../../../embeddable/public'; + +const mockField = { + name: 'bytes', + indexPattern: { + id: 'logstash-*', + }, + filterable: true, + format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), +}; + +describe('createFiltersFromValueClick', () => { + let dataPoints: ValueClickTriggerContext['data']['data']; + + beforeEach(() => { + dataPoints = [ + { + table: { + columns: [ + { + name: 'test', + id: '1-1', + meta: { + type: 'histogram', + indexPatternId: 'logstash-*', + aggConfigParams: { + field: 'bytes', + interval: 30, + otherBucket: true, + }, + }, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value: 'test', + }, + ]; + + mockDataServices(); + setIndexPatterns(({ + ...dataPluginMock.createStartContract().indexPatterns, + get: async () => ({ + id: 'logstash-*', + fields: { + getByName: () => mockField, + filter: () => [mockField], + }, + }), + } as unknown) as IndexPatternsContract); + }); + + test('ignores event when value for rows is not provided', async () => { + dataPoints[0].table.rows[0]['1-1'] = null; + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + + expect(filters.length).toEqual(0); + }); + + test('handles an event when aggregations type is a terms', async () => { + if (dataPoints[0].table.columns[0].meta) { + dataPoints[0].table.columns[0].meta.type = 'terms'; + } + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + + expect(filters.length).toEqual(1); + expect(filters[0].query.match_phrase.bytes).toEqual('2048'); + }); + + test('handles an event when aggregations type is not terms', async () => { + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); + + expect(filters.length).toEqual(1); + + const [rangeFilter] = filters; + + if (esFilters.isRangeFilter(rangeFilter)) { + expect(rangeFilter.range.bytes.gte).toEqual(2048); + expect(rangeFilter.range.bytes.lt).toEqual(2078); + } + }); +}); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts new file mode 100644 index 0000000000000..2b426813a98a4 --- /dev/null +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -0,0 +1,138 @@ +/* + * 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 { KibanaDatatable } from '../../../../../plugins/expressions/public'; +import { deserializeAggConfig } from '../../search/expressions'; +import { esFilters, Filter } from '../../../public'; +import { getIndexPatterns } from '../../../public/services'; +import { ValueClickTriggerContext } from '../../../../embeddable/public'; + +/** + * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter + * terms based on a specific cell in the tabified data. + * + * @param {EventData['table']} table - tabified table data + * @param {number} columnIndex - current column index + * @param {number} rowIndex - current row index + * @return {array} - array of terms to filter against + */ +const getOtherBucketFilterTerms = ( + table: Pick, + columnIndex: number, + rowIndex: number +) => { + if (rowIndex === -1) { + return []; + } + + // get only rows where cell value matches current row for all the fields before columnIndex + const rows = table.rows.filter(row => { + return table.columns.every((column, i) => { + return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; + }); + }); + const terms: any[] = rows.map(row => row[table.columns[columnIndex].id]); + + return [ + ...new Set( + terms.filter(term => { + const notOther = term !== '__other__'; + const notMissing = term !== '__missing__'; + return notOther && notMissing; + }) + ), + ]; +}; + +/** + * Assembles the filters needed to apply filtering against a specific cell value, while accounting + * for cases like if the value is a terms agg in an `__other__` or `__missing__` bucket. + * + * @param {EventData['table']} table - tabified table data + * @param {number} columnIndex - current column index + * @param {number} rowIndex - current row index + * @param {string} cellValue - value of the current cell + * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() + */ +const createFilter = async ( + table: Pick, + columnIndex: number, + rowIndex: number +) => { + if (!table || !table.columns || !table.columns[columnIndex]) { + return; + } + const column = table.columns[columnIndex]; + if (!column.meta || !column.meta.indexPatternId) { + return; + } + const aggConfig = deserializeAggConfig({ + type: column.meta.type, + aggConfigParams: column.meta.aggConfigParams ? column.meta.aggConfigParams : {}, + indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), + }); + let filter: Filter[] = []; + const value: any = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; + if (value === null || value === undefined || !aggConfig.isFilterable()) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + + if (!filter) { + return; + } + + if (!Array.isArray(filter)) { + filter = [filter]; + } + + return filter; +}; + +/** @public */ +export const createFiltersFromValueClickAction = async ({ + data, + negate, +}: ValueClickTriggerContext['data']) => { + const filters: Filter[] = []; + + await Promise.all( + data + .filter(point => point) + .map(async val => { + const { table, column, row } = val; + const filter: Filter[] = (await createFilter(table, column, row)) || []; + if (filter) { + filter.forEach(f => { + if (negate) { + f = esFilters.toggleFilterNegated(f); + } + filters.push(f); + }); + } + }) + ); + + return esFilters.mapAndFlattenFilters(filters); +}; diff --git a/src/plugins/data/public/actions/index.ts b/src/plugins/data/public/actions/index.ts index cdb84ff13f25e..ef9014aafe82d 100644 --- a/src/plugins/data/public/actions/index.ts +++ b/src/plugins/data/public/actions/index.ts @@ -18,6 +18,7 @@ */ export { ACTION_GLOBAL_APPLY_FILTER, createFilterAction } from './apply_filter_action'; -export { createFiltersFromEvent } from './filters/create_filters_from_event'; +export { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; +export { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; export { selectRangeAction } from './select_range_action'; export { valueClickAction } from './value_click_action'; diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 6e1f16a09e803..4882e8eafc0d3 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -23,19 +23,17 @@ import { IncompatibleActionError, ActionByType, } from '../../../../plugins/ui_actions/public'; -import { onBrushEvent } from './filters/brush_event'; +import { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; +import { RangeSelectTriggerContext } from '../../../embeddable/public'; import { FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; -export interface SelectRangeActionContext { - data: any; - timeFieldName: string; -} +export type SelectRangeActionContext = RangeSelectTriggerContext; async function isCompatible(context: SelectRangeActionContext) { try { - return Boolean(await onBrushEvent(context.data)); + return Boolean(await createFiltersFromRangeSelectAction(context.data)); } catch { return false; } @@ -48,6 +46,7 @@ export function selectRangeAction( return createAction({ type: ACTION_SELECT_RANGE, id: ACTION_SELECT_RANGE, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', @@ -59,13 +58,7 @@ export function selectRangeAction( throw new IncompatibleActionError(); } - const filter = await onBrushEvent(data); - - if (!filter) { - return; - } - - const selectedFilters = esFilters.mapAndFlattenFilters([filter]); + const selectedFilters = await createFiltersFromRangeSelectAction(data); if (timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 01c32e27da07d..210a58b3f75aa 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -26,21 +26,17 @@ import { } from '../../../../plugins/ui_actions/public'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; -import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; +import { ValueClickTriggerContext } from '../../../embeddable/public'; import { Filter, FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; -export interface ValueClickActionContext { - data: any; - timeFieldName: string; -} +export type ValueClickActionContext = ValueClickTriggerContext; async function isCompatible(context: ValueClickActionContext) { try { - const filters: Filter[] = - (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || - []; + const filters: Filter[] = await createFiltersFromValueClickAction(context.data); return filters.length > 0; } catch { return false; @@ -54,23 +50,23 @@ export function valueClickAction( return createAction({ type: ACTION_VALUE_CLICK, id: ACTION_VALUE_CLICK, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', }); }, isCompatible, - execute: async ({ timeFieldName, data }: ValueClickActionContext) => { - if (!(await isCompatible({ timeFieldName, data }))) { + execute: async (context: ValueClickActionContext) => { + if (!(await isCompatible(context))) { throw new IncompatibleActionError(); } - const filters: Filter[] = - (await createFiltersFromEvent(data.data || [data], data.negate)) || []; + const filters: Filter[] = await createFiltersFromValueClickAction(context.data); - let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); + let selectedFilters = filters; - if (selectedFilters.length > 1) { + if (filters.length > 1) { const indexPatterns = await Promise.all( filters.map(filter => { return getIndexPatterns().get(filter.meta.index!); @@ -102,9 +98,9 @@ export function valueClickAction( selectedFilters = await filterSelectionPromise; } - if (timeFieldName) { + if (context.timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - timeFieldName, + context.timeFieldName, selectedFilters ); filterManager.addFilters(restOfFilters); diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index b62b728beca35..75deff23ce20d 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -204,11 +204,13 @@ export const fieldFormats = { export { IFieldFormat, + FieldFormatInstanceType, IFieldFormatsRegistry, FieldFormatsContentType, FieldFormatsGetConfigFn, FieldFormatConfig, FieldFormatId, + FieldFormat, } from '../common'; /* @@ -255,6 +257,7 @@ export { AggregationRestrictions as IndexPatternAggRestrictions, // TODO: exported only in stub_index_pattern test. Move into data plugin and remove export. FieldList as IndexPatternFieldList, + Field, } from './index_patterns'; export { @@ -285,14 +288,10 @@ export { import { // aggs - AggConfigs, - aggTypeFilters, - aggGroupNamesMap, CidrMask, - convertDateRangeToString, - convertIPRangeToString, - intervalOptions, // only used in Discover + intervalOptions, isDateHistogramBucketAggConfig, + isNumberType, isStringType, isType, parentPipelineType, @@ -322,26 +321,22 @@ export { ParsedInterval } from '../common'; export { // aggs + AggGroupLabels, + AggGroupName, AggGroupNames, - AggParam, // only the type is used externally, only in vis editor - AggParamOption, // only the type is used externally + AggParam, + AggParamOption, AggParamType, - AggTypeFieldFilters, // TODO convert to interface - AggTypeFilters, // TODO convert to interface AggConfigOptions, BUCKET_TYPES, - DateRangeKey, // only used in field formatter deserialization, which will live in data IAggConfig, IAggConfigs, - IAggGroupNames, IAggType, IFieldParamType, IMetricAggType, - IpRangeKey, // only used in field formatter deserialization, which will live in data METRIC_TYPES, - OptionedParamEditorProps, // only type is used externally OptionedParamType, - OptionedValueProp, // only type is used externally + OptionedValueProp, // search ES_SEARCH_STRATEGY, SYNC_SEARCH_STRATEGY, @@ -364,8 +359,6 @@ export { SearchResponse, SearchError, ISearchSource, - SearchSource, - createSearchSource, SearchSourceFields, EsQuerySortValue, SortDirection, @@ -381,17 +374,13 @@ export { // Search namespace export const search = { aggs: { - AggConfigs, - aggGroupNamesMap, - aggTypeFilters, CidrMask, - convertDateRangeToString, - convertIPRangeToString, dateHistogramInterval, - intervalOptions, // only used in Discover + intervalOptions, InvalidEsCalendarIntervalError, InvalidEsIntervalFormatError, - isDateHistogramBucketAggConfig, + isDateHistogramBucketAggConfig, // TODO: remove in build_pipeline refactor + isNumberType, isStringType, isType, isValidEsInterval, diff --git a/src/plugins/data/public/index_patterns/fields/field.ts b/src/plugins/data/public/index_patterns/fields/field.ts index 0fb92393d56f7..d83c0a7d3445e 100644 --- a/src/plugins/data/public/index_patterns/fields/field.ts +++ b/src/plugins/data/public/index_patterns/fields/field.ts @@ -47,6 +47,7 @@ export class Field implements IFieldType { indexPattern?: IndexPattern; format: any; $$spec: FieldSpec; + conflictDescriptions?: Record; constructor( indexPattern: IndexPattern, diff --git a/src/plugins/data/public/index_patterns/fields/field_list.ts b/src/plugins/data/public/index_patterns/fields/field_list.ts index d6067280fd7b6..9772370199b24 100644 --- a/src/plugins/data/public/index_patterns/fields/field_list.ts +++ b/src/plugins/data/public/index_patterns/fields/field_list.ts @@ -29,6 +29,7 @@ export interface IFieldList extends Array { getByType(type: Field['type']): Field[]; add(field: FieldSpec): void; remove(field: IFieldType): void; + update(field: FieldSpec): void; } export class FieldList extends Array implements IFieldList { diff --git a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx new file mode 100644 index 0000000000000..6b71739862f62 --- /dev/null +++ b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx @@ -0,0 +1,98 @@ +/* + * 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 { contains } from 'lodash'; +import React from 'react'; +import { History } from 'history'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import { toMountPoint } from '../../../../kibana_react/public'; +import { IndexPatternsContract } from './index_patterns'; + +export type EnsureDefaultIndexPattern = (history: History) => Promise | undefined; + +export const createEnsureDefaultIndexPattern = (core: CoreStart) => { + let bannerId: string; + let timeoutId: NodeJS.Timeout | undefined; + + /** + * Checks whether a default index pattern is set and exists and defines + * one otherwise. + * + * If there are no index patterns, redirect to management page and show + * banner. In this case the promise returned from this function will never + * resolve to wait for the URL change to happen. + */ + return async function ensureDefaultIndexPattern(this: IndexPatternsContract, history: History) { + const patterns = await this.getIds(); + let defaultId = core.uiSettings.get('defaultIndex'); + let defined = !!defaultId; + const exists = contains(patterns, defaultId); + + if (defined && !exists) { + core.uiSettings.remove('defaultIndex'); + defaultId = defined = false; + } + + if (defined) { + return; + } + + // If there is any index pattern created, set the first as default + if (patterns.length >= 1) { + defaultId = patterns[0]; + core.uiSettings.set('defaultIndex', defaultId); + } else { + const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; + const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + // Avoid being hostile to new users who don't have an index pattern setup yet + // give them a friendly info message instead of a terse error message + bannerId = core.overlays.banners.replace( + bannerId, + toMountPoint( + + ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + timeoutId = setTimeout(() => { + core.overlays.banners.remove(bannerId); + timeoutId = undefined; + }, 15000); + + history.push(redirectTarget); + + // return never-resolving promise to stop resolving and wait for the url change + return new Promise(() => {}); + } + }; +}; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index c429431b632bd..cf1f83d0e28cb 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -21,9 +21,9 @@ import { IndexPatternsService } from './index_patterns'; import { SavedObjectsClientContract, - IUiSettingsClient, HttpSetup, SavedObjectsFindResponsePublic, + CoreStart, } from 'kibana/public'; jest.mock('./index_pattern', () => { @@ -61,10 +61,10 @@ describe('IndexPatterns', () => { }) as Promise> ); - const uiSettings = {} as IUiSettingsClient; + const core = {} as CoreStart; const http = {} as HttpSetup; - indexPatterns = new IndexPatternsService(uiSettings, savedObjectsClient, http); + indexPatterns = new IndexPatternsService(core, savedObjectsClient, http); }); test('does cache gets for the same id', async () => { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index acce5ed57683c..b5d66a6aab60a 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -22,11 +22,16 @@ import { SimpleSavedObject, IUiSettingsClient, HttpStart, + CoreStart, } from 'src/core/public'; import { createIndexPatternCache } from './_pattern_cache'; import { IndexPattern } from './index_pattern'; import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_client'; +import { + createEnsureDefaultIndexPattern, + EnsureDefaultIndexPattern, +} from './ensure_default_index_pattern'; const indexPatternCache = createIndexPatternCache(); @@ -37,15 +42,13 @@ export class IndexPatternsService { private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array>> | null; private apiClient: IndexPatternsApiClient; + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; - constructor( - config: IUiSettingsClient, - savedObjectsClient: SavedObjectsClientContract, - http: HttpStart - ) { + constructor(core: CoreStart, savedObjectsClient: SavedObjectsClientContract, http: HttpStart) { this.apiClient = new IndexPatternsApiClient(http); - this.config = config; + this.config = core.uiSettings; this.savedObjectsClient = savedObjectsClient; + this.ensureDefaultIndexPattern = createEnsureDefaultIndexPattern(core); } private async refreshSavedObjectsCache() { diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 2d43cae79ac98..ba1df89c41358 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -45,7 +45,8 @@ const createStartContract = (): Start => { const queryStartMock = queryServiceMock.createStartContract(); return { actions: { - createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), + createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), + createFiltersFromRangeSelectAction: jest.fn(), }, autocomplete: autocompleteMock, search: searchStartMock, @@ -56,6 +57,7 @@ const createStartContract = (): Start => { SearchBar: jest.fn(), }, indexPatterns: ({ + ensureDefaultIndexPattern: jest.fn(), make: () => ({ fieldsFetcher: { fetchForWildcard: jest.fn(), @@ -67,7 +69,7 @@ const createStartContract = (): Start => { }; }; -export { searchSourceMock } from './search/mocks'; +export { createSearchSourceMock } from './search/mocks'; export { getCalculateAutoTimeExpression } from './search/aggs'; export const dataPluginMock = { diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 1723545b32522..f3a88287313a0 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -24,13 +24,13 @@ import { Plugin, PackageInfo, } from 'src/core/public'; -import { Storage, IStorageWrapper } from '../../kibana_utils/public'; +import { Storage, IStorageWrapper, createStartServicesGetter } from '../../kibana_utils/public'; import { DataPublicPluginSetup, DataPublicPluginStart, DataSetupDependencies, DataStartDependencies, - GetInternalStartServicesFn, + InternalStartServices, } from './types'; import { AutocompleteService } from './autocomplete'; import { SearchService } from './search/search_service'; @@ -48,8 +48,6 @@ import { setQueryService, setSearchService, setUiSettings, - getFieldFormats, - getNotifications, } from './services'; import { createSearchBar } from './ui/search_bar/create_search_bar'; import { esaggs } from './search/expressions'; @@ -58,7 +56,12 @@ import { VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, } from '../../ui_actions/public'; -import { ACTION_GLOBAL_APPLY_FILTER, createFilterAction, createFiltersFromEvent } from './actions'; +import { + ACTION_GLOBAL_APPLY_FILTER, + createFilterAction, + createFiltersFromValueClickAction, + createFiltersFromRangeSelectAction, +} from './actions'; import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action'; import { selectRangeAction, @@ -99,15 +102,21 @@ export class DataPublicPlugin implements Plugin { + const { core: coreStart, self }: any = startServices(); + return { + fieldFormats: self.fieldFormats, + notifications: coreStart.notifications, + uiSettings: coreStart.uiSettings, + searchService: self.search, + injectedMetadata: coreStart.injectedMetadata, + }; + }; expressions.registerFunction(esaggs); - const getInternalStartServices: GetInternalStartServicesFn = () => ({ - fieldFormats: getFieldFormats(), - notifications: getNotifications(), - }); - const queryService = this.queryService.setup({ uiSettings: core.uiSettings, storage: this.storage, @@ -130,6 +139,7 @@ export class DataPublicPlugin implements Plugin; - // (undocumented) - schema?: string; - // (undocumented) +export type AggConfigOptions = Assign; + +// Warning: (ae-missing-release-tag) "AggGroupLabels" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const AggGroupLabels: { + [AggGroupNames.Buckets]: string; + [AggGroupNames.Metrics]: string; + [AggGroupNames.None]: string; +}; + +// Warning: (ae-missing-release-tag) "AggGroupName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AggGroupName = $Values; // Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -112,40 +118,14 @@ export class AggParamType extends Ba // (undocumented) allowedAggs: string[]; // (undocumented) - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; } -// Warning: (ae-missing-release-tag) "AggTypeFieldFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFieldFilter" -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggType" -// -// @public -export class AggTypeFieldFilters { - // Warning: (ae-forgotten-export) The symbol "AggTypeFieldFilter" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFieldFilter" - addFilter(filter: AggTypeFieldFilter): void; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "any" - filter(fields: IndexPatternField_2[], aggConfig: IAggConfig): IndexPatternField_2[]; - } - -// Warning: (ae-missing-release-tag) "AggTypeFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFilter" -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggConfig" -// -// @public -export class AggTypeFilters { - // Warning: (ae-forgotten-export) The symbol "AggTypeFilter" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggTypeFilter" - addFilter(filter: AggTypeFilter): void; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AggType" - filter(aggTypes: IAggType[], indexPattern: IndexPattern, aggConfig: IAggConfig, aggFilter: string[]): IAggType[]; - } - // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[]; +export const baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[]; // Warning: (ae-missing-release-tag) "BUCKET_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -210,9 +190,6 @@ export const connectToQueryState: ({ timefilter: { timefil // @public (undocumented) export const createSavedQueryService: (savedObjectsClient: Pick) => SavedQueryService; -// @public -export const createSearchSource: (indexPatterns: Pick) => (searchSourceJson: string, references: SavedObjectReference[]) => Promise; - // Warning: (ae-missing-release-tag) "CustomFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -248,7 +225,8 @@ export interface DataPublicPluginSetup { export interface DataPublicPluginStart { // (undocumented) actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; // Warning: (ae-forgotten-export) The symbol "AutocompleteStart" needs to be exported by the entry point index.d.ts // @@ -275,16 +253,6 @@ export interface DataPublicPluginStart { }; } -// Warning: (ae-missing-release-tag) "DateRangeKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface DateRangeKey { - // (undocumented) - from: number; - // (undocumented) - to: number; -} - // @public (undocumented) export enum ES_FIELD_TYPES { // (undocumented) @@ -460,6 +428,97 @@ export interface FetchOptions { searchStrategyId?: string; } +// Warning: (ae-missing-release-tag) "Field" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class Field implements IFieldType { + // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + $$spec: FieldSpec; + constructor(indexPattern: IndexPattern, spec: FieldSpec | Field, shortDotsEnable?: boolean); + // (undocumented) + aggregatable?: boolean; + // (undocumented) + conflictDescriptions?: Record; + // (undocumented) + count?: number; + // (undocumented) + displayName?: string; + // (undocumented) + esTypes?: string[]; + // (undocumented) + filterable?: boolean; + // (undocumented) + format: any; + // (undocumented) + indexPattern?: IndexPattern; + // (undocumented) + lang?: string; + // (undocumented) + name: string; + // (undocumented) + script?: string; + // (undocumented) + scripted?: boolean; + // (undocumented) + searchable?: boolean; + // (undocumented) + sortable?: boolean; + // (undocumented) + subType?: IFieldSubType; + // (undocumented) + type: string; + // (undocumented) + visualizable?: boolean; +} + +export { Field } + +export { Field as IndexPatternField } + +// Warning: (ae-missing-release-tag) "FieldFormat" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export abstract class FieldFormat { + // Warning: (ae-forgotten-export) The symbol "IFieldFormatMetaParams" needs to be exported by the entry point index.d.ts + constructor(_params?: IFieldFormatMetaParams, getConfig?: FieldFormatsGetConfigFn); + // Warning: (ae-forgotten-export) The symbol "HtmlContextTypeOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "TextContextTypeOptions" needs to be exported by the entry point index.d.ts + convert(value: any, contentType?: FieldFormatsContentType, options?: HtmlContextTypeOptions | TextContextTypeOptions): string; + // Warning: (ae-forgotten-export) The symbol "FieldFormatConvert" needs to be exported by the entry point index.d.ts + convertObject: FieldFormatConvert | undefined; + static fieldType: string | string[]; + // Warning: (ae-incompatible-release-tags) The symbol "from" is marked as @public, but its signature references "FieldFormatInstanceType" which is marked as @internal + // + // (undocumented) + static from(convertFn: FieldFormatConvertFunction): FieldFormatInstanceType; + // (undocumented) + protected getConfig: FieldFormatsGetConfigFn | undefined; + // Warning: (ae-forgotten-export) The symbol "FieldFormatConvertFunction" needs to be exported by the entry point index.d.ts + getConverterFor(contentType?: FieldFormatsContentType): FieldFormatConvertFunction; + getParamDefaults(): Record; + // Warning: (ae-forgotten-export) The symbol "HtmlContextTypeConvert" needs to be exported by the entry point index.d.ts + htmlConvert: HtmlContextTypeConvert | undefined; + static id: string; + // (undocumented) + static isInstanceOfFieldFormat(fieldFormat: any): fieldFormat is FieldFormat; + param(name: string): any; + params(): Record; + // (undocumented) + protected readonly _params: any; + // (undocumented) + setupContentType(): FieldFormatConvert; + // Warning: (ae-forgotten-export) The symbol "TextContextTypeConvert" needs to be exported by the entry point index.d.ts + textConvert: TextContextTypeConvert | undefined; + static title: string; + toJSON(): { + id: unknown; + params: _.Dictionary | undefined; + }; + type: any; +} + // Warning: (ae-missing-release-tag) "FieldFormatConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -478,13 +537,20 @@ export interface FieldFormatConfig { // @public export type FieldFormatId = FIELD_FORMAT_IDS | string; +// @internal (undocumented) +export type FieldFormatInstanceType = (new (params?: any, getConfig?: FieldFormatsGetConfigFn) => FieldFormat) & { + id: FieldFormatId; + title: string; + fieldType: string | string[]; +}; + // Warning: (ae-missing-release-tag) "fieldFormats" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const fieldFormats: { FieldFormat: typeof FieldFormat; FieldFormatsRegistry: typeof FieldFormatsRegistry; - serialize: (agg: import("./search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; @@ -610,7 +676,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) // @@ -622,11 +691,6 @@ export type IAggConfig = AggConfig; // @internal export type IAggConfigs = AggConfigs; -// Warning: (ae-missing-release-tag) "IAggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type IAggGroupNames = $Values; - // Warning: (ae-forgotten-export) The symbol "AggType" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -636,21 +700,21 @@ export type IAggType = AggType; // Warning: (ae-missing-release-tag) "IDataPluginServices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface IDataPluginServices extends Partial { +export interface IDataPluginServices extends Partial { // (undocumented) appName: string; // (undocumented) data: DataPublicPluginStart; // (undocumented) - http: CoreStart['http']; + http: CoreStart_2['http']; // (undocumented) - notifications: CoreStart['notifications']; + notifications: CoreStart_2['notifications']; // (undocumented) - savedObjects: CoreStart['savedObjects']; + savedObjects: CoreStart_2['savedObjects']; // (undocumented) storage: IStorageWrapper; // (undocumented) - uiSettings: CoreStart['uiSettings']; + uiSettings: CoreStart_2['uiSettings']; } // Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -753,6 +817,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; @@ -834,17 +900,17 @@ export class IndexPattern implements IIndexPattern { }[]; }; // (undocumented) - getFieldByName(name: string): IndexPatternField | void; + getFieldByName(name: string): Field | void; // (undocumented) - getNonScriptedFields(): IndexPatternField[]; + getNonScriptedFields(): Field[]; // (undocumented) - getScriptedFields(): IndexPatternField[]; + getScriptedFields(): Field[]; // (undocumented) getSourceFiltering(): { excludes: any[]; }; // (undocumented) - getTimeField(): IndexPatternField | undefined; + getTimeField(): Field | undefined; // (undocumented) id?: string; // (undocumented) @@ -921,58 +987,15 @@ export interface IndexPatternAttributes { typeMeta: string; } -// Warning: (ae-missing-release-tag) "Field" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class IndexPatternField implements IFieldType { - // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - $$spec: FieldSpec; - constructor(indexPattern: IndexPattern, spec: FieldSpec | IndexPatternField, shortDotsEnable?: boolean); - // (undocumented) - aggregatable?: boolean; - // (undocumented) - count?: number; - // (undocumented) - displayName?: string; - // (undocumented) - esTypes?: string[]; - // (undocumented) - filterable?: boolean; - // (undocumented) - format: any; - // (undocumented) - indexPattern?: IndexPattern; - // (undocumented) - lang?: string; - // (undocumented) - name: string; - // (undocumented) - script?: string; - // (undocumented) - scripted?: boolean; - // (undocumented) - searchable?: boolean; - // (undocumented) - sortable?: boolean; - // (undocumented) - subType?: IFieldSubType; - // (undocumented) - type: string; - // (undocumented) - visualizable?: boolean; -} - // Warning: (ae-missing-release-tag) "FieldList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class IndexPatternFieldList extends Array implements IFieldList { +export class IndexPatternFieldList extends Array implements IFieldList { constructor(indexPattern: IndexPattern, specs?: FieldSpec[], shortDotsEnable?: boolean); // (undocumented) add: (field: Record) => void; // (undocumented) - getByName: (name: string) => IndexPatternField | undefined; + getByName: (name: string) => Field | undefined; // (undocumented) getByType: (type: string) => any[]; // (undocumented) @@ -1050,18 +1073,6 @@ export type InputTimeRange = TimeRange | { to: Moment; }; -// Warning: (ae-missing-release-tag) "IpRangeKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type IpRangeKey = { - type: 'mask'; - mask: string; -} | { - type: 'range'; - from: string; - to: string; -}; - // Warning: (ae-missing-release-tag) "IRequestTypesMap" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1098,7 +1109,7 @@ export type ISearch = // @public (undocumented) export interface ISearchContext { // (undocumented) - core: CoreStart_2; + core: CoreStart; // (undocumented) getSearchStrategy: (name: T) => TSearchStrategyProvider; } @@ -1116,7 +1127,7 @@ export interface ISearchOptions { signal?: AbortSignal; } -// Warning: (ae-missing-release-tag) "ISearchSource" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-forgotten-export) The symbol "SearchSource" needs to be exported by the entry point index.d.ts // // @public (undocumented) export type ISearchSource = Pick; @@ -1239,16 +1250,6 @@ export enum METRIC_TYPES { TOP_HITS = "top_hits" } -// Warning: (ae-missing-release-tag) "OptionedParamEditorProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface OptionedParamEditorProps { - // (undocumented) - aggParam: { - options: T[]; - }; -} - // Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1311,7 +1312,7 @@ export class Plugin implements Plugin_2 Record<"metrics" | "buckets", string>; - aggTypeFilters: import("./search/aggs/filter/agg_type_filters").AggTypeFilters; CidrMask: typeof CidrMask; - convertDateRangeToString: typeof convertDateRangeToString; - convertIPRangeToString: (range: import("./search").IpRangeKey, format: (val: any) => string) => string; dateHistogramInterval: typeof dateHistogramInterval; intervalOptions: ({ display: string; @@ -1545,8 +1541,9 @@ export const search: { InvalidEsCalendarIntervalError: typeof InvalidEsCalendarIntervalError; InvalidEsIntervalFormatError: typeof InvalidEsIntervalFormatError; isDateHistogramBucketAggConfig: typeof isDateHistogramBucketAggConfig; + isNumberType: (agg: import("./search").AggConfig) => boolean; isStringType: (agg: import("./search").AggConfig) => boolean; - isType: (type: string) => (agg: import("./search").AggConfig) => boolean; + isType: (...types: string[]) => (agg: import("./search").AggConfig) => boolean; isValidEsInterval: typeof isValidEsInterval; isValidInterval: typeof isValidInterval; parentPipelineType: string; @@ -1629,61 +1626,6 @@ export type SearchRequest = any; // @public (undocumented) export type SearchResponse = any; -// Warning: (ae-missing-release-tag) "SearchSource" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class SearchSource { - constructor(fields?: SearchSourceFields); - // (undocumented) - create(): SearchSource; - // (undocumented) - createChild(options?: {}): SearchSource; - // (undocumented) - createCopy(): SearchSource; - destroy(): void; - fetch(options?: FetchOptions): Promise; - getField(field: K, recurse?: boolean): SearchSourceFields[K]; - // (undocumented) - getFields(): { - type?: string | undefined; - query?: import("../..").Query | undefined; - filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; - sort?: Record | Record[] | undefined; - highlight?: any; - highlightAll?: boolean | undefined; - aggs?: any; - from?: number | undefined; - size?: number | undefined; - source?: string | boolean | string[] | undefined; - version?: boolean | undefined; - fields?: string | boolean | string[] | undefined; - index?: import("../..").IndexPattern | undefined; - searchAfter?: import("./types").EsQuerySearchAfter | undefined; - timeout?: string | undefined; - terminate_after?: number | undefined; - }; - // (undocumented) - getId(): string; - getOwnField(field: K): SearchSourceFields[K]; - getParent(): SearchSource | undefined; - // (undocumented) - getSearchRequestBody(): Promise; - // (undocumented) - history: SearchRequest[]; - onRequestStart(handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise): void; - serialize(): { - searchSourceJSON: string; - references: SavedObjectReference[]; - }; - // (undocumented) - setField(field: K, value: SearchSourceFields[K]): this; - // (undocumented) - setFields(newFields: SearchSourceFields): this; - // Warning: (ae-forgotten-export) The symbol "SearchSourceOptions" needs to be exported by the entry point index.d.ts - setParent(parent?: ISearchSource, options?: SearchSourceOptions): this; - setPreferredSearchStrategyId(searchStrategyId: string): void; -} - // Warning: (ae-missing-release-tag) "SearchSourceFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1851,7 +1793,6 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "luceneStringToDsl" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:135:21 - (ae-forgotten-export) The symbol "decorateQuery" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "FieldFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "FieldFormatsRegistry" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BoolFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "BytesFormat" needs to be exported by the entry point index.d.ts @@ -1867,32 +1808,32 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:177:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "convertDateRangeToString" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getRoutes" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:375:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:377:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:378:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:389:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:60:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:61:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) 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 a8eb3a3fe8102..4dba157a6f554 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 fa15406189041..9cdd25d3213ce 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 a6260e782c12f..034af03842ab8 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 4fbdac47fb3b0..86ef69be572a9 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/agg_config.test.ts b/src/plugins/data/public/search/aggs/agg_config.test.ts index 2813e3b9c5373..b5df90313230c 100644 --- a/src/plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/plugins/data/public/search/aggs/agg_config.test.ts @@ -24,18 +24,21 @@ import { AggConfigs, CreateAggConfigParams } from './agg_configs'; import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { MetricAggType } from './metrics/metric_agg_type'; import { Field as IndexPatternField, IndexPattern } from '../../index_patterns'; import { stubIndexPatternWithFields } from '../../../public/stubs'; +import { FieldFormatsStart } from '../../field_formats'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('AggConfig', () => { let indexPattern: IndexPattern; let typesRegistry: AggTypesRegistryStart; - const fieldFormats = fieldFormatsServiceMock.createStartContract(); + let fieldFormats: FieldFormatsStart; beforeEach(() => { jest.restoreAllMocks(); mockDataServices(); + fieldFormats = fieldFormatsServiceMock.createStartContract(); indexPattern = stubIndexPatternWithFields as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); @@ -325,7 +328,7 @@ describe('AggConfig', () => { }); }); - describe('#toJSON', () => { + describe('#serialize', () => { it('includes the aggs id, params, type and schema', () => { const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { @@ -342,7 +345,7 @@ describe('AggConfig', () => { expect(aggConfig.type).toHaveProperty('name', 'date_histogram'); expect(typeof aggConfig.schema).toBe('string'); - const state = aggConfig.toJSON(); + const state = aggConfig.serialize(); expect(state).toHaveProperty('id', '1'); expect(typeof state.params).toBe('object'); expect(state).toHaveProperty('type', 'date_histogram'); @@ -367,6 +370,201 @@ describe('AggConfig', () => { }); }); + describe('#toExpressionAst', () => { + beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; + indexPattern.fields.getByName = name => + ({ + format: { + getConverterFor: (t?: string) => t || identity, + }, + } as IndexPatternField); + }); + + it('works with primitive param types', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'machine.os.keyword', + order: 'asc', + }, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "segment", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + } + `); + }); + + it('creates a subexpression for params of type "agg"', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + orderAgg: { + enabled: true, + type: 'terms', + params: { + field: 'bytes', + order: 'asc', + size: 5, + }, + }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const aggArg = aggConfig.toExpressionAst()?.arguments.orderAgg; + expect(aggArg).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1-orderAgg", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "orderAgg", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => { + // Overwrite the `ranges` param in the `range` agg with a mock toExpressionAst function + const range: MetricAggType = typesRegistry.get('range'); + range.expressionName = 'aggRange'; + const rangesParam = range.params.find(p => p.name === 'ranges'); + rangesParam!.toExpressionAst = (val: any) => ({ + type: 'function', + function: 'aggRanges', + arguments: { + ranges: ['oh hi there!'], + }, + }); + + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'range', + params: { + field: 'bytes', + }, + }; + + const aggConfig = ac.createAggConfig(configStates); + const ranges = aggConfig.toExpressionAst()!.arguments.ranges; + expect(ranges).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "ranges": Array [ + "oh hi there!", + ], + }, + "function": "aggRanges", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('stringifies any other params which are an object', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + json: { foo: 'bar' }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const json = aggConfig.toExpressionAst()?.arguments.json; + expect(json).toEqual([JSON.stringify(configStates.params.json)]); + }); + + it(`returns undefined if an expressionName doesn't exist on the agg type`, () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'unknown type', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toBe(undefined); + }); + }); + describe('#makeLabel', () => { let aggConfig: AggConfig; @@ -422,6 +620,9 @@ describe('AggConfig', () => { let aggConfig: AggConfig; beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; indexPattern.fields.getByName = name => ({ format: { @@ -434,11 +635,7 @@ describe('AggConfig', () => { type: 'histogram', schema: 'bucket', params: { - field: { - format: { - getConverterFor: (t?: string) => t || identity, - }, - }, + field: 'bytes', }, }; const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry, fieldFormats }); @@ -446,6 +643,11 @@ describe('AggConfig', () => { }); it("returns the field's formatter", () => { + aggConfig.params.field = { + format: { + getConverterFor: (t?: string) => t || identity, + }, + }; expect(aggConfig.fieldFormatter().toString()).toBe( aggConfig .getField() diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index 6188849e0e6d4..973c69e3d4f5f 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -19,6 +19,8 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; @@ -27,11 +29,17 @@ import { ISearchSource } from '../search_source'; import { FieldFormatsContentType, KBN_FIELD_TYPES } from '../../../common'; import { FieldFormatsStart } from '../../field_formats'; -export interface AggConfigOptions { - type: IAggType; +type State = string | number | boolean | null | undefined | SerializableState; + +interface SerializableState { + [key: string]: State | State[]; +} + +export interface AggConfigSerialized { + type: string; enabled?: boolean; id?: string; - params?: Record; + params?: SerializableState; schema?: string; } @@ -39,6 +47,8 @@ export interface AggConfigDependencies { fieldFormats: FieldFormatsStart; } +export type AggConfigOptions = Assign; + /** * @name AggConfig * @@ -257,7 +267,10 @@ export class AggConfig { return configDsl; } - toJSON() { + /** + * @returns Returns a serialized representation of an AggConfig. + */ + serialize(): AggConfigSerialized { const params = this.params; const outParams = _.transform( @@ -281,7 +294,64 @@ export class AggConfig { enabled: this.enabled, type: this.type && this.type.name, schema: this.schema, - params: outParams, + params: outParams as SerializableState, + }; + } + + /** + * @deprecated - Use serialize() instead. + */ + toJSON(): AggConfigSerialized { + return this.serialize(); + } + + /** + * @returns Returns an ExpressionAst representing the function for this agg type. + */ + toExpressionAst(): ExpressionAstFunction | undefined { + const functionName = this.type && this.type.expressionName; + const { type, ...rest } = this.serialize(); + if (!functionName || !rest.params) { + // Return undefined - there is no matching expression function for this agg + return; + } + + // Go through each of the params and convert to an array of expression args. + const params = Object.entries(rest.params).reduce((acc, [key, value]) => { + const deserializedParam = this.getAggParams().find(p => p.name === key); + + if (deserializedParam && deserializedParam.toExpressionAst) { + // If the param provides `toExpressionAst`, we call it with the value + const paramExpressionAst = deserializedParam.toExpressionAst(this.getParam(key)); + if (paramExpressionAst) { + acc[key] = [ + { + type: 'expression', + chain: [paramExpressionAst], + }, + ]; + } + } else if (typeof value === 'object') { + // For object params which don't provide `toExpressionAst`, we stringify + acc[key] = [JSON.stringify(value)]; + } else if (typeof value !== 'undefined') { + // Everything else just gets stored in an array if it is defined + acc[key] = [value]; + } + + return acc; + }, {} as Record); + + return { + type: 'function', + function: functionName, + arguments: { + ...params, + // Expression args which are provided to all functions + id: [this.id], + enabled: [this.enabled], + ...(this.schema ? { schema: [this.schema] } : {}), // schema may be undefined + }, }; } diff --git a/src/plugins/data/public/search/aggs/agg_configs.ts b/src/plugins/data/public/search/aggs/agg_configs.ts index 5ad09f824d3e4..d2151a2c5ed4d 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { Assign } from '@kbn/utility-types'; -import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; +import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; @@ -51,7 +51,7 @@ export interface AggConfigsOptions { fieldFormats: FieldFormatsStart; } -export type CreateAggConfigParams = Assign; +export type CreateAggConfigParams = Assign; /** * @name AggConfigs diff --git a/src/plugins/data/public/search/aggs/agg_groups.ts b/src/plugins/data/public/search/aggs/agg_groups.ts index 9cebff76c9684..dec3397126e67 100644 --- a/src/plugins/data/public/search/aggs/agg_groups.ts +++ b/src/plugins/data/public/search/aggs/agg_groups.ts @@ -25,15 +25,17 @@ export const AggGroupNames = Object.freeze({ Metrics: 'metrics' as 'metrics', None: 'none' as 'none', }); -export type IAggGroupNames = $Values; -type IAggGroupNamesMap = () => Record<'buckets' | 'metrics', string>; +export type AggGroupName = $Values; -export const aggGroupNamesMap: IAggGroupNamesMap = () => ({ +export const AggGroupLabels = { + [AggGroupNames.Buckets]: i18n.translate('data.search.aggs.aggGroups.bucketsText', { + defaultMessage: 'Buckets', + }), [AggGroupNames.Metrics]: i18n.translate('data.search.aggs.aggGroups.metricsText', { defaultMessage: 'Metrics', }), - [AggGroupNames.Buckets]: i18n.translate('data.search.aggs.aggGroups.bucketsText', { - defaultMessage: 'Buckets', + [AggGroupNames.None]: i18n.translate('data.search.aggs.aggGroups.noneText', { + defaultMessage: 'None', }), -}); +}; diff --git a/src/plugins/data/public/search/aggs/agg_params.test.ts b/src/plugins/data/public/search/aggs/agg_params.test.ts index 784be803e2644..e116bdca157ff 100644 --- a/src/plugins/data/public/search/aggs/agg_params.test.ts +++ b/src/plugins/data/public/search/aggs/agg_params.test.ts @@ -25,13 +25,15 @@ import { AggParamType } from '../aggs/param_types/agg'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../src/core/public/mocks'; import { AggTypeDependencies } from './agg_type'; +import { InternalStartServices } from '../../types'; describe('AggParams class', () => { const aggTypesDependencies: AggTypeDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; describe('constructor args', () => { diff --git a/src/plugins/data/public/search/aggs/agg_type.test.ts b/src/plugins/data/public/search/aggs/agg_type.test.ts index 0c9e110c34ae6..369ae0ce0b3a5 100644 --- a/src/plugins/data/public/search/aggs/agg_type.test.ts +++ b/src/plugins/data/public/search/aggs/agg_type.test.ts @@ -21,19 +21,21 @@ import { AggType, AggTypeConfig, AggTypeDependencies } from './agg_type'; import { IAggConfig } from './agg_config'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../types'; describe('AggType Class', () => { let dependencies: AggTypeDependencies; beforeEach(() => { dependencies = { - getInternalStartServices: () => ({ - fieldFormats: { - ...fieldFormatsServiceMock.createStartContract(), - getDefaultInstance: jest.fn(() => 'default') as any, - }, - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: { + ...fieldFormatsServiceMock.createStartContract(), + getDefaultInstance: jest.fn(() => 'default') as any, + }, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/public/search/aggs/agg_type.ts index 70c116d560c6f..fb0cb609a08cf 100644 --- a/src/plugins/data/public/search/aggs/agg_type.ts +++ b/src/plugins/data/public/search/aggs/agg_type.ts @@ -39,6 +39,7 @@ export interface AggTypeConfig< createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any; type?: string; dslName?: string; + expressionName?: string; makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string); ordered?: any; hasNoDsl?: boolean; @@ -88,6 +89,14 @@ export class AggType< * @type {string} */ dslName: string; + /** + * the name of the expression function that this aggType represents. + * TODO: this should probably be a required field. + * + * @property name + * @type {string} + */ + expressionName?: string; /** * the user friendly name that will be shown in the ui for this aggType * @@ -219,6 +228,7 @@ export class AggType< this.name = config.name; this.type = config.type || 'metrics'; this.dslName = config.dslName || config.name; + this.expressionName = config.expressionName; this.title = config.title; this.makeLabel = config.makeLabel || constant(this.name); this.ordered = config.ordered; diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts index 4b154c338d48c..da07f581c9274 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/public/search/aggs/agg_types.ts @@ -37,6 +37,7 @@ import { getDerivativeMetricAgg } from './metrics/derivative'; import { getCumulativeSumMetricAgg } from './metrics/cumulative_sum'; import { getMovingAvgMetricAgg } from './metrics/moving_avg'; import { getSerialDiffMetricAgg } from './metrics/serial_diff'; + import { getDateHistogramBucketAgg } from './buckets/date_histogram'; import { getHistogramBucketAgg } from './buckets/histogram'; import { getRangeBucketAgg } from './buckets/range'; @@ -103,3 +104,7 @@ export const getAggTypes = ({ getGeoTitleBucketAgg({ getInternalStartServices }), ], }); + +import { aggTerms } from './buckets/terms_fn'; + +export const getAggTypesFunctions = () => [aggTerms]; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts index 7778fcb36bcd6..bb73c8a39df19 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_histogram.test.ts @@ -32,6 +32,7 @@ import { RangeFilter } from '../../../../../common'; import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; import { queryServiceMock } from '../../../../query/mocks'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('date_histogram', () => { @@ -47,10 +48,11 @@ describe('AggConfig Filters', () => { aggTypesDependencies = { uiSettings, query: queryServiceMock.createSetupContract(), - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; mockDataServices(); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts index 4207fa92736f8..0d66d9cfcdca2 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/date_range.test.ts @@ -28,6 +28,7 @@ import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('Date range', () => { @@ -38,10 +39,11 @@ describe('AggConfig Filters', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts index bf05f7463db6c..0fdb07cc4198a 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.test.ts @@ -24,6 +24,7 @@ import { mockAggTypesRegistry } from '../../test_helpers'; import { IBucketAggConfig } from '../bucket_agg_type'; import { coreMock, notificationServiceMock } from '../../../../../../../core/public/mocks'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('filters', () => { @@ -34,10 +35,11 @@ describe('AggConfig Filters', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts index 1999b759a23d0..72d2029a12b0d 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/filters.ts @@ -28,6 +28,6 @@ export const createFilterFilters = (aggConfig: IBucketAggConfig, key: string) => const indexPattern = aggConfig.getIndexPattern(); if (filter && indexPattern && indexPattern.id) { - return buildQueryFilter(filter.query, indexPattern.id, key); + return buildQueryFilter(filter, indexPattern.id, key); } }; diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts index d85576a0ccb14..990adde5f8a0b 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/ip_range.test.ts @@ -26,16 +26,18 @@ import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../core/public/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('IP range', () => { const fieldFormats = fieldFormatsServiceMock.createStartContract(); const typesRegistry = mockAggTypesRegistry([ getIpRangeBucketAgg({ - getInternalStartServices: () => ({ - fieldFormats, - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }), ]); const getAggConfigs = (aggs: CreateAggConfigParams[]) => { diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts index cadd8e9fe13ed..564e7b4763c8d 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/range.test.ts @@ -26,6 +26,7 @@ import { BUCKET_TYPES } from '../bucket_agg_types'; import { IBucketAggConfig } from '../bucket_agg_type'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../core/public/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('range', () => { @@ -33,10 +34,11 @@ describe('AggConfig Filters', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; mockDataServices(); diff --git a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts index d9ff63613b640..36e4bef025ef9 100644 --- a/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/create_filter/terms.test.ts @@ -27,6 +27,7 @@ import { Filter, ExistsFilter } from '../../../../../common'; import { RangeBucketAggDependencies } from '../range'; import { fieldFormatsServiceMock } from '../../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../core/public/mocks'; +import { InternalStartServices } from '../../../../types'; describe('AggConfig Filters', () => { describe('terms', () => { @@ -34,10 +35,11 @@ describe('AggConfig Filters', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); 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 57f3aa85ad944..3ecdc17cb57f3 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/aggs/buckets/date_range.test.ts b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts index f78f0cce732e7..e1881c3bbc7f4 100644 --- a/src/plugins/data/public/search/aggs/buckets/date_range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/date_range.test.ts @@ -23,6 +23,7 @@ import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { BUCKET_TYPES } from './bucket_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; +import { InternalStartServices } from '../../../types'; describe('date_range params', () => { let aggTypesDependencies: DateRangeBucketAggDependencies; @@ -32,10 +33,11 @@ describe('date_range params', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index a42cb70a62b7d..fe013928bba65 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -107,7 +107,7 @@ export const getFiltersBucketAgg = ({ (typeof filter.input.query === 'string' ? filter.input.query : toAngularJSON(filter.input.query)); - filters[label] = { query }; + filters[label] = query; }, {} ); diff --git a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts index 226faefe43482..877a817984dc6 100644 --- a/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/geo_hash.test.ts @@ -24,6 +24,7 @@ import { BUCKET_TYPES } from './bucket_agg_types'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; +import { InternalStartServices } from '../../../types'; describe('Geohash Agg', () => { let aggTypesDependencies: GeoHashBucketAggDependencies; @@ -31,10 +32,11 @@ describe('Geohash Agg', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; geoHashBucketAgg = getGeoHashBucketAgg(aggTypesDependencies); diff --git a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts index a55c32951232a..4756669f5b4b3 100644 --- a/src/plugins/data/public/search/aggs/buckets/histogram.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/histogram.test.ts @@ -29,6 +29,7 @@ import { } from './histogram'; import { BucketAggType } from './bucket_agg_type'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; +import { InternalStartServices } from '../../../types'; describe('Histogram Agg', () => { let aggTypesDependencies: HistogramBucketAggDependencies; @@ -38,10 +39,11 @@ describe('Histogram Agg', () => { aggTypesDependencies = { uiSettings, - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts b/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts index 0beeb1c372275..116f8cfad60f6 100644 --- a/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts +++ b/src/plugins/data/public/search/aggs/buckets/migrate_include_exclude_format.ts @@ -21,20 +21,22 @@ import { isString, isObject } from 'lodash'; import { IBucketAggConfig, BucketAggType, BucketAggParam } from './bucket_agg_type'; import { IAggConfig } from '../agg_config'; -export const isType = (type: string) => { +export const isType = (...types: string[]) => { return (agg: IAggConfig): boolean => { const field = agg.params.field; - return field && field.type === type; + return types.some(type => field && field.type === type); }; }; +export const isNumberType = isType('number'); export const isStringType = isType('string'); +export const isStringOrNumberType = isType('string', 'number'); export const migrateIncludeExcludeFormat = { serialize(this: BucketAggParam, value: any, agg: IBucketAggConfig) { if (this.shouldShow && !this.shouldShow(agg)) return; - if (!value || isString(value)) return value; + if (!value || isString(value) || Array.isArray(value)) return value; else return value.pattern; }, write( @@ -44,7 +46,12 @@ export const migrateIncludeExcludeFormat = { ) { const value = aggConfig.getParam(this.name); - if (isObject(value)) { + if (Array.isArray(value) && value.length > 0 && isNumberType(aggConfig)) { + const parsedValue = value.filter((val): val is number => Number.isFinite(val)); + if (parsedValue.length) { + output.params[this.name] = parsedValue; + } + } else if (isObject(value)) { output.params[this.name] = value.pattern; } else if (value && isStringType(aggConfig)) { output.params[this.name] = value; diff --git a/src/plugins/data/public/search/aggs/buckets/range.test.ts b/src/plugins/data/public/search/aggs/buckets/range.test.ts index 144d2b779e950..4c2d3af1ab734 100644 --- a/src/plugins/data/public/search/aggs/buckets/range.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/range.test.ts @@ -24,6 +24,7 @@ import { BUCKET_TYPES } from './bucket_agg_types'; import { FieldFormatsGetConfigFn, NumberFormat } from '../../../../common'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; const buckets = [ { @@ -50,10 +51,11 @@ describe('Range Agg', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; mockDataServices(); diff --git a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts index d0ace5a50c28d..156f7f8108482 100644 --- a/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/significant_terms.test.ts @@ -26,6 +26,7 @@ import { } from './significant_terms'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('Significant Terms Agg', () => { describe('order agg editor UI', () => { @@ -34,10 +35,11 @@ describe('Significant Terms Agg', () => { beforeEach(() => { aggTypesDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; }); diff --git a/src/plugins/data/public/search/aggs/buckets/terms.test.ts b/src/plugins/data/public/search/aggs/buckets/terms.test.ts index 0dc052bd1fdf6..9769efb6da749 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.test.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.test.ts @@ -75,5 +75,65 @@ describe('Terms Agg', () => { expect(params.include).toBe('404'); expect(params.exclude).toBe('400'); }); + + test('accepts string from string field type and writes this value', () => { + const aggConfigs = getAggConfigs({ + include: 'include value', + exclude: 'exclude value', + field: { + name: 'string_field', + type: 'string', + }, + orderAgg: { + type: 'count', + }, + }); + + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params.field).toBe('string_field'); + expect(params.include).toBe('include value'); + expect(params.exclude).toBe('exclude value'); + }); + + test('accepts empty array from number field type and does not write a value', () => { + const aggConfigs = getAggConfigs({ + include: [], + exclude: [], + field: { + name: 'empty_number_field', + type: 'number', + }, + orderAgg: { + type: 'count', + }, + }); + + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params.field).toBe('empty_number_field'); + expect(params.include).toBe(undefined); + expect(params.exclude).toBe(undefined); + }); + + test('filters array with empty strings from number field type and writes only numbers', () => { + const aggConfigs = getAggConfigs({ + include: [1.1, 2, '', 3.33, ''], + exclude: ['', 4, 5.555, '', 6], + field: { + name: 'number_field', + type: 'number', + }, + orderAgg: { + type: 'count', + }, + }); + + const { [BUCKET_TYPES.TERMS]: params } = aggConfigs.aggs[0].toDsl(); + + expect(params.field).toBe('number_field'); + expect(params.include).toStrictEqual([1.1, 2, 3.33]); + expect(params.exclude).toStrictEqual([4, 5.555, 6]); + }); }); }); diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index 5baa38af0e8d6..a12a1d7ac2d3d 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -22,8 +22,11 @@ import { i18n } from '@kbn/i18n'; import { BucketAggType, IBucketAggConfig } from './bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterTerms } from './create_filter/terms'; -import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; -import { IAggConfigs } from '../agg_configs'; +import { + isStringOrNumberType, + migrateIncludeExcludeFormat, +} from './migrate_include_exclude_format'; +import { AggConfigSerialized, IAggConfigs } from '../types'; import { Adapters } from '../../../../../inspector/public'; import { ISearchSource } from '../../search_source'; @@ -60,10 +63,27 @@ export interface TermsBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsTerms { + field: string; + order: 'asc' | 'desc'; + orderBy: string; + orderAgg?: AggConfigSerialized; + size?: number; + missingBucket?: boolean; + missingBucketLabel?: string; + otherBucket?: boolean; + otherBucketLabel?: string; + // advanced + exclude?: string; + include?: string; + json?: string; +} + export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) => new BucketAggType( { name: BUCKET_TYPES.TERMS, + expressionName: 'aggTerms', title: termsTitle, makeLabel(agg) { const params = agg.params; @@ -151,8 +171,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe type: 'agg', allowedAggs: termsAggFilter, default: null, - makeAgg(termsAgg, state) { - state = state || {}; + makeAgg(termsAgg, state = { type: 'count' }) { state.schema = 'orderAgg'; const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false, @@ -266,7 +285,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe }), type: 'string', advanced: true, - shouldShow: isStringType, + shouldShow: isStringOrNumberType, ...migrateIncludeExcludeFormat, }, { @@ -276,7 +295,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe }), type: 'string', advanced: true, - shouldShow: isStringType, + shouldShow: isStringOrNumberType, ...migrateIncludeExcludeFormat, }, ], diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts new file mode 100644 index 0000000000000..f55f1de796013 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts @@ -0,0 +1,164 @@ +/* + * 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 { functionWrapper } from '../test_helpers'; +import { aggTerms } from './terms_fn'; + +describe('agg_expression_functions', () => { + describe('aggTerms', () => { + const fn = functionWrapper(aggTerms()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + order: 'desc', + orderBy: '2', + size: 6, + missingBucket: true, + missingBucketLabel: 'missing', + otherBucket: true, + otherBucketLabel: 'other', + exclude: 'ios', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "exclude": "ios", + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": true, + "missingBucketLabel": "missing", + "order": "desc", + "orderAgg": undefined, + "orderBy": "2", + "otherBucket": true, + "otherBucketLabel": "other", + "size": 6, + }, + "schema": "whatever", + "type": "terms", + } + `); + }); + + test('handles orderAgg as a subexpression', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + orderAgg: fn({ field: 'name', order: 'asc', orderBy: '1' }), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "name", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts new file mode 100644 index 0000000000000..7980bfabe79fb --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts @@ -0,0 +1,181 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs } from '../'; + +const aggName = 'terms'; +const fnName = 'aggTerms'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +// Since the orderAgg param is an agg nested in a subexpression, we need to +// overwrite the param type to expect a value of type AggExpressionType. +type Arguments = AggArgs & + Assign< + AggArgs, + { orderAgg?: AggArgs['orderAgg'] extends undefined ? undefined : AggExpressionType } + >; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggTerms = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.terms.help', { + defaultMessage: 'Generates a serialized agg config for a terms agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.terms.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + order: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.order.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + orderBy: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.orderBy.help', { + defaultMessage: 'Field to order results by', + }), + }, + orderAgg: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.buckets.terms.orderAgg.help', { + defaultMessage: 'Agg config to use for ordering results', + }), + }, + size: { + types: ['number'], + default: 5, + help: i18n.translate('data.search.aggs.buckets.terms.size.help', { + defaultMessage: 'Max number of buckets to retrieve', + }), + }, + missingBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.missingBucket.help', { + defaultMessage: 'When set to true, groups together any buckets with missing fields', + }), + }, + missingBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', { + defaultMessage: 'Missing', + description: `Default label used in charts when documents are missing a field. + Visible when you create a chart with a terms aggregation and enable "Show missing values"`, + }), + help: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel.help', { + defaultMessage: 'Default label used in charts when documents are missing a field.', + }), + }, + otherBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.otherBucket.help', { + defaultMessage: 'When set to true, groups together any buckets beyond the allowed size', + }), + }, + otherBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', { + defaultMessage: 'Other', + }), + help: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel.help', { + defaultMessage: 'Default label used in charts for documents in the Other bucket', + }), + }, + exclude: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.exclude.help', { + defaultMessage: 'Specific bucket values to exclude from results', + }), + }, + include: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.include.help', { + defaultMessage: 'Specific bucket values to include in results', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + let json; + try { + json = args.json ? JSON.parse(args.json) : undefined; + } catch (e) { + throw new Error('Unable to parse json argument string'); + } + + // Need to spread this object to work around TS bug: + // https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742 + const orderAgg = args.orderAgg?.value ? { ...args.orderAgg.value } : undefined; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: aggName, + params: { + ...rest, + orderAgg, + json, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/filter/agg_type_filters.test.ts b/src/plugins/data/public/search/aggs/filter/agg_type_filters.test.ts deleted file mode 100644 index 58f5aef0b9dfd..0000000000000 --- a/src/plugins/data/public/search/aggs/filter/agg_type_filters.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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 { IndexPattern } from '../../../index_patterns'; -import { AggTypeFilters } from './agg_type_filters'; -import { IAggConfig, IAggType } from '../types'; - -describe('AggTypeFilters', () => { - let registry: AggTypeFilters; - const indexPattern = ({ id: '1234', fields: [], title: 'foo' } as unknown) as IndexPattern; - const aggConfig = {} as IAggConfig; - - beforeEach(() => { - registry = new AggTypeFilters(); - }); - - it('should filter nothing without registered filters', async () => { - const aggTypes = [{ name: 'count' }, { name: 'sum' }] as IAggType[]; - const filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual(aggTypes); - }); - - it('should pass all aggTypes to the registered filter', async () => { - const aggTypes = [{ name: 'count' }, { name: 'sum' }] as IAggType[]; - const filter = jest.fn(); - registry.addFilter(filter); - registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filter).toHaveBeenCalledWith(aggTypes[0], indexPattern, aggConfig, []); - expect(filter).toHaveBeenCalledWith(aggTypes[1], indexPattern, aggConfig, []); - }); - - it('should allow registered filters to filter out aggTypes', async () => { - const aggTypes = [{ name: 'count' }, { name: 'sum' }, { name: 'avg' }] as IAggType[]; - let filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual(aggTypes); - - registry.addFilter(() => true); - registry.addFilter(aggType => aggType.name !== 'count'); - filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual([aggTypes[1], aggTypes[2]]); - - registry.addFilter(aggType => aggType.name !== 'avg'); - filtered = registry.filter(aggTypes, indexPattern, aggConfig, []); - expect(filtered).toEqual([aggTypes[1]]); - }); -}); diff --git a/src/plugins/data/public/search/aggs/filter/agg_type_filters.ts b/src/plugins/data/public/search/aggs/filter/agg_type_filters.ts deleted file mode 100644 index b8d192cd66b5a..0000000000000 --- a/src/plugins/data/public/search/aggs/filter/agg_type_filters.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { IndexPattern } from '../../../index_patterns'; -import { IAggConfig, IAggType } from '../types'; - -type AggTypeFilter = ( - aggType: IAggType, - indexPattern: IndexPattern, - aggConfig: IAggConfig, - aggFilter: string[] -) => boolean; - -/** - * A registry to store {@link AggTypeFilter} which are used to filter down - * available aggregations for a specific visualization and {@link AggConfig}. - */ -class AggTypeFilters { - private filters = new Set(); - - /** - * Register a new {@link AggTypeFilter} with this registry. - * - * @param filter The filter to register. - */ - public addFilter(filter: AggTypeFilter): void { - this.filters.add(filter); - } - - /** - * Returns the {@link AggType|aggTypes} filtered by all registered filters. - * - * @param aggTypes A list of aggTypes that will be filtered down by this registry. - * @param indexPattern The indexPattern for which this list should be filtered down. - * @param aggConfig The aggConfig for which the returning list will be used. - * @param schema - * @return A filtered list of the passed aggTypes. - */ - public filter( - aggTypes: IAggType[], - indexPattern: IndexPattern, - aggConfig: IAggConfig, - aggFilter: string[] - ) { - const allFilters = Array.from(this.filters); - const allowedAggTypes = aggTypes.filter(aggType => { - const isAggTypeAllowed = allFilters.every(filter => - filter(aggType, indexPattern, aggConfig, aggFilter) - ); - return isAggTypeAllowed; - }); - return allowedAggTypes; - } -} - -const aggTypeFilters = new AggTypeFilters(); - -export { aggTypeFilters, AggTypeFilters }; diff --git a/src/plugins/data/public/search/aggs/filter/index.ts b/src/plugins/data/public/search/aggs/filter/index.ts deleted file mode 100644 index 35d06807d0ec2..0000000000000 --- a/src/plugins/data/public/search/aggs/filter/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { aggTypeFilters, AggTypeFilters } from './agg_type_filters'; -export { propFilter } from './prop_filter'; diff --git a/src/plugins/data/public/search/aggs/index.test.ts b/src/plugins/data/public/search/aggs/index.test.ts index 419c3fdab1caf..382c5a10c2da5 100644 --- a/src/plugins/data/public/search/aggs/index.test.ts +++ b/src/plugins/data/public/search/aggs/index.test.ts @@ -24,6 +24,7 @@ import { isBucketAggType } from './buckets/bucket_agg_type'; import { isMetricAggType } from './metrics/metric_agg_type'; import { QueryStart } from '../../query'; import { FieldFormatsStart } from '../../field_formats'; +import { InternalStartServices } from '../../types'; describe('AggTypesComponent', () => { const coreSetup = coreMock.createSetup(); @@ -32,10 +33,11 @@ describe('AggTypesComponent', () => { const aggTypes = getAggTypes({ uiSettings: coreSetup.uiSettings, query: {} as QueryStart, - getInternalStartServices: () => ({ - notifications: coreStart.notifications, - fieldFormats: {} as FieldFormatsStart, - }), + getInternalStartServices: () => + (({ + notifications: coreStart.notifications, + fieldFormats: {} as FieldFormatsStart, + } as unknown) as InternalStartServices), }); const { buckets, metrics } = aggTypes; diff --git a/src/plugins/data/public/search/aggs/index.ts b/src/plugins/data/public/search/aggs/index.ts index 5dfb6aeff8d14..1139d9c7ff722 100644 --- a/src/plugins/data/public/search/aggs/index.ts +++ b/src/plugins/data/public/search/aggs/index.ts @@ -24,7 +24,6 @@ export * from './agg_type'; export * from './agg_types'; export * from './agg_types_registry'; export * from './buckets'; -export * from './filter'; export * from './metrics'; export * from './param_types'; export * from './types'; diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 3868d8f1bcd16..947394c97bdcd 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -36,14 +36,14 @@ const metricAggFilter = [ '!geo_centroid', ]; -const parentPipelineType = i18n.translate( +export const parentPipelineType = i18n.translate( 'data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle', { defaultMessage: 'Parent Pipeline Aggregations', } ); -const parentPipelineAggHelper = { +export const parentPipelineAggHelper = { subtype: parentPipelineType, params() { return [ @@ -56,13 +56,9 @@ const parentPipelineAggHelper = { name: 'customMetric', type: 'agg', allowedAggs: metricAggFilter, - makeAgg(termsAgg, state: any) { - state = state || { type: 'count' }; - + makeAgg(termsAgg, state = { type: 'count' }) { const metricAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); - metricAgg.id = termsAgg.id + '-metric'; - return metricAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -89,5 +85,3 @@ const parentPipelineAggHelper = { return subAgg ? subAgg.type.getFormat(subAgg) : new (FieldFormat.from(identity))(); }, }; - -export { parentPipelineAggHelper, parentPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index c1d05a39285b7..cee7841a8c3b9 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -43,14 +43,14 @@ const metricAggFilter: string[] = [ ]; const bucketAggFilter: string[] = []; -const siblingPipelineType = i18n.translate( +export const siblingPipelineType = i18n.translate( 'data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle', { defaultMessage: 'Sibling pipeline aggregations', } ); -const siblingPipelineAggHelper = { +export const siblingPipelineAggHelper = { subtype: siblingPipelineType, params() { return [ @@ -59,11 +59,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: bucketAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'date_histogram' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-bucket'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -76,11 +74,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: metricAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'count' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'count' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-metric'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -98,5 +94,3 @@ const siblingPipelineAggHelper = { : new (FieldFormat.from(identity))(); }, }; - -export { siblingPipelineAggHelper, siblingPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/median.test.ts b/src/plugins/data/public/search/aggs/metrics/median.test.ts index de3ca646ead9e..71c48f04a3ca8 100644 --- a/src/plugins/data/public/search/aggs/metrics/median.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/median.test.ts @@ -23,14 +23,16 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypeMetricMedianProvider class', () => { let aggConfigs: IAggConfigs; const aggTypesDependencies: MedianMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; beforeEach(() => { diff --git a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts index 3beb92a2fa000..f386034ea8a7b 100644 --- a/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/parent_pipeline.test.ts @@ -25,14 +25,15 @@ import { AggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { GetInternalStartServicesFn } from '../../../types'; +import { GetInternalStartServicesFn, InternalStartServices } from '../../../types'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; describe('parent pipeline aggs', function() { - const getInternalStartServices: GetInternalStartServicesFn = () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }); + const getInternalStartServices: GetInternalStartServicesFn = () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices); const typesRegistry = mockAggTypesRegistry(); diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index 1b94ecd602075..7491f15aa3002 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -25,19 +25,28 @@ import { import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +import { FieldFormatsStart } from '../../../field_formats'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentileRanksProvider class', function() { let aggConfigs: IAggConfigs; - const aggTypesDependencies: PercentileRanksMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), - }; + let fieldFormats: FieldFormatsStart; + let aggTypesDependencies: PercentileRanksMetricAggDependencies; beforeEach(() => { + fieldFormats = fieldFormatsServiceMock.createStartContract(); + fieldFormats.getDefaultInstance = (() => ({ + convert: (t?: string) => t, + })) as any; + aggTypesDependencies = { + getInternalStartServices: () => + (({ + fieldFormats, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), + }; const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]); const field = { name: 'bytes', @@ -59,12 +68,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { type: METRIC_TYPES.PERCENTILE_RANKS, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => x), - }, - }, + field: 'bytes', customLabel: 'my custom field label', values: [5000, 10000], }, diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts index 76da2fe3eb62c..76382c01bcc10 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -27,14 +27,16 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentilesProvider class', () => { let aggConfigs: IAggConfigs; const aggTypesDependencies: PercentilesMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; beforeEach(() => { @@ -59,12 +61,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { type: METRIC_TYPES.PERCENTILES, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => `${x}th`), - }, - }, + field: 'bytes', customLabel: 'prince', percents: [95], }, diff --git a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts index a47aa2c677ade..5e1834d3b4935 100644 --- a/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/sibling_pipeline.test.ts @@ -26,14 +26,15 @@ import { AggConfigs } from '../agg_configs'; import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; import { mockAggTypesRegistry } from '../test_helpers'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; -import { GetInternalStartServicesFn } from '../../../types'; +import { GetInternalStartServicesFn, InternalStartServices } from '../../../types'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; describe('sibling pipeline aggs', () => { - const getInternalStartServices: GetInternalStartServicesFn = () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }); + const getInternalStartServices: GetInternalStartServicesFn = () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices); const typesRegistry = mockAggTypesRegistry(); diff --git a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts index d2370e1fed02c..536764b2bcf0b 100644 --- a/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/std_deviation.test.ts @@ -27,13 +27,15 @@ import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('AggTypeMetricStandardDeviationProvider class', () => { const aggTypesDependencies: StdDeviationMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; const typesRegistry = mockAggTypesRegistry([getStdDeviationMetricAgg(aggTypesDependencies)]); const getAggConfigs = (customLabel?: string) => { diff --git a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts index 142b8e4c83301..617e458cf6243 100644 --- a/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/top_hit.test.ts @@ -25,15 +25,17 @@ import { IMetricAggConfig } from './metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../common'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('Top hit metric', () => { let aggDsl: Record; let aggConfig: IMetricAggConfig; const aggTypesDependencies: TopHitMetricAggDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; const init = ({ diff --git a/src/plugins/data/public/search/aggs/param_types/agg.ts b/src/plugins/data/public/search/aggs/param_types/agg.ts index e5b53020c3159..e3f8c7c922170 100644 --- a/src/plugins/data/public/search/aggs/param_types/agg.ts +++ b/src/plugins/data/public/search/aggs/param_types/agg.ts @@ -17,13 +17,13 @@ * under the License. */ -import { AggConfig, IAggConfig } from '../agg_config'; +import { AggConfig, IAggConfig, AggConfigSerialized } from '../agg_config'; import { BaseParamType } from './base'; export class AggParamType extends BaseParamType< TAggConfig > { - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; allowedAggs: string[] = []; constructor(config: Record) { @@ -42,17 +42,25 @@ export class AggParamType extends Ba } if (!config.serialize) { this.serialize = (agg: TAggConfig) => { - return agg.toJSON(); + return agg.serialize(); }; } if (!config.deserialize) { - this.deserialize = (state: unknown, agg?: TAggConfig): TAggConfig => { + this.deserialize = (state: AggConfigSerialized, agg?: TAggConfig): TAggConfig => { if (!agg) { throw new Error('aggConfig was not provided to AggParamType deserialize function'); } return this.makeAgg(agg, state); }; } + if (!config.toExpressionAst) { + this.toExpressionAst = (agg: TAggConfig) => { + if (!agg || !agg.toExpressionAst) { + throw new Error('aggConfig was not provided to AggParamType toExpressionAst function'); + } + return agg.toExpressionAst(); + }; + } this.makeAgg = config.makeAgg; this.valueType = AggConfig; diff --git a/src/plugins/data/public/search/aggs/param_types/base.ts b/src/plugins/data/public/search/aggs/param_types/base.ts index 2cbc5866e284d..a6f7e5adea043 100644 --- a/src/plugins/data/public/search/aggs/param_types/base.ts +++ b/src/plugins/data/public/search/aggs/param_types/base.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ExpressionAstFunction } from 'src/plugins/expressions/public'; import { IAggConfigs } from '../agg_configs'; import { IAggConfig } from '../agg_config'; import { FetchOptions } from '../../fetch'; @@ -37,6 +38,7 @@ export class BaseParamType { ) => void; serialize: (value: any, aggConfig?: TAggConfig) => any; deserialize: (value: any, aggConfig?: TAggConfig) => any; + toExpressionAst?: (value: any) => ExpressionAstFunction | undefined; options: any[]; valueType?: any; @@ -77,6 +79,7 @@ export class BaseParamType { this.write = config.write || defaultWrite; this.serialize = config.serialize; this.deserialize = config.deserialize; + this.toExpressionAst = config.toExpressionAst; this.options = config.options; this.modifyAggConfigOnSearchRequestStart = config.modifyAggConfigOnSearchRequestStart || function() {}; diff --git a/src/plugins/data/public/search/aggs/param_types/field.test.ts b/src/plugins/data/public/search/aggs/param_types/field.test.ts index ea7931130b84a..2c51d9709f906 100644 --- a/src/plugins/data/public/search/aggs/param_types/field.test.ts +++ b/src/plugins/data/public/search/aggs/param_types/field.test.ts @@ -23,13 +23,15 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../common'; import { IAggConfig } from '../agg_config'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; +import { InternalStartServices } from '../../../types'; describe('Field', () => { const fieldParamTypeDependencies: FieldParamTypeDependencies = { - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), }; const indexPattern = { diff --git a/src/plugins/data/public/search/aggs/param_types/field.ts b/src/plugins/data/public/search/aggs/param_types/field.ts index 4d67f41905c5a..63dbed9cec612 100644 --- a/src/plugins/data/public/search/aggs/param_types/field.ts +++ b/src/plugins/data/public/search/aggs/param_types/field.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { IAggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; -import { propFilter } from '../filter'; +import { propFilter } from '../utils'; import { isNestedField, KBN_FIELD_TYPES } from '../../../../common'; import { Field as IndexPatternField } from '../../../index_patterns'; import { GetInternalStartServicesFn } from '../../../types'; diff --git a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts b/src/plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts deleted file mode 100644 index f776a3deb23a1..0000000000000 --- a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 { AggTypeFieldFilters } from './field_filters'; -import { IAggConfig } from '../../agg_config'; -import { Field as IndexPatternField } from '../../../../index_patterns'; - -describe('AggTypeFieldFilters', () => { - let registry: AggTypeFieldFilters; - const aggConfig = {} as IAggConfig; - - beforeEach(() => { - registry = new AggTypeFieldFilters(); - }); - - it('should filter nothing without registered filters', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; - const filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual(fields); - }); - - it('should pass all fields to the registered filter', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; - const filter = jest.fn(); - registry.addFilter(filter); - registry.filter(fields, aggConfig); - expect(filter).toHaveBeenCalledWith(fields[0], aggConfig); - expect(filter).toHaveBeenCalledWith(fields[1], aggConfig); - }); - - it('should allow registered filters to filter out fields', async () => { - const fields = [{ name: 'foo' }, { name: 'bar' }] as IndexPatternField[]; - let filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual(fields); - - registry.addFilter(() => true); - registry.addFilter(field => field.name !== 'foo'); - filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual([fields[1]]); - - registry.addFilter(field => field.name !== 'bar'); - filtered = registry.filter(fields, aggConfig); - expect(filtered).toEqual([]); - }); -}); diff --git a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.ts b/src/plugins/data/public/search/aggs/param_types/filter/field_filters.ts deleted file mode 100644 index 1cbf0c9ae3624..0000000000000 --- a/src/plugins/data/public/search/aggs/param_types/filter/field_filters.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 { IndexPatternField } from 'src/plugins/data/public'; -import { IAggConfig } from '../../agg_config'; - -type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: IAggConfig) => boolean; - -/** - * A registry to store {@link AggTypeFieldFilter} which are used to filter down - * available fields for a specific visualization and {@link AggType}. - */ -class AggTypeFieldFilters { - private filters = new Set(); - - /** - * Register a new {@link AggTypeFieldFilter} with this registry. - * This will be used by the {@link #filter|filter method}. - * - * @param filter The filter to register. - */ - public addFilter(filter: AggTypeFieldFilter): void { - this.filters.add(filter); - } - - /** - * Returns the {@link any|fields} filtered by all registered filters. - * - * @param fields An array of fields that will be filtered down by this registry. - * @param aggConfig The aggConfig for which the returning list will be used. - * @return A filtered list of the passed fields. - */ - public filter(fields: IndexPatternField[], aggConfig: IAggConfig) { - const allFilters = Array.from(this.filters); - const allowedAggTypeFields = fields.filter(field => { - const isAggTypeFieldAllowed = allFilters.every(filter => filter(field, aggConfig)); - return isAggTypeFieldAllowed; - }); - return allowedAggTypeFields; - } -} - -const aggTypeFieldFilters = new AggTypeFieldFilters(); - -export { aggTypeFieldFilters, AggTypeFieldFilters }; diff --git a/src/plugins/data/public/search/aggs/param_types/filter/index.ts b/src/plugins/data/public/search/aggs/param_types/filter/index.ts deleted file mode 100644 index 2e0039c96a192..0000000000000 --- a/src/plugins/data/public/search/aggs/param_types/filter/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 { aggTypeFieldFilters, AggTypeFieldFilters } from './field_filters'; diff --git a/src/plugins/data/public/search/aggs/param_types/index.ts b/src/plugins/data/public/search/aggs/param_types/index.ts index c9e8a9879f427..e25dd55dbd3f2 100644 --- a/src/plugins/data/public/search/aggs/param_types/index.ts +++ b/src/plugins/data/public/search/aggs/param_types/index.ts @@ -20,7 +20,6 @@ export * from './agg'; export * from './base'; export * from './field'; -export * from './filter'; export * from './json'; export * from './optioned'; export * from './string'; diff --git a/src/plugins/data/public/search/aggs/param_types/optioned.ts b/src/plugins/data/public/search/aggs/param_types/optioned.ts index 9eb7ceda60711..45d0a65f69170 100644 --- a/src/plugins/data/public/search/aggs/param_types/optioned.ts +++ b/src/plugins/data/public/search/aggs/param_types/optioned.ts @@ -27,12 +27,6 @@ export interface OptionedValueProp { isCompatible: (agg: IAggConfig) => boolean; } -export interface OptionedParamEditorProps { - aggParam: { - options: T[]; - }; -} - export class OptionedParamType extends BaseParamType { options: OptionedValueProp[]; diff --git a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts new file mode 100644 index 0000000000000..cb0e37c0296d7 --- /dev/null +++ b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts @@ -0,0 +1,49 @@ +/* + * 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 { mapValues } from 'lodash'; +import { + AnyExpressionFunctionDefinition, + ExpressionFunctionDefinition, + ExecutionContext, +} from '../../../../../../plugins/expressions/public'; + +/** + * Takes a function spec and passes in default args, + * overriding with any provided args. + * + * Similar to the test helper used in Expressions & Canvas, + * however in this case we are ignoring the input & execution + * context, as they are not applicable to the agg type + * expression functions. + */ +export const functionWrapper = (spec: T) => { + const defaultArgs = mapValues(spec.args, argSpec => argSpec.default); + return ( + args: T extends ExpressionFunctionDefinition< + infer Name, + infer Input, + infer Arguments, + infer Output, + infer Context + > + ? Arguments + : never + ) => spec.fn(null, { ...defaultArgs, ...args }, {} as ExecutionContext); +}; diff --git a/src/plugins/data/public/search/aggs/test_helpers/index.ts b/src/plugins/data/public/search/aggs/test_helpers/index.ts index 131f921586144..63f8ae0ce5f58 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/index.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/index.ts @@ -17,5 +17,6 @@ * under the License. */ +export { functionWrapper } from './function_wrapper'; export { mockAggTypesRegistry } from './mock_agg_types_registry'; export { mockDataServices } from './mock_data_services'; diff --git a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts index 2383affa2a8c5..3ff2fbf35ad7e 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/mock_agg_types_registry.ts @@ -17,7 +17,6 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { coreMock, notificationServiceMock } from '../../../../../../../src/core/public/mocks'; import { AggTypesRegistry, AggTypesRegistryStart } from '../agg_types_registry'; import { getAggTypes } from '../agg_types'; @@ -25,6 +24,7 @@ import { BucketAggType } from '../buckets/bucket_agg_type'; import { MetricAggType } from '../metrics/metric_agg_type'; import { queryServiceMock } from '../../../query/mocks'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; +import { InternalStartServices } from '../../../types'; /** * Testing utility which creates a new instance of AggTypesRegistry, @@ -53,14 +53,19 @@ export function mockAggTypesRegistry | MetricAggTyp } }); } else { - const core = coreMock.createSetup(); + const coreSetup = coreMock.createSetup(); + const coreStart = coreMock.createStart(); + const aggTypes = getAggTypes({ - uiSettings: core.uiSettings, + uiSettings: coreSetup.uiSettings, query: queryServiceMock.createSetupContract(), - getInternalStartServices: () => ({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - }), + getInternalStartServices: () => + (({ + fieldFormats: fieldFormatsServiceMock.createStartContract(), + notifications: notificationServiceMock.createStartContract(), + uiSettings: coreStart.uiSettings, + injectedMetadata: coreStart.injectedMetadata, + } as unknown) as InternalStartServices), }); aggTypes.buckets.forEach(type => registrySetup.registerBucket(type)); diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index 4b2b1620ad1d3..1c5b5b458ce90 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -19,21 +19,16 @@ import { IndexPattern } from '../../index_patterns'; import { - AggType, + AggConfigSerialized, + AggConfigs, + AggParamsTerms, AggTypesRegistrySetup, AggTypesRegistryStart, - AggConfig, - AggConfigs, CreateAggConfigParams, - FieldParamType, getCalculateAutoTimeExpression, - MetricAggType, - aggTypeFieldFilters, - parentPipelineAggHelper, - siblingPipelineAggHelper, } from './'; -export { IAggConfig } from './agg_config'; +export { IAggConfig, AggConfigSerialized } from './agg_config'; export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; export { IAggType } from './agg_type'; export { AggParam, AggParamOption } from './agg_params'; @@ -41,7 +36,7 @@ export { IFieldParamType } from './param_types'; export { IMetricAggType } from './metrics/metric_agg_type'; export { DateRangeKey } from './buckets/lib/date_range'; export { IpRangeKey } from './buckets/lib/ip_range'; -export { OptionedValueProp, OptionedParamEditorProps } from './param_types/optioned'; +export { OptionedValueProp } from './param_types/optioned'; /** @internal */ export interface SearchAggsSetup { @@ -49,17 +44,6 @@ export interface SearchAggsSetup { types: AggTypesRegistrySetup; } -/** @internal */ -export interface SearchAggsStartLegacy { - AggConfig: typeof AggConfig; - AggType: typeof AggType; - aggTypeFieldFilters: typeof aggTypeFieldFilters; - FieldParamType: typeof FieldParamType; - MetricAggType: typeof MetricAggType; - parentPipelineAggHelper: typeof parentPipelineAggHelper; - siblingPipelineAggHelper: typeof siblingPipelineAggHelper; -} - /** @internal */ export interface SearchAggsStart { calculateAutoTimeExpression: ReturnType; @@ -70,3 +54,25 @@ export interface SearchAggsStart { ) => InstanceType; types: AggTypesRegistryStart; } + +/** @internal */ +export interface AggExpressionType { + type: 'agg_type'; + value: AggConfigSerialized; +} + +/** @internal */ +export type AggExpressionFunctionArgs< + Name extends keyof AggParamsMapping +> = AggParamsMapping[Name] & Pick; + +/** + * A global list of the param interfaces for each agg type. + * For now this is internal, but eventually we will probably + * want to make it public. + * + * @internal + */ +export interface AggParamsMapping { + terms: AggParamsTerms; +} diff --git a/src/plugins/data/public/search/aggs/utils/index.ts b/src/plugins/data/public/search/aggs/utils/index.ts index 23606bd109342..169d872b17d3a 100644 --- a/src/plugins/data/public/search/aggs/utils/index.ts +++ b/src/plugins/data/public/search/aggs/utils/index.ts @@ -18,4 +18,5 @@ */ export * from './calculate_auto_time_expression'; +export * from './prop_filter'; export * from './to_angular_json'; diff --git a/src/plugins/data/public/search/aggs/filter/prop_filter.test.ts b/src/plugins/data/public/search/aggs/utils/prop_filter.test.ts similarity index 100% rename from src/plugins/data/public/search/aggs/filter/prop_filter.test.ts rename to src/plugins/data/public/search/aggs/utils/prop_filter.test.ts diff --git a/src/plugins/data/public/search/aggs/filter/prop_filter.ts b/src/plugins/data/public/search/aggs/utils/prop_filter.ts similarity index 97% rename from src/plugins/data/public/search/aggs/filter/prop_filter.ts rename to src/plugins/data/public/search/aggs/utils/prop_filter.ts index e6b5f3831e65d..01e98a68d3949 100644 --- a/src/plugins/data/public/search/aggs/filter/prop_filter.ts +++ b/src/plugins/data/public/search/aggs/utils/prop_filter.ts @@ -28,7 +28,7 @@ type FilterFunc

= (item: T[P]) => boolean; * * @returns the filter function which can be registered with angular */ -function propFilter

(prop: P) { +export function propFilter

(prop: P) { /** * List filtering function which accepts an array or list of values that a property * must contain @@ -92,5 +92,3 @@ function propFilter

(prop: P) { }); }; } - -export { propFilter }; diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index 2341f4fe447db..eec75b0841133 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -30,10 +30,17 @@ import { PersistedState } from '../../../../../plugins/visualizations/public'; import { Adapters } from '../../../../../plugins/inspector/public'; import { IAggConfigs } from '../aggs'; -import { ISearchSource, SearchSource } from '../search_source'; +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 { test('Passes the additional arguments it is given to the search strategy', () => { const searchRequests = [{ _searchStrategyId: 0 }]; - const args = { searchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; + const args = { legacySearchService: {}, config: {}, esShardTimeout: 0 } as FetchHandlers; callClient(searchRequests, [], args); diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts index 835b02b3cd5c7..9e3d65a69bf02 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.test.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.test.ts @@ -62,10 +62,10 @@ describe('defaultSearchStrategy', function() { }, ], esShardTimeout: 0, - searchService, + legacySearchService: searchService.__LEGACY, }; - es = searchArgs.searchService.__LEGACY.esClient; + es = searchArgs.legacySearchService.esClient; }); test('does not send max_concurrent_shard_requests by default', async () => { diff --git a/src/plugins/data/public/search/legacy/default_search_strategy.ts b/src/plugins/data/public/search/legacy/default_search_strategy.ts index 1552410f9090c..3216803dcbfa2 100644 --- a/src/plugins/data/public/search/legacy/default_search_strategy.ts +++ b/src/plugins/data/public/search/legacy/default_search_strategy.ts @@ -32,11 +32,11 @@ export const defaultSearchStrategy: SearchStrategyProvider = { function msearch({ searchRequests, - searchService, + legacySearchService, config, esShardTimeout, }: SearchStrategySearchParams) { - const es = searchService.__LEGACY.esClient; + const es = legacySearchService.esClient; const inlineRequests = searchRequests.map(({ index, body, search_type: searchType }) => { const inlineHeader = { index: index.title || index, diff --git a/src/plugins/data/public/search/mocks.ts b/src/plugins/data/public/search/mocks.ts index cb1c625a72959..44082040b5b0b 100644 --- a/src/plugins/data/public/search/mocks.ts +++ b/src/plugins/data/public/search/mocks.ts @@ -18,33 +18,26 @@ */ import { searchAggsSetupMock, searchAggsStartMock } from './aggs/mocks'; -import { AggTypeFieldFilters } from './aggs/param_types/filter'; import { ISearchStart } from './types'; +import { searchSourceMock, createSearchSourceMock } from './search_source/mocks'; -export * from './search_source/mocks'; - -export const searchSetupMock = { +const searchSetupMock = { aggs: searchAggsSetupMock(), registerSearchStrategyContext: jest.fn(), registerSearchStrategyProvider: jest.fn(), }; -export const searchStartMock: jest.Mocked = { +const searchStartMock: jest.Mocked = { aggs: searchAggsStartMock(), setInterceptor: jest.fn(), search: jest.fn(), - createSearchSource: jest.fn(), + searchSource: searchSourceMock, __LEGACY: { - AggConfig: jest.fn() as any, - AggType: jest.fn(), - aggTypeFieldFilters: new AggTypeFieldFilters(), - FieldParamType: jest.fn(), - MetricAggType: jest.fn(), - parentPipelineAggHelper: jest.fn() as any, - siblingPipelineAggHelper: jest.fn() as any, esClient: { search: jest.fn(), msearch: jest.fn(), }, }, }; + +export { searchSetupMock, searchStartMock, createSearchSourceMock }; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 19308dd387d3a..b1f7925bec4bb 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -18,9 +18,10 @@ */ import { coreMock } from '../../../../core/public/mocks'; +import { CoreSetup } from '../../../../core/public'; +import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks'; import { SearchService } from './search_service'; -import { CoreSetup } from '../../../../core/public'; describe('Search service', () => { let searchService: SearchService; @@ -35,6 +36,7 @@ describe('Search service', () => { it('exposes proper contract', async () => { const setup = searchService.setup(mockCoreSetup, { packageInfo: { version: '8' }, + expressions: expressionsPluginMock.createSetupContract(), } as any); expect(setup).toHaveProperty('registerSearchStrategyProvider'); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 916278a96659b..4d183797dfef0 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -18,43 +18,44 @@ */ import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public'; +import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy'; +import { + createSearchSourceFromJSON, + SearchSource, + SearchSourceDependencies, + SearchSourceFields, +} from './search_source'; import { ISearchSetup, ISearchStart, TSearchStrategyProvider, TSearchStrategiesMap } from './types'; import { TStrategyTypes } from './strategy_types'; import { getEsClient, LegacyApiCaller } from './legacy'; import { ES_SEARCH_STRATEGY, DEFAULT_SEARCH_STRATEGY } from '../../common/search'; import { esSearchStrategyProvider } from './es_search'; import { IndexPatternsContract } from '../index_patterns/index_patterns'; -import { createSearchSource } from './search_source'; import { QuerySetup } from '../query'; import { GetInternalStartServicesFn } from '../types'; import { SearchInterceptor } from './search_interceptor'; import { getAggTypes, - AggType, + getAggTypesFunctions, AggTypesRegistry, - AggConfig, AggConfigs, - FieldParamType, getCalculateAutoTimeExpression, - MetricAggType, - aggTypeFieldFilters, - parentPipelineAggHelper, - siblingPipelineAggHelper, } from './aggs'; - import { FieldFormatsStart } from '../field_formats'; +import { ISearchGeneric } from './i_search'; interface SearchServiceSetupDependencies { + expressions: ExpressionsSetup; + getInternalStartServices: GetInternalStartServicesFn; packageInfo: PackageInfo; query: QuerySetup; - getInternalStartServices: GetInternalStartServicesFn; } -interface SearchStartDependencies { - fieldFormats: FieldFormatsStart; +interface SearchServiceStartDependencies { indexPatterns: IndexPatternsContract; + fieldFormats: FieldFormatsStart; } /** @@ -92,22 +93,27 @@ export class SearchService implements Plugin { public setup( core: CoreSetup, - { packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies + { expressions, packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies ): ISearchSetup { this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo); this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider); this.registerSearchStrategyProvider(ES_SEARCH_STRATEGY, esSearchStrategyProvider); const aggTypesSetup = this.aggTypesRegistry.setup(); + + // register each agg type const aggTypes = getAggTypes({ query, uiSettings: core.uiSettings, getInternalStartServices, }); - aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b)); aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m)); + // register expression functions for each agg type + const aggFunctions = getAggTypesFunctions(); + aggFunctions.forEach(fn => expressions.registerFunction(fn)); + return { aggs: { calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), @@ -117,10 +123,7 @@ export class SearchService implements Plugin { }; } - public start( - core: CoreStart, - { fieldFormats, indexPatterns }: SearchStartDependencies - ): ISearchStart { + public start(core: CoreStart, dependencies: SearchServiceStartDependencies): ISearchStart { /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. @@ -135,40 +138,47 @@ export class SearchService implements Plugin { const aggTypesStart = this.aggTypesRegistry.start(); + const search: ISearchGeneric = (request, options, strategyName) => { + const strategyProvider = this.getSearchStrategy(strategyName || DEFAULT_SEARCH_STRATEGY); + const searchStrategy = strategyProvider({ + core, + getSearchStrategy: this.getSearchStrategy, + }); + return this.searchInterceptor.search(searchStrategy.search as any, request, options); + }; + + const legacySearch = { + esClient: this.esClient!, + }; + + const searchSourceDependencies: SearchSourceDependencies = { + uiSettings: core.uiSettings, + injectedMetadata: core.injectedMetadata, + search, + legacySearch, + }; + return { aggs: { calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), createAggConfigs: (indexPattern, configStates = [], schemas) => { return new AggConfigs(indexPattern, configStates, { + fieldFormats: dependencies.fieldFormats, typesRegistry: aggTypesStart, - fieldFormats, }); }, types: aggTypesStart, }, - search: (request, options, strategyName) => { - const strategyProvider = this.getSearchStrategy(strategyName || DEFAULT_SEARCH_STRATEGY); - const { search } = strategyProvider({ - core, - getSearchStrategy: this.getSearchStrategy, - }); - return this.searchInterceptor.search(search as any, request, options); + search, + searchSource: { + create: (fields?: SearchSourceFields) => new SearchSource(fields, searchSourceDependencies), + fromJSON: createSearchSourceFromJSON(dependencies.indexPatterns, searchSourceDependencies), }, setInterceptor: (searchInterceptor: SearchInterceptor) => { // TODO: should an intercepror have a destroy method? this.searchInterceptor = searchInterceptor; }, - createSearchSource: createSearchSource(indexPatterns), - __LEGACY: { - esClient: this.esClient!, - AggConfig, - AggType, - aggTypeFieldFilters, - FieldParamType, - MetricAggType, - parentPipelineAggHelper, - siblingPipelineAggHelper, - }, + __LEGACY: legacySearch, }; } diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts index d49ce5a0d11f8..efa63b0722e28 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -16,30 +16,43 @@ * specific language governing permissions and limitations * under the License. */ -import { createSearchSource as createSearchSourceFactory } from './create_search_source'; +import { createSearchSourceFromJSON } from './create_search_source'; import { IIndexPattern } from '../../../common/index_patterns'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { Filter } from '../../../common/es_query/filters'; +import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../mocks'; -describe('createSearchSource', function() { - let createSearchSource: ReturnType; +describe('createSearchSource', () => { const indexPatternMock: IIndexPattern = {} as IIndexPattern; let indexPatternContractMock: jest.Mocked; + let dependencies: any; + let createSearchSource: ReturnType; beforeEach(() => { + const core = coreMock.createStart(); + const data = dataPluginMock.createStartContract(); + + dependencies = { + searchService: data.search, + uiSettings: core.uiSettings, + injectedMetadata: core.injectedMetadata, + }; + indexPatternContractMock = ({ get: jest.fn().mockReturnValue(Promise.resolve(indexPatternMock)), } as unknown) as jest.Mocked; - createSearchSource = createSearchSourceFactory(indexPatternContractMock); + + createSearchSource = createSearchSourceFromJSON(indexPatternContractMock, dependencies); }); - it('should fail if JSON is invalid', () => { + test('should fail if JSON is invalid', () => { expect(createSearchSource('{', [])).rejects.toThrow(); expect(createSearchSource('0', [])).rejects.toThrow(); expect(createSearchSource('"abcdefg"', [])).rejects.toThrow(); }); - it('should set fields', async () => { + test('should set fields', async () => { const searchSource = await createSearchSource( JSON.stringify({ highlightAll: true, @@ -50,6 +63,7 @@ describe('createSearchSource', function() { }), [] ); + expect(searchSource.getOwnField('highlightAll')).toBe(true); expect(searchSource.getOwnField('query')).toEqual({ query: '', @@ -57,7 +71,7 @@ describe('createSearchSource', function() { }); }); - it('should resolve referenced index pattern', async () => { + test('should resolve referenced index pattern', async () => { const searchSource = await createSearchSource( JSON.stringify({ indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', @@ -70,11 +84,12 @@ describe('createSearchSource', function() { }, ] ); + expect(indexPatternContractMock.get).toHaveBeenCalledWith('123-456'); expect(searchSource.getOwnField('index')).toBe(indexPatternMock); }); - it('should set filters and resolve referenced index patterns', async () => { + test('should set filters and resolve referenced index patterns', async () => { const searchSource = await createSearchSource( JSON.stringify({ filter: [ @@ -110,6 +125,7 @@ describe('createSearchSource', function() { ] ); const filters = searchSource.getOwnField('filter') as Filter[]; + expect(filters[0]).toMatchInlineSnapshot(` Object { "$state": Object { @@ -135,7 +151,7 @@ describe('createSearchSource', function() { `); }); - it('should migrate legacy queries on the fly', async () => { + test('should migrate legacy queries on the fly', async () => { const searchSource = await createSearchSource( JSON.stringify({ highlightAll: true, @@ -143,6 +159,7 @@ describe('createSearchSource', function() { }), [] ); + expect(searchSource.getOwnField('query')).toEqual({ query: 'a:b', language: 'lucene', diff --git a/src/plugins/data/public/search/search_source/create_search_source.ts b/src/plugins/data/public/search/search_source/create_search_source.ts index 35b7ac4eb9762..cc98f433b3a03 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.ts @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import _ from 'lodash'; +import { transform, defaults, isFunction } from 'lodash'; import { SavedObjectReference } from 'kibana/public'; import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; import { InvalidJSONProperty } from '../../../../kibana_utils/public'; -import { SearchSource } from './search_source'; +import { SearchSourceDependencies, SearchSource, ISearchSource } from './search_source'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { SearchSourceFields } from './types'; @@ -38,12 +38,16 @@ import { SearchSourceFields } from './types'; * returned by `serializeSearchSource` and `references`, a list of references including the ones * returned by `serializeSearchSource`. * + * * @public */ -export const createSearchSource = (indexPatterns: IndexPatternsContract) => async ( +export const createSearchSourceFromJSON = ( + indexPatterns: IndexPatternsContract, + searchSourceDependencies: SearchSourceDependencies +) => async ( searchSourceJson: string, references: SavedObjectReference[] -) => { - const searchSource = new SearchSource(); +): Promise => { + const searchSource = new SearchSource({}, searchSourceDependencies); // if we have a searchSource, set its values based on the searchSourceJson field let searchSourceValues: Record; @@ -90,17 +94,17 @@ export const createSearchSource = (indexPatterns: IndexPatternsContract) => asyn } const searchSourceFields = searchSource.getFields(); - const fnProps = _.transform( + const fnProps = transform( searchSourceFields, function(dynamic, val, name) { - if (_.isFunction(val) && name) dynamic[name] = val; + if (isFunction(val) && name) dynamic[name] = val; }, {} ); // This assignment might hide problems because the type of values passed from the parsed JSON // might not fit the SearchSourceFields interface. - const newFields: SearchSourceFields = _.defaults(searchSourceValues, fnProps); + const newFields: SearchSourceFields = defaults(searchSourceValues, fnProps); searchSource.setFields(newFields); const query = searchSource.getOwnField('query'); diff --git a/src/plugins/data/public/search/search_source/index.ts b/src/plugins/data/public/search/search_source/index.ts index 0e9f530d0968a..9c4106b2dc616 100644 --- a/src/plugins/data/public/search/search_source/index.ts +++ b/src/plugins/data/public/search/search_source/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export * from './search_source'; -export { createSearchSource } from './create_search_source'; +export { SearchSource, ISearchSource, SearchSourceDependencies } from './search_source'; +export { createSearchSourceFromJSON } from './create_search_source'; export { SortDirection, EsQuerySortValue, SearchSourceFields } from './types'; diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index 1ef7c1187a9e0..157331ea87bb0 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -17,9 +17,15 @@ * under the License. */ -import { ISearchSource } from './search_source'; +import { + injectedMetadataServiceMock, + uiSettingsServiceMock, +} from '../../../../../core/public/mocks'; -export const searchSourceMock: MockedKeys = { +import { ISearchSource, SearchSource } from './search_source'; +import { SearchSourceFields } from './types'; + +export const searchSourceInstanceMock: MockedKeys = { setPreferredSearchStrategyId: jest.fn(), setFields: jest.fn().mockReturnThis(), setField: jest.fn().mockReturnThis(), @@ -39,3 +45,21 @@ export const searchSourceMock: MockedKeys = { history: [], serialize: jest.fn(), }; + +export const searchSourceMock = { + create: jest.fn().mockReturnValue(searchSourceInstanceMock), + fromJSON: jest.fn().mockReturnValue(searchSourceInstanceMock), +}; + +export const createSearchSourceMock = (fields?: SearchSourceFields) => + new SearchSource(fields, { + search: jest.fn(), + legacySearch: { + esClient: { + search: jest.fn(), + msearch: jest.fn(), + }, + }, + uiSettings: uiSettingsServiceMock.createStartContract(), + injectedMetadata: injectedMetadataServiceMock.createStartContract(), + }); diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index 6e878844664ad..7783e65889a12 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -16,28 +16,13 @@ * specific language governing permissions and limitations * under the License. */ - +import { Observable } from 'rxjs'; import { SearchSource } from './search_source'; import { IndexPattern, SortDirection } from '../..'; -import { mockDataServices } from '../aggs/test_helpers'; -import { setSearchService } from '../../services'; -import { searchStartMock } from '../mocks'; import { fetchSoon } from '../legacy'; -import { CoreStart } from 'kibana/public'; -import { Observable } from 'rxjs'; - -// Setup search service mock -searchStartMock.search = jest.fn(() => { - return new Observable(subscriber => { - setTimeout(() => { - subscriber.next({ - rawResponse: '', - }); - subscriber.complete(); - }, 100); - }); -}) as any; -setSearchService(searchStartMock); +import { IUiSettingsClient } from '../../../../../core/public'; +import { dataPluginMock } from '../../../../data/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; jest.mock('../legacy', () => ({ fetchSoon: jest.fn().mockResolvedValue({}), @@ -48,48 +33,70 @@ const getComputedFields = () => ({ scriptFields: [], docvalueFields: [], }); + const mockSource = { excludes: ['foo-*'] }; const mockSource2 = { excludes: ['bar-*'] }; + const indexPattern = ({ title: 'foo', getComputedFields, getSourceFiltering: () => mockSource, } as unknown) as IndexPattern; + const indexPattern2 = ({ title: 'foo', getComputedFields, getSourceFiltering: () => mockSource2, } as unknown) as IndexPattern; -describe('SearchSource', function() { - let uiSettingsMock: jest.Mocked; +describe('SearchSource', () => { + let mockSearchMethod: any; + let searchSourceDependencies: any; + beforeEach(() => { - const { core } = mockDataServices(); - uiSettingsMock = core.uiSettings; - jest.clearAllMocks(); + const core = coreMock.createStart(); + const data = dataPluginMock.createStartContract(); + + mockSearchMethod = jest.fn(() => { + return new Observable(subscriber => { + setTimeout(() => { + subscriber.next({ + rawResponse: '', + }); + subscriber.complete(); + }, 100); + }); + }); + + searchSourceDependencies = { + search: mockSearchMethod, + legacySearch: data.search.__LEGACY, + injectedMetadata: core.injectedMetadata, + uiSettings: core.uiSettings, + }; }); - describe('#setField()', function() { - it('sets the value for the property', function() { - const searchSource = new SearchSource(); + describe('#setField()', () => { + test('sets the value for the property', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('aggs', 5); expect(searchSource.getField('aggs')).toBe(5); }); }); - describe('#getField()', function() { - it('gets the value for the property', function() { - const searchSource = new SearchSource(); + describe('#getField()', () => { + test('gets the value for the property', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('aggs', 5); expect(searchSource.getField('aggs')).toBe(5); }); }); - describe(`#setField('index')`, function() { - describe('auto-sourceFiltering', function() { - describe('new index pattern assigned', function() { - it('generates a searchSource filter', async function() { - const searchSource = new SearchSource(); + describe(`#setField('index')`, () => { + describe('auto-sourceFiltering', () => { + describe('new index pattern assigned', () => { + test('generates a searchSource filter', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); expect(searchSource.getField('index')).toBe(undefined); expect(searchSource.getField('source')).toBe(undefined); searchSource.setField('index', indexPattern); @@ -98,8 +105,8 @@ describe('SearchSource', function() { expect(request._source).toBe(mockSource); }); - it('removes created searchSource filter on removal', async function() { - const searchSource = new SearchSource(); + test('removes created searchSource filter on removal', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern); searchSource.setField('index', undefined); const request = await searchSource.getSearchRequestBody(); @@ -107,9 +114,9 @@ describe('SearchSource', function() { }); }); - describe('new index pattern assigned over another', function() { - it('replaces searchSource filter with new', async function() { - const searchSource = new SearchSource(); + describe('new index pattern assigned over another', () => { + test('replaces searchSource filter with new', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern); searchSource.setField('index', indexPattern2); expect(searchSource.getField('index')).toBe(indexPattern2); @@ -117,8 +124,8 @@ describe('SearchSource', function() { expect(request._source).toBe(mockSource2); }); - it('removes created searchSource filter on removal', async function() { - const searchSource = new SearchSource(); + test('removes created searchSource filter on removal', async () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern); searchSource.setField('index', indexPattern2); searchSource.setField('index', undefined); @@ -130,8 +137,8 @@ describe('SearchSource', function() { }); describe('#onRequestStart()', () => { - it('should be called when starting a request', async () => { - const searchSource = new SearchSource({ index: indexPattern }); + test('should be called when starting a request', async () => { + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const fn = jest.fn(); searchSource.onRequestStart(fn); const options = {}; @@ -139,9 +146,9 @@ describe('SearchSource', function() { expect(fn).toBeCalledWith(searchSource, options); }); - it('should not be called on parent searchSource', async () => { - const parent = new SearchSource(); - const searchSource = new SearchSource({ index: indexPattern }); + test('should not be called on parent searchSource', async () => { + const parent = new SearchSource({}, searchSourceDependencies); + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const fn = jest.fn(); searchSource.onRequestStart(fn); @@ -154,9 +161,12 @@ describe('SearchSource', function() { expect(parentFn).not.toBeCalled(); }); - it('should be called on parent searchSource if callParentStartHandlers is true', async () => { - const parent = new SearchSource(); - const searchSource = new SearchSource({ index: indexPattern }).setParent(parent, { + test('should be called on parent searchSource if callParentStartHandlers is true', async () => { + const parent = new SearchSource({}, searchSourceDependencies); + const searchSource = new SearchSource( + { index: indexPattern }, + searchSourceDependencies + ).setParent(parent, { callParentStartHandlers: true, }); @@ -174,19 +184,21 @@ describe('SearchSource', function() { describe('#legacy fetch()', () => { beforeEach(() => { - uiSettingsMock.get.mockImplementation(() => { - return true; // batchSearches = true - }); - }); + const core = coreMock.createStart(); - afterEach(() => { - uiSettingsMock.get.mockImplementation(() => { - return false; // batchSearches = false - }); + searchSourceDependencies = { + ...searchSourceDependencies, + uiSettings: { + ...core.uiSettings, + get: jest.fn(() => { + return true; // batchSearches = true + }), + } as IUiSettingsClient, + }; }); - it('should call msearch', async () => { - const searchSource = new SearchSource({ index: indexPattern }); + test('should call msearch', async () => { + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const options = {}; await searchSource.fetch(options); expect(fetchSoon).toBeCalledTimes(1); @@ -194,18 +206,19 @@ describe('SearchSource', function() { }); describe('#search service fetch()', () => { - it('should call msearch', async () => { - const searchSource = new SearchSource({ index: indexPattern }); + test('should call msearch', async () => { + const searchSource = new SearchSource({ index: indexPattern }, searchSourceDependencies); const options = {}; + await searchSource.fetch(options); - expect(searchStartMock.search).toBeCalledTimes(1); + expect(mockSearchMethod).toBeCalledTimes(1); }); }); - describe('#serialize', function() { - it('should reference index patterns', () => { + describe('#serialize', () => { + test('should reference index patterns', () => { const indexPattern123 = { id: '123' } as IndexPattern; - const searchSource = new SearchSource(); + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('index', indexPattern123); const { searchSourceJSON, references } = searchSource.serialize(); expect(references[0].id).toEqual('123'); @@ -213,8 +226,8 @@ describe('SearchSource', function() { expect(JSON.parse(searchSourceJSON).indexRefName).toEqual(references[0].name); }); - it('should add other fields', () => { - const searchSource = new SearchSource(); + test('should add other fields', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('highlightAll', true); searchSource.setField('from', 123456); const { searchSourceJSON } = searchSource.serialize(); @@ -222,8 +235,8 @@ describe('SearchSource', function() { expect(JSON.parse(searchSourceJSON).from).toEqual(123456); }); - it('should omit sort and size', () => { - const searchSource = new SearchSource(); + test('should omit sort and size', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); searchSource.setField('highlightAll', true); searchSource.setField('from', 123456); searchSource.setField('sort', { field: SortDirection.asc }); @@ -232,8 +245,8 @@ describe('SearchSource', function() { expect(Object.keys(JSON.parse(searchSourceJSON))).toEqual(['highlightAll', 'from']); }); - it('should serialize filters', () => { - const searchSource = new SearchSource(); + test('should serialize filters', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); const filter = [ { query: 'query', @@ -249,8 +262,8 @@ describe('SearchSource', function() { expect(JSON.parse(searchSourceJSON).filter).toEqual(filter); }); - it('should reference index patterns in filters separately from index field', () => { - const searchSource = new SearchSource(); + test('should reference index patterns in filters separately from index field', () => { + const searchSource = new SearchSource({}, searchSourceDependencies); const indexPattern123 = { id: '123' } as IndexPattern; searchSource.setField('index', indexPattern123); const filter = [ diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 9d2bb889953cf..091a27a6f418d 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -69,34 +69,45 @@ * `appSearchSource`. */ -import _ from 'lodash'; +import { uniqueId, uniq, extend, pick, difference, set, omit, keys, isFunction } from 'lodash'; import { map } from 'rxjs/operators'; -import { SavedObjectReference } from 'kibana/public'; +import { CoreStart, SavedObjectReference } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; -import { IIndexPattern, SearchRequest } from '../..'; +import { IIndexPattern, ISearchGeneric, SearchRequest } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { FetchOptions, RequestFailure, getSearchParams, handleResponse } from '../fetch'; -import { getSearchService, getUiSettings, getInjectedMetadata } from '../../services'; import { getEsQueryConfig, buildEsQuery, Filter } from '../../../common'; import { getHighlightRequest } from '../../../common/field_formats'; import { fetchSoon } from '../legacy'; +import { ISearchStartLegacy } from '../types'; -export type ISearchSource = Pick; +export interface SearchSourceDependencies { + uiSettings: CoreStart['uiSettings']; + search: ISearchGeneric; + legacySearch: ISearchStartLegacy; + injectedMetadata: CoreStart['injectedMetadata']; +} +/** @public **/ export class SearchSource { - private id: string = _.uniqueId('data_source'); + private id: string = uniqueId('data_source'); private searchStrategyId?: string; private parent?: SearchSource; private requestStartHandlers: Array< - (searchSource: ISearchSource, options?: FetchOptions) => Promise + (searchSource: SearchSource, options?: FetchOptions) => Promise > = []; private inheritOptions: SearchSourceOptions = {}; public history: SearchRequest[] = []; + private fields: SearchSourceFields; + private readonly dependencies: SearchSourceDependencies; - constructor(private fields: SearchSourceFields = {}) {} + constructor(fields: SearchSourceFields = {}, dependencies: SearchSourceDependencies) { + this.fields = fields; + this.dependencies = dependencies; + } /** *** * PUBLIC API @@ -147,11 +158,11 @@ export class SearchSource { } create() { - return new SearchSource(); + return new SearchSource({}, this.dependencies); } createCopy() { - const newSearchSource = new SearchSource(); + const newSearchSource = new SearchSource({}, this.dependencies); newSearchSource.setFields({ ...this.fields }); // when serializing the internal fields we lose the internal classes used in the index // pattern, so we have to set it again to workaround this behavior @@ -161,7 +172,7 @@ export class SearchSource { } createChild(options = {}) { - const childSearchSource = new SearchSource(); + const childSearchSource = new SearchSource({}, this.dependencies); childSearchSource.setParent(this, options); return childSearchSource; } @@ -191,16 +202,17 @@ export class SearchSource { * @return {Observable>} */ private fetch$(searchRequest: SearchRequest, signal?: AbortSignal) { - const esShardTimeout = getInjectedMetadata().getInjectedVar('esShardTimeout') as number; - const searchParams = getSearchParams(getUiSettings(), esShardTimeout); + const { search, injectedMetadata, uiSettings } = this.dependencies; + const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number; + const searchParams = getSearchParams(uiSettings, esShardTimeout); const params = { index: searchRequest.index.title || searchRequest.index, body: searchRequest.body, ...searchParams, }; - return getSearchService() - .search({ params, indexType: searchRequest.indexType }, { signal }) - .pipe(map(({ rawResponse }) => handleResponse(searchRequest, rawResponse))); + return search({ params, indexType: searchRequest.indexType }, { signal }).pipe( + map(({ rawResponse }) => handleResponse(searchRequest, rawResponse)) + ); } /** @@ -208,7 +220,9 @@ export class SearchSource { * @return {Promise>} */ private async legacyFetch(searchRequest: SearchRequest, options: FetchOptions) { - const esShardTimeout = getInjectedMetadata().getInjectedVar('esShardTimeout') as number; + const { injectedMetadata, legacySearch, uiSettings } = this.dependencies; + const esShardTimeout = injectedMetadata.getInjectedVar('esShardTimeout') as number; + return await fetchSoon( searchRequest, { @@ -216,8 +230,8 @@ export class SearchSource { ...options, }, { - searchService: getSearchService(), - config: getUiSettings(), + legacySearchService: legacySearch, + config: uiSettings, esShardTimeout, } ); @@ -228,13 +242,14 @@ export class SearchSource { * @async */ async fetch(options: FetchOptions = {}) { + const { uiSettings } = this.dependencies; await this.requestIsStarting(options); const searchRequest = await this.flatten(); this.history = [searchRequest]; let response; - if (getUiSettings().get('courier:batchSearches')) { + if (uiSettings.get('courier:batchSearches')) { response = await this.legacyFetch(searchRequest, options); } else { response = this.fetch$(searchRequest, options.abortSignal).toPromise(); @@ -253,7 +268,7 @@ export class SearchSource { * @return {undefined} */ onRequestStart( - handler: (searchSource: ISearchSource, options?: FetchOptions) => Promise + handler: (searchSource: SearchSource, options?: FetchOptions) => Promise ) { this.requestStartHandlers.push(handler); } @@ -326,13 +341,15 @@ export class SearchSource { } }; + const { uiSettings } = this.dependencies; + switch (key) { case 'filter': return addToRoot('filters', (data.filters || []).concat(val)); case 'query': return addToRoot(key, (data[key] || []).concat(val)); case 'fields': - const fields = _.uniq((data[key] || []).concat(val)); + const fields = uniq((data[key] || []).concat(val)); return addToRoot(key, fields); case 'index': case 'type': @@ -346,7 +363,7 @@ export class SearchSource { const sort = normalizeSortRequest( val, this.getField('index'), - getUiSettings().get('sort:options') + uiSettings.get('sort:options') ); return addToBody(key, sort); default: @@ -389,7 +406,7 @@ export class SearchSource { body.stored_fields = computedFields.storedFields; body.script_fields = body.script_fields || {}; - _.extend(body.script_fields, computedFields.scriptFields); + extend(body.script_fields, computedFields.scriptFields); const defaultDocValueFields = computedFields.docvalueFields ? computedFields.docvalueFields @@ -400,9 +417,11 @@ export class SearchSource { body._source = index.getSourceFiltering(); } + const { uiSettings } = this.dependencies; + if (body._source) { // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(body._source.excludes, getUiSettings().get('metaFields')); + const filter = fieldWildcardFilter(body._source.excludes, uiSettings.get('metaFields')); body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => filter(docvalueField.field) ); @@ -412,42 +431,22 @@ export class SearchSource { if (fields) { // filter out the docvalue_fields, and script_fields to only include those that we are concerned with body.docvalue_fields = filterDocvalueFields(body.docvalue_fields, fields); - body.script_fields = _.pick(body.script_fields, fields); + body.script_fields = pick(body.script_fields, fields); // request the remaining fields from both stored_fields and _source - const remainingFields = _.difference(fields, _.keys(body.script_fields)); + const remainingFields = difference(fields, keys(body.script_fields)); body.stored_fields = remainingFields; - _.set(body, '_source.includes', remainingFields); + set(body, '_source.includes', remainingFields); } - const esQueryConfigs = getEsQueryConfig(getUiSettings()); + const esQueryConfigs = getEsQueryConfig(uiSettings); body.query = buildEsQuery(index, query, filters, esQueryConfigs); if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, getUiSettings().get('doc_table:highlight')); + body.highlight = getHighlightRequest(body.query, uiSettings.get('doc_table:highlight')); delete searchRequest.highlightAll; } - const translateToQuery = (filter: Filter) => filter && (filter.query || filter); - - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function(id) { - const agg = aggBranch[id]; - - if (agg.filters) { - // translate filters aggregations - const { filters: aggFilters } = agg.filters; - Object.keys(aggFilters).forEach(filterId => { - aggFilters[filterId] = translateToQuery(aggFilters[filterId]); - }); - } - - recurse(agg.aggs || agg.aggregations); - }); - })(body.aggs || body.aggregations); - return searchRequest; } @@ -467,7 +466,7 @@ export class SearchSource { const { filter: originalFilters, ...searchSourceFields - }: Omit = _.omit(this.getFields(), ['sort', 'size']); + }: Omit = omit(this.getFields(), ['sort', 'size']); let serializedSearchSourceFields: Omit & { indexRefName?: string; filter?: Array & { meta: Filter['meta'] & { indexRefName?: string } }>; @@ -524,10 +523,13 @@ export class SearchSource { return filterField; } - if (_.isFunction(filterField)) { + if (isFunction(filterField)) { return this.getFilters(filterField()); } return [filterField]; } } + +/** @public **/ +export type ISearchSource = Pick; diff --git a/src/plugins/data/public/search/tabify/buckets.test.ts b/src/plugins/data/public/search/tabify/buckets.test.ts index 98048cb25db2f..81d9f3d5ca3fd 100644 --- a/src/plugins/data/public/search/tabify/buckets.test.ts +++ b/src/plugins/data/public/search/tabify/buckets.test.ts @@ -19,6 +19,11 @@ import { TabifyBuckets } from './buckets'; import { AggGroupNames } from '../aggs'; +import moment from 'moment'; + +interface Bucket { + key: number | string; +} describe('Buckets wrapper', () => { 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 971e820ac6ddf..cd52a09caeaad 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 e93e989034252..9cb55f94537c5 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 1e051880d3f19..72e91eb58c8a9 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/public/search/types.ts b/src/plugins/data/public/search/types.ts index 2122e4e82ec1d..1687c8f983393 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -17,13 +17,13 @@ * under the License. */ -import { CoreStart } from 'kibana/public'; -import { createSearchSource } from './search_source'; -import { SearchAggsSetup, SearchAggsStart, SearchAggsStartLegacy } from './aggs'; +import { CoreStart, SavedObjectReference } from 'kibana/public'; +import { SearchAggsSetup, SearchAggsStart } from './aggs'; import { ISearch, ISearchGeneric } from './i_search'; import { TStrategyTypes } from './strategy_types'; import { LegacyApiCaller } from './legacy/es_client'; import { SearchInterceptor } from './search_interceptor'; +import { ISearchSource, SearchSourceFields } from './search_source'; export interface ISearchContext { core: CoreStart; @@ -60,7 +60,7 @@ export type TRegisterSearchStrategyProvider = ( searchStrategyProvider: TSearchStrategyProvider ) => void; -interface ISearchStartLegacy { +export interface ISearchStartLegacy { esClient: LegacyApiCaller; } @@ -81,6 +81,12 @@ export interface ISearchStart { aggs: SearchAggsStart; setInterceptor: (searchInterceptor: SearchInterceptor) => void; search: ISearchGeneric; - createSearchSource: ReturnType; - __LEGACY: ISearchStartLegacy & SearchAggsStartLegacy; + searchSource: { + create: (fields?: SearchSourceFields) => ISearchSource; + fromJSON: ( + searchSourceJson: string, + references: SavedObjectReference[] + ) => Promise; + }; + __LEGACY: ISearchStartLegacy; } diff --git a/src/plugins/data/public/services.ts b/src/plugins/data/public/services.ts index 199ba17b3b81b..ba0b2de393bde 100644 --- a/src/plugins/data/public/services.ts +++ b/src/plugins/data/public/services.ts @@ -17,7 +17,7 @@ * under the License. */ -import { NotificationsStart, CoreSetup, CoreStart } from 'src/core/public'; +import { NotificationsStart, CoreStart } from 'src/core/public'; import { FieldFormatsStart } from './field_formats'; import { createGetterSetter } from '../../kibana_utils/public'; import { IndexPatternsContract } from './index_patterns'; @@ -48,7 +48,7 @@ export const [getQueryService, setQueryService] = createGetterSetter< >('Query'); export const [getInjectedMetadata, setInjectedMetadata] = createGetterSetter< - CoreSetup['injectedMetadata'] + CoreStart['injectedMetadata'] >('InjectedMetadata'); export const [getSearchService, setSearchService] = createGetterSetter< diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index e24e01d241278..aaef403979de6 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -24,7 +24,7 @@ import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; -import { createFiltersFromEvent } from './actions'; +import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; import { ISearchSetup, ISearchStart } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternSelectProps } from './ui/index_pattern_select'; @@ -49,7 +49,8 @@ export interface DataPublicPluginSetup { export interface DataPublicPluginStart { actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; autocomplete: AutocompleteStart; indexPatterns: IndexPatternsContract; @@ -74,8 +75,11 @@ export interface IDataPluginServices extends Partial { /** @internal **/ export interface InternalStartServices { - fieldFormats: FieldFormatsStart; - notifications: CoreStart['notifications']; + readonly fieldFormats: FieldFormatsStart; + readonly notifications: CoreStart['notifications']; + readonly uiSettings: CoreStart['uiSettings']; + readonly searchService: DataPublicPluginStart['search']; + readonly injectedMetadata: CoreStart['injectedMetadata']; } /** @internal **/ diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index c56060bb9c288..6eb4f82a940b1 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -27,6 +27,7 @@ import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../co import { getTitle } from '../../index_patterns/lib'; export type IndexPatternSelectProps = Required< + // Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'append' | 'prepend' | 'sortMatchesBy'>, Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder' > & { diff --git a/src/plugins/data/server/field_formats/field_formats_service.ts b/src/plugins/data/server/field_formats/field_formats_service.ts index 0dac64fb5dc1d..3404fe8cee9fd 100644 --- a/src/plugins/data/server/field_formats/field_formats_service.ts +++ b/src/plugins/data/server/field_formats/field_formats_service.ts @@ -17,16 +17,20 @@ * under the License. */ import { has } from 'lodash'; -import { FieldFormatsRegistry, IFieldFormatType, baseFormatters } from '../../common/field_formats'; +import { + FieldFormatsRegistry, + FieldFormatInstanceType, + baseFormatters, +} from '../../common/field_formats'; import { IUiSettingsClient } from '../../../../core/server'; import { DateFormat } from './converters'; export class FieldFormatsService { - private readonly fieldFormatClasses: IFieldFormatType[] = [DateFormat, ...baseFormatters]; + private readonly fieldFormatClasses: FieldFormatInstanceType[] = [DateFormat, ...baseFormatters]; public setup() { return { - register: (customFieldFormat: IFieldFormatType) => + register: (customFieldFormat: FieldFormatInstanceType) => this.fieldFormatClasses.push(customFieldFormat), }; } diff --git a/src/plugins/data/server/index_patterns/capabilities_provider.ts b/src/plugins/data/server/index_patterns/capabilities_provider.ts new file mode 100644 index 0000000000000..d603830e44a53 --- /dev/null +++ b/src/plugins/data/server/index_patterns/capabilities_provider.ts @@ -0,0 +1,24 @@ +/* + * 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 const capabilitiesProvider = () => ({ + indexPatterns: { + save: true, + }, +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts new file mode 100644 index 0000000000000..784b0b4d4f3d7 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values'; + +describe('shouldReadFieldFromDocValues', () => { + test('should read field from doc values for aggregatable "number" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'number')).toBe(true); + }); + + test('should not read field from doc values for non-aggregatable "number "field', async () => { + expect(shouldReadFieldFromDocValues(false, 'number')).toBe(false); + }); + + test('should not read field from doc values for "text" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'text')).toBe(false); + }); + + test('should not read field from doc values for "geo_shape" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'geo_shape')).toBe(false); + }); + + test('should not read field from doc values for underscore field', async () => { + expect(shouldReadFieldFromDocValues(true, '_source')).toBe(false); + }); +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts index 6d58f7a02c134..56a1cf3ccd161 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts @@ -18,5 +18,5 @@ */ export function shouldReadFieldFromDocValues(aggregatable: boolean, esType: string) { - return aggregatable && esType !== 'text' && !esType.startsWith('_'); + return aggregatable && !['text', 'geo_shape'].includes(esType) && !esType.startsWith('_'); } diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 58e8fbae9f9e2..3e31f8e8a566d 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -20,10 +20,12 @@ import { CoreSetup, Plugin } from 'kibana/server'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; +import { capabilitiesProvider } from './capabilities_provider'; export class IndexPatternsService implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(indexPatternSavedObjectType); + core.capabilities.registerProvider(capabilitiesProvider); registerRoutes(core.http); } diff --git a/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts b/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts index e45ad796bd9d4..3dfaa9c6d0a98 100644 --- a/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts +++ b/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts @@ -22,14 +22,16 @@ import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { registerKqlTelemetryRoute } from './route'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { makeKQLUsageCollector } from './usage_collector'; +import { kqlTelemetry } from '../saved_objects'; export class KqlTelemetryService implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup( - { http, getStartServices }: CoreSetup, + { http, getStartServices, savedObjects }: CoreSetup, { usageCollection }: { usageCollection?: UsageCollectionSetup } ) { + savedObjects.registerType(kqlTelemetry); registerKqlTelemetryRoute( http.createRouter(), getStartServices, diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 5d980974474de..4326200141179 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -20,3 +20,4 @@ export { searchSavedObjectType } from './search'; export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; +export { kqlTelemetry } from './kql_telementry'; 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 7a16386ea484c..c64f7361a8cf4 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/kql_telementry.ts b/src/plugins/data/server/saved_objects/kql_telementry.ts new file mode 100644 index 0000000000000..6539d5eacfde2 --- /dev/null +++ b/src/plugins/data/server/saved_objects/kql_telementry.ts @@ -0,0 +1,35 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; + +export const kqlTelemetry: SavedObjectsType = { + name: 'kql-telemetry', + namespaceType: 'agnostic', + hidden: false, + mappings: { + properties: { + optInCount: { + type: 'long', + }, + optOutCount: { + type: 'long', + }, + }, + }, +}; diff --git a/src/plugins/data/server/saved_objects/search_migrations.ts b/src/plugins/data/server/saved_objects/search_migrations.ts index 45fa5e11e2a3d..c8ded51193c92 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/data/server/server.api.md b/src/plugins/data/server/server.api.md index c41023eab6d20..df4ba23244b4d 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -283,7 +283,7 @@ export interface FieldFormatConfig { export const fieldFormats: { FieldFormatsRegistry: typeof FieldFormatsRegistry; FieldFormat: typeof FieldFormat; - serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; @@ -408,6 +408,8 @@ export interface IIndexPattern { // (undocumented) fields: IFieldType[]; // (undocumented) + getTimeField?(): IFieldType | undefined; + // (undocumented) id?: string; // (undocumented) timeFieldName?: string; @@ -609,7 +611,7 @@ export class Plugin implements Plugin_2 { // (undocumented) setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../public").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 91d6358d44c18..2d41293f26369 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -1,6 +1,6 @@ { "id": "discover", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss b/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss index 25aa530976719..ec2beca15a546 100644 --- a/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss +++ b/src/plugins/discover/public/components/doc_viewer/_doc_viewer.scss @@ -43,6 +43,14 @@ } .kbnDocViewer__buttons { width: 60px; + + // Show all icons if one is focused, + // IE doesn't support, but the fallback is just the focused button becomes visible + &:focus-within { + .kbnDocViewer__actionButton { + opacity: 1; + } + } } .kbnDocViewer__field { @@ -51,7 +59,12 @@ .kbnDocViewer__actionButton { opacity: 0; + + &:focus { + opacity: 1; + } } + .kbnDocViewer__warning { margin-right: $euiSizeS; } diff --git a/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap b/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap index 9f48e6e57e0ff..afb541253d994 100644 --- a/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap +++ b/src/plugins/discover/public/components/field_name/__snapshots__/field_name.test.tsx.snap @@ -8,12 +8,12 @@ exports[`FieldName renders a geo field, useShortDots is set to true 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" >
@@ -39,12 +39,12 @@ exports[`FieldName renders a number field by providing a field record, useShortD class="euiFlexItem euiFlexItem--flexGrowZero" >
@@ -70,12 +70,12 @@ exports[`FieldName renders a string field by providing fieldType and fieldName 1 class="euiFlexItem euiFlexItem--flexGrowZero" >
diff --git a/src/plugins/discover/server/capabilities_provider.ts b/src/plugins/discover/server/capabilities_provider.ts new file mode 100644 index 0000000000000..2e03631894aee --- /dev/null +++ b/src/plugins/discover/server/capabilities_provider.ts @@ -0,0 +1,27 @@ +/* + * 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 const capabilitiesProvider = () => ({ + discover: { + show: true, + createShortUrl: true, + save: true, + saveQuery: true, + }, +}); diff --git a/src/plugins/discover/server/index.ts b/src/plugins/discover/server/index.ts new file mode 100644 index 0000000000000..15a948c56148e --- /dev/null +++ b/src/plugins/discover/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { DiscoverServerPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new DiscoverServerPlugin(initContext); diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts new file mode 100644 index 0000000000000..04502f5fc14e6 --- /dev/null +++ b/src/plugins/discover/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; + +export class DiscoverServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('discover: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('discover: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index bdb7bfbddc308..5ee66f9d19ac0 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -47,7 +47,8 @@ export { EmbeddableOutput, EmbeddablePanel, EmbeddableRoot, - EmbeddableVisTriggerContext, + ValueClickTriggerContext, + RangeSelectTriggerContext, ErrorEmbeddable, IContainer, IEmbeddable, diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts index 5297cf6cd365c..636ce3e623c5b 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts @@ -31,6 +31,14 @@ test('has expected display name', () => { expect(action.getDisplayName({} as any)).toMatchInlineSnapshot(`"Apply filter to current view"`); }); +describe('getIconType()', () => { + test('returns "filter" icon', async () => { + const action = createFilterAction(); + const result = action.getIconType({} as any); + expect(result).toBe('filter'); + }); +}); + describe('isCompatible()', () => { test('when embeddable filters and filters exist, returns true', async () => { const action = createFilterAction(); diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts index 4680512fb81c8..1cdb5af00e748 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts @@ -42,6 +42,7 @@ export function createFilterAction(): ActionByType { return createAction({ type: ACTION_APPLY_FILTER, id: ACTION_APPLY_FILTER, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('embeddableApi.actions.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index d07bf915845e9..fc5438b8c8dcb 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -41,7 +41,7 @@ class EditableEmbeddable extends Embeddable { } test('is compatible when edit url is available, in edit mode and editable', async () => { - const action = new EditPanelAction(getFactory); + const action = new EditPanelAction(getFactory, {} as any); expect( await action.isCompatible({ embeddable: new EditableEmbeddable({ id: '123', viewMode: ViewMode.EDIT }, true), @@ -50,7 +50,7 @@ test('is compatible when edit url is available, in edit mode and editable', asyn }); test('getHref returns the edit urls', async () => { - const action = new EditPanelAction(getFactory); + const action = new EditPanelAction(getFactory, {} as any); expect(action.getHref).toBeDefined(); if (action.getHref) { @@ -64,7 +64,7 @@ test('getHref returns the edit urls', async () => { }); test('is not compatible when edit url is not available', async () => { - const action = new EditPanelAction(getFactory); + const action = new EditPanelAction(getFactory, {} as any); const embeddable = new ContactCardEmbeddable( { id: '123', @@ -83,7 +83,7 @@ test('is not compatible when edit url is not available', async () => { }); test('is not visible when edit url is available but in view mode', async () => { - const action = new EditPanelAction(getFactory); + const action = new EditPanelAction(getFactory, {} as any); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( @@ -98,7 +98,7 @@ test('is not visible when edit url is available but in view mode', async () => { }); test('is not compatible when edit url is available, in edit mode, but not editable', async () => { - const action = new EditPanelAction(getFactory); + const action = new EditPanelAction(getFactory, {} as any); expect( await action.isCompatible({ embeddable: new EditableEmbeddable( diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 044e7b5d35ad8..0abbc25ff49a6 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { ApplicationStart } from 'kibana/public'; import { Action } from 'src/plugins/ui_actions/public'; import { ViewMode } from '../types'; import { EmbeddableFactoryNotFoundError } from '../errors'; @@ -35,7 +36,10 @@ export class EditPanelAction implements Action { public readonly id = ACTION_EDIT_PANEL; public order = 15; - constructor(private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']) {} + constructor( + private readonly getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'], + private readonly application: ApplicationStart + ) {} public getDisplayName({ embeddable }: ActionContext) { const factory = this.getEmbeddableFactory(embeddable.type); @@ -56,18 +60,35 @@ export class EditPanelAction implements Action { public async isCompatible({ embeddable }: ActionContext) { const canEditEmbeddable = Boolean( - embeddable && embeddable.getOutput().editable && embeddable.getOutput().editUrl + embeddable && + embeddable.getOutput().editable && + (embeddable.getOutput().editUrl || + (embeddable.getOutput().editApp && embeddable.getOutput().editPath)) ); const inDashboardEditMode = embeddable.getInput().viewMode === ViewMode.EDIT; return Boolean(canEditEmbeddable && inDashboardEditMode); } public async execute(context: ActionContext) { + const appTarget = this.getAppTarget(context); + + if (appTarget) { + await this.application.navigateToApp(appTarget.app, { path: appTarget.path }); + return; + } + const href = await this.getHref(context); if (href) { - // TODO: when apps start using browser router instead of hash router this has to be fixed - // https://github.com/elastic/kibana/issues/58217 window.location.href = href; + return; + } + } + + public getAppTarget({ embeddable }: ActionContext): { app: string; path: string } | undefined { + const app = embeddable ? embeddable.getOutput().editApp : undefined; + const path = embeddable ? embeddable.getOutput().editPath : undefined; + if (app && path) { + return { app, path }; } } diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx index 2a0ffd723850b..b046376a304ae 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx @@ -66,6 +66,7 @@ test('EmbeddableChildPanel renders an embeddable when it is done loading', async getAllEmbeddableFactories={start.getEmbeddableFactories} getEmbeddableFactory={getEmbeddableFactory} notifications={{} as any} + application={{} as any} overlays={{} as any} inspector={inspector} SavedObjectFinder={() => null} @@ -105,6 +106,7 @@ test(`EmbeddableChildPanel renders an error message if the factory doesn't exist getEmbeddableFactory={(() => undefined) as any} notifications={{} as any} overlays={{} as any} + application={{} as any} inspector={inspector} SavedObjectFinder={() => null} /> diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx index 4c08a80a356bf..70628665e6e8c 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.tsx @@ -40,6 +40,7 @@ export interface EmbeddableChildPanelProps { getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; + application: CoreStart['application']; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; } @@ -101,6 +102,7 @@ export class EmbeddableChildPanel extends React.Component { getAllEmbeddableFactories={start.getEmbeddableFactories} getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} + application={{} as any} overlays={{} as any} inspector={inspector} SavedObjectFinder={() => null} @@ -198,6 +199,7 @@ const renderInEditModeAndOpenContextMenu = async ( getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} + application={{} as any} inspector={inspector} SavedObjectFinder={() => null} /> @@ -296,6 +298,7 @@ test('HelloWorldContainer in edit mode shows edit mode actions', async () => { getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} + application={{} as any} inspector={inspector} SavedObjectFinder={() => null} /> @@ -358,6 +361,7 @@ test('Updates when hidePanelTitles is toggled', async () => { getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} + application={{} as any} inspector={inspector} SavedObjectFinder={() => null} /> @@ -410,6 +414,7 @@ test('Check when hide header option is false', async () => { getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} + application={{} as any} inspector={inspector} SavedObjectFinder={() => null} hideHeader={false} @@ -447,6 +452,7 @@ test('Check when hide header option is true', async () => { getEmbeddableFactory={start.getEmbeddableFactory} notifications={{} as any} overlays={{} as any} + application={{} as any} inspector={inspector} SavedObjectFinder={() => null} hideHeader={true} diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index b95060a73252f..c43359382a33d 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -45,6 +45,7 @@ interface Props { getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; notifications: CoreStart['notifications']; + application: CoreStart['application']; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; hideHeader?: boolean; @@ -243,7 +244,7 @@ export class EmbeddablePanel extends React.Component { ), new InspectPanelAction(this.props.inspector), new RemovePanelAction(), - new EditPanelAction(this.props.getEmbeddableFactory), + new EditPanelAction(this.props.getEmbeddableFactory, this.props.application), ]; const sorted = actions diff --git a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx index a88c3ba086325..31e14a0af59d7 100644 --- a/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx +++ b/src/plugins/embeddable/public/lib/test_samples/embeddables/hello_world_container.tsx @@ -49,6 +49,7 @@ interface HelloWorldContainerOptions { getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; overlays: CoreStart['overlays']; + application: CoreStart['application']; notifications: CoreStart['notifications']; inspector: InspectorStartContract; SavedObjectFinder: React.ComponentType; @@ -81,6 +82,7 @@ export class HelloWorldContainer extends Container; @@ -112,6 +113,7 @@ export class HelloWorldContainerComponent extends Component { getAllEmbeddableFactories={this.props.getAllEmbeddableFactories} overlays={this.props.overlays} notifications={this.props.notifications} + application={this.props.application} inspector={this.props.inspector} SavedObjectFinder={this.props.SavedObjectFinder} /> diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index e29302fd6cc13..da7be1eea199a 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -18,18 +18,34 @@ */ import { Trigger } from '../../../../ui_actions/public'; +import { KibanaDatatable } from '../../../../expressions'; import { IEmbeddable } from '..'; export interface EmbeddableContext { embeddable: IEmbeddable; } -export interface EmbeddableVisTriggerContext { +export interface ValueClickTriggerContext { embeddable?: IEmbeddable; timeFieldName?: string; data: { - e?: MouseEvent; - data: unknown; + data: Array<{ + table: Pick; + column: number; + row: number; + value: any; + }>; + negate?: boolean; + }; +} + +export interface RangeSelectTriggerContext { + embeddable?: IEmbeddable; + timeFieldName?: string; + data: { + table: KibanaDatatable; + column: number; + range: number[]; }; } diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx index 01fbf52c80182..36f49f2508e80 100644 --- a/src/plugins/embeddable/public/plugin.tsx +++ b/src/plugins/embeddable/public/plugin.tsx @@ -118,6 +118,7 @@ export class EmbeddablePublicPlugin implements Plugin diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts index 3bd414ecf0d4a..ebb76c743393b 100644 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts @@ -110,6 +110,7 @@ test('ApplyFilterAction is incompatible if the root container does not accept a getAllEmbeddableFactories: api.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector, SavedObjectFinder: () => null, } @@ -145,6 +146,7 @@ test('trying to execute on incompatible context throws an error ', async () => { getAllEmbeddableFactories: api.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector, SavedObjectFinder: () => null, } diff --git a/src/plugins/embeddable/public/tests/container.test.ts b/src/plugins/embeddable/public/tests/container.test.ts index 1aae43550ec6f..d2769e208ba42 100644 --- a/src/plugins/embeddable/public/tests/container.test.ts +++ b/src/plugins/embeddable/public/tests/container.test.ts @@ -74,6 +74,7 @@ async function creatHelloWorldContainerAndEmbeddable( getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, }); @@ -147,6 +148,7 @@ test('Container.removeEmbeddable removes and cleans up', async done => { getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } @@ -327,6 +329,7 @@ test(`Container updates its state when a child's input is updated`, async done = getEmbeddableFactory: start.getEmbeddableFactory, notifications: coreStart.notifications, overlays: coreStart.overlays, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, }); @@ -584,6 +587,7 @@ test('Container changes made directly after adding a new embeddable are propagat getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } @@ -708,6 +712,7 @@ test('untilEmbeddableLoaded() throws an error if there is no such child panel in getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } @@ -742,6 +747,7 @@ test('untilEmbeddableLoaded() resolves if child is loaded in the container', asy getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } @@ -781,6 +787,7 @@ test('untilEmbeddableLoaded resolves with undefined if child is subsequently rem getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } @@ -821,6 +828,7 @@ test('adding a panel then subsequently removing it before its loaded removes the getAllEmbeddableFactories: start.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } diff --git a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx index 19e461b8bde7e..a9cb83504d958 100644 --- a/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx +++ b/src/plugins/embeddable/public/tests/customize_panel_modal.test.tsx @@ -63,6 +63,7 @@ beforeEach(async () => { getAllEmbeddableFactories: api.getEmbeddableFactories, overlays: coreStart.overlays, notifications: coreStart.notifications, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } diff --git a/src/plugins/embeddable/public/tests/explicit_input.test.ts b/src/plugins/embeddable/public/tests/explicit_input.test.ts index 0e03db3ec8358..ef3c4b6f17e7f 100644 --- a/src/plugins/embeddable/public/tests/explicit_input.test.ts +++ b/src/plugins/embeddable/public/tests/explicit_input.test.ts @@ -88,6 +88,7 @@ test('Explicit embeddable input mapped to undefined with no inherited value will getEmbeddableFactory: start.getEmbeddableFactory, notifications: coreStart.notifications, overlays: coreStart.overlays, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } @@ -136,6 +137,7 @@ test('Explicit input tests in async situations', (done: () => void) => { getEmbeddableFactory: start.getEmbeddableFactory, notifications: coreStart.notifications, overlays: coreStart.overlays, + application: coreStart.application, inspector: {} as any, SavedObjectFinder: () => null, } diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx new file mode 100644 index 0000000000000..68f1cf2045efb --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/authorization_provider.tsx @@ -0,0 +1,73 @@ +/* + * 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 { HttpSetup } from 'kibana/public'; +import React, { createContext, useContext } from 'react'; + +import { useRequest } from '../../../public'; + +import { Error as CustomError } from './section_error'; + +import { Privileges } from '../types'; + +interface Authorization { + isLoading: boolean; + apiError: CustomError | null; + privileges: Privileges; +} + +const initialValue: Authorization = { + isLoading: true, + apiError: null, + privileges: { + hasAllPrivileges: true, + missingPrivileges: {}, + }, +}; + +export const AuthorizationContext = createContext(initialValue); + +export const useAuthorizationContext = () => { + const ctx = useContext(AuthorizationContext); + if (!ctx) { + throw new Error('AuthorizationContext can only be used inside of AuthorizationProvider!'); + } + return ctx; +}; + +interface Props { + privilegesEndpoint: string; + children: React.ReactNode; + httpClient: HttpSetup; +} + +export const AuthorizationProvider = ({ privilegesEndpoint, httpClient, children }: Props) => { + const { isLoading, error, data: privilegesData } = useRequest(httpClient, { + path: privilegesEndpoint, + method: 'get', + }); + + const value = { + isLoading, + privileges: isLoading ? { hasAllPrivileges: true, missingPrivileges: {} } : privilegesData, + apiError: error ? error : null, + } as Authorization; + + return {children}; +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts new file mode 100644 index 0000000000000..71be3cc6152ca --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { + AuthorizationProvider, + AuthorizationContext, + useAuthorizationContext, +} from './authorization_provider'; + +export { WithPrivileges } from './with_privileges'; + +export { NotAuthorizedSection } from './not_authorized_section'; + +export { Error, SectionError } from './section_error'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/not_authorized_section.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/not_authorized_section.tsx new file mode 100644 index 0000000000000..c35f674ef9ec4 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/not_authorized_section.tsx @@ -0,0 +1,30 @@ +/* + * 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 React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; + +interface Props { + title: React.ReactNode; + message: React.ReactNode | string; +} + +export const NotAuthorizedSection = ({ title, message }: Props) => ( + {title}} body={

{message}

} /> +); diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx new file mode 100644 index 0000000000000..3d56309adae97 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/section_error.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import React, { Fragment } from 'react'; + +export interface Error { + error: string; + cause?: string[]; + message?: string; +} + +interface Props { + title: React.ReactNode; + error: Error; + actions?: JSX.Element; +} + +export const SectionError: React.FunctionComponent = ({ + title, + error, + actions, + ...rest +}) => { + const { + error: errorString, + cause, // wrapEsError() on the server adds a "cause" array + message, + } = error; + + return ( + + {cause ? message || errorString :

{message || errorString}

} + {cause && ( + + +
    + {cause.map((causeMsg, i) => ( +
  • {causeMsg}
  • + ))} +
+
+ )} + {actions ? actions : null} +
+ ); +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx new file mode 100644 index 0000000000000..8f4b2b976d141 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/components/with_privileges.tsx @@ -0,0 +1,90 @@ +/* + * 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 { MissingPrivileges } from '../types'; + +import { useAuthorizationContext } from './authorization_provider'; + +interface Props { + /** + * Each required privilege must have the format "section.privilege". + * To indicate that *all* privileges from a section are required, we can use the asterix + * e.g. "index.*" + */ + privileges: string | string[]; + children: (childrenProps: { + isLoading: boolean; + hasPrivileges: boolean; + privilegesMissing: MissingPrivileges; + }) => JSX.Element; +} + +type Privilege = [string, string]; + +const toArray = (value: string | string[]): string[] => + Array.isArray(value) ? (value as string[]) : ([value] as string[]); + +export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { + const { isLoading, privileges } = useAuthorizationContext(); + + const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map(p => { + const [section, privilege] = p.split('.'); + if (!privilege) { + // Oh! we forgot to use the dot "." notation. + throw new Error('Required privilege must have the format "section.privilege"'); + } + return [section, privilege]; + }); + + const hasPrivileges = isLoading + ? false + : privilegesToArray.every(privilege => { + const [section, requiredPrivilege] = privilege; + if (!privileges.missingPrivileges[section]) { + // if the section does not exist in our missingPriviledges, everything is OK + return true; + } + if (privileges.missingPrivileges[section]!.length === 0) { + return true; + } + if (requiredPrivilege === '*') { + // If length > 0 and we require them all... KO + return false; + } + // If we require _some_ privilege, we make sure that the one + // we require is *not* in the missingPrivilege array + return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); + }); + + const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { + if (privilege === '*') { + acc[section] = privileges.missingPrivileges[section] || []; + } else if ( + privileges.missingPrivileges[section] && + privileges.missingPrivileges[section]!.includes(privilege) + ) { + const missing: string[] = acc[section] || []; + acc[section] = [...missing, privilege]; + } + + return acc; + }, {} as MissingPrivileges); + + return children({ isLoading, hasPrivileges, privilegesMissing }); +}; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts new file mode 100644 index 0000000000000..ad89052b3bb54 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { + WithPrivileges, + NotAuthorizedSection, + AuthorizationProvider, + AuthorizationContext, + SectionError, + Error, + useAuthorizationContext, +} from './components'; + +export { Privileges, MissingPrivileges } from './types'; diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts new file mode 100644 index 0000000000000..cdc2052122688 --- /dev/null +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/authorization/types.ts @@ -0,0 +1,27 @@ +/* + * 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 interface MissingPrivileges { + [key: string]: string[] | undefined; +} + +export interface Privileges { + hasAllPrivileges: boolean; + missingPrivileges: MissingPrivileges; +} diff --git a/src/plugins/es_ui_shared/public/authorization/index.ts b/src/plugins/es_ui_shared/public/authorization/index.ts new file mode 100644 index 0000000000000..3a02c0d2694f3 --- /dev/null +++ b/src/plugins/es_ui_shared/public/authorization/index.ts @@ -0,0 +1,20 @@ +/* + * 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 * from '../../__packages_do_not_import__/authorization'; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts index a0371bf351193..7e5510d7c9c65 100644 --- a/src/plugins/es_ui_shared/public/index.ts +++ b/src/plugins/es_ui_shared/public/index.ts @@ -47,6 +47,18 @@ export { expandLiteralStrings, } from './console_lang'; +export { + AuthorizationContext, + AuthorizationProvider, + NotAuthorizedSection, + WithPrivileges, + Privileges, + MissingPrivileges, + SectionError, + Error, + useAuthorizationContext, +} from './authorization'; + /** dummy plugin, we just want esUiShared to have its own bundle */ export function plugin() { return new (class EsUiSharedPlugin { diff --git a/src/plugins/expressions/common/execution/execution.abortion.test.ts b/src/plugins/expressions/common/execution/execution.abortion.test.ts new file mode 100644 index 0000000000000..ecbf94eceae64 --- /dev/null +++ b/src/plugins/expressions/common/execution/execution.abortion.test.ts @@ -0,0 +1,96 @@ +/* + * 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 { Execution } from './execution'; +import { parseExpression } from '../ast'; +import { createUnitTestExecutor } from '../test_helpers'; + +jest.useFakeTimers(); + +beforeEach(() => { + jest.clearAllTimers(); +}); + +const createExecution = ( + expression: string = 'foo bar=123', + context: Record = {}, + debug: boolean = false +) => { + const executor = createUnitTestExecutor(); + const execution = new Execution({ + executor, + ast: parseExpression(expression), + context, + debug, + }); + return execution; +}; + +describe('Execution abortion tests', () => { + test('can abort an expression immediately', async () => { + const execution = createExecution('sleep 10'); + + execution.start(); + execution.cancel(); + + const result = await execution.result; + + expect(result).toMatchObject({ + type: 'error', + error: { + message: 'The expression was aborted.', + name: 'AbortError', + }, + }); + }); + + test('can abort an expression which has function running mid flight', async () => { + const execution = createExecution('sleep 300'); + + execution.start(); + jest.advanceTimersByTime(100); + execution.cancel(); + + const result = await execution.result; + + expect(result).toMatchObject({ + type: 'error', + error: { + message: 'The expression was aborted.', + name: 'AbortError', + }, + }); + }); + + test('cancelling execution after it completed has no effect', async () => { + jest.useRealTimers(); + + const execution = createExecution('sleep 1'); + + execution.start(); + + const result = await execution.result; + + execution.cancel(); + + expect(result).toBe(null); + + jest.useFakeTimers(); + }); +}); diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index d0ab178296408..6ee12d97a6422 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -22,7 +22,7 @@ import { Executor } from '../executor'; import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer, now } from '../../../kibana_utils/common'; -import { AbortError } from '../../../data/common'; +import { toPromise } from '../../../data/common/utils/abort_utils'; import { RequestAdapter, DataAdapter } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { @@ -38,6 +38,12 @@ import { ArgumentType, ExpressionFunction } from '../expression_functions'; import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; +const createAbortErrorValue = () => + createError({ + message: 'The expression was aborted.', + name: 'AbortError', + }); + export interface ExecutionParams< ExtraContext extends Record = Record > { @@ -70,7 +76,7 @@ export class Execution< /** * Dynamic state of the execution. */ - public readonly state: ExecutionContainer; + public readonly state: ExecutionContainer; /** * Initial input of the execution. @@ -91,6 +97,18 @@ export class Execution< */ private readonly abortController = new AbortController(); + /** + * Promise that rejects if/when abort controller sends "abort" signal. + */ + private readonly abortRejection = toPromise(this.abortController.signal, true); + + /** + * Races a given promise against the "abort" event of `abortController`. + */ + private race(promise: Promise): Promise { + return Promise.race([this.abortRejection, promise]); + } + /** * Whether .start() method has been called. */ @@ -99,7 +117,7 @@ export class Execution< /** * Future that tracks result or error of this execution. */ - private readonly firstResultFuture = new Defer(); + private readonly firstResultFuture = new Defer(); /** * Contract is a public representation of `Execution` instances. Contract we @@ -114,7 +132,7 @@ export class Execution< public readonly expression: string; - public get result(): Promise { + public get result(): Promise { return this.firstResultFuture.promise; } @@ -134,7 +152,7 @@ export class Execution< this.expression = params.expression || formatExpression(params.ast!); const ast = params.ast || parseExpression(this.expression); - this.state = createExecutionContainer({ + this.state = createExecutionContainer({ ...executor.state.get(), state: 'not-started', ast, @@ -173,7 +191,12 @@ export class Execution< this.state.transitions.start(); const { resolve, reject } = this.firstResultFuture; - this.invokeChain(this.state.get().ast.chain, input).then(resolve, reject); + const chainPromise = this.invokeChain(this.state.get().ast.chain, input); + + this.race(chainPromise).then(resolve, error => { + if (this.abortController.signal.aborted) resolve(createAbortErrorValue()); + else reject(error); + }); this.firstResultFuture.promise.then( result => { @@ -189,11 +212,6 @@ export class Execution< if (!chainArr.length) return input; for (const link of chainArr) { - // if execution was aborted return error - if (this.context.abortSignal && this.context.abortSignal.aborted) { - return createError(new AbortError('The expression was aborted.')); - } - const { function: fnName, arguments: fnArgs } = link; const fn = getByAlias(this.state.get().functions, fnName); @@ -207,10 +225,10 @@ export class Execution< try { // `resolveArgs` returns an object because the arguments themselves might // actually have a `then` function which would be treated as a `Promise`. - const { resolvedArgs } = await this.resolveArgs(fn, input, fnArgs); + const { resolvedArgs } = await this.race(this.resolveArgs(fn, input, fnArgs)); args = resolvedArgs; timeStart = this.params.debug ? now() : 0; - const output = await this.invokeFunction(fn, input, resolvedArgs); + const output = await this.race(this.invokeFunction(fn, input, resolvedArgs)); if (this.params.debug) { const timeEnd: number = now(); @@ -256,7 +274,7 @@ export class Execution< args: Record ): Promise { const normalizedInput = this.cast(input, fn.inputTypes); - const output = await fn.fn(normalizedInput, args, this.context); + const output = await this.race(fn.fn(normalizedInput, args, this.context)); // Validate that the function returned the type it said it would. // This isn't required, but it keeps function developers honest. diff --git a/src/plugins/expressions/common/expression_types/specs/error.ts b/src/plugins/expressions/common/expression_types/specs/error.ts index 4b255a0f967b2..35554954d0828 100644 --- a/src/plugins/expressions/common/expression_types/specs/error.ts +++ b/src/plugins/expressions/common/expression_types/specs/error.ts @@ -31,7 +31,7 @@ export type ExpressionValueError = ExpressionValueBoxed< name?: string; stack?: string; }; - info: unknown; + info?: unknown; } >; diff --git a/src/plugins/expressions/common/util/create_error.ts b/src/plugins/expressions/common/util/create_error.ts index 8236ff8709a82..bc27b0eda4959 100644 --- a/src/plugins/expressions/common/util/create_error.ts +++ b/src/plugins/expressions/common/util/create_error.ts @@ -17,9 +17,11 @@ * under the License. */ +import { ExpressionValueError } from '../../public'; + type ErrorLike = Partial>; -export const createError = (err: string | ErrorLike) => ({ +export const createError = (err: string | ErrorLike): ExpressionValueError => ({ type: 'error', error: { stack: @@ -28,7 +30,7 @@ export const createError = (err: string | ErrorLike) => ({ : typeof err === 'object' ? err.stack : undefined, - message: typeof err === 'string' ? err : err.message, + message: typeof err === 'string' ? err : String(err.message), name: typeof err === 'object' ? err.name || 'Error' : 'Error', }, }); diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json index cba693dd4bc20..5d2112103e94d 100644 --- a/src/plugins/expressions/kibana.json +++ b/src/plugins/expressions/kibana.json @@ -4,7 +4,6 @@ "server": true, "ui": true, "requiredPlugins": [ - "bfetch", - "inspector" + "bfetch" ] } diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index fbe2f37c648d6..418ff6fdf8614 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -19,13 +19,14 @@ import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { filter, map } from 'rxjs/operators'; -import { Adapters, InspectorSession } from '../../inspector/public'; -import { ExpressionRenderHandler } from './render'; +import { Adapters } from '../../inspector/public'; import { IExpressionLoaderParams } from './types'; import { ExpressionAstExpression } from '../common'; -import { getInspector, getExpressionsService } from './services'; import { ExecutionContract } from '../common/execution/execution_contract'; +import { ExpressionRenderHandler } from './render'; +import { getExpressionsService } from './services'; + type Data = any; export class ExpressionLoader { @@ -120,15 +121,6 @@ export class ExpressionLoader { return this.renderHandler.getElement(); } - openInspector(title: string): InspectorSession | undefined { - const inspector = this.inspect(); - if (inspector) { - return getInspector().open(inspector, { - title, - }); - } - } - inspect(): Adapters | undefined { return this.execution ? (this.execution.inspect() as Adapters) : undefined; } diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index cb7089f814643..b8f2f693e9c77 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -22,7 +22,6 @@ import { ExpressionsSetup, ExpressionsStart, plugin as pluginInitializer } from /* eslint-disable */ import { coreMock } from '../../../core/public/mocks'; -import { inspectorPluginMock } from '../../inspector/public/mocks'; import { bfetchPluginMock } from '../../bfetch/public/mocks'; /* eslint-enable */ @@ -89,7 +88,6 @@ const createPlugin = async () => { const plugin = pluginInitializer(pluginInitializerContext); const setup = await plugin.setup(coreSetup, { bfetch: bfetchPluginMock.createSetupContract(), - inspector: inspectorPluginMock.createSetupContract(), }); return { @@ -101,7 +99,6 @@ const createPlugin = async () => { doStart: async () => await plugin.start(coreStart, { bfetch: bfetchPluginMock.createStartContract(), - inspector: inspectorPluginMock.createStartContract(), }), }; }; diff --git a/src/plugins/expressions/public/plugin.ts b/src/plugins/expressions/public/plugin.ts index 7c0de271b7706..720c3b701d504 100644 --- a/src/plugins/expressions/public/plugin.ts +++ b/src/plugins/expressions/public/plugin.ts @@ -29,11 +29,9 @@ import { ExpressionsServiceStart, ExecutionContext, } from '../common'; -import { Setup as InspectorSetup, Start as InspectorStart } from '../../inspector/public'; import { BfetchPublicSetup, BfetchPublicStart } from '../../bfetch/public'; import { setCoreStart, - setInspector, setInterpreter, setRenderersRegistry, setNotifications, @@ -45,12 +43,10 @@ import { render, ExpressionRenderHandler } from './render'; export interface ExpressionsSetupDeps { bfetch: BfetchPublicSetup; - inspector: InspectorSetup; } export interface ExpressionsStartDeps { bfetch: BfetchPublicStart; - inspector: InspectorStart; } export interface ExpressionsSetup extends ExpressionsServiceSetup { @@ -120,7 +116,7 @@ export class ExpressionsPublicPlugin }); } - public setup(core: CoreSetup, { inspector, bfetch }: ExpressionsSetupDeps): ExpressionsSetup { + public setup(core: CoreSetup, { bfetch }: ExpressionsSetupDeps): ExpressionsSetup { this.configureExecutor(core); const { expressions } = this; @@ -180,9 +176,8 @@ export class ExpressionsPublicPlugin return Object.freeze(setup); } - public start(core: CoreStart, { inspector, bfetch }: ExpressionsStartDeps): ExpressionsStart { + public start(core: CoreStart, { bfetch }: ExpressionsStartDeps): ExpressionsStart { setCoreStart(core); - setInspector(inspector); setNotifications(core.notifications); const { expressions } = this; diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 242a49c6d6639..2c99f173c9f33 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -17,8 +17,7 @@ * under the License. */ -import { useRef, useEffect, useState, useLayoutEffect } from 'react'; -import React from 'react'; +import React, { useRef, useEffect, useState, useLayoutEffect } from 'react'; import classNames from 'classnames'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index ad4d16bcd1323..4aaf0da60fc60 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -21,10 +21,11 @@ import * as Rx from 'rxjs'; import { Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; import { RenderError, RenderErrorHandlerFnType, IExpressionLoaderParams } from './types'; -import { getRenderersRegistry } from './services'; import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler'; import { IInterpreterRenderHandlers, ExpressionAstExpression } from '../common'; +import { getRenderersRegistry } from './services'; + export type IExpressionRendererExtraHandlers = Record; export interface ExpressionRenderHandlerParams { diff --git a/src/plugins/expressions/public/services.ts b/src/plugins/expressions/public/services.ts index a203e87414571..016456c956666 100644 --- a/src/plugins/expressions/public/services.ts +++ b/src/plugins/expressions/public/services.ts @@ -20,14 +20,11 @@ import { NotificationsStart } from 'kibana/public'; import { createKibanaUtilsCore, createGetterSetter } from '../../kibana_utils/public'; import { ExpressionInterpreter } from './types'; -import { Start as IInspector } from '../../inspector/public'; import { ExpressionsSetup } from './plugin'; import { ExpressionsService } from '../common'; export const { getCoreStart, setCoreStart } = createKibanaUtilsCore(); -export const [getInspector, setInspector] = createGetterSetter('Inspector'); - export const [getInterpreter, setInterpreter] = createGetterSetter( 'Interpreter' ); diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index d5b047924f599..1c4b44a946e62 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -4,5 +4,5 @@ "server": true, "ui": true, "requiredPlugins": ["data", "kibanaLegacy"], - "optionalPlugins": ["usage_collection", "telemetry"] + "optionalPlugins": ["usageCollection", "telemetry"] } diff --git a/src/plugins/home/server/capabilities_provider.ts b/src/plugins/home/server/capabilities_provider.ts new file mode 100644 index 0000000000000..1c662e2301e16 --- /dev/null +++ b/src/plugins/home/server/capabilities_provider.ts @@ -0,0 +1,29 @@ +/* + * 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 const capabilitiesProvider = () => ({ + catalogue: { + discover: true, + dashboard: true, + visualize: true, + console: true, + advanced_settings: true, + index_patterns: true, + }, +}); diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index d2f2d7041024e..1050c19362ae1 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -26,9 +26,11 @@ import { SampleDataRegistryStart, } from './services'; import { UsageCollectionSetup } from '../../usage_collection/server'; +import { capabilitiesProvider } from './capabilities_provider'; +import { sampleDataTelemetry } from './saved_objects'; interface HomeServerPluginSetupDependencies { - usage_collection?: UsageCollectionSetup; + usageCollection?: UsageCollectionSetup; } export class HomeServerPlugin implements Plugin { @@ -37,9 +39,11 @@ export class HomeServerPlugin implements Plugin { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx rename to src/plugins/input_control_vis/public/components/editor/options_tab.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 97850879a2d38..b6b852bcfa707 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -24,11 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { - IIndexPattern, - IFieldType, - IndexPatternSelect, -} from '../../../../../../plugins/data/public'; +import { IIndexPattern, IFieldType, IndexPatternSelect } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface RangeControlEditorProps { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap index 59ae99260cecd..43e2af6d099e8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap +++ b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap @@ -46,6 +46,7 @@ exports[`renders ListControl 1`] = ` placeholder="Select..." selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> `; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss b/src/plugins/input_control_vis/public/components/vis/_index.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss rename to src/plugins/input_control_vis/public/components/vis/_index.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss b/src/plugins/input_control_vis/public/components/vis/_vis.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss rename to src/plugins/input_control_vis/public/components/vis/_vis.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index 1712f024f5b7b..b0b674ad7b6ee 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -28,8 +28,6 @@ import { InputControlVis } from './input_control_vis'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const mockListControl: ListControl = { id: 'mock-list-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx index e2497287f35d0..c0ef99664fdf8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -23,8 +23,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { CONTROL_TYPES } from '../../editor_utils'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -import { ListControl as ListControlComponent } from '../vis/list_control'; -import { RangeControl as RangeControlComponent } from '../vis/range_control'; +import { ListControl as ListControlComponent } from './list_control'; +import { RangeControl as RangeControlComponent } from './range_control'; function isListControl(control: RangeControl | ListControl): control is ListControl { return control.type === CONTROL_TYPES.LIST; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.test.tsx index 639616151a395..ff5d572fa21c4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx @@ -23,8 +23,6 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { RangeControl, ceilWithPrecision, floorWithPrecision } from './range_control'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const control: RangeControlClass = { id: 'mock-range-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.tsx index 0cd2a2b331980..f028feaf5f84f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.tsx @@ -19,7 +19,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; -import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; +import { ValidatedDualRange } from '../../../../kibana_react/public'; import { FormRow } from './form_row'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts b/src/plugins/input_control_vis/public/control/control.test.ts similarity index 94% rename from src/legacy/core_plugins/input_control_vis/public/control/control.test.ts rename to src/plugins/input_control_vis/public/control/control.test.ts index e76b199a0262c..a2d220c14a3f7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts +++ b/src/plugins/input_control_vis/public/control/control.test.ts @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import { Control } from './control'; import { ControlParams } from '../editor_utils'; import { FilterManager as BaseFilterManager } from './filter_manager/filter_manager'; -import { SearchSource } from '../legacy_imports'; function createControlParams(id: string, label: string): ControlParams { return { @@ -51,18 +50,12 @@ class ControlMock extends Control { destroy() {} } -const mockKbnApi: SearchSource = {} as SearchSource; describe('hasChanged', () => { let control: ControlMock; beforeEach(() => { - control = new ControlMock( - createControlParams('3', 'control'), - mockFilterManager, - false, - mockKbnApi - ); + control = new ControlMock(createControlParams('3', 'control'), mockFilterManager, false); }); afterEach(() => { @@ -93,20 +86,17 @@ describe('ancestors', () => { grandParentControl = new ControlMock( createControlParams('1', 'grandparent control'), mockFilterManager, - false, - mockKbnApi + false ); parentControl = new ControlMock( createControlParams('2', 'parent control'), mockFilterManager, - false, - mockKbnApi + false ); childControl = new ControlMock( createControlParams('3', 'child control'), mockFilterManager, - false, - mockKbnApi + false ); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.ts b/src/plugins/input_control_vis/public/control/control.ts similarity index 95% rename from src/legacy/core_plugins/input_control_vis/public/control/control.ts rename to src/plugins/input_control_vis/public/control/control.ts index 6fddef777f73e..c57b09a19ebc8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.ts +++ b/src/plugins/input_control_vis/public/control/control.ts @@ -22,8 +22,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Filter } from '../../../../../plugins/data/public'; -import { SearchSource as SearchSourceClass } from '../legacy_imports'; +import { Filter } from 'src/plugins/data/public'; import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; @@ -61,8 +60,7 @@ export abstract class Control { constructor( public controlParams: ControlParams, public filterManager: FilterManager, - public useTimeFilter: boolean, - public SearchSource: SearchSourceClass + public useTimeFilter: boolean ) { this.id = controlParams.id; this.controlParams = controlParams; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts b/src/plugins/input_control_vis/public/control/control_factory.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts rename to src/plugins/input_control_vis/public/control/control_factory.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/plugins/input_control_vis/public/control/create_search_source.ts similarity index 83% rename from src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts rename to src/plugins/input_control_vis/public/control/create_search_source.ts index f238a2287ecdb..d6772a7cba5b8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/plugins/input_control_vis/public/control/create_search_source.ts @@ -17,11 +17,16 @@ * under the License. */ -import { PhraseFilter, IndexPattern, TimefilterContract } from '../../../../../plugins/data/public'; -import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; +import { + SearchSourceFields, + PhraseFilter, + IndexPattern, + TimefilterContract, + DataPublicPluginStart, +} from 'src/plugins/data/public'; export function createSearchSource( - SearchSource: SearchSourceClass, + { create }: DataPublicPluginStart['search']['searchSource'], initialState: SearchSourceFields | null, indexPattern: IndexPattern, aggs: any, @@ -29,7 +34,8 @@ export function createSearchSource( filters: PhraseFilter[] = [], timefilter: TimefilterContract ) { - const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); + const searchSource = create(initialState || {}); + // Do not not inherit from rootSearchSource to avoid picking up time and globals searchSource.setParent(undefined); searchSource.setField('filter', () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index 39c9d843e6bce..a9b7550be44ae 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -20,12 +20,8 @@ import expect from '@kbn/expect'; import { FilterManager } from './filter_manager'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; const setupMock = coreMock.createSetup(); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index 90b88a56950e2..bb806b336c3e0 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -19,11 +19,7 @@ import _ from 'lodash'; -import { - FilterManager as QueryFilterManager, - IndexPattern, - Filter, -} from '../../../../../../plugins/data/public'; +import { FilterManager as QueryFilterManager, IndexPattern, Filter } from '../../../../data/public'; export abstract class FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index 5be5d0157541e..6398c10b63a8c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -19,11 +19,7 @@ import expect from '@kbn/expect'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; import { PhraseFilterManager } from './phrase_filter_manager'; describe('PhraseFilterManager', function() { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 6f4a95b491907..bf167afa69bcf 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -25,7 +25,7 @@ import { esFilters, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; export class PhraseFilterManager extends FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index c776042ea4ba6..6e66b6942e5d3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -25,7 +25,7 @@ import { RangeFilterMeta, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; describe('RangeFilterManager', function() { const controlId = 'control1'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts similarity index 95% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 7a6719e85961b..1a884cf267c41 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -20,12 +20,7 @@ import _ from 'lodash'; import { FilterManager } from './filter_manager'; -import { - esFilters, - RangeFilter, - RangeFilterParams, - IFieldType, -} from '../../../../../../plugins/data/public'; +import { esFilters, RangeFilter, RangeFilterParams, IFieldType } from '../../../../data/public'; interface SliderValue { min?: string | number; diff --git a/src/plugins/input_control_vis/public/control/list_control_factory.test.ts b/src/plugins/input_control_vis/public/control/list_control_factory.test.ts new file mode 100644 index 0000000000000..72070175a233c --- /dev/null +++ b/src/plugins/input_control_vis/public/control/list_control_factory.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { listControlFactory, ListControl } from './list_control_factory'; +import { ControlParams, CONTROL_TYPES } from '../editor_utils'; +import { getDepsMock, getSearchSourceMock } from '../test_utils'; + +describe('listControlFactory', () => { + const searchSourceMock = getSearchSourceMock(); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + describe('hasValue', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + const useTimeFilter = false; + + let listControl: ListControl; + beforeEach(async () => { + listControl = await listControlFactory(controlParams, useTimeFilter, deps); + }); + + test('should be false when control has no value', () => { + expect(listControl.hasValue()).toBe(false); + }); + + test('should be true when control has value', () => { + listControl.set([{ value: 'selected option', label: 'selection option' }]); + expect(listControl.hasValue()).toBe(true); + }); + + test('should be true when control has value that is the string "false"', () => { + listControl.set([{ value: 'false', label: 'selection option' }]); + expect(listControl.hasValue()).toBe(true); + }); + }); + + describe('fetch', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + const useTimeFilter = false; + + let listControl: ListControl; + beforeEach(async () => { + listControl = await listControlFactory(controlParams, useTimeFilter, deps); + }); + + test('should pass in timeout parameters from injected vars', async () => { + await listControl.fetch(); + expect(searchSourceMock).toHaveBeenCalledWith({ + timeout: `1000ms`, + terminate_after: 100000, + }); + }); + + test('should set selectOptions to results of terms aggregation', async () => { + await listControl.fetch(); + expect(listControl.selectOptions).toEqual([ + 'Zurich Airport', + 'Xi an Xianyang International Airport', + ]); + }); + }); + + describe('fetch with ancestors', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + const useTimeFilter = false; + + let listControl: ListControl; + let parentControl; + beforeEach(async () => { + listControl = await listControlFactory(controlParams, useTimeFilter, deps); + + const parentControlParams: ControlParams = { + id: 'parent', + fieldName: 'myField', + options: {} as any, + type: CONTROL_TYPES.LIST, + label: 'test', + indexPattern: {} as any, + parent: 'parent', + }; + parentControl = await listControlFactory(parentControlParams, useTimeFilter, deps); + parentControl.clear(); + listControl.setAncestors([parentControl]); + }); + + describe('ancestor does not have value', () => { + test('should disable control', async () => { + await listControl.fetch(); + expect(listControl.isEnabled()).toBe(false); + }); + + test('should reset lastAncestorValues', async () => { + listControl.lastAncestorValues = 'last ancestor value'; + await listControl.fetch(); + expect(listControl.lastAncestorValues).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts b/src/plugins/input_control_vis/public/control/list_control_factory.ts similarity index 94% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts rename to src/plugins/input_control_vis/public/control/list_control_factory.ts index 8364c82efecdb..123ef83277e0b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/list_control_factory.ts @@ -19,14 +19,17 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; - -import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; +import { + IFieldType, + TimefilterContract, + SearchSourceFields, + DataPublicPluginStart, +} from 'src/plugins/data/public'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterContract } from '../../../../../plugins/data/public'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators @@ -75,6 +78,7 @@ const termsAgg = ({ field, size, direction, query }: TermsAggArgs) => { export class ListControl extends Control { private getInjectedVar: InputControlVisDependencies['core']['injectedMetadata']['getInjectedVar']; private timefilter: TimefilterContract; + private searchSource: DataPublicPluginStart['search']['searchSource']; abortController?: AbortController; lastAncestorValues: any; @@ -86,12 +90,13 @@ export class ListControl extends Control { controlParams: ControlParams, filterManager: PhraseFilterManager, useTimeFilter: boolean, - SearchSource: SearchSourceClass, + searchSource: DataPublicPluginStart['search']['searchSource'], deps: InputControlVisDependencies ) { - super(controlParams, filterManager, useTimeFilter, SearchSource); + super(controlParams, filterManager, useTimeFilter); this.getInjectedVar = deps.core.injectedMetadata.getInjectedVar; this.timefilter = deps.data.query.timefilter.timefilter; + this.searchSource = searchSource; } fetch = async (query?: string) => { @@ -143,7 +148,7 @@ export class ListControl extends Control { query, }); const searchSource = createSearchSource( - this.SearchSource, + this.searchSource, initialSearchSourceState, indexPattern, aggs, @@ -202,7 +207,6 @@ export class ListControl extends Control { export async function listControlFactory( controlParams: ControlParams, useTimeFilter: boolean, - SearchSource: SearchSourceClass, deps: InputControlVisDependencies ) { const [, { data: dataPluginStart }] = await deps.core.getStartServices(); @@ -225,7 +229,7 @@ export async function listControlFactory( deps.data.query.filterManager ), useTimeFilter, - SearchSource, + dataPluginStart.search.searchSource, deps ); return listControl; diff --git a/src/plugins/input_control_vis/public/control/range_control_factory.test.ts b/src/plugins/input_control_vis/public/control/range_control_factory.test.ts new file mode 100644 index 0000000000000..084c02e138a2d --- /dev/null +++ b/src/plugins/input_control_vis/public/control/range_control_factory.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { rangeControlFactory } from './range_control_factory'; +import { ControlParams, CONTROL_TYPES } from '../editor_utils'; +import { getDepsMock, getSearchSourceMock } from '../test_utils'; + +describe('rangeControlFactory', () => { + describe('fetch', () => { + const controlParams: ControlParams = { + id: '1', + fieldName: 'myNumberField', + options: {}, + type: CONTROL_TYPES.RANGE, + label: 'test', + indexPattern: {} as any, + parent: {} as any, + }; + const useTimeFilter = false; + + test('should set min and max from aggregation results', async () => { + const esSearchResponse = { + aggregations: { + maxAgg: { value: 100 }, + minAgg: { value: 10 }, + }, + }; + const searchSourceMock = getSearchSourceMock(esSearchResponse); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + const rangeControl = await rangeControlFactory(controlParams, useTimeFilter, deps); + await rangeControl.fetch(); + + expect(rangeControl.isEnabled()).toBe(true); + expect(rangeControl.min).toBe(10); + expect(rangeControl.max).toBe(100); + }); + + test('should disable control when there are 0 hits', async () => { + // ES response when the query does not match any documents + const esSearchResponse = { + aggregations: { + maxAgg: { value: null }, + minAgg: { value: null }, + }, + }; + const searchSourceMock = getSearchSourceMock(esSearchResponse); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + const rangeControl = await rangeControlFactory(controlParams, useTimeFilter, deps); + await rangeControl.fetch(); + + expect(rangeControl.isEnabled()).toBe(false); + }); + + test('should disable control when response is empty', async () => { + // ES response for dashboardonly user who does not have read permissions on index is 200 (which is weird) + // and there is not aggregations key + const esSearchResponse = {}; + const searchSourceMock = getSearchSourceMock(esSearchResponse); + const deps = getDepsMock({ + searchSource: { + create: searchSourceMock, + }, + }); + + const rangeControl = await rangeControlFactory(controlParams, useTimeFilter, deps); + await rangeControl.fetch(); + + expect(rangeControl.isEnabled()).toBe(false); + }); + }); +}); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts b/src/plugins/input_control_vis/public/control/range_control_factory.ts similarity index 91% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts rename to src/plugins/input_control_vis/public/control/range_control_factory.ts index d9b43c9dff201..326756ad5ffc6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/range_control_factory.ts @@ -20,13 +20,12 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { SearchSource as SearchSourceClass } from '../legacy_imports'; +import { IFieldType, TimefilterContract, DataPublicPluginStart } from 'src/plugins/data/public'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterContract } from '../.../../../../../../plugins/data/public'; const minMaxAgg = (field?: IFieldType) => { const aggBody: any = {}; @@ -52,6 +51,8 @@ const minMaxAgg = (field?: IFieldType) => { }; export class RangeControl extends Control { + private searchSource: DataPublicPluginStart['search']['searchSource']; + timefilter: TimefilterContract; abortController: any; min: any; @@ -61,11 +62,12 @@ export class RangeControl extends Control { controlParams: ControlParams, filterManager: RangeFilterManager, useTimeFilter: boolean, - SearchSource: SearchSourceClass, + searchSource: DataPublicPluginStart['search']['searchSource'], deps: InputControlVisDependencies ) { - super(controlParams, filterManager, useTimeFilter, SearchSource); + super(controlParams, filterManager, useTimeFilter); this.timefilter = deps.data.query.timefilter.timefilter; + this.searchSource = searchSource; } async fetch() { @@ -83,7 +85,7 @@ export class RangeControl extends Control { const fieldName = this.filterManager.fieldName; const aggs = minMaxAgg(indexPattern.fields.getByName(fieldName)); const searchSource = createSearchSource( - this.SearchSource, + this.searchSource, null, indexPattern, aggs, @@ -129,7 +131,6 @@ export class RangeControl extends Control { export async function rangeControlFactory( controlParams: ControlParams, useTimeFilter: boolean, - SearchSource: SearchSourceClass, deps: InputControlVisDependencies ): Promise { const [, { data: dataPluginStart }] = await deps.core.getStartServices(); @@ -144,7 +145,7 @@ export async function rangeControlFactory( deps.data.query.filterManager ), useTimeFilter, - SearchSource, + dataPluginStart.search.searchSource, deps ); } diff --git a/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts b/src/plugins/input_control_vis/public/editor_utils.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/editor_utils.ts rename to src/plugins/input_control_vis/public/editor_utils.ts diff --git a/src/plugins/input_control_vis/public/index.scss b/src/plugins/input_control_vis/public/index.scss new file mode 100644 index 0000000000000..42fded23d7761 --- /dev/null +++ b/src/plugins/input_control_vis/public/index.scss @@ -0,0 +1,9 @@ +// Prefix all styles with "icv" to avoid conflicts. +// Examples +// icvChart +// icvChart__legend +// icvChart__legend--small +// icvChart__legend-isLoading + +@import './components/editor/index'; +@import './components/vis/index'; diff --git a/src/plugins/input_control_vis/public/index.ts b/src/plugins/input_control_vis/public/index.ts new file mode 100644 index 0000000000000..8edd3fd9996c3 --- /dev/null +++ b/src/plugins/input_control_vis/public/index.ts @@ -0,0 +1,27 @@ +/* + * 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 './index.scss'; + +import { PluginInitializerContext } from '../../../core/public'; +import { InputControlVisPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts b/src/plugins/input_control_vis/public/input_control_fn.test.ts similarity index 86% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts rename to src/plugins/input_control_vis/public/input_control_fn.test.ts index acc214ed31180..f3ea2d2d6f0ba 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.test.ts @@ -18,11 +18,7 @@ */ import { createInputControlVisFn } from './input_control_fn'; - -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; - -jest.mock('./legacy_imports.ts'); +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#input_control_vis', () => { const fn = functionWrapper(createInputControlVisFn()); @@ -48,8 +44,9 @@ describe('interpreter/functions#input_control_vis', () => { pinFilters: false, }; - it('returns an object with the correct structure', async () => { + test('returns an object with the correct structure', async () => { const actual = await fn(null, { visConfig: JSON.stringify(visConfig) }); + expect(actual).toMatchSnapshot(); }); }); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts rename to src/plugins/input_control_vis/public/input_control_fn.ts index e779c6d344ab5..59c0e03505bb7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; interface Arguments { visConfig: string; diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts similarity index 96% rename from src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts rename to src/plugins/input_control_vis/public/input_control_vis_type.ts index badea68eec19f..8114dbf110f8b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -23,7 +23,7 @@ import { createInputControlVisController } from './vis_controller'; import { getControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; import { InputControlVisDependencies } from './plugin'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { const InputControlVisController = createInputControlVisController(deps); diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/index.ts b/src/plugins/input_control_vis/public/lineage/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/index.ts rename to src/plugins/input_control_vis/public/lineage/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.ts diff --git a/src/plugins/input_control_vis/public/plugin.ts b/src/plugins/input_control_vis/public/plugin.ts new file mode 100644 index 0000000000000..9fc7df24c2dcc --- /dev/null +++ b/src/plugins/input_control_vis/public/plugin.ts @@ -0,0 +1,70 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; + +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; +import { createInputControlVisFn } from './input_control_fn'; +import { createInputControlVisTypeDefinition } from './input_control_vis_type'; + +type InputControlVisCoreSetup = CoreSetup; + +export interface InputControlVisDependencies { + core: InputControlVisCoreSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; +} + +/** @internal */ +export interface InputControlVisPluginStartDependencies { + expressions: ReturnType; + visualizations: VisualizationsStart; + data: DataPublicPluginStart; +} + +/** @internal */ +export class InputControlVisPlugin implements Plugin { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: InputControlVisCoreSetup, + { expressions, visualizations, data }: InputControlVisPluginSetupDependencies + ) { + const visualizationDependencies: Readonly = { + core, + data, + }; + + expressions.registerFunction(createInputControlVisFn); + visualizations.createBaseVisualization( + createInputControlVisTypeDefinition(visualizationDependencies) + ); + } + + public start(core: CoreStart, deps: InputControlVisPluginStartDependencies) { + // nothing to do here + } +} diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx b/src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx similarity index 85% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx rename to src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx index 78a4ef3a5597a..efd7d4c020854 100644 --- a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx +++ b/src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { InputControlVisDependencies } from '../plugin'; +import { getSearchSourceMock } from './get_search_service_mock'; const fields = [] as any; fields.push({ name: 'myField' } as any); @@ -26,13 +27,20 @@ fields.getByName = (name: any) => { return fields.find(({ name: n }: { name: string }) => n === name); }; -export const getDepsMock = (): InputControlVisDependencies => +export const getDepsMock = ({ + searchSource = { + create: getSearchSourceMock(), + }, +} = {}): InputControlVisDependencies => ({ core: { getStartServices: jest.fn().mockReturnValue([ null, { data: { + search: { + searchSource, + }, ui: { IndexPatternSelect: () => (
) as any, }, @@ -58,6 +66,11 @@ export const getDepsMock = (): InputControlVisDependencies => }, }, data: { + search: { + searchSource: { + create: getSearchSourceMock(), + }, + }, query: { filterManager: { fieldName: 'myField', diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts similarity index 91% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts index 94a460086e9da..24b7d7bcbb5c1 100644 --- a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts +++ b/src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts @@ -17,9 +17,7 @@ * under the License. */ -import { SearchSource } from '../legacy_imports'; - -export const getSearchSourceMock = (esSearchResponse?: any): SearchSource => +export const getSearchSourceMock = (esSearchResponse?: any) => jest.fn().mockImplementation(() => ({ setParent: jest.fn(), setField: jest.fn(), diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts b/src/plugins/input_control_vis/public/test_utils/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts rename to src/plugins/input_control_vis/public/test_utils/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts b/src/plugins/input_control_vis/public/test_utils/update_component.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts rename to src/plugins/input_control_vis/public/test_utils/update_component.ts diff --git a/src/plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx new file mode 100644 index 0000000000000..97506556d7e0a --- /dev/null +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -0,0 +1,224 @@ +/* + * 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 React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { I18nStart } from 'kibana/public'; +import { InputControlVis } from './components/vis/input_control_vis'; +import { getControlFactory } from './control/control_factory'; +import { getLineageMap } from './lineage'; +import { ControlParams } from './editor_utils'; +import { RangeControl } from './control/range_control_factory'; +import { ListControl } from './control/list_control_factory'; +import { InputControlVisDependencies } from './plugin'; +import { FilterManager, Filter } from '../../data/public'; +import { VisParams, Vis } from '../../visualizations/public'; + +export const createInputControlVisController = (deps: InputControlVisDependencies) => { + return class InputControlVisController { + private I18nContext?: I18nStart['Context']; + + controls: Array; + queryBarUpdateHandler: () => void; + filterManager: FilterManager; + updateSubsciption: any; + visParams?: VisParams; + + constructor(public el: Element, public vis: Vis) { + this.controls = []; + + this.queryBarUpdateHandler = this.updateControlsFromKbn.bind(this); + + this.filterManager = deps.data.query.filterManager; + this.updateSubsciption = this.filterManager + .getUpdates$() + .subscribe(this.queryBarUpdateHandler); + } + + async render(visData: any, visParams: VisParams) { + this.visParams = visParams; + this.controls = []; + this.controls = await this.initControls(); + const [{ i18n }] = await deps.core.getStartServices(); + this.I18nContext = i18n.Context; + this.drawVis(); + } + + destroy() { + this.updateSubsciption.unsubscribe(); + unmountComponentAtNode(this.el); + this.controls.forEach(control => control.destroy()); + } + + drawVis = () => { + if (!this.I18nContext) { + throw new Error('no i18n context found'); + } + + render( + + + , + this.el + ); + }; + + async initControls() { + const controlParamsList = (this.visParams?.controls as ControlParams[])?.filter( + controlParams => { + // ignore controls that do not have indexPattern or field + return controlParams.indexPattern && controlParams.fieldName; + } + ); + + const controlFactoryPromises = controlParamsList.map(controlParams => { + const factory = getControlFactory(controlParams); + + return factory(controlParams, this.visParams?.useTimeFilter, deps); + }); + const controls = await Promise.all(controlFactoryPromises); + + const getControl = (controlId: string) => { + return controls.find(({ id }) => id === controlId); + }; + + const controlInitPromises: Array> = []; + getLineageMap(controlParamsList).forEach((lineage, controlId) => { + // first lineage item is the control. remove it + lineage.shift(); + const ancestors: Array = []; + lineage.forEach(ancestorId => { + const control = getControl(ancestorId); + + if (control) { + ancestors.push(control); + } + }); + const control = getControl(controlId); + + if (control) { + control.setAncestors(ancestors); + controlInitPromises.push(control.fetch()); + } + }); + + await Promise.all(controlInitPromises); + return controls; + } + + stageFilter = async (controlIndex: number, newValue: any) => { + this.controls[controlIndex].set(newValue); + if (this.visParams?.updateFiltersOnChange) { + // submit filters on each control change + this.submitFilters(); + } else { + // Do not submit filters, just update vis so controls are updated with latest value + await this.updateNestedControls(); + this.drawVis(); + } + }; + + submitFilters = () => { + const stagedControls = this.controls.filter(control => { + return control.hasChanged(); + }); + + const newFilters = stagedControls + .map(control => control.getKbnFilter()) + .filter((filter): filter is Filter => { + return filter !== null; + }); + + stagedControls.forEach(control => { + // to avoid duplicate filters, remove any old filters for control + control.filterManager.findFilters().forEach(existingFilter => { + this.filterManager.removeFilter(existingFilter); + }); + }); + + // Clean up filter pills for nested controls that are now disabled because ancestors are not set. + // This has to be done after looking up the staged controls because otherwise removing a filter + // will re-sync the controls of all other filters. + this.controls.map(control => { + if (control.hasAncestors() && control.hasUnsetAncestor()) { + control.filterManager.findFilters().forEach(existingFilter => { + this.filterManager.removeFilter(existingFilter); + }); + } + }); + + this.filterManager.addFilters(newFilters, this.visParams?.pinFilters); + }; + + clearControls = async () => { + this.controls.forEach(control => { + control.clear(); + }); + await this.updateNestedControls(); + this.drawVis(); + }; + + updateControlsFromKbn = async () => { + this.controls.forEach(control => { + control.reset(); + }); + await this.updateNestedControls(); + this.drawVis(); + }; + + async updateNestedControls() { + const fetchPromises = this.controls.map(async control => { + if (control.hasAncestors()) { + await control.fetch(); + } + }); + return await Promise.all(fetchPromises); + } + + hasChanges = () => { + return this.controls.map(control => control.hasChanged()).some(control => control); + }; + + hasValues = () => { + return this.controls + .map(control => { + return control.hasValue(); + }) + .reduce((a, b) => { + return a || b; + }); + }; + + refreshControl = async (controlIndex: number, query: any) => { + await this.controls[controlIndex].fetch(query); + this.drawVis(); + }; + }; +}; diff --git a/src/plugins/input_control_vis/server/index.ts b/src/plugins/input_control_vis/server/index.ts new file mode 100644 index 0000000000000..043657ba98a3c --- /dev/null +++ b/src/plugins/input_control_vis/server/index.ts @@ -0,0 +1,30 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export const config: PluginConfigDescriptor = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index 71cd57ef2d72e..295cf27688c80 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -92,9 +92,9 @@ export const configureAppAngularModule = ( ) => { const core = 'core' in newPlatform ? newPlatform.core : newPlatform; const packageInfo = - 'injectedMetadata' in newPlatform - ? newPlatform.injectedMetadata.getLegacyMetadata() - : newPlatform.env.packageInfo; + 'env' in newPlatform + ? newPlatform.env.packageInfo + : newPlatform.injectedMetadata.getLegacyMetadata(); if ('injectedMetadata' in newPlatform) { forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index 8e9a05b186191..2fdd0d8b4be59 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -25,6 +25,7 @@ export type Start = jest.Mocked>; const createSetupContract = (): Setup => ({ forwardApp: jest.fn(), + registerLegacyAppAlias: jest.fn(), registerLegacyApp: jest.fn(), config: { defaultAppId: 'home', @@ -37,6 +38,7 @@ const createSetupContract = (): Setup => ({ const createStartContract = (): Start => ({ getApps: jest.fn(), + getLegacyAppAliases: jest.fn(), getForwards: jest.fn(), config: { defaultAppId: 'home', diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 2ad620f355848..831fc3f0d4a71 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -28,12 +28,18 @@ import { Observable } from 'rxjs'; import { ConfigSchema } from '../config'; import { getDashboardConfig } from './dashboard_config'; -interface ForwardDefinition { +interface LegacyAppAliasDefinition { legacyAppId: string; newAppId: string; keepPrefix: boolean; } +interface ForwardDefinition { + legacyAppId: string; + newAppId: string; + rewritePath: (legacyPath: string) => string; +} + export type AngularRenderedAppUpdater = ( app: AppBase ) => Partial | undefined; @@ -54,7 +60,8 @@ export interface AngularRenderedApp extends App { export class KibanaLegacyPlugin { private apps: AngularRenderedApp[] = []; - private forwards: ForwardDefinition[] = []; + private legacyAppAliases: LegacyAppAliasDefinition[] = []; + private forwardDefinitions: ForwardDefinition[] = []; constructor(private readonly initializerContext: PluginInitializerContext) {} @@ -94,17 +101,55 @@ export class KibanaLegacyPlugin { * renaming or nesting plugins. For route changes after the prefix, please * use the routing mechanism of your app. * + * This method just redirects URLs within the legacy `kibana` app. + * * @param legacyAppId The name of the old app to forward URLs from * @param newAppId The name of the new app that handles the URLs now * @param options Whether the prefix of the old app is kept to nest the legacy * path into the new path */ - forwardApp: ( + registerLegacyAppAlias: ( legacyAppId: string, newAppId: string, options: { keepPrefix: boolean } = { keepPrefix: false } ) => { - this.forwards.push({ legacyAppId, newAppId, ...options }); + this.legacyAppAliases.push({ legacyAppId, newAppId, ...options }); + }, + + /** + * Forwards URLs within the legacy `kibana` app to a new platform application. + * + * @param legacyAppId The name of the old app to forward URLs from + * @param newAppId The name of the new app that handles the URLs now + * @param rewritePath Function to rewrite the legacy sub path of the app to the new path in the core app + * path into the new path + * + * Example usage: + * ``` + * kibanaLegacy.forwardApp( + * 'old', + * 'new', + * path => { + * const [, id] = /old/item\/(.*)$/.exec(path) || []; + * if (!id) { + * return '#/home'; + * } + * return '#/items/${id}'; + * } + * ); + * ``` + * This will cause the following redirects: + * + * * app/kibana#/old/ -> app/new#/home + * * app/kibana#/old/item/123 -> app/new#/items/123 + * + */ + forwardApp: ( + legacyAppId: string, + newAppId: string, + rewritePath: (legacyPath: string) => string + ) => { + this.forwardDefinitions.push({ legacyAppId, newAppId, rewritePath }); }, /** @@ -132,7 +177,12 @@ export class KibanaLegacyPlugin { * @deprecated * Just exported for wiring up with legacy platform, should not be used. */ - getForwards: () => this.forwards, + getLegacyAppAliases: () => this.legacyAppAliases, + /** + * @deprecated + * Just exported for wiring up with legacy platform, should not be used. + */ + getForwards: () => this.forwardDefinitions, config: this.initializerContext.config.get(), dashboardConfig: getDashboardConfig(!application.capabilities.dashboard.showWriteControls), }; diff --git a/src/plugins/kibana_utils/public/core/create_start_service_getter.ts b/src/plugins/kibana_utils/public/core/create_start_service_getter.ts index e507d1ae778e5..5e385eb5ed473 100644 --- a/src/plugins/kibana_utils/public/core/create_start_service_getter.ts +++ b/src/plugins/kibana_utils/public/core/create_start_service_getter.ts @@ -30,6 +30,48 @@ export type StartServicesGetter = () = OwnContract >; +/** + * Use this utility to create a synchronous *start* service getter in *setup* + * life-cycle of your plugin. + * + * Below is a usage example in a Kibana plugin. + * + * ```ts + * export interface MyPluginStartDeps { + * data: DataPublicPluginStart; + * expressions: ExpressionsStart; + * inspector: InspectorStart; + * uiActions: UiActionsStart; + * } + * + * class MyPlugin implements Plugin { + * setup(core: CoreSetup, plugins) { + * const start = createStartServicesGetter(core.getStartServices); + * plugins.expressions.registerFunction(myExpressionFunction(start)); + * } + * + * start(core, plugins: MyPluginStartDeps) { + * + * } + * } + * ``` + * + * In `myExpressionFunction` you can make sure you are picking only the dependencies + * your function needs using the `Pick` type. + * + * ```ts + * const myExpressionFunction = + * (start: StartServicesGetter>) => { + * + * start().plugins.indexPatterns.something(123); + * } + * ``` + * + * @param accessor Asynchronous start service accessor provided by platform. + * @returns Returns a function which synchronously returns *start* core services + * and plugin contracts. If you call this function before the *start* life-cycle + * has started it will throw. + */ export const createStartServicesGetter = ( accessor: StartServicesAccessor ): StartServicesGetter => { diff --git a/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx b/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx deleted file mode 100644 index 7992f650cb372..0000000000000 --- a/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 { contains } from 'lodash'; -import React from 'react'; -import { History } from 'history'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut } from '@elastic/eui'; -import { CoreStart } from 'kibana/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { toMountPoint } from '../../../kibana_react/public'; - -let bannerId: string; -let timeoutId: NodeJS.Timeout | undefined; - -/** - * Checks whether a default index pattern is set and exists and defines - * one otherwise. - * - * If there are no index patterns, redirect to management page and show - * banner. In this case the promise returned from this function will never - * resolve to wait for the URL change to happen. - */ -export async function ensureDefaultIndexPattern( - core: CoreStart, - data: DataPublicPluginStart, - history: History -) { - const patterns = await data.indexPatterns.getIds(); - let defaultId = core.uiSettings.get('defaultIndex'); - let defined = !!defaultId; - const exists = contains(patterns, defaultId); - - if (defined && !exists) { - core.uiSettings.remove('defaultIndex'); - defaultId = defined = false; - } - - if (defined) { - return; - } - - // If there is any index pattern created, set the first as default - if (patterns.length >= 1) { - defaultId = patterns[0]; - core.uiSettings.set('defaultIndex', defaultId); - } else { - const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; - const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - // Avoid being hostile to new users who don't have an index pattern setup yet - // give them a friendly info message instead of a terse error message - bannerId = core.overlays.banners.replace( - bannerId, - toMountPoint( - - ) - ); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - timeoutId = setTimeout(() => { - core.overlays.banners.remove(bannerId); - timeoutId = undefined; - }, 15000); - - history.push(redirectTarget); - - // return never-resolving promise to stop resolving and wait for the url change - return new Promise(() => {}); - } -} diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index 1a73bbb6b04a1..bb13ea09f928a 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -19,4 +19,3 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; -export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 2f139050e994a..c634322b23d0b 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -74,7 +74,7 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; +export { removeQueryParam, redirectWhenMissing } from './history'; export { applyDiff } from './state_management/utils/diff_object'; /** dummy plugin, we just want kibanaUtils to have its own bundle */ diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index 6e4c505c62ebc..513c70e60048a 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -31,6 +31,7 @@ import { setStateToKbnUrl, getStateFromKbnUrl, } from './kbn_url_storage'; +import { ScopedHistory } from '../../../../../core/public'; describe('kbn_url_storage', () => { describe('getStateFromUrl & setStateToUrl', () => { @@ -187,23 +188,54 @@ describe('kbn_url_storage', () => { urlControls.update('/', true); }); - const getCurrentUrl = () => window.location.href; + const getCurrentUrl = () => history.createHref(history.location); it('should flush async url updates', async () => { const pr1 = urlControls.updateAsync(() => '/1', false); const pr2 = urlControls.updateAsync(() => '/2', false); const pr3 = urlControls.updateAsync(() => '/3', false); - expect(getCurrentUrl()).toBe('http://localhost/'); - expect(urlControls.flush()).toBe('http://localhost/3'); - expect(getCurrentUrl()).toBe('http://localhost/3'); + expect(getCurrentUrl()).toBe('/'); + expect(urlControls.flush()).toBe('/3'); + expect(getCurrentUrl()).toBe('/3'); + await Promise.all([pr1, pr2, pr3]); + expect(getCurrentUrl()).toBe('/3'); + }); + + it('flush() should return undefined, if no url updates happened', () => { + expect(urlControls.flush()).toBeUndefined(); + urlControls.updateAsync(() => '/1', false); + urlControls.updateAsync(() => '/', false); + expect(urlControls.flush()).toBeUndefined(); + }); + }); + + describe('urlControls - scoped history integration', () => { + let history: History; + let urlControls: IKbnUrlControls; + beforeEach(() => { + const parentHistory = createBrowserHistory(); + parentHistory.replace('/app/kibana/'); + history = new ScopedHistory(parentHistory, '/app/kibana/'); + urlControls = createKbnUrlControls(history); + }); + + const getCurrentUrl = () => history.createHref(history.location); + + it('should flush async url updates', async () => { + const pr1 = urlControls.updateAsync(() => '/app/kibana/1', false); + const pr2 = urlControls.updateAsync(() => '/app/kibana/2', false); + const pr3 = urlControls.updateAsync(() => '/app/kibana/3', false); + expect(getCurrentUrl()).toBe('/app/kibana/'); + expect(urlControls.flush()).toBe('/app/kibana/3'); + expect(getCurrentUrl()).toBe('/app/kibana/3'); await Promise.all([pr1, pr2, pr3]); - expect(getCurrentUrl()).toBe('http://localhost/3'); + expect(getCurrentUrl()).toBe('/app/kibana/3'); }); it('flush() should return undefined, if no url updates happened', () => { expect(urlControls.flush()).toBeUndefined(); - urlControls.updateAsync(() => 'http://localhost/1', false); - urlControls.updateAsync(() => 'http://localhost/', false); + urlControls.updateAsync(() => '/app/kibana/1', false); + urlControls.updateAsync(() => '/app/kibana/', false); expect(urlControls.flush()).toBeUndefined(); }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index 40a411d425a54..337d122e2854b 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -154,7 +154,7 @@ export const createKbnUrlControls = ( let shouldReplace = true; function updateUrl(newUrl: string, replace = false): string | undefined { - const currentUrl = getCurrentUrl(); + const currentUrl = getCurrentUrl(history); if (newUrl === currentUrl) return undefined; // skip update const historyPath = getRelativeToHistoryPath(newUrl, history); @@ -165,7 +165,7 @@ export const createKbnUrlControls = ( history.push(historyPath); } - return getCurrentUrl(); + return getCurrentUrl(history); } // queue clean up @@ -187,7 +187,10 @@ export const createKbnUrlControls = ( function getPendingUrl() { if (updateQueue.length === 0) return undefined; - const resultUrl = updateQueue.reduce((url, nextUpdate) => nextUpdate(url), getCurrentUrl()); + const resultUrl = updateQueue.reduce( + (url, nextUpdate) => nextUpdate(url), + getCurrentUrl(history) + ); return resultUrl; } diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts index af8811b1969e6..8adbbfb06e1ed 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_tracker.ts @@ -57,6 +57,7 @@ export function createKbnUrlTracker({ navLinkUpdater$, toastNotifications, history, + getHistory, storage, shouldTrackUrlUpdate = pathname => { const currentAppName = defaultSubUrl.slice(2); // cut hash and slash symbols @@ -103,6 +104,12 @@ export function createKbnUrlTracker({ * History object to use to track url changes. If this isn't provided, a local history instance will be created. */ history?: History; + + /** + * Lazily retrieve history instance + */ + getHistory?: () => History; + /** * Storage object to use to persist currently active url. If this isn't provided, the browser wide session storage instance will be used. */ @@ -158,7 +165,7 @@ export function createKbnUrlTracker({ function onMountApp() { unsubscribe(); - const historyInstance = history || createHashHistory(); + const historyInstance = history || (getHistory && getHistory()) || createHashHistory(); // track current hash when within app unsubscribeURLHistory = historyInstance.listen(location => { if (shouldTrackUrlUpdate(location.pathname)) { diff --git a/src/plugins/kibana_utils/public/state_management/url/parse.ts b/src/plugins/kibana_utils/public/state_management/url/parse.ts index 95041d0662f56..6339002ea5c68 100644 --- a/src/plugins/kibana_utils/public/state_management/url/parse.ts +++ b/src/plugins/kibana_utils/public/state_management/url/parse.ts @@ -18,12 +18,11 @@ */ import { parse as _parseUrl } from 'url'; +import { History } from 'history'; export const parseUrl = (url: string) => _parseUrl(url, true); export const parseUrlHash = (url: string) => { const hash = parseUrl(url).hash; return hash ? parseUrl(hash.slice(1)) : null; }; -export const getCurrentUrl = () => window.location.href; -export const parseCurrentUrl = () => parseUrl(getCurrentUrl()); -export const parseCurrentUrlHash = () => parseUrlHash(getCurrentUrl()); +export const getCurrentUrl = (history: History) => history.createHref(history.location); diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index cc3f1df7c1e00..8a9a4ea71ee9a 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -21,6 +21,7 @@ import { createKbnUrlStateStorage, IKbnUrlStateStorage } from './create_kbn_url_ import { History, createBrowserHistory } from 'history'; import { takeUntil, toArray } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { ScopedHistory } from '../../../../../core/public'; describe('KbnUrlStateStorage', () => { describe('useHash: false', () => { @@ -132,4 +133,78 @@ describe('KbnUrlStateStorage', () => { expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); }); }); + + describe('ScopedHistory integration', () => { + let urlStateStorage: IKbnUrlStateStorage; + let history: ScopedHistory; + const getCurrentUrl = () => history.createHref(history.location); + beforeEach(() => { + const parentHistory = createBrowserHistory(); + parentHistory.push('/kibana/app/'); + history = new ScopedHistory(parentHistory, '/kibana/app/'); + urlStateStorage = createKbnUrlStateStorage({ useHash: false, history }); + }); + + it('should persist state to url', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + await urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/#?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + }); + + it('should flush state to url', () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); + expect(urlStateStorage.flush()).toBe(true); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/#?_s=(ok:1,test:test)"`); + expect(urlStateStorage.get(key)).toEqual(state); + + expect(urlStateStorage.flush()).toBe(false); // nothing to flush, not update + }); + + it('should cancel url updates', async () => { + const state = { test: 'test', ok: 1 }; + const key = '_s'; + const pr = urlStateStorage.set(key, state); + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); + urlStateStorage.cancel(); + await pr; + expect(getCurrentUrl()).toMatchInlineSnapshot(`"/kibana/app/"`); + expect(urlStateStorage.get(key)).toEqual(null); + }); + + it('should cancel url updates if synchronously returned to the same state', async () => { + const state1 = { test: 'test', ok: 1 }; + const state2 = { test: 'test', ok: 2 }; + const key = '_s'; + const pr1 = urlStateStorage.set(key, state1); + await pr1; + const historyLength = history.length; + const pr2 = urlStateStorage.set(key, state2); + const pr3 = urlStateStorage.set(key, state1); + await Promise.all([pr2, pr3]); + expect(history.length).toBe(historyLength); + }); + + it('should notify about url changes', async () => { + expect(urlStateStorage.change$).toBeDefined(); + const key = '_s'; + const destroy$ = new Subject(); + const result = urlStateStorage.change$!(key) + .pipe(takeUntil(destroy$), toArray()) + .toPromise(); + + history.push(`/#?${key}=(ok:1,test:test)`); + history.push(`/?query=test#?${key}=(ok:2,test:test)&some=test`); + history.push(`/?query=test#?some=test`); + + destroy$.next(); + destroy$.complete(); + + expect(await result).toEqual([{ test: 'test', ok: 1 }, { test: 'test', ok: 2 }, null]); + }); + }); }); diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 1789b7cd5ddba..cc411a8c6a25c 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -1,7 +1,7 @@ { "id": "management", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["kibanaLegacy", "home"] } diff --git a/src/plugins/management/public/management_service.test.ts b/src/plugins/management/public/management_service.test.ts index ceb91837921eb..18569ef285ff3 100644 --- a/src/plugins/management/public/management_service.test.ts +++ b/src/plugins/management/public/management_service.test.ts @@ -29,9 +29,8 @@ test('Provides default sections', () => { () => {}, coreMock.createSetup().getStartServices ); - expect(service.getAllSections().length).toEqual(3); + expect(service.getAllSections().length).toEqual(2); expect(service.getSection('kibana')).not.toBeUndefined(); - expect(service.getSection('logstash')).not.toBeUndefined(); expect(service.getSection('elasticsearch')).not.toBeUndefined(); }); diff --git a/src/plugins/management/public/management_service.ts b/src/plugins/management/public/management_service.ts index ed31a22992da8..8fc207e32e6ce 100644 --- a/src/plugins/management/public/management_service.ts +++ b/src/plugins/management/public/management_service.ts @@ -80,7 +80,6 @@ export class ManagementService { ); register({ id: 'kibana', title: 'Kibana', order: 30, euiIconType: 'logoKibana' }); - register({ id: 'logstash', title: 'Logstash', order: 30, euiIconType: 'logoLogstash' }); register({ id: 'elasticsearch', title: 'Elasticsearch', diff --git a/src/plugins/management/server/capabilities_provider.ts b/src/plugins/management/server/capabilities_provider.ts new file mode 100644 index 0000000000000..9a69749c8233b --- /dev/null +++ b/src/plugins/management/server/capabilities_provider.ts @@ -0,0 +1,32 @@ +/* + * 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 const capabilitiesProvider = () => ({ + management: { + /* + * Management settings correspond to management section/link ids, and should not be changed + * without also updating those definitions. + */ + kibana: { + settings: true, + index_patterns: true, + objects: true, + }, + }, +}); diff --git a/src/plugins/management/server/index.ts b/src/plugins/management/server/index.ts new file mode 100644 index 0000000000000..afc7adf8832e1 --- /dev/null +++ b/src/plugins/management/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { ManagementServerPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new ManagementServerPlugin(initContext); diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts new file mode 100644 index 0000000000000..f8fda7da9b95a --- /dev/null +++ b/src/plugins/management/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; + +export class ManagementServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('management: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('management: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/maps_legacy/public/__tests__/map/service_settings.js b/src/plugins/maps_legacy/public/__tests__/map/service_settings.js index 4cbe098501c67..822378163a7eb 100644 --- a/src/plugins/maps_legacy/public/__tests__/map/service_settings.js +++ b/src/plugins/maps_legacy/public/__tests__/map/service_settings.js @@ -26,7 +26,7 @@ import EMS_TILES from './ems_mocks/sample_tiles.json'; import EMS_STYLE_ROAD_MAP_BRIGHT from './ems_mocks/sample_style_bright'; import EMS_STYLE_ROAD_MAP_DESATURATED from './ems_mocks/sample_style_desaturated'; import EMS_STYLE_DARK_MAP from './ems_mocks/sample_style_dark'; -import { ORIGIN } from '../../common/origin'; +import { ORIGIN } from '../../common/constants/origin'; describe('service_settings (FKA tilemaptest)', function() { let serviceSettings; diff --git a/src/plugins/maps_legacy/public/common/origin.ts b/src/plugins/maps_legacy/public/common/constants/origin.ts similarity index 100% rename from src/plugins/maps_legacy/public/common/origin.ts rename to src/plugins/maps_legacy/public/common/constants/origin.ts diff --git a/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts b/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts new file mode 100644 index 0000000000000..be9c4d0d9c37b --- /dev/null +++ b/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts @@ -0,0 +1,47 @@ +/* + * 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 { TmsLayer } from '../../index'; +import { MapTypes } from './map_types'; + +export interface WMSOptions { + selectedTmsLayer?: TmsLayer; + enabled: boolean; + url?: string; + options: { + version?: string; + layers?: string; + format: string; + transparent: boolean; + attribution?: string; + styles?: string; + }; +} + +export interface TileMapVisParams { + colorSchema: string; + mapType: MapTypes; + isDesaturated: boolean; + addTooltip: boolean; + heatClusterSize: number; + legendPosition: 'bottomright' | 'bottomleft' | 'topright' | 'topleft'; + mapZoom: number; + mapCenter: [number, number]; + wms: WMSOptions; +} diff --git a/src/plugins/maps_legacy/public/common/types/index.ts b/src/plugins/maps_legacy/public/common/types/index.ts new file mode 100644 index 0000000000000..e6cabdde82cd9 --- /dev/null +++ b/src/plugins/maps_legacy/public/common/types/index.ts @@ -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. + */ + +/** + * Use * syntax so that these exports do not break when internal + * types are stripped. + */ +export * from './external_basemap_types'; +export * from './map_types'; +export * from './region_map_types'; diff --git a/src/legacy/core_plugins/tile_map/public/map_types.ts b/src/plugins/maps_legacy/public/common/types/map_types.ts similarity index 100% rename from src/legacy/core_plugins/tile_map/public/map_types.ts rename to src/plugins/maps_legacy/public/common/types/map_types.ts diff --git a/src/plugins/maps_legacy/public/common/types/region_map_types.ts b/src/plugins/maps_legacy/public/common/types/region_map_types.ts new file mode 100644 index 0000000000000..0da597068f11e --- /dev/null +++ b/src/plugins/maps_legacy/public/common/types/region_map_types.ts @@ -0,0 +1,36 @@ +/* + * 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 { VectorLayer, FileLayerField } from '../../index'; +import { WMSOptions } from './external_basemap_types'; + +export interface RegionMapVisParams { + readonly addTooltip: true; + readonly legendPosition: 'bottomright'; + colorSchema: string; + emsHotLink?: string | null; + mapCenter: [number, number]; + mapZoom: number; + outlineWeight: number | ''; + isDisplayWarning: boolean; + showAllShapes: boolean; + selectedLayer?: VectorLayer; + selectedJoinField?: FileLayerField; + wms: WMSOptions; +} diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx similarity index 77% rename from src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx rename to src/plugins/maps_legacy/public/components/wms_internal_options.tsx index 47f5b8f31e62b..d1def8153d1a8 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_internal_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx @@ -21,8 +21,8 @@ import React from 'react'; import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TextInputOption } from '../../../../../plugins/charts/public'; -import { WMSOptions } from '../types'; +import { TextInputOption } from '../../../charts/public'; +import { WMSOptions } from '../common/types/external_basemap_types'; interface WmsInternalOptions { wms: WMSOptions; @@ -32,14 +32,14 @@ interface WmsInternalOptions { function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { const wmsLink = ( - + ); const footnoteText = ( <> @@ -64,7 +64,7 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { @@ -74,14 +74,14 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} @@ -95,14 +95,17 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} @@ -117,7 +120,7 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { label={ <> @@ -126,7 +129,7 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { helpText={ <> {footnote} @@ -140,14 +143,17 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} @@ -161,13 +167,13 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { } helpText={ } @@ -179,14 +185,17 @@ function WmsInternalOptions({ wms, setValue }: WmsInternalOptions) { - + } helpText={ <> {footnote} diff --git a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx b/src/plugins/maps_legacy/public/components/wms_options.tsx similarity index 82% rename from src/legacy/core_plugins/tile_map/public/components/wms_options.tsx rename to src/plugins/maps_legacy/public/components/wms_options.tsx index e74c260d3b8e5..4892463bb9f85 100644 --- a/src/legacy/core_plugins/tile_map/public/components/wms_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_options.tsx @@ -21,12 +21,12 @@ import React, { useMemo } from 'react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TmsLayer } from '../../../../../plugins/maps_legacy/public'; -import { Vis } from '../../../../../plugins/visualizations/public'; -import { RegionMapVisParams } from '../../../region_map/public/types'; -import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public'; +import { TmsLayer } from '../index'; +import { Vis } from '../../../visualizations/public'; +import { RegionMapVisParams } from '../common/types/region_map_types'; +import { SelectOption, SwitchOption } from '../../../charts/public'; import { WmsInternalOptions } from './wms_internal_options'; -import { WMSOptions, TileMapVisParams } from '../types'; +import { WMSOptions, TileMapVisParams } from '../common/types/external_basemap_types'; interface Props { stateParams: TileMapVisParams | RegionMapVisParams; @@ -59,7 +59,7 @@ function WmsOptions({ stateParams, setValue, vis }: Props) {

@@ -67,10 +67,10 @@ function WmsOptions({ stateParams, setValue, vis }: Props) { new KibanaMap(...args); +} + +export function getBaseMapsVis(core: CoreSetup, serviceSettings: IServiceSettings) { + const getKibanaMap = getKibanaMapFactoryProvider(core); + return new BaseMapsVisualizationProvider(getKibanaMap, serviceSettings); +} + +export * from './common/types'; +export { ORIGIN } from './common/constants/origin'; + +export { WmsOptions } from './components/wms_options'; + export type MapsLegacyPluginSetup = ReturnType; export type MapsLegacyPluginStart = ReturnType; diff --git a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js similarity index 89% rename from src/legacy/core_plugins/tile_map/public/base_maps_visualization.js rename to src/plugins/maps_legacy/public/map/base_maps_visualization.js index 1dac4607280cc..c4ac671a5187c 100644 --- a/src/legacy/core_plugins/tile_map/public/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -19,16 +19,14 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaMap } from '../../../../plugins/maps_legacy/public'; import * as Rx from 'rxjs'; import { filter, first } from 'rxjs/operators'; -import { toastNotifications } from 'ui/notify'; -import chrome from 'ui/chrome'; +import { getInjectedVarFunc, getUiSettings, getToasts } from '../kibana_services'; const WMS_MINZOOM = 0; const WMS_MAXZOOM = 22; //increase this to 22. Better for WMS -export function BaseMapsVisualizationProvider(mapServiceSettings, notificationService) { +export function BaseMapsVisualizationProvider(getKibanaMap, mapServiceSettings) { /** * Abstract base class for a visualization consisting of a map with a single baselayer. * @class BaseMapsVisualization @@ -36,7 +34,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe */ const serviceSettings = mapServiceSettings; - const toastService = notificationService; + const toastService = getToasts(); return class BaseMapsVisualization { constructor(element, vis) { @@ -99,7 +97,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter; const services = { toastService }; - this._kibanaMap = new KibanaMap(this._container, options, services); + this._kibanaMap = getKibanaMap(this._container, options, services); this._kibanaMap.setMinZoom(WMS_MINZOOM); //use a default this._kibanaMap.setMaxZoom(WMS_MAXZOOM); //use a default @@ -118,20 +116,20 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe _tmsConfigured() { const { wms } = this._getMapsParams(); - const hasTmsBaseLayer = !!wms.selectedTmsLayer; + const hasTmsBaseLayer = wms && !!wms.selectedTmsLayer; return hasTmsBaseLayer; } _wmsConfigured() { const { wms } = this._getMapsParams(); - const hasWmsBaseLayer = !!wms.enabled; + const hasWmsBaseLayer = wms && !!wms.enabled; return hasWmsBaseLayer; } async _updateBaseLayer() { - const emsTileLayerId = chrome.getInjected('emsTileLayerId', true); + const emsTileLayerId = getInjectedVarFunc()('emsTileLayerId', true); if (!this._kibanaMap) { return; @@ -149,7 +147,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe this._setTmsLayer(initBasemapLayer); } } catch (e) { - toastNotifications.addWarning(e.message); + toastService.addWarning(e.message); return; } return; @@ -176,7 +174,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe this._setTmsLayer(selectedTmsLayer); } } catch (tmsLoadingError) { - toastNotifications.addWarning(tmsLoadingError.message); + toastService.addWarning(tmsLoadingError.message); } } @@ -190,7 +188,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe if (typeof isDesaturated !== 'boolean') { isDesaturated = true; } - const isDarkMode = chrome.getUiSettingsClient().get('theme:darkMode'); + const isDarkMode = getUiSettings().get('theme:darkMode'); const meta = await serviceSettings.getAttributesForTMSLayer( tmsLayer, isDesaturated, @@ -208,7 +206,7 @@ export function BaseMapsVisualizationProvider(mapServiceSettings, notificationSe async _updateData() { throw new Error( - i18n.translate('tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage', { + i18n.translate('maps_legacy.baseMapsVisualization.childShouldImplementMethodErrorMessage', { defaultMessage: 'Child should implement this method to respond to data-update', }) ); diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index 1c4d0882cb7da..c7cec1b14159a 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -24,7 +24,8 @@ import $ from 'jquery'; import _ from 'lodash'; import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; -import { ORIGIN } from '../common/origin'; +import { ORIGIN } from '../common/constants/origin'; +import { getToasts } from '../kibana_services'; function makeFitControl(fitContainer, kibanaMap) { const FitControl = L.Control.extend({ @@ -101,7 +102,7 @@ function makeLegendControl(container, kibanaMap, position) { * Serves as simple abstraction for leaflet as well. */ export class KibanaMap extends EventEmitter { - constructor(containerNode, options, services) { + constructor(containerNode, options) { super(); this._containerNode = containerNode; this._leafletBaseLayer = null; @@ -116,7 +117,6 @@ export class KibanaMap extends EventEmitter { this._layers = []; this._listeners = []; this._showTooltip = false; - this.toastService = services ? services.toastService : null; const leafletOptions = { minZoom: options.minZoom, @@ -483,21 +483,19 @@ export class KibanaMap extends EventEmitter { } _addMaxZoomMessage = layer => { - if (this.toastService) { - const zoomWarningMsg = createZoomWarningMsg( - this.toastService, - this.getZoomLevel, - this.getMaxZoomLevel - ); + const zoomWarningMsg = createZoomWarningMsg( + getToasts(), + this.getZoomLevel, + this.getMaxZoomLevel + ); - this._leafletMap.on('zoomend', zoomWarningMsg); - this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); + this._leafletMap.on('zoomend', zoomWarningMsg); + this._containerNode.setAttribute('data-test-subj', 'zoomWarningEnabled'); - layer.on('remove', () => { - this._leafletMap.off('zoomend', zoomWarningMsg); - this._containerNode.removeAttribute('data-test-subj'); - }); - } + layer.on('remove', () => { + this._leafletMap.off('zoomend', zoomWarningMsg); + this._containerNode.removeAttribute('data-test-subj'); + }); }; setLegendPosition(position) { diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index f4f0d66ee20de..8e3a0648e99d4 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -22,7 +22,7 @@ import MarkdownIt from 'markdown-it'; import { EMSClient } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; import { getInjectedVarFunc } from '../kibana_services'; -import { ORIGIN } from '../common/origin'; +import { ORIGIN } from '../common/constants/origin'; const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; diff --git a/src/plugins/maps_legacy/public/plugin.ts b/src/plugins/maps_legacy/public/plugin.ts index 751be65e1dbf6..acc7655a5e263 100644 --- a/src/plugins/maps_legacy/public/plugin.ts +++ b/src/plugins/maps_legacy/public/plugin.ts @@ -33,18 +33,20 @@ import { MapsLegacyPluginSetup, MapsLegacyPluginStart } from './index'; * @public */ +export const bindSetupCoreAndPlugins = (core: CoreSetup) => { + setToasts(core.notifications.toasts); + setUiSettings(core.uiSettings); + setInjectedVarFunc(core.injectedMetadata.getInjectedVar); +}; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsLegacySetupDependencies {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsLegacyStartDependencies {} export class MapsLegacyPlugin implements Plugin { - constructor() {} - public setup(core: CoreSetup, plugins: MapsLegacySetupDependencies) { - setToasts(core.notifications.toasts); - setUiSettings(core.uiSettings); - setInjectedVarFunc(core.injectedMetadata.getInjectedVar); + bindSetupCoreAndPlugins(core); return { serviceSettings: new ServiceSettings(), diff --git a/src/plugins/maps_legacy/public/tooltip_provider.js b/src/plugins/maps_legacy/public/tooltip_provider.js new file mode 100644 index 0000000000000..8563140816997 --- /dev/null +++ b/src/plugins/maps_legacy/public/tooltip_provider.js @@ -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 React from 'react'; +import ReactDOMServer from 'react-dom/server'; + +function getToolTipContent(details) { + return ReactDOMServer.renderToStaticMarkup( + + + {details.map((detail, i) => ( + + + + + ))} + +
{detail.label}{detail.value}
+ ); +} + +export function mapTooltipProvider(element, formatter) { + return (...args) => { + const details = formatter(...args); + return details && getToolTipContent(details); + }; +} diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss index 5befe4789dd6c..a6ddf7a8b4264 100644 --- a/src/plugins/navigation/public/top_nav_menu/_index.scss +++ b/src/plugins/navigation/public/top_nav_menu/_index.scss @@ -8,4 +8,8 @@ padding: 0 $euiSizeS; } } + + .kbnTopNavMenu-isFullScreen { + padding: 0; + } } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 8e0e8b3031132..74cfd125c2e3a 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -75,4 +75,17 @@ describe('TopNavMenu', () => { expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); }); + + it('Should render with a class name', () => { + const component = shallowWithIntl( + + ); + expect(component.find('.kbnTopNavMenu').length).toBe(1); + expect(component.find('.myCoolClass').length).toBeTruthy(); + }); }); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 14ad40f13e388..d492c7feb61a7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; @@ -29,6 +30,7 @@ export type TopNavMenuProps = StatefulSearchBarProps & { config?: TopNavMenuData[]; showSearchBar?: boolean; data?: DataPublicPluginStart; + className?: string; }; /* @@ -65,6 +67,7 @@ export function TopNavMenu(props: TopNavMenuProps) { } function renderLayout() { + const className = classNames('kbnTopNavMenu', props.className); return ( {renderItems()} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts index 9776887b6d741..df687a051fc7d 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts @@ -17,9 +17,9 @@ * under the License. */ import _ from 'lodash'; -import { EsResponse, SavedObject, SavedObjectConfig } from '../../types'; +import { EsResponse, SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from '../../types'; import { expandShorthand, SavedObjectNotFound } from '../../../../kibana_utils/public'; -import { DataPublicPluginStart, IndexPattern } from '../../../../data/public'; +import { IndexPattern } from '../../../../data/public'; /** * A given response of and ElasticSearch containing a plain saved object is applied to the given @@ -29,7 +29,7 @@ export async function applyESResp( resp: EsResponse, savedObject: SavedObject, config: SavedObjectConfig, - createSearchSource: DataPublicPluginStart['search']['createSearchSource'] + dependencies: SavedObjectKibanaServices ) { const mapping = expandShorthand(config.mapping); const esType = config.type || ''; @@ -65,7 +65,10 @@ export async function applyESResp( if (config.searchSource) { try { - savedObject.searchSource = await createSearchSource(meta.searchSourceJSON, resp.references); + savedObject.searchSource = await dependencies.search.searchSource.fromJSON( + meta.searchSourceJSON, + resp.references + ); } catch (error) { if ( error.constructor.name === 'SavedObjectNotFound' && diff --git a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts index e8faef4e9e040..e5b0e18e7b433 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/build_saved_object.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import _ from 'lodash'; -import { SearchSource } from '../../../../data/public'; +import { once } from 'lodash'; import { hydrateIndexPattern } from './hydrate_index_pattern'; import { intializeSavedObject } from './initialize_saved_object'; import { serializeSavedObject } from './serialize_saved_object'; @@ -55,7 +54,9 @@ export function buildSavedObject( savedObject.isSaving = false; savedObject.defaults = config.defaults || {}; // optional search source which this object configures - savedObject.searchSource = config.searchSource ? new SearchSource() : undefined; + savedObject.searchSource = config.searchSource + ? services.search.searchSource.create() + : undefined; // the id of the document savedObject.id = config.id || void 0; // the migration version of the document, should only be set on imports @@ -79,10 +80,9 @@ export function buildSavedObject( * @return {Promise} * @resolved {SavedObject} */ - savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); + savedObject.init = once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); - savedObject.applyESResp = (resp: EsResponse) => - applyESResp(resp, savedObject, config, services.search.createSearchSource); + savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config, services); /** * Serialize this object diff --git a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts index 60c66f84080b2..f7e67dbe3ee1d 100644 --- a/src/plugins/saved_objects/public/saved_object/saved_object.test.ts +++ b/src/plugins/saved_objects/public/saved_object/saved_object.test.ts @@ -28,9 +28,8 @@ import { // @ts-ignore import StubIndexPattern from 'test_utils/stub_index_pattern'; -import { InvalidJSONProperty } from '../../../kibana_utils/public'; import { coreMock } from '../../../../core/public/mocks'; -import { dataPluginMock } from '../../../../plugins/data/public/mocks'; +import { dataPluginMock, createSearchSourceMock } from '../../../../plugins/data/public/mocks'; import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; import { IIndexPattern } from '../../../data/common/index_patterns'; @@ -40,9 +39,9 @@ describe('Saved Object', () => { const startMock = coreMock.createStart(); const dataStartMock = dataPluginMock.createStartContract(); const saveOptionsMock = {} as SavedObjectSaveOpts; + const savedObjectsClientStub = startMock.savedObjects.client; let SavedObjectClass: new (config: SavedObjectConfig) => SavedObject; - const savedObjectsClientStub = startMock.savedObjects.client; /** * Returns a fake doc response with the given index and id, of type dashboard @@ -99,16 +98,22 @@ describe('Saved Object', () => { function createInitializedSavedObject(config: SavedObjectConfig = {}) { const savedObject = new SavedObjectClass(config); savedObject.title = 'my saved object'; + return savedObject.init!(); } beforeEach(() => { - (dataStartMock.search.createSearchSource as jest.Mock).mockReset(); - SavedObjectClass = createSavedObjectClass({ + SavedObjectClass = createSavedObjectClass(({ savedObjectsClient: savedObjectsClientStub, indexPatterns: dataStartMock.indexPatterns, - search: dataStartMock.search, - } as SavedObjectKibanaServices); + search: { + ...dataStartMock.search, + searchSource: { + ...dataStartMock.search.searchSource, + create: createSearchSourceMock, + }, + }, + } as unknown) as SavedObjectKibanaServices); }); describe('save', () => { @@ -411,27 +416,6 @@ describe('Saved Object', () => { }); }); - it('forwards thrown exceptions from createSearchSource', async () => { - (dataStartMock.search.createSearchSource as jest.Mock).mockImplementation(() => { - throw new InvalidJSONProperty(''); - }); - const savedObject = await createInitializedSavedObject({ - type: 'dashboard', - searchSource: true, - }); - const response = { - found: true, - _source: {}, - }; - - try { - await savedObject.applyESResp(response); - throw new Error('applyESResp should have failed, but did not.'); - } catch (err) { - expect(err instanceof InvalidJSONProperty).toBe(true); - } - }); - it('preserves original defaults if not overridden', () => { const id = 'anid'; const preserveMeValue = 'here to stay!'; @@ -589,42 +573,45 @@ describe('Saved Object', () => { it('passes references to search source parsing function', async () => { const savedObject = new SavedObjectClass({ type: 'dashboard', searchSource: true }); - return savedObject.init!().then(() => { - const searchSourceJSON = JSON.stringify({ - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', - filter: [ - { - meta: { - indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - }, - }, - ], - }); - const response = { - found: true, - _source: { - kibanaSavedObjectMeta: { - searchSourceJSON, + await savedObject.init!(); + + const searchSourceJSON = JSON.stringify({ + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index', + filter: [ + { + meta: { + indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', }, }, - references: [ - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - type: 'index-pattern', - id: 'my-index-1', - }, - { - name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', - type: 'index-pattern', - id: 'my-index-2', - }, - ], - }; - savedObject.applyESResp(response); - expect(dataStartMock.search.createSearchSource).toBeCalledWith( - searchSourceJSON, - response.references - ); + ], + }); + const response = { + found: true, + _source: { + kibanaSavedObjectMeta: { + searchSourceJSON, + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'my-index-1', + }, + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index', + type: 'index-pattern', + id: 'my-index-2', + }, + ], + }; + const result = await savedObject.applyESResp(response); + + expect(result._source).toEqual({ + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index","filter":[{"meta":{"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index"}}]}', + }, }); }); }); diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts index 23c2b75169555..f98b1762511f4 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.test.ts @@ -25,6 +25,7 @@ import { } from './resolve_saved_objects'; import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; import { IndexPatternsContract } from '../../../data/public'; +import { dataPluginMock } from '../../../data/public/mocks'; class SavedObjectNotFound extends Error { constructor(options: Record) { @@ -233,6 +234,19 @@ describe('resolveSavedObjects', () => { }); describe('resolveIndexPatternConflicts', () => { + let dependencies: Parameters[3]; + + beforeEach(() => { + const search = dataPluginMock.createStartContract().search; + + dependencies = { + indexPatterns: ({ + get: (id: string) => Promise.resolve({ id }), + } as unknown) as IndexPatternsContract, + search, + }; + }); + it('should resave resolutions', async () => { const save = jest.fn(); @@ -284,11 +298,13 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ - get: (id: string) => Promise.resolve({ id }), - } as unknown) as IndexPatternsContract); - expect(conflictedIndexPatterns[0].obj.searchSource!.getField('index')!.id).toEqual('2'); - expect(conflictedIndexPatterns[1].obj.searchSource!.getField('index')!.id).toEqual('4'); + await resolveIndexPatternConflicts( + resolutions, + conflictedIndexPatterns, + overwriteAll, + dependencies + ); + expect(save.mock.calls.length).toBe(2); expect(save).toHaveBeenCalledWith({ confirmOverwrite: !overwriteAll }); }); @@ -345,13 +361,13 @@ describe('resolveSavedObjects', () => { const overwriteAll = false; - await resolveIndexPatternConflicts(resolutions, conflictedIndexPatterns, overwriteAll, ({ - get: (id: string) => Promise.resolve({ id }), - } as unknown) as IndexPatternsContract); + await resolveIndexPatternConflicts( + resolutions, + conflictedIndexPatterns, + overwriteAll, + dependencies + ); - expect(conflictedIndexPatterns[0].obj.searchSource!.getField('filter')).toEqual([ - { meta: { index: 'newFilterIndex' } }, - ]); expect(save.mock.calls.length).toBe(2); }); }); diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts index 15e03ed39d88c..d4764b8949a60 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { cloneDeep } from 'lodash'; import { OverlayStart, SavedObjectReference } from 'src/core/public'; import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; -import { IndexPatternsContract, IIndexPattern, createSearchSource } from '../../../data/public'; +import { IndexPatternsContract, IIndexPattern, DataPublicPluginStart } from '../../../data/public'; type SavedObjectsRawDoc = Record; @@ -162,7 +162,10 @@ export async function resolveIndexPatternConflicts( resolutions: Array<{ oldId: string; newId: string }>, conflictedIndexPatterns: any[], overwriteAll: boolean, - indexPatterns: IndexPatternsContract + dependencies: { + indexPatterns: IndexPatternsContract; + search: DataPublicPluginStart['search']; + } ) { let importCount = 0; @@ -208,7 +211,7 @@ export async function resolveIndexPatternConflicts( // The user decided to skip this conflict so do nothing return; } - obj.searchSource = await createSearchSource(indexPatterns)( + obj.searchSource = await dependencies.search.searchSource.fromJSON( JSON.stringify(serializedSearchSource), replacedReferences ); diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index 6f03f97079bb6..c1daf3445219f 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -17,23 +17,15 @@ * under the License. */ -import React, { useEffect } from 'react'; +import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; -import { HashRouter, Switch, Route, useParams, useLocation } from 'react-router-dom'; -import { parse } from 'query-string'; -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { HashRouter, Switch, Route } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { CoreSetup, CoreStart, ChromeBreadcrumb, Capabilities } from 'src/core/public'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { CoreSetup, Capabilities } from 'src/core/public'; import { ManagementAppMountParams } from '../../../management/public'; -import { DataPublicPluginStart } from '../../../data/public'; import { StartDependencies, SavedObjectsManagementPluginStart } from '../plugin'; -import { - ISavedObjectsManagementServiceRegistry, - SavedObjectsManagementActionServiceStart, -} from '../services'; -import { SavedObjectsTable } from './objects_table'; -import { SavedObjectEdition } from './object_view'; +import { ISavedObjectsManagementServiceRegistry } from '../services'; import { getAllowedTypes } from './../lib'; interface MountParams { @@ -44,6 +36,8 @@ interface MountParams { let allowedObjectTypes: string[] | undefined; +const SavedObjectsEditionPage = lazy(() => import('./saved_objects_edition_page')); +const SavedObjectsTablePage = lazy(() => import('./saved_objects_table_page')); export const mountManagementSection = async ({ core, mountParams, @@ -63,23 +57,27 @@ export const mountManagementSection = async ({ - + }> + + - + }> + + @@ -103,109 +101,3 @@ const RedirectToHomeIfUnauthorized: React.FunctionComponent<{ } return children! as React.ReactElement; }; - -const SavedObjectsEditionPage = ({ - coreStart, - serviceRegistry, - setBreadcrumbs, -}: { - coreStart: CoreStart; - serviceRegistry: ISavedObjectsManagementServiceRegistry; - setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; -}) => { - const { service: serviceName, id } = useParams<{ service: string; id: string }>(); - const capabilities = coreStart.application.capabilities; - - const { search } = useLocation(); - const query = parse(search); - const service = serviceRegistry.get(serviceName); - - useEffect(() => { - setBreadcrumbs([ - { - text: i18n.translate('savedObjectsManagement.breadcrumb.index', { - defaultMessage: 'Saved objects', - }), - href: '#/management/kibana/objects', - }, - { - text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { - defaultMessage: 'Edit {savedObjectType}', - values: { savedObjectType: service?.service.type ?? 'object' }, - }), - }, - ]); - }, [setBreadcrumbs, service]); - - return ( - - ); -}; - -const SavedObjectsTablePage = ({ - coreStart, - dataStart, - allowedTypes, - serviceRegistry, - actionRegistry, - setBreadcrumbs, -}: { - coreStart: CoreStart; - dataStart: DataPublicPluginStart; - allowedTypes: string[]; - serviceRegistry: ISavedObjectsManagementServiceRegistry; - actionRegistry: SavedObjectsManagementActionServiceStart; - setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; -}) => { - const capabilities = coreStart.application.capabilities; - const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); - - useEffect(() => { - setBreadcrumbs([ - { - text: i18n.translate('savedObjectsManagement.breadcrumb.index', { - defaultMessage: 'Saved objects', - }), - href: '#/management/kibana/objects', - }, - ]); - }, [setBreadcrumbs]); - - return ( - { - const { editUrl } = savedObject.meta; - if (editUrl) { - // previously, kbnUrl.change(object.meta.editUrl); was used. - // using direct access to location.hash seems the only option for now, - // as using react-router-dom will prefix the url with the router's basename - // which should be ignored there. - window.location.hash = editUrl; - } - }} - canGoInApp={savedObject => { - const { inAppUrl } = savedObject.meta; - return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false; - }} - /> - ); -}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index fe64df6ff51d1..1860a4625afc0 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -238,6 +238,7 @@ exports[`SavedObjectsTable import should show the flyout 1`] = ` indexPatterns={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } @@ -256,6 +257,32 @@ exports[`SavedObjectsTable import should show the flyout 1`] = ` "openModal": [MockFunction], } } + search={ + Object { + "__LEGACY": Object { + "esClient": Object { + "msearch": [MockFunction], + "search": [MockFunction], + }, + }, + "aggs": Object { + "calculateAutoTimeExpression": [Function], + "createAggConfigs": [MockFunction], + "types": Object { + "get": [Function], + "getAll": [Function], + "getBuckets": [Function], + "getMetrics": [Function], + }, + }, + "search": [MockFunction], + "searchSource": Object { + "create": [MockFunction], + "fromJSON": [MockFunction], + }, + "setInterceptor": [MockFunction], + } + } serviceRegistry={ Object { "all": [MockFunction], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 5d713ff044f24..c915a8a2be8f8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -33,6 +33,7 @@ import { coreMock } from '../../../../../../core/public/mocks'; import { serviceRegistryMock } from '../../../services/service_registry.mock'; import { Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; +import { dataPluginMock } from '../../../../../data/public/mocks'; const mockFile = ({ name: 'foo.ndjson', @@ -56,6 +57,7 @@ describe('Flyout', () => { beforeEach(() => { const { http, overlays } = coreMock.createStart(); + const search = dataPluginMock.createStartContract().search; defaultProps = { close: jest.fn(), @@ -68,6 +70,7 @@ describe('Flyout', () => { http, allowedTypes: ['search', 'index-pattern', 'visualization'], serviceRegistry: serviceRegistryMock.create(), + search, }; }); @@ -499,7 +502,10 @@ describe('Flyout', () => { component.instance().resolutions, mockConflictedIndexPatterns, true, - defaultProps.indexPatterns + { + search: defaultProps.search, + indexPatterns: defaultProps.indexPatterns, + } ); expect(saveObjectsMock).toHaveBeenCalledWith( mockConflictedSavedObjectsLinkedToSavedSearches, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 45788dcb601ae..fbcfeafe291a3 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -48,7 +48,11 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { OverlayStart, HttpStart } from 'src/core/public'; -import { IndexPatternsContract, IIndexPattern } from '../../../../../data/public'; +import { + IndexPatternsContract, + IIndexPattern, + DataPublicPluginStart, +} from '../../../../../data/public'; import { importFile, importLegacyFile, @@ -75,6 +79,7 @@ export interface FlyoutProps { indexPatterns: IndexPatternsContract; overlays: OverlayStart; http: HttpStart; + search: DataPublicPluginStart['search']; } export interface FlyoutState { @@ -362,7 +367,7 @@ export class Flyout extends Component { failedImports, } = this.state; - const { serviceRegistry, indexPatterns } = this.props; + const { serviceRegistry, indexPatterns, search } = this.props; this.setState({ error: undefined, @@ -388,7 +393,10 @@ export class Flyout extends Component { resolutions, conflictedIndexPatterns!, isOverwriteAllChecked, - indexPatterns + { + indexPatterns, + search, + } ); } this.setState({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index ddb262138d565..d9e39f31d181a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -32,7 +32,7 @@ import { EuiText, EuiSpacer, } from '@elastic/eui'; -import { FilterConfig } from '@elastic/eui/src/components/search_bar/filters/filters'; +import { SearchFilterConfig } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { IBasePath } from 'src/core/public'; @@ -284,7 +284,7 @@ export class Relationships extends Component { let overlays: ReturnType; let notifications: ReturnType; let savedObjects: ReturnType; + let search: ReturnType['search']; const shallowRender = (overrides: Partial = {}) => { return (shallowWithI18nProvider( @@ -106,6 +107,7 @@ describe('SavedObjectsTable', () => { overlays = overlayServiceMock.createStartContract(); notifications = notificationServiceMock.createStartContract(); savedObjects = savedObjectsServiceMock.createStartContract(); + search = dataPluginMock.createStartContract().search; const applications = applicationServiceMock.createStartContract(); applications.capabilities = { @@ -141,6 +143,7 @@ describe('SavedObjectsTable', () => { perPageConfig: 15, goInspectObject: () => {}, canGoInApp: () => true, + search, }; findObjectsMock.mockImplementation(() => ({ diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index c76fea5a0fb29..b9ebaf2b236f4 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -73,6 +73,7 @@ import { SavedObjectsManagementActionServiceStart, } from '../../services'; import { Header, Table, Flyout, Relationships } from './components'; +import { DataPublicPluginStart } from '../../../../../plugins/data/public'; interface ExportAllOption { id: string; @@ -86,6 +87,7 @@ export interface SavedObjectsTableProps { savedObjectsClient: SavedObjectsClientContract; indexPatterns: IndexPatternsContract; http: HttpStart; + search: DataPublicPluginStart['search']; overlays: OverlayStart; notifications: NotificationsStart; applications: ApplicationStart; @@ -467,6 +469,7 @@ export class SavedObjectsTable extends Component ); } diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx new file mode 100644 index 0000000000000..5ac6e8e103c47 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx @@ -0,0 +1,76 @@ +/* + * 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 React, { useEffect } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { parse } from 'query-string'; +import { i18n } from '@kbn/i18n'; +import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; +import { ISavedObjectsManagementServiceRegistry } from '../services'; +import { SavedObjectEdition } from './object_view'; + +const SavedObjectsEditionPage = ({ + coreStart, + serviceRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const { service: serviceName, id } = useParams<{ service: string; id: string }>(); + const capabilities = coreStart.application.capabilities; + + const { search } = useLocation(); + const query = parse(search); + const service = serviceRegistry.get(serviceName); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + { + text: i18n.translate('savedObjectsManagement.breadcrumb.edit', { + defaultMessage: 'Edit {savedObjectType}', + values: { savedObjectType: service?.service.type ?? 'object' }, + }), + }, + ]); + }, [setBreadcrumbs, service]); + + return ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SavedObjectsEditionPage as default }; diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx new file mode 100644 index 0000000000000..7660d17f91c5b --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -0,0 +1,91 @@ +/* + * 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 React, { useEffect } from 'react'; +import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { + ISavedObjectsManagementServiceRegistry, + SavedObjectsManagementActionServiceStart, +} from '../services'; +import { SavedObjectsTable } from './objects_table'; + +const SavedObjectsTablePage = ({ + coreStart, + dataStart, + allowedTypes, + serviceRegistry, + actionRegistry, + setBreadcrumbs, +}: { + coreStart: CoreStart; + dataStart: DataPublicPluginStart; + allowedTypes: string[]; + serviceRegistry: ISavedObjectsManagementServiceRegistry; + actionRegistry: SavedObjectsManagementActionServiceStart; + setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; +}) => { + const capabilities = coreStart.application.capabilities; + const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); + + useEffect(() => { + setBreadcrumbs([ + { + text: i18n.translate('savedObjectsManagement.breadcrumb.index', { + defaultMessage: 'Saved objects', + }), + href: '#/management/kibana/objects', + }, + ]); + }, [setBreadcrumbs]); + + return ( + { + const { editUrl } = savedObject.meta; + if (editUrl) { + // previously, kbnUrl.change(object.meta.editUrl); was used. + // using direct access to location.hash seems the only option for now, + // as using react-router-dom will prefix the url with the router's basename + // which should be ignored there. + window.location.hash = editUrl; + } + }} + canGoInApp={savedObject => { + const { inAppUrl } = savedObject.meta; + return inAppUrl ? get(capabilities, inAppUrl.uiCapabilitiesPath) : false; + }} + /> + ); +}; +// eslint-disable-next-line import/no-default-export +export { SavedObjectsTablePage as default }; diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index c8dede3da9263..28eac96dcbf46 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -116,8 +116,9 @@ export class SavedObjectsManagementPlugin }; } - public start(core: CoreStart) { + public start(core: CoreStart, { data }: StartDependencies) { const actionStart = this.actionService.start(); + return { actions: actionStart, }; diff --git a/src/plugins/saved_objects_management/server/capabilities_provider.ts b/src/plugins/saved_objects_management/server/capabilities_provider.ts new file mode 100644 index 0000000000000..bd621de4a6195 --- /dev/null +++ b/src/plugins/saved_objects_management/server/capabilities_provider.ts @@ -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 const capabilitiesProvider = () => ({ + savedObjectsManagement: { + delete: true, + edit: true, + read: true, + }, +}); diff --git a/src/plugins/saved_objects_management/server/plugin.ts b/src/plugins/saved_objects_management/server/plugin.ts index b72644b500967..4e39b08f5c62c 100644 --- a/src/plugins/saved_objects_management/server/plugin.ts +++ b/src/plugins/saved_objects_management/server/plugin.ts @@ -23,6 +23,7 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from ' import { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './types'; import { SavedObjectsManagement } from './services'; import { registerRoutes } from './routes'; +import { capabilitiesProvider } from './capabilities_provider'; export class SavedObjectsManagementPlugin implements Plugin { @@ -33,13 +34,15 @@ export class SavedObjectsManagementPlugin this.logger = this.context.logger.get(); } - public async setup({ http }: CoreSetup) { + public async setup({ http, capabilities }: CoreSetup) { this.logger.debug('Setting up SavedObjectsManagement plugin'); registerRoutes({ http, managementServicePromise: this.managementService$.pipe(first()).toPromise(), }); + capabilities.registerProvider(capabilitiesProvider); + return {}; } diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index bcb681a50652a..0d9f183d13404 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -19,12 +19,14 @@ import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { createRoutes } from './routes/create_routes'; +import { url } from './saved_objects'; export class SharePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup(core: CoreSetup) { createRoutes(core, this.initializerContext.logger.get()); + core.savedObjects.registerType(url); } public start() { diff --git a/src/plugins/share/server/saved_objects/index.ts b/src/plugins/share/server/saved_objects/index.ts new file mode 100644 index 0000000000000..956f031d2f1a5 --- /dev/null +++ b/src/plugins/share/server/saved_objects/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { url } from './url'; diff --git a/src/plugins/share/server/saved_objects/url.ts b/src/plugins/share/server/saved_objects/url.ts new file mode 100644 index 0000000000000..c76c21993a13f --- /dev/null +++ b/src/plugins/share/server/saved_objects/url.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. + */ +import { SavedObjectsType } from 'kibana/server'; + +export const url: SavedObjectsType = { + name: 'url', + namespaceType: 'single', + hidden: false, + management: { + icon: 'link', + defaultSearchField: 'url', + importableAndExportable: true, + getTitle(obj) { + return `/goto/${encodeURIComponent(obj.id)}`; + }, + }, + mappings: { + properties: { + accessCount: { + type: 'long', + }, + accessDate: { + type: 'date', + }, + createDate: { + type: 'date', + }, + url: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, +}; diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts index 4e0f02242961a..fd88f520205f5 100644 --- a/src/plugins/telemetry/public/mocks.ts +++ b/src/plugins/telemetry/public/mocks.ts @@ -25,11 +25,20 @@ import { httpServiceMock } from '../../../core/public/http/http_service.mock'; import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock'; import { TelemetryService } from './services/telemetry_service'; import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications'; -import { TelemetryPluginStart } from './plugin'; +import { TelemetryPluginStart, TelemetryPluginConfig } from './plugin'; + +// The following is to be able to access private methods +/* eslint-disable dot-notation */ + +export interface TelemetryServiceMockOptions { + reportOptInStatusChange?: boolean; + config?: Partial; +} export function mockTelemetryService({ reportOptInStatusChange, -}: { reportOptInStatusChange?: boolean } = {}) { + config: configOverride = {}, +}: TelemetryServiceMockOptions = {}) { const config = { enabled: true, url: 'http://localhost', @@ -39,14 +48,22 @@ export function mockTelemetryService({ banner: true, allowChangingOptInStatus: true, telemetryNotifyUserAboutOptInDefault: true, + ...configOverride, }; - return new TelemetryService({ + const telemetryService = new TelemetryService({ config, http: httpServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), reportOptInStatusChange, }); + + const originalReportOptInStatus = telemetryService['reportOptInStatus']; + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(optInPayload => { + return originalReportOptInStatus(optInPayload); // Actually calling the original method + }); + + return telemetryService; } export function mockTelemetryNotifications({ diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index 0a49b0ae3084e..16faa0cfc7536 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -67,17 +67,31 @@ describe('TelemetryService', () => { }); describe('setOptIn', () => { + it('does not call the api if canChangeOptInStatus==false', async () => { + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: false }, + }); + expect(await telemetryService.setOptIn(true)).toBe(false); + + expect(telemetryService['http'].post).toBeCalledTimes(0); + }); + it('calls api if canChangeOptInStatus', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['http'].post).toBeCalledTimes(1); }); it('sends enabled true if optedIn: true', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); const optedIn = true; await telemetryService.setOptIn(optedIn); @@ -87,8 +101,10 @@ describe('TelemetryService', () => { }); it('sends enabled false if optedIn: false', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); const optedIn = false; await telemetryService.setOptIn(optedIn); @@ -98,9 +114,10 @@ describe('TelemetryService', () => { }); it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); @@ -108,9 +125,10 @@ describe('TelemetryService', () => { }); it('calls reportOptInStatus if reportOptInStatusChange is true', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: true, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); @@ -118,9 +136,10 @@ describe('TelemetryService', () => { }); it('adds an error toast on api error', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { if (url === '/api/telemetry/v2/optIn') { throw Error('failed to update opt in.'); @@ -133,9 +152,13 @@ describe('TelemetryService', () => { expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); }); + // This one should not happen because the entire method is fully caught but hey! :) it('adds an error toast on reportOptInStatus error', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: true, + config: { allowChangingOptInStatus: true }, + }); + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => { throw Error('failed to report OptIn Status.'); }); @@ -146,4 +169,50 @@ describe('TelemetryService', () => { expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); }); }); + + describe('getTelemetryUrl', () => { + it('should return the config.url parameter', async () => { + const url = 'http://test.com'; + const telemetryService = mockTelemetryService({ + config: { url }, + }); + + expect(telemetryService.getTelemetryUrl()).toBe(url); + }); + }); + + describe('setUserHasSeenNotice', () => { + it('should hit the API and change the config', async () => { + const telemetryService = mockTelemetryService({ + config: { telemetryNotifyUserAboutOptInDefault: undefined }, + }); + + expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + await telemetryService.setUserHasSeenNotice(); + expect(telemetryService['http'].put).toBeCalledTimes(1); + expect(telemetryService.userHasSeenOptedInNotice).toBe(true); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(true); + }); + + it('should show a toast notification if the request fail', async () => { + const telemetryService = mockTelemetryService({ + config: { telemetryNotifyUserAboutOptInDefault: undefined }, + }); + + telemetryService['http'].put = jest.fn().mockImplementation((url: string) => { + if (url === '/api/telemetry/v2/userHasSeenNotice') { + throw Error('failed to update opt in.'); + } + }); + + expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + await telemetryService.setUserHasSeenNotice(); + expect(telemetryService['http'].put).toBeCalledTimes(1); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + expect(telemetryService.userHasSeenOptedInNotice).toBe(false); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + }); + }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index cac4e3fdf5f50..6d87a74197fe5 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -122,11 +122,15 @@ export class TelemetryService { } try { - await this.http.post('/api/telemetry/v2/optIn', { + // Report the option to the Kibana server to store the settings. + // It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}] + const optInPayload = await this.http.post('/api/telemetry/v2/optIn', { body: JSON.stringify({ enabled: optedIn }), }); if (this.reportOptInStatusChange) { - await this.reportOptInStatus(optedIn); + // Use the response to report about the change to the remote telemetry cluster. + // If it's opt-out, this will be the last communication to the remote service. + await this.reportOptInStatus(optInPayload); } this.isOptedIn = optedIn; } catch (err) { @@ -162,7 +166,11 @@ export class TelemetryService { } }; - private reportOptInStatus = async (OptInStatus: boolean): Promise => { + /** + * Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service + * @param optInPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings + */ + private reportOptInStatus = async (optInPayload: string[]): Promise => { const telemetryOptInStatusUrl = this.getOptInStatusUrl(); try { @@ -171,7 +179,7 @@ export class TelemetryService { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ enabled: OptInStatus }), + body: JSON.stringify(optInPayload), }); } catch (err) { // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. diff --git a/src/plugins/telemetry/server/config.ts b/src/plugins/telemetry/server/config.ts index 9621a8b5619b2..99dde0c3b3d96 100644 --- a/src/plugins/telemetry/server/config.ts +++ b/src/plugins/telemetry/server/config.ts @@ -36,8 +36,8 @@ export const configSchema = schema.object({ config: schema.string({ defaultValue: getConfigPath() }), banner: schema.boolean({ defaultValue: true }), url: schema.conditional( - schema.contextRef('dev'), - schema.literal(true), + schema.contextRef('dist'), + schema.literal(false), // Point to staging if it's not a distributable release schema.string({ defaultValue: `https://telemetry-staging.elastic.co/xpack/${ENDPOINT_VERSION}/send`, }), @@ -46,8 +46,8 @@ export const configSchema = schema.object({ }) ), optInStatusUrl: schema.conditional( - schema.contextRef('dev'), - schema.literal(true), + schema.contextRef('dist'), + schema.literal(false), // Point to staging if it's not a distributable release schema.string({ defaultValue: `https://telemetry-staging.elastic.co/opt_in_status/${ENDPOINT_VERSION}/send`, }), diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index e65ade0ab8aaa..4ed5dbf251275 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -22,7 +22,10 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; +import { + StatsGetterConfig, + TelemetryCollectionManagerPluginSetup, +} from 'src/plugins/telemetry_collection_manager/server'; import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config'; import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats'; @@ -79,23 +82,30 @@ export function registerTelemetryOptInRoutes({ }); } + const statsGetterConfig: StatsGetterConfig = { + start: moment() + .subtract(20, 'minutes') + .toISOString(), + end: moment().toISOString(), + unencrypted: false, + }; + + const optInStatus = await telemetryCollectionManager.getOptInStats( + newOptInStatus, + statsGetterConfig + ); + if (config.sendUsageFrom === 'server') { const optInStatusUrl = config.optInStatusUrl; await sendTelemetryOptInStatus( telemetryCollectionManager, { optInStatusUrl, newOptInStatus }, - { - start: moment() - .subtract(20, 'minutes') - .toISOString(), - end: moment().toISOString(), - unencrypted: false, - } + statsGetterConfig ); } await updateTelemetrySavedObject(context.core.savedObjects.client, attributes); - return res.ok({}); + return res.ok({ body: optInStatus }); } ); } diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts index c04625eb1dd42..6d64268569e06 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.test.ts @@ -22,14 +22,14 @@ import { encryptTelemetry, getKID } from './encrypt'; describe('getKID', () => { it(`returns 'kibana_dev' kid for development`, async () => { - const isProd = false; - const kid = getKID(isProd); + const useProdKey = false; + const kid = getKID(useProdKey); expect(kid).toBe('kibana_dev'); }); it(`returns 'kibana_prod' kid for development`, async () => { - const isProd = true; - const kid = getKID(isProd); + const useProdKey = true; + const kid = getKID(useProdKey); expect(kid).toBe('kibana'); }); }); @@ -41,19 +41,19 @@ describe('encryptTelemetry', () => { it('encrypts payload', async () => { const payload = { some: 'value' }; - await encryptTelemetry(payload, { isProd: true }); + await encryptTelemetry(payload, { useProdKey: true }); expect(createRequestEncryptor).toBeCalledWith(telemetryJWKS); }); - it('uses kibana kid on { isProd: true }', async () => { + it('uses kibana kid on { useProdKey: true }', async () => { const payload = { some: 'value' }; - await encryptTelemetry(payload, { isProd: true }); + await encryptTelemetry(payload, { useProdKey: true }); expect(mockEncrypt).toBeCalledWith('kibana', payload); }); - it('uses kibana_dev kid on { isProd: false }', async () => { + it('uses kibana_dev kid on { useProdKey: false }', async () => { const payload = { some: 'value' }; - await encryptTelemetry(payload, { isProd: false }); + await encryptTelemetry(payload, { useProdKey: false }); expect(mockEncrypt).toBeCalledWith('kibana_dev', payload); }); }); diff --git a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts index 44f053064cfcb..89f34d794f059 100644 --- a/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts +++ b/src/plugins/telemetry_collection_manager/server/encryption/encrypt.ts @@ -20,12 +20,15 @@ import { createRequestEncryptor } from '@elastic/request-crypto'; import { telemetryJWKS } from './telemetry_jwks'; -export function getKID(isProd = false): string { - return isProd ? 'kibana' : 'kibana_dev'; +export function getKID(useProdKey = false): string { + return useProdKey ? 'kibana' : 'kibana_dev'; } -export async function encryptTelemetry(payload: any, { isProd = false } = {}): Promise { - const kid = getKID(isProd); +export async function encryptTelemetry( + payload: any, + { useProdKey = false } = {} +): Promise { + const kid = getKID(useProdKey); const encryptor = await createRequestEncryptor(telemetryJWKS); const clusters = [].concat(payload); return Promise.all(clusters.map((cluster: any) => encryptor.encrypt(kid, cluster))); diff --git a/src/plugins/telemetry_collection_manager/server/plugin.ts b/src/plugins/telemetry_collection_manager/server/plugin.ts index f2f20e215c535..0b57fae83c0fb 100644 --- a/src/plugins/telemetry_collection_manager/server/plugin.ts +++ b/src/plugins/telemetry_collection_manager/server/plugin.ts @@ -50,12 +50,12 @@ export class TelemetryCollectionManagerPlugin private readonly collections: Array> = []; private usageGetterMethodPriority = -1; private usageCollection?: UsageCollectionSetup; - private readonly isDev: boolean; + private readonly isDistributable: boolean; private readonly version: string; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.isDev = initializerContext.env.mode.dev; + this.isDistributable = initializerContext.env.packageInfo.dist; this.version = initializerContext.env.packageInfo.version; } @@ -158,7 +158,7 @@ export class TelemetryCollectionManagerPlugin if (config.unencrypted) { return optInStats; } - return encryptTelemetry(optInStats, { isProd: !this.isDev }); + return encryptTelemetry(optInStats, { useProdKey: this.isDistributable }); } } catch (err) { this.logger.debug(`Failed to collect any opt in stats with registered collections.`); @@ -176,7 +176,6 @@ export class TelemetryCollectionManagerPlugin ) => { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), - isDev: this.isDev, version: this.version, ...collection.customContext, }; @@ -206,7 +205,7 @@ export class TelemetryCollectionManagerPlugin return usageData; } - return encryptTelemetry(usageData, { isProd: !this.isDev }); + return encryptTelemetry(usageData, { useProdKey: this.isDistributable }); } } catch (err) { this.logger.debug( @@ -225,7 +224,6 @@ export class TelemetryCollectionManagerPlugin ): Promise { const context: StatsCollectionContext = { logger: this.logger.get(collection.title), - isDev: this.isDev, version: this.version, ...collection.customContext, }; diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index e23d6a4c388f4..d3a47694d38a7 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -101,7 +101,6 @@ export interface ESLicense { export interface StatsCollectionContext { logger: Logger; - isDev: boolean; version: string; } diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap new file mode 100644 index 0000000000000..8c0117e5a7266 --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -0,0 +1,492 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` + + + + + + +

+ +

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

+ } + /> + + +

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

+

+ + + +

+ , + "displayName": "Provide usage statistics", + "name": "telemetry:enabled", + "type": "boolean", + "value": true, + } + } + toasts={null} + /> +
+
+
+`; + +exports[`TelemetryManagementSectionComponent renders null because allowChangingOptInStatus is false 1`] = ` + +`; + +exports[`TelemetryManagementSectionComponent renders null because query does not match the SEARCH_TERMS 1`] = ` + +`; + +exports[`TelemetryManagementSectionComponent test the wrapper (for coverage purposes) 1`] = `""`; diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx new file mode 100644 index 0000000000000..d0c2bd13f802d --- /dev/null +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -0,0 +1,284 @@ +/* + * 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 React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { TelemetryManagementSection } from './telemetry_management_section'; +import { TelemetryService } from '../../../telemetry/public/services'; +import { coreMock } from '../../../../core/public/mocks'; +import { telemetryManagementSectionWrapper } from './telemetry_management_section_wrapper'; + +describe('TelemetryManagementSectionComponent', () => { + const coreStart = coreMock.createStart(); + const coreSetup = coreMock.createSetup(); + + it('renders as expected', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: true, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + expect( + shallowWithIntl( + + ) + ).toMatchSnapshot(); + }); + + it('renders null because query does not match the SEARCH_TERMS', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect( + component.setProps({ ...component.props(), query: { text: 'asssdasdsad' } }) + ).toMatchSnapshot(); + expect(onQueryMatchChange).toHaveBeenCalledWith(false); + expect(onQueryMatchChange).toHaveBeenCalledTimes(1); + } finally { + component.unmount(); + } + }); + + it('renders because query matches the SEARCH_TERMS', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect( + component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } }).html() + ).not.toBe(''); // Renders something. + // I can't check against snapshot because of https://github.com/facebook/jest/issues/8618 + // expect(component).toMatchSnapshot(); + + // It should also render if there is no query at all. + expect(component.setProps({ ...component.props(), query: {} }).html()).not.toBe(''); + expect(onQueryMatchChange).toHaveBeenCalledWith(true); + + // Should only be called once because the second time does not change the result + expect(onQueryMatchChange).toHaveBeenCalledTimes(1); + } finally { + component.unmount(); + } + }); + + it('renders null because allowChangingOptInStatus is false', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: false, + optIn: true, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + expect(component).toMatchSnapshot(); + component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } }); + expect(onQueryMatchChange).toHaveBeenCalledWith(false); + } finally { + component.unmount(); + } + }); + + it('shows the OptInExampleFlyout', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + const toggleExampleComponent = component.find('p > EuiLink[onClick]'); + const updatedView = toggleExampleComponent.simulate('click'); + updatedView.find('OptInExampleFlyout'); + updatedView.simulate('close'); + } finally { + component.unmount(); + } + }); + + it('toggles the OptIn button', async () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: true, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + + const component = mountWithIntl( + + ); + try { + const toggleOptInComponent = component.find('Field'); + await expect( + toggleOptInComponent.prop('handleChange')() + ).resolves.toBe(true); + expect((component.state() as any).enabled).toBe(true); + await expect( + toggleOptInComponent.prop('handleChange')() + ).resolves.toBe(true); + expect((component.state() as any).enabled).toBe(false); + telemetryService.setOptIn = jest.fn().mockRejectedValue(Error('test-error')); + await expect( + toggleOptInComponent.prop('handleChange')() + ).rejects.toStrictEqual(Error('test-error')); + } finally { + component.unmount(); + } + }); + + it('test the wrapper (for coverage purposes)', () => { + const onQueryMatchChange = jest.fn(); + const telemetryService = new TelemetryService({ + config: { + enabled: true, + url: '', + banner: true, + allowChangingOptInStatus: false, + optIn: false, + optInStatusUrl: '', + sendUsageFrom: 'browser', + }, + reportOptInStatusChange: false, + notifications: coreStart.notifications, + http: coreSetup.http, + }); + const Wrapper = telemetryManagementSectionWrapper(telemetryService); + expect( + shallowWithIntl( + + ).html() + ).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index 26e075b666593..361c5ff719c54 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -69,7 +69,9 @@ export class TelemetryManagementSection extends Component { const { query } = nextProps; const searchTerm = (query.text || '').toLowerCase(); - const searchTermMatches = SEARCH_TERMS.some(term => term.indexOf(searchTerm) >= 0); + const searchTermMatches = + this.props.telemetryService.getCanChangeOptInStatus() && + SEARCH_TERMS.some(term => term.indexOf(searchTerm) >= 0); if (searchTermMatches !== this.state.queryMatches) { this.setState( diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index c7e6d61e15f31..e6247a8bafff7 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -19,9 +19,10 @@ import { ActionByType } from './actions/action'; import { TriggerInternal } from './triggers/trigger_internal'; -import { EmbeddableVisTriggerContext, IEmbeddable } from '../../embeddable/public'; import { Filter } from '../../data/public'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; +import { IEmbeddable } from '../../embeddable/public'; +import { RangeSelectTriggerContext, ValueClickTriggerContext } from '../../embeddable/public'; export type TriggerRegistry = Map>; export type ActionRegistry = Map>; @@ -36,8 +37,8 @@ export type TriggerContext = BaseContext; export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; - [SELECT_RANGE_TRIGGER]: EmbeddableVisTriggerContext; - [VALUE_CLICK_TRIGGER]: EmbeddableVisTriggerContext; + [SELECT_RANGE_TRIGGER]: RangeSelectTriggerContext; + [VALUE_CLICK_TRIGGER]: ValueClickTriggerContext; [APPLY_FILTER_TRIGGER]: { embeddable: IEmbeddable; filters: Filter[]; diff --git a/src/plugins/vis_default_editor/public/agg_filters/agg_type_field_filters.ts b/src/plugins/vis_default_editor/public/agg_filters/agg_type_field_filters.ts new file mode 100644 index 0000000000000..15df2f0acccd1 --- /dev/null +++ b/src/plugins/vis_default_editor/public/agg_filters/agg_type_field_filters.ts @@ -0,0 +1,49 @@ +/* + * 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 { IAggConfig, IndexPatternField } from '../../../data/public'; + +type AggTypeFieldFilter = (field: IndexPatternField, aggConfig: IAggConfig) => boolean; + +const filters: AggTypeFieldFilter[] = [ + /** + * Check index pattern aggregation restrictions + * and limit available fields for a given aggType based on that. + */ + (field, aggConfig) => { + const indexPattern = aggConfig.getIndexPattern(); + const aggRestrictions = indexPattern.getAggregationRestrictions(); + + if (!aggRestrictions) { + return true; + } + + const aggName = aggConfig.type && aggConfig.type.name; + const aggFields = aggRestrictions[aggName]; + return !!aggFields && !!aggFields[field.name]; + }, +]; + +export function filterAggTypeFields(fields: IndexPatternField[], aggConfig: IAggConfig) { + const allowedAggTypeFields = fields.filter(field => { + const isAggTypeFieldAllowed = filters.every(filter => filter(field, aggConfig)); + return isAggTypeFieldAllowed; + }); + return allowedAggTypeFields; +} diff --git a/src/plugins/vis_default_editor/public/agg_filters/agg_type_filters.ts b/src/plugins/vis_default_editor/public/agg_filters/agg_type_filters.ts new file mode 100644 index 0000000000000..2cf1acba4d228 --- /dev/null +++ b/src/plugins/vis_default_editor/public/agg_filters/agg_type_filters.ts @@ -0,0 +1,75 @@ +/* + * 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 { IAggType, IAggConfig, IndexPattern, search } from '../../../data/public'; + +const { propFilter } = search.aggs; +const filterByName = propFilter('name'); + +type AggTypeFilter = ( + aggType: IAggType, + indexPattern: IndexPattern, + aggConfig: IAggConfig, + aggFilter: string[] +) => boolean; + +const filters: AggTypeFilter[] = [ + /** + * This filter checks the defined aggFilter in the schemas of that visualization + * and limits available aggregations based on that. + */ + (aggType, indexPattern, aggConfig, aggFilter) => { + const doesSchemaAllowAggType = filterByName([aggType], aggFilter).length !== 0; + return doesSchemaAllowAggType; + }, + /** + * Check index pattern aggregation restrictions and limit available aggTypes. + */ + (aggType, indexPattern, aggConfig, aggFilter) => { + const aggRestrictions = indexPattern.getAggregationRestrictions(); + + if (!aggRestrictions) { + return true; + } + + const aggName = aggType.name; + // Only return agg types which are specified in the agg restrictions, + // except for `count` which should always be returned. + return ( + aggName === 'count' || + (!!aggRestrictions && Object.keys(aggRestrictions).includes(aggName)) || + false + ); + }, +]; + +export function filterAggTypes( + aggTypes: IAggType[], + indexPattern: IndexPattern, + aggConfig: IAggConfig, + aggFilter: string[] +) { + const allowedAggTypes = aggTypes.filter(aggType => { + const isAggTypeAllowed = filters.every(filter => + filter(aggType, indexPattern, aggConfig, aggFilter) + ); + return isAggTypeAllowed; + }); + return allowedAggTypes; +} diff --git a/src/plugins/vis_default_editor/public/agg_filters/index.ts b/src/plugins/vis_default_editor/public/agg_filters/index.ts new file mode 100644 index 0000000000000..2b08449fb3161 --- /dev/null +++ b/src/plugins/vis_default_editor/public/agg_filters/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * from './agg_type_filters'; +export * from './agg_type_field_filters'; diff --git a/src/plugins/vis_default_editor/public/components/agg_common_props.ts b/src/plugins/vis_default_editor/public/components/agg_common_props.ts index 0c130a96230b4..40d7b79bfbefc 100644 --- a/src/plugins/vis_default_editor/public/components/agg_common_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_common_props.ts @@ -18,7 +18,7 @@ */ import { VisParams } from 'src/plugins/visualizations/public'; -import { IAggType, IAggConfig, IAggGroupNames } from 'src/plugins/data/public'; +import { IAggType, IAggConfig, AggGroupName } from 'src/plugins/data/public'; import { Schema } from '../schemas'; import { EditorVisState } from './sidebar/state/reducers'; @@ -30,7 +30,7 @@ export type ReorderAggs = (sourceAgg: IAggConfig, destinationAgg: IAggConfig) => export interface DefaultEditorCommonProps { formIsTouched: boolean; - groupName: IAggGroupNames; + groupName: AggGroupName; metricAggs: IAggConfig[]; state: EditorVisState; setAggParamValue: ( diff --git a/src/plugins/vis_default_editor/public/components/agg_group.tsx b/src/plugins/vis_default_editor/public/components/agg_group.tsx index ecbc41f28003c..fae9de6959ef1 100644 --- a/src/plugins/vis_default_editor/public/components/agg_group.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_group.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AggGroupNames, search, IAggConfig, TimeRange } from '../../../data/public'; +import { AggGroupNames, AggGroupLabels, IAggConfig, TimeRange } from '../../../data/public'; import { DefaultEditorAgg } from './agg'; import { DefaultEditorAggAdd } from './agg_add'; import { AddSchema, ReorderAggs, DefaultEditorAggCommonProps } from './agg_common_props'; @@ -70,7 +70,7 @@ function DefaultEditorAggGroup({ setValidity, timeRange, }: DefaultEditorAggGroupProps) { - const groupNameLabel = (search.aggs.aggGroupNamesMap() as any)[groupName]; + const groupNameLabel = AggGroupLabels[groupName]; // e.g. buckets can have no aggs const schemaNames = schemas.map(s => s.name); const group: IAggConfig[] = useMemo( @@ -170,7 +170,7 @@ function DefaultEditorAggGroup({ agg={agg} aggIndex={index} aggIsTooLow={calcAggIsTooLow(agg, index, group, schemas)} - dragHandleProps={provided.dragHandleProps} + dragHandleProps={provided.dragHandleProps || null} formIsTouched={aggsState[agg.id] ? aggsState[agg.id].touched : false} groupName={groupName} isDraggable={stats.count > 1} diff --git a/src/plugins/vis_default_editor/public/components/agg_param_props.ts b/src/plugins/vis_default_editor/public/components/agg_param_props.ts index aec332e8674d7..076bddc9551ea 100644 --- a/src/plugins/vis_default_editor/public/components/agg_param_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_param_props.ts @@ -17,7 +17,12 @@ * under the License. */ -import { IAggConfig, AggParam, IndexPatternField } from 'src/plugins/data/public'; +import { + IAggConfig, + AggParam, + IndexPatternField, + OptionedValueProp, +} from 'src/plugins/data/public'; import { ComboBoxGroupedOptions } from '../utils'; import { EditorConfig } from './utils'; import { Schema } from '../schemas'; @@ -46,3 +51,9 @@ export interface AggParamEditorProps extends AggParamCommonProp setValidity(isValid: boolean): void; setTouched(): void; } + +export interface OptionedParamEditorProps { + aggParam: { + options: T[]; + }; +} diff --git a/src/plugins/vis_default_editor/public/components/agg_params.tsx b/src/plugins/vis_default_editor/public/components/agg_params.tsx index 3674e39b558d2..d36c2d0e7625b 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params.tsx +++ b/src/plugins/vis_default_editor/public/components/agg_params.tsx @@ -112,20 +112,8 @@ function DefaultEditorAggParams({ fieldName, ]); const params = useMemo( - () => - getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }, - services.data.search.__LEGACY.aggTypeFieldFilters - ), - [ - agg, - editorConfig, - metricAggs, - state, - schemas, - hideCustomLabel, - services.data.search.__LEGACY.aggTypeFieldFilters, - ] + () => getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }), + [agg, editorConfig, metricAggs, state, schemas, hideCustomLabel] ); const allParams = [...params.basic, ...params.advanced]; const [paramsState, onChangeParamsState] = useReducer( diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts index bed2561341737..834ad8b70ad0d 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.test.ts @@ -23,7 +23,6 @@ import { IAggConfig, IAggType, IndexPattern, - IndexPatternField, } from 'src/plugins/data/public'; import { getAggParamsToRender, @@ -39,12 +38,6 @@ jest.mock('../utils', () => ({ groupAndSortBy: jest.fn(() => ['indexedFields']), })); -const mockFilter: any = { - filter(fields: IndexPatternField[]): IndexPatternField[] { - return fields; - }, -}; - describe('DefaultEditorAggParams helpers', () => { describe('getAggParamsToRender', () => { let agg: IAggConfig; @@ -72,20 +65,14 @@ describe('DefaultEditorAggParams helpers', () => { }, schema: 'metric', } as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); it('should not create any param if there is no agg type', () => { agg = { schema: 'metric' } as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); @@ -101,10 +88,7 @@ describe('DefaultEditorAggParams helpers', () => { hidden: true, }, }; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); @@ -116,10 +100,7 @@ describe('DefaultEditorAggParams helpers', () => { }, schema: 'metric2', } as any) as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual(emptyParams); }); @@ -152,16 +133,14 @@ describe('DefaultEditorAggParams helpers', () => { { name: '@timestamp', type: 'date' }, { name: 'geo_desc', type: 'string' }, ], + getAggregationRestrictions: jest.fn(), })), params: { orderBy: 'orderBy', field: 'field', }, } as any) as IAggConfig; - const params = getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas }, - mockFilter - ); + const params = getAggParamsToRender({ agg, editorConfig, metricAggs, state, schemas }); expect(params).toEqual({ basic: [ @@ -190,7 +169,6 @@ describe('DefaultEditorAggParams helpers', () => { ], advanced: [], }); - expect(agg.getIndexPattern).toBeCalledTimes(1); }); }); diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index a32bd76bafa5a..9977f1e5e71fc 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -20,7 +20,6 @@ import { get, isEmpty } from 'lodash'; import { - AggTypeFieldFilters, IAggConfig, AggParam, IFieldParamType, @@ -28,13 +27,13 @@ import { IndexPattern, IndexPatternField, } from 'src/plugins/data/public'; +import { filterAggTypes, filterAggTypeFields } from '../agg_filters'; import { groupAndSortBy, ComboBoxGroupedOptions } from '../utils'; import { AggTypeState, AggParamsState } from './agg_params_state'; import { AggParamEditorProps } from './agg_param_props'; import { aggParamsMap } from './agg_params_map'; import { EditorConfig } from './utils'; import { Schema, getSchemaByName } from '../schemas'; -import { search } from '../../../data/public'; import { EditorVisState } from './sidebar/state/reducers'; interface ParamInstanceBase { @@ -53,10 +52,14 @@ export interface ParamInstance extends ParamInstanceBase { value: unknown; } -function getAggParamsToRender( - { agg, editorConfig, metricAggs, state, schemas, hideCustomLabel }: ParamInstanceBase, - aggTypeFieldFilters: AggTypeFieldFilters -) { +function getAggParamsToRender({ + agg, + editorConfig, + metricAggs, + state, + schemas, + hideCustomLabel, +}: ParamInstanceBase) { const params = { basic: [] as ParamInstance[], advanced: [] as ParamInstance[], @@ -89,7 +92,7 @@ function getAggParamsToRender( availableFields = availableFields.filter(field => field.type === 'number'); } } - fields = aggTypeFieldFilters.filter(availableFields, agg); + fields = filterAggTypeFields(availableFields, agg); indexedFields = groupAndSortBy(fields, 'type', 'name'); if (fields && !indexedFields.length && index > 0) { @@ -138,12 +141,7 @@ function getAggTypeOptions( groupName: string, allowedAggs: string[] ): ComboBoxGroupedOptions { - const aggTypeOptions = search.aggs.aggTypeFilters.filter( - aggTypes[groupName], - indexPattern, - agg, - allowedAggs - ); + const aggTypeOptions = filterAggTypes(aggTypes[groupName], indexPattern, agg, allowedAggs); return groupAndSortBy(aggTypeOptions as any[], 'subtype', 'title'); } diff --git a/src/plugins/vis_default_editor/public/components/agg_params_map.ts b/src/plugins/vis_default_editor/public/components/agg_params_map.ts index 5af3cfc5b0928..9bc3146b9903b 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_map.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_map.ts @@ -58,6 +58,8 @@ const buckets = { size: controls.SizeParamEditor, }, [BUCKET_TYPES.TERMS]: { + include: controls.IncludeExcludeParamEditor, + exclude: controls.IncludeExcludeParamEditor, orderBy: controls.OrderByParamEditor, orderAgg: controls.OrderAggParamEditor, order: wrapWithInlineComp(controls.OrderParamEditor), diff --git a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts index dac86249ebbb9..5cb594ade8dba 100644 --- a/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts +++ b/src/plugins/vis_default_editor/public/components/controls/components/number_list/utils.ts @@ -119,7 +119,7 @@ function getNextModel(list: NumberRowModel[], range: NumberListRange): NumberRow }; } -function getInitModelList(list: Array): NumberRowModel[] { +function getInitModelList(list: Array): NumberRowModel[] { return list.length ? list.map(num => ({ value: (num === undefined ? EMPTY_STRING : num) as NumberRowModel['value'], diff --git a/src/plugins/vis_default_editor/public/components/controls/components/simple_number_list.tsx b/src/plugins/vis_default_editor/public/components/controls/components/simple_number_list.tsx new file mode 100644 index 0000000000000..becf8e47ef573 --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/components/simple_number_list.tsx @@ -0,0 +1,140 @@ +/* + * 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 React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; +import { isArray } from 'lodash'; +import { EuiButtonEmpty, EuiFlexItem, EuiFormRow, EuiSpacer, htmlIdGenerator } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EMPTY_STRING, getInitModelList, getRange, parse } from './number_list/utils'; +import { NumberRow, NumberRowModel } from './number_list/number_row'; +import { AggParamEditorProps } from '../../agg_param_props'; + +const generateId = htmlIdGenerator(); + +function SimpleNumberList({ + agg, + aggParam, + value, + setValue, + setTouched, +}: AggParamEditorProps>) { + const [numbers, setNumbers] = useState( + getInitModelList(value && isArray(value) ? value : [EMPTY_STRING]) + ); + const numberRange = useMemo(() => getRange('[-Infinity,Infinity]'), []); + + // This useEffect is needed to discard changes, it sets numbers a mapped value if they are different + useEffect(() => { + if ( + isArray(value) && + (value.length !== numbers.length || + !value.every((numberValue, index) => numberValue === numbers[index].value)) + ) { + setNumbers( + value.map(numberValue => ({ + id: generateId(), + value: numberValue, + isInvalid: false, + })) + ); + } + }, [numbers, value]); + + const onUpdate = useCallback( + (numberList: NumberRowModel[]) => { + setNumbers(numberList); + setValue(numberList.map(({ value: numberValue }) => numberValue)); + }, + [setValue] + ); + + const onChangeValue = useCallback( + (numberField: { id: string; value: string }) => { + onUpdate( + numbers.map(number => + number.id === numberField.id + ? { + id: numberField.id, + value: parse(numberField.value), + isInvalid: false, + } + : number + ) + ); + }, + [numbers, onUpdate] + ); + + // Add an item to the end of the list + const onAdd = useCallback(() => { + const newArray = [ + ...numbers, + { + id: generateId(), + value: EMPTY_STRING as '', + isInvalid: false, + }, + ]; + onUpdate(newArray); + }, [numbers, onUpdate]); + + const onDelete = useCallback( + (id: string) => onUpdate(numbers.filter(number => number.id !== id)), + [numbers, onUpdate] + ); + + return ( + + <> + {numbers.map((number, arrayIndex) => ( + + + {numbers.length - 1 !== arrayIndex && } + + ))} + + + + + + + + + ); +} + +export { SimpleNumberList }; diff --git a/src/plugins/vis_default_editor/public/components/controls/include_exclude.tsx b/src/plugins/vis_default_editor/public/components/controls/include_exclude.tsx new file mode 100644 index 0000000000000..f60f6ce7ce249 --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/include_exclude.tsx @@ -0,0 +1,49 @@ +/* + * 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 React, { useEffect } from 'react'; +import { AggParamEditorProps } from '../agg_param_props'; +import { StringParamEditor } from './string'; +import { search } from '../../../../data/public'; +import { SimpleNumberList } from './components/simple_number_list'; +const { isNumberType } = search.aggs; + +export function IncludeExcludeParamEditor(props: AggParamEditorProps>) { + const { agg, value, setValue } = props; + const isAggOfNumberType = isNumberType(agg); + + // This useEffect converts value from string type to number and back when the field type is changed + useEffect(() => { + if (isAggOfNumberType && !Array.isArray(value) && value !== undefined) { + const numberArray = value + .split('|') + .map(item => parseFloat(item)) + .filter(number => Number.isFinite(number)); + setValue(numberArray.length ? numberArray : ['']); + } else if (!isAggOfNumberType && Array.isArray(value) && value !== undefined) { + setValue(value.filter(item => item !== '').join('|')); + } + }, [isAggOfNumberType, setValue, value]); + + return isAggOfNumberType ? ( + } /> + ) : ( + + ); +} diff --git a/src/plugins/vis_default_editor/public/components/controls/index.ts b/src/plugins/vis_default_editor/public/components/controls/index.ts index e8944aa667853..cfb236e5e22e3 100644 --- a/src/plugins/vis_default_editor/public/components/controls/index.ts +++ b/src/plugins/vis_default_editor/public/components/controls/index.ts @@ -24,6 +24,7 @@ export { ExtendedBoundsParamEditor } from './extended_bounds'; export { FieldParamEditor } from './field'; export { FiltersParamEditor } from './filters'; export { HasExtendedBoundsParamEditor } from './has_extended_bounds'; +export { IncludeExcludeParamEditor } from './include_exclude'; export { IpRangesParamEditor } from './ip_ranges'; export { IpRangeTypeParamEditor } from './ip_range_type'; export { IsFilteredByCollarParamEditor } from './is_filtered_by_collar'; diff --git a/src/plugins/vis_default_editor/public/components/controls/order.tsx b/src/plugins/vis_default_editor/public/components/controls/order.tsx index e609bf9adf790..3c0224564300a 100644 --- a/src/plugins/vis_default_editor/public/components/controls/order.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/order.tsx @@ -21,8 +21,8 @@ import React, { useEffect } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { OptionedValueProp, OptionedParamEditorProps } from 'src/plugins/data/public'; -import { AggParamEditorProps } from '../agg_param_props'; +import { OptionedValueProp } from 'src/plugins/data/public'; +import { AggParamEditorProps, OptionedParamEditorProps } from '../agg_param_props'; function OrderParamEditor({ aggParam, diff --git a/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx b/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx index ad23cec87800f..66abb88b97d29 100644 --- a/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx +++ b/src/plugins/vis_default_editor/public/components/controls/top_aggregate.tsx @@ -26,10 +26,9 @@ import { IAggConfig, AggParam, OptionedValueProp, - OptionedParamEditorProps, OptionedParamType, } from 'src/plugins/data/public'; -import { AggParamEditorProps } from '../agg_param_props'; +import { AggParamEditorProps, OptionedParamEditorProps } from '../agg_param_props'; export interface AggregateValueProp extends OptionedValueProp { isCompatible(aggConfig: IAggConfig): boolean; diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index 1c2ddbc314f99..8088921ba7fda 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -22,7 +22,6 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EditorRenderProps } from 'src/plugins/visualize/public'; import { PanelsContainer, Panel } from '../../kibana_react/public'; -import './vis_type_agg_filter'; import { DefaultEditorSideBar } from './components/sidebar'; import { DefaultEditorControllerState } from './default_editor_controller'; import { getInitialWidth } from './editor_size'; diff --git a/src/plugins/vis_default_editor/public/schemas.ts b/src/plugins/vis_default_editor/public/schemas.ts index 05ba5fa9c9419..26d1cbe91b996 100644 --- a/src/plugins/vis_default_editor/public/schemas.ts +++ b/src/plugins/vis_default_editor/public/schemas.ts @@ -21,7 +21,7 @@ import _, { defaults } from 'lodash'; import { Optional } from '@kbn/utility-types'; -import { AggGroupNames, AggParam, IAggGroupNames } from '../../data/public'; +import { AggGroupNames, AggParam, AggGroupName } from '../../data/public'; export interface ISchemas { [AggGroupNames.Buckets]: Schema[]; @@ -32,7 +32,7 @@ export interface ISchemas { export interface Schema { aggFilter: string[]; editor: boolean | string; - group: IAggGroupNames; + group: AggGroupName; max: number; min: number; name: string; diff --git a/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts b/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts deleted file mode 100644 index bf5661f42a9f5..0000000000000 --- a/src/plugins/vis_default_editor/public/vis_type_agg_filter.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { IAggType, IAggConfig, IndexPattern, search } from '../../data/public'; - -const { aggTypeFilters, propFilter } = search.aggs; -const filterByName = propFilter('name'); - -/** - * This filter checks the defined aggFilter in the schemas of that visualization - * and limits available aggregations based on that. - */ -aggTypeFilters.addFilter( - (aggType: IAggType, indexPatterns: IndexPattern, aggConfig: IAggConfig, aggFilter: string[]) => { - const doesSchemaAllowAggType = filterByName([aggType], aggFilter).length !== 0; - return doesSchemaAllowAggType; - } -); diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index b84d9638eb973..3309330d7527c 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { MarkdownVisWrapper } from './markdown_vis_controller'; import { MarkdownOptions } from './markdown_options'; -import { SettingsOptions } from './settings_options'; +import { SettingsOptions } from './settings_options_lazy'; import { DefaultEditorSize } from '../../vis_default_editor/public'; export const markdownVisDefinition = { diff --git a/src/plugins/vis_type_markdown/public/settings_options.tsx b/src/plugins/vis_type_markdown/public/settings_options.tsx index 6f6a80564ce07..bf4570db5d4a0 100644 --- a/src/plugins/vis_type_markdown/public/settings_options.tsx +++ b/src/plugins/vis_type_markdown/public/settings_options.tsx @@ -52,4 +52,6 @@ function SettingsOptions({ stateParams, setValue }: VisOptionsProps import('./settings_options')); + +export const SettingsOptions = (props: any) => ( + }> + + +); diff --git a/src/plugins/vis_type_table/config.ts b/src/plugins/vis_type_table/config.ts new file mode 100644 index 0000000000000..6749bd83de39f --- /dev/null +++ b/src/plugins/vis_type_table/config.ts @@ -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. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_table/kibana.json b/src/plugins/vis_type_table/kibana.json new file mode 100644 index 0000000000000..bb0f6478a4240 --- /dev/null +++ b/src/plugins/vis_type_table/kibana.json @@ -0,0 +1,11 @@ +{ + "id": "visTypeTable", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "expressions", + "visualizations", + "data" + ] +} diff --git a/src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap b/src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap rename to src/plugins/vis_type_table/public/__snapshots__/table_vis_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_table/public/_table_vis.scss b/src/plugins/vis_type_table/public/_table_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/_table_vis.scss rename to src/plugins/vis_type_table/public/_table_vis.scss diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/_agg_table.scss b/src/plugins/vis_type_table/public/agg_table/_agg_table.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/_agg_table.scss rename to src/plugins/vis_type_table/public/agg_table/_agg_table.scss diff --git a/src/plugins/vis_type_table/public/agg_table/_index.scss b/src/plugins/vis_type_table/public/agg_table/_index.scss new file mode 100644 index 0000000000000..340e08a76f1bd --- /dev/null +++ b/src/plugins/vis_type_table/public/agg_table/_index.scss @@ -0,0 +1 @@ +@import './agg_table'; diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.html b/src/plugins/vis_type_table/public/agg_table/agg_table.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table.html rename to src/plugins/vis_type_table/public/agg_table/agg_table.html diff --git a/src/plugins/vis_type_table/public/agg_table/agg_table.js b/src/plugins/vis_type_table/public/agg_table/agg_table.js new file mode 100644 index 0000000000000..0cd501e2d0344 --- /dev/null +++ b/src/plugins/vis_type_table/public/agg_table/agg_table.js @@ -0,0 +1,272 @@ +/* + * 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 _ from 'lodash'; +import aggTableTemplate from './agg_table.html'; +import { getFormatService } from '../services'; +import { i18n } from '@kbn/i18n'; + +export function KbnAggTable(config, RecursionHelper) { + return { + restrict: 'E', + template: aggTableTemplate, + scope: { + table: '=', + dimensions: '=', + perPage: '=?', + sort: '=?', + exportTitle: '=?', + showTotal: '=', + totalFunc: '=', + percentageCol: '=', + filter: '=', + }, + controllerAs: 'aggTable', + compile: function($el) { + // Use the compile function from the RecursionHelper, + // And return the linking function(s) which it returns + return RecursionHelper.compile($el); + }, + controller: function($scope) { + const self = this; + + self._saveAs = require('@elastic/filesaver').saveAs; + self.csv = { + separator: config.get('csv:separator'), + quoteValues: config.get('csv:quoteValues'), + }; + + self.exportAsCsv = function(formatted) { + const csv = new Blob([self.toCsv(formatted)], { type: 'text/plain;charset=utf-8' }); + self._saveAs(csv, self.csv.filename); + }; + + self.toCsv = function(formatted) { + const rows = $scope.table.rows; + const columns = formatted ? $scope.formattedColumns : $scope.table.columns; + const nonAlphaNumRE = /[^a-zA-Z0-9]/; + const allDoubleQuoteRE = /"/g; + + function escape(val) { + if (!formatted && _.isObject(val)) val = val.valueOf(); + val = String(val); + if (self.csv.quoteValues && nonAlphaNumRE.test(val)) { + val = '"' + val.replace(allDoubleQuoteRE, '""') + '"'; + } + return val; + } + + // escape each cell in each row + const csvRows = rows.map(function(row) { + return Object.entries(row).map(([k, v]) => { + const column = columns.find(c => c.id === k); + if (formatted && column) { + return escape(column.formatter.convert(v)); + } + return escape(v); + }); + }); + + // add the columns to the rows + csvRows.unshift( + columns.map(function(col) { + return escape(formatted ? col.title : col.name); + }) + ); + + return csvRows + .map(function(row) { + return row.join(self.csv.separator) + '\r\n'; + }) + .join(''); + }; + + $scope.$watchMulti( + ['table', 'exportTitle', 'percentageCol', 'totalFunc', '=scope.dimensions'], + function() { + const { table, exportTitle, percentageCol } = $scope; + const showPercentage = percentageCol !== ''; + + if (!table) { + $scope.rows = null; + $scope.formattedColumns = null; + return; + } + + self.csv.filename = (exportTitle || table.title || 'table') + '.csv'; + $scope.rows = table.rows; + $scope.formattedColumns = []; + + if (typeof $scope.dimensions === 'undefined') return; + + const { buckets, metrics, splitColumn } = $scope.dimensions; + + $scope.formattedColumns = table.columns + .map(function(col, i) { + const isBucket = buckets.find(bucket => bucket.accessor === i); + const isSplitColumn = splitColumn + ? splitColumn.find(splitColumn => splitColumn.accessor === i) + : undefined; + const dimension = + isBucket || isSplitColumn || metrics.find(metric => metric.accessor === i); + + if (!dimension) return; + + const formatter = getFormatService().deserialize(dimension.format); + + const formattedColumn = { + id: col.id, + title: col.name, + formatter: formatter, + filterable: !!isBucket, + }; + + const last = i === table.columns.length - 1; + + if (last || !isBucket) { + formattedColumn.class = 'visualize-table-right'; + } + + const isDate = + dimension.format?.id === 'date' || dimension.format?.params?.id === 'date'; + const allowsNumericalAggregations = formatter?.allowsNumericalAggregations; + + let { totalFunc } = $scope; + if (typeof totalFunc === 'undefined' && showPercentage) { + totalFunc = 'sum'; + } + + if (allowsNumericalAggregations || isDate || totalFunc === 'count') { + const sum = tableRows => { + return _.reduce( + tableRows, + function(prev, curr) { + // some metrics return undefined for some of the values + // derivative is an example of this as it returns undefined in the first row + if (curr[col.id] === undefined) return prev; + return prev + curr[col.id]; + }, + 0 + ); + }; + + formattedColumn.sumTotal = sum(table.rows); + switch (totalFunc) { + case 'sum': { + if (!isDate) { + const total = formattedColumn.sumTotal; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = formattedColumn.sumTotal; + } + break; + } + case 'avg': { + if (!isDate) { + const total = sum(table.rows) / table.rows.length; + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + } + break; + } + case 'min': { + const total = _.chain(table.rows) + .map(col.id) + .min() + .value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'max': { + const total = _.chain(table.rows) + .map(col.id) + .max() + .value(); + formattedColumn.formattedTotal = formatter.convert(total); + formattedColumn.total = total; + break; + } + case 'count': { + const total = table.rows.length; + formattedColumn.formattedTotal = total; + formattedColumn.total = total; + break; + } + default: + break; + } + } + + return formattedColumn; + }) + .filter(column => column); + + if (showPercentage) { + const insertAtIndex = _.findIndex($scope.formattedColumns, { title: percentageCol }); + + // column to show percentage for was removed + if (insertAtIndex < 0) return; + + const { cols, rows } = addPercentageCol( + $scope.formattedColumns, + percentageCol, + table.rows, + insertAtIndex + ); + $scope.rows = rows; + $scope.formattedColumns = cols; + } + } + ); + }, + }; +} + +/** + * @param {Object[]} columns - the formatted columns that will be displayed + * @param {String} title - the title of the column to add to + * @param {Object[]} rows - the row data for the columns + * @param {Number} insertAtIndex - the index to insert the percentage column at + * @returns {Object} - cols and rows for the table to render now included percentage column(s) + */ +function addPercentageCol(columns, title, rows, insertAtIndex) { + const { id, sumTotal } = columns[insertAtIndex]; + const newId = `${id}-percents`; + const formatter = getFormatService().deserialize({ id: 'percent' }); + const i18nTitle = i18n.translate('visTypeTable.params.percentageTableColumnName', { + defaultMessage: '{title} percentages', + values: { title }, + }); + const newCols = insert(columns, insertAtIndex, { + title: i18nTitle, + id: newId, + formatter, + }); + const newRows = rows.map(row => ({ + [newId]: formatter.convert(row[id] / sumTotal / 100), + ...row, + })); + + return { cols: newCols, rows: newRows }; +} + +function insert(arr, index, ...items) { + const newArray = [...arr]; + newArray.splice(index + 1, 0, ...items); + return newArray; +} diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.html b/src/plugins/vis_type_table/public/agg_table/agg_table_group.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.html rename to src/plugins/vis_type_table/public/agg_table/agg_table_group.html diff --git a/src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.js b/src/plugins/vis_type_table/public/agg_table/agg_table_group.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/agg_table/agg_table_group.js rename to src/plugins/vis_type_table/public/agg_table/agg_table_group.js diff --git a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx rename to src/plugins/vis_type_table/public/components/table_vis_options.tsx index 265528f33f9cd..68348d5ef1060 100644 --- a/src/legacy/core_plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -24,12 +24,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { search } from '../../../../../plugins/data/public'; -import { - SwitchOption, - SelectOption, - NumberInputOption, -} from '../../../../../plugins/charts/public'; +import { search } from '../../../data/public'; +import { SwitchOption, SelectOption, NumberInputOption } from '../../../charts/public'; import { TableVisParams } from '../types'; import { totalAggregations } from './utils'; diff --git a/src/legacy/core_plugins/vis_type_table/public/components/utils.ts b/src/plugins/vis_type_table/public/components/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/components/utils.ts rename to src/plugins/vis_type_table/public/components/utils.ts diff --git a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts b/src/plugins/vis_type_table/public/get_inner_angular.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts rename to src/plugins/vis_type_table/public/get_inner_angular.ts index 6208e358b4184..e8404f918d609 100644 --- a/src/legacy/core_plugins/vis_type_table/public/get_inner_angular.ts +++ b/src/plugins/vis_type_table/public/get_inner_angular.ts @@ -21,9 +21,11 @@ // these are necessary to bootstrap the local angular. // They can stay even after NP cutover import angular from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import 'angular-recursion'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; -import { CoreStart, LegacyCoreStart, IUiSettingsClient } from 'kibana/public'; +import { CoreStart, IUiSettingsClient, PluginInitializerContext } from 'kibana/public'; import { initAngularBootstrap, PaginateDirectiveProvider, @@ -32,15 +34,15 @@ import { watchMultiDecorator, KbnAccessibleClickProvider, configureAppAngularModule, -} from '../../../../plugins/kibana_legacy/public'; +} from '../../kibana_legacy/public'; initAngularBootstrap(); const thirdPartyAngularDependencies = ['ngSanitize', 'ui.bootstrap', 'RecursionHelper']; -export function getAngularModule(name: string, core: CoreStart) { +export function getAngularModule(name: string, core: CoreStart, context: PluginInitializerContext) { const uiModule = getInnerAngular(name, core); - configureAppAngularModule(uiModule, core as LegacyCoreStart, true); + configureAppAngularModule(uiModule, { core, env: context.env }, true); return uiModule; } diff --git a/src/plugins/vis_type_table/public/index.scss b/src/plugins/vis_type_table/public/index.scss new file mode 100644 index 0000000000000..0972c85e0dbe0 --- /dev/null +++ b/src/plugins/vis_type_table/public/index.scss @@ -0,0 +1,10 @@ +// Prefix all styles with "tbv" to avoid conflicts. +// Examples +// tbvChart +// tbvChart__legend +// tbvChart__legend--small +// tbvChart__legend-isLoading + +@import './agg_table/index'; +@import './paginated_table/index'; +@import './table_vis'; diff --git a/src/plugins/vis_type_table/public/index.ts b/src/plugins/vis_type_table/public/index.ts new file mode 100644 index 0000000000000..5621fdb094772 --- /dev/null +++ b/src/plugins/vis_type_table/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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 './index.scss'; +import { PluginInitializerContext } from 'kibana/public'; +import { TableVisPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_table/public/paginated_table/_index.scss b/src/plugins/vis_type_table/public/paginated_table/_index.scss new file mode 100644 index 0000000000000..23d56c09b2818 --- /dev/null +++ b/src/plugins/vis_type_table/public/paginated_table/_index.scss @@ -0,0 +1 @@ +@import './_table_cell_filter'; diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss b/src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss rename to src/plugins/vis_type_table/public/paginated_table/_table_cell_filter.scss diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.html b/src/plugins/vis_type_table/public/paginated_table/paginated_table.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.html rename to src/plugins/vis_type_table/public/paginated_table/paginated_table.html diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.js b/src/plugins/vis_type_table/public/paginated_table/paginated_table.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.js rename to src/plugins/vis_type_table/public/paginated_table/paginated_table.js diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts b/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts rename to src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts index 7352236f03feb..23e4aee0378dc 100644 --- a/src/legacy/core_plugins/vis_type_table/public/paginated_table/paginated_table.test.ts +++ b/src/plugins/vis_type_table/public/paginated_table/paginated_table.test.ts @@ -25,10 +25,9 @@ import 'angular-mocks'; import { getAngularModule } from '../get_inner_angular'; import { initTableVisLegacyModule } from '../table_vis_legacy_module'; -import { coreMock } from '../../../../../core/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; -jest.mock('ui/new_platform'); -jest.mock('../../../../../plugins/kibana_legacy/public/angular/angular_config', () => ({ +jest.mock('../../../kibana_legacy/public/angular/angular_config', () => ({ configureAppAngularModule: () => {}, })); @@ -73,7 +72,11 @@ describe('Table Vis - Paginated table', () => { let paginatedTable: any; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', coreMock.createStart()); + const tableVisModule = getAngularModule( + 'kibana/table_vis', + coreMock.createStart(), + coreMock.createPluginInitializerContext() + ); initTableVisLegacyModule(tableVisModule); }; diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/rows.js b/src/plugins/vis_type_table/public/paginated_table/rows.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/rows.js rename to src/plugins/vis_type_table/public/paginated_table/rows.js diff --git a/src/legacy/core_plugins/vis_type_table/public/paginated_table/table_cell_filter.html b/src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/paginated_table/table_cell_filter.html rename to src/plugins/vis_type_table/public/paginated_table/table_cell_filter.html diff --git a/src/plugins/vis_type_table/public/plugin.ts b/src/plugins/vis_type_table/public/plugin.ts new file mode 100644 index 0000000000000..a41d939523bcc --- /dev/null +++ b/src/plugins/vis_type_table/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; + +import { createTableVisFn } from './table_vis_fn'; +import { getTableVisTypeDefinition } from './table_vis_type'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService } from './services'; + +/** @internal */ +export interface TablePluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; +} + +/** @internal */ +export interface TablePluginStartDependencies { + data: DataPublicPluginStart; +} + +/** @internal */ +export class TableVisPlugin implements Plugin, void> { + initializerContext: PluginInitializerContext; + createBaseVisualization: any; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public async setup( + core: CoreSetup, + { expressions, visualizations }: TablePluginSetupDependencies + ) { + expressions.registerFunction(createTableVisFn); + visualizations.createBaseVisualization( + getTableVisTypeDefinition(core, this.initializerContext) + ); + } + + public start(core: CoreStart, { data }: TablePluginStartDependencies) { + setFormatService(data.fieldFormats); + } +} diff --git a/src/plugins/vis_type_table/public/services.ts b/src/plugins/vis_type_table/public/services.ts new file mode 100644 index 0000000000000..3aaffe75e27f1 --- /dev/null +++ b/src/plugins/vis_type_table/public/services.ts @@ -0,0 +1,25 @@ +/* + * 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 { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('table data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis.html b/src/plugins/vis_type_table/public/table_vis.html similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis.html rename to src/plugins/vis_type_table/public/table_vis.html diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.js b/src/plugins/vis_type_table/public/table_vis_controller.js similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_controller.js rename to src/plugins/vis_type_table/public/table_vis_controller.js diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/table_vis_controller.test.ts similarity index 89% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts rename to src/plugins/vis_type_table/public/table_vis_controller.test.ts index 8d6f88bf8dd4a..4607324ca150c 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_controller.test.ts @@ -26,24 +26,23 @@ import $ from 'jquery'; import StubIndexPattern from 'test_utils/stub_index_pattern'; import { getAngularModule } from './get_inner_angular'; import { initTableVisLegacyModule } from './table_vis_legacy_module'; -import { tableVisTypeDefinition } from './table_vis_type'; -import { Vis } from '../../../../plugins/visualizations/public'; +import { getTableVisTypeDefinition } from './table_vis_type'; +import { Vis } from '../../visualizations/public'; // eslint-disable-next-line -import { stubFields } from '../../../../plugins/data/public/stubs'; +import { stubFields } from '../../data/public/stubs'; // eslint-disable-next-line import { tableVisResponseHandler } from './table_vis_response_handler'; -import { coreMock } from '../../../../core/public/mocks'; +import { coreMock } from '../../../core/public/mocks'; +import { IAggConfig, search } from '../../data/public'; +// TODO: remove linting disable // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { npStart } from './legacy_imports'; -import { IAggConfig, search } from '../../../../plugins/data/public'; +import { searchStartMock } from '../../data/public/search/mocks'; -// should be mocked once get rid of 'ui/new_platform' legacy imports -const { createAggConfigs } = npStart.plugins.data.search.aggs; +const { createAggConfigs } = searchStartMock.aggs; const { tabifyAggResponse } = search; -jest.mock('ui/new_platform'); -jest.mock('../../../../plugins/kibana_legacy/public/angular/angular_config', () => ({ +jest.mock('../../kibana_legacy/public/angular/angular_config', () => ({ configureAppAngularModule: () => {}, })); @@ -89,7 +88,11 @@ describe('Table Vis - Controller', () => { let stubIndexPattern: any; const initLocalAngular = () => { - const tableVisModule = getAngularModule('kibana/table_vis', coreMock.createStart()); + const tableVisModule = getAngularModule( + 'kibana/table_vis', + coreMock.createStart(), + coreMock.createPluginInitializerContext() + ); initTableVisLegacyModule(tableVisModule); }; @@ -110,9 +113,13 @@ describe('Table Vis - Controller', () => { (cfg: any) => cfg, 'time', stubFields, - coreMock.createStart() + coreMock.createSetup() ); }); + const tableVisTypeDefinition = getTableVisTypeDefinition( + coreMock.createSetup(), + coreMock.createPluginInitializerContext() + ); function getRangeVis(params?: object) { return ({ diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts b/src/plugins/vis_type_table/public/table_vis_fn.test.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts rename to src/plugins/vis_type_table/public/table_vis_fn.test.ts index 36392c10f93f3..9accf8950d910 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.test.ts @@ -21,7 +21,7 @@ import { createTableVisFn } from './table_vis_fn'; import { tableVisResponseHandler } from './table_vis_response_handler'; // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; jest.mock('./table_vis_response_handler', () => ({ tableVisResponseHandler: jest.fn().mockReturnValue({ diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts rename to src/plugins/vis_type_table/public/table_vis_fn.ts index a97e596e89754..9739a7a284e6c 100644 --- a/src/legacy/core_plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; export type Input = KibanaDatatable; diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_legacy_module.ts b/src/plugins/vis_type_table/public/table_vis_legacy_module.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_legacy_module.ts rename to src/plugins/vis_type_table/public/table_vis_legacy_module.ts diff --git a/src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts b/src/plugins/vis_type_table/public/table_vis_response_handler.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_table/public/table_vis_response_handler.ts rename to src/plugins/vis_type_table/public/table_vis_response_handler.ts diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts new file mode 100644 index 0000000000000..26e5ac8cfd71a --- /dev/null +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -0,0 +1,100 @@ +/* + * 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 { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; +import { Vis } from '../../visualizations/public'; +import { tableVisResponseHandler } from './table_vis_response_handler'; +// @ts-ignore +import tableVisTemplate from './table_vis.html'; +import { TableOptions } from './components/table_vis_options'; +import { getTableVisualizationControllerClass } from './vis_controller'; + +export function getTableVisTypeDefinition(core: CoreSetup, context: PluginInitializerContext) { + return { + type: 'table', + name: 'table', + title: i18n.translate('visTypeTable.tableVisTitle', { + defaultMessage: 'Data Table', + }), + icon: 'visTable', + description: i18n.translate('visTypeTable.tableVisDescription', { + defaultMessage: 'Display values in a table', + }), + visualization: getTableVisualizationControllerClass(core, context), + visConfig: { + defaults: { + perPage: 10, + showPartialRows: false, + showMetricsAtAllLevels: false, + sort: { + columnIndex: null, + direction: null, + }, + showTotal: false, + totalFunc: 'sum', + percentageCol: '', + }, + template: tableVisTemplate, + }, + editorConfig: { + optionsTemplate: TableOptions, + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + aggFilter: ['!geo_centroid', '!geo_bounds'], + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + min: 1, + defaults: [{ type: 'count', schema: 'metric' }], + }, + { + group: AggGroupNames.Buckets, + name: 'bucket', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.bucketTitle', { + defaultMessage: 'Split rows', + }), + aggFilter: ['!filter'], + }, + { + group: AggGroupNames.Buckets, + name: 'split', + title: i18n.translate('visTypeTable.tableVisEditorConfig.schemas.splitTitle', { + defaultMessage: 'Split table', + }), + min: 0, + max: 1, + aggFilter: ['!filter'], + }, + ]), + }, + responseHandler: tableVisResponseHandler, + hierarchicalData: (vis: Vis) => { + return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); + }, + }; +} diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts new file mode 100644 index 0000000000000..39023d1305cb6 --- /dev/null +++ b/src/plugins/vis_type_table/public/types.ts @@ -0,0 +1,48 @@ +/* + * 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 { SchemaConfig } from '../../visualizations/public'; + +export enum AggTypes { + SUM = 'sum', + AVG = 'avg', + MIN = 'min', + MAX = 'max', + COUNT = 'count', +} + +export interface Dimensions { + buckets: SchemaConfig[]; + metrics: SchemaConfig[]; +} + +export interface TableVisParams { + type: 'table'; + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; + sort: { + columnIndex: number | null; + direction: string | null; + }; + showTotal: boolean; + totalFunc: AggTypes; + percentageCol: string; + dimensions: Dimensions; +} diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/vis_controller.ts new file mode 100644 index 0000000000000..d49dd32c8c89c --- /dev/null +++ b/src/plugins/vis_type_table/public/vis_controller.ts @@ -0,0 +1,109 @@ +/* + * 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 { CoreSetup, PluginInitializerContext } from 'kibana/public'; +import angular, { IModule, auto, IRootScopeService, IScope, ICompileService } from 'angular'; +import $ from 'jquery'; + +import { VisParams, ExprVis } from '../../visualizations/public'; +import { getAngularModule } from './get_inner_angular'; +import { initTableVisLegacyModule } from './table_vis_legacy_module'; + +const innerAngularName = 'kibana/table_vis'; + +export function getTableVisualizationControllerClass( + core: CoreSetup, + context: PluginInitializerContext +) { + return class TableVisualizationController { + private tableVisModule: IModule | undefined; + private injector: auto.IInjectorService | undefined; + el: JQuery; + vis: ExprVis; + $rootScope: IRootScopeService | null = null; + $scope: (IScope & { [key: string]: any }) | undefined; + $compile: ICompileService | undefined; + + constructor(domeElement: Element, vis: ExprVis) { + this.el = $(domeElement); + this.vis = vis; + } + + getInjector() { + if (!this.injector) { + const mountpoint = document.createElement('div'); + mountpoint.setAttribute('style', 'height: 100%; width: 100%;'); + this.injector = angular.bootstrap(mountpoint, [innerAngularName]); + this.el.append(mountpoint); + } + + return this.injector; + } + + async initLocalAngular() { + if (!this.tableVisModule) { + const [coreStart] = await core.getStartServices(); + this.tableVisModule = getAngularModule(innerAngularName, coreStart, context); + initTableVisLegacyModule(this.tableVisModule); + } + } + + async render(esResponse: object, visParams: VisParams) { + await this.initLocalAngular(); + + return new Promise(async (resolve, reject) => { + if (!this.$rootScope) { + const $injector = this.getInjector(); + this.$rootScope = $injector.get('$rootScope'); + this.$compile = $injector.get('$compile'); + } + const updateScope = () => { + if (!this.$scope) { + return; + } + this.$scope.vis = this.vis; + this.$scope.visState = { params: visParams }; + this.$scope.esResponse = esResponse; + + this.$scope.visParams = visParams; + this.$scope.renderComplete = resolve; + this.$scope.renderFailed = reject; + this.$scope.resize = Date.now(); + this.$scope.$apply(); + }; + + if (!this.$scope && this.$compile) { + this.$scope = this.$rootScope.$new(); + this.$scope.uiState = this.vis.getUiState(); + updateScope(); + this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); + this.$scope.$apply(); + } else { + updateScope(); + } + }); + } + + destroy() { + if (this.$rootScope) { + this.$rootScope.$destroy(); + this.$rootScope = null; + } + } + }; +} diff --git a/src/plugins/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts new file mode 100644 index 0000000000000..882958a28777d --- /dev/null +++ b/src/plugins/vis_type_table/server/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('table_vis.enabled', 'vis_type_table.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/vis_type_tagcloud/config.ts b/src/plugins/vis_type_tagcloud/config.ts new file mode 100644 index 0000000000000..6749bd83de39f --- /dev/null +++ b/src/plugins/vis_type_tagcloud/config.ts @@ -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. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_type_tagcloud/kibana.json new file mode 100644 index 0000000000000..dbc9a1b9ef692 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "visTypeTagcloud", + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["data", "expressions", "visualizations", "charts"] +} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap rename to src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/_tag_cloud.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss rename to src/plugins/vis_type_tagcloud/public/_tag_cloud.scss diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js rename to src/plugins/vis_type_tagcloud/public/components/feedback_message.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js rename to src/plugins/vis_type_tagcloud/public/components/label.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx similarity index 91% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index 7a64549edd747..d33576e4e5529 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,9 +20,9 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; -import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public'; +import { VisOptionsProps } from '../../../vis_default_editor/public'; +import { ValidatedDualRange } from '../../../kibana_react/public'; +import { SelectOption, SwitchOption } from '../../../charts/public'; import { TagCloudVisParams } from '../types'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js diff --git a/src/plugins/vis_type_tagcloud/public/index.scss b/src/plugins/vis_type_tagcloud/public/index.scss new file mode 100644 index 0000000000000..e6893b9a2474c --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/index.scss @@ -0,0 +1,8 @@ +// Prefix all styles with "tgc" to avoid conflicts. +// Examples +// tgcChart +// tgcChart__legend +// tgcChart__legend--small +// tgcChart__legend-isLoading + +@import './tag_cloud'; diff --git a/src/plugins/vis_type_tagcloud/public/index.ts b/src/plugins/vis_type_tagcloud/public/index.ts new file mode 100644 index 0000000000000..ff27d96b710fa --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/index.ts @@ -0,0 +1,25 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/public'; +import { TagCloudPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_tagcloud/public/plugin.ts b/src/plugins/vis_type_tagcloud/public/plugin.ts new file mode 100644 index 0000000000000..6978186058b1d --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/plugin.ts @@ -0,0 +1,74 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../charts/public'; + +import { createTagCloudFn } from './tag_cloud_fn'; +import { createTagCloudVisTypeDefinition } from './tag_cloud_type'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService } from './services'; +import { ConfigSchema } from '../config'; + +import './index.scss'; + +/** @internal */ +export interface TagCloudPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface TagCloudVisDependencies { + colors: ChartsPluginSetup['colors']; +} + +/** @internal */ +export interface TagCloudVisPluginStartDependencies { + data: DataPublicPluginStart; +} + +/** @internal */ +export class TagCloudPlugin implements Plugin { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup( + core: CoreSetup, + { expressions, visualizations, charts }: TagCloudPluginSetupDependencies + ) { + const visualizationDependencies: TagCloudVisDependencies = { + colors: charts.colors, + }; + expressions.registerFunction(createTagCloudFn); + visualizations.createBaseVisualization( + createTagCloudVisTypeDefinition(visualizationDependencies) + ); + } + + public start(core: CoreStart, { data }: TagCloudVisPluginStartDependencies) { + setFormatService(data.fieldFormats); + } +} diff --git a/src/plugins/vis_type_tagcloud/public/services.ts b/src/plugins/vis_type_tagcloud/public/services.ts new file mode 100644 index 0000000000000..f6002afc66493 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/public/services.ts @@ -0,0 +1,25 @@ +/* + * 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 { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('data.fieldFormats'); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts similarity index 91% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts index 65c54766133d1..eb16b0855a138 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts @@ -19,8 +19,7 @@ import { createTagCloudFn } from './tag_cloud_fn'; -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#tagcloud', () => { const fn = functionWrapper(createTagCloudFn()); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts index 31c7fd118cefd..05cf05ab00b75 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; import { TagCloudVisParams } from './types'; const name = 'tagcloud'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index b7dfa62c93fb9..5a8cc3004a315 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { Schemas } from '../../vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/types.ts b/src/plugins/vis_type_tagcloud/public/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/types.ts rename to src/plugins/vis_type_tagcloud/public/types.ts diff --git a/src/plugins/vis_type_tagcloud/server/index.ts b/src/plugins/vis_type_tagcloud/server/index.ts new file mode 100644 index 0000000000000..bd9656b29c524 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/server/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json index 38662c6a7ff89..9053d2543e0d0 100644 --- a/src/plugins/vis_type_timeseries/kibana.json +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -4,6 +4,6 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "visualizations"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations"], "optionalPlugins": ["usageCollection"] } diff --git a/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap index d269f61beefab..dd454231a9ac2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/icon_select/__snapshots__/icon_select.test.js.snap @@ -85,6 +85,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/icon_select/icon_sele "asPlainText": true, } } + sortMatchesBy="none" /> `; diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index 78436720dd66c..3bf8b77621cf5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -223,6 +223,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js "asPlainText": true, } } + sortMatchesBy="none" /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 7075e86eb56bf..9fdb8ccc919b7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -240,3 +240,7 @@ VisEditor.propTypes = { timeRange: PropTypes.object, appState: PropTypes.object, }; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { VisEditor as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx new file mode 100644 index 0000000000000..d81bd95d8d771 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx @@ -0,0 +1,30 @@ +/* + * 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 React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +// @ts-ignore +const VisEditorComponent = lazy(() => import('./vis_editor')); + +export const VisEditor = (props: any) => ( + }> + + +); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js index 024f59c3abb1c..d29b795b10ec8 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js @@ -53,7 +53,7 @@ export const TimeseriesConfig = injectI18n(function(props) { point_size: '', value_template: '{{value}}', offset_time: '', - split_color_mode: 'gradient', + split_color_mode: 'kibana', axis_min: '', axis_max: '', stacked: STACKED_OPTIONS.NONE, @@ -140,10 +140,10 @@ export const TimeseriesConfig = injectI18n(function(props) { const splitColorOptions = [ { label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.gradientLabel', - defaultMessage: 'Gradient', + id: 'visTypeTimeseries.timeSeries.defaultPaletteLabel', + defaultMessage: 'Default palette', }), - value: 'gradient', + value: 'kibana', }, { label: intl.formatMessage({ @@ -152,6 +152,13 @@ export const TimeseriesConfig = injectI18n(function(props) { }), value: 'rainbow', }, + { + label: intl.formatMessage({ + id: 'visTypeTimeseries.timeSeries.gradientLabel', + defaultMessage: 'Gradient', + }), + value: 'gradient', + }, ]; const selectedSplitColorOption = splitColorOptions.find(option => { return model.split_color_mode === option.value; diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js index af50d3a06d1fc..f21b5f947bca7 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -21,7 +21,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { fetchIndexPatternFields } from './lib/fetch_fields'; import { getSavedObjectsClient, getUISettings, getI18n } from '../services'; -import { VisEditor } from './components/vis_editor'; +import { VisEditor } from './components/vis_editor_lazy'; export class EditorController { constructor(el, vis, eventEmitter, embeddableHandler) { diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index b1c3c7ac6b67a..5cf1619150e5c 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -33,7 +33,7 @@ import { import { EuiIcon } from '@elastic/eui'; import { getTimezone } from '../../../lib/get_timezone'; import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor'; -import { getUISettings } from '../../../../services'; +import { getUISettings, getChartsSetup } from '../../../../services'; import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants'; import { AreaSeriesDecorator } from './decorators/area_decorator'; import { BarSeriesDecorator } from './decorators/bar_decorator'; @@ -94,6 +94,12 @@ export const TimeSeries = ({ // apply legend style change if bgColor is configured const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor)); + // If the color isn't configured by the user, use the color mapping service + // to assign a color from the Kibana palette. Colors will be shared across the + // session, including dashboards. + const { colors } = getChartsSetup(); + colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label)); + return ( ; visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; } /** @internal */ @@ -56,10 +59,11 @@ export class MetricsPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations }: MetricsPluginSetupDependencies + { expressions, visualizations, charts }: MetricsPluginSetupDependencies ) { expressions.registerFunction(createMetricsFn); setUISettings(core.uiSettings); + setChartsSetup(charts); visualizations.createReactVisualization(metricsVisDefinition); } diff --git a/src/plugins/vis_type_timeseries/public/services.ts b/src/plugins/vis_type_timeseries/public/services.ts index d93a376584eac..9aa84478fb78b 100644 --- a/src/plugins/vis_type_timeseries/public/services.ts +++ b/src/plugins/vis_type_timeseries/public/services.ts @@ -19,6 +19,7 @@ import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public'; import { createGetterSetter } from '../../kibana_utils/public'; +import { ChartsPluginSetup } from '../../charts/public'; import { DataPublicPluginStart } from '../../data/public'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -36,3 +37,7 @@ export const [getCoreStart, setCoreStart] = createGetterSetter('CoreS export const [getDataStart, setDataStart] = createGetterSetter('DataStart'); export const [getI18n, setI18n] = createGetterSetter('I18n'); + +export const [getChartsSetup, setChartsSetup] = createGetterSetter( + 'ChartsPluginSetup' +); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js index ff8d9077b0871..cad8c8f2025a1 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js @@ -19,7 +19,7 @@ import Color from 'color'; -export function getSplitColors(inputColor, size = 10, style = 'gradient') { +export function getSplitColors(inputColor, size = 10, style = 'kibana') { const color = new Color(inputColor); const colors = []; let workingColor = Color.hsl(color.hsl().object()); @@ -49,7 +49,7 @@ export function getSplitColors(inputColor, size = 10, style = 'gradient') { '#0F1419', '#666666', ]; - } else { + } else if (style === 'gradient') { colors.push(color.string()); const rotateBy = color.luminosity() / (size - 1); for (let i = 0; i < size - 1; i++) { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js index 0874d944033f5..376d32d0da13f 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js @@ -106,7 +106,7 @@ describe('getSplits(resp, panel, series)', () => { ]); }); - test('should return a splits for terms group bys', () => { + describe('terms group bys', () => { const resp = { aggregations: { SERIES: { @@ -126,38 +126,89 @@ describe('getSplits(resp, panel, series)', () => { }, }, }; - const series = { - id: 'SERIES', - color: '#F00', - split_mode: 'terms', - terms_field: 'beat.hostname', - terms_size: 10, - metrics: [ - { id: 'AVG', type: 'avg', field: 'cpu' }, - { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, - ], - }; - const panel = { type: 'timeseries' }; - expect(getSplits(resp, panel, series)).toEqual([ - { - id: 'SERIES:example-01', - key: 'example-01', - label: 'example-01', - meta: { bucketSize: 10 }, - color: 'rgb(255, 0, 0)', - timeseries: { buckets: [] }, - SIBAGG: { value: 1 }, - }, - { - id: 'SERIES:example-02', - key: 'example-02', - label: 'example-02', - meta: { bucketSize: 10 }, - color: 'rgb(147, 0, 0)', - timeseries: { buckets: [] }, - SIBAGG: { value: 2 }, - }, - ]); + + test('should return a splits with no color', () => { + const series = { + id: 'SERIES', + color: '#F00', + split_mode: 'terms', + terms_field: 'beat.hostname', + terms_size: 10, + metrics: [ + { id: 'AVG', type: 'avg', field: 'cpu' }, + { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, + ], + }; + const panel = { type: 'timeseries' }; + expect(getSplits(resp, panel, series)).toEqual([ + { + id: 'SERIES:example-01', + key: 'example-01', + label: 'example-01', + meta: { bucketSize: 10 }, + color: undefined, + timeseries: { buckets: [] }, + SIBAGG: { value: 1 }, + }, + { + id: 'SERIES:example-02', + key: 'example-02', + label: 'example-02', + meta: { bucketSize: 10 }, + color: undefined, + timeseries: { buckets: [] }, + SIBAGG: { value: 2 }, + }, + ]); + }); + + test('should return gradient color', () => { + const series = { + id: 'SERIES', + color: '#F00', + split_mode: 'terms', + split_color_mode: 'gradient', + terms_field: 'beat.hostname', + terms_size: 10, + metrics: [ + { id: 'AVG', type: 'avg', field: 'cpu' }, + { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, + ], + }; + const panel = { type: 'timeseries' }; + expect(getSplits(resp, panel, series)).toEqual([ + expect.objectContaining({ + color: 'rgb(255, 0, 0)', + }), + expect.objectContaining({ + color: 'rgb(147, 0, 0)', + }), + ]); + }); + + test('should return rainbow color', () => { + const series = { + id: 'SERIES', + color: '#F00', + split_mode: 'terms', + split_color_mode: 'rainbow', + terms_field: 'beat.hostname', + terms_size: 10, + metrics: [ + { id: 'AVG', type: 'avg', field: 'cpu' }, + { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, + ], + }; + const panel = { type: 'timeseries' }; + expect(getSplits(resp, panel, series)).toEqual([ + expect.objectContaining({ + color: '#68BC00', + }), + expect.objectContaining({ + color: '#009CE0', + }), + ]); + }); }); test('should return a splits for filters group bys', () => { diff --git a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts index 3127e03ada0ef..fa4427fbb8c12 100644 --- a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts +++ b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts @@ -36,6 +36,7 @@ const queryObject = Joi.object({ language: Joi.string().allow(''), query: Joi.string().allow(''), }); +const stringOrNumberOptionalNullable = Joi.alternatives([stringOptionalNullable, numberOptional]); const numberOptionalOrEmptyString = Joi.alternatives(numberOptional, Joi.string().valid('')); const annotationsItems = Joi.object({ @@ -78,7 +79,7 @@ const metricsItems = Joi.object({ unit: stringOptionalNullable, model_type: stringOptionalNullable, mode: stringOptionalNullable, - lag: numberOptional, + lag: numberOptionalOrEmptyString, alpha: numberOptional, beta: numberOptional, gamma: numberOptional, @@ -130,8 +131,8 @@ const seriesItems = Joi.object({ aggregate_by: stringOptionalNullable, aggregate_function: stringOptionalNullable, axis_position: stringRequired, - axis_max: stringOptionalNullable, - axis_min: stringOptionalNullable, + axis_max: stringOrNumberOptionalNullable, + axis_min: stringOrNumberOptionalNullable, chart_type: stringRequired, color: stringRequired, color_rules: Joi.array() @@ -198,8 +199,8 @@ export const visPayloadSchema = Joi.object({ axis_formatter: stringRequired, axis_position: stringRequired, axis_scale: stringRequired, - axis_min: stringOptionalNullable, - axis_max: stringOptionalNullable, + axis_min: stringOrNumberOptionalNullable, + axis_max: stringOrNumberOptionalNullable, bar_color_rules: arrayNullable.optional(), background_color: stringOptionalNullable, background_color_rules: Joi.array() @@ -221,9 +222,9 @@ export const visPayloadSchema = Joi.object({ .optional(), gauge_width: [stringOptionalNullable, numberOptional], gauge_inner_color: stringOptionalNullable, - gauge_inner_width: Joi.alternatives(stringOptionalNullable, numberIntegerOptional), + gauge_inner_width: stringOrNumberOptionalNullable, gauge_style: stringOptionalNullable, - gauge_max: stringOptionalNullable, + gauge_max: stringOrNumberOptionalNullable, id: stringRequired, ignore_global_filters: numberOptional, ignore_global_filter: numberOptional, diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/index.ts b/src/plugins/vis_type_timeseries/server/saved_objects/index.ts new file mode 100644 index 0000000000000..5f7f5767f423d --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/saved_objects/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { tsvbTelemetrySavedObjectType } from './tsvb_telemetry'; 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 new file mode 100644 index 0000000000000..1e5508b44ee0e --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts @@ -0,0 +1,46 @@ +/* + * 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 { flow } from 'lodash'; +import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server'; + +const resetCount: SavedObjectMigrationFn = doc => ({ + ...doc, + attributes: { + ...doc.attributes, + failedRequests: 0, + }, +}); + +export const tsvbTelemetrySavedObjectType: SavedObjectsType = { + name: 'tsvb-validation-telemetry', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + failedRequests: { + type: 'long', + }, + }, + }, + migrations: { + '7.7.0': flow(resetCount), + '7.8.0': flow(resetCount), + }, +}; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts deleted file mode 100644 index f18fa1e4cc2fa..0000000000000 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 { flow } from 'lodash'; -import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server'; - -const resetCount: SavedObjectMigrationFn = doc => ({ - ...doc, - attributes: { - ...doc.attributes, - failedRequests: 0, - }, -}); - -export const tsvbTelemetrySavedObjectType: SavedObjectsType = { - name: 'tsvb-validation-telemetry', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - failedRequests: { - type: 'long', - }, - }, - }, - migrations: { - '7.7.0': flow(resetCount), - }, -}; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index 779d9441df2fd..e4b8ca19094e4 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -19,7 +19,7 @@ import { APICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../usage_collection/server'; -import { tsvbTelemetrySavedObjectType } from './saved_object_type'; +import { tsvbTelemetrySavedObjectType } from '../saved_objects'; export interface ValidationTelemetryServiceSetup { logFailedValidation: () => void; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index 6bfd6c9536df4..f1f82e7f5b7ad 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -2,5 +2,6 @@ "id": "visTypeVega", "version": "kibana", "server": true, - "ui": true + "ui": true, + "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"] } diff --git a/src/plugins/vis_type_vega/public/__mocks__/services.ts b/src/plugins/vis_type_vega/public/__mocks__/services.ts new file mode 100644 index 0000000000000..1bf051232e4c9 --- /dev/null +++ b/src/plugins/vis_type_vega/public/__mocks__/services.ts @@ -0,0 +1,55 @@ +/* + * 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 { createGetterSetter } from '../../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; + +export const [getData, setData] = createGetterSetter('Data'); +setData(dataPluginMock.createStartContract()); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); +setNotifications(coreMock.createStart().notifications); + +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); +setUISettings(coreMock.createStart().uiSettings); + +export const [getSavedObjects, setSavedObjects] = createGetterSetter( + 'SavedObjects' +); +setSavedObjects(coreMock.createStart().savedObjects); + +export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ + esShardTimeout: number; + enableExternalUrls: boolean; + emsTileLayerId: unknown; +}>('InjectedVars'); +setInjectedVars({ + emsTileLayerId: {}, + enableExternalUrls: true, + esShardTimeout: 10000, +}); + +export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; +export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; +export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss b/src/plugins/vis_type_vega/public/_vega_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss rename to src/plugins/vis_type_vega/public/_vega_editor.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss rename to src/plugins/vis_type_vega/public/_vega_vis.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/index.ts b/src/plugins/vis_type_vega/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/index.ts rename to src/plugins/vis_type_vega/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_help_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx rename to src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js rename to src/plugins/vis_type_vega/public/data_model/ems_file_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js b/src/plugins/vis_type_vega/public/data_model/search_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js b/src/plugins/vis_type_vega/public/data_model/search_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js b/src/plugins/vis_type_vega/public/data_model/time_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js b/src/plugins/vis_type_vega/public/data_model/time_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js b/src/plugins/vis_type_vega/public/data_model/url_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js rename to src/plugins/vis_type_vega/public/data_model/url_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js b/src/plugins/vis_type_vega/public/data_model/utils.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js rename to src/plugins/vis_type_vega/public/data_model/utils.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson rename to src/plugins/vis_type_vega/public/default.spec.hjson diff --git a/src/plugins/vis_type_vega/public/index.scss b/src/plugins/vis_type_vega/public/index.scss new file mode 100644 index 0000000000000..78d9eb61999f7 --- /dev/null +++ b/src/plugins/vis_type_vega/public/index.scss @@ -0,0 +1,9 @@ +// Prefix all styles with "vga" to avoid conflicts. +// Examples +// vgaChart +// vgaChart__legend +// vgaChart__legend--small +// vgaChart__legend-isLoading + +@import './vega_vis'; +@import './vega_editor'; diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_type_vega/public/index.ts index 71f3474f8217e..78878d38e1674 100644 --- a/src/plugins/vis_type_vega/public/index.ts +++ b/src/plugins/vis_type_vega/public/index.ts @@ -19,20 +19,8 @@ import { PluginInitializerContext } from 'kibana/public'; import { ConfigSchema } from '../config'; +import { VegaPlugin as Plugin } from './plugin'; -export const plugin = (initializerContext: PluginInitializerContext) => ({ - setup() { - return { - /** - * The configuration is temporarily exposed to allow the legacy vega plugin to consume - * the setting. Once the vega plugin is migrated completely, this will become an implementation - * detail. - * @deprecated - */ - config: initializerContext.config.get(), - }; - }, - start() {}, -}); - -export type VisTypeVegaSetup = ReturnType['setup']>; +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts new file mode 100644 index 0000000000000..b52dcfbd914f9 --- /dev/null +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -0,0 +1,98 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { Plugin as DataPublicPlugin } from '../../data/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { + setNotifications, + setData, + setSavedObjects, + setInjectedVars, + setUISettings, + setKibanaMapFactory, +} from './services'; + +import { createVegaFn } from './vega_fn'; +import { createVegaTypeDefinition } from './vega_type'; +import { getKibanaMapFactoryProvider, IServiceSettings } from '../../maps_legacy/public'; +import './index.scss'; +import { ConfigSchema } from '../config'; + +/** @internal */ +export interface VegaVisualizationDependencies { + core: CoreSetup; + plugins: { + data: ReturnType; + }; + serviceSettings: IServiceSettings; +} + +/** @internal */ +export interface VegaPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + data: ReturnType; + mapsLegacy: any; +} + +/** @internal */ +export interface VegaPluginStartDependencies { + data: ReturnType; +} + +/** @internal */ +export class VegaPlugin implements Plugin, void> { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public async setup( + core: CoreSetup, + { data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies + ) { + setInjectedVars({ + enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, + esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, + emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), + }); + setUISettings(core.uiSettings); + setKibanaMapFactory(getKibanaMapFactoryProvider(core)); + + const visualizationDependencies: Readonly = { + core, + plugins: { + data, + }, + serviceSettings: mapsLegacy.serviceSettings, + }; + + expressions.registerFunction(() => createVegaFn(visualizationDependencies)); + + visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); + } + + public start(core: CoreStart, { data }: VegaPluginStartDependencies) { + setNotifications(core.notifications); + setSavedObjects(core.savedObjects); + setData(data); + } +} diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts new file mode 100644 index 0000000000000..f81f87d7ad2e1 --- /dev/null +++ b/src/plugins/vis_type_vega/public/services.ts @@ -0,0 +1,48 @@ +/* + * 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 { SavedObjectsStart } from 'kibana/public'; +import { NotificationsStart, IUiSettingsClient } from 'src/core/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { createGetterSetter } from '../../kibana_utils/public'; + +export const [getData, setData] = createGetterSetter('Data'); + +export const [getNotifications, setNotifications] = createGetterSetter( + 'Notifications' +); +export const [getKibanaMapFactory, setKibanaMapFactory] = createGetterSetter( + 'KibanaMapFactory' +); + +export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); + +export const [getSavedObjects, setSavedObjects] = createGetterSetter( + 'SavedObjects' +); + +export const [getInjectedVars, setInjectedVars] = createGetterSetter<{ + esShardTimeout: number; + enableExternalUrls: boolean; + emsTileLayerId: unknown; +}>('InjectedVars'); + +export const getEsShardTimeout = () => getInjectedVars().esShardTimeout; +export const getEnableExternalUrls = () => getInjectedVars().enableExternalUrls; +export const getEmsTileLayerId = () => getInjectedVars().emsTileLayerId; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts rename to src/plugins/vis_type_vega/public/vega_fn.ts index 2a0da81a31a96..6d45e043f7cee 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -19,11 +19,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts similarity index 75% rename from src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts rename to src/plugins/vis_type_vega/public/vega_request_handler.ts index f63efc0007c3b..efc02e368efa8 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -17,12 +17,8 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getSearchService } from '../../../../plugins/data/public/services'; -import { Filter, esQuery, TimeRange, Query } from '../../../../plugins/data/public'; +import { Filter, esQuery, TimeRange, Query } from '../../data/public'; -// @ts-ignore -import { VegaParser } from './data_model/vega_parser'; // @ts-ignore import { SearchCache } from './data_model/search_cache'; // @ts-ignore @@ -30,6 +26,7 @@ import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; import { VisParams } from './vega_fn'; +import { getData } from './services'; interface VegaRequestHandlerParams { query: Query; @@ -43,18 +40,31 @@ export function createVegaRequestHandler({ core: { uiSettings }, serviceSettings, }: VegaVisualizationDependencies) { - const { esClient } = getSearchService().__LEGACY; - const searchCache = new SearchCache(esClient, { max: 10, maxAge: 4 * 1000 }); + let searchCache: SearchCache | undefined; const { timefilter } = data.query.timefilter; const timeCache = new TimeCache(timefilter, 3 * 1000); - return ({ timeRange, filters, query, visParams }: VegaRequestHandlerParams) => { + return async function vegaRequestHandler({ + timeRange, + filters, + query, + visParams, + }: VegaRequestHandlerParams) { + if (!searchCache) { + searchCache = new SearchCache(getData().search.__LEGACY.esClient, { + max: 10, + maxAge: 4 * 1000, + }); + } + timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); + // @ts-ignore + const { VegaParser } = await import('./data_model/vega_parser'); const vp = new VegaParser(visParams.spec, searchCache, timeCache, filtersDsl, serviceSettings); - return vp.parseAsync(); + return await vp.parseAsync(); }; } diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts similarity index 92% rename from src/legacy/core_plugins/vis_type_vega/public/vega_type.ts rename to src/plugins/vis_type_vega/public/vega_type.ts index f56d7682efc6f..c864553c118b9 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,10 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js similarity index 99% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index c90f059ff7c94..be98d2b69ad87 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -26,7 +26,7 @@ import { Utils } from '../data_model/utils'; import { VISUALIZATION_COLORS } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; -import { esFilters } from '../../../../../plugins/data/public'; +import { esFilters } from '../../../data/public'; import { getEnableExternalUrls } from '../services'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js index d43eb9c3351ea..8e4009eab8488 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -19,7 +19,7 @@ import L from 'leaflet'; import 'leaflet-vega'; -import { KibanaMapLayer } from '../../../../../plugins/maps_legacy/public'; +import { KibanaMapLayer } from '../../../maps_legacy/public'; export class VegaMapLayer extends KibanaMapLayer { constructor(spec, options) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js similarity index 90% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 03aef29dc5739..895d496a896aa 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -21,13 +21,11 @@ import * as vega from 'vega-lib'; import { i18n } from '@kbn/i18n'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; -import { getEmsTileLayerId, getUISettings } from '../services'; +import { getEmsTileLayerId, getUISettings, getKibanaMapFactory } from '../services'; export class VegaMapView extends VegaBaseView { - constructor(opts, services) { + constructor(opts) { super(opts); - this.services = services; } async _initViewCustomizations() { @@ -107,18 +105,14 @@ export class VegaMapView extends VegaBaseView { // maxBounds = L.latLngBounds(L.latLng(b[1], b[0]), L.latLng(b[3], b[2])); // } - this._kibanaMap = new KibanaMap( - this._$container.get(0), - { - zoom, - minZoom, - maxZoom, - center: [mapConfig.latitude, mapConfig.longitude], - zoomControl: mapConfig.zoomControl, - scrollWheelZoom: mapConfig.scrollWheelZoom, - }, - this.services - ); + this._kibanaMap = getKibanaMapFactory()(this._$container.get(0), { + zoom, + minZoom, + maxZoom, + center: [mapConfig.latitude, mapConfig.longitude], + zoomControl: mapConfig.zoomControl, + scrollWheelZoom: mapConfig.scrollWheelZoom, + }); if (baseMapOpts) { this._kibanaMap.setBaseLayer({ diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js rename to src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_view.js diff --git a/src/plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js new file mode 100644 index 0000000000000..1fcb89f04457d --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_visualization.js @@ -0,0 +1,131 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { getNotifications, getData, getSavedObjects } from './services'; + +export const createVegaVisualization = ({ serviceSettings }) => + class VegaVisualization { + constructor(el, vis) { + this._el = el; + this._vis = vis; + + this.savedObjectsClient = getSavedObjects(); + this.dataPlugin = getData(); + } + + /** + * Find index pattern by its title, of if not given, gets default + * @param {string} [index] + * @returns {Promise} index id + */ + async findIndex(index) { + const { indexPatterns } = this.dataPlugin; + let idxObj; + + if (index) { + idxObj = indexPatterns.findByTitle(this.savedObjectsClient, index); + if (!idxObj) { + throw new Error( + i18n.translate('visTypeVega.visualization.indexNotFoundErrorMessage', { + defaultMessage: 'Index {index} not found', + values: { index: `"${index}"` }, + }) + ); + } + } else { + idxObj = await indexPatterns.getDefault(); + if (!idxObj) { + throw new Error( + i18n.translate('visTypeVega.visualization.unableToFindDefaultIndexErrorMessage', { + defaultMessage: 'Unable to find default index', + }) + ); + } + } + return idxObj.id; + } + + /** + * + * @param {VegaParser} visData + * @param {*} status + * @returns {Promise} + */ + async render(visData) { + const { toasts } = getNotifications(); + + if (!visData && !this._vegaView) { + toasts.addWarning( + i18n.translate('visTypeVega.visualization.unableToRenderWithoutDataWarningMessage', { + defaultMessage: 'Unable to render without data', + }) + ); + return; + } + + try { + await this._render(visData); + } catch (error) { + if (this._vegaView) { + this._vegaView.onError(error); + } else { + toasts.addError(error, { + title: i18n.translate('visTypeVega.visualization.renderErrorTitle', { + defaultMessage: 'Vega error', + }), + }); + } + } + } + + async _render(vegaParser) { + if (vegaParser) { + // New data received, rebuild the graph + if (this._vegaView) { + await this._vegaView.destroy(); + this._vegaView = null; + } + + const { filterManager } = this.dataPlugin.query; + const { timefilter } = this.dataPlugin.query.timefilter; + const vegaViewParams = { + parentEl: this._el, + vegaParser, + serviceSettings, + filterManager, + timefilter, + findIndex: this.findIndex.bind(this), + }; + + if (vegaParser.useMap) { + const services = { toastService: getNotifications().toasts }; + const { VegaMapView } = await import('./vega_view/vega_map_view'); + this._vegaView = new VegaMapView(vegaViewParams, services); + } else { + const { VegaView } = await import('./vega_view/vega_view'); + this._vegaView = new VegaView(vegaViewParams); + } + await this._vegaView.init(); + } + } + + destroy() { + return this._vegaView && this._vegaView.destroy(); + } + }; diff --git a/src/plugins/vis_type_vislib/kibana.json b/src/plugins/vis_type_vislib/kibana.json new file mode 100644 index 0000000000000..5b3088b399ebf --- /dev/null +++ b/src/plugins/vis_type_vislib/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "visTypeVislib", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["charts", "data", "expressions", "visualizations"], + "optionalPlugins": ["visTypeXy"] +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap rename to src/plugins/vis_type_vislib/public/__snapshots__/pie_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/area.ts b/src/plugins/vis_type_vislib/public/area.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/area.ts rename to src/plugins/vis_type_vislib/public/area.ts index 8a196da64fc4b..c42962ad50a4b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/area.ts +++ b/src/plugins/vis_type_vislib/public/area.ts @@ -23,8 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -39,7 +39,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createAreaVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'area', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts b/src/plugins/vis_type_vislib/public/components/common/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/index.ts rename to src/plugins/vis_type_vislib/public/components/common/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx b/src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/truncate_labels.tsx rename to src/plugins/vis_type_vislib/public/components/common/truncate_labels.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx b/src/plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx rename to src/plugins/vis_type_vislib/public/components/common/validation_wrapper.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/index.ts b/src/plugins/vis_type_vislib/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/index.ts rename to src/plugins/vis_type_vislib/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/index.tsx rename to src/plugins/vis_type_vislib/public/components/options/gauge/index.tsx diff --git a/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx new file mode 100644 index 0000000000000..0bd5694f71021 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/labels_panel.tsx @@ -0,0 +1,82 @@ +/* + * 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 React from 'react'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SwitchOption, TextInputOption } from '../../../../../charts/public'; +import { GaugeOptionsInternalProps } from '../gauge'; + +function LabelsPanel({ stateParams, setValue, setGaugeValue }: GaugeOptionsInternalProps) { + return ( + + +

+ +

+
+ + + + setGaugeValue('labels', { ...stateParams.gauge.labels, [paramName]: value }) + } + /> + + + + setGaugeValue('style', { ...stateParams.gauge.style, [paramName]: value }) + } + /> + + +
+ ); +} + +export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx index 433cc4edeb47b..c297fb08e4b68 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/ranges_panel.tsx @@ -29,8 +29,8 @@ import { SetColorRangeValue, SwitchOption, ColorSchemas, -} from '../../../../../../../plugins/charts/public'; -import { GaugeOptionsInternalProps } from '.'; +} from '../../../../../charts/public'; +import { GaugeOptionsInternalProps } from '../gauge'; import { Gauge } from '../../../gauge'; function RangesPanel({ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx similarity index 91% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx index 48711de7d171a..b299b2e86ca40 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/gauge/style_panel.tsx @@ -22,9 +22,9 @@ import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SelectOption } from '../../../../../../../plugins/charts/public'; -import { GaugeOptionsInternalProps } from '.'; -import { AggGroupNames } from '../../../../../../../plugins/data/public'; +import { SelectOption } from '../../../../../charts/public'; +import { GaugeOptionsInternalProps } from '../gauge'; +import { AggGroupNames } from '../../../../../data/public'; function StylePanel({ aggs, setGaugeValue, stateParams, vis }: GaugeOptionsInternalProps) { const diasableAlignment = diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx new file mode 100644 index 0000000000000..7a89496d9441e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/heatmap/index.tsx @@ -0,0 +1,188 @@ +/* + * 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 React, { useCallback, useEffect, useState } from 'react'; + +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { + BasicOptions, + ColorRanges, + ColorSchemaOptions, + NumberInputOption, + SelectOption, + SwitchOption, + SetColorSchemaOptionsValue, + SetColorRangeValue, +} from '../../../../../charts/public'; +import { HeatmapVisParams } from '../../../heatmap'; +import { ValueAxis } from '../../../types'; +import { LabelsPanel } from './labels_panel'; + +function HeatmapOptions(props: VisOptionsProps) { + const { stateParams, vis, uiState, setValue, setValidity, setTouched } = props; + const [valueAxis] = stateParams.valueAxes; + const isColorsNumberInvalid = stateParams.colorsNumber < 2 || stateParams.colorsNumber > 10; + const [isColorRangesValid, setIsColorRangesValid] = useState(false); + + const setValueAxisScale = useCallback( + (paramName: T, value: ValueAxis['scale'][T]) => + setValue('valueAxes', [ + { + ...valueAxis, + scale: { + ...valueAxis.scale, + [paramName]: value, + }, + }, + ]), + [valueAxis, setValue] + ); + + useEffect(() => { + setValidity(stateParams.setColorRange ? isColorRangesValid : !isColorsNumberInvalid); + }, [stateParams.setColorRange, isColorRangesValid, isColorsNumberInvalid, setValidity]); + + return ( + <> + + +

+ +

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

+ +

+
+ + + + + + + + + + + + + + + + + {stateParams.setColorRange && ( + + )} +
+ + + + + + ); +} + +export { HeatmapOptions }; diff --git a/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx new file mode 100644 index 0000000000000..8d5f529ce0fc7 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/heatmap/labels_panel.tsx @@ -0,0 +1,126 @@ +/* + * 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 React, { useCallback } from 'react'; + +import { EuiColorPicker, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { ValueAxis } from '../../../types'; +import { HeatmapVisParams } from '../../../heatmap'; +import { SwitchOption } from '../../../../../charts/public'; + +const VERTICAL_ROTATION = 270; + +interface LabelsPanelProps { + valueAxis: ValueAxis; + setValue: VisOptionsProps['setValue']; +} + +function LabelsPanel({ valueAxis, setValue }: LabelsPanelProps) { + const rotateLabels = valueAxis.labels.rotate === VERTICAL_ROTATION; + + const setValueAxisLabels = useCallback( + (paramName: T, value: ValueAxis['labels'][T]) => + setValue('valueAxes', [ + { + ...valueAxis, + labels: { + ...valueAxis.labels, + [paramName]: value, + }, + }, + ]), + [valueAxis, setValue] + ); + + const setRotateLabels = useCallback( + (paramName: 'rotate', value: boolean) => + setValueAxisLabels(paramName, value ? VERTICAL_ROTATION : 0), + [setValueAxisLabels] + ); + + const setColor = useCallback(value => setValueAxisLabels('color', value), [setValueAxisLabels]); + + return ( + + +

+ +

+
+ + + + + + + + + + + +
+ ); +} + +export { LabelsPanel }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/index.ts b/src/plugins/vis_type_vislib/public/components/options/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/index.ts rename to src/plugins/vis_type_vislib/public/components/options/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/category_axis_panel.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/custom_extents_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/index.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/label_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/line_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axes_panel.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/value_axis_options.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/__snapshots__/y_extents.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx index 91cdcd0f456b1..44ed0d5aeddab 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.test.tsx @@ -25,8 +25,6 @@ import { Positions } from '../../../utils/collections'; import { LabelOptions } from './label_options'; import { categoryAxis, vis } from './mocks'; -jest.mock('ui/new_platform'); - describe('CategoryAxisPanel component', () => { let setCategoryAxis: jest.Mock; let onPositionChanged: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx index 246c20a14807c..468fb1f8c315a 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/category_axis_panel.tsx @@ -25,7 +25,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { Axis } from '../../../types'; -import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { Positions } from '../../../utils/collections'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx index c913fd4f35713..e2d4a0db9f1f9 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.test.tsx @@ -25,8 +25,6 @@ import { LineOptions } from './line_options'; import { ChartTypes, ChartModes } from '../../../utils/collections'; import { valueAxis, seriesParam, vis } from './mocks'; -jest.mock('ui/new_platform'); - describe('ChartOptions component', () => { let setParamByIndex: jest.Mock; let changeValueAxis: jest.Mock; diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx new file mode 100644 index 0000000000000..623a8d1f348e9 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/chart_options.tsx @@ -0,0 +1,146 @@ +/* + * 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 React, { useMemo, useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +import { Vis } from '../../../../../visualizations/public'; +import { SeriesParam, ValueAxis } from '../../../types'; +import { ChartTypes } from '../../../utils/collections'; +import { SelectOption } from '../../../../../charts/public'; +import { LineOptions } from './line_options'; +import { SetParamByIndex, ChangeValueAxis } from '.'; + +export type SetChart = (paramName: T, value: SeriesParam[T]) => void; + +export interface ChartOptionsParams { + chart: SeriesParam; + index: number; + changeValueAxis: ChangeValueAxis; + setParamByIndex: SetParamByIndex; + valueAxes: ValueAxis[]; + vis: Vis; +} + +function ChartOptions({ + chart, + index, + valueAxes, + vis, + changeValueAxis, + setParamByIndex, +}: ChartOptionsParams) { + const setChart: SetChart = useCallback( + (paramName, value) => { + setParamByIndex('seriesParams', index, paramName, value); + }, + [setParamByIndex, index] + ); + + const setValueAxis = useCallback( + (paramName, value) => { + changeValueAxis(index, paramName, value); + }, + [changeValueAxis, index] + ); + + const valueAxesOptions = useMemo( + () => [ + ...valueAxes.map(({ id, name }: ValueAxis) => ({ + text: name, + value: id, + })), + { + text: i18n.translate('visTypeVislib.controls.pointSeries.series.newAxisLabel', { + defaultMessage: 'New axis…', + }), + value: 'new', + }, + ], + [valueAxes] + ); + + return ( + <> + + + + + + + + + + + + + + {chart.type === ChartTypes.AREA && ( + <> + + + + + )} + + {chart.type === ChartTypes.LINE && ( + + )} + + ); +} + +export { ChartOptions }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx index a93ee454a7afd..4798c67928f7f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.test.tsx @@ -28,8 +28,6 @@ const DEFAULT_Y_EXTENTS = 'defaultYExtents'; const SCALE = 'scale'; const SET_Y_EXTENTS = 'setYExtents'; -jest.mock('ui/new_platform'); - describe('CustomExtentsOptions component', () => { let setValueAxis: jest.Mock; let setValueAxisScale: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx index a3a97df9e35ae..634d6b3f0641c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/custom_extents_options.tsx @@ -21,7 +21,7 @@ import React, { useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { ValueAxis } from '../../../types'; -import { NumberInputOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; import { YExtents } from './y_extents'; import { SetScale } from './value_axis_options'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx index 48fcbdf8f9082..f500b7e58e9fd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.test.tsx @@ -23,8 +23,6 @@ import { LabelOptions, LabelOptionsProps } from './label_options'; import { TruncateLabelsOption } from '../../common'; import { valueAxis } from './mocks'; -jest.mock('ui/new_platform'); - const FILTER = 'filter'; const ROTATE = 'rotate'; const DISABLED = 'disabled'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx index bc687e56646f6..14e1da6ebcc70 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/label_options.tsx @@ -26,7 +26,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Axis } from '../../../types'; import { TruncateLabelsOption } from '../../common'; import { getRotateOptions } from '../../../utils/collections'; -import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; export type SetAxisLabel = ( paramName: T, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx index 5354bc9c033e6..e90c96146ec2c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.test.tsx @@ -20,11 +20,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { LineOptions, LineOptionsParams } from './line_options'; -import { NumberInputOption } from '../../../../../../../plugins/charts/public'; +import { NumberInputOption } from '../../../../../charts/public'; import { seriesParam, vis } from './mocks'; -jest.mock('ui/new_platform'); - const LINE_WIDTH = 'lineWidth'; const DRAW_LINES = 'drawLinesBetweenPoints'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx index 76f95bd93caf8..4b0cce94267f1 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/line_options.tsx @@ -22,13 +22,9 @@ import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { SeriesParam } from '../../../types'; -import { - NumberInputOption, - SelectOption, - SwitchOption, -} from '../../../../../../../plugins/charts/public'; +import { NumberInputOption, SelectOption, SwitchOption } from '../../../../../charts/public'; import { SetChart } from './chart_options'; export interface LineOptionsParams { diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts new file mode 100644 index 0000000000000..277fcf0cdbc3d --- /dev/null +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/mocks.ts @@ -0,0 +1,106 @@ +/* + * 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 { Vis } from '../../../../../visualizations/public'; +import { Axis, ValueAxis, SeriesParam } from '../../../types'; +import { + ChartTypes, + ChartModes, + InterpolationModes, + ScaleTypes, + Positions, + AxisTypes, + getScaleTypes, + getAxisModes, + getPositions, + getInterpolationModes, +} from '../../../utils/collections'; +import { Style } from '../../../../../charts/public'; + +const defaultValueAxisId = 'ValueAxis-1'; + +const axis = { + show: true, + style: {} as Style, + title: { + text: '', + }, + labels: { + show: true, + filter: true, + truncate: 0, + color: 'black', + }, +}; + +const categoryAxis: Axis = { + ...axis, + id: 'CategoryAxis-1', + type: AxisTypes.CATEGORY, + position: Positions.BOTTOM, + scale: { + type: ScaleTypes.LINEAR, + }, +}; + +const valueAxis: ValueAxis = { + ...axis, + id: defaultValueAxisId, + name: 'ValueAxis-1', + type: AxisTypes.VALUE, + position: Positions.LEFT, + scale: { + type: ScaleTypes.LINEAR, + boundsMargin: 1, + defaultYExtents: true, + min: 1, + max: 2, + setYExtents: true, + }, +}; + +const seriesParam: SeriesParam = { + show: true, + type: ChartTypes.HISTOGRAM, + mode: ChartModes.STACKED, + data: { + label: 'Count', + id: '1', + }, + drawLinesBetweenPoints: true, + lineWidth: 2, + showCircles: true, + interpolate: InterpolationModes.LINEAR, + valueAxis: defaultValueAxisId, +}; + +const positions = getPositions(); +const axisModes = getAxisModes(); +const scaleTypes = getScaleTypes(); +const interpolationModes = getInterpolationModes(); + +const vis = ({ + type: { + editorConfig: { + collections: { scaleTypes, axisModes, positions, interpolationModes }, + }, + }, +} as any) as Vis; + +export { defaultValueAxisId, categoryAxis, valueAxis, seriesParam, vis }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx index 22a726b53363b..27c423860972c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/series_panel.tsx @@ -23,10 +23,10 @@ import { EuiPanel, EuiTitle, EuiSpacer, EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { ValueAxis, SeriesParam } from '../../../types'; import { ChartOptions } from './chart_options'; -import { SetParamByIndex, ChangeValueAxis } from './'; +import { SetParamByIndex, ChangeValueAxis } from '.'; export interface SeriesPanelProps { changeValueAxis: ChangeValueAxis; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/utils.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx index 141273fa6bc3f..2f7dd4071b52c 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.test.tsx @@ -25,8 +25,6 @@ import { Positions } from '../../../utils/collections'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { valueAxis, seriesParam, vis } from './mocks'; -jest.mock('ui/new_platform'); - describe('ValueAxesPanel component', () => { let setParamByIndex: jest.Mock; let onValueAxisPositionChanged: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx index 912c3b904b110..b17f67b81d2b0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axes_panel.tsx @@ -31,10 +31,10 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { SeriesParam, ValueAxis } from '../../../types'; import { ValueAxisOptions } from './value_axis_options'; -import { SetParamByIndex } from './'; +import { SetParamByIndex } from '.'; export interface ValueAxesPanelProps { isCategoryAxisHorizontal: boolean; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx index 876a6917ee0b4..1977bdba6eadf 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.test.tsx @@ -21,13 +21,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ValueAxisOptions, ValueAxisOptionsParams } from './value_axis_options'; import { ValueAxis } from '../../../types'; -import { TextInputOption } from '../../../../../../../plugins/charts/public'; +import { TextInputOption } from '../../../../../charts/public'; import { LabelOptions } from './label_options'; import { ScaleTypes, Positions } from '../../../utils/collections'; import { valueAxis, vis } from './mocks'; -jest.mock('ui/new_platform'); - const POSITION = 'position'; interface PositionOption { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx index 1b89a766d0591..52962fe813b44 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/value_axis_options.tsx @@ -21,18 +21,14 @@ import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiAccordion, EuiHorizontalRule } from '@elastic/eui'; -import { Vis } from '../../../../../../../plugins/visualizations/public'; +import { Vis } from '../../../../../visualizations/public'; import { ValueAxis } from '../../../types'; import { Positions } from '../../../utils/collections'; -import { - SelectOption, - SwitchOption, - TextInputOption, -} from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption, TextInputOption } from '../../../../../charts/public'; import { LabelOptions, SetAxisLabel } from './label_options'; import { CustomExtentsOptions } from './custom_extents_options'; import { isAxisHorizontal } from './utils'; -import { SetParamByIndex } from './'; +import { SetParamByIndex } from '.'; export type SetScale = ( paramName: T, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx index b5ed475ca8e31..3bacb0be34d13 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.test.tsx @@ -21,9 +21,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { YExtents, YExtentsProps } from './y_extents'; import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../../../plugins/charts/public'; - -jest.mock('ui/new_platform'); +import { NumberInputOption } from '../../../../../charts/public'; describe('YExtents component', () => { let setMultipleValidity: jest.Mock; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx rename to src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx index faeb6069b5126..c2aa917dd3a6f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/y_extents.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { Scale } from '../../../types'; import { ScaleTypes } from '../../../utils/collections'; -import { NumberInputOption } from '../../../../../../../plugins/charts/public'; +import { NumberInputOption } from '../../../../../charts/public'; import { SetScale } from './value_axis_options'; const rangeError = i18n.translate('visTypeVislib.controls.pointSeries.valueAxes.minErrorMessage', { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx b/src/plugins/vis_type_vislib/public/components/options/pie.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx rename to src/plugins/vis_type_vislib/public/components/options/pie.tsx index f6be9cd0bd8fe..54ba307982967 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/pie.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/pie.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { TruncateLabelsOption } from '../common'; -import { BasicOptions, SwitchOption } from '../../../../../../plugins/charts/public'; +import { BasicOptions, SwitchOption } from '../../../../charts/public'; import { PieVisParams } from '../../pie'; function PieOptions(props: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx index 392d180d2c5f2..0126dce37c9f2 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/grid_panel.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { SelectOption, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { SelectOption, SwitchOption } from '../../../../../charts/public'; import { BasicVislibParams, ValueAxis } from '../../../types'; function GridPanel({ stateParams, setValue, hasHistogramAgg }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/index.ts b/src/plugins/vis_type_vislib/public/components/options/point_series/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/index.ts rename to src/plugins/vis_type_vislib/public/components/options/point_series/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx b/src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx rename to src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx index 903c1917751d9..60458b1f5c41f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/point_series.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ValidationVisOptionsProps } from '../../common'; -import { BasicOptions, SwitchOption } from '../../../../../../../plugins/charts/public'; +import { BasicOptions, SwitchOption } from '../../../../../charts/public'; import { GridPanel } from './grid_panel'; import { ThresholdPanel } from './threshold_panel'; import { BasicVislibParams } from '../../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx b/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx similarity index 98% rename from src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx rename to src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx index 12f058ec7dd1f..0823180300756 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/point_series/threshold_panel.tsx @@ -26,7 +26,7 @@ import { SelectOption, SwitchOption, RequiredNumberInputOption, -} from '../../../../../../../plugins/charts/public'; +} from '../../../../../charts/public'; import { BasicVislibParams } from '../../../types'; function ThresholdPanel({ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_normal.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_normal.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_normal.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_normal.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_percentage.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_percentage.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_config_percentage.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_config_percentage.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_d3.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_d3.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_d3.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_d3.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_data_point.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_data_point.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_bar_chart_data_point.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_bar_chart_data_point.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_config.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_config.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_config.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_config.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_d3.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_d3.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_d3.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_d3.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_data_point.json b/src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_data_point.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/dispatch_heatmap_data_point.json rename to src/plugins/vis_type_vislib/public/fixtures/dispatch_heatmap_data_point.json diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns.js new file mode 100644 index 0000000000000..ff8538021d275 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns.js @@ -0,0 +1,319 @@ +/* + * 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 moment from 'moment'; + +export default { + columns: [ + { + label: '200: response', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826608440, + max: 1415827508440, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1415826600000, + y: 4, + }, + { + x: 1415826630000, + y: 8, + }, + { + x: 1415826660000, + y: 7, + }, + { + x: 1415826690000, + y: 5, + }, + { + x: 1415826720000, + y: 5, + }, + { + x: 1415826750000, + y: 4, + }, + { + x: 1415826780000, + y: 10, + }, + { + x: 1415826810000, + y: 7, + }, + { + x: 1415826840000, + y: 9, + }, + { + x: 1415826870000, + y: 8, + }, + { + x: 1415826900000, + y: 9, + }, + { + x: 1415826930000, + y: 8, + }, + { + x: 1415826960000, + y: 3, + }, + { + x: 1415826990000, + y: 9, + }, + { + x: 1415827020000, + y: 6, + }, + { + x: 1415827050000, + y: 8, + }, + { + x: 1415827080000, + y: 7, + }, + { + x: 1415827110000, + y: 4, + }, + { + x: 1415827140000, + y: 6, + }, + { + x: 1415827170000, + y: 10, + }, + { + x: 1415827200000, + y: 2, + }, + { + x: 1415827230000, + y: 8, + }, + { + x: 1415827260000, + y: 5, + }, + { + x: 1415827290000, + y: 6, + }, + { + x: 1415827320000, + y: 6, + }, + { + x: 1415827350000, + y: 10, + }, + { + x: 1415827380000, + y: 6, + }, + { + x: 1415827410000, + y: 6, + }, + { + x: 1415827440000, + y: 12, + }, + { + x: 1415827470000, + y: 9, + }, + { + x: 1415827500000, + y: 1, + }, + ], + }, + ], + }, + { + label: '503: response', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826608440, + max: 1415827508440, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1415826630000, + y: 1, + }, + { + x: 1415826660000, + y: 1, + }, + { + x: 1415826720000, + y: 1, + }, + { + x: 1415826780000, + y: 1, + }, + { + x: 1415826900000, + y: 1, + }, + { + x: 1415827020000, + y: 1, + }, + { + x: 1415827080000, + y: 1, + }, + { + x: 1415827110000, + y: 2, + }, + ], + }, + ], + }, + { + label: '404: response', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826608440, + max: 1415827508440, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1415826660000, + y: 1, + }, + { + x: 1415826720000, + y: 1, + }, + { + x: 1415826810000, + y: 1, + }, + { + x: 1415826960000, + y: 1, + }, + { + x: 1415827050000, + y: 1, + }, + { + x: 1415827260000, + y: 1, + }, + { + x: 1415827380000, + y: 1, + }, + { + x: 1415827410000, + y: 1, + }, + ], + }, + ], + }, + ], + xAxisOrderedValues: [ + 1415826600000, + 1415826630000, + 1415826660000, + 1415826690000, + 1415826720000, + 1415826750000, + 1415826780000, + 1415826810000, + 1415826840000, + 1415826870000, + 1415826900000, + 1415826930000, + 1415826960000, + 1415826990000, + 1415827020000, + 1415827050000, + 1415827080000, + 1415827110000, + 1415827140000, + 1415827170000, + 1415827200000, + 1415827230000, + 1415827260000, + 1415827290000, + 1415827320000, + 1415827350000, + 1415827380000, + 1415827410000, + 1415827440000, + 1415827470000, + 1415827500000, + ], + hits: 225, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows.js new file mode 100644 index 0000000000000..6367197acdece --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows.js @@ -0,0 +1,1697 @@ +/* + * 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 moment from 'moment'; + +export default { + rows: [ + { + label: '0.0-1000.0: bytes', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826260456, + max: 1415827160456, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'jpg', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 1, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 1, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 1, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 1, + y0: 0, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 1, + y0: 0, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 1, + y0: 0, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 1, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 1, + }, + { + x: 1415827110000, + y: 1, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 1, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 0, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 1, + }, + { + x: 1415827110000, + y: 0, + y0: 2, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 1, + }, + { + x: 1415826330000, + y: 0, + y0: 1, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 1, + y0: 0, + }, + { + x: 1415826480000, + y: 1, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 3, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 1, + }, + { + x: 1415826660000, + y: 1, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 1, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 1, + y0: 1, + }, + { + x: 1415826810000, + y: 0, + y0: 0, + }, + { + x: 1415826840000, + y: 0, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 1, + y0: 1, + }, + { + x: 1415827110000, + y: 1, + y0: 2, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + ], + }, + { + label: '1000.0-2000.0: bytes', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1415826260457, + max: 1415827160457, + }, + yAxisLabel: 'Count of documents', + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + series: [ + { + label: 'jpg', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 1, + y0: 0, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 1, + y0: 0, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 2, + y0: 0, + }, + { + x: 1415826840000, + y: 1, + y0: 0, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 1, + y0: 0, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 1, + y0: 0, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 1, + y0: 0, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 1, + y0: 0, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 1, + y0: 0, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 1, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1415826240000, + y: 0, + y0: 0, + }, + { + x: 1415826270000, + y: 0, + y0: 0, + }, + { + x: 1415826300000, + y: 0, + y0: 0, + }, + { + x: 1415826330000, + y: 0, + y0: 0, + }, + { + x: 1415826360000, + y: 0, + y0: 0, + }, + { + x: 1415826390000, + y: 0, + y0: 0, + }, + { + x: 1415826420000, + y: 0, + y0: 0, + }, + { + x: 1415826450000, + y: 0, + y0: 1, + }, + { + x: 1415826480000, + y: 0, + y0: 0, + }, + { + x: 1415826510000, + y: 0, + y0: 0, + }, + { + x: 1415826540000, + y: 0, + y0: 0, + }, + { + x: 1415826570000, + y: 0, + y0: 1, + }, + { + x: 1415826600000, + y: 0, + y0: 0, + }, + { + x: 1415826630000, + y: 0, + y0: 0, + }, + { + x: 1415826660000, + y: 0, + y0: 1, + }, + { + x: 1415826690000, + y: 0, + y0: 0, + }, + { + x: 1415826720000, + y: 0, + y0: 0, + }, + { + x: 1415826750000, + y: 0, + y0: 0, + }, + { + x: 1415826780000, + y: 0, + y0: 0, + }, + { + x: 1415826810000, + y: 0, + y0: 2, + }, + { + x: 1415826840000, + y: 0, + y0: 1, + }, + { + x: 1415826870000, + y: 0, + y0: 0, + }, + { + x: 1415826900000, + y: 0, + y0: 1, + }, + { + x: 1415826930000, + y: 0, + y0: 0, + }, + { + x: 1415826960000, + y: 0, + y0: 0, + }, + { + x: 1415826990000, + y: 0, + y0: 1, + }, + { + x: 1415827020000, + y: 0, + y0: 1, + }, + { + x: 1415827050000, + y: 0, + y0: 0, + }, + { + x: 1415827080000, + y: 0, + y0: 0, + }, + { + x: 1415827110000, + y: 0, + y0: 1, + }, + { + x: 1415827140000, + y: 0, + y0: 0, + }, + ], + }, + ], + }, + ], + xAxisOrderedValues: [ + 1415826240000, + 1415826270000, + 1415826300000, + 1415826330000, + 1415826360000, + 1415826390000, + 1415826420000, + 1415826450000, + 1415826480000, + 1415826510000, + 1415826540000, + 1415826570000, + 1415826600000, + 1415826630000, + 1415826660000, + 1415826690000, + 1415826720000, + 1415826750000, + 1415826780000, + 1415826810000, + 1415826840000, + 1415826870000, + 1415826900000, + 1415826930000, + 1415826960000, + 1415826990000, + 1415827020000, + 1415827050000, + 1415827080000, + 1415827110000, + 1415827140000, + ], + hits: 236, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes.js new file mode 100644 index 0000000000000..ba0d8bf251c6f --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes.js @@ -0,0 +1,142 @@ +/* + * 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 moment from 'moment'; + +export const rowsSeriesWithHoles = { + rows: [ + { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: 41, + }, + { + x: 1411761510000, + y: 22, + }, + { + x: 1411761540000, + y: 17, + }, + { + x: 1411761840000, + y: 20, + }, + { + x: 1411761870000, + y: 20, + }, + { + x: 1411761900000, + y: 21, + }, + { + x: 1411761930000, + y: 17, + }, + { + x: 1411761960000, + y: 20, + }, + { + x: 1411761990000, + y: 13, + }, + { + x: 1411762020000, + y: 14, + }, + { + x: 1411762050000, + y: 25, + }, + { + x: 1411762080000, + y: 17, + }, + { + x: 1411762110000, + y: 14, + }, + { + x: 1411762140000, + y: 22, + }, + { + x: 1411762170000, + y: 14, + }, + { + x: 1411762200000, + y: 19, + }, + { + x: 1411762320000, + y: 15, + }, + { + x: 1411762350000, + y: 4, + }, + ], + }, + ], + hits: 533, + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: [ + 1411761450000, + 1411761510000, + 1411761540000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762320000, + 1411762350000, + ], +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series.js new file mode 100644 index 0000000000000..89e4f9a32cee1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series.js @@ -0,0 +1,203 @@ +/* + * 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 moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: 41, + }, + { + x: 1411761480000, + y: 18, + }, + { + x: 1411761510000, + y: 22, + }, + { + x: 1411761540000, + y: 17, + }, + { + x: 1411761570000, + y: 17, + }, + { + x: 1411761600000, + y: 21, + }, + { + x: 1411761630000, + y: 16, + }, + { + x: 1411761660000, + y: 17, + }, + { + x: 1411761690000, + y: 15, + }, + { + x: 1411761720000, + y: 19, + }, + { + x: 1411761750000, + y: 11, + }, + { + x: 1411761780000, + y: 13, + }, + { + x: 1411761810000, + y: 24, + }, + { + x: 1411761840000, + y: 20, + }, + { + x: 1411761870000, + y: 20, + }, + { + x: 1411761900000, + y: 21, + }, + { + x: 1411761930000, + y: 17, + }, + { + x: 1411761960000, + y: 20, + }, + { + x: 1411761990000, + y: 13, + }, + { + x: 1411762020000, + y: 14, + }, + { + x: 1411762050000, + y: 25, + }, + { + x: 1411762080000, + y: 17, + }, + { + x: 1411762110000, + y: 14, + }, + { + x: 1411762140000, + y: 22, + }, + { + x: 1411762170000, + y: 14, + }, + { + x: 1411762200000, + y: 19, + }, + { + x: 1411762230000, + y: 22, + }, + { + x: 1411762260000, + y: 17, + }, + { + x: 1411762290000, + y: 8, + }, + { + x: 1411762320000, + y: 15, + }, + { + x: 1411762350000, + y: 4, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1411761450000, + 1411761480000, + 1411761510000, + 1411761540000, + 1411761570000, + 1411761600000, + 1411761630000, + 1411761660000, + 1411761690000, + 1411761720000, + 1411761750000, + 1411761780000, + 1411761810000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762230000, + 1411762260000, + 1411762290000, + 1411762320000, + 1411762350000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval.js new file mode 100644 index 0000000000000..85078a2ec15af --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval.js @@ -0,0 +1,108 @@ +/* + * 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 moment from 'moment'; + +export const seriesMonthlyInterval = { + label: '', + xAxisLabel: '@timestamp per month', + ordered: { + date: true, + min: 1451631600000, + max: 1483254000000, + interval: 2678000000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1451631600000, + y: 10220, + }, + { + x: 1454310000000, + y: 9997, + }, + { + x: 1456815600000, + y: 10792, + }, + { + x: 1459490400000, + y: 10262, + }, + { + x: 1462082400000, + y: 10080, + }, + { + x: 1464760800000, + y: 11161, + }, + { + x: 1467352800000, + y: 9933, + }, + { + x: 1470031200000, + y: 10342, + }, + { + x: 1472709600000, + y: 10887, + }, + { + x: 1475301600000, + y: 9666, + }, + { + x: 1477980000000, + y: 9556, + }, + { + x: 1480575600000, + y: 11644, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1451631600000, + 1454310000000, + 1456815600000, + 1459490400000, + 1462082400000, + 1464760800000, + 1467352800000, + 1470031200000, + 1472709600000, + 1475301600000, + 1477980000000, + 1480575600000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg.js new file mode 100644 index 0000000000000..821c04685d22e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg.js @@ -0,0 +1,203 @@ +/* + * 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 moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: -41, + }, + { + x: 1411761480000, + y: -18, + }, + { + x: 1411761510000, + y: -22, + }, + { + x: 1411761540000, + y: -17, + }, + { + x: 1411761570000, + y: -17, + }, + { + x: 1411761600000, + y: -21, + }, + { + x: 1411761630000, + y: -16, + }, + { + x: 1411761660000, + y: -17, + }, + { + x: 1411761690000, + y: -15, + }, + { + x: 1411761720000, + y: -19, + }, + { + x: 1411761750000, + y: -11, + }, + { + x: 1411761780000, + y: -13, + }, + { + x: 1411761810000, + y: -24, + }, + { + x: 1411761840000, + y: -20, + }, + { + x: 1411761870000, + y: -20, + }, + { + x: 1411761900000, + y: -21, + }, + { + x: 1411761930000, + y: -17, + }, + { + x: 1411761960000, + y: -20, + }, + { + x: 1411761990000, + y: -13, + }, + { + x: 1411762020000, + y: -14, + }, + { + x: 1411762050000, + y: -25, + }, + { + x: 1411762080000, + y: -17, + }, + { + x: 1411762110000, + y: -14, + }, + { + x: 1411762140000, + y: -22, + }, + { + x: 1411762170000, + y: -14, + }, + { + x: 1411762200000, + y: -19, + }, + { + x: 1411762230000, + y: -22, + }, + { + x: 1411762260000, + y: -17, + }, + { + x: 1411762290000, + y: -8, + }, + { + x: 1411762320000, + y: -15, + }, + { + x: 1411762350000, + y: -4, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1411761450000, + 1411761480000, + 1411761510000, + 1411761540000, + 1411761570000, + 1411761600000, + 1411761630000, + 1411761660000, + 1411761690000, + 1411761720000, + 1411761750000, + 1411761780000, + 1411761810000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762230000, + 1411762260000, + 1411762290000, + 1411762320000, + 1411762350000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg.js new file mode 100644 index 0000000000000..65821ac58eb0d --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg.js @@ -0,0 +1,203 @@ +/* + * 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 moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + min: 1411761457636, + max: 1411762357636, + interval: 30000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 1411761450000, + y: 41, + }, + { + x: 1411761480000, + y: 18, + }, + { + x: 1411761510000, + y: -22, + }, + { + x: 1411761540000, + y: -17, + }, + { + x: 1411761570000, + y: -17, + }, + { + x: 1411761600000, + y: -21, + }, + { + x: 1411761630000, + y: -16, + }, + { + x: 1411761660000, + y: 17, + }, + { + x: 1411761690000, + y: 15, + }, + { + x: 1411761720000, + y: 19, + }, + { + x: 1411761750000, + y: 11, + }, + { + x: 1411761780000, + y: -13, + }, + { + x: 1411761810000, + y: -24, + }, + { + x: 1411761840000, + y: -20, + }, + { + x: 1411761870000, + y: -20, + }, + { + x: 1411761900000, + y: -21, + }, + { + x: 1411761930000, + y: 17, + }, + { + x: 1411761960000, + y: 20, + }, + { + x: 1411761990000, + y: -13, + }, + { + x: 1411762020000, + y: -14, + }, + { + x: 1411762050000, + y: 25, + }, + { + x: 1411762080000, + y: -17, + }, + { + x: 1411762110000, + y: -14, + }, + { + x: 1411762140000, + y: -22, + }, + { + x: 1411762170000, + y: -14, + }, + { + x: 1411762200000, + y: 19, + }, + { + x: 1411762230000, + y: 22, + }, + { + x: 1411762260000, + y: 17, + }, + { + x: 1411762290000, + y: 8, + }, + { + x: 1411762320000, + y: -15, + }, + { + x: 1411762350000, + y: -4, + }, + ], + }, + ], + hits: 533, + xAxisOrderedValues: [ + 1411761450000, + 1411761480000, + 1411761510000, + 1411761540000, + 1411761570000, + 1411761600000, + 1411761630000, + 1411761660000, + 1411761690000, + 1411761720000, + 1411761750000, + 1411761780000, + 1411761810000, + 1411761840000, + 1411761870000, + 1411761900000, + 1411761930000, + 1411761960000, + 1411761990000, + 1411762020000, + 1411762050000, + 1411762080000, + 1411762110000, + 1411762140000, + 1411762170000, + 1411762200000, + 1411762230000, + 1411762260000, + 1411762290000, + 1411762320000, + 1411762350000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series.js new file mode 100644 index 0000000000000..b6f731c9655d4 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series.js @@ -0,0 +1,1576 @@ +/* + * 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 moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 10 min', + ordered: { + date: true, + min: 1413544140087, + max: 1413587340087, + interval: 600000, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'html', + values: [ + { + x: 1413543600000, + y: 140, + }, + { + x: 1413544200000, + y: 1388, + }, + { + x: 1413544800000, + y: 1308, + }, + { + x: 1413545400000, + y: 1356, + }, + { + x: 1413546000000, + y: 1314, + }, + { + x: 1413546600000, + y: 1343, + }, + { + x: 1413547200000, + y: 1353, + }, + { + x: 1413547800000, + y: 1353, + }, + { + x: 1413548400000, + y: 1334, + }, + { + x: 1413549000000, + y: 1433, + }, + { + x: 1413549600000, + y: 1331, + }, + { + x: 1413550200000, + y: 1349, + }, + { + x: 1413550800000, + y: 1323, + }, + { + x: 1413551400000, + y: 1203, + }, + { + x: 1413552000000, + y: 1231, + }, + { + x: 1413552600000, + y: 1227, + }, + { + x: 1413553200000, + y: 1187, + }, + { + x: 1413553800000, + y: 1119, + }, + { + x: 1413554400000, + y: 1159, + }, + { + x: 1413555000000, + y: 1117, + }, + { + x: 1413555600000, + y: 1152, + }, + { + x: 1413556200000, + y: 1057, + }, + { + x: 1413556800000, + y: 1009, + }, + { + x: 1413557400000, + y: 979, + }, + { + x: 1413558000000, + y: 975, + }, + { + x: 1413558600000, + y: 848, + }, + { + x: 1413559200000, + y: 873, + }, + { + x: 1413559800000, + y: 808, + }, + { + x: 1413560400000, + y: 784, + }, + { + x: 1413561000000, + y: 799, + }, + { + x: 1413561600000, + y: 684, + }, + { + x: 1413562200000, + y: 727, + }, + { + x: 1413562800000, + y: 621, + }, + { + x: 1413563400000, + y: 615, + }, + { + x: 1413564000000, + y: 569, + }, + { + x: 1413564600000, + y: 523, + }, + { + x: 1413565200000, + y: 474, + }, + { + x: 1413565800000, + y: 470, + }, + { + x: 1413566400000, + y: 466, + }, + { + x: 1413567000000, + y: 394, + }, + { + x: 1413567600000, + y: 404, + }, + { + x: 1413568200000, + y: 389, + }, + { + x: 1413568800000, + y: 312, + }, + { + x: 1413569400000, + y: 274, + }, + { + x: 1413570000000, + y: 285, + }, + { + x: 1413570600000, + y: 299, + }, + { + x: 1413571200000, + y: 207, + }, + { + x: 1413571800000, + y: 213, + }, + { + x: 1413572400000, + y: 119, + }, + { + x: 1413573600000, + y: 122, + }, + { + x: 1413574200000, + y: 169, + }, + { + x: 1413574800000, + y: 151, + }, + { + x: 1413575400000, + y: 152, + }, + { + x: 1413576000000, + y: 115, + }, + { + x: 1413576600000, + y: 117, + }, + { + x: 1413577200000, + y: 108, + }, + { + x: 1413577800000, + y: 100, + }, + { + x: 1413578400000, + y: 78, + }, + { + x: 1413579000000, + y: 88, + }, + { + x: 1413579600000, + y: 63, + }, + { + x: 1413580200000, + y: 58, + }, + { + x: 1413580800000, + y: 45, + }, + { + x: 1413581400000, + y: 57, + }, + { + x: 1413582000000, + y: 34, + }, + { + x: 1413582600000, + y: 41, + }, + { + x: 1413583200000, + y: 24, + }, + { + x: 1413583800000, + y: 27, + }, + { + x: 1413584400000, + y: 19, + }, + { + x: 1413585000000, + y: 24, + }, + { + x: 1413585600000, + y: 18, + }, + { + x: 1413586200000, + y: 17, + }, + { + x: 1413586800000, + y: 14, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1413543600000, + y: 90, + }, + { + x: 1413544200000, + y: 949, + }, + { + x: 1413544800000, + y: 1012, + }, + { + x: 1413545400000, + y: 1027, + }, + { + x: 1413546000000, + y: 1073, + }, + { + x: 1413546600000, + y: 992, + }, + { + x: 1413547200000, + y: 1005, + }, + { + x: 1413547800000, + y: 1014, + }, + { + x: 1413548400000, + y: 987, + }, + { + x: 1413549000000, + y: 982, + }, + { + x: 1413549600000, + y: 1086, + }, + { + x: 1413550200000, + y: 998, + }, + { + x: 1413550800000, + y: 935, + }, + { + x: 1413551400000, + y: 995, + }, + { + x: 1413552000000, + y: 926, + }, + { + x: 1413552600000, + y: 897, + }, + { + x: 1413553200000, + y: 873, + }, + { + x: 1413553800000, + y: 885, + }, + { + x: 1413554400000, + y: 859, + }, + { + x: 1413555000000, + y: 852, + }, + { + x: 1413555600000, + y: 779, + }, + { + x: 1413556200000, + y: 739, + }, + { + x: 1413556800000, + y: 783, + }, + { + x: 1413557400000, + y: 784, + }, + { + x: 1413558000000, + y: 687, + }, + { + x: 1413558600000, + y: 660, + }, + { + x: 1413559200000, + y: 672, + }, + { + x: 1413559800000, + y: 600, + }, + { + x: 1413560400000, + y: 659, + }, + { + x: 1413561000000, + y: 540, + }, + { + x: 1413561600000, + y: 539, + }, + { + x: 1413562200000, + y: 481, + }, + { + x: 1413562800000, + y: 498, + }, + { + x: 1413563400000, + y: 444, + }, + { + x: 1413564000000, + y: 452, + }, + { + x: 1413564600000, + y: 408, + }, + { + x: 1413565200000, + y: 358, + }, + { + x: 1413565800000, + y: 321, + }, + { + x: 1413566400000, + y: 305, + }, + { + x: 1413567000000, + y: 292, + }, + { + x: 1413567600000, + y: 289, + }, + { + x: 1413568200000, + y: 239, + }, + { + x: 1413568800000, + y: 256, + }, + { + x: 1413569400000, + y: 220, + }, + { + x: 1413570000000, + y: 205, + }, + { + x: 1413570600000, + y: 201, + }, + { + x: 1413571200000, + y: 183, + }, + { + x: 1413571800000, + y: 172, + }, + { + x: 1413572400000, + y: 73, + }, + { + x: 1413573600000, + y: 90, + }, + { + x: 1413574200000, + y: 130, + }, + { + x: 1413574800000, + y: 104, + }, + { + x: 1413575400000, + y: 108, + }, + { + x: 1413576000000, + y: 92, + }, + { + x: 1413576600000, + y: 79, + }, + { + x: 1413577200000, + y: 90, + }, + { + x: 1413577800000, + y: 72, + }, + { + x: 1413578400000, + y: 68, + }, + { + x: 1413579000000, + y: 52, + }, + { + x: 1413579600000, + y: 60, + }, + { + x: 1413580200000, + y: 51, + }, + { + x: 1413580800000, + y: 32, + }, + { + x: 1413581400000, + y: 37, + }, + { + x: 1413582000000, + y: 30, + }, + { + x: 1413582600000, + y: 29, + }, + { + x: 1413583200000, + y: 24, + }, + { + x: 1413583800000, + y: 16, + }, + { + x: 1413584400000, + y: 15, + }, + { + x: 1413585000000, + y: 15, + }, + { + x: 1413585600000, + y: 10, + }, + { + x: 1413586200000, + y: 9, + }, + { + x: 1413586800000, + y: 9, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1413543600000, + y: 44, + }, + { + x: 1413544200000, + y: 495, + }, + { + x: 1413544800000, + y: 489, + }, + { + x: 1413545400000, + y: 492, + }, + { + x: 1413546000000, + y: 556, + }, + { + x: 1413546600000, + y: 536, + }, + { + x: 1413547200000, + y: 511, + }, + { + x: 1413547800000, + y: 479, + }, + { + x: 1413548400000, + y: 544, + }, + { + x: 1413549000000, + y: 513, + }, + { + x: 1413549600000, + y: 501, + }, + { + x: 1413550200000, + y: 532, + }, + { + x: 1413550800000, + y: 440, + }, + { + x: 1413551400000, + y: 455, + }, + { + x: 1413552000000, + y: 455, + }, + { + x: 1413552600000, + y: 471, + }, + { + x: 1413553200000, + y: 428, + }, + { + x: 1413553800000, + y: 457, + }, + { + x: 1413554400000, + y: 450, + }, + { + x: 1413555000000, + y: 418, + }, + { + x: 1413555600000, + y: 398, + }, + { + x: 1413556200000, + y: 397, + }, + { + x: 1413556800000, + y: 359, + }, + { + x: 1413557400000, + y: 398, + }, + { + x: 1413558000000, + y: 339, + }, + { + x: 1413558600000, + y: 363, + }, + { + x: 1413559200000, + y: 297, + }, + { + x: 1413559800000, + y: 323, + }, + { + x: 1413560400000, + y: 302, + }, + { + x: 1413561000000, + y: 260, + }, + { + x: 1413561600000, + y: 276, + }, + { + x: 1413562200000, + y: 249, + }, + { + x: 1413562800000, + y: 248, + }, + { + x: 1413563400000, + y: 235, + }, + { + x: 1413564000000, + y: 234, + }, + { + x: 1413564600000, + y: 188, + }, + { + x: 1413565200000, + y: 192, + }, + { + x: 1413565800000, + y: 173, + }, + { + x: 1413566400000, + y: 160, + }, + { + x: 1413567000000, + y: 137, + }, + { + x: 1413567600000, + y: 158, + }, + { + x: 1413568200000, + y: 111, + }, + { + x: 1413568800000, + y: 145, + }, + { + x: 1413569400000, + y: 118, + }, + { + x: 1413570000000, + y: 104, + }, + { + x: 1413570600000, + y: 80, + }, + { + x: 1413571200000, + y: 79, + }, + { + x: 1413571800000, + y: 86, + }, + { + x: 1413572400000, + y: 47, + }, + { + x: 1413573600000, + y: 49, + }, + { + x: 1413574200000, + y: 68, + }, + { + x: 1413574800000, + y: 78, + }, + { + x: 1413575400000, + y: 77, + }, + { + x: 1413576000000, + y: 50, + }, + { + x: 1413576600000, + y: 51, + }, + { + x: 1413577200000, + y: 40, + }, + { + x: 1413577800000, + y: 42, + }, + { + x: 1413578400000, + y: 29, + }, + { + x: 1413579000000, + y: 24, + }, + { + x: 1413579600000, + y: 30, + }, + { + x: 1413580200000, + y: 18, + }, + { + x: 1413580800000, + y: 15, + }, + { + x: 1413581400000, + y: 19, + }, + { + x: 1413582000000, + y: 18, + }, + { + x: 1413582600000, + y: 13, + }, + { + x: 1413583200000, + y: 11, + }, + { + x: 1413583800000, + y: 11, + }, + { + x: 1413584400000, + y: 13, + }, + { + x: 1413585000000, + y: 9, + }, + { + x: 1413585600000, + y: 9, + }, + { + x: 1413586200000, + y: 9, + }, + { + x: 1413586800000, + y: 3, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1413543600000, + y: 35, + }, + { + x: 1413544200000, + y: 360, + }, + { + x: 1413544800000, + y: 343, + }, + { + x: 1413545400000, + y: 329, + }, + { + x: 1413546000000, + y: 345, + }, + { + x: 1413546600000, + y: 336, + }, + { + x: 1413547200000, + y: 330, + }, + { + x: 1413547800000, + y: 334, + }, + { + x: 1413548400000, + y: 326, + }, + { + x: 1413549000000, + y: 351, + }, + { + x: 1413549600000, + y: 334, + }, + { + x: 1413550200000, + y: 351, + }, + { + x: 1413550800000, + y: 337, + }, + { + x: 1413551400000, + y: 306, + }, + { + x: 1413552000000, + y: 346, + }, + { + x: 1413552600000, + y: 317, + }, + { + x: 1413553200000, + y: 298, + }, + { + x: 1413553800000, + y: 288, + }, + { + x: 1413554400000, + y: 283, + }, + { + x: 1413555000000, + y: 262, + }, + { + x: 1413555600000, + y: 245, + }, + { + x: 1413556200000, + y: 259, + }, + { + x: 1413556800000, + y: 267, + }, + { + x: 1413557400000, + y: 230, + }, + { + x: 1413558000000, + y: 218, + }, + { + x: 1413558600000, + y: 241, + }, + { + x: 1413559200000, + y: 213, + }, + { + x: 1413559800000, + y: 239, + }, + { + x: 1413560400000, + y: 208, + }, + { + x: 1413561000000, + y: 187, + }, + { + x: 1413561600000, + y: 166, + }, + { + x: 1413562200000, + y: 154, + }, + { + x: 1413562800000, + y: 184, + }, + { + x: 1413563400000, + y: 148, + }, + { + x: 1413564000000, + y: 153, + }, + { + x: 1413564600000, + y: 149, + }, + { + x: 1413565200000, + y: 102, + }, + { + x: 1413565800000, + y: 110, + }, + { + x: 1413566400000, + y: 121, + }, + { + x: 1413567000000, + y: 120, + }, + { + x: 1413567600000, + y: 86, + }, + { + x: 1413568200000, + y: 96, + }, + { + x: 1413568800000, + y: 71, + }, + { + x: 1413569400000, + y: 92, + }, + { + x: 1413570000000, + y: 65, + }, + { + x: 1413570600000, + y: 54, + }, + { + x: 1413571200000, + y: 68, + }, + { + x: 1413571800000, + y: 57, + }, + { + x: 1413572400000, + y: 33, + }, + { + x: 1413573600000, + y: 47, + }, + { + x: 1413574200000, + y: 42, + }, + { + x: 1413574800000, + y: 39, + }, + { + x: 1413575400000, + y: 25, + }, + { + x: 1413576000000, + y: 31, + }, + { + x: 1413576600000, + y: 37, + }, + { + x: 1413577200000, + y: 35, + }, + { + x: 1413577800000, + y: 19, + }, + { + x: 1413578400000, + y: 15, + }, + { + x: 1413579000000, + y: 21, + }, + { + x: 1413579600000, + y: 16, + }, + { + x: 1413580200000, + y: 18, + }, + { + x: 1413580800000, + y: 10, + }, + { + x: 1413581400000, + y: 13, + }, + { + x: 1413582000000, + y: 14, + }, + { + x: 1413582600000, + y: 11, + }, + { + x: 1413583200000, + y: 4, + }, + { + x: 1413583800000, + y: 6, + }, + { + x: 1413584400000, + y: 3, + }, + { + x: 1413585000000, + y: 6, + }, + { + x: 1413585600000, + y: 6, + }, + { + x: 1413586200000, + y: 2, + }, + { + x: 1413586800000, + y: 3, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1413543600000, + y: 21, + }, + { + x: 1413544200000, + y: 191, + }, + { + x: 1413544800000, + y: 176, + }, + { + x: 1413545400000, + y: 166, + }, + { + x: 1413546000000, + y: 183, + }, + { + x: 1413546600000, + y: 170, + }, + { + x: 1413547200000, + y: 153, + }, + { + x: 1413547800000, + y: 202, + }, + { + x: 1413548400000, + y: 175, + }, + { + x: 1413549000000, + y: 161, + }, + { + x: 1413549600000, + y: 174, + }, + { + x: 1413550200000, + y: 167, + }, + { + x: 1413550800000, + y: 171, + }, + { + x: 1413551400000, + y: 176, + }, + { + x: 1413552000000, + y: 139, + }, + { + x: 1413552600000, + y: 145, + }, + { + x: 1413553200000, + y: 157, + }, + { + x: 1413553800000, + y: 148, + }, + { + x: 1413554400000, + y: 149, + }, + { + x: 1413555000000, + y: 135, + }, + { + x: 1413555600000, + y: 118, + }, + { + x: 1413556200000, + y: 142, + }, + { + x: 1413556800000, + y: 141, + }, + { + x: 1413557400000, + y: 146, + }, + { + x: 1413558000000, + y: 114, + }, + { + x: 1413558600000, + y: 115, + }, + { + x: 1413559200000, + y: 136, + }, + { + x: 1413559800000, + y: 106, + }, + { + x: 1413560400000, + y: 92, + }, + { + x: 1413561000000, + y: 97, + }, + { + x: 1413561600000, + y: 90, + }, + { + x: 1413562200000, + y: 69, + }, + { + x: 1413562800000, + y: 66, + }, + { + x: 1413563400000, + y: 93, + }, + { + x: 1413564000000, + y: 75, + }, + { + x: 1413564600000, + y: 68, + }, + { + x: 1413565200000, + y: 55, + }, + { + x: 1413565800000, + y: 73, + }, + { + x: 1413566400000, + y: 57, + }, + { + x: 1413567000000, + y: 48, + }, + { + x: 1413567600000, + y: 41, + }, + { + x: 1413568200000, + y: 39, + }, + { + x: 1413568800000, + y: 32, + }, + { + x: 1413569400000, + y: 33, + }, + { + x: 1413570000000, + y: 39, + }, + { + x: 1413570600000, + y: 35, + }, + { + x: 1413571200000, + y: 25, + }, + { + x: 1413571800000, + y: 28, + }, + { + x: 1413572400000, + y: 8, + }, + { + x: 1413573600000, + y: 13, + }, + { + x: 1413574200000, + y: 23, + }, + { + x: 1413574800000, + y: 19, + }, + { + x: 1413575400000, + y: 16, + }, + { + x: 1413576000000, + y: 22, + }, + { + x: 1413576600000, + y: 13, + }, + { + x: 1413577200000, + y: 21, + }, + { + x: 1413577800000, + y: 11, + }, + { + x: 1413578400000, + y: 12, + }, + { + x: 1413579000000, + y: 10, + }, + { + x: 1413579600000, + y: 7, + }, + { + x: 1413580200000, + y: 4, + }, + { + x: 1413580800000, + y: 5, + }, + { + x: 1413581400000, + y: 7, + }, + { + x: 1413582000000, + y: 9, + }, + { + x: 1413582600000, + y: 2, + }, + { + x: 1413583200000, + y: 2, + }, + { + x: 1413583800000, + y: 4, + }, + { + x: 1413584400000, + y: 6, + }, + { + x: 1413585600000, + y: 2, + }, + { + x: 1413586200000, + y: 4, + }, + { + x: 1413586800000, + y: 4, + }, + ], + }, + ], + hits: 108970, + xAxisOrderedValues: [ + 1413543600000, + 1413544200000, + 1413544800000, + 1413545400000, + 1413546000000, + 1413546600000, + 1413547200000, + 1413547800000, + 1413548400000, + 1413549000000, + 1413549600000, + 1413550200000, + 1413550800000, + 1413551400000, + 1413552000000, + 1413552600000, + 1413553200000, + 1413553800000, + 1413554400000, + 1413555000000, + 1413555600000, + 1413556200000, + 1413556800000, + 1413557400000, + 1413558000000, + 1413558600000, + 1413559200000, + 1413559800000, + 1413560400000, + 1413561000000, + 1413561600000, + 1413562200000, + 1413562800000, + 1413563400000, + 1413564000000, + 1413564600000, + 1413565200000, + 1413565800000, + 1413566400000, + 1413567000000, + 1413567600000, + 1413568200000, + 1413568800000, + 1413569400000, + 1413570000000, + 1413570600000, + 1413571200000, + 1413571800000, + 1413572400000, + 1413573600000, + 1413574200000, + 1413574800000, + 1413575400000, + 1413576000000, + 1413576600000, + 1413577200000, + 1413577800000, + 1413578400000, + 1413579000000, + 1413579600000, + 1413580200000, + 1413580800000, + 1413581400000, + 1413582000000, + 1413582600000, + 1413583200000, + 1413583800000, + 1413584400000, + 1413585000000, + 1413585600000, + 1413586200000, + 1413586800000, + ], + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_columns.js new file mode 100644 index 0000000000000..8144a996e3424 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_columns.js @@ -0,0 +1,127 @@ +/* + * 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 _ from 'lodash'; + +export default { + columns: [ + { + label: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 10379, + }, + { + x: 'png', + y: 6395, + }, + ], + }, + ], + xAxisOrderedValues: ['css', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 9253, + }, + { + x: 'png', + y: 5571, + }, + ], + }, + ], + xAxisOrderedValues: ['css', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 7740, + }, + { + x: 'png', + y: 4697, + }, + ], + }, + ], + xAxisOrderedValues: ['css', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171443, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_rows.js new file mode 100644 index 0000000000000..e783246972e4a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_rows.js @@ -0,0 +1,122 @@ +/* + * 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 _ from 'lodash'; + +export default { + rows: [ + { + label: '200: response', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 25260, + }, + { + x: 'png', + y: 15311, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '404: response', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 1352, + }, + { + x: 'png', + y: 826, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '503: response', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 761, + }, + { + x: 'png', + y: 527, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171443, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_series.js new file mode 100644 index 0000000000000..71ee039f98938 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/filters/_series.js @@ -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 _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'filters', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'css', + y: 27374, + }, + { + x: 'html', + y: 0, + }, + { + x: 'png', + y: 16663, + }, + ], + }, + ], + hits: 171454, + xAxisOrderedValues: ['css', 'html', 'png'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_columns.js new file mode 100644 index 0000000000000..c1044160c0e7a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_columns.js @@ -0,0 +1,2918 @@ +/* + * 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 _ from 'lodash'; + +export default { + columns: [ + { + title: 'Top 2 geo.dest: CN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 42, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 42, + value: 42, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 31, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 30, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 25, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 25, + value: 25, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 22, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 22, + value: 22, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 22, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 22, + value: 22, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 21, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 21, + value: 21, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 19, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 19, + value: 19, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 18, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 18, + value: 18, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 11, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 10, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 9, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 9, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 8, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 8, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 6, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 6, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 4, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 3, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 2, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 1, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [90, -45], + [90, 0], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -67.5], + }, + properties: { + value: 1, + geohash: '5', + center: [-22.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '5', + value: '5', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -90], + [0, -90], + [0, -45], + [-45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 1, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -22.5], + }, + properties: { + value: 1, + geohash: '3', + center: [-112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '3', + value: '3', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, -45], + [-90, -45], + [-90, 0], + [-135, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 42, + }, + }, + }, + { + label: 'Top 2 geo.dest: IN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 32, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 32, + value: 32, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 31, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 28, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 28, + value: 28, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 27, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 27, + value: 27, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 24, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 24, + value: 24, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 23, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 23, + value: 23, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 17, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 17, + value: 17, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 16, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 16, + value: 16, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 14, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 14, + value: 14, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 13, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 13, + value: 13, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 9, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 6, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 6, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 6, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 5, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 5, + value: 5, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 4, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 3, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 2, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 1, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 1, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -67.5], + }, + properties: { + value: 1, + geohash: 'p', + center: [157.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'p', + value: 'p', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -90], + [180, -90], + [180, -45], + [135, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 1, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [90, -45], + [90, 0], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 1, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: false, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 32, + }, + }, + }, + ], +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_geo_json.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_geo_json.js new file mode 100644 index 0000000000000..a26dc9bd8b181 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_geo_json.js @@ -0,0 +1,1326 @@ +/* + * 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 _ from 'lodash'; + +export default { + valueFormatter: _.identity, + geohashGridAgg: { vis: { params: {} } }, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 608, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 608, + value: 608, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [0, 45], + [45, 45], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 522, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 522, + value: 522, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 90], + [0, 135], + [45, 135], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 517, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 517, + value: 517, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -90], + [-45, -45], + [0, -45], + [0, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 446, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 446, + value: 446, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -90], + [0, -45], + [45, -45], + [45, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 426, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 426, + value: 426, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [45, 45], + [90, 45], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 413, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 413, + value: 413, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [0, 90], + [45, 90], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 362, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 362, + value: 362, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [-45, 45], + [0, 45], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 352, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 352, + value: 352, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -135], + [0, -90], + [45, -90], + [45, -135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 216, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 216, + value: 216, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [0, 0], + [45, 0], + [45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 183, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 183, + value: 183, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [45, 90], + [90, 90], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 158, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 158, + value: 158, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 135], + [-45, 180], + [0, 180], + [0, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 139, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 139, + value: 139, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 90], + [45, 135], + [90, 135], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 110, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 110, + value: 110, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -135], + [45, -90], + [90, -90], + [90, -135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 101, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 101, + value: 101, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 90], + [-45, 135], + [0, 135], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 101, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 101, + value: 101, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [-45, 0], + [0, 0], + [0, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 92, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 92, + value: 92, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -90], + [45, -45], + [90, -45], + [90, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 75, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 75, + value: 75, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -180], + [45, -135], + [90, -135], + [90, -180], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 64, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 64, + value: 64, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [45, 0], + [90, 0], + [90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 36, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 36, + value: 36, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 135], + [45, 180], + [90, 180], + [90, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 34, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 34, + value: 34, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 135], + [0, 180], + [45, 180], + [45, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 30, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-90, -45], + [-45, -45], + [-45, -90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 16, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 16, + value: 16, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [-45, 90], + [0, 90], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -67.5], + }, + properties: { + value: 10, + geohash: '5', + center: [-22.5, -67.5], + aggConfigResult: { + $parent: { + key: '5', + value: '5', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-90, 0], + [-45, 0], + [-45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -67.5], + }, + properties: { + value: 6, + geohash: 'p', + center: [157.5, -67.5], + aggConfigResult: { + $parent: { + key: 'p', + value: 'p', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 135], + [-90, 180], + [-45, 180], + [-45, 135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, -22.5], + }, + properties: { + value: 6, + geohash: '2', + center: [-157.5, -22.5], + aggConfigResult: { + $parent: { + key: '2', + value: '2', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -180], + [-45, -135], + [0, -135], + [0, -180], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -67.5], + }, + properties: { + value: 4, + geohash: 'h', + center: [22.5, -67.5], + aggConfigResult: { + $parent: { + key: 'h', + value: 'h', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-90, 45], + [-45, 45], + [-45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -67.5], + }, + properties: { + value: 2, + geohash: 'n', + center: [112.5, -67.5], + aggConfigResult: { + $parent: { + key: 'n', + value: 'n', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 90], + [-90, 135], + [-45, 135], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -67.5], + }, + properties: { + value: 2, + geohash: 'j', + center: [67.5, -67.5], + aggConfigResult: { + $parent: { + key: 'j', + value: 'j', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-90, 90], + [-45, 90], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -22.5], + }, + properties: { + value: 1, + geohash: '3', + center: [-112.5, -22.5], + aggConfigResult: { + $parent: { + key: '3', + value: '3', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -135], + [-45, -90], + [0, -90], + [0, -135], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -67.5], + }, + properties: { + value: 1, + geohash: '1', + center: [-112.5, -67.5], + aggConfigResult: { + $parent: { + key: '1', + value: '1', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -135], + [-90, -90], + [-45, -90], + [-45, -135], + ], + }, + }, + ], + properties: { + min: 1, + max: 608, + zoom: 2, + center: [5, 15], + }, + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_rows.js new file mode 100644 index 0000000000000..ca4cb2a7feee1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/geohash/_rows.js @@ -0,0 +1,2858 @@ +/* + * 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 _ from 'lodash'; + +export default { + rows: [ + { + title: 'Top 2 geo.dest: CN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 39, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 39, + value: 39, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 31, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 30, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 25, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 25, + value: 25, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 23, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 23, + value: 23, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 23, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 23, + value: 23, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 22, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 22, + value: 22, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 20, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 20, + value: 20, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 18, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 18, + value: 18, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 11, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 11, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 10, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 10, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 10, + value: 10, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 8, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 8, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 8, + value: 8, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 6, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 6, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 4, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 3, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 2, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 2, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -67.5], + }, + properties: { + value: 1, + geohash: '5', + center: [-22.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '5', + value: '5', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -90], + [0, -90], + [0, -45], + [-45, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, -22.5], + }, + properties: { + value: 1, + geohash: '3', + center: [-112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'CN', + value: 'CN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '3', + value: '3', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, -45], + [-90, -45], + [-90, 0], + [-135, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 39, + }, + }, + }, + { + label: 'Top 2 geo.dest: IN', + valueFormatter: _.identity, + geoJson: { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -22.5], + }, + properties: { + value: 31, + geohash: '6', + center: [-67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '6', + value: '6', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 31, + value: 31, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -45], + [-45, -45], + [-45, 0], + [-90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 22.5], + }, + properties: { + value: 30, + geohash: 's', + center: [22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 's', + value: 's', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 30, + value: 30, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 0], + [45, 0], + [45, 45], + [0, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 22.5], + }, + properties: { + value: 29, + geohash: 'w', + center: [112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'w', + value: 'w', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 29, + value: 29, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 0], + [135, 0], + [135, 45], + [90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 22.5], + }, + properties: { + value: 28, + geohash: 'd', + center: [-67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'd', + value: 'd', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 28, + value: 28, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 0], + [-45, 0], + [-45, 45], + [-90, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 22.5], + }, + properties: { + value: 25, + geohash: 't', + center: [67.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 't', + value: 't', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 25, + value: 25, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 0], + [90, 0], + [90, 45], + [45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, -22.5], + }, + properties: { + value: 24, + geohash: 'k', + center: [22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'k', + value: 'k', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 24, + value: 24, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, -45], + [45, -45], + [45, 0], + [0, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [22.5, 67.5], + }, + properties: { + value: 20, + geohash: 'u', + center: [22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'u', + value: 'u', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 20, + value: 20, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [0, 45], + [45, 45], + [45, 90], + [0, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 22.5], + }, + properties: { + value: 18, + geohash: '9', + center: [-112.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '9', + value: '9', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 18, + value: 18, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 0], + [-90, 0], + [-90, 45], + [-135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, 67.5], + }, + properties: { + value: 14, + geohash: 'v', + center: [67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'v', + value: 'v', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 14, + value: 14, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, 45], + [90, 45], + [90, 90], + [45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 22.5], + }, + properties: { + value: 11, + geohash: 'e', + center: [-22.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'e', + value: 'e', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 11, + value: 11, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 0], + [0, 0], + [0, 45], + [-45, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -22.5], + }, + properties: { + value: 9, + geohash: 'r', + center: [157.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'r', + value: 'r', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 9, + value: 9, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -45], + [180, -45], + [180, 0], + [135, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, 67.5], + }, + properties: { + value: 6, + geohash: 'y', + center: [112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'y', + value: 'y', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, 45], + [135, 45], + [135, 90], + [90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, 67.5], + }, + properties: { + value: 6, + geohash: 'f', + center: [-67.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'f', + value: 'f', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 6, + value: 6, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, 45], + [-45, 45], + [-45, 90], + [-90, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, 67.5], + }, + properties: { + value: 5, + geohash: 'g', + center: [-22.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'g', + value: 'g', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 5, + value: 5, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, 45], + [0, 45], + [0, 90], + [-45, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-112.5, 67.5], + }, + properties: { + value: 5, + geohash: 'c', + center: [-112.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'c', + value: 'c', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 5, + value: 5, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-135, 45], + [-90, 45], + [-90, 90], + [-135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-157.5, 67.5], + }, + properties: { + value: 4, + geohash: 'b', + center: [-157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'b', + value: 'b', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 4, + value: 4, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-180, 45], + [-135, 45], + [-135, 90], + [-180, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [112.5, -22.5], + }, + properties: { + value: 3, + geohash: 'q', + center: [112.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'q', + value: 'q', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 3, + value: 3, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [90, -45], + [135, -45], + [135, 0], + [90, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-67.5, -67.5], + }, + properties: { + value: 2, + geohash: '4', + center: [-67.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '4', + value: '4', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 2, + value: 2, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-90, -90], + [-45, -90], + [-45, -45], + [-90, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 67.5], + }, + properties: { + value: 1, + geohash: 'z', + center: [157.5, 67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'z', + value: 'z', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 45], + [180, 45], + [180, 90], + [135, 90], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, 22.5], + }, + properties: { + value: 1, + geohash: 'x', + center: [157.5, 22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'x', + value: 'x', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, 0], + [180, 0], + [180, 45], + [135, 45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [157.5, -67.5], + }, + properties: { + value: 1, + geohash: 'p', + center: [157.5, -67.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'p', + value: 'p', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [135, -90], + [180, -90], + [180, -45], + [135, -45], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [67.5, -22.5], + }, + properties: { + value: 1, + geohash: 'm', + center: [67.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: 'm', + value: 'm', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [45, -45], + [90, -45], + [90, 0], + [45, 0], + ], + }, + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-22.5, -22.5], + }, + properties: { + value: 1, + geohash: '7', + center: [-22.5, -22.5], + aggConfigResult: { + $parent: { + $parent: { + $parent: null, + key: 'IN', + value: 'IN', + aggConfig: { + id: '3', + type: 'terms', + schema: 'split', + params: { + field: 'geo.dest', + size: 2, + order: 'desc', + orderBy: '1', + row: true, + }, + }, + type: 'bucket', + }, + key: '7', + value: '7', + aggConfig: { + id: '2', + type: 'geohash_grid', + schema: 'segment', + params: { + field: 'geo.coordinates', + precision: 1, + }, + }, + type: 'bucket', + }, + key: 1, + value: 1, + aggConfig: { + id: '1', + type: 'count', + schema: 'metric', + params: {}, + }, + type: 'metric', + }, + rectangle: [ + [-45, -45], + [0, -45], + [0, 0], + [-45, 0], + ], + }, + }, + ], + properties: { + min: 1, + max: 31, + }, + }, + }, + ], + hits: 1639, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns.js new file mode 100644 index 0000000000000..c93365234d158 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns.js @@ -0,0 +1,381 @@ +/* + * 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 _ from 'lodash'; + +export default { + columns: [ + { + label: '404: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 1, + y0: 0, + }, + { + x: 3221225400, + y: 0, + y0: 0, + }, + { + x: 4294967200, + y: 0, + y0: 0, + }, + { + x: 5368709100, + y: 0, + y0: 0, + }, + { + x: 6442450900, + y: 0, + y0: 0, + }, + { + x: 7516192700, + y: 0, + y0: 0, + }, + { + x: 8589934500, + y: 0, + y0: 0, + }, + { + x: 10737418200, + y: 0, + y0: 0, + }, + { + x: 11811160000, + y: 0, + y0: 0, + }, + { + x: 12884901800, + y: 1, + y0: 0, + }, + { + x: 13958643700, + y: 0, + y0: 0, + }, + { + x: 15032385500, + y: 0, + y0: 0, + }, + { + x: 16106127300, + y: 0, + y0: 0, + }, + { + x: 18253611000, + y: 0, + y0: 0, + }, + { + x: 19327352800, + y: 0, + y0: 0, + }, + { + x: 20401094600, + y: 0, + y0: 0, + }, + { + x: 21474836400, + y: 0, + y0: 0, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '200: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 0, + y0: 0, + }, + { + x: 3221225400, + y: 2, + y0: 0, + }, + { + x: 4294967200, + y: 3, + y0: 0, + }, + { + x: 5368709100, + y: 3, + y0: 0, + }, + { + x: 6442450900, + y: 1, + y0: 0, + }, + { + x: 7516192700, + y: 1, + y0: 0, + }, + { + x: 8589934500, + y: 4, + y0: 0, + }, + { + x: 10737418200, + y: 0, + y0: 0, + }, + { + x: 11811160000, + y: 1, + y0: 0, + }, + { + x: 12884901800, + y: 1, + y0: 0, + }, + { + x: 13958643700, + y: 1, + y0: 0, + }, + { + x: 15032385500, + y: 2, + y0: 0, + }, + { + x: 16106127300, + y: 3, + y0: 0, + }, + { + x: 18253611000, + y: 4, + y0: 0, + }, + { + x: 19327352800, + y: 5, + y0: 0, + }, + { + x: 20401094600, + y: 2, + y0: 0, + }, + { + x: 21474836400, + y: 2, + y0: 0, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '503: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 0, + y0: 0, + }, + { + x: 3221225400, + y: 0, + y0: 0, + }, + { + x: 4294967200, + y: 0, + y0: 0, + }, + { + x: 5368709100, + y: 0, + y0: 0, + }, + { + x: 6442450900, + y: 0, + y0: 0, + }, + { + x: 7516192700, + y: 0, + y0: 0, + }, + { + x: 8589934500, + y: 0, + y0: 0, + }, + { + x: 10737418200, + y: 1, + y0: 0, + }, + { + x: 11811160000, + y: 0, + y0: 0, + }, + { + x: 12884901800, + y: 0, + y0: 0, + }, + { + x: 13958643700, + y: 0, + y0: 0, + }, + { + x: 15032385500, + y: 0, + y0: 0, + }, + { + x: 16106127300, + y: 0, + y0: 0, + }, + { + x: 18253611000, + y: 0, + y0: 0, + }, + { + x: 19327352800, + y: 0, + y0: 0, + }, + { + x: 20401094600, + y: 0, + y0: 0, + }, + { + x: 21474836400, + y: 0, + y0: 0, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: [ + 2147483600, + 3221225400, + 4294967200, + 5368709100, + 6442450900, + 7516192700, + 8589934500, + 10737418200, + 11811160000, + 12884901800, + 13958643700, + 15032385500, + 16106127300, + 18253611000, + 19327352800, + 20401094600, + 21474836400, + ], + hits: 40, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows.js new file mode 100644 index 0000000000000..d88197c3737e5 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows.js @@ -0,0 +1,225 @@ +/* + * 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 _ from 'lodash'; + +export default { + rows: [ + { + label: '404: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 2147483600, + y: 1, + }, + { + x: 10737418200, + y: 1, + }, + { + x: 15032385500, + y: 2, + }, + { + x: 19327352800, + y: 1, + }, + { + x: 32212254700, + y: 1, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '200: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 3221225400, + y: 4, + }, + { + x: 4294967200, + y: 3, + }, + { + x: 5368709100, + y: 3, + }, + { + x: 6442450900, + y: 2, + }, + { + x: 7516192700, + y: 2, + }, + { + x: 8589934500, + y: 2, + }, + { + x: 9663676400, + y: 3, + }, + { + x: 11811160000, + y: 3, + }, + { + x: 12884901800, + y: 2, + }, + { + x: 13958643700, + y: 1, + }, + { + x: 15032385500, + y: 2, + }, + { + x: 16106127300, + y: 3, + }, + { + x: 17179869100, + y: 1, + }, + { + x: 18253611000, + y: 4, + }, + { + x: 19327352800, + y: 1, + }, + { + x: 20401094600, + y: 1, + }, + { + x: 21474836400, + y: 4, + }, + { + x: 32212254700, + y: 3, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '503: response', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 10737418200, + y: 1, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: [ + 2147483600, + 3221225400, + 4294967200, + 5368709100, + 6442450900, + 7516192700, + 8589934500, + 9663676400, + 10737418200, + 11811160000, + 12884901800, + 13958643700, + 15032385500, + 16106127300, + 17179869100, + 18253611000, + 19327352800, + 20401094600, + 21474836400, + 32212254700, + ], + hits: 51, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_series.js new file mode 100644 index 0000000000000..99511e693ff02 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_series.js @@ -0,0 +1,141 @@ +/* + * 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 _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'machine.ram', + ordered: { + interval: 100, + }, + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 3221225400, + y: 5, + }, + { + x: 4294967200, + y: 2, + }, + { + x: 5368709100, + y: 5, + }, + { + x: 6442450900, + y: 4, + }, + { + x: 7516192700, + y: 1, + }, + { + x: 9663676400, + y: 9, + }, + { + x: 10737418200, + y: 5, + }, + { + x: 11811160000, + y: 5, + }, + { + x: 12884901800, + y: 2, + }, + { + x: 13958643700, + y: 3, + }, + { + x: 15032385500, + y: 3, + }, + { + x: 16106127300, + y: 3, + }, + { + x: 17179869100, + y: 1, + }, + { + x: 18253611000, + y: 6, + }, + { + x: 19327352800, + y: 3, + }, + { + x: 20401094600, + y: 3, + }, + { + x: 21474836400, + y: 7, + }, + { + x: 32212254700, + y: 4, + }, + ], + }, + ], + hits: 71, + xAxisOrderedValues: [ + 3221225400, + 4294967200, + 5368709100, + 6442450900, + 7516192700, + 9663676400, + 10737418200, + 11811160000, + 12884901800, + 13958643700, + 15032385500, + 16106127300, + 17179869100, + 18253611000, + 19327352800, + 20401094600, + 21474836400, + 32212254700, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_slices.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_slices.js new file mode 100644 index 0000000000000..c23a89b755b5b --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_slices.js @@ -0,0 +1,328 @@ +/* + * 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 _ from 'lodash'; + +export default { + label: '', + slices: { + children: [ + { + name: 0, + size: 378611, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 1000, + size: 205997, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 2000, + size: 397189, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 3000, + size: 397195, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 4000, + size: 398429, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 5000, + size: 397843, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 6000, + size: 398140, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 7000, + size: 398076, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 8000, + size: 396746, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 9000, + size: 397418, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 10000, + size: 20222, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 11000, + size: 20173, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 12000, + size: 20026, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 13000, + size: 19986, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 14000, + size: 20091, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 15000, + size: 20052, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 16000, + size: 20349, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 17000, + size: 20290, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 18000, + size: 20399, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 19000, + size: 20133, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + { + name: 20000, + size: 9, + aggConfig: { + type: 'histogram', + schema: 'segment', + fieldFormatter: _.constant(String), + params: { + interval: 1000, + extended_bounds: {}, + }, + }, + }, + ], + }, + names: [ + 0, + 1000, + 2000, + 3000, + 4000, + 5000, + 6000, + 7000, + 8000, + 9000, + 10000, + 11000, + 12000, + 13000, + 14000, + 15000, + 16000, + 17000, + 18000, + 19000, + 20000, + ], + hits: 3967374, + tooltipFormatter: function(event) { + return event.point; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/not_enough_data/_one_point.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/not_enough_data/_one_point.js new file mode 100644 index 0000000000000..df71f4efc58b5 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/not_enough_data/_one_point.js @@ -0,0 +1,51 @@ +/* + * 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 _ from 'lodash'; + +export default { + label: '', + xAxisLabel: '', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '_all', + y: 274, + }, + ], + }, + ], + hits: 274, + xAxisOrderedValues: ['_all'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_columns.js new file mode 100644 index 0000000000000..b5b931383f732 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_columns.js @@ -0,0 +1,79 @@ +/* + * 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 _ from 'lodash'; + +export default { + columns: [ + { + label: 'apache: _type', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 13309, + }, + { + x: '1000.0-2000.0', + y: 7196, + }, + ], + }, + ], + }, + { + label: 'nginx: _type', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 3278, + }, + { + x: '1000.0-2000.0', + y: 1804, + }, + ], + }, + ], + }, + ], + hits: 171499, + xAxisOrderedValues: ['0.0-1000.0', '1000.0-2000.0'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows.js new file mode 100644 index 0000000000000..bc7e4c9f49625 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows.js @@ -0,0 +1,107 @@ +/* + * 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 _ from 'lodash'; + +export default { + rows: [ + { + label: 'Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1: agent.raw', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 6422, + y0: 0, + }, + { + x: '1000.0-2000.0', + y: 3446, + y0: 0, + }, + ], + }, + ], + }, + { + label: + 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24: agent.raw', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 5430, + y0: 0, + }, + { + x: '1000.0-2000.0', + y: 3010, + y0: 0, + }, + ], + }, + ], + }, + { + label: + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322): agent.raw', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 4735, + y0: 0, + }, + { + x: '1000.0-2000.0', + y: 2542, + y0: 0, + }, + ], + }, + ], + }, + ], + hits: 171501, + xAxisOrderedValues: ['0.0-1000.0', '1000.0-2000.0'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_series.js new file mode 100644 index 0000000000000..40c14beeb4f3e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/range/_series.js @@ -0,0 +1,55 @@ +/* + * 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 _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'bytes ranges', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: '0.0-1000.0', + y: 16576, + }, + { + x: '1000.0-2000.0', + y: 9005, + }, + ], + }, + ], + hits: 171500, + xAxisOrderedValues: ['0.0-1000.0', '1000.0-2000.0'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_columns.js new file mode 100644 index 0000000000000..bf4fcb7e9e526 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_columns.js @@ -0,0 +1,251 @@ +/* + * 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 _ from 'lodash'; + +export default { + columns: [ + { + label: 'http: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128237, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10258, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'info: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 108148, + }, + { + x: 'info', + y: 96242, + }, + { + x: 'security', + y: 25889, + }, + { + x: 'error', + y: 7673, + }, + { + x: 'warning', + y: 12842, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'www.slate.com: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 98056, + }, + { + x: 'info', + y: 87344, + }, + { + x: 'security', + y: 23577, + }, + { + x: 'error', + y: 7004, + }, + { + x: 'warning', + y: 11759, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'twitter.com: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 74154, + }, + { + x: 'info', + y: 65963, + }, + { + x: 'security', + y: 17832, + }, + { + x: 'error', + y: 5258, + }, + { + x: 'warning', + y: 8906, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'www.www.slate.com: links', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 62591, + }, + { + x: 'info', + y: 55822, + }, + { + x: 'security', + y: 15100, + }, + { + x: 'error', + y: 4564, + }, + { + x: 'warning', + y: 7498, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171446, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_rows.js new file mode 100644 index 0000000000000..5d737131dc998 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_rows.js @@ -0,0 +1,251 @@ +/* + * 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 _ from 'lodash'; + +export default { + rows: [ + { + label: 'h3: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128235, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10257, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'h5: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128235, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10257, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'http: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 144000, + }, + { + x: 'info', + y: 128235, + }, + { + x: 'security', + y: 34518, + }, + { + x: 'error', + y: 10257, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'success: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 120689, + }, + { + x: 'info', + y: 107621, + }, + { + x: 'security', + y: 28916, + }, + { + x: 'error', + y: 8590, + }, + { + x: 'warning', + y: 14548, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: 'www.slate.com: headings', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 62292, + }, + { + x: 'info', + y: 55646, + }, + { + x: 'security', + y: 14823, + }, + { + x: 'error', + y: 4441, + }, + { + x: 'warning', + y: 7539, + }, + ], + }, + ], + xAxisOrderedValues: ['success', 'info', 'security', 'error', 'warning'], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + hits: 171445, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_series.js new file mode 100644 index 0000000000000..36df8e091ba89 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/significant_terms/_series.js @@ -0,0 +1,66 @@ +/* + * 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 _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'Top 5 unusual terms in @tags', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'success', + y: 143995, + }, + { + x: 'info', + y: 128233, + }, + { + x: 'security', + y: 34515, + }, + { + x: 'error', + y: 10256, + }, + { + x: 'warning', + y: 17188, + }, + ], + }, + ], + hits: 171439, + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/stacked/_stacked.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/stacked/_stacked.js new file mode 100644 index 0000000000000..a914f20a7ffc6 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/stacked/_stacked.js @@ -0,0 +1,1654 @@ +/* + * 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 moment from 'moment'; + +export default { + label: '', + xAxisLabel: '@timestamp per 30 sec', + ordered: { + date: true, + interval: 30000, + min: 1416850340336, + max: 1416852140336, + }, + yAxisLabel: 'Count of documents', + xAxisOrderedValues: [ + 1416850320000, + 1416850350000, + 1416850380000, + 1416850410000, + 1416850440000, + 1416850470000, + 1416850500000, + 1416850530000, + 1416850560000, + 1416850590000, + 1416850620000, + 1416850650000, + 1416850680000, + 1416850710000, + 1416850740000, + 1416850770000, + 1416850800000, + 1416850830000, + 1416850860000, + 1416850890000, + 1416850920000, + 1416850950000, + 1416850980000, + 1416851010000, + 1416851040000, + 1416851070000, + 1416851100000, + 1416851130000, + 1416851160000, + 1416851190000, + 1416851220000, + 1416851250000, + 1416851280000, + 1416851310000, + 1416851340000, + 1416851370000, + 1416851400000, + 1416851430000, + 1416851460000, + 1416851490000, + 1416851520000, + 1416851550000, + 1416851580000, + 1416851610000, + 1416851640000, + 1416851670000, + 1416851700000, + 1416851730000, + 1416851760000, + 1416851790000, + 1416851820000, + 1416851850000, + 1416851880000, + 1416851910000, + 1416851940000, + 1416851970000, + 1416852000000, + 1416852030000, + 1416852060000, + 1416852090000, + 1416852120000, + ], + series: [ + { + label: 'jpg', + values: [ + { + x: 1416850320000, + y: 110, + y0: 0, + }, + { + x: 1416850350000, + y: 24, + y0: 0, + }, + { + x: 1416850380000, + y: 34, + y0: 0, + }, + { + x: 1416850410000, + y: 21, + y0: 0, + }, + { + x: 1416850440000, + y: 32, + y0: 0, + }, + { + x: 1416850470000, + y: 24, + y0: 0, + }, + { + x: 1416850500000, + y: 16, + y0: 0, + }, + { + x: 1416850530000, + y: 27, + y0: 0, + }, + { + x: 1416850560000, + y: 24, + y0: 0, + }, + { + x: 1416850590000, + y: 38, + y0: 0, + }, + { + x: 1416850620000, + y: 33, + y0: 0, + }, + { + x: 1416850650000, + y: 33, + y0: 0, + }, + { + x: 1416850680000, + y: 31, + y0: 0, + }, + { + x: 1416850710000, + y: 24, + y0: 0, + }, + { + x: 1416850740000, + y: 24, + y0: 0, + }, + { + x: 1416850770000, + y: 38, + y0: 0, + }, + { + x: 1416850800000, + y: 34, + y0: 0, + }, + { + x: 1416850830000, + y: 30, + y0: 0, + }, + { + x: 1416850860000, + y: 38, + y0: 0, + }, + { + x: 1416850890000, + y: 19, + y0: 0, + }, + { + x: 1416850920000, + y: 23, + y0: 0, + }, + { + x: 1416850950000, + y: 33, + y0: 0, + }, + { + x: 1416850980000, + y: 28, + y0: 0, + }, + { + x: 1416851010000, + y: 24, + y0: 0, + }, + { + x: 1416851040000, + y: 22, + y0: 0, + }, + { + x: 1416851070000, + y: 28, + y0: 0, + }, + { + x: 1416851100000, + y: 27, + y0: 0, + }, + { + x: 1416851130000, + y: 32, + y0: 0, + }, + { + x: 1416851160000, + y: 32, + y0: 0, + }, + { + x: 1416851190000, + y: 30, + y0: 0, + }, + { + x: 1416851220000, + y: 32, + y0: 0, + }, + { + x: 1416851250000, + y: 36, + y0: 0, + }, + { + x: 1416851280000, + y: 32, + y0: 0, + }, + { + x: 1416851310000, + y: 29, + y0: 0, + }, + { + x: 1416851340000, + y: 22, + y0: 0, + }, + { + x: 1416851370000, + y: 29, + y0: 0, + }, + { + x: 1416851400000, + y: 33, + y0: 0, + }, + { + x: 1416851430000, + y: 28, + y0: 0, + }, + { + x: 1416851460000, + y: 39, + y0: 0, + }, + { + x: 1416851490000, + y: 28, + y0: 0, + }, + { + x: 1416851520000, + y: 28, + y0: 0, + }, + { + x: 1416851550000, + y: 28, + y0: 0, + }, + { + x: 1416851580000, + y: 30, + y0: 0, + }, + { + x: 1416851610000, + y: 29, + y0: 0, + }, + { + x: 1416851640000, + y: 30, + y0: 0, + }, + { + x: 1416851670000, + y: 23, + y0: 0, + }, + { + x: 1416851700000, + y: 23, + y0: 0, + }, + { + x: 1416851730000, + y: 27, + y0: 0, + }, + { + x: 1416851760000, + y: 21, + y0: 0, + }, + { + x: 1416851790000, + y: 24, + y0: 0, + }, + { + x: 1416851820000, + y: 26, + y0: 0, + }, + { + x: 1416851850000, + y: 26, + y0: 0, + }, + { + x: 1416851880000, + y: 21, + y0: 0, + }, + { + x: 1416851910000, + y: 33, + y0: 0, + }, + { + x: 1416851940000, + y: 23, + y0: 0, + }, + { + x: 1416851970000, + y: 46, + y0: 0, + }, + { + x: 1416852000000, + y: 27, + y0: 0, + }, + { + x: 1416852030000, + y: 20, + y0: 0, + }, + { + x: 1416852060000, + y: 34, + y0: 0, + }, + { + x: 1416852090000, + y: 15, + y0: 0, + }, + { + x: 1416852120000, + y: 18, + y0: 0, + }, + ], + }, + { + label: 'css', + values: [ + { + x: 1416850320000, + y: 3, + y0: 11, + }, + { + x: 1416850350000, + y: 13, + y0: 24, + }, + { + x: 1416850380000, + y: 5, + y0: 34, + }, + { + x: 1416850410000, + y: 12, + y0: 21, + }, + { + x: 1416850440000, + y: 9, + y0: 32, + }, + { + x: 1416850470000, + y: 12, + y0: 24, + }, + { + x: 1416850500000, + y: 6, + y0: 16, + }, + { + x: 1416850530000, + y: 6, + y0: 27, + }, + { + x: 1416850560000, + y: 11, + y0: 24, + }, + { + x: 1416850590000, + y: 11, + y0: 38, + }, + { + x: 1416850620000, + y: 6, + y0: 33, + }, + { + x: 1416850650000, + y: 8, + y0: 33, + }, + { + x: 1416850680000, + y: 6, + y0: 31, + }, + { + x: 1416850710000, + y: 4, + y0: 24, + }, + { + x: 1416850740000, + y: 9, + y0: 24, + }, + { + x: 1416850770000, + y: 3, + y0: 38, + }, + { + x: 1416850800000, + y: 5, + y0: 34, + }, + { + x: 1416850830000, + y: 6, + y0: 30, + }, + { + x: 1416850860000, + y: 9, + y0: 38, + }, + { + x: 1416850890000, + y: 5, + y0: 19, + }, + { + x: 1416850920000, + y: 8, + y0: 23, + }, + { + x: 1416850950000, + y: 9, + y0: 33, + }, + { + x: 1416850980000, + y: 5, + y0: 28, + }, + { + x: 1416851010000, + y: 6, + y0: 24, + }, + { + x: 1416851040000, + y: 9, + y0: 22, + }, + { + x: 1416851070000, + y: 9, + y0: 28, + }, + { + x: 1416851100000, + y: 11, + y0: 27, + }, + { + x: 1416851130000, + y: 5, + y0: 32, + }, + { + x: 1416851160000, + y: 8, + y0: 32, + }, + { + x: 1416851190000, + y: 6, + y0: 30, + }, + { + x: 1416851220000, + y: 10, + y0: 32, + }, + { + x: 1416851250000, + y: 5, + y0: 36, + }, + { + x: 1416851280000, + y: 6, + y0: 32, + }, + { + x: 1416851310000, + y: 4, + y0: 29, + }, + { + x: 1416851340000, + y: 8, + y0: 22, + }, + { + x: 1416851370000, + y: 3, + y0: 29, + }, + { + x: 1416851400000, + y: 8, + y0: 33, + }, + { + x: 1416851430000, + y: 10, + y0: 28, + }, + { + x: 1416851460000, + y: 5, + y0: 39, + }, + { + x: 1416851490000, + y: 7, + y0: 28, + }, + { + x: 1416851520000, + y: 6, + y0: 28, + }, + { + x: 1416851550000, + y: 4, + y0: 28, + }, + { + x: 1416851580000, + y: 9, + y0: 30, + }, + { + x: 1416851610000, + y: 3, + y0: 29, + }, + { + x: 1416851640000, + y: 9, + y0: 30, + }, + { + x: 1416851670000, + y: 6, + y0: 23, + }, + { + x: 1416851700000, + y: 11, + y0: 23, + }, + { + x: 1416851730000, + y: 4, + y0: 27, + }, + { + x: 1416851760000, + y: 8, + y0: 21, + }, + { + x: 1416851790000, + y: 5, + y0: 24, + }, + { + x: 1416851820000, + y: 7, + y0: 26, + }, + { + x: 1416851850000, + y: 7, + y0: 26, + }, + { + x: 1416851880000, + y: 4, + y0: 21, + }, + { + x: 1416851910000, + y: 8, + y0: 33, + }, + { + x: 1416851940000, + y: 6, + y0: 23, + }, + { + x: 1416851970000, + y: 6, + y0: 46, + }, + { + x: 1416852000000, + y: 3, + y0: 27, + }, + { + x: 1416852030000, + y: 6, + y0: 20, + }, + { + x: 1416852060000, + y: 5, + y0: 34, + }, + { + x: 1416852090000, + y: 5, + y0: 15, + }, + { + x: 1416852120000, + y: 1, + y0: 18, + }, + ], + }, + { + label: 'gif', + values: [ + { + x: 1416850320000, + y: 1, + y0: 14, + }, + { + x: 1416850350000, + y: 2, + y0: 37, + }, + { + x: 1416850380000, + y: 4, + y0: 39, + }, + { + x: 1416850410000, + y: 2, + y0: 33, + }, + { + x: 1416850440000, + y: 3, + y0: 41, + }, + { + x: 1416850470000, + y: 1, + y0: 36, + }, + { + x: 1416850500000, + y: 1, + y0: 22, + }, + { + x: 1416850530000, + y: 1, + y0: 33, + }, + { + x: 1416850560000, + y: 2, + y0: 35, + }, + { + x: 1416850590000, + y: 5, + y0: 49, + }, + { + x: 1416850620000, + y: 1, + y0: 39, + }, + { + x: 1416850650000, + y: 1, + y0: 41, + }, + { + x: 1416850680000, + y: 4, + y0: 37, + }, + { + x: 1416850710000, + y: 1, + y0: 28, + }, + { + x: 1416850740000, + y: 3, + y0: 33, + }, + { + x: 1416850770000, + y: 2, + y0: 41, + }, + { + x: 1416850800000, + y: 2, + y0: 39, + }, + { + x: 1416850830000, + y: 5, + y0: 36, + }, + { + x: 1416850860000, + y: 3, + y0: 47, + }, + { + x: 1416850890000, + y: 1, + y0: 24, + }, + { + x: 1416850920000, + y: 3, + y0: 31, + }, + { + x: 1416850950000, + y: 4, + y0: 42, + }, + { + x: 1416850980000, + y: 3, + y0: 33, + }, + { + x: 1416851010000, + y: 5, + y0: 30, + }, + { + x: 1416851040000, + y: 2, + y0: 31, + }, + { + x: 1416851070000, + y: 3, + y0: 37, + }, + { + x: 1416851100000, + y: 5, + y0: 38, + }, + { + x: 1416851130000, + y: 3, + y0: 37, + }, + { + x: 1416851160000, + y: 4, + y0: 40, + }, + { + x: 1416851190000, + y: 9, + y0: 36, + }, + { + x: 1416851220000, + y: 7, + y0: 42, + }, + { + x: 1416851250000, + y: 2, + y0: 41, + }, + { + x: 1416851280000, + y: 1, + y0: 38, + }, + { + x: 1416851310000, + y: 2, + y0: 33, + }, + { + x: 1416851340000, + y: 5, + y0: 30, + }, + { + x: 1416851370000, + y: 3, + y0: 32, + }, + { + x: 1416851400000, + y: 5, + y0: 41, + }, + { + x: 1416851430000, + y: 4, + y0: 38, + }, + { + x: 1416851460000, + y: 5, + y0: 44, + }, + { + x: 1416851490000, + y: 2, + y0: 35, + }, + { + x: 1416851520000, + y: 2, + y0: 34, + }, + { + x: 1416851550000, + y: 4, + y0: 32, + }, + { + x: 1416851580000, + y: 3, + y0: 39, + }, + { + x: 1416851610000, + y: 4, + y0: 32, + }, + { + x: 1416851640000, + y: 0, + y0: 39, + }, + { + x: 1416851670000, + y: 2, + y0: 29, + }, + { + x: 1416851700000, + y: 1, + y0: 34, + }, + { + x: 1416851730000, + y: 3, + y0: 31, + }, + { + x: 1416851760000, + y: 0, + y0: 29, + }, + { + x: 1416851790000, + y: 4, + y0: 29, + }, + { + x: 1416851820000, + y: 3, + y0: 33, + }, + { + x: 1416851850000, + y: 3, + y0: 33, + }, + { + x: 1416851880000, + y: 0, + y0: 25, + }, + { + x: 1416851910000, + y: 0, + y0: 41, + }, + { + x: 1416851940000, + y: 3, + y0: 29, + }, + { + x: 1416851970000, + y: 3, + y0: 52, + }, + { + x: 1416852000000, + y: 1, + y0: 30, + }, + { + x: 1416852030000, + y: 5, + y0: 26, + }, + { + x: 1416852060000, + y: 3, + y0: 39, + }, + { + x: 1416852090000, + y: 1, + y0: 20, + }, + { + x: 1416852120000, + y: 2, + y0: 19, + }, + ], + }, + { + label: 'png', + values: [ + { + x: 1416850320000, + y: 1, + y0: 15, + }, + { + x: 1416850350000, + y: 6, + y0: 39, + }, + { + x: 1416850380000, + y: 6, + y0: 43, + }, + { + x: 1416850410000, + y: 5, + y0: 35, + }, + { + x: 1416850440000, + y: 3, + y0: 44, + }, + { + x: 1416850470000, + y: 5, + y0: 37, + }, + { + x: 1416850500000, + y: 6, + y0: 23, + }, + { + x: 1416850530000, + y: 1, + y0: 34, + }, + { + x: 1416850560000, + y: 3, + y0: 37, + }, + { + x: 1416850590000, + y: 2, + y0: 54, + }, + { + x: 1416850620000, + y: 1, + y0: 40, + }, + { + x: 1416850650000, + y: 1, + y0: 42, + }, + { + x: 1416850680000, + y: 2, + y0: 41, + }, + { + x: 1416850710000, + y: 5, + y0: 29, + }, + { + x: 1416850740000, + y: 7, + y0: 36, + }, + { + x: 1416850770000, + y: 2, + y0: 43, + }, + { + x: 1416850800000, + y: 3, + y0: 41, + }, + { + x: 1416850830000, + y: 6, + y0: 41, + }, + { + x: 1416850860000, + y: 2, + y0: 50, + }, + { + x: 1416850890000, + y: 4, + y0: 25, + }, + { + x: 1416850920000, + y: 2, + y0: 34, + }, + { + x: 1416850950000, + y: 3, + y0: 46, + }, + { + x: 1416850980000, + y: 8, + y0: 36, + }, + { + x: 1416851010000, + y: 4, + y0: 35, + }, + { + x: 1416851040000, + y: 4, + y0: 33, + }, + { + x: 1416851070000, + y: 1, + y0: 40, + }, + { + x: 1416851100000, + y: 2, + y0: 43, + }, + { + x: 1416851130000, + y: 4, + y0: 40, + }, + { + x: 1416851160000, + y: 3, + y0: 44, + }, + { + x: 1416851190000, + y: 4, + y0: 45, + }, + { + x: 1416851220000, + y: 2, + y0: 49, + }, + { + x: 1416851250000, + y: 4, + y0: 43, + }, + { + x: 1416851280000, + y: 8, + y0: 39, + }, + { + x: 1416851310000, + y: 4, + y0: 35, + }, + { + x: 1416851340000, + y: 4, + y0: 35, + }, + { + x: 1416851370000, + y: 7, + y0: 35, + }, + { + x: 1416851400000, + y: 2, + y0: 46, + }, + { + x: 1416851430000, + y: 3, + y0: 42, + }, + { + x: 1416851460000, + y: 3, + y0: 49, + }, + { + x: 1416851490000, + y: 3, + y0: 37, + }, + { + x: 1416851520000, + y: 4, + y0: 36, + }, + { + x: 1416851550000, + y: 3, + y0: 36, + }, + { + x: 1416851580000, + y: 4, + y0: 42, + }, + { + x: 1416851610000, + y: 5, + y0: 36, + }, + { + x: 1416851640000, + y: 3, + y0: 39, + }, + { + x: 1416851670000, + y: 3, + y0: 31, + }, + { + x: 1416851700000, + y: 2, + y0: 35, + }, + { + x: 1416851730000, + y: 5, + y0: 34, + }, + { + x: 1416851760000, + y: 4, + y0: 29, + }, + { + x: 1416851790000, + y: 5, + y0: 33, + }, + { + x: 1416851820000, + y: 1, + y0: 36, + }, + { + x: 1416851850000, + y: 3, + y0: 36, + }, + { + x: 1416851880000, + y: 6, + y0: 25, + }, + { + x: 1416851910000, + y: 4, + y0: 41, + }, + { + x: 1416851940000, + y: 7, + y0: 32, + }, + { + x: 1416851970000, + y: 5, + y0: 55, + }, + { + x: 1416852000000, + y: 2, + y0: 31, + }, + { + x: 1416852030000, + y: 2, + y0: 31, + }, + { + x: 1416852060000, + y: 4, + y0: 42, + }, + { + x: 1416852090000, + y: 6, + y0: 21, + }, + { + x: 1416852120000, + y: 2, + y0: 21, + }, + ], + }, + { + label: 'php', + values: [ + { + x: 1416850320000, + y: 0, + y0: 16, + }, + { + x: 1416850350000, + y: 1, + y0: 45, + }, + { + x: 1416850380000, + y: 0, + y0: 49, + }, + { + x: 1416850410000, + y: 2, + y0: 40, + }, + { + x: 1416850440000, + y: 0, + y0: 47, + }, + { + x: 1416850470000, + y: 0, + y0: 42, + }, + { + x: 1416850500000, + y: 3, + y0: 29, + }, + { + x: 1416850530000, + y: 1, + y0: 35, + }, + { + x: 1416850560000, + y: 3, + y0: 40, + }, + { + x: 1416850590000, + y: 2, + y0: 56, + }, + { + x: 1416850620000, + y: 2, + y0: 41, + }, + { + x: 1416850650000, + y: 5, + y0: 43, + }, + { + x: 1416850680000, + y: 2, + y0: 43, + }, + { + x: 1416850710000, + y: 1, + y0: 34, + }, + { + x: 1416850740000, + y: 2, + y0: 43, + }, + { + x: 1416850770000, + y: 2, + y0: 45, + }, + { + x: 1416850800000, + y: 1, + y0: 44, + }, + { + x: 1416850830000, + y: 1, + y0: 47, + }, + { + x: 1416850860000, + y: 1, + y0: 52, + }, + { + x: 1416850890000, + y: 1, + y0: 29, + }, + { + x: 1416850920000, + y: 2, + y0: 36, + }, + { + x: 1416850950000, + y: 2, + y0: 49, + }, + { + x: 1416850980000, + y: 0, + y0: 44, + }, + { + x: 1416851010000, + y: 3, + y0: 39, + }, + { + x: 1416851040000, + y: 2, + y0: 37, + }, + { + x: 1416851070000, + y: 2, + y0: 41, + }, + { + x: 1416851100000, + y: 2, + y0: 45, + }, + { + x: 1416851130000, + y: 0, + y0: 44, + }, + { + x: 1416851160000, + y: 1, + y0: 47, + }, + { + x: 1416851190000, + y: 2, + y0: 49, + }, + { + x: 1416851220000, + y: 4, + y0: 51, + }, + { + x: 1416851250000, + y: 0, + y0: 47, + }, + { + x: 1416851280000, + y: 3, + y0: 47, + }, + { + x: 1416851310000, + y: 3, + y0: 39, + }, + { + x: 1416851340000, + y: 2, + y0: 39, + }, + { + x: 1416851370000, + y: 2, + y0: 42, + }, + { + x: 1416851400000, + y: 3, + y0: 48, + }, + { + x: 1416851430000, + y: 1, + y0: 45, + }, + { + x: 1416851460000, + y: 0, + y0: 52, + }, + { + x: 1416851490000, + y: 2, + y0: 40, + }, + { + x: 1416851520000, + y: 1, + y0: 40, + }, + { + x: 1416851550000, + y: 3, + y0: 39, + }, + { + x: 1416851580000, + y: 1, + y0: 46, + }, + { + x: 1416851610000, + y: 2, + y0: 41, + }, + { + x: 1416851640000, + y: 1, + y0: 42, + }, + { + x: 1416851670000, + y: 2, + y0: 34, + }, + { + x: 1416851700000, + y: 3, + y0: 37, + }, + { + x: 1416851730000, + y: 1, + y0: 39, + }, + { + x: 1416851760000, + y: 1, + y0: 33, + }, + { + x: 1416851790000, + y: 1, + y0: 38, + }, + { + x: 1416851820000, + y: 1, + y0: 37, + }, + { + x: 1416851850000, + y: 1, + y0: 39, + }, + { + x: 1416851880000, + y: 1, + y0: 31, + }, + { + x: 1416851910000, + y: 2, + y0: 45, + }, + { + x: 1416851940000, + y: 0, + y0: 39, + }, + { + x: 1416851970000, + y: 0, + y0: 60, + }, + { + x: 1416852000000, + y: 1, + y0: 33, + }, + { + x: 1416852030000, + y: 2, + y0: 33, + }, + { + x: 1416852060000, + y: 1, + y0: 46, + }, + { + x: 1416852090000, + y: 1, + y0: 27, + }, + { + x: 1416852120000, + y: 0, + y0: 23, + }, + ], + }, + ], + hits: 2595, + xAxisFormatter: function(thing) { + return moment(thing); + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns.js new file mode 100644 index 0000000000000..8891d9badb2be --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns.js @@ -0,0 +1,159 @@ +/* + * 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 _ from 'lodash'; + +export default { + columns: [ + { + label: 'logstash: index', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 110710, + }, + { + x: 'css', + y: 27376, + }, + { + x: 'png', + y: 16664, + }, + { + x: 'gif', + y: 11264, + }, + { + x: 'php', + y: 5448, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '2014.11.12: index', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 110643, + }, + { + x: 'css', + y: 27350, + }, + { + x: 'png', + y: 16648, + }, + { + x: 'gif', + y: 11257, + }, + { + x: 'php', + y: 5440, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '2014.11.11: index', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 67, + }, + { + x: 'css', + y: 26, + }, + { + x: 'png', + y: 16, + }, + { + x: 'gif', + y: 7, + }, + { + x: 'php', + y: 8, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: ['jpg', 'css', 'png', 'gif', 'php'], + hits: 171462, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_rows.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_rows.js new file mode 100644 index 0000000000000..09a1c15989760 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_rows.js @@ -0,0 +1,115 @@ +/* + * 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 _ from 'lodash'; + +export default { + rows: [ + { + label: '0.0-1000.0: bytes', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 3378, + }, + { + x: 'css', + y: 762, + }, + { + x: 'png', + y: 527, + }, + { + x: 'gif', + y: 11258, + }, + { + x: 'php', + y: 653, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + { + label: '1000.0-2000.0: bytes', + xAxisLabel: 'Top 5 extension', + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 6422, + }, + { + x: 'css', + y: 1591, + }, + { + x: 'png', + y: 430, + }, + { + x: 'gif', + y: 8, + }, + { + x: 'php', + y: 561, + }, + ], + }, + ], + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, + }, + ], + xAxisOrderedValues: ['jpg', 'css', 'png', 'gif', 'php'], + hits: 171458, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series.js new file mode 100644 index 0000000000000..c55bff5631e88 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series.js @@ -0,0 +1,67 @@ +/* + * 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 _ from 'lodash'; + +export default { + label: '', + xAxisLabel: 'Top 5 extension', + xAxisOrderedValues: ['jpg', 'css', 'png', 'gif', 'php'], + yAxisLabel: 'Count of documents', + series: [ + { + label: 'Count', + values: [ + { + x: 'jpg', + y: 110710, + }, + { + x: 'css', + y: 27389, + }, + { + x: 'png', + y: 16661, + }, + { + x: 'gif', + y: 11269, + }, + { + x: 'php', + y: 5447, + }, + ], + }, + ], + hits: 171476, + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple.js b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple.js new file mode 100644 index 0000000000000..372325120ee8e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple.js @@ -0,0 +1,105 @@ +/* + * 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 _ from 'lodash'; + +export default { + xAxisOrderedValues: ['_all'], + yAxisLabel: 'Count', + zAxisLabel: 'machine.os.raw: Descending', + yScale: null, + series: [ + { + label: 'ios', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 2820, + series: 'ios', + }, + ], + }, + { + label: 'win 7', + aggId: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 2319, + series: 'win 7', + }, + ], + }, + { + label: 'win 8', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 1835, + series: 'win 8', + }, + ], + }, + { + label: 'windows xp service pack 2 version 20123452', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 734, + series: 'win xp', + }, + ], + }, + { + label: 'osx', + id: '1', + yAxisFormatter: _.identity, + values: [ + { + x: '_all', + y: 1352, + series: 'osx', + }, + ], + }, + ], + hits: 14005, + xAxisFormatter: function(val) { + if (_.isObject(val)) { + return JSON.stringify(val); + } else if (val == null) { + return ''; + } else { + return '' + val; + } + }, + yAxisFormatter: function(val) { + return val; + }, + tooltipFormatter: function(d) { + return d; + }, +}; diff --git a/src/plugins/vis_type_vislib/public/fixtures/mocks.js b/src/plugins/vis_type_vislib/public/fixtures/mocks.js new file mode 100644 index 0000000000000..60edf6c1ff05c --- /dev/null +++ b/src/plugins/vis_type_vislib/public/fixtures/mocks.js @@ -0,0 +1,37 @@ +/* + * 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 { setFormatService } from '../services'; + +setFormatService({ + deserialize: () => ({ + convert: v => v, + }), +}); + +export const getMockUiState = () => { + const map = new Map(); + + return (() => ({ + get: (...args) => map.get(...args), + set: (...args) => map.set(...args), + setSilent: (...args) => map.set(...args), + on: () => undefined, + }))(); +}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts b/src/plugins/vis_type_vislib/public/gauge.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_vislib/public/gauge.ts rename to src/plugins/vis_type_vislib/public/gauge.ts index 5e0b2b8fbd36c..561c45d26fa7f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/gauge.ts +++ b/src/plugins/vis_type_vislib/public/gauge.ts @@ -19,17 +19,11 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; -import { AggGroupNames } from '../../../../plugins/data/public'; +import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; import { GaugeOptions } from './components/options'; import { getGaugeCollections, Alignments, GaugeTypes } from './utils/collections'; -import { - ColorModes, - ColorSchemas, - ColorSchemaParams, - Labels, - Style, -} from '../../../../plugins/charts/public'; +import { ColorModes, ColorSchemas, ColorSchemaParams, Labels, Style } from '../../charts/public'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts b/src/plugins/vis_type_vislib/public/goal.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/goal.ts rename to src/plugins/vis_type_vislib/public/goal.ts index 0f70dca69728d..5f74698938a0b 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/goal.ts +++ b/src/plugins/vis_type_vislib/public/goal.ts @@ -23,9 +23,9 @@ import { GaugeOptions } from './components/options'; import { getGaugeCollections, GaugeTypes } from './utils/collections'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { ColorModes, ColorSchemas } from '../../../../plugins/charts/public'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { ColorModes, ColorSchemas } from '../../charts/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; export const createGoalVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'goal', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts b/src/plugins/vis_type_vislib/public/heatmap.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts rename to src/plugins/vis_type_vislib/public/heatmap.ts index 9feed60b984ba..ced7a38568ffd 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/heatmap.ts +++ b/src/plugins/vis_type_vislib/public/heatmap.ts @@ -19,15 +19,15 @@ import { i18n } from '@kbn/i18n'; -import { RangeValues, Schemas } from '../../../../plugins/vis_default_editor/public'; -import { AggGroupNames } from '../../../../plugins/data/public'; +import { RangeValues, Schemas } from '../../vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; import { AxisTypes, getHeatmapCollections, Positions, ScaleTypes } from './utils/collections'; import { HeatmapOptions } from './components/options'; import { createVislibVisController } from './vis_controller'; import { TimeMarker } from './vislib/visualizations/time_marker'; import { CommonVislibParams, ValueAxis } from './types'; import { VisTypeVislibDependencies } from './plugin'; -import { ColorSchemas, ColorSchemaParams } from '../../../../plugins/charts/public'; +import { ColorSchemas, ColorSchemaParams } from '../../charts/public'; export interface HeatmapVisParams extends CommonVislibParams, ColorSchemaParams { type: 'heatmap'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts b/src/plugins/vis_type_vislib/public/histogram.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_vislib/public/histogram.ts rename to src/plugins/vis_type_vislib/public/histogram.ts index 54ccf66f362ca..52242ad11e8f5 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/histogram.ts +++ b/src/plugins/vis_type_vislib/public/histogram.ts @@ -23,8 +23,8 @@ import { palettes } from '@elastic/eui/lib/services'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -38,7 +38,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createHistogramVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'histogram', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts b/src/plugins/vis_type_vislib/public/horizontal_bar.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts rename to src/plugins/vis_type_vislib/public/horizontal_bar.ts index 6f73271726660..a58c15f136431 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/horizontal_bar.ts +++ b/src/plugins/vis_type_vislib/public/horizontal_bar.ts @@ -19,12 +19,10 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { palettes } from '@elastic/eui/lib/services'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -38,7 +36,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createHorizontalBarVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'horizontal_bar', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/_index.scss b/src/plugins/vis_type_vislib/public/index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/_index.scss rename to src/plugins/vis_type_vislib/public/index.scss diff --git a/src/plugins/vis_type_vislib/public/index.ts b/src/plugins/vis_type_vislib/public/index.ts new file mode 100644 index 0000000000000..665643a6763f6 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { PluginInitializerContext } from '../../../core/public'; +import { VisTypeVislibPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} + +export * from './types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/line.ts b/src/plugins/vis_type_vislib/public/line.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_vislib/public/line.ts rename to src/plugins/vis_type_vislib/public/line.ts index 1f9a8d77398e6..a94fd3f3945ab 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/line.ts +++ b/src/plugins/vis_type_vislib/public/line.ts @@ -19,12 +19,10 @@ import { i18n } from '@kbn/i18n'; // @ts-ignore -import { palettes } from '@elastic/eui/lib/services'; -// @ts-ignore -import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; +import { palettes, euiPaletteColorBlind } from '@elastic/eui/lib/services'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { Positions, ChartTypes, @@ -39,7 +37,7 @@ import { import { getAreaOptionTabs, countLabel } from './utils/common_config'; import { createVislibVisController } from './vis_controller'; import { VisTypeVislibDependencies } from './plugin'; -import { Rotates } from '../../../../plugins/charts/public'; +import { Rotates } from '../../charts/public'; export const createLineVisTypeDefinition = (deps: VisTypeVislibDependencies) => ({ name: 'line', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts b/src/plugins/vis_type_vislib/public/pie.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/pie.ts rename to src/plugins/vis_type_vislib/public/pie.ts index 2774836baa381..a68bc5893406f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie.ts +++ b/src/plugins/vis_type_vislib/public/pie.ts @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; -import { AggGroupNames } from '../../../../plugins/data/public'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { AggGroupNames } from '../../data/public'; +import { Schemas } from '../../vis_default_editor/public'; import { PieOptions } from './components/options'; import { getPositions, Positions } from './utils/collections'; import { createVislibVisController } from './vis_controller'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts b/src/plugins/vis_type_vislib/public/pie_fn.test.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts rename to src/plugins/vis_type_vislib/public/pie_fn.test.ts index 15c80e4719487..a8c03eba2b449 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.test.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.test.ts @@ -18,12 +18,11 @@ */ // eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createPieVisFn } from './pie_fn'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; -jest.mock('ui/new_platform'); jest.mock('./vislib/response_handler', () => ({ vislibSlicesResponseHandler: jest.fn().mockReturnValue({ hits: 1, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts b/src/plugins/vis_type_vislib/public/pie_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts rename to src/plugins/vis_type_vislib/public/pie_fn.ts index 452e0be0df3e4..52da0f7ac14ec 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/pie_fn.ts +++ b/src/plugins/vis_type_vislib/public/pie_fn.ts @@ -18,11 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSlicesResponseHandler } from './vislib/response_handler'; diff --git a/src/plugins/vis_type_vislib/public/plugin.ts b/src/plugins/vis_type_vislib/public/plugin.ts new file mode 100644 index 0000000000000..e19a2ec451f2b --- /dev/null +++ b/src/plugins/vis_type_vislib/public/plugin.ts @@ -0,0 +1,116 @@ +/* + * 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 './index.scss'; + +import { + CoreSetup, + CoreStart, + Plugin, + IUiSettingsClient, + PluginInitializerContext, +} from 'kibana/public'; + +import { VisTypeXyPluginSetup } from 'src/plugins/vis_type_xy/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; +import { createPieVisFn } from './pie_fn'; +import { + createHistogramVisTypeDefinition, + createLineVisTypeDefinition, + createPieVisTypeDefinition, + createAreaVisTypeDefinition, + createHeatmapVisTypeDefinition, + createHorizontalBarVisTypeDefinition, + createGaugeVisTypeDefinition, + createGoalVisTypeDefinition, +} from './vis_type_vislib_vis_types'; +import { ChartsPluginSetup } from '../../charts/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { setFormatService, setDataActions } from './services'; + +export interface VisTypeVislibDependencies { + uiSettings: IUiSettingsClient; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface VisTypeVislibPluginSetupDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; + charts: ChartsPluginSetup; + visTypeXy?: VisTypeXyPluginSetup; +} + +/** @internal */ +export interface VisTypeVislibPluginStartDependencies { + data: DataPublicPluginStart; +} + +type VisTypeVislibCoreSetup = CoreSetup; + +/** @internal */ +export class VisTypeVislibPlugin implements Plugin { + constructor(public initializerContext: PluginInitializerContext) {} + + public async setup( + core: VisTypeVislibCoreSetup, + { expressions, visualizations, charts, visTypeXy }: VisTypeVislibPluginSetupDependencies + ) { + const visualizationDependencies: Readonly = { + uiSettings: core.uiSettings, + charts, + }; + const vislibTypes = [ + createHistogramVisTypeDefinition, + createLineVisTypeDefinition, + createPieVisTypeDefinition, + createAreaVisTypeDefinition, + createHeatmapVisTypeDefinition, + createHorizontalBarVisTypeDefinition, + createGaugeVisTypeDefinition, + createGoalVisTypeDefinition, + ]; + const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()]; + + // if visTypeXy plugin is disabled it's config will be undefined + if (!visTypeXy) { + const convertedTypes: any[] = []; + const convertedFns: any[] = []; + + // Register legacy vislib types that have been converted + convertedFns.forEach(expressions.registerFunction); + convertedTypes.forEach(vis => + visualizations.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + // Register non-converted types + vislibFns.forEach(expressions.registerFunction); + vislibTypes.forEach(vis => + visualizations.createBaseVisualization(vis(visualizationDependencies)) + ); + } + + public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { + setFormatService(data.fieldFormats); + setDataActions(data.actions); + } +} diff --git a/src/plugins/vis_type_vislib/public/services.ts b/src/plugins/vis_type_vislib/public/services.ts new file mode 100644 index 0000000000000..633fae9c7f2a6 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/services.ts @@ -0,0 +1,29 @@ +/* + * 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 { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getDataActions, setDataActions] = createGetterSetter< + DataPublicPluginStart['actions'] +>('vislib data.actions'); + +export const [getFormatService, setFormatService] = createGetterSetter< + DataPublicPluginStart['fieldFormats'] +>('vislib data.fieldFormats'); diff --git a/src/plugins/vis_type_vislib/public/types.ts b/src/plugins/vis_type_vislib/public/types.ts new file mode 100644 index 0000000000000..83d0b49b1c551 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/types.ts @@ -0,0 +1,96 @@ +/* + * 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 { TimeMarker } from './vislib/visualizations/time_marker'; +import { + Positions, + ChartModes, + ChartTypes, + AxisModes, + AxisTypes, + InterpolationModes, + ScaleTypes, + ThresholdLineStyles, +} from './utils/collections'; +import { Labels, Style } from '../../charts/public'; + +export interface CommonVislibParams { + addTooltip: boolean; + legendPosition: Positions; +} + +export interface Scale { + boundsMargin?: number | ''; + defaultYExtents?: boolean; + max?: number | null; + min?: number | null; + mode?: AxisModes; + setYExtents?: boolean; + type: ScaleTypes; +} + +interface ThresholdLine { + show: boolean; + value: number | null; + width: number | null; + style: ThresholdLineStyles; + color: string; +} + +export interface Axis { + id: string; + labels: Labels; + position: Positions; + scale: Scale; + show: boolean; + style: Style; + title: { text: string }; + type: AxisTypes; +} + +export interface ValueAxis extends Axis { + name: string; +} + +export interface SeriesParam { + data: { label: string; id: string }; + drawLinesBetweenPoints: boolean; + interpolate: InterpolationModes; + lineWidth?: number; + mode: ChartModes; + show: boolean; + showCircles: boolean; + type: ChartTypes; + valueAxis: string; +} + +export interface BasicVislibParams extends CommonVislibParams { + addTimeMarker: boolean; + categoryAxes: Axis[]; + orderBucketsBySum?: boolean; + labels: Labels; + thresholdLine: ThresholdLine; + valueAxes: ValueAxis[]; + grid: { + categoryLines: boolean; + valueAxis?: string; + }; + seriesParams: SeriesParam[]; + times: TimeMarker[]; +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts b/src/plugins/vis_type_vislib/public/utils/collections.ts similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts rename to src/plugins/vis_type_vislib/public/utils/collections.ts index 2024c43dd1c8b..44df4864bfd68 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/utils/collections.ts +++ b/src/plugins/vis_type_vislib/public/utils/collections.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { $Values } from '@kbn/utility-types'; -import { colorSchemas, Rotates } from '../../../../../plugins/charts/public'; +import { colorSchemas, Rotates } from '../../../charts/public'; export const Positions = Object.freeze({ RIGHT: 'right' as 'right', diff --git a/src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx b/src/plugins/vis_type_vislib/public/utils/common_config.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/utils/common_config.tsx rename to src/plugins/vis_type_vislib/public/utils/common_config.tsx diff --git a/src/plugins/vis_type_vislib/public/vis_controller.tsx b/src/plugins/vis_type_vislib/public/vis_controller.tsx new file mode 100644 index 0000000000000..65acc08b58da0 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vis_controller.tsx @@ -0,0 +1,145 @@ +/* + * 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 $ from 'jquery'; +import React, { RefObject } from 'react'; + +// @ts-ignore +import { Vis as Vislib } from './vislib/vis'; +import { Positions } from './utils/collections'; +import { VisTypeVislibDependencies } from './plugin'; +import { mountReactNode } from '../../../core/public/utils'; +import { VisLegend, CUSTOM_LEGEND_VIS_TYPES } from './vislib/components/legend'; +import { VisParams, ExprVis } from '../../visualizations/public'; + +const legendClassName = { + top: 'visLib--legend-top', + bottom: 'visLib--legend-bottom', + left: 'visLib--legend-left', + right: 'visLib--legend-right', +}; + +export const createVislibVisController = (deps: VisTypeVislibDependencies) => { + return class VislibVisController { + unmount: (() => void) | null = null; + visParams?: VisParams; + legendRef: RefObject; + container: HTMLDivElement; + chartEl: HTMLDivElement; + legendEl: HTMLDivElement; + vislibVis: any; + + constructor(public el: Element, public vis: ExprVis) { + this.el = el; + this.vis = vis; + this.unmount = null; + this.legendRef = React.createRef(); + + // vis mount point + this.container = document.createElement('div'); + this.container.className = 'visLib'; + this.el.appendChild(this.container); + + // chart mount point + this.chartEl = document.createElement('div'); + this.chartEl.className = 'visLib__chart'; + this.container.appendChild(this.chartEl); + + // legend mount point + this.legendEl = document.createElement('div'); + this.legendEl.className = 'visLib__legend'; + this.container.appendChild(this.legendEl); + } + + render(esResponse: any, visParams: VisParams) { + if (this.vislibVis) { + this.destroy(); + } + + return new Promise(async resolve => { + if (this.el.clientWidth === 0 || this.el.clientHeight === 0) { + return resolve(); + } + + this.vislibVis = new Vislib(this.chartEl, visParams, deps); + this.vislibVis.on('brush', this.vis.API.events.brush); + this.vislibVis.on('click', this.vis.API.events.filter); + this.vislibVis.on('renderComplete', resolve); + + this.vislibVis.initVisConfig(esResponse, this.vis.getUiState()); + + if (visParams.addLegend) { + $(this.container) + .attr('class', (i, cls) => { + return cls.replace(/visLib--legend-\S+/g, ''); + }) + .addClass((legendClassName as any)[visParams.legendPosition]); + + this.mountLegend(esResponse, visParams.legendPosition); + } + + this.vislibVis.render(esResponse, this.vis.getUiState()); + + // refreshing the legend after the chart is rendered. + // this is necessary because some visualizations + // provide data necessary for the legend only after a render cycle. + if ( + visParams.addLegend && + CUSTOM_LEGEND_VIS_TYPES.includes(this.vislibVis.visConfigArgs.type) + ) { + this.unmountLegend(); + this.mountLegend(esResponse, visParams.legendPosition); + this.vislibVis.render(esResponse, this.vis.getUiState()); + } + }); + } + + mountLegend(visData: any, position: Positions) { + this.unmount = mountReactNode( + + )(this.legendEl); + } + + unmountLegend() { + if (this.unmount) { + this.unmount(); + } + } + + destroy() { + if (this.unmount) { + this.unmount(); + } + + if (this.vislibVis) { + this.vislibVis.off('brush', this.vis.API.events.brush); + this.vislibVis.off('click', this.vis.API.events.filter); + this.vislibVis.destroy(); + delete this.vislibVis; + } + } + }; +}; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts rename to src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts index 854b70b04e58a..a4243c6d25c41 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts +++ b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_fn.ts @@ -18,11 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; // @ts-ignore import { vislibSeriesResponseHandler } from './vislib/response_handler'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts b/src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts rename to src/plugins/vis_type_vislib/public/vis_type_vislib_vis_types.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/VISLIB.md b/src/plugins/vis_type_vislib/public/vislib/VISLIB.md similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/VISLIB.md rename to src/plugins/vis_type_vislib/public/vislib/VISLIB.md diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss b/src/plugins/vis_type_vislib/public/vislib/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_variables.scss b/src/plugins/vis_type_vislib/public/vislib/_variables.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/_variables.scss rename to src/plugins/vis_type_vislib/public/vislib/_variables.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss b/src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss rename to src/plugins/vis_type_vislib/public/vislib/_vislib_vis_type.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/data_array.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/data_array.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/data_array.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/data_array.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/flatten_series.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/index.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/index.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/labels.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/labels.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/labels.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js new file mode 100644 index 0000000000000..838275d44d2a1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/labels/labels.test.js @@ -0,0 +1,469 @@ +/* + * 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 _ from 'lodash'; + +import { labels } from './labels'; +import { dataArray } from './data_array'; +import { uniqLabels } from './uniq_labels'; +import { flattenSeries as getSeries } from './flatten_series'; + +let seriesLabels; +let rowsLabels; +let seriesArr; +let rowsArr; + +const seriesData = { + label: '', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], +}; + +const rowsData = { + rows: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +const columnsData = { + columns: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +describe('Vislib Labels Module Test Suite', function() { + let uniqSeriesLabels; + describe('Labels (main)', function() { + beforeEach(() => { + seriesLabels = labels(seriesData); + rowsLabels = labels(rowsData); + seriesArr = Array.isArray(seriesLabels); + rowsArr = Array.isArray(rowsLabels); + uniqSeriesLabels = _.chain(rowsData.rows) + .pluck('series') + .flattenDeep() + .pluck('label') + .uniq() + .value(); + }); + + it('should be a function', function() { + expect(typeof labels).toBe('function'); + }); + + it('should return an array if input is data.series', function() { + expect(seriesArr).toBe(true); + }); + + it('should return an array if input is data.rows', function() { + expect(rowsArr).toBe(true); + }); + + it('should throw an error if input is not an object', function() { + expect(function() { + labels('string not object'); + }).toThrow(); + }); + + it('should return unique label values', function() { + expect(rowsLabels[0]).toEqual(uniqSeriesLabels[0]); + expect(rowsLabels[1]).toEqual(uniqSeriesLabels[1]); + expect(rowsLabels[2]).toEqual(uniqSeriesLabels[2]); + }); + }); + + describe('Data array', function() { + const childrenObject = { + children: [], + }; + const seriesObject = { + series: [], + }; + const rowsObject = { + rows: [], + }; + const columnsObject = { + columns: [], + }; + const string = 'string'; + const number = 23; + const boolean = false; + const emptyArray = []; + const nullValue = null; + let notAValue; + let testSeries; + let testRows; + + beforeEach(() => { + seriesLabels = dataArray(seriesData); + rowsLabels = dataArray(rowsData); + testSeries = Array.isArray(seriesLabels); + testRows = Array.isArray(rowsLabels); + }); + + it('should throw an error if the input is not an object', function() { + expect(function() { + dataArray(string); + }).toThrow(); + + expect(function() { + dataArray(number); + }).toThrow(); + + expect(function() { + dataArray(boolean); + }).toThrow(); + + expect(function() { + dataArray(emptyArray); + }).toThrow(); + + expect(function() { + dataArray(nullValue); + }).toThrow(); + + expect(function() { + dataArray(notAValue); + }).toThrow(); + }); + + it( + 'should throw an error if property series, rows, or ' + 'columns is not present', + function() { + expect(function() { + dataArray(childrenObject); + }).toThrow(); + } + ); + + it( + 'should not throw an error if object has property series, rows, or ' + 'columns', + function() { + expect(function() { + dataArray(seriesObject); + }).not.toThrow(); + + expect(function() { + dataArray(rowsObject); + }).not.toThrow(); + + expect(function() { + dataArray(columnsObject); + }).not.toThrow(); + } + ); + + it('should be a function', function() { + expect(typeof dataArray).toEqual('function'); + }); + + it('should return an array of objects if input is data.series', function() { + expect(testSeries).toEqual(true); + }); + + it('should return an array of objects if input is data.rows', function() { + expect(testRows).toEqual(true); + }); + + it('should return an array of same length as input data.series', function() { + expect(seriesLabels.length).toEqual(seriesData.series.length); + }); + + it('should return an array of same length as input data.rows', function() { + expect(rowsLabels.length).toEqual(rowsData.rows.length); + }); + + it('should return an array of objects with obj.labels and obj.values', function() { + expect(seriesLabels[0].label).toEqual('100'); + expect(seriesLabels[0].values[0].x).toEqual(0); + expect(seriesLabels[0].values[0].y).toEqual(1); + }); + }); + + describe('Unique labels', function() { + const arrObj = [ + { label: 'a' }, + { label: 'b' }, + { label: 'b' }, + { label: 'c' }, + { label: 'c' }, + { label: 'd' }, + { label: 'f' }, + ]; + const string = 'string'; + const number = 24; + const boolean = false; + const nullValue = null; + const emptyObject = {}; + const emptyArray = []; + let notAValue; + let uniq; + let testArr; + + beforeEach(() => { + uniq = uniqLabels(arrObj, function(d) { + return d; + }); + testArr = Array.isArray(uniq); + }); + + it('should throw an error if input is not an array', function() { + expect(function() { + uniqLabels(string); + }).toThrow(); + + expect(function() { + uniqLabels(number); + }).toThrow(); + + expect(function() { + uniqLabels(boolean); + }).toThrow(); + + expect(function() { + uniqLabels(nullValue); + }).toThrow(); + + expect(function() { + uniqLabels(emptyObject); + }).toThrow(); + + expect(function() { + uniqLabels(notAValue); + }).toThrow(); + }); + + it('should not throw an error if the input is an array', function() { + expect(function() { + uniqLabels(emptyArray); + }).not.toThrow(); + }); + + it('should be a function', function() { + expect(typeof uniqLabels).toBe('function'); + }); + + it('should return an array', function() { + expect(testArr).toBe(true); + }); + + it('should return array of 5 unique values', function() { + expect(uniq.length).toBe(5); + }); + }); + + describe('Get series', function() { + const string = 'string'; + const number = 24; + const boolean = false; + const nullValue = null; + const rowsObject = { + rows: [], + }; + const columnsObject = { + columns: [], + }; + const emptyObject = {}; + const emptyArray = []; + let notAValue; + let columnsLabels; + let rowsLabels; + let columnsArr; + let rowsArr; + + beforeEach(() => { + columnsLabels = getSeries(columnsData); + rowsLabels = getSeries(rowsData); + columnsArr = Array.isArray(columnsLabels); + rowsArr = Array.isArray(rowsLabels); + }); + + it('should throw an error if input is not an object', function() { + expect(function() { + getSeries(string); + }).toThrow(); + + expect(function() { + getSeries(number); + }).toThrow(); + + expect(function() { + getSeries(boolean); + }).toThrow(); + + expect(function() { + getSeries(nullValue); + }).toThrow(); + + expect(function() { + getSeries(emptyArray); + }).toThrow(); + + expect(function() { + getSeries(notAValue); + }).toThrow(); + }); + + it('should throw an if property rows or columns is not set on the object', function() { + expect(function() { + getSeries(emptyObject); + }).toThrow(); + }); + + it('should not throw an error if rows or columns set on object', function() { + expect(function() { + getSeries(rowsObject); + }).not.toThrow(); + + expect(function() { + getSeries(columnsObject); + }).not.toThrow(); + }); + + it('should be a function', function() { + expect(typeof getSeries).toBe('function'); + }); + + it('should return an array if input is data.columns', function() { + expect(columnsArr).toBe(true); + }); + + it('should return an array if input is data.rows', function() { + expect(rowsArr).toBe(true); + }); + + it('should return an array of the same length as as input data.columns', function() { + expect(columnsLabels.length).toBe(columnsData.columns.length); + }); + + it('should return an array of the same length as as input data.rows', function() { + expect(rowsLabels.length).toBe(rowsData.rows.length); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/truncate_labels.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js b/src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js rename to src/plugins/vis_type_vislib/public/vislib/components/labels/uniq_labels.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap b/src/plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap rename to src/plugins/vis_type_vislib/public/vislib/components/legend/__snapshots__/legend.test.tsx.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss b/src/plugins/vis_type_vislib/public/vislib/components/legend/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/components/legend/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss b/src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss rename to src/plugins/vis_type_vislib/public/vislib/components/legend/_legend.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/index.ts b/src/plugins/vis_type_vislib/public/vislib/components/legend/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/index.ts rename to src/plugins/vis_type_vislib/public/vislib/components/legend/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx rename to src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index c378ae7b05b37..c203642f353c5 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -31,12 +31,10 @@ jest.mock('@elastic/eui', () => ({ htmlIdGenerator: jest.fn().mockReturnValue(() => 'legendId'), })); -jest.mock('../../../legacy_imports', () => ({ - getTableAggs: jest.fn(), -})); - jest.mock('../../../services', () => ({ - getDataActions: () => ({ createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']) }), + getDataActions: () => ({ + createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), + }), })); const vis = { diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx new file mode 100644 index 0000000000000..7eb25e3930718 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -0,0 +1,279 @@ +/* + * 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 React, { BaseSyntheticEvent, KeyboardEvent, PureComponent } from 'react'; +import classNames from 'classnames'; +import { compact, uniq, map, every, isUndefined } from 'lodash'; + +import { i18n } from '@kbn/i18n'; +import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; + +import { getDataActions } from '../../../services'; +import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; +import { VisLegendItem } from './legend_item'; +import { getPieNames } from './pie_utils'; + +export interface VisLegendProps { + vis: any; + vislibVis: any; + visData: any; + uiState: any; + position: 'top' | 'bottom' | 'left' | 'right'; +} + +export interface VisLegendState { + open: boolean; + labels: any[]; + filterableLabels: Set; + selectedLabel: string | null; +} + +export class VisLegend extends PureComponent { + legendId = htmlIdGenerator()('legend'); + getColor: (label: string) => string = () => ''; + + constructor(props: VisLegendProps) { + super(props); + const open = props.uiState.get('vis.legendOpen', true); + + this.state = { + open, + labels: [], + filterableLabels: new Set(), + selectedLabel: null, + }; + } + + componentDidMount() { + this.refresh(); + } + + toggleLegend = () => { + const bwcAddLegend = this.props.vis.params.addLegend; + const bwcLegendStateDefault = bwcAddLegend == null ? true : bwcAddLegend; + const newOpen = !this.props.uiState.get('vis.legendOpen', bwcLegendStateDefault); + this.setState({ open: newOpen }); + // open should be applied on template before we update uiState + setTimeout(() => { + this.props.uiState.set('vis.legendOpen', newOpen); + }); + }; + + setColor = (label: string, color: string) => (event: BaseSyntheticEvent) => { + if ((event as KeyboardEvent).keyCode && (event as KeyboardEvent).keyCode !== keyCodes.ENTER) { + return; + } + + const colors = this.props.uiState.get('vis.colors') || {}; + if (colors[label] === color) delete colors[label]; + else colors[label] = color; + this.props.uiState.setSilent('vis.colors', null); + this.props.uiState.set('vis.colors', colors); + this.props.uiState.emit('colorChanged'); + this.refresh(); + }; + + filter = ({ values: data }: LegendItem, negate: boolean) => { + this.props.vis.API.events.filter({ data, negate }); + }; + + canFilter = async (item: LegendItem): Promise => { + if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { + return false; + } + + if (item.values && every(item.values, isUndefined)) { + return false; + } + + const filters = await getDataActions().createFiltersFromValueClickAction({ data: item.values }); + return Boolean(filters.length); + }; + + toggleDetails = (label: string | null) => (event?: BaseSyntheticEvent) => { + if ( + event && + (event as KeyboardEvent).keyCode && + (event as KeyboardEvent).keyCode !== keyCodes.ENTER + ) { + return; + } + this.setState({ selectedLabel: this.state.selectedLabel === label ? null : label }); + }; + + getSeriesLabels = (data: any[]) => { + const values = data.map(chart => chart.series).reduce((a, b) => a.concat(b), []); + + return compact(uniq(values, 'label')).map((label: any) => ({ + ...label, + values: [label.values[0].seriesRaw], + })); + }; + + setFilterableLabels = (items: LegendItem[]): Promise => + new Promise(async resolve => { + const filterableLabels = new Set(); + items.forEach(async item => { + const canFilter = await this.canFilter(item); + if (canFilter) { + filterableLabels.add(item.label); + } + }); + + this.setState({ filterableLabels }, resolve); + }); + + setLabels = (data: any, type: string) => { + let labels = []; + if (CUSTOM_LEGEND_VIS_TYPES.includes(type)) { + const legendLabels = this.props.vislibVis.getLegendLabels(); + if (legendLabels) { + labels = map(legendLabels, label => { + return { label }; + }); + } + } else { + if (!data) return []; + data = data.columns || data.rows || [data]; + + labels = type === 'pie' ? getPieNames(data) : this.getSeriesLabels(data); + } + + this.setFilterableLabels(labels); + + this.setState({ + labels, + }); + }; + + refresh = () => { + const vislibVis = this.props.vislibVis; + if (!vislibVis || !vislibVis.visConfig) { + this.setState({ + labels: [ + { + label: i18n.translate('visTypeVislib.vislib.legend.loadingLabel', { + defaultMessage: 'loading…', + }), + }, + ], + }); + return; + } // make sure vislib is defined at this point + + if ( + this.props.uiState.get('vis.legendOpen') == null && + this.props.vis.params.addLegend != null + ) { + this.setState({ open: this.props.vis.params.addLegend }); + } + + if (vislibVis.visConfig) { + this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); + } + + this.setLabels(this.props.visData, vislibVis.visConfigArgs.type); + }; + + highlight = (event: BaseSyntheticEvent) => { + const el = event.currentTarget; + const handler = this.props.vislibVis && this.props.vislibVis.handler; + + // there is no guarantee that a Chart will set the highlight-function on its handler + if (!handler || typeof handler.highlight !== 'function') { + return; + } + handler.highlight.call(el, handler.el); + }; + + unhighlight = (event: BaseSyntheticEvent) => { + const el = event.currentTarget; + const handler = this.props.vislibVis && this.props.vislibVis.handler; + + // there is no guarantee that a Chart will set the unhighlight-function on its handler + if (!handler || typeof handler.unHighlight !== 'function') { + return; + } + handler.unHighlight.call(el, handler.el); + }; + + getAnchorPosition = () => { + const { position } = this.props; + + switch (position) { + case 'bottom': + return 'upCenter'; + case 'left': + return 'rightUp'; + case 'right': + return 'leftUp'; + default: + return 'downCenter'; + } + }; + + renderLegend = (anchorPosition: EuiPopoverProps['anchorPosition']) => ( +
    + {this.state.labels.map(item => ( + + ))} +
+ ); + + render() { + const { open } = this.state; + const anchorPosition = this.getAnchorPosition(); + + return ( +
+ + {open && this.renderLegend(anchorPosition)} +
+ ); + } +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx rename to src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/models.ts b/src/plugins/vis_type_vislib/public/vislib/components/legend/models.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/models.ts rename to src/plugins/vis_type_vislib/public/vislib/components/legend/models.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts b/src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts rename to src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts index d9eea83d40b48..0167a542c6372 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts +++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/pie_utils.ts @@ -25,7 +25,7 @@ import _ from 'lodash'; * * > Duplicated utilty method from vislib Data class to decouple `vislib_vis_legend` from `vislib` * - * @see src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/data.js + * @see src/plugins/vis_type_vislib/public/vislib/lib/data.js * * @returns {Array} Array of unique names (strings) */ diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_collect_branch.test.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_hierarchical_tooltip_formatter.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.test.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.test.js new file mode 100644 index 0000000000000..953c115cc0d02 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_pointseries_tooltip_formatter.test.js @@ -0,0 +1,88 @@ +/* + * 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 _ from 'lodash'; +import $ from 'jquery'; + +import { pointSeriesTooltipFormatter } from './_pointseries_tooltip_formatter'; + +describe('tooltipFormatter', function() { + const tooltipFormatter = pointSeriesTooltipFormatter(); + + function cell($row, i) { + return $row + .eq(i) + .text() + .trim(); + } + + const baseEvent = { + data: { + xAxisLabel: 'inner', + xAxisFormatter: _.identity, + yAxisLabel: 'middle', + yAxisFormatter: _.identity, + zAxisLabel: 'top', + zAxisFormatter: _.identity, + series: [ + { + rawId: '1', + label: 'middle', + zLabel: 'top', + yAxisFormatter: _.identity, + zAxisFormatter: _.identity, + }, + ], + }, + datum: { + x: 3, + y: 2, + z: 1, + extraMetrics: [], + seriesId: '1', + }, + }; + + it('returns html based on the mouse event', function() { + const event = _.cloneDeep(baseEvent); + const $el = $(tooltipFormatter(event)); + const $rows = $el.find('tr'); + expect($rows.length).toBe(3); + + const $row1 = $rows.eq(0).find('td'); + expect(cell($row1, 0)).toBe('inner'); + expect(cell($row1, 1)).toBe('3'); + + const $row2 = $rows.eq(1).find('td'); + expect(cell($row2, 0)).toBe('middle'); + expect(cell($row2, 1)).toBe('2'); + + const $row3 = $rows.eq(2).find('td'); + expect(cell($row3, 0)).toBe('top'); + expect(cell($row3, 1)).toBe('1'); + }); + + it('renders correctly on missing extraMetrics in datum', function() { + const event = _.cloneDeep(baseEvent); + delete event.datum.extraMetrics; + const $el = $(tooltipFormatter(event)); + const $rows = $el.find('tr'); + expect($rows.length).toBe(3); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/_tooltip.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/index.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/index.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.test.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.test.js new file mode 100644 index 0000000000000..c184d0e9fbcf7 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/position_tooltip.test.js @@ -0,0 +1,274 @@ +/* + * 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 $ from 'jquery'; +import _ from 'lodash'; +import sinon from 'sinon'; + +import { positionTooltip } from './position_tooltip'; + +describe('Tooltip Positioning', function() { + const sandbox = sinon.createSandbox(); + const positions = ['north', 'south', 'east', 'west']; + const bounds = ['top', 'left', 'bottom', 'right', 'area']; + let $window; + let $chart; + let $tooltip; + let $sizer; + + function testEl(width, height, $children) { + const $el = $('
'); + + const size = { + width: _.random(width[0], width[1]), + height: _.random(height[0], height[1]), + }; + + $el + .css({ + width: size.width, + height: size.height, + visibility: 'hidden', + }) + .appendTo('body'); + + if ($children) { + $el.append($children); + } + + $el.testSize = size; + + return $el; + } + + beforeEach(function() { + $window = testEl( + [500, 1000], + [600, 800], + ($chart = testEl([600, 750], [350, 550], ($tooltip = testEl([50, 100], [35, 75])))) + ); + + $sizer = $tooltip.clone().appendTo($window); + }); + + afterEach(function() { + $window.remove(); + $window = $chart = $tooltip = $sizer = null; + positionTooltip.removeClone(); + sandbox.restore(); + }); + + function makeEvent(xPercent, yPercent) { + xPercent = xPercent || 0.5; + yPercent = yPercent || 0.5; + + const base = $chart.offset(); + + return { + clientX: base.left + $chart.testSize.width * xPercent, + clientY: base.top + $chart.testSize.height * yPercent, + }; + } + + describe('getTtSize()', function() { + it('should measure the outer-size of the tooltip using an un-obstructed clone', function() { + const w = sandbox.spy($.fn, 'outerWidth'); + const h = sandbox.spy($.fn, 'outerHeight'); + + positionTooltip.getTtSize($tooltip.html(), $sizer); + + [w, h].forEach(function(spy) { + expect(spy).toHaveProperty('callCount', 1); + const matchHtml = w.thisValues.filter(function($t) { + return !$t.is($tooltip) && $t.html() === $tooltip.html(); + }); + expect(matchHtml).toHaveLength(1); + }); + }); + }); + + describe('getBasePosition()', function() { + it('calculates the offset values for the four positions', function() { + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + const pos = positionTooltip.getBasePosition(size, makeEvent()); + + positions.forEach(function(p) { + expect(pos).toHaveProperty(p); + }); + + expect(pos.north).toBeLessThan(pos.south); + expect(pos.east).toBeGreaterThan(pos.west); + }); + }); + + describe('getBounds()', function() { + it('returns the offsets for the tlrb of the element', function() { + const cbounds = positionTooltip.getBounds($chart); + + bounds.forEach(function(b) { + expect(cbounds).toHaveProperty(b); + }); + + expect(cbounds.top).toBeLessThan(cbounds.bottom); + expect(cbounds.left).toBeLessThan(cbounds.right); + }); + }); + + describe('getOverflow()', function() { + it('determines how much the base placement overflows the containing bounds in each direction', function() { + // size the tooltip very small so it won't collide with the edges + $tooltip.css({ width: 15, height: 15 }); + $sizer.css({ width: 15, height: 15 }); + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + expect(size).toHaveProperty('width', 15); + expect(size).toHaveProperty('height', 15); + + // position the element based on a mouse that is in the middle of the chart + const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); + + const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); + positions.forEach(function(p) { + expect(overflow).toHaveProperty(p); + + // all positions should be less than 0 because the tooltip is so much smaller than the chart + expect(overflow[p]).toBeLessThan(0); + }); + }); + + it('identifies an overflow with a positive value in that direction', function() { + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + + // position the element based on a mouse that is in the bottom right hand corner of the chart + const pos = positionTooltip.getBasePosition(size, makeEvent(0.99, 0.99)); + const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); + + positions.forEach(function(p) { + expect(overflow).toHaveProperty(p); + + if (p === 'south' || p === 'east') { + expect(overflow[p]).toBeGreaterThan(0); + } else { + expect(overflow[p]).toBeLessThan(0); + } + }); + }); + + it('identifies only right overflow when tooltip overflows both sides of inner container but outer contains tooltip', function() { + // Size $tooltip larger than chart + const largeWidth = $chart.width() + 10; + $tooltip.css({ width: largeWidth }); + $sizer.css({ width: largeWidth }); + const size = positionTooltip.getTtSize($tooltip.html(), $sizer); + expect(size).toHaveProperty('width', largeWidth); + + // $chart is flush with the $window on the left side + expect(positionTooltip.getBounds($chart).left).toBe(0); + + // Size $window large enough for tooltip on right side + $window.css({ width: $chart.width() * 3 }); + + // Position click event in center of $chart so $tooltip overflows both sides of chart + const pos = positionTooltip.getBasePosition(size, makeEvent(0.5, 0.5)); + + const overflow = positionTooltip.getOverflow(size, pos, [$chart, $window]); + + // no overflow on left (east) + expect(overflow.east).toBeLessThan(0); + // overflow on right (west) + expect(overflow.west).toBeGreaterThan(0); + }); + }); + + describe('positionTooltip() integration', function() { + it('returns nothing if the $chart or $tooltip are not passed in', function() { + expect(positionTooltip() === void 0).toBe(true); + expect(positionTooltip(null, null, null) === void 0).toBe(true); + expect(positionTooltip(null, $(), $()) === void 0).toBe(true); + }); + + function check(xPercent, yPercent /*, prev, directions... */) { + const directions = _.drop(arguments, 2); + const event = makeEvent(xPercent, yPercent); + const placement = positionTooltip({ + $window: $window, + $chart: $chart, + $sizer: $sizer, + event: event, + $el: $tooltip, + prev: _.isObject(directions[0]) ? directions.shift() : null, + }); + + expect(placement).toHaveProperty('top'); + expect(placement).toHaveProperty('left'); + + directions.forEach(function(dir) { + switch (dir) { + case 'top': + expect(placement.top).toBeLessThan(event.clientY); + return; + case 'bottom': + expect(placement.top).toBeGreaterThan(event.clientY); + return; + case 'right': + expect(placement.left).toBeGreaterThan(event.clientX); + return; + case 'left': + expect(placement.left).toBeLessThan(event.clientX); + return; + } + }); + + return placement; + } + + describe('calculates placement of the tooltip properly', function() { + it('mouse is in the middle', function() { + check(0.5, 0.5, 'bottom', 'right'); + }); + + it('mouse is in the top left', function() { + check(0.1, 0.1, 'bottom', 'right'); + }); + + it('mouse is in the top right', function() { + check(0.99, 0.1, 'bottom', 'left'); + }); + + it('mouse is in the bottom right', function() { + check(0.99, 0.99, 'top', 'left'); + }); + + it('mouse is in the bottom left', function() { + check(0.1, 0.99, 'top', 'right'); + }); + }); + + describe('maintain the direction of the tooltip on reposition', function() { + it('mouse moves from the top right to the middle', function() { + const pos = check(0.99, 0.1, 'bottom', 'left'); + check(0.5, 0.5, pos, 'bottom', 'left'); + }); + + it('mouse moves from the bottom left to the middle', function() { + const pos = check(0.1, 0.99, 'top', 'right'); + check(0.5, 0.5, pos, 'top', 'right'); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js b/src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js rename to src/plugins/vis_type_vislib/public/vislib/components/tooltip/tooltip.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/flatten_data.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/inject_zeros.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/ordered_x_keys.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/uniq_keys.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_fill_data_array.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js rename to src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_filled_array.js diff --git a/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js new file mode 100644 index 0000000000000..df502b7cde3df --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/components/zero_injection/zero_injection.test.js @@ -0,0 +1,509 @@ +/* + * 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 _ from 'lodash'; +import { injectZeros } from './inject_zeros'; +import { orderXValues } from './ordered_x_keys'; +import { getUniqKeys } from './uniq_keys'; +import { flattenData } from './flatten_data'; +import { createZeroFilledArray } from './zero_filled_array'; +import { zeroFillDataArray } from './zero_fill_data_array'; + +describe('Vislib Zero Injection Module Test Suite', function() { + const dateHistogramRowsObj = { + xAxisOrderedValues: [ + 1418410560000, + 1418410620000, + 1418410680000, + 1418410740000, + 1418410800000, + 1418410860000, + 1418410920000, + ], + series: [ + { + label: 'html', + values: [ + { x: 1418410560000, y: 2 }, + { x: 1418410620000, y: 4 }, + { x: 1418410680000, y: 1 }, + { x: 1418410740000, y: 5 }, + { x: 1418410800000, y: 2 }, + { x: 1418410860000, y: 3 }, + { x: 1418410920000, y: 2 }, + ], + }, + { + label: 'css', + values: [ + { x: 1418410560000, y: 1 }, + { x: 1418410620000, y: 3 }, + { x: 1418410680000, y: 1 }, + { x: 1418410740000, y: 4 }, + { x: 1418410800000, y: 2 }, + ], + }, + ], + }; + const dateHistogramRows = dateHistogramRowsObj.series; + + const seriesDataObj = { + xAxisOrderedValues: ['v1', 'v2', 'v3', 'v4', 'v5'], + series: [ + { + label: '200', + values: [ + { x: 'v1', y: 234 }, + { x: 'v2', y: 34 }, + { x: 'v3', y: 834 }, + { x: 'v4', y: 1234 }, + { x: 'v5', y: 4 }, + ], + }, + ], + }; + const seriesData = seriesDataObj.series; + + const multiSeriesDataObj = { + xAxisOrderedValues: ['1', '2', '3', '4', '5'], + series: [ + { + label: '200', + values: [ + { x: '1', y: 234 }, + { x: '2', y: 34 }, + { x: '3', y: 834 }, + { x: '4', y: 1234 }, + { x: '5', y: 4 }, + ], + }, + { + label: '404', + values: [ + { x: '1', y: 1234 }, + { x: '3', y: 234 }, + { x: '5', y: 34 }, + ], + }, + { + label: '503', + values: [{ x: '3', y: 834 }], + }, + ], + }; + const multiSeriesData = multiSeriesDataObj.series; + + const multiSeriesNumberedDataObj = { + xAxisOrderedValues: [1, 2, 3, 4, 5], + series: [ + { + label: '200', + values: [ + { x: 1, y: 234 }, + { x: 2, y: 34 }, + { x: 3, y: 834 }, + { x: 4, y: 1234 }, + { x: 5, y: 4 }, + ], + }, + { + label: '404', + values: [ + { x: 1, y: 1234 }, + { x: 3, y: 234 }, + { x: 5, y: 34 }, + ], + }, + { + label: '503', + values: [{ x: 3, y: 834 }], + }, + ], + }; + const multiSeriesNumberedData = multiSeriesNumberedDataObj.series; + + const emptyObject = {}; + const str = 'string'; + const number = 24; + const boolean = false; + const nullValue = null; + const emptyArray = []; + let notAValue; + + describe('Zero Injection (main)', function() { + let sample1; + let sample2; + let sample3; + + beforeEach(() => { + sample1 = injectZeros(seriesData, seriesDataObj); + sample2 = injectZeros(multiSeriesData, multiSeriesDataObj); + sample3 = injectZeros(multiSeriesNumberedData, multiSeriesNumberedDataObj); + }); + + it('should be a function', function() { + expect(_.isFunction(injectZeros)).toBe(true); + }); + + it('should return an object with series[0].values', function() { + expect(_.isObject(sample1)).toBe(true); + expect(_.isObject(sample1[0].values)).toBe(true); + }); + + it('should return the same array of objects when the length of the series array is 1', function() { + expect(sample1[0].values[0].x).toBe(seriesData[0].values[0].x); + expect(sample1[0].values[1].x).toBe(seriesData[0].values[1].x); + expect(sample1[0].values[2].x).toBe(seriesData[0].values[2].x); + expect(sample1[0].values[3].x).toBe(seriesData[0].values[3].x); + expect(sample1[0].values[4].x).toBe(seriesData[0].values[4].x); + }); + + it('should inject zeros in the input array', function() { + expect(sample2[1].values[1].y).toBe(0); + expect(sample2[2].values[0].y).toBe(0); + expect(sample2[2].values[1].y).toBe(0); + expect(sample2[2].values[4].y).toBe(0); + expect(sample3[1].values[1].y).toBe(0); + expect(sample3[2].values[0].y).toBe(0); + expect(sample3[2].values[1].y).toBe(0); + expect(sample3[2].values[4].y).toBe(0); + }); + + it('should return values arrays with the same x values', function() { + expect(sample2[1].values[0].x).toBe(sample2[2].values[0].x); + expect(sample2[1].values[1].x).toBe(sample2[2].values[1].x); + expect(sample2[1].values[2].x).toBe(sample2[2].values[2].x); + expect(sample2[1].values[3].x).toBe(sample2[2].values[3].x); + expect(sample2[1].values[4].x).toBe(sample2[2].values[4].x); + }); + + it('should return values arrays of the same length', function() { + expect(sample2[0].values.length).toBe(sample2[1].values.length); + expect(sample2[0].values.length).toBe(sample2[2].values.length); + expect(sample2[1].values.length).toBe(sample2[2].values.length); + }); + }); + + describe('Order X Values', function() { + let results; + let numberedResults; + + beforeEach(() => { + results = orderXValues(multiSeriesDataObj); + numberedResults = orderXValues(multiSeriesNumberedDataObj); + }); + + it('should return a function', function() { + expect(_.isFunction(orderXValues)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results)).toBe(true); + }); + + it('should return an array of values ordered by their index by default', function() { + expect(results[0]).toBe('1'); + expect(results[1]).toBe('2'); + expect(results[2]).toBe('3'); + expect(results[3]).toBe('4'); + expect(results[4]).toBe('5'); + expect(numberedResults[0]).toBe(1); + expect(numberedResults[1]).toBe(2); + expect(numberedResults[2]).toBe(3); + expect(numberedResults[3]).toBe(4); + expect(numberedResults[4]).toBe(5); + }); + + it('should return an array of values that preserve the index from xAxisOrderedValues', function() { + const data = { + xAxisOrderedValues: ['1', '2', '3', '4', '5'], + series: [ + { + label: '200', + values: [ + { x: '2', y: 34 }, + { x: '4', y: 1234 }, + ], + }, + { + label: '404', + values: [ + { x: '1', y: 1234 }, + { x: '3', y: 234 }, + { x: '5', y: 34 }, + ], + }, + { + label: '503', + values: [{ x: '3', y: 834 }], + }, + ], + }; + const result = orderXValues(data); + expect(result).toEqual(['1', '2', '3', '4', '5']); + }); + + it('should return an array of values ordered by their sum when orderBucketsBySum is true', function() { + const orderBucketsBySum = true; + results = orderXValues(multiSeriesDataObj, orderBucketsBySum); + numberedResults = orderXValues(multiSeriesNumberedDataObj, orderBucketsBySum); + + expect(results[0]).toBe('3'); + expect(results[1]).toBe('1'); + expect(results[2]).toBe('4'); + expect(results[3]).toBe('5'); + expect(results[4]).toBe('2'); + expect(numberedResults[0]).toBe(3); + expect(numberedResults[1]).toBe(1); + expect(numberedResults[2]).toBe(4); + expect(numberedResults[3]).toBe(5); + expect(numberedResults[4]).toBe(2); + }); + }); + + describe('Unique Keys', function() { + let results; + + beforeEach(() => { + results = getUniqKeys(multiSeriesDataObj); + }); + + it('should throw an error if input is not an object', function() { + expect(function() { + getUniqKeys(str); + }).toThrow(); + + expect(function() { + getUniqKeys(number); + }).toThrow(); + + expect(function() { + getUniqKeys(boolean); + }).toThrow(); + + expect(function() { + getUniqKeys(nullValue); + }).toThrow(); + + expect(function() { + getUniqKeys(emptyArray); + }).toThrow(); + + expect(function() { + getUniqKeys(notAValue); + }).toThrow(); + }); + + it('should return a function', function() { + expect(_.isFunction(getUniqKeys)).toBe(true); + }); + + it('should return an object', function() { + expect(_.isObject(results)).toBe(true); + }); + + it('should return an object of unique keys', function() { + expect(_.uniq(_.keys(results)).length).toBe(_.keys(results).length); + }); + }); + + describe('Flatten Data', function() { + let results; + + beforeEach(() => { + results = flattenData(multiSeriesDataObj); + }); + + it('should return a function', function() { + expect(_.isFunction(flattenData)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results)).toBe(true); + }); + + it('should return an array of objects', function() { + expect(_.isObject(results[0])).toBe(true); + expect(_.isObject(results[1])).toBe(true); + expect(_.isObject(results[2])).toBe(true); + }); + }); + + describe('Zero Filled Array', function() { + const arr1 = [1, 2, 3, 4, 5]; + const arr2 = ['1', '2', '3', '4', '5']; + let results1; + let results2; + + beforeEach(() => { + results1 = createZeroFilledArray(arr1); + results2 = createZeroFilledArray(arr2); + }); + + it('should throw an error if input is not an array', function() { + expect(function() { + createZeroFilledArray(str); + }).toThrow(); + + expect(function() { + createZeroFilledArray(number); + }).toThrow(); + + expect(function() { + createZeroFilledArray(boolean); + }).toThrow(); + + expect(function() { + createZeroFilledArray(nullValue); + }).toThrow(); + + expect(function() { + createZeroFilledArray(emptyObject); + }).toThrow(); + + expect(function() { + createZeroFilledArray(notAValue); + }).toThrow(); + }); + + it('should return a function', function() { + expect(_.isFunction(createZeroFilledArray)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results1)).toBe(true); + }); + + it('should return an array of objects', function() { + expect(_.isObject(results1[0])).toBe(true); + expect(_.isObject(results1[1])).toBe(true); + expect(_.isObject(results1[2])).toBe(true); + expect(_.isObject(results1[3])).toBe(true); + expect(_.isObject(results1[4])).toBe(true); + }); + + it('should return an array of objects where each y value is 0', function() { + expect(results1[0].y).toBe(0); + expect(results1[1].y).toBe(0); + expect(results1[2].y).toBe(0); + expect(results1[3].y).toBe(0); + expect(results1[4].y).toBe(0); + }); + + it('should return an array of objects where each x values are numbers', function() { + expect(_.isNumber(results1[0].x)).toBe(true); + expect(_.isNumber(results1[1].x)).toBe(true); + expect(_.isNumber(results1[2].x)).toBe(true); + expect(_.isNumber(results1[3].x)).toBe(true); + expect(_.isNumber(results1[4].x)).toBe(true); + }); + + it('should return an array of objects where each x values are strings', function() { + expect(_.isString(results2[0].x)).toBe(true); + expect(_.isString(results2[1].x)).toBe(true); + expect(_.isString(results2[2].x)).toBe(true); + expect(_.isString(results2[3].x)).toBe(true); + expect(_.isString(results2[4].x)).toBe(true); + }); + }); + + describe('Zero Filled Data Array', function() { + const xValueArr = [1, 2, 3, 4, 5]; + let arr1; + const arr2 = [{ x: 3, y: 834 }]; + let results; + + beforeEach(() => { + arr1 = createZeroFilledArray(xValueArr); + // Takes zero array as 1st arg and data array as 2nd arg + results = zeroFillDataArray(arr1, arr2); + }); + + it('should throw an error if input are not arrays', function() { + expect(function() { + zeroFillDataArray(str, str); + }).toThrow(); + + expect(function() { + zeroFillDataArray(number, number); + }).toThrow(); + + expect(function() { + zeroFillDataArray(boolean, boolean); + }).toThrow(); + + expect(function() { + zeroFillDataArray(nullValue, nullValue); + }).toThrow(); + + expect(function() { + zeroFillDataArray(emptyObject, emptyObject); + }).toThrow(); + + expect(function() { + zeroFillDataArray(notAValue, notAValue); + }).toThrow(); + }); + + it('should return a function', function() { + expect(_.isFunction(zeroFillDataArray)).toBe(true); + }); + + it('should return an array', function() { + expect(Array.isArray(results)).toBe(true); + }); + + it('should return an array of objects', function() { + expect(_.isObject(results[0])).toBe(true); + expect(_.isObject(results[1])).toBe(true); + expect(_.isObject(results[2])).toBe(true); + }); + + it('should return an array with zeros injected in the appropriate objects as y values', function() { + expect(results[0].y).toBe(0); + expect(results[1].y).toBe(0); + expect(results[3].y).toBe(0); + expect(results[4].y).toBe(0); + }); + }); + + describe('Injected Zero values return in the correct order', function() { + let results; + + beforeEach(() => { + results = injectZeros(dateHistogramRows, dateHistogramRowsObj); + }); + + it('should return an array of objects', function() { + results.forEach(function(row) { + expect(Array.isArray(row.values)).toBe(true); + }); + }); + + it('should return ordered x values', function() { + const values = results[0].values; + expect(values[0].x).toBeLessThan(values[1].x); + expect(values[1].x).toBeLessThan(values[2].x); + expect(values[2].x).toBeLessThan(values[3].x); + expect(values[3].x).toBeLessThan(values[4].x); + expect(values[4].x).toBeLessThan(values[5].x); + expect(values[5].x).toBeLessThan(values[6].x); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/errors.ts b/src/plugins/vis_type_vislib/public/vislib/errors.ts similarity index 95% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/errors.ts rename to src/plugins/vis_type_vislib/public/vislib/errors.ts index 9014349c38d25..c2965e8165759 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/errors.ts +++ b/src/plugins/vis_type_vislib/public/vislib/errors.ts @@ -19,7 +19,7 @@ /* eslint-disable max-classes-per-file */ -import { KbnError } from '../../../../../plugins/kibana_utils/public'; +import { KbnError } from '../../../kibana_utils/public'; export class VislibError extends KbnError { constructor(message: string) { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts index 2c6d62ed084b5..c3b82f72af482 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/hierarchical/build_hierarchical_data.ts @@ -18,7 +18,7 @@ */ import { toArray } from 'lodash'; -import { SerializedFieldFormat } from '../../../../../../../plugins/expressions/common/types'; +import { SerializedFieldFormat } from '../../../../../expressions/common/types'; import { getFormatService } from '../../../services'; import { Table } from '../../types'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/index.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_add_to_siri.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_fake_x_aspect.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_aspects.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts similarity index 97% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts index 0c79c5b263cea..dc10c9f4938a0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts +++ b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IFieldFormatsRegistry } from '../../../../../../../plugins/data/common'; +import { IFieldFormatsRegistry } from '../../../../../data/common'; import { getPoint } from './_get_point'; import { setFormatService } from '../../../services'; import { Aspect } from './point_series'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_point.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_get_series.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_x_axis.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_init_y_axis.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/_ordered_date_axis.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/index.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts b/src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts rename to src/plugins/vis_type_vislib/public/vislib/helpers/point_series/point_series.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/__snapshots__/dispatch_heatmap.test.js.snap b/src/plugins/vis_type_vislib/public/vislib/lib/__snapshots__/dispatch_heatmap.test.js.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/__snapshots__/dispatch_heatmap.test.js.snap rename to src/plugins/vis_type_vislib/public/vislib/lib/__snapshots__/dispatch_heatmap.test.js.snap diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_alerts.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_alerts.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_alerts.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/_alerts.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_data_label.js b/src/plugins/vis_type_vislib/public/vislib/lib/_data_label.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_data_label.js rename to src/plugins/vis_type_vislib/public/vislib/lib/_data_label.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_error_handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_error_handler.js rename to src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.test.js new file mode 100644 index 0000000000000..b7764b92b7805 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/_error_handler.test.js @@ -0,0 +1,48 @@ +/* + * 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 { ErrorHandler } from './_error_handler'; + +describe('Vislib ErrorHandler Test Suite', function() { + let errorHandler; + + beforeEach(() => { + errorHandler = new ErrorHandler(); + }); + + describe('validateWidthandHeight Method', function() { + it('should throw an error when width and/or height is 0', function() { + expect(function() { + errorHandler.validateWidthandHeight(0, 200); + }).toThrow(); + expect(function() { + errorHandler.validateWidthandHeight(200, 0); + }).toThrow(); + }); + + it('should throw an error when width and/or height is NaN', function() { + expect(function() { + errorHandler.validateWidthandHeight(null, 200); + }).toThrow(); + expect(function() { + errorHandler.validateWidthandHeight(200, null); + }).toThrow(); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_handler.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_handler.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/_handler.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_index.scss b/src/plugins/vis_type_vislib/public/vislib/lib/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/alerts.js b/src/plugins/vis_type_vislib/public/vislib/lib/alerts.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/alerts.js rename to src/plugins/vis_type_vislib/public/vislib/lib/alerts.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.test.js new file mode 100644 index 0000000000000..dec7de5ceeda9 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis.test.js @@ -0,0 +1,241 @@ +/* + * 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 d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; + +import { Axis } from './axis'; +import { VisConfig } from '../vis_config'; +import { getMockUiState } from '../../../fixtures/mocks'; + +describe('Vislib Axis Class Test Suite', function() { + let mockUiState; + let yAxis; + let el; + let fixture; + let seriesData; + + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734130000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + ], + }, + { + label: 'Count2', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734140000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + ], + }, + ], + xAxisFormatter: function(thing) { + return new Date(thing); + }, + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + mockUiState = getMockUiState(); + el = d3 + .select('body') + .append('div') + .attr('class', 'visAxis--x') + .style('height', '40px'); + + fixture = el.append('div').attr('class', 'x-axis-div'); + + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + $('.x-axis-div')[0], + () => undefined + ); + yAxis = new Axis(visConfig, { + type: 'value', + id: 'ValueAxis-1', + }); + + seriesData = data.series.map(series => { + return series.values; + }); + }); + + afterEach(function() { + fixture.remove(); + el.remove(); + }); + + describe('_stackNegAndPosVals Method', function() { + it('should correctly stack positive values', function() { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 8, + }, + { + x: 1408734090000, + y: 23, + y0: 23, + }, + { + x: 1408734120000, + y: 30, + y0: 30, + }, + { + x: 1408734140000, + y: 30, + y0: 0, + }, + { + x: 1408734150000, + y: 28, + y0: 28, + }, + ]; + const stackedData = yAxis._stackNegAndPosVals(seriesData); + expect(stackedData[1]).toEqual(expectedResult); + }); + + it('should correctly stack pos and neg values', function() { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 0, + }, + { + x: 1408734090000, + y: 23, + y0: 0, + }, + { + x: 1408734120000, + y: 30, + y0: 0, + }, + { + x: 1408734140000, + y: 30, + y0: 0, + }, + { + x: 1408734150000, + y: 28, + y0: 0, + }, + ]; + const dataClone = _.cloneDeep(seriesData); + dataClone[0].forEach(value => { + value.y = -value.y; + }); + const stackedData = yAxis._stackNegAndPosVals(dataClone); + expect(stackedData[1]).toEqual(expectedResult); + }); + + it('should correctly stack mixed pos and neg values', function() { + const expectedResult = [ + { + x: 1408734060000, + y: 8, + y0: 8, + }, + { + x: 1408734090000, + y: 23, + y0: 0, + }, + { + x: 1408734120000, + y: 30, + y0: 30, + }, + { + x: 1408734140000, + y: 30, + y0: 0, + }, + { + x: 1408734150000, + y: 28, + y0: 28, + }, + ]; + const dataClone = _.cloneDeep(seriesData); + dataClone[0].forEach((value, i) => { + if (i % 2 === 1) value.y = -value.y; + }); + const stackedData = yAxis._stackNegAndPosVals(dataClone); + expect(stackedData[1]).toEqual(expectedResult); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.test.js new file mode 100644 index 0000000000000..7901919d306d2 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_title.test.js @@ -0,0 +1,211 @@ +/* + * 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 d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; + +import { AxisTitle } from './axis_title'; +import { AxisConfig } from './axis_config'; +import { VisConfig } from '../vis_config'; +import { Data } from '../data'; +import { getMockUiState } from '../../../fixtures/mocks'; + +describe('Vislib AxisTitle Class Test Suite', function() { + let el; + let dataObj; + let xTitle; + let yTitle; + let visConfig; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper'); + + el.append('div') + .attr('class', 'visAxis__column--bottom') + .append('div') + .attr('class', 'axis-title y-axis-title') + .style('height', '20px') + .style('width', '20px'); + + el.append('div') + .attr('class', 'visAxis__column--left') + .append('div') + .attr('class', 'axis-title x-axis-title') + .style('height', '20px') + .style('width', '20px'); + + const uiState = getMockUiState(); + uiState.set('vis.colors', []); + dataObj = new Data(data, getMockUiState(), () => undefined); + visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + getMockUiState(), + el.node(), + () => undefined + ); + const xAxisConfig = new AxisConfig(visConfig, { + position: 'bottom', + title: { + text: dataObj.get('xAxisLabel'), + }, + }); + const yAxisConfig = new AxisConfig(visConfig, { + position: 'left', + title: { + text: dataObj.get('yAxisLabel'), + }, + }); + xTitle = new AxisTitle(xAxisConfig); + yTitle = new AxisTitle(yAxisConfig); + }); + + afterEach(function() { + el.remove(); + }); + + it('should not do anything if title.show is set to false', function() { + const xAxisConfig = new AxisConfig(visConfig, { + position: 'bottom', + show: false, + title: { + text: dataObj.get('xAxisLabel'), + }, + }); + xTitle = new AxisTitle(xAxisConfig); + xTitle.render(); + expect( + $(el.node()) + .find('.x-axis-title') + .find('svg').length + ).toBe(0); + }); + + describe('render Method', function() { + beforeEach(function() { + xTitle.render(); + yTitle.render(); + }); + + it('should append an svg to div', function() { + expect(el.select('.x-axis-title').selectAll('svg').length).toBe(1); + expect(el.select('.y-axis-title').selectAll('svg').length).toBe(1); + }); + + it('should append a g element to the svg', function() { + expect( + el + .select('.x-axis-title') + .selectAll('svg') + .select('g').length + ).toBe(1); + expect( + el + .select('.y-axis-title') + .selectAll('svg') + .select('g').length + ).toBe(1); + }); + + it('should append text', function() { + expect( + !!el + .select('.x-axis-title') + .selectAll('svg') + .selectAll('text') + ).toBe(true); + expect( + !!el + .select('.y-axis-title') + .selectAll('svg') + .selectAll('text') + ).toBe(true); + }); + }); + + describe('draw Method', function() { + it('should be a function', function() { + expect(_.isFunction(xTitle.draw())).toBe(true); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/index.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/index.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/scale_modes.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js rename to src/plugins/vis_type_vislib/public/vislib/lib/axis/time_ticks.test.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/x_axis.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/x_axis.test.js new file mode 100644 index 0000000000000..d007a8a14de13 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/x_axis.test.js @@ -0,0 +1,254 @@ +/* + * 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 d3 from 'd3'; +import _ from 'lodash'; +import $ from 'jquery'; + +import { Axis } from './axis'; +import { VisConfig } from '../vis_config'; +import { getMockUiState } from '../../../fixtures/mocks'; + +describe('Vislib xAxis Class Test Suite', function() { + let mockUiState; + let xAxis; + let el; + let fixture; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + xAxisOrderedValues: [ + 1408734060000, + 1408734090000, + 1408734120000, + 1408734150000, + 1408734180000, + 1408734210000, + 1408734240000, + 1408734270000, + 1408734300000, + 1408734330000, + ], + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisFormatter: function(thing) { + return new Date(thing); + }, + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + mockUiState = getMockUiState(); + el = d3 + .select('body') + .append('div') + .attr('class', 'visAxis--x') + .style('height', '40px'); + + fixture = el.append('div').attr('class', 'x-axis-div'); + + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + $('.x-axis-div')[0], + () => undefined + ); + xAxis = new Axis(visConfig, { + type: 'category', + id: 'CategoryAxis-1', + }); + }); + + afterEach(function() { + fixture.remove(); + el.remove(); + }); + + describe('render Method', function() { + beforeEach(function() { + xAxis.render(); + }); + + it('should append an svg to div', function() { + expect(el.selectAll('svg').length).toBe(1); + }); + + it('should append a g element to the svg', function() { + expect(el.selectAll('svg').select('g').length).toBe(1); + }); + + it('should append ticks with text', function() { + expect(!!el.selectAll('svg').selectAll('.tick text')).toBe(true); + }); + }); + + describe('getScale, getDomain, getTimeDomain, and getRange Methods', function() { + let timeScale; + let width; + let range; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.getAxis(width); + timeScale = xAxis.getScale(); + range = xAxis.axisScale.getRange(width); + }); + + it('should return a function', function() { + expect(_.isFunction(timeScale)).toBe(true); + }); + + it('should return the correct domain', function() { + expect(_.isDate(timeScale.domain()[0])).toBe(true); + expect(_.isDate(timeScale.domain()[1])).toBe(true); + }); + + it('should return the min and max dates', function() { + expect(timeScale.domain()[0].toDateString()).toBe(new Date(1408734060000).toDateString()); + expect(timeScale.domain()[1].toDateString()).toBe(new Date(1408734330000).toDateString()); + }); + + it('should return the correct range', function() { + expect(range[0]).toBe(0); + expect(range[1]).toBe(width); + }); + }); + + describe('getOrdinalDomain Method', function() { + let ordinalScale; + let ordinalDomain; + let width; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.ordered = null; + xAxis.axisConfig.ordered = null; + xAxis.getAxis(width); + ordinalScale = xAxis.getScale(); + ordinalDomain = ordinalScale.domain(['this', 'should', 'be', 'an', 'array']); + }); + + it('should return an ordinal scale', function() { + expect(ordinalDomain.domain()[0]).toBe('this'); + expect(ordinalDomain.domain()[4]).toBe('array'); + }); + + it('should return an array of values', function() { + expect(Array.isArray(ordinalDomain.domain())).toBe(true); + }); + }); + + describe('getXScale Method', function() { + let width; + let xScale; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.getAxis(width); + xScale = xAxis.getScale(); + }); + + it('should return a function', function() { + expect(_.isFunction(xScale)).toBe(true); + }); + + it('should return a domain', function() { + expect(_.isDate(xScale.domain()[0])).toBe(true); + expect(_.isDate(xScale.domain()[1])).toBe(true); + }); + + it('should return a range', function() { + expect(xScale.range()[0]).toBe(0); + expect(xScale.range()[1]).toBe(width); + }); + }); + + describe('getXAxis Method', function() { + let width; + + beforeEach(function() { + width = $('.x-axis-div').width(); + xAxis.getAxis(width); + }); + + it('should create an getScale function on the xAxis class', function() { + expect(_.isFunction(xAxis.getScale())).toBe(true); + }); + }); + + describe('draw Method', function() { + it('should be a function', function() { + expect(_.isFunction(xAxis.draw())).toBe(true); + }); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/y_axis.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/y_axis.test.js new file mode 100644 index 0000000000000..85378ff1a14e8 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/y_axis.test.js @@ -0,0 +1,381 @@ +/* + * 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 _ from 'lodash'; +import d3 from 'd3'; +import $ from 'jquery'; + +import { Axis } from './axis'; +import { VisConfig } from '../vis_config'; +import { getMockUiState } from '../../../fixtures/mocks'; + +const YAxis = Axis; +let mockUiState; +let el; +let buildYAxis; +let yAxis; +let yAxisDiv; + +const timeSeries = [ + 1408734060000, + 1408734090000, + 1408734120000, + 1408734150000, + 1408734180000, + 1408734210000, + 1408734240000, + 1408734270000, + 1408734300000, + 1408734330000, +]; + +const defaultGraphData = [ + [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], + [2, 13, 20, 18, 26, 20, 16, 12, 19, 14], +]; + +function makeSeriesData(data) { + return timeSeries.map(function(timestamp, i) { + return { + x: timestamp, + y: data[i] || 0, + }; + }); +} + +function createData(seriesData) { + const data = { + hits: 621, + label: 'test', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: seriesData.map(function(series) { + return { values: makeSeriesData(series) }; + }), + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + const node = $('
') + .css({ + height: 40, + width: 40, + }) + .appendTo('body') + .addClass('y-axis-wrapper') + .get(0); + + el = d3.select(node).datum(data); + + yAxisDiv = el.append('div').attr('class', 'y-axis-div'); + + buildYAxis = function(params) { + const visConfig = new VisConfig( + { + type: 'histogram', + }, + data, + mockUiState, + node, + () => undefined + ); + return new YAxis( + visConfig, + _.merge( + {}, + { + id: 'ValueAxis-1', + type: 'value', + scale: { + defaultYMin: true, + setYExtents: false, + }, + }, + params + ) + ); + }; + + yAxis = buildYAxis(); +} + +describe('Vislib yAxis Class Test Suite', function() { + beforeEach(() => { + mockUiState = getMockUiState(); + expect($('.y-axis-wrapper')).toHaveLength(0); + }); + + afterEach(function() { + if (el) { + el.remove(); + yAxisDiv.remove(); + } + }); + + describe('render Method', function() { + beforeEach(function() { + createData(defaultGraphData); + yAxis.render(); + }); + + it('should append an svg to div', function() { + expect(el.selectAll('svg').length).toBe(1); + }); + + it('should append a g element to the svg', function() { + expect(el.selectAll('svg').select('g').length).toBe(1); + }); + + it('should append ticks with text', function() { + expect(!!el.selectAll('svg').selectAll('.tick text')).toBe(true); + }); + }); + + describe('getYScale Method', function() { + let yScale; + let graphData; + let domain; + const height = 50; + + function checkDomain(min, max) { + const domain = yScale.domain(); + expect(domain[0]).toBeLessThan(min + 1); + expect(domain[1]).toBeGreaterThan(max - 1); + return domain; + } + + function checkRange() { + expect(yScale.range()[0]).toBe(height); + expect(yScale.range()[1]).toBe(0); + } + + describe('API', function() { + beforeEach(function() { + createData(defaultGraphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should return a function', function() { + expect(_.isFunction(yScale)).toBe(true); + }); + }); + + describe('positive values', function() { + beforeEach(function() { + graphData = defaultGraphData; + createData(graphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should have domain between 0 and max value', function() { + const min = 0; + const max = _.max(_.flattenDeep(graphData)); + const domain = checkDomain(min, max); + expect(domain[1]).toBeGreaterThan(0); + checkRange(); + }); + }); + + describe('negative values', function() { + beforeEach(function() { + graphData = [ + [-8, -23, -30, -28, -36, -30, -26, -22, -29, -24], + [-22, -8, -30, -4, 0, 0, -3, -22, -14, -24], + ]; + createData(graphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should have domain between min value and 0', function() { + const min = _.min(_.flattenDeep(graphData)); + const max = 0; + const domain = checkDomain(min, max); + expect(domain[0]).toBeLessThan(0); + checkRange(); + }); + }); + + describe('positive and negative values', function() { + beforeEach(function() { + graphData = [ + [8, 23, 30, 28, 36, 30, 26, 22, 29, 24], + [22, 8, -30, -4, 0, 0, 3, -22, 14, 24], + ]; + createData(graphData); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should have domain between min and max values', function() { + const min = _.min(_.flattenDeep(graphData)); + const max = _.max(_.flattenDeep(graphData)); + const domain = checkDomain(min, max); + expect(domain[0]).toBeLessThan(0); + expect(domain[1]).toBeGreaterThan(0); + checkRange(); + }); + }); + + describe('validate user defined values', function() { + beforeEach(function() { + createData(defaultGraphData); + yAxis.axisConfig.set('scale.stacked', true); + yAxis.axisConfig.set('scale.setYExtents', false); + yAxis.getAxis(height); + yScale = yAxis.getScale(); + }); + + it('should throw a NaN error', function() { + const min = 'Not a number'; + const max = 12; + + expect(function() { + yAxis.axisScale.validateUserExtents(min, max); + }).toThrow(); + }); + + it('should return a decimal value', function() { + yAxis.axisConfig.set('scale.mode', 'percentage'); + yAxis.axisConfig.set('scale.setYExtents', true); + yAxis.getAxis(height); + domain = []; + domain[0] = 20; + domain[1] = 80; + const newDomain = yAxis.axisScale.validateUserExtents(domain); + + expect(newDomain[0]).toBe(domain[0] / 100); + expect(newDomain[1]).toBe(domain[1] / 100); + }); + + it('should return the user defined value', function() { + domain = [20, 50]; + const newDomain = yAxis.axisScale.validateUserExtents(domain); + + expect(newDomain[0]).toBe(domain[0]); + expect(newDomain[1]).toBe(domain[1]); + }); + }); + + describe('should throw an error when', function() { + it('min === max', function() { + const min = 12; + const max = 12; + + expect(function() { + yAxis.axisScale.validateAxisExtents(min, max); + }).toThrow(); + }); + + it('min > max', function() { + const min = 30; + const max = 10; + + expect(function() { + yAxis.axisScale.validateAxisExtents(min, max); + }).toThrow(); + }); + }); + }); + + describe('getScaleType method', function() { + const fnNames = ['linear', 'log', 'square root']; + + it('should return a function', function() { + fnNames.forEach(function(fnName) { + expect(yAxis.axisScale.getD3Scale(fnName)).toEqual(expect.any(Function)); + }); + + // if no value is provided to the function, scale should default to a linear scale + expect(yAxis.axisScale.getD3Scale()).toEqual(expect.any(Function)); + }); + + it('should throw an error if function name is undefined', function() { + expect(function() { + yAxis.axisScale.getD3Scale('square'); + }).toThrow(); + }); + }); + + describe('_logDomain method', function() { + it('should throw an error', function() { + expect(function() { + yAxis.axisScale.logDomain(-10, -5); + }).toThrow(); + expect(function() { + yAxis.axisScale.logDomain(-10, 5); + }).toThrow(); + expect(function() { + yAxis.axisScale.logDomain(0, -5); + }).toThrow(); + }); + + it('should return a yMin value of 1', function() { + const yMin = yAxis.axisScale.logDomain(0, 200)[0]; + expect(yMin).toBe(1); + }); + }); + + describe('getYAxis method', function() { + let yMax; + beforeEach(function() { + createData(defaultGraphData); + yMax = yAxis.yMax; + }); + + afterEach(function() { + yAxis.yMax = yMax; + yAxis = buildYAxis(); + }); + + it('should use decimal format for small values', function() { + yAxis.yMax = 1; + const tickFormat = yAxis.getAxis().tickFormat(); + expect(tickFormat(0.8)).toBe('0.8'); + }); + }); + + describe('draw Method', function() { + beforeEach(function() { + createData(defaultGraphData); + }); + + it('should be a function', function() { + expect(_.isFunction(yAxis.draw())).toBe(true); + }); + }); + + describe('tickScale Method', function() { + beforeEach(function() { + createData(defaultGraphData); + }); + + it('should return the correct number of ticks', function() { + expect(yAxis.tickScale(1000)).toBe(11); + expect(yAxis.tickScale(40)).toBe(3); + expect(yAxis.tickScale(20)).toBe(0); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/binder.ts b/src/plugins/vis_type_vislib/public/vislib/lib/binder.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/binder.ts rename to src/plugins/vis_type_vislib/public/vislib/lib/binder.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_grid.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_grid.js rename to src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/chart_title.js rename to src/plugins/vis_type_vislib/public/vislib/lib/chart_title.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/data.js b/src/plugins/vis_type_vislib/public/vislib/lib/data.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/data.js rename to src/plugins/vis_type_vislib/public/vislib/lib/data.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/data.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/data.test.js new file mode 100644 index 0000000000000..b1a91979b3d9d --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/data.test.js @@ -0,0 +1,309 @@ +/* + * 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 _ from 'lodash'; + +import { Data } from './data'; +import { getMockUiState } from '../../fixtures/mocks'; + +const seriesData = { + label: '', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], +}; + +const rowsData = { + rows: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +const colsData = { + columns: [ + { + label: 'a', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'b', + series: [ + { + label: '300', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'c', + series: [ + { + label: '100', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + { + label: 'd', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: 2 }, + { x: 2, y: 3 }, + ], + }, + ], + }, + ], +}; + +describe('Vislib Data Class Test Suite', function() { + let mockUiState; + + beforeEach(() => { + mockUiState = getMockUiState(); + }); + + describe('Data Class (main)', function() { + it('should be a function', function() { + expect(_.isFunction(Data)).toBe(true); + }); + + it('should return an object', function() { + const rowIn = new Data(rowsData, mockUiState, () => undefined); + expect(_.isObject(rowIn)).toBe(true); + }); + }); + + describe('_removeZeroSlices', function() { + let data; + const pieData = { + slices: { + children: [{ size: 30 }, { size: 20 }, { size: 0 }], + }, + }; + + beforeEach(function() { + data = new Data(pieData, mockUiState, () => undefined); + }); + + it('should remove zero values', function() { + const slices = data._removeZeroSlices(data.data.slices); + expect(slices.children.length).toBe(2); + }); + }); + + describe('Data.flatten', function() { + let serIn; + let serOut; + + beforeEach(function() { + serIn = new Data(seriesData, mockUiState, () => undefined); + serOut = serIn.flatten(); + }); + + it('should return an array of value objects from every series', function() { + expect(serOut.every(_.isObject)).toBe(true); + }); + + it('should return all points from every series', testLength(seriesData)); + it('should return all points from every series in the rows', testLength(rowsData)); + it('should return all points from every series in the columns', testLength(colsData)); + + function testLength(inputData) { + return function() { + const data = new Data(inputData, mockUiState, () => undefined); + const len = _.reduce( + data.chartData(), + function(sum, chart) { + return ( + sum + + chart.series.reduce(function(sum, series) { + return sum + series.values.length; + }, 0) + ); + }, + 0 + ); + + expect(data.flatten()).toHaveLength(len); + }; + } + }); + + describe('geohashGrid methods', function() { + let data; + const geohashGridData = { + hits: 3954, + rows: [ + { + title: 'Top 5 _type: apache', + label: 'Top 5 _type: apache', + geoJson: { + type: 'FeatureCollection', + features: [], + properties: { + min: 2, + max: 331, + zoom: 3, + center: [47.517200697839414, -112.06054687499999], + }, + }, + }, + { + title: 'Top 5 _type: nginx', + label: 'Top 5 _type: nginx', + geoJson: { + type: 'FeatureCollection', + features: [], + properties: { + min: 1, + max: 88, + zoom: 3, + center: [47.517200697839414, -112.06054687499999], + }, + }, + }, + ], + }; + + beforeEach(function() { + data = new Data(geohashGridData, mockUiState, () => undefined); + }); + + describe('getVisData', function() { + it('should return the rows property', function() { + const visData = data.getVisData(); + expect(visData[0].title).toEqual(geohashGridData.rows[0].title); + }); + }); + + describe('getGeoExtents', function() { + it('should return the min and max geoJson properties', function() { + const minMax = data.getGeoExtents(); + expect(minMax.min).toBe(1); + expect(minMax.max).toBe(331); + }); + }); + }); + + describe('null value check', function() { + it('should return false', function() { + const data = new Data(rowsData, mockUiState, () => undefined); + expect(data.hasNullValues()).toBe(false); + }); + + it('should return true', function() { + const nullRowData = { rows: rowsData.rows.slice(0) }; + nullRowData.rows.push({ + label: 'e', + series: [ + { + label: '200', + values: [ + { x: 0, y: 1 }, + { x: 1, y: null }, + { x: 2, y: 3 }, + ], + }, + ], + }); + + const data = new Data(nullRowData, mockUiState, () => undefined); + expect(data.hasNullValues()).toBe(true); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/dispatch.js rename to src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_heatmap.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_heatmap.test.js similarity index 75% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_heatmap.test.js rename to src/plugins/vis_type_vislib/public/vislib/lib/dispatch_heatmap.test.js index e22f19ea643fd..4e650d4c20f97 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/dispatch_heatmap.test.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_heatmap.test.js @@ -17,12 +17,11 @@ * under the License. */ -import mockDispatchDataD3 from './fixtures/dispatch_heatmap_d3.json'; -import { Dispatch } from '../../lib/dispatch'; -import mockdataPoint from './fixtures/dispatch_heatmap_data_point.json'; -import mockConfigPercentage from './fixtures/dispatch_heatmap_config.json'; +import mockDispatchDataD3 from '../../fixtures/dispatch_heatmap_d3.json'; +import { Dispatch } from './dispatch'; +import mockdataPoint from '../../fixtures/dispatch_heatmap_data_point.json'; +import mockConfigPercentage from '../../fixtures/dispatch_heatmap_config.json'; -jest.mock('ui/new_platform'); jest.mock('d3', () => ({ event: { target: { @@ -32,15 +31,6 @@ jest.mock('d3', () => ({ }, }, })); -jest.mock('../../../legacy_imports.ts', () => ({ - ...jest.requireActual('../../../legacy_imports.ts'), - chrome: { - getUiSettingsClient: () => ({ - get: () => '', - }), - addBasePath: () => {}, - }, -})); function getHandlerMock(config = {}, data = {}) { return { diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js new file mode 100644 index 0000000000000..a680788281fb1 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch_vertical_bar_chart.test.js @@ -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 mockDispatchDataD3 from '../../fixtures/dispatch_bar_chart_d3.json'; +import { Dispatch } from './dispatch'; +import mockdataPoint from '../../fixtures/dispatch_bar_chart_data_point.json'; +import mockConfigPercentage from '../../fixtures/dispatch_bar_chart_config_percentage.json'; +import mockConfigNormal from '../../fixtures/dispatch_bar_chart_config_normal.json'; + +jest.mock('d3', () => ({ + event: { + target: { + nearestViewportElement: { + __data__: mockDispatchDataD3, + }, + }, + }, +})); + +function getHandlerMock(config = {}, data = {}) { + return { + visConfig: { get: (id, fallback) => config[id] || fallback }, + data, + }; +} + +describe('Vislib event responses dispatcher', () => { + test('return data for a vertical bars popover in percentage mode', () => { + const dataPoint = mockdataPoint; + const handlerMock = getHandlerMock(mockConfigPercentage); + const dispatch = new Dispatch(handlerMock); + const actual = dispatch.eventResponse(dataPoint, 0); + expect(actual.isPercentageMode).toBeTruthy(); + }); + + test('return data for a vertical bars popover in normal mode', () => { + const dataPoint = mockdataPoint; + const handlerMock = getHandlerMock(mockConfigNormal); + const dispatch = new Dispatch(handlerMock); + const actual = dispatch.eventResponse(dataPoint, 0); + expect(actual.isPercentageMode).toBeFalsy(); + }); +}); diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js new file mode 100644 index 0000000000000..f5b1c13f1a83f --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -0,0 +1,256 @@ +/* + * 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 d3 from 'd3'; +import _ from 'lodash'; +import MarkdownIt from 'markdown-it'; + +import { NoResults } from '../errors'; +import { Layout } from './layout/layout'; +import { ChartTitle } from './chart_title'; +import { Alerts } from './alerts'; +import { Axis } from './axis/axis'; +import { ChartGrid as Grid } from './chart_grid'; +import { visTypes as chartTypes } from '../visualizations/vis_types'; +import { Binder } from './binder'; +import { dispatchRenderComplete } from '../../../../kibana_utils/public'; + +const markdownIt = new MarkdownIt({ + html: false, + linkify: true, +}); + +/** + * Handles building all the components of the visualization + * + * @class Handler + * @constructor + * @param vis {Object} Reference to the Vis Class Constructor + * @param opts {Object} Reference to Visualization constructors needed to + * create the visualization + */ +export class Handler { + constructor(vis, visConfig, deps) { + this.el = visConfig.get('el'); + this.ChartClass = chartTypes[visConfig.get('type')]; + this.deps = deps; + this.charts = []; + + this.vis = vis; + this.visConfig = visConfig; + this.data = visConfig.data; + + this.categoryAxes = visConfig + .get('categoryAxes') + .map(axisArgs => new Axis(visConfig, axisArgs)); + this.valueAxes = visConfig.get('valueAxes').map(axisArgs => new Axis(visConfig, axisArgs)); + this.chartTitle = new ChartTitle(visConfig); + this.alerts = new Alerts(this, visConfig.get('alerts')); + this.grid = new Grid(this, visConfig.get('grid')); + + if (visConfig.get('type') === 'point_series') { + this.data.stackData(this); + } + + if (visConfig.get('resize', false)) { + this.resize = visConfig.get('resize'); + } + + this.layout = new Layout(visConfig); + this.binder = new Binder(); + this.renderArray = _.filter([this.layout, this.chartTitle, this.alerts], Boolean); + + this.renderArray = this.renderArray + .concat(this.valueAxes) + // category axes need to render in reverse order https://github.com/elastic/kibana/issues/13551 + .concat(this.categoryAxes.slice().reverse()); + + // memoize so that the same function is returned every time, + // allowing us to remove/re-add the same function + this.getProxyHandler = _.memoize(function(eventType) { + const self = this; + return function(eventPayload) { + switch (eventType) { + case 'brush': + const xRaw = _.get(eventPayload.data, 'series[0].values[0].xRaw'); + if (!xRaw) return; // not sure if this is possible? + return self.vis.emit(eventType, { + table: xRaw.table, + range: eventPayload.range, + column: xRaw.column, + }); + case 'click': + return self.vis.emit(eventType, eventPayload); + } + }; + }); + + /** + * Enables events, i.e. binds specific events to the chart + * object(s) `on` method. For example, `click` or `mousedown` events. + * + * @method enable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + this.enable = this.chartEventProxyToggle('on'); + + /** + * Disables events for all charts + * + * @method disable + * @param event {String} Event type + * @param chart {Object} Chart + * @returns {*} + */ + this.disable = this.chartEventProxyToggle('off'); + } + /** + * Validates whether data is actually present in the data object + * used to render the Vis. Throws a no results error if data is not + * present. + * + * @private + */ + _validateData() { + const dataType = this.data.type; + + if (!dataType) { + throw new NoResults(); + } + } + + /** + * Renders the constructors that create the visualization, + * including the chart constructor + * + * @method render + * @returns {HTMLElement} With the visualization child element + */ + render() { + if (this.visConfig.get('error', null)) return this.error(this.visConfig.get('error')); + + const self = this; + const { binder, charts = [] } = this; + const selection = d3.select(this.el); + + selection.selectAll('*').remove(); + + this._validateData(); + this.renderArray.forEach(function(property) { + if (typeof property.render === 'function') { + property.render(); + } + }); + + // render the chart(s) + let loadedCount = 0; + const chartSelection = selection.selectAll('.chart'); + chartSelection.each(function(chartData) { + const chart = new self.ChartClass(self, this, chartData, self.deps); + + self.vis.eventNames().forEach(function(event) { + self.enable(event, chart); + }); + + binder.on(chart.events, 'rendered', () => { + loadedCount++; + if (loadedCount === chartSelection.length) { + // events from all charts are propagated to vis, we only need to fire renderComplete once they all finish + self.vis.emit('renderComplete'); + } + }); + + charts.push(chart); + chart.render(); + }); + } + + chartEventProxyToggle(method) { + return function(event, chart) { + const proxyHandler = this.getProxyHandler(event); + + _.each(chart ? [chart] : this.charts, function(chart) { + chart.events[method](event, proxyHandler); + }); + }; + } + + /** + * Removes all DOM elements from the HTML element provided + * + * @method removeAll + * @param el {HTMLElement} Reference to the HTML Element that + * contains the chart + * @returns {D3.Selection|D3.Transition.Transition} With the chart + * child element removed + */ + removeAll(el) { + return d3 + .select(el) + .selectAll('*') + .remove(); + } + + /** + * Displays an error message in the DOM + * + * @method error + * @param message {String} Error message to display + * @returns {HTMLElement} Displays the input message + */ + error(message) { + this.removeAll(this.el); + + const div = d3 + .select(this.el) + .append('div') + // class name needs `chart` in it for the polling checkSize function + // to continuously call render on resize + .attr('class', 'visError chart error') + .attr('data-test-subj', 'visLibVisualizeError'); + + div.append('h4').text(markdownIt.renderInline(message)); + + dispatchRenderComplete(this.el); + return div; + } + + /** + * Destroys all the charts in the visualization + * + * @method destroy + */ + destroy() { + this.binder.destroy(); + + this.renderArray.forEach(function(renderable) { + if (_.isFunction(renderable.destroy)) { + renderable.destroy(); + } + }); + + this.charts.splice(0).forEach(function(chart) { + if (_.isFunction(chart.destroy)) { + chart.destroy(); + } + }); + } +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss b/src/plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss b/src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/_layout.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/index.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/index.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.test.js new file mode 100644 index 0000000000000..0bc11e5124a07 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout_types.test.js @@ -0,0 +1,38 @@ +/* + * 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 _ from 'lodash'; + +import { layoutTypes as layoutType } from './layout_types'; + +describe('Vislib Layout Types Test Suite', function() { + let layoutFunc; + + beforeEach(() => { + layoutFunc = layoutType.point_series; + }); + + it('should be an object', function() { + expect(_.isObject(layoutType)).toBe(true); + }); + + it('should return a function', function() { + expect(typeof layoutFunc).toBe('function'); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/chart_title_split.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/splits.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/splits.test.js new file mode 100644 index 0000000000000..117b346efda89 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/splits.test.js @@ -0,0 +1,278 @@ +/* + * 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 d3 from 'd3'; +import $ from 'jquery'; + +import { chartSplit } from './chart_split'; +import { chartTitleSplit } from './chart_title_split'; +import { xAxisSplit } from './x_axis_split'; +import { yAxisSplit } from './y_axis_split'; + +describe('Vislib Split Function Test Suite', function() { + describe('Column Chart', function() { + let el; + const data = { + rows: [ + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + ], + }; + + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization') + .datum(data); + }); + + afterEach(function() { + el.remove(); + }); + + describe('chart split function', function() { + let fixture; + + beforeEach(function() { + fixture = d3.select('.visualization').call(chartSplit); + }); + + afterEach(function() { + fixture.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.chart').length).toBe(2); + }); + + it('should add the correct class name', function() { + expect(!!$('.visWrapper__splitCharts--row').length).toBe(true); + }); + }); + + describe('chart title split function', function() { + let visEl; + let newEl; + let fixture; + + beforeEach(function() { + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + + newEl = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper') + .datum({ series: [] }); + + newEl.append('div').attr('class', 'visAxis__splitTitles--x'); + newEl.append('div').attr('class', 'visAxis__splitTitles--y'); + newEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + newEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + + fixture = newEl.selectAll(this.childNodes)[0].length; + }); + + afterEach(function() { + newEl.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.chart-title').length).toBe(2); + }); + + it('should remove the correct div', function() { + expect($('.visAxis__splitTitles--y').length).toBe(1); + expect($('.visAxis__splitTitles--x').length).toBe(0); + }); + + it('should remove all chart title divs when only one chart is rendered', function() { + expect(fixture).toBe(0); + }); + }); + + describe('x axis split function', function() { + let fixture; + let divs; + + beforeEach(function() { + fixture = d3 + .select('body') + .append('div') + .attr('class', 'columns') + .datum({ columns: [{}, {}] }); + d3.select('.columns').call(xAxisSplit); + divs = d3.selectAll('.x-axis-div')[0]; + }); + + afterEach(function() { + fixture.remove(); + $(divs).remove(); + }); + + it('should append the correct number of divs', function() { + expect(divs.length).toBe(2); + }); + }); + + describe('y axis split function', function() { + let fixture; + let divs; + + beforeEach(function() { + fixture = d3 + .select('body') + .append('div') + .attr('class', 'rows') + .datum({ rows: [{}, {}] }); + + d3.select('.rows').call(yAxisSplit); + + divs = d3.selectAll('.y-axis-div')[0]; + }); + + afterEach(function() { + fixture.remove(); + $(divs).remove(); + }); + + it('should append the correct number of divs', function() { + expect(divs.length).toBe(2); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/x_axis_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/column_chart/y_axis_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/chart_title_split.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/splits.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/splits.test.js new file mode 100644 index 0000000000000..05f6f72246d4a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/gauge_chart/splits.test.js @@ -0,0 +1,203 @@ +/* + * 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 d3 from 'd3'; +import $ from 'jquery'; + +import { chartSplit } from './chart_split'; +import { chartTitleSplit } from './chart_title_split'; + +describe('Vislib Gauge Split Function Test Suite', function() { + describe('Column Chart', function() { + let el; + const data = { + rows: [ + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + { + hits: 621, + label: '', + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }, + ], + }; + + beforeEach(function() { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization') + .datum(data); + }); + + afterEach(function() { + el.remove(); + }); + + describe('chart split function', function() { + let fixture; + + beforeEach(function() { + fixture = d3.select('.visualization').call(chartSplit); + }); + + afterEach(function() { + fixture.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.chart').length).toBe(2); + }); + + it('should add the correct class name', function() { + expect(!!$('.visWrapper__splitCharts--row').length).toBe(true); + }); + }); + + describe('chart title split function', function() { + let visEl; + + beforeEach(function() { + visEl = el.append('div').attr('class', 'visWrapper'); + visEl.append('div').attr('class', 'visAxis__splitTitles--x'); + visEl.append('div').attr('class', 'visAxis__splitTitles--y'); + visEl.select('.visAxis__splitTitles--x').call(chartTitleSplit); + visEl.select('.visAxis__splitTitles--y').call(chartTitleSplit); + }); + + afterEach(function() { + visEl.remove(); + }); + + it('should append the correct number of divs', function() { + expect($('.visAxis__splitTitles--x .chart-title').length).toBe(2); + expect($('.visAxis__splitTitles--y .chart-title').length).toBe(2); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/splits/pie_chart/chart_title_split.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.test.js new file mode 100644 index 0000000000000..a27ee57e64a5a --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/column_layout.test.js @@ -0,0 +1,109 @@ +/* + * 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 d3 from 'd3'; +import _ from 'lodash'; + +import { layoutTypes } from '../layout_types'; + +describe('Vislib Column Layout Test Suite', function() { + let columnLayout; + let el; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(function() { + el = d3 + .select('body') + .append('div') + .attr('class', 'visualization'); + columnLayout = layoutTypes.point_series(el, data); + }); + + afterEach(function() { + el.remove(); + }); + + it('should return an array of objects', function() { + expect(Array.isArray(columnLayout)).toBe(true); + expect(_.isObject(columnLayout[0])).toBe(true); + }); + + it('should throw an error when the wrong number or no arguments provided', function() { + expect(function() { + layoutTypes.point_series(el); + }).toThrow(); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/types/gauge_layout.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/types/pie_layout.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/gauge.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/gauge.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/gauge.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/gauge.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/index.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/index.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/index.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/index.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/pie.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/pie.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/pie.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/pie.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/types/point_series.js rename to src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js new file mode 100644 index 0000000000000..684d8f346744e --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/types/point_series.test.js @@ -0,0 +1,232 @@ +/* + * 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 stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series'; +import { vislibPointSeriesTypes } from './point_series'; +import percentileTestdata from './testdata_linechart_percentile.json'; +import percentileTestdataResult from './testdata_linechart_percentile_result.json'; + +const maxBucketData = { + get: prop => { + return maxBucketData[prop] || maxBucketData.data[prop] || null; + }, + getLabels: () => [], + data: { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { label: 's1', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's2', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's3', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's4', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's5', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's6', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's7', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's8', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's9', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's10', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's11', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's12', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's13', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's14', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's15', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's16', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's17', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's18', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's19', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's20', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's21', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's22', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's23', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's24', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's25', values: [{ x: 1408734060000, y: 8 }] }, + { label: 's26', values: [{ x: 1408734060000, y: 8 }] }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'series', + yAxisFormatter: () => 'test', + }, +}; + +describe('vislibPointSeriesTypes', () => { + const heatmapConfig = { + type: 'heatmap', + addLegend: true, + addTooltip: true, + colorsNumber: 4, + colorSchema: 'Greens', + setColorRange: false, + percentageMode: true, + invertColors: false, + colorsRange: [], + heatmapMaxBuckets: 20, + }; + + const stackedData = { + get: prop => { + return stackedSeries[prop] || null; + }, + getLabels: () => [], + data: stackedSeries, + }; + + describe('axis formatters', () => { + it('should create a value axis config with the default y axis formatter', () => { + const parsedConfig = vislibPointSeriesTypes.line({}, maxBucketData); + expect(parsedConfig.valueAxes.length).toEqual(1); + expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe( + maxBucketData.data.yAxisFormatter + ); + }); + + it('should use the formatter of the first series matching the axis if there is a descriptor', () => { + const axisFormatter1 = jest.fn(); + const axisFormatter2 = jest.fn(); + const axisFormatter3 = jest.fn(); + const parsedConfig = vislibPointSeriesTypes.line( + { + valueAxes: [ + { + id: 'ValueAxis-1', + labels: {}, + }, + { + id: 'ValueAxis-2', + labels: {}, + }, + ], + seriesParams: [ + { + valueAxis: 'ValueAxis-1', + data: { + id: '2', + }, + }, + { + valueAxis: 'ValueAxis-2', + data: { + id: '3', + }, + }, + { + valueAxis: 'ValueAxis-2', + data: { + id: '4', + }, + }, + ], + }, + { + ...maxBucketData, + data: { + ...maxBucketData.data, + series: [ + { id: '2.1', label: 's1', values: [], yAxisFormatter: axisFormatter1 }, + { id: '2.2', label: 's2', values: [], yAxisFormatter: axisFormatter1 }, + { id: '3.1', label: 's3', values: [], yAxisFormatter: axisFormatter2 }, + { id: '3.2', label: 's4', values: [], yAxisFormatter: axisFormatter2 }, + { id: '4.1', label: 's5', values: [], yAxisFormatter: axisFormatter3 }, + { id: '4.2', label: 's6', values: [], yAxisFormatter: axisFormatter3 }, + ], + }, + } + ); + expect(parsedConfig.valueAxes.length).toEqual(2); + expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBe(axisFormatter1); + expect(parsedConfig.valueAxes[1].labels.axisFormatter).toBe(axisFormatter2); + }); + }); + + describe('heatmap()', () => { + it('should return an error when more than 20 series are provided', () => { + const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, maxBucketData); + expect(parsedConfig.error).toMatchInlineSnapshot( + `"There are too many series defined (26). The configured maximum is 20."` + ); + }); + + it('should return valid config when less than 20 series are provided', () => { + const parsedConfig = vislibPointSeriesTypes.heatmap(heatmapConfig, stackedData); + expect(parsedConfig.error).toBeUndefined(); + expect(parsedConfig.valueAxes[0].show).toBeFalsy(); + expect(parsedConfig.categoryAxes.length).toBe(2); + expect(parsedConfig.error).toBeUndefined(); + }); + }); +}); + +describe('Point Series Config Type Class Test Suite', function() { + let parsedConfig; + const histogramConfig = { + type: 'histogram', + addLegend: true, + tooltip: { + show: true, + }, + categoryAxes: [ + { + id: 'CategoryAxis-1', + type: 'category', + title: {}, + }, + ], + valueAxes: [ + { + id: 'ValueAxis-1', + type: 'value', + labels: {}, + title: {}, + }, + ], + }; + + describe('histogram chart', function() { + beforeEach(function() { + parsedConfig = vislibPointSeriesTypes.column(histogramConfig, maxBucketData); + }); + it('should not throw an error when more than 25 series are provided', function() { + expect(parsedConfig.error).toBeUndefined(); + }); + + it('should set axis title and formatter from data', () => { + expect(parsedConfig.categoryAxes[0].title.text).toEqual(maxBucketData.data.xAxisLabel); + expect(parsedConfig.valueAxes[0].labels.axisFormatter).toBeDefined(); + }); + }); + + describe('line chart', function() { + beforeEach(function() { + const percentileDataObj = { + get: prop => { + return maxBucketData[prop] || maxBucketData.data[prop] || null; + }, + getLabels: () => [], + data: percentileTestdata.data, + }; + parsedConfig = vislibPointSeriesTypes.line(percentileTestdata.cfg, percentileDataObj); + }); + it('should render a percentile line chart', function() { + expect(JSON.stringify(parsedConfig)).toEqual(JSON.stringify(percentileTestdataResult)); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile.json rename to src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile_result.json b/src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/types/testdata_linechart_percentile_result.json rename to src/plugins/vis_type_vislib/public/vislib/lib/types/testdata_linechart_percentile_result.json diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/vis_config.js rename to src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.test.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.test.js new file mode 100644 index 0000000000000..1ba7d4aaa8a0c --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.test.js @@ -0,0 +1,140 @@ +/* + * 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 d3 from 'd3'; + +import { VisConfig } from './vis_config'; +import { getMockUiState } from '../../fixtures/mocks'; + +describe('Vislib VisConfig Class Test Suite', function() { + let el; + let visConfig; + const data = { + hits: 621, + ordered: { + date: true, + interval: 30000, + max: 1408734982458, + min: 1408734082458, + }, + series: [ + { + label: 'Count', + values: [ + { + x: 1408734060000, + y: 8, + }, + { + x: 1408734090000, + y: 23, + }, + { + x: 1408734120000, + y: 30, + }, + { + x: 1408734150000, + y: 28, + }, + { + x: 1408734180000, + y: 36, + }, + { + x: 1408734210000, + y: 30, + }, + { + x: 1408734240000, + y: 26, + }, + { + x: 1408734270000, + y: 22, + }, + { + x: 1408734300000, + y: 29, + }, + { + x: 1408734330000, + y: 24, + }, + ], + }, + ], + xAxisLabel: 'Date Histogram', + yAxisLabel: 'Count', + }; + + beforeEach(() => { + el = d3 + .select('body') + .append('div') + .attr('class', 'visWrapper') + .node(); + + visConfig = new VisConfig( + { + type: 'point_series', + }, + data, + getMockUiState(), + el, + () => undefined + ); + }); + + afterEach(() => { + el.remove(); + }); + + describe('get Method', function() { + it('should be a function', function() { + expect(typeof visConfig.get).toBe('function'); + }); + + it('should get the property', function() { + expect(visConfig.get('el')).toBe(el); + expect(visConfig.get('type')).toBe('point_series'); + }); + + it('should return defaults if property does not exist', function() { + expect(visConfig.get('this.does.not.exist', 'defaults')).toBe('defaults'); + }); + + it('should throw an error if property does not exist and defaults were not provided', function() { + expect(function() { + visConfig.get('this.does.not.exist'); + }).toThrow(); + }); + }); + + describe('set Method', function() { + it('should be a function', function() { + expect(typeof visConfig.set).toBe('function'); + }); + + it('should set a property', function() { + visConfig.set('this.does.not.exist', 'it.does.now'); + expect(visConfig.get('this.does.not.exist')).toBe('it.does.now'); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html b/src/plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html rename to src/plugins/vis_type_vislib/public/vislib/partials/touchdown.tmpl.html diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js b/src/plugins/vis_type_vislib/public/vislib/response_handler.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.js rename to src/plugins/vis_type_vislib/public/vislib/response_handler.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts b/src/plugins/vis_type_vislib/public/vislib/response_handler.test.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/response_handler.test.ts rename to src/plugins/vis_type_vislib/public/vislib/response_handler.test.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts b/src/plugins/vis_type_vislib/public/vislib/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/types.ts rename to src/plugins/vis_type_vislib/public/vislib/types.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/vis.js rename to src/plugins/vis_type_vislib/public/vislib/vis.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/_meter.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/gauge_types.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js similarity index 99% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js index 62de6d8413664..deafe010b6773 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauges/meter.js @@ -20,7 +20,7 @@ import d3 from 'd3'; import _ from 'lodash'; -import { getHeatmapColors } from '../../../../../../../plugins/charts/public'; +import { getHeatmapColors } from '../../../../../charts/public'; const arcAngles = { angleFactor: 0.75, diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_index.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_labels.scss diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/_point_series.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.js diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js new file mode 100644 index 0000000000000..6f497ae057d72 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.js @@ -0,0 +1,326 @@ +/* + * 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 _ from 'lodash'; +import moment from 'moment'; + +import { isColorDark } from '@elastic/eui'; + +import { PointSeries } from './_point_series'; +import { getHeatmapColors } from '../../../../../../plugins/charts/public'; + +const defaults = { + color: undefined, // todo + fillColor: undefined, // todo +}; +/** + * Line Chart Visualization + * + * @class HeatmapChart + * @constructor + * @extends Chart + * @param handler {Object} Reference to the Handler Class Constructor + * @param el {HTMLElement} HTML element to which the chart will be appended + * @param chartData {Object} Elasticsearch query results for this specific chart + */ +export class HeatmapChart extends PointSeries { + constructor(handler, chartEl, chartData, seriesConfigArgs, deps) { + super(handler, chartEl, chartData, seriesConfigArgs, deps); + + this.seriesConfig = _.defaults(seriesConfigArgs || {}, defaults); + + this.handler.visConfig.set('legend', { + labels: this.getHeatmapLabels(this.handler.visConfig), + colors: this.getHeatmapColors(this.handler.visConfig), + }); + + const colors = this.handler.visConfig.get('legend.colors', null); + if (colors) { + this.handler.vis.uiState.setSilent('vis.defaultColors', null); + this.handler.vis.uiState.setSilent('vis.defaultColors', colors); + } + } + + getHeatmapLabels(cfg) { + const percentageMode = cfg.get('percentageMode'); + const colorsNumber = cfg.get('colorsNumber'); + const colorsRange = cfg.get('colorsRange'); + const zAxisConfig = this.getValueAxis().axisConfig; + const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); + const zScale = this.getValueAxis().getScale(); + const [min, max] = zScale.domain(); + const labels = []; + const maxColorCnt = 10; + if (cfg.get('setColorRange')) { + colorsRange.forEach(range => { + const from = isFinite(range.from) ? zAxisFormatter(range.from) : range.from; + const to = isFinite(range.to) ? zAxisFormatter(range.to) : range.to; + labels.push(`${from} - ${to}`); + }); + } else { + if (max === min) { + return [min.toString()]; + } + for (let i = 0; i < colorsNumber; i++) { + let label; + let val = i / colorsNumber; + let nextVal = (i + 1) / colorsNumber; + if (percentageMode) { + val = Math.ceil(val * 100); + nextVal = Math.ceil(nextVal * 100); + label = `${val}% - ${nextVal}%`; + } else { + val = val * (max - min) + min; + nextVal = nextVal * (max - min) + min; + if (max - min > maxColorCnt) { + const valInt = Math.ceil(val); + if (i === 0) { + val = valInt === val ? val : valInt - 1; + } else { + val = valInt; + } + nextVal = Math.ceil(nextVal); + } + if (isFinite(val)) val = zAxisFormatter(val); + if (isFinite(nextVal)) nextVal = zAxisFormatter(nextVal); + label = `${val} - ${nextVal}`; + } + + labels.push(label); + } + } + + return labels; + } + + getHeatmapColors(cfg) { + const invertColors = cfg.get('invertColors'); + const colorSchema = cfg.get('colorSchema'); + const labels = this.getHeatmapLabels(cfg); + const colors = {}; + for (const i in labels) { + if (labels[i]) { + const val = invertColors ? 1 - i / labels.length : i / labels.length; + colors[labels[i]] = getHeatmapColors(val, colorSchema); + } + } + return colors; + } + + addSquares(svg, data) { + const xScale = this.getCategoryAxis().getScale(); + const yScale = this.handler.categoryAxes[1].getScale(); + const zScale = this.getValueAxis().getScale(); + const tooltip = this.baseChart.tooltip; + const isTooltip = this.handler.visConfig.get('tooltip.show'); + const isHorizontal = this.getCategoryAxis().axisConfig.isHorizontal(); + const colorsNumber = this.handler.visConfig.get('colorsNumber'); + const setColorRange = this.handler.visConfig.get('setColorRange'); + const colorsRange = this.handler.visConfig.get('colorsRange'); + const color = this.handler.data.getColorFunc(); + const labels = this.handler.visConfig.get('legend.labels'); + const zAxisConfig = this.getValueAxis().axisConfig; + const zAxisFormatter = zAxisConfig.get('labels.axisFormatter'); + const showLabels = zAxisConfig.get('labels.show'); + const overwriteLabelColor = zAxisConfig.get('labels.overwriteColor', false); + + const layer = svg.append('g').attr('class', 'series'); + + const squares = layer.selectAll('g.square').data(data.values); + + squares.exit().remove(); + + let barWidth; + if (this.getCategoryAxis().axisConfig.isTimeDomain()) { + const { min, interval } = this.handler.data.get('ordered'); + const start = min; + const end = moment(min) + .add(interval) + .valueOf(); + + barWidth = xScale(end) - xScale(start); + if (!isHorizontal) barWidth *= -1; + } + + function x(d) { + return xScale(d.x); + } + + function y(d) { + return yScale(d.series); + } + + const [min, max] = zScale.domain(); + function getColorBucket(d) { + let val = 0; + if (setColorRange && colorsRange.length) { + const bucket = _.find(colorsRange, range => { + return range.from <= d.y && range.to > d.y; + }); + return bucket ? colorsRange.indexOf(bucket) : -1; + } else { + if (isNaN(min) || isNaN(max)) { + val = colorsNumber - 1; + } else if (min === max) { + val = 0; + } else { + val = (d.y - min) / (max - min); /* get val from 0 - 1 */ + val = Math.min(colorsNumber - 1, Math.floor(val * colorsNumber)); + } + } + if (d.y == null) { + return -1; + } + return !isNaN(val) ? val : -1; + } + + function label(d) { + const colorBucket = getColorBucket(d); + // colorBucket id should always GTE 0 + if (colorBucket < 0) d.hide = true; + return labels[colorBucket]; + } + + function z(d) { + if (label(d) === '') return 'transparent'; + return color(label(d)); + } + + const squareWidth = barWidth || xScale.rangeBand(); + const squareHeight = yScale.rangeBand(); + + squares + .enter() + .append('g') + .attr('class', 'square'); + + squares + .append('rect') + .attr('x', x) + .attr('width', squareWidth) + .attr('y', y) + .attr('height', squareHeight) + .attr('data-label', label) + .attr('fill', z) + .attr('style', 'cursor: pointer; stroke: black; stroke-width: 0.1px') + .style('display', d => { + return d.hide ? 'none' : 'initial'; + }); + + // todo: verify that longest label is not longer than the barwidth + // or barwidth is not smaller than textheight (and vice versa) + // + if (showLabels) { + const rotate = zAxisConfig.get('labels.rotate'); + const rotateRad = (rotate * Math.PI) / 180; + const cellPadding = 5; + const maxLength = + Math.min( + Math.abs(squareWidth / Math.cos(rotateRad)), + Math.abs(squareHeight / Math.sin(rotateRad)) + ) - cellPadding; + const maxHeight = + Math.min( + Math.abs(squareWidth / Math.sin(rotateRad)), + Math.abs(squareHeight / Math.cos(rotateRad)) + ) - cellPadding; + + let labelColor; + if (overwriteLabelColor) { + // If overwriteLabelColor is true, use the manual specified color + labelColor = zAxisConfig.get('labels.color'); + } else { + // Otherwise provide a function that will calculate a light or dark color + labelColor = d => { + const bgColor = z(d); + const color = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/.exec(bgColor); + return color && isColorDark(parseInt(color[1]), parseInt(color[2]), parseInt(color[3])) + ? '#FFF' + : '#222'; + }; + } + + let hiddenLabels = false; + squares + .append('text') + .text(d => zAxisFormatter(d.y)) + .style('display', function(d) { + const textLength = this.getBBox().width; + const textHeight = this.getBBox().height; + const textTooLong = textLength > maxLength; + const textTooWide = textHeight > maxHeight; + if (!d.hide && (textTooLong || textTooWide)) { + hiddenLabels = true; + } + return d.hide || textTooLong || textTooWide ? 'none' : 'initial'; + }) + .style('dominant-baseline', 'central') + .style('text-anchor', 'middle') + .style('fill', labelColor) + .attr('x', function(d) { + const center = x(d) + squareWidth / 2; + return center; + }) + .attr('y', function(d) { + const center = y(d) + squareHeight / 2; + return center; + }) + .attr('transform', function(d) { + const horizontalCenter = x(d) + squareWidth / 2; + const verticalCenter = y(d) + squareHeight / 2; + return `rotate(${rotate},${horizontalCenter},${verticalCenter})`; + }); + if (hiddenLabels) { + this.baseChart.handler.alerts.show('Some labels were hidden due to size constraints'); + } + } + + if (isTooltip) { + squares.call(tooltip.render()); + } + + return squares.selectAll('rect'); + } + + /** + * Renders d3 visualization + * + * @method draw + * @returns {Function} Creates the line chart + */ + draw() { + const self = this; + + return function(selection) { + selection.each(function() { + const svg = self.chartEl.append('g'); + svg.data([self.chartData]); + + const squares = self.addSquares(svg, self.chartData); + self.addCircleEvents(squares); + + self.events.emit('rendered', { + chart: self.chartData, + }); + + return svg; + }); + }; + } +} diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/series_types.js diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts rename to src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.d.ts diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.js diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.test.js new file mode 100644 index 0000000000000..058bdb5de8785 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/time_marker.test.js @@ -0,0 +1,140 @@ +/* + * 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 d3 from 'd3'; +import $ from 'jquery'; + +import series from '../../fixtures/mock_data/date_histogram/_series'; +import terms from '../../fixtures/mock_data/terms/_columns'; +import { TimeMarker } from './time_marker'; + +describe('Vislib Time Marker Test Suite', function() { + const height = 50; + const color = '#ff0000'; + const opacity = 0.5; + const width = 3; + const customClass = 'custom-time-marker'; + const dateMathTimes = ['now-1m', 'now-5m', 'now-15m']; + const myTimes = dateMathTimes.map(function(dateMathString) { + return { + time: dateMathString, + class: customClass, + color: color, + opacity: opacity, + width: width, + }; + }); + const getExtent = function(dataArray, func) { + return func(dataArray, function(obj) { + return func(obj.values, function(d) { + return d.x; + }); + }); + }; + const times = []; + let defaultMarker; + let customMarker; + let selection; + let xScale; + let minDomain; + let maxDomain; + let domain; + + beforeEach(function() { + minDomain = getExtent(series.series, d3.min); + maxDomain = getExtent(series.series, d3.max); + domain = [minDomain, maxDomain]; + xScale = d3.time + .scale() + .domain(domain) + .range([0, 500]); + defaultMarker = new TimeMarker(times, xScale, height); + customMarker = new TimeMarker(myTimes, xScale, height); + + selection = d3 + .select('body') + .append('div') + .attr('class', 'marker'); + selection.datum(series); + }); + + afterEach(function() { + selection.remove('*'); + selection = null; + defaultMarker = null; + }); + + describe('_isTimeBaseChart method', function() { + let boolean; + let newSelection; + + it('should return true when data is time based', function() { + boolean = defaultMarker._isTimeBasedChart(selection); + expect(boolean).toBe(true); + }); + + it('should return false when data is not time based', function() { + newSelection = selection.datum(terms); + boolean = defaultMarker._isTimeBasedChart(newSelection); + expect(boolean).toBe(false); + }); + }); + + describe('render method', function() { + let lineArray; + + beforeEach(function() { + defaultMarker.render(selection); + customMarker.render(selection); + lineArray = document.getElementsByClassName('custom-time-marker'); + }); + + it('should render the default line', function() { + expect(!!$('line.time-marker').length).toBe(true); + }); + + it('should render the custom (user defined) lines', function() { + expect($('line.custom-time-marker').length).toBe(myTimes.length); + }); + + it('should set the class', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(line.getAttribute('class')).toBe(customClass); + }); + }); + + it('should set the stroke', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(line.getAttribute('stroke')).toBe(color); + }); + }); + + it('should set the stroke-opacity', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(+line.getAttribute('stroke-opacity')).toBe(opacity); + }); + }); + + it('should set the stroke-width', function() { + Array.prototype.forEach.call(lineArray, function(line) { + expect(+line.getAttribute('stroke-width')).toBe(width); + }); + }); + }); +}); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js rename to src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.js diff --git a/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.test.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.test.js new file mode 100644 index 0000000000000..df044f46460c8 --- /dev/null +++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/vis_types.test.js @@ -0,0 +1,38 @@ +/* + * 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 _ from 'lodash'; + +import { visTypes } from './vis_types'; + +describe('Vislib Vis Types Test Suite', function() { + let visFunc; + + beforeEach(function() { + visFunc = visTypes.point_series; + }); + + it('should be an object', function() { + expect(_.isObject(visTypes)).toBe(true); + }); + + it('should return a function', function() { + expect(typeof visFunc).toBe('function'); + }); +}); diff --git a/src/plugins/vis_type_vislib/server/index.ts b/src/plugins/vis_type_vislib/server/index.ts new file mode 100644 index 0000000000000..355c01d255ce7 --- /dev/null +++ b/src/plugins/vis_type_vislib/server/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index cd22b1375ae1b..f3f9cbd8341ec 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection"] + "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"] } diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index bf2d174f594b2..8e51bd4ac5d4f 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -28,8 +28,9 @@ import { getTimeFilter, getCapabilities, } from '../services'; +import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; -export const createVisEmbeddableFromObject = async ( +export const createVisEmbeddableFromObject = (deps: VisualizeEmbeddableFactoryDeps) => async ( vis: Vis, input: Partial & { id: string }, parent?: IContainer @@ -58,6 +59,7 @@ export const createVisEmbeddableFromObject = async ( indexPatterns, editUrl, editable, + deps, }, input, parent diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index e64d200251797..1c545bb36cff0 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -33,7 +33,6 @@ import { EmbeddableInput, EmbeddableOutput, Embeddable, - EmbeddableVisTriggerContext, IContainer, } from '../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public'; @@ -42,6 +41,7 @@ import { buildPipeline } from '../legacy/build_pipeline'; import { Vis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; +import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -50,6 +50,7 @@ export interface VisualizeEmbeddableConfiguration { indexPatterns?: IIndexPattern[]; editUrl: string; editable: boolean; + deps: VisualizeEmbeddableFactoryDeps; } export interface VisualizeInput extends EmbeddableInput { @@ -84,10 +85,11 @@ export class VisualizeEmbeddable extends Embeddable { - if (this.handler) { - return this.handler.openInspector(this.getTitle() || ''); - } + if (!this.handler) return; + + const adapters = this.handler.inspect(); + if (!adapters) return; + + this.deps.start().plugins.inspector.open(adapters, { + title: this.getTitle() || '', + }); }; /** @@ -252,7 +260,7 @@ export class VisualizeEmbeddable extends Embeddable>; +} + export class VisualizeEmbeddableFactory implements EmbeddableFactoryDefinition< @@ -79,7 +85,8 @@ export class VisualizeEmbeddableFactory return visType.stage !== 'experimental'; }, }; - constructor() {} + + constructor(private readonly deps: VisualizeEmbeddableFactoryDeps) {} public async isEditable() { return getCapabilities().visualize.save as boolean; @@ -101,7 +108,7 @@ export class VisualizeEmbeddableFactory try { const savedObject = await savedVisualizations.get(savedObjectId); const vis = new Vis(savedObject.visState.type, await convertToSerializedVis(savedObject)); - return createVisEmbeddableFromObject(vis, input, parent); + return createVisEmbeddableFromObject(this.deps)(vis, input, parent); } catch (e) { console.error(e); // eslint-disable-line no-console return new ErrorEmbeddable(e, input, parent); diff --git a/src/plugins/visualizations/public/expressions/vis.ts b/src/plugins/visualizations/public/expressions/vis.ts index a7d4a28070620..eafa112156716 100644 --- a/src/plugins/visualizations/public/expressions/vis.ts +++ b/src/plugins/visualizations/public/expressions/vis.ts @@ -69,7 +69,15 @@ export class ExprVis extends EventEmitter { events: { filter: (data: any) => { if (!this.eventsSubject) return; - this.eventsSubject.next({ name: 'filterBucket', data }); + this.eventsSubject.next({ + name: 'filterBucket', + data: data.data + ? { + data: data.data, + negate: data.negate, + } + : { data: [data] }, + }); }, brush: (data: any) => { if (!this.eventsSubject) return; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 7df420e7ba585..e475684ed5934 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -43,6 +43,8 @@ export type VisualizeEmbeddableContract = PublicContract; export { VisualizeInput } from './embeddable'; export type ExprVis = ExprVisClass; export { SchemaConfig } from './legacy/build_pipeline'; +// @ts-ignore +export { updateOldState } from './legacy/vis_update_state'; export { PersistedState } from './persisted_state'; export { VisualizationController, diff --git a/src/plugins/visualizations/public/legacy/vis_update.js b/src/plugins/visualizations/public/legacy/vis_update.js deleted file mode 100644 index 338a322e6aa57..0000000000000 --- a/src/plugins/visualizations/public/legacy/vis_update.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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. - */ - -// TODO: this should be moved to vis_update_state -// Currently the migration takes place in Vis when calling setCurrentState. -// It should rather convert the raw saved object before starting to instantiate -// any JavaScript classes from it. -const updateVisualizationConfig = (stateConfig, config) => { - if (!stateConfig || stateConfig.seriesParams) return; - if (!['line', 'area', 'histogram'].includes(config.type)) return; - - // update value axis options - const isUserDefinedYAxis = config.setYExtents; - const defaultYExtents = config.defaultYExtents; - const mode = ['stacked', 'overlap'].includes(config.mode) ? 'normal' : config.mode || 'normal'; - config.valueAxes[0].scale = { - ...config.valueAxes[0].scale, - type: config.scale || 'linear', - setYExtents: config.setYExtents || false, - defaultYExtents: config.defaultYExtents || false, - boundsMargin: defaultYExtents ? config.boundsMargin : 0, - min: isUserDefinedYAxis ? config.yAxis.min : undefined, - max: isUserDefinedYAxis ? config.yAxis.max : undefined, - mode: mode, - }; - - // update series options - const interpolate = config.smoothLines ? 'cardinal' : config.interpolate; - const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(config.mode); - config.seriesParams[0] = { - ...config.seriesParams[0], - type: config.type || 'line', - mode: stacked ? 'stacked' : 'normal', - interpolate: interpolate, - drawLinesBetweenPoints: config.drawLinesBetweenPoints, - showCircles: config.showCircles, - radiusRatio: config.radiusRatio, - }; -}; - -export { updateVisualizationConfig }; diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js index 45610701e08c8..e345b9e5b8c9a 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.js +++ b/src/plugins/visualizations/public/legacy/vis_update_state.js @@ -75,6 +75,77 @@ function convertDateHistogramScaleMetrics(visState) { } } +function convertSeriesParams(visState) { + if (visState.params.seriesParams) { + return; + } + + // update value axis options + const isUserDefinedYAxis = visState.params.setYExtents; + const defaultYExtents = visState.params.defaultYExtents; + const mode = ['stacked', 'overlap'].includes(visState.params.mode) + ? 'normal' + : visState.params.mode || 'normal'; + + if (!visState.params.valueAxes || !visState.params.valueAxes.length) { + visState.params.valueAxes = [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ]; + } + + visState.params.valueAxes[0].scale = { + ...visState.params.valueAxes[0].scale, + type: visState.params.scale || 'linear', + setYExtents: visState.params.setYExtents || false, + defaultYExtents: visState.params.defaultYExtents || false, + boundsMargin: defaultYExtents ? visState.params.boundsMargin : 0, + min: isUserDefinedYAxis ? visState.params.yAxis.min : undefined, + max: isUserDefinedYAxis ? visState.params.yAxis.max : undefined, + mode: mode, + }; + + // update series options + const interpolate = visState.params.smoothLines ? 'cardinal' : visState.params.interpolate; + const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(visState.params.mode); + visState.params.seriesParams = [ + { + show: true, + type: visState.params.type || 'line', + mode: stacked ? 'stacked' : 'normal', + interpolate: interpolate, + drawLinesBetweenPoints: visState.params.drawLinesBetweenPoints, + showCircles: visState.params.showCircles, + radiusRatio: visState.params.radiusRatio, + data: { + label: 'Count', + id: '1', + }, + lineWidth: 2, + valueAxis: 'ValueAxis-1', + }, + ]; +} + /** * This function is responsible for updating old visStates - the actual saved object * object - into the format, that will be required by the current Kibana version. @@ -90,6 +161,10 @@ export const updateOldState = visState => { convertPropertyNames(newState); convertDateHistogramScaleMetrics(newState); + if (visState.params && ['line', 'area', 'histogram'].includes(visState.params.type)) { + convertSeriesParams(newState); + } + if (visState.type === 'gauge' && visState.fontSize) { delete newState.fontSize; _.set(newState, 'gauge.style.fontSize', visState.fontSize); diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 2aa346423297a..d6eeffdb01459 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -26,6 +26,7 @@ import { expressionsPluginMock } from '../../../plugins/expressions/public/mocks import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { usageCollectionPluginMock } from '../../../plugins/usage_collection/public/mocks'; import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; +import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -53,14 +54,16 @@ const createInstance = async () => { const setup = plugin.setup(coreMock.createSetup(), { data: dataPluginMock.createSetupContract(), - expressions: expressionsPluginMock.createSetupContract(), embeddable: embeddablePluginMock.createSetupContract(), + expressions: expressionsPluginMock.createSetupContract(), + inspector: inspectorPluginMock.createSetupContract(), usageCollection: usageCollectionPluginMock.createSetupContract(), }); const doStart = () => plugin.start(coreMock.createStart(), { data: dataPluginMock.createStartContract(), expressions: expressionsPluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), uiActions: uiActionsPluginMock.createStartContract(), }); diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 8fcb84b19a9be..b3e8c9b5b61b3 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -43,18 +43,23 @@ import { VisualizeEmbeddableFactory, createVisEmbeddableFromObject, } from './embeddable'; -import { ExpressionsSetup, ExpressionsStart } from '../../../plugins/expressions/public'; -import { EmbeddableSetup } from '../../../plugins/embeddable/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../expressions/public'; +import { EmbeddableSetup } from '../../embeddable/public'; import { visualization as visualizationFunction } from './expressions/visualization_function'; import { visualization as visualizationRenderer } from './expressions/visualization_renderer'; import { range as rangeExpressionFunction } from './expression_functions/range'; import { visDimension as visDimensionExpressionFunction } from './expression_functions/vis_dimension'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../plugins/data/public'; -import { UsageCollectionSetup } from '../../../plugins/usage_collection/public'; +import { + Setup as InspectorSetup, + Start as InspectorStart, +} from '../../../plugins/inspector/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; +import { createStartServicesGetter, StartServicesGetter } from '../../kibana_utils/public'; import { createSavedVisLoader, SavedVisualizationsLoader } from './saved_visualizations'; import { SerializedVis, Vis } from './vis'; import { showNewVisModal } from './wizard'; -import { UiActionsStart } from '../../../plugins/ui_actions/public'; +import { UiActionsStart } from '../../ui_actions/public'; import { convertFromSerializedVis, convertToSerializedVis, @@ -74,19 +79,21 @@ export interface VisualizationsStart extends TypesStart { convertToSerializedVis: typeof convertToSerializedVis; convertFromSerializedVis: typeof convertFromSerializedVis; showNewVisModal: typeof showNewVisModal; - __LEGACY: { createVisEmbeddableFromObject: typeof createVisEmbeddableFromObject }; + __LEGACY: { createVisEmbeddableFromObject: ReturnType }; } export interface VisualizationsSetupDeps { - expressions: ExpressionsSetup; + data: DataPublicPluginSetup; embeddable: EmbeddableSetup; + expressions: ExpressionsSetup; + inspector: InspectorSetup; usageCollection: UsageCollectionSetup; - data: DataPublicPluginSetup; } export interface VisualizationsStartDeps { data: DataPublicPluginStart; expressions: ExpressionsStart; + inspector: InspectorStart; uiActions: UiActionsStart; } @@ -107,13 +114,16 @@ export class VisualizationsPlugin VisualizationsStartDeps > { private readonly types: TypesService = new TypesService(); + private getStartServicesOrDie?: StartServicesGetter; constructor(initializerContext: PluginInitializerContext) {} public setup( - core: CoreSetup, + core: CoreSetup, { expressions, embeddable, usageCollection, data }: VisualizationsSetupDeps ): VisualizationsSetup { + const start = (this.getStartServicesOrDie = createStartServicesGetter(core.getStartServices)); + setUISettings(core.uiSettings); setUsageCollector(usageCollection); @@ -122,7 +132,7 @@ export class VisualizationsPlugin expressions.registerFunction(rangeExpressionFunction); expressions.registerFunction(visDimensionExpressionFunction); - const embeddableFactory = new VisualizeEmbeddableFactory(); + const embeddableFactory = new VisualizeEmbeddableFactory({ start }); embeddable.registerEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE, embeddableFactory); return { @@ -171,7 +181,11 @@ export class VisualizationsPlugin convertToSerializedVis, convertFromSerializedVis, savedVisualizationsLoader, - __LEGACY: { createVisEmbeddableFromObject }, + __LEGACY: { + createVisEmbeddableFromObject: createVisEmbeddableFromObject({ + start: this.getStartServicesOrDie!, + }), + }, }; } diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index c99c7a4c2caa1..8bc98ca4b4784 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -32,7 +32,7 @@ import { // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; import { extractReferences, injectReferences } from './saved_visualization_references'; -import { IIndexPattern, ISearchSource, SearchSource } from '../../../../plugins/data/public'; +import { IIndexPattern, ISearchSource } from '../../../../plugins/data/public'; import { ISavedVis, SerializedVis } from '../types'; import { createSavedSearchesLoader } from '../../../../plugins/discover/public'; import { getChrome, getOverlays, getIndexPatterns, getSavedObjects, getSearch } from '../services'; @@ -80,20 +80,24 @@ export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { }; const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => { + const search = getSearch(); + const searchSource = inputSearchSource.createCopy ? inputSearchSource.createCopy() - : new SearchSource({ ...(inputSearchSource as any).fields }); + : search.searchSource.create({ ...(inputSearchSource as any).fields }); + if (savedSearchId) { const savedSearch = await createSavedSearchesLoader({ + search, savedObjectsClient: getSavedObjects().client, indexPatterns: getIndexPatterns(), - search: getSearch(), chrome: getChrome(), overlays: getOverlays(), }).get(savedSearchId); searchSource.setParent(savedSearch.searchSource); } + searchSource!.setField('size', 0); return searchSource; }; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 3cab4faf2a27f..009dd71b9a912 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -29,8 +29,6 @@ import { isFunction, defaults, cloneDeep } from 'lodash'; import { PersistedState } from './persisted_state'; -// @ts-ignore -import { updateVisualizationConfig } from './legacy/vis_update'; import { getTypes, getAggs } from './services'; import { VisType } from './vis_types'; import { @@ -121,9 +119,6 @@ export class Vis { this.params = this.getParams(state.params); } - // move to migration script - updateVisualizationConfig(state.params, this.params); - if (state.data && state.data.searchSource) { this.data.searchSource = state.data.searchSource!; this.data.indexPattern = this.data.searchSource.getField('index'); diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 860150d688711..6aef2c2b2ceb7 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -1214,7 +1214,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1` data-test-subj="visNewDialogTypes" role="menu" > -
- - +
- - +
- + @@ -2669,7 +2669,7 @@ exports[`NewVisModal should render as expected 1`] = ` data-test-subj="visNewDialogTypes" role="menu" > - - - + - - + - + diff --git a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx index bb5037545cc82..acf562d5bf363 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx @@ -27,7 +27,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiKeyPadMenu, - EuiKeyPadMenuItemButton, + EuiKeyPadMenuItem, EuiModalHeader, EuiModalHeaderTitle, EuiScreenReaderOnly, @@ -262,7 +262,7 @@ class TypeSelection extends React.Component{visType.title}
} onClick={onClick} @@ -282,7 +282,7 @@ class TypeSelection extends React.Component - + ); }; diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index 26f8278cd3d43..83d53d27e41fd 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1460,4 +1460,62 @@ describe('migration visualization', () => { expect(migratedParams.gauge_color_rules[1]).toEqual(params.gauge_color_rules[1]); }); }); + + describe('7.8.0 tsvb split_color_mode', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.8.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const generateDoc = (params: any) => ({ + attributes: { + title: 'My Vis', + type: 'visualization', + description: 'This is my super cool vis.', + visState: JSON.stringify(params), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + + it('should change a missing split_color_mode to gradient', () => { + const params = { type: 'metrics', params: { series: [{}] } }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].split_color_mode).toEqual('gradient'); + }); + + it('should not change the color mode if it is set', () => { + const params = { type: 'metrics', params: { series: [{ split_color_mode: 'gradient' }] } }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].split_color_mode).toEqual('gradient'); + }); + + it('should not change the color mode if it is non-default', () => { + const params = { type: 'metrics', params: { series: [{ split_color_mode: 'rainbow' }] } }; + const testDoc1 = generateDoc(params); + const migratedTestDoc1 = migrate(testDoc1); + const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series; + + expect(series[0].split_color_mode).toEqual('rainbow'); + }); + + it('should not migrate a visualization of unknown type', () => { + const params = { type: 'unknown', params: { series: [{}] } }; + const doc = generateDoc(params); + const migratedDoc = migrate(doc); + const series = JSON.parse(migratedDoc.attributes.visState).params.series; + + expect(series[0].split_color_mode).toBeUndefined(); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index 80783e41863ea..f6455d0c1e43f 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) { @@ -602,7 +602,39 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { }; } } + return doc; +}; + +// [TSVB] Default color palette is changing, keep the default for older viz +const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.type === 'metrics') { + const series: any[] = get(visState, 'params.series') || []; + series.forEach(part => { + // The default value was not saved before + if (!part.split_color_mode) { + part.split_color_mode = 'gradient'; + } + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } return doc; }; @@ -617,26 +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.3.1': flow>(migrateFiltersAggQueryStringQueries), + '7.4.2': flow>(transformSplitFiltersStringToQueryObject), + '7.7.0': flow>(migrateOperatorKeyTypo), + '7.8.0': flow>(migrateTsvbDefaultColorPalettes), }; diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index a7afa0697a5eb..d536d2f246a6b 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -1,7 +1,7 @@ { "id": "visualize", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": [ "data", diff --git a/src/plugins/visualize/public/application/application.ts b/src/plugins/visualize/public/application/application.ts index 9d8a1b98ef023..19551bba9a43e 100644 --- a/src/plugins/visualize/public/application/application.ts +++ b/src/plugins/visualize/public/application/application.ts @@ -20,6 +20,8 @@ import './index.scss'; import angular, { IModule } from 'angular'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { AppMountContext } from 'kibana/public'; diff --git a/src/plugins/visualize/public/application/legacy_app.js b/src/plugins/visualize/public/application/legacy_app.js index 7c5e3ce9408f0..c7cc11c1f3ff5 100644 --- a/src/plugins/visualize/public/application/legacy_app.js +++ b/src/plugins/visualize/public/application/legacy_app.js @@ -21,11 +21,7 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { - createKbnUrlStateStorage, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../kibana_utils/public'; +import { createKbnUrlStateStorage, redirectWhenMissing } from '../../../kibana_utils/public'; import { createSavedSearchesLoader } from '../../../discover/public'; import editorTemplate from './editor/editor.html'; @@ -127,7 +123,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => false, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { @@ -138,7 +134,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => true, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.CREATE_PATH, { @@ -147,7 +143,7 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getCreateBreadcrumbs, resolve: { resolved: function($route, history) { - const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; + const { data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; @@ -164,7 +160,8 @@ export function initVisualizeApp(app, deps) { ); } - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params)) .then(getResolvedResults(deps)) .catch( @@ -183,9 +180,10 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getEditBreadcrumbs, resolve: { resolved: function($route, history) { - const { chrome, core, data, savedVisualizations, toastNotifications } = deps; + const { chrome, data, savedVisualizations, toastNotifications } = deps; - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { chrome.recentlyAccessed.add(savedVis.getFullPath(), savedVis.title, savedVis.id); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index ab64e083a553d..df8479bc891b8 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -123,6 +123,8 @@ export class VisualizePlugin }; setServices(deps); + // make sure the index pattern list is up to date + await pluginsStart.data.indexPatterns.clearCache(); const { renderApp } = await import('./application/application'); const unmount = renderApp(params.element, params.appBasePath, deps); return () => { diff --git a/src/plugins/visualize/server/capabilities_provider.ts b/src/plugins/visualize/server/capabilities_provider.ts new file mode 100644 index 0000000000000..3b09b28f53c77 --- /dev/null +++ b/src/plugins/visualize/server/capabilities_provider.ts @@ -0,0 +1,28 @@ +/* + * 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 const capabilitiesProvider = () => ({ + visualize: { + show: true, + createShortUrl: true, + delete: true, + save: true, + saveQuery: true, + }, +}); diff --git a/src/plugins/visualize/server/index.ts b/src/plugins/visualize/server/index.ts new file mode 100644 index 0000000000000..5cebef71d8d22 --- /dev/null +++ b/src/plugins/visualize/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { VisualizeServerPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new VisualizeServerPlugin(initContext); diff --git a/src/plugins/visualize/server/plugin.ts b/src/plugins/visualize/server/plugin.ts new file mode 100644 index 0000000000000..7cc57c25f3229 --- /dev/null +++ b/src/plugins/visualize/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; + +export class VisualizeServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('visualize: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('visualize: Started'); + return {}; + } + + public stop() {} +} diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 4e106ef3e039a..f87edbf04f220 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -25,6 +25,7 @@ import { DllCompiler } from '../../src/optimize/dynamic_dll_plugin'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); +const buildHash = String(Number.MAX_SAFE_INTEGER); module.exports = function(grunt) { function pickBrowser() { @@ -53,29 +54,34 @@ module.exports = function(grunt) { function getKarmaFiles(shardNum) { return [ 'http://localhost:5610/test_bundle/built_css.css', + // Sets global variables normally set by the bootstrap.js script + 'http://localhost:5610/test_bundle/karma/globals.js', ...UiSharedDeps.jsDepFilenames.map( - chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` + chunkFilename => + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${chunkFilename}` ), - `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - 'http://localhost:5610/built_assets/dlls/vendors_runtime.bundle.dll.js', + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors_runtime.bundle.dll.js`, ...DllCompiler.getRawDllConfig().chunks.map( - chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.bundle.dll.js` + chunk => + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.bundle.dll.js` ), shardNum === undefined - ? `http://localhost:5610/bundles/tests.bundle.js` - : `http://localhost:5610/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, + ? `http://localhost:5610/${buildHash}/bundles/tests.bundle.js` + : `http://localhost:5610/${buildHash}/bundles/tests.bundle.js?shards=${TOTAL_CI_SHARDS}&shard_num=${shardNum}`, - `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + `http://localhost:5610/${buildHash}/bundles/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, // this causes tilemap tests to fail, probably because the eui styles haven't been // included in the karma harness a long some time, if ever // `http://localhost:5610/bundles/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}`, ...DllCompiler.getRawDllConfig().chunks.map( - chunk => `http://localhost:5610/built_assets/dlls/vendors${chunk}.style.dll.css` + chunk => + `http://localhost:5610/${buildHash}/built_assets/dlls/vendors${chunk}.style.dll.css` ), - 'http://localhost:5610/bundles/tests.style.css', + `http://localhost:5610/${buildHash}/bundles/tests.style.css`, ]; } @@ -125,9 +131,9 @@ module.exports = function(grunt) { proxies: { '/tests/': 'http://localhost:5610/tests/', - '/bundles/': 'http://localhost:5610/bundles/', - '/built_assets/dlls/': 'http://localhost:5610/built_assets/dlls/', '/test_bundle/': 'http://localhost:5610/test_bundle/', + [`/${buildHash}/bundles/`]: `http://localhost:5610/${buildHash}/bundles/`, + [`/${buildHash}/built_assets/dlls/`]: `http://localhost:5610/${buildHash}/built_assets/dlls/`, }, client: { diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 086b13ecee2b3..0168626d4a1a9 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -33,7 +33,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { ['geo.src', 'IN'], ]; - describe('Discover', () => { + // FLAKY: https://github.com/elastic/kibana/issues/62497 + describe.skip('Discover', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index c5bfc847d0041..0c4028905657d 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 0000000000000..c79f5cb470890 --- /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 0000000000000..e4654ee3985f3 --- /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 0000000000000..84bfd8a755c11 --- /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 0000000000000..4413c672fb46c --- /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); + }); + }); +} diff --git a/test/examples/config.js b/test/examples/config.js index 49d75da286075..2be34459d8d06 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -28,6 +28,7 @@ export default async function({ readConfigFile }) { require.resolve('./search'), require.resolve('./embeddables'), require.resolve('./ui_actions'), + require.resolve('./state_sync'), ], services: { ...functionalConfig.get('services'), diff --git a/test/examples/state_sync/index.ts b/test/examples/state_sync/index.ts new file mode 100644 index 0000000000000..3c524f0feb619 --- /dev/null +++ b/test/examples/state_sync/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function({ + getService, + getPageObjects, + loadTestFile, +}: PluginFunctionalProviderContext) { + const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); + + describe('state sync examples', function() { + before(async () => { + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('settings'); + }); + + loadTestFile(require.resolve('./todo_app')); + }); +} diff --git a/test/examples/state_sync/todo_app.ts b/test/examples/state_sync/todo_app.ts new file mode 100644 index 0000000000000..4933d746ca4fd --- /dev/null +++ b/test/examples/state_sync/todo_app.ts @@ -0,0 +1,189 @@ +/* + * 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 { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const retry = getService('retry'); + const appsMenu = getService('appsMenu'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['common']); + const log = getService('log'); + + describe('TODO app', () => { + describe("TODO app with browser history (platform's ScopedHistory)", async () => { + const appId = 'stateContainersExampleBrowserHistory'; + let base: string; + + before(async () => { + base = await PageObjects.common.getHostPort(); + await appsMenu.clickLink('State containers example - browser history routing'); + }); + + it('links are rendered correctly and state is preserved in links', async () => { + const getHrefByLinkTestSubj = async (linkTestSubj: string) => + (await testSubjects.find(linkTestSubj)).getAttribute('href'); + + await expectPathname(await getHrefByLinkTestSubj('filterLinkCompleted'), '/completed'); + await expectPathname( + await getHrefByLinkTestSubj('filterLinkNotCompleted'), + '/not-completed' + ); + await expectPathname(await getHrefByLinkTestSubj('filterLinkAll'), '/'); + }); + + it('TODO app state is synced with url, back navigation works', async () => { + // checking that in initial state checkbox is unchecked and state is synced with url + expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + + // check the checkbox by clicking the label (clicking checkbox directly fails as it is "no intractable") + (await find.byCssSelector('label[for="0"]')).click(); + + // wait for react to update dom and checkbox in checked state + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + // checking that url is updated with checked state + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + + // checking back and forward button + await browser.goBack(); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + }); + + await browser.goForward(); + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + }); + + it('links navigation works', async () => { + // click link to filter only not completed + await testSubjects.click('filterLinkNotCompleted'); + await expectPathname(await browser.getCurrentUrl(), '/not-completed'); + // checkbox should be missing because it is "completed" + await testSubjects.missingOrFail('todoCheckbox-0'); + }); + + /** + * Parses app's scoped pathname from absolute url and asserts it against `expectedPathname` + * Also checks that hashes are equal (detail of todo app that state is rendered in links) + * @param absoluteUrl + * @param expectedPathname + */ + async function expectPathname(absoluteUrl: string, expectedPathname: string) { + const scoped = await getScopedUrl(absoluteUrl); + const [pathname, newHash] = scoped.split('#'); + expect(pathname).to.be(expectedPathname); + const [, currentHash] = (await browser.getCurrentUrl()).split('#'); + expect(newHash.replace(/%27/g, "'")).to.be(currentHash.replace(/%27/g, "'")); + } + + /** + * Get's part of url scoped to this app (removed kibana's host and app's pathname) + * @param url - absolute url + */ + async function getScopedUrl(url: string): Promise { + expect(url).to.contain(base); + expect(url).to.contain(appId); + const scopedUrl = url.slice(url.indexOf(appId) + appId.length); + expect(scopedUrl).not.to.contain(appId); // app id in url only once + return scopedUrl; + } + }); + + describe('TODO app with hash history ', async () => { + before(async () => { + await appsMenu.clickLink('State containers example - hash history routing'); + }); + + it('Links are rendered correctly and state is preserved in links', async () => { + const getHrefByLinkTestSubj = async (linkTestSubj: string) => + (await testSubjects.find(linkTestSubj)).getAttribute('href'); + await expectHashPathname(await getHrefByLinkTestSubj('filterLinkCompleted'), '/completed'); + await expectHashPathname( + await getHrefByLinkTestSubj('filterLinkNotCompleted'), + '/not-completed' + ); + await expectHashPathname(await getHrefByLinkTestSubj('filterLinkAll'), '/'); + }); + + it('TODO app state is synced with url, back navigation works', async () => { + // checking that in initial state checkbox is unchecked and state is synced with url + expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + // check the checkbox by clicking the label (clicking checkbox directly fails as it is "no intractable") + (await find.byCssSelector('label[for="0"]')).click(); + + // wait for react to update dom and checkbox in checked state + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + // checking that url is updated with checked state + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + + // checking back and forward button + await browser.goBack(); + expect(await browser.getCurrentUrl()).to.contain('completed:!f'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(false); + }); + + await browser.goForward(); + expect(await browser.getCurrentUrl()).to.contain('completed:!t'); + await retry.tryForTime(1000, async () => { + await expect(await testSubjects.isChecked('todoCheckbox-0')).to.be(true); + }); + }); + + it('links navigation works', async () => { + // click link to filter only not completed + await testSubjects.click('filterLinkNotCompleted'); + await expectHashPathname(await browser.getCurrentUrl(), '/not-completed'); + // checkbox should be missing because it is "completed" + await testSubjects.missingOrFail('todoCheckbox-0'); + }); + + /** + * Parses app's pathname in hash from absolute url and asserts it against `expectedPathname` + * Also checks that queries in hashes are equal (detail of todo app that state is rendered in links) + * @param absoluteUrl + * @param expectedPathname + */ + async function expectHashPathname(hash: string, expectedPathname: string) { + log.debug(`expect hash pathname ${hash} to be ${expectedPathname}`); + const hashPath = hash.split('#')[1]; + const [hashPathname, hashQuery] = hashPath.split('?'); + const [, currentHash] = (await browser.getCurrentUrl()).split('#'); + const [, currentHashQuery] = currentHash.split('?'); + expect(currentHashQuery.replace(/%27/g, "'")).to.be(hashQuery.replace(/%27/g, "'")); + expect(hashPathname).to.be(expectedPathname); + } + }); + }); +} diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 456752c0cd6eb..47b8a4a2e9f70 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -40,7 +40,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'console']); describe('console app', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); await PageObjects.common.navigateToApp('console'); diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index a56a85546bbcd..9a0130d39bc2f 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -33,7 +33,6 @@ export default function({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/53308 describe.skip('context link in discover', function contextSize() { - this.tags('smoke'); before(async function() { await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index f388993dcaf7d..8846a753f3794 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -38,7 +38,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); describe('dashboard filtering', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('dashboard/current/kibana'); diff --git a/test/functional/apps/dashboard/dashboard_save.js b/test/functional/apps/dashboard/dashboard_save.js index 2ea1389b89ad4..7ffe951faa398 100644 --- a/test/functional/apps/dashboard/dashboard_save.js +++ b/test/functional/apps/dashboard/dashboard_save.js @@ -24,7 +24,7 @@ export default function({ getPageObjects, getService }) { const listingTable = getService('listingTable'); describe('dashboard save', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); const dashboardName = 'Dashboard Save Test'; const dashboardNameEnterKey = 'Dashboard Save Test with Enter Key'; diff --git a/test/functional/apps/dashboard/dashboard_saved_query.js b/test/functional/apps/dashboard/dashboard_saved_query.js new file mode 100644 index 0000000000000..99d0aed082e70 --- /dev/null +++ b/test/functional/apps/dashboard/dashboard_saved_query.js @@ -0,0 +1,128 @@ +/* + * 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'; + +export default function({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']); + const browser = getService('browser'); + const queryBar = getService('queryBar'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); + const testSubjects = getService('testSubjects'); + + describe('dashboard saved queries', function describeIndexTests() { + before(async function() { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + }); + + describe('saved query management component functionality', function() { + before(async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('should show the saved query management component when there are no saved queries', async () => { + await savedQueryManagementComponent.openSavedQueryManagementComponent(); + const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); + expect(descriptionText).to.eql( + 'SAVED QUERIES\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' + ); + }); + + it('should allow a query to be saved via the saved objects management component', async () => { + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.savedQueryTextExist('response:200'); + }); + + it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + }); + + it('preserves the currently loaded query when the page is reloaded', async () => { + await browser.refresh(); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); + }); + + it('allows saving changes to a currently loaded query via the saved query management component', async () => { + await queryBar.setQuery('response:404'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'OkResponse', + '404 responses', + false, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows saving the currently loaded query as a new query', async () => { + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'OkResponseCopy', + '200 responses', + false, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponseCopy'); + }); + + it('allows deleting the currently loaded saved query in the saved query management component and clears the query', async () => { + await savedQueryManagementComponent.deleteSavedQuery('OkResponseCopy'); + await savedQueryManagementComponent.savedQueryMissingOrFail('OkResponseCopy'); + expect(await queryBar.getQueryString()).to.eql(''); + }); + + it('resets any changes to a loaded query on reloading the same saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.setQuery('response:503'); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows clearing the currently loaded saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/full_screen_mode.js b/test/functional/apps/dashboard/full_screen_mode.js index df00f64530ca0..17eb6d8f08a9c 100644 --- a/test/functional/apps/dashboard/full_screen_mode.js +++ b/test/functional/apps/dashboard/full_screen_mode.js @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['dashboard', 'common']); + const filterBar = getService('filterBar'); describe('full screen mode', () => { before(async () => { @@ -81,5 +82,22 @@ export default function({ getService, getPageObjects }) { expect(isChromeVisible).to.be(true); }); }); + + it('shows filter bar in fullscreen mode', async () => { + await filterBar.addFilter('bytes', 'is', '12345678'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.clickFullScreenMode(); + await retry.try(async () => { + const isChromeHidden = await PageObjects.common.isChromeHidden(); + expect(isChromeHidden).to.be(true); + }); + expect(await filterBar.getFilterCount()).to.be(1); + await PageObjects.dashboard.clickExitFullScreenLogoButton(); + await retry.try(async () => { + const isChromeVisible = await PageObjects.common.isChromeVisible(); + expect(isChromeVisible).to.be(true); + }); + await filterBar.removeFilter('bytes'); + }); }); } diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 6666ccc57d584..bd8e6812147e1 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -74,6 +74,7 @@ export default function({ getService, loadTestFile }) { loadTestFile(require.resolve('./panel_expand_toggle')); loadTestFile(require.resolve('./dashboard_grid')); loadTestFile(require.resolve('./view_edit')); + loadTestFile(require.resolve('./dashboard_saved_query')); // Order of test suites *shouldn't* be important but there's a bug for the view_edit test above // https://github.com/elastic/kibana/issues/46752 // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index 6e24b9f3570a3..279adb22a1cfa 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -43,8 +43,6 @@ export default function({ getService, getPageObjects }) { const dashboardName = 'Dashboard Panel Controls Test'; describe('dashboard panel controls', function viewEditModeTests() { - this.tags('smoke'); - before(async function() { await PageObjects.dashboard.initTests(); await PageObjects.dashboard.preserveCrossAppState(); diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index b7698a7d6ac4b..564eb790eb8d1 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -27,7 +27,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']); describe('dashboard time zones', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('dashboard/current/kibana'); diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.js index eeef3333aab0f..dcd185eba00e6 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.js @@ -48,7 +48,7 @@ export default function({ getService, getPageObjects }) { log.debug('create long_window_logstash index pattern'); // NOTE: long_window_logstash load does NOT create index pattern - await PageObjects.settings.createIndexPattern('long-window-logstash-'); + await PageObjects.settings.createIndexPattern('long-window-logstash-*'); await kibanaServer.uiSettings.replace(defaultSettings); await browser.refresh(); diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 08e0cb0b8d23a..ebce8dcafadb4 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -33,7 +33,6 @@ export default function({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/62281 describe.skip('doc link in discover', function contextSize() { - this.tags('smoke'); before(async function() { await esArchiver.loadIfNeeded('logstash_functional'); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index 62d42f3da5c84..ace9710665f10 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -28,7 +28,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); describe('discover tab', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async function() { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 76f3a3aea365f..9b50eeda20073 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -74,6 +74,7 @@ export default function({ getService, getPageObjects }) { true ); await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.savedQueryTextExist('response:200'); }); it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { diff --git a/test/functional/apps/getting_started/_shakespeare.js b/test/functional/apps/getting_started/_shakespeare.js index 9a4bb0081b7ad..3a3d6b93e166b 100644 --- a/test/functional/apps/getting_started/_shakespeare.js +++ b/test/functional/apps/getting_started/_shakespeare.js @@ -59,9 +59,9 @@ export default function({ getService, getPageObjects }) { it('should create shakespeare index pattern', async function() { log.debug('Create shakespeare index pattern'); - await PageObjects.settings.createIndexPattern('shakes', null); + await PageObjects.settings.createIndexPattern('shakespeare', null); const patternName = await PageObjects.settings.getIndexPageHeading(); - expect(patternName).to.be('shakes*'); + expect(patternName).to.be('shakespeare'); }); // https://www.elastic.co/guide/en/kibana/current/tutorial-visualizing.html @@ -74,7 +74,7 @@ export default function({ getService, getPageObjects }) { log.debug('create shakespeare vertical bar chart'); await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVerticalBarChart(); - await PageObjects.visualize.clickNewSearch('shakes*'); + await PageObjects.visualize.clickNewSearch('shakespeare'); await PageObjects.visChart.waitForVisualization(); const expectedChartValues = [111396]; diff --git a/test/functional/apps/getting_started/index.js b/test/functional/apps/getting_started/index.js index b73c16e2583b5..41ee71a753712 100644 --- a/test/functional/apps/getting_started/index.js +++ b/test/functional/apps/getting_started/index.js @@ -21,7 +21,7 @@ export default function({ getService, loadTestFile }) { const browser = getService('browser'); describe('Getting Started ', function() { - this.tags(['ciGroup6', 'smoke']); + this.tags(['ciGroup6']); before(async function() { await browser.setWindowSize(1200, 800); diff --git a/test/functional/apps/home/_home.js b/test/functional/apps/home/_home.js index 6587c2f113b7f..3c56c22c046dd 100644 --- a/test/functional/apps/home/_home.js +++ b/test/functional/apps/home/_home.js @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'home']); describe('Kibana takes you home', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); it('clicking on kibana logo should take you to home page', async () => { await PageObjects.common.navigateToApp('settings'); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 5812b9b96e42a..f46b3390cbcf8 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -32,8 +32,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); describe('sample data', function describeIndexTests() { - this.tags('smoke'); - before(async () => { await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index c8d11d8555d37..65d852b249ea0 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -25,8 +25,6 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings', 'common']); describe('"Create Index Pattern" wizard', function() { - this.tags('smoke'); - before(async function() { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index 616e2297b2f51..2545b8f324d1b 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -44,10 +44,7 @@ export default function({ getService, getPageObjects }) { it('should handle special charaters in template input', async () => { await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.settings.setIndexPatternField({ - indexPatternName: '❤️', - expectWildcard: false, - }); + await PageObjects.settings.setIndexPatternField('❤️'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { diff --git a/test/functional/apps/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index c45a95abab86e..22d36d671cb68 100644 --- a/test/functional/apps/visualize/_experimental_vis.js +++ b/test/functional/apps/visualize/_experimental_vis.js @@ -24,8 +24,6 @@ export default ({ getService, getPageObjects }) => { const PageObjects = getPageObjects(['visualize']); describe('experimental visualizations in visualize app ', function() { - this.tags('smoke'); - describe('experimental visualizations', () => { beforeEach(async () => { log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index 7ebb4548f967b..d7a30f39250f3 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -28,8 +28,6 @@ export default function({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/45089 describe('gauge chart', function indexPatternCreation() { - this.tags('smoke'); - async function initGaugeVis() { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index cbf7ab8df6831..842599fa1d0a1 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -25,7 +25,6 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('heatmap chart', function indexPatternCreation() { - this.tags('smoke'); const vizName1 = 'Visualization HeatmapChart'; before(async function() { diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index d989f8e2539a0..256c0362226e5 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -24,7 +24,6 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { - this.tags('smoke'); before(async function() { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVerticalBarChart(); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 867db66ac81dc..27a06cc05b45c 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -29,7 +29,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); describe('visual builder', function describeIndexTests() { - this.tags('smoke'); beforeEach(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/_tsvb_time_series.ts b/test/functional/apps/visualize/_tsvb_time_series.ts index fa79190a5bf94..ac89c2b55e514 100644 --- a/test/functional/apps/visualize/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/_tsvb_time_series.ts @@ -25,7 +25,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); const kibanaServer = getService('kibanaServer'); - const testSubjects = getService('testSubjects'); describe('visual builder', function describeIndexTests() { beforeEach(async () => { @@ -126,20 +125,18 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(actualCountMin).to.be('3 hours'); }); - // --reversed class is not implemented in @elastic\chart - describe.skip('Dark mode', () => { + describe('Dark mode', () => { before(async () => { await kibanaServer.uiSettings.update({ 'theme:darkMode': true, }); }); - it(`viz should have 'reversed' class when background color is white`, async () => { + it(`viz should have light class when background color is white`, async () => { await visualBuilder.clickPanelOptions('timeSeries'); await visualBuilder.setBackgroundColor('#FFFFFF'); - const classNames = await testSubjects.getAttribute('timeseriesChart', 'class'); - expect(classNames.includes('tvbVisTimeSeries--reversed')).to.be(true); + expect(await visualBuilder.checkTimeSeriesIsLight()).to.be(true); }); after(async () => { diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js index b56a37218aba5..b5231f3161377 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.js +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.js @@ -27,7 +27,7 @@ export default function({ getService, getPageObjects }) { const comboBox = getService('comboBox'); describe('chained controls', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 862e5127bb670..93debdcc37f0a 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')) { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index a20d7ae9a5372..b76ce141a4418 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -215,6 +215,8 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide public async clickNewDashboard() { await listingTable.clickNewButton('createDashboardPromptButton'); + // make sure the dashboard page is shown + await this.waitForRenderComplete(); } public async clickCreateDashboardPrompt() { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 6dcd017335c85..81d22838d1e8b 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -206,15 +206,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getFieldsTabCount() { return retry.try(async () => { - const text = await testSubjects.getVisibleText('tab-count-indexedFields'); - return text.replace(/\((.*)\)/, '$1'); + const indexedFieldsTab = await find.byCssSelector('#indexedFields .euiTab__content'); + const text = await indexedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } async getScriptedFieldsTabCount() { return await retry.try(async () => { - const theText = await testSubjects.getVisibleText('tab-count-scriptedFields'); - return theText.replace(/\((.*)\)/, '$1'); + const scriptedFieldsTab = await find.byCssSelector('#scriptedFields .euiTab__content'); + const text = await scriptedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } @@ -241,13 +243,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async setFieldTypeFilter(type: string) { await find.clickByCssSelector( - 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[label="' + type + '"]' + 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[value="' + type + '"]' ); } async setScriptedFieldLanguageFilter(language: string) { await find.clickByCssSelector( - 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[label="' + + 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[value="' + language + '"]' ); @@ -332,7 +334,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider } await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { - await this.setIndexPatternField({ indexPatternName }); + await this.setIndexPatternField(indexPatternName); }); await PageObjects.common.sleep(2000); await (await this.getCreateIndexPatternGoToStep2Button()).click(); @@ -373,14 +375,32 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider return indexPatternId; } - async setIndexPatternField({ indexPatternName = 'logstash-', expectWildcard = true } = {}) { + async setIndexPatternField(indexPatternName = 'logstash-*') { log.debug(`setIndexPatternField(${indexPatternName})`); const field = await this.getIndexPatternField(); await field.clearValue(); - await field.type(indexPatternName, { charByChar: true }); + if ( + indexPatternName.charAt(0) === '*' && + indexPatternName.charAt(indexPatternName.length - 1) === '*' + ) { + // this is a special case when the index pattern name starts with '*' + // like '*:makelogs-*' where the UI will not append * + await field.type(indexPatternName, { charByChar: true }); + } else if (indexPatternName.charAt(indexPatternName.length - 1) === '*') { + // the common case where the UI will append '*' automatically so we won't type it + const tempName = indexPatternName.slice(0, -1); + await field.type(tempName, { charByChar: true }); + } else { + // case where we don't want the * appended so we'll remove it if it was added + await field.type(indexPatternName, { charByChar: true }); + const tempName = await field.getAttribute('value'); + if (tempName.length > indexPatternName.length) { + await field.type(browser.keys.DELETE, { charByChar: true }); + } + } const currentName = await field.getAttribute('value'); log.debug(`setIndexPatternField set to ${currentName}`); - expect(currentName).to.eql(`${indexPatternName}${expectWildcard ? '*' : ''}`); + expect(currentName).to.eql(indexPatternName); } async getCreateIndexPatternGoToStep2Button() { @@ -412,17 +432,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickFieldsTab() { log.debug('click Fields tab'); - await testSubjects.click('tab-indexFields'); + await find.clickByCssSelector('#indexedFields'); } async clickScriptedFieldsTab() { log.debug('click Scripted Fields tab'); - await testSubjects.click('tab-scriptedFields'); + await find.clickByCssSelector('#scriptedFields'); } async clickSourceFiltersTab() { log.debug('click Source Filters tab'); - await testSubjects.click('tab-sourceFilters'); + await find.clickByCssSelector('#sourceFilters'); } async editScriptedField(name: string) { diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index b8e6c812b46bd..12962b3a5cdef 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -71,6 +71,10 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro } } + public async checkTimeSeriesIsLight() { + return await find.existsByCssSelector('.tvbVisTimeSeriesLight'); + } + public async checkTimeSeriesLegendIsPresent() { const isPresent = await find.existsByCssSelector('.echLegend'); if (!isPresent) { diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index 244c1cd214de5..66bf15f3da53c 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -151,6 +151,12 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide await testSubjects.existOrFail(`~load-saved-query-${title}-button`); } + async savedQueryTextExist(text: string) { + await this.openSavedQueryManagementComponent(); + const queryString = await queryBar.getQueryString(); + expect(queryString).to.eql(text); + } + async savedQueryMissingOrFail(title: string) { await retry.try(async () => { await this.openSavedQueryManagementComponent(); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 611e16e5a942d..001293be44829 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0", "react-dom": "^16.12.0" } 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 0000000000000..5ea151dffdc8e --- /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 031a0e3576ccc..9590f9f8c1794 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/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index 914ff39884fa3..4ca705137a12c 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 9ee7845816faa..818621ffe5ae5 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 3b3d69c06ff3e..59b7337f5ff07 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "react": "^16.12.0" }, "scripts": { 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 b6d13a5604011..c384e41851e15 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, diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh index 0129d4f1bce9f..2bba6e06d76d7 100755 --- a/test/scripts/jenkins_firefox_smoke.sh +++ b/test/scripts/jenkins_firefox_smoke.sh @@ -6,5 +6,5 @@ checks-reporter-with-killswitch "Firefox smoke test" \ node scripts/functional_tests \ --bail --debug \ --kibana-install-dir "$installDir" \ - --include-tag "smoke" \ + --include-tag "includeFirefox" \ --config test/functional/config.firefox.js; diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh index 67d88b308ed91..951ba8e22d885 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/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh index 5fe8b41cc0010..fdaee76cafa9d 100755 --- a/test/scripts/jenkins_xpack_firefox_smoke.sh +++ b/test/scripts/jenkins_xpack_firefox_smoke.sh @@ -6,5 +6,5 @@ checks-reporter-with-killswitch "X-Pack firefox smoke test" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --include-tag "smoke" \ + --include-tag "includeFirefox" \ --config test/functional/config.firefox.js; diff --git a/vars/workers.groovy b/vars/workers.groovy index 1c55c676d9425..b4e4a115f2011 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -57,6 +57,26 @@ def base(Map params, Closure closure) { // Try to clone from Github up to 8 times, waiting 15 secs between attempts retryWithDelay(8, 15) { scmVars = checkout scm + + def mergeBase + if (env.ghprbTargetBranch) { + sh( + script: "cd kibana && git fetch origin ${env.ghprbTargetBranch}", + label: "update reference to target branch 'origin/${env.ghprbTargetBranch}'" + ) + mergeBase = sh( + script: "cd kibana && git merge-base HEAD FETCH_HEAD", + label: "determining merge point with target branch 'origin/${env.ghprbTargetBranch}'", + returnStdout: true + ).trim() + } + + ciStats.reportGitInfo( + env.ghprbSourceBranch ?: scmVars.GIT_LOCAL_BRANCH ?: scmVars.GIT_BRANCH, + scmVars.GIT_COMMIT, + env.ghprbTargetBranch, + mergeBase + ) } } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index c8715ac3447bd..9f43bf8da0601 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -8,19 +8,20 @@ "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": "legacy/plugins/beats_management", "xpack.canvas": "legacy/plugins/canvas", - "xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication", + "xpack.crossClusterReplication": "plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", "xpack.data": "plugins/data_enhanced", "xpack.drilldowns": "plugins/drilldowns", "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", - "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], + "xpack.graph": ["plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", "xpack.infra": "plugins/infra", "xpack.ingestManager": "plugins/ingest_manager", + "xpack.ingestPipelines": "plugins/ingest_pipelines", "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", @@ -36,14 +37,14 @@ "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": ["legacy/plugins/security", "plugins/security"], "xpack.server": "legacy/server", - "xpack.siem": ["plugins/siem", "legacy/plugins/siem"], + "xpack.siem": "plugins/siem", "xpack.snapshotRestore": "plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", "xpack.transform": "plugins/transform", "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", - "xpack.uptime": ["plugins/uptime", "legacy/plugins/uptime"], + "xpack.uptime": ["plugins/uptime"], "xpack.watcher": "plugins/watcher" }, "translations": [ diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 3068cdd0daa5b..4f1251321b005 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -32,7 +32,22 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, '^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: [ diff --git a/x-pack/index.js b/x-pack/index.js index 7fbd992120ea6..89cbb03f084eb 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,59 +5,33 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { graph } from './legacy/plugins/graph'; import { monitoring } from './legacy/plugins/monitoring'; import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; -import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; -import { apm } from './legacy/plugins/apm'; import { maps } from './legacy/plugins/maps'; -import { indexManagement } from './legacy/plugins/index_management'; import { spaces } from './legacy/plugins/spaces'; import { canvas } from './legacy/plugins/canvas'; import { infra } from './legacy/plugins/infra'; import { taskManager } from './legacy/plugins/task_manager'; -import { rollup } from './legacy/plugins/rollup'; -import { siem } from './legacy/plugins/siem'; -import { remoteClusters } from './legacy/plugins/remote_clusters'; -import { crossClusterReplication } from './legacy/plugins/cross_cluster_replication'; -import { upgradeAssistant } from './legacy/plugins/upgrade_assistant'; -import { uptime } from './legacy/plugins/uptime'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; -import { actions } from './legacy/plugins/actions'; -import { alerting } from './legacy/plugins/alerting'; import { ingestManager } from './legacy/plugins/ingest_manager'; -import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; module.exports = function(kibana) { return [ xpackMain(kibana), - graph(kibana), monitoring(kibana), reporting(kibana), spaces(kibana), security(kibana), dashboardMode(kibana), - logstash(kibana), beats(kibana), - apm(kibana), maps(kibana), canvas(kibana), - indexManagement(kibana), infra(kibana), taskManager(kibana), - rollup(kibana), - siem(kibana), - remoteClusters(kibana), - crossClusterReplication(kibana), - upgradeAssistant(kibana), - uptime(kibana), encryptedSavedObjects(kibana), - actions(kibana), - alerting(kibana), ingestManager(kibana), - triggersActionsUI(kibana), ]; }; diff --git a/x-pack/legacy/plugins/actions/index.ts b/x-pack/legacy/plugins/actions/index.ts deleted file mode 100644 index 276d1ea3accea..0000000000000 --- a/x-pack/legacy/plugins/actions/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './server/'; diff --git a/x-pack/legacy/plugins/actions/server/index.ts b/x-pack/legacy/plugins/actions/server/index.ts deleted file mode 100644 index 7235eda88149f..0000000000000 --- a/x-pack/legacy/plugins/actions/server/index.ts +++ /dev/null @@ -1,33 +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 { Root } from 'joi'; -import { Legacy } from 'kibana'; -import mappings from './mappings.json'; - -export function actions(kibana: any) { - return new kibana.Plugin({ - id: 'actions', - configPrefix: 'xpack.actions', - config(Joi: Root) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }) - .unknown(true) - .default(); - }, - require: ['kibana', 'elasticsearch'], - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.encryptedSavedObjects.enabled') === true && - config.get('xpack.actions.enabled') === true && - config.get('xpack.task_manager.enabled') === true - ); - }, - uiExports: { - mappings, - }, - }); -} diff --git a/x-pack/legacy/plugins/alerting/index.ts b/x-pack/legacy/plugins/alerting/index.ts deleted file mode 100644 index 0d0a698841269..0000000000000 --- a/x-pack/legacy/plugins/alerting/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './server'; diff --git a/x-pack/legacy/plugins/alerting/server/index.ts b/x-pack/legacy/plugins/alerting/server/index.ts deleted file mode 100644 index 5bf7cda51bda6..0000000000000 --- a/x-pack/legacy/plugins/alerting/server/index.ts +++ /dev/null @@ -1,35 +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 { Legacy } from 'kibana'; -import { Root } from 'joi'; -import mappings from './mappings.json'; - -export function alerting(kibana: any) { - return new kibana.Plugin({ - id: 'alerting', - configPrefix: 'xpack.alerting', - require: ['kibana', 'elasticsearch', 'actions', 'task_manager', 'encryptedSavedObjects'], - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.alerting.enabled') === true && - config.get('xpack.actions.enabled') === true && - config.get('xpack.encryptedSavedObjects.enabled') === true && - config.get('xpack.task_manager.enabled') === true - ); - }, - config(Joi: Root) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - }) - .default(); - }, - uiExports: { - mappings, - }, - }); -} diff --git a/x-pack/legacy/plugins/apm/.prettierrc b/x-pack/legacy/plugins/apm/.prettierrc deleted file mode 100644 index 650cb880f6f5a..0000000000000 --- a/x-pack/legacy/plugins/apm/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "singleQuote": true, - "semi": true -} diff --git a/x-pack/legacy/plugins/apm/e2e/README.md b/x-pack/legacy/plugins/apm/e2e/README.md deleted file mode 100644 index d4a95fa83fbb2..0000000000000 --- a/x-pack/legacy/plugins/apm/e2e/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# End-To-End (e2e) Test for APM UI - -**Run E2E tests** - -```sh -x-pack/legacy/plugins/apm/e2e/run-e2e.sh -``` - -_Starts APM Server, Elasticsearch (with sample data) and runs the tests_ diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/legacy/plugins/apm/e2e/cypress/integration/snapshots.js deleted file mode 100644 index 968c2675a62e7..0000000000000 --- a/x-pack/legacy/plugins/apm/e2e/cypress/integration/snapshots.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - "APM": { - "Transaction duration charts": { - "1": "500 ms", - "2": "250 ms", - "3": "0 ms" - } - }, - "__version": "4.2.0" -} diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts deleted file mode 100644 index d2383acd45eba..0000000000000 --- a/x-pack/legacy/plugins/apm/index.ts +++ /dev/null @@ -1,166 +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 { i18n } from '@kbn/i18n'; -import { Server } from 'hapi'; -import { resolve } from 'path'; -import { APMPluginContract } from '../../../plugins/apm/server'; -import { LegacyPluginInitializer } from '../../../../src/legacy/types'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; -import mappings from './mappings.json'; - -export const apm: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - require: [ - 'kibana', - 'elasticsearch', - 'xpack_main', - 'apm_oss', - 'task_manager' - ], - id: 'apm', - configPrefix: 'xpack.apm', - publicDir: resolve(__dirname, 'public'), - uiExports: { - app: { - title: 'APM', - description: i18n.translate('xpack.apm.apmForESDescription', { - defaultMessage: 'APM for the Elastic Stack' - }), - main: 'plugins/apm/index', - icon: 'plugins/apm/icon.svg', - euiIconType: 'apmApp', - order: 8100, - category: DEFAULT_APP_CATEGORIES.observability - }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - home: ['plugins/apm/legacy_register_feature'], - - // TODO: get proper types - injectDefaultVars(server: Server) { - const config = server.config(); - return { - apmUiEnabled: config.get('xpack.apm.ui.enabled'), - // TODO: rename to apm_oss.indexPatternTitle in 7.0 (breaking change) - apmIndexPatternTitle: config.get('apm_oss.indexPattern'), - apmServiceMapEnabled: config.get('xpack.apm.serviceMapEnabled') - }; - }, - savedObjectSchemas: { - 'apm-services-telemetry': { - isNamespaceAgnostic: true - }, - 'apm-indices': { - isNamespaceAgnostic: true - } - }, - mappings - }, - - // TODO: get proper types - config(Joi: any) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - transactionGroupBucketSize: Joi.number().default(100), - maxTraceItems: Joi.number().default(1000) - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - - // index patterns - autocreateApmIndexPattern: Joi.boolean().default(true), - - // service map - serviceMapEnabled: Joi.boolean().default(true), - serviceMapFingerprintBucketSize: Joi.number().default(100), - serviceMapTraceIdBucketSize: Joi.number().default(65), - serviceMapFingerprintGlobalBucketSize: Joi.number().default(1000), - serviceMapTraceIdGlobalBucketSize: Joi.number().default(6), - serviceMapMaxTracesPerRequest: Joi.number().default(50), - - // telemetry - telemetryCollectionEnabled: Joi.boolean().default(true) - }).default(); - }, - - // TODO: get proper types - init(server: Server) { - server.plugins.xpack_main.registerFeature({ - id: 'apm', - name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { - defaultMessage: 'APM' - }), - order: 900, - icon: 'apmApp', - navLinkId: 'apm', - app: ['apm', 'kibana'], - catalogue: ['apm'], - // see x-pack/plugins/features/common/feature_kibana_privileges.ts - privileges: { - all: { - app: ['apm', 'kibana'], - api: [ - 'apm', - 'apm_write', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all' - ], - catalogue: ['apm'], - savedObject: { - all: ['alert', 'action', 'action_task_params'], - read: [] - }, - ui: [ - 'show', - 'save', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete' - ] - }, - read: { - app: ['apm', 'kibana'], - api: [ - 'apm', - 'actions-read', - 'actions-all', - 'alerting-read', - 'alerting-all' - ], - catalogue: ['apm'], - savedObject: { - all: ['alert', 'action', 'action_task_params'], - read: [] - }, - ui: [ - 'show', - 'alerting:show', - 'actions:show', - 'alerting:save', - 'actions:save', - 'alerting:delete', - 'actions:delete' - ] - } - } - }); - const apmPlugin = server.newPlatform.setup.plugins - .apm as APMPluginContract; - - apmPlugin.registerLegacyAPI({ - server - }); - } - }); -}; diff --git a/x-pack/legacy/plugins/apm/mappings.json b/x-pack/legacy/plugins/apm/mappings.json deleted file mode 100644 index 6ca9f13792085..0000000000000 --- a/x-pack/legacy/plugins/apm/mappings.json +++ /dev/null @@ -1,933 +0,0 @@ -{ - "apm-telemetry": { - "properties": { - "agents": { - "properties": { - "dotnet": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "go": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "java": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "js-base": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "nodejs": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "python": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "ruby": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - }, - "rum-js": { - "properties": { - "agent": { - "properties": { - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "service": { - "properties": { - "framework": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "language": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - }, - "runtime": { - "properties": { - "composite": { - "type": "keyword", - "ignore_above": 1024 - }, - "name": { - "type": "keyword", - "ignore_above": 1024 - }, - "version": { - "type": "keyword", - "ignore_above": 1024 - } - } - } - } - } - } - } - } - }, - "counts": { - "properties": { - "agent_configuration": { - "properties": { - "all": { - "type": "long" - } - } - }, - "error": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "max_error_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "max_transaction_groups_per_service": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "services": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "sourcemap": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "span": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - }, - "traces": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "1d": { - "type": "long" - }, - "all": { - "type": "long" - } - } - } - } - }, - "cardinality": { - "properties": { - "user_agent": { - "properties": { - "original": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - }, - "transaction": { - "properties": { - "name": { - "properties": { - "all_agents": { - "properties": { - "1d": { - "type": "long" - } - } - }, - "rum": { - "properties": { - "1d": { - "type": "long" - } - } - } - } - } - } - } - } - }, - "has_any_services": { - "type": "boolean" - }, - "indices": { - "properties": { - "all": { - "properties": { - "total": { - "properties": { - "docs": { - "properties": { - "count": { - "type": "long" - } - } - }, - "store": { - "properties": { - "size_in_bytes": { - "type": "long" - } - } - } - } - } - } - }, - "shards": { - "properties": { - "total": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "ml": { - "properties": { - "all_jobs_count": { - "type": "long" - } - } - } - } - }, - "retainment": { - "properties": { - "error": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "metric": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "onboarding": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "span": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "transaction": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services_per_agent": { - "properties": { - "dotnet": { - "type": "long", - "null_value": 0 - }, - "go": { - "type": "long", - "null_value": 0 - }, - "java": { - "type": "long", - "null_value": 0 - }, - "js-base": { - "type": "long", - "null_value": 0 - }, - "nodejs": { - "type": "long", - "null_value": 0 - }, - "python": { - "type": "long", - "null_value": 0 - }, - "ruby": { - "type": "long", - "null_value": 0 - }, - "rum-js": { - "type": "long", - "null_value": 0 - } - } - }, - "tasks": { - "properties": { - "agent_configuration": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "agents": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "cardinality": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "groupings": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "indices_stats": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "integrations": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "processor_events": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "services": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - }, - "versions": { - "properties": { - "took": { - "properties": { - "ms": { - "type": "long" - } - } - } - } - } - } - }, - "version": { - "properties": { - "apm_server": { - "properties": { - "major": { - "type": "long" - }, - "minor": { - "type": "long" - }, - "patch": { - "type": "long" - } - } - } - } - } - } - }, - "apm-indices": { - "properties": { - "apm_oss.sourcemapIndices": { - "type": "keyword" - }, - "apm_oss.errorIndices": { - "type": "keyword" - }, - "apm_oss.onboardingIndices": { - "type": "keyword" - }, - "apm_oss.spanIndices": { - "type": "keyword" - }, - "apm_oss.transactionIndices": { - "type": "keyword" - }, - "apm_oss.metricsIndices": { - "type": "keyword" - } - } - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx deleted file mode 100644 index 490bf472065e3..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ /dev/null @@ -1,209 +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 { - EuiButtonEmpty, - EuiPanel, - EuiSpacer, - EuiTab, - EuiTabs, - EuiTitle, - EuiIcon, - EuiToolTip -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Location } from 'history'; -import React from 'react'; -import styled from 'styled-components'; -import { first } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupAPIResponse } from '../../../../../../../../plugins/apm/server/lib/errors/get_error_group'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { px, unit, units } from '../../../../style/variables'; -import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { history } from '../../../../utils/history'; -import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata'; -import { Stacktrace } from '../../../shared/Stacktrace'; -import { - ErrorTab, - exceptionStacktraceTab, - getTabs, - logStacktraceTab -} from './ErrorTabs'; -import { Summary } from '../../../shared/Summary'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; -import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem'; -import { ExceptionStacktrace } from './ExceptionStacktrace'; - -const HeaderContainer = styled.div` - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: ${px(unit)}; -`; - -const TransactionLinkName = styled.div` - margin-left: ${px(units.half)}; - display: inline-block; - vertical-align: middle; -`; - -interface Props { - errorGroup: ErrorGroupAPIResponse; - urlParams: IUrlParams; - location: Location; -} - -// TODO: Move query-string-based tabs into a re-usable component? -function getCurrentTab( - tabs: ErrorTab[] = [], - currentTabKey: string | undefined -) { - const selectedTab = tabs.find(({ key }) => key === currentTabKey); - return selectedTab ? selectedTab : first(tabs) || {}; -} - -export function DetailView({ errorGroup, urlParams, location }: Props) { - const { transaction, error, occurrencesCount } = errorGroup; - - if (!error) { - return null; - } - - const tabs = getTabs(error); - const currentTab = getCurrentTab(tabs, urlParams.detailTab); - - const errorUrl = error.error.page?.url || error.url?.full; - - const method = error.http?.request?.method; - const status = error.http?.response?.status_code; - - return ( - - - -

- {i18n.translate( - 'xpack.apm.errorGroupDetails.errorOccurrenceTitle', - { - defaultMessage: 'Error occurrence' - } - )} -

-
- - - {i18n.translate( - 'xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel', - { - defaultMessage: - 'View {occurrencesCount} {occurrencesCount, plural, one {occurrence} other {occurrences}} in Discover.', - values: { occurrencesCount } - } - )} - - -
- - , - errorUrl && method ? ( - - ) : null, - transaction && transaction.user_agent ? ( - - ) : null, - transaction && ( - - - - - {transaction.transaction.name} - - - - ) - ]} - /> - - - - - {tabs.map(({ key, label }) => { - return ( - { - history.replace({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - detailTab: key - }) - }); - }} - isSelected={currentTab.key === key} - key={key} - > - {label} - - ); - })} - - - - - ); -} - -function TabContent({ - error, - currentTab -}: { - error: APMError; - currentTab: ErrorTab; -}) { - const codeLanguage = error.service.language?.name; - const exceptions = error.error.exception || []; - const logStackframes = error.error.log?.stacktrace; - - switch (currentTab.key) { - case logStacktraceTab.key: - return ( - - ); - case exceptionStacktraceTab.key: - return ( - - ); - default: - return ; - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx deleted file mode 100644 index ccd720ceee075..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ /dev/null @@ -1,209 +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 { - EuiBadge, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle -} from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../plugins/apm/common/i18n'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables'; -import { ApmHeader } from '../../shared/ApmHeader'; -import { DetailView } from './DetailView'; -import { ErrorDistribution } from './Distribution'; -import { useLocation } from '../../../hooks/useLocation'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; - -const Titles = styled.div` - margin-bottom: ${px(units.plus)}; -`; - -const Label = styled.div` - margin-bottom: ${px(units.quarter)}; - font-size: ${fontSizes.small}; - color: ${theme.euiColorMediumShade}; -`; - -const Message = styled.div` - font-family: ${fontFamilyCode}; - font-weight: bold; - font-size: ${fontSizes.large}; - margin-bottom: ${px(units.half)}; -`; - -const Culprit = styled.div` - font-family: ${fontFamilyCode}; -`; - -function getShortGroupId(errorGroupId?: string) { - if (!errorGroupId) { - return NOT_AVAILABLE_LABEL; - } - - return errorGroupId.slice(0, 5); -} - -export function ErrorGroupDetails() { - const location = useLocation(); - const { urlParams, uiFilters } = useUrlParams(); - const { serviceName, start, end, errorGroupId } = urlParams; - - const { data: errorGroupData } = useFetcher( - callApmApi => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/{groupId}', - params: { - path: { - serviceName, - groupId: errorGroupId - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); - - const { data: errorDistributionData } = useFetcher( - callApmApi => { - if (serviceName && start && end && errorGroupId) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName - }, - query: { - start, - end, - groupId: errorGroupId, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - } - }, - [serviceName, start, end, errorGroupId, uiFilters] - ); - - useTrackPageview({ app: 'apm', path: 'error_group_details' }); - useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); - - if (!errorGroupData || !errorDistributionData) { - return null; - } - - // If there are 0 occurrences, show only distribution chart w. empty message - const showDetails = errorGroupData.occurrencesCount !== 0; - const logMessage = errorGroupData.error?.error.log?.message; - const excMessage = errorGroupData.error?.error.exception?.[0].message; - const culprit = errorGroupData.error?.error.culprit; - const isUnhandled = - errorGroupData.error?.error.exception?.[0].handled === false; - - return ( -
- - - - -

- {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { - defaultMessage: 'Error group {errorGroupId}', - values: { - errorGroupId: getShortGroupId(urlParams.errorGroupId) - } - })} -

-
-
- {isUnhandled && ( - - - {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { - defaultMessage: 'Unhandled' - })} - - - )} -
-
- - - - - {showDetails && ( - - - {logMessage && ( - - - {logMessage} - - )} - - {excMessage || NOT_AVAILABLE_LABEL} - - {culprit || NOT_AVAILABLE_LABEL} - - - )} - - - - - {showDetails && ( - - )} -
- ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx deleted file mode 100644 index 250b9a5d188d0..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx +++ /dev/null @@ -1,199 +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 { EuiBadge, EuiToolTip } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ErrorGroupListAPIResponse } from '../../../../../../../../plugins/apm/server/lib/errors/get_error_groups'; -import { - fontFamilyCode, - fontSizes, - px, - truncate, - unit -} from '../../../../style/variables'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { ManagedTable } from '../../../shared/ManagedTable'; -import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; -import { TimestampTooltip } from '../../../shared/TimestampTooltip'; -import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; -import { APMQueryParams } from '../../../shared/Links/url_helpers'; - -const GroupIdLink = styled(ErrorDetailLink)` - font-family: ${fontFamilyCode}; -`; - -const MessageAndCulpritCell = styled.div` - ${truncate('100%')}; -`; - -const ErrorLink = styled(ErrorOverviewLink)` - ${truncate('100%')}; -`; - -const MessageLink = styled(ErrorDetailLink)` - font-family: ${fontFamilyCode}; - font-size: ${fontSizes.large}; - ${truncate('100%')}; -`; - -const Culprit = styled.div` - font-family: ${fontFamilyCode}; -`; - -interface Props { - items: ErrorGroupListAPIResponse; -} - -const ErrorGroupList: React.FC = props => { - const { items } = props; - const { urlParams } = useUrlParams(); - const { serviceName } = urlParams; - - if (!serviceName) { - throw new Error('Service name is required'); - } - - const columns = useMemo( - () => [ - { - name: i18n.translate('xpack.apm.errorsTable.groupIdColumnLabel', { - defaultMessage: 'Group ID' - }), - field: 'groupId', - sortable: false, - width: px(unit * 6), - render: (groupId: string) => { - return ( - - {groupId.slice(0, 5) || NOT_AVAILABLE_LABEL} - - ); - } - }, - { - name: i18n.translate('xpack.apm.errorsTable.typeColumnLabel', { - defaultMessage: 'Type' - }), - field: 'type', - sortable: false, - render: (type: string, item: ErrorGroupListAPIResponse[0]) => { - return ( - - {type} - - ); - } - }, - { - name: i18n.translate( - 'xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel', - { - defaultMessage: 'Error message and culprit' - } - ), - field: 'message', - sortable: false, - width: '50%', - render: (message: string, item: ErrorGroupListAPIResponse[0]) => { - return ( - - - - {message || NOT_AVAILABLE_LABEL} - - -
- - {item.culprit || NOT_AVAILABLE_LABEL} - -
- ); - } - }, - { - name: '', - field: 'handled', - sortable: false, - align: 'right', - render: (isUnhandled: boolean) => - isUnhandled === false && ( - - {i18n.translate('xpack.apm.errorsTable.unhandledLabel', { - defaultMessage: 'Unhandled' - })} - - ) - }, - { - name: i18n.translate('xpack.apm.errorsTable.occurrencesColumnLabel', { - defaultMessage: 'Occurrences' - }), - field: 'occurrenceCount', - sortable: true, - dataType: 'number', - render: (value?: number) => - value ? numeral(value).format('0.[0]a') : NOT_AVAILABLE_LABEL - }, - { - field: 'latestOccurrenceAt', - sortable: true, - name: i18n.translate( - 'xpack.apm.errorsTable.latestOccurrenceColumnLabel', - { - defaultMessage: 'Latest occurrence' - } - ), - align: 'right', - render: (value?: number) => - value ? ( - - ) : ( - NOT_AVAILABLE_LABEL - ) - } - ], - [serviceName, urlParams] - ); - - return ( - - ); -}; - -export { ErrorGroupList }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx deleted file mode 100644 index 8c5a4545f1043..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx +++ /dev/null @@ -1,137 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiTitle -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; -import { ErrorGroupList } from './List'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; - -const ErrorGroupOverview: React.FC = () => { - const { urlParams, uiFilters } = useUrlParams(); - - const { serviceName, start, end, sortField, sortDirection } = urlParams; - - const { data: errorDistributionData } = useFetcher( - callApmApi => { - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors/distribution', - params: { - path: { - serviceName - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - } - }, - [serviceName, start, end, uiFilters] - ); - - const { data: errorGroupListData } = useFetcher( - callApmApi => { - const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; - - if (serviceName && start && end) { - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/errors', - params: { - path: { - serviceName - }, - query: { - start, - end, - sortField, - sortDirection: normalizedSortDirection, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - } - }, - [serviceName, start, end, sortField, sortDirection, uiFilters] - ); - - useTrackPageview({ - app: 'apm', - path: 'error_group_overview' - }); - useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); - - const localUIFiltersConfig = useMemo(() => { - const config: React.ComponentProps = { - filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], - params: { - serviceName - }, - projection: PROJECTION.ERROR_GROUPS - }; - - return config; - }, [serviceName]); - - if (!errorDistributionData || !errorGroupListData) { - return null; - } - - return ( - <> - - - - - - - - - - - - - - - - - - -

Errors

-
- - - -
-
-
- - ); -}; - -export { ErrorGroupOverview }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx deleted file mode 100644 index c87e56fe9eff6..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx +++ /dev/null @@ -1,253 +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 { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../../../../../plugins/apm/common/service_nodes'; -import { ErrorGroupDetails } from '../../ErrorGroupDetails'; -import { ServiceDetails } from '../../ServiceDetails'; -import { TransactionDetails } from '../../TransactionDetails'; -import { Home } from '../../Home'; -import { BreadcrumbRoute } from '../ProvideBreadcrumbs'; -import { RouteName } from './route_names'; -import { Settings } from '../../Settings'; -import { AgentConfigurations } from '../../Settings/AgentConfigurations'; -import { ApmIndices } from '../../Settings/ApmIndices'; -import { toQuery } from '../../../shared/Links/url_helpers'; -import { ServiceNodeMetrics } from '../../ServiceNodeMetrics'; -import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams'; -import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -import { TraceLink } from '../../TraceLink'; -import { CustomizeUI } from '../../Settings/CustomizeUI'; -import { - EditAgentConfigurationRouteHandler, - CreateAgentConfigurationRouteHandler -} from './route_handlers/agent_configuration'; - -const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', { - defaultMessage: 'Metrics' -}); - -interface RouteParams { - serviceName: string; -} - -const renderAsRedirectTo = (to: string) => { - return ({ location }: RouteComponentProps) => ( - - ); -}; - -export const routes: BreadcrumbRoute[] = [ - { - exact: true, - path: '/', - render: renderAsRedirectTo('/services'), - breadcrumb: 'APM', - name: RouteName.HOME - }, - { - exact: true, - path: '/services', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { - defaultMessage: 'Services' - }), - name: RouteName.SERVICES - }, - { - exact: true, - path: '/traces', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { - defaultMessage: 'Traces' - }), - name: RouteName.TRACES - }, - { - exact: true, - path: '/settings', - render: renderAsRedirectTo('/settings/agent-configuration'), - breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', { - defaultMessage: 'Settings' - }), - name: RouteName.SETTINGS - }, - { - exact: true, - path: '/settings/apm-indices', - component: () => ( - - - - ), - breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', { - defaultMessage: 'Indices' - }), - name: RouteName.INDICES - }, - { - exact: true, - path: '/settings/agent-configuration', - component: () => ( - - - - ), - breadcrumb: i18n.translate( - 'xpack.apm.breadcrumb.settings.agentConfigurationTitle', - { defaultMessage: 'Agent Configuration' } - ), - name: RouteName.AGENT_CONFIGURATION - }, - - { - exact: true, - path: '/settings/agent-configuration/create', - breadcrumb: i18n.translate( - 'xpack.apm.breadcrumb.settings.createAgentConfigurationTitle', - { defaultMessage: 'Create Agent Configuration' } - ), - name: RouteName.AGENT_CONFIGURATION_CREATE, - component: () => - }, - { - exact: true, - path: '/settings/agent-configuration/edit', - breadcrumb: i18n.translate( - 'xpack.apm.breadcrumb.settings.editAgentConfigurationTitle', - { defaultMessage: 'Edit Agent Configuration' } - ), - name: RouteName.AGENT_CONFIGURATION_EDIT, - component: () => - }, - { - exact: true, - path: '/services/:serviceName', - breadcrumb: ({ match }) => match.params.serviceName, - render: (props: RouteComponentProps) => - renderAsRedirectTo( - `/services/${props.match.params.serviceName}/transactions` - )(props), - name: RouteName.SERVICE - }, - // errors - { - exact: true, - path: '/services/:serviceName/errors/:groupId', - component: ErrorGroupDetails, - breadcrumb: ({ match }) => match.params.groupId, - name: RouteName.ERROR - }, - { - exact: true, - path: '/services/:serviceName/errors', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', { - defaultMessage: 'Errors' - }), - name: RouteName.ERRORS - }, - // transactions - { - exact: true, - path: '/services/:serviceName/transactions', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', { - defaultMessage: 'Transactions' - }), - name: RouteName.TRANSACTIONS - }, - // metrics - { - exact: true, - path: '/services/:serviceName/metrics', - component: () => , - breadcrumb: metricsBreadcrumb, - name: RouteName.METRICS - }, - // service nodes, only enabled for java agents for now - { - exact: true, - path: '/services/:serviceName/nodes', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', { - defaultMessage: 'JVMs' - }), - name: RouteName.SERVICE_NODES - }, - // node metrics - { - exact: true, - path: '/services/:serviceName/nodes/:serviceNodeName/metrics', - component: () => , - breadcrumb: ({ location }) => { - const { serviceNodeName } = resolveUrlParams(location, {}); - - if (serviceNodeName === SERVICE_NODE_NAME_MISSING) { - return UNIDENTIFIED_SERVICE_NODES_LABEL; - } - - return serviceNodeName || ''; - }, - name: RouteName.SERVICE_NODE_METRICS - }, - { - exact: true, - path: '/services/:serviceName/transactions/view', - component: TransactionDetails, - breadcrumb: ({ location }) => { - const query = toQuery(location.search); - return query.transactionName as string; - }, - name: RouteName.TRANSACTION_NAME - }, - { - exact: true, - path: '/link-to/trace/:traceId', - component: TraceLink, - breadcrumb: null, - name: RouteName.LINK_TO_TRACE - }, - - { - exact: true, - path: '/service-map', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map' - }), - name: RouteName.SERVICE_MAP - }, - { - exact: true, - path: '/services/:serviceName/service-map', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map' - }), - name: RouteName.SINGLE_SERVICE_MAP - }, - { - exact: true, - path: '/settings/customize-ui', - component: () => ( - - - - ), - breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', { - defaultMessage: 'Customize UI' - }), - name: RouteName.CUSTOMIZE_UI - } -]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx deleted file mode 100644 index 7e8d057a7be6c..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx +++ /dev/null @@ -1,30 +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 React from 'react'; -import { AlertType } from '../../../../../../../../../plugins/apm/common/alert_types'; -import { AlertAdd } from '../../../../../../../../../plugins/triggers_actions_ui/public'; - -type AlertAddProps = React.ComponentProps; - -interface Props { - addFlyoutVisible: AlertAddProps['addFlyoutVisible']; - setAddFlyoutVisibility: AlertAddProps['setAddFlyoutVisibility']; - alertType: AlertType | null; -} - -export function AlertingFlyout(props: Props) { - const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; - - return alertType ? ( - - ) : null; -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx deleted file mode 100644 index 92b325ab00d35..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ /dev/null @@ -1,146 +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 { - EuiButtonEmpty, - EuiContextMenu, - EuiPopover, - EuiContextMenuPanelDescriptor -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { AlertType } from '../../../../../../../../plugins/apm/common/alert_types'; -import { AlertingFlyout } from './AlertingFlyout'; -import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; - -const alertLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.alerts', - { - defaultMessage: 'Alerts' - } -); - -const createThresholdAlertLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert', - { - defaultMessage: 'Create threshold alert' - } -); - -const CREATE_THRESHOLD_ALERT_PANEL_ID = 'create_threshold'; - -interface Props { - canReadAlerts: boolean; - canSaveAlerts: boolean; -} - -export function AlertIntegrations(props: Props) { - const { canSaveAlerts, canReadAlerts } = props; - - const plugin = useApmPluginContext(); - - const [popoverOpen, setPopoverOpen] = useState(false); - - const [alertType, setAlertType] = useState(null); - - const button = ( - setPopoverOpen(true)} - > - {i18n.translate('xpack.apm.serviceDetails.alertsMenu.alerts', { - defaultMessage: 'Alerts' - })} - - ); - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: alertLabel, - items: [ - ...(canSaveAlerts - ? [ - { - name: createThresholdAlertLabel, - panel: CREATE_THRESHOLD_ALERT_PANEL_ID, - icon: 'bell' - } - ] - : []), - ...(canReadAlerts - ? [ - { - name: i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts', - { - defaultMessage: 'View active alerts' - } - ), - href: plugin.core.http.basePath.prepend( - '/app/kibana#/management/kibana/triggersActions/alerts' - ), - icon: 'tableOfContents' - } - ] - : []) - ] - }, - { - id: CREATE_THRESHOLD_ALERT_PANEL_ID, - title: createThresholdAlertLabel, - items: [ - { - name: i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', - { - defaultMessage: 'Transaction duration' - } - ), - onClick: () => { - setAlertType(AlertType.TransactionDuration); - } - }, - { - name: i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.errorRate', - { - defaultMessage: 'Error rate' - } - ), - onClick: () => { - setAlertType(AlertType.ErrorRate); - } - } - ] - } - ]; - - return ( - <> - setPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downRight" - > - - - { - if (!visible) { - setAlertType(null); - } - }} - /> - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx deleted file mode 100644 index cc5c62e25b491..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ /dev/null @@ -1,159 +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 { i18n } from '@kbn/i18n'; -import React, { Component } from 'react'; -import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; -import { startMLJob } from '../../../../../services/rest/ml'; -import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; -import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { MachineLearningFlyoutView } from './view'; -import { ApmPluginContext } from '../../../../../context/ApmPluginContext'; - -interface Props { - isOpen: boolean; - onClose: () => void; - urlParams: IUrlParams; -} - -interface State { - isCreatingJob: boolean; -} - -export class MachineLearningFlyout extends Component { - static contextType = ApmPluginContext; - - public state: State = { - isCreatingJob: false - }; - - public onClickCreate = async ({ - transactionType - }: { - transactionType: string; - }) => { - this.setState({ isCreatingJob: true }); - try { - const { http } = this.context.core; - const { serviceName } = this.props.urlParams; - if (!serviceName) { - throw new Error('Service name is required to create this ML job'); - } - const res = await startMLJob({ http, serviceName, transactionType }); - const didSucceed = res.datafeeds[0].success && res.jobs[0].success; - if (!didSucceed) { - throw new Error('Creating ML job failed'); - } - this.addSuccessToast({ transactionType }); - } catch (e) { - this.addErrorToast(); - } - - this.setState({ isCreatingJob: false }); - this.props.onClose(); - }; - - public addErrorToast = () => { - const { core } = this.context; - - const { urlParams } = this.props; - const { serviceName } = urlParams; - - if (!serviceName) { - return; - } - - core.notifications.toasts.addWarning({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle', - { - defaultMessage: 'Job creation failed' - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText', - { - defaultMessage: - 'Your current license may not allow for creating machine learning jobs, or this job may already exist.' - } - )} -

- ) - }); - }; - - public addSuccessToast = ({ - transactionType - }: { - transactionType: string; - }) => { - const { core } = this.context; - const { urlParams } = this.props; - const { serviceName } = urlParams; - - if (!serviceName) { - return; - } - - core.notifications.toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle', - { - defaultMessage: 'Job successfully created' - } - ), - text: toMountPoint( -

- {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText', - { - defaultMessage: - 'The analysis is now running for {serviceName} ({transactionType}). It might take a while before results are added to the response times graph.', - values: { - serviceName, - transactionType - } - } - )}{' '} - - - {i18n.translate( - 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText', - { - defaultMessage: 'View job' - } - )} - - -

- ) - }); - }; - - public render() { - const { isOpen, onClose, urlParams } = this.props; - const { serviceName } = urlParams; - const { isCreatingJob } = this.state; - - if (!isOpen || !serviceName) { - return null; - } - - return ( - - ); - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx deleted file mode 100644 index 31c227d8bbcab..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ /dev/null @@ -1,212 +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 { EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { storiesOf } from '@storybook/react'; -import cytoscape from 'cytoscape'; -import React from 'react'; -import { Cytoscape } from './Cytoscape'; -import serviceMapResponse from './cytoscape-layout-test-response.json'; -import { iconForNode } from './icons'; - -const elementsFromResponses = serviceMapResponse.elements; - -storiesOf('app/ServiceMap/Cytoscape', module).add( - 'example', - () => { - const elements: cytoscape.ElementDefinition[] = [ - { - data: { - id: 'opbeans-python', - 'service.name': 'opbeans-python', - 'agent.name': 'python' - } - }, - { - data: { - id: 'opbeans-node', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs' - } - }, - { - data: { - id: 'opbeans-ruby', - 'service.name': 'opbeans-ruby', - 'agent.name': 'ruby' - } - }, - { data: { source: 'opbeans-python', target: 'opbeans-node' } }, - { - data: { - bidirectional: true, - source: 'opbeans-python', - target: 'opbeans-ruby' - } - } - ]; - const height = 300; - const width = 1340; - const serviceName = 'opbeans-python'; - return ( - - ); - }, - { - info: { - propTables: false, - source: false - } - } -); - -storiesOf('app/ServiceMap/Cytoscape', module) - .add( - 'node icons', - () => { - const cy = cytoscape(); - const elements = [ - { data: { id: 'default' } }, - { data: { id: 'cache', 'span.type': 'cache' } }, - { data: { id: 'database', 'span.type': 'db' } }, - { - data: { - id: 'elasticsearch', - 'span.type': 'db', - 'span.subtype': 'elasticsearch' - } - }, - { data: { id: 'external', 'span.type': 'external' } }, - { data: { id: 'ext', 'span.type': 'ext' } }, - { data: { id: 'messaging', 'span.type': 'messaging' } }, - { - data: { - id: 'dotnet', - 'service.name': 'dotnet service', - 'agent.name': 'dotnet' - } - }, - { - data: { - id: 'go', - 'service.name': 'go service', - 'agent.name': 'go' - } - }, - { - data: { - id: 'java', - 'service.name': 'java service', - 'agent.name': 'java' - } - }, - { - data: { - id: 'RUM (js-base)', - 'service.name': 'RUM service', - 'agent.name': 'js-base' - } - }, - { - data: { - id: 'RUM (rum-js)', - 'service.name': 'RUM service', - 'agent.name': 'rum-js' - } - }, - { - data: { - id: 'nodejs', - 'service.name': 'nodejs service', - 'agent.name': 'nodejs' - } - }, - { - data: { - id: 'php', - 'service.name': 'php service', - 'agent.name': 'php' - } - }, - { - data: { - id: 'python', - 'service.name': 'python service', - 'agent.name': 'python' - } - }, - { - data: { - id: 'ruby', - 'service.name': 'ruby service', - 'agent.name': 'ruby' - } - } - ]; - cy.add(elements); - - return ( - - {cy.nodes().map(node => ( - - - agent.name: {node.data('agent.name') || 'undefined'}, - span.type: {node.data('span.type') || 'undefined'}, - span.subtype: {node.data('span.subtype') || 'undefined'} - - } - icon={ - {node.data('label')} - } - title={node.data('id')} - /> - - ))} - - ); - }, - { - info: { - propTables: false, - source: false - } - } - ) - .add( - 'layout', - () => { - const height = 640; - const width = 1340; - const serviceName = undefined; // global service map - - return ( - - ); - }, - { - info: { - source: false - } - } - ) - .addParameters({ options: { showPanel: false } }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx deleted file mode 100644 index 102b135f3cd1f..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx +++ /dev/null @@ -1,119 +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 { EuiPopover } from '@elastic/eui'; -import cytoscape from 'cytoscape'; -import React, { - CSSProperties, - useCallback, - useContext, - useEffect, - useRef, - useState -} from 'react'; -import { SERVICE_NAME } from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { CytoscapeContext } from '../Cytoscape'; -import { Contents } from './Contents'; -import { animationOptions } from '../cytoscapeOptions'; - -interface PopoverProps { - focusedServiceName?: string; -} - -export function Popover({ focusedServiceName }: PopoverProps) { - const cy = useContext(CytoscapeContext); - const [selectedNode, setSelectedNode] = useState< - cytoscape.NodeSingular | undefined - >(undefined); - const deselect = useCallback(() => { - if (cy) { - cy.elements().unselect(); - } - setSelectedNode(undefined); - }, [cy, setSelectedNode]); - const renderedHeight = selectedNode?.renderedHeight() ?? 0; - const renderedWidth = selectedNode?.renderedWidth() ?? 0; - const { x, y } = selectedNode?.renderedPosition() ?? { x: -10000, y: -10000 }; - const isOpen = !!selectedNode; - const isService = selectedNode?.data(SERVICE_NAME) !== undefined; - const triggerStyle: CSSProperties = { - background: 'transparent', - height: renderedHeight, - position: 'absolute', - width: renderedWidth - }; - const trigger =
; - const zoom = cy?.zoom() ?? 1; - const height = selectedNode?.height() ?? 0; - const translateY = y - ((zoom + 1) * height) / 4; - const popoverStyle: CSSProperties = { - position: 'absolute', - transform: `translate(${x}px, ${translateY}px)` - }; - const selectedNodeData = selectedNode?.data() ?? {}; - const selectedNodeServiceName = selectedNodeData.id; - const label = selectedNodeData.label || selectedNodeServiceName; - const popoverRef = useRef(null); - - // Set up Cytoscape event handlers - useEffect(() => { - const selectHandler: cytoscape.EventHandler = event => { - setSelectedNode(event.target); - }; - - if (cy) { - cy.on('select', 'node', selectHandler); - cy.on('unselect', 'node', deselect); - cy.on('data viewport', deselect); - } - - return () => { - if (cy) { - cy.removeListener('select', 'node', selectHandler); - cy.removeListener('unselect', 'node', deselect); - cy.removeListener('data viewport', undefined, deselect); - } - }; - }, [cy, deselect]); - - // Handle positioning of popover. This makes it so the popover positions - // itself correctly and the arrows are always pointing to where they should. - useEffect(() => { - if (popoverRef.current) { - popoverRef.current.positionPopoverFluid(); - } - }, [popoverRef, x, y]); - - const centerSelectedNode = useCallback(() => { - if (cy) { - cy.animate({ - ...animationOptions, - center: { eles: cy.getElementById(selectedNodeServiceName) } - }); - } - }, [cy, selectedNodeServiceName]); - - const isAlreadyFocused = focusedServiceName === selectedNodeServiceName; - - return ( - {}} - isOpen={isOpen} - ref={popoverRef} - style={popoverStyle} - > - - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts deleted file mode 100644 index 095c2d9250e27..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ /dev/null @@ -1,85 +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 cytoscape from 'cytoscape'; -import { isRumAgentName } from '../../../../../../../plugins/apm/common/agent_name'; -import { - AGENT_NAME, - SERVICE_NAME, - SPAN_SUBTYPE, - SPAN_TYPE -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import databaseIcon from './icons/database.svg'; -import defaultIconImport from './icons/default.svg'; -import documentsIcon from './icons/documents.svg'; -import dotNetIcon from './icons/dot-net.svg'; -import elasticsearchIcon from './icons/elasticsearch.svg'; -import globeIcon from './icons/globe.svg'; -import goIcon from './icons/go.svg'; -import javaIcon from './icons/java.svg'; -import nodeJsIcon from './icons/nodejs.svg'; -import phpIcon from './icons/php.svg'; -import pythonIcon from './icons/python.svg'; -import rubyIcon from './icons/ruby.svg'; -import rumJsIcon from './icons/rumjs.svg'; - -export const defaultIcon = defaultIconImport; - -// The colors here are taken from the logos of the corresponding technologies -const icons: { [key: string]: string } = { - cache: databaseIcon, - db: databaseIcon, - ext: globeIcon, - external: globeIcon, - messaging: documentsIcon, - resource: globeIcon -}; - -const serviceIcons: { [key: string]: string } = { - dotnet: dotNetIcon, - go: goIcon, - java: javaIcon, - 'js-base': rumJsIcon, - nodejs: nodeJsIcon, - php: phpIcon, - python: pythonIcon, - ruby: rubyIcon -}; - -// IE 11 does not properly load some SVGs, which causes a runtime error and the -// map to not work at all. We would prefer to do some kind of feature detection -// rather than browser detection, but IE 11 does support SVG, just not well -// enough for our use in loading icons. -// -// This method of detecting IE is from a Stack Overflow answer: -// https://stackoverflow.com/a/21825207 -// -// @ts-ignore `documentMode` is not recognized as a valid property of `document`. -const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; - -export function iconForNode(node: cytoscape.NodeSingular) { - const type = node.data(SPAN_TYPE); - - if (node.data(SERVICE_NAME)) { - const agentName = node.data(AGENT_NAME); - // RUM can have multiple names. Normalize it - const normalizedAgentName = isRumAgentName(agentName) - ? 'js-base' - : agentName; - return serviceIcons[normalizedAgentName]; - } else if (isIE11) { - return defaultIcon; - } else if ( - node.data(SPAN_TYPE) === 'db' && - node.data(SPAN_SUBTYPE) === 'elasticsearch' - ) { - return elasticsearchIcon; - } else if (icons[type]) { - return icons[type]; - } else { - return defaultIcon; - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx deleted file mode 100644 index d93caa601f0b6..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ /dev/null @@ -1,45 +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 { render } from '@testing-library/react'; -import React, { FunctionComponent } from 'react'; -import { License } from '../../../../../../../plugins/licensing/common/license'; -import { LicenseContext } from '../../../context/LicenseContext'; -import { ServiceMap } from './'; -import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; - -const expiredLicense = new License({ - signature: 'test signature', - license: { - expiryDateInMillis: 0, - mode: 'platinum', - status: 'expired', - type: 'platinum', - uid: '1' - } -}); - -const Wrapper: FunctionComponent = ({ children }) => { - return ( - - {children} - - ); -}; - -describe('ServiceMap', () => { - describe('with an inactive license', () => { - it('renders the license banner', async () => { - expect( - ( - await render(, { - wrapper: Wrapper - }).findAllByText(/Platinum/) - ).length - ).toBeGreaterThan(0); - }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx deleted file mode 100644 index 94e42f1b91160..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ /dev/null @@ -1,101 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import React from 'react'; -import { - invalidLicenseMessage, - isValidPlatinumLicense -} from '../../../../../../../plugins/apm/common/service_map'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useLicense } from '../../../hooks/useLicense'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { callApmApi } from '../../../services/rest/createCallApmApi'; -import { LicensePrompt } from '../../shared/LicensePrompt'; -import { Controls } from './Controls'; -import { Cytoscape } from './Cytoscape'; -import { cytoscapeDivStyle } from './cytoscapeOptions'; -import { EmptyBanner } from './EmptyBanner'; -import { Popover } from './Popover'; -import { useRefDimensions } from './useRefDimensions'; -import { BetaBadge } from './BetaBadge'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; - -interface ServiceMapProps { - serviceName?: string; -} - -export function ServiceMap({ serviceName }: ServiceMapProps) { - const license = useLicense(); - const { urlParams } = useUrlParams(); - - const { data = { elements: [] } } = useFetcher(() => { - // When we don't have a license or a valid license, don't make the request. - if (!license || !isValidPlatinumLicense(license)) { - return; - } - - const { start, end, environment } = urlParams; - if (start && end) { - return callApmApi({ - isCachable: false, - pathname: '/api/apm/service-map', - params: { - query: { - start, - end, - environment, - serviceName - } - } - }); - } - }, [license, serviceName, urlParams]); - - const { ref, height, width } = useRefDimensions(); - - useTrackPageview({ app: 'apm', path: 'service_map' }); - useTrackPageview({ app: 'apm', path: 'service_map', delay: 15000 }); - - if (!license) { - return null; - } - - return isValidPlatinumLicense(license) ? ( -
- - - - {serviceName && } - - -
- ) : ( - - - - - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx deleted file mode 100644 index 060e635e83549..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMetrics/index.tsx +++ /dev/null @@ -1,69 +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 { - EuiFlexGrid, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiFlexGroup -} from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; -import { MetricsChart } from '../../shared/charts/MetricsChart'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; - -interface ServiceMetricsProps { - agentName: string; -} - -export function ServiceMetrics({ agentName }: ServiceMetricsProps) { - const { urlParams } = useUrlParams(); - const { serviceName, serviceNodeName } = urlParams; - const { data } = useServiceMetricCharts(urlParams, agentName); - const { start, end } = urlParams; - - const localFiltersConfig: React.ComponentProps = useMemo( - () => ({ - filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], - params: { - serviceName, - serviceNodeName - }, - projection: PROJECTION.METRICS, - showCount: false - }), - [serviceName, serviceNodeName] - ); - - return ( - <> - - - - - - - - - {data.charts.map(chart => ( - - - - - - ))} - - - - - - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx deleted file mode 100644 index 2bf26946932ea..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx +++ /dev/null @@ -1,186 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiHorizontalRule, - EuiFlexGrid, - EuiPanel, - EuiSpacer, - EuiStat, - EuiToolTip, - EuiCallOut -} from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../../../../plugins/apm/common/service_nodes'; -import { ApmHeader } from '../../shared/ApmHeader'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useAgentName } from '../../../hooks/useAgentName'; -import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; -import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { MetricsChart } from '../../shared/charts/MetricsChart'; -import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher'; -import { truncate, px, unit } from '../../../style/variables'; -import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; - -const INITIAL_DATA = { - host: '', - containerId: '' -}; - -const Truncate = styled.span` - display: block; - ${truncate(px(unit * 12))} -`; - -export function ServiceNodeMetrics() { - const { urlParams, uiFilters } = useUrlParams(); - const { serviceName, serviceNodeName } = urlParams; - - const { agentName } = useAgentName(); - const { data } = useServiceMetricCharts(urlParams, agentName); - const { start, end } = urlParams; - - const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher( - callApmApi => { - if (serviceName && serviceNodeName && start && end) { - return callApmApi({ - pathname: - '/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', - params: { - path: { serviceName, serviceNodeName }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - } - }, - [serviceName, serviceNodeName, start, end, uiFilters] - ); - - const isLoading = status === FETCH_STATUS.LOADING; - - const isAggregatedData = serviceNodeName === SERVICE_NODE_NAME_MISSING; - - return ( -
- - - - -

{serviceName}

-
-
-
-
- - {isAggregatedData ? ( - - - {i18n.translate( - 'xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningDocumentationLink', - { defaultMessage: 'documentation of APM Server' } - )} - - ) - }} - /> - - ) : ( - - - - {serviceName} - - } - /> - - - - {host} - - } - /> - - - - {containerId} - - } - /> - - - )} - - {agentName && serviceNodeName && ( - - - {data.charts.map(chart => ( - - - - - - ))} - - - - )} -
- ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx deleted file mode 100644 index 3af1a70ef3fdc..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx +++ /dev/null @@ -1,187 +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 React, { useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiToolTip, - EuiSpacer -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../../../plugins/apm/common/i18n'; -import { SERVICE_NODE_NAME_MISSING } from '../../../../../../../plugins/apm/common/service_nodes'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { - asDynamicBytes, - asInteger, - asPercent -} from '../../../utils/formatters'; -import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; -import { truncate, px, unit } from '../../../style/variables'; - -const INITIAL_PAGE_SIZE = 25; -const INITIAL_SORT_FIELD = 'cpu'; -const INITIAL_SORT_DIRECTION = 'desc'; - -const ServiceNodeName = styled.div` - ${truncate(px(8 * unit))} -`; - -const ServiceNodeOverview = () => { - const { uiFilters, urlParams } = useUrlParams(); - const { serviceName, start, end } = urlParams; - - const localFiltersConfig: React.ComponentProps = useMemo( - () => ({ - filterNames: ['host', 'containerId', 'podName'], - params: { - serviceName - }, - projection: PROJECTION.SERVICE_NODES - }), - [serviceName] - ); - - const { data: items = [] } = useFetcher( - callApmApi => { - if (!serviceName || !start || !end) { - return undefined; - } - return callApmApi({ - pathname: '/api/apm/services/{serviceName}/serviceNodes', - params: { - path: { - serviceName - }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - }, - [serviceName, start, end, uiFilters] - ); - - if (!serviceName) { - return null; - } - - const columns: Array> = [ - { - name: ( - - <> - {i18n.translate('xpack.apm.jvmsTable.nameColumnLabel', { - defaultMessage: 'Name' - })} - - - ), - field: 'name', - sortable: true, - render: (name: string) => { - const { displayedName, tooltip } = - name === SERVICE_NODE_NAME_MISSING - ? { - displayedName: UNIDENTIFIED_SERVICE_NODES_LABEL, - tooltip: i18n.translate( - 'xpack.apm.jvmsTable.explainServiceNodeNameMissing', - { - defaultMessage: - 'We could not identify which JVMs these metrics belong to. This is likely caused by running a version of APM Server that is older than 7.5. Upgrading to APM Server 7.5 or higher should resolve this issue.' - } - ) - } - : { displayedName: name, tooltip: name }; - - return ( - - - {displayedName} - - - ); - } - }, - { - name: i18n.translate('xpack.apm.jvmsTable.cpuColumnLabel', { - defaultMessage: 'CPU avg' - }), - field: 'cpu', - sortable: true, - render: (value: number | null) => asPercent(value || 0, 1) - }, - { - name: i18n.translate('xpack.apm.jvmsTable.heapMemoryColumnLabel', { - defaultMessage: 'Heap memory avg' - }), - field: 'heapMemory', - sortable: true, - render: asDynamicBytes - }, - { - name: i18n.translate('xpack.apm.jvmsTable.nonHeapMemoryColumnLabel', { - defaultMessage: 'Non-heap memory avg' - }), - field: 'nonHeapMemory', - sortable: true, - render: asDynamicBytes - }, - { - name: i18n.translate('xpack.apm.jvmsTable.threadCountColumnLabel', { - defaultMessage: 'Thread count max' - }), - field: 'threadCount', - sortable: true, - render: asInteger - } - ]; - - return ( - <> - - - - - - - - - - - - - ); -}; - -export { ServiceNodeOverview }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx deleted file mode 100644 index 1ac29c5626e3a..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ /dev/null @@ -1,135 +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 { EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceListAPIResponse } from '../../../../../../../../plugins/apm/server/lib/services/get_services'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -import { fontSizes, truncate } from '../../../../style/variables'; -import { asDecimal, convertTo } from '../../../../utils/formatters'; -import { ManagedTable } from '../../../shared/ManagedTable'; -import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; -import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; - -interface Props { - items: ServiceListAPIResponse['items']; - noItemsMessage?: React.ReactNode; -} - -function formatNumber(value: number) { - if (value === 0) { - return '0'; - } else if (value <= 0.1) { - return '< 0.1'; - } else { - return asDecimal(value); - } -} - -function formatString(value?: string | null) { - return value || NOT_AVAILABLE_LABEL; -} - -const AppLink = styled(TransactionOverviewLink)` - font-size: ${fontSizes.large}; - ${truncate('100%')}; -`; - -export const SERVICE_COLUMNS = [ - { - field: 'serviceName', - name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { - defaultMessage: 'Name' - }), - width: '40%', - sortable: true, - render: (serviceName: string) => ( - - {formatString(serviceName)} - - ) - }, - { - field: 'environments', - name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { - defaultMessage: 'Environment' - }), - width: '20%', - sortable: true, - render: (environments: string[]) => ( - - ) - }, - { - field: 'agentName', - name: i18n.translate('xpack.apm.servicesTable.agentColumnLabel', { - defaultMessage: 'Agent' - }), - sortable: true, - render: (agentName: string) => formatString(agentName) - }, - { - field: 'avgResponseTime', - name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', { - defaultMessage: 'Avg. response time' - }), - sortable: true, - dataType: 'number', - render: (time: number) => - convertTo({ - unit: 'milliseconds', - microseconds: time - }).formatted - }, - { - field: 'transactionsPerMinute', - name: i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel', - { - defaultMessage: 'Trans. per minute' - } - ), - sortable: true, - dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteUnitLabel', - { - defaultMessage: 'tpm' - } - )}` - }, - { - field: 'errorsPerMinute', - name: i18n.translate('xpack.apm.servicesTable.errorsPerMinuteColumnLabel', { - defaultMessage: 'Errors per minute' - }), - sortable: true, - dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.errorsPerMinuteUnitLabel', - { - defaultMessage: 'err.' - } - )}` - } -]; - -export function ServiceList({ items, noItemsMessage }: Props) { - return ( - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx deleted file mode 100644 index 52bc414a93a23..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ /dev/null @@ -1,119 +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 { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { EuiLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useEffect, useMemo } from 'react'; -import url from 'url'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { NoServicesMessage } from './NoServicesMessage'; -import { ServiceList } from './ServiceList'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; - -const initalData = { - items: [], - hasHistoricalData: true, - hasLegacyData: false -}; - -let hasDisplayedToast = false; - -export function ServiceOverview() { - const { core } = useApmPluginContext(); - const { - urlParams: { start, end }, - uiFilters - } = useUrlParams(); - const { data = initalData, status } = useFetcher( - callApmApi => { - if (start && end) { - return callApmApi({ - pathname: '/api/apm/services', - params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) } - } - }); - } - }, - [start, end, uiFilters] - ); - - useEffect(() => { - if (data.hasLegacyData && !hasDisplayedToast) { - hasDisplayedToast = true; - - core.notifications.toasts.addWarning({ - title: i18n.translate('xpack.apm.serviceOverview.toastTitle', { - defaultMessage: - 'Legacy data was detected within the selected time range' - }), - text: toMountPoint( -

- {i18n.translate('xpack.apm.serviceOverview.toastText', { - defaultMessage: - "You're running Elastic Stack 7.0+ and we've detected incompatible data from a previous 6.x version. If you want to view this data in APM, you should migrate it. See more in " - })} - - - {i18n.translate( - 'xpack.apm.serviceOverview.upgradeAssistantLink', - { - defaultMessage: 'the upgrade assistant' - } - )} - -

- ) - }); - } - }, [data.hasLegacyData, core.http.basePath, core.notifications.toasts]); - - useTrackPageview({ app: 'apm', path: 'services_overview' }); - useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); - - const localFiltersConfig: React.ComponentProps = useMemo( - () => ({ - filterNames: ['host', 'agentName'], - projection: PROJECTION.SERVICES - }), - [] - ); - - return ( - <> - - - - - - - - - } - /> - - - - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx deleted file mode 100644 index 531e557b6ef86..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * 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 { storiesOf } from '@storybook/react'; -import React from 'react'; -import { HttpSetup } from 'kibana/public'; -import { AgentConfiguration } from '../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; -import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; -import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; -import { AgentConfigurationCreateEdit } from './index'; -import { - ApmPluginContext, - ApmPluginContextValue -} from '../../../../../context/ApmPluginContext'; - -storiesOf( - 'app/Settings/AgentConfigurations/AgentConfigurationCreateEdit', - module -).add( - 'with config', - () => { - const httpMock = {}; - - // mock - createCallApmApi((httpMock as unknown) as HttpSetup); - - const contextMock = { - core: { - notifications: { toasts: { addWarning: () => {}, addDanger: () => {} } } - } - }; - return ( - - - - ); - }, - { - info: { - source: false - } - } -); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx deleted file mode 100644 index 638e518563f8c..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx +++ /dev/null @@ -1,157 +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 { isEmpty } from 'lodash'; -import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; -import React, { useState, useEffect, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FetcherResult } from '../../../../../hooks/useFetcher'; -import { history } from '../../../../../utils/history'; -import { - AgentConfigurationIntake, - AgentConfiguration -} from '../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; -import { ServicePage } from './ServicePage/ServicePage'; -import { SettingsPage } from './SettingsPage/SettingsPage'; -import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; - -type PageStep = 'choose-service-step' | 'choose-settings-step' | 'review-step'; - -function getInitialNewConfig( - existingConfig: AgentConfigurationIntake | undefined -) { - return { - agent_name: existingConfig?.agent_name, - service: existingConfig?.service || {}, - settings: existingConfig?.settings || {} - }; -} - -function setPage(pageStep: PageStep) { - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - pageStep - }) - }); -} - -function getUnsavedChanges({ - newConfig, - existingConfig -}: { - newConfig: AgentConfigurationIntake; - existingConfig?: AgentConfigurationIntake; -}) { - return Object.fromEntries( - Object.entries(newConfig.settings).filter(([key, value]) => { - const existingValue = existingConfig?.settings?.[key]; - - // don't highlight changes that were added and removed - if (value === '' && existingValue == null) { - return false; - } - - return existingValue !== value; - }) - ); -} - -export function AgentConfigurationCreateEdit({ - pageStep, - existingConfigResult -}: { - pageStep: PageStep; - existingConfigResult?: FetcherResult; -}) { - const existingConfig = existingConfigResult?.data; - const isEditMode = Boolean(existingConfigResult); - const [newConfig, setNewConfig] = useState( - getInitialNewConfig(existingConfig) - ); - - const resetSettings = useCallback(() => { - setNewConfig(_newConfig => ({ - ..._newConfig, - settings: existingConfig?.settings || {} - })); - }, [existingConfig]); - - // update newConfig when existingConfig has loaded - useEffect(() => { - setNewConfig(getInitialNewConfig(existingConfig)); - }, [existingConfig]); - - useEffect(() => { - // the user tried to edit the service of an existing config - if (pageStep === 'choose-service-step' && isEditMode) { - setPage('choose-settings-step'); - } - - // the user skipped the first step (select service) - if ( - pageStep === 'choose-settings-step' && - !isEditMode && - isEmpty(newConfig.service) - ) { - setPage('choose-service-step'); - } - }, [isEditMode, newConfig, pageStep]); - - const unsavedChanges = getUnsavedChanges({ newConfig, existingConfig }); - - return ( - <> - -

- {isEditMode - ? i18n.translate('xpack.apm.agentConfig.editConfigTitle', { - defaultMessage: 'Edit configuration' - }) - : i18n.translate('xpack.apm.agentConfig.createConfigTitle', { - defaultMessage: 'Create configuration' - })} -

-
- - - {i18n.translate('xpack.apm.agentConfig.newConfig.description', { - defaultMessage: `This allows you to fine-tune your agent configuration directly in - Kibana. Best of all, changes are automatically propagated to your APM - agents so there’s no need to redeploy.` - })} - - - - - {pageStep === 'choose-service-step' && ( - setPage('choose-settings-step')} - /> - )} - - {pageStep === 'choose-settings-step' && ( - setPage('choose-service-step')} - newConfig={newConfig} - setNewConfig={setNewConfig} - resetSettings={resetSettings} - isEditMode={isEditMode} - /> - )} - - {/* - TODO: Add review step - {pageStep === 'review-step' &&
Review will be here
} - */} - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx deleted file mode 100644 index 6d5f65121d8fd..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ /dev/null @@ -1,221 +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 React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiEmptyPrompt, - EuiButton, - EuiButtonEmpty, - EuiHealth, - EuiToolTip, - EuiButtonIcon -} from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; -import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; -import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/settings/agent_configuration/list_configurations'; -import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; -import { px, units } from '../../../../../style/variables'; -import { getOptionLabel } from '../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; -import { - createAgentConfigurationHref, - editAgentConfigurationHref -} from '../../../../shared/Links/apm/agentConfigurationLinks'; -import { ConfirmDeleteModal } from './ConfirmDeleteModal'; - -type Config = AgentConfigurationListAPIResponse[0]; - -export function AgentConfigurationList({ - status, - data, - refetch -}: { - status: FETCH_STATUS; - data: Config[]; - refetch: () => void; -}) { - const [configToBeDeleted, setConfigToBeDeleted] = useState( - null - ); - - const emptyStatePrompt = ( - - {i18n.translate( - 'xpack.apm.agentConfig.configTable.emptyPromptTitle', - { defaultMessage: 'No configurations found.' } - )} - - } - body={ - <> -

- {i18n.translate( - 'xpack.apm.agentConfig.configTable.emptyPromptText', - { - defaultMessage: - "Let's change that! You can fine-tune agent configuration directly from Kibana without having to redeploy. Get started by creating your first configuration." - } - )} -

- - } - actions={ - - {i18n.translate( - 'xpack.apm.agentConfig.configTable.createConfigButtonLabel', - { defaultMessage: 'Create configuration' } - )} - - } - /> - ); - - const failurePrompt = ( - -

- {i18n.translate( - 'xpack.apm.agentConfig.configTable.configTable.failurePromptText', - { - defaultMessage: - 'The list of agent configurations could not be fetched. Your user may not have the sufficient permissions.' - } - )} -

- - } - /> - ); - - if (status === FETCH_STATUS.FAILURE) { - return failurePrompt; - } - - if (status === FETCH_STATUS.SUCCESS && isEmpty(data)) { - return emptyStatePrompt; - } - - const columns: Array> = [ - { - field: 'applied_by_agent', - align: 'center', - width: px(units.double), - name: '', - sortable: true, - render: (isApplied: boolean) => ( - - - - ) - }, - { - field: 'service.name', - name: i18n.translate( - 'xpack.apm.agentConfig.configTable.serviceNameColumnLabel', - { defaultMessage: 'Service name' } - ), - sortable: true, - render: (_, config: Config) => ( - - {getOptionLabel(config.service.name)} - - ) - }, - { - field: 'service.environment', - name: i18n.translate( - 'xpack.apm.agentConfig.configTable.environmentColumnLabel', - { defaultMessage: 'Service environment' } - ), - sortable: true, - render: (environment: string) => getOptionLabel(environment) - }, - { - align: 'right', - field: '@timestamp', - name: i18n.translate( - 'xpack.apm.agentConfig.configTable.lastUpdatedColumnLabel', - { defaultMessage: 'Last updated' } - ), - sortable: true, - render: (value: number) => ( - - ) - }, - { - width: px(units.double), - name: '', - render: (config: Config) => ( - - ) - }, - { - width: px(units.double), - name: '', - render: (config: Config) => ( - setConfigToBeDeleted(config)} - /> - ) - } - ]; - - return ( - <> - {configToBeDeleted && ( - setConfigToBeDeleted(null)} - onConfirm={() => { - setConfigToBeDeleted(null); - refetch(); - }} - /> - )} - - } - columns={columns} - items={data} - initialSortField="service.name" - initialSortDirection="asc" - initialPageSize={20} - /> - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx deleted file mode 100644 index 8171e339adc82..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx +++ /dev/null @@ -1,77 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiButton -} from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import { useFetcher } from '../../../../hooks/useFetcher'; -import { AgentConfigurationList } from './List'; -import { useTrackPageview } from '../../../../../../../../plugins/observability/public'; -import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; - -export function AgentConfigurations() { - const { refetch, data = [], status } = useFetcher( - callApmApi => - callApmApi({ pathname: '/api/apm/settings/agent-configuration' }), - [], - { preservePreviousData: false } - ); - - useTrackPageview({ app: 'apm', path: 'agent_configuration' }); - useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); - - const hasConfigurations = !isEmpty(data); - - return ( - <> - - - - -

- {i18n.translate( - 'xpack.apm.agentConfig.configurationsPanelTitle', - { defaultMessage: 'Agent remote configuration' } - )} -

-
-
- - {hasConfigurations ? : null} -
- - - - -
- - ); -} - -function CreateConfigurationButton() { - const href = createAgentConfigurationHref(); - return ( - - - - - {i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', { - defaultMessage: 'Create configuration' - })} - - - - - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx deleted file mode 100644 index 272c4b3add415..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx +++ /dev/null @@ -1,35 +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 { render, wait } from '@testing-library/react'; -import React from 'react'; -import { ApmIndices } from '.'; -import * as hooks from '../../../../hooks/useFetcher'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; - -describe('ApmIndices', () => { - it('should not get stuck in infinite loop', async () => { - spyOn(hooks, 'useFetcher').and.returnValue({ - data: undefined, - status: 'loading' - }); - const { getByText } = render( - - - - ); - - expect(getByText('Indices')).toMatchInlineSnapshot(` -

- Indices -

- `); - - await wait(); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts deleted file mode 100644 index 0a63cfcff9aa5..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts +++ /dev/null @@ -1,147 +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 { - getSelectOptions, - replaceTemplateVariables -} from '../CustomLinkFlyout/helper'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; - -describe('Custom link helper', () => { - describe('getSelectOptions', () => { - it('returns all available options when no filters were selected', () => { - expect( - getSelectOptions( - [ - { key: '', value: '' }, - { key: '', value: '' }, - { key: '', value: '' }, - { key: '', value: '' } - ], - '' - ) - ).toEqual([ - { value: 'DEFAULT', text: 'Select field...' }, - { value: 'service.name', text: 'service.name' }, - { value: 'service.environment', text: 'service.environment' }, - { value: 'transaction.type', text: 'transaction.type' }, - { value: 'transaction.name', text: 'transaction.name' } - ]); - }); - it('removes item added in another filter', () => { - expect( - getSelectOptions( - [ - { key: 'service.name', value: 'foo' }, - { key: '', value: '' }, - { key: '', value: '' }, - { key: '', value: '' } - ], - '' - ) - ).toEqual([ - { value: 'DEFAULT', text: 'Select field...' }, - { value: 'service.environment', text: 'service.environment' }, - { value: 'transaction.type', text: 'transaction.type' }, - { value: 'transaction.name', text: 'transaction.name' } - ]); - }); - it('removes item added in another filter but keep the current selected', () => { - expect( - getSelectOptions( - [ - { key: 'service.name', value: 'foo' }, - { key: 'transaction.name', value: 'bar' }, - { key: '', value: '' }, - { key: '', value: '' } - ], - 'transaction.name' - ) - ).toEqual([ - { value: 'DEFAULT', text: 'Select field...' }, - { value: 'service.environment', text: 'service.environment' }, - { value: 'transaction.type', text: 'transaction.type' }, - { value: 'transaction.name', text: 'transaction.name' } - ]); - }); - it('returns empty when all option were selected', () => { - expect( - getSelectOptions( - [ - { key: 'service.name', value: 'foo' }, - { key: 'transaction.name', value: 'bar' }, - { key: 'service.environment', value: 'baz' }, - { key: 'transaction.type', value: 'qux' } - ], - '' - ) - ).toEqual([{ value: 'DEFAULT', text: 'Select field...' }]); - }); - }); - - describe('replaceTemplateVariables', () => { - const transaction = ({ - service: { name: 'foo' }, - trace: { id: '123' } - } as unknown) as Transaction; - - it('replaces template variables', () => { - expect( - replaceTemplateVariables( - 'https://elastic.co?service.name={{service.name}}&trace.id={{trace.id}}', - transaction - ) - ).toEqual({ - error: undefined, - formattedUrl: 'https://elastic.co?service.name=foo&trace.id=123' - }); - }); - - it('returns error when transaction is not defined', () => { - const expectedResult = { - error: - "We couldn't find a matching transaction document based on the defined filters.", - formattedUrl: 'https://elastic.co?service.name=&trace.id=' - }; - expect( - replaceTemplateVariables( - 'https://elastic.co?service.name={{service.name}}&trace.id={{trace.id}}' - ) - ).toEqual(expectedResult); - expect( - replaceTemplateVariables( - 'https://elastic.co?service.name={{service.name}}&trace.id={{trace.id}}', - ({} as unknown) as Transaction - ) - ).toEqual(expectedResult); - }); - - it('returns error when could not replace variables', () => { - expect( - replaceTemplateVariables( - 'https://elastic.co?service.name={{service.nam}}&trace.id={{trace.i}}', - transaction - ) - ).toEqual({ - error: - "We couldn't find a value match for {{service.nam}}, {{trace.i}} in the example transaction document.", - formattedUrl: 'https://elastic.co?service.name=&trace.id=' - }); - }); - - it('returns error when variable is invalid', () => { - expect( - replaceTemplateVariables( - 'https://elastic.co?service.name={{service.name}', - transaction - ) - ).toEqual({ - error: - "We couldn't find an example transaction document due to invalid variable(s) defined.", - formattedUrl: 'https://elastic.co?service.name={{service.name}' - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts deleted file mode 100644 index 7bfdbf1655e0d..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts +++ /dev/null @@ -1,120 +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 { i18n } from '@kbn/i18n'; -import Mustache from 'mustache'; -import { isEmpty, get } from 'lodash'; -import { FILTER_OPTIONS } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_filter_options'; -import { - Filter, - FilterKey -} from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; - -interface FilterSelectOption { - value: 'DEFAULT' | FilterKey; - text: string; -} - -export const DEFAULT_OPTION: FilterSelectOption = { - value: 'DEFAULT', - text: i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyOut.filters.defaultOption', - { defaultMessage: 'Select field...' } - ) -}; - -export const FILTER_SELECT_OPTIONS: FilterSelectOption[] = [ - DEFAULT_OPTION, - ...FILTER_OPTIONS.map(filter => ({ - value: filter, - text: filter - })) -]; - -/** - * Returns the options available, removing filters already added, but keeping the selected filter. - * - * @param filters - * @param selectedKey - */ -export const getSelectOptions = ( - filters: Filter[], - selectedKey: Filter['key'] -) => { - return FILTER_SELECT_OPTIONS.filter( - ({ value }) => - !filters.some(({ key }) => key === value && key !== selectedKey) - ); -}; - -const getInvalidTemplateVariables = ( - template: string, - transaction: Transaction -) => { - return (Mustache.parse(template) as Array<[string, string]>) - .filter(([type]) => type === 'name') - .map(([, value]) => value) - .filter(templateVar => get(transaction, templateVar) == null); -}; - -const validateUrl = (url: string, transaction?: Transaction) => { - if (!transaction || isEmpty(transaction)) { - return i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.preview.transaction.notFound', - { - defaultMessage: - "We couldn't find a matching transaction document based on the defined filters." - } - ); - } - try { - const invalidVariables = getInvalidTemplateVariables(url, transaction); - if (!isEmpty(invalidVariables)) { - return i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.preview.contextVariable.noMatch', - { - defaultMessage: - "We couldn't find a value match for {variables} in the example transaction document.", - values: { - variables: invalidVariables - .map(variable => `{{${variable}}}`) - .join(', ') - } - } - ); - } - } catch (e) { - return i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.preview.contextVariable.invalid', - { - defaultMessage: - "We couldn't find an example transaction document due to invalid variable(s) defined." - } - ); - } -}; - -export const replaceTemplateVariables = ( - url: string, - transaction?: Transaction -) => { - const error = validateUrl(url, transaction); - try { - return { formattedUrl: Mustache.render(url, transaction), error }; - } catch (e) { - // errors will be caught on validateUrl function - return { formattedUrl: url, error }; - } -}; - -export const convertFiltersToQuery = (filters: Filter[]) => { - return filters.reduce((acc: Record, { key, value }) => { - if (key && value) { - acc[key] = value; - } - return acc; - }, {}); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx deleted file mode 100644 index 0b25a0a79edd9..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx +++ /dev/null @@ -1,142 +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 { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiPortal, - EuiSpacer, - EuiText, - EuiTitle -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import { Filter } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; -import { FiltersSection } from './FiltersSection'; -import { FlyoutFooter } from './FlyoutFooter'; -import { LinkSection } from './LinkSection'; -import { saveCustomLink } from './saveCustomLink'; -import { LinkPreview } from './LinkPreview'; -import { Documentation } from './Documentation'; - -interface Props { - onClose: () => void; - onSave: () => void; - onDelete: () => void; - defaults?: { - url?: string; - label?: string; - filters?: Filter[]; - }; - customLinkId?: string; -} - -const filtersEmptyState: Filter[] = [{ key: '', value: '' }]; - -export const CustomLinkFlyout = ({ - onClose, - onSave, - onDelete, - defaults, - customLinkId -}: Props) => { - const { toasts } = useApmPluginContext().core.notifications; - const [isSaving, setIsSaving] = useState(false); - - const [label, setLabel] = useState(defaults?.label || ''); - const [url, setUrl] = useState(defaults?.url || ''); - const [filters, setFilters] = useState( - defaults?.filters?.length ? defaults.filters : filtersEmptyState - ); - - const isFormValid = !!label && !!url; - - const onSubmit = async ( - event: - | React.FormEvent - | React.MouseEvent - ) => { - event.preventDefault(); - setIsSaving(true); - await saveCustomLink({ - id: customLinkId, - label, - url, - filters, - toasts - }); - setIsSaving(false); - onSave(); - }; - - return ( - -
- - - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.title', - { - defaultMessage: 'Create link' - } - )} -

-
-
- - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.label', - { - defaultMessage: - 'Links will be available in the context of transaction details throughout the APM app. You can create an unlimited number of links. You can refer to dynamic variables by using any of the transaction metadata to fill in your URLs. More information, including examples, are available in the' - } - )}{' '} - -

-
- - - - - - - - - - - - -
- - -
-
-
- ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx deleted file mode 100644 index 0c04b7cccbd23..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ /dev/null @@ -1,354 +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 { fireEvent, render, wait, RenderResult } from '@testing-library/react'; -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import * as apmApi from '../../../../../services/rest/createCallApmApi'; -import { License } from '../../../../../../../../../plugins/licensing/common/license'; -import * as hooks from '../../../../../hooks/useFetcher'; -import { LicenseContext } from '../../../../../context/LicenseContext'; -import { CustomLinkOverview } from '.'; -import { - expectTextsInDocument, - expectTextsNotInDocument -} from '../../../../../utils/testHelpers'; -import * as saveCustomLink from './CustomLinkFlyout/saveCustomLink'; -import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; - -const data = [ - { - id: '1', - label: 'label 1', - url: 'url 1', - 'service.name': 'opbeans-java' - }, - { - id: '2', - label: 'label 2', - url: 'url 2', - 'transaction.type': 'request' - } -]; - -describe('CustomLink', () => { - let callApmApiSpy: Function; - beforeAll(() => { - callApmApiSpy = spyOn(apmApi, 'callApmApi').and.returnValue({}); - }); - afterAll(() => { - jest.resetAllMocks(); - }); - const goldLicense = new License({ - signature: 'test signature', - license: { - expiryDateInMillis: 0, - mode: 'gold', - status: 'active', - type: 'gold', - uid: '1' - } - }); - describe('empty prompt', () => { - beforeAll(() => { - spyOn(hooks, 'useFetcher').and.returnValue({ - data: [], - status: 'success' - }); - }); - - afterAll(() => { - jest.clearAllMocks(); - }); - it('shows when no link is available', () => { - const component = render( - - - - ); - expectTextsInDocument(component, ['No links found.']); - }); - }); - - describe('overview', () => { - beforeAll(() => { - spyOn(hooks, 'useFetcher').and.returnValue({ - data, - status: 'success' - }); - }); - - afterAll(() => { - jest.clearAllMocks(); - }); - - it('shows a table with all custom link', () => { - const component = render( - - - - - - ); - expectTextsInDocument(component, [ - 'label 1', - 'url 1', - 'label 2', - 'url 2' - ]); - }); - - it('checks if create custom link button is available and working', () => { - const { queryByText, getByText } = render( - - - - - - ); - expect(queryByText('Create link')).not.toBeInTheDocument(); - act(() => { - fireEvent.click(getByText('Create custom link')); - }); - expect(queryByText('Create link')).toBeInTheDocument(); - }); - }); - - describe('Flyout', () => { - const refetch = jest.fn(); - let saveCustomLinkSpy: Function; - beforeAll(() => { - saveCustomLinkSpy = spyOn(saveCustomLink, 'saveCustomLink'); - spyOn(hooks, 'useFetcher').and.returnValue({ - data, - status: 'success', - refetch - }); - }); - afterEach(() => { - jest.resetAllMocks(); - }); - - const openFlyout = async () => { - const component = render( - - - - - - ); - expect(component.queryByText('Create link')).not.toBeInTheDocument(); - act(() => { - fireEvent.click(component.getByText('Create custom link')); - }); - await wait(() => component.queryByText('Create link')); - expect(component.queryByText('Create link')).toBeInTheDocument(); - return component; - }; - - it('creates a custom link', async () => { - const component = await openFlyout(); - const labelInput = component.getByTestId('label'); - act(() => { - fireEvent.change(labelInput, { - target: { value: 'foo' } - }); - }); - const urlInput = component.getByTestId('url'); - act(() => { - fireEvent.change(urlInput, { - target: { value: 'bar' } - }); - }); - await act(async () => { - await wait(() => fireEvent.submit(component.getByText('Save'))); - }); - expect(saveCustomLinkSpy).toHaveBeenCalledTimes(1); - }); - - it('deletes a custom link', async () => { - const component = render( - - - - - - ); - expect(component.queryByText('Create link')).not.toBeInTheDocument(); - const editButtons = component.getAllByLabelText('Edit'); - expect(editButtons.length).toEqual(2); - act(() => { - fireEvent.click(editButtons[0]); - }); - expect(component.queryByText('Create link')).toBeInTheDocument(); - await act(async () => { - await wait(() => fireEvent.click(component.getByText('Delete'))); - }); - expect(callApmApiSpy).toHaveBeenCalled(); - expect(refetch).toHaveBeenCalled(); - }); - - describe('Filters', () => { - const addFilterField = (component: RenderResult, amount: number) => { - for (let i = 1; i <= amount; i++) { - fireEvent.click(component.getByText('Add another filter')); - } - }; - it('checks if add filter button is disabled after all elements have been added', async () => { - const component = await openFlyout(); - expect(component.getAllByText('service.name').length).toEqual(1); - addFilterField(component, 1); - expect(component.getAllByText('service.name').length).toEqual(2); - addFilterField(component, 2); - expect(component.getAllByText('service.name').length).toEqual(4); - // After 4 items, the button is disabled - addFilterField(component, 2); - expect(component.getAllByText('service.name').length).toEqual(4); - }); - it('removes items already selected', async () => { - const component = await openFlyout(); - - const addFieldAndCheck = ( - fieldName: string, - selectValue: string, - addNewFilter: boolean, - optionsExpected: string[] - ) => { - if (addNewFilter) { - addFilterField(component, 1); - } - const field = component.getByTestId(fieldName) as HTMLSelectElement; - const optionsAvailable = Object.values(field) - .map(option => (option as HTMLOptionElement).text) - .filter(option => option); - - act(() => { - fireEvent.change(field, { - target: { value: selectValue } - }); - }); - expect(field.value).toEqual(selectValue); - expect(optionsAvailable).toEqual(optionsExpected); - }; - - addFieldAndCheck('filter-0', 'transaction.name', false, [ - 'Select field...', - 'service.name', - 'service.environment', - 'transaction.type', - 'transaction.name' - ]); - - addFieldAndCheck('filter-1', 'service.name', true, [ - 'Select field...', - 'service.name', - 'service.environment', - 'transaction.type' - ]); - - addFieldAndCheck('filter-2', 'transaction.type', true, [ - 'Select field...', - 'service.environment', - 'transaction.type' - ]); - - addFieldAndCheck('filter-3', 'service.environment', true, [ - 'Select field...', - 'service.environment' - ]); - }); - }); - }); - - describe('invalid license', () => { - beforeAll(() => { - spyOn(hooks, 'useFetcher').and.returnValue({ - data: [], - status: 'success' - }); - }); - it('shows license prompt when user has a basic license', () => { - const license = new License({ - signature: 'test signature', - license: { - expiryDateInMillis: 0, - mode: 'basic', - status: 'active', - type: 'basic', - uid: '1' - } - }); - const component = render( - - - - - - ); - expectTextsInDocument(component, ['Start free 30-day trial']); - }); - it('shows license prompt when user has an invalid gold license', () => { - const license = new License({ - signature: 'test signature', - license: { - expiryDateInMillis: 0, - mode: 'gold', - status: 'invalid', - type: 'gold', - uid: '1' - } - }); - const component = render( - - - - - - ); - expectTextsInDocument(component, ['Start free 30-day trial']); - }); - it('shows license prompt when user has an invalid trial license', () => { - const license = new License({ - signature: 'test signature', - license: { - expiryDateInMillis: 0, - mode: 'trial', - status: 'invalid', - type: 'trial', - uid: '1' - } - }); - const component = render( - - - - - - ); - expectTextsInDocument(component, ['Start free 30-day trial']); - }); - it('doesnt show license prompt when user has a trial license', () => { - const license = new License({ - signature: 'test signature', - license: { - expiryDateInMillis: 0, - mode: 'trial', - status: 'active', - type: 'trial', - uid: '1' - } - }); - const component = render( - - - - - - ); - expectTextsNotInDocument(component, ['Start free 30-day trial']); - }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx deleted file mode 100644 index e9a915e0f59bc..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ /dev/null @@ -1,110 +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 { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import React, { useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { CustomLink } from '../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { useLicense } from '../../../../../hooks/useLicense'; -import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; -import { CustomLinkFlyout } from './CustomLinkFlyout'; -import { CustomLinkTable } from './CustomLinkTable'; -import { EmptyPrompt } from './EmptyPrompt'; -import { Title } from './Title'; -import { CreateCustomLinkButton } from './CreateCustomLinkButton'; -import { LicensePrompt } from '../../../../shared/LicensePrompt'; - -export const CustomLinkOverview = () => { - const license = useLicense(); - const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); - - const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); - const [customLinkSelected, setCustomLinkSelected] = useState< - CustomLink | undefined - >(); - - const { data: customLinks, status, refetch } = useFetcher( - callApmApi => callApmApi({ pathname: '/api/apm/settings/custom_links' }), - [] - ); - - useEffect(() => { - if (customLinkSelected) { - setIsFlyoutOpen(true); - } - }, [customLinkSelected]); - - const onCloseFlyout = () => { - setCustomLinkSelected(undefined); - setIsFlyoutOpen(false); - }; - - const onCreateCustomLinkClick = () => { - setIsFlyoutOpen(true); - }; - - const showEmptyPrompt = - status === FETCH_STATUS.SUCCESS && isEmpty(customLinks); - - return ( - <> - {isFlyoutOpen && ( - { - onCloseFlyout(); - refetch(); - }} - onDelete={() => { - onCloseFlyout(); - refetch(); - }} - /> - )} - - - - - </EuiFlexItem> - {hasValidLicense && !showEmptyPrompt && ( - <EuiFlexItem> - <EuiFlexGroup alignItems="center" justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <CreateCustomLinkButton onClick={onCreateCustomLinkClick} /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - )} - </EuiFlexGroup> - - <EuiSpacer size="m" /> - {hasValidLicense ? ( - showEmptyPrompt ? ( - <EmptyPrompt onCreateCustomLinkClick={onCreateCustomLinkClick} /> - ) : ( - <CustomLinkTable - items={customLinks} - onCustomLinkSelected={setCustomLinkSelected} - /> - ) - ) : ( - <LicensePrompt - text={i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.license.text', - { - defaultMessage: - "To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services." - } - )} - /> - )} - </EuiPanel> - </> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx deleted file mode 100644 index f0301f8917d10..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiEmptyPrompt } from '@elastic/eui'; -import React from 'react'; -import { Redirect } from 'react-router-dom'; -import styled from 'styled-components'; -import url from 'url'; -import { TRACE_ID } from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { useUrlParams } from '../../../hooks/useUrlParams'; - -const CentralizedContainer = styled.div` - height: 100%; - display: flex; -`; - -const redirectToTransactionDetailPage = ({ - transaction, - rangeFrom, - rangeTo -}: { - transaction: Transaction; - rangeFrom?: string; - rangeTo?: string; -}) => - url.format({ - pathname: `/services/${transaction.service.name}/transactions/view`, - query: { - traceId: transaction.trace.id, - transactionId: transaction.transaction.id, - transactionName: transaction.transaction.name, - transactionType: transaction.transaction.type, - rangeFrom, - rangeTo - } - }); - -const redirectToTracePage = ({ - traceId, - rangeFrom, - rangeTo -}: { - traceId: string; - rangeFrom?: string; - rangeTo?: string; -}) => - url.format({ - pathname: `/traces`, - query: { - kuery: encodeURIComponent(`${TRACE_ID} : "${traceId}"`), - rangeFrom, - rangeTo - } - }); - -export const TraceLink = () => { - const { urlParams } = useUrlParams(); - const { traceIdLink: traceId, rangeFrom, rangeTo } = urlParams; - - const { data = { transaction: null }, status } = useFetcher( - callApmApi => { - if (traceId) { - return callApmApi({ - pathname: '/api/apm/transaction/{traceId}', - params: { - path: { - traceId - } - } - }); - } - }, - [traceId] - ); - if (traceId && status === FETCH_STATUS.SUCCESS) { - const to = data.transaction - ? redirectToTransactionDetailPage({ - transaction: data.transaction, - rangeFrom, - rangeTo - }) - : redirectToTracePage({ traceId, rangeFrom, rangeTo }); - return <Redirect to={to} />; - } - - return ( - <CentralizedContainer> - <EuiEmptyPrompt iconType="apmTrace" title={<h2>Fetching trace...</h2>} /> - </CentralizedContainer> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx deleted file mode 100644 index bfbad78a5c026..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/index.tsx +++ /dev/null @@ -1,67 +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 { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; -import { TraceList } from './TraceList'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; - -export function TraceOverview() { - const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; - const { status, data = [] } = useFetcher( - callApmApi => { - if (start && end) { - return callApmApi({ - pathname: '/api/apm/traces', - params: { - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters) - } - } - }); - } - }, - [start, end, uiFilters] - ); - - useTrackPageview({ app: 'apm', path: 'traces_overview' }); - useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 }); - - const localUIFiltersConfig = useMemo(() => { - const config: React.ComponentProps<typeof LocalUIFilters> = { - filterNames: ['transactionResult', 'host', 'containerId', 'podName'], - projection: PROJECTION.TRACES - }; - - return config; - }, []); - - return ( - <> - <EuiSpacer /> - <EuiFlexGroup> - <EuiFlexItem grow={1}> - <LocalUIFilters {...localUIFiltersConfig} showCount={false} /> - </EuiFlexItem> - <EuiFlexItem grow={7}> - <EuiPanel> - <TraceList - items={data} - isLoading={status === FETCH_STATUS.LOADING} - /> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> - </> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx deleted file mode 100644 index e70133aabb679..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ /dev/null @@ -1,212 +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 { EuiIconTip, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import d3 from 'd3'; -import React, { FunctionComponent, useCallback } from 'react'; -import { isEmpty } from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionDistributionAPIResponse } from '../../../../../../../../plugins/apm/server/lib/transactions/distribution'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../../../../plugins/apm/server/lib/transactions/distribution/get_buckets/transform'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { getDurationFormatter } from '../../../../utils/formatters'; -// @ts-ignore -import Histogram from '../../../shared/charts/Histogram'; -import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { history } from '../../../../utils/history'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; - -interface IChartPoint { - samples: IBucket['samples']; - x0: number; - x: number; - y: number; - style: { - cursor: string; - }; -} - -export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) { - if (!buckets) { - return []; - } - - return buckets.map( - ({ samples, count, key }): IChartPoint => { - return { - samples, - x0: key, - x: key + bucketSize, - y: count, - style: { - cursor: isEmpty(samples) ? 'default' : 'pointer' - } - }; - } - ); -} - -const getFormatYShort = (transactionType: string | undefined) => ( - t: number -) => { - return i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel', - { - defaultMessage: - '{transCount} {transType, select, request {req.} other {trans.}}', - values: { - transCount: t, - transType: transactionType - } - } - ); -}; - -const getFormatYLong = (transactionType: string | undefined) => (t: number) => { - return transactionType === 'request' - ? i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel', - { - defaultMessage: - '{transCount, plural, =0 {# request} one {# request} other {# requests}}', - values: { - transCount: t - } - } - ) - : i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel', - { - defaultMessage: - '{transCount, plural, =0 {# transaction} one {# transaction} other {# transactions}}', - values: { - transCount: t - } - } - ); -}; - -interface Props { - distribution?: TransactionDistributionAPIResponse; - urlParams: IUrlParams; - isLoading: boolean; - bucketIndex: number; -} - -export const TransactionDistribution: FunctionComponent<Props> = ( - props: Props -) => { - const { - distribution, - urlParams: { transactionType }, - isLoading, - bucketIndex - } = props; - - const formatYShort = useCallback(getFormatYShort(transactionType), [ - transactionType - ]); - - const formatYLong = useCallback(getFormatYLong(transactionType), [ - transactionType - ]); - - // no data in response - if (!distribution || distribution.noHits) { - // only show loading state if there is no data - else show stale data until new data has loaded - if (isLoading) { - return <LoadingStatePrompt />; - } - - return ( - <EmptyMessage - heading={i18n.translate('xpack.apm.transactionDetails.notFoundLabel', { - defaultMessage: 'No transactions were found.' - })} - /> - ); - } - - const buckets = getFormattedBuckets( - distribution.buckets, - distribution.bucketSize - ); - - const xMax = d3.max(buckets, d => d.x) || 0; - const timeFormatter = getDurationFormatter(xMax); - - return ( - <div> - <EuiTitle size="xs"> - <h5> - {i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle', - { - defaultMessage: 'Transactions duration distribution' - } - )}{' '} - <EuiIconTip - title={i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingLabel', - { - defaultMessage: 'Sampling' - } - )} - content={i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingDescription', - { - defaultMessage: - "Each bucket will show a sample transaction. If there's no sample available, it's most likely because of the sampling limit set in the agent configuration." - } - )} - position="top" - /> - </h5> - </EuiTitle> - - <Histogram - buckets={buckets} - bucketSize={distribution.bucketSize} - bucketIndex={bucketIndex} - onClick={(bucket: IChartPoint) => { - if (!isEmpty(bucket.samples)) { - const sample = bucket.samples[0]; - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionId: sample.transactionId, - traceId: sample.traceId - }) - }); - } - }} - formatX={(time: number) => timeFormatter(time).formatted} - formatYShort={formatYShort} - formatYLong={formatYLong} - verticalLineHover={(bucket: IChartPoint) => isEmpty(bucket.samples)} - backgroundHover={(bucket: IChartPoint) => !isEmpty(bucket.samples)} - tooltipHeader={(bucket: IChartPoint) => { - const xFormatted = timeFormatter(bucket.x); - const x0Formatted = timeFormatter(bucket.x0); - return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; - }} - tooltipFooter={(bucket: IChartPoint) => - isEmpty(bucket.samples) && - i18n.translate( - 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip', - { - defaultMessage: 'No sample available for this bucket' - } - ) - } - /> - </div> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx deleted file mode 100644 index 87102a486ab5f..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ /dev/null @@ -1,239 +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 { - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiHorizontalRule, - EuiPortal, - EuiSpacer, - EuiTabbedContent, - EuiTitle, - EuiBadge, - EuiToolTip -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import styled from 'styled-components'; -import { px, units } from '../../../../../../../style/variables'; -import { Summary } from '../../../../../../shared/Summary'; -import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; -import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { DiscoverSpanLink } from '../../../../../../shared/Links/DiscoverLinks/DiscoverSpanLink'; -import { Stacktrace } from '../../../../../../shared/Stacktrace'; -import { ResponsiveFlyout } from '../ResponsiveFlyout'; -import { DatabaseContext } from './DatabaseContext'; -import { StickySpanProperties } from './StickySpanProperties'; -import { HttpInfoSummaryItem } from '../../../../../../shared/Summary/HttpInfoSummaryItem'; -import { SpanMetadata } from '../../../../../../shared/MetadataTable/SpanMetadata'; -import { SyncBadge } from '../SyncBadge'; - -function formatType(type: string) { - switch (type) { - case 'db': - return 'DB'; - case 'hard-navigation': - return i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel', - { - defaultMessage: 'Navigation timing' - } - ); - default: - return type; - } -} - -function formatSubtype(subtype: string | undefined) { - switch (subtype) { - case 'mysql': - return 'MySQL'; - default: - return subtype; - } -} - -function getSpanTypes(span: Span) { - const { type, subtype, action } = span.span; - - return { - spanType: formatType(type), - spanSubtype: formatSubtype(subtype), - spanAction: action - }; -} - -const SpanBadge = styled(EuiBadge)` - display: inline-block; - margin-right: ${px(units.quarter)}; -`; - -const HttpInfoContainer = styled('div')` - margin-right: ${px(units.quarter)}; -`; - -interface Props { - span?: Span; - parentTransaction?: Transaction; - totalDuration?: number; - onClose: () => void; -} - -export function SpanFlyout({ - span, - parentTransaction, - totalDuration, - onClose -}: Props) { - if (!span) { - return null; - } - - const stackframes = span.span.stacktrace; - const codeLanguage = parentTransaction?.service.language?.name; - const dbContext = span.span.db; - const httpContext = span.span.http; - const spanTypes = getSpanTypes(span); - const spanHttpStatusCode = httpContext?.response?.status_code; - const spanHttpUrl = httpContext?.url?.original; - const spanHttpMethod = httpContext?.method; - - return ( - <EuiPortal> - <ResponsiveFlyout onClose={onClose} size="m" ownFocus={true}> - <EuiFlyoutHeader hasBorder> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiTitle> - <h2> - {i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.spanDetailsTitle', - { - defaultMessage: 'Span details' - } - )} - </h2> - </EuiTitle> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <DiscoverSpanLink span={span}> - <EuiButtonEmpty iconType="discoverApp"> - {i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel', - { - defaultMessage: 'View span in Discover' - } - )} - </EuiButtonEmpty> - </DiscoverSpanLink> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <StickySpanProperties span={span} transaction={parentTransaction} /> - <EuiSpacer size="m" /> - <Summary - items={[ - <TimestampTooltip time={span.timestamp.us / 1000} />, - <DurationSummaryItem - duration={span.span.duration.us} - totalDuration={totalDuration} - parentType="transaction" - />, - <> - {spanHttpUrl && ( - <HttpInfoContainer> - <HttpInfoSummaryItem - method={spanHttpMethod} - url={spanHttpUrl} - status={spanHttpStatusCode} - /> - </HttpInfoContainer> - )} - <EuiToolTip - content={i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.spanType', - { defaultMessage: 'Type' } - )} - > - <SpanBadge color="hollow">{spanTypes.spanType}</SpanBadge> - </EuiToolTip> - {spanTypes.spanSubtype && ( - <EuiToolTip - content={i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.spanSubtype', - { defaultMessage: 'Subtype' } - )} - > - <SpanBadge color="hollow"> - {spanTypes.spanSubtype} - </SpanBadge> - </EuiToolTip> - )} - {spanTypes.spanAction && ( - <EuiToolTip - content={i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.spanAction', - { defaultMessage: 'Action' } - )} - > - <SpanBadge color="hollow">{spanTypes.spanAction}</SpanBadge> - </EuiToolTip> - )} - <SyncBadge sync={span.span.sync} /> - </> - ]} - /> - <EuiHorizontalRule /> - <DatabaseContext dbContext={dbContext} /> - <EuiTabbedContent - tabs={[ - { - id: 'stack-trace', - name: i18n.translate( - 'xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel', - { - defaultMessage: 'Stack Trace' - } - ), - content: ( - <Fragment> - <EuiSpacer size="l" /> - <Stacktrace - stackframes={stackframes} - codeLanguage={codeLanguage} - /> - </Fragment> - ) - }, - { - id: 'metadata', - name: i18n.translate( - 'xpack.apm.propertiesTable.tabs.metadataLabel', - { - defaultMessage: 'Metadata' - } - ), - content: ( - <Fragment> - <EuiSpacer size="m" /> - <SpanMetadata span={span} /> - </Fragment> - ) - } - ]} - /> - </EuiFlyoutBody> - </ResponsiveFlyout> - </EuiPortal> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx deleted file mode 100644 index e24414bb28d52..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiPortal, - EuiSpacer, - EuiTitle, - EuiHorizontalRule -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { TransactionActionMenu } from '../../../../../../shared/TransactionActionMenu/TransactionActionMenu'; -import { TransactionSummary } from '../../../../../../shared/Summary/TransactionSummary'; -import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties'; -import { ResponsiveFlyout } from '../ResponsiveFlyout'; -import { TransactionMetadata } from '../../../../../../shared/MetadataTable/TransactionMetadata'; -import { DroppedSpansWarning } from './DroppedSpansWarning'; - -interface Props { - onClose: () => void; - transaction?: Transaction; - errorCount?: number; - rootTransactionDuration?: number; -} - -function TransactionPropertiesTable({ - transaction -}: { - transaction: Transaction; -}) { - return ( - <div> - <EuiTitle size="s"> - <h4>Metadata</h4> - </EuiTitle> - <TransactionMetadata transaction={transaction} /> - </div> - ); -} - -export function TransactionFlyout({ - transaction: transactionDoc, - onClose, - errorCount = 0, - rootTransactionDuration -}: Props) { - if (!transactionDoc) { - return null; - } - - return ( - <EuiPortal> - <ResponsiveFlyout onClose={onClose} ownFocus={true} maxWidth={false}> - <EuiFlyoutHeader hasBorder> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiTitle> - <h4> - {i18n.translate( - 'xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle', - { - defaultMessage: 'Transaction details' - } - )} - </h4> - </EuiTitle> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <TransactionActionMenu transaction={transactionDoc} /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutHeader> - <EuiFlyoutBody> - <FlyoutTopLevelProperties transaction={transactionDoc} /> - <EuiSpacer size="m" /> - <TransactionSummary - transaction={transactionDoc} - totalDuration={rootTransactionDuration} - errorCount={errorCount} - /> - <EuiHorizontalRule margin="m" /> - <DroppedSpansWarning transactionDoc={transactionDoc} /> - <TransactionPropertiesTable transaction={transactionDoc} /> - </EuiFlyoutBody> - </ResponsiveFlyout> - </EuiPortal> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx deleted file mode 100644 index 1082052c6929d..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx +++ /dev/null @@ -1,138 +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 { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiPagination, - EuiPanel, - EuiSpacer, - EuiTitle -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Location } from 'history'; -import React, { useEffect, useState } from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../../../../plugins/apm/server/lib/transactions/distribution/get_buckets/transform'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { history } from '../../../../utils/history'; -import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; -import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; -import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; -import { MaybeViewTraceLink } from './MaybeViewTraceLink'; -import { TransactionTabs } from './TransactionTabs'; -import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; - -interface Props { - urlParams: IUrlParams; - location: Location; - waterfall: IWaterfall; - exceedsMax: boolean; - isLoading: boolean; - traceSamples: IBucket['samples']; -} - -export const WaterfallWithSummmary: React.FC<Props> = ({ - urlParams, - location, - waterfall, - exceedsMax, - isLoading, - traceSamples -}) => { - const [sampleActivePage, setSampleActivePage] = useState(0); - - useEffect(() => { - setSampleActivePage(0); - }, [traceSamples]); - - const goToSample = (index: number) => { - setSampleActivePage(index); - const sample = traceSamples[index]; - history.push({ - ...history.location, - search: fromQuery({ - ...toQuery(history.location.search), - transactionId: sample.transactionId, - traceId: sample.traceId - }) - }); - }; - - const { entryTransaction } = waterfall; - if (!entryTransaction) { - const content = isLoading ? ( - <LoadingStatePrompt /> - ) : ( - <EuiEmptyPrompt - title={ - <div> - {i18n.translate('xpack.apm.transactionDetails.traceNotFound', { - defaultMessage: 'The selected trace cannot be found' - })} - </div> - } - titleSize="s" - /> - ); - - return <EuiPanel paddingSize="m">{content}</EuiPanel>; - } - - return ( - <EuiPanel paddingSize="m"> - <EuiFlexGroup> - <EuiFlexItem style={{ flexDirection: 'row', alignItems: 'center' }}> - <EuiTitle size="xs"> - <h5> - {i18n.translate('xpack.apm.transactionDetails.traceSampleTitle', { - defaultMessage: 'Trace sample' - })} - </h5> - </EuiTitle> - {traceSamples && ( - <EuiPagination - pageCount={traceSamples.length} - activePage={sampleActivePage} - onPageClick={goToSample} - compressed - /> - )} - </EuiFlexItem> - <EuiFlexItem> - <EuiFlexGroup justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <TransactionActionMenu transaction={entryTransaction} /> - </EuiFlexItem> - <MaybeViewTraceLink - transaction={entryTransaction} - waterfall={waterfall} - /> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - <TransactionSummary - errorCount={waterfall.errorsCount} - totalDuration={waterfall.rootTransaction?.transaction.duration.us} - transaction={entryTransaction} - /> - <EuiSpacer size="s" /> - - <TransactionTabs - transaction={entryTransaction} - location={location} - urlParams={urlParams} - waterfall={waterfall} - exceedsMax={exceedsMax} - /> - </EuiPanel> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx deleted file mode 100644 index e2634be0e0be8..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/index.tsx +++ /dev/null @@ -1,127 +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 { - EuiPanel, - EuiSpacer, - EuiTitle, - EuiHorizontalRule, - EuiFlexGroup, - EuiFlexItem -} from '@elastic/eui'; -import _ from 'lodash'; -import React, { useMemo } from 'react'; -import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; -import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; -import { useWaterfall } from '../../../hooks/useWaterfall'; -import { TransactionCharts } from '../../shared/charts/TransactionCharts'; -import { ApmHeader } from '../../shared/ApmHeader'; -import { TransactionDistribution } from './Distribution'; -import { WaterfallWithSummmary } from './WaterfallWithSummmary'; -import { useLocation } from '../../../hooks/useLocation'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; -import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { HeightRetainer } from '../../shared/HeightRetainer'; - -export function TransactionDetails() { - const location = useLocation(); - const { urlParams } = useUrlParams(); - const { - data: distributionData, - status: distributionStatus - } = useTransactionDistribution(urlParams); - - const { data: transactionChartsData } = useTransactionCharts(); - const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall( - urlParams - ); - const { transactionName, transactionType, serviceName } = urlParams; - - useTrackPageview({ app: 'apm', path: 'transaction_details' }); - useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 }); - - const localUIFiltersConfig = useMemo(() => { - const config: React.ComponentProps<typeof LocalUIFilters> = { - filterNames: ['transactionResult', 'serviceVersion'], - projection: PROJECTION.TRANSACTIONS, - params: { - transactionName, - transactionType, - serviceName - } - }; - return config; - }, [transactionName, transactionType, serviceName]); - - const bucketIndex = distributionData.buckets.findIndex(bucket => - bucket.samples.some( - sample => - sample.transactionId === urlParams.transactionId && - sample.traceId === urlParams.traceId - ) - ); - - const traceSamples = distributionData.buckets[bucketIndex]?.samples; - - return ( - <div> - <ApmHeader> - <EuiTitle size="l"> - <h1>{transactionName}</h1> - </EuiTitle> - </ApmHeader> - - <EuiFlexGroup> - <EuiFlexItem grow={1}> - <LocalUIFilters {...localUIFiltersConfig} /> - </EuiFlexItem> - <EuiFlexItem grow={7}> - <ChartsSyncContextProvider> - <TransactionBreakdown /> - - <EuiSpacer size="s" /> - - <TransactionCharts - hasMLJob={false} - charts={transactionChartsData} - urlParams={urlParams} - location={location} - /> - </ChartsSyncContextProvider> - - <EuiHorizontalRule size="full" margin="l" /> - - <EuiPanel> - <TransactionDistribution - distribution={distributionData} - isLoading={distributionStatus === FETCH_STATUS.LOADING} - urlParams={urlParams} - bucketIndex={bucketIndex} - /> - </EuiPanel> - - <EuiSpacer size="s" /> - - <HeightRetainer> - <WaterfallWithSummmary - location={location} - urlParams={urlParams} - waterfall={waterfall} - isLoading={waterfallStatus === FETCH_STATUS.LOADING} - exceedsMax={exceedsMax} - traceSamples={traceSamples} - /> - </HeightRetainer> - </EuiFlexItem> - </EuiFlexGroup> - </div> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx deleted file mode 100644 index 16fda7c600906..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/List/index.tsx +++ /dev/null @@ -1,160 +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 { EuiIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ITransactionGroup } from '../../../../../../../../plugins/apm/server/lib/transaction_groups/transform'; -import { fontFamilyCode, truncate } from '../../../../style/variables'; -import { asDecimal, convertTo } from '../../../../utils/formatters'; -import { ImpactBar } from '../../../shared/ImpactBar'; -import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; -import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; -import { EmptyMessage } from '../../../shared/EmptyMessage'; -import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; - -const TransactionNameLink = styled(TransactionDetailLink)` - ${truncate('100%')}; - font-family: ${fontFamilyCode}; -`; - -interface Props { - items: ITransactionGroup[]; - isLoading: boolean; -} - -const toMilliseconds = (time: number) => - convertTo({ - unit: 'milliseconds', - microseconds: time - }).formatted; - -export function TransactionList({ items, isLoading }: Props) { - const columns: Array<ITableColumn<ITransactionGroup>> = useMemo( - () => [ - { - field: 'name', - name: i18n.translate('xpack.apm.transactionsTable.nameColumnLabel', { - defaultMessage: 'Name' - }), - width: '50%', - sortable: true, - render: (transactionName: string, { sample }: ITransactionGroup) => { - return ( - <EuiToolTip - id="transaction-name-link-tooltip" - content={transactionName || NOT_AVAILABLE_LABEL} - > - <TransactionNameLink - serviceName={sample.service.name} - transactionId={sample.transaction.id} - traceId={sample.trace.id} - transactionName={sample.transaction.name} - transactionType={sample.transaction.type} - > - {transactionName || NOT_AVAILABLE_LABEL} - </TransactionNameLink> - </EuiToolTip> - ); - } - }, - { - field: 'averageResponseTime', - name: i18n.translate( - 'xpack.apm.transactionsTable.avgDurationColumnLabel', - { - defaultMessage: 'Avg. duration' - } - ), - sortable: true, - dataType: 'number', - render: (time: number) => toMilliseconds(time) - }, - { - field: 'p95', - name: i18n.translate( - 'xpack.apm.transactionsTable.95thPercentileColumnLabel', - { - defaultMessage: '95th percentile' - } - ), - sortable: true, - dataType: 'number', - render: (time: number) => toMilliseconds(time) - }, - { - field: 'transactionsPerMinute', - name: i18n.translate( - 'xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel', - { - defaultMessage: 'Trans. per minute' - } - ), - sortable: true, - dataType: 'number', - render: (value: number) => - `${asDecimal(value)} ${i18n.translate( - 'xpack.apm.transactionsTable.transactionsPerMinuteUnitLabel', - { - defaultMessage: 'tpm' - } - )}` - }, - { - field: 'impact', - name: ( - <EuiToolTip - content={i18n.translate( - 'xpack.apm.transactionsTable.impactColumnDescription', - { - defaultMessage: - "The most used and slowest endpoints in your service. It's calculated by taking the relative average duration times the number of transactions per minute." - } - )} - > - <> - {i18n.translate('xpack.apm.transactionsTable.impactColumnLabel', { - defaultMessage: 'Impact' - })}{' '} - <EuiIcon - size="s" - color="subdued" - type="questionInCircle" - className="eui-alignTop" - /> - </> - </EuiToolTip> - ), - sortable: true, - dataType: 'number', - render: (value: number) => <ImpactBar value={value} /> - } - ], - [] - ); - - const noItemsMessage = ( - <EmptyMessage - heading={i18n.translate('xpack.apm.transactionsTable.notFoundLabel', { - defaultMessage: 'No transactions were found.' - })} - /> - ); - - return ( - <ManagedTable - noItemsMessage={isLoading ? <LoadingStatePrompt /> : noItemsMessage} - columns={columns} - items={items} - initialSortField="impact" - initialSortDirection="desc" - initialPageSize={25} - /> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx deleted file mode 100644 index b008c98417867..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ /dev/null @@ -1,162 +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 { - EuiPanel, - EuiSpacer, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule -} from '@elastic/eui'; -import { Location } from 'history'; -import { first } from 'lodash'; -import React, { useMemo } from 'react'; -import { useTransactionList } from '../../../hooks/useTransactionList'; -import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; -import { IUrlParams } from '../../../context/UrlParamsContext/types'; -import { TransactionCharts } from '../../shared/charts/TransactionCharts'; -import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; -import { TransactionList } from './List'; -import { useRedirect } from './useRedirect'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { getHasMLJob } from '../../../services/rest/ml'; -import { history } from '../../../utils/history'; -import { useLocation } from '../../../hooks/useLocation'; -import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; -import { useTrackPageview } from '../../../../../../../plugins/observability/public'; -import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; -import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; - -function getRedirectLocation({ - urlParams, - location, - serviceTransactionTypes -}: { - location: Location; - urlParams: IUrlParams; - serviceTransactionTypes: string[]; -}): Location | undefined { - const { transactionType } = urlParams; - const firstTransactionType = first(serviceTransactionTypes); - - if (!transactionType && firstTransactionType) { - return { - ...location, - search: fromQuery({ - ...toQuery(location.search), - transactionType: firstTransactionType - }) - }; - } -} - -export function TransactionOverview() { - const location = useLocation(); - const { urlParams } = useUrlParams(); - const { serviceName, transactionType } = urlParams; - - // TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context? - const serviceTransactionTypes = useServiceTransactionTypes(urlParams); - - // redirect to first transaction type - useRedirect( - history, - getRedirectLocation({ - urlParams, - location, - serviceTransactionTypes - }) - ); - - const { data: transactionCharts } = useTransactionCharts(); - - useTrackPageview({ app: 'apm', path: 'transaction_overview' }); - useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); - const { - data: transactionListData, - status: transactionListStatus - } = useTransactionList(urlParams); - - const { http } = useApmPluginContext().core; - - const { data: hasMLJob = false } = useFetcher(() => { - if (serviceName && transactionType) { - return getHasMLJob({ serviceName, transactionType, http }); - } - }, [http, serviceName, transactionType]); - - const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( - () => ({ - filterNames: [ - 'transactionResult', - 'host', - 'containerId', - 'podName', - 'serviceVersion' - ], - params: { - serviceName, - transactionType - }, - projection: PROJECTION.TRANSACTION_GROUPS - }), - [serviceName, transactionType] - ); - - // TODO: improve urlParams typings. - // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed - if (!serviceName || !transactionType) { - return null; - } - - return ( - <> - <EuiSpacer /> - <EuiFlexGroup> - <EuiFlexItem grow={1}> - <LocalUIFilters {...localFiltersConfig}> - <TransactionTypeFilter transactionTypes={serviceTransactionTypes} /> - <EuiSpacer size="xl" /> - <EuiHorizontalRule margin="none" /> - </LocalUIFilters> - </EuiFlexItem> - <EuiFlexItem grow={7}> - <ChartsSyncContextProvider> - <TransactionBreakdown initialIsOpen={true} /> - - <EuiSpacer size="s" /> - - <TransactionCharts - hasMLJob={hasMLJob} - charts={transactionCharts} - location={location} - urlParams={urlParams} - /> - </ChartsSyncContextProvider> - - <EuiSpacer size="s" /> - - <EuiPanel> - <EuiTitle size="xs"> - <h3>Transactions</h3> - </EuiTitle> - <EuiSpacer size="s" /> - <TransactionList - isLoading={transactionListStatus === 'loading'} - items={transactionListData} - /> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGroup> - </> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx deleted file mode 100644 index e911011f0979c..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ /dev/null @@ -1,112 +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 { EuiSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { useLocation } from '../../../hooks/useLocation'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { history } from '../../../utils/history'; -import { fromQuery, toQuery } from '../Links/url_helpers'; -import { - ENVIRONMENT_ALL, - ENVIRONMENT_NOT_DEFINED -} from '../../../../../../../plugins/apm/common/environment_filter_values'; - -function updateEnvironmentUrl( - location: ReturnType<typeof useLocation>, - environment?: string -) { - const nextEnvironmentQueryParam = - environment !== ENVIRONMENT_ALL ? environment : undefined; - history.push({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - environment: nextEnvironmentQueryParam - }) - }); -} - -const ALL_OPTION = { - value: ENVIRONMENT_ALL, - text: i18n.translate('xpack.apm.filter.environment.allLabel', { - defaultMessage: 'All' - }) -}; - -const NOT_DEFINED_OPTION = { - value: ENVIRONMENT_NOT_DEFINED, - text: i18n.translate('xpack.apm.filter.environment.notDefinedLabel', { - defaultMessage: 'Not defined' - }) -}; - -const SEPARATOR_OPTION = { - text: `- ${i18n.translate( - 'xpack.apm.filter.environment.selectEnvironmentLabel', - { defaultMessage: 'Select environment' } - )} -`, - disabled: true -}; - -function getOptions(environments: string[]) { - const environmentOptions = environments - .filter(env => env !== ENVIRONMENT_NOT_DEFINED) - .map(environment => ({ - value: environment, - text: environment - })); - - return [ - ALL_OPTION, - ...(environments.includes(ENVIRONMENT_NOT_DEFINED) - ? [NOT_DEFINED_OPTION] - : []), - ...(environmentOptions.length > 0 ? [SEPARATOR_OPTION] : []), - ...environmentOptions - ]; -} - -export const EnvironmentFilter: React.FC = () => { - const location = useLocation(); - const { urlParams, uiFilters } = useUrlParams(); - const { start, end, serviceName } = urlParams; - - const { environment } = uiFilters; - const { data: environments = [], status = 'loading' } = useFetcher( - callApmApi => { - if (start && end) { - return callApmApi({ - pathname: '/api/apm/ui_filters/environments', - params: { - query: { - start, - end, - serviceName - } - } - }); - } - }, - [start, end, serviceName] - ); - - return ( - <EuiSelect - prepend={i18n.translate('xpack.apm.filter.environment.label', { - defaultMessage: 'environment' - })} - options={getOptions(environments)} - value={environment || ENVIRONMENT_ALL} - onChange={event => { - updateEnvironmentUrl(location, event.target.value); - }} - isLoading={status === 'loading'} - /> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx deleted file mode 100644 index b7e23c2979cb8..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx +++ /dev/null @@ -1,87 +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 React from 'react'; -import { EuiFieldNumber } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isFinite } from 'lodash'; -import { ForLastExpression } from '../../../../../../../plugins/triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG } from '../../../../../../../plugins/apm/common/alert_types'; -import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; -import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; - -export interface ErrorRateAlertTriggerParams { - windowSize: number; - windowUnit: string; - threshold: number; -} - -interface Props { - alertParams: ErrorRateAlertTriggerParams; - setAlertParams: (key: string, value: any) => void; - setAlertProperty: (key: string, value: any) => void; -} - -export function ErrorRateAlertTrigger(props: Props) { - const { setAlertParams, setAlertProperty, alertParams } = props; - - const defaults = { - threshold: 25, - windowSize: 1, - windowUnit: 'm' - }; - - const params = { - ...defaults, - ...alertParams - }; - - const threshold = isFinite(params.threshold) ? params.threshold : ''; - - const fields = [ - <PopoverExpression - title={i18n.translate('xpack.apm.errorRateAlertTrigger.isAbove', { - defaultMessage: 'is above' - })} - value={threshold.toString()} - > - <EuiFieldNumber - value={threshold} - step={0} - onChange={e => - setAlertParams('threshold', parseInt(e.target.value, 10)) - } - compressed - append={i18n.translate('xpack.apm.errorRateAlertTrigger.errors', { - defaultMessage: 'errors' - })} - /> - </PopoverExpression>, - <ForLastExpression - onChangeWindowSize={windowSize => - setAlertParams('windowSize', windowSize || '') - } - onChangeWindowUnit={windowUnit => - setAlertParams('windowUnit', windowUnit) - } - timeWindowSize={params.windowSize} - timeWindowUnit={params.windowUnit} - errors={{ - timeWindowSize: [], - timeWindowUnit: [] - }} - /> - ]; - - return ( - <ServiceAlertTrigger - alertTypeName={ALERT_TYPES_CONFIG['apm.error_rate'].name} - defaults={defaults} - fields={fields} - setAlertParams={setAlertParams} - setAlertProperty={setAlertProperty} - /> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx deleted file mode 100644 index dba31822dd23e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ /dev/null @@ -1,156 +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 React, { useState } from 'react'; -import { uniqueId, startsWith } from 'lodash'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { fromQuery, toQuery } from '../Links/url_helpers'; -// @ts-ignore -import { Typeahead } from './Typeahead'; -import { getBoolFilter } from './get_bool_filter'; -import { useLocation } from '../../../hooks/useLocation'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { history } from '../../../utils/history'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; -import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; -import { - QuerySuggestion, - esKuery, - IIndexPattern -} from '../../../../../../../../src/plugins/data/public'; - -const Container = styled.div` - margin-bottom: 10px; -`; - -interface State { - suggestions: QuerySuggestion[]; - isLoadingSuggestions: boolean; -} - -function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { - const ast = esKuery.fromKueryExpression(kuery); - return esKuery.toElasticsearchQuery(ast, indexPattern); -} - -export function KueryBar() { - const [state, setState] = useState<State>({ - suggestions: [], - isLoadingSuggestions: false - }); - const { urlParams } = useUrlParams(); - const location = useLocation(); - const { data } = useApmPluginContext().plugins; - - let currentRequestCheck; - - const { processorEvent } = urlParams; - - const examples = { - transaction: 'transaction.duration.us > 300000', - error: 'http.response.status_code >= 400', - metric: 'process.pid = "1234"', - defaults: - 'transaction.duration.us > 300000 AND http.response.status_code >= 400' - }; - - const example = examples[processorEvent || 'defaults']; - - const { indexPattern } = useDynamicIndexPattern(processorEvent); - - const placeholder = i18n.translate('xpack.apm.kueryBar.placeholder', { - defaultMessage: `Search {event, select, - transaction {transactions} - metric {metrics} - error {errors} - other {transactions, errors and metrics} - } (E.g. {queryExample})`, - values: { - queryExample: example, - event: processorEvent - } - }); - - // The bar should be disabled when viewing the service map - const disabled = /\/service-map$/.test(location.pathname); - const disabledPlaceholder = i18n.translate( - 'xpack.apm.kueryBar.disabledPlaceholder', - { defaultMessage: 'Search is not available for service map' } - ); - - async function onChange(inputValue: string, selectionStart: number) { - if (indexPattern == null) { - return; - } - - setState({ ...state, suggestions: [], isLoadingSuggestions: true }); - - const currentRequest = uniqueId(); - currentRequestCheck = currentRequest; - - try { - const suggestions = ( - (await data.autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - boolFilter: getBoolFilter(urlParams), - query: inputValue, - selectionStart, - selectionEnd: selectionStart - })) || [] - ) - .filter(suggestion => !startsWith(suggestion.text, 'span.')) - .slice(0, 15); - - if (currentRequest !== currentRequestCheck) { - return; - } - - setState({ ...state, suggestions, isLoadingSuggestions: false }); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error while fetching suggestions', e); - } - } - - function onSubmit(inputValue: string) { - if (indexPattern == null) { - return; - } - - try { - const res = convertKueryToEsQuery(inputValue, indexPattern); - if (!res) { - return; - } - - history.push({ - ...location, - search: fromQuery({ - ...toQuery(location.search), - kuery: encodeURIComponent(inputValue.trim()) - }) - }); - } catch (e) { - console.log('Invalid kuery syntax'); // eslint-disable-line no-console - } - } - - return ( - <Container> - <Typeahead - disabled={disabled} - isLoading={state.isLoadingSuggestions} - initialValue={urlParams.kuery} - onChange={onChange} - onSubmit={onSubmit} - suggestions={state.suggestions} - placeholder={disabled ? disabledPlaceholder : placeholder} - /> - </Container> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx deleted file mode 100644 index cede3e394cfab..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ /dev/null @@ -1,95 +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 React from 'react'; -import { - EuiTitle, - EuiSpacer, - EuiHorizontalRule, - EuiButtonEmpty -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { Filter } from './Filter'; -import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; -import { PROJECTION } from '../../../../../../../plugins/apm/common/projections/typings'; - -interface Props { - projection: PROJECTION; - filterNames: LocalUIFilterName[]; - params?: Record<string, string | number | boolean | undefined>; - showCount?: boolean; - children?: React.ReactNode; -} - -const ButtonWrapper = styled.div` - display: inline-block; -`; - -const LocalUIFilters = ({ - projection, - params, - filterNames, - children, - showCount = true -}: Props) => { - const { filters, setFilterValue, clearValues } = useLocalUIFilters({ - filterNames, - projection, - params - }); - - const hasValues = filters.some(filter => filter.value.length > 0); - - return ( - <> - <EuiTitle size="s"> - <h3> - {i18n.translate('xpack.apm.localFiltersTitle', { - defaultMessage: 'Filters' - })} - </h3> - </EuiTitle> - <EuiSpacer size="s" /> - {children} - {filters.map(filter => { - return ( - <React.Fragment key={filter.name}> - <Filter - {...filter} - onChange={value => { - setFilterValue(filter.name, value); - }} - showCount={showCount} - /> - <EuiHorizontalRule margin="none" /> - </React.Fragment> - ); - })} - {hasValues ? ( - <> - <EuiSpacer size="s" /> - <ButtonWrapper> - <EuiButtonEmpty - size="xs" - iconType="cross" - flush="left" - onClick={clearValues} - > - {i18n.translate('xpack.apm.clearFilters', { - defaultMessage: 'Clear filters' - })} - </EuiButtonEmpty> - </ButtonWrapper> - </> - ) : null} - </> - ); -}; - -export { LocalUIFilters }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx deleted file mode 100644 index ce991d8b0dc00..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx +++ /dev/null @@ -1,23 +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 React, { useMemo } from 'react'; -import { ERROR_METADATA_SECTIONS } from './sections'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { getSectionsWithRows } from '../helper'; -import { MetadataTable } from '..'; - -interface Props { - error: APMError; -} - -export function ErrorMetadata({ error }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(ERROR_METADATA_SECTIONS, error), - [error] - ); - return <MetadataTable sections={sectionsWithRows} />; -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx deleted file mode 100644 index 2134f12531a7a..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx +++ /dev/null @@ -1,23 +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 React, { useMemo } from 'react'; -import { SPAN_METADATA_SECTIONS } from './sections'; -import { Span } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { getSectionsWithRows } from '../helper'; -import { MetadataTable } from '..'; - -interface Props { - span: Span; -} - -export function SpanMetadata({ span }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(SPAN_METADATA_SECTIONS, span), - [span] - ); - return <MetadataTable sections={sectionsWithRows} />; -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx deleted file mode 100644 index 6f93de4e87e49..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx +++ /dev/null @@ -1,23 +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 React, { useMemo } from 'react'; -import { TRANSACTION_METADATA_SECTIONS } from './sections'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { getSectionsWithRows } from '../helper'; -import { MetadataTable } from '..'; - -interface Props { - transaction: Transaction; -} - -export function TransactionMetadata({ transaction }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(TRANSACTION_METADATA_SECTIONS, transaction), - [transaction] - ); - return <MetadataTable sections={sectionsWithRows} />; -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts deleted file mode 100644 index b65b52bf30a5c..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts +++ /dev/null @@ -1,85 +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 { getSectionsWithRows, filterSectionsByTerm } from '../helper'; -import { LABELS, HTTP, SERVICE } from '../sections'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; - -describe('MetadataTable Helper', () => { - const sections = [ - { ...LABELS, required: true }, - HTTP, - { ...SERVICE, properties: ['environment'] } - ]; - const apmDoc = ({ - http: { - headers: { - Connection: 'close', - Host: 'opbeans:3000', - request: { method: 'get' } - } - }, - service: { - framework: { name: 'express' }, - environment: 'production' - } - } as unknown) as Transaction; - const metadataItems = getSectionsWithRows(sections, apmDoc); - - it('returns flattened data and required section', () => { - expect(metadataItems).toEqual([ - { key: 'labels', label: 'Labels', required: true, rows: [] }, - { - key: 'http', - label: 'HTTP', - rows: [ - { key: 'http.headers.Connection', value: 'close' }, - { key: 'http.headers.Host', value: 'opbeans:3000' }, - { key: 'http.headers.request.method', value: 'get' } - ] - }, - { - key: 'service', - label: 'Service', - properties: ['environment'], - rows: [{ key: 'service.environment', value: 'production' }] - } - ]); - }); - describe('filter', () => { - it('items by key', () => { - const filteredItems = filterSectionsByTerm(metadataItems, 'http'); - expect(filteredItems).toEqual([ - { - key: 'http', - label: 'HTTP', - rows: [ - { key: 'http.headers.Connection', value: 'close' }, - { key: 'http.headers.Host', value: 'opbeans:3000' }, - { key: 'http.headers.request.method', value: 'get' } - ] - } - ]); - }); - - it('items by value', () => { - const filteredItems = filterSectionsByTerm(metadataItems, 'product'); - expect(filteredItems).toEqual([ - { - key: 'service', - label: 'Service', - properties: ['environment'], - rows: [{ key: 'service.environment', value: 'production' }] - } - ]); - }); - - it('returns empty when no item matches', () => { - const filteredItems = filterSectionsByTerm(metadataItems, 'post'); - expect(filteredItems).toEqual([]); - }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/helper.ts b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/helper.ts deleted file mode 100644 index ef329abafa61b..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/helper.ts +++ /dev/null @@ -1,55 +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 { get, pick, isEmpty } from 'lodash'; -import { Section } from './sections'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { APMError } from '../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { flattenObject, KeyValuePair } from '../../../utils/flattenObject'; - -export type SectionsWithRows = ReturnType<typeof getSectionsWithRows>; - -export const getSectionsWithRows = ( - sections: Section[], - apmDoc: Transaction | APMError | Span -) => { - return sections - .map(section => { - const sectionData: Record<string, unknown> = get(apmDoc, section.key); - const filteredData: - | Record<string, unknown> - | undefined = section.properties - ? pick(sectionData, section.properties) - : sectionData; - - const rows: KeyValuePair[] = flattenObject(filteredData, section.key); - return { ...section, rows }; - }) - .filter(({ required, rows }) => required || !isEmpty(rows)); -}; - -export const filterSectionsByTerm = ( - sections: SectionsWithRows, - searchTerm: string -) => { - if (!searchTerm) { - return sections; - } - return sections - .map(section => { - const { rows = [] } = section; - const filteredRows = rows.filter(({ key, value }) => { - const valueAsString = String(value).toLowerCase(); - return ( - key.toLowerCase().includes(searchTerm) || - valueAsString.includes(searchTerm) - ); - }); - return { ...section, rows: filteredRows }; - }) - .filter(({ rows }) => !isEmpty(rows)); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts deleted file mode 100644 index 9b6d74033e1c5..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts +++ /dev/null @@ -1,173 +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 { IStackframe } from '../../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; -import { getGroupedStackframes } from '../index'; -import stacktracesMock from './stacktraces.json'; - -describe('Stacktrace/index', () => { - describe('getGroupedStackframes', () => { - it('should collapse the library frames into a set of grouped stackframes', () => { - const result = getGroupedStackframes(stacktracesMock as IStackframe[]); - expect(result).toMatchSnapshot(); - }); - - it('should group stackframes when `library_frame` is identical and `exclude_from_grouping` is false', () => { - const stackframes = [ - { - library_frame: false, - exclude_from_grouping: false, - filename: 'file-a.txt' - }, - { - library_frame: false, - exclude_from_grouping: false, - filename: 'file-b.txt' - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'file-c.txt' - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'file-d.txt' - } - ] as IStackframe[]; - - const result = getGroupedStackframes(stackframes); - - expect(result).toEqual([ - { - excludeFromGrouping: false, - isLibraryFrame: false, - stackframes: [ - { - exclude_from_grouping: false, - filename: 'file-a.txt', - library_frame: false - }, - { - exclude_from_grouping: false, - filename: 'file-b.txt', - library_frame: false - } - ] - }, - { - excludeFromGrouping: false, - isLibraryFrame: true, - stackframes: [ - { - exclude_from_grouping: false, - filename: 'file-c.txt', - library_frame: true - }, - { - exclude_from_grouping: false, - filename: 'file-d.txt', - library_frame: true - } - ] - } - ]); - }); - - it('should not group stackframes when `library_frame` is the different', () => { - const stackframes = [ - { - library_frame: false, - exclude_from_grouping: false, - filename: 'file-a.txt' - }, - { - library_frame: true, - exclude_from_grouping: false, - filename: 'file-b.txt' - } - ] as IStackframe[]; - const result = getGroupedStackframes(stackframes); - expect(result).toEqual([ - { - excludeFromGrouping: false, - isLibraryFrame: false, - stackframes: [ - { - exclude_from_grouping: false, - filename: 'file-a.txt', - library_frame: false - } - ] - }, - { - excludeFromGrouping: false, - isLibraryFrame: true, - stackframes: [ - { - exclude_from_grouping: false, - filename: 'file-b.txt', - library_frame: true - } - ] - } - ]); - }); - - it('should not group stackframes when `exclude_from_grouping` is true', () => { - const stackframes = [ - { - library_frame: false, - exclude_from_grouping: false, - filename: 'file-a.txt' - }, - { - library_frame: false, - exclude_from_grouping: true, - filename: 'file-b.txt' - } - ] as IStackframe[]; - const result = getGroupedStackframes(stackframes); - expect(result).toEqual([ - { - excludeFromGrouping: false, - isLibraryFrame: false, - stackframes: [ - { - exclude_from_grouping: false, - filename: 'file-a.txt', - library_frame: false - } - ] - }, - { - excludeFromGrouping: true, - isLibraryFrame: false, - stackframes: [ - { - exclude_from_grouping: true, - filename: 'file-b.txt', - library_frame: false - } - ] - } - ]); - }); - - it('should handle empty stackframes', () => { - const result = getGroupedStackframes([] as IStackframe[]); - expect(result).toHaveLength(0); - }); - - it('should handle one stackframe', () => { - const result = getGroupedStackframes([ - stacktracesMock[0] - ] as IStackframe[]); - expect(result).toHaveLength(1); - expect(result[0].stackframes).toHaveLength(1); - }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx deleted file mode 100644 index 141ed544a6166..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/index.tsx +++ /dev/null @@ -1,103 +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 { EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEmpty, last } from 'lodash'; -import React, { Fragment } from 'react'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; -import { EmptyMessage } from '../../shared/EmptyMessage'; -import { LibraryStacktrace } from './LibraryStacktrace'; -import { Stackframe } from './Stackframe'; - -interface Props { - stackframes?: IStackframe[]; - codeLanguage?: string; -} - -export function Stacktrace({ stackframes = [], codeLanguage }: Props) { - if (isEmpty(stackframes)) { - return ( - <EmptyMessage - heading={i18n.translate( - 'xpack.apm.stacktraceTab.noStacktraceAvailableLabel', - { - defaultMessage: 'No stack trace available.' - } - )} - hideSubheading - /> - ); - } - - const groups = getGroupedStackframes(stackframes); - - return ( - <Fragment> - {groups.map((group, i) => { - // library frame - if (group.isLibraryFrame && groups.length > 1) { - return ( - <Fragment key={i}> - <EuiSpacer size="m" /> - <LibraryStacktrace - id={i.toString()} - stackframes={group.stackframes} - codeLanguage={codeLanguage} - /> - <EuiSpacer size="m" /> - </Fragment> - ); - } - - // non-library frame - return group.stackframes.map((stackframe, idx) => ( - <Fragment key={`${i}-${idx}`}> - {idx > 0 && <EuiSpacer size="m" />} - <Stackframe - codeLanguage={codeLanguage} - id={`${i}-${idx}`} - initialIsOpen={i === 0 && groups.length > 1} - stackframe={stackframe} - /> - </Fragment> - )); - })} - <EuiSpacer size="m" /> - </Fragment> - ); -} - -interface StackframesGroup { - isLibraryFrame: boolean; - excludeFromGrouping: boolean; - stackframes: IStackframe[]; -} - -export function getGroupedStackframes(stackframes: IStackframe[]) { - return stackframes.reduce((acc, stackframe) => { - const prevGroup = last(acc); - const shouldAppend = - prevGroup && - prevGroup.isLibraryFrame === stackframe.library_frame && - !prevGroup.excludeFromGrouping && - !stackframe.exclude_from_grouping; - - // append to group - if (shouldAppend) { - prevGroup.stackframes.push(stackframe); - return acc; - } - - // create new group - acc.push({ - isLibraryFrame: Boolean(stackframe.library_frame), - excludeFromGrouping: Boolean(stackframe.exclude_from_grouping), - stackframes: [stackframe] - }); - return acc; - }, [] as StackframesGroup[]); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx deleted file mode 100644 index 94ae9e63644b9..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ /dev/null @@ -1,60 +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 React from 'react'; -import { EuiToolTip, EuiBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { units, px, truncate, unit } from '../../../../style/variables'; -import { HttpStatusBadge } from '../HttpStatusBadge'; - -const HttpInfoBadge = styled(EuiBadge)` - margin-right: ${px(units.quarter)}; -`; - -const Url = styled('span')` - display: inline-block; - vertical-align: bottom; - ${truncate(px(unit * 24))}; -`; -interface HttpInfoProps { - method?: string; - status?: number; - url: string; -} - -const Span = styled('span')` - white-space: nowrap; -`; - -export function HttpInfoSummaryItem({ status, method, url }: HttpInfoProps) { - if (!url) { - return null; - } - - const methodLabel = i18n.translate( - 'xpack.apm.transactionDetails.requestMethodLabel', - { - defaultMessage: 'Request method' - } - ); - - return ( - <Span> - <HttpInfoBadge title={undefined}> - {method && ( - <EuiToolTip content={methodLabel}> - <>{method.toUpperCase()}</> - </EuiToolTip> - )}{' '} - <EuiToolTip content={url}> - <Url>{url}</Url> - </EuiToolTip> - </HttpInfoBadge> - {status && <HttpStatusBadge status={status} />} - </Span> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx deleted file mode 100644 index ef99f3a4933a7..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/index.tsx +++ /dev/null @@ -1,47 +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 React from 'react'; -import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { px, units } from '../../../../public/style/variables'; -import { Maybe } from '../../../../../../../plugins/apm/typings/common'; - -interface Props { - items: Array<Maybe<React.ReactElement>>; -} - -// TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840) -const theme = euiLightVars; - -const Item = styled(EuiFlexItem)` - flex-wrap: nowrap; - border-right: 1px solid ${theme.euiColorLightShade}; - padding-right: ${px(units.half)}; - flex-flow: row nowrap; - line-height: 1.5; - align-items: center !important; - &:last-child { - border-right: none; - padding-right: 0; - } -`; - -const Summary = ({ items }: Props) => { - const filteredItems = items.filter(Boolean) as React.ReactElement[]; - - return ( - <EuiFlexGrid gutterSize="s"> - {filteredItems.map((item, index) => ( - <Item key={index} grow={false}> - {item} - </Item> - ))} - </EuiFlexGrid> - ); -}; - -export { Summary }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx deleted file mode 100644 index 9d1eeb9a3136d..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx +++ /dev/null @@ -1,128 +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 React from 'react'; -import { render, act, fireEvent } from '@testing-library/react'; -import { CustomLink } from '.'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { - expectTextsInDocument, - expectTextsNotInDocument -} from '../../../../utils/testHelpers'; -import { CustomLink as CustomLinkType } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; - -describe('Custom links', () => { - it('shows empty message when no custom link is available', () => { - const component = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - /> - ); - - expectTextsInDocument(component, [ - 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.' - ]); - expectTextsNotInDocument(component, ['Create']); - }); - - it('shows loading while custom links are fetched', () => { - const { getByTestId } = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.LOADING} - /> - ); - expect(getByTestId('loading-spinner')).toBeInTheDocument(); - }); - - it('shows first 3 custom links available', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'foo' }, - { id: '2', label: 'bar', url: 'bar' }, - { id: '3', label: 'baz', url: 'baz' }, - { id: '4', label: 'qux', url: 'qux' } - ] as CustomLinkType[]; - const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - /> - ); - expectTextsInDocument(component, ['foo', 'bar', 'baz']); - expectTextsNotInDocument(component, ['qux']); - }); - - it('clicks on See more button', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'foo' }, - { id: '2', label: 'bar', url: 'bar' }, - { id: '3', label: 'baz', url: 'baz' }, - { id: '4', label: 'qux', url: 'qux' } - ] as CustomLinkType[]; - const onSeeMoreClickMock = jest.fn(); - const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={onSeeMoreClickMock} - status={FETCH_STATUS.SUCCESS} - /> - ); - expect(onSeeMoreClickMock).not.toHaveBeenCalled(); - act(() => { - fireEvent.click(component.getByText('See more')); - }); - expect(onSeeMoreClickMock).toHaveBeenCalled(); - }); - - describe('create custom link buttons', () => { - it('shows create button below empty message', () => { - const component = render( - <CustomLink - customLinks={[]} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - /> - ); - - expectTextsInDocument(component, ['Create custom link']); - expectTextsNotInDocument(component, ['Create']); - }); - it('shows create button besides the title', () => { - const customLinks = [ - { id: '1', label: 'foo', url: 'foo' }, - { id: '2', label: 'bar', url: 'bar' }, - { id: '3', label: 'baz', url: 'baz' }, - { id: '4', label: 'qux', url: 'qux' } - ] as CustomLinkType[]; - const component = render( - <CustomLink - customLinks={customLinks} - transaction={({} as unknown) as Transaction} - onCreateCustomLinkClick={jest.fn()} - onSeeMoreClick={jest.fn()} - status={FETCH_STATUS.SUCCESS} - /> - ); - expectTextsInDocument(component, ['Create']); - expectTextsNotInDocument(component, ['Create custom link']); - }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx deleted file mode 100644 index 38b672a181fce..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx +++ /dev/null @@ -1,128 +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 React from 'react'; -import { - EuiText, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiButtonEmpty -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; -import { CustomLink as CustomLinkType } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { - ActionMenuDivider, - SectionSubtitle -} from '../../../../../../../../plugins/observability/public'; -import { CustomLinkSection } from './CustomLinkSection'; -import { ManageCustomLink } from './ManageCustomLink'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { LoadingStatePrompt } from '../../LoadingStatePrompt'; -import { px } from '../../../../style/variables'; - -const SeeMoreButton = styled.button<{ show: boolean }>` - display: ${props => (props.show ? 'flex' : 'none')}; - align-items: center; - width: 100%; - justify-content: space-between; - &:hover { - text-decoration: underline; - } -`; - -export const CustomLink = ({ - customLinks, - status, - onCreateCustomLinkClick, - onSeeMoreClick, - transaction -}: { - customLinks: CustomLinkType[]; - status: FETCH_STATUS; - onCreateCustomLinkClick: () => void; - onSeeMoreClick: () => void; - transaction: Transaction; -}) => { - const renderEmptyPrompt = ( - <> - <EuiText size="xs" grow={false} style={{ width: px(300) }}> - {i18n.translate('xpack.apm.customLink.empty', { - defaultMessage: - 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.' - })} - </EuiText> - <EuiSpacer size="s" /> - <EuiButtonEmpty - iconType="plusInCircle" - size="xs" - onClick={onCreateCustomLinkClick} - > - {i18n.translate('xpack.apm.customLink.buttom.create', { - defaultMessage: 'Create custom link' - })} - </EuiButtonEmpty> - </> - ); - - const renderCustomLinkBottomSection = isEmpty(customLinks) ? ( - renderEmptyPrompt - ) : ( - <SeeMoreButton onClick={onSeeMoreClick} show={customLinks.length > 3}> - <EuiText size="s"> - {i18n.translate('xpack.apm.transactionActionMenu.customLink.seeMore', { - defaultMessage: 'See more' - })} - </EuiText> - <EuiIcon type="arrowRight" /> - </SeeMoreButton> - ); - - return ( - <> - <ActionMenuDivider /> - <EuiFlexGroup> - <EuiFlexItem style={{ justifyContent: 'center' }}> - <EuiText size={'s'} grow={false}> - <h5> - {i18n.translate( - 'xpack.apm.transactionActionMenu.customLink.section', - { - defaultMessage: 'Custom Links' - } - )} - </h5> - </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <ManageCustomLink - onCreateCustomLinkClick={onCreateCustomLinkClick} - showCreateCustomLinkButton={!!customLinks.length} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="s" /> - <SectionSubtitle> - {i18n.translate('xpack.apm.transactionActionMenu.customLink.subtitle', { - defaultMessage: 'Links will open in a new window.' - })} - </SectionSubtitle> - <CustomLinkSection - customLinks={customLinks.slice(0, 3)} - transaction={transaction} - /> - <EuiSpacer size="s" /> - {status === FETCH_STATUS.LOADING ? ( - <LoadingStatePrompt /> - ) : ( - renderCustomLinkBottomSection - )} - </> - ); -}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx deleted file mode 100644 index 50ea169c017f9..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx +++ /dev/null @@ -1,58 +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 React, { useMemo } from 'react'; -import numeral from '@elastic/numeral'; -import { throttle } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -import { - Coordinate, - TimeSeries -} from '../../../../../../../../plugins/apm/typings/timeseries'; -import { Maybe } from '../../../../../../../../plugins/apm/typings/common'; -import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; -import { asPercent } from '../../../../utils/formatters'; -import { unit } from '../../../../style/variables'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { useUiTracker } from '../../../../../../../../plugins/observability/public'; - -interface Props { - timeseries: TimeSeries[]; -} - -const tickFormatY = (y: Maybe<number>) => { - return numeral(y || 0).format('0 %'); -}; - -const formatTooltipValue = (coordinate: Coordinate) => { - return isValidCoordinateValue(coordinate.y) - ? asPercent(coordinate.y, 1) - : NOT_AVAILABLE_LABEL; -}; - -const TransactionBreakdownGraph: React.FC<Props> = props => { - const { timeseries } = props; - const trackApmEvent = useUiTracker({ app: 'apm' }); - const handleHover = useMemo( - () => - throttle(() => trackApmEvent({ metric: 'hover_breakdown_chart' }), 60000), - [trackApmEvent] - ); - - return ( - <TransactionLineChart - series={timeseries} - tickFormatY={tickFormatY} - formatTooltipValue={formatTooltipValue} - yMax={1} - height={unit * 12} - stacked={true} - onHover={handleHover} - /> - ); -}; - -export { TransactionBreakdownGraph }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx deleted file mode 100644 index 85f5f83fb920e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React, { useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown'; -import { TransactionBreakdownHeader } from './TransactionBreakdownHeader'; -import { TransactionBreakdownKpiList } from './TransactionBreakdownKpiList'; -import { TransactionBreakdownGraph } from './TransactionBreakdownGraph'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { useUiTracker } from '../../../../../../../plugins/observability/public'; - -const emptyMessage = i18n.translate('xpack.apm.transactionBreakdown.noData', { - defaultMessage: 'No data within this time range.' -}); - -const TransactionBreakdown: React.FC<{ - initialIsOpen?: boolean; -}> = ({ initialIsOpen }) => { - const [showChart, setShowChart] = useState(!!initialIsOpen); - const { data, status } = useTransactionBreakdown(); - const trackApmEvent = useUiTracker({ app: 'apm' }); - const { kpis, timeseries } = data; - const noHits = data.kpis.length === 0 && status === FETCH_STATUS.SUCCESS; - const showEmptyMessage = noHits && !showChart; - - return ( - <EuiPanel> - <EuiFlexGroup direction="column" gutterSize="s"> - <EuiFlexItem grow={false}> - <TransactionBreakdownHeader - showChart={showChart} - onToggleClick={() => { - setShowChart(!showChart); - if (showChart) { - trackApmEvent({ metric: 'hide_breakdown_chart' }); - } else { - trackApmEvent({ metric: 'show_breakdown_chart' }); - } - }} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - {showEmptyMessage ? ( - <EuiText>{emptyMessage}</EuiText> - ) : ( - <TransactionBreakdownKpiList kpis={kpis} /> - )} - </EuiFlexItem> - {showChart ? ( - <EuiFlexItem grow={false}> - <TransactionBreakdownGraph timeseries={timeseries} /> - </EuiFlexItem> - ) : null} - </EuiFlexGroup> - </EuiPanel> - ); -}; - -export { TransactionBreakdown }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx deleted file mode 100644 index 077e6535a8b21..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx +++ /dev/null @@ -1,149 +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 React from 'react'; -import { map } from 'lodash'; -import { EuiFieldNumber, EuiSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ForLastExpression } from '../../../../../../../plugins/triggers_actions_ui/public'; -import { - TRANSACTION_ALERT_AGGREGATION_TYPES, - ALERT_TYPES_CONFIG -} from '../../../../../../../plugins/apm/common/alert_types'; -import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; -import { useUrlParams } from '../../../hooks/useUrlParams'; -import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; -import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; - -interface Params { - windowSize: number; - windowUnit: string; - threshold: number; - aggregationType: 'avg' | '95th' | '99th'; - serviceName: string; - transactionType: string; -} - -interface Props { - alertParams: Params; - setAlertParams: (key: string, value: any) => void; - setAlertProperty: (key: string, value: any) => void; -} - -export function TransactionDurationAlertTrigger(props: Props) { - const { setAlertParams, alertParams, setAlertProperty } = props; - - const { urlParams } = useUrlParams(); - - const transactionTypes = useServiceTransactionTypes(urlParams); - - if (!transactionTypes.length) { - return null; - } - - const defaults = { - threshold: 1500, - aggregationType: 'avg', - windowSize: 5, - windowUnit: 'm', - transactionType: transactionTypes[0] - }; - - const params = { - ...defaults, - ...alertParams - }; - - const fields = [ - <PopoverExpression - value={params.transactionType} - title={i18n.translate('xpack.apm.transactionDurationAlertTrigger.type', { - defaultMessage: 'Type' - })} - > - <EuiSelect - value={params.transactionType} - options={transactionTypes.map(key => { - return { - text: key, - value: key - }; - })} - onChange={e => - setAlertParams( - 'transactionType', - e.target.value as Params['transactionType'] - ) - } - compressed - /> - </PopoverExpression>, - <PopoverExpression - value={params.aggregationType} - title={i18n.translate('xpack.apm.transactionDurationAlertTrigger.when', { - defaultMessage: 'When' - })} - > - <EuiSelect - value={params.aggregationType} - options={map(TRANSACTION_ALERT_AGGREGATION_TYPES, (label, key) => { - return { - text: label, - value: key - }; - })} - onChange={e => - setAlertParams( - 'aggregationType', - e.target.value as Params['aggregationType'] - ) - } - compressed - /> - </PopoverExpression>, - <PopoverExpression - value={params.threshold ? `${params.threshold}ms` : ''} - title={i18n.translate( - 'xpack.apm.transactionDurationAlertTrigger.isAbove', - { - defaultMessage: 'is above' - } - )} - > - <EuiFieldNumber - value={params.threshold ?? ''} - onChange={e => setAlertParams('threshold', e.target.value)} - append={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', { - defaultMessage: 'ms' - })} - compressed - /> - </PopoverExpression>, - <ForLastExpression - onChangeWindowSize={timeWindowSize => - setAlertParams('windowSize', timeWindowSize || '') - } - onChangeWindowUnit={timeWindowUnit => - setAlertParams('windowUnit', timeWindowUnit) - } - timeWindowSize={params.windowSize} - timeWindowUnit={params.windowUnit} - errors={{ - timeWindowSize: [], - timeWindowUnit: [] - }} - /> - ]; - - return ( - <ServiceAlertTrigger - alertTypeName={ALERT_TYPES_CONFIG['apm.transaction_duration'].name} - fields={fields} - defaults={defaults} - setAlertParams={setAlertParams} - setAlertProperty={setAlertProperty} - /> - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx deleted file mode 100644 index 2ceac87d9aab3..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx +++ /dev/null @@ -1,105 +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 { EuiTitle } from '@elastic/eui'; -import React from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GenericMetricsChart } from '../../../../../../../../plugins/apm/server/lib/metrics/transform_metrics_chart'; -// @ts-ignore -import CustomPlot from '../CustomPlot'; -import { - asDecimal, - asPercent, - asInteger, - asDynamicBytes, - getFixedByteFormatter, - asDuration -} from '../../../../utils/formatters'; -import { Coordinate } from '../../../../../../../../plugins/apm/typings/timeseries'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { useChartsSync } from '../../../../hooks/useChartsSync'; -import { Maybe } from '../../../../../../../../plugins/apm/typings/common'; - -interface Props { - start: Maybe<number | string>; - end: Maybe<number | string>; - chart: GenericMetricsChart; -} - -export function MetricsChart({ chart }: Props) { - const formatYValue = getYTickFormatter(chart); - const formatTooltip = getTooltipFormatter(chart); - - const transformedSeries = chart.series.map(series => ({ - ...series, - legendValue: formatYValue(series.overallValue) - })); - - const syncedChartProps = useChartsSync(); - - return ( - <React.Fragment> - <EuiTitle size="xs"> - <span>{chart.title}</span> - </EuiTitle> - <CustomPlot - {...syncedChartProps} - series={transformedSeries} - tickFormatY={formatYValue} - formatTooltipValue={formatTooltip} - yMax={chart.yUnit === 'percent' ? 1 : 'max'} - /> - </React.Fragment> - ); -} - -function getYTickFormatter(chart: GenericMetricsChart) { - switch (chart.yUnit) { - case 'bytes': { - const max = Math.max( - ...chart.series.map(({ data }) => - Math.max(...data.map(({ y }) => y || 0)) - ) - ); - return getFixedByteFormatter(max); - } - case 'percent': { - return (y: Maybe<number>) => asPercent(y || 0, 1); - } - case 'time': { - return (y: Maybe<number>) => asDuration(y); - } - case 'integer': { - return (y: Maybe<number>) => - isValidCoordinateValue(y) ? asInteger(y) : y; - } - default: { - return (y: Maybe<number>) => - isValidCoordinateValue(y) ? asDecimal(y) : y; - } - } -} - -function getTooltipFormatter({ yUnit }: GenericMetricsChart) { - switch (yUnit) { - case 'bytes': { - return (c: Coordinate) => asDynamicBytes(c.y); - } - case 'percent': { - return (c: Coordinate) => asPercent(c.y || 0, 1); - } - case 'time': { - return (c: Coordinate) => asDuration(c.y); - } - case 'integer': { - return (c: Coordinate) => - isValidCoordinateValue(c.y) ? asInteger(c.y) : c.y; - } - default: { - return (c: Coordinate) => - isValidCoordinateValue(c.y) ? asDecimal(c.y) : c.y; - } - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx deleted file mode 100644 index c9c31b05e264c..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; -import { - Coordinate, - RectCoordinate -} from '../../../../../../../../../plugins/apm/typings/timeseries'; -import { useChartsSync } from '../../../../../hooks/useChartsSync'; -// @ts-ignore -import CustomPlot from '../../CustomPlot'; - -interface Props { - series: Array<{ - color: string; - title: React.ReactNode; - titleShort?: React.ReactNode; - data: Array<Coordinate | RectCoordinate>; - type: string; - }>; - truncateLegends?: boolean; - tickFormatY: (y: number) => React.ReactNode; - formatTooltipValue: (c: Coordinate) => React.ReactNode; - yMax?: string | number; - height?: number; - stacked?: boolean; - onHover?: () => void; -} - -const TransactionLineChart: React.FC<Props> = (props: Props) => { - const { - series, - tickFormatY, - formatTooltipValue, - yMax = 'max', - height, - truncateLegends, - stacked = false, - onHover - } = props; - - const syncedChartsProps = useChartsSync(); - - // combine callback for syncedChartsProps.onHover and props.onHover - const combinedOnHover = useCallback( - (hoverX: number) => { - if (onHover) { - onHover(); - } - return syncedChartsProps.onHover(hoverX); - }, - [syncedChartsProps, onHover] - ); - - return ( - <CustomPlot - series={series} - {...syncedChartsProps} - onHover={combinedOnHover} - tickFormatY={tickFormatY} - formatTooltipValue={formatTooltipValue} - yMax={yMax} - height={height} - truncateLegends={truncateLegends} - {...(stacked ? { stackBy: 'y' } : {})} - /> - ); -}; - -export { TransactionLineChart }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx deleted file mode 100644 index 368a39e4ad228..0000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ /dev/null @@ -1,269 +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 { - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiPanel, - EuiText, - EuiTitle, - EuiSpacer -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Location } from 'history'; -import React, { Component } from 'react'; -import { isEmpty, flatten } from 'lodash'; -import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../plugins/apm/common/i18n'; -import { - Coordinate, - TimeSeries -} from '../../../../../../../../plugins/apm/typings/timeseries'; -import { ITransactionChartData } from '../../../../selectors/chartSelectors'; -import { IUrlParams } from '../../../../context/UrlParamsContext/types'; -import { - asInteger, - tpmUnit, - TimeFormatter, - getDurationFormatter -} from '../../../../utils/formatters'; -import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; -import { LicenseContext } from '../../../../context/LicenseContext'; -import { TransactionLineChart } from './TransactionLineChart'; -import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { BrowserLineChart } from './BrowserLineChart'; -import { DurationByCountryMap } from './DurationByCountryMap'; -import { - TRANSACTION_PAGE_LOAD, - TRANSACTION_ROUTE_CHANGE, - TRANSACTION_REQUEST -} from '../../../../../../../../plugins/apm/common/transaction_types'; - -interface TransactionChartProps { - hasMLJob: boolean; - charts: ITransactionChartData; - location: Location; - urlParams: IUrlParams; -} - -const ShiftedIconWrapper = styled.span` - padding-right: 5px; - position: relative; - top: -1px; - display: inline-block; -`; - -const ShiftedEuiText = styled(EuiText)` - position: relative; - top: 5px; -`; - -export function getResponseTimeTickFormatter(formatter: TimeFormatter) { - return (t: number) => formatter(t).formatted; -} - -export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { - return (p: Coordinate) => { - return isValidCoordinateValue(p.y) - ? formatter(p.y).formatted - : NOT_AVAILABLE_LABEL; - }; -} - -export function getMaxY(responseTimeSeries: TimeSeries[]) { - const coordinates = flatten( - responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) - ); - - const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0)); - - return Math.max(...numbers, 0); -} - -export class TransactionCharts extends Component<TransactionChartProps> { - public getTPMFormatter = (t: number) => { - const { urlParams } = this.props; - const unit = tpmUnit(urlParams.transactionType); - return `${asInteger(t)} ${unit}`; - }; - - public getTPMTooltipFormatter = (p: Coordinate) => { - return isValidCoordinateValue(p.y) - ? this.getTPMFormatter(p.y) - : NOT_AVAILABLE_LABEL; - }; - - public renderMLHeader(hasValidMlLicense: boolean | undefined) { - const { hasMLJob } = this.props; - if (!hasValidMlLicense || !hasMLJob) { - return null; - } - - const { serviceName, transactionType, kuery } = this.props.urlParams; - if (!serviceName) { - return null; - } - - const hasKuery = !isEmpty(kuery); - const icon = hasKuery ? ( - <EuiIconTip - aria-label="Warning" - type="alert" - color="warning" - content="The Machine learning results are hidden when the search bar is used for filtering" - /> - ) : ( - <EuiIconTip - content={i18n.translate( - 'xpack.apm.metrics.transactionChart.machineLearningTooltip', - { - defaultMessage: - 'The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores >= 75.' - } - )} - /> - ); - - return ( - <EuiFlexItem grow={false}> - <ShiftedEuiText size="xs"> - <ShiftedIconWrapper>{icon}</ShiftedIconWrapper> - <span> - {i18n.translate( - 'xpack.apm.metrics.transactionChart.machineLearningLabel', - { - defaultMessage: 'Machine learning:' - } - )}{' '} - </span> - <MLJobLink - serviceName={serviceName} - transactionType={transactionType} - > - View Job - </MLJobLink> - </ShiftedEuiText> - </EuiFlexItem> - ); - } - - public render() { - const { charts, urlParams } = this.props; - const { responseTimeSeries, tpmSeries } = charts; - const { transactionType } = urlParams; - const maxY = getMaxY(responseTimeSeries); - const formatter = getDurationFormatter(maxY); - - return ( - <> - <EuiFlexGrid columns={2} gutterSize="s"> - <EuiFlexItem data-cy={`transaction-duration-charts`}> - <EuiPanel> - <React.Fragment> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem> - <EuiTitle size="xs"> - <span>{responseTimeLabel(transactionType)}</span> - </EuiTitle> - </EuiFlexItem> - <LicenseContext.Consumer> - {license => - this.renderMLHeader(license?.getFeature('ml').isAvailable) - } - </LicenseContext.Consumer> - </EuiFlexGroup> - <TransactionLineChart - series={responseTimeSeries} - tickFormatY={getResponseTimeTickFormatter(formatter)} - formatTooltipValue={getResponseTimeTooltipFormatter( - formatter - )} - /> - </React.Fragment> - </EuiPanel> - </EuiFlexItem> - - <EuiFlexItem style={{ flexShrink: 1 }}> - <EuiPanel> - <React.Fragment> - <EuiTitle size="xs"> - <span>{tpmLabel(transactionType)}</span> - </EuiTitle> - <TransactionLineChart - series={tpmSeries} - tickFormatY={this.getTPMFormatter} - formatTooltipValue={this.getTPMTooltipFormatter} - truncateLegends - /> - </React.Fragment> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGrid> - {transactionType === TRANSACTION_PAGE_LOAD && ( - <> - <EuiSpacer size="s" /> - <EuiFlexGrid columns={2} gutterSize="s"> - <EuiFlexItem> - <EuiPanel> - <DurationByCountryMap /> - </EuiPanel> - </EuiFlexItem> - <EuiFlexItem> - <EuiPanel> - <BrowserLineChart /> - </EuiPanel> - </EuiFlexItem> - </EuiFlexGrid> - </> - )} - </> - ); - } -} - -function tpmLabel(type?: string) { - return type === TRANSACTION_REQUEST - ? i18n.translate( - 'xpack.apm.metrics.transactionChart.requestsPerMinuteLabel', - { - defaultMessage: 'Requests per minute' - } - ) - : i18n.translate( - 'xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel', - { - defaultMessage: 'Transactions per minute' - } - ); -} - -function responseTimeLabel(type?: string) { - switch (type) { - case TRANSACTION_PAGE_LOAD: - return i18n.translate( - 'xpack.apm.metrics.transactionChart.pageLoadTimesLabel', - { - defaultMessage: 'Page load times' - } - ); - case TRANSACTION_ROUTE_CHANGE: - return i18n.translate( - 'xpack.apm.metrics.transactionChart.routeChangeTimesLabel', - { - defaultMessage: 'Route change times' - } - ); - default: - return i18n.translate( - 'xpack.apm.metrics.transactionChart.transactionDurationLabel', - { - defaultMessage: 'Transaction duration' - } - ); - } -} diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx deleted file mode 100644 index acc3886586889..0000000000000 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx +++ /dev/null @@ -1,19 +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 { createContext } from 'react'; -import { AppMountContext } from 'kibana/public'; -import { ApmPluginSetupDeps, ConfigSchema } from '../../new-platform/plugin'; - -export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; - -export interface ApmPluginContextValue { - config: ConfigSchema; - core: AppMountContext['core']; - plugins: ApmPluginSetupDeps; -} - -export const ApmPluginContext = createContext({} as ApmPluginContextValue); diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx deleted file mode 100644 index 62cdbd3bbc995..0000000000000 --- a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx +++ /dev/null @@ -1,30 +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 React from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { ILicense } from '../../../../../../plugins/licensing/public'; -import { useApmPluginContext } from '../../hooks/useApmPluginContext'; -import { InvalidLicenseNotification } from './InvalidLicenseNotification'; - -export const LicenseContext = React.createContext<ILicense | undefined>( - undefined -); - -export function LicenseProvider({ children }: { children: React.ReactChild }) { - const { license$ } = useApmPluginContext().plugins.licensing; - const license = useObservable(license$); - // if license is not loaded yet, consider it valid - const hasInvalidLicense = license?.isActive === false; - - // if license is invalid show an error message - if (hasInvalidLicense) { - return <InvalidLicenseNotification />; - } - - // render rest of application and pass down license via context - return <LicenseContext.Provider value={license} children={children} />; -} diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts deleted file mode 100644 index b80db0e9ae073..0000000000000 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ /dev/null @@ -1,131 +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 { compact, pick } from 'lodash'; -import datemath from '@elastic/datemath'; -import { IUrlParams } from './types'; -import { ProcessorEvent } from '../../../../../../plugins/apm/common/processor_event'; - -interface PathParams { - processorEvent?: ProcessorEvent; - serviceName?: string; - errorGroupId?: string; - serviceNodeName?: string; - traceId?: string; -} - -export function getParsedDate(rawDate?: string, opts = {}) { - if (rawDate) { - const parsed = datemath.parse(rawDate, opts); - if (parsed) { - return parsed.toISOString(); - } - } -} - -export function getStart(prevState: IUrlParams, rangeFrom?: string) { - if (prevState.rangeFrom !== rangeFrom) { - return getParsedDate(rangeFrom); - } - return prevState.start; -} - -export function getEnd(prevState: IUrlParams, rangeTo?: string) { - if (prevState.rangeTo !== rangeTo) { - return getParsedDate(rangeTo, { roundUp: true }); - } - return prevState.end; -} - -export function toNumber(value?: string) { - if (value !== undefined) { - return parseInt(value, 10); - } -} - -export function toString(value?: string) { - if (value === '' || value === 'null' || value === 'undefined') { - return; - } - return value; -} - -export function toBoolean(value?: string) { - return value === 'true'; -} - -export function getPathAsArray(pathname: string = '') { - return compact(pathname.split('/')); -} - -export function removeUndefinedProps<T>(obj: T): Partial<T> { - return pick(obj, value => value !== undefined); -} - -export function getPathParams(pathname: string = ''): PathParams { - const paths = getPathAsArray(pathname); - const pageName = paths[0]; - // TODO: use react router's real match params instead of guessing the path order - - switch (pageName) { - case 'services': - let servicePageName = paths[2]; - const serviceName = paths[1]; - const serviceNodeName = paths[3]; - - if (servicePageName === 'nodes' && paths.length > 3) { - servicePageName = 'metrics'; - } - - switch (servicePageName) { - case 'transactions': - return { - processorEvent: ProcessorEvent.transaction, - serviceName - }; - case 'errors': - return { - processorEvent: ProcessorEvent.error, - serviceName, - errorGroupId: paths[3] - }; - case 'metrics': - return { - processorEvent: ProcessorEvent.metric, - serviceName, - serviceNodeName - }; - case 'nodes': - return { - processorEvent: ProcessorEvent.metric, - serviceName - }; - case 'service-map': - return { - serviceName - }; - default: - return {}; - } - - case 'traces': - return { - processorEvent: ProcessorEvent.transaction - }; - case 'link-to': - const link = paths[1]; - switch (link) { - case 'trace': - return { - traceId: paths[2] - }; - default: - return {}; - } - default: - return {}; - } -} diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx deleted file mode 100644 index 588936039c2bc..0000000000000 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/index.tsx +++ /dev/null @@ -1,101 +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 React, { - createContext, - useMemo, - useCallback, - useRef, - useState -} from 'react'; -import { withRouter } from 'react-router-dom'; -import { uniqueId, mapValues } from 'lodash'; -import { IUrlParams } from './types'; -import { getParsedDate } from './helpers'; -import { resolveUrlParams } from './resolveUrlParams'; -import { UIFilters } from '../../../../../../plugins/apm/typings/ui_filters'; -import { - localUIFilterNames, - LocalUIFilterName - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { pickKeys } from '../../utils/pickKeys'; -import { useDeepObjectIdentity } from '../../hooks/useDeepObjectIdentity'; - -interface TimeRange { - rangeFrom: string; - rangeTo: string; -} - -function useUiFilters(params: IUrlParams): UIFilters { - const { kuery, environment, ...urlParams } = params; - const localUiFilters = mapValues( - pickKeys(urlParams, ...localUIFilterNames), - val => (val ? val.split(',') : []) - ) as Partial<Record<LocalUIFilterName, string[]>>; - - return useDeepObjectIdentity({ kuery, environment, ...localUiFilters }); -} - -const defaultRefresh = (time: TimeRange) => {}; - -const UrlParamsContext = createContext({ - urlParams: {} as IUrlParams, - refreshTimeRange: defaultRefresh, - uiFilters: {} as UIFilters -}); - -const UrlParamsProvider: React.ComponentClass<{}> = withRouter( - ({ location, children }) => { - const refUrlParams = useRef(resolveUrlParams(location, {})); - - const { start, end, rangeFrom, rangeTo } = refUrlParams.current; - - const [, forceUpdate] = useState(''); - - const urlParams = useMemo( - () => - resolveUrlParams(location, { - start, - end, - rangeFrom, - rangeTo - }), - [location, start, end, rangeFrom, rangeTo] - ); - - refUrlParams.current = urlParams; - - const refreshTimeRange = useCallback( - (timeRange: TimeRange) => { - refUrlParams.current = { - ...refUrlParams.current, - start: getParsedDate(timeRange.rangeFrom), - end: getParsedDate(timeRange.rangeTo, { roundUp: true }) - }; - - forceUpdate(uniqueId()); - }, - [forceUpdate] - ); - - const uiFilters = useUiFilters(urlParams); - - const contextValue = useMemo(() => { - return { - urlParams, - refreshTimeRange, - uiFilters - }; - }, [urlParams, refreshTimeRange, uiFilters]); - - return ( - <UrlParamsContext.Provider children={children} value={contextValue} /> - ); - } -); - -export { UrlParamsContext, UrlParamsProvider, useUiFilters }; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts deleted file mode 100644 index acde09308ab46..0000000000000 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts +++ /dev/null @@ -1,37 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { ProcessorEvent } from '../../../../../../plugins/apm/common/processor_event'; - -export type IUrlParams = { - detailTab?: string; - end?: string; - errorGroupId?: string; - flyoutDetailTab?: string; - kuery?: string; - environment?: string; - rangeFrom?: string; - rangeTo?: string; - refreshInterval?: number; - refreshPaused?: boolean; - serviceName?: string; - sortDirection?: string; - sortField?: string; - start?: string; - traceId?: string; - transactionId?: string; - transactionName?: string; - transactionType?: string; - waterfallItemId?: string; - page?: number; - pageSize?: number; - serviceNodeName?: string; - searchTerm?: string; - processorEvent?: ProcessorEvent; - traceIdLink?: string; -} & Partial<Record<LocalUIFilterName, string>>; diff --git a/x-pack/legacy/plugins/apm/public/index.scss b/x-pack/legacy/plugins/apm/public/index.scss deleted file mode 100644 index 04a070c304d6f..0000000000000 --- a/x-pack/legacy/plugins/apm/public/index.scss +++ /dev/null @@ -1,16 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -/* APM plugin styles */ - -// Prefix all styles with "apm" to avoid conflicts. -// Examples -// apmChart -// apmChart__legend -// apmChart__legend--small -// apmChart__legend-isLoading - -.apmReactRoot { - overflow-x: auto; - height: 100%; -} diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx deleted file mode 100644 index 59b2fedaafba6..0000000000000 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ /dev/null @@ -1,36 +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 { npSetup, npStart } from 'ui/new_platform'; -import 'react-vis/dist/style.css'; -import { PluginInitializerContext } from 'kibana/public'; -import 'ui/autoload/all'; -import chrome from 'ui/chrome'; -import { plugin } from './new-platform'; -import { REACT_APP_ROOT_ID } from './new-platform/plugin'; -import './style/global_overrides.css'; -import template from './templates/index.html'; - -// This will be moved to core.application.register when the new platform -// migration is complete. -// @ts-ignore -chrome.setRootTemplate(template); - -const checkForRoot = () => { - return new Promise(resolve => { - const ready = !!document.getElementById(REACT_APP_ROOT_ID); - if (ready) { - resolve(); - } else { - setTimeout(() => resolve(checkForRoot()), 10); - } - }); -}; -checkForRoot().then(() => { - const pluginInstance = plugin({} as PluginInitializerContext); - pluginInstance.setup(npSetup.core, npSetup.plugins); - pluginInstance.start(npStart.core, npStart.plugins); -}); diff --git a/x-pack/legacy/plugins/apm/public/legacy_register_feature.ts b/x-pack/legacy/plugins/apm/public/legacy_register_feature.ts deleted file mode 100644 index f12865399054e..0000000000000 --- a/x-pack/legacy/plugins/apm/public/legacy_register_feature.ts +++ /dev/null @@ -1,24 +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 { npSetup } from 'ui/new_platform'; -import { featureCatalogueEntry } from './new-platform/featureCatalogueEntry'; - -const { - core, - plugins: { home } -} = npSetup; -const apmUiEnabled = core.injectedMetadata.getInjectedVar( - 'apmUiEnabled' -) as boolean; - -if (apmUiEnabled) { - home.featureCatalogue.register(featureCatalogueEntry); -} - -home.environment.update({ - apmUi: apmUiEnabled -}); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/getConfigFromInjectedMetadata.ts b/x-pack/legacy/plugins/apm/public/new-platform/getConfigFromInjectedMetadata.ts deleted file mode 100644 index 6dc77f7733b2d..0000000000000 --- a/x-pack/legacy/plugins/apm/public/new-platform/getConfigFromInjectedMetadata.ts +++ /dev/null @@ -1,24 +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 { npStart } from 'ui/new_platform'; -import { ConfigSchema } from './plugin'; - -const { core } = npStart; - -export function getConfigFromInjectedMetadata(): ConfigSchema { - const { - apmIndexPatternTitle, - apmServiceMapEnabled, - apmUiEnabled - } = core.injectedMetadata.getInjectedVars(); - - return { - indexPatternTitle: `${apmIndexPatternTitle}`, - serviceMapEnabled: !!apmServiceMapEnabled, - ui: { enabled: !!apmUiEnabled } - }; -} diff --git a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx b/x-pack/legacy/plugins/apm/public/new-platform/index.tsx deleted file mode 100644 index 0674dc48316f4..0000000000000 --- a/x-pack/legacy/plugins/apm/public/new-platform/index.tsx +++ /dev/null @@ -1,13 +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 { PluginInitializer } from '../../../../../../src/core/public'; -import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; - -export const plugin: PluginInitializer< - ApmPluginSetup, - ApmPluginStart -> = pluginInitializerContext => new ApmPlugin(pluginInitializerContext); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx deleted file mode 100644 index 80a45ba66c4fa..0000000000000 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ /dev/null @@ -1,203 +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 { ApmRoute } from '@elastic/apm-rum-react'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Route, Router, Switch } from 'react-router-dom'; -import styled from 'styled-components'; -import { - CoreSetup, - CoreStart, - Plugin, - PluginInitializerContext -} from '../../../../../../src/core/public'; -import { DataPublicPluginSetup } from '../../../../../../src/plugins/data/public'; -import { HomePublicPluginSetup } from '../../../../../../src/plugins/home/public'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { PluginSetupContract as AlertingPluginPublicSetup } from '../../../../../plugins/alerting/public'; -import { AlertType } from '../../../../../plugins/apm/common/alert_types'; -import { LicensingPluginSetup } from '../../../../../plugins/licensing/public'; -import { - AlertsContextProvider, - TriggersAndActionsUIPublicPluginSetup -} from '../../../../../plugins/triggers_actions_ui/public'; -import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; -import { routes } from '../components/app/Main/route_config'; -import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; -import { ErrorRateAlertTrigger } from '../components/shared/ErrorRateAlertTrigger'; -import { TransactionDurationAlertTrigger } from '../components/shared/TransactionDurationAlertTrigger'; -import { ApmPluginContext } from '../context/ApmPluginContext'; -import { LicenseProvider } from '../context/LicenseContext'; -import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; -import { LocationProvider } from '../context/LocationContext'; -import { MatchedRouteProvider } from '../context/MatchedRouteContext'; -import { UrlParamsProvider } from '../context/UrlParamsContext'; -import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { createStaticIndexPattern } from '../services/rest/index_pattern'; -import { px, unit, units } from '../style/variables'; -import { history } from '../utils/history'; -import { featureCatalogueEntry } from './featureCatalogueEntry'; -import { getConfigFromInjectedMetadata } from './getConfigFromInjectedMetadata'; -import { setHelpExtension } from './setHelpExtension'; -import { toggleAppLinkInNav } from './toggleAppLinkInNav'; -import { setReadonlyBadge } from './updateBadge'; - -export const REACT_APP_ROOT_ID = 'react-apm-root'; - -const MainContainer = styled.div` - min-width: ${px(unit * 50)}; - padding: ${px(units.plus)}; - height: 100%; -`; - -const App = () => { - return ( - <MainContainer data-test-subj="apmMainContainer" role="main"> - <UpdateBreadcrumbs routes={routes} /> - <Route component={ScrollToTopOnPathChange} /> - <APMIndicesPermission> - <Switch> - {routes.map((route, i) => ( - <ApmRoute key={i} {...route} /> - ))} - </Switch> - </APMIndicesPermission> - </MainContainer> - ); -}; - -export type ApmPluginSetup = void; -export type ApmPluginStart = void; - -export interface ApmPluginSetupDeps { - alerting?: AlertingPluginPublicSetup; - data: DataPublicPluginSetup; - home: HomePublicPluginSetup; - licensing: LicensingPluginSetup; - triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; -} - -export interface ConfigSchema { - indexPatternTitle: string; - serviceMapEnabled: boolean; - ui: { - enabled: boolean; - }; -} - -export class ApmPlugin - implements Plugin<ApmPluginSetup, ApmPluginStart, ApmPluginSetupDeps, {}> { - // When we switch over from the old platform to new platform the plugins will - // be coming from setup instead of start, since that's where we do - // `core.application.register`. During the transitions we put plugins on an - // instance property so we can use it in start. - setupPlugins: ApmPluginSetupDeps = {} as ApmPluginSetupDeps; - - constructor( - // @ts-ignore Not using initializerContext now, but will be once NP - // migration is complete. - private readonly initializerContext: PluginInitializerContext<ConfigSchema> - ) {} - - // Take the DOM element as the constructor, so we can mount the app. - public setup(_core: CoreSetup, plugins: ApmPluginSetupDeps) { - plugins.home.featureCatalogue.register(featureCatalogueEntry); - this.setupPlugins = plugins; - } - - public start(core: CoreStart) { - const i18nCore = core.i18n; - const plugins = this.setupPlugins; - createCallApmApi(core.http); - - // Once we're actually an NP plugin we'll get the config from the - // initializerContext like: - // - // const config = this.initializerContext.config.get<ConfigSchema>(); - // - // Until then we use a shim to get it from legacy injectedMetadata: - const config = getConfigFromInjectedMetadata(); - - // render APM feedback link in global help menu - setHelpExtension(core); - setReadonlyBadge(core); - toggleAppLinkInNav(core, config); - - const apmPluginContextValue = { - config, - core, - plugins - }; - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.ErrorRate, - name: i18n.translate('xpack.apm.alertTypes.errorRate', { - defaultMessage: 'Error rate' - }), - iconClass: 'bell', - alertParamsExpression: ErrorRateAlertTrigger, - validate: () => ({ - errors: [] - }) - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDuration, - name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { - defaultMessage: 'Transaction duration' - }), - iconClass: 'bell', - alertParamsExpression: TransactionDurationAlertTrigger, - validate: () => ({ - errors: [] - }) - }); - - ReactDOM.render( - <ApmPluginContext.Provider value={apmPluginContextValue}> - <AlertsContextProvider - value={{ - http: core.http, - docLinks: core.docLinks, - toastNotifications: core.notifications.toasts, - actionTypeRegistry: plugins.triggers_actions_ui.actionTypeRegistry, - alertTypeRegistry: plugins.triggers_actions_ui.alertTypeRegistry - }} - > - <KibanaContextProvider services={{ ...core, ...plugins }}> - <i18nCore.Context> - <Router history={history}> - <LocationProvider> - <MatchedRouteProvider routes={routes}> - <UrlParamsProvider> - <LoadingIndicatorProvider> - <LicenseProvider> - <App /> - </LicenseProvider> - </LoadingIndicatorProvider> - </UrlParamsProvider> - </MatchedRouteProvider> - </LocationProvider> - </Router> - </i18nCore.Context> - </KibanaContextProvider> - </AlertsContextProvider> - </ApmPluginContext.Provider>, - document.getElementById(REACT_APP_ROOT_ID) - ); - - // create static index pattern and store as saved object. Not needed by APM UI but for legacy reasons in Discover, Dashboard etc. - createStaticIndexPattern().catch(e => { - // eslint-disable-next-line no-console - console.log('Error fetching static index pattern', e); - }); - } - - public stop() {} -} diff --git a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts b/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts deleted file mode 100644 index 1efcc98bbbd66..0000000000000 --- a/x-pack/legacy/plugins/apm/public/services/rest/index_pattern.ts +++ /dev/null @@ -1,14 +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 { callApmApi } from './createCallApmApi'; - -export const createStaticIndexPattern = async () => { - return await callApmApi({ - method: 'POST', - pathname: '/api/apm/index_pattern/static' - }); -}; diff --git a/x-pack/legacy/plugins/apm/public/style/global_overrides.css b/x-pack/legacy/plugins/apm/public/style/global_overrides.css deleted file mode 100644 index 75b4532f7c9a1..0000000000000 --- a/x-pack/legacy/plugins/apm/public/style/global_overrides.css +++ /dev/null @@ -1,34 +0,0 @@ -/* -Hide unused secondary Kibana navigation -*/ -.kuiLocalNav { - min-height: initial; -} - -.kuiLocalNavRow.kuiLocalNavRow--secondary { - display: none; -} - -/* -Remove unnecessary space below the navigation dropdown -*/ -.kuiLocalDropdown { - margin-bottom: 0; - border-bottom: none; -} - -/* -Hide the "0-10 of 100" text in KUIPager component for all KUIControlledTable -*/ -.kuiControlledTable .kuiPagerText { - display: none; -} - -/* -Hide default dashed gridlines in EUI chart component for all APM graphs -*/ - -.rv-xy-plot__grid-lines__line { - stroke-opacity: 1; - stroke-dasharray: 1; -} diff --git a/x-pack/legacy/plugins/apm/public/templates/index.html b/x-pack/legacy/plugins/apm/public/templates/index.html deleted file mode 100644 index 78e0ade3ad624..0000000000000 --- a/x-pack/legacy/plugins/apm/public/templates/index.html +++ /dev/null @@ -1 +0,0 @@ -<div id="react-apm-root" class="apmReactRoot"></div> diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json b/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json deleted file mode 100644 index 8f6b0f35e4b52..0000000000000 --- a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "include": [ - "./plugins/apm/**/*", - "./legacy/plugins/apm/**/*", - "./typings/**/*" - ], - "exclude": [ - "**/__fixtures__/**/*", - "./legacy/plugins/apm/e2e/cypress/**/*" - ] -} diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/index.ts deleted file mode 100644 index bdc57eac412fc..0000000000000 --- a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/index.ts +++ /dev/null @@ -1,208 +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. - */ - -// This script downloads the telemetry mapping, runs the APM telemetry tasks, -// generates a bunch of randomized data based on the downloaded sample, -// and uploads it to a cluster of your choosing in the same format as it is -// stored in the telemetry cluster. Its purpose is twofold: -// - Easier testing of the telemetry tasks -// - Validate whether we can run the queries we want to on the telemetry data - -import fs from 'fs'; -import path from 'path'; -// @ts-ignore -import { Octokit } from '@octokit/rest'; -import { merge, chunk, flatten, pick, identity } from 'lodash'; -import axios from 'axios'; -import yaml from 'js-yaml'; -import { Client } from 'elasticsearch'; -import { argv } from 'yargs'; -import { promisify } from 'util'; -import { Logger } from 'kibana/server'; -// @ts-ignore -import consoleStamp from 'console-stamp'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { CollectTelemetryParams } from '../../../../../plugins/apm/server/lib/apm_telemetry/collect_data_telemetry'; -import { downloadTelemetryTemplate } from './download-telemetry-template'; -import mapping from '../../mappings.json'; -import { generateSampleDocuments } from './generate-sample-documents'; - -consoleStamp(console, '[HH:MM:ss.l]'); - -const githubToken = process.env.GITHUB_TOKEN; - -if (!githubToken) { - throw new Error('GITHUB_TOKEN was not provided.'); -} - -const kibanaConfigDir = path.join(__filename, '../../../../../../../config'); -const kibanaDevConfig = path.join(kibanaConfigDir, 'kibana.dev.yml'); -const kibanaConfig = path.join(kibanaConfigDir, 'kibana.yml'); - -const xpackTelemetryIndexName = 'xpack-phone-home'; - -const loadedKibanaConfig = (yaml.safeLoad( - fs.readFileSync( - fs.existsSync(kibanaDevConfig) ? kibanaDevConfig : kibanaConfig, - 'utf8' - ) -) || {}) as {}; - -const cliEsCredentials = pick( - { - 'elasticsearch.username': process.env.ELASTICSEARCH_USERNAME, - 'elasticsearch.password': process.env.ELASTICSEARCH_PASSWORD, - 'elasticsearch.hosts': process.env.ELASTICSEARCH_HOST - }, - identity -) as { - 'elasticsearch.username': string; - 'elasticsearch.password': string; - 'elasticsearch.hosts': string; -}; - -const config = { - 'apm_oss.transactionIndices': 'apm-*', - 'apm_oss.metricsIndices': 'apm-*', - 'apm_oss.errorIndices': 'apm-*', - 'apm_oss.spanIndices': 'apm-*', - 'apm_oss.onboardingIndices': 'apm-*', - 'apm_oss.sourcemapIndices': 'apm-*', - 'elasticsearch.hosts': 'http://localhost:9200', - ...loadedKibanaConfig, - ...cliEsCredentials -}; - -async function uploadData() { - const octokit = new Octokit({ - auth: githubToken - }); - - const telemetryTemplate = await downloadTelemetryTemplate(octokit); - - const kibanaMapping = mapping['apm-telemetry']; - - const httpAuth = - config['elasticsearch.username'] && config['elasticsearch.password'] - ? { - username: config['elasticsearch.username'], - password: config['elasticsearch.password'] - } - : null; - - const client = new Client({ - host: config['elasticsearch.hosts'], - ...(httpAuth - ? { - httpAuth: `${httpAuth.username}:${httpAuth.password}` - } - : {}) - }); - - if (argv.clear) { - try { - await promisify(client.indices.delete.bind(client))({ - index: xpackTelemetryIndexName - }); - } catch (err) { - // 404 = index not found, totally okay - if (err.status !== 404) { - throw err; - } - } - } - - const axiosInstance = axios.create({ - baseURL: config['elasticsearch.hosts'], - ...(httpAuth ? { auth: httpAuth } : {}) - }); - - const newTemplate = merge(telemetryTemplate, { - settings: { - index: { mapping: { total_fields: { limit: 10000 } } } - } - }); - - // override apm mapping instead of merging - newTemplate.mappings.properties.stack_stats.properties.kibana.properties.plugins.properties.apm = kibanaMapping; - - await axiosInstance.put(`/_template/xpack-phone-home`, newTemplate); - - const sampleDocuments = await generateSampleDocuments({ - collectTelemetryParams: { - logger: (console as unknown) as Logger, - indices: { - ...config, - apmCustomLinkIndex: '.apm-custom-links', - apmAgentConfigurationIndex: '.apm-agent-configuration' - }, - search: body => { - return promisify(client.search.bind(client))({ - ...body, - requestTimeout: 120000 - }) as any; - }, - indicesStats: body => { - return promisify(client.indices.stats.bind(client))({ - ...body, - requestTimeout: 120000 - }) as any; - }, - transportRequest: (params => { - return axiosInstance[params.method](params.path); - }) as CollectTelemetryParams['transportRequest'] - } - }); - - const chunks = chunk(sampleDocuments, 250); - - await chunks.reduce<Promise<any>>((prev, documents) => { - return prev.then(async () => { - const body = flatten( - documents.map(doc => [{ index: { _index: 'xpack-phone-home' } }, doc]) - ); - - return promisify(client.bulk.bind(client))({ - body, - refresh: true - }).then((response: any) => { - if (response.errors) { - const firstError = response.items.filter( - (item: any) => item.index.status >= 400 - )[0].index.error; - throw new Error(`Failed to upload documents: ${firstError.reason} `); - } - }); - }); - }, Promise.resolve()); -} - -uploadData() - .catch(e => { - if ('response' in e) { - if (typeof e.response === 'string') { - // eslint-disable-next-line no-console - console.log(e.response); - } else { - // eslint-disable-next-line no-console - console.log( - JSON.stringify( - e.response, - ['status', 'statusText', 'headers', 'data'], - 2 - ) - ); - } - } else { - // eslint-disable-next-line no-console - console.log(e); - } - process.exit(1); - }) - .then(() => { - // eslint-disable-next-line no-console - console.log('Finished uploading generated telemetry data'); - }); diff --git a/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx b/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx index be8a56486577e..7fa0231cf3409 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/tag/tag_badge.tsx @@ -31,7 +31,7 @@ export const TagBadge = (props: TagBadgeProps) => { <EuiBadge color={tag.color || 'primary'} iconType={iconType} - onClick={onClick} + onClick={onClick as React.MouseEventHandler<HTMLButtonElement>} onClickAriaLabel={onClickAriaLabel} > {idToRender} diff --git a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js index 72b0b8f0e533f..a81483d1e7a17 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js @@ -7,6 +7,7 @@ import path from 'path'; import moment from 'moment'; import 'moment-timezone'; +import ReactDOM from "react-dom"; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; @@ -24,6 +25,9 @@ moment.tz.setDefault('UTC'); const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 Date.now = jest.fn(() => testTime); +// Mock telemetry service +jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => { } })); + // Mock EUI generated ids to be consistently predictable for snapshots. jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); @@ -32,7 +36,7 @@ jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `gene jest.mock('@elastic/eui/lib/components/code/code', () => { const React = require.requireActual('react'); return { - EuiCode: ({children, className}) => ( + EuiCode: ({ children, className }) => ( <span> <code>{children}</code> </span> @@ -61,6 +65,12 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }; }); + +// Mock React Portal for components that use modals, tooltips, etc +ReactDOM.createPortal = jest.fn((element) => { + return element; +}); + jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { htmlIdGenerator: () => () => `generated-id`, @@ -71,7 +81,7 @@ jest.mock('plugins/interpreter/registries', () => ({})); // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', () => { return 'Disabled Panel'; } diff --git a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js index cc74faeac6a96..963cf831ef698 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js @@ -177,8 +177,10 @@ module.exports = async ({ config }) => { }), // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin(/lib\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), new webpack.NormalModuleReplacementPlugin(/lib\/download_workpad/, path.resolve(__dirname, '../tasks/mocks/downloadWorkpad')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/custom_element_service/, path.resolve(__dirname, '../tasks/mocks/customElementService')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/ui_metric/, path.resolve(__dirname, '../tasks/mocks/uiMetric')), ); // Tell Webpack about relevant extensions diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png deleted file mode 100644 index 93456066429d9..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts index 0650ac15c656e..df829e8b97676 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const areaChart: ElementFactory = () => ({ name: 'areaChart', - displayName: 'Area chart', + displayName: 'Area', help: 'A line chart with a filled body', - tags: ['chart'], - image: header, + type: 'chart', + icon: 'visArea', expression: `filters | demodata | pointseries x="time" y="mean(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png deleted file mode 100644 index db541fe7c53b8..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts index 7ab510e419769..7ac1d0ac83b0b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts @@ -5,16 +5,15 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const bubbleChart: ElementFactory = () => ({ name: 'bubbleChart', - displayName: 'Bubble chart', - tags: ['chart'], + displayName: 'Bubble', + type: 'chart', help: 'A customizable bubble chart', width: 700, height: 300, - image: header, + icon: 'heatmap', expression: `filters | demodata | pointseries x="project" y="sum(price)" color="state" size="size(username)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png deleted file mode 100644 index 37ab329a49bb8..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts index 914982951d664..ec8477f8f1017 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const debug: ElementFactory = () => ({ name: 'debug', - displayName: 'Debug', - tags: ['text'], + displayName: 'Debug data', help: 'Just dumps the configuration of the element', - image: header, + icon: 'bug', expression: `demodata | render as=debug`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png deleted file mode 100644 index 4bbfb6f8f68fc..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts deleted file mode 100644 index 4ea8037d2073e..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts +++ /dev/null @@ -1,21 +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 { ElementFactory } from '../../../types'; -import header from './header.png'; - -export const donut: ElementFactory = () => ({ - name: 'donut', - displayName: 'Donut chart', - tags: ['chart', 'proportion'], - help: 'A customizable donut chart', - image: header, - expression: `filters -| demodata -| pointseries color="project" size="max(price)" -| pie hole=50 labels=false legend="ne" -| render`, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png deleted file mode 100644 index 727b4d23941fd..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts index bde223d2a606e..bb1c13ca618be 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const dropdownFilter: ElementFactory = () => ({ - name: 'dropdown_filter', - displayName: 'Dropdown filter', - tags: ['filter'], + name: 'dropdownFilter', + displayName: 'Dropdown select', + type: 'filter', help: 'A dropdown from which you can select values for an "exactly" filter', - image: header, + icon: 'filter', height: 50, expression: `demodata | dropdownControl valueColumn=project filterColumn=project | render`, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts new file mode 100644 index 0000000000000..35a4a75f49c4e --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { ElementFactory } from '../../../types'; + +export const filterDebug: ElementFactory = () => ({ + name: 'filterDebug', + displayName: 'Debug filter', + help: 'Shows the underlying global filters in a workpad', + icon: 'bug', + expression: `filters +| render as=debug`, +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png deleted file mode 100644 index 9b6ee47d88698..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts index 7fddf48c70385..9567336decd5d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalBarChart: ElementFactory = () => ({ name: 'horizontalBarChart', - displayName: 'Horizontal bar chart', - tags: ['chart'], + displayName: 'Bar horizontal', + type: 'chart', help: 'A customizable horizontal bar chart', - image: header, + icon: 'visBarHorizontal', expression: `filters | demodata | pointseries x="size(cost)" y="project" color="project" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png deleted file mode 100644 index f28ad4a3ce4be..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts index f4a50a007c5de..529a74893a5de 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalProgressBar: ElementFactory = () => ({ name: 'horizontalProgressBar', - displayName: 'Horizontal progress bar', - tags: ['chart', 'proportion'], + displayName: 'Horizontal bar', + type: 'progress', help: 'Displays progress as a portion of a horizontal bar', width: 400, height: 30, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png deleted file mode 100644 index 2eaeb2e976a78..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts index 9b3aea2e55324..d5eba32325d1a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalProgressPill: ElementFactory = () => ({ name: 'horizontalProgressPill', - displayName: 'Horizontal progress pill', - tags: ['chart', 'proportion'], + displayName: 'Horizontal pill', + type: 'progress', help: 'Displays progress as a portion of a horizontal pill', width: 400, height: 30, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png deleted file mode 100644 index 7f29fc64c36b9..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts index eec1e2af61aad..ed7f6a99ddc32 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const image: ElementFactory = () => ({ name: 'image', displayName: 'Image', - tags: ['graphic'], + type: 'image', help: 'A static image', - image: header, + icon: 'image', expression: `image dataurl=null mode="contain" | render`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts index feba88dbe8b90..ec3b8a7798be1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -8,15 +8,15 @@ import { applyElementStrings } from '../../i18n/elements'; import { areaChart } from './area_chart'; import { bubbleChart } from './bubble_chart'; import { debug } from './debug'; -import { donut } from './donut'; import { dropdownFilter } from './dropdown_filter'; +import { filterDebug } from './filter_debug'; import { horizontalBarChart } from './horizontal_bar_chart'; import { horizontalProgressBar } from './horizontal_progress_bar'; import { horizontalProgressPill } from './horizontal_progress_pill'; import { image } from './image'; import { lineChart } from './line_chart'; import { markdown } from './markdown'; -import { metric } from './metric'; +import { metricElementInitializer } from './metric'; import { pie } from './pie'; import { plot } from './plot'; import { progressGauge } from './progress_gauge'; @@ -26,25 +26,26 @@ import { repeatImage } from './repeat_image'; import { revealImage } from './reveal_image'; import { shape } from './shape'; import { table } from './table'; -import { tiltedPie } from './tilted_pie'; import { timeFilter } from './time_filter'; import { verticalBarChart } from './vert_bar_chart'; import { verticalProgressBar } from './vertical_progress_bar'; import { verticalProgressPill } from './vertical_progress_pill'; -export const elementSpecs = applyElementStrings([ +import { SetupInitializer } from '../plugin'; +import { ElementFactory } from '../../types'; + +const elementSpecs = [ areaChart, bubbleChart, debug, - donut, dropdownFilter, + filterDebug, image, horizontalBarChart, horizontalProgressBar, horizontalProgressPill, lineChart, markdown, - metric, pie, plot, progressGauge, @@ -54,9 +55,19 @@ export const elementSpecs = applyElementStrings([ revealImage, shape, table, - tiltedPie, timeFilter, verticalBarChart, verticalProgressBar, verticalProgressPill, -]); +]; + +const initializeElementFactories = [metricElementInitializer]; + +export const initializeElements: SetupInitializer<ElementFactory[]> = (core, plugins) => { + const specs = [ + ...elementSpecs, + ...initializeElementFactories.map(factory => factory(core, plugins)), + ]; + + return applyElementStrings(specs); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png deleted file mode 100644 index eea133ee3680b..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts index 5b8533eea65bc..d19ddeb00dd67 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const lineChart: ElementFactory = () => ({ name: 'lineChart', - displayName: 'Line chart', - tags: ['chart'], + displayName: 'Line', + type: 'chart', help: 'A customizable line chart', - image: header, + icon: 'visLine', expression: `filters | demodata | pointseries x="time" y="mean(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png deleted file mode 100644 index a8b8550f5baea..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts index 1c7013834cbe4..7b114daa11870 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import header from './header.png'; - import { ElementFactory } from '../../../types'; export const markdown: ElementFactory = () => ({ name: 'markdown', - displayName: 'Markdown', - tags: ['text'], - help: 'Markup from Markdown', - image: header, + displayName: 'Text', + type: 'text', + help: 'Add text using Markdown', + icon: 'visText', expression: `filters | demodata | markdown "### Welcome to the Markdown element diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png deleted file mode 100644 index 0510342cdc54a..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index c08c090f11f91..7256657903aab 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -5,24 +5,25 @@ */ import { openSans } from '../../../common/lib/fonts'; -import header from './header.png'; -import { getAdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; - import { ElementFactory } from '../../../types'; -export const metric: ElementFactory = () => ({ - name: 'metric', - displayName: 'Metric', - tags: ['text'], - help: 'A number with a label', - width: 200, - height: 100, - image: header, - expression: `filters -| demodata -| math "unique(country)" -| metric "Countries" - metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} - labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${getAdvancedSettings().get('format:number:defaultPattern')}" -| render`, -}); +import { SetupInitializer } from '../../plugin'; + +export const metricElementInitializer: SetupInitializer<ElementFactory> = (core, setup) => { + return () => ({ + name: 'metric', + displayName: 'Metric', + type: 'chart', + help: 'A number with a label', + width: 200, + height: 100, + icon: 'visMetric', + expression: `filters + | demodata + | math "unique(country)" + | metric "Countries" + metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} + labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${core.uiSettings.get('format:number:defaultPattern')}" + | render`, + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png deleted file mode 100644 index deecd1067427c..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts index cfb9031325254..b7606ea94bc9f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts @@ -4,17 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import header from './header.png'; - import { ElementFactory } from '../../../types'; export const pie: ElementFactory = () => ({ name: 'pie', - displayName: 'Pie chart', - tags: ['chart', 'proportion'], + displayName: 'Pie', + type: 'chart', width: 300, height: 300, help: 'A simple pie chart', - image: header, + icon: 'visPie', expression: `filters | demodata | pointseries color="state" size="max(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png deleted file mode 100644 index d48c789ae5a92..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts index dd1660d558667..8648b65def4b2 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const plot: ElementFactory = () => ({ name: 'plot', displayName: 'Coordinate plot', - tags: ['chart'], + type: 'chart', help: 'Mixed line, bar or dot charts', - image: header, expression: `filters | demodata | pointseries x="time" y="sum(price)" color="state" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png deleted file mode 100644 index 8340c8a53b6ce..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts index 4ec192fb787fe..b21b7df286ace 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts @@ -6,16 +6,15 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressGauge: ElementFactory = () => ({ name: 'progressGauge', - displayName: 'Progress gauge', - tags: ['chart', 'proportion'], + displayName: 'Gauge', + type: 'progress', help: 'Displays progress as a portion of a gauge', width: 200, height: 200, - image: header, + icon: 'visGoal', expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png deleted file mode 100644 index b5b708529edd4..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts index 91fcb24996bc0..9ccb9489e8306 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressSemicircle: ElementFactory = () => ({ name: 'progressSemicircle', - displayName: 'Progress semicircle', - tags: ['chart', 'proportion'], + displayName: 'Semicircle', + type: 'progress', help: 'Displays progress as a portion of a semicircle', width: 200, height: 100, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png deleted file mode 100644 index 71e5d7e29444e..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts index 05c537f88756b..42bf36346c303 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressWheel: ElementFactory = () => ({ name: 'progressWheel', - displayName: 'Progress wheel', - tags: ['chart', 'proportion'], + displayName: 'Wheel', + type: 'progress', help: 'Displays progress as a portion of a wheel', width: 200, height: 200, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png deleted file mode 100644 index 9843c9a6d02c0..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts index df79651620642..0bf0ec4c2dc1f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const repeatImage: ElementFactory = () => ({ name: 'repeatImage', displayName: 'Image repeat', - tags: ['graphic', 'proportion'], + type: 'image', help: 'Repeats an image N times', - image: header, expression: `filters | demodata | math "mean(cost)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png deleted file mode 100644 index 8dc33b5a7259e..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts index 01c66ed3a26ec..88000f6b66a91 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const revealImage: ElementFactory = () => ({ name: 'revealImage', displayName: 'Image reveal', - tags: ['graphic', 'proportion'], + type: 'image', help: 'Reveals a percentage of an image', - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png deleted file mode 100644 index 3212d47591c07..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts index 3f3954ff02b02..f922473fa818f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts @@ -5,16 +5,15 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const shape: ElementFactory = () => ({ name: 'shape', displayName: 'Shape', - tags: ['graphic'], + type: 'shape', help: 'A customizable shape', width: 200, height: 200, - image: header, + icon: 'node', expression: 'shape "square" fill="#4cbce4" border="rgba(255,255,255,0)" borderWidth=0 maintainAspect=false | render', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png deleted file mode 100644 index a883faa693c1f..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts index ac13b7dc21293..ec26773fc3bf9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const table: ElementFactory = () => ({ name: 'table', displayName: 'Data table', - tags: ['text'], + type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', - image: header, + icon: 'visTable', expression: `filters | demodata | table diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png deleted file mode 100644 index b3329f991158c..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts deleted file mode 100644 index 21d8ba1e1b04a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts +++ /dev/null @@ -1,23 +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 { ElementFactory } from '../../../types'; -import header from './header.png'; - -export const tiltedPie: ElementFactory = () => ({ - name: 'tiltedPie', - displayName: 'Tilted pie chart', - tags: ['chart', 'proportion'], - width: 500, - height: 250, - help: 'A customizable tilted pie chart', - image: header, - expression: `filters -| demodata -| pointseries color="project" size="max(price)" -| pie tilt=0.5 -| render`, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png deleted file mode 100644 index d36b4cc97e5b1..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts index b384707c243d1..702ccb8a2312f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const timeFilter: ElementFactory = () => ({ - name: 'time_filter', + name: 'timeFilter', displayName: 'Time filter', - tags: ['filter'], + type: 'filter', help: 'Set a time window', - image: header, + icon: 'calendar', height: 50, expression: `timefilterControl compact=true column=@timestamp | render`, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png deleted file mode 100644 index 90505dd0dc77d..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts index ac4e3a0a72150..2354be0328e7c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalBarChart: ElementFactory = () => ({ name: 'verticalBarChart', displayName: 'Vertical bar chart', - tags: ['chart'], + type: 'chart', help: 'A customizable vertical bar chart', - image: header, + icon: 'visBarVertical', expression: `filters | demodata | pointseries x="project" y="size(cost)" color="project" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png deleted file mode 100644 index b9ff963e92c31..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts index e12903dafede9..b5e6c9816e3f5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalProgressBar: ElementFactory = () => ({ name: 'verticalProgressBar', displayName: 'Vertical progress bar', - tags: ['chart', 'proportion'], + type: 'progress', help: 'Displays progress as a portion of a vertical bar', width: 80, height: 400, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png deleted file mode 100644 index a4ac6b57da236..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts index 8926a12da8a47..28e80372494db 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalProgressPill: ElementFactory = () => ({ name: 'verticalProgressPill', displayName: 'Vertical progress pill', - tags: ['chart', 'proportion'], + type: 'progress', help: 'Displays progress as a portion of a vertical pill', width: 80, height: 400, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts index 2f7f5c26ad0b7..61ecd66455e78 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts @@ -8,7 +8,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Render } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; -interface Arguments { +export interface Arguments { column: string; compact: boolean; filterGroup: string; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts index a654c6b28b350..4452e5e9e31fe 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts @@ -14,18 +14,16 @@ import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; // @ts-ignore: untyped local import { renderFunctions, renderFunctionFactories } from './renderers'; - -import { elementSpecs } from './elements'; +import { initializeElements } from './elements'; // @ts-ignore Untyped Local import { transformSpecs } from './uis/transforms'; // @ts-ignore Untyped Local import { datasourceSpecs } from './uis/datasources'; // @ts-ignore Untyped Local import { modelSpecs } from './uis/models'; +import { initializeViews } from './uis/views'; // @ts-ignore Untyped Local -import { viewSpecs } from './uis/views'; -// @ts-ignore Untyped Local -import { args as argSpecs } from './uis/arguments'; +import { initializeArgs } from './uis/arguments'; import { tagSpecs } from './uis/tags'; import { templateSpecs } from './templates'; @@ -39,6 +37,9 @@ export interface StartDeps { inspector: InspectorStart; } +export type SetupInitializer<T> = (core: CoreSetup<StartDeps>, plugins: SetupDeps) => T; +export type StartInitializer<T> = (core: CoreStart, plugins: StartDeps) => T; + /** @internal */ export class CanvasSrcPlugin implements Plugin<void, void, SetupDeps, StartDeps> { public setup(core: CoreSetup<StartDeps>, plugins: SetupDeps) { @@ -53,11 +54,11 @@ export class CanvasSrcPlugin implements Plugin<void, void, SetupDeps, StartDeps> ); }); - plugins.canvas.addElements(elementSpecs); + plugins.canvas.addElements(initializeElements(core, plugins)); plugins.canvas.addDatasourceUIs(datasourceSpecs); plugins.canvas.addModelUIs(modelSpecs); - plugins.canvas.addViewUIs(viewSpecs); - plugins.canvas.addArgumentUIs(argSpecs); + plugins.canvas.addViewUIs(initializeViews(core, plugins)); + plugins.canvas.addArgumentUIs(initializeArgs(core, plugins)); plugins.canvas.addTagUIs(tagSpecs); plugins.canvas.addTemplates(templateSpecs); plugins.canvas.addTransformUIs(transformSpecs); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index a1096d50c1653..1e8983a0ca5e5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -6,7 +6,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { I18nContext } from 'ui/i18n'; import { CoreStart } from '../../../../../../../src/core/public'; import { StartDeps } from '../../plugin'; import { @@ -30,6 +29,8 @@ const embeddablesRegistry: { } = {}; const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { + const I18nContext = core.i18n.Context; + return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { return ( <div @@ -44,6 +45,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { getAllEmbeddableFactories={plugins.embeddable.getEmbeddableFactories} notifications={core.notifications} overlays={core.overlays} + application={core.application} inspector={plugins.inspector} SavedObjectFinder={getSavedObjectFinder(core.savedObjects, core.uiSettings)} /> diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js index 84f92f5149893..263f2d8ec30b5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js @@ -20,7 +20,7 @@ import { revealImage } from './reveal_image'; import { shape } from './shape'; import { table } from './table'; import { text } from './text'; -import { timeFilter } from './time_filter'; +import { timeFilterFactory } from './time_filter'; export const renderFunctions = [ advancedFilter, @@ -38,7 +38,6 @@ export const renderFunctions = [ shape, table, text, - timeFilter, ]; -export const renderFunctionFactories = [embeddableRendererFactory]; +export const renderFunctionFactories = [embeddableRendererFactory, timeFilterFactory]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx index 55a453720e2f0..85ea754de670d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx @@ -4,22 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; -import { TimeFilter as Component, Props } from './time_filter'; +import { TimeFilter } from './time_filter'; -export const TimeFilter = (props: Props) => { - const customQuickRanges = (getAdvancedSettings().get('timepicker:quickRanges') || []).map( - ({ from, to, display }: { from: string; to: string; display: string }) => ({ - start: from, - end: to, - label: display, - }) - ); - - const customDateFormat = getAdvancedSettings().get('dateFormat'); - - return ( - <Component {...props} commonlyUsedRanges={customQuickRanges} dateFormat={customDateFormat} /> - ); -}; +export { TimeFilter }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js deleted file mode 100644 index cbc514e218d74..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js +++ /dev/null @@ -1,47 +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 ReactDOM from 'react-dom'; -import React from 'react'; -import { toExpression } from '@kbn/interpreter/common'; -import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; -import { RendererStrings } from '../../../i18n'; -import { TimeFilter } from './components'; - -const { timeFilter: strings } = RendererStrings; - -export const timeFilter = () => ({ - name: 'time_filter', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, // must be true, otherwise popovers don't work - render(domNode, config, handlers) { - const filterExpression = handlers.getFilter(); - - if (filterExpression !== '') { - // NOTE: setFilter() will cause a data refresh, avoid calling unless required - // compare expression and filter, update filter if needed - const { changed, newAst } = syncFilterExpression(config, filterExpression, [ - 'column', - 'filterGroup', - ]); - - if (changed) { - handlers.setFilter(toExpression(newAst)); - } - } - - ReactDOM.render( - <TimeFilter compact={config.compact} commit={handlers.setFilter} filter={filterExpression} />, - domNode, - () => handlers.done() - ); - - handlers.onDestroy(() => { - ReactDOM.unmountComponentAtNode(domNode); - }); - }, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx new file mode 100644 index 0000000000000..ef5bfb70d4b3d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ReactDOM from 'react-dom'; +import React from 'react'; +import { toExpression } from '@kbn/interpreter/common'; +import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; +import { RendererStrings } from '../../../i18n'; +import { TimeFilter } from './components'; +import { StartInitializer } from '../../plugin'; +import { RendererHandlers } from '../../../types'; +import { Arguments } from '../../functions/common/timefilterControl'; +import { RendererFactory } from '../../../types'; + +const { timeFilter: strings } = RendererStrings; + +export const timeFilterFactory: StartInitializer<RendererFactory<Arguments>> = (core, plugins) => { + const { uiSettings } = core; + + const customQuickRanges = (uiSettings.get('timepicker:quickRanges') || []).map( + ({ from, to, display }: { from: string; to: string; display: string }) => ({ + start: from, + end: to, + label: display, + }) + ); + + const customDateFormat = uiSettings.get('dateFormat'); + + return () => ({ + name: 'time_filter', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, // must be true, otherwise popovers don't work + render: async (domNode: HTMLElement, config: Arguments, handlers: RendererHandlers) => { + const filterExpression = handlers.getFilter(); + + if (filterExpression !== '') { + // NOTE: setFilter() will cause a data refresh, avoid calling unless required + // compare expression and filter, update filter if needed + const { changed, newAst } = syncFilterExpression(config, filterExpression, [ + 'column', + 'filterGroup', + ]); + + if (changed) { + handlers.setFilter(toExpression(newAst)); + } + } + + ReactDOM.render( + <TimeFilter + commit={handlers.setFilter} + filter={filterExpression} + commonlyUsedRanges={customQuickRanges} + dateFormat={customDateFormat} + />, + domNode, + () => handlers.done() + ); + + handlers.onDestroy(() => { + ReactDOM.unmountComponentAtNode(domNode); + }); + }, + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts index d19bfa64bae76..655a362fe6d33 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts @@ -7,38 +7,40 @@ import { compose, withProps } from 'recompose'; import moment from 'moment'; import { DateFormatArgInput as Component, Props as ComponentProps } from './date_format'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; -const { DateFormat: strings } = ArgumentStrings; +import { SetupInitializer } from '../../../plugin'; -const getFormatMap = () => ({ - DEFAULT: getAdvancedSettings().get('dateFormat'), - NANOS: getAdvancedSettings().get('dateNanosFormat'), - ISO8601: '', - LOCAL_LONG: 'LLLL', - LOCAL_SHORT: 'LLL', - LOCAL_DATE: 'l', - LOCAL_TIME_WITH_SECONDS: 'LTS', -}); +const { DateFormat: strings } = ArgumentStrings; -const now = moment(); +export const dateFormatInitializer: SetupInitializer<ArgumentFactory<ComponentProps>> = ( + core, + plugins +) => { + const formatMap = { + DEFAULT: core.uiSettings.get('dateFormat'), + NANOS: core.uiSettings.get('dateNanosFormat'), + ISO8601: '', + LOCAL_LONG: 'LLLL', + LOCAL_SHORT: 'LLL', + LOCAL_DATE: 'l', + LOCAL_TIME_WITH_SECONDS: 'LTS', + }; -const dateFormats = Object.values(getFormatMap()).map(format => ({ - value: format, - text: moment.utc(now).format(format), -})); + const dateFormats = Object.values(formatMap).map(format => ({ + value: format, + text: moment.utc(moment()).format(format), + })); -export const DateFormatArgInput = compose<ComponentProps, null>(withProps({ dateFormats }))( - Component -); + const DateFormatArgInput = compose<ComponentProps, null>(withProps({ dateFormats }))(Component); -export const dateFormat: ArgumentFactory<ComponentProps> = () => ({ - name: 'dateFormat', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(DateFormatArgInput), -}); + return () => ({ + name: 'dateFormat', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(DateFormatArgInput), + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js deleted file mode 100644 index 829fe3e3bef3d..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js +++ /dev/null @@ -1,39 +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 { axisConfig } from './axis_config'; -import { datacolumn } from './datacolumn'; -import { dateFormat } from './date_format'; -import { filterGroup } from './filter_group'; -import { imageUpload } from './image_upload'; -import { number } from './number'; -import { numberFormat } from './number_format'; -import { palette } from './palette'; -import { percentage } from './percentage'; -import { range } from './range'; -import { select } from './select'; -import { shape } from './shape'; -import { string } from './string'; -import { textarea } from './textarea'; -import { toggle } from './toggle'; - -export const args = [ - axisConfig, - datacolumn, - dateFormat, - filterGroup, - imageUpload, - number, - numberFormat, - palette, - percentage, - range, - select, - shape, - string, - textarea, - toggle, -]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts new file mode 100644 index 0000000000000..bd84a2eb97d24 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { axisConfig } from './axis_config'; +// @ts-ignore untyped local +import { datacolumn } from './datacolumn'; +import { dateFormatInitializer } from './date_format'; +// @ts-ignore untyped local +import { filterGroup } from './filter_group'; +// @ts-ignore untyped local +import { imageUpload } from './image_upload'; +// @ts-ignore untyped local +import { number } from './number'; +import { numberFormatInitializer } from './number_format'; +// @ts-ignore untyped local +import { palette } from './palette'; +// @ts-ignore untyped local +import { percentage } from './percentage'; +// @ts-ignore untyped local +import { range } from './range'; +// @ts-ignore untyped local +import { select } from './select'; +// @ts-ignore untyped local +import { shape } from './shape'; +// @ts-ignore untyped local +import { string } from './string'; +// @ts-ignore untyped local +import { textarea } from './textarea'; +// @ts-ignore untyped local +import { toggle } from './toggle'; + +import { SetupInitializer } from '../../plugin'; + +export const args = [ + axisConfig, + datacolumn, + filterGroup, + imageUpload, + number, + palette, + percentage, + range, + select, + shape, + string, + textarea, + toggle, +]; + +export const initializers = [dateFormatInitializer, numberFormatInitializer]; + +export const initializeArgs: SetupInitializer<any> = (core, plugins) => { + return [...args, ...initializers.map(initializer => initializer(core, plugins))]; +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts index ce6c90c89a5a0..4025d4deaf997 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -6,40 +6,42 @@ import { compose, withProps } from 'recompose'; import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; +import { SetupInitializer } from '../../../plugin'; const { NumberFormat: strings } = ArgumentStrings; -const getFormatMap = () => ({ - NUMBER: getAdvancedSettings().get('format:number:defaultPattern'), - PERCENT: getAdvancedSettings().get('format:percent:defaultPattern'), - CURRENCY: getAdvancedSettings().get('format:currency:defaultPattern'), - DURATION: '00:00:00', - BYTES: getAdvancedSettings().get('format:bytes:defaultPattern'), -}); +export const numberFormatInitializer: SetupInitializer<ArgumentFactory<ComponentProps>> = ( + core, + plugins +) => { + const formatMap = { + NUMBER: core.uiSettings.get('format:number:defaultPattern'), + PERCENT: core.uiSettings.get('format:percent:defaultPattern'), + CURRENCY: core.uiSettings.get('format:currency:defaultPattern'), + DURATION: '00:00:00', + BYTES: core.uiSettings.get('format:bytes:defaultPattern'), + }; -const getNumberFormats = () => { - const formatMap = getFormatMap(); - return [ + const numberFormats = [ { value: formatMap.NUMBER, text: strings.getFormatNumber() }, { value: formatMap.PERCENT, text: strings.getFormatPercent() }, { value: formatMap.CURRENCY, text: strings.getFormatCurrency() }, { value: formatMap.DURATION, text: strings.getFormatDuration() }, { value: formatMap.BYTES, text: strings.getFormatBytes() }, ]; -}; -export const NumberFormatArgInput = compose<ComponentProps, null>( - withProps({ numberFormats: getNumberFormats() }) -)(Component); + const NumberFormatArgInput = compose<ComponentProps, null>(withProps({ numberFormats }))( + Component + ); -export const numberFormat: ArgumentFactory<ComponentProps> = () => ({ - name: 'numberFormat', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(NumberFormatArgInput), -}); + return () => ({ + name: 'numberFormat', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(NumberFormatArgInput), + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts deleted file mode 100644 index 4c535a42c3c44..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts +++ /dev/null @@ -1,15 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const chart: TagFactory = () => ({ - name: strings.chart(), - color: euiVisPalette[4], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts deleted file mode 100644 index 5249856dec271..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts +++ /dev/null @@ -1,16 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; - -const euiVisPalette = euiPaletteColorBlind(); - -export const filter: TagFactory = () => ({ - name: strings.filter(), - color: euiVisPalette[1], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts deleted file mode 100644 index 36d66801ef681..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts +++ /dev/null @@ -1,15 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const graphic: TagFactory = () => ({ - name: strings.graphic(), - color: euiVisPalette[5], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts index 2587665a452b5..2e711437c72a8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { chart } from './chart'; -import { filter } from './filter'; -import { graphic } from './graphic'; import { presentation } from './presentation'; -import { proportion } from './proportion'; import { report } from './report'; -import { text } from './text'; // Registry expects a function that returns a spec object -export const tagSpecs = [chart, filter, graphic, presentation, proportion, report, text]; +export const tagSpecs = [presentation, report]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js deleted file mode 100644 index 6a59a6795d45a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js +++ /dev/null @@ -1,10 +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 { euiPaletteColorBlind } from '@elastic/eui'; -const euiVisPalette = euiPaletteColorBlind(); - -export const proportion = () => ({ name: 'proportion', color: euiVisPalette[3] }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts deleted file mode 100644 index 4d37ecfaa367a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts +++ /dev/null @@ -1,15 +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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const proportion: TagFactory = () => ({ - name: strings.proportion(), - color: euiVisPalette[3], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts deleted file mode 100644 index f9faf2ad2e3ca..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts +++ /dev/null @@ -1,13 +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 { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; - -export const text: TagFactory = () => ({ - name: strings.text(), - color: '#D3DAE6', -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js deleted file mode 100644 index 6e2685dcb9893..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js +++ /dev/null @@ -1,37 +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 { dropdownControl } from './dropdownControl'; -import { getCell } from './getCell'; -import { image } from './image'; -import { markdown } from './markdown'; -import { metric } from './metric'; -import { pie } from './pie'; -import { plot } from './plot'; -import { progress } from './progress'; -import { repeatImage } from './repeatImage'; -import { revealImage } from './revealImage'; -import { render } from './render'; -import { shape } from './shape'; -import { table } from './table'; -import { timefilterControl } from './timefilterControl'; - -export const viewSpecs = [ - dropdownControl, - getCell, - image, - markdown, - metric, - pie, - plot, - progress, - repeatImage, - revealImage, - render, - shape, - table, - timefilterControl, -]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts new file mode 100644 index 0000000000000..fdcaf21982050 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +// @ts-ignore untyped local +import { dropdownControl } from './dropdownControl'; +// @ts-ignore untyped local +import { getCell } from './getCell'; +// @ts-ignore untyped local +import { image } from './image'; +// @ts-ignore untyped local +import { markdown } from './markdown'; +// @ts-ignore untyped local +import { metricInitializer } from './metric'; +// @ts-ignore untyped local +import { pie } from './pie'; +// @ts-ignore untyped local +import { plot } from './plot'; +// @ts-ignore untyped local +import { progress } from './progress'; +// @ts-ignore untyped local +import { repeatImage } from './repeatImage'; +// @ts-ignore untyped local +import { revealImage } from './revealImage'; +// @ts-ignore untyped local +import { render } from './render'; +// @ts-ignore untyped local +import { shape } from './shape'; +// @ts-ignore untyped local +import { table } from './table'; +// @ts-ignore untyped local +import { timefilterControl } from './timefilterControl'; + +import { SetupInitializer } from '../../plugin'; + +export const viewSpecs = [ + dropdownControl, + getCell, + image, + markdown, + pie, + plot, + progress, + repeatImage, + revealImage, + render, + shape, + table, + timefilterControl, +]; + +export const viewInitializers = [metricInitializer]; + +export const initializeViews: SetupInitializer<unknown[]> = (core, plugins) => { + return [...viewSpecs, ...viewInitializers.map(initializer => initializer(core, plugins))]; +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js deleted file mode 100644 index e69f8f1de5952..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ /dev/null @@ -1,48 +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 { openSans } from '../../../common/lib/fonts'; -import { getAdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; -import { ViewStrings } from '../../../i18n'; - -const { Metric: strings } = ViewStrings; - -export const metric = () => ({ - name: 'metric', - displayName: strings.getDisplayName(), - modelArgs: [['_', { label: strings.getNumberDisplayName() }]], - requiresContext: false, - args: [ - { - name: 'metricFormat', - displayName: strings.getMetricFormatDisplayName(), - help: strings.getMetricFormatHelp(), - argType: 'numberFormat', - default: `"${getAdvancedSettings().get('format:number:defaultPattern')}"`, - }, - { - name: '_', - displayName: strings.getLabelDisplayName(), - help: strings.getLabelHelp(), - argType: 'string', - default: '""', - }, - { - name: 'metricFont', - displayName: strings.getMetricFontDisplayName(), - help: strings.getMetricFontHelp(), - argType: 'font', - default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, - }, - { - name: 'labelFont', - displayName: strings.getLabelFontDisplayName(), - help: strings.getLabelFontHelp(), - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, - }, - ], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts new file mode 100644 index 0000000000000..93912b7b0517f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { openSans } from '../../../common/lib/fonts'; +import { ViewStrings } from '../../../i18n'; +import { SetupInitializer } from '../../plugin'; + +const { Metric: strings } = ViewStrings; + +export const metricInitializer: SetupInitializer<unknown> = (core, plugin) => { + return () => ({ + name: 'metric', + displayName: strings.getDisplayName(), + modelArgs: [['_', { label: strings.getNumberDisplayName() }]], + requiresContext: false, + args: [ + { + name: 'metricFormat', + displayName: strings.getMetricFormatDisplayName(), + help: strings.getMetricFormatHelp(), + argType: 'numberFormat', + default: `"${core.uiSettings.get('format:number:defaultPattern')}"`, + }, + { + name: '_', + displayName: strings.getLabelDisplayName(), + help: strings.getLabelHelp(), + argType: 'string', + default: '""', + }, + { + name: 'metricFont', + displayName: strings.getMetricFontDisplayName(), + help: strings.getMetricFontHelp(), + argType: 'font', + default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, + }, + { + name: 'labelFont', + displayName: strings.getLabelFontDisplayName(), + help: strings.getLabelFontHelp(), + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, + }, + ], + }); +}; diff --git a/x-pack/legacy/plugins/canvas/common/lib/constants.ts b/x-pack/legacy/plugins/canvas/common/lib/constants.ts index ac8e80b8d7b89..a37dc3fd6a7b3 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/constants.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/constants.ts @@ -40,3 +40,4 @@ export const API_ROUTE_SHAREABLE_ZIP = '/public/canvas/zip'; export const API_ROUTE_SHAREABLE_RUNTIME = '/public/canvas/runtime'; export const API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD = `/public/canvas/${SHAREABLE_RUNTIME_NAME}.js`; export const CANVAS_EMBEDDABLE_CLASSNAME = `canvasEmbeddable`; +export const CONTEXT_MENU_TOP_BORDER_CLASSNAME = 'canvasContextMenu--topBorder'; diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index d0a9051d7af87..de16bc2101e8c 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -15,7 +15,7 @@ export const ComponentStrings = { }), getTitleText: () => i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Embed Object', + defaultMessage: 'Add from Visualize library', }), }, AdvancedFilter: { @@ -305,21 +305,21 @@ export const ComponentStrings = { }), }, ElementControls: { - getEditTooltip: () => - i18n.translate('xpack.canvas.elementControls.editToolTip', { - defaultMessage: 'Edit', - }), - getEditAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.editAriaLabel', { - defaultMessage: 'Edit element', + getDeleteAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { + defaultMessage: 'Delete element', }), getDeleteTooltip: () => i18n.translate('xpack.canvas.elementControls.deleteToolTip', { defaultMessage: 'Delete', }), - getDeleteAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { - defaultMessage: 'Delete element', + getEditAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.editAriaLabel', { + defaultMessage: 'Edit element', + }), + getEditTooltip: () => + i18n.translate('xpack.canvas.elementControls.editToolTip', { + defaultMessage: 'Edit', }), }, ElementSettings: { @@ -336,53 +336,6 @@ export const ComponentStrings = { description: 'This tab contains the settings for how data is displayed in a Canvas element', }), }, - ElementTypes: { - getEditElementTitle: () => - i18n.translate('xpack.canvas.elementTypes.editElementTitle', { - defaultMessage: 'Edit element', - }), - getDeleteElementTitle: (elementName: string) => - i18n.translate('xpack.canvas.elementTypes.deleteElementTitle', { - defaultMessage: `Delete element '{elementName}'?`, - values: { - elementName, - }, - }), - getDeleteElementDescription: () => - i18n.translate('xpack.canvas.elementTypes.deleteElementDescription', { - defaultMessage: 'Are you sure you want to delete this element?', - }), - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.elementTypes.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getDeleteButtonLabel: () => - i18n.translate('xpack.canvas.elementTypes.deleteButtonLabel', { - defaultMessage: 'Delete', - }), - getAddNewElementTitle: () => - i18n.translate('xpack.canvas.elementTypes.addNewElementTitle', { - defaultMessage: 'Add new elements', - }), - getAddNewElementDescription: () => - i18n.translate('xpack.canvas.elementTypes.addNewElementDescription', { - defaultMessage: 'Group and save workpad elements to create new elements', - }), - getFindElementPlaceholder: () => - i18n.translate('xpack.canvas.elementTypes.findElementPlaceholder', { - defaultMessage: 'Find element', - }), - getElementsTitle: () => - i18n.translate('xpack.canvas.elementTypes.elementsTitle', { - defaultMessage: 'Elements', - description: 'Title for the "Elements" tab when adding a new element', - }), - getMyElementsTitle: () => - i18n.translate('xpack.canvas.elementTypes.myElementsTitle', { - defaultMessage: 'My elements', - description: 'Title for the "My elements" tab when adding a new element', - }), - }, Error: { getDescription: () => i18n.translate('xpack.canvas.errorComponent.description', { @@ -633,6 +586,61 @@ export const ComponentStrings = { defaultMessage: 'Delete', }), }, + SavedElementsModal: { + getAddNewElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementDescription', { + defaultMessage: 'Group and save workpad elements to create new elements', + }), + getAddNewElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementTitle', { + defaultMessage: 'Add new elements', + }), + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getDeleteButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + getDeleteElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementDescription', { + defaultMessage: 'Are you sure you want to delete this element?', + }), + getDeleteElementTitle: (elementName: string) => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementTitle', { + defaultMessage: `Delete element '{elementName}'?`, + values: { + elementName, + }, + }), + getEditElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.editElementTitle', { + defaultMessage: 'Edit element', + }), + getElementsTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.elementsTitle', { + defaultMessage: 'Elements', + description: 'Title for the "Elements" tab when adding a new element', + }), + getFindElementPlaceholder: () => + i18n.translate('xpack.canvas.savedElementsModal.findElementPlaceholder', { + defaultMessage: 'Find element', + }), + getModalTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.modalTitle', { + defaultMessage: 'My elements', + }), + getMyElementsTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.myElementsTitle', { + defaultMessage: 'My elements', + description: 'Title for the "My elements" tab when adding a new element', + }), + getSavedElementsModalCloseButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { + defaultMessage: 'Close', + }), + }, ShareWebsiteFlyout: { getRuntimeStepTitle: () => i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', { @@ -652,7 +660,7 @@ export const ComponentStrings = { defaultMessage: 'Share on a website', }), getUnsupportedRendererWarning: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning', { defaultMessage: 'This workpad contains render functions that are not supported by the {CANVAS} Shareable Workpad Runtime. These elements will not be rendered:', values: { @@ -796,17 +804,6 @@ export const ComponentStrings = { }), }, SidebarHeader: { - getAlignmentMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.alignmentMenuItemLabel', { - defaultMessage: 'Alignment', - description: - 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' + - 'alignment options of the selected elements', - }), - getBottomAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel', { - defaultMessage: 'Bottom', - }), getBringForwardAriaLabel: () => i18n.translate('xpack.canvas.sidebarHeader.bringForwardArialLabel', { defaultMessage: 'Move element up one layer', @@ -815,56 +812,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.sidebarHeader.bringToFrontArialLabel', { defaultMessage: 'Move element to top layer', }), - getCenterAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.centerAlignMenuItemLabel', { - defaultMessage: 'Center', - description: 'This refers to alignment centered horizontally.', - }), - getContextMenuTitle: () => - i18n.translate('xpack.canvas.sidebarHeader.contextMenuAriaLabel', { - defaultMessage: 'Element options', - }), - getCreateElementModalTitle: () => - i18n.translate('xpack.canvas.sidebarHeader.createElementModalTitle', { - defaultMessage: 'Create new element', - }), - getDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.distributionMenutItemLabel', { - defaultMessage: 'Distribution', - description: - 'This refers to the options to evenly spacing the selected elements horizontall or vertically.', - }), - getGroupMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.groupMenuItemLabel', { - defaultMessage: 'Group', - description: 'This refers to grouping multiple selected elements.', - }), - getHorizontalDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel', { - defaultMessage: 'Horizontal', - }), - getLeftAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.leftAlignMenuItemLabel', { - defaultMessage: 'Left', - }), - getMiddleAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.middleAlignMenuItemLabel', { - defaultMessage: 'Middle', - description: 'This refers to alignment centered vertically.', - }), - getOrderMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.orderMenuItemLabel', { - defaultMessage: 'Order', - description: 'Refers to the order of the elements displayed on the page from front to back', - }), - getRightAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.rightAlignMenuItemLabel', { - defaultMessage: 'Right', - }), - getSaveElementMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.savedElementMenuItemLabel', { - defaultMessage: 'Save as new element', - }), getSendBackwardAriaLabel: () => i18n.translate('xpack.canvas.sidebarHeader.sendBackwardArialLabel', { defaultMessage: 'Move element down one layer', @@ -873,19 +820,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.sidebarHeader.sendToBackArialLabel', { defaultMessage: 'Move element to bottom layer', }), - getTopAlignMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.topAlignMenuItemLabel', { - defaultMessage: 'Top', - }), - getUngroupMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.ungroupMenuItemLabel', { - defaultMessage: 'Ungroup', - description: 'This refers to ungrouping a grouped element', - }), - getVerticalDistributionMenuItemLabel: () => - i18n.translate('xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel', { - defaultMessage: 'Vertical', - }), }, TextStylePicker: { getAlignCenterOption: () => @@ -900,6 +834,10 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.alignRightOption', { defaultMessage: 'Align right', }), + getFontColorLabel: () => + i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { + defaultMessage: 'Font Color', + }), getStyleBoldOption: () => i18n.translate('xpack.canvas.textStylePicker.styleBoldOption', { defaultMessage: 'Bold', @@ -912,10 +850,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', { defaultMessage: 'Underline', }), - getFontColorLabel: () => - i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { - defaultMessage: 'Font Color', - }), }, TimePicker: { getApplyButtonLabel: () => @@ -962,6 +896,10 @@ export const ComponentStrings = { description: '"stylesheet" refers to the collection of CSS style rules entered by the user.', }), + getBackgroundColorLabel: () => + i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { + defaultMessage: 'Background color', + }), getFlipDimensionAriaLabel: () => i18n.translate('xpack.canvas.workpadConfig.swapDimensionsAriaLabel', { defaultMessage: `Swap the page's width and height`, @@ -1013,10 +951,6 @@ export const ComponentStrings = { defaultMessage: 'US Letter', description: 'This is referring to the dimensions of U.S. standard letter paper.', }), - getBackgroundColorLabel: () => - i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { - defaultMessage: 'Background color', - }), }, WorkpadCreate: { getWorkpadCreateButtonLabel: () => @@ -1029,14 +963,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', { defaultMessage: 'Add element', }), - getAddElementModalCloseButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { - defaultMessage: 'Close', - }), - getEmbedObjectButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.embedObjectButtonLabel', { - defaultMessage: 'Embed object', - }), getFullScreenButtonAriaLabel: () => i18n.translate('xpack.canvas.workpadHeader.fullscreenButtonAriaLabel', { defaultMessage: 'View fullscreen', @@ -1079,12 +1005,6 @@ export const ComponentStrings = { defaultMessage: 'Refresh elements', }), }, - WorkpadHeaderControlSettings: { - getTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderControlSettings.settingsTooltip', { - defaultMessage: 'Control settings', - }), - }, WorkpadHeaderCustomInterval: { getButtonLabel: () => i18n.translate('xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel', { @@ -1105,6 +1025,144 @@ export const ComponentStrings = { defaultMessage: 'Set a custom interval', }), }, + WorkpadHeaderEditMenu: { + getAlignmentMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel', { + defaultMessage: 'Alignment', + description: + 'This refers to the vertical (i.e. left, center, right) and horizontal (i.e. top, middle, bottom) ' + + 'alignment options of the selected elements', + }), + getBottomAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel', { + defaultMessage: 'Bottom', + }), + getCenterAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel', { + defaultMessage: 'Center', + description: 'This refers to alignment centered horizontally.', + }), + getCreateElementModalTitle: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.createElementModalTitle', { + defaultMessage: 'Create new element', + }), + getDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel', { + defaultMessage: 'Distribution', + description: + 'This refers to the options to evenly spacing the selected elements horizontall or vertically.', + }), + getEditMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuButtonLabel', { + defaultMessage: 'Edit', + }), + getEditMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.editMenuLabel', { + defaultMessage: 'Edit options', + }), + getGroupMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel', { + defaultMessage: 'Group', + description: 'This refers to grouping multiple selected elements.', + }), + getHorizontalDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel', { + defaultMessage: 'Horizontal', + }), + getLeftAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel', { + defaultMessage: 'Left', + }), + getMiddleAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel', { + defaultMessage: 'Middle', + description: 'This refers to alignment centered vertically.', + }), + getOrderMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel', { + defaultMessage: 'Order', + description: 'Refers to the order of the elements displayed on the page from front to back', + }), + getRedoMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.redoMenuItemLabel', { + defaultMessage: 'Redo', + }), + getRightAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel', { + defaultMessage: 'Right', + }), + getSaveElementMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel', { + defaultMessage: 'Save as new element', + }), + getTopAlignMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel', { + defaultMessage: 'Top', + }), + getUndoMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.undoMenuItemLabel', { + defaultMessage: 'Undo', + }), + getUngroupMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel', { + defaultMessage: 'Ungroup', + description: 'This refers to ungrouping a grouped element', + }), + getVerticalDistributionMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel', { + defaultMessage: 'Vertical', + }), + }, + WorkpadHeaderElementMenu: { + getAssetsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel', { + defaultMessage: 'Manage assets', + }), + getChartMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.chartMenuItemLabel', { + defaultMessage: 'Chart', + }), + getElementMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuButtonLabel', { + defaultMessage: 'Add element', + }), + getElementMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuLabel', { + defaultMessage: 'Add an element', + }), + getEmbedObjectMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { + defaultMessage: 'Add from Visualize library', + }), + getFilterMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { + defaultMessage: 'Filter', + }), + getImageMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.imageMenuItemLabel', { + defaultMessage: 'Image', + }), + getMyElementsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.myElementsMenuItemLabel', { + defaultMessage: 'My elements', + }), + getOtherMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.otherMenuItemLabel', { + defaultMessage: 'Other', + }), + getProgressMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.progressMenuItemLabel', { + defaultMessage: 'Progress', + }), + getShapeMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.shapeMenuItemLabel', { + defaultMessage: 'Shape', + }), + getTextMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.textMenuItemLabel', { + defaultMessage: 'Text', + }), + }, WorkpadHeaderKioskControls: { getCycleFormLabel: () => i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleFormLabel', { @@ -1129,9 +1187,9 @@ export const ComponentStrings = { defaultMessage: 'Refresh data', }), }, - WorkpadHeaderWorkpadExport: { + WorkpadHeaderShareMenu: { getCopyPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyPDFMessage', { defaultMessage: 'The {PDF} generation {URL} was copied to your clipboard.', values: { PDF, @@ -1139,15 +1197,15 @@ export const ComponentStrings = { }, }), getCopyReportingConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage', { defaultMessage: 'Copied reporting configuration to clipboard', }), getCopyShareConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { defaultMessage: 'Copied share markup to clipboard', }), getExportPDFErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage', { defaultMessage: "Failed to create {PDF} for '{workpadName}'", values: { PDF, @@ -1155,14 +1213,14 @@ export const ComponentStrings = { }, }), getExportPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFMessage', { defaultMessage: 'Exporting {PDF}. You can track the progress in Management.', values: { PDF, }, }), getExportPDFTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFTitle', { defaultMessage: "{PDF} export of workpad '{workpadName}'", values: { PDF, @@ -1170,7 +1228,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel', { defaultMessage: 'Alternatively, you can generate a {PDF} from a script or with Watcher by using this {URL}. Press Enter to copy the {URL} to clipboard.', values: { @@ -1179,7 +1237,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel', { defaultMessage: 'Copy {POST} {URL}', values: { POST, @@ -1187,7 +1245,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyDescription: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription', { defaultMessage: 'Alternatively, copy this {POST} {URL} to call generation from outside {KIBANA} or from Watcher.', values: { @@ -1197,14 +1255,14 @@ export const ComponentStrings = { }, }), getPDFPanelGenerateButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel', { defaultMessage: 'Generate {PDF}', values: { PDF, }, }), getPDFPanelGenerateDescription: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription', { defaultMessage: '{PDF}s can take a minute or two to generate based on the size of your workpad.', values: { @@ -1212,7 +1270,7 @@ export const ComponentStrings = { }, }), getShareableZipErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { defaultMessage: "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", values: { @@ -1221,69 +1279,117 @@ export const ComponentStrings = { }, }), getShareDownloadJSONTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle', { defaultMessage: 'Download as {JSON}', values: { JSON, }, }), getShareDownloadPDFTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle', { defaultMessage: '{PDF} reports', values: { PDF, }, }), + getShareMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareMenuButtonLabel', { + defaultMessage: 'Share', + }), getShareWebsiteTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle', { defaultMessage: 'Share on a website', }), getShareWorkpadMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage', { defaultMessage: 'Share this workpad', }), getUnknownExportErrorMessage: (type: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { defaultMessage: 'Unknown export type: {type}', values: { type, }, }), }, - WorkpadHeaderWorkpadZoom: { + WorkpadHeaderViewMenu: { + getAutoplayOffMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOffMenuItemLabel', { + defaultMessage: 'Turn autoplay off', + }), + getAutoplayOnMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplayOnMenuItemLabel', { + defaultMessage: 'Turn autoplay on', + }), + getAutoplaySettingsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.autoplaySettingsMenuItemLabel', { + defaultMessage: 'Autoplay settings', + }), + getFullscreenMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel', { + defaultMessage: 'Enter fullscreen mode', + }), + getHideEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.hideEditModeLabel', { + defaultMessage: 'Hide editing controls', + }), + getRefreshMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshMenuItemLabel', { + defaultMessage: 'Refresh data', + }), + getRefreshSettingsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshSettingsMenuItemLabel', { + defaultMessage: 'Auto refresh settings', + }), + getShowEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.showEditModeLabel', { + defaultMessage: 'Show editing controls', + }), + getViewMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuButtonLabel', { + defaultMessage: 'View', + }), + getViewMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuLabel', { + defaultMessage: 'View options', + }), getZoomControlsAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel', { defaultMessage: 'Zoom controls', }), getZoomControlsTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip', { defaultMessage: 'Zoom controls', }), getZoomFitToWindowText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText', { defaultMessage: 'Fit to window', }), getZoomInText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomInText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomInText', { defaultMessage: 'Zoom in', }), + getZoomMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel', { + defaultMessage: 'Zoom', + }), getZoomOutText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomOutText', { defaultMessage: 'Zoom out', }), getZoomPanelTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle', { defaultMessage: 'Zoom', }), getZoomPercentage: (scale: number) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomResetText', { defaultMessage: '{scalePercentage}%', values: { scalePercentage: scale * 100, }, }), getZoomResetText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue', { defaultMessage: 'Reset', }), }, diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts index 41f88a3c75f90..4464ed5dbf185 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts @@ -7,8 +7,6 @@ import { ElementFactory } from '../../types'; import { getElementStrings } from './index'; -import { TagStrings } from '../../i18n'; - /** * This function takes a set of Canvas Element specification factories, runs them, * replaces relevant strings (if available) and returns a new factory. We do this @@ -34,17 +32,6 @@ export const applyElementStrings = (elements: ElementFactory[]) => { if (displayName) { result.displayName = displayName; } - - // Set translated tags - if (result.tags) { - result.tags = result.tags.map(tag => { - if (tag in TagStrings) { - return TagStrings[tag](); - } - - return tag; - }); - } } return () => result; diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts index 3d835bdf31bf8..c28229bdab33f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts @@ -3,11 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); import { getElementStrings } from './element_strings'; -import { elementSpecs } from '../../canvas_plugin_src/elements'; +import { initializeElements } from '../../canvas_plugin_src/elements'; +import { coreMock } from '../../../../../../src/core/public/mocks'; -import { TagStrings } from '../tags'; +const elementSpecs = initializeElements(coreMock.createSetup() as any, {} as any); describe('ElementStrings', () => { const elementStrings = getElementStrings(); @@ -35,15 +35,4 @@ describe('ElementStrings', () => { expect(value).toHaveProperty('help'); }); }); - - test('All elements should have tags that are defined', () => { - const tagNames = Object.keys(TagStrings); - - elementSpecs.forEach(spec => { - const element = spec(); - if (element.tags) { - element.tags.forEach((tagName: string) => expect(tagNames).toContain(tagName)); - } - }); - }); }); diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts index df23dff1c7127..595ef4cb92206 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts @@ -23,7 +23,7 @@ interface ElementStringDict { export const getElementStrings = (): ElementStringDict => ({ areaChart: { displayName: i18n.translate('xpack.canvas.elements.areaChartDisplayName', { - defaultMessage: 'Area chart', + defaultMessage: 'Area', }), help: i18n.translate('xpack.canvas.elements.areaChartHelpText', { defaultMessage: 'A line chart with a filled body', @@ -31,7 +31,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, bubbleChart: { displayName: i18n.translate('xpack.canvas.elements.bubbleChartDisplayName', { - defaultMessage: 'Bubble chart', + defaultMessage: 'Bubble', }), help: i18n.translate('xpack.canvas.elements.bubbleChartHelpText', { defaultMessage: 'A customizable bubble chart', @@ -39,31 +39,31 @@ export const getElementStrings = (): ElementStringDict => ({ }, debug: { displayName: i18n.translate('xpack.canvas.elements.debugDisplayName', { - defaultMessage: 'Debug', + defaultMessage: 'Debug data', }), help: i18n.translate('xpack.canvas.elements.debugHelpText', { defaultMessage: 'Just dumps the configuration of the element', }), }, - donut: { - displayName: i18n.translate('xpack.canvas.elements.donutChartDisplayName', { - defaultMessage: 'Donut chart', - }), - help: i18n.translate('xpack.canvas.elements.donutChartHelpText', { - defaultMessage: 'A customizable donut chart', - }), - }, - dropdown_filter: { + dropdownFilter: { displayName: i18n.translate('xpack.canvas.elements.dropdownFilterDisplayName', { - defaultMessage: 'Dropdown filter', + defaultMessage: 'Dropdown select', }), help: i18n.translate('xpack.canvas.elements.dropdownFilterHelpText', { defaultMessage: 'A dropdown from which you can select values for an "exactly" filter', }), }, + filterDebug: { + displayName: i18n.translate('xpack.canvas.elements.filterDebugDisplayName', { + defaultMessage: 'Debug filters', + }), + help: i18n.translate('xpack.canvas.elements.filterDebugHelpText', { + defaultMessage: 'Shows the underlying global filters in a workpad', + }), + }, horizontalBarChart: { displayName: i18n.translate('xpack.canvas.elements.horizontalBarChartDisplayName', { - defaultMessage: 'Horizontal bar chart', + defaultMessage: 'Horizontal bar', }), help: i18n.translate('xpack.canvas.elements.horizontalBarChartHelpText', { defaultMessage: 'A customizable horizontal bar chart', @@ -71,7 +71,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, horizontalProgressBar: { displayName: i18n.translate('xpack.canvas.elements.horizontalProgressBarDisplayName', { - defaultMessage: 'Horizontal progress bar', + defaultMessage: 'Horizontal bar', }), help: i18n.translate('xpack.canvas.elements.horizontalProgressBarHelpText', { defaultMessage: 'Displays progress as a portion of a horizontal bar', @@ -79,7 +79,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, horizontalProgressPill: { displayName: i18n.translate('xpack.canvas.elements.horizontalProgressPillDisplayName', { - defaultMessage: 'Horizontal progress pill', + defaultMessage: 'Horizontal pill', }), help: i18n.translate('xpack.canvas.elements.horizontalProgressPillHelpText', { defaultMessage: 'Displays progress as a portion of a horizontal pill', @@ -95,7 +95,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, lineChart: { displayName: i18n.translate('xpack.canvas.elements.lineChartDisplayName', { - defaultMessage: 'Line chart', + defaultMessage: 'Line', }), help: i18n.translate('xpack.canvas.elements.lineChartHelpText', { defaultMessage: 'A customizable line chart', @@ -119,7 +119,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, pie: { displayName: i18n.translate('xpack.canvas.elements.pieDisplayName', { - defaultMessage: 'Pie chart', + defaultMessage: 'Pie', }), help: i18n.translate('xpack.canvas.elements.pieHelpText', { defaultMessage: 'Pie chart', @@ -135,7 +135,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressGauge: { displayName: i18n.translate('xpack.canvas.elements.progressGaugeDisplayName', { - defaultMessage: 'Progress gauge', + defaultMessage: 'Gauge', }), help: i18n.translate('xpack.canvas.elements.progressGaugeHelpText', { defaultMessage: 'Displays progress as a portion of a gauge', @@ -143,7 +143,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressSemicircle: { displayName: i18n.translate('xpack.canvas.elements.progressSemicircleDisplayName', { - defaultMessage: 'Progress semicircle', + defaultMessage: 'Semicircle', }), help: i18n.translate('xpack.canvas.elements.progressSemicircleHelpText', { defaultMessage: 'Displays progress as a portion of a semicircle', @@ -151,7 +151,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressWheel: { displayName: i18n.translate('xpack.canvas.elements.progressWheelDisplayName', { - defaultMessage: 'Progress wheel', + defaultMessage: 'Wheel', }), help: i18n.translate('xpack.canvas.elements.progressWheelHelpText', { defaultMessage: 'Displays progress as a portion of a wheel', @@ -189,15 +189,7 @@ export const getElementStrings = (): ElementStringDict => ({ defaultMessage: 'A scrollable grid for displaying data in a tabular format', }), }, - tiltedPie: { - displayName: i18n.translate('xpack.canvas.elements.tiltedPieDisplayName', { - defaultMessage: 'Tilted pie chart', - }), - help: i18n.translate('xpack.canvas.elements.tiltedPieHelpText', { - defaultMessage: 'A customizable tilted pie chart', - }), - }, - time_filter: { + timeFilter: { displayName: i18n.translate('xpack.canvas.elements.timeFilterDisplayName', { defaultMessage: 'Time filter', }), @@ -207,7 +199,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalBarChart: { displayName: i18n.translate('xpack.canvas.elements.verticalBarChartDisplayName', { - defaultMessage: 'Vertical bar chart', + defaultMessage: 'Vertical bar', }), help: i18n.translate('xpack.canvas.elements.verticalBarChartHelpText', { defaultMessage: 'A customizable vertical bar chart', @@ -215,7 +207,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalProgressBar: { displayName: i18n.translate('xpack.canvas.elements.verticalProgressBarDisplayName', { - defaultMessage: 'Vertical progress bar', + defaultMessage: 'Vertical bar', }), help: i18n.translate('xpack.canvas.elements.verticalProgressBarHelpText', { defaultMessage: 'Displays progress as a portion of a vertical bar', @@ -223,7 +215,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalProgressPill: { displayName: i18n.translate('xpack.canvas.elements.verticalProgressPillDisplayName', { - defaultMessage: 'Vertical progress pill', + defaultMessage: 'Vertical pill', }), help: i18n.translate('xpack.canvas.elements.verticalProgressPillHelpText', { defaultMessage: 'Displays progress as a portion of a vertical pill', diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts index 836c7395ac448..cc601b0ea0e31 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { alterColumn } from '../../../canvas_plugin_src/functions/common/alterColumn'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { DATATABLE_COLUMN_TYPES } from '../../../common/lib'; +import { DATATABLE_COLUMN_TYPES } from '../../../common/lib/constants'; export const help: FunctionHelp<FunctionFactory<typeof alterColumn>> = { help: i18n.translate('xpack.canvas.functions.alterColumnHelpText', { diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts index e105f7e678af3..41bf86055f1e3 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts @@ -5,12 +5,12 @@ */ import { i18n } from '@kbn/i18n'; -import { timelion } from '../../../public/functions/timelion'; +import { timelionFunctionFactory } from '../../../public/functions/timelion'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; import { ELASTICSEARCH, DATEMATH, MOMENTJS_TIMEZONE_URL } from '../../constants'; -export const help: FunctionHelp<FunctionFactory<typeof timelion>> = { +export const help: FunctionHelp<FunctionFactory<ReturnType<typeof timelionFunctionFactory>>> = { help: i18n.translate('xpack.canvas.functions.timelionHelpText', { defaultMessage: 'Use Timelion to extract one or more timeseries from many sources.', }), diff --git a/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts b/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts index 32b07a45c17db..124d70ff3095f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts +++ b/x-pack/legacy/plugins/canvas/i18n/shortcuts.ts @@ -42,10 +42,10 @@ export const ShortcutStrings = { defaultMessage: 'Delete', }), BRING_FORWARD: i18n.translate('xpack.canvas.keyboardShortcuts.bringFowardShortcutHelpText', { - defaultMessage: 'Bring to front', + defaultMessage: 'Bring forward', }), BRING_TO_FRONT: i18n.translate('xpack.canvas.keyboardShortcuts.bringToFrontShortcutHelpText', { - defaultMessage: 'Bring forward', + defaultMessage: 'Bring to front', }), SEND_BACKWARD: i18n.translate('xpack.canvas.keyboardShortcuts.sendBackwardShortcutHelpText', { defaultMessage: 'Send backward', diff --git a/x-pack/legacy/plugins/canvas/i18n/tags.ts b/x-pack/legacy/plugins/canvas/i18n/tags.ts index 41007c58d738d..9595554260297 100644 --- a/x-pack/legacy/plugins/canvas/i18n/tags.ts +++ b/x-pack/legacy/plugins/canvas/i18n/tags.ts @@ -7,32 +7,12 @@ import { i18n } from '@kbn/i18n'; export const TagStrings: { [key: string]: () => string } = { - chart: () => - i18n.translate('xpack.canvas.tags.chartTag', { - defaultMessage: 'chart', - }), - filter: () => - i18n.translate('xpack.canvas.tags.filterTag', { - defaultMessage: 'filter', - }), - graphic: () => - i18n.translate('xpack.canvas.tags.graphicTag', { - defaultMessage: 'graphic', - }), presentation: () => i18n.translate('xpack.canvas.tags.presentationTag', { defaultMessage: 'presentation', }), - proportion: () => - i18n.translate('xpack.canvas.tags.proportionTag', { - defaultMessage: 'proportion', - }), report: () => i18n.translate('xpack.canvas.tags.reportTag', { defaultMessage: 'report', }), - text: () => - i18n.translate('xpack.canvas.tags.textTag', { - defaultMessage: 'text', - }), }; diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index f75b3b427c41b..f746a24e9b261 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -30,8 +30,12 @@ import { VALUE_CLICK_TRIGGER, ActionByType } from '../../../../../src/plugins/ui /* eslint-disable */ import { ACTION_VALUE_CLICK } from '../../../../../src/plugins/data/public/actions/value_click_action'; /* eslint-enable */ +import { init as initStatsReporter } from './lib/ui_metric'; import { CapabilitiesStrings } from '../i18n'; + +import { startServices, stopServices, services } from './services'; + const { ReadOnlyBadge: strings } = CapabilitiesStrings; let restoreAction: ActionByType<any> | undefined; @@ -50,8 +54,14 @@ export const renderApp = ( { element }: AppMountParameters, canvasStore: Store ) => { + const canvasServices = Object.entries(services).reduce((reduction, [key, provider]) => { + reduction[key] = provider.getService(); + + return reduction; + }, {} as Record<string, any>); + ReactDOM.render( - <KibanaContextProvider services={{ ...plugins, ...coreStart }}> + <KibanaContextProvider services={{ ...plugins, ...coreStart, canvas: canvasServices }}> <I18nProvider> <Provider store={canvasStore}> <App /> @@ -70,6 +80,8 @@ export const initializeCanvas = async ( startPlugins: CanvasStartDeps, registries: SetupRegistries ) => { + startServices(coreSetup, coreStart, setupPlugins, startPlugins); + // Create Store const canvasStore = await createStore(coreSetup, setupPlugins); @@ -121,10 +133,15 @@ export const initializeCanvas = async ( startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, emptyAction); } + if (setupPlugins.usageCollection) { + initStatsReporter(setupPlugins.usageCollection.reportUiStats); + } + return canvasStore; }; export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => { + stopServices(); destroyRegistries(); resetInterpreter(); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js b/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js index 718443fcdd990..4e3920bf34f67 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js @@ -6,7 +6,7 @@ import { ErrorStrings } from '../../../i18n'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; +import { notifyService } from '../../services'; import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; import { getDefaultWorkpad } from '../../state/defaults'; import { setWorkpad } from '../../state/actions/workpad'; @@ -33,7 +33,9 @@ export const routes = [ dispatch(resetAssets()); router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: strings.getCreateFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getCreateFailureErrorMessage() }); router.redirectTo('home'); } }, @@ -59,7 +61,9 @@ export const routes = [ // reset transient properties when changing workpads dispatch(setZoomScale(1)); } catch (err) { - notify.error(err, { title: strings.getLoadFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getLoadFailureErrorMessage() }); return router.redirectTo('home'); } } diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js index b8e1bece6ac30..fc3ac9922355a 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.js @@ -43,7 +43,7 @@ export class WorkpadApp extends React.PureComponent { <div className="canvasLayout__cols"> <div className="canvasLayout__stage"> <div className="canvasLayout__stageHeader"> - <WorkpadHeader /> + <WorkpadHeader commit={this.interactivePageLayout || (() => {})} /> </div> <div @@ -66,7 +66,7 @@ export class WorkpadApp extends React.PureComponent { {isWriteable && ( <div className="canvasLayout__sidebar hide-for-sharing"> - <Sidebar commit={this.interactivePageLayout || (() => {})} /> + <Sidebar /> </div> )} </div> diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss index c5161439b71c3..c7dae8452a93c 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: ($euiSizeXS + 1px) $euiSize $euiSizeXS; + padding: 1px $euiSize 0; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js b/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js index e837f5200a159..2886aa868eb9e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js +++ b/x-pack/legacy/plugins/canvas/public/components/app/track_route_change.js @@ -7,13 +7,17 @@ import { get } from 'lodash'; import { getWindow } from '../../lib/get_window'; import { CANVAS_APP } from '../../../common/lib/constants'; -import { getCoreStart, getStartPlugins } from '../../legacy'; +import { platformService } from '../../services'; export function trackRouteChange() { - const basePath = getCoreStart().http.basePath.get(); - // storage.set(LOCALSTORAGE_LASTPAGE, pathname); - getStartPlugins().__LEGACY.trackSubUrlForApp( - CANVAS_APP, - getStartPlugins().__LEGACY.absoluteToParsedUrl(get(getWindow(), 'location.href'), basePath) - ); + const basePath = platformService.getService().coreStart.http.basePath.get(); + + platformService + .getService() + .startPlugins.__LEGACY.trackSubUrlForApp( + CANVAS_APP, + platformService + .getService() + .startPlugins.__LEGACY.absoluteToParsedUrl(get(getWindow(), 'location.href'), basePath) + ); } diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot index 5cb64964ee38f..6601f570209e9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot @@ -18,13 +18,13 @@ exports[`Storyshots components/Assets/Asset airplane 1`] = ` className="canvasAsset__thumb canvasCheckered" > <figure - className="euiImage canvasAsset__img" - role="figure" + className="euiImage canvasAsset__img " > <img alt="Asset thumbnail" className="euiImage__img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=" + style={Object {}} /> </figure> </div> @@ -192,13 +192,13 @@ exports[`Storyshots components/Assets/Asset marker 1`] = ` className="canvasAsset__thumb canvasCheckered" > <figure - className="euiImage canvasAsset__img" - role="figure" + className="euiImage canvasAsset__img " > <img alt="Asset thumbnail" className="euiImage__img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=" + style={Object {}} /> </figure> </div> diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot new file mode 100644 index 0000000000000..aff630b21c770 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot @@ -0,0 +1,761 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` +Array [ + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={1} + />, + <div + data-focus-lock-disabled={false} + onBlur={[Function]} + onFocus={[Function]} + > + <div + className="euiModal canvasAssetManager canvasModal--fixedSize" + onKeyDown={[Function]} + style={ + Object { + "maxWidth": "1000px", + } + } + tabIndex={0} + > + <button + aria-label="Closes this modal window" + className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon" + onClick={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="cross" + size="m" + /> + </button> + <div + className="euiModal__flex" + > + <div + className="euiModalHeader canvasAssetManager__modalHeader" + > + <div + className="euiModalHeader__title canvasAssetManager__modalHeaderTitle" + > + Manage workpad assets + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive canvasAssetManager__fileUploadWrapper" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + className="euiFilePicker euiFilePicker--compressed" + > + <div + className="euiFilePicker__wrap" + > + <input + accept="image/*" + aria-describedby="generated-id" + className="euiFilePicker__input" + multiple={true} + onChange={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDrop={[Function]} + type="file" + /> + <div + className="euiFilePicker__prompt" + id="generated-id" + > + <div + aria-hidden="true" + className="euiFilePicker__icon" + data-euiicon-type="importAction" + size="m" + /> + <div + className="euiFilePicker__promptText" + > + Select or drag and drop images + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div + className="euiModalBody" + > + <div + className="euiModalBody__overflow" + > + <div + className="euiText euiText--small" + > + <div + className="euiTextColor euiTextColor--subdued" + > + <p> + Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets. + </p> + </div> + </div> + <div + className="euiSpacer euiSpacer--l" + /> + <div + className="euiPanel euiPanel--paddingMedium canvasAssetManager__emptyPanel" + > + <div + className="euiEmptyPrompt" + > + <div + color="subdued" + data-euiicon-type="importAction" + size="xxl" + /> + <div + className="euiSpacer euiSpacer--s" + /> + <span + className="euiTextColor euiTextColor--subdued" + > + <h2 + className="euiTitle euiTitle--xsmall" + > + Import your assets to get started + </h2> + <div + className="euiSpacer euiSpacer--m" + /> + </span> + </div> + </div> + </div> + </div> + <div + className="euiModalFooter canvasAssetManager__modalFooter" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow canvasAssetManager__meterWrapper" + > + <div + className="euiFlexItem" + > + <progress + aria-labelledby="CanvasAssetManagerLabel" + className="euiProgress euiProgress--native euiProgress--s euiProgress--secondary" + max={25000} + value={0} + /> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero eui-textNoWrap" + > + <div + className="euiText euiText--medium" + id="CanvasAssetManagerLabel" + > + 0% space used + </div> + </div> + </div> + <button + className="euiButton euiButton--primary euiButton--small" + onClick={[Function]} + type="button" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + Close + </span> + </span> + </button> + </div> + </div> + </div> + </div>, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, +] +`; + +exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` +Array [ + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={1} + />, + <div + data-focus-lock-disabled={false} + onBlur={[Function]} + onFocus={[Function]} + > + <div + className="euiModal canvasAssetManager canvasModal--fixedSize" + onKeyDown={[Function]} + style={ + Object { + "maxWidth": "1000px", + } + } + tabIndex={0} + > + <button + aria-label="Closes this modal window" + className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon" + onClick={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="cross" + size="m" + /> + </button> + <div + className="euiModal__flex" + > + <div + className="euiModalHeader canvasAssetManager__modalHeader" + > + <div + className="euiModalHeader__title canvasAssetManager__modalHeaderTitle" + > + Manage workpad assets + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive canvasAssetManager__fileUploadWrapper" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + className="euiFilePicker euiFilePicker--compressed" + > + <div + className="euiFilePicker__wrap" + > + <input + accept="image/*" + aria-describedby="generated-id" + className="euiFilePicker__input" + multiple={true} + onChange={[Function]} + onDragLeave={[Function]} + onDragOver={[Function]} + onDrop={[Function]} + type="file" + /> + <div + className="euiFilePicker__prompt" + id="generated-id" + > + <div + aria-hidden="true" + className="euiFilePicker__icon" + data-euiicon-type="importAction" + size="m" + /> + <div + className="euiFilePicker__promptText" + > + Select or drag and drop images + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div + className="euiModalBody" + > + <div + className="euiModalBody__overflow" + > + <div + className="euiText euiText--small" + > + <div + className="euiTextColor euiTextColor--subdued" + > + <p> + Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets. + </p> + </div> + </div> + <div + className="euiSpacer euiSpacer--l" + /> + <div + className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" + > + <div + className="euiFlexItem" + > + <div + className="euiPanel euiPanel--paddingSmall canvasAsset" + > + <div + className="canvasAsset__thumb canvasCheckered" + > + <figure + className="euiImage canvasAsset__img " + > + <img + alt="Asset thumbnail" + className="euiImage__img" + src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=" + style={Object {}} + /> + </figure> + </div> + <div + className="euiSpacer euiSpacer--s" + /> + <div + className="euiText euiText--extraSmall eui-textBreakAll" + > + <p + className="eui-textBreakAll" + > + <strong> + airplane + </strong> + <br /> + <span + className="euiTextColor euiTextColor--subdued" + > + <small> + ( + 1 + kb) + </small> + </span> + </p> + </div> + <div + className="euiSpacer euiSpacer--s" + /> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero asset-create-image" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Create image element" + className="euiButtonIcon euiButtonIcon--primary" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="vector" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero asset-download" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <div + className="canvasDownload" + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex={0} + > + <button + aria-label="Download" + className="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="sortDown" + size="m" + /> + </button> + </div> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <div + className="canvasClipboard" + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex={0} + > + <button + aria-label="Copy id to clipboard" + className="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="copyClipboard" + size="m" + /> + </button> + </div> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete" + className="euiButtonIcon euiButtonIcon--danger" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + </div> + <div + className="euiFlexItem" + > + <div + className="euiPanel euiPanel--paddingSmall canvasAsset" + > + <div + className="canvasAsset__thumb canvasCheckered" + > + <figure + className="euiImage canvasAsset__img " + > + <img + alt="Asset thumbnail" + className="euiImage__img" + src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=" + style={Object {}} + /> + </figure> + </div> + <div + className="euiSpacer euiSpacer--s" + /> + <div + className="euiText euiText--extraSmall eui-textBreakAll" + > + <p + className="eui-textBreakAll" + > + <strong> + marker + </strong> + <br /> + <span + className="euiTextColor euiTextColor--subdued" + > + <small> + ( + 1 + kb) + </small> + </span> + </p> + </div> + <div + className="euiSpacer euiSpacer--s" + /> + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--alignItemsBaseline euiFlexGroup--justifyContentCenter euiFlexGroup--directionRow" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero asset-create-image" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Create image element" + className="euiButtonIcon euiButtonIcon--primary" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="vector" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero asset-download" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <div + className="canvasDownload" + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex={0} + > + <button + aria-label="Download" + className="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="sortDown" + size="m" + /> + </button> + </div> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <div + className="canvasClipboard" + onClick={[Function]} + onKeyPress={[Function]} + role="button" + tabIndex={0} + > + <button + aria-label="Copy id to clipboard" + className="euiButtonIcon euiButtonIcon--primary" + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="copyClipboard" + size="m" + /> + </button> + </div> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete" + className="euiButtonIcon euiButtonIcon--danger" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div + className="euiModalFooter canvasAssetManager__modalFooter" + > + <div + className="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow canvasAssetManager__meterWrapper" + > + <div + className="euiFlexItem" + > + <progress + aria-labelledby="CanvasAssetManagerLabel" + className="euiProgress euiProgress--native euiProgress--s euiProgress--secondary" + max={25000} + value={2} + /> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero eui-textNoWrap" + > + <div + className="euiText euiText--medium" + id="CanvasAssetManagerLabel" + > + 0% space used + </div> + </div> + </div> + <button + className="euiButton euiButton--primary euiButton--small" + onClick={[Function]} + type="button" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + Close + </span> + </span> + </button> + </div> + </div> + </div> + </div>, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, +] +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx deleted file mode 100644 index 045a3b4f64a44..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx +++ /dev/null @@ -1,47 +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 { action } from '@storybook/addon-actions'; -import { storiesOf } from '@storybook/react'; -import React from 'react'; -import { AssetType } from '../../../../types'; -import { AssetManager } from '../asset_manager'; - -const AIRPLANE: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'airplane', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -const MARKER: AssetType = { - '@created': '2018-10-13T16:44:44.648Z', - id: 'marker', - type: 'dataurl', - value: - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', -}; - -storiesOf('components/Assets/AssetManager', module) - .add('no assets', () => ( - // @ts-ignore @types/react has not been updated to support defaultProps yet. - <AssetManager - onAddImageElement={action('onAddImageElement')} - onAssetAdd={action('onAssetAdd')} - onAssetCopy={action('onAssetCopy')} - onAssetDelete={action('onAssetDelete')} - /> - )) - .add('two assets', () => ( - <AssetManager - assetValues={[AIRPLANE, MARKER]} - onAddImageElement={action('onAddImageElement')} - onAssetAdd={action('onAssetAdd')} - onAssetCopy={action('onAssetCopy')} - onAssetDelete={action('onAssetDelete')} - /> - )); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx new file mode 100644 index 0000000000000..cb42823ccab7b --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { AssetType } from '../../../../types'; +import { AssetManager } from '../asset_manager'; + +const AIRPLANE: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'airplane', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1Ni4zMSA1Ni4zMSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMDc4YTA7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPlBsYW5lIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDkuNTEsNDguOTMsNDEuMjYsMjIuNTIsNTMuNzYsMTBhNS4yOSw1LjI5LDAsMCwwLTcuNDgtNy40N2wtMTIuNSwxMi41TDcuMzgsNi43OUEuNy43LDAsMCwwLDYuNjksN0wxLjIsMTIuNDVhLjcuNywwLDAsMCwwLDFMMTkuODUsMjlsLTcuMjQsNy4yNC03Ljc0LS42YS43MS43MSwwLDAsMC0uNTMuMkwxLjIxLDM5YS42Ny42NywwLDAsMCwuMDgsMUw5LjQ1LDQ2bC4wNywwYy4xMS4xMy4yMi4yNi4zNC4zOHMuMjUuMjMuMzguMzRhLjM2LjM2LDAsMCwwLDAsLjA3TDE2LjMzLDU1YS42OC42OCwwLDAsMCwxLC4wN0wyMC40OSw1MmEuNjcuNjcsMCwwLDAsLjE5LS41NGwtLjU5LTcuNzQsNy4yNC03LjI0TDQyLjg1LDU1LjA2YS42OC42OCwwLDAsMCwxLDBsNS41LTUuNUEuNjYuNjYsMCwwLDAsNDkuNTEsNDguOTNaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +const MARKER: AssetType = { + '@created': '2018-10-13T16:44:44.648Z', + id: 'marker', + type: 'dataurl', + value: + 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzOC4zOSA1Ny41NyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7c3Ryb2tlOiMwMTliOGY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOjJweDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkxvY2F0aW9uIEljb248L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTkuMTksMUExOC4xOSwxOC4xOSwwLDAsMCwyLjk0LDI3LjM2aDBhMTkuNTEsMTkuNTEsMCwwLDAsMSwxLjc4TDE5LjE5LDU1LjU3LDM0LjM4LDI5LjIxQTE4LjE5LDE4LjE5LDAsMCwwLDE5LjE5LDFabTAsMjMuMjlhNS41Myw1LjUzLDAsMSwxLDUuNTMtNS41M0E1LjUzLDUuNTMsMCwwLDEsMTkuMTksMjQuMjlaIi8+PC9nPjwvZz48L3N2Zz4=', +}; + +storiesOf('components/Assets/AssetManager', module) + .add('no assets', () => ( + <AssetManager + onAddImageElement={action('onAddImageElement')} + onAssetAdd={action('onAssetAdd')} + onAssetCopy={action('onAssetCopy')} + onAssetDelete={action('onAssetDelete')} + onClose={action('onClose')} + /> + )) + .add('two assets', () => ( + <AssetManager + assetValues={[AIRPLANE, MARKER]} + onAddImageElement={action('onAddImageElement')} + onAssetAdd={action('onAssetAdd')} + onAssetCopy={action('onAssetCopy')} + onAssetDelete={action('onAssetDelete')} + onClose={action('onClose')} + /> + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx index 3785f81cc25b9..c27f0c002c3d1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx @@ -4,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore (elastic/eui#1557) EuiFilePicker is not exported yet - EuiFilePicker, - // @ts-ignore (elastic/eui#1557) EuiImage is not exported yet - EuiImage, -} from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { Fragment, PureComponent } from 'react'; @@ -22,7 +15,7 @@ import { AssetModal } from './asset_modal'; const { AssetManager: strings } = ComponentStrings; -interface Props { +export interface Props { /** A list of assets, if available */ assetValues: AssetType[]; /** Function to invoke when an asset is selected to be added as an element to the workpad */ @@ -33,13 +26,13 @@ interface Props { onAssetCopy: () => void; /** Function to invoke when an asset is added */ onAssetAdd: (asset: File) => void; + /** Function to invoke when an asset modal is closed */ + onClose: () => void; } interface State { /** The id of the asset to delete, if applicable. Is set if the viewer clicks the delete icon */ deleteId: string | null; - /** Determines if the modal is currently visible */ - isModalVisible: boolean; /** Indicates if the modal is currently loading */ isLoading: boolean; } @@ -51,6 +44,7 @@ export class AssetManager extends PureComponent<Props, State> { onAssetAdd: PropTypes.func.isRequired, onAssetCopy: PropTypes.func.isRequired, onAssetDelete: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, }; public static defaultProps = { @@ -60,12 +54,11 @@ export class AssetManager extends PureComponent<Props, State> { public state = { deleteId: null, isLoading: false, - isModalVisible: false, }; public render() { - const { isModalVisible, isLoading } = this.state; - const { assetValues, onAssetCopy, onAddImageElement } = this.props; + const { isLoading } = this.state; + const { assetValues, onAssetCopy, onAddImageElement, onClose } = this.props; const assetModal = ( <AssetModal @@ -74,10 +67,10 @@ export class AssetManager extends PureComponent<Props, State> { onAssetCopy={onAssetCopy} onAssetCreate={(createdAsset: AssetType) => { onAddImageElement(createdAsset.id); - this.setState({ isModalVisible: false }); + onClose(); }} onAssetDelete={(asset: AssetType) => this.setState({ deleteId: asset.id })} - onClose={() => this.setState({ isModalVisible: false })} + onClose={onClose} onFileUpload={this.handleFileUpload} /> ); @@ -95,14 +88,12 @@ export class AssetManager extends PureComponent<Props, State> { return ( <Fragment> - <EuiButtonEmpty onClick={this.showModal}>{strings.getButtonLabel()}</EuiButtonEmpty> - {isModalVisible ? assetModal : null} + {assetModal} {confirmModal} </Fragment> ); } - private showModal = () => this.setState({ isModalVisible: true }); private resetDelete = () => this.setState({ deleteId: null }); private doDelete = () => { diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx index 3dfbb1b1fde3c..8637db8e9f962 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx @@ -98,6 +98,7 @@ export const AssetModal: FunctionComponent<Props> = props => { <EuiFilePicker initialPromptText={strings.getFilePickerPromptText()} compressed + display="default" multiple onChange={onFileUpload} accept="image/*" diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.js b/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.js deleted file mode 100644 index 6c05eec0c3c09..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.js +++ /dev/null @@ -1,94 +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 { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { set, get } from 'lodash'; -import { fromExpression, toExpression } from '@kbn/interpreter/common'; -import { notify } from '../../lib/notify'; -import { getAssets } from '../../state/selectors/assets'; -import { removeAsset, createAsset } from '../../state/actions/assets'; -import { elementsRegistry } from '../../lib/elements_registry'; -import { addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { encode } from '../../../common/lib/dataurl'; -import { getId } from '../../lib/get_id'; -import { findExistingAsset } from '../../lib/find_existing_asset'; -import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; -import { AssetManager as Component } from './asset_manager'; - -const mapStateToProps = state => ({ - assets: getAssets(state), - selectedPage: getSelectedPage(state), -}); - -const mapDispatchToProps = dispatch => ({ - onAddImageElement: pageId => assetId => { - const imageElement = elementsRegistry.get('image'); - const elementAST = fromExpression(imageElement.expression); - const selector = ['chain', '0', 'arguments', 'dataurl']; - const subExp = [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'asset', - arguments: { - _: [assetId], - }, - }, - ], - }, - ]; - const newAST = set(elementAST, selector, subExp); - imageElement.expression = toExpression(newAST); - dispatch(addElement(pageId, imageElement)); - }, - onAssetAdd: (type, content) => { - // make the ID here and pass it into the action - const assetId = getId('asset'); - dispatch(createAsset(type, content, assetId)); - - // then return the id, so the caller knows the id that will be created - return assetId; - }, - onAssetDelete: assetId => dispatch(removeAsset(assetId)), -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { assets, selectedPage } = stateProps; - const { onAssetAdd } = dispatchProps; - const assetValues = Object.values(assets); // pull values out of assets object - - return { - ...ownProps, - ...dispatchProps, - onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage), - selectedPage, - assetValues, - onAssetAdd: file => { - const [type, subtype] = get(file, 'type', '').split('/'); - if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { - return encode(file).then(dataurl => { - const type = 'dataurl'; - const existingId = findExistingAsset(type, dataurl, assetValues); - if (existingId) { - return existingId; - } - return onAssetAdd(type, dataurl); - }); - } - - return false; - }, - }; -}; - -export const AssetManager = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps({ onAssetCopy: asset => notify.success(`Copied '${asset.id}' to clipboard`) }) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.ts b/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.ts new file mode 100644 index 0000000000000..3fd34d6d2a9bb --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/index.ts @@ -0,0 +1,109 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { set, get } from 'lodash'; +import { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { getAssets } from '../../state/selectors/assets'; +// @ts-ignore Untyped local +import { removeAsset, createAsset } from '../../state/actions/assets'; +// @ts-ignore Untyped local +import { elementsRegistry } from '../../lib/elements_registry'; +// @ts-ignore Untyped local +import { addElement } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { encode } from '../../../common/lib/dataurl'; +import { getId } from '../../lib/get_id'; +// @ts-ignore Untyped Local +import { findExistingAsset } from '../../lib/find_existing_asset'; +import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { WithKibanaProps } from '../../'; +import { AssetManager as Component, Props as AssetManagerProps } from './asset_manager'; + +import { State, ExpressionAstExpression, AssetType } from '../../../types'; + +const mapStateToProps = (state: State) => ({ + assets: getAssets(state), + selectedPage: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: (action: any) => void) => ({ + onAddImageElement: (pageId: string) => (assetId: string) => { + const imageElement = elementsRegistry.get('image'); + const elementAST = fromExpression(imageElement.expression); + const selector = ['chain', '0', 'arguments', 'dataurl']; + const subExp: ExpressionAstExpression[] = [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'asset', + arguments: { + _: [assetId], + }, + }, + ], + }, + ]; + const newAST = set<ExpressionAstExpression>(elementAST, selector, subExp); + imageElement.expression = toExpression(newAST); + dispatch(addElement(pageId, imageElement)); + }, + onAssetAdd: (type: string, content: string) => { + // make the ID here and pass it into the action + const assetId = getId('asset'); + dispatch(createAsset(type, content, assetId)); + + // then return the id, so the caller knows the id that will be created + return assetId; + }, + onAssetDelete: (assetId: string) => dispatch(removeAsset(assetId)), +}); + +const mergeProps = ( + stateProps: ReturnType<typeof mapStateToProps>, + dispatchProps: ReturnType<typeof mapDispatchToProps>, + ownProps: AssetManagerProps +) => { + const { assets, selectedPage } = stateProps; + const { onAssetAdd } = dispatchProps; + const assetValues = Object.values(assets); // pull values out of assets object + + return { + ...ownProps, + ...dispatchProps, + onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage), + selectedPage, + assetValues, + onAssetAdd: (file: File) => { + const [type, subtype] = get(file, 'type', '').split('/'); + if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { + return encode(file).then(dataurl => { + const dataurlType = 'dataurl'; + const existingId = findExistingAsset(dataurlType, dataurl, assetValues); + if (existingId) { + return existingId; + } + return onAssetAdd(dataurlType, dataurl); + }); + } + + return false; + }, + }; +}; + +export const AssetManager = compose<any, any>( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withProps(({ kibana }: WithKibanaProps) => ({ + onAssetCopy: (asset: AssetType) => + kibana.services.canvas.notify.success(`Copied '${asset.id}' to clipboard`), + })) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot deleted file mode 100644 index 6572aa582f48c..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot +++ /dev/null @@ -1,797 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` -<div - style={ - Object { - "width": "1000px", - } - } -> - <div - className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" - > - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Custom Element 1 - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - sample description - </p> - </div> - </div> - <div - className="euiCard__footer" - /> - </div> - <div - className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Edit element" - className="euiButtonIcon euiButtonIcon--primary" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="pencil" - size="m" - /> - </button> - </span> - </div> - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Delete element" - className="euiButtonIcon euiButtonIcon--danger" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="trash" - size="m" - /> - </button> - </span> - </div> - </div> - </div> - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Custom Element 2 - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - Aenean eu justo auctor, placerat felis non, scelerisque dolor. - </p> - </div> - </div> - <div - className="euiCard__footer" - /> - </div> - <div - className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Edit element" - className="euiButtonIcon euiButtonIcon--primary" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="pencil" - size="m" - /> - </button> - </span> - </div> - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Delete element" - className="euiButtonIcon euiButtonIcon--danger" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="trash" - size="m" - /> - </button> - </span> - </div> - </div> - </div> - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Custom Element 3 - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. - </p> - </div> - </div> - <div - className="euiCard__footer" - /> - </div> - <div - className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Edit element" - className="euiButtonIcon euiButtonIcon--primary" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="pencil" - size="m" - /> - </button> - </span> - </div> - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Delete element" - className="euiButtonIcon euiButtonIcon--danger" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="trash" - size="m" - /> - </button> - </span> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`Storyshots components/Elements/ElementGrid with controls and filter 1`] = ` -<div - style={ - Object { - "width": "1000px", - } - } -> - <div - className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" - > - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Custom Element 3 - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. - </p> - </div> - </div> - <div - className="euiCard__footer" - /> - </div> - <div - className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Edit element" - className="euiButtonIcon euiButtonIcon--primary" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="pencil" - size="m" - /> - </button> - </span> - </div> - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Delete element" - className="euiButtonIcon euiButtonIcon--danger" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="trash" - size="m" - /> - </button> - </span> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`Storyshots components/Elements/ElementGrid with tags filter 1`] = ` -<div - style={ - Object { - "width": "1000px", - } - } -> - <div - className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" - > - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Image - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - A static image - </p> - </div> - </div> - <div - className="euiCard__footer" - > - <span - className="euiBadge euiBadge--iconLeft" - style={ - Object { - "backgroundColor": "#666666", - "color": "#fff", - } - } - > - <span - className="euiBadge__content" - > - <span - className="euiBadge__text" - > - graphic - </span> - </span> - </span> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`Storyshots components/Elements/ElementGrid with text filter 1`] = ` -<div - style={ - Object { - "width": "1000px", - } - } -> - <div - className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" - > - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Data table - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - A scrollable grid for displaying data in a tabular format - </p> - </div> - </div> - <div - className="euiCard__footer" - > - <span - className="euiBadge euiBadge--iconLeft" - style={ - Object { - "backgroundColor": "#666666", - "color": "#fff", - } - } - > - <span - className="euiBadge__content" - > - <span - className="euiBadge__text" - > - text - </span> - </span> - </span> - </div> - </div> - </div> - </div> -</div> -`; - -exports[`Storyshots components/Elements/ElementGrid without controls 1`] = ` -<div - style={ - Object { - "width": "1000px", - } - } -> - <div - className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" - > - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Area chart - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - A line chart with a filled body - </p> - </div> - </div> - <div - className="euiCard__footer" - > - <span - className="euiBadge euiBadge--iconLeft" - style={ - Object { - "backgroundColor": "#666666", - "color": "#fff", - } - } - > - <span - className="euiBadge__content" - > - <span - className="euiBadge__text" - > - chart - </span> - </span> - </span> - </div> - </div> - </div> - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Image - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - A static image - </p> - </div> - </div> - <div - className="euiCard__footer" - > - <span - className="euiBadge euiBadge--iconLeft" - style={ - Object { - "backgroundColor": "#666666", - "color": "#fff", - } - } - > - <span - className="euiBadge__content" - > - <span - className="euiBadge__text" - > - graphic - </span> - </span> - </span> - </div> - </div> - </div> - <div - className="euiFlexItem canvasElementCard__wrapper" - > - <div - className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" - onClick={[Function]} - > - <div - className="euiCard__top" - > - <img - alt="" - className="euiCard__image" - src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" - /> - </div> - <div - className="euiCard__content" - > - <span - className="euiTitle euiTitle--small euiCard__title" - id="generated-idTitle" - > - <button - aria-describedby=" generated-idDescription" - className="euiCard__titleButton" - onClick={[Function]} - > - Data table - </button> - </span> - <div - className="euiText euiText--small euiCard__description" - id="generated-idDescription" - > - <p> - A scrollable grid for displaying data in a tabular format - </p> - </div> - </div> - <div - className="euiCard__footer" - > - <span - className="euiBadge euiBadge--iconLeft" - style={ - Object { - "backgroundColor": "#666666", - "color": "#fff", - } - } - > - <span - className="euiBadge__content" - > - <span - className="euiBadge__text" - > - text - </span> - </span> - </span> - </div> - </div> - </div> - </div> -</div> -`; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_grid.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_grid.stories.tsx deleted file mode 100644 index d12c00bc5dd2e..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_grid.stories.tsx +++ /dev/null @@ -1,58 +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 React from 'react'; -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import { ElementGrid } from '../element_grid'; -import { testElements, testCustomElements } from './fixtures/test_elements'; - -storiesOf('components/Elements/ElementGrid', module) - .addDecorator(story => ( - <div - style={{ - width: '1000px', - }} - > - {story()} - </div> - )) - .add('without controls', () => ( - <ElementGrid elements={testElements} handleClick={action('addElement')} /> - )) - .add('with controls', () => ( - <ElementGrid - elements={testCustomElements} - handleClick={action('addCustomElement')} - showControls - onDelete={action('onDelete')} - onEdit={action('onEdit')} - /> - )) - .add('with text filter', () => ( - <ElementGrid - elements={testElements} - handleClick={action('addCustomElement')} - filterText="table" - /> - )) - .add('with tags filter', () => ( - <ElementGrid - elements={testElements} - handleClick={action('addCustomElement')} - filterTags={['graphic']} - /> - )) - .add('with controls and filter', () => ( - <ElementGrid - elements={testCustomElements} - handleClick={action('addCustomElement')} - filterText="Lorem" - showControls - onDelete={action('onDelete')} - onEdit={action('onEdit')} - /> - )); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx b/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx deleted file mode 100644 index 852b987fcfd24..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx +++ /dev/null @@ -1,110 +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 React from 'react'; -import PropTypes from 'prop-types'; -import { map } from 'lodash'; -import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui'; -import { ElementControls } from './element_controls'; -import { CustomElement, ElementSpec } from '../../../types'; -import { ElementCard } from '../element_card'; - -export interface Props { - /** - * list of elements to generate cards for - */ - elements: Array<ElementSpec | CustomElement>; - /** - * text to filter out cards - */ - filterText: string; - /** - * tags to filter out cards - */ - filterTags: string[]; - /** - * indicate whether or not edit/delete controls should be displayed - */ - showControls: boolean; - /** - * handler invoked when clicking a card - */ - handleClick: (element: ElementSpec | CustomElement) => void; - /** - * click handler for the edit button - */ - onEdit?: (element: ElementSpec | CustomElement) => void; - /** - * click handler for the delete button - */ - onDelete?: (element: ElementSpec | CustomElement) => void; -} - -export const ElementGrid = ({ - elements, - filterText, - filterTags, - handleClick, - onEdit, - onDelete, - showControls, -}: Props) => { - filterText = filterText.toLowerCase(); - - return ( - <EuiFlexGrid gutterSize="l" columns={4}> - {map(elements, (element: ElementSpec | CustomElement, index) => { - const { name, displayName = '', help = '', image, tags = [] } = element; - const whenClicked = () => handleClick(element); - let textMatch = false; - let tagsMatch = false; - - if ( - !filterText.length || - name.toLowerCase().includes(filterText) || - displayName.toLowerCase().includes(filterText) || - help.toLowerCase().includes(filterText) - ) { - textMatch = true; - } - - if (!filterTags.length || filterTags.every(tag => tags.includes(tag))) { - tagsMatch = true; - } - - if (!textMatch || !tagsMatch) { - return null; - } - - return ( - <EuiFlexItem key={index} className="canvasElementCard__wrapper"> - <ElementCard - title={displayName || name} - description={help} - image={image} - tags={tags} - onClick={whenClicked} - /> - {showControls && onEdit && onDelete && ( - <ElementControls onEdit={() => onEdit(element)} onDelete={() => onDelete(element)} /> - )} - </EuiFlexItem> - ); - })} - </EuiFlexGrid> - ); -}; - -ElementGrid.propTypes = { - elements: PropTypes.array.isRequired, - handleClick: PropTypes.func.isRequired, - showControls: PropTypes.bool, -}; - -ElementGrid.defaultProps = { - showControls: false, - filterTags: [], - filterText: '', -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js b/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js deleted file mode 100644 index dabf06a24aeb6..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js +++ /dev/null @@ -1,224 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiModalBody, - EuiTabbedContent, - EuiEmptyPrompt, - EuiSearchBar, - EuiSpacer, - EuiOverlayMask, -} from '@elastic/eui'; -import { map, sortBy } from 'lodash'; -import { ConfirmModal } from '../confirm_modal/confirm_modal'; -import { CustomElementModal } from '../custom_element_modal'; -import { getTagsFilter } from '../../lib/get_tags_filter'; -import { extractSearch } from '../../lib/extract_search'; -import { ComponentStrings } from '../../../i18n'; -import { ElementGrid } from './element_grid'; - -const { ElementTypes: strings } = ComponentStrings; - -const tagType = 'badge'; -export class ElementTypes extends Component { - static propTypes = { - addCustomElement: PropTypes.func.isRequired, - addElement: PropTypes.func.isRequired, - customElements: PropTypes.array.isRequired, - elements: PropTypes.object, - filterTags: PropTypes.arrayOf(PropTypes.string).isRequired, - findCustomElements: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - removeCustomElement: PropTypes.func.isRequired, - search: PropTypes.string, - setCustomElements: PropTypes.func.isRequired, - setSearch: PropTypes.func.isRequired, - setFilterTags: PropTypes.func.isRequired, - updateCustomElement: PropTypes.func.isRequired, - }; - - state = { - elementToDelete: null, - elementToEdit: null, - }; - - componentDidMount() { - // fetch custom elements - this.props.findCustomElements(); - } - - _showEditModal = elementToEdit => this.setState({ elementToEdit }); - - _hideEditModal = () => this.setState({ elementToEdit: null }); - - _handleEdit = async (name, description, image) => { - const { updateCustomElement } = this.props; - const { elementToEdit } = this.state; - await updateCustomElement(elementToEdit.id, name, description, image); - this._hideEditModal(); - }; - - _showDeleteModal = elementToDelete => this.setState({ elementToDelete }); - - _hideDeleteModal = () => this.setState({ elementToDelete: null }); - - _handleDelete = async () => { - const { removeCustomElement } = this.props; - const { elementToDelete } = this.state; - await removeCustomElement(elementToDelete.id); - this._hideDeleteModal(); - }; - - _renderEditModal = () => { - const { elementToEdit } = this.state; - - if (!elementToEdit) { - return null; - } - - return ( - <EuiOverlayMask> - <CustomElementModal - title={strings.getEditElementTitle()} - name={elementToEdit.displayName} - description={elementToEdit.help} - image={elementToEdit.image} - onSave={this._handleEdit} - onCancel={this._hideEditModal} - /> - </EuiOverlayMask> - ); - }; - - _renderDeleteModal = () => { - const { elementToDelete } = this.state; - - if (!elementToDelete) { - return null; - } - - return ( - <ConfirmModal - isOpen - title={strings.getDeleteElementTitle(elementToDelete.displayName)} - message={strings.getDeleteElementDescription()} - confirmButtonText={strings.getDeleteButtonLabel()} - cancelButtonText={strings.getCancelButtonLabel()} - onConfirm={this._handleDelete} - onCancel={this._hideDeleteModal} - /> - ); - }; - - _sortElements = elements => - sortBy( - map(elements, (element, name) => ({ name, ...element })), - 'displayName' - ); - - render() { - const { - search, - setSearch, - addElement, - addCustomElement, - filterTags, - setFilterTags, - } = this.props; - let { elements, customElements } = this.props; - elements = this._sortElements(elements); - - let customElementContent = ( - <EuiEmptyPrompt - iconType="vector" - title={<h2>{strings.getAddNewElementTitle()}</h2>} - body={<p>{strings.getAddNewElementDescription()}</p>} - titleSize="s" - /> - ); - - if (customElements.length) { - customElements = this._sortElements(customElements); - customElementContent = ( - <ElementGrid - elements={customElements} - filter={search} - handleClick={addCustomElement} - showControls - onEdit={this._showEditModal} - onDelete={this._showDeleteModal} - /> - ); - } - - const filters = [getTagsFilter(tagType)]; - const onSearch = ({ queryText }) => { - const { searchTerm, filterTags } = extractSearch(queryText); - setSearch(searchTerm); - setFilterTags(filterTags); - }; - - const tabs = [ - { - id: 'elements', - name: strings.getElementsTitle(), - content: ( - <div className="canvasElements__filter"> - <EuiSpacer /> - <EuiSearchBar - defaultQuery={search} - box={{ - placeholder: strings.getFindElementPlaceholder(), - incremental: true, - }} - filters={filters} - onChange={onSearch} - /> - <EuiSpacer /> - <ElementGrid - elements={elements} - filterText={search} - filterTags={filterTags} - handleClick={addElement} - /> - </div> - ), - }, - { - id: 'customElements', - name: strings.getMyElementsTitle(), - content: ( - <Fragment> - <EuiSpacer /> - <EuiSearchBar - defaultQuery={search} - box={{ - placeholder: strings.getFindElementPlaceholder(), - incremental: true, - }} - onChange={onSearch} - /> - <EuiSpacer /> - {customElementContent} - </Fragment> - ), - }, - ]; - - return ( - <Fragment> - <EuiModalBody style={{ paddingRight: '1px' }}> - <EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} /> - </EuiModalBody> - - {this._renderDeleteModal()} - {this._renderEditModal()} - </Fragment> - ); - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js deleted file mode 100644 index 8faaf278a07de..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js +++ /dev/null @@ -1,103 +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 PropTypes from 'prop-types'; -import { compose, withProps, withState } from 'recompose'; -import { connect } from 'react-redux'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { elementsRegistry } from '../../lib/elements_registry'; -import { notify } from '../../lib/notify'; -import { selectToplevelNodes } from '../../state/actions/transient'; -import { insertNodes, addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { ElementTypes as Component } from './element_types'; - -const customElementAdded = 'elements-custom-added'; - -const mapStateToProps = state => ({ pageId: getSelectedPage(state) }); - -const mapDispatchToProps = dispatch => ({ - selectToplevelNodes: nodes => - dispatch(selectToplevelNodes(nodes.filter(e => !e.position.parent).map(e => e.id))), - insertNodes: (selectedNodes, pageId) => dispatch(insertNodes(selectedNodes, pageId)), - addElement: (pageId, partialElement) => dispatch(addElement(pageId, partialElement)), -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { pageId, ...remainingStateProps } = stateProps; - const { addElement, insertNodes, selectToplevelNodes } = dispatchProps; - const { search, setCustomElements, onClose } = ownProps; - - return { - ...remainingStateProps, - ...ownProps, - // add built-in element to the page - addElement: element => { - addElement(pageId, element); - onClose(); - }, - // add custom element to the page - addCustomElement: customElement => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async text => { - try { - const { customElements } = await customElementService.find(text); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't find custom elements` }); - } - }, - // remove custom element - removeCustomElement: async id => { - try { - await customElementService.remove(id).then(); - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't delete custom elements` }); - } - }, - // update custom element - updateCustomElement: async (id, name, description, image) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't update custom elements` }); - } - }, - }; -}; - -export const ElementTypes = compose( - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - withState('filterTags', 'setFilterTags', []), - withProps(() => ({ elements: elementsRegistry.toJS() })), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); - -ElementTypes.propTypes = { - onClose: PropTypes.func, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot index 503677687ba12..d59d03578a363 100644 --- a/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/keyboard_shortcuts_doc/__examples__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot @@ -212,7 +212,7 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` <dt className="euiDescriptionList__title" > - Bring forward + Bring to front </dt> <dd className="euiDescriptionList__description" @@ -234,7 +234,7 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` <dt className="euiDescriptionList__title" > - Bring to front + Bring forward </dt> <dd className="euiDescriptionList__description" diff --git a/x-pack/legacy/plugins/canvas/public/components/popover/index.ts b/x-pack/legacy/plugins/canvas/public/components/popover/index.ts index f560da14079b5..63626f08fa43b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/popover/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/popover/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Popover } from './popover'; +export { Popover, ClosePopoverFn } from './popover'; diff --git a/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx b/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx index 25b2e6587c869..9f3d86576e6a7 100644 --- a/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx @@ -24,6 +24,8 @@ interface Props { className?: string; } +export type ClosePopoverFn = () => void; + interface State { isPopoverOpen: boolean; } @@ -61,7 +63,7 @@ export class Popover extends Component<Props, State> { })); }; - closePopover = () => { + closePopover: ClosePopoverFn = () => { this.setState({ isPopoverOpen: false, }); diff --git a/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js b/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js index 68c3ba79dd488..cc234d2287c0c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js @@ -7,7 +7,7 @@ import { compose, withProps, withPropsOnChange } from 'recompose'; import PropTypes from 'prop-types'; import isEqual from 'react-fast-compare'; -import { notify } from '../../lib/notify'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { RenderWithFn as Component } from './render_with_fn'; import { ElementHandlers } from './lib/handlers'; @@ -19,9 +19,10 @@ export const RenderWithFn = compose( handlers: Object.assign(new ElementHandlers(), handlers), }) ), - withProps({ - onError: notify.error, - }) + withKibana, + withProps(props => ({ + onError: props.kibana.services.canvas.notify.error, + })) )(Component); RenderWithFn.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot similarity index 88% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot index 5ce6fe8c85589..6f12f68356467 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots components/Elements/ElementControls has two buttons 1`] = ` +exports[`Storyshots components/SavedElementsModal/ElementControls has two buttons 1`] = ` <div style={ Object { @@ -22,6 +22,7 @@ exports[`Storyshots components/Elements/ElementControls has two buttons 1`] = ` <button aria-label="Edit element" className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -47,6 +48,7 @@ exports[`Storyshots components/Elements/ElementControls has two buttons 1`] = ` <button aria-label="Delete element" className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" onBlur={[Function]} onClick={[Function]} onFocus={[Function]} diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot new file mode 100644 index 0000000000000..b7fe63434abb3 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_grid.stories.storyshot @@ -0,0 +1,333 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/SavedElementsModal/ElementGrid default 1`] = ` +<div + style={ + Object { + "width": "1000px", + } + } +> + <div + className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" + > + <div + className="euiFlexItem canvasElementCard__wrapper" + > + <div + className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" + onClick={[Function]} + > + <div + className="euiCard__top" + > + <img + alt="" + className="euiCard__image" + src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" + /> + </div> + <div + className="euiCard__content" + > + <span + className="euiTitle euiTitle--small euiCard__title" + id="generated-idTitle" + > + <button + aria-describedby=" generated-idDescription" + className="euiCard__titleButton" + onClick={[Function]} + > + Custom Element 1 + </button> + </span> + <div + className="euiText euiText--small euiCard__description" + id="generated-idDescription" + > + <p> + sample description + </p> + </div> + </div> + <div + className="euiCard__footer" + /> + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Edit element" + className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="pencil" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete element" + className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + <div + className="euiFlexItem canvasElementCard__wrapper" + > + <div + className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" + onClick={[Function]} + > + <div + className="euiCard__top" + > + <img + alt="" + className="euiCard__image" + src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" + /> + </div> + <div + className="euiCard__content" + > + <span + className="euiTitle euiTitle--small euiCard__title" + id="generated-idTitle" + > + <button + aria-describedby=" generated-idDescription" + className="euiCard__titleButton" + onClick={[Function]} + > + Custom Element 2 + </button> + </span> + <div + className="euiText euiText--small euiCard__description" + id="generated-idDescription" + > + <p> + Aenean eu justo auctor, placerat felis non, scelerisque dolor. + </p> + </div> + </div> + <div + className="euiCard__footer" + /> + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Edit element" + className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="pencil" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete element" + className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + <div + className="euiFlexItem canvasElementCard__wrapper" + > + <div + className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" + onClick={[Function]} + > + <div + className="euiCard__top" + > + <img + alt="" + className="euiCard__image" + src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" + /> + </div> + <div + className="euiCard__content" + > + <span + className="euiTitle euiTitle--small euiCard__title" + id="generated-idTitle" + > + <button + aria-describedby=" generated-idDescription" + className="euiCard__titleButton" + onClick={[Function]} + > + Custom Element 3 + </button> + </span> + <div + className="euiText euiText--small euiCard__description" + id="generated-idDescription" + > + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. + </p> + </div> + </div> + <div + className="euiCard__footer" + /> + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Edit element" + className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="pencil" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete element" + className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + </div> +</div> +`; + +exports[`Storyshots components/SavedElementsModal/ElementGrid with text filter 1`] = ` +<div + style={ + Object { + "width": "1000px", + } + } +> + <div + className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" + /> +</div> +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot new file mode 100644 index 0000000000000..c73309ad8b14c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot @@ -0,0 +1,933 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` +Array [ + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={1} + />, + <div + data-focus-lock-disabled={false} + onBlur={[Function]} + onFocus={[Function]} + > + <div + className="euiModal canvasModal--fixedSize" + onKeyDown={[Function]} + style={ + Object { + "maxWidth": "1000px", + } + } + tabIndex={0} + > + <button + aria-label="Closes this modal window" + className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon" + onClick={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="cross" + size="m" + /> + </button> + <div + className="euiModal__flex" + > + <div + className="euiModalHeader canvasAssetManager__modalHeader" + > + <div + className="euiModalHeader__title canvasAssetManager__modalHeaderTitle" + > + My elements + </div> + </div> + <div + className="euiModalBody" + style={ + Object { + "paddingRight": "1px", + } + } + > + <div + className="euiModalBody__overflow" + > + <div + className="euiFormControlLayout euiFormControlLayout--fullWidth" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <input + className="euiFieldSearch euiFieldSearch--fullWidth" + onChange={[Function]} + onKeyUp={[Function]} + placeholder="Find element" + type="search" + value="" + /> + <div + className="euiFormControlLayoutIcons" + > + <span + className="euiFormControlLayoutCustomIcon" + > + <div + aria-hidden="true" + className="euiFormControlLayoutCustomIcon__icon" + data-euiicon-type="search" + /> + </span> + </div> + </div> + </div> + <div + className="euiSpacer euiSpacer--l" + /> + <div + className="euiEmptyPrompt" + > + <div + color="subdued" + data-euiicon-type="vector" + size="xxl" + /> + <div + className="euiSpacer euiSpacer--s" + /> + <span + className="euiTextColor euiTextColor--subdued" + > + <h2 + className="euiTitle euiTitle--small" + > + Add new elements + </h2> + <div + className="euiSpacer euiSpacer--m" + /> + <div + className="euiText euiText--medium" + > + <p> + Group and save workpad elements to create new elements + </p> + </div> + </span> + </div> + </div> + </div> + <div + className="euiModalFooter" + > + <button + className="euiButton euiButton--primary euiButton--small" + data-test-subj="saved-elements-modal-close-button" + onClick={[Function]} + type="button" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + Close + </span> + </span> + </button> + </div> + </div> + </div> + </div>, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, +] +`; + +exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` +Array [ + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={1} + />, + <div + data-focus-lock-disabled={false} + onBlur={[Function]} + onFocus={[Function]} + > + <div + className="euiModal canvasModal--fixedSize" + onKeyDown={[Function]} + style={ + Object { + "maxWidth": "1000px", + } + } + tabIndex={0} + > + <button + aria-label="Closes this modal window" + className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon" + onClick={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="cross" + size="m" + /> + </button> + <div + className="euiModal__flex" + > + <div + className="euiModalHeader canvasAssetManager__modalHeader" + > + <div + className="euiModalHeader__title canvasAssetManager__modalHeaderTitle" + > + My elements + </div> + </div> + <div + className="euiModalBody" + style={ + Object { + "paddingRight": "1px", + } + } + > + <div + className="euiModalBody__overflow" + > + <div + className="euiFormControlLayout euiFormControlLayout--fullWidth" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <input + className="euiFieldSearch euiFieldSearch--fullWidth" + onChange={[Function]} + onKeyUp={[Function]} + placeholder="Find element" + type="search" + value="" + /> + <div + className="euiFormControlLayoutIcons" + > + <span + className="euiFormControlLayoutCustomIcon" + > + <div + aria-hidden="true" + className="euiFormControlLayoutCustomIcon__icon" + data-euiicon-type="search" + /> + </span> + </div> + </div> + </div> + <div + className="euiSpacer euiSpacer--l" + /> + <div + className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" + > + <div + className="euiFlexItem canvasElementCard__wrapper" + > + <div + className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" + onClick={[Function]} + > + <div + className="euiCard__top" + > + <img + alt="" + className="euiCard__image" + src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" + /> + </div> + <div + className="euiCard__content" + > + <span + className="euiTitle euiTitle--small euiCard__title" + id="generated-idTitle" + > + <button + aria-describedby=" generated-idDescription" + className="euiCard__titleButton" + onClick={[Function]} + > + Custom Element 1 + </button> + </span> + <div + className="euiText euiText--small euiCard__description" + id="generated-idDescription" + > + <p> + sample description + </p> + </div> + </div> + <div + className="euiCard__footer" + /> + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Edit element" + className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="pencil" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete element" + className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + <div + className="euiFlexItem canvasElementCard__wrapper" + > + <div + className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" + onClick={[Function]} + > + <div + className="euiCard__top" + > + <img + alt="" + className="euiCard__image" + src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" + /> + </div> + <div + className="euiCard__content" + > + <span + className="euiTitle euiTitle--small euiCard__title" + id="generated-idTitle" + > + <button + aria-describedby=" generated-idDescription" + className="euiCard__titleButton" + onClick={[Function]} + > + Custom Element 2 + </button> + </span> + <div + className="euiText euiText--small euiCard__description" + id="generated-idDescription" + > + <p> + Aenean eu justo auctor, placerat felis non, scelerisque dolor. + </p> + </div> + </div> + <div + className="euiCard__footer" + /> + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Edit element" + className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="pencil" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete element" + className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + <div + className="euiFlexItem canvasElementCard__wrapper" + > + <div + className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" + onClick={[Function]} + > + <div + className="euiCard__top" + > + <img + alt="" + className="euiCard__image" + src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" + /> + </div> + <div + className="euiCard__content" + > + <span + className="euiTitle euiTitle--small euiCard__title" + id="generated-idTitle" + > + <button + aria-describedby=" generated-idDescription" + className="euiCard__titleButton" + onClick={[Function]} + > + Custom Element 3 + </button> + </span> + <div + className="euiText euiText--small euiCard__description" + id="generated-idDescription" + > + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. + </p> + </div> + </div> + <div + className="euiCard__footer" + /> + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Edit element" + className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="pencil" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete element" + className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + </div> + </div> + </div> + <div + className="euiModalFooter" + > + <button + className="euiButton euiButton--primary euiButton--small" + data-test-subj="saved-elements-modal-close-button" + onClick={[Function]} + type="button" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + Close + </span> + </span> + </button> + </div> + </div> + </div> + </div>, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, +] +`; + +exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` +Array [ + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={1} + />, + <div + data-focus-lock-disabled={false} + onBlur={[Function]} + onFocus={[Function]} + > + <div + className="euiModal canvasModal--fixedSize" + onKeyDown={[Function]} + style={ + Object { + "maxWidth": "1000px", + } + } + tabIndex={0} + > + <button + aria-label="Closes this modal window" + className="euiButtonIcon euiButtonIcon--text euiModal__closeIcon" + onClick={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="cross" + size="m" + /> + </button> + <div + className="euiModal__flex" + > + <div + className="euiModalHeader canvasAssetManager__modalHeader" + > + <div + className="euiModalHeader__title canvasAssetManager__modalHeaderTitle" + > + My elements + </div> + </div> + <div + className="euiModalBody" + style={ + Object { + "paddingRight": "1px", + } + } + > + <div + className="euiModalBody__overflow" + > + <div + className="euiFormControlLayout euiFormControlLayout--fullWidth" + > + <div + className="euiFormControlLayout__childrenWrapper" + > + <input + className="euiFieldSearch euiFieldSearch--fullWidth" + onChange={[Function]} + onKeyUp={[Function]} + placeholder="Find element" + type="search" + value="Element 2" + /> + <div + className="euiFormControlLayoutIcons" + > + <span + className="euiFormControlLayoutCustomIcon" + > + <div + aria-hidden="true" + className="euiFormControlLayoutCustomIcon__icon" + data-euiicon-type="search" + /> + </span> + </div> + <div + className="euiFormControlLayoutIcons euiFormControlLayoutIcons--right" + > + <button + aria-label="Clear input" + className="euiFormControlLayoutClearButton" + onClick={[Function]} + type="button" + > + <div + className="euiFormControlLayoutClearButton__icon" + data-euiicon-type="cross" + /> + </button> + </div> + </div> + </div> + <div + className="euiSpacer euiSpacer--l" + /> + <div + className="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--fourths euiFlexGrid--responsive" + > + <div + className="euiFlexItem canvasElementCard__wrapper" + > + <div + className="euiCard euiCard--leftAligned euiCard--isClickable canvasElementCard" + onClick={[Function]} + > + <div + className="euiCard__top" + > + <img + alt="" + className="euiCard__image" + src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmlld0JveD0iMCAwIDI3MC42MDAwMSAyNjkuNTQ2NjYiCiAgIGhlaWdodD0iMjY5LjU0NjY2IgogICB3aWR0aD0iMjcwLjYwMDAxIgogICB4bWw6c3BhY2U9InByZXNlcnZlIgogICBpZD0ic3ZnMiIKICAgdmVyc2lvbj0iMS4xIj48bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE4Ij48cmRmOlJERj48Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+PGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+PGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPjwvY2M6V29yaz48L3JkZjpSREY+PC9tZXRhZGF0YT48ZGVmcwogICAgIGlkPSJkZWZzNiIgLz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDI2OS41NDY2NykiCiAgICAgaWQ9ImcxMCI+PGcKICAgICAgIHRyYW5zZm9ybT0ic2NhbGUoMC4xKSIKICAgICAgIGlkPSJnMTIiPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMTQiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMjAyOS40OCw5NjIuNDQxIGMgMCwxNzAuMDk5IC0xMDUuNDYsMzE4Ljc5OSAtMjY0LjE3LDM3Ni42NTkgNi45OCwzNS44NiAxMC42Miw3MS43MSAxMC42MiwxMDkuMDUgMCwzMTYuMTkgLTI1Ny4yNCw1NzMuNDMgLTU3My40Nyw1NzMuNDMgLTE4NC43MiwwIC0zNTYuNTU4LC04OC41OSAtNDY0LjUzLC0yMzcuODUgLTUzLjA5LDQxLjE4IC0xMTguMjg1LDYzLjc1IC0xODYuMzA1LDYzLjc1IC0xNjcuODM2LDAgLTMwNC4zODMsLTEzNi41NCAtMzA0LjM4MywtMzA0LjM4IDAsLTM3LjA4IDYuNjE3LC03Mi41OCAxOS4wMzEsLTEwNi4wOCBDIDEwOC40ODgsMTM4MC4wOSAwLDEyMjcuODkgMCwxMDU4Ljg4IDAsODg3LjkxIDEwNS45NzcsNzM4LjUzOSAyNjUuMzk4LDY4MS4wOSBjIC02Ljc2OSwtMzUuNDQyIC0xMC40NiwtNzIuMDIgLTEwLjQ2LC0xMDkgQyAyNTQuOTM4LDI1Ni42MjEgNTExLjU2NiwwIDgyNy4wMjcsMCAxMDEyLjIsMCAxMTgzLjk0LDg4Ljk0MTQgMTI5MS4zLDIzOC44MzIgYyA1My40NSwtNDEuOTYxIDExOC44LC02NC45OTIgMTg2LjU2LC02NC45OTIgMTY3LjgzLDAgMzA0LjM4LDEzNi40OTIgMzA0LjM4LDMwNC4zMzIgMCwzNy4wNzggLTYuNjIsNzIuNjI5IC0xOS4wMywxMDYuMTI5IDE1Ny43OCw1Ni44NzkgMjY2LjI3LDIwOS4xMjkgMjY2LjI3LDM3OC4xNCIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDE2IgogICAgICAgICBzdHlsZT0iZmlsbDojZmFjZjA5O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDc5Ny44OTgsMTE1MC45MyA0NDQuMDcyLC0yMDIuNDUgNDQ4LjA1LDM5Mi41OCBjIDYuNDksMzIuMzkgOS42Niw2NC42NyA5LjY2LDk4LjQ2IDAsMjc2LjIzIC0yMjQuNjgsNTAwLjk1IC01MDAuOSw1MDAuOTUgLTE2NS4yNCwwIC0zMTkuMzcsLTgxLjM2IC00MTMuMDUzLC0yMTcuNzkgbCAtNzQuNTI0LC0zODYuNjQgODYuNjk1LC0xODUuMTEiIC8+PHBhdGgKICAgICAgICAgaWQ9InBhdGgxOCIKICAgICAgICAgc3R5bGU9ImZpbGw6IzQ5YzFhZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIKICAgICAgICAgZD0ibSAzMzguMjIzLDY4MC42NzIgYyAtNi40ODksLTMyLjM4MyAtOS44MDksLTY1Ljk4MSAtOS44MDksLTk5Ljk3MyAwLC0yNzYuOTI5IDIyNS4zMzYsLTUwMi4yNTc2IDUwMi4zMTMsLTUwMi4yNTc2IDE2Ni41OTMsMCAzMjEuNDczLDgyLjExNzYgNDE1LjAxMywyMTkuOTQ5NiBsIDczLjk3LDM4NS4zNDcgLTk4LjcyLDE4OC42MjEgTCA3NzUuMTU2LDEwNzUuNTcgMzM4LjIyMyw2ODAuNjcyIiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjAiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNlZjI5OWI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMzM1LjQxLDE0NDkuMTggMzA0LjMzMiwtNzEuODYgNjYuNjgsMzQ2LjAyIGMgLTQxLjU4NiwzMS43OCAtOTIuOTMsNDkuMTggLTE0NS43MzEsNDkuMTggLTEzMi4yNSwwIC0yMzkuODEyLC0xMDcuNjEgLTIzOS44MTIsLTIzOS44NyAwLC0yOS4yMSA0Ljg3OSwtNTcuMjIgMTQuNTMxLC04My40NyIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDIyIgogICAgICAgICBzdHlsZT0iZmlsbDojNGNhYmU0O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJNIDMwOC45OTIsMTM3Ni43IEMgMTczLjAyLDEzMzEuNjQgNzguNDgwNSwxMjAxLjMgNzguNDgwNSwxMDU3LjkzIDc4LjQ4MDUsOTE4LjM0IDE2NC44Miw3OTMuNjggMjk0LjQwNiw3NDQuMzUyIGwgNDI2Ljk4MSwzODUuOTM4IC03OC4zOTUsMTY3LjUxIC0zMzQsNzguOSIgLz48cGF0aAogICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICBzdHlsZT0iZmlsbDojODVjZTI2O2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIgogICAgICAgICBkPSJtIDEzMjMuOCwyOTguNDEgYyA0MS43NCwtMzIuMDkgOTIuODMsLTQ5LjU5IDE0NC45OCwtNDkuNTkgMTMyLjI1LDAgMjM5LjgxLDEwNy41NTkgMjM5LjgxLDIzOS44MjEgMCwyOS4xNiAtNC44OCw1Ny4xNjggLTE0LjUzLDgzLjQxOCBsIC0zMDQuMDgsNzEuMTYgLTY2LjE4LC0zNDQuODA5IiAvPjxwYXRoCiAgICAgICAgIGlkPSJwYXRoMjYiCiAgICAgICAgIHN0eWxlPSJmaWxsOiMzMTc3YTc7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiCiAgICAgICAgIGQ9Im0gMTM4NS42Nyw3MjIuOTMgMzM0Ljc2LC03OC4zMDEgYyAxMzYuMDIsNDQuOTYxIDIzMC41NiwxNzUuMzUxIDIzMC41NiwzMTguNzYyIDAsMTM5LjMzOSAtODYuNTQsMjYzLjg1OSAtMjE2LjM4LDMxMy4wMzkgbCAtNDM3Ljg0LC0zODMuNTkgODguOSwtMTY5LjkxIiAvPjwvZz48L2c+PC9zdmc+" + /> + </div> + <div + className="euiCard__content" + > + <span + className="euiTitle euiTitle--small euiCard__title" + id="generated-idTitle" + > + <button + aria-describedby=" generated-idDescription" + className="euiCard__titleButton" + onClick={[Function]} + > + Custom Element 2 + </button> + </span> + <div + className="euiText euiText--small euiCard__description" + id="generated-idDescription" + > + <p> + Aenean eu justo auctor, placerat felis non, scelerisque dolor. + </p> + </div> + </div> + <div + className="euiCard__footer" + /> + </div> + <div + className="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasElementCard__controls" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Edit element" + className="euiButtonIcon euiButtonIcon--primary" + data-test-subj="canvasElementCard__editButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="pencil" + size="m" + /> + </button> + </span> + </div> + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} + > + <button + aria-label="Delete element" + className="euiButtonIcon euiButtonIcon--danger" + data-test-subj="canvasElementCard__deleteButton" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" + > + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="trash" + size="m" + /> + </button> + </span> + </div> + </div> + </div> + </div> + </div> + </div> + <div + className="euiModalFooter" + > + <button + className="euiButton euiButton--primary euiButton--small" + data-test-subj="saved-elements-modal-close-button" + onClick={[Function]} + type="button" + > + <span + className="euiButton__content" + > + <span + className="euiButton__text" + > + Close + </span> + </span> + </button> + </div> + </div> + </div> + </div>, + <div + data-focus-guard={true} + style={ + Object { + "height": "0px", + "left": "1px", + "overflow": "hidden", + "padding": 0, + "position": "fixed", + "top": "1px", + "width": "1px", + } + } + tabIndex={0} + />, +] +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx similarity index 90% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx index 52736ac952e53..5210210ebaa74 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { ElementControls } from '../element_controls'; -storiesOf('components/Elements/ElementControls', module) +storiesOf('components/SavedElementsModal/ElementControls', module) .addDecorator(story => ( <div style={{ diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx new file mode 100644 index 0000000000000..db5b7ddd9c00c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_grid.stories.tsx @@ -0,0 +1,39 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { ElementGrid } from '../element_grid'; +import { testCustomElements } from './fixtures/test_elements'; + +storiesOf('components/SavedElementsModal/ElementGrid', module) + .addDecorator(story => ( + <div + style={{ + width: '1000px', + }} + > + {story()} + </div> + )) + .add('default', () => ( + <ElementGrid + elements={testCustomElements} + onClick={action('addCustomElement')} + onDelete={action('onDelete')} + onEdit={action('onEdit')} + /> + )) + .add('with text filter', () => ( + <ElementGrid + elements={testCustomElements} + onClick={action('addCustomElement')} + filterText="table" + onDelete={action('onDelete')} + onEdit={action('onEdit')} + /> + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx index eec7a86d52f25..d1ff565b4955a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx @@ -6,41 +6,6 @@ import { elasticLogo } from '../../../../lib/elastic_logo'; -export const testElements = [ - { - name: 'areaChart', - displayName: 'Area chart', - help: 'A line chart with a filled body', - tags: ['chart'], - image: elasticLogo, - expression: `filters - | demodata - | pointseries x="time" y="mean(price)" - | plot defaultStyle={seriesStyle lines=1 fill=1} - | render`, - }, - { - name: 'image', - displayName: 'Image', - help: 'A static image', - tags: ['graphic'], - image: elasticLogo, - expression: `image dataurl=null mode="contain" - | render`, - }, - { - name: 'table', - displayName: 'Data table', - tags: ['text'], - help: 'A scrollable grid for displaying data in a tabular format', - image: elasticLogo, - expression: `filters - | demodata - | table - | render`, - }, -]; - export const testCustomElements = [ { id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx new file mode 100644 index 0000000000000..4941d8cb2efa7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { SavedElementsModal } from '../saved_elements_modal'; +import { testCustomElements } from './fixtures/test_elements'; +import { CustomElement } from '../../../../types'; + +storiesOf('components/SavedElementsModal', module) + .add('no custom elements', () => ( + <SavedElementsModal + customElements={[] as CustomElement[]} + search="" + setSearch={action('setSearch')} + onClose={action('onClose')} + addCustomElement={action('addCustomElement')} + findCustomElements={action('findCustomElements')} + updateCustomElement={action('updateCustomElement')} + removeCustomElement={action('removeCustomElement')} + /> + )) + .add('with custom elements', () => ( + <SavedElementsModal + customElements={testCustomElements} + search="" + setSearch={action('setSearch')} + onClose={action('onClose')} + addCustomElement={action('addCustomElement')} + findCustomElements={action('findCustomElements')} + updateCustomElement={action('updateCustomElement')} + removeCustomElement={action('removeCustomElement')} + /> + )) + .add('with text filter', () => ( + <SavedElementsModal + customElements={testCustomElements} + search="Element 2" + onClose={action('onClose')} + setSearch={action('setSearch')} + addCustomElement={action('addCustomElement')} + findCustomElements={action('findCustomElements')} + updateCustomElement={action('updateCustomElement')} + removeCustomElement={action('removeCustomElement')} + /> + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx similarity index 85% rename from x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx index a23274296f64f..998b15c15f487 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx @@ -30,7 +30,12 @@ export const ElementControls: FunctionComponent<Props> = ({ onDelete, onEdit }) > <EuiFlexItem grow={false}> <EuiToolTip content={strings.getEditTooltip()}> - <EuiButtonIcon iconType="pencil" aria-label={strings.getEditAriaLabel()} onClick={onEdit} /> + <EuiButtonIcon + iconType="pencil" + aria-label={strings.getEditAriaLabel()} + onClick={onEdit} + data-test-subj="canvasElementCard__editButton" + /> </EuiToolTip> </EuiFlexItem> <EuiFlexItem grow={false}> @@ -40,6 +45,7 @@ export const ElementControls: FunctionComponent<Props> = ({ onDelete, onEdit }) iconType="trash" aria-label={strings.getDeleteAriaLabel()} onClick={onDelete} + data-test-subj="canvasElementCard__deleteButton" /> </EuiToolTip> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx new file mode 100644 index 0000000000000..f86e2c0147035 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { map } from 'lodash'; +import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui'; +import { ElementControls } from './element_controls'; +import { CustomElement } from '../../../types'; +import { ElementCard } from '../element_card'; + +export interface Props { + /** + * list of elements to generate cards for + */ + elements: CustomElement[]; + /** + * text to filter out cards + */ + filterText: string; + /** + * handler invoked when clicking a card + */ + onClick: (element: CustomElement) => void; + /** + * click handler for the edit button + */ + onEdit: (element: CustomElement) => void; + /** + * click handler for the delete button + */ + onDelete: (element: CustomElement) => void; +} + +export const ElementGrid = ({ elements, filterText, onClick, onEdit, onDelete }: Props) => { + filterText = filterText.toLowerCase(); + + return ( + <EuiFlexGrid gutterSize="l" columns={4}> + {map(elements, (element: CustomElement, index) => { + const { name, displayName = '', help = '', image } = element; + const whenClicked = () => onClick(element); + + if ( + filterText.length && + !name.toLowerCase().includes(filterText) && + !displayName.toLowerCase().includes(filterText) && + !help.toLowerCase().includes(filterText) + ) { + return null; + } + + return ( + <EuiFlexItem key={index} className="canvasElementCard__wrapper"> + <ElementCard + title={displayName || name} + description={help} + image={image} + onClick={whenClicked} + /> + <ElementControls onEdit={() => onEdit(element)} onDelete={() => onDelete(element)} /> + </EuiFlexItem> + ); + })} + </EuiFlexGrid> + ); +}; + +ElementGrid.propTypes = { + elements: PropTypes.array.isRequired, + filterText: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, +}; + +ElementGrid.defaultProps = { + filterText: '', +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts new file mode 100644 index 0000000000000..60d1d7462daa9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -0,0 +1,135 @@ +/* + * 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 { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { compose, withState } from 'recompose'; +import { camelCase } from 'lodash'; +// @ts-ignore Untyped local +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { WithKibanaProps } from '../../'; +// @ts-ignore Untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-ignore Untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { SavedElementsModal as Component, Props as ComponentProps } from './saved_elements_modal'; +import { State, PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +interface OwnProps { + onClose: () => void; +} + +interface OwnPropsWithState extends OwnProps { + customElements: CustomElement[]; + setCustomElements: (customElements: CustomElement[]) => void; + search: string; + setSearch: (search: string) => void; +} + +interface DispatchProps { + selectToplevelNodes: (nodes: PositionedElement[]) => void; + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; +} + +interface StateProps { + pageId: string; +} + +const mapStateToProps = (state: State): StateProps => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ), + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnPropsWithState & WithKibanaProps +): ComponentProps => { + const { pageId } = stateProps; + const { onClose, search, setCustomElements } = ownProps; + + const findCustomElements = async () => { + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + }; + + return { + ...ownProps, + // add custom element to the page + addCustomElement: (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }, + // custom element search + findCustomElements: async (text?: string) => { + try { + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't find custom elements`, + }); + } + }, + // remove custom element + removeCustomElement: async (id: string) => { + try { + await customElementService.remove(id); + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }, + // update custom element + updateCustomElement: async (id: string, name: string, description: string, image: string) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't update custom elements`, + }); + } + }, + }; +}; + +export const SavedElementsModal = compose<ComponentProps, OwnProps>( + withKibana, + withState('search', 'setSearch', ''), + withState('customElements', 'setCustomElements', []), + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx new file mode 100644 index 0000000000000..dba97a15fee5c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx @@ -0,0 +1,217 @@ +/* + * 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, { Fragment, ChangeEvent, FunctionComponent, useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiEmptyPrompt, + EuiFieldSearch, + EuiSpacer, + EuiOverlayMask, + EuiButton, +} from '@elastic/eui'; +import { map, sortBy } from 'lodash'; +import { ComponentStrings } from '../../../i18n'; +import { CustomElement } from '../../../types'; +import { ConfirmModal } from '../confirm_modal/confirm_modal'; +import { CustomElementModal } from '../custom_element_modal'; +import { ElementGrid } from './element_grid'; + +const { SavedElementsModal: strings } = ComponentStrings; + +export interface Props { + /** + * Adds the custom element to the workpad + */ + addCustomElement: (customElement: CustomElement) => void; + /** + * Queries ES for custom element saved objects + */ + findCustomElements: () => void; + /** + * Handler invoked when the modal closes + */ + onClose: () => void; + /** + * Deletes the custom element + */ + removeCustomElement: (id: string) => void; + /** + * Saved edits to the custom element + */ + updateCustomElement: (id: string, name: string, description: string, image: string) => void; + /** + * Array of custom elements to display + */ + customElements: CustomElement[]; + /** + * Text used to filter custom elements list + */ + search: string; + /** + * Setter for search text + */ + setSearch: (search: string) => void; +} + +export const SavedElementsModal: FunctionComponent<Props> = ({ + search, + setSearch, + customElements, + addCustomElement, + findCustomElements, + onClose, + removeCustomElement, + updateCustomElement, +}) => { + const [elementToDelete, setElementToDelete] = useState<CustomElement | null>(null); + const [elementToEdit, setElementToEdit] = useState<CustomElement | null>(null); + + useEffect(() => { + findCustomElements(); + }); + + const showEditModal = (element: CustomElement) => setElementToEdit(element); + const hideEditModal = () => setElementToEdit(null); + + const handleEdit = async (name: string, description: string, image: string) => { + if (elementToEdit) { + await updateCustomElement(elementToEdit.id, name, description, image); + } + hideEditModal(); + }; + + const showDeleteModal = (element: CustomElement) => setElementToDelete(element); + const hideDeleteModal = () => setElementToDelete(null); + + const handleDelete = async () => { + if (elementToDelete) { + await removeCustomElement(elementToDelete.id); + } + hideDeleteModal(); + }; + + const renderEditModal = () => { + if (!elementToEdit) { + return null; + } + + return ( + <EuiOverlayMask> + <CustomElementModal + title={strings.getEditElementTitle()} + name={elementToEdit.displayName} + description={elementToEdit.help} + image={elementToEdit.image} + onSave={handleEdit} + onCancel={hideEditModal} + /> + </EuiOverlayMask> + ); + }; + + const renderDeleteModal = () => { + if (!elementToDelete) { + return null; + } + + return ( + <ConfirmModal + isOpen + title={strings.getDeleteElementTitle(elementToDelete.displayName)} + message={strings.getDeleteElementDescription()} + confirmButtonText={strings.getDeleteButtonLabel()} + cancelButtonText={strings.getCancelButtonLabel()} + onConfirm={handleDelete} + onCancel={hideDeleteModal} + /> + ); + }; + + const sortElements = (elements: CustomElement[]): CustomElement[] => + sortBy( + map(elements, (element, name) => ({ name, ...element })), + 'displayName' + ); + + const onSearch = (e: ChangeEvent<HTMLInputElement>) => setSearch(e.target.value); + + let customElementContent = ( + <EuiEmptyPrompt + iconType="vector" + title={<h2>{strings.getAddNewElementTitle()}</h2>} + body={<p>{strings.getAddNewElementDescription()}</p>} + titleSize="s" + /> + ); + + if (customElements.length) { + customElementContent = ( + <ElementGrid + elements={sortElements(customElements)} + filterText={search} + onClick={addCustomElement} + onEdit={showEditModal} + onDelete={showDeleteModal} + /> + ); + } + + return ( + <Fragment> + <EuiOverlayMask> + <EuiModal + onClose={onClose} + className="canvasModal--fixedSize" + maxWidth="1000px" + initialFocus=".canvasElements__filter input" + > + <EuiModalHeader className="canvasAssetManager__modalHeader"> + <EuiModalHeaderTitle className="canvasAssetManager__modalHeaderTitle"> + {strings.getModalTitle()} + </EuiModalHeaderTitle> + </EuiModalHeader> + + <EuiModalBody style={{ paddingRight: '1px' }}> + <EuiFieldSearch + fullWidth + value={search} + placeholder={strings.getFindElementPlaceholder()} + onChange={onSearch} + /> + <EuiSpacer /> + {customElementContent} + </EuiModalBody> + <EuiModalFooter> + <EuiButton + size="s" + onClick={onClose} + data-test-subj="saved-elements-modal-close-button" + > + {strings.getSavedElementsModalCloseButtonLabel()} + </EuiButton> + </EuiModalFooter> + </EuiModal> + </EuiOverlayMask> + + {renderDeleteModal()} + {renderEditModal()} + </Fragment> + ); +}; + +SavedElementsModal.propTypes = { + addCustomElement: PropTypes.func.isRequired, + findCustomElements: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + removeCustomElement: PropTypes.func.isRequired, + updateCustomElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js index 9de5f81b440ba..f333705a1a3c6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar/sidebar_content.js @@ -10,7 +10,6 @@ import { compose, branch, renderComponent } from 'recompose'; import { EuiSpacer } from '@elastic/eui'; import { getSelectedToplevelNodes, getSelectedElementId } from '../../state/selectors/workpad'; import { SidebarHeader } from '../sidebar_header'; -import { globalStateUpdater } from '../workpad_page/integration_utils'; import { ComponentStrings } from '../../../i18n'; import { MultiElementSettings } from './multi_element_settings'; import { GroupSettings } from './group_settings'; @@ -22,45 +21,19 @@ const { SidebarContent: strings } = ComponentStrings; const mapStateToProps = state => ({ selectedToplevelNodes: getSelectedToplevelNodes(state), selectedElementId: getSelectedElementId(state), - state, }); -const mergeProps = ( - { state, ...restStateProps }, - { dispatch, ...restDispatchProps }, - ownProps -) => ({ - ...ownProps, - ...restDispatchProps, - ...restStateProps, - updateGlobalState: globalStateUpdater(dispatch, state), -}); - -const withGlobalState = (commit, updateGlobalState) => (type, payload) => { - const newLayoutState = commit(type, payload); - if (newLayoutState.currentScene.gestureEnd) { - updateGlobalState(newLayoutState); - } -}; - -const MultiElementSidebar = ({ commit, updateGlobalState }) => ( +const MultiElementSidebar = () => ( <Fragment> - <SidebarHeader - title={strings.getMultiElementSidebarTitle()} - commit={withGlobalState(commit, updateGlobalState)} - /> + <SidebarHeader title={strings.getMultiElementSidebarTitle()} /> <EuiSpacer /> <MultiElementSettings /> </Fragment> ); -const GroupedElementSidebar = ({ commit, updateGlobalState }) => ( +const GroupedElementSidebar = () => ( <Fragment> - <SidebarHeader - title={strings.getGroupedElementSidebarTitle()} - commit={withGlobalState(commit, updateGlobalState)} - groupIsSelected - /> + <SidebarHeader title={strings.getGroupedElementSidebarTitle()} groupIsSelected /> <EuiSpacer /> <GroupSettings /> </Fragment> @@ -92,7 +65,4 @@ const branches = [ ), ]; -export const SidebarContent = compose( - connect(mapStateToProps, null, mergeProps), - ...branches -)(GlobalConfig); +export const SidebarContent = compose(connect(mapStateToProps), ...branches)(GlobalConfig); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot index d46a509251d35..4d5b9570ee20f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot @@ -20,6 +20,30 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` Selected layer </h3> </div> + </div> +</div> +`; + +exports[`Storyshots components/Sidebar/SidebarHeader with layer controls 1`] = ` +<div + style={ + Object { + "width": "300px", + } + } +> + <div + className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasLayout__sidebarHeader" + > + <div + className="euiFlexItem euiFlexItem--flexGrowZero" + > + <h3 + className="euiTitle euiTitle--xsmall" + > + Grouped element + </h3> + </div> <div className="euiFlexItem euiFlexItem--flexGrowZero" > @@ -35,7 +59,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` onMouseOver={[Function]} > <button - aria-label="Save as new element" + aria-label="Move element to top layer" className="euiButtonIcon euiButtonIcon--text" onBlur={[Function]} onClick={[Function]} @@ -45,7 +69,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` <div aria-hidden="true" className="euiButtonIcon__icon" - data-euiicon-type="indexOpen" + data-euiicon-type="sortUp" size="m" /> </button> @@ -54,75 +78,28 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` <div className="euiFlexItem euiFlexItem--flexGrowZero" > - <div - className="euiPopover euiPopover--anchorDownCenter canvasContextMenu" - container={null} - id="sidebar-context-menu-popover" - onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} > - <div - className="euiPopover__anchor" + <button + aria-label="Move element up one layer" + className="euiButtonIcon euiButtonIcon--text" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Element options" - className="euiButtonIcon euiButtonIcon--text" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="boxesVertical" - size="m" - /> - </button> - </span> - </div> - </div> + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="arrowUp" + size="m" + /> + </button> + </span> </div> - </div> - </div> - </div> -</div> -`; - -exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] = ` -<div - style={ - Object { - "width": "300px", - } - } -> - <div - className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--justifyContentSpaceBetween euiFlexGroup--directionRow euiFlexGroup--responsive canvasLayout__sidebarHeader" - > - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <h3 - className="euiTitle euiTitle--xsmall" - > - Grouped element - </h3> - </div> - <div - className="euiFlexItem euiFlexItem--flexGrowZero" - > - <div - className="euiFlexGroup euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive" - > <div className="euiFlexItem euiFlexItem--flexGrowZero" > @@ -132,7 +109,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] onMouseOver={[Function]} > <button - aria-label="Save as new element" + aria-label="Move element down one layer" className="euiButtonIcon euiButtonIcon--text" onBlur={[Function]} onClick={[Function]} @@ -142,7 +119,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] <div aria-hidden="true" className="euiButtonIcon__icon" - data-euiicon-type="indexOpen" + data-euiicon-type="arrowDown" size="m" /> </button> @@ -151,42 +128,27 @@ exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] <div className="euiFlexItem euiFlexItem--flexGrowZero" > - <div - className="euiPopover euiPopover--anchorDownCenter canvasContextMenu" - container={null} - id="sidebar-context-menu-popover" - onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} + <span + className="euiToolTipAnchor" + onMouseOut={[Function]} + onMouseOver={[Function]} > - <div - className="euiPopover__anchor" + <button + aria-label="Move element to bottom layer" + className="euiButtonIcon euiButtonIcon--text" + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + type="button" > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Element options" - className="euiButtonIcon euiButtonIcon--text" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="boxesVertical" - size="m" - /> - </button> - </span> - </div> - </div> + <div + aria-hidden="true" + className="euiButtonIcon__icon" + data-euiicon-type="sortDown" + size="m" + /> + </button> + </span> </div> </div> </div> diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx index a852571a3ea65..11c66906a6ef6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/sidebar_header.stories.tsx @@ -10,31 +10,15 @@ import { action } from '@storybook/addon-actions'; import { SidebarHeader } from '../sidebar_header'; const handlers = { - cloneNodes: action('cloneNodes'), - copyNodes: action('copyNodes'), - cutNodes: action('cutNodes'), - pasteNodes: action('pasteNodes'), - deleteNodes: action('deleteNodes'), bringToFront: action('bringToFront'), bringForward: action('bringForward'), sendBackward: action('sendBackward'), sendToBack: action('sendToBack'), - createCustomElement: action('createCustomElement'), - groupNodes: action('groupNodes'), - ungroupNodes: action('ungroupNodes'), - alignLeft: action('alignLeft'), - alignMiddle: action('alignMiddle'), - alignRight: action('alignRight'), - alignTop: action('alignTop'), - alignCenter: action('alignCenter'), - alignBottom: action('alignBottom'), - distributeHorizontally: action('distributeHorizontally'), - distributeVertically: action('distributeVertically'), }; storiesOf('components/Sidebar/SidebarHeader', module) .addDecorator(story => <div style={{ width: '300px' }}>{story()}</div>) .add('default', () => <SidebarHeader title="Selected layer" {...handlers} />) - .add('without layer controls', () => ( - <SidebarHeader title="Grouped element" showLayerControls={false} {...handlers} /> + .add('with layer controls', () => ( + <SidebarHeader title="Grouped element" showLayerControls={true} {...handlers} /> )); diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx index d4dec6cca064b..024a2dbb41a24 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/sidebar_header.tsx @@ -4,27 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; +import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiButtonIcon, - EuiContextMenu, - EuiToolTip, - EuiContextMenuPanelItemDescriptor, - EuiContextMenuPanelDescriptor, - EuiOverlayMask, -} from '@elastic/eui'; -import { Popover } from '../popover'; -import { CustomElementModal } from '../custom_element_modal'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; import { ComponentStrings } from '../../../i18n/components'; import { ShortcutStrings } from '../../../i18n/shortcuts'; -const topBorderClassName = 'canvasContextMenu--topBorder'; - const { SidebarHeader: strings } = ComponentStrings; const shortcutHelp = ShortcutStrings.getShortcutHelp(); @@ -37,26 +23,6 @@ interface Props { * indicated whether or not layer controls should be displayed */ showLayerControls?: boolean; - /** - * cuts selected elements - */ - cutNodes: () => void; - /** - * copies selected elements to clipboard - */ - copyNodes: () => void; - /** - * pastes elements stored in clipboard to page - */ - pasteNodes: () => void; - /** - * clones selected elements - */ - cloneNodes: () => void; - /** - * deletes selected elements - */ - deleteNodes: () => void; /** * moves selected element to top layer */ @@ -73,488 +39,117 @@ interface Props { * moves selected element to bottom layer */ sendToBack: () => void; - /** - * saves the selected elements as an custom-element saved object - */ - createCustomElement: (name: string, description: string, image: string) => void; - /** - * indicated whether the selected element is a group or not - */ - groupIsSelected: boolean; - /** - * only more than one selected element can be grouped - */ - selectedNodes: string[]; - /** - * groups selected elements - */ - groupNodes: () => void; - /** - * ungroups selected group - */ - ungroupNodes: () => void; - /** - * left align selected elements - */ - alignLeft: () => void; - /** - * center align selected elements - */ - alignCenter: () => void; - /** - * right align selected elements - */ - alignRight: () => void; - /** - * top align selected elements - */ - alignTop: () => void; - /** - * middle align selected elements - */ - alignMiddle: () => void; - /** - * bottom align selected elements - */ - alignBottom: () => void; - /** - * horizontally distribute selected elements - */ - distributeHorizontally: () => void; - /** - * vertically distribute selected elements - */ - distributeVertically: () => void; } -interface State { - /** - * indicates whether or not the custom element modal is open - */ - isModalVisible: boolean; -} - -interface MenuTuple { - menuItem: EuiContextMenuPanelItemDescriptor; - panel: EuiContextMenuPanelDescriptor; -} - -const contextMenuButton = (handleClick: React.MouseEventHandler<HTMLButtonElement>) => ( - <EuiButtonIcon - color="text" - iconType="boxesVertical" - onClick={handleClick} - aria-label={strings.getContextMenuTitle()} - /> -); - -export class SidebarHeader extends Component<Props, State> { - public static propTypes = { - title: PropTypes.string.isRequired, - showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements - cutNodes: PropTypes.func.isRequired, - copyNodes: PropTypes.func.isRequired, - pasteNodes: PropTypes.func.isRequired, - cloneNodes: PropTypes.func.isRequired, - deleteNodes: PropTypes.func.isRequired, - bringToFront: PropTypes.func.isRequired, - bringForward: PropTypes.func.isRequired, - sendBackward: PropTypes.func.isRequired, - sendToBack: PropTypes.func.isRequired, - createCustomElement: PropTypes.func.isRequired, - groupIsSelected: PropTypes.bool, - selectedNodes: PropTypes.array, - groupNodes: PropTypes.func.isRequired, - ungroupNodes: PropTypes.func.isRequired, - alignLeft: PropTypes.func.isRequired, - alignCenter: PropTypes.func.isRequired, - alignRight: PropTypes.func.isRequired, - alignTop: PropTypes.func.isRequired, - alignMiddle: PropTypes.func.isRequired, - alignBottom: PropTypes.func.isRequired, - distributeHorizontally: PropTypes.func.isRequired, - distributeVertically: PropTypes.func.isRequired, - }; - - public static defaultProps = { - groupIsSelected: false, - showLayerControls: false, - selectedNodes: [], - }; - - public state = { - isModalVisible: false, - }; - - private _isMounted = false; - private _showModal = () => this._isMounted && this.setState({ isModalVisible: true }); - private _hideModal = () => this._isMounted && this.setState({ isModalVisible: false }); - - public componentDidMount() { - this._isMounted = true; - } - - public componentWillUnmount() { - this._isMounted = false; - } - - private _renderLayoutControls = () => { - const { bringToFront, bringForward, sendBackward, sendToBack } = this.props; - return ( - <Fragment> - <EuiFlexItem grow={false}> - <EuiToolTip - position="bottom" - content={ - <span> - {shortcutHelp.BRING_TO_FRONT} - <ToolTipShortcut namespace="ELEMENT" action="BRING_TO_FRONT" /> - </span> - } - > - <EuiButtonIcon - color="text" - iconType="sortUp" - onClick={bringToFront} - aria-label={strings.getBringToFrontAriaLabel()} - /> - </EuiToolTip> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip - position="bottom" - content={ - <span> - {shortcutHelp.BRING_FORWARD} - <ToolTipShortcut namespace="ELEMENT" action="BRING_FORWARD" /> - </span> - } - > - <EuiButtonIcon - color="text" - iconType="arrowUp" - onClick={bringForward} - aria-label={strings.getBringForwardAriaLabel()} - /> - </EuiToolTip> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip - position="bottom" - content={ - <span> - {shortcutHelp.SEND_BACKWARD} - <ToolTipShortcut namespace="ELEMENT" action="SEND_BACKWARD" /> - </span> - } - > - <EuiButtonIcon - color="text" - iconType="arrowDown" - onClick={sendBackward} - aria-label={strings.getSendBackwardAriaLabel()} - /> - </EuiToolTip> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip - position="bottom" - content={ - <span> - {shortcutHelp.SEND_TO_BACK} - <ToolTipShortcut namespace="ELEMENT" action="SEND_TO_BACK" /> - </span> - } - > - <EuiButtonIcon - color="text" - iconType="sortDown" - onClick={sendToBack} - aria-label={strings.getSendToBackAriaLabel()} - /> - </EuiToolTip> - </EuiFlexItem> - </Fragment> - ); - }; - - private _getLayerMenuItems = (): MenuTuple => { - const { bringToFront, bringForward, sendBackward, sendToBack } = this.props; - - return { - menuItem: { name: strings.getOrderMenuItemLabel(), className: topBorderClassName, panel: 1 }, - panel: { - id: 1, - title: strings.getOrderMenuItemLabel(), - items: [ - { - name: shortcutHelp.BRING_TO_FRONT, // TODO: check against current element position and disable if already top layer - icon: 'sortUp', - onClick: bringToFront, - }, - { - name: shortcutHelp.BRING_TO_FRONT, // TODO: same as above - icon: 'arrowUp', - onClick: bringForward, - }, - { - name: shortcutHelp.SEND_BACKWARD, // TODO: check against current element position and disable if already bottom layer - icon: 'arrowDown', - onClick: sendBackward, - }, - { - name: shortcutHelp.SEND_TO_BACK, // TODO: same as above - icon: 'sortDown', - onClick: sendToBack, - }, - ], - }, - }; - }; - - private _getAlignmentMenuItems = (close: (fn: () => void) => () => void): MenuTuple => { - const { alignLeft, alignCenter, alignRight, alignTop, alignMiddle, alignBottom } = this.props; - - return { - menuItem: { - name: strings.getAlignmentMenuItemLabel(), - className: 'canvasContextMenu', - panel: 2, - }, - panel: { - id: 2, - title: strings.getAlignmentMenuItemLabel(), - items: [ - { - name: strings.getLeftAlignMenuItemLabel(), - icon: 'editorItemAlignLeft', - onClick: close(alignLeft), - }, - { - name: strings.getCenterAlignMenuItemLabel(), - icon: 'editorItemAlignCenter', - onClick: close(alignCenter), - }, - { - name: strings.getRightAlignMenuItemLabel(), - icon: 'editorItemAlignRight', - onClick: close(alignRight), - }, - { - name: strings.getTopAlignMenuItemLabel(), - icon: 'editorItemAlignTop', - onClick: close(alignTop), - }, - { - name: strings.getMiddleAlignMenuItemLabel(), - icon: 'editorItemAlignMiddle', - onClick: close(alignMiddle), - }, - { - name: strings.getBottomAlignMenuItemLabel(), - icon: 'editorItemAlignBottom', - onClick: close(alignBottom), - }, - ], - }, - }; - }; - - private _getDistributionMenuItems = (close: (fn: () => void) => () => void): MenuTuple => { - const { distributeHorizontally, distributeVertically } = this.props; - - return { - menuItem: { - name: strings.getDistributionMenuItemLabel(), - className: 'canvasContextMenu', - panel: 3, - }, - panel: { - id: 3, - title: strings.getDistributionMenuItemLabel(), - items: [ - { - name: strings.getHorizontalDistributionMenuItemLabel(), - icon: 'editorDistributeHorizontal', - onClick: close(distributeHorizontally), - }, - { - name: strings.getVerticalDistributionMenuItemLabel(), - icon: 'editorDistributeVertical', - onClick: close(distributeVertically), - }, - ], - }, - }; - }; - - private _getGroupMenuItems = ( - close: (fn: () => void) => () => void - ): EuiContextMenuPanelItemDescriptor[] => { - const { groupIsSelected, ungroupNodes, groupNodes, selectedNodes } = this.props; - return groupIsSelected - ? [ - { - name: strings.getUngroupMenuItemLabel(), - className: topBorderClassName, - onClick: close(ungroupNodes), - }, - ] - : selectedNodes.length > 1 - ? [ - { - name: strings.getGroupMenuItemLabel(), - className: topBorderClassName, - onClick: close(groupNodes), - }, - ] - : []; - }; - - private _getPanels = (closePopover: () => void): EuiContextMenuPanelDescriptor[] => { - const { - showLayerControls, - cutNodes, - copyNodes, - pasteNodes, - deleteNodes, - cloneNodes, - } = this.props; - - // closes popover after invoking fn - const close = (fn: () => void) => () => { - fn(); - closePopover(); - }; - - const items: EuiContextMenuPanelItemDescriptor[] = [ - { - name: shortcutHelp.CUT, - icon: 'cut', - onClick: close(cutNodes), - }, - { - name: shortcutHelp.COPY, - icon: 'copy', - onClick: copyNodes, - }, - { - name: shortcutHelp.PASTE, // TODO: can this be disabled if clipboard is empty? - icon: 'copyClipboard', - onClick: close(pasteNodes), - }, - { - name: shortcutHelp.DELETE, - icon: 'trash', - onClick: close(deleteNodes), - }, - { - name: shortcutHelp.CLONE, - onClick: close(cloneNodes), - }, - ...this._getGroupMenuItems(close), - ]; - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: strings.getContextMenuTitle(), - items, - }, - ]; - - const fillMenu = ({ menuItem, panel }: MenuTuple) => { - items.push(menuItem); // add Order menu item to first panel - panels.push(panel); // add nested panel for layers controls - }; - - if (showLayerControls) { - fillMenu(this._getLayerMenuItems()); - } - - if (this.props.selectedNodes.length > 1) { - fillMenu(this._getAlignmentMenuItems(close)); - } - - if (this.props.selectedNodes.length > 2) { - fillMenu(this._getDistributionMenuItems(close)); - } - - items.push({ - name: strings.getSaveElementMenuItemLabel(), - icon: 'indexOpen', - className: topBorderClassName, - onClick: this._showModal, - }); - - return panels; - }; - - private _renderContextMenu = () => ( - <Popover - id="sidebar-context-menu-popover" - className="canvasContextMenu" - button={contextMenuButton} - panelPaddingSize="none" - tooltip={strings.getContextMenuTitle()} - tooltipPosition="bottom" - > - {({ closePopover }: { closePopover: () => void }) => ( - <EuiContextMenu initialPanelId={0} panels={this._getPanels(closePopover)} /> - )} - </Popover> - ); - - private _handleSave = (name: string, description: string, image: string) => { - const { createCustomElement } = this.props; - createCustomElement(name, description, image); - this._hideModal(); - }; - - render() { - const { title, showLayerControls } = this.props; - const { isModalVisible } = this.state; - - return ( - <Fragment> - <EuiFlexGroup - className="canvasLayout__sidebarHeader" - gutterSize="none" - alignItems="center" - justifyContent="spaceBetween" - > +export const SidebarHeader: FunctionComponent<Props> = ({ + title, + showLayerControls, + bringToFront, + bringForward, + sendBackward, + sendToBack, +}) => ( + <EuiFlexGroup + className="canvasLayout__sidebarHeader" + gutterSize="none" + alignItems="center" + justifyContent="spaceBetween" + > + <EuiFlexItem grow={false}> + <EuiTitle size="xs"> + <h3>{title}</h3> + </EuiTitle> + </EuiFlexItem> + {showLayerControls ? ( + <EuiFlexItem grow={false}> + <EuiFlexGroup alignItems="center" gutterSize="none"> <EuiFlexItem grow={false}> - <EuiTitle size="xs"> - <h3>{title}</h3> - </EuiTitle> + <EuiToolTip + position="bottom" + content={ + <span> + {shortcutHelp.BRING_TO_FRONT} + <ToolTipShortcut namespace="ELEMENT" action="BRING_TO_FRONT" /> + </span> + } + > + <EuiButtonIcon + color="text" + iconType="sortUp" + onClick={bringToFront} + aria-label={strings.getBringToFrontAriaLabel()} + /> + </EuiToolTip> </EuiFlexItem> <EuiFlexItem grow={false}> - <EuiFlexGroup alignItems="center" gutterSize="none"> - {showLayerControls ? this._renderLayoutControls() : null} - <EuiFlexItem grow={false}> - <EuiToolTip position="bottom" content={strings.getSaveElementMenuItemLabel()}> - <EuiButtonIcon - color="text" - iconType="indexOpen" - onClick={this._showModal} - aria-label={strings.getSaveElementMenuItemLabel()} - /> - </EuiToolTip> - </EuiFlexItem> - <EuiFlexItem grow={false}>{this._renderContextMenu()}</EuiFlexItem> - </EuiFlexGroup> + <EuiToolTip + position="bottom" + content={ + <span> + {shortcutHelp.BRING_FORWARD} + <ToolTipShortcut namespace="ELEMENT" action="BRING_FORWARD" /> + </span> + } + > + <EuiButtonIcon + color="text" + iconType="arrowUp" + onClick={bringForward} + aria-label={strings.getBringForwardAriaLabel()} + /> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip + position="bottom" + content={ + <span> + {shortcutHelp.SEND_BACKWARD} + <ToolTipShortcut namespace="ELEMENT" action="SEND_BACKWARD" /> + </span> + } + > + <EuiButtonIcon + color="text" + iconType="arrowDown" + onClick={sendBackward} + aria-label={strings.getSendBackwardAriaLabel()} + /> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip + position="bottom" + content={ + <span> + {shortcutHelp.SEND_TO_BACK} + <ToolTipShortcut namespace="ELEMENT" action="SEND_TO_BACK" /> + </span> + } + > + <EuiButtonIcon + color="text" + iconType="sortDown" + onClick={sendToBack} + aria-label={strings.getSendToBackAriaLabel()} + /> + </EuiToolTip> </EuiFlexItem> </EuiFlexGroup> - {isModalVisible ? ( - <EuiOverlayMask> - <CustomElementModal - title={strings.getCreateElementModalTitle()} - onSave={this._handleSave} - onCancel={this._hideModal} - /> - </EuiOverlayMask> - ) : null} - </Fragment> - ); - } -} + </EuiFlexItem> + ) : null} + </EuiFlexGroup> +); + +SidebarHeader.propTypes = { + title: PropTypes.string.isRequired, + showLayerControls: PropTypes.bool, // TODO: remove when we support relayering multiple elements + bringToFront: PropTypes.func.isRequired, + bringForward: PropTypes.func.isRequired, + sendBackward: PropTypes.func.isRequired, + sendToBack: PropTypes.func.isRequired, +}; + +SidebarHeader.defaultProps = { + showLayerControls: false, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss deleted file mode 100644 index 3d217dd1fc180..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss +++ /dev/null @@ -1,7 +0,0 @@ -.canvasControlSettings__popover { - width: 600px; -} - -.canvasControlSettings__list { - columns: 2; -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.tsx deleted file mode 100644 index f0bd140a0b725..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/control_settings.tsx +++ /dev/null @@ -1,84 +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 React, { MouseEventHandler } from 'react'; -import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -// @ts-ignore untyped local -import { Popover } from '../../popover'; -import { AutoRefreshControls } from './auto_refresh_controls'; -import { KioskControls } from './kiosk_controls'; - -import { ComponentStrings } from '../../../../i18n'; -const { WorkpadHeaderControlSettings: strings } = ComponentStrings; - -interface Props { - refreshInterval: number; - setRefreshInterval: (interval: number | undefined) => void; - autoplayEnabled: boolean; - autoplayInterval: number; - enableAutoplay: (enable: boolean) => void; - setAutoplayInterval: (interval: number | undefined) => void; -} - -export const ControlSettings = ({ - setRefreshInterval, - refreshInterval, - autoplayEnabled, - autoplayInterval, - enableAutoplay, - setAutoplayInterval, -}: Props) => { - const setRefresh = (val: number | undefined) => setRefreshInterval(val); - - const disableInterval = () => { - setRefresh(0); - }; - - const popoverButton = (handleClick: MouseEventHandler<HTMLButtonElement>) => ( - <EuiToolTip position="bottom" content={strings.getTooltip()}> - <EuiButtonIcon iconType="gear" aria-label={strings.getTooltip()} onClick={handleClick} /> - </EuiToolTip> - ); - - return ( - <Popover - id="auto-refresh-popover" - button={popoverButton} - anchorPosition="rightUp" - panelClassName="canvasControlSettings__popover" - > - {() => ( - <EuiFlexGroup> - <EuiFlexItem> - <AutoRefreshControls - refreshInterval={refreshInterval} - setRefresh={val => setRefresh(val)} - disableInterval={() => disableInterval()} - /> - </EuiFlexItem> - <EuiFlexItem> - <KioskControls - autoplayEnabled={autoplayEnabled} - autoplayInterval={autoplayInterval} - onSetInterval={setAutoplayInterval} - onSetEnabled={enableAutoplay} - /> - </EuiFlexItem> - </EuiFlexGroup> - )} - </Popover> - ); -}; - -ControlSettings.propTypes = { - refreshInterval: PropTypes.number, - setRefreshInterval: PropTypes.func.isRequired, - autoplayEnabled: PropTypes.bool, - autoplayInterval: PropTypes.number, - enableAutoplay: PropTypes.func.isRequired, - setAutoplayInterval: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts deleted file mode 100644 index 316a49c85c09d..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/index.ts +++ /dev/null @@ -1,35 +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 { connect } from 'react-redux'; -import { - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, - // @ts-ignore untyped local -} from '../../../state/actions/workpad'; -// @ts-ignore untyped local -import { getRefreshInterval, getAutoplay } from '../../../state/selectors/workpad'; -import { State } from '../../../../types'; -import { ControlSettings as Component } from './control_settings'; - -const mapStateToProps = (state: State) => { - const { enabled, interval } = getAutoplay(state); - - return { - refreshInterval: getRefreshInterval(state), - autoplayEnabled: enabled, - autoplayInterval: interval, - }; -}; - -const mapDispatchToProps = { - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, -}; - -export const ControlSettings = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot new file mode 100644 index 0000000000000..42c59d41dca62 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot @@ -0,0 +1,205 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/EditMenu 2 elements selected 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="Edit options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + data-test-subj="canvasWorkpadEditMenuButton" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + Edit + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu 3+ elements selected 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="Edit options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + data-test-subj="canvasWorkpadEditMenuButton" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + Edit + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu clipboard data exists 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="Edit options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + data-test-subj="canvasWorkpadEditMenuButton" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + Edit + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu default 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="Edit options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + data-test-subj="canvasWorkpadEditMenuButton" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + Edit + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu single element selected 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="Edit options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + data-test-subj="canvasWorkpadEditMenuButton" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + Edit + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/EditMenu single grouped element selected 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="Edit options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + data-test-subj="canvasWorkpadEditMenuButton" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + Edit + </span> + </span> + </button> + </div> +</div> +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx new file mode 100644 index 0000000000000..a0ab8d53485f5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx @@ -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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { EditMenu } from '../edit_menu'; + +const handlers = { + cutNodes: action('cutNodes'), + copyNodes: action('copyNodes'), + pasteNodes: action('pasteNodes'), + deleteNodes: action('deleteNodes'), + cloneNodes: action('cloneNodes'), + bringToFront: action('bringToFront'), + bringForward: action('bringForward'), + sendBackward: action('sendBackward'), + sendToBack: action('sendToBack'), + alignLeft: action('alignLeft'), + alignCenter: action('alignCenter'), + alignRight: action('alignRight'), + alignTop: action('alignTop'), + alignMiddle: action('alignMiddle'), + alignBottom: action('alignBottom'), + distributeHorizontally: action('distributeHorizontally'), + distributeVertically: action('distributeVertically'), + createCustomElement: action('createCustomElement'), + groupNodes: action('groupNodes'), + ungroupNodes: action('ungroupNodes'), + undoHistory: action('undoHistory'), + redoHistory: action('redoHistory'), +}; + +storiesOf('components/WorkpadHeader/EditMenu', module) + .add('default', () => ( + <EditMenu selectedNodes={[]} groupIsSelected={false} hasPasteData={false} {...handlers} /> + )) + .add('clipboard data exists', () => ( + <EditMenu selectedNodes={[]} groupIsSelected={false} hasPasteData={true} {...handlers} /> + )) + .add('single element selected', () => ( + <EditMenu selectedNodes={['foo']} groupIsSelected={false} hasPasteData={false} {...handlers} /> + )) + .add('single grouped element selected', () => ( + <EditMenu + selectedNodes={['foo', 'bar']} + groupIsSelected={true} + hasPasteData={false} + {...handlers} + /> + )) + .add('2 elements selected', () => ( + <EditMenu + selectedNodes={['foo', 'bar']} + groupIsSelected={false} + hasPasteData={false} + {...handlers} + /> + )) + .add('3+ elements selected', () => ( + <EditMenu + selectedNodes={['foo', 'bar', 'fizz']} + groupIsSelected={false} + hasPasteData={false} + {...handlers} + /> + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx new file mode 100644 index 0000000000000..15191b8d416ff --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx @@ -0,0 +1,448 @@ +/* + * 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, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { EuiButtonEmpty, EuiContextMenu, EuiIcon, EuiOverlayMask } from '@elastic/eui'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ShortcutStrings } from '../../../../i18n/shortcuts'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { CustomElementModal } from '../../custom_element_modal'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib/constants'; + +const { WorkpadHeaderEditMenu: strings } = ComponentStrings; +const shortcutHelp = ShortcutStrings.getShortcutHelp(); + +export interface Props { + /** + * cuts selected elements + */ + cutNodes: () => void; + /** + * copies selected elements to clipboard + */ + copyNodes: () => void; + /** + * pastes elements stored in clipboard to page + */ + pasteNodes: () => void; + /** + * clones selected elements + */ + cloneNodes: () => void; + /** + * deletes selected elements + */ + deleteNodes: () => void; + /** + * moves selected element to top layer + */ + bringToFront: () => void; + /** + * moves selected element up one layer + */ + bringForward: () => void; + /** + * moves selected element down one layer + */ + sendBackward: () => void; + /** + * moves selected element to bottom layer + */ + sendToBack: () => void; + /** + * saves the selected elements as an custom-element saved object + */ + createCustomElement: (name: string, description: string, image: string) => void; + /** + * indicated whether the selected element is a group or not + */ + groupIsSelected: boolean; + /** + * only more than one selected element can be grouped + */ + selectedNodes: string[]; + /** + * groups selected elements + */ + groupNodes: () => void; + /** + * ungroups selected group + */ + ungroupNodes: () => void; + /** + * left align selected elements + */ + alignLeft: () => void; + /** + * center align selected elements + */ + alignCenter: () => void; + /** + * right align selected elements + */ + alignRight: () => void; + /** + * top align selected elements + */ + alignTop: () => void; + /** + * middle align selected elements + */ + alignMiddle: () => void; + /** + * bottom align selected elements + */ + alignBottom: () => void; + /** + * horizontally distribute selected elements + */ + distributeHorizontally: () => void; + /** + * vertically distribute selected elements + */ + distributeVertically: () => void; + /** + * Reverts last change to the workpad + */ + undoHistory: () => void; + /** + * Reapplies last reverted change to the workpad + */ + redoHistory: () => void; + /** + * Is there element clipboard data to paste? + */ + hasPasteData: boolean; +} + +export const EditMenu: FunctionComponent<Props> = ({ + cutNodes, + copyNodes, + pasteNodes, + deleteNodes, + cloneNodes, + bringToFront, + bringForward, + sendBackward, + sendToBack, + alignLeft, + alignCenter, + alignRight, + alignTop, + alignMiddle, + alignBottom, + distributeHorizontally, + distributeVertically, + createCustomElement, + selectedNodes, + groupIsSelected, + groupNodes, + ungroupNodes, + undoHistory, + redoHistory, + hasPasteData, +}) => { + const [isModalVisible, setModalVisible] = useState(false); + const showModal = () => setModalVisible(true); + const hideModal = () => setModalVisible(false); + + const handleSave = (name: string, description: string, image: string) => { + createCustomElement(name, description, image); + hideModal(); + }; + + const editControl = (togglePopover: React.MouseEventHandler<any>) => ( + <EuiButtonEmpty + size="xs" + aria-label={strings.getEditMenuLabel()} + onClick={togglePopover} + data-test-subj="canvasWorkpadEditMenuButton" + > + {strings.getEditMenuButtonLabel()} + </EuiButtonEmpty> + ); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const groupMenuItem = groupIsSelected + ? { + name: strings.getUngroupMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: <EuiIcon type="empty" size="m" />, + onClick: () => { + ungroupNodes(); + closePopover(); + }, + } + : { + name: strings.getGroupMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: <EuiIcon type="empty" size="m" />, + disabled: selectedNodes.length < 2, + onClick: () => { + groupNodes(); + closePopover(); + }, + }; + + const orderMenuItem = { + name: strings.getOrderMenuItemLabel(), + disabled: selectedNodes.length !== 1, // TODO: change to === 0 when we support relayering multiple elements + icon: <EuiIcon type="empty" size="m" />, + panel: { + id: 1, + title: strings.getOrderMenuItemLabel(), + items: [ + { + name: shortcutHelp.BRING_TO_FRONT, // TODO: check against current element position and disable if already top layer + icon: 'sortUp', + onClick: bringToFront, + }, + { + name: shortcutHelp.BRING_FORWARD, // TODO: same as above + icon: 'arrowUp', + onClick: bringForward, + }, + { + name: shortcutHelp.SEND_BACKWARD, // TODO: check against current element position and disable if already bottom layer + icon: 'arrowDown', + onClick: sendBackward, + }, + { + name: shortcutHelp.SEND_TO_BACK, // TODO: same as above + icon: 'sortDown', + onClick: sendToBack, + }, + ], + }, + }; + + const alignmentMenuItem = { + name: strings.getAlignmentMenuItemLabel(), + className: 'canvasContextMenu', + disabled: groupIsSelected || selectedNodes.length < 2, + icon: <EuiIcon type="empty" size="m" />, + panel: { + id: 2, + title: strings.getAlignmentMenuItemLabel(), + items: [ + { + name: strings.getLeftAlignMenuItemLabel(), + icon: 'editorItemAlignLeft', + onClick: () => { + alignLeft(); + closePopover(); + }, + }, + { + name: strings.getCenterAlignMenuItemLabel(), + icon: 'editorItemAlignCenter', + onClick: () => { + alignCenter(); + closePopover(); + }, + }, + { + name: strings.getRightAlignMenuItemLabel(), + icon: 'editorItemAlignRight', + onClick: () => { + alignRight(); + closePopover(); + }, + }, + { + name: strings.getTopAlignMenuItemLabel(), + icon: 'editorItemAlignTop', + onClick: () => { + alignTop(); + closePopover(); + }, + }, + { + name: strings.getMiddleAlignMenuItemLabel(), + icon: 'editorItemAlignMiddle', + onClick: () => { + alignMiddle(); + closePopover(); + }, + }, + { + name: strings.getBottomAlignMenuItemLabel(), + icon: 'editorItemAlignBottom', + onClick: () => { + alignBottom(); + closePopover(); + }, + }, + ], + }, + }; + + const distributionMenuItem = { + name: strings.getDistributionMenuItemLabel(), + className: 'canvasContextMenu', + disabled: groupIsSelected || selectedNodes.length < 3, + icon: <EuiIcon type="empty" size="m" />, + panel: { + id: 3, + title: strings.getAlignmentMenuItemLabel(), + items: [ + { + name: strings.getHorizontalDistributionMenuItemLabel(), + icon: 'editorDistributeHorizontal', + onClick: () => { + distributeHorizontally(); + closePopover(); + }, + }, + { + name: strings.getVerticalDistributionMenuItemLabel(), + icon: 'editorDistributeVertical', + onClick: () => { + distributeVertically(); + closePopover(); + }, + }, + ], + }, + }; + + const savedElementMenuItem = { + name: strings.getSaveElementMenuItemLabel(), + icon: <EuiIcon type="indexOpen" size="m" />, + disabled: selectedNodes.length < 1, + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'canvasWorkpadEditMenu__saveElementButton', + onClick: () => { + showModal(); + closePopover(); + }, + }; + + const items = [ + { + // TODO: check history and disable when there are no more changes to revert + name: strings.getUndoMenuItemLabel(), + icon: <EuiIcon type="editorUndo" size="m" />, + onClick: () => { + undoHistory(); + }, + }, + { + // TODO: check history and disable when there are no more changes to reapply + name: strings.getRedoMenuItemLabel(), + icon: <EuiIcon type="editorRedo" size="m" />, + onClick: () => { + redoHistory(); + }, + }, + { + name: shortcutHelp.CUT, + icon: <EuiIcon type="cut" size="m" />, + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + disabled: selectedNodes.length < 1, + onClick: () => { + cutNodes(); + closePopover(); + }, + }, + { + name: shortcutHelp.COPY, + disabled: selectedNodes.length < 1, + icon: <EuiIcon type="copy" size="m" />, + onClick: () => { + copyNodes(); + }, + }, + { + name: shortcutHelp.PASTE, // TODO: can this be disabled if clipboard is empty? + icon: <EuiIcon type="copyClipboard" size="m" />, + disabled: !hasPasteData, + onClick: () => { + pasteNodes(); + closePopover(); + }, + }, + { + name: shortcutHelp.DELETE, + icon: <EuiIcon type="trash" size="m" />, + disabled: selectedNodes.length < 1, + onClick: () => { + deleteNodes(); + closePopover(); + }, + }, + { + name: shortcutHelp.CLONE, + icon: <EuiIcon type="empty" size="m" />, + disabled: selectedNodes.length < 1, + onClick: () => { + cloneNodes(); + closePopover(); + }, + }, + groupMenuItem, + orderMenuItem, + alignmentMenuItem, + distributionMenuItem, + savedElementMenuItem, + ]; + + return { + id: 0, + // title: strings.getEditMenuLabel(), + items, + }; + }; + + return ( + <Fragment> + <Popover button={editControl} panelPaddingSize="none" anchorPosition="downLeft"> + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + <EuiContextMenu + initialPanelId={0} + panels={flattenPanelTree(getPanelTree(closePopover))} + /> + )} + </Popover> + {isModalVisible ? ( + <EuiOverlayMask> + <CustomElementModal + title={strings.getCreateElementModalTitle()} + onSave={handleSave} + onCancel={hideModal} + /> + </EuiOverlayMask> + ) : null} + </Fragment> + ); +}; + +EditMenu.propTypes = { + cutNodes: PropTypes.func.isRequired, + copyNodes: PropTypes.func.isRequired, + pasteNodes: PropTypes.func.isRequired, + deleteNodes: PropTypes.func.isRequired, + cloneNodes: PropTypes.func.isRequired, + bringToFront: PropTypes.func.isRequired, + bringForward: PropTypes.func.isRequired, + sendBackward: PropTypes.func.isRequired, + sendToBack: PropTypes.func.isRequired, + alignLeft: PropTypes.func.isRequired, + alignCenter: PropTypes.func.isRequired, + alignRight: PropTypes.func.isRequired, + alignTop: PropTypes.func.isRequired, + alignMiddle: PropTypes.func.isRequired, + alignBottom: PropTypes.func.isRequired, + distributeHorizontally: PropTypes.func.isRequired, + distributeVertically: PropTypes.func.isRequired, + createCustomElement: PropTypes.func.isRequired, + selectedNodes: PropTypes.arrayOf(PropTypes.string).isRequired, + groupIsSelected: PropTypes.bool.isRequired, + groupNodes: PropTypes.func.isRequired, + ungroupNodes: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/index.ts new file mode 100644 index 0000000000000..a8bb7177dbd24 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/edit_menu/index.ts @@ -0,0 +1,123 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withHandlers, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { State, PositionedElement } from '../../../../types'; +import { getClipboardData } from '../../../lib/clipboard'; +// @ts-ignore Untyped local +import { flatten } from '../../../lib/aeroelastic/functional'; +// @ts-ignore Untyped local +import { globalStateUpdater } from '../../workpad_page/integration_utils'; +// @ts-ignore Untyped local +import { crawlTree } from '../../workpad_page/integration_utils'; +// @ts-ignore Untyped local +import { insertNodes, elementLayer, removeElements } from '../../../state/actions/elements'; +// @ts-ignore Untyped local +import { undoHistory, redoHistory } from '../../../state/actions/history'; +// @ts-ignore Untyped local +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { + getSelectedPage, + getNodes, + getSelectedToplevelNodes, +} from '../../../state/selectors/workpad'; +import { + layerHandlerCreators, + clipboardHandlerCreators, + basicHandlerCreators, + groupHandlerCreators, + alignmentDistributionHandlerCreators, +} from '../../../lib/element_handler_creators'; +import { EditMenu as Component, Props as ComponentProps } from './edit_menu'; + +type LayoutState = any; + +type CommitFn = (type: string, payload: any) => LayoutState; + +interface OwnProps { + commit: CommitFn; +} + +const withGlobalState = ( + commit: CommitFn, + updateGlobalState: (layoutState: LayoutState) => void +) => (type: string, payload: any) => { + const newLayoutState = commit(type, payload); + if (newLayoutState.currentScene.gestureEnd) { + updateGlobalState(newLayoutState); + } +}; + +/* + * TODO: this is all copied from interactive_workpad_page and workpad_shortcuts + */ +const mapStateToProps = (state: State) => { + const pageId = getSelectedPage(state); + const nodes = getNodes(state, pageId) as PositionedElement[]; + const selectedToplevelNodes = getSelectedToplevelNodes(state); + const selectedPrimaryShapeObjects = selectedToplevelNodes + .map((id: string) => nodes.find((s: PositionedElement) => s.id === id)) + .filter((shape?: PositionedElement) => shape) as PositionedElement[]; + const selectedPersistentPrimaryNodes = flatten( + selectedPrimaryShapeObjects.map((shape: PositionedElement) => + nodes.find((n: PositionedElement) => n.id === shape.id) // is it a leaf or a persisted group? + ? [shape.id] + : nodes.filter((s: PositionedElement) => s.position.parent === shape.id).map(s => s.id) + ) + ); + const selectedNodeIds = flatten(selectedPersistentPrimaryNodes.map(crawlTree(nodes))); + + return { + pageId, + selectedToplevelNodes, + selectedNodes: selectedNodeIds.map((id: string) => nodes.find(s => s.id === id)), + state, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), + removeNodes: (nodeIds: string[], pageId: string) => dispatch(removeElements(nodeIds, pageId)), + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes(nodes.filter((e: PositionedElement) => !e.position.parent).map(e => e.id)) + ), + elementLayer: (pageId: string, elementId: string, movement: number) => { + dispatch(elementLayer({ pageId, elementId, movement })); + }, + undoHistory: () => dispatch(undoHistory()), + redoHistory: () => dispatch(redoHistory()), + dispatch, +}); + +const mergeProps = ( + { state, selectedToplevelNodes, ...restStateProps }: ReturnType<typeof mapStateToProps>, + { dispatch, ...restDispatchProps }: ReturnType<typeof mapDispatchToProps>, + { commit }: OwnProps +) => { + const updateGlobalState = globalStateUpdater(dispatch, state); + + return { + ...restDispatchProps, + ...restStateProps, + commit: withGlobalState(commit, updateGlobalState), + groupIsSelected: + selectedToplevelNodes.length === 1 && selectedToplevelNodes[0].includes('group'), + }; +}; + +export const EditMenu = compose<ComponentProps, OwnProps>( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), + withHandlers(basicHandlerCreators), + withHandlers(clipboardHandlerCreators), + withHandlers(layerHandlerCreators), + withHandlers(groupHandlerCreators), + withHandlers(alignmentDistributionHandlerCreators) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.examples.storyshot new file mode 100644 index 0000000000000..82ed28acced7d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.examples.storyshot @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/ElementMenu default 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="Add an element" + className="euiButton euiButton--primary euiButton--small canvasElementMenu__popoverButton euiButton--fill" + data-test-subj="add-element-button" + onClick={[Function]} + type="button" + > + <span + className="euiButton__content" + > + <div + aria-hidden="true" + className="euiButton__icon" + data-euiicon-type="plusInCircle" + size="m" + /> + <span + className="euiButton__text" + > + Add element + </span> + </span> + </button> + </div> +</div> +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx new file mode 100644 index 0000000000000..9aca5ce33ba02 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx @@ -0,0 +1,150 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ElementSpec } from '../../../../../types'; +import { ElementMenu } from '../element_menu'; + +const testElements: { [key: string]: ElementSpec } = { + areaChart: { + name: 'areaChart', + displayName: 'Area chart', + help: 'A line chart with a filled body', + type: 'chart', + expression: `filters + | demodata + | pointseries x="time" y="mean(price)" + | plot defaultStyle={seriesStyle lines=1 fill=1} + | render`, + }, + debug: { + name: 'debug', + displayName: 'Debug data', + help: 'Just dumps the configuration of the element', + icon: 'bug', + expression: `demodata + | render as=debug`, + }, + dropdownFilter: { + name: 'dropdownFilter', + displayName: 'Dropdown select', + type: 'filter', + help: 'A dropdown from which you can select values for an "exactly" filter', + icon: 'filter', + height: 50, + expression: `demodata + | dropdownControl valueColumn=project filterColumn=project | render`, + filter: '', + }, + filterDebug: { + name: 'filterDebug', + displayName: 'Debug filter', + help: 'Shows the underlying global filters in a workpad', + icon: 'bug', + expression: `filters + | render as=debug`, + }, + image: { + name: 'image', + displayName: 'Image', + help: 'A static image', + type: 'image', + expression: `image dataurl=null mode="contain" + | render`, + }, + markdown: { + name: 'markdown', + displayName: 'Text', + type: 'text', + help: 'Add text using Markdown', + icon: 'visText', + expression: `filters +| demodata +| markdown "### Welcome to the Markdown element + +Good news! You're already connected to some demo data! + +The data table contains +**{{rows.length}} rows**, each containing +the following columns: +{{#each columns}} +**{{name}}** +{{/each}} + +You can use standard Markdown in here, but you can also access your piped-in data using Handlebars. If you want to know more, check out the [Handlebars documentation](https://handlebarsjs.com/guide/expressions.html). + +#### Enjoy!" | render`, + }, + progressGauge: { + name: 'progressGauge', + displayName: 'Gauge', + type: 'progress', + help: 'Displays progress as a portion of a gauge', + width: 200, + height: 200, + icon: 'visGoal', + expression: `filters + | demodata + | math "mean(percent_uptime)" + | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="Helvetica" color="#000000" align=center} + | render`, + }, + revealImage: { + name: 'revealImage', + displayName: 'Image reveal', + type: 'image', + help: 'Reveals a percentage of an image', + expression: `filters + | demodata + | math "mean(percent_uptime)" + | revealImage origin=bottom image=null + | render`, + }, + shape: { + name: 'shape', + displayName: 'Shape', + type: 'shape', + help: 'A customizable shape', + width: 200, + height: 200, + icon: 'node', + expression: + 'shape "square" fill="#4cbce4" border="rgba(255,255,255,0)" borderWidth=0 maintainAspect=false | render', + }, + table: { + name: 'table', + displayName: 'Data table', + type: 'chart', + help: 'A scrollable grid for displaying data in a tabular format', + expression: `filters + | demodata + | table + | render`, + }, + timeFilter: { + name: 'timeFilter', + displayName: 'Time filter', + type: 'filter', + help: 'Set a time window', + icon: 'calendar', + height: 50, + expression: `timefilterControl compact=true column=@timestamp + | render`, + filter: 'timefilter column=@timestamp from=now-24h to=now', + }, +}; + +const mockRenderEmbedPanel = () => <div id="embeddablePanel" />; + +storiesOf('components/WorkpadHeader/ElementMenu', module).add('default', () => ( + <ElementMenu + elements={testElements} + addElement={action('addElement')} + renderEmbedPanel={mockRenderEmbedPanel} + /> +)); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss new file mode 100644 index 0000000000000..a946ee5519ce4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss @@ -0,0 +1,3 @@ +.canvasElementMenu__popoverButton { + margin-right: $euiSizeS; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx new file mode 100644 index 0000000000000..fbb5d70dfc55c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -0,0 +1,215 @@ +/* + * 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 { sortBy } from 'lodash'; +import React, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ElementSpec } from '../../../../types'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { getId } from '../../../lib/get_id'; +import { Popover, ClosePopoverFn } from '../../popover'; +// @ts-ignore Untyped local +import { AssetManager } from '../../asset_manager'; +import { SavedElementsModal } from '../../saved_elements_modal'; + +interface CategorizedElementLists { + [key: string]: ElementSpec[]; +} + +interface ElementTypeMeta { + [key: string]: { name: string; icon: string }; +} + +export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; + +// label and icon for the context menu item for each element type +const elementTypeMeta: ElementTypeMeta = { + chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, + filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, + image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, + other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, + progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, + shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, + text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, +}; + +const getElementType = (element: ElementSpec): string => + element && element.type && Object.keys(elementTypeMeta).includes(element.type) + ? element.type + : 'other'; + +const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { + elements = sortBy(elements, 'displayName'); + + const categories: CategorizedElementLists = { other: [] }; + + elements.forEach((element: ElementSpec) => { + const type = getElementType(element); + + if (categories[type]) { + categories[type].push(element); + } else { + categories[type] = [element]; + } + }); + + return categories; +}; + +export interface Props { + /** + * Dictionary of elements from elements registry + */ + elements: { [key: string]: ElementSpec }; + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: ElementSpec) => void; + /** + * Renders embeddable flyout + */ + renderEmbedPanel: (onClose: () => void) => JSX.Element; +} + +export const ElementMenu: FunctionComponent<Props> = ({ + elements, + addElement, + renderEmbedPanel, +}) => { + const [isAssetModalVisible, setAssetModalVisible] = useState(false); + const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); + const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); + + const hideAssetModal = () => setAssetModalVisible(false); + const showAssetModal = () => setAssetModalVisible(true); + const hideEmbedPanel = () => setEmbedPanelVisible(false); + const showEmbedPanel = () => setEmbedPanelVisible(true); + const hideSavedElementsModal = () => setSavedElementsModalVisible(false); + const showSavedElementsModal = () => setSavedElementsModalVisible(true); + + const { + chart: chartElements, + filter: filterElements, + image: imageElements, + other: otherElements, + progress: progressElements, + shape: shapeElements, + text: textElements, + } = categorizeElementsByType(Object.values(elements)); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ + name: element.displayName || element.name, + icon: element.icon, + onClick: () => { + addElement(element); + closePopover(); + }, + }); + + const elementListToMenuItems = (elementList: ElementSpec[]) => { + const type = getElementType(elementList[0]); + const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; + + if (elementList.length > 1) { + return { + name, + icon: <EuiIcon type={icon} size="m" />, + panel: { + id: getId('element-type'), + title: name, + items: elementList.map(elementToMenuItem), + }, + }; + } + + return elementToMenuItem(elementList[0]); + }; + + return { + id: 0, + items: [ + elementListToMenuItems(textElements), + elementListToMenuItems(shapeElements), + elementListToMenuItems(chartElements), + elementListToMenuItems(imageElements), + elementListToMenuItems(filterElements), + elementListToMenuItems(progressElements), + elementListToMenuItems(otherElements), + { + name: strings.getMyElementsMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'saved-elements-menu-option', + icon: <EuiIcon type="empty" size="m" />, + onClick: () => { + showSavedElementsModal(); + closePopover(); + }, + }, + { + name: strings.getAssetsMenuItemLabel(), + icon: <EuiIcon type="empty" size="m" />, + onClick: () => { + showAssetModal(); + closePopover(); + }, + }, + { + name: strings.getEmbedObjectMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: <EuiIcon type="logoKibana" size="m" />, + onClick: () => { + showEmbedPanel(); + closePopover(); + }, + }, + ], + }; + }; + + const exportControl = (togglePopover: React.MouseEventHandler<any>) => ( + <EuiButton + fill + iconType="plusInCircle" + size="s" + aria-label={strings.getElementMenuLabel()} + onClick={togglePopover} + className="canvasElementMenu__popoverButton" + data-test-subj="add-element-button" + > + {strings.getElementMenuButtonLabel()} + </EuiButton> + ); + + return ( + <Fragment> + <Popover button={exportControl} panelPaddingSize="none" anchorPosition="downLeft"> + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + <EuiContextMenu + initialPanelId={0} + panels={flattenPanelTree(getPanelTree(closePopover))} + /> + )} + </Popover> + {isAssetModalVisible ? <AssetManager onClose={hideAssetModal} /> : null} + {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} + {isSavedElementsModalVisible ? <SavedElementsModal onClose={hideSavedElementsModal} /> : null} + </Fragment> + ); +}; + +ElementMenu.propTypes = { + elements: PropTypes.object, + addElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx new file mode 100644 index 0000000000000..40571a9341f69 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { State, ElementSpec } from '../../../../types'; +// @ts-ignore Untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; +// @ts-ignore Untyped local +import { addElement } from '../../../state/actions/elements'; +import { getSelectedPage } from '../../../state/selectors/workpad'; +import { AddEmbeddablePanel } from '../../embeddable_flyout'; + +interface StateProps { + pageId: string; +} + +interface DispatchProps { + addElement: (pageId: string) => (partialElement: ElementSpec) => void; +} + +const mapStateToProps = (state: State) => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), +}); + +const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ + ...stateProps, + ...dispatchProps, + addElement: dispatchProps.addElement(stateProps.pageId), + // Moved this section out of the main component to enable stories + renderEmbedPanel: (onClose: () => void) => <AddEmbeddablePanel onClose={onClose} />, +}); + +export const ElementMenu = compose<ComponentProps, {}>( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withProps(() => ({ elements: elementsRegistry.toJS() })) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx index 1768adf9be79d..d651e649128f9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/refresh_control/refresh_control.tsx @@ -32,6 +32,7 @@ export const RefreshControl = ({ doRefresh, inFlight }: Props) => ( iconType="refresh" aria-label={strings.getRefreshAriaLabel()} onClick={doRefresh} + data-test-subj="canvas-refresh-control" /> </EuiToolTip> ); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot index 43adcb37c5f4c..9ad2714a78ec9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots components/Export/PDFPanel default 1`] = ` +exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = ` <div className="euiPanel" > <div - className="canvasWorkpadExport__panelContent" + className="canvasShareMenu__panelContent" > <div className="euiText euiText--small" diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.examples.storyshot new file mode 100644 index 0000000000000..1610ef5fd1fac --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.examples.storyshot @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/ShareMenu default 1`] = ` +<div> + <div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} + > + <div + className="euiPopover__anchor" + > + <button + aria-label="Share this workpad" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + Share + </span> + </span> + </button> + </div> + </div> +</div> +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx similarity index 92% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx index 76a40f51148a7..eb99dbc494a32 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { PDFPanel } from '../pdf_panel'; -storiesOf('components/Export/PDFPanel', module) +storiesOf('components/WorkpadHeader/ShareMenu/PDFPanel', module) .addParameters({ info: { inline: true, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx new file mode 100644 index 0000000000000..ab9137b1676c9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ShareMenu } from '../share_menu'; + +storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( + <ShareMenu + onCopy={action('onCopy')} + onExport={action('onExport')} + getExportUrl={(type: string) => { + action(`getExportUrl('${type}')`); + return type; + }} + /> +)); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx index af30d8d4fc20b..886ddcfd763e1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { ShareWebsiteFlyout } from '../share_website_flyout'; -storiesOf('components/Export/ShareWebsiteFlyout', module) +storiesOf('components/WorkpadHeader/ShareMenu/ShareWebsiteFlyout', module) .addParameters({ info: { inline: true, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts new file mode 100644 index 0000000000000..4377635acac88 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -0,0 +1,99 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { + getWorkpad, + getRenderedWorkpad, + getRenderedWorkpadExpressions, +} from '../../../../state/selectors/workpad'; +// @ts-ignore Untyped local +import { + downloadRenderedWorkpad, + downloadRuntime, + downloadZippedRuntime, +} from '../../../../lib/download_workpad'; +import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; +import { State, CanvasWorkpad } from '../../../../../types'; +import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; +import { arrayBufferFetch } from '../../../../../common/lib/fetch'; +import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; +import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; + +import { ComponentStrings } from '../../../../../i18n/components'; +import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public/'; +import { OnCloseFn } from '../share_menu'; +import { WithKibanaProps } from '../../../../index'; +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +const getUnsupportedRenderers = (state: State) => { + const renderers: string[] = []; + const expressions = getRenderedWorkpadExpressions(state); + expressions.forEach(expression => { + if (!renderFunctionNames.includes(expression)) { + renderers.push(expression); + } + }); + + return renderers; +}; + +const mapStateToProps = (state: State) => ({ + renderedWorkpad: getRenderedWorkpad(state), + unsupportedRenderers: getUnsupportedRenderers(state), + workpad: getWorkpad(state), +}); + +interface Props { + onClose: OnCloseFn; + renderedWorkpad: CanvasRenderedWorkpad; + unsupportedRenderers: string[]; + workpad: CanvasWorkpad; +} + +export const ShareWebsiteFlyout = compose<ComponentProps, Pick<Props, 'onClose'>>( + connect(mapStateToProps), + withKibana, + withProps( + ({ + unsupportedRenderers, + renderedWorkpad, + onClose, + workpad, + kibana, + }: Props & WithKibanaProps): ComponentProps => ({ + unsupportedRenderers, + onClose, + onCopy: () => { + kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); + }, + onDownload: type => { + switch (type) { + case 'share': + downloadRenderedWorkpad(renderedWorkpad); + return; + case 'shareRuntime': + downloadRuntime(kibana.services.http.basePath.get()); + return; + case 'shareZip': + const basePath = kibana.services.http.basePath.get(); + arrayBufferFetch + .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) + .then(blob => downloadZippedRuntime(blob.data)) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getShareableZipErrorTitle(workpad.name), + }); + }); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx similarity index 98% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx index 8dcbb18ffed86..5fd381baa73f5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ComponentStrings } from '../../../../../i18n/components'; import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants'; -import { OnCloseFn } from '../workpad_export'; +import { OnCloseFn } from '../share_menu'; import { WorkpadStep } from './workpad_step'; import { RuntimeStep } from './runtime_step'; import { SnippetsStep } from './snippets_step'; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx similarity index 98% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index c19ad6d77b131..81f559651eb25 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -42,7 +42,7 @@ export const SnippetsStep: FC<{ onCopy: OnCopyFn }> = ({ onCopy }) => ( <EuiSpacer size="s" /> <Clipboard content={HTML} onCopy={onCopy}> <EuiCodeBlock - className="canvasWorkpadExport__reportingConfig" + className="canvasShareMenu__reportingConfig" paddingSize="s" fontSize="s" language="html" diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/workpad_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/workpad_step.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/workpad_step.tsx diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/index.ts new file mode 100644 index 0000000000000..d6565f0e43db7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/index.ts @@ -0,0 +1,95 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; +import { getWorkpad, getPages } from '../../../state/selectors/workpad'; +import { getWindow } from '../../../lib/get_window'; +import { downloadWorkpad } from '../../../lib/download_workpad'; +import { ShareMenu as Component, Props as ComponentProps } from './share_menu'; +import { getPdfUrl, createPdf } from './utils'; +import { State, CanvasWorkpad } from '../../../../types'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { WithKibanaProps } from '../../../index'; + +import { ComponentStrings } from '../../../../i18n'; + +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +const mapStateToProps = (state: State) => ({ + workpad: getWorkpad(state), + pageCount: getPages(state).length, +}); + +const getAbsoluteUrl = (path: string) => { + const { location } = getWindow(); + + if (!location) { + return path; + } // fallback for mocked window object + + const { protocol, hostname, port } = location; + return `${protocol}//${hostname}:${port}${path}`; +}; + +interface Props { + workpad: CanvasWorkpad; + pageCount: number; +} + +export const ShareMenu = compose<ComponentProps, {}>( + connect(mapStateToProps), + withKibana, + withProps( + ({ workpad, pageCount, kibana }: Props & WithKibanaProps): ComponentProps => ({ + getExportUrl: type => { + if (type === 'pdf') { + const pdfUrl = getPdfUrl(workpad, { pageCount }, kibana.services.http.basePath); + return getAbsoluteUrl(pdfUrl); + } + + throw new Error(strings.getUnknownExportErrorMessage(type)); + }, + onCopy: type => { + switch (type) { + case 'pdf': + kibana.services.canvas.notify.info(strings.getCopyPDFMessage()); + break; + case 'reportingConfig': + kibana.services.canvas.notify.info(strings.getCopyReportingConfigMessage()); + break; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + onExport: type => { + switch (type) { + case 'pdf': + return createPdf(workpad, { pageCount }, kibana.services.http.basePath) + .then(({ data }: { data: { job: { id: string } } }) => { + kibana.services.canvas.notify.info(strings.getExportPDFMessage(), { + title: strings.getExportPDFTitle(workpad.name), + }); + + // register the job so a completion notification shows up when it's ready + jobCompletionNotifications.add(data.job.id); + }) + .catch((err: Error) => { + kibana.services.canvas.notify.error(err, { + title: strings.getExportPDFErrorTitle(workpad.name), + }); + }); + case 'json': + downloadWorkpad(workpad.id); + return; + default: + throw new Error(strings.getUnknownExportErrorMessage(type)); + } + }, + }) + ) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx similarity index 92% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx index ef70079cf697b..a178964e0b566 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { Clipboard } from '../../clipboard'; import { ComponentStrings } from '../../../../i18n/components'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; interface Props { /** The URL that will invoke PDF Report generation. */ @@ -24,7 +24,7 @@ interface Props { * A panel displayed in the Export Menu with options in which to generate PDF Reports. */ export const PDFPanel = ({ pdfURL, onExport, onCopy }: Props) => ( - <div className="canvasWorkpadExport__panelContent"> + <div className="canvasShareMenu__panelContent"> <EuiText size="s"> <p>{strings.getPDFPanelGenerateDescription()}</p> </EuiText> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss new file mode 100644 index 0000000000000..03227f77e0de5 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss @@ -0,0 +1,11 @@ +.canvasShareMenu__panelContent { + padding: $euiSize; +} + +.canvasShareMenu__reportingConfig { + .euiCodeBlock__pre { + @include euiScrollBar; + overflow-x: auto; + white-space: pre; + } +} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx new file mode 100644 index 0000000000000..2ac0591a1bdd4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -0,0 +1,121 @@ +/* + * 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, { FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; +import { ComponentStrings } from '../../../../i18n/components'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { PDFPanel } from './pdf_panel'; +import { ShareWebsiteFlyout } from './flyout'; + +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; + +type CopyTypes = 'pdf' | 'reportingConfig'; +type ExportTypes = 'pdf' | 'json'; +type ExportUrlTypes = 'pdf'; +type CloseTypes = 'share'; + +export type OnCopyFn = (type: CopyTypes) => void; +export type OnExportFn = (type: ExportTypes) => void; +export type OnCloseFn = (type: CloseTypes) => void; +export type GetExportUrlFn = (type: ExportUrlTypes) => string; + +export interface Props { + /** Handler to invoke when an export URL is copied to the clipboard. */ + onCopy: OnCopyFn; + /** Handler to invoke when an end product is exported. */ + onExport: OnExportFn; + /** Handler to retrive an export URL based on the type of export requested. */ + getExportUrl: GetExportUrlFn; +} + +/** + * The Menu for Exporting a Workpad from Canvas. + */ +export const ShareMenu: FunctionComponent<Props> = ({ onCopy, onExport, getExportUrl }) => { + const [showFlyout, setShowFlyout] = useState(false); + + const onClose = () => { + setShowFlyout(false); + }; + + const getPDFPanel = (closePopover: ClosePopoverFn) => { + return ( + <PDFPanel + pdfURL={getExportUrl('pdf')} + onExport={() => { + onExport('pdf'); + closePopover(); + }} + onCopy={() => { + onCopy('pdf'); + closePopover(); + }} + /> + ); + }; + + const getPanelTree = (closePopover: ClosePopoverFn) => ({ + id: 0, + items: [ + { + name: strings.getShareDownloadJSONTitle(), + icon: <EuiIcon type="exportAction" size="m" />, + onClick: () => { + onExport('json'); + closePopover(); + }, + }, + { + name: strings.getShareDownloadPDFTitle(), + icon: 'document', + panel: { + id: 1, + title: strings.getShareDownloadPDFTitle(), + content: getPDFPanel(closePopover), + }, + }, + { + name: strings.getShareWebsiteTitle(), + icon: <EuiIcon type="globe" size="m" />, + onClick: () => { + setShowFlyout(true); + closePopover(); + }, + }, + ], + }); + + const shareControl = (togglePopover: React.MouseEventHandler<any>) => ( + <EuiButtonEmpty size="xs" aria-label={strings.getShareWorkpadMessage()} onClick={togglePopover}> + {strings.getShareMenuButtonLabel()} + </EuiButtonEmpty> + ); + + const flyout = showFlyout ? <ShareWebsiteFlyout onClose={onClose} /> : null; + + return ( + <div> + <Popover button={shareControl} panelPaddingSize="none" anchorPosition="downLeft"> + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + <EuiContextMenu + initialPanelId={0} + panels={flattenPanelTree(getPanelTree(closePopover))} + /> + )} + </Popover> + {flyout} + </div> + ); +}; + +ShareMenu.propTypes = { + onCopy: PropTypes.func.isRequired, + onExport: PropTypes.func.isRequired, + getExportUrl: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.ts diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot new file mode 100644 index 0000000000000..eb45f97452ae1 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/ViewMenu edit mode 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="View options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + View + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/ViewMenu read only mode 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="View options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + View + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/ViewMenu with autoplay enabled 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="View options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + View + </span> + </span> + </button> + </div> +</div> +`; + +exports[`Storyshots components/WorkpadHeader/ViewMenu with refresh enabled 1`] = ` +<div + className="euiPopover euiPopover--anchorDownLeft" + container={null} + onKeyDown={[Function]} + onMouseDown={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchStart={[Function]} +> + <div + className="euiPopover__anchor" + > + <button + aria-label="View options" + className="euiButtonEmpty euiButtonEmpty--primary euiButtonEmpty--xSmall" + onClick={[Function]} + type="button" + > + <span + className="euiButtonEmpty__content" + > + <span + className="euiButtonEmpty__text" + > + View + </span> + </span> + </button> + </div> +</div> +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx new file mode 100644 index 0000000000000..5b4de05da3a3d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ViewMenu } from '../view_menu'; + +const handlers = { + setZoomScale: action('setZoomScale'), + zoomIn: action('zoomIn'), + zoomOut: action('zoomOut'), + toggleWriteable: action('toggleWriteable'), + resetZoom: action('resetZoom'), + enterFullscreen: action('enterFullscreen'), + doRefresh: action('doRefresh'), + fitToWindow: action('fitToWindow'), + setRefreshInterval: action('setRefreshInterval'), + setAutoplayInterval: action('setAutoplayInterval'), + enableAutoplay: action('enableAutoplay'), +}; + +storiesOf('components/WorkpadHeader/ViewMenu', module) + .add('edit mode', () => ( + <ViewMenu + isWriteable={true} + zoomScale={1} + refreshInterval={0} + autoplayInterval={0} + autoplayEnabled={false} + {...handlers} + /> + )) + .add('read only mode', () => ( + <ViewMenu + isWriteable={false} + zoomScale={1} + refreshInterval={0} + autoplayInterval={0} + autoplayEnabled={false} + {...handlers} + /> + )) + .add('with refresh enabled', () => ( + <ViewMenu + isWriteable={false} + zoomScale={1} + refreshInterval={1000} + autoplayInterval={0} + autoplayEnabled={false} + {...handlers} + /> + )) + .add('with autoplay enabled', () => ( + <ViewMenu + isWriteable={false} + zoomScale={1} + refreshInterval={0} + autoplayInterval={5000} + autoplayEnabled={true} + {...handlers} + /> + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx similarity index 83% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx index 97d8920d50dd3..cfd599b1d9f3f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx @@ -22,7 +22,6 @@ import { htmlIdGenerator, } from '@elastic/eui'; import { timeDuration } from '../../../lib/time_duration'; -import { RefreshControl } from '../refresh_control'; import { CustomInterval } from './custom_interval'; import { ComponentStrings, UnitStrings } from '../../../../i18n'; @@ -69,7 +68,11 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv const intervalTitleId = generateId(); return ( - <EuiFlexGroup direction="column" justifyContent="spaceBetween"> + <EuiFlexGroup + direction="column" + justifyContent="spaceBetween" + className="canvasViewMenu__kioskSettings" + > <EuiFlexItem grow={false}> <EuiFlexGroup alignItems="center" justifyContent="spaceAround" gutterSize="xs"> <EuiFlexItem> @@ -97,9 +100,6 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv </EuiToolTip> </EuiFlexItem> ) : null} - <EuiFlexItem grow={false}> - <RefreshControl /> - </EuiFlexItem> </EuiFlexGroup> </EuiFlexItem> </EuiFlexGroup> @@ -112,16 +112,6 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv <EuiSpacer size="s" /> <EuiText size="s"> <ListGroup aria-labelledby={intervalTitleId} className="canvasControlSettings__list"> - <RefreshItem - duration={5000} - label={getSecondsText(5)} - descriptionId={intervalTitleId} - /> - <RefreshItem - duration={15000} - label={getSecondsText(15)} - descriptionId={intervalTitleId} - /> <RefreshItem duration={30000} label={getSecondsText(30)} @@ -137,11 +127,6 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv label={getMinutesText(5)} descriptionId={intervalTitleId} /> - <RefreshItem - duration={900000} - label={getMinutesText(15)} - descriptionId={intervalTitleId} - /> <RefreshItem duration={1800000} label={getMinutesText(30)} @@ -152,16 +137,6 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv label={getHoursText(1)} descriptionId={intervalTitleId} /> - <RefreshItem - duration={7200000} - label={getHoursText(2)} - descriptionId={intervalTitleId} - /> - <RefreshItem - duration={21600000} - label={getHoursText(6)} - descriptionId={intervalTitleId} - /> <RefreshItem duration={43200000} label={getHoursText(12)} @@ -175,7 +150,6 @@ export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterv </ListGroup> </EuiText> </EuiFlexItem> - <EuiFlexItem grow={false}> <CustomInterval onSubmit={value => setRefresh(value)} /> </EuiFlexItem> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/custom_interval.tsx diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts new file mode 100644 index 0000000000000..e1ad9782c8aef --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -0,0 +1,104 @@ +/* + * 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 { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { Dispatch } from 'redux'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; +import { State, CanvasWorkpadBoundingBox } from '../../../../types'; +// @ts-ignore Untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +// @ts-ignore Untyped local +import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +// @ts-ignore Untyped local +import { + setWriteable, + setRefreshInterval, + enableAutoplay, + setAutoplayInterval, + // @ts-ignore Untyped local +} from '../../../state/actions/workpad'; +import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; +import { + getWorkpadBoundingBox, + getWorkpadWidth, + getWorkpadHeight, + isWriteable, + getRefreshInterval, + getAutoplay, +} from '../../../state/selectors/workpad'; +import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; +import { getFitZoomScale } from './lib/get_fit_zoom_scale'; + +interface StateProps { + zoomScale: number; + boundingBox: CanvasWorkpadBoundingBox; + workpadWidth: number; + workpadHeight: number; + isWriteable: boolean; +} + +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; + setZoomScale: (scale: number) => void; + setFullscreen: (showFullscreen: boolean) => void; +} + +const mapStateToProps = (state: State) => { + const { enabled, interval } = getAutoplay(state); + + return { + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), + refreshInterval: getRefreshInterval(state), + autoplayEnabled: enabled, + autoplayInterval: interval, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), + setFullscreen: (value: boolean) => { + dispatch(setFullscreen(value)); + + if (value) { + dispatch(selectToplevelNodes([])); + } + }, + doRefresh: () => dispatch(fetchAllRenderables()), + setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), + enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(autoplay)), + setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + + return { + ...remainingStateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + enterFullscreen: () => dispatchProps.setFullscreen(true), + fitToWindow: () => + dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), + }; +}; + +export const ViewMenu = compose<ComponentProps, {}>( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withHandlers(zoomHandlerCreators) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx similarity index 86% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx index 9e6f0a91c6120..e63eed9f9df53 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx @@ -14,7 +14,6 @@ import { EuiHorizontalRule, EuiLink, EuiSpacer, - EuiSwitch, EuiText, EuiFlexItem, EuiFlexGroup, @@ -29,9 +28,7 @@ const { time: timeStrings } = UnitStrings; const { getSecondsText, getMinutesText } = timeStrings; interface Props { - autoplayEnabled: boolean; autoplayInterval: number; - onSetEnabled: (enabled: boolean) => void; onSetInterval: (interval: number | undefined) => void; } @@ -54,12 +51,7 @@ const ListGroup = ({ children, ...rest }: ListGroupProps) => ( const generateId = htmlIdGenerator(); -export const KioskControls = ({ - autoplayEnabled, - autoplayInterval, - onSetEnabled, - onSetInterval, -}: Props) => { +export const KioskControls = ({ autoplayInterval, onSetInterval }: Props) => { const RefreshItem = ({ duration, label, descriptionId }: RefreshItemProps) => ( <li> <EuiLink onClick={() => onSetInterval(duration)} aria-describedby={descriptionId}> @@ -72,7 +64,11 @@ export const KioskControls = ({ const intervalTitleId = generateId(); return ( - <EuiFlexGroup direction="column" justifyContent="spaceBetween"> + <EuiFlexGroup + direction="column" + justifyContent="spaceBetween" + className="canvasViewMenu__kioskSettings" + > <EuiFlexItem grow={false}> <EuiDescriptionList textStyle="reverse"> <EuiDescriptionListTitle>{strings.getTitle()}</EuiDescriptionListTitle> @@ -81,14 +77,6 @@ export const KioskControls = ({ </EuiDescriptionListDescription> </EuiDescriptionList> <EuiHorizontalRule margin="m" /> - - <EuiSwitch - checked={autoplayEnabled} - label={strings.getCycleToggleSwitch()} - onChange={ev => onSetEnabled(ev.target.checked)} - /> - <EuiSpacer size="m" /> - <EuiTitle size="xxxs" id={intervalTitleId}> <p>{strings.getCycleFormLabel()}</p> </EuiTitle> @@ -137,8 +125,6 @@ export const KioskControls = ({ }; KioskControls.propTypes = { - autoplayEnabled: PropTypes.bool.isRequired, autoplayInterval: PropTypes.number.isRequired, - onSetEnabled: PropTypes.func.isRequired, onSetInterval: PropTypes.func.isRequired, }; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts new file mode 100644 index 0000000000000..783d6340c33c4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts @@ -0,0 +1,39 @@ +/* + * 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 { + CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR, + WORKPAD_CANVAS_BUFFER, +} from '../../../../../common/lib'; +import { CanvasWorkpadBoundingBox } from '../../../../../types'; + +export const getFitZoomScale = ( + boundingBox: CanvasWorkpadBoundingBox, + workpadWidth: number, + workpadHeight: number +) => { + const canvasLayoutContent = document.querySelector( + `#${CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}` + ) as HTMLElement; + const layoutWidth = canvasLayoutContent.clientWidth; + const layoutHeight = canvasLayoutContent.clientHeight; + const offsetLeft = boundingBox.left; + const offsetTop = boundingBox.top; + const offsetRight = boundingBox.right - workpadWidth; + const offsetBottom = boundingBox.bottom - workpadHeight; + const boundingWidth = + workpadWidth + + Math.max(Math.abs(offsetLeft), Math.abs(offsetRight)) * 2 + + WORKPAD_CANVAS_BUFFER; + const boundingHeight = + workpadHeight + + Math.max(Math.abs(offsetTop), Math.abs(offsetBottom)) * 2 + + WORKPAD_CANVAS_BUFFER; + const xScale = layoutWidth / boundingWidth; + const yScale = layoutHeight / boundingHeight; + + return Math.min(xScale, yScale); +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.scss new file mode 100644 index 0000000000000..c4e06881981c7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.scss @@ -0,0 +1,4 @@ +.canvasViewMenu__kioskSettings, +.canvasViewMenu__refreshSettings { + padding: $euiSize; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx new file mode 100644 index 0000000000000..b6f108cda37f6 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx @@ -0,0 +1,273 @@ +/* + * 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, { FunctionComponent } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButtonEmpty, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { + MAX_ZOOM_LEVEL, + MIN_ZOOM_LEVEL, + CONTEXT_MENU_TOP_BORDER_CLASSNAME, +} from '../../../../common/lib/constants'; +import { ComponentStrings } from '../../../../i18n/components'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; +import { AutoRefreshControls } from './auto_refresh_controls'; +import { KioskControls } from './kiosk_controls'; + +const { WorkpadHeaderViewMenu: strings } = ComponentStrings; + +const QUICK_ZOOM_LEVELS = [0.5, 1, 2]; + +export interface Props { + /** + * Is the workpad edittable? + */ + isWriteable: boolean; + /** + * current workpad zoom level + */ + zoomScale: number; + /** + * zooms to fit entire workpad into view + */ + fitToWindow: () => void; + /** + * handler to set the workpad zoom level to a specific value + */ + setZoomScale: (scale: number) => void; + /** + * handler to increase the workpad zoom level + */ + zoomIn: () => void; + /** + * handler to decrease workpad zoom level + */ + zoomOut: () => void; + /** + * reset zoom to 100% + */ + resetZoom: () => void; + /** + * toggle edit/read only mode + */ + toggleWriteable: () => void; + /** + * enter fullscreen mode + */ + enterFullscreen: () => void; + /** + * triggers a refresh of the workpad + */ + doRefresh: () => void; + /** + * Current auto refresh interval + */ + refreshInterval: number; + /** + * Sets auto refresh interval + */ + setRefreshInterval: (interval?: number) => void; + /** + * Is autoplay enabled? + */ + autoplayEnabled: boolean; + /** + * Current autoplay interval + */ + autoplayInterval: number; + /** + * Enables autoplay + */ + enableAutoplay: (autoplay: boolean) => void; + /** + * Sets autoplay interval + */ + setAutoplayInterval: (interval?: number) => void; +} + +export const ViewMenu: FunctionComponent<Props> = ({ + enterFullscreen, + fitToWindow, + isWriteable, + resetZoom, + setZoomScale, + toggleWriteable, + zoomIn, + zoomOut, + zoomScale, + doRefresh, + refreshInterval, + setRefreshInterval, + autoplayEnabled, + autoplayInterval, + enableAutoplay, + setAutoplayInterval, +}) => { + const setRefresh = (val: number | undefined) => setRefreshInterval(val); + + const disableInterval = () => { + setRefresh(0); + }; + + const viewControl = (togglePopover: React.MouseEventHandler<any>) => ( + <EuiButtonEmpty size="xs" aria-label={strings.getViewMenuLabel()} onClick={togglePopover}> + {strings.getViewMenuButtonLabel()} + </EuiButtonEmpty> + ); + + const getScaleMenuItems = (): EuiContextMenuPanelItemDescriptor[] => + QUICK_ZOOM_LEVELS.map((scale: number) => ({ + name: strings.getZoomPercentage(scale), + icon: 'empty', + onClick: () => setZoomScale(scale), + })); + + const getZoomMenuItems = (): EuiContextMenuPanelItemDescriptor[] => [ + { + name: strings.getZoomFitToWindowText(), + icon: 'empty', + onClick: fitToWindow, + disabled: zoomScale === MAX_ZOOM_LEVEL, + }, + ...getScaleMenuItems(), + { + name: strings.getZoomInText(), + icon: 'magnifyWithPlus', + onClick: zoomIn, + disabled: zoomScale === MAX_ZOOM_LEVEL, + className: 'canvasContextMenu--topBorder', + }, + { + name: strings.getZoomOutText(), + icon: 'magnifyWithMinus', + onClick: zoomOut, + disabled: zoomScale <= MIN_ZOOM_LEVEL, + }, + { + name: strings.getZoomResetText(), + icon: 'empty', + onClick: resetZoom, + disabled: zoomScale >= MAX_ZOOM_LEVEL, + className: 'canvasContextMenu--topBorder', + }, + ]; + + const getPanelTree = (closePopover: ClosePopoverFn) => ({ + id: 0, + items: [ + { + name: strings.getRefreshMenuItemLabel(), + icon: 'refresh', + onClick: () => { + doRefresh(); + }, + }, + { + name: strings.getRefreshSettingsMenuItemLabel(), + icon: 'empty', + panel: { + id: 1, + title: strings.getRefreshSettingsMenuItemLabel(), + content: ( + <AutoRefreshControls + refreshInterval={refreshInterval} + setRefresh={val => setRefresh(val)} + disableInterval={() => disableInterval()} + /> + ), + }, + }, + { + name: strings.getFullscreenMenuItemLabel(), + icon: <EuiIcon type="fullScreen" size="m" />, + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + onClick: () => { + enterFullscreen(); + closePopover(); + }, + }, + { + name: autoplayEnabled + ? strings.getAutoplayOffMenuItemLabel() + : strings.getAutoplayOnMenuItemLabel(), + icon: autoplayEnabled ? 'stop' : 'play', + onClick: () => { + enableAutoplay(!autoplayEnabled); + closePopover(); + }, + }, + { + name: strings.getAutoplaySettingsMenuItemLabel(), + icon: 'empty', + panel: { + id: 2, + title: strings.getAutoplaySettingsMenuItemLabel(), + content: ( + <KioskControls + autoplayInterval={autoplayInterval} + onSetInterval={setAutoplayInterval} + /> + ), + }, + }, + { + name: isWriteable ? strings.getHideEditModeLabel() : strings.getShowEditModeLabel(), + icon: <EuiIcon type={isWriteable ? 'eyeClosed' : 'eye'} size="m" />, + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + onClick: () => { + toggleWriteable(); + closePopover(); + }, + }, + { + name: strings.getZoomMenuItemLabel(), + icon: 'magnifyWithPlus', + panel: { + id: 3, + title: strings.getZoomMenuItemLabel(), + items: getZoomMenuItems(), + }, + }, + ], + }); + + return ( + <Popover button={viewControl} panelPaddingSize="none" anchorPosition="downLeft"> + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + <EuiContextMenu + initialPanelId={0} + panels={flattenPanelTree(getPanelTree(closePopover))} + className="canvasViewMenu" + /> + )} + </Popover> + ); +}; + +ViewMenu.propTypes = { + isWriteable: PropTypes.bool.isRequired, + zoomScale: PropTypes.number.isRequired, + fitToWindow: PropTypes.func.isRequired, + setZoomScale: PropTypes.func.isRequired, + zoomIn: PropTypes.func.isRequired, + zoomOut: PropTypes.func.isRequired, + resetZoom: PropTypes.func.isRequired, + toggleWriteable: PropTypes.func.isRequired, + enterFullscreen: PropTypes.func.isRequired, + doRefresh: PropTypes.func.isRequired, + refreshInterval: PropTypes.number.isRequired, + setRefreshInterval: PropTypes.func.isRequired, + autoplayEnabled: PropTypes.bool.isRequired, + autoplayInterval: PropTypes.number.isRequired, + enableAutoplay: PropTypes.func.isRequired, + setAutoplayInterval: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot deleted file mode 100644 index ef96320e7bc65..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/Export/WorkpadExport enabled 1`] = ` -<div> - <div - className="euiPopover euiPopover--anchorDownCenter" - container={null} - onKeyDown={[Function]} - onMouseDown={[Function]} - onMouseUp={[Function]} - onTouchEnd={[Function]} - onTouchStart={[Function]} - > - <div - className="euiPopover__anchor" - > - <span - className="euiToolTipAnchor" - onMouseOut={[Function]} - onMouseOver={[Function]} - > - <button - aria-label="Share this workpad" - className="euiButtonIcon euiButtonIcon--primary" - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - type="button" - > - <div - aria-hidden="true" - className="euiButtonIcon__icon" - data-euiicon-type="share" - size="m" - /> - </button> - </span> - </div> - </div> -</div> -`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx deleted file mode 100644 index 92e7cca40ee3a..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx +++ /dev/null @@ -1,20 +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 { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import { WorkpadExport } from '../workpad_export'; - -storiesOf('components/Export/WorkpadExport', module).add('enabled', () => ( - <WorkpadExport - onCopy={action('onCopy')} - onExport={action('onExport')} - getExportUrl={(type: string) => { - action(`getExportUrl('${type}')`); - return type; - }} - /> -)); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts deleted file mode 100644 index 2bf3e1f0ef1f4..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts +++ /dev/null @@ -1,102 +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 { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -// @ts-ignore Untyped local -import { - getWorkpad, - getRenderedWorkpad, - getRenderedWorkpadExpressions, -} from '../../../../state/selectors/workpad'; -// @ts-ignore Untyped local -import { notify } from '../../../../lib/notify'; -// @ts-ignore Untyped local -import { - downloadRenderedWorkpad, - downloadRuntime, - downloadZippedRuntime, - // @ts-ignore Untyped local -} from '../../../../lib/download_workpad'; -import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; -import { State, CanvasWorkpad } from '../../../../../types'; -import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; -// @ts-ignore Untyped local. -import { fetch, arrayBufferFetch } from '../../../../../common/lib/fetch'; -import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; -import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; - -import { ComponentStrings } from '../../../../../i18n/components'; -import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public/'; -import { OnCloseFn } from '../workpad_export'; -import { WithKibanaProps } from '../../../../index'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; - -const getUnsupportedRenderers = (state: State) => { - const renderers: string[] = []; - const expressions = getRenderedWorkpadExpressions(state); - expressions.forEach(expression => { - if (!renderFunctionNames.includes(expression)) { - renderers.push(expression); - } - }); - - return renderers; -}; - -const mapStateToProps = (state: State) => ({ - renderedWorkpad: getRenderedWorkpad(state), - unsupportedRenderers: getUnsupportedRenderers(state), - workpad: getWorkpad(state), -}); - -interface Props { - onClose: OnCloseFn; - renderedWorkpad: CanvasRenderedWorkpad; - unsupportedRenderers: string[]; - workpad: CanvasWorkpad; -} - -export const ShareWebsiteFlyout = compose<ComponentProps, Pick<Props, 'onClose'>>( - connect(mapStateToProps), - withKibana, - withProps( - ({ - unsupportedRenderers, - renderedWorkpad, - onClose, - workpad, - kibana, - }: Props & WithKibanaProps): ComponentProps => ({ - unsupportedRenderers, - onClose, - onCopy: () => { - notify.info(strings.getCopyShareConfigMessage()); - }, - onDownload: type => { - switch (type) { - case 'share': - downloadRenderedWorkpad(renderedWorkpad); - return; - case 'shareRuntime': - downloadRuntime(kibana.services.http.basePath.get()); - return; - case 'shareZip': - const basePath = kibana.services.http.basePath.get(); - arrayBufferFetch - .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) - .then(blob => downloadZippedRuntime(blob.data)) - .catch((err: Error) => { - notify.error(err, { title: strings.getShareableZipErrorTitle(workpad.name) }); - }); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts deleted file mode 100644 index b0083eb4f87e2..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/index.ts +++ /dev/null @@ -1,102 +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 { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; -// @ts-ignore Untyped local -import { getWorkpad, getPages } from '../../../state/selectors/workpad'; -// @ts-ignore Untyped local -import { notify } from '../../../lib/notify'; -import { getWindow } from '../../../lib/get_window'; -// @ts-ignore Untyped local -import { - downloadWorkpad, - // @ts-ignore Untyped local -} from '../../../lib/download_workpad'; -import { WorkpadExport as Component, Props as ComponentProps } from './workpad_export'; -import { getPdfUrl, createPdf } from './utils'; -import { State, CanvasWorkpad } from '../../../../types'; -// @ts-ignore Untyped local. -import { fetch, arrayBufferFetch } from '../../../../common/lib/fetch'; -import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; -import { WithKibanaProps } from '../../../index'; - -import { ComponentStrings } from '../../../../i18n'; - -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; - -const mapStateToProps = (state: State) => ({ - workpad: getWorkpad(state), - pageCount: getPages(state).length, -}); - -const getAbsoluteUrl = (path: string) => { - const { location } = getWindow(); - - if (!location) { - return path; - } // fallback for mocked window object - - const { protocol, hostname, port } = location; - return `${protocol}//${hostname}:${port}${path}`; -}; - -interface Props { - workpad: CanvasWorkpad; - pageCount: number; -} - -export const WorkpadExport = compose<ComponentProps, {}>( - connect(mapStateToProps), - withKibana, - withProps( - ({ workpad, pageCount, kibana }: Props & WithKibanaProps): ComponentProps => ({ - getExportUrl: type => { - if (type === 'pdf') { - const pdfUrl = getPdfUrl(workpad, { pageCount }, kibana.services.http.basePath); - return getAbsoluteUrl(pdfUrl); - } - - throw new Error(strings.getUnknownExportErrorMessage(type)); - }, - onCopy: type => { - switch (type) { - case 'pdf': - notify.info(strings.getCopyPDFMessage()); - break; - case 'reportingConfig': - notify.info(strings.getCopyReportingConfigMessage()); - break; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - onExport: type => { - switch (type) { - case 'pdf': - return createPdf(workpad, { pageCount }, kibana.services.http.basePath) - .then(({ data }: { data: { job: { id: string } } }) => { - notify.info(strings.getExportPDFMessage(), { - title: strings.getExportPDFTitle(workpad.name), - }); - - // register the job so a completion notification shows up when it's ready - jobCompletionNotifications.add(data.job.id); - }) - .catch((err: Error) => { - notify.error(err, { title: strings.getExportPDFErrorTitle(workpad.name) }); - }); - case 'json': - downloadWorkpad(workpad.id); - return; - default: - throw new Error(strings.getUnknownExportErrorMessage(type)); - } - }, - }) - ) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss deleted file mode 100644 index 44209aaa72d63..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss +++ /dev/null @@ -1,11 +0,0 @@ -.canvasWorkpadExport__panelContent { - padding: $euiSize; -} - -.canvasWorkpadExport__reportingConfig { - .euiCodeBlock__pre { - @include euiScrollBar; - overflow-x: auto; - white-space: pre; - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx deleted file mode 100644 index 522be043ec457..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx +++ /dev/null @@ -1,149 +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 React, { FunctionComponent, useState } from 'react'; -import PropTypes from 'prop-types'; -import { EuiButtonIcon, EuiContextMenu, EuiIcon } from '@elastic/eui'; -// @ts-ignore Untyped local -import { Popover } from '../../popover'; -import { PDFPanel } from './pdf_panel'; -import { ShareWebsiteFlyout } from './flyout'; - -import { ComponentStrings } from '../../../../i18n/components'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; - -type ClosePopoverFn = () => void; - -type CopyTypes = 'pdf' | 'reportingConfig'; -type ExportTypes = 'pdf' | 'json'; -type ExportUrlTypes = 'pdf'; -type CloseTypes = 'share'; - -export type OnCopyFn = (type: CopyTypes) => void; -export type OnExportFn = (type: ExportTypes) => void; -export type OnCloseFn = (type: CloseTypes) => void; -export type GetExportUrlFn = (type: ExportUrlTypes) => string; - -export interface Props { - /** Handler to invoke when an export URL is copied to the clipboard. */ - onCopy: OnCopyFn; - /** Handler to invoke when an end product is exported. */ - onExport: OnExportFn; - /** Handler to retrive an export URL based on the type of export requested. */ - getExportUrl: GetExportUrlFn; -} - -/** - * The Menu for Exporting a Workpad from Canvas. - */ -export const WorkpadExport: FunctionComponent<Props> = ({ onCopy, onExport, getExportUrl }) => { - const [showFlyout, setShowFlyout] = useState(false); - - const onClose = () => { - setShowFlyout(false); - }; - - // TODO: Fix all of this magic from EUI; this code is boilerplate from - // EUI examples and isn't easily typed. - const flattenPanelTree = (tree: any, array: any[] = []) => { - array.push(tree); - - if (tree.items) { - tree.items.forEach((item: any) => { - const { panel } = item; - if (panel) { - flattenPanelTree(panel, array); - item.panel = panel.id; - } - }); - } - - return array; - }; - - const getPDFPanel = (closePopover: ClosePopoverFn) => { - return ( - <PDFPanel - pdfURL={getExportUrl('pdf')} - onExport={() => { - onExport('pdf'); - closePopover(); - }} - onCopy={() => { - onCopy('pdf'); - closePopover(); - }} - /> - ); - }; - - const getPanelTree = (closePopover: ClosePopoverFn) => ({ - id: 0, - title: strings.getShareWorkpadMessage(), - items: [ - { - name: strings.getShareDownloadJSONTitle(), - icon: <EuiIcon type="exportAction" size="m" />, - onClick: () => { - onExport('json'); - closePopover(); - }, - }, - { - name: strings.getShareDownloadPDFTitle(), - icon: 'document', - panel: { - id: 1, - title: strings.getShareDownloadPDFTitle(), - content: getPDFPanel(closePopover), - }, - }, - { - name: strings.getShareWebsiteTitle(), - icon: <EuiIcon type="globe" size="m" />, - onClick: () => { - setShowFlyout(true); - closePopover(); - }, - }, - ], - }); - - const exportControl = (togglePopover: React.MouseEventHandler<any>) => ( - <EuiButtonIcon - iconType="share" - aria-label={strings.getShareWorkpadMessage()} - onClick={togglePopover} - /> - ); - - const flyout = showFlyout ? <ShareWebsiteFlyout onClose={onClose} /> : null; - - return ( - <div> - <Popover - button={exportControl} - panelPaddingSize="none" - tooltip={strings.getShareWorkpadMessage()} - tooltipPosition="bottom" - > - {({ closePopover }: { closePopover: ClosePopoverFn }) => ( - <EuiContextMenu - initialPanelId={0} - panels={flattenPanelTree(getPanelTree(closePopover))} - /> - )} - </Popover> - {flyout} - </div> - ); -}; - -WorkpadExport.propTypes = { - onCopy: PropTypes.func.isRequired, - onExport: PropTypes.func.isRequired, - getExportUrl: PropTypes.func.isRequired, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx index 31ad0593f58bb..4aab8280a9f24 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -4,38 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; // @ts-ignore no @types definition import { Shortcuts } from 'react-shortcuts'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiButtonIcon, - EuiButton, - EuiButtonEmpty, - EuiOverlayMask, - EuiModal, - EuiModalFooter, - EuiToolTip, -} from '@elastic/eui'; - +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; - -// @ts-ignore untyped local -import { AssetManager } from '../asset_manager'; -// @ts-ignore untyped local -import { ElementTypes } from '../element_types'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { AddEmbeddablePanel } from '../embeddable_flyout'; -// @ts-ignore untyped local -import { ControlSettings } from './control_settings'; // @ts-ignore untyped local import { RefreshControl } from './refresh_control'; // @ts-ignore untyped local import { FullscreenControl } from './fullscreen_control'; -import { WorkpadExport } from './workpad_export'; -import { WorkpadZoom } from './workpad_zoom'; +import { EditMenu } from './edit_menu'; +import { ElementMenu } from './element_menu'; +import { ShareMenu } from './share_menu'; +import { ViewMenu } from './view_menu'; const { WorkpadHeader: strings } = ComponentStrings; @@ -43,23 +26,22 @@ export interface Props { isWriteable: boolean; toggleWriteable: () => void; canUserWrite: boolean; - selectedPage: string; + commit: (type: string, payload: any) => any; } -interface State { - isModalVisible: boolean; - isPanelVisible: boolean; -} - -export class WorkpadHeader extends React.PureComponent<Props, State> { - static propTypes = { - isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, +export const WorkpadHeader: FunctionComponent<Props> = ({ + isWriteable, + canUserWrite, + toggleWriteable, + commit, +}) => { + const keyHandler = (action: string) => { + if (action === 'EDITING') { + toggleWriteable(); + } }; - state = { isModalVisible: false, isPanelVisible: false }; - - _fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( + const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( <EuiToolTip position="bottom" content={ @@ -77,52 +59,20 @@ export class WorkpadHeader extends React.PureComponent<Props, State> { </EuiToolTip> ); - _keyHandler = (action: string) => { - if (action === 'EDITING') { - this.props.toggleWriteable(); - } - }; - - _hideElementModal = () => this.setState({ isModalVisible: false }); - _showElementModal = () => this.setState({ isModalVisible: true }); - - _hideEmbeddablePanel = () => this.setState({ isPanelVisible: false }); - _showEmbeddablePanel = () => this.setState({ isPanelVisible: true }); - - _elementAdd = () => ( - <EuiOverlayMask> - <EuiModal - onClose={this._hideElementModal} - className="canvasModal--fixedSize" - maxWidth="1000px" - initialFocus=".canvasElements__filter input" - > - <ElementTypes onClose={this._hideElementModal} /> - <EuiModalFooter> - <EuiButton size="s" onClick={this._hideElementModal}> - {strings.getAddElementModalCloseButtonLabel()} - </EuiButton> - </EuiModalFooter> - </EuiModal> - </EuiOverlayMask> - ); - - _embeddableAdd = () => <AddEmbeddablePanel onClose={this._hideEmbeddablePanel} />; - - _getEditToggleToolTipText = () => { - if (!this.props.canUserWrite) { + const getEditToggleToolTipText = () => { + if (!canUserWrite) { return strings.getNoWritePermissionTooltipText(); } - const content = this.props.isWriteable + const content = isWriteable ? strings.getHideEditControlTooltip() : strings.getShowEditControlTooltip(); return content; }; - _getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { - const content = this._getEditToggleToolTipText(); + const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { + const content = getEditToggleToolTipText(); if (textOnly) { return content; @@ -135,86 +85,67 @@ export class WorkpadHeader extends React.PureComponent<Props, State> { ); }; - render() { - const { isWriteable, canUserWrite, toggleWriteable } = this.props; - const { isModalVisible, isPanelVisible } = this.state; - - return ( - <div> - {isModalVisible ? this._elementAdd() : null} - {isPanelVisible ? this._embeddableAdd() : null} - <EuiFlexGroup - gutterSize="s" - alignItems="center" - justifyContent="spaceBetween" - className="canvasLayout__stageHeaderInner" - > - <EuiFlexItem grow={false}> - <EuiFlexGroup alignItems="center" gutterSize="xs"> - <EuiFlexItem grow={false}> - <ControlSettings /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <RefreshControl /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <FullscreenControl>{this._fullscreenButton}</FullscreenControl> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <WorkpadZoom /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <WorkpadExport /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - {canUserWrite && ( - <Shortcuts - name="EDITOR" - handler={this._keyHandler} - targetNodeSelector="body" - global - isolate - /> - )} - <EuiToolTip position="bottom" content={this._getEditToggleToolTip()}> - <EuiButtonIcon - iconType={isWriteable ? 'lockOpen' : 'lock'} - onClick={toggleWriteable} - size="s" - aria-label={this._getEditToggleToolTipText()} - isDisabled={!canUserWrite} - /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - {isWriteable ? ( + return ( + <EuiFlexGroup + gutterSize="none" + alignItems="center" + justifyContent="spaceBetween" + className="canvasLayout__stageHeaderInner" + > + <EuiFlexItem grow={false}> + <EuiFlexGroup alignItems="center" gutterSize="none"> + {isWriteable && ( <EuiFlexItem grow={false}> - <EuiFlexGroup alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}> - <AssetManager /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty onClick={this._showEmbeddablePanel}> - {strings.getEmbedObjectButtonLabel()} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - size="s" - iconType="vector" - data-test-subj="add-element-button" - onClick={this._showElementModal} - > - {strings.getAddElementButtonLabel()} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> + <ElementMenu /> </EuiFlexItem> - ) : null} + )} + <EuiFlexItem grow={false}> + <ViewMenu /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EditMenu commit={commit} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <ShareMenu /> + </EuiFlexItem> </EuiFlexGroup> - </div> - ); - } -} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + {canUserWrite && ( + <Shortcuts + name="EDITOR" + handler={keyHandler} + targetNodeSelector="body" + global + isolate + /> + )} + <EuiToolTip position="bottom" content={getEditToggleToolTip()}> + <EuiButtonIcon + iconType={isWriteable ? 'eyeClosed' : 'eye'} + onClick={toggleWriteable} + size="s" + aria-label={getEditToggleToolTipText()} + isDisabled={!canUserWrite} + /> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <RefreshControl /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <FullscreenControl>{fullscreenButton}</FullscreenControl> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +WorkpadHeader.propTypes = { + isWriteable: PropTypes.bool, + toggleWriteable: PropTypes.func, + canUserWrite: PropTypes.bool, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx deleted file mode 100644 index b22a9d35aa793..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx +++ /dev/null @@ -1,38 +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 { compose, withHandlers } from 'recompose'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { getZoomScale } from '../../../state/selectors/app'; -import { - getWorkpadBoundingBox, - getWorkpadWidth, - getWorkpadHeight, -} from '../../../state/selectors/workpad'; -// @ts-ignore unconverted local file -import { setZoomScale } from '../../../state/actions/transient'; -import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; -import { WorkpadZoom as Component, Props as ComponentProps } from './workpad_zoom'; -import { State } from '../../../../types'; - -const mapStateToProps = (state: State) => { - return { - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), -}); - -export const WorkpadZoom = compose<ComponentProps, {}>( - connect(mapStateToProps, mapDispatchToProps), - withHandlers(zoomHandlerCreators) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss deleted file mode 100644 index 44209aaa72d63..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss +++ /dev/null @@ -1,11 +0,0 @@ -.canvasWorkpadExport__panelContent { - padding: $euiSize; -} - -.canvasWorkpadExport__reportingConfig { - .euiCodeBlock__pre { - @include euiScrollBar; - overflow-x: auto; - white-space: pre; - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx deleted file mode 100644 index 4e37a525761cd..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx +++ /dev/null @@ -1,176 +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 React, { MouseEventHandler, PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiButtonIcon, - EuiContextMenu, - EuiContextMenuPanelDescriptor, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui'; -// @ts-ignore untyped local -import { Popover } from '../../popover'; -import { - MAX_ZOOM_LEVEL, - MIN_ZOOM_LEVEL, - WORKPAD_CANVAS_BUFFER, - CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR, -} from '../../../../common/lib/constants'; - -import { ComponentStrings } from '../../../../i18n'; -const { WorkpadHeaderWorkpadZoom: strings } = ComponentStrings; - -export interface Props { - /** - * current workpad zoom level - */ - zoomScale: number; - /** - * minimum bounding box for the workpad - */ - boundingBox: { left: number; right: number; top: number; bottom: number }; - /** - * width of the workpad page - */ - workpadWidth: number; - /** - * height of the workpad page - */ - workpadHeight: number; - /** - * handler to set the workpad zoom level to a specific value - */ - setZoomScale: (scale: number) => void; - /** - * handler to increase the workpad zoom level - */ - zoomIn: () => void; - /** - * handler to decrease workpad zoom level - */ - zoomOut: () => void; - /** - * reset zoom to 100% - */ - resetZoom: () => void; -} - -const QUICK_ZOOM_LEVELS = [0.5, 1, 2]; - -export class WorkpadZoom extends PureComponent<Props> { - static propTypes = { - zoomScale: PropTypes.number.isRequired, - setZoomScale: PropTypes.func.isRequired, - zoomIn: PropTypes.func.isRequired, - zoomOut: PropTypes.func.isRequired, - resetZoom: PropTypes.func.isRequired, - boundingBox: PropTypes.shape({ - left: PropTypes.number.isRequired, - right: PropTypes.number.isRequired, - top: PropTypes.number.isRequired, - bottom: PropTypes.number.isRequired, - }), - workpadWidth: PropTypes.number.isRequired, - workpadHeight: PropTypes.number.isRequired, - }; - - _fitToWindow = () => { - const { boundingBox, setZoomScale, workpadWidth, workpadHeight } = this.props; - const canvasLayoutContent = document.querySelector( - `#${CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}` - ) as HTMLElement; - const layoutWidth = canvasLayoutContent.clientWidth; - const layoutHeight = canvasLayoutContent.clientHeight; - const offsetLeft = boundingBox.left; - const offsetTop = boundingBox.top; - const offsetRight = boundingBox.right - workpadWidth; - const offsetBottom = boundingBox.bottom - workpadHeight; - const boundingWidth = - workpadWidth + - Math.max(Math.abs(offsetLeft), Math.abs(offsetRight)) * 2 + - WORKPAD_CANVAS_BUFFER; - const boundingHeight = - workpadHeight + - Math.max(Math.abs(offsetTop), Math.abs(offsetBottom)) * 2 + - WORKPAD_CANVAS_BUFFER; - const xScale = layoutWidth / boundingWidth; - const yScale = layoutHeight / boundingHeight; - - setZoomScale(Math.min(xScale, yScale)); - }; - - _button = (togglePopover: MouseEventHandler<HTMLButtonElement>) => ( - <EuiButtonIcon - iconType="magnifyWithPlus" - aria-label={strings.getZoomControlsAriaLabel()} - onClick={togglePopover} - /> - ); - - _getScaleMenuItems = (): EuiContextMenuPanelItemDescriptor[] => - QUICK_ZOOM_LEVELS.map(scale => ({ - name: strings.getZoomPercentage(scale), - icon: 'empty', - onClick: () => this.props.setZoomScale(scale), - })); - - _getPanels = (): EuiContextMenuPanelDescriptor[] => { - const { zoomScale, zoomIn, zoomOut, resetZoom } = this.props; - const items: EuiContextMenuPanelItemDescriptor[] = [ - { - name: strings.getZoomFitToWindowText(), - icon: 'empty', - onClick: this._fitToWindow, - disabled: zoomScale === MAX_ZOOM_LEVEL, - }, - ...this._getScaleMenuItems(), - { - name: strings.getZoomInText(), - icon: 'magnifyWithPlus', - onClick: zoomIn, - disabled: zoomScale === MAX_ZOOM_LEVEL, - className: 'canvasContextMenu--topBorder', - }, - { - name: strings.getZoomOutText(), - icon: 'magnifyWithMinus', - onClick: zoomOut, - disabled: zoomScale <= MIN_ZOOM_LEVEL, - }, - { - name: strings.getZoomResetText(), - icon: 'empty', - onClick: resetZoom, - disabled: zoomScale >= MAX_ZOOM_LEVEL, - className: 'canvasContextMenu--topBorder', - }, - ]; - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: strings.getZoomPanelTitle(), - items, - }, - ]; - - return panels; - }; - - render() { - return ( - <Popover - button={this._button} - panelPaddingSize="none" - tooltip={strings.getZoomControlsTooltip()} - tooltipPosition="bottom" - > - {() => <EuiContextMenu initialPanelId={0} panels={this._getPanels()} />} - </Popover> - ); - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js index 429d27afb3f0d..9379379e54d97 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js @@ -6,14 +6,15 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { compose, withState, getContext, withHandlers } from 'recompose'; +import { compose, withState, getContext, withHandlers, withProps } from 'recompose'; +import moment from 'moment'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; import { canUserWrite } from '../../state/selectors/app'; import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; import { downloadWorkpad } from '../../lib/download_workpad'; import { ComponentStrings, ErrorStrings } from '../../../i18n'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { WorkpadLoader as Component } from './workpad_loader'; const { WorkpadLoader: strings } = ComponentStrings; @@ -30,7 +31,11 @@ export const WorkpadLoader = compose( }), connect(mapStateToProps), withState('workpads', 'setWorkpads', null), - withHandlers({ + withKibana, + withProps(({ kibana }) => ({ + notify: kibana.services.canvas.notify, + })), + withHandlers(({ kibana }) => ({ // Workpad creation via navigation createWorkpad: props => async workpad => { // workpad data uploaded, create and load it @@ -39,7 +44,9 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: errors.getUploadFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); } return; } @@ -53,7 +60,7 @@ export const WorkpadLoader = compose( const workpads = await workpadService.find(text); setWorkpads(workpads); } catch (err) { - notify.error(err, { title: errors.getFindFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { title: errors.getFindFailureErrorMessage() }); } }, @@ -69,7 +76,7 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: errors.getCloneFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); } }, @@ -90,7 +97,7 @@ export const WorkpadLoader = compose( return Promise.all(removeWorkpads).then(results => { let redirectHome = false; - const [passes, errors] = results.reduce( + const [passes, errored] = results.reduce( ([passes, errors], result) => { if (result.id === loadedWorkpad && !result.err) { redirectHome = true; @@ -114,8 +121,8 @@ export const WorkpadLoader = compose( workpads: remainingWorkpads, }; - if (errors.length > 0) { - notify.error(errors.getDeleteFailureErrorMessage()); + if (errored.length > 0) { + kibana.services.canvas.notify.error(errors.getDeleteFailureErrorMessage()); } setWorkpads(workpadState); @@ -124,8 +131,14 @@ export const WorkpadLoader = compose( props.router.navigateTo('home'); } - return errors.map(({ id }) => id); + return errored.map(({ id }) => id); }); }, - }) + })), + withProps(props => ({ + formatDate: date => { + const dateFormat = props.kibana.services.uiSettings.get('dateFormat'); + return date && moment(date).format(dateFormat); + }, + })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js index a7fcf7449ce40..fd25fb03a9ca9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js @@ -6,12 +6,11 @@ import { get } from 'lodash'; import { getId } from '../../lib/get_id'; -import { notify } from '../../lib/notify'; import { ErrorStrings } from '../../../i18n'; const { WorkpadFileUpload: errors } = ErrorStrings; -export const uploadWorkpad = (file, onUpload) => { +export const uploadWorkpad = (file, onUpload, notify) => { if (!file) { return; } diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js index ac716d37f532d..ab0c064d5ef07 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js @@ -6,7 +6,6 @@ import PropTypes from 'prop-types'; import { compose, withHandlers } from 'recompose'; -import { notify } from '../../../lib/notify'; import { uploadWorkpad } from '../upload_workpad'; import { ErrorStrings } from '../../../../i18n'; import { WorkpadDropzone as Component } from './workpad_dropzone'; @@ -14,7 +13,7 @@ import { WorkpadDropzone as Component } from './workpad_dropzone'; const { WorkpadFileUpload: errors } = ErrorStrings; export const WorkpadDropzone = compose( - withHandlers({ + withHandlers(({ notify }) => ({ onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload), onDropRejected: () => ([file]) => { notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { @@ -23,7 +22,7 @@ export const WorkpadDropzone = compose( : errors.getFileUploadFailureWithoutFileNameErrorMessage(), }); }, - }) + })) )(Component); WorkpadDropzone.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 30d4ded8571c5..cb5af27144c7f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -20,12 +20,10 @@ import { EuiLink, } from '@elastic/eui'; import { sortByOrder } from 'lodash'; -import moment from 'moment'; import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { Paginate } from '../paginate'; import { ComponentStrings } from '../../../i18n'; -import { getAdvancedSettings } from '../../lib/kibana_advanced_settings'; import { WorkpadDropzone } from './workpad_dropzone'; import { WorkpadCreate } from './workpad_create'; import { WorkpadSearch } from './workpad_search'; @@ -33,8 +31,6 @@ import { uploadWorkpad } from './upload_workpad'; const { WorkpadLoader: strings } = ComponentStrings; -const formatDate = date => date && moment(date).format(getAdvancedSettings().get('dateFormat')); - const getDisplayName = (name, workpad, loadedWorkpad) => { const workpadName = name.length ? name : <em>{workpad.id}</em>; return workpad.id === loadedWorkpad ? <strong>{workpadName}</strong> : workpadName; @@ -51,6 +47,7 @@ export class WorkpadLoader extends React.PureComponent { removeWorkpads: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, workpads: PropTypes.object, + formatDate: PropTypes.func.isRequired, }; state = { @@ -205,7 +202,7 @@ export class WorkpadLoader extends React.PureComponent { sortable: true, dataType: 'date', width: '20%', - render: date => formatDate(date), + render: date => this.props.formatDate(date), }, { field: '@timestamp', @@ -213,7 +210,7 @@ export class WorkpadLoader extends React.PureComponent { sortable: true, dataType: 'date', width: '20%', - render: date => formatDate(date), + render: date => this.props.formatDate(date), }, { name: '', actions, width: '5%' }, ]; @@ -252,7 +249,11 @@ export class WorkpadLoader extends React.PureComponent { return ( <Fragment> - <WorkpadDropzone onUpload={this.onUpload} disabled={createPending || !canUserWrite}> + <WorkpadDropzone + onUpload={this.onUpload} + disabled={createPending || !canUserWrite} + notify={this.props.notify} + > <EuiBasicTable items={rows} itemId="id" @@ -330,7 +331,7 @@ export class WorkpadLoader extends React.PureComponent { compressed className="canvasWorkpad__upload--compressed" initialPromptText={strings.getFilePickerPlaceholder()} - onChange={([file]) => uploadWorkpad(file, this.onUpload)} + onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)} accept="application/json" disabled={createPending || !canUserWrite} /> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js index 139d0f283bf1a..1890ca1f9d2d6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js @@ -7,9 +7,9 @@ import PropTypes from 'prop-types'; import { compose, getContext, withHandlers, withProps } from 'recompose'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; import { getId } from '../../lib/get_id'; import { templatesRegistry } from '../../lib/templates_registry'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { WorkpadTemplates as Component } from './workpad_templates'; export const WorkpadTemplates = compose( @@ -19,7 +19,8 @@ export const WorkpadTemplates = compose( withProps(() => ({ templates: templatesRegistry.toJS(), })), - withHandlers({ + withKibana, + withHandlers(({ kibana }) => ({ // Clone workpad given an id cloneWorkpad: props => workpad => { workpad.id = getId('workpad'); @@ -31,7 +32,9 @@ export const WorkpadTemplates = compose( return workpadService .create(workpad) .then(() => props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 })) - .catch(err => notify.error(err, { title: `Couldn't clone workpad template` })); + .catch(err => + kibana.services.canvas.notify.error(err, { title: `Couldn't clone workpad template` }) + ); }, - }) + })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/functions/index.ts b/x-pack/legacy/plugins/canvas/public/functions/index.ts index 27fb7d83274a4..5e098d8f175c5 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/index.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/index.ts @@ -4,16 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { asset } from './asset'; import { filtersFunctionFactory } from './filters'; -import { timelion } from './timelion'; +import { timelionFunctionFactory } from './timelion'; import { toFunctionFactory } from './to'; +import { CanvasSetupDeps, CoreSetup } from '../plugin'; export interface InitializeArguments { - typesRegistry: ExpressionsSetup['__LEGACY']['types']; + prependBasePath: CoreSetup['http']['basePath']['prepend']; + typesRegistry: CanvasSetupDeps['expressions']['__LEGACY']['types']; + timefilter: CanvasSetupDeps['data']['query']['timefilter']['timefilter']; } export function initFunctions(initialize: InitializeArguments) { - return [asset, filtersFunctionFactory(initialize), timelion, toFunctionFactory(initialize)]; + return [ + asset, + filtersFunctionFactory(initialize), + timelionFunctionFactory(initialize), + toFunctionFactory(initialize), + ]; } diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index ae87e858cf796..e59d798108945 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -6,8 +6,6 @@ import { flatten } from 'lodash'; import moment from 'moment-timezone'; -import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; @@ -15,6 +13,7 @@ import { fetch } from '../../common/lib/fetch'; import { buildBoolArray } from '../../server/lib/build_bool_array'; import { Datatable, Filter } from '../../types'; import { getFunctionHelp } from '../../i18n'; +import { InitializeArguments } from './'; interface Arguments { query: string; @@ -30,13 +29,17 @@ interface Arguments { * @param timeRange time range to parse * @param timeZone time zone to do the parsing in */ -function parseDateMath(timeRange: TimeRange, timeZone: string) { +function parseDateMath( + timeRange: TimeRange, + timeZone: string, + timefilter: InitializeArguments['timefilter'] +) { // the datemath plugin always parses dates by using the current default moment time zone. // to use the configured time zone, we are switching just for the bounds calculation. const defaultTimezone = moment().zoneName(); moment.tz.setDefault(timeZone); - const parsedRange = npStart.plugins.data.query.timefilter.timefilter.calculateBounds(timeRange); + const parsedRange = timefilter.calculateBounds(timeRange); // reset default moment timezone moment.tz.setDefault(defaultTimezone); @@ -44,96 +47,100 @@ function parseDateMath(timeRange: TimeRange, timeZone: string) { return parsedRange; } -export function timelion(): ExpressionFunctionDefinition< +type TimelionFunction = ExpressionFunctionDefinition< 'timelion', Filter, Arguments, Promise<Datatable> -> { - const { help, args: argHelp } = getFunctionHelp().timelion; +>; - return { - name: 'timelion', - type: 'datatable', - inputTypes: ['filter'], - help, - args: { - query: { - types: ['string'], - aliases: ['_', 'q'], - help: argHelp.query, - default: '".es(*)"', - }, - interval: { - types: ['string'], - help: argHelp.interval, - default: 'auto', - }, - from: { - types: ['string'], - help: argHelp.from, - default: 'now-1y', - }, - to: { - types: ['string'], - help: argHelp.to, - default: 'now', - }, - timezone: { - types: ['string'], - help: argHelp.timezone, - default: 'UTC', +export function timelionFunctionFactory(initialize: InitializeArguments): () => TimelionFunction { + return () => { + const { help, args: argHelp } = getFunctionHelp().timelion; + + return { + name: 'timelion', + type: 'datatable', + inputTypes: ['filter'], + help, + args: { + query: { + types: ['string'], + aliases: ['_', 'q'], + help: argHelp.query, + default: '".es(*)"', + }, + interval: { + types: ['string'], + help: argHelp.interval, + default: 'auto', + }, + from: { + types: ['string'], + help: argHelp.from, + default: 'now-1y', + }, + to: { + types: ['string'], + help: argHelp.to, + default: 'now', + }, + timezone: { + types: ['string'], + help: argHelp.timezone, + default: 'UTC', + }, }, - }, - fn: (input, args): Promise<Datatable> => { - // Timelion requires a time range. Use the time range from the timefilter element in the - // workpad, if it exists. Otherwise fall back on the function args. - const timeFilter = input.and.find(and => and.type === 'time'); - const range = timeFilter - ? { min: timeFilter.from, max: timeFilter.to } - : parseDateMath({ from: args.from, to: args.to }, args.timezone); + fn: (input, args): Promise<Datatable> => { + // Timelion requires a time range. Use the time range from the timefilter element in the + // workpad, if it exists. Otherwise fall back on the function args. + const timeFilter = input.and.find(and => and.type === 'time'); + const range = timeFilter + ? { min: timeFilter.from, max: timeFilter.to } + : parseDateMath({ from: args.from, to: args.to }, args.timezone, initialize.timefilter); - const body = { - extended: { - es: { - filter: { - bool: { - must: buildBoolArray(input.and), + const body = { + extended: { + es: { + filter: { + bool: { + must: buildBoolArray(input.and), + }, }, }, }, - }, - sheet: [args.query], - time: { - from: range.min, - to: range.max, - interval: args.interval, - timezone: args.timezone, - }, - }; + sheet: [args.query], + time: { + from: range.min, + to: range.max, + interval: args.interval, + timezone: args.timezone, + }, + }; - return fetch(chrome.addBasePath(`/api/timelion/run`), { - method: 'POST', - responseType: 'json', - data: body, - }).then(resp => { - const seriesList = resp.data.sheet[0].list; - const rows = flatten( - seriesList.map((series: { data: any[]; label: string }) => - series.data.map(row => ({ '@timestamp': row[0], value: row[1], label: series.label })) - ) - ) as DatatableRow[]; + return fetch(initialize.prependBasePath(`/api/timelion/run`), { + method: 'POST', + responseType: 'json', + data: body, + }).then(resp => { + const seriesList = resp.data.sheet[0].list; + const rows = flatten( + seriesList.map((series: { data: any[]; label: string }) => + series.data.map(row => ({ '@timestamp': row[0], value: row[1], label: series.label })) + ) + ) as DatatableRow[]; - return { - type: 'datatable', - columns: [ - { name: '@timestamp', type: 'date' }, - { name: 'value', type: 'number' }, - { name: 'label', type: 'string' }, - ], - rows, - }; - }); - }, + return { + type: 'datatable', + columns: [ + { name: '@timestamp', type: 'date' }, + { name: 'value', type: 'number' }, + { name: 'label', type: 'string' }, + ], + rows, + }; + }); + }, + }; }; } diff --git a/x-pack/legacy/plugins/canvas/public/index.ts b/x-pack/legacy/plugins/canvas/public/index.ts index b8358bfe022e6..b053920fec6e4 100644 --- a/x-pack/legacy/plugins/canvas/public/index.ts +++ b/x-pack/legacy/plugins/canvas/public/index.ts @@ -10,6 +10,7 @@ import { CoreStart, } from '../../../../../src/core/public'; import { CanvasSetup, CanvasStart, CanvasSetupDeps, CanvasStartDeps, CanvasPlugin } from './plugin'; +import { CanvasServices } from './services'; export const plugin: PluginInitializer< CanvasSetup, @@ -22,7 +23,7 @@ export const plugin: PluginInitializer< export interface WithKibanaProps { kibana: { - services: CoreStart & CanvasStartDeps; + services: CoreStart & CanvasStartDeps & { canvas: CanvasServices }; }; } diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index 4af7c9b2bd057..f83887bbcbdfd 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -21,8 +21,10 @@ const shimCoreStart = { }; const shimSetupPlugins: CanvasSetupDeps = { + data: npSetup.plugins.data, expressions: npSetup.plugins.expressions, home: npSetup.plugins.home, + usageCollection: npSetup.plugins.usageCollection, }; const shimStartPlugins: CanvasStartDeps = { ...npStart.plugins, @@ -33,8 +35,6 @@ const shimStartPlugins: CanvasStartDeps = { __LEGACY: { // ToDo: Copy directly into canvas absoluteToParsedUrl, - // ToDo: Copy directly into canvas - formatMsg, // ToDo: Won't be a part of New Platform. Will need to handle internally trackSubUrlForApp: chrome.trackSubUrlForApp, }, diff --git a/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts b/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts index 834d5868c35ea..57b513affd781 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/breadcrumbs.ts @@ -5,7 +5,7 @@ */ import { ChromeBreadcrumb } from '../../../../../../src/core/public'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; export const getBaseBreadcrumb = () => ({ text: 'Canvas', @@ -24,6 +24,6 @@ export const getWorkpadBreadcrumb = ({ }; export const setBreadcrumb = (paths: ChromeBreadcrumb | ChromeBreadcrumb[]) => { - const setBreadCrumbs = getCoreStart().chrome.setBreadcrumbs; + const setBreadCrumbs = platformService.getService().coreStart.chrome.setBreadcrumbs; setBreadCrumbs(Array.isArray(paths) ? paths : [paths]); }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts index 478e2f8f18cf5..8952802dc2f2b 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts @@ -5,15 +5,13 @@ */ import { AxiosPromise } from 'axios'; -// @ts-ignore unconverted local file import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; -// @ts-ignore unconverted local file import { fetch } from '../../common/lib/fetch'; import { CustomElement } from '../../types'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; const getApiPath = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_CUSTOM_ELEMENT}`; }; @@ -25,7 +23,7 @@ export const get = (customElementId: string): Promise<CustomElement> => .get(`${getApiPath()}/${customElementId}`) .then(({ data: element }: { data: CustomElement }) => element); -export const update = (id: string, element: CustomElement): AxiosPromise => +export const update = (id: string, element: Partial<CustomElement>): AxiosPromise => fetch.put(`${getApiPath()}/${id}`, element); export const remove = (id: string): AxiosPromise => fetch.delete(`${getApiPath()}/${id}`); diff --git a/x-pack/legacy/plugins/canvas/public/lib/default_header.png b/x-pack/legacy/plugins/canvas/public/lib/default_header.png deleted file mode 100644 index 0b5c5b8f58f9b..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/public/lib/default_header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts b/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts index 40e09ee39d714..6430f7d87d4f7 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/documentation_links.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; export const getDocumentationLinks = () => ({ - canvas: `${getCoreStart().docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - getCoreStart().docLinks.DOC_LINK_VERSION + canvas: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ + platformService.getService().coreStart.docLinks.DOC_LINK_VERSION }/canvas.html`, - numeral: `${getCoreStart().docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ - getCoreStart().docLinks.DOC_LINK_VERSION + numeral: `${platformService.getService().coreStart.docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${ + platformService.getService().coreStart.docLinks.DOC_LINK_VERSION }/guide/numeral.html`, }); diff --git a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts index e4866641fd9e1..fb038d8b6ace2 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts @@ -6,8 +6,7 @@ import fileSaver from 'file-saver'; import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants'; import { ErrorStrings } from '../../i18n'; -// @ts-ignore untyped local -import { notify } from './notify'; +import { notifyService } from '../services'; // @ts-ignore untyped local import * as workpadService from './workpad_service'; import { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; @@ -20,7 +19,7 @@ export const downloadWorkpad = async (workpadId: string) => { const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' }); fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`); } catch (err) { - notify.error(err, { title: strings.getDownloadFailureErrorMessage() }); + notifyService.getService().error(err, { title: strings.getDownloadFailureErrorMessage() }); } }; @@ -32,7 +31,9 @@ export const downloadRenderedWorkpad = async (renderedWorkpad: CanvasRenderedWor `canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json` ); } catch (err) { - notify.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); } }; @@ -42,7 +43,9 @@ export const downloadRuntime = async (basePath: string) => { window.open(path); return; } catch (err) { - notify.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); } }; @@ -51,6 +54,8 @@ export const downloadZippedRuntime = async (data: any) => { const zip = new Blob([data], { type: 'octet/stream' }); fileSaver.saveAs(zip, 'canvas-workpad-embed.zip'); } catch (err) { - notify.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); } }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/element.ts b/x-pack/legacy/plugins/canvas/public/lib/element.ts index 121c253668ed9..ef1cf601b6e26 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/element.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/element.ts @@ -5,18 +5,16 @@ */ import { ElementSpec } from '../../types'; -import defaultHeader from './default_header.png'; -import { tagsRegistry } from './tags_registry'; export class Element { /** The name of the Element. This must match the name of the function that is used to create the `type: render` object */ public name: string; /** A more friendly name for the Element */ public displayName: string; - /** Relevant labels to help identify the elements */ - public tags: string[]; - /** An image to use in the Element type selector */ - public image: string; + /** The type of the Element */ + public type?: string; + /** The name of the EUI icon to display in the element menu */ + public icon: string; /** A sentence or few about what this Element does */ public help: string; /** A default expression that allows Canvas to render the Element */ @@ -28,23 +26,17 @@ export class Element { public height?: number; constructor(config: ElementSpec) { - const { name, image, displayName, tags, expression, filter, help, width, height } = config; + const { name, icon, displayName, type, expression, filter, help, width, height } = config; this.name = name; this.displayName = displayName || name; - this.image = image || defaultHeader; + this.icon = icon || 'empty'; this.help = help || ''; if (!config.expression) { throw new Error('Element types must have a default expression'); } - this.tags = tags || []; - - this.tags.forEach(tag => { - if (!tagsRegistry.get(tag)) { - tagsRegistry.register(() => ({ name: tag, color: '#666666' })); - } - }); + this.type = type; this.expression = expression; this.filter = filter; this.width = width || 500; diff --git a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts index bce6bc51b366c..a8744b4820842 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Http2ServerResponse } from 'http2'; import { camelCase } from 'lodash'; // @ts-ignore unconverted local file import { getClipboardData, setClipboardData } from './clipboard'; // @ts-ignore unconverted local file import { cloneSubgraphs } from './clone_subgraphs'; -// @ts-ignore unconverted local file -import { notify } from './notify'; +import { notifyService } from '../services'; import * as customElementService from './custom_element_service'; import { getId } from './get_id'; import { PositionedElement } from '../../types'; @@ -86,15 +84,17 @@ export const basicHandlerCreators = { customElementService .create(customElement) .then(() => - notify.success( - `Custom element '${customElement.displayName || customElement.id}' was saved`, - { - 'data-test-subj': 'canvasCustomElementCreate-success', - } - ) + notifyService + .getService() + .success( + `Custom element '${customElement.displayName || customElement.id}' was saved`, + { + 'data-test-subj': 'canvasCustomElementCreate-success', + } + ) ) - .catch((result: Http2ServerResponse) => - notify.warning(result, { + .catch((error: Error) => + notifyService.getService().warning(error, { title: `Custom element '${customElement.displayName || customElement.id}' was not saved`, }) @@ -138,13 +138,13 @@ export const clipboardHandlerCreators = { if (selectedNodes.length) { setClipboardData({ selectedNodes }); removeNodes(selectedNodes.map(extractId), pageId); - notify.success('Cut element to clipboard'); + notifyService.getService().success('Cut element to clipboard'); } }, copyNodes: ({ selectedNodes }: Props) => (): void => { if (selectedNodes.length) { setClipboardData({ selectedNodes }); - notify.success('Copied element to clipboard'); + notifyService.getService().success('Copied element to clipboard'); } }, pasteNodes: ({ insertNodes, pageId, selectToplevelNodes }: Props) => (): void => { diff --git a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts index 32f4fe041423c..184f4f3c8af7c 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts @@ -10,23 +10,22 @@ import { API_ROUTE } from '../../common/lib/constants'; // @ts-ignore untyped local import { fetch } from '../../common/lib/fetch'; import { ErrorStrings } from '../../i18n'; -// @ts-ignore untyped local -import { notify } from './notify'; -import { getCoreStart } from '../legacy'; +import { notifyService } from '../services'; +import { platformService } from '../services'; const { esService: strings } = ErrorStrings; const getApiPath = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return basePath + API_ROUTE; }; const getSavedObjectsClient = function() { - return getCoreStart().savedObjects.client; + return platformService.getService().coreStart.savedObjects.client; }; const getAdvancedSettings = function() { - return getCoreStart().uiSettings; + return platformService.getService().coreStart.uiSettings; }; export const getFields = (index = '_all') => { @@ -38,7 +37,7 @@ export const getFields = (index = '_all') => { .sort() ) .catch((err: Error) => - notify.error(err, { + notifyService.getService().error(err, { title: strings.getFieldsFetchErrorMessage(index), }) ); @@ -57,7 +56,9 @@ export const getIndices = () => return savedObject.attributes.title; }); }) - .catch((err: Error) => notify.error(err, { title: strings.getIndicesFetchErrorMessage() })); + .catch((err: Error) => + notifyService.getService().error(err, { title: strings.getIndicesFetchErrorMessage() }) + ); export const getDefaultIndex = () => { const defaultIndexId = getAdvancedSettings().get('defaultIndex'); @@ -66,6 +67,10 @@ export const getDefaultIndex = () => { ? getSavedObjectsClient() .get<IndexPatternAttributes>('index-pattern', defaultIndexId) .then(defaultIndex => defaultIndex.attributes.title) - .catch(err => notify.error(err, { title: strings.getDefaultIndexFetchErrorMessage() })) + .catch(err => + notifyService + .getService() + .error(err, { title: strings.getDefaultIndexFetchErrorMessage() }) + ) : Promise.resolve(''); }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts b/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts new file mode 100644 index 0000000000000..a059d07725727 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +// TODO: Fix all of this magic from EUI; this code is boilerplate from +// EUI examples and isn't easily typed. +export const flattenPanelTree = (tree: any, array: any[] = []) => { + array.push(tree); + + if (tree.items) { + tree.items.forEach((item: any) => { + const { panel } = item; + if (panel) { + flattenPanelTree(panel, array); + item.panel = panel.id; + } + }); + } + + return array; +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts b/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts deleted file mode 100644 index f57f3188a8184..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts +++ /dev/null @@ -1,9 +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 { getCoreStart } from '../legacy'; - -export const getAdvancedSettings = () => getCoreStart().uiSettings; diff --git a/x-pack/legacy/plugins/canvas/public/lib/notify.js b/x-pack/legacy/plugins/canvas/public/lib/notify.js deleted file mode 100644 index 64876a02a3c64..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/notify.js +++ /dev/null @@ -1,52 +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 { get } from 'lodash'; -import { getCoreStart, getStartPlugins } from '../legacy'; - -const getToastNotifications = function() { - return getCoreStart().notifications.toasts; -}; - -const formatMsg = function(...args) { - return getStartPlugins().__LEGACY.formatMsg(...args); -}; - -const getToast = (err, opts = {}) => { - const errData = get(err, 'response') || err; - const errMsg = formatMsg(errData); - const { title, ...rest } = opts; - let text = null; - - if (title) { - text = errMsg; - } - - return { - ...rest, - title: title || errMsg, - text, - }; -}; - -export const notify = { - /* - * @param {(string | Object)} err: message or Error object - * @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md - */ - error(err, opts) { - getToastNotifications().addDanger(getToast(err, opts)); - }, - warning(err, opts) { - getToastNotifications().addWarning(getToast(err, opts)); - }, - info(err, opts) { - getToastNotifications().add(getToast(err, opts)); - }, - success(err, opts) { - getToastNotifications().addSuccess(getToast(err, opts)); - }, -}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts index fbbaf0ccf280e..df338f40e08d9 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts @@ -6,8 +6,7 @@ import { fromExpression, getType } from '@kbn/interpreter/common'; import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; -// @ts-ignore Untyped Local -import { notify } from './notify'; +import { notifyService } from '../services'; import { CanvasStartDeps, CanvasSetupDeps } from '../plugin'; @@ -85,7 +84,7 @@ export async function runInterpreter( throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); } catch (err) { - notify.error(err); + notifyService.getService().error(err); throw err; } } diff --git a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts index 33976a147df46..2a1a4b88b7264 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts @@ -4,10 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -export const trackCanvasUiMetric = createUiStatsReporter('canvas'); export { METRIC_TYPE }; + +export let reportUiStats: UsageCollectionSetup['reportUiStats'] | undefined; + +export function init(_reportUiStats: UsageCollectionSetup['reportUiStats']): void { + reportUiStats = _reportUiStats; +} + +export function trackCanvasUiMetric(metricType: UiStatsMetricType, name: string | string[]) { + if (!reportUiStats) { + return; + } + + reportUiStats('canvas', metricType, name); +} diff --git a/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js b/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js index bf97928452c71..75307816b4371 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js +++ b/x-pack/legacy/plugins/canvas/public/lib/window_error_handler.js @@ -47,9 +47,16 @@ window.canvasInitErrorHandler = () => { window.onerror = (...args) => { const [message, , , , err] = args; - const isKnownError = Object.keys(knownErrors).find(errorName => { - return err.constructor.name === errorName || message.indexOf(errorName) >= 0; - }); + // ResizeObserver error does not have an `err` object + // It is thrown during workpad loading due to layout thrashing + // https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded + // https://github.com/elastic/eui/issues/3346 + console.log(message); + const isKnownError = + message.includes('ResizeObserver loop') || + Object.keys(knownErrors).find(errorName => { + return err.constructor.name === errorName || message.indexOf(errorName) >= 0; + }); if (isKnownError) { return; } diff --git a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js index f3681f50c56a5..e6628399f53c2 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js +++ b/x-pack/legacy/plugins/canvas/public/lib/workpad_service.js @@ -11,7 +11,7 @@ import { DEFAULT_WORKPAD_CSS, } from '../../common/lib/constants'; import { fetch } from '../../common/lib/fetch'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; /* Remove any top level keys from the workpad which will be rejected by validation */ @@ -43,17 +43,17 @@ const sanitizeWorkpad = function(workpad) { }; const getApiPath = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD}`; }; const getApiPathStructures = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD_STRUCTURES}`; }; const getApiPathAssets = function() { - const basePath = getCoreStart().http.basePath.get(); + const basePath = platformService.getService().coreStart.http.basePath.get(); return `${basePath}${API_ROUTE_WORKPAD_ASSETS}`; }; diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 3ea3ce625ca71..baeb4ebd453d2 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -10,8 +10,10 @@ import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { initLoadingIndicator } from './lib/loading_indicator'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; // @ts-ignore untyped local import { argTypeSpecs } from './expression_types/arg_types'; @@ -20,7 +22,7 @@ import { legacyRegistries } from './legacy_plugin_support'; import { getPluginApi, CanvasApi } from './plugin_api'; import { initFunctions } from './functions'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; -export { CoreStart }; +export { CoreStart, CoreSetup }; /** * These are the private interfaces for the services your plugin depends on. @@ -28,18 +30,20 @@ export { CoreStart }; */ // This interface will be built out as we require other plugins for setup export interface CanvasSetupDeps { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; home: HomePublicPluginSetup; + usageCollection?: UsageCollectionSetup; } export interface CanvasStartDeps { embeddable: EmbeddableStart; expressions: ExpressionsStart; inspector: InspectorStart; + uiActions: UiActionsStart; __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; - formatMsg: any; trackSubUrlForApp: Chrome['trackSubUrlForApp']; }; } @@ -59,6 +63,7 @@ export class CanvasPlugin implements Plugin<CanvasSetup, CanvasStart, CanvasSetupDeps, CanvasStartDeps> { // TODO: Do we want to completely move canvas_plugin_src into it's own plugin? private srcPlugin = new CanvasSrcPlugin(); + private startPlugins: CanvasStartDeps | undefined; public setup(core: CoreSetup<CanvasStartDeps>, plugins: CanvasSetupDeps) { const { api: canvasApi, registries } = getPluginApi(plugins.expressions); @@ -68,14 +73,26 @@ export class CanvasPlugin core.application.register({ id: 'canvas', title: 'Canvas App', - async mount(context, params) { + mount: async (context, params) => { // Load application bundle const { renderApp, initializeCanvas, teardownCanvas } = await import('./application'); // Get start services const [coreStart, depsStart] = await core.getStartServices(); - const canvasStore = await initializeCanvas(core, coreStart, plugins, depsStart, registries); + // TODO: We only need this to get the __LEGACY stuff that isn't coming from getStartSevices. + // We won't need this as soon as we move over to NP Completely + if (!this.startPlugins) { + throw new Error('Start Plugins not ready at mount time'); + } + + const canvasStore = await initializeCanvas( + core, + coreStart, + plugins, + this.startPlugins, + registries + ); const unmount = renderApp(coreStart, depsStart, params, canvasStore); @@ -94,7 +111,13 @@ export class CanvasPlugin canvasApi.addTypes(legacyRegistries.types.getOriginalFns()); // Register core canvas stuff - canvasApi.addFunctions(initFunctions({ typesRegistry: plugins.expressions.__LEGACY.types })); + canvasApi.addFunctions( + initFunctions({ + timefilter: plugins.data.query.timefilter.timefilter, + prependBasePath: core.http.basePath.prepend, + typesRegistry: plugins.expressions.__LEGACY.types, + }) + ); canvasApi.addArgumentUIs(argTypeSpecs); canvasApi.addTransitions(transitions); @@ -104,6 +127,7 @@ export class CanvasPlugin } public start(core: CoreStart, plugins: CanvasStartDeps) { + this.startPlugins = plugins; this.srcPlugin.start(core, plugins); initLoadingIndicator(core.http.addLoadingCountSource); } diff --git a/x-pack/legacy/plugins/canvas/public/services/index.ts b/x-pack/legacy/plugins/canvas/public/services/index.ts new file mode 100644 index 0000000000000..17d836f1441c9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/index.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, CoreStart } from '../../../../../../src/core/public'; +import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; +import { notifyServiceFactory } from './notify'; +import { platformServiceFactory } from './platform'; + +export type CanvasServiceFactory<Service> = ( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps +) => Service; + +class CanvasServiceProvider<Service> { + private factory: CanvasServiceFactory<Service>; + private service: Service | undefined; + + constructor(factory: CanvasServiceFactory<Service>) { + this.factory = factory; + } + + start( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps + ) { + this.service = this.factory(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins); + } + + getService(): Service { + if (!this.service) { + throw new Error('Service not ready'); + } + + return this.service; + } + + stop() { + this.service = undefined; + } +} + +export type ServiceFromProvider<P> = P extends CanvasServiceProvider<infer T> ? T : never; + +export const services = { + notify: new CanvasServiceProvider(notifyServiceFactory), + platform: new CanvasServiceProvider(platformServiceFactory), +}; + +export interface CanvasServices { + notify: ServiceFromProvider<typeof services.notify>; +} + +export const startServices = ( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps +) => { + Object.entries(services).forEach(([key, provider]) => + provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins) + ); +}; + +export const stopServices = () => { + Object.entries(services).forEach(([key, provider]) => provider.stop()); +}; + +export const { notify: notifyService, platform: platformService } = services; diff --git a/x-pack/legacy/plugins/canvas/public/services/notify.ts b/x-pack/legacy/plugins/canvas/public/services/notify.ts new file mode 100644 index 0000000000000..3e18e2178a818 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/notify.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { CanvasServiceFactory } from '.'; +import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; +import { ToastInputFields } from '../../../../../../src/core/public'; + +const getToast = (err: Error | string, opts: ToastInputFields = {}) => { + const errData = (get(err, 'response') || err) as Error | string; + const errMsg = formatMsg(errData); + const { title, ...rest } = opts; + let text; + + if (title) { + text = errMsg; + } + + return { + ...rest, + title: title || errMsg, + text, + }; +}; + +interface NotifyService { + error: (err: string | Error, opts?: ToastInputFields) => void; + warning: (err: string | Error, opts?: ToastInputFields) => void; + info: (err: string | Error, opts?: ToastInputFields) => void; + success: (err: string | Error, opts?: ToastInputFields) => void; +} + +export const notifyServiceFactory: CanvasServiceFactory<NotifyService> = (setup, start) => { + const toasts = start.notifications.toasts; + + return { + /* + * @param {(string | Object)} err: message or Error object + * @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md + */ + error(err, opts) { + toasts.addDanger(getToast(err, opts)); + }, + warning(err, opts) { + toasts.addWarning(getToast(err, opts)); + }, + info(err, opts) { + toasts.add(getToast(err, opts)); + }, + success(err, opts) { + toasts.addSuccess(getToast(err, opts)); + }, + }; +}; diff --git a/x-pack/legacy/plugins/canvas/public/services/platform.ts b/x-pack/legacy/plugins/canvas/public/services/platform.ts new file mode 100644 index 0000000000000..440e9523044c1 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/platform.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CanvasServiceFactory } from '.'; +import { CoreStart, CoreSetup, CanvasSetupDeps, CanvasStartDeps } from '../plugin'; + +interface PlatformService { + coreSetup: CoreSetup; + coreStart: CoreStart; + setupPlugins: CanvasSetupDeps; + startPlugins: CanvasStartDeps; +} + +export const platformServiceFactory: CanvasServiceFactory<PlatformService> = ( + coreSetup, + coreStart, + setupPlugins, + startPlugins +) => { + return { coreSetup, coreStart, setupPlugins, startPlugins }; +}; diff --git a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js index 1798aaab22f06..f4a3393b8962d 100644 --- a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js @@ -13,9 +13,9 @@ import { getPages, getNodeById, getNodes, getSelectedPageIndex } from '../select import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; import { ErrorStrings } from '../../../i18n'; -import { notify } from '../../lib/notify'; import { runInterpreter, interpretAst } from '../../lib/run_interpreter'; import { subMultitree } from '../../lib/aeroelastic/functional'; +import { services } from '../../services'; import { selectToplevelNodes } from './transient'; import * as args from './resolved_args'; @@ -134,7 +134,7 @@ const fetchRenderableWithContextFn = ({ dispatch }, element, ast, context) => { dispatch(getAction(renderable)); }) .catch(err => { - notify.error(err); + services.notify.getService().error(err); dispatch(getAction(err)); }); }; @@ -176,7 +176,7 @@ export const fetchAllRenderables = createThunk( return runInterpreter(ast, null, { castToRender: true }) .then(renderable => ({ path: argumentPath, value: renderable })) .catch(err => { - notify.error(err); + services.notify.getService().error(err); return { path: argumentPath, value: err }; }); }); @@ -293,7 +293,7 @@ const setAst = createThunk('setAst', ({ dispatch }, ast, element, pageId, doRend const expression = toExpression(ast); dispatch(setExpression(expression, element.id, pageId, doRender)); } catch (err) { - notify.error(err); + services.notify.getService().error(err); // TODO: remove this, may have been added just to cause a re-render, but why? dispatch(setExpression(element.expression, element.id, pageId, doRender)); diff --git a/x-pack/legacy/plugins/canvas/public/state/initial_state.js b/x-pack/legacy/plugins/canvas/public/state/initial_state.js index 40c017543147f..bfa68b33908e0 100644 --- a/x-pack/legacy/plugins/canvas/public/state/initial_state.js +++ b/x-pack/legacy/plugins/canvas/public/state/initial_state.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { getCoreStart } from '../legacy'; +import { platformService } from '../services'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = path => { @@ -13,7 +13,7 @@ export const getInitialState = path => { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - canUserWrite: getCoreStart().application.capabilities.canvas.save, + canUserWrite: platformService.getService().coreStart.application.capabilities.canvas.save, zoomScale: 1, elementStats: { total: 0, diff --git a/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js b/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js index bcbfc3544981a..a197cdf893244 100644 --- a/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js +++ b/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js @@ -14,7 +14,7 @@ import { setAssets, resetAssets } from '../actions/assets'; import * as transientActions from '../actions/transient'; import * as resolvedArgsActions from '../actions/resolved_args'; import { update, updateAssets, updateWorkpad } from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; +import { services } from '../../services'; import { canUserWrite } from '../selectors/app'; const { esPersist: strings } = ErrorStrings; @@ -62,15 +62,15 @@ export const esPersistMiddleware = ({ getState }) => { const statusCode = err.response && err.response.status; switch (statusCode) { case 400: - return notify.error(err.response, { + return services.notify.getService().error(err.response, { title: strings.getSaveFailureTitle(), }); case 413: - return notify.error(strings.getTooLargeErrorMessage(), { + return services.notify.getService().error(strings.getTooLargeErrorMessage(), { title: strings.getSaveFailureTitle(), }); default: - return notify.error(err, { + return services.notify.getService().error(err, { title: strings.getUpdateFailureTitle(), }); } diff --git a/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js b/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js index 12733680ed32d..30f9c638a054f 100644 --- a/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js +++ b/x-pack/legacy/plugins/canvas/public/state/reducers/workpad.js @@ -5,7 +5,7 @@ */ import { handleActions } from 'redux-actions'; -import { getCoreStart } from '../../legacy'; +import { platformService } from '../../services'; import { getDefaultWorkpad } from '../defaults'; import { setWorkpad, @@ -22,11 +22,13 @@ import { APP_ROUTE_WORKPAD } from '../../../common/lib/constants'; export const workpadReducer = handleActions( { [setWorkpad]: (workpadState, { payload }) => { - getCoreStart().chrome.recentlyAccessed.add( - `${APP_ROUTE_WORKPAD}/${payload.id}`, - payload.name, - payload.id - ); + platformService + .getService() + .coreStart.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${payload.id}`, + payload.name, + payload.id + ); return payload; }, @@ -39,11 +41,13 @@ export const workpadReducer = handleActions( }, [setName]: (workpadState, { payload }) => { - getCoreStart().chrome.recentlyAccessed.add( - `${APP_ROUTE_WORKPAD}/${workpadState.id}`, - payload, - workpadState.id - ); + platformService + .getService() + .coreStart.chrome.recentlyAccessed.add( + `${APP_ROUTE_WORKPAD}/${workpadState.id}`, + payload, + workpadState.id + ); return { ...workpadState, name: payload }; }, diff --git a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts index 84fab0cb0ae6d..80a7c34e8bef5 100644 --- a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts @@ -11,7 +11,12 @@ import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/comm import { append } from '../../lib/modify_path'; import { getAssets } from './assets'; import { State, CanvasWorkpad, CanvasPage, CanvasElement, ResolvedArgType } from '../../../types'; -import { ExpressionContext, CanvasGroup, PositionedElement } from '../../../types'; +import { + ExpressionContext, + CanvasGroup, + PositionedElement, + CanvasWorkpadBoundingBox, +} from '../../../types'; import { ExpressionAstArgument, ExpressionAstFunction, @@ -91,7 +96,7 @@ export function getWorkpadWidth(state: State): number { return get(state, append(workpadRoot, 'width')); } -export function getWorkpadBoundingBox(state: State) { +export function getWorkpadBoundingBox(state: State): CanvasWorkpadBoundingBox { return getPages(state).reduce( (boundingBox, page) => { page.elements.forEach(({ position }) => { @@ -358,7 +363,11 @@ export function getNodesForPage(page: CanvasPage, withAst: boolean): CanvasEleme } // todo unify or DRY up with `getElements` -export function getNodes(state: State, pageId: string, withAst = true): CanvasElement[] { +export function getNodes( + state: State, + pageId: string, + withAst = true +): CanvasElement[] | PositionedElement[] { const id = pageId || getSelectedPage(state); if (!id) { return []; diff --git a/x-pack/legacy/plugins/canvas/public/style/index.scss b/x-pack/legacy/plugins/canvas/public/style/index.scss index 56f9ed8d18cbe..7b4e1271cca1d 100644 --- a/x-pack/legacy/plugins/canvas/public/style/index.scss +++ b/x-pack/legacy/plugins/canvas/public/style/index.scss @@ -51,8 +51,9 @@ @import '../components/toolbar/tray/tray'; @import '../components/tooltip_annotation/tooltip_annotation'; @import '../components/workpad/workpad'; -@import '../components/workpad_header/control_settings/control_settings'; -@import '../components/workpad_header/workpad_export/workpad_export'; +@import '../components/workpad_header/element_menu/element_menu'; +@import '../components/workpad_header/share_menu/share_menu'; +@import '../components/workpad_header/view_menu/view_menu'; @import '../components/workpad_loader/workpad_loader'; @import '../components/workpad_loader/workpad_dropzone/workpad_dropzone'; @import '../components/workpad_page/workpad_page'; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts index b05379df6b0b1..0780ab46cd873 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import 'core-js/stable'; +import 'regenerator-runtime/runtime'; import 'whatwg-fetch'; -import 'babel-polyfill'; export * from './shareable'; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx index 628632a753693..1650cbad3a237 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/autoplay_settings.tsx @@ -13,7 +13,7 @@ import { } from '../../../context'; import { createTimeInterval } from '../../../../public/lib/time_interval'; // @ts-ignore Untyped local -import { CustomInterval } from '../../../../public/components/workpad_header/control_settings/custom_interval'; +import { CustomInterval } from '../../../../public/components/workpad_header/view_menu/custom_interval'; export type onSetAutoplayFn = (autoplay: boolean) => void; export type onSetIntervalFn = (interval: string) => void; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json b/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json index f9f999440c7ce..b725afab2b10f 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json @@ -28878,7 +28878,7 @@ "type": "render", "as": "markdown", "value": { - "content": "```\nexport const githubLimitGauge = () => ({\n name: 'githubLimitGauge',\n displayName: 'Github Limit Gauge',\n help: 'A progress pill displaying...',\n image: header,\n expression: `github_rate_limit \n | filterrows fn={getCell name | eq core} \n | if \n condition={math limit | eq 0} \n then=0 \n else={math \"remaining / limit\"}\n | progress \n label=\"Core\"\n shape=\"horizontalPill\" \n | render\n `,\n };\n}\n```", + "content": "```\nexport const githubLimitGauge = () => ({\n name: 'githubLimitGauge',\n displayName: 'Github Limit Gauge',\n help: 'A progress pill displaying...',\n expression: `github_rate_limit \n | filterrows fn={getCell name | eq core} \n | if \n condition={math limit | eq 0} \n then=0 \n else={math \"remaining / limit\"}\n | progress \n label=\"Core\"\n shape=\"horizontalPill\" \n | render\n `,\n };\n}\n```", "font": { "type": "style", "spec": { @@ -28919,7 +28919,7 @@ "type": "render", "as": "markdown", "value": { - "content": "```\nexport function randomNumber() {\n return {\n name: 'randomNumber',\n displayName: 'Random Number',\n help: 'A random number between 0 and 1.',\n image: header,\n expression: \n 'random \n | math \"round(value, 3)\" \n | metric \"Random Number\"\n ',\n };\n}\n```", + "content": "```\nexport function randomNumber() {\n return {\n name: 'randomNumber',\n displayName: 'Random Number',\n help: 'A random number between 0 and 1.',\n expression: \n 'random \n | math \"round(value, 3)\" \n | metric \"Random Number\"\n ',\n };\n}\n```", "font": { "type": "style", "spec": { diff --git a/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js b/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js new file mode 100644 index 0000000000000..3162638cb6c5d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const testCustomElements = [ + { + id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', + name: 'customElement1', + displayName: 'Custom Element 1', + help: 'sample description', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"element-3383b40a-de5d-4efb-8719-f4d8cffbfa74\",\"position\":{\"left\":142,\"top\":146,\"width\":700,\"height\":300,\"angle\":0,\"parent\":null,\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| pointseries x=\\\"project\\\" y=\\\"sum(price)\\\" color=\\\"state\\\" size=\\\"size(username)\\\"\\n| plot defaultStyle={seriesStyle points=5 fill=1}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"pointseries\",\"arguments\":{\"x\":[\"project\"],\"y\":[\"sum(price)\"],\"color\":[\"state\"],\"size\":[\"size(username)\"]}},{\"type\":\"function\",\"function\":\"plot\",\"arguments\":{\"defaultStyle\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"seriesStyle\",\"arguments\":{\"points\":[5],\"fill\":[1]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-b22d8d10-6116-46fb-9b46-c3f3340d3aaa', + name: 'customElement2', + displayName: 'Custom Element 2', + help: 'Aenean eu justo auctor, placerat felis non, scelerisque dolor. ', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-', + name: 'customElement3', + displayName: 'Custom Element 3', + help: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, +]; + +export const create = () => {}; + +export const get = () => {}; + +export const update = () => {}; + +export const remove = () => {}; + +export const find = () => {}; diff --git a/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js b/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js new file mode 100644 index 0000000000000..c7e7088812148 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js @@ -0,0 +1,7 @@ +/* + * 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 const trackCanvasUiMetric = () => {}; diff --git a/x-pack/legacy/plugins/canvas/types/canvas.ts b/x-pack/legacy/plugins/canvas/types/canvas.ts index f0137479a0b7f..0250b921aadb6 100644 --- a/x-pack/legacy/plugins/canvas/types/canvas.ts +++ b/x-pack/legacy/plugins/canvas/types/canvas.ts @@ -56,3 +56,10 @@ export type CanvasTemplate = CanvasWorkpad & { help: string; tags: string[]; }; + +export interface CanvasWorkpadBoundingBox { + left: number; + right: number; + top: number; + bottom: number; +} diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index acb1cb9cd7625..5de6b4968545f 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -9,10 +9,10 @@ import { CanvasElement } from '.'; export interface ElementSpec { name: string; - image: string; + icon?: string; expression: string; displayName?: string; - tags?: string[]; + type?: string; help?: string; filter?: string; width?: number; @@ -42,10 +42,6 @@ export interface CustomElement { * base 64 data URL string of the preview image */ image?: string; - /** - * tags associated with the element - */ - tags?: string[]; /** * the element object stringified */ @@ -79,4 +75,8 @@ export interface ElementPosition { parent: string | null; } -export type PositionedElement = CanvasElement & { ast: ExpressionAstExpression }; +export type PositionedElement = CanvasElement & { + ast: ExpressionAstExpression; +} & { + position: ElementPosition; +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js deleted file mode 100644 index cfa37ff2e0358..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js +++ /dev/null @@ -1,128 +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 '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { FollowerIndexForm } from '../../public/np_ready/app/components/follower_index_form/follower_index_form'; -import { FOLLOWER_INDEX_EDIT } from './helpers/constants'; - -jest.mock('ui/new_platform'); - -const { setup } = pageHelpers.followerIndexEdit; -const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd; - -describe('Edit Auto-follow pattern', () => { - let server; - let httpRequestsMockHelpers; - - beforeAll(() => { - ({ server, httpRequestsMockHelpers } = setupEnvironment()); - }); - - afterAll(() => { - server.restore(); - }); - - describe('on component mount', () => { - let find; - let component; - - const remoteClusters = [{ name: 'new-york', seeds: ['localhost:123'], isConnected: true }]; - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); - httpRequestsMockHelpers.setGetFollowerIndexResponse(FOLLOWER_INDEX_EDIT); - ({ component, find } = setup()); - - await nextTick(); - component.update(); - }); - - /** - * As the "edit" follower index component uses the same form underneath that - * the "create" follower index, we won't test it again but simply make sure that - * the form component is indeed shared between the 2 app sections. - */ - test('should use the same Form component as the "<FollowerIndexAdd />" component', async () => { - const { component: addFollowerIndexComponent } = setupFollowerIndexAdd(); - - await nextTick(); - addFollowerIndexComponent.update(); - - const formEdit = component.find(FollowerIndexForm); - const formAdd = addFollowerIndexComponent.find(FollowerIndexForm); - - expect(formEdit.length).toBe(1); - expect(formAdd.length).toBe(1); - }); - - test('should populate the form fields with the values from the follower index loaded', () => { - const inputToPropMap = { - remoteClusterInput: 'remoteCluster', - leaderIndexInput: 'leaderIndex', - followerIndexInput: 'name', - maxReadRequestOperationCountInput: 'maxReadRequestOperationCount', - maxOutstandingReadRequestsInput: 'maxOutstandingReadRequests', - maxReadRequestSizeInput: 'maxReadRequestSize', - maxWriteRequestOperationCountInput: 'maxWriteRequestOperationCount', - maxWriteRequestSizeInput: 'maxWriteRequestSize', - maxOutstandingWriteRequestsInput: 'maxOutstandingWriteRequests', - maxWriteBufferCountInput: 'maxWriteBufferCount', - maxWriteBufferSizeInput: 'maxWriteBufferSize', - maxRetryDelayInput: 'maxRetryDelay', - readPollTimeoutInput: 'readPollTimeout', - }; - - Object.entries(inputToPropMap).forEach(([input, prop]) => { - const expected = FOLLOWER_INDEX_EDIT[prop]; - const { value } = find(input).props(); - try { - expect(value).toBe(expected); - } catch { - throw new Error( - `Input "${input}" does not equal "${expected}". (Value received: "${value}")` - ); - } - }); - }); - }); - - describe('when the remote cluster is disconnected', () => { - let find; - let exists; - let component; - let actions; - let form; - - beforeEach(async () => { - httpRequestsMockHelpers.setLoadRemoteClustersResponse([ - { name: 'new-york', seeds: ['localhost:123'], isConnected: false }, - ]); - httpRequestsMockHelpers.setGetFollowerIndexResponse(FOLLOWER_INDEX_EDIT); - ({ component, find, exists, actions, form } = setup()); - - await nextTick(); - component.update(); - }); - - test('should display an error and have a button to edit the remote cluster', () => { - const error = find('remoteClusterFormField.notConnectedError'); - - expect(error.length).toBe(1); - expect(error.find('.euiCallOutHeader__title').text()).toBe( - `Can't edit follower index because remote cluster '${FOLLOWER_INDEX_EDIT.remoteCluster}' is not connected` - ); - expect(exists('remoteClusterFormField.notConnectedError.editButton')).toBe(true); - }); - - test('should prevent saving the form and display an error message for the required remote cluster', () => { - actions.clickSaveForm(); - - expect(form.getErrorsMessages()).toEqual(['A connected remote cluster is required.']); - expect(find('submitButton').props().disabled).toBe(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js deleted file mode 100644 index 664ad909ba8e7..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js +++ /dev/null @@ -1,22 +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 { registerTestBed } from '../../../../../../test_utils'; -import { CrossClusterReplicationHome } from '../../../public/np_ready/app/sections/home/home'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; -import { BASE_PATH } from '../../../common/constants'; - -const testBedConfig = { - store: ccrStore, - memoryRouter: { - initialEntries: [`${BASE_PATH}/follower_indices`], - componentRoutePath: `${BASE_PATH}/:section`, - onRouter: router => (routing.reactRouter = router), - }, -}; - -export const setup = registerTestBed(CrossClusterReplicationHome, testBedConfig); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts deleted file mode 100644 index 4ce0a2f5644f3..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts +++ /dev/null @@ -1,10 +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. - */ - -export const APPS = { - CCR_APP: 'ccr', - REMOTE_CLUSTER_APP: 'remote_cluster', -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts deleted file mode 100644 index 0a948793e07db..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts +++ /dev/null @@ -1,11 +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. - */ - -export const BASE_PATH = '/management/elasticsearch/cross_cluster_replication'; -export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters'; -export const API_BASE_PATH = '/api/cross_cluster_replication'; -export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters'; -export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts deleted file mode 100644 index 300afb4e2d2ff..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export * from './plugin'; -export * from './base_path'; -export * from './app'; -export * from './settings'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts deleted file mode 100644 index bd5bb50514c01..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts +++ /dev/null @@ -1,9 +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. - */ - -export const PLUGIN = { - ID: 'cross_cluster_replication', -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts deleted file mode 100644 index 0993a74c8f1fd..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts +++ /dev/null @@ -1,18 +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. - */ - -export const FOLLOWER_INDEX_ADVANCED_SETTINGS = { - maxReadRequestOperationCount: 5120, - maxOutstandingReadRequests: 12, - maxReadRequestSize: '32mb', - maxWriteRequestOperationCount: 5120, - maxWriteRequestSize: '9223372036854775807b', - maxOutstandingWriteRequests: 9, - maxWriteBufferCount: 2147483647, - maxWriteBufferSize: '512mb', - maxRetryDelay: '500ms', - readPollTimeout: '1m', -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap deleted file mode 100644 index d001459e8234d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap +++ /dev/null @@ -1,128 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = ` -Object { - "leaderIndex": undefined, - "maxOutstandingReadRequests": undefined, - "maxOutstandingWriteRequests": undefined, - "maxReadRequestOperationCount": undefined, - "maxReadRequestSize": undefined, - "maxRetryDelay": undefined, - "maxWriteBufferCount": undefined, - "maxWriteBufferSize": undefined, - "maxWriteRequestOperationCount": undefined, - "maxWriteRequestSize": undefined, - "name": undefined, - "readPollTimeout": undefined, - "remoteCluster": undefined, - "shards": Array [ - Object { - "bytesReadCount": undefined, - "failedReadRequestsCount": undefined, - "failedWriteRequestsCount": undefined, - "followerGlobalCheckpoint": undefined, - "followerMappingVersion": undefined, - "followerMaxSequenceNum": undefined, - "followerSettingsVersion": undefined, - "id": "shard 1", - "lastRequestedSequenceNum": undefined, - "leaderGlobalCheckpoint": undefined, - "leaderIndex": undefined, - "leaderMaxSequenceNum": undefined, - "operationsReadCount": undefined, - "operationsWrittenCount": undefined, - "outstandingReadRequestsCount": undefined, - "outstandingWriteRequestsCount": undefined, - "readExceptions": undefined, - "remoteCluster": undefined, - "successfulReadRequestCount": undefined, - "successfulWriteRequestsCount": undefined, - "timeSinceLastReadMs": undefined, - "totalReadRemoteExecTimeMs": undefined, - "totalReadTimeMs": undefined, - "totalWriteTimeMs": undefined, - "writeBufferOperationsCount": undefined, - "writeBufferSizeBytes": undefined, - }, - Object { - "bytesReadCount": undefined, - "failedReadRequestsCount": undefined, - "failedWriteRequestsCount": undefined, - "followerGlobalCheckpoint": undefined, - "followerMappingVersion": undefined, - "followerMaxSequenceNum": undefined, - "followerSettingsVersion": undefined, - "id": "shard 2", - "lastRequestedSequenceNum": undefined, - "leaderGlobalCheckpoint": undefined, - "leaderIndex": undefined, - "leaderMaxSequenceNum": undefined, - "operationsReadCount": undefined, - "operationsWrittenCount": undefined, - "outstandingReadRequestsCount": undefined, - "outstandingWriteRequestsCount": undefined, - "readExceptions": undefined, - "remoteCluster": undefined, - "successfulReadRequestCount": undefined, - "successfulWriteRequestsCount": undefined, - "timeSinceLastReadMs": undefined, - "totalReadRemoteExecTimeMs": undefined, - "totalReadTimeMs": undefined, - "totalWriteTimeMs": undefined, - "writeBufferOperationsCount": undefined, - "writeBufferSizeBytes": undefined, - }, - ], - "status": "active", -} -`; - -exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = ` -Object { - "bytesReadCount": "bytes read", - "failedReadRequestsCount": "failed read requests", - "failedWriteRequestsCount": "failed write requests", - "followerGlobalCheckpoint": "follower global checkpoint", - "followerMappingVersion": "follower mapping version", - "followerMaxSequenceNum": "follower max seq no", - "followerSettingsVersion": "follower settings version", - "id": "shard id", - "lastRequestedSequenceNum": "last requested seq no", - "leaderGlobalCheckpoint": "leader global checkpoint", - "leaderIndex": "leader index", - "leaderMaxSequenceNum": "leader max seq no", - "operationsReadCount": "operations read", - "operationsWrittenCount": "operations written", - "outstandingReadRequestsCount": "outstanding read requests", - "outstandingWriteRequestsCount": "outstanding write requests", - "readExceptions": Array [ - "read exception", - ], - "remoteCluster": "remote cluster", - "successfulReadRequestCount": "successful read requests", - "successfulWriteRequestsCount": "successful write requests", - "timeSinceLastReadMs": "time since last read millis", - "totalReadRemoteExecTimeMs": "total read remote exec time millis", - "totalReadTimeMs": "total read time millis", - "totalWriteTimeMs": "total write time millis", - "writeBufferOperationsCount": "write buffer operation count", - "writeBufferSizeBytes": "write buffer size in bytes", -} -`; - -exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = ` -Object { - "leader_index": "leader index", - "max_outstanding_read_requests": "foo", - "max_outstanding_write_requests": "foo", - "max_read_request_operation_count": "foo", - "max_read_request_size": "foo", - "max_retry_delay": "foo", - "max_write_buffer_count": "foo", - "max_write_buffer_size": "foo", - "max_write_request_operation_count": "foo", - "max_write_request_size": "foo", - "read_poll_timeout": "foo", - "remote_cluster": "remote cluster", -} -`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js deleted file mode 100644 index ae13c625a7d80..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js +++ /dev/null @@ -1,41 +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. - */ - -export const deserializeAutoFollowPattern = ( - { - name, - pattern: { - active, - // eslint-disable-next-line camelcase - remote_cluster, - // eslint-disable-next-line camelcase - leader_index_patterns, - // eslint-disable-next-line camelcase - follow_index_pattern, - }, - } = { - pattern: {}, - } -) => ({ - name, - active, - remoteCluster: remote_cluster, - leaderIndexPatterns: leader_index_patterns, - followIndexPattern: follow_index_pattern, -}); - -export const deserializeListAutoFollowPatterns = autoFollowPatterns => - autoFollowPatterns.map(deserializeAutoFollowPattern); - -export const serializeAutoFollowPattern = ({ - remoteCluster, - leaderIndexPatterns, - followIndexPattern, -}) => ({ - remote_cluster: remoteCluster, - leader_index_patterns: leaderIndexPatterns, - follow_index_pattern: followIndexPattern, -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js deleted file mode 100644 index eef87a6cc4c89..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js +++ /dev/null @@ -1,102 +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 { - deserializeAutoFollowPattern, - deserializeListAutoFollowPatterns, - serializeAutoFollowPattern, -} from './auto_follow_pattern_serialization'; - -describe('[CCR] auto-follow_serialization', () => { - describe('deserializeAutoFollowPattern()', () => { - it('should return empty object if name or esObject are not provided', () => { - expect(deserializeAutoFollowPattern()).toEqual({}); - }); - - it('should deserialize Elasticsearch object', () => { - const expected = { - name: 'some-name', - remoteCluster: 'foo', - leaderIndexPatterns: ['foo-*'], - followIndexPattern: 'bar', - }; - - const esObject = { - name: 'some-name', - pattern: { - remote_cluster: expected.remoteCluster, - leader_index_patterns: expected.leaderIndexPatterns, - follow_index_pattern: expected.followIndexPattern, - }, - }; - - expect(deserializeAutoFollowPattern(esObject)).toEqual(expected); - }); - }); - - describe('deserializeListAutoFollowPatterns()', () => { - it('should deserialize list of Elasticsearch objects', () => { - const name1 = 'foo1'; - const name2 = 'foo2'; - - const expected = [ - { - name: name1, - remoteCluster: 'foo1', - leaderIndexPatterns: ['foo1-*'], - followIndexPattern: 'bar2', - }, - { - name: name2, - remoteCluster: 'foo2', - leaderIndexPatterns: ['foo2-*'], - followIndexPattern: 'bar2', - }, - ]; - - const esObjects = { - patterns: [ - { - name: name1, - pattern: { - remote_cluster: expected[0].remoteCluster, - leader_index_patterns: expected[0].leaderIndexPatterns, - follow_index_pattern: expected[0].followIndexPattern, - }, - }, - { - name: name2, - pattern: { - remote_cluster: expected[1].remoteCluster, - leader_index_patterns: expected[1].leaderIndexPatterns, - follow_index_pattern: expected[1].followIndexPattern, - }, - }, - ], - }; - - expect(deserializeListAutoFollowPatterns(esObjects.patterns)).toEqual(expected); - }); - }); - - describe('serializeAutoFollowPattern()', () => { - it('should serialize object to Elasticsearch object', () => { - const expected = { - remote_cluster: 'foo', - leader_index_patterns: ['bar-*'], - follow_index_pattern: 'faz', - }; - - const object = { - remoteCluster: expected.remote_cluster, - leaderIndexPatterns: expected.leader_index_patterns, - followIndexPattern: expected.follow_index_pattern, - }; - - expect(serializeAutoFollowPattern(object)).toEqual(expected); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js deleted file mode 100644 index c41fde8f7818d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js +++ /dev/null @@ -1,135 +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. - */ - -/* eslint-disable camelcase */ -export const deserializeShard = ({ - remote_cluster, - leader_index, - shard_id, - leader_global_checkpoint, - leader_max_seq_no, - follower_global_checkpoint, - follower_max_seq_no, - last_requested_seq_no, - outstanding_read_requests, - outstanding_write_requests, - write_buffer_operation_count, - write_buffer_size_in_bytes, - follower_mapping_version, - follower_settings_version, - total_read_time_millis, - total_read_remote_exec_time_millis, - successful_read_requests, - failed_read_requests, - operations_read, - bytes_read, - total_write_time_millis, - successful_write_requests, - failed_write_requests, - operations_written, - read_exceptions, - time_since_last_read_millis, -}) => ({ - id: shard_id, - remoteCluster: remote_cluster, - leaderIndex: leader_index, - leaderGlobalCheckpoint: leader_global_checkpoint, - leaderMaxSequenceNum: leader_max_seq_no, - followerGlobalCheckpoint: follower_global_checkpoint, - followerMaxSequenceNum: follower_max_seq_no, - lastRequestedSequenceNum: last_requested_seq_no, - outstandingReadRequestsCount: outstanding_read_requests, - outstandingWriteRequestsCount: outstanding_write_requests, - writeBufferOperationsCount: write_buffer_operation_count, - writeBufferSizeBytes: write_buffer_size_in_bytes, - followerMappingVersion: follower_mapping_version, - followerSettingsVersion: follower_settings_version, - totalReadTimeMs: total_read_time_millis, - totalReadRemoteExecTimeMs: total_read_remote_exec_time_millis, - successfulReadRequestCount: successful_read_requests, - failedReadRequestsCount: failed_read_requests, - operationsReadCount: operations_read, - bytesReadCount: bytes_read, - totalWriteTimeMs: total_write_time_millis, - successfulWriteRequestsCount: successful_write_requests, - failedWriteRequestsCount: failed_write_requests, - operationsWrittenCount: operations_written, - // This is an array of exception objects - readExceptions: read_exceptions, - timeSinceLastReadMs: time_since_last_read_millis, -}); -/* eslint-enable camelcase */ - -/* eslint-disable camelcase */ -export const deserializeFollowerIndex = ({ - follower_index, - remote_cluster, - leader_index, - status, - parameters: { - max_read_request_operation_count, - max_outstanding_read_requests, - max_read_request_size, - max_write_request_operation_count, - max_write_request_size, - max_outstanding_write_requests, - max_write_buffer_count, - max_write_buffer_size, - max_retry_delay, - read_poll_timeout, - } = {}, - shards, -}) => ({ - name: follower_index, - remoteCluster: remote_cluster, - leaderIndex: leader_index, - status, - maxReadRequestOperationCount: max_read_request_operation_count, - maxOutstandingReadRequests: max_outstanding_read_requests, - maxReadRequestSize: max_read_request_size, - maxWriteRequestOperationCount: max_write_request_operation_count, - maxWriteRequestSize: max_write_request_size, - maxOutstandingWriteRequests: max_outstanding_write_requests, - maxWriteBufferCount: max_write_buffer_count, - maxWriteBufferSize: max_write_buffer_size, - maxRetryDelay: max_retry_delay, - readPollTimeout: read_poll_timeout, - shards: shards && shards.map(deserializeShard), -}); -/* eslint-enable camelcase */ - -export const deserializeListFollowerIndices = followerIndices => - followerIndices.map(deserializeFollowerIndex); - -export const serializeAdvancedSettings = ({ - maxReadRequestOperationCount, - maxOutstandingReadRequests, - maxReadRequestSize, - maxWriteRequestOperationCount, - maxWriteRequestSize, - maxOutstandingWriteRequests, - maxWriteBufferCount, - maxWriteBufferSize, - maxRetryDelay, - readPollTimeout, -}) => ({ - max_read_request_operation_count: maxReadRequestOperationCount, - max_outstanding_read_requests: maxOutstandingReadRequests, - max_read_request_size: maxReadRequestSize, - max_write_request_operation_count: maxWriteRequestOperationCount, - max_write_request_size: maxWriteRequestSize, - max_outstanding_write_requests: maxOutstandingWriteRequests, - max_write_buffer_count: maxWriteBufferCount, - max_write_buffer_size: maxWriteBufferSize, - max_retry_delay: maxRetryDelay, - read_poll_timeout: readPollTimeout, -}); - -export const serializeFollowerIndex = followerIndex => ({ - remote_cluster: followerIndex.remoteCluster, - leader_index: followerIndex.leaderIndex, - ...serializeAdvancedSettings(followerIndex), -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js deleted file mode 100644 index e1df917d899ad..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js +++ /dev/null @@ -1,175 +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 { - deserializeShard, - deserializeFollowerIndex, - deserializeListFollowerIndices, - serializeFollowerIndex, -} from './follower_index_serialization'; - -describe('[CCR] follower index serialization', () => { - describe('deserializeShard()', () => { - it('deserializes shard', () => { - const serializedShard = { - remote_cluster: 'remote cluster', - leader_index: 'leader index', - shard_id: 'shard id', - leader_global_checkpoint: 'leader global checkpoint', - leader_max_seq_no: 'leader max seq no', - follower_global_checkpoint: 'follower global checkpoint', - follower_max_seq_no: 'follower max seq no', - last_requested_seq_no: 'last requested seq no', - outstanding_read_requests: 'outstanding read requests', - outstanding_write_requests: 'outstanding write requests', - write_buffer_operation_count: 'write buffer operation count', - write_buffer_size_in_bytes: 'write buffer size in bytes', - follower_mapping_version: 'follower mapping version', - follower_settings_version: 'follower settings version', - total_read_time_millis: 'total read time millis', - total_read_remote_exec_time_millis: 'total read remote exec time millis', - successful_read_requests: 'successful read requests', - failed_read_requests: 'failed read requests', - operations_read: 'operations read', - bytes_read: 'bytes read', - total_write_time_millis: 'total write time millis', - successful_write_requests: 'successful write requests', - failed_write_requests: 'failed write requests', - operations_written: 'operations written', - read_exceptions: ['read exception'], - time_since_last_read_millis: 'time since last read millis', - }; - - expect(deserializeShard(serializedShard)).toMatchSnapshot(); - }); - }); - - describe('deserializeFollowerIndex()', () => { - it('deserializes Elasticsearch follower index object', () => { - const serializedFollowerIndex = { - index: 'follower index name', - status: 'active', - shards: [ - { - shard_id: 'shard 1', - }, - { - shard_id: 'shard 2', - }, - ], - }; - - expect(deserializeFollowerIndex(serializedFollowerIndex)).toMatchSnapshot(); - }); - }); - - describe('deserializeListFollowerIndices()', () => { - it('deserializes list of Elasticsearch follower index objects', () => { - const serializedFollowerIndexList = [ - { - follower_index: 'follower index 1', - remote_cluster: 'cluster 1', - leader_index: 'leader 1', - status: 'active', - parameters: { - max_read_request_operation_count: 1, - max_outstanding_read_requests: 1, - max_read_request_size: 1, - max_write_request_operation_count: 1, - max_write_request_size: 1, - max_outstanding_write_requests: 1, - max_write_buffer_count: 1, - max_write_buffer_size: 1, - max_retry_delay: 1, - read_poll_timeout: 1, - }, - shards: [], - }, - { - follower_index: 'follower index 2', - remote_cluster: 'cluster 2', - leader_index: 'leader 2', - status: 'paused', - parameters: { - max_read_request_operation_count: 2, - max_outstanding_read_requests: 2, - max_read_request_size: 2, - max_write_request_operation_count: 2, - max_write_request_size: 2, - max_outstanding_write_requests: 2, - max_write_buffer_count: 2, - max_write_buffer_size: 2, - max_retry_delay: 2, - read_poll_timeout: 2, - }, - shards: [], - }, - ]; - - const deserializedFollowerIndexList = [ - { - name: 'follower index 1', - remoteCluster: 'cluster 1', - leaderIndex: 'leader 1', - status: 'active', - maxReadRequestOperationCount: 1, - maxOutstandingReadRequests: 1, - maxReadRequestSize: 1, - maxWriteRequestOperationCount: 1, - maxWriteRequestSize: 1, - maxOutstandingWriteRequests: 1, - maxWriteBufferCount: 1, - maxWriteBufferSize: 1, - maxRetryDelay: 1, - readPollTimeout: 1, - shards: [], - }, - { - name: 'follower index 2', - remoteCluster: 'cluster 2', - leaderIndex: 'leader 2', - status: 'paused', - maxReadRequestOperationCount: 2, - maxOutstandingReadRequests: 2, - maxReadRequestSize: 2, - maxWriteRequestOperationCount: 2, - maxWriteRequestSize: 2, - maxOutstandingWriteRequests: 2, - maxWriteBufferCount: 2, - maxWriteBufferSize: 2, - maxRetryDelay: 2, - readPollTimeout: 2, - shards: [], - }, - ]; - - expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual( - deserializedFollowerIndexList - ); - }); - }); - - describe('serializeFollowerIndex()', () => { - it('serializes object to Elasticsearch follower index object', () => { - const deserializedFollowerIndex = { - remoteCluster: 'remote cluster', - leaderIndex: 'leader index', - maxReadRequestOperationCount: 'foo', - maxOutstandingReadRequests: 'foo', - maxReadRequestSize: 'foo', - maxWriteRequestOperationCount: 'foo', - maxWriteRequestSize: 'foo', - maxOutstandingWriteRequests: 'foo', - maxWriteBufferCount: 'foo', - maxWriteBufferSize: 'foo', - maxRetryDelay: 'foo', - readPollTimeout: 'foo', - }; - - expect(serializeFollowerIndex(deserializedFollowerIndex)).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js deleted file mode 100644 index 3d8c97f45327c..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js +++ /dev/null @@ -1,28 +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. - */ -export const arrify = val => (Array.isArray(val) ? val : [val]); - -/** - * Utilty to add some latency in a Promise chain - * - * @param {number} time Time in millisecond to wait - */ -export const wait = (time = 1000) => data => { - return new Promise(resolve => { - setTimeout(() => resolve(data), time); - }); -}; - -/** - * Utility to remove empty fields ("") from a request body - */ -export const removeEmptyFields = body => - Object.entries(body).reduce((acc, [key, value]) => { - if (value !== '') { - acc[key] = value; - } - return acc; - }, {}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js deleted file mode 100644 index 804fe80cd27b4..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js +++ /dev/null @@ -1,49 +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 { getRandomString } from '../../../../test_utils'; - -export const getAutoFollowPatternMock = ( - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndexPatterns = [getRandomString()], - followIndexPattern = getRandomString() -) => ({ - name, - pattern: { - remote_cluster: remoteCluster, - leader_index_patterns: leaderIndexPatterns, - follow_index_pattern: followIndexPattern, - }, -}); - -export const getAutoFollowPatternListMock = (total = 3) => { - const list = { - patterns: [], - }; - - let i = total; - while (i--) { - list.patterns.push(getAutoFollowPatternMock()); - } - - return list; -}; - -// ----------------- -// Client test mock -// ----------------- -export const getAutoFollowPatternClientMock = ({ - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndexPatterns = [`${getRandomString()}-*`], - followIndexPattern = getRandomString(), -}) => ({ - name, - remoteCluster, - leaderIndexPatterns, - followIndexPattern, -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js deleted file mode 100644 index a042375e82715..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js +++ /dev/null @@ -1,45 +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. - */ - -/** - * Errors mocks to throw during development to help visualizing - * the different flows in the UI - * - * TODO: Consult the ES team and make sure the error shapes are correct - * for each statusCode. - */ - -const error400 = new Error('Something went wrong'); -error400.statusCode = 400; -error400.response = ` - { - "error": { - "root_cause": [ - { - "type": "x_content_parse_exception", - "reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found" - } - ], - "type": "x_content_parse_exception", - "reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found" - }, - "status": 400 -}`; - -const error403 = new Error('Unauthorized'); -error403.statusCode = 403; -error403.response = ` - { - "acknowledged": true, - "trial_was_started": false, - "error_message": "Operation failed: Trial was already activated." - } -`; - -export const esErrors = { - 400: error400, - 403: error403, -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js deleted file mode 100644 index 6c535a665978c..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js +++ /dev/null @@ -1,216 +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. - */ - -const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies -const chance = new Chance(); -import { getRandomString } from '../../../../test_utils'; - -const serializeShard = ({ - id, - remoteCluster, - leaderIndex, - leaderGlobalCheckpoint, - leaderMaxSequenceNum, - followerGlobalCheckpoint, - followerMaxSequenceNum, - lastRequestedSequenceNum, - outstandingReadRequestsCount, - outstandingWriteRequestsCount, - writeBufferOperationsCount, - writeBufferSizeBytes, - followerMappingVersion, - followerSettingsVersion, - totalReadTimeMs, - totalReadRemoteExecTimeMs, - successfulReadRequestCount, - failedReadRequestsCount, - operationsReadCount, - bytesReadCount, - totalWriteTimeMs, - successfulWriteRequestsCount, - failedWriteRequestsCount, - operationsWrittenCount, - readExceptions, - timeSinceLastReadMs, -}) => ({ - shard_id: id, - remote_cluster: remoteCluster, - leader_index: leaderIndex, - leader_global_checkpoint: leaderGlobalCheckpoint, - leader_max_seq_no: leaderMaxSequenceNum, - follower_global_checkpoint: followerGlobalCheckpoint, - follower_max_seq_no: followerMaxSequenceNum, - last_requested_seq_no: lastRequestedSequenceNum, - outstanding_read_requests: outstandingReadRequestsCount, - outstanding_write_requests: outstandingWriteRequestsCount, - write_buffer_operation_count: writeBufferOperationsCount, - write_buffer_size_in_bytes: writeBufferSizeBytes, - follower_mapping_version: followerMappingVersion, - follower_settings_version: followerSettingsVersion, - total_read_time_millis: totalReadTimeMs, - total_read_remote_exec_time_millis: totalReadRemoteExecTimeMs, - successful_read_requests: successfulReadRequestCount, - failed_read_requests: failedReadRequestsCount, - operations_read: operationsReadCount, - bytes_read: bytesReadCount, - total_write_time_millis: totalWriteTimeMs, - successful_write_requests: successfulWriteRequestsCount, - failed_write_requests: failedWriteRequestsCount, - operations_written: operationsWrittenCount, - read_exceptions: readExceptions, - time_since_last_read_millis: timeSinceLastReadMs, -}); - -export const getFollowerIndexStatsMock = ( - name = chance.string(), - shards = [ - { - id: chance.string(), - remoteCluster: chance.string(), - leaderIndex: chance.string(), - leaderGlobalCheckpoint: chance.integer(), - leaderMaxSequenceNum: chance.integer(), - followerGlobalCheckpoint: chance.integer(), - followerMaxSequenceNum: chance.integer(), - lastRequestedSequenceNum: chance.integer(), - outstandingReadRequestsCount: chance.integer(), - outstandingWriteRequestsCount: chance.integer(), - writeBufferOperationsCount: chance.integer(), - writeBufferSizeBytes: chance.integer(), - followerMappingVersion: chance.integer(), - followerSettingsVersion: chance.integer(), - totalReadTimeMs: chance.integer(), - totalReadRemoteExecTimeMs: chance.integer(), - successfulReadRequestCount: chance.integer(), - failedReadRequestsCount: chance.integer(), - operationsReadCount: chance.integer(), - bytesReadCount: chance.integer(), - totalWriteTimeMs: chance.integer(), - successfulWriteRequestsCount: chance.integer(), - failedWriteRequestsCount: chance.integer(), - operationsWrittenCount: chance.integer(), - readExceptions: [chance.string()], - timeSinceLastReadMs: chance.integer(), - }, - ] -) => ({ - index: name, - shards: shards.map(serializeShard), -}); - -export const getFollowerIndexListStatsMock = (total = 3, names) => { - const list = { - follow_stats: { - indices: [], - }, - }; - - for (let i = 0; i < total; i++) { - list.follow_stats.indices.push(getFollowerIndexStatsMock(names[i])); - } - - return list; -}; - -export const getFollowerIndexInfoMock = ( - name = chance.string(), - status = chance.string(), - parameters = { - maxReadRequestOperationCount: chance.string(), - maxOutstandingReadRequests: chance.string(), - maxReadRequestSize: chance.string(), - maxWriteRequestOperationCount: chance.string(), - maxWriteRequestSize: chance.string(), - maxOutstandingWriteRequests: chance.string(), - maxWriteBufferCount: chance.string(), - maxWriteBufferSize: chance.string(), - maxRetryDelay: chance.string(), - readPollTimeout: chance.string(), - } -) => { - return { - follower_index: name, - status, - max_read_request_operation_count: parameters.maxReadRequestOperationCount, - max_outstanding_read_requests: parameters.maxOutstandingReadRequests, - max_read_request_size: parameters.maxReadRequestSize, - max_write_request_operation_count: parameters.maxWriteRequestOperationCount, - max_write_request_size: parameters.maxWriteRequestSize, - max_outstanding_write_requests: parameters.maxOutstandingWriteRequests, - max_write_buffer_count: parameters.maxWriteBufferCount, - max_write_buffer_size: parameters.maxWriteBufferSize, - max_retry_delay: parameters.maxRetryDelay, - read_poll_timeout: parameters.readPollTimeout, - }; -}; - -export const getFollowerIndexListInfoMock = (total = 3) => { - const list = { - follower_indices: [], - }; - - for (let i = 0; i < total; i++) { - list.follower_indices.push(getFollowerIndexInfoMock()); - } - - return list; -}; - -// ----------------- -// Client test mock -// ----------------- - -export const getFollowerIndexMock = ({ - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndex = getRandomString(), - status = 'Active', -} = {}) => ({ - name, - remoteCluster, - leaderIndex, - status, - maxReadRequestOperationCount: chance.integer(), - maxOutstandingReadRequests: chance.integer(), - maxReadRequestSize: getRandomString({ length: 5 }), - maxWriteRequestOperationCount: chance.integer(), - maxWriteRequestSize: '9223372036854775807b', - maxOutstandingWriteRequests: chance.integer(), - maxWriteBufferCount: chance.integer(), - maxWriteBufferSize: getRandomString({ length: 5 }), - maxRetryDelay: getRandomString({ length: 5 }), - readPollTimeout: getRandomString({ length: 5 }), - shards: [ - { - id: 0, - remoteCluster: remoteCluster, - leaderIndex: leaderIndex, - leaderGlobalCheckpoint: chance.integer(), - leaderMaxSequenceNum: chance.integer(), - followerGlobalCheckpoint: chance.integer(), - followerMaxSequenceNum: chance.integer(), - lastRequestedSequenceNum: chance.integer(), - outstandingReadRequestsCount: chance.integer(), - outstandingWriteRequestsCount: chance.integer(), - writeBufferOperationsCount: chance.integer(), - writeBufferSizeBytes: chance.integer(), - followerMappingVersion: chance.integer(), - followerSettingsVersion: chance.integer(), - totalReadTimeMs: chance.integer(), - totalReadRemoteExecTimeMs: chance.integer(), - successfulReadRequestCount: chance.integer(), - failedReadRequestsCount: chance.integer(), - operationsReadCount: chance.integer(), - bytesReadCount: chance.integer(), - totalWriteTimeMs: chance.integer(), - successfulWriteRequestsCount: chance.integer(), - failedWriteRequestsCount: chance.integer(), - operationsWrittenCount: chance.integer(), - readExceptions: [], - timeSinceLastReadMs: chance.integer(), - }, - ], -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js deleted file mode 100644 index ccfdf8b19f3ee..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js +++ /dev/null @@ -1,16 +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. - */ - -export { getAutoFollowPatternMock, getAutoFollowPatternListMock } from './auto_follow_pattern'; - -export { esErrors } from './es_errors'; - -export { - getFollowerIndexStatsMock, - getFollowerIndexListStatsMock, - getFollowerIndexInfoMock, - getFollowerIndexListInfoMock, -} from './follower_index'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/index.js b/x-pack/legacy/plugins/cross_cluster_replication/index.js deleted file mode 100644 index aff4cc5b56738..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/index.js +++ /dev/null @@ -1,57 +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 { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { plugin } from './server/np_ready'; - -export function crossClusterReplication(kibana) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.ccr', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main', 'remoteClusters', 'index_management'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/cross_cluster_replication'], - injectDefaultVars(server) { - const config = server.config(); - return { - ccrUiEnabled: - config.get('xpack.ccr.ui.enabled') && config.get('xpack.remote_clusters.ui.enabled'), - }; - }, - }, - - config(Joi) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - }).default(); - }, - isEnabled(config) { - return ( - config.get('xpack.ccr.enabled') && - config.get('xpack.index_management.enabled') && - config.get('xpack.remote_clusters.enabled') - ); - }, - init: function initCcrPlugin(server) { - plugin({}).setup(server.newPlatform.setup.core, { - indexManagement: server.newPlatform.setup.plugins.indexManagement, - __LEGACY: { - server, - ccrUIEnabled: server.config().get('xpack.ccr.ui.enabled'), - }, - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/index.js deleted file mode 100644 index e92c44da34474..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js +++ /dev/null @@ -1,7 +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 './register_routes'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss deleted file mode 100644 index 31317e16e3e9f..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Cross-Cluster Replication plugin styles - -// Prefix all styles with "ccr" to avoid conflicts. -// Examples -// ccrChart -// ccrChart__legend -// ccrChart__legend--small -// ccrChart__legend-isLoading - -@import 'np_ready/app/app'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/main.html b/x-pack/legacy/plugins/cross_cluster_replication/public/main.html deleted file mode 100644 index 2129f26267827..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/main.html +++ /dev/null @@ -1,3 +0,0 @@ -<kbn-management-app section="elasticsearch/ccr"> - <div id="ccrReactRoot"></div> -</kbn-management-app> diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss deleted file mode 100644 index 5ee862b1d9e44..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss +++ /dev/null @@ -1,14 +0,0 @@ -.ccrFollowerIndicesFormRow { - padding-bottom: 0; -} - -.ccrFollowerIndicesHelpText { - transform: translateY(-3px); -} - -/** - * 1. Prevent context menu popover appearing above confirmation modal - */ -.ccrFollowerIndicesDetailPanel { - z-index: $euiZMask - 1; /* 1 */ -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js deleted file mode 100644 index 968646a4bd1b0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js +++ /dev/null @@ -1,220 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPageContent, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { BASE_PATH } from '../../../common/constants'; -import { getFatalErrors } from './services/notifications'; -import { SectionError } from './components'; -import routing from './services/routing'; -import { loadPermissions } from './services/api'; - -import { - CrossClusterReplicationHome, - AutoFollowPatternAdd, - AutoFollowPatternEdit, - FollowerIndexAdd, - FollowerIndexEdit, -} from './sections'; - -class AppComponent extends Component { - static propTypes = { - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired, - }).isRequired, - }; - - constructor(...args) { - super(...args); - this.registerRouter(); - - this.state = { - isFetchingPermissions: false, - fetchPermissionError: undefined, - hasPermission: false, - missingClusterPrivileges: [], - }; - } - - UNSAFE_componentWillMount() { - routing.userHasLeftApp = false; - } - - componentDidMount() { - this.checkPermissions(); - } - - componentWillUnmount() { - routing.userHasLeftApp = true; - } - - async checkPermissions() { - this.setState({ - isFetchingPermissions: true, - }); - - try { - const { hasPermission, missingClusterPrivileges } = await loadPermissions(); - - this.setState({ - isFetchingPermissions: false, - hasPermission, - missingClusterPrivileges, - }); - } catch (error) { - // Expect an error in the shape provided by Angular's $http service. - if (error && error.body) { - return this.setState({ - isFetchingPermissions: false, - fetchPermissionError: error, - }); - } - - // This error isn't an HTTP error, so let the fatal error screen tell the user something - // unexpected happened. - getFatalErrors().add( - error, - i18n.translate('xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle', { - defaultMessage: 'Cross-Cluster Replication app', - }) - ); - } - } - - registerRouter() { - const { history, location } = this.props; - routing.reactRouter = { - history, - route: { - location, - }, - }; - } - - render() { - const { - isFetchingPermissions, - fetchPermissionError, - hasPermission, - missingClusterPrivileges, - } = this.state; - - if (isFetchingPermissions) { - return ( - <EuiPageContent horizontalPosition="center"> - <EuiFlexGroup alignItems="center" gutterSize="m"> - <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="l" /> - </EuiFlexItem> - - <EuiFlexItem> - <EuiTitle size="s"> - <h2> - <FormattedMessage - id="xpack.crossClusterReplication.app.permissionCheckTitle" - defaultMessage="Checking permissions…" - /> - </h2> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPageContent> - ); - } - - if (fetchPermissionError) { - return ( - <Fragment> - <SectionError - title={ - <FormattedMessage - id="xpack.crossClusterReplication.app.permissionCheckErrorTitle" - defaultMessage="Error checking permissions" - /> - } - error={fetchPermissionError} - /> - - <EuiSpacer size="m" /> - </Fragment> - ); - } - - if (!hasPermission) { - return ( - <EuiPageContent horizontalPosition="center"> - <EuiEmptyPrompt - iconType="securityApp" - iconColor={null} - title={ - <h2> - <FormattedMessage - id="xpack.crossClusterReplication.app.deniedPermissionTitle" - defaultMessage="You're missing cluster privileges" - /> - </h2> - } - body={ - <p> - <FormattedMessage - id="xpack.crossClusterReplication.app.deniedPermissionDescription" - defaultMessage="To use Cross-Cluster Replication, you must have {clusterPrivilegesCount, - plural, one {this cluster privilege} other {these cluster privileges}}: {clusterPrivileges}." - values={{ - clusterPrivileges: missingClusterPrivileges.join(', '), - clusterPrivilegesCount: missingClusterPrivileges.length, - }} - /> - </p> - } - /> - </EuiPageContent> - ); - } - - return ( - <div> - <Switch> - <Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}/follower_indices`} /> - <Route - exact - path={`${BASE_PATH}/auto_follow_patterns/add`} - component={AutoFollowPatternAdd} - /> - <Route - exact - path={`${BASE_PATH}/auto_follow_patterns/edit/:id`} - component={AutoFollowPatternEdit} - /> - <Route exact path={`${BASE_PATH}/follower_indices/add`} component={FollowerIndexAdd} /> - <Route - exact - path={`${BASE_PATH}/follower_indices/edit/:id`} - component={FollowerIndexEdit} - /> - <Route exact path={`${BASE_PATH}/:section`} component={CrossClusterReplicationHome} /> - </Switch> - </div> - ); - } -} - -export const App = withRouter(AppComponent); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js deleted file mode 100644 index cc81fce4eebe7..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js +++ /dev/null @@ -1,25 +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 React from 'react'; -import { render } from 'react-dom'; -import { Provider } from 'react-redux'; -import { HashRouter } from 'react-router-dom'; - -import { App } from './app'; -import { ccrStore } from './store'; - -export const renderReact = async (elem, I18nContext) => { - render( - <I18nContext> - <Provider store={ccrStore}> - <HashRouter> - <App /> - </HashRouter> - </Provider> - </I18nContext>, - elem - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js deleted file mode 100644 index 1a6d5e6efe35a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js +++ /dev/null @@ -1,370 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; - -import { - EuiButtonEmpty, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiHealth, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, - EuiTextColor, - EuiTitle, -} from '@elastic/eui'; - -import { - AutoFollowPatternIndicesPreview, - AutoFollowPatternActionMenu, -} from '../../../../../components'; - -export class DetailPanel extends Component { - static propTypes = { - apiStatus: PropTypes.string, - autoFollowPatternId: PropTypes.string, - autoFollowPattern: PropTypes.object, - closeDetailPanel: PropTypes.func.isRequired, - }; - - renderAutoFollowPattern({ - followIndexPatternPrefix, - followIndexPatternSuffix, - remoteCluster, - leaderIndexPatterns, - active, - }) { - return ( - <> - <section> - <EuiDescriptionList> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.statusLabel" - defaultMessage="Status" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="status"> - {!active ? ( - <EuiHealth color="subdued"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.pausedStatus" - defaultMessage="Paused" - /> - </EuiHealth> - ) : ( - <EuiHealth color="success"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.activeStatus" - defaultMessage="Active" - /> - </EuiHealth> - )} - </EuiDescriptionListDescription> - </EuiDescriptionList> - </section> - - <EuiSpacer size="s" /> - <section - aria-labelledby="ccrAutoFollowPatternDetailSettingsTitle" - data-test-subj="settingsSection" - > - <EuiTitle size="s"> - <h3 id="ccrAutoFollowPatternDetailSettingsTitle"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.statusTitle" - defaultMessage="Settings" - /> - </h3> - </EuiTitle> - - <EuiSpacer size="s" /> - - <EuiDescriptionList data-test-subj="settingsValues"> - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.remoteClusterLabel" - defaultMessage="Remote cluster" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="remoteCluster"> - {remoteCluster} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.leaderPatternsLabel" - defaultMessage="Leader patterns" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="leaderIndexPatterns"> - {leaderIndexPatterns.join(', ')} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.prefixLabel" - defaultMessage="Prefix" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="patternPrefix"> - {followIndexPatternPrefix || ( - <em> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.prefixEmptyValue" - defaultMessage="No prefix" - /> - </em> - )} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.suffixLabel" - defaultMessage="Suffix" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="patternSuffix"> - {followIndexPatternSuffix || ( - <em> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.suffixEmptyValue" - defaultMessage="No suffix" - /> - </em> - )} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - </EuiDescriptionList> - </section> - </> - ); - } - - renderIndicesPreview(prefix, suffix, leaderIndexPatterns) { - return ( - <section data-test-subj="indicesPreviewSection"> - <AutoFollowPatternIndicesPreview - prefix={prefix} - suffix={suffix} - leaderIndexPatterns={leaderIndexPatterns} - /> - </section> - ); - } - - renderAutoFollowPatternNotFound() { - return ( - <EuiFlyoutBody> - <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiIcon size="m" type="alert" color="danger" /> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiText> - <EuiTextColor color="subdued"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.notFoundLabel" - defaultMessage="Auto-follow pattern not found" - /> - </EuiTextColor> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - ); - } - - renderAutoFollowPatternErrors(autoFollowPattern) { - if (!autoFollowPattern.errors.length) { - return null; - } - - return ( - <section data-test-subj="errors"> - <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiIcon type="alert" color="danger" /> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiTitle size="s" data-test-subj="titleErrors"> - <h3> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.recentErrorsTitle" - defaultMessage="Recent errors" - /> - </h3> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="s" /> - <EuiText> - <ul> - {autoFollowPattern.errors.map((error, i) => ( - <li key={i} data-test-subj="recentError"> - {error.autoFollowException.reason} - </li> - ))} - </ul> - </EuiText> - </section> - ); - } - - renderFlyoutBody() { - const { autoFollowPattern } = this.props; - - if (!autoFollowPattern) { - return this.renderAutoFollowPatternNotFound(); - } - - const { - followIndexPatternPrefix, - followIndexPatternSuffix, - leaderIndexPatterns, - } = autoFollowPattern; - - let indexManagementFilter; - - if (followIndexPatternPrefix) { - indexManagementFilter = `name:${followIndexPatternPrefix}`; - } else if (followIndexPatternSuffix) { - indexManagementFilter = `name:${followIndexPatternSuffix}`; - } - - const indexManagementUri = getIndexListUri(indexManagementFilter); - - return ( - <EuiFlyoutBody> - {this.renderAutoFollowPattern(autoFollowPattern)} - - <EuiSpacer size="m" /> - - {this.renderIndicesPreview( - followIndexPatternPrefix, - followIndexPatternSuffix, - leaderIndexPatterns - )} - - <EuiSpacer size="l" /> - - <EuiLink href={indexManagementUri} data-test-subj="viewIndexManagementLink"> - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.viewIndicesLink" - defaultMessage="View your follower indices in Index Management" - /> - </EuiLink> - - <EuiSpacer size="l" /> - - {this.renderAutoFollowPatternErrors(autoFollowPattern)} - </EuiFlyoutBody> - ); - } - - renderFlyoutFooter() { - const { autoFollowPattern, closeDetailPanel } = this.props; - - return ( - <EuiFlyoutFooter> - <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - iconType="cross" - flush="left" - onClick={closeDetailPanel} - data-test-subj="closeFlyoutButton" - > - <FormattedMessage - id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.closeButtonLabel" - defaultMessage="Close" - /> - </EuiButtonEmpty> - </EuiFlexItem> - - {autoFollowPattern && ( - <EuiFlexItem grow={false}> - <EuiFlexGroup alignItems="center"> - <EuiFlexItem grow={false}> - <AutoFollowPatternActionMenu - edit - arrowDirection="up" - patterns={[autoFollowPattern]} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - )} - </EuiFlexGroup> - </EuiFlyoutFooter> - ); - } - - render() { - const { autoFollowPatternId, closeDetailPanel } = this.props; - - return ( - <EuiFlyout - data-test-subj="autoFollowPatternDetail" - onClose={closeDetailPanel} - aria-labelledby="autoFollowPatternDetailsFlyoutTitle" - size="m" - maxWidth={400} - > - <EuiFlyoutHeader> - <EuiTitle size="m" id="autoFollowPatternDetailsFlyoutTitle" data-test-subj="title"> - <h2>{autoFollowPatternId}</h2> - </EuiTitle> - </EuiFlyoutHeader> - - {this.renderFlyoutBody()} - {this.renderFlyoutFooter()} - </EuiFlyout> - ); - } -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js deleted file mode 100644 index 3e8cf6d3e2f78..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js +++ /dev/null @@ -1,510 +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 React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiCodeEditor, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiHealth, - EuiIcon, - EuiLoadingSpinner, - EuiSpacer, - EuiText, - EuiTextColor, - EuiTitle, -} from '@elastic/eui'; -import 'brace/theme/textmate'; - -import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; -import { API_STATUS } from '../../../../../constants'; -import { ContextMenu } from '../context_menu'; - -export class DetailPanel extends Component { - static propTypes = { - apiStatus: PropTypes.string, - followerIndexId: PropTypes.string, - followerIndex: PropTypes.object, - closeDetailPanel: PropTypes.func.isRequired, - }; - - renderFollowerIndex() { - const { - followerIndex: { - remoteCluster, - leaderIndex, - isPaused, - shards, - maxReadRequestOperationCount, - maxOutstandingReadRequests, - maxReadRequestSize, - maxWriteRequestOperationCount, - maxWriteRequestSize, - maxOutstandingWriteRequests, - maxWriteBufferCount, - maxWriteBufferSize, - maxRetryDelay, - readPollTimeout, - }, - } = this.props; - - return ( - <Fragment> - <EuiFlyoutBody> - <section> - <EuiDescriptionList> - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.statusLabel" - defaultMessage="Status" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="status"> - {isPaused ? ( - <EuiHealth color="subdued"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.pausedStatus" - defaultMessage="Paused" - /> - </EuiHealth> - ) : ( - <EuiHealth color="success"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.activeStatus" - defaultMessage="Active" - /> - </EuiHealth> - )} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.remoteClusterLabel" - defaultMessage="Remote cluster" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="remoteCluster"> - {remoteCluster} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.leaderIndexLabel" - defaultMessage="Leader index" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="leaderIndex"> - {leaderIndex} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - </EuiDescriptionList> - </section> - - <EuiSpacer size="l" /> - - <section - aria-labelledby="ccrFollowerIndexDetailSettingsTitle" - data-test-subj="settingsSection" - > - <EuiTitle size="s"> - <h3 id="ccrFollowerIndexDetailSettingsTitle"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.settingsTitle" - defaultMessage="Settings" - /> - </h3> - </EuiTitle> - - <EuiSpacer size="s" /> - - {isPaused ? ( - <EuiCallOut - size="s" - title={ - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.pausedFollowerCalloutTitle" - defaultMessage="A paused follower index does not have settings or shard statistics." - /> - } - /> - ) : ( - <EuiDescriptionList data-test-subj="settingsValues"> - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountTitle" - defaultMessage="Max read request operation count" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxReadReqOpCount"> - {maxReadRequestOperationCount} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsTitle" - defaultMessage="Max outstanding read requests" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxOutstandingReadReq"> - {maxOutstandingReadRequests} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeTitle" - defaultMessage="Max read request size" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxReadReqSize"> - {maxReadRequestSize} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountTitle" - defaultMessage="Max write request operation count" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxWriteReqOpCount"> - {maxWriteRequestOperationCount} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeTitle" - defaultMessage="Max write request size" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxWriteReqSize"> - {maxWriteRequestSize} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsTitle" - defaultMessage="Max outstanding write requests" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxOutstandingWriteReq"> - {maxOutstandingWriteRequests} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountTitle" - defaultMessage="Max write buffer count" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxWriteBufferCount"> - {maxWriteBufferCount} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeTitle" - defaultMessage="Max write buffer size" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxWriteBufferSize"> - {maxWriteBufferSize} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - <EuiFlexGroup> - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayTitle" - defaultMessage="Max retry delay" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="maxRetryDelay"> - {maxRetryDelay} - </EuiDescriptionListDescription> - </EuiFlexItem> - - <EuiFlexItem> - <EuiDescriptionListTitle> - <EuiTitle size="xs"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutTitle" - defaultMessage="Read poll timeout" - /> - </EuiTitle> - </EuiDescriptionListTitle> - - <EuiDescriptionListDescription data-test-subj="readPollTimeout"> - {readPollTimeout} - </EuiDescriptionListDescription> - </EuiFlexItem> - </EuiFlexGroup> - </EuiDescriptionList> - )} - </section> - - <EuiSpacer size="l" /> - - <section data-test-subj="shardsStatsSection"> - {shards && - shards.map((shard, i) => ( - <Fragment key={i}> - <EuiSpacer size="m" /> - <EuiTitle size="xs"> - <h3> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.shardStatsTitle" - defaultMessage="Shard {id} stats" - values={{ - id: shard.id, - }} - /> - </h3> - </EuiTitle> - <EuiSpacer size="s" /> - <EuiCodeEditor - mode="json" - theme="textmate" - width="100%" - isReadOnly - setOptions={{ maxLines: Infinity }} - value={JSON.stringify(shard, null, 2)} - editorProps={{ - $blockScrolling: Infinity, - }} - data-test-subj={`shardsStats${i}`} - /> - </Fragment> - ))} - </section> - </EuiFlyoutBody> - </Fragment> - ); - } - - renderContent() { - const { apiStatus, followerIndex } = this.props; - - if (apiStatus === API_STATUS.LOADING) { - return ( - <EuiFlyoutBody> - <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiLoadingSpinner size="m" /> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiText> - <EuiTextColor color="subdued"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.loadingLabel" - defaultMessage="Loading follower index…" - /> - </EuiTextColor> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - ); - } - - if (!followerIndex) { - return ( - <EuiFlyoutBody> - <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiIcon size="m" type="alert" color="danger" /> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiText> - <EuiTextColor color="subdued"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.notFoundLabel" - defaultMessage="Follower index not found" - /> - </EuiTextColor> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutBody> - ); - } - - return this.renderFollowerIndex(); - } - - renderFooter() { - const { followerIndexId, followerIndex, closeDetailPanel } = this.props; - - // Use ID instead of followerIndex, because followerIndex may not be loaded yet. - const indexManagementUri = getIndexListUri(`name:${followerIndexId}`); - - return ( - <EuiFlyoutFooter> - <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - iconType="cross" - flush="left" - onClick={closeDetailPanel} - data-test-subj="closeFlyoutButton" - > - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.closeButtonLabel" - defaultMessage="Close" - /> - </EuiButtonEmpty> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiButton href={indexManagementUri} data-test-subj="viewIndexManagementButton"> - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.viewIndexLink" - defaultMessage="View in Index Management" - /> - </EuiButton> - </EuiFlexItem> - - {followerIndex && ( - <EuiFlexItem grow={false}> - <ContextMenu - iconSide="left" - iconType="arrowUp" - anchorPosition="upRight" - label={ - <FormattedMessage - id="xpack.crossClusterReplication.followerIndexDetailPanel.manageButtonLabel" - defaultMessage="Manage" - /> - } - followerIndices={[followerIndex]} - testSubj="manageButton" - /> - </EuiFlexItem> - )} - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutFooter> - ); - } - - render() { - const { followerIndexId, closeDetailPanel } = this.props; - - return ( - <EuiFlyout - className="ccrFollowerIndicesDetailPanel" - data-test-subj="followerIndexDetail" - onClose={closeDetailPanel} - aria-labelledby="followerIndexDetailsFlyoutTitle" - size="m" - maxWidth={600} - > - <EuiFlyoutHeader> - <EuiTitle size="m" id="followerIndexDetailsFlyoutTitle" data-test-subj="title"> - <h2>{followerIndexId}</h2> - </EuiTitle> - </EuiFlyoutHeader> - - {this.renderContent()} - {this.renderFooter()} - </EuiFlyout> - ); - } -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts deleted file mode 100644 index b7c75108d4ef0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts +++ /dev/null @@ -1,10 +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. - */ - -jest.mock('./breadcrumbs', () => ({ - ...jest.requireActual('./breadcrumbs'), - setBreadcrumbs: jest.fn(), -})); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts deleted file mode 100644 index dc64cdee07f7d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts +++ /dev/null @@ -1,47 +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 { i18n } from '@kbn/i18n'; -import { ChromeBreadcrumb } from 'src/core/public'; - -import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public'; - -import { BASE_PATH } from '../../../../common/constants'; - -let setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; - -export const setBreadcrumbSetter = ({ - __LEGACY, -}: { - __LEGACY: { - chrome: any; - MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; - }; -}): void => { - setBreadcrumbs = (crumbs: ChromeBreadcrumb[]) => { - __LEGACY.chrome.breadcrumbs.set([__LEGACY.MANAGEMENT_BREADCRUMB, ...crumbs]); - }; -}; - -export const listBreadcrumb = { - text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', { - defaultMessage: 'Cross-Cluster Replication', - }), - href: `#${BASE_PATH}`, -}; - -export const addBreadcrumb = { - text: i18n.translate('xpack.crossClusterReplication.addBreadcrumbTitle', { - defaultMessage: 'Add', - }), -}; - -export const editBreadcrumb = { - text: i18n.translate('xpack.crossClusterReplication.editBreadcrumbTitle', { - defaultMessage: 'Edit', - }), -}; - -export { setBreadcrumbs }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts deleted file mode 100644 index f17926d2bee10..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts +++ /dev/null @@ -1,22 +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. - */ - -let esBase: string; - -export const setDocLinks = ({ - DOC_LINK_VERSION, - ELASTIC_WEBSITE_URL, -}: { - ELASTIC_WEBSITE_URL: string; - DOC_LINK_VERSION: string; -}) => { - esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; -}; - -export const getAutoFollowPatternUrl = () => `${esBase}/ccr-put-auto-follow-pattern.html`; -export const getFollowerIndexUrl = () => `${esBase}/ccr-put-follow.html`; -export const getByteUnitsUrl = () => `${esBase}/common-options.html#byte-units`; -export const getTimeUnitsUrl = () => `${esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts deleted file mode 100644 index 5e1c3e9e99437..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts +++ /dev/null @@ -1,20 +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 { NotificationsSetup, IToasts, FatalErrorsSetup } from 'src/core/public'; - -let _notifications: IToasts; -let _fatalErrors: FatalErrorsSetup; - -export const setNotifications = ( - notifications: NotificationsSetup, - fatalErrorsSetup: FatalErrorsSetup -) => { - _notifications = notifications.toasts; - _fatalErrors = fatalErrorsSetup; -}; - -export const getNotifications = () => _notifications; -export const getFatalErrors = () => _fatalErrors; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js deleted file mode 100644 index 36b9c185b487d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js +++ /dev/null @@ -1,27 +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 { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; -import { UIM_APP_NAME } from '../constants'; - -export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); -export { METRIC_TYPE }; -/** - * Transparently return provided request Promise, while allowing us to track - * a successful completion of the request. - */ -export function trackUserRequest(request, actionType) { - // Only track successful actions. - return request.then(response => { - trackUiMetric(METRIC_TYPE.LOADED, actionType); - // We return the response immediately without waiting for the tracking request to resolve, - // to avoid adding additional latency. - return response; - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js deleted file mode 100644 index b81cd30f3977a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js +++ /dev/null @@ -1,257 +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 { i18n } from '@kbn/i18n'; -import { getNotifications } from '../../services/notifications'; -import { SECTIONS, API_STATUS } from '../../constants'; -import { - loadAutoFollowPatterns as loadAutoFollowPatternsRequest, - getAutoFollowPattern as getAutoFollowPatternRequest, - createAutoFollowPattern as createAutoFollowPatternRequest, - updateAutoFollowPattern as updateAutoFollowPatternRequest, - deleteAutoFollowPattern as deleteAutoFollowPatternRequest, - pauseAutoFollowPattern as pauseAutoFollowPatternRequest, - resumeAutoFollowPattern as resumeAutoFollowPatternRequest, -} from '../../services/api'; -import routing from '../../services/routing'; -import * as t from '../action_types'; -import { sendApiRequest } from './api'; -import { getSelectedAutoFollowPatternId } from '../selectors'; - -const { AUTO_FOLLOW_PATTERN: scope } = SECTIONS; - -export const selectDetailAutoFollowPattern = id => ({ - type: t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL, - payload: id, -}); - -export const selectEditAutoFollowPattern = id => ({ - type: t.AUTO_FOLLOW_PATTERN_SELECT_EDIT, - payload: id, -}); - -export const loadAutoFollowPatterns = (isUpdating = false) => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_LOAD, - scope, - status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, - handler: async () => await loadAutoFollowPatternsRequest(), - }); - -export const getAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_GET, - scope: `${scope}-get`, - handler: async () => await getAutoFollowPatternRequest(id), - }); - -export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) => - sendApiRequest({ - label: isUpdating ? t.AUTO_FOLLOW_PATTERN_UPDATE : t.AUTO_FOLLOW_PATTERN_CREATE, - status: API_STATUS.SAVING, - scope: `${scope}-save`, - handler: async () => { - if (isUpdating) { - return await updateAutoFollowPatternRequest(id, autoFollowPattern); - } - return await createAutoFollowPatternRequest({ id, ...autoFollowPattern }); - }, - onSuccess() { - const successMessage = isUpdating - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.updateAction.successNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' updated successfully`, - values: { name: id }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.addAction.successNotificationTitle', - { - defaultMessage: `Added auto-follow pattern '{name}'`, - values: { name: id }, - } - ); - - getNotifications().addSuccess(successMessage); - routing.navigate(`/auto_follow_patterns`, undefined, { - pattern: encodeURIComponent(id), - }); - }, - }); - -export const deleteAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_DELETE, - scope: `${scope}-delete`, - status: API_STATUS.DELETING, - handler: async () => deleteAutoFollowPatternRequest(id), - onSuccess(response, dispatch, getState) { - /** - * We can have 1 or more auto-follow pattern delete operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error removing {count} auto-follow patterns`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorSingleNotificationTitle', - { - defaultMessage: `Error removing the '{name}' auto-follow pattern`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsDeleted.length) { - const hasMultipleDelete = response.itemsDeleted.length > 1; - - const successMessage = hasMultipleDelete - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} auto-follow patterns were removed`, - values: { count: response.itemsDeleted.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successSingleNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' was removed`, - values: { name: response.itemsDeleted[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - - // If we've just deleted a pattern we were looking at, we need to close the panel. - const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState()); - if (response.itemsDeleted.includes(autoFollowPatternId)) { - dispatch(selectDetailAutoFollowPattern(null)); - } - } - }, - }); - -export const pauseAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_PAUSE, - scope: `${scope}-pause`, - status: API_STATUS.UPDATING, - handler: () => pauseAutoFollowPatternRequest(id), - onSuccess: response => { - /** - * We can have 1 or more auto-follow pattern pause operations - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error pausing {count} auto-follow patterns`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorSingleNotificationTitle', - { - defaultMessage: `Error pausing the '{name}' auto-follow pattern`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsPaused.length) { - const hasMultiple = response.itemsPaused.length > 1; - - const successMessage = hasMultiple - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} auto-follow patterns were paused`, - values: { count: response.itemsPaused.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successSingleNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' was paused`, - values: { name: response.itemsPaused[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - }, - }); - -export const resumeAutoFollowPattern = id => - sendApiRequest({ - label: t.AUTO_FOLLOW_PATTERN_RESUME, - scope: `${scope}-resume`, - status: API_STATUS.UPDATING, - handler: () => resumeAutoFollowPatternRequest(id), - onSuccess: response => { - /** - * We can have 1 or more auto-follow pattern resume operations - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error resuming {count} auto-follow patterns`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorSingleNotificationTitle', - { - defaultMessage: `Error resuming the '{name}' auto-follow pattern`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsResumed.length) { - const hasMultiple = response.itemsResumed.length > 1; - - const successMessage = hasMultiple - ? i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} auto-follow patterns were resumed`, - values: { count: response.itemsResumed.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successSingleNotificationTitle', - { - defaultMessage: `Auto-follow pattern '{name}' was resumed`, - values: { name: response.itemsResumed[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - }, - }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js deleted file mode 100644 index ebdee067ced75..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js +++ /dev/null @@ -1,286 +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 { i18n } from '@kbn/i18n'; - -import routing from '../../services/routing'; -import { getNotifications } from '../../services/notifications'; -import { SECTIONS, API_STATUS } from '../../constants'; -import { - loadFollowerIndices as loadFollowerIndicesRequest, - getFollowerIndex as getFollowerIndexRequest, - createFollowerIndex as createFollowerIndexRequest, - pauseFollowerIndex as pauseFollowerIndexRequest, - resumeFollowerIndex as resumeFollowerIndexRequest, - unfollowLeaderIndex as unfollowLeaderIndexRequest, - updateFollowerIndex as updateFollowerIndexRequest, -} from '../../services/api'; -import * as t from '../action_types'; -import { sendApiRequest } from './api'; -import { getSelectedFollowerIndexId } from '../selectors'; - -const { FOLLOWER_INDEX: scope } = SECTIONS; - -export const selectDetailFollowerIndex = id => ({ - type: t.FOLLOWER_INDEX_SELECT_DETAIL, - payload: id, -}); - -export const selectEditFollowerIndex = id => ({ - type: t.FOLLOWER_INDEX_SELECT_EDIT, - payload: id, -}); - -export const loadFollowerIndices = (isUpdating = false) => - sendApiRequest({ - label: t.FOLLOWER_INDEX_LOAD, - scope, - status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, - handler: async () => await loadFollowerIndicesRequest(), - }); - -export const getFollowerIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_GET, - scope: `${scope}-get`, - handler: async () => await getFollowerIndexRequest(id), - }); - -export const saveFollowerIndex = (name, followerIndex, isUpdating = false) => - sendApiRequest({ - label: t.FOLLOWER_INDEX_CREATE, - status: API_STATUS.SAVING, - scope: `${scope}-save`, - handler: async () => { - if (isUpdating) { - return await updateFollowerIndexRequest(name, followerIndex); - } - return await createFollowerIndexRequest({ name, ...followerIndex }); - }, - onSuccess() { - const successMessage = isUpdating - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.updateAction.successNotificationTitle', - { - defaultMessage: `Follower index '{name}' updated successfully`, - values: { name }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.addAction.successNotificationTitle', - { - defaultMessage: `Added follower index '{name}'`, - values: { name }, - } - ); - - getNotifications().addSuccess(successMessage); - routing.navigate(`/follower_indices`, undefined, { - name: encodeURIComponent(name), - }); - }, - }); - -export const pauseFollowerIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_PAUSE, - status: API_STATUS.SAVING, - scope, - handler: async () => pauseFollowerIndexRequest(id), - onSuccess(response, dispatch) { - /** - * We can have 1 or more follower index pause operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error pausing {count} follower indices`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.errorSingleNotificationTitle', - { - defaultMessage: `Error pausing follower index '{name}'`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsPaused.length) { - const hasMultiplePaused = response.itemsPaused.length > 1; - - const successMessage = hasMultiplePaused - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} follower indices were paused`, - values: { count: response.itemsPaused.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.pauseAction.successSingleNotificationTitle', - { - defaultMessage: `Follower index '{name}' was paused`, - values: { name: response.itemsPaused[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - - // Refresh list - dispatch(loadFollowerIndices(true)); - } - }, - }); - -export const resumeFollowerIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_RESUME, - status: API_STATUS.SAVING, - scope, - handler: async () => resumeFollowerIndexRequest(id), - onSuccess(response, dispatch) { - /** - * We can have 1 or more follower index resume operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error resuming {count} follower indices`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.errorSingleNotificationTitle', - { - defaultMessage: `Error resuming follower index '{name}'`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsResumed.length) { - const hasMultipleResumed = response.itemsResumed.length > 1; - - const successMessage = hasMultipleResumed - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.successMultipleNotificationTitle', - { - defaultMessage: `{count} follower indices were resumed`, - values: { count: response.itemsResumed.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.resumeAction.successSingleNotificationTitle', - { - defaultMessage: `Follower index '{name}' was resumed`, - values: { name: response.itemsResumed[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - - // Refresh list - dispatch(loadFollowerIndices(true)); - }, - }); - -export const unfollowLeaderIndex = id => - sendApiRequest({ - label: t.FOLLOWER_INDEX_UNFOLLOW, - status: API_STATUS.DELETING, - scope: `${scope}-delete`, - handler: async () => unfollowLeaderIndexRequest(id), - onSuccess(response, dispatch, getState) { - /** - * We can have 1 or more follower index unfollow operation - * that can fail or succeed. We will show 1 toast notification for each. - */ - if (response.errors.length) { - const hasMultipleErrors = response.errors.length > 1; - const errorMessage = hasMultipleErrors - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorMultipleNotificationTitle', - { - defaultMessage: `Error unfollowing leader index of {count} follower indices`, - values: { count: response.errors.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorSingleNotificationTitle', - { - defaultMessage: `Error unfollowing leader index of follower index '{name}'`, - values: { name: response.errors[0].id }, - } - ); - - getNotifications().addDanger(errorMessage); - } - - if (response.itemsUnfollowed.length) { - const hasMultipleUnfollow = response.itemsUnfollowed.length > 1; - - const successMessage = hasMultipleUnfollow - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.successMultipleNotificationTitle', - { - defaultMessage: `Leader indices of {count} follower indices were unfollowed`, - values: { count: response.itemsUnfollowed.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.successSingleNotificationTitle', - { - defaultMessage: `Leader index of follower index '{name}' was unfollowed`, - values: { name: response.itemsUnfollowed[0] }, - } - ); - - getNotifications().addSuccess(successMessage); - } - - if (response.itemsNotOpen.length) { - const hasMultipleNotOpen = response.itemsNotOpen.length > 1; - - const warningMessage = hasMultipleNotOpen - ? i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningMultipleNotificationTitle', - { - defaultMessage: `{count} indices could not be re-opened`, - values: { count: response.itemsNotOpen.length }, - } - ) - : i18n.translate( - 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningSingleNotificationTitle', - { - defaultMessage: `Index '{name}' could not be re-opened`, - values: { name: response.itemsNotOpen[0] }, - } - ); - - getNotifications().addWarning(warningMessage); - } - - // If we've just unfollowed a follower index we were looking at, we need to close the panel. - const followerIndexId = getSelectedFollowerIndexId('detail')(getState()); - if (response.itemsUnfollowed.includes(followerIndexId)) { - dispatch(selectDetailFollowerIndex(null)); - } - }, - }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts deleted file mode 100644 index 4ffe0db4e3c4e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts +++ /dev/null @@ -1,28 +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 { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; - -const propertyPath = 'isFollowerIndex'; - -const followerBadgeExtension = { - matchIndex: (index: any) => { - return get(index, propertyPath); - }, - label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', { - defaultMessage: 'Follower', - }), - color: 'default', - filterExpression: 'isFollowerIndex:true', -}; - -export const extendIndexManagement = (indexManagement?: IndexManagementPluginSetup) => { - if (indexManagement) { - indexManagement.extensionsService.addBadge(followerBadgeExtension); - } -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts deleted file mode 100644 index 11aea6b7b5de4..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts +++ /dev/null @@ -1,11 +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 { PluginInitializerContext } from 'src/core/public'; - -import { CrossClusterReplicationUIPlugin } from './plugin'; - -export const plugin = (ctx: PluginInitializerContext) => new CrossClusterReplicationUIPlugin(ctx); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts deleted file mode 100644 index 46259c698b282..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts +++ /dev/null @@ -1,44 +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 { - ChromeBreadcrumb, - CoreSetup, - Plugin, - PluginInitializerContext, - DocLinksStart, -} from 'src/core/public'; - -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; - -// @ts-ignore; -import { setHttpClient } from './app/services/api'; -import { setBreadcrumbSetter } from './app/services/breadcrumbs'; -import { setDocLinks } from './app/services/documentation_links'; -import { setNotifications } from './app/services/notifications'; -import { extendIndexManagement } from './extend_index_management'; - -interface PluginDependencies { - indexManagement: IndexManagementPluginSetup; - __LEGACY: { - chrome: any; - MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; - docLinks: DocLinksStart; - }; -} - -export class CrossClusterReplicationUIPlugin implements Plugin { - // @ts-ignore - constructor(private readonly ctx: PluginInitializerContext) {} - setup({ http, notifications, fatalErrors }: CoreSetup, deps: PluginDependencies) { - setHttpClient(http); - setBreadcrumbSetter(deps); - setDocLinks(deps.__LEGACY.docLinks); - setNotifications(notifications, fatalErrors); - extendIndexManagement(deps.indexManagement); - } - - start() {} -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js b/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js deleted file mode 100644 index 838939f46e523..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js +++ /dev/null @@ -1,94 +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 { unmountComponentAtNode } from 'react-dom'; -import chrome from 'ui/chrome'; -import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { npSetup, npStart } from 'ui/new_platform'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { i18n } from '@kbn/i18n'; - -import template from './main.html'; -import { BASE_PATH } from '../common/constants'; - -import { plugin } from './np_ready'; - -/** - * TODO: When this file is deleted, use the management section for rendering - */ -import { renderReact } from './np_ready/app'; - -const isAvailable = xpackInfo.get('features.crossClusterReplication.isAvailable'); -const isActive = xpackInfo.get('features.crossClusterReplication.isActive'); -const isLicenseOK = isAvailable && isActive; -const isCcrUiEnabled = chrome.getInjected('ccrUiEnabled'); - -if (isLicenseOK && isCcrUiEnabled) { - const esSection = management.getSection('elasticsearch'); - - esSection.register('ccr', { - visible: true, - display: i18n.translate('xpack.crossClusterReplication.appTitle', { - defaultMessage: 'Cross-Cluster Replication', - }), - order: 4, - url: `#${BASE_PATH}`, - }); - - let elem; - - const CCR_REACT_ROOT = 'ccrReactRoot'; - - plugin({}).setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - chrome, - docLinks: npStart.core.docLinks, - MANAGEMENT_BREADCRUMB, - }, - }); - - const unmountReactApp = () => elem && unmountComponentAtNode(elem); - - routes.when(`${BASE_PATH}/:section?/:subsection?/:view?/:id?`, { - template, - controllerAs: 'ccr', - controller: class CrossClusterReplicationController { - constructor($scope, $route) { - // React-router's <Redirect> does not play well with the angular router. It will cause this controller - // to re-execute without the $destroy handler being called. This means that the app will be mounted twice - // creating a memory leak when leaving (only 1 app will be unmounted). - // To avoid this, we unmount the React app each time we enter the controller. - unmountReactApp(); - - $scope.$$postDigest(() => { - elem = document.getElementById(CCR_REACT_ROOT); - renderReact(elem, npStart.core.i18n.Context); - - // Angular Lifecycle - const appRoute = $route.current; - const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template; - - // When we navigate within CCR, prevent Angular from re-matching the route and rebuild the app - if (isNavigationInApp) { - $route.current = appRoute; - } else { - // Any clean up when User leaves the CCR - } - - $scope.$on('$destroy', () => { - stopListeningForLocationChange && stopListeningForLocationChange(); - unmountReactApp(); - }); - }); - }); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js deleted file mode 100644 index 91527b8eb7cc5..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js +++ /dev/null @@ -1,198 +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. - */ - -export const elasticsearchJsPlugin = (Client, config, components) => { - const ca = components.clientAction.factory; - - Client.prototype.ccr = components.clientAction.namespaceFactory(); - const ccr = Client.prototype.ccr.prototype; - - ccr.permissions = ca({ - urls: [ - { - fmt: '/_security/user/_has_privileges', - }, - ], - needBody: true, - method: 'POST', - }); - - ccr.autoFollowPatterns = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow', - }, - ], - method: 'GET', - }); - - ccr.autoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.saveAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ccr.deleteAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'DELETE', - }); - - ccr.pauseAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>/pause', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.resumeAutoFollowPattern = ca({ - urls: [ - { - fmt: '/_ccr/auto_follow/<%=id%>/resume', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.info = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/info', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.stats = ca({ - urls: [ - { - fmt: '/_ccr/stats', - }, - ], - method: 'GET', - }); - - ccr.followerIndexStats = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/stats', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - ccr.saveFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=name%>/_ccr/follow', - req: { - name: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'PUT', - }); - - ccr.pauseFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/pause_follow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); - - ccr.resumeFollowerIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/resume_follow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - needBody: true, - method: 'POST', - }); - - ccr.unfollowLeaderIndex = ca({ - urls: [ - { - fmt: '/<%=id%>/_ccr/unfollow', - req: { - id: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts deleted file mode 100644 index ae15073b979e1..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts +++ /dev/null @@ -1,36 +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 { APICaller } from 'src/core/server'; -import { Index } from '../../../../../plugins/index_management/server'; - -export const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => { - if (!indicesList?.length) { - return indicesList; - } - const params = { - path: '/_all/_ccr/info', - method: 'GET', - }; - try { - const { follower_indices: followerIndices } = await callWithRequest( - 'transport.request', - params - ); - return indicesList.map(index => { - const isFollowerIndex = !!followerIndices.find( - (followerIndex: { follower_index: string }) => { - return followerIndex.follower_index === index.name; - } - ); - return { - ...index, - isFollowerIndex, - }; - }); - } catch (e) { - return indicesList; - } -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts deleted file mode 100644 index 7a38d024d99a2..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts +++ /dev/null @@ -1,11 +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 { PluginInitializerContext } from 'src/core/server'; -import { CrossClusterReplicationServerPlugin } from './plugin'; - -export const plugin = (ctx: PluginInitializerContext) => - new CrossClusterReplicationServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap deleted file mode 100644 index 92ac6070904b5..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`[CCR] auto-follow stats serialization should deserialize auto-follow stats 1`] = ` -Object { - "autoFollowedClusters": Array [ - Object { - "clusterName": "new-york", - "lastSeenMetadataVersion": 15, - "timeSinceLastCheckMillis": 2426, - }, - ], - "numberOfFailedFollowIndices": 0, - "numberOfFailedRemoteClusterStateRequests": 0, - "numberOfSuccessfulFollowIndices": 0, - "recentAutoFollowErrors": Array [ - Object { - "autoFollowException": Object { - "reason": "index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]", - "type": "exception", - }, - "leaderIndex": "pattern-1:kibana_sample_1", - }, - Object { - "autoFollowException": Object { - "reason": "index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]", - "type": "exception", - }, - "leaderIndex": "pattern-2:kibana_sample_1", - }, - ], -} -`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index 99d72ce1a0e6e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,20 +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 { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../../client/elasticsearch_ccr'; - -const callWithRequest = once(server => { - const config = { plugins: [elasticsearchJsPlugin] }; - const cluster = server.plugins.elasticsearch.createCluster('ccr', config); - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js deleted file mode 100644 index e4d2f8d64d1bb..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js +++ /dev/null @@ -1,42 +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. - */ - -/* eslint-disable camelcase */ -export const deserializeRecentAutoFollowErrors = ({ - leader_index, - auto_follow_exception: { type, reason }, -}) => ({ - leaderIndex: leader_index, - autoFollowException: { - type, - reason, - }, -}); - -export const deserializeAutoFollowedClusters = ({ - cluster_name, - time_since_last_check_millis, - last_seen_metadata_version, -}) => ({ - clusterName: cluster_name, - timeSinceLastCheckMillis: time_since_last_check_millis, - lastSeenMetadataVersion: last_seen_metadata_version, -}); - -export const deserializeAutoFollowStats = ({ - number_of_failed_follow_indices, - number_of_failed_remote_cluster_state_requests, - number_of_successful_follow_indices, - recent_auto_follow_errors, - auto_followed_clusters, -}) => ({ - numberOfFailedFollowIndices: number_of_failed_follow_indices, - numberOfFailedRemoteClusterStateRequests: number_of_failed_remote_cluster_state_requests, - numberOfSuccessfulFollowIndices: number_of_successful_follow_indices, - recentAutoFollowErrors: recent_auto_follow_errors.map(deserializeRecentAutoFollowErrors), - autoFollowedClusters: auto_followed_clusters.map(deserializeAutoFollowedClusters), -}); -/* eslint-enable camelcase */ diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js deleted file mode 100644 index 5120c56701e5b..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js +++ /dev/null @@ -1,44 +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 { deserializeAutoFollowStats } from './ccr_stats_serialization'; - -describe('[CCR] auto-follow stats serialization', () => { - it('should deserialize auto-follow stats', () => { - const esObject = { - number_of_failed_follow_indices: 0, - number_of_failed_remote_cluster_state_requests: 0, - number_of_successful_follow_indices: 0, - recent_auto_follow_errors: [ - { - leader_index: 'pattern-1:kibana_sample_1', - auto_follow_exception: { - type: 'exception', - reason: - 'index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]', - }, - }, - { - leader_index: 'pattern-2:kibana_sample_1', - auto_follow_exception: { - type: 'exception', - reason: - 'index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]', - }, - }, - ], - auto_followed_clusters: [ - { - cluster_name: 'new-york', - time_since_last_check_millis: 2426, - last_seen_metadata_version: 15, - }, - ], - }; - - expect(deserializeAutoFollowStats(esObject)).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js deleted file mode 100644 index 6cf12896fa472..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js +++ /dev/null @@ -1,70 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - const pluginName = 'Cross-Cluster Replication'; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate( - 'xpack.crossClusterReplication.checkLicense.errorUnavailableMessage', - { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - } - ), - }; - } - - const VALID_LICENSE_MODES = ['trial', 'platinum', 'enterprise']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - isActive: false, - message: i18n.translate( - 'xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage', - { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - } - ), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - isAvailable: true, - isActive: false, - message: i18n.translate('xpack.crossClusterReplication.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid and active - return { - isAvailable: true, - isActive: true, - }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js deleted file mode 100644 index f2c070fd44b6e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { checkLicense } from './check_license'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js deleted file mode 100644 index 11a6fd4e1d816..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js +++ /dev/null @@ -1,33 +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 expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - originalError.response = '{}'; - }); - - it('should return the correct object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.statusCode).to.be(originalError.statusCode); - expect(wrappedError.message).to.be(originalError.message); - }); - - it('should return the correct object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.statusCode).to.be(originalError.statusCode); - expect(wrappedError.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts deleted file mode 100644 index 3756b0c74fb10..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { wrapEsError } from './wrap_es_error'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts deleted file mode 100644 index 8afd5f1a018eb..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts +++ /dev/null @@ -1,65 +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. - */ - -function extractCausedByChain( - causedBy: Record<string, any> = {}, - accumulator: string[] = [] -): string[] { - const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase - - if (reason) { - accumulator.push(reason); - } - - // eslint-disable-next-line @typescript-eslint/camelcase - if (caused_by) { - return extractCausedByChain(caused_by, accumulator); - } - - return accumulator; -} - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - */ -export function wrapEsError( - err: any, - statusCodeToMessageMap: Record<string, string> = {} -): { message: string; body?: { cause?: string[] }; statusCode: number } { - const { statusCode, response } = err; - - const { - error: { - root_cause = [], // eslint-disable-line @typescript-eslint/camelcase - caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase - } = {}, - } = JSON.parse(response); - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response and return it - if (!statusCodeToMessageMap[statusCode]) { - // The caused_by chain has the most information so use that if it's available. If not then - // settle for the root_cause. - const causedByChain = extractCausedByChain(caused_by); - const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; - - return { - message: err.message, - statusCode, - body: { - cause: causedByChain.length ? causedByChain : defaultCause, - }, - }; - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return { message, statusCode }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index 5f2141cce9395..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,44 +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 expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from 'lodash'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError, - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts deleted file mode 100644 index 441648a8701e0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { isEsErrorFactory } from './is_es_error_factory'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts deleted file mode 100644 index fc6405b8e7513..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts +++ /dev/null @@ -1,18 +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 { memoize } from 'lodash'; - -const esErrorsFactory = memoize((server: any) => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server: any) { - const esErrors = esErrorsFactory(server); - return function isEsError(err: any) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts deleted file mode 100644 index d22505f0e315a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts +++ /dev/null @@ -1,64 +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 { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockDeps: any; - let mockLicenseCheckResults: any; - - const anyContext: any = {}; - const anyRequest: any = {}; - - beforeEach(() => { - mockDeps = { - __LEGACY: { - server: { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }, - }, - requestHandler: jest.fn(), - }; - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', async () => { - const licensePreRouting = licensePreRoutingFactory(mockDeps); - const response = await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); - expect(response.status).toBe(403); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('it calls the wrapped handler', async () => { - const licensePreRouting = licensePreRoutingFactory(mockDeps); - await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); - expect(mockDeps.requestHandler).toHaveBeenCalledTimes(1); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts deleted file mode 100644 index 0743e443955f4..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index c47faa940a650..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ /dev/null @@ -1,32 +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 { RequestHandler } from 'src/core/server'; -import { PLUGIN } from '../../../../common/constants'; - -export const licensePreRoutingFactory = <P, Q, B>({ - __LEGACY, - requestHandler, -}: { - __LEGACY: { server: any }; - requestHandler: RequestHandler<P, Q, B>; -}) => { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - - // License checking and enable/disable logic - const licensePreRouting: RequestHandler<P, Q, B> = (ctx, request, response) => { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - return response.forbidden({ - body: licenseCheckResults.message, - }); - } else { - return requestHandler(ctx, request, response); - } - }; - - return licensePreRouting; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js deleted file mode 100644 index b9bb34a80ce79..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,21 +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 { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; -import { PLUGIN } from '../../../../common/constants'; -import { checkLicense } from '../check_license'; - -export function registerLicenseChecker(__LEGACY) { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - const ccrPluggin = __LEGACY.server.plugins[PLUGIN.ID]; - - mirrorPluginStatus(xpackMainPlugin, ccrPluggin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts deleted file mode 100644 index 829de10ad0177..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts +++ /dev/null @@ -1,38 +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 { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server'; - -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/server'; - -// @ts-ignore -import { registerLicenseChecker } from './lib/register_license_checker'; -// @ts-ignore -import { registerRoutes } from './routes/register_routes'; -import { ccrDataEnricher } from './cross_cluster_replication_data'; - -interface PluginDependencies { - indexManagement: IndexManagementPluginSetup; - __LEGACY: { - server: any; - ccrUIEnabled: boolean; - }; -} - -export class CrossClusterReplicationServerPlugin implements Plugin { - // @ts-ignore - constructor(private readonly ctx: PluginInitializerContext) {} - setup({ http }: CoreSetup, { indexManagement, __LEGACY }: PluginDependencies) { - registerLicenseChecker(__LEGACY); - - const router = http.createRouter(); - registerRoutes({ router, __LEGACY }); - if (__LEGACY.ccrUIEnabled && indexManagement && indexManagement.indexDataEnricher) { - indexManagement.indexDataEnricher.add(ccrDataEnricher); - } - } - start() {} -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js deleted file mode 100644 index f3024515c7213..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js +++ /dev/null @@ -1,330 +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 { deserializeAutoFollowPattern } from '../../../../../common/services/auto_follow_pattern_serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../../../fixtures'; -import { registerAutoFollowPatternRoutes } from '../auto_follow_pattern'; - -import { createRouter, callRoute } from './helpers'; - -jest.mock('../../../lib/call_with_request_factory'); -jest.mock('../../../lib/is_es_error_factory'); -jest.mock('../../../lib/license_pre_routing_factory', () => ({ - licensePreRoutingFactory: ({ requestHandler }) => requestHandler, -})); - -const DESERIALIZED_KEYS = Object.keys(deserializeAutoFollowPattern(getAutoFollowPatternMock())); - -let routeRegistry; - -/** - * Helper to extract all the different server route handler so we can easily call them in our tests. - * - * Important: This method registers the handlers in the order that they appear in the file, so - * if a "server.route()" call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. - */ -const registerHandlers = () => { - const HANDLER_INDEX_TO_ACTION = { - 0: 'list', - 1: 'create', - 2: 'update', - 3: 'get', - 4: 'delete', - 5: 'pause', - 6: 'resume', - }; - - routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); - - registerAutoFollowPatternRoutes({ - __LEGACY: {}, - router: routeRegistry.router, - }); -}; - -/** - * Queue to save request response and errors - * It allows us to fake multiple responses from the - * callWithRequestFactory() when the request handler call it - * multiple times. - */ -let requestResponseQueue = []; - -/** - * Helper to mock the response from the call to Elasticsearch - * - * @param {*} err The mock error to throw - * @param {*} response The response to return - */ -const setHttpRequestResponse = (error, response) => { - requestResponseQueue.push({ error, response }); -}; - -const resetHttpRequestResponses = () => (requestResponseQueue = []); - -const getNextResponseFromQueue = () => { - if (!requestResponseQueue.length) { - return null; - } - - const next = requestResponseQueue.shift(); - if (next.error) { - return Promise.reject(next.error); - } - return Promise.resolve(next.response); -}; - -describe('[CCR API Routes] Auto Follow Pattern', () => { - let routeHandler; - - beforeAll(() => { - isEsErrorFactory.mockReturnValue(() => false); - callWithRequestFactory.mockReturnValue(getNextResponseFromQueue); - registerHandlers(); - }); - - describe('list()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().list; - }); - - it('should deserialize the response from Elasticsearch', async () => { - const totalResult = 2; - setHttpRequestResponse(null, getAutoFollowPatternListMock(totalResult)); - - const { - options: { body: response }, - } = await callRoute(routeHandler); - const autoFollowPattern = response.patterns[0]; - - expect(response.patterns.length).toEqual(totalResult); - expect(Object.keys(autoFollowPattern)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('create()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().create; - }); - - it('should throw a 409 conflict error if id already exists', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute( - routeHandler, - {}, - { - body: { - id: 'some-id', - foo: 'bar', - }, - } - ); - - expect(response.status).toEqual(409); - }); - - it('should return 200 status when the id does not exist', async () => { - const error = new Error('Resource not found.'); - error.statusCode = 404; - setHttpRequestResponse(error); - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute( - routeHandler, - {}, - { - body: { - id: 'some-id', - foo: 'bar', - }, - } - ); - - expect(response).toEqual({ acknowledge: true }); - }); - }); - - describe('update()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().update; - }); - - it('should serialize the payload before sending it to Elasticsearch', async () => { - callWithRequestFactory.mockReturnValueOnce((_, payload) => payload); - - const request = { - params: { id: 'foo' }, - body: { - remoteCluster: 'bar1', - leaderIndexPatterns: ['bar2'], - followIndexPattern: 'bar3', - }, - }; - - const response = await callRoute(routeHandler, {}, request); - - expect(response.options.body).toEqual({ - id: 'foo', - body: { - remote_cluster: 'bar1', - leader_index_patterns: ['bar2'], - follow_index_pattern: 'bar3', - }, - }); - }); - }); - - describe('get()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().get; - }); - - it('should return a single resource even though ES return an array with 1 item', async () => { - const autoFollowPattern = getAutoFollowPatternMock(); - const esResponse = { patterns: [autoFollowPattern] }; - - setHttpRequestResponse(null, esResponse); - - const response = await callRoute(routeHandler, {}, { params: { id: 1 } }); - expect(Object.keys(response.options.body)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('delete()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().delete; - }); - - it('should delete a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsDeleted).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to delete', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsDeleted).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsDeleted).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); - - describe('pause()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().pause; - }); - - it('accept a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsPaused).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of items to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsPaused).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsPaused).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); - - describe('resume()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().resume; - }); - - it('accept a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsResumed).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of items to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsResumed).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsResumed).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js deleted file mode 100644 index f0139e5bd7011..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js +++ /dev/null @@ -1,312 +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 { deserializeFollowerIndex } from '../../../../../common/services/follower_index_serialization'; -import { - getFollowerIndexStatsMock, - getFollowerIndexListStatsMock, - getFollowerIndexInfoMock, - getFollowerIndexListInfoMock, -} from '../../../../../fixtures'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { registerFollowerIndexRoutes } from '../follower_index'; -import { createRouter, callRoute } from './helpers'; - -jest.mock('../../../lib/call_with_request_factory'); -jest.mock('../../../lib/is_es_error_factory'); -jest.mock('../../../lib/license_pre_routing_factory', () => ({ - licensePreRoutingFactory: ({ requestHandler }) => requestHandler, -})); - -const DESERIALIZED_KEYS = Object.keys( - deserializeFollowerIndex({ - ...getFollowerIndexInfoMock(), - ...getFollowerIndexStatsMock(), - }) -); - -let routeRegistry; - -/** - * Helper to extract all the different server route handler so we can easily call them in our tests. - * - * Important: This method registers the handlers in the order that they appear in the file, so - * if a 'server.route()' call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. - */ -const registerHandlers = () => { - const HANDLER_INDEX_TO_ACTION = { - 0: 'list', - 1: 'get', - 2: 'create', - 3: 'edit', - 4: 'pause', - 5: 'resume', - 6: 'unfollow', - }; - - routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); - registerFollowerIndexRoutes({ - __LEGACY: {}, - router: routeRegistry.router, - }); -}; - -/** - * Queue to save request response and errors - * It allows us to fake multiple responses from the - * callWithRequestFactory() when the request handler call it - * multiple times. - */ -let requestResponseQueue = []; - -/** - * Helper to mock the response from the call to Elasticsearch - * - * @param {*} err The mock error to throw - * @param {*} response The response to return - */ -const setHttpRequestResponse = (error, response) => { - requestResponseQueue.push({ error, response }); -}; - -const resetHttpRequestResponses = () => (requestResponseQueue = []); - -const getNextResponseFromQueue = () => { - if (!requestResponseQueue.length) { - return null; - } - - const next = requestResponseQueue.shift(); - if (next.error) { - return Promise.reject(next.error); - } - return Promise.resolve(next.response); -}; - -describe('[CCR API Routes] Follower Index', () => { - let routeHandler; - - beforeAll(() => { - isEsErrorFactory.mockReturnValue(() => false); - callWithRequestFactory.mockReturnValue(getNextResponseFromQueue); - registerHandlers(); - }); - - describe('list()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().list; - }); - - it('deserializes the response from Elasticsearch', async () => { - const totalResult = 2; - const infoResult = getFollowerIndexListInfoMock(totalResult); - const statsResult = getFollowerIndexListStatsMock( - totalResult, - infoResult.follower_indices.map(index => index.follower_index) - ); - setHttpRequestResponse(null, infoResult); - setHttpRequestResponse(null, statsResult); - - const { - options: { body: response }, - } = await callRoute(routeHandler); - const followerIndex = response.indices[0]; - - expect(response.indices.length).toEqual(totalResult); - expect(Object.keys(followerIndex)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('get()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().get; - }); - - it('should return a single resource even though ES return an array with 1 item', async () => { - const mockId = 'test1'; - const followerIndexInfo = getFollowerIndexInfoMock(mockId); - const followerIndexStats = getFollowerIndexStatsMock(mockId); - - setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] }); - setHttpRequestResponse(null, { indices: [followerIndexStats] }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: mockId } }); - expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('create()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().create; - }); - - it('should return 200 status when follower index is created', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute( - routeHandler, - {}, - { - body: { - name: 'follower_index', - remoteCluster: 'remote_cluster', - leaderIndex: 'leader_index', - }, - } - ); - - expect(response.options.body).toEqual({ acknowledge: true }); - }); - }); - - describe('pause()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().pause; - }); - - it('should pause a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsPaused).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsPaused).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsPaused).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); - - describe('resume()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().resume; - }); - - it('should resume a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsResumed).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to resume', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsResumed).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsResumed).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); - - describe('unfollow()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().unfollow; - }); - - it('should unfollow await single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsUnfollowed).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to unfollow', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsUnfollowed).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsUnfollowed).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts deleted file mode 100644 index 555fc0937c0ad..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts +++ /dev/null @@ -1,37 +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 { RequestHandler } from 'src/core/server'; -import { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; - -export const callRoute = ( - route: RequestHandler<any, any, any>, - ctx = {}, - request = {}, - response = kibanaResponseFactory -) => { - return route(ctx as any, request as any, response); -}; - -export const createRouter = (indexToActionMap: Record<number, string>) => { - let index = 0; - const routeHandlers: Record<string, RequestHandler<any, any, any>> = {}; - const addHandler = (ignoreCtxForNow: any, handler: RequestHandler) => { - // Save handler and increment index - routeHandlers[indexToActionMap[index]] = handler; - index++; - }; - - return { - getRoutes: () => routeHandlers, - router: { - get: addHandler, - post: addHandler, - put: addHandler, - delete: addHandler, - }, - }; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts deleted file mode 100644 index d458f1ccb354b..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts +++ /dev/null @@ -1,301 +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 { schema } from '@kbn/config-schema'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -// @ts-ignore -import { - deserializeAutoFollowPattern, - deserializeListAutoFollowPatterns, - serializeAutoFollowPattern, - // @ts-ignore -} from '../../../../common/services/auto_follow_pattern_serialization'; - -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../../common/constants'; - -import { RouteDependencies } from '../types'; -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; - -export const registerAutoFollowPatternRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns a list of all auto-follow patterns - */ - router.get( - { - path: `${API_BASE_PATH}/auto_follow_patterns`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const result = await callWithRequest('ccr.autoFollowPatterns'); - return response.ok({ - body: { - patterns: deserializeListAutoFollowPatterns(result.patterns), - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Create an auto-follow pattern - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns`, - validate: { - body: schema.object( - { - id: schema.string(), - }, - { unknowns: 'allow' } - ), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id, ...rest } = request.body; - const body = serializeAutoFollowPattern(rest); - - /** - * First let's make sur that an auto-follow pattern with - * the same id does not exist. - */ - try { - await callWithRequest('ccr.autoFollowPattern', { id }); - // If we get here it means that an auto-follow pattern with the same id exists - return response.conflict({ - body: `An auto-follow pattern with the name "${id}" already exists.`, - }); - } catch (err) { - if (err.statusCode !== 404) { - return mapErrorToKibanaHttpResponse(err); - } - } - - try { - return response.ok({ - body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Update an auto-follow pattern - */ - router.put( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({}, { unknowns: 'allow' }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const body = serializeAutoFollowPattern(request.body); - - try { - return response.ok({ - body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns a single auto-follow pattern - */ - router.get( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - try { - const result = await callWithRequest('ccr.autoFollowPattern', { id }); - const autoFollowPattern = result.patterns[0]; - - return response.ok({ - body: deserializeAutoFollowPattern(autoFollowPattern), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Delete an auto-follow pattern - */ - router.delete( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsDeleted: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.deleteAutoFollowPattern', { id: _id }) - .then(() => itemsDeleted.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsDeleted, - errors, - }, - }); - }, - }) - ); - - /** - * Pause auto-follow pattern(s) - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseAutoFollowPattern', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsPaused, - errors, - }, - }); - }, - }) - ); - - /** - * Resume auto-follow pattern(s) - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeAutoFollowPattern', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsResumed, - errors, - }, - }); - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts deleted file mode 100644 index b08b056ad2c8a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts +++ /dev/null @@ -1,112 +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 { API_BASE_PATH } from '../../../../common/constants'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -// @ts-ignore -import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; -import { RouteDependencies } from '../types'; - -export const registerCcrRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns Auto-follow stats - */ - router.get( - { - path: `${API_BASE_PATH}/stats/auto_follow`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats'); - - return response.ok({ - body: deserializeAutoFollowStats(autoFollowStats), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns whether the user has CCR permissions - */ - router.get( - { - path: `${API_BASE_PATH}/permissions`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - const xpackInfo = xpackMainPlugin && xpackMainPlugin.info; - - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - return response.customError({ - statusCode: 503, - body: 'Security info unavailable', - }); - } - - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { - // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR. - return response.ok({ - body: { - hasPermission: true, - missingClusterPrivileges: [], - }, - }); - } - - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { has_all_requested: hasPermission, cluster } = await callWithRequest( - 'ccr.permissions', - { - body: { - cluster: ['manage', 'manage_ccr'], - }, - } - ); - - const missingClusterPrivileges = Object.keys(cluster).reduce( - (permissions: any, permissionName: any) => { - if (!cluster[permissionName]) { - permissions.push(permissionName); - return permissions; - } - }, - [] as any[] - ); - - return response.ok({ - body: { - hasPermission, - missingClusterPrivileges, - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts deleted file mode 100644 index 1d7dacf4a8688..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts +++ /dev/null @@ -1,357 +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 { schema } from '@kbn/config-schema'; -import { - deserializeFollowerIndex, - deserializeListFollowerIndices, - serializeFollowerIndex, - serializeAdvancedSettings, - // @ts-ignore -} from '../../../../common/services/follower_index_serialization'; -import { API_BASE_PATH } from '../../../../common/constants'; -// @ts-ignore -import { removeEmptyFields } from '../../../../common/services/utils'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -import { RouteDependencies } from '../types'; -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; - -export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns a list of all follower indices - */ - router.get( - { - path: `${API_BASE_PATH}/follower_indices`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { - id: '_all', - }); - - const { - follow_stats: { indices: followerIndicesStats }, - } = await callWithRequest('ccr.stats'); - - const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { - map[stats.index] = stats; - return map; - }, {}); - - const collatedFollowerIndices = followerIndices.map((followerIndex: any) => { - return { - ...followerIndex, - ...followerIndicesStatsMap[followerIndex.follower_index], - }; - }); - - return response.ok({ - body: { - indices: deserializeListFollowerIndices(collatedFollowerIndices), - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns a single follower index pattern - */ - router.get( - { - path: `${API_BASE_PATH}/follower_indices/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - - const followerIndexInfo = followerIndices && followerIndices[0]; - - if (!followerIndexInfo) { - return response.notFound({ - body: `The follower index "${id}" does not exist.`, - }); - } - - // If this follower is paused, skip call to ES stats api since it will return 404 - if (followerIndexInfo.status === 'paused') { - return response.ok({ - body: deserializeFollowerIndex({ - ...followerIndexInfo, - }), - }); - } else { - const { - indices: followerIndicesStats, - } = await callWithRequest('ccr.followerIndexStats', { id }); - - return response.ok({ - body: deserializeFollowerIndex({ - ...followerIndexInfo, - ...(followerIndicesStats ? followerIndicesStats[0] : {}), - }), - }); - } - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Create a follower index - */ - router.post( - { - path: `${API_BASE_PATH}/follower_indices`, - validate: { - body: schema.object( - { - name: schema.string(), - }, - { unknowns: 'allow' } - ), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { name, ...rest } = request.body; - const body = removeEmptyFields(serializeFollowerIndex(rest)); - - try { - return response.ok({ - body: await callWithRequest('ccr.saveFollowerIndex', { name, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Edit a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}`, - validate: { - params: schema.object({ id: schema.string() }), - body: schema.object({ - maxReadRequestOperationCount: schema.maybe(schema.number()), - maxOutstandingReadRequests: schema.maybe(schema.number()), - maxReadRequestSize: schema.maybe(schema.string()), // byte value - maxWriteRequestOperationCount: schema.maybe(schema.number()), - maxWriteRequestSize: schema.maybe(schema.string()), // byte value - maxOutstandingWriteRequests: schema.maybe(schema.number()), - maxWriteBufferCount: schema.maybe(schema.number()), - maxWriteBufferSize: schema.maybe(schema.string()), // byte value - maxRetryDelay: schema.maybe(schema.string()), // time value - readPollTimeout: schema.maybe(schema.string()), // time value - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - // We need to first pause the follower and then resume it passing the advanced settings - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - const followerIndexInfo = followerIndices && followerIndices[0]; - if (!followerIndexInfo) { - return response.notFound({ body: `The follower index "${id}" does not exist.` }); - } - - // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. - const isPaused = followerIndexInfo.status === 'paused'; - // Pause follower if not already paused - if (!isPaused) { - await callWithRequest('ccr.pauseFollowerIndex', { id }); - } - - // Resume follower - const body = removeEmptyFields(serializeAdvancedSettings(request.body)); - return response.ok({ - body: await callWithRequest('ccr.resumeFollowerIndex', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Pauses a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/pause`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseFollowerIndex', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch((err: Error) => { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - }) - ) - ); - - return response.ok({ - body: { - itemsPaused, - errors, - }, - }); - }, - }) - ); - - /** - * Resumes a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/resume`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeFollowerIndex', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch((err: Error) => { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - }) - ) - ); - - return response.ok({ - body: { - itemsResumed, - errors, - }, - }); - }, - }) - ); - - /** - * Unfollow follower index's leader index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsUnfollowed: string[] = []; - const itemsNotOpen: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(async _id => { - try { - // Try to pause follower, let it fail silently since it may already be paused - try { - await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); - } catch (e) { - // Swallow errors - } - - // Close index - await callWithRequest('indices.close', { index: _id }); - - // Unfollow leader - await callWithRequest('ccr.unfollowLeaderIndex', { id: _id }); - - // Try to re-open the index, store failures in a separate array to surface warnings in the UI - // This will allow users to query their index normally after unfollowing - try { - await callWithRequest('indices.open', { index: _id }); - } catch (e) { - itemsNotOpen.push(_id); - } - - // Push success - itemsUnfollowed.push(_id); - } catch (err) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ); - - return response.ok({ - body: { - itemsUnfollowed, - itemsNotOpen, - errors, - }, - }); - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts deleted file mode 100644 index 6a81bd26dc47d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts +++ /dev/null @@ -1,26 +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 { kibanaResponseFactory } from '../../../../../../../src/core/server'; -// @ts-ignore -import { wrapEsError } from '../lib/error_wrappers'; -import { isEsError } from '../lib/is_es_error'; - -export const mapErrorToKibanaHttpResponse = (err: any) => { - if (isEsError(err)) { - const { statusCode, message, body } = wrapEsError(err); - return kibanaResponseFactory.customError({ - statusCode, - body: { - message, - attributes: { - cause: body?.cause, - }, - }, - }); - } - return kibanaResponseFactory.internalError(err); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts deleted file mode 100644 index 7e59417550691..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts +++ /dev/null @@ -1,16 +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 { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern'; -import { registerFollowerIndexRoutes } from './api/follower_index'; -import { registerCcrRoutes } from './api/ccr'; -import { RouteDependencies } from './types'; - -export function registerRoutes(deps: RouteDependencies) { - registerAutoFollowPatternRoutes(deps); - registerFollowerIndexRoutes(deps); - registerCcrRoutes(deps); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts deleted file mode 100644 index 7f57c20c536e0..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts +++ /dev/null @@ -1,13 +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 { IRouter } from 'src/core/server'; - -export interface RouteDependencies { - router: IRouter; - __LEGACY: { - server: any; - }; -} diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts deleted file mode 100644 index 5c7f8fa46c18b..0000000000000 --- a/x-pack/legacy/plugins/graph/index.ts +++ /dev/null @@ -1,32 +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. - */ - -// @ts-ignore -import migrations from './migrations'; -import mappings from './mappings.json'; -import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; - -export const graph: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - id: 'graph', - configPrefix: 'xpack.graph', - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - mappings, - migrations, - }, - - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - canEditDrillDownUrls: Joi.boolean().default(true), - savePolicy: Joi.string() - .valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']) - .default('configAndData'), - }).default(); - }, - }); -}; diff --git a/x-pack/legacy/plugins/graph/mappings.json b/x-pack/legacy/plugins/graph/mappings.json deleted file mode 100644 index f1950c459eee5..0000000000000 --- a/x-pack/legacy/plugins/graph/mappings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "graph-workspace": { - "properties": { - "description": { - "type": "text" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "numLinks": { - "type": "integer" - }, - "numVertices": { - "type": "integer" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - }, - "wsState": { - "type": "text" - } - } - } -} diff --git a/x-pack/legacy/plugins/graph/migrations.js b/x-pack/legacy/plugins/graph/migrations.js deleted file mode 100644 index 0cefe6217b45d..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.js +++ /dev/null @@ -1,41 +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 { get } from 'lodash'; - -export default { - 'graph-workspace': { - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - // Migrate index pattern - const wsState = get(doc, 'attributes.wsState'); - if (typeof wsState !== 'string') { - return doc; - } - let state; - try { - state = JSON.parse(JSON.parse(wsState)); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc; - } - const { indexPattern } = state; - if (!indexPattern) { - return doc; - } - state.indexPatternRefName = 'indexPattern_0'; - delete state.indexPattern; - doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); - doc.references.push({ - name: 'indexPattern_0', - type: 'index-pattern', - id: indexPattern, - }); - return doc; - }, - }, -}; diff --git a/x-pack/legacy/plugins/graph/migrations.test.js b/x-pack/legacy/plugins/graph/migrations.test.js deleted file mode 100644 index 93162d94857ce..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.test.js +++ /dev/null @@ -1,102 +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 migrations from './migrations'; - -describe('graph-workspace', () => { - describe('7.0.0', () => { - const migration = migrations['graph-workspace']['7.0.0']; - - test('returns doc on empty object', () => { - expect(migration({})).toMatchInlineSnapshot(` -Object { - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not a string', () => { - const doc = { - id: '1', - attributes: { - wsState: true, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": true, - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not valid JSON', () => { - const doc = { - id: '1', - attributes: { - wsState: '123abc', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "123abc", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when "indexPattern" is missing from wsState', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true })), - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('extract "indexPattern" attribute from doc', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), - bar: true, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "bar": true, - "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "indexPattern_0", - "type": "index-pattern", - }, - ], -} -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/index_management/index.ts b/x-pack/legacy/plugins/index_management/index.ts deleted file mode 100644 index afca15203b970..0000000000000 --- a/x-pack/legacy/plugins/index_management/index.ts +++ /dev/null @@ -1,13 +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. - */ - -// TODO: Remove this once CCR is migrated to the plugins directory. -export function indexManagement(kibana: any) { - return new kibana.Plugin({ - id: 'index_management', - configPrefix: 'xpack.index_management', - }); -} diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts index 47c6478f66471..df9923d9f11ec 100644 --- a/x-pack/legacy/plugins/ingest_manager/index.ts +++ b/x-pack/legacy/plugins/ingest_manager/index.ts @@ -4,43 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { resolve } from 'path'; -import { - savedObjectMappings, - OUTPUT_SAVED_OBJECT_TYPE, - AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, - PACKAGES_SAVED_OBJECT_TYPE, -} from '../../../plugins/ingest_manager/server'; - -// TODO https://github.com/elastic/kibana/issues/46373 -// const INDEX_NAMES = { -// INGEST: '.kibana', -// }; export function ingestManager(kibana: any) { return new kibana.Plugin({ id: 'ingestManager', publicDir: resolve(__dirname, '../../../plugins/ingest_manager/public'), - uiExports: { - savedObjectSchemas: { - [AGENT_CONFIG_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - [OUTPUT_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - [DATASOURCE_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - [PACKAGES_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - }, - mappings: savedObjectMappings, - }, }); } diff --git a/x-pack/legacy/plugins/logstash/README.md b/x-pack/legacy/plugins/logstash/README.md deleted file mode 100755 index 7d181249300fa..0000000000000 --- a/x-pack/legacy/plugins/logstash/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Logstash Plugin - -This plugin adds Logstash specific UI code to x-pack. Currently this plugin adds just the management features. diff --git a/x-pack/legacy/plugins/logstash/common/lib/__tests__/get_moment.js b/x-pack/legacy/plugins/logstash/common/lib/__tests__/get_moment.js deleted file mode 100755 index 2e63b231bec32..0000000000000 --- a/x-pack/legacy/plugins/logstash/common/lib/__tests__/get_moment.js +++ /dev/null @@ -1,33 +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 expect from '@kbn/expect'; -import { getMoment } from '../get_moment'; - -describe('get_moment', () => { - describe('getMoment', () => { - it(`returns a moment object when passed a date`, () => { - const moment = getMoment('2017-03-30T14:53:08.121Z'); - - expect(moment.constructor.name).to.be('Moment'); - }); - - it(`returns null when passed falsy`, () => { - const results = [ - getMoment(false), - getMoment(0), - getMoment(''), - getMoment(null), - getMoment(undefined), - getMoment(NaN), - ]; - - results.forEach(result => { - expect(result).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/common/lib/get_moment.js b/x-pack/legacy/plugins/logstash/common/lib/get_moment.js deleted file mode 100755 index 7a808ec9a0336..0000000000000 --- a/x-pack/legacy/plugins/logstash/common/lib/get_moment.js +++ /dev/null @@ -1,15 +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 moment from 'moment'; - -export function getMoment(date) { - if (!date) { - return null; - } - - return moment(date); -} diff --git a/x-pack/legacy/plugins/logstash/common/lib/index.js b/x-pack/legacy/plugins/logstash/common/lib/index.js deleted file mode 100755 index 6ed1d24a37791..0000000000000 --- a/x-pack/legacy/plugins/logstash/common/lib/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { getMoment } from './get_moment'; diff --git a/x-pack/legacy/plugins/logstash/index.js b/x-pack/legacy/plugins/logstash/index.js deleted file mode 100755 index 29f01032f3413..0000000000000 --- a/x-pack/legacy/plugins/logstash/index.js +++ /dev/null @@ -1,32 +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 { resolve } from 'path'; -import { registerLicenseChecker } from './server/lib/register_license_checker'; -import { PLUGIN } from '../../../plugins/logstash/common/constants'; - -export const logstash = kibana => - new kibana.Plugin({ - id: PLUGIN.ID, - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - configPrefix: 'xpack.logstash', - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - uiExports: { - managementSections: [ - 'plugins/logstash/sections/pipeline_list', - 'plugins/logstash/sections/pipeline_edit', - ], - home: ['plugins/logstash/lib/register_home_feature'], - }, - init: server => { - registerLicenseChecker(server); - }, - }); diff --git a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts b/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts deleted file mode 100644 index 2e1ee2afb9ce6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/lib/register_home_feature.ts +++ /dev/null @@ -1,35 +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 { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -// @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; -// @ts-ignore -import { PLUGIN } from '../../../../../plugins/logstash/common/constants'; - -const { - plugins: { home }, -} = npSetup; - -const enableLinks = Boolean(xpackInfo.get(`features.${PLUGIN.ID}.enableLinks`)); - -if (enableLinks) { - home.featureCatalogue.register({ - id: 'management_logstash', - title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { - defaultMessage: 'Logstash Pipelines', - }), - description: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesDescription', { - defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', - }), - icon: 'pipelineApp', - path: '/app/kibana#/management/logstash/pipelines', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }); -} diff --git a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/index.js b/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/index.js deleted file mode 100755 index 9d53d4dd61163..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { updateLogstashSections } from './update_logstash_sections'; diff --git a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js b/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js deleted file mode 100755 index 8da687529c846..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/lib/update_management_sections/update_logstash_sections.js +++ /dev/null @@ -1,22 +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 { management } from 'ui/management'; - -export function updateLogstashSections(pipelineId) { - const editSection = management.getSection('logstash/pipelines/pipeline/edit'); - const newSection = management.getSection('logstash/pipelines/pipeline/new'); - - newSection.hide(); - editSection.hide(); - - if (pipelineId) { - editSection.url = `#/management/logstash/pipelines/pipeline/${pipelineId}/edit`; - editSection.show(); - } else { - newSection.show(); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/sections/breadcrumbs.js b/x-pack/legacy/plugins/logstash/public/sections/breadcrumbs.js deleted file mode 100644 index 3121a58ff6a74..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/breadcrumbs.js +++ /dev/null @@ -1,41 +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 { i18n } from '@kbn/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; - -export function getPipelineListBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.logstash.pipelines.listBreadcrumb', { - defaultMessage: 'Pipelines', - }), - href: '#/management/logstash/pipelines', - }, - ]; -} - -export function getPipelineEditBreadcrumbs($route) { - const { pipeline } = $route.current.locals; - return [ - ...getPipelineListBreadcrumbs(), - { - text: pipeline.id, - }, - ]; -} - -export function getPipelineCreateBreadcrumbs() { - return [ - ...getPipelineListBreadcrumbs(), - { - text: i18n.translate('xpack.logstash.pipelines.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]; -} diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js deleted file mode 100755 index 5889bbdf96a93..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/index.js +++ /dev/null @@ -1,7 +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 './pipeline_edit'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js deleted file mode 100755 index 83446278fdeca..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/pipeline_edit/pipeline_edit.js +++ /dev/null @@ -1,60 +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 React from 'react'; -import { render } from 'react-dom'; -import { isEmpty } from 'lodash'; -import { uiModules } from 'ui/modules'; -import { npSetup } from 'ui/new_platform'; -import { toastNotifications } from 'ui/notify'; -import { I18nContext } from 'ui/i18n'; -import { PipelineEditor } from '../../../../components/pipeline_editor'; -import 'plugins/logstash/services/license'; -import { logstashSecurity } from 'plugins/logstash/services/security'; -import 'ace'; - -const app = uiModules.get('xpack/logstash'); - -app.directive('pipelineEdit', function($injector) { - const pipelineService = $injector.get('pipelineService'); - const licenseService = $injector.get('logstashLicenseService'); - const kbnUrl = $injector.get('kbnUrl'); - const $route = $injector.get('$route'); - - return { - restrict: 'E', - link: async (scope, el) => { - const close = () => scope.$evalAsync(kbnUrl.change('/management/logstash/pipelines', {})); - const open = id => - scope.$evalAsync(kbnUrl.change(`/management/logstash/pipelines/${id}/edit`)); - - const userResource = logstashSecurity.isSecurityEnabled() - ? await npSetup.plugins.security.authc.getCurrentUser() - : null; - - render( - <I18nContext> - <PipelineEditor - kbnUrl={kbnUrl} - close={close} - open={open} - isNewPipeline={isEmpty(scope.pipeline.id)} - username={userResource ? userResource.username : null} - pipeline={scope.pipeline} - pipelineService={pipelineService} - routeService={$route} - toastNotifications={toastNotifications} - licenseService={licenseService} - /> - </I18nContext>, - el[0] - ); - }, - scope: { - pipeline: '=', - }, - }; -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js deleted file mode 100755 index 3a9a6b860c51f..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/index.js +++ /dev/null @@ -1,7 +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 './upgrade_failure'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js deleted file mode 100755 index 2ef99d3b47672..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/components/upgrade_failure/upgrade_failure.js +++ /dev/null @@ -1,49 +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 React from 'react'; -import { render } from 'react-dom'; -import { isEmpty } from 'lodash'; -import { uiModules } from 'ui/modules'; -import { I18nContext } from 'ui/i18n'; -import { UpgradeFailure } from '../../../../components/upgrade_failure'; - -const app = uiModules.get('xpack/logstash'); - -app.directive('upgradeFailure', $injector => { - const $route = $injector.get('$route'); - const kbnUrl = $injector.get('kbnUrl'); - - return { - link: (scope, el) => { - const onRetry = () => { - $route.updateParams({ retry: true }); - $route.reload(); - }; - const onClose = () => { - scope.$evalAsync(kbnUrl.change('management/logstash/pipelines', {})); - }; - const isNewPipeline = isEmpty(scope.pipeline.id); - const isManualUpgrade = !!$route.current.params.retry; - - render( - <I18nContext> - <UpgradeFailure - isNewPipeline={isNewPipeline} - isManualUpgrade={isManualUpgrade} - onRetry={onRetry} - onClose={onClose} - /> - </I18nContext>, - el[0] - ); - }, - restrict: 'E', - scope: { - pipeline: '=', - }, - }; -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/index.js deleted file mode 100755 index 4b699ed79cd26..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/index.js +++ /dev/null @@ -1,7 +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 './pipeline_edit_route'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html deleted file mode 100755 index e1c422d46dfdb..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.html +++ /dev/null @@ -1,5 +0,0 @@ -<kbn-management-app section="logstash/pipelines" omit-breadcrumb-pages="['pipeline']"> - <pipeline-edit ng-if="pipelineEditRoute.isUpgraded" pipeline="pipelineEditRoute.pipeline"></pipeline-edit> - - <upgrade-failure ng-if="!pipelineEditRoute.isUpgraded" pipeline="pipelineEditRoute.pipeline"></upgrade-failure> -</kbn-management-app> diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js deleted file mode 100755 index 733f7dc3ae2e6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_edit/pipeline_edit_route.js +++ /dev/null @@ -1,87 +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 routes from 'ui/routes'; -import { toastNotifications } from 'ui/notify'; -import { i18n } from '@kbn/i18n'; -import template from './pipeline_edit_route.html'; -import 'plugins/logstash/services/pipeline'; -import 'plugins/logstash/services/license'; -import 'plugins/logstash/services/upgrade'; -import './components/pipeline_edit'; -import './components/upgrade_failure'; -import { updateLogstashSections } from 'plugins/logstash/lib/update_management_sections'; -import { Pipeline } from 'plugins/logstash/models/pipeline'; -import { getPipelineCreateBreadcrumbs, getPipelineEditBreadcrumbs } from '../breadcrumbs'; - -routes - .when('/management/logstash/pipelines/pipeline/:id/edit', { - k7Breadcrumbs: getPipelineEditBreadcrumbs, - }) - .when('/management/logstash/pipelines/new-pipeline', { - k7Breadcrumbs: getPipelineCreateBreadcrumbs, - }) - .defaults(/management\/logstash\/pipelines\/(new-pipeline|pipeline\/:id\/edit)/, { - template: template, - controller: class PipelineEditRouteController { - constructor($injector) { - const $route = $injector.get('$route'); - this.pipeline = $route.current.locals.pipeline; - this.isUpgraded = $route.current.locals.isUpgraded; - } - }, - controllerAs: 'pipelineEditRoute', - resolve: { - logstashTabs: $injector => { - const $route = $injector.get('$route'); - const pipelineId = $route.current.params.id; - updateLogstashSections(pipelineId); - }, - pipeline: function($injector) { - const $route = $injector.get('$route'); - const pipelineService = $injector.get('pipelineService'); - const licenseService = $injector.get('logstashLicenseService'); - const kbnUrl = $injector.get('kbnUrl'); - - const pipelineId = $route.current.params.id; - - if (!pipelineId) return new Pipeline(); - - return pipelineService - .loadPipeline(pipelineId) - .then(pipeline => (!!$route.current.params.clone ? pipeline.clone : pipeline)) - .catch(err => { - return licenseService.checkValidity().then(() => { - if (err.status !== 403) { - toastNotifications.addDanger( - i18n.translate('xpack.logstash.couldNotLoadPipelineErrorNotification', { - defaultMessage: `Couldn't load pipeline. Error: '{errStatusText}'.`, - values: { - errStatusText: err.statusText, - }, - }) - ); - } - - kbnUrl.redirect('/management/logstash/pipelines'); - return Promise.reject(); - }); - }); - }, - checkLicense: $injector => { - const licenseService = $injector.get('logstashLicenseService'); - return licenseService.checkValidity(); - }, - isUpgraded: $injector => { - const upgradeService = $injector.get('upgradeService'); - return upgradeService.executeUpgrade(); - }, - }, - }); - -routes.when('/management/logstash/pipelines/pipeline/:id', { - redirectTo: '/management/logstash/pipelines/pipeline/:id/edit', -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js deleted file mode 100755 index 7e8ca0e4c2c57..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/index.js +++ /dev/null @@ -1,7 +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 './pipeline_list'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js deleted file mode 100755 index b856979aed8b6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/components/pipeline_list/pipeline_list.js +++ /dev/null @@ -1,58 +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 React from 'react'; -import { render } from 'react-dom'; -import { uiModules } from 'ui/modules'; -import { toastNotifications } from 'ui/notify'; -import { I18nContext } from 'ui/i18n'; -import { PipelineList } from '../../../../components/pipeline_list'; -import 'plugins/logstash/services/pipelines'; -import 'plugins/logstash/services/license'; -import 'plugins/logstash/services/cluster'; -import 'plugins/logstash/services/monitoring'; - -const app = uiModules.get('xpack/logstash'); - -app.directive('pipelineList', function($injector) { - const pipelinesService = $injector.get('pipelinesService'); - const licenseService = $injector.get('logstashLicenseService'); - const clusterService = $injector.get('xpackLogstashClusterService'); - const monitoringService = $injector.get('xpackLogstashMonitoringService'); - const kbnUrl = $injector.get('kbnUrl'); - - return { - restrict: 'E', - link: (scope, el) => { - const openPipeline = id => - scope.$evalAsync(kbnUrl.change(`management/logstash/pipelines/pipeline/${id}/edit`)); - const createPipeline = () => - scope.$evalAsync(kbnUrl.change('management/logstash/pipelines/new-pipeline')); - const clonePipeline = id => - scope.$evalAsync(kbnUrl.change(`management/logstash/pipelines/pipeline/${id}/edit?clone`)); - render( - <I18nContext> - <PipelineList - clonePipeline={clonePipeline} - clusterService={clusterService} - isReadOnly={licenseService.isReadOnly} - isForbidden={true} - isLoading={false} - licenseService={licenseService} - monitoringService={monitoringService} - openPipeline={openPipeline} - createPipeline={createPipeline} - pipelinesService={pipelinesService} - toastNotifications={toastNotifications} - /> - </I18nContext>, - el[0] - ); - }, - scope: {}, - controllerAs: 'pipelineList', - }; -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/index.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/index.js deleted file mode 100755 index f60decd1378d5..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/index.js +++ /dev/null @@ -1,8 +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 './register_management_section'; -import './pipeline_list_route'; diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html deleted file mode 100755 index 55b3fabd70161..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.html +++ /dev/null @@ -1,3 +0,0 @@ -<kbn-management-app section="logstash/pipelines"> - <pipeline-list></pipeline-list> -</kbn-management-app> diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js deleted file mode 100755 index eb593207572c2..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/pipeline_list_route.js +++ /dev/null @@ -1,33 +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 routes from 'ui/routes'; -import { management } from 'ui/management'; -import template from './pipeline_list_route.html'; -import './components/pipeline_list'; -import 'plugins/logstash/services/license'; -import { getPipelineListBreadcrumbs } from '../breadcrumbs'; - -routes.when('/management/logstash/pipelines/', { - template, - k7Breadcrumbs: getPipelineListBreadcrumbs, -}); - -routes.defaults(/\/management/, { - resolve: { - logstashManagementSection: $injector => { - const licenseService = $injector.get('logstashLicenseService'); - const logstashSection = management.getSection('logstash/pipelines'); - - if (licenseService.enableLinks) { - logstashSection.show(); - logstashSection.enable(); - } else { - logstashSection.hide(); - } - }, - }, -}); diff --git a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/register_management_section.js b/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/register_management_section.js deleted file mode 100755 index e285418f5f2ae..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/sections/pipeline_list/register_management_section.js +++ /dev/null @@ -1,36 +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 { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; - -management.getSection('logstash').register('pipelines', { - display: i18n.translate('xpack.logstash.managementSection.pipelinesTitle', { - defaultMessage: 'Pipelines', - }), - order: 10, - url: '#/management/logstash/pipelines/', -}); - -management.getSection('logstash/pipelines').register('pipeline', { - visible: false, -}); - -management.getSection('logstash/pipelines/pipeline').register('edit', { - display: i18n.translate('xpack.logstash.managementSection.editPipelineTitle', { - defaultMessage: 'Edit pipeline', - }), - order: 1, - visible: false, -}); - -management.getSection('logstash/pipelines/pipeline').register('new', { - display: i18n.translate('xpack.logstash.managementSection.createPipelineTitle', { - defaultMessage: 'Create pipeline', - }), - order: 1, - visible: false, -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.factory.js deleted file mode 100755 index 0fee2804c704d..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.factory.js +++ /dev/null @@ -1,13 +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 { uiModules } from 'ui/modules'; -import { ClusterService } from './cluster_service'; - -uiModules.get('xpack/logstash').factory('xpackLogstashClusterService', $injector => { - const $http = $injector.get('$http'); - return new ClusterService($http); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js b/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js deleted file mode 100755 index e89c2fe7d11bf..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/cluster/cluster_service.js +++ /dev/null @@ -1,31 +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 chrome from 'ui/chrome'; -import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; -import { Cluster } from 'plugins/logstash/models/cluster'; - -export class ClusterService { - constructor($http) { - this.$http = $http; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - loadCluster() { - return this.$http.get(`${this.basePath}/cluster`).then(response => { - if (!response.data) { - return; - } - return Cluster.fromUpstreamJSON(response.data.cluster); - }); - } - - isClusterInfoAvailable() { - return this.loadCluster() - .then(cluster => Boolean(cluster)) - .catch(() => false); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/cluster/index.js b/x-pack/legacy/plugins/logstash/public/services/cluster/index.js deleted file mode 100755 index ba52657a27ca8..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/cluster/index.js +++ /dev/null @@ -1,7 +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 './cluster_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/license/index.js b/x-pack/legacy/plugins/logstash/public/services/license/index.js deleted file mode 100755 index 8be8fb5ccbc64..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/license/index.js +++ /dev/null @@ -1,7 +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 './license_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/license/license_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/license/license_service.factory.js deleted file mode 100755 index 0e131f9b94008..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/license/license_service.factory.js +++ /dev/null @@ -1,14 +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 { uiModules } from 'ui/modules'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import 'ui/url'; -import { LogstashLicenseService } from './logstash_license_service'; - -uiModules.get('xpack/logstash').factory('logstashLicenseService', ($timeout, kbnUrl) => { - return new LogstashLicenseService(xpackInfo, kbnUrl, $timeout); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js b/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js deleted file mode 100755 index 69cc8614a6ae2..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/license/logstash_license_service.js +++ /dev/null @@ -1,62 +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 React from 'react'; -import { toastNotifications } from 'ui/notify'; -import { MarkdownSimple } from '../../../../../../../src/plugins/kibana_react/public'; -import { PLUGIN } from '../../../../../../plugins/logstash/common/constants'; - -export class LogstashLicenseService { - constructor(xpackInfoService, kbnUrlService, $timeout) { - this.xpackInfoService = xpackInfoService; - this.kbnUrlService = kbnUrlService; - this.$timeout = $timeout; - } - - get enableLinks() { - return Boolean(this.xpackInfoService.get(`features.${PLUGIN.ID}.enableLinks`)); - } - - get isAvailable() { - return Boolean(this.xpackInfoService.get(`features.${PLUGIN.ID}.isAvailable`)); - } - - get isReadOnly() { - return Boolean(this.xpackInfoService.get(`features.${PLUGIN.ID}.isReadOnly`)); - } - - get message() { - return this.xpackInfoService.get(`features.${PLUGIN.ID}.message`); - } - - notifyAndRedirect() { - toastNotifications.addDanger({ - title: ( - <MarkdownSimple> - {this.xpackInfoService.get(`features.${PLUGIN.ID}.message`)} - </MarkdownSimple> - ), - }); - this.kbnUrlService.redirect('/management'); - } - - /** - * Checks if the license is valid or the license can perform downgraded UI tasks. - * Otherwise, notifies and redirects. - */ - checkValidity() { - return new Promise((resolve, reject) => { - this.$timeout(() => { - if (this.isAvailable) { - return resolve(); - } - - this.notifyAndRedirect(); - return reject(); - }, 10); // To allow latest XHR call to update license info - }); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/monitoring/index.js b/x-pack/legacy/plugins/logstash/public/services/monitoring/index.js deleted file mode 100755 index 83b2105beb5ef..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/monitoring/index.js +++ /dev/null @@ -1,7 +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 './monitoring_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.factory.js deleted file mode 100755 index 271c776dd6f69..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.factory.js +++ /dev/null @@ -1,18 +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 { uiModules } from 'ui/modules'; -import { MonitoringService } from './monitoring_service'; -import '../cluster'; - -uiModules.get('xpack/logstash').factory('xpackLogstashMonitoringService', $injector => { - const $http = $injector.get('$http'); - const Promise = $injector.get('Promise'); - const monitoringUiEnabled = - $injector.has('monitoringUiEnabled') && $injector.get('monitoringUiEnabled'); - const clusterService = $injector.get('xpackLogstashClusterService'); - return new MonitoringService($http, Promise, monitoringUiEnabled, clusterService); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js b/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js deleted file mode 100755 index 6103e730c2171..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/monitoring/monitoring_service.js +++ /dev/null @@ -1,48 +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 moment from 'moment'; -import chrome from 'ui/chrome'; -import { ROUTES, MONITORING } from '../../../../../../plugins/logstash/common/constants'; -import { PipelineListItem } from 'plugins/logstash/models/pipeline_list_item'; - -export class MonitoringService { - constructor($http, Promise, monitoringUiEnabled, clusterService) { - this.$http = $http; - this.Promise = Promise; - this.monitoringUiEnabled = monitoringUiEnabled; - this.clusterService = clusterService; - this.basePath = chrome.addBasePath(ROUTES.MONITORING_API_ROOT); - } - - isMonitoringEnabled() { - return this.monitoringUiEnabled; - } - - getPipelineList() { - if (!this.isMonitoringEnabled()) { - return Promise.resolve([]); - } - - return this.clusterService - .loadCluster() - .then(cluster => { - const url = `${this.basePath}/v1/clusters/${cluster.uuid}/logstash/pipeline_ids`; - const now = moment.utc(); - const body = { - timeRange: { - max: now.toISOString(), - min: now.subtract(MONITORING.ACTIVE_PIPELINE_RANGE_S, 'seconds').toISOString(), - }, - }; - return this.$http.post(url, body); - }) - .then(response => - response.data.map(pipeline => PipelineListItem.fromUpstreamMonitoringJSON(pipeline)) - ) - .catch(() => []); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/pipeline/index.js b/x-pack/legacy/plugins/logstash/public/services/pipeline/index.js deleted file mode 100755 index 3b0e28bd555e6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipeline/index.js +++ /dev/null @@ -1,7 +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 './pipeline_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.factory.js deleted file mode 100755 index cf93915425213..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.factory.js +++ /dev/null @@ -1,14 +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 { uiModules } from 'ui/modules'; -import { PipelineService } from './pipeline_service'; - -uiModules.get('xpack/logstash').factory('pipelineService', $injector => { - const $http = $injector.get('$http'); - const pipelinesService = $injector.get('pipelinesService'); - return new PipelineService($http, pipelinesService); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js b/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js deleted file mode 100755 index b5d0dbeb852d5..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipeline/pipeline_service.js +++ /dev/null @@ -1,40 +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 chrome from 'ui/chrome'; -import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; -import { Pipeline } from 'plugins/logstash/models/pipeline'; - -export class PipelineService { - constructor($http, pipelinesService) { - this.$http = $http; - this.pipelinesService = pipelinesService; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - loadPipeline(id) { - return this.$http.get(`${this.basePath}/pipeline/${id}`).then(response => { - return Pipeline.fromUpstreamJSON(response.data); - }); - } - - savePipeline(pipelineModel) { - return this.$http - .put(`${this.basePath}/pipeline/${pipelineModel.id}`, pipelineModel.upstreamJSON) - .catch(e => { - throw e.data.message; - }); - } - - deletePipeline(id) { - return this.$http - .delete(`${this.basePath}/pipeline/${id}`) - .then(() => this.pipelinesService.addToRecentlyDeleted(id)) - .catch(e => { - throw e.data.message; - }); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/pipelines/index.js b/x-pack/legacy/plugins/logstash/public/services/pipelines/index.js deleted file mode 100755 index e273e12d46c6d..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipelines/index.js +++ /dev/null @@ -1,7 +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 './pipelines_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.factory.js deleted file mode 100755 index 9295949e001eb..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.factory.js +++ /dev/null @@ -1,17 +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 { uiModules } from 'ui/modules'; -import { PipelinesService } from './pipelines_service'; -import '../monitoring'; - -uiModules.get('xpack/logstash').factory('pipelinesService', $injector => { - const $http = $injector.get('$http'); - const $window = $injector.get('$window'); - const Promise = $injector.get('Promise'); - const monitoringService = $injector.get('xpackLogstashMonitoringService'); - return new PipelinesService($http, $window, Promise, monitoringService); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js b/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js deleted file mode 100755 index d70c8be06fde4..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/pipelines/pipelines_service.js +++ /dev/null @@ -1,135 +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 chrome from 'ui/chrome'; -import { ROUTES, MONITORING } from '../../../../../../plugins/logstash/common/constants'; -import { PipelineListItem } from 'plugins/logstash/models/pipeline_list_item'; - -const RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY = 'xpack.logstash.recentlyDeletedPipelines'; - -export class PipelinesService { - constructor($http, $window, Promise, monitoringService) { - this.$http = $http; - this.$window = $window; - this.Promise = Promise; - this.monitoringService = monitoringService; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - getPipelineList() { - return this.Promise.all([ - this.getManagementPipelineList(), - this.getMonitoringPipelineList(), - ]).then(([managementPipelines, monitoringPipelines]) => { - const now = Date.now(); - - // Monitoring will report centrally-managed pipelines as well, including recently-deleted centrally-managed ones. - // If there's a recently-deleted pipeline we're keeping track of BUT monitoring doesn't report it, that means - // it's not running in Logstash any more. So we can stop tracking it as a recently-deleted pipeline. - const monitoringPipelineIds = monitoringPipelines.map(pipeline => pipeline.id); - this.getRecentlyDeleted().forEach(recentlyDeletedPipeline => { - // We don't want to stop tracking the recently-deleted pipeline until Monitoring has had some - // time to report on it. Otherwise, if we stop tracking first, *then* Monitoring reports it, we'll - // still end up showing it in the list until Monitoring stops reporting it. - if (now - recentlyDeletedPipeline.deletedOn < MONITORING.ACTIVE_PIPELINE_RANGE_S * 1000) { - return; - } - - // If Monitoring is still reporting the pipeline, don't stop tracking it yet - if (monitoringPipelineIds.includes(recentlyDeletedPipeline.id)) { - return; - } - - this.removeFromRecentlyDeleted(recentlyDeletedPipeline.id); - }); - - // Merge centrally-managed pipelines with pipelines reported by monitoring. Take care to dedupe - // while merging because monitoring will (rightly) report centrally-managed pipelines as well, - // including recently-deleted ones! - const managementPipelineIds = managementPipelines.map(pipeline => pipeline.id); - return managementPipelines.concat( - monitoringPipelines.filter( - monitoringPipeline => - !managementPipelineIds.includes(monitoringPipeline.id) && - !this.isRecentlyDeleted(monitoringPipeline.id) - ) - ); - }); - } - - getManagementPipelineList() { - return this.$http - .get(`${this.basePath}/pipelines`) - .then(response => - response.data.pipelines.map(pipeline => PipelineListItem.fromUpstreamJSON(pipeline)) - ); - } - - getMonitoringPipelineList() { - return this.monitoringService.getPipelineList(); - } - - /** - * Delete a collection of pipelines - * - * @param pipelineIds Array of pipeline IDs - * @return Promise { numSuccesses, numErrors } - */ - deletePipelines(pipelineIds) { - const body = { - pipelineIds, - }; - return this.$http.post(`${this.basePath}/pipelines/delete`, body).then(response => { - this.addToRecentlyDeleted(...pipelineIds); - return response.data.results; - }); - } - - addToRecentlyDeleted(...pipelineIds) { - const recentlyDeletedPipelines = this.getRecentlyDeleted(); - const recentlyDeletedPipelineIds = recentlyDeletedPipelines.map(pipeline => pipeline.id); - pipelineIds.forEach(pipelineId => { - if (!recentlyDeletedPipelineIds.includes(pipelineId)) { - recentlyDeletedPipelines.push({ - id: pipelineId, - deletedOn: Date.now(), - }); - } - }); - this.setRecentlyDeleted(recentlyDeletedPipelines); - } - - removeFromRecentlyDeleted(...pipelineIds) { - const recentlyDeletedPipelinesToKeep = this.getRecentlyDeleted().filter( - recentlyDeletedPipeline => !pipelineIds.includes(recentlyDeletedPipeline.id) - ); - this.setRecentlyDeleted(recentlyDeletedPipelinesToKeep); - } - - isRecentlyDeleted(pipelineId) { - return this.getRecentlyDeleted() - .map(pipeline => pipeline.id) - .includes(pipelineId); - } - - getRecentlyDeleted() { - const recentlyDeletedPipelines = this.$window.localStorage.getItem( - RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY - ); - if (!recentlyDeletedPipelines) { - return []; - } - - return JSON.parse(recentlyDeletedPipelines); - } - - setRecentlyDeleted(recentlyDeletedPipelineIds) { - this.$window.localStorage.setItem( - RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY, - JSON.stringify(recentlyDeletedPipelineIds) - ); - } -} diff --git a/x-pack/legacy/plugins/logstash/public/services/security/index.js b/x-pack/legacy/plugins/logstash/public/services/security/index.js deleted file mode 100755 index c9ff911723156..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/security/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { logstashSecurity } from './logstash_security'; diff --git a/x-pack/legacy/plugins/logstash/public/services/security/logstash_security.js b/x-pack/legacy/plugins/logstash/public/services/security/logstash_security.js deleted file mode 100755 index 0949038c9b6c7..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/security/logstash_security.js +++ /dev/null @@ -1,13 +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 { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -export const logstashSecurity = { - isSecurityEnabled() { - return Boolean(xpackInfo.get(`features.security`)); - }, -}; diff --git a/x-pack/legacy/plugins/logstash/public/services/upgrade/index.js b/x-pack/legacy/plugins/logstash/public/services/upgrade/index.js deleted file mode 100755 index 345d0d0ff68c6..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/upgrade/index.js +++ /dev/null @@ -1,7 +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 './upgrade_service.factory'; diff --git a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.factory.js b/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.factory.js deleted file mode 100755 index 925c6ae677bdf..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.factory.js +++ /dev/null @@ -1,13 +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 { uiModules } from 'ui/modules'; -import { UpgradeService } from './upgrade_service'; - -uiModules.get('xpack/logstash').factory('upgradeService', $injector => { - const $http = $injector.get('$http'); - return new UpgradeService($http); -}); diff --git a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js b/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js deleted file mode 100755 index 2019bdc1bf1aa..0000000000000 --- a/x-pack/legacy/plugins/logstash/public/services/upgrade/upgrade_service.js +++ /dev/null @@ -1,24 +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 chrome from 'ui/chrome'; -import { ROUTES } from '../../../../../../plugins/logstash/common/constants'; - -export class UpgradeService { - constructor($http) { - this.$http = $http; - this.basePath = chrome.addBasePath(ROUTES.API_ROOT); - } - - executeUpgrade() { - return this.$http - .post(`${this.basePath}/upgrade`) - .then(response => response.data.is_upgraded) - .catch(e => { - throw e.data.message; - }); - } -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/check_license/__tests__/check_license.js b/x-pack/legacy/plugins/logstash/server/lib/check_license/__tests__/check_license.js deleted file mode 100755 index 5fcce0aaa1219..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/check_license/__tests__/check_license.js +++ /dev/null @@ -1,179 +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 expect from '@kbn/expect'; -import { set } from 'lodash'; -import { checkLicense } from '../check_license'; - -describe('check_license', function() { - let mockLicenseInfo; - beforeEach(() => (mockLicenseInfo = {})); - - describe('license information is undefined', () => { - beforeEach(() => (mockLicenseInfo = undefined)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => 'basic'); - }); - - describe('& license is > basic', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', () => true); - mockLicenseInfo.feature = () => ({ isEnabled: () => true }); // Security feature is enabled - }); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should set isReadOnly to true', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(true); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', () => false); - mockLicenseInfo.feature = () => ({ isEnabled: () => true }); // Security feature is enabled - }); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& security is disabled', () => { - beforeEach(() => { - mockLicenseInfo.feature = () => ({ isEnabled: () => false }); // Security feature is disabled - set(mockLicenseInfo, 'license.isOneOf', () => true); - set(mockLicenseInfo, 'license.isActive', () => true); - }); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set isReadOnly to false', () => { - expect(checkLicense(mockLicenseInfo).isReadOnly).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/logstash/server/lib/check_license/check_license.js b/x-pack/legacy/plugins/logstash/server/lib/check_license/check_license.js deleted file mode 100755 index 31136ae1c72a5..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/check_license/check_license.js +++ /dev/null @@ -1,86 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable the Logstash pipeline UI - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - enableLinks: false, - isReadOnly: false, - message: i18n.translate( - 'xpack.logstash.managementSection.notPossibleToManagePipelinesMessage', - { - defaultMessage: - 'You cannot manage Logstash pipelines because license information is not available at this time.', - } - ), - }; - } - - const VALID_LICENSE_MODES = ['trial', 'standard', 'gold', 'platinum', 'enterprise']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - const isSecurityEnabled = xpackLicenseInfo.feature('security').isEnabled(); - - // Security is not enabled in ES - if (!isSecurityEnabled) { - const message = i18n.translate('xpack.logstash.managementSection.enableSecurityDescription', { - defaultMessage: - 'Security must be enabled in order to use Logstash pipeline management features.' + - ' Please set xpack.security.enabled: true in your elasticsearch.yml.', - }); - return { - isAvailable: false, - enableLinks: false, - isReadOnly: false, - message, - }; - } - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - enableLinks: false, - isReadOnly: false, - message: i18n.translate('xpack.logstash.managementSection.licenseDoesNotSupportDescription', { - defaultMessage: - 'Your {licenseType} license does not support Logstash pipeline management features. Please upgrade your license.', - values: { licenseType }, - }), - }; - } - - // License is valid but not active, we go into a read-only mode. - if (!isLicenseActive) { - return { - isAvailable: true, - enableLinks: true, - isReadOnly: true, - message: i18n.translate( - 'xpack.logstash.managementSection.pipelineCrudOperationsNotAllowedDescription', - { - defaultMessage: - 'You cannot edit, create, or delete your Logstash pipelines because your {licenseType} license has expired.', - values: { licenseType }, - } - ), - }; - } - - // License is valid and active - return { - isAvailable: true, - enableLinks: true, - isReadOnly: false, - }; -} diff --git a/x-pack/legacy/plugins/logstash/server/lib/check_license/index.js b/x-pack/legacy/plugins/logstash/server/lib/check_license/index.js deleted file mode 100755 index f2c070fd44b6e..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/check_license/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { checkLicense } from './check_license'; diff --git a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/index.js b/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/index.js deleted file mode 100755 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js deleted file mode 100755 index a0d06e77b410d..0000000000000 --- a/x-pack/legacy/plugins/logstash/server/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,21 +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 { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status'; -import { checkLicense } from '../check_license'; -import { PLUGIN } from '../../../../../../plugins/logstash/common/constants'; - -export function registerLicenseChecker(server) { - const xpackMainPlugin = server.plugins.xpack_main; - const logstashPlugin = server.plugins.logstash; - - mirrorPluginStatus(xpackMainPlugin, logstashPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/maps/common/constants.ts b/x-pack/legacy/plugins/maps/common/constants.ts deleted file mode 100644 index 98945653c25dc..0000000000000 --- a/x-pack/legacy/plugins/maps/common/constants.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/constants'; diff --git a/x-pack/legacy/plugins/maps/common/descriptor_types.ts b/x-pack/legacy/plugins/maps/common/descriptor_types.ts deleted file mode 100644 index 1f0eda26e7f7d..0000000000000 --- a/x-pack/legacy/plugins/maps/common/descriptor_types.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/descriptor_types'; diff --git a/x-pack/legacy/plugins/maps/common/get_join_key.ts b/x-pack/legacy/plugins/maps/common/get_join_key.ts deleted file mode 100644 index 004f12ca08d2e..0000000000000 --- a/x-pack/legacy/plugins/maps/common/get_join_key.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/get_join_key'; diff --git a/x-pack/legacy/plugins/maps/common/i18n_getters.ts b/x-pack/legacy/plugins/maps/common/i18n_getters.ts deleted file mode 100644 index f9d186dea2e2b..0000000000000 --- a/x-pack/legacy/plugins/maps/common/i18n_getters.ts +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/i18n_getters'; diff --git a/x-pack/legacy/plugins/maps/common/parse_xml_string.js b/x-pack/legacy/plugins/maps/common/parse_xml_string.js deleted file mode 100644 index 34ec144472828..0000000000000 --- a/x-pack/legacy/plugins/maps/common/parse_xml_string.js +++ /dev/null @@ -1,8 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../plugins/maps/common/parse_xml_string'; diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index f4e01efc05f45..a1186e04ee27a 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -9,9 +9,14 @@ import mappings from './mappings.json'; import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; import { migrations } from './migrations'; -import { getAppTitle } from './common/i18n_getters'; +import { getAppTitle } from '../../../plugins/maps/common/i18n_getters'; import { MapPlugin } from './server/plugin'; -import { APP_ID, APP_ICON, createMapPath, MAP_SAVED_OBJECT_TYPE } from './common/constants'; +import { + APP_ID, + APP_ICON, + createMapPath, + MAP_SAVED_OBJECT_TYPE, +} from '../../../plugins/maps/common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; export function maps(kibana) { @@ -49,11 +54,9 @@ 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', {}), }; }, - embeddableFactories: ['plugins/maps/embeddable/map_embeddable_factory'], - home: ['plugins/maps/legacy_register_feature'], styleSheetPaths: `${__dirname}/public/index.scss`, savedObjectSchemas: { 'maps-telemetry': { @@ -78,7 +81,6 @@ export function maps(kibana) { }, mappings, migrations, - hacks: ['plugins/maps/register_vis_type_alias'], }, config(Joi) { return Joi.object({ diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index a8e69eef7a02f..d3666025082b7 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { extractReferences } from './common/migrations/references'; -import { emsRasterTileToEmsVectorTile } from './common/migrations/ems_raster_tile_to_ems_vector_tile'; -import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort'; -import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_global_query'; -import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; -import { migrateSymbolStyleDescriptor } from './common/migrations/migrate_symbol_style_descriptor'; -import { migrateUseTopHitsToScalingType } from './common/migrations/scaling_type'; -import { migrateJoinAggKey } from './common/migrations/join_agg_key'; +import { extractReferences } from '../../../plugins/maps/common/migrations/references'; +import { emsRasterTileToEmsVectorTile } from '../../../plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile'; +import { topHitsTimeToSort } from '../../../plugins/maps/common/migrations/top_hits_time_to_sort'; +import { moveApplyGlobalQueryToSources } from '../../../plugins/maps/common/migrations/move_apply_global_query'; +import { addFieldMetaOptions } from '../../../plugins/maps/common/migrations/add_field_meta_options'; +import { migrateSymbolStyleDescriptor } from '../../../plugins/maps/common/migrations/migrate_symbol_style_descriptor'; +import { migrateUseTopHitsToScalingType } from '../../../plugins/maps/common/migrations/scaling_type'; +import { migrateJoinAggKey } from '../../../plugins/maps/common/migrations/join_agg_key'; export const migrations = { map: { diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts deleted file mode 100644 index 34f8c30b51874..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts +++ /dev/null @@ -1,8 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -export * from '../../../../../plugins/maps/public/actions/map_actions'; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js deleted file mode 100644 index 7bfbf5761c5b8..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ /dev/null @@ -1,978 +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 _ from 'lodash'; -import turf from 'turf'; -import turfBooleanContains from '@turf/boolean-contains'; -import uuid from 'uuid/v4'; -import { - getLayerList, - getLayerListRaw, - getDataFilters, - getSelectedLayerId, - getMapReady, - getWaitingForMapReadyLayerListRaw, - getTransientLayerId, - getOpenTooltips, - getQuery, - getDataRequestDescriptor, -} from '../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../plugins/maps/public/reducers/ui'; -import { - cancelRequest, - registerCancelCallback, - unregisterCancelCallback, - getEventHandlers, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; -import { updateFlyout } from '../actions/ui_actions'; -import { - FEATURE_ID_PROPERTY_NAME, - LAYER_TYPE, - SOURCE_DATA_ID_ORIGIN, -} from '../../common/constants'; - -import { - SET_SELECTED_LAYER, - SET_TRANSIENT_LAYER, - UPDATE_LAYER_ORDER, - ADD_LAYER, - SET_LAYER_ERROR_STATUS, - ADD_WAITING_FOR_MAP_READY_LAYER, - CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, - REMOVE_LAYER, - SET_LAYER_VISIBILITY, - MAP_EXTENT_CHANGED, - MAP_READY, - MAP_DESTROYED, - LAYER_DATA_LOAD_STARTED, - LAYER_DATA_LOAD_ENDED, - LAYER_DATA_LOAD_ERROR, - UPDATE_SOURCE_DATA_REQUEST, - SET_JOINS, - SET_QUERY, - TRIGGER_REFRESH_TIMER, - UPDATE_LAYER_PROP, - UPDATE_LAYER_STYLE, - SET_LAYER_STYLE_META, - UPDATE_SOURCE_PROP, - SET_REFRESH_CONFIG, - SET_MOUSE_COORDINATES, - CLEAR_MOUSE_COORDINATES, - SET_GOTO, - CLEAR_GOTO, - TRACK_CURRENT_LAYER_STATE, - ROLLBACK_TO_TRACKED_LAYER_STATE, - REMOVE_TRACKED_LAYER_STATE, - SET_OPEN_TOOLTIPS, - UPDATE_DRAW_STATE, - SET_SCROLL_ZOOM, - SET_MAP_INIT_ERROR, - SET_INTERACTIVE, - DISABLE_TOOLTIP_CONTROL, - HIDE_TOOLBAR_OVERLAY, - HIDE_LAYER_CONTROL, - HIDE_VIEW_CONTROL, - SET_WAITING_FOR_READY_HIDDEN_LAYERS, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/actions/map_actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../../plugins/maps/public/actions/map_actions'; - -function getLayerLoadingCallbacks(dispatch, getState, layerId) { - return { - startLoading: (dataId, requestToken, meta) => - dispatch(startDataLoad(layerId, dataId, requestToken, meta)), - stopLoading: (dataId, requestToken, data, meta) => - dispatch(endDataLoad(layerId, dataId, requestToken, data, meta)), - onLoadError: (dataId, requestToken, errorMessage) => - dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)), - updateSourceData: newData => { - dispatch(updateSourceDataRequest(layerId, newData)); - }, - isRequestStillActive: (dataId, requestToken) => { - const dataRequest = getDataRequestDescriptor(getState(), layerId, dataId); - if (!dataRequest) { - return false; - } - return dataRequest.dataRequestToken === requestToken; - }, - registerCancelCallback: (requestToken, callback) => - dispatch(registerCancelCallback(requestToken, callback)), - }; -} - -function getLayerById(layerId, state) { - return getLayerList(state).find(layer => { - return layerId === layer.getId(); - }); -} - -async function syncDataForAllLayers(dispatch, getState, dataFilters) { - const state = getState(); - const layerList = getLayerList(state); - const syncs = layerList.map(layer => { - const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layer.getId()); - return layer.syncData({ ...loadingFunctions, dataFilters }); - }); - await Promise.all(syncs); -} - -export function cancelAllInFlightRequests() { - return (dispatch, getState) => { - getLayerList(getState()).forEach(layer => { - dispatch(clearDataRequests(layer)); - }); - }; -} - -function clearDataRequests(layer) { - return dispatch => { - layer.getInFlightRequestTokens().forEach(requestToken => { - dispatch(cancelRequest(requestToken)); - }); - dispatch({ - type: UPDATE_LAYER_PROP, - id: layer.getId(), - propName: '__dataRequests', - newValue: [], - }); - }; -} - -export function setMapInitError(errorMessage) { - return { - type: SET_MAP_INIT_ERROR, - errorMessage, - }; -} - -export function trackCurrentLayerState(layerId) { - return { - type: TRACK_CURRENT_LAYER_STATE, - layerId: layerId, - }; -} - -export function rollbackToTrackedLayerStateForSelectedLayer() { - return async (dispatch, getState) => { - const layerId = getSelectedLayerId(getState()); - await dispatch({ - type: ROLLBACK_TO_TRACKED_LAYER_STATE, - layerId: layerId, - }); - - // Ensure updateStyleMeta is triggered - // syncDataForLayer may not trigger endDataLoad if no re-fetch is required - dispatch(updateStyleMeta(layerId)); - - dispatch(syncDataForLayer(layerId)); - }; -} - -export function removeTrackedLayerStateForSelectedLayer() { - return (dispatch, getState) => { - const layerId = getSelectedLayerId(getState()); - dispatch({ - type: REMOVE_TRACKED_LAYER_STATE, - layerId: layerId, - }); - }; -} - -export function replaceLayerList(newLayerList) { - return (dispatch, getState) => { - const isMapReady = getMapReady(getState()); - if (!isMapReady) { - dispatch({ - type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, - }); - } else { - getLayerListRaw(getState()).forEach(({ id }) => { - dispatch(removeLayerFromLayerList(id)); - }); - } - - newLayerList.forEach(layerDescriptor => { - dispatch(addLayer(layerDescriptor)); - }); - }; -} - -export function cloneLayer(layerId) { - return async (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer) { - return; - } - - const clonedDescriptor = await layer.cloneDescriptor(); - dispatch(addLayer(clonedDescriptor)); - }; -} - -export function addLayer(layerDescriptor) { - return (dispatch, getState) => { - const isMapReady = getMapReady(getState()); - if (!isMapReady) { - dispatch({ - type: ADD_WAITING_FOR_MAP_READY_LAYER, - layer: layerDescriptor, - }); - return; - } - - dispatch({ - type: ADD_LAYER, - layer: layerDescriptor, - }); - dispatch(syncDataForLayer(layerDescriptor.id)); - }; -} - -// Do not use when rendering a map. Method exists to enable selectors for getLayerList when -// rendering is not needed. -export function addLayerWithoutDataSync(layerDescriptor) { - return { - type: ADD_LAYER, - layer: layerDescriptor, - }; -} - -function setLayerDataLoadErrorStatus(layerId, errorMessage) { - return dispatch => { - dispatch({ - type: SET_LAYER_ERROR_STATUS, - isInErrorState: errorMessage !== null, - layerId, - errorMessage, - }); - }; -} - -export function cleanTooltipStateForLayer(layerId, layerFeatures = []) { - return (dispatch, getState) => { - let featuresRemoved = false; - const openTooltips = getOpenTooltips(getState()) - .map(tooltipState => { - const nextFeatures = tooltipState.features.filter(tooltipFeature => { - if (tooltipFeature.layerId !== layerId) { - // feature from another layer, keep it - return true; - } - - // Keep feature if it is still in layer - return layerFeatures.some(layerFeature => { - return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; - }); - }); - - if (tooltipState.features.length !== nextFeatures.length) { - featuresRemoved = true; - } - - return { ...tooltipState, features: nextFeatures }; - }) - .filter(tooltipState => { - return tooltipState.features.length > 0; - }); - - if (featuresRemoved) { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips, - }); - } - }; -} - -export function setLayerVisibility(layerId, makeVisible) { - return async (dispatch, getState) => { - //if the current-state is invisible, we also want to sync data - //e.g. if a layer was invisible at start-up, it won't have any data loaded - const layer = getLayerById(layerId, getState()); - - // If the layer visibility is already what we want it to be, do nothing - if (!layer || layer.isVisible() === makeVisible) { - return; - } - - if (!makeVisible) { - dispatch(cleanTooltipStateForLayer(layerId)); - } - - await dispatch({ - type: SET_LAYER_VISIBILITY, - layerId, - visibility: makeVisible, - }); - if (makeVisible) { - dispatch(syncDataForLayer(layerId)); - } - }; -} - -export function toggleLayerVisible(layerId) { - return async (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer) { - return; - } - const makeVisible = !layer.isVisible(); - - dispatch(setLayerVisibility(layerId, makeVisible)); - }; -} - -export function setSelectedLayer(layerId) { - return async (dispatch, getState) => { - const oldSelectedLayer = getSelectedLayerId(getState()); - if (oldSelectedLayer) { - await dispatch(rollbackToTrackedLayerStateForSelectedLayer()); - } - if (layerId) { - dispatch(trackCurrentLayerState(layerId)); - } - dispatch({ - type: SET_SELECTED_LAYER, - selectedLayerId: layerId, - }); - }; -} - -export function removeTransientLayer() { - return async (dispatch, getState) => { - const transientLayerId = getTransientLayerId(getState()); - if (transientLayerId) { - await dispatch(removeLayerFromLayerList(transientLayerId)); - await dispatch(setTransientLayer(null)); - } - }; -} - -export function setTransientLayer(layerId) { - return { - type: SET_TRANSIENT_LAYER, - transientLayerId: layerId, - }; -} - -export function clearTransientLayerStateAndCloseFlyout() { - return async dispatch => { - await dispatch(updateFlyout(FLYOUT_STATE.NONE)); - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - }; -} - -export function updateLayerOrder(newLayerOrder) { - return { - type: UPDATE_LAYER_ORDER, - newLayerOrder, - }; -} - -export function mapReady() { - return (dispatch, getState) => { - dispatch({ - type: MAP_READY, - }); - - getWaitingForMapReadyLayerListRaw(getState()).forEach(layerDescriptor => { - dispatch(addLayer(layerDescriptor)); - }); - - dispatch({ - type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, - }); - }; -} - -export function mapDestroyed() { - return { - type: MAP_DESTROYED, - }; -} - -export function mapExtentChanged(newMapConstants) { - return async (dispatch, getState) => { - const state = getState(); - const dataFilters = getDataFilters(state); - const { extent, zoom: newZoom } = newMapConstants; - const { buffer, zoom: currentZoom } = dataFilters; - - if (extent) { - let doesBufferContainExtent = false; - if (buffer) { - const bufferGeometry = turf.bboxPolygon([ - buffer.minLon, - buffer.minLat, - buffer.maxLon, - buffer.maxLat, - ]); - const extentGeometry = turf.bboxPolygon([ - extent.minLon, - extent.minLat, - extent.maxLon, - extent.maxLat, - ]); - - doesBufferContainExtent = turfBooleanContains(bufferGeometry, extentGeometry); - } - - if (!doesBufferContainExtent || currentZoom !== newZoom) { - const scaleFactor = 0.5; // TODO put scale factor in store and fetch with selector - const width = extent.maxLon - extent.minLon; - const height = extent.maxLat - extent.minLat; - dataFilters.buffer = { - minLon: extent.minLon - width * scaleFactor, - minLat: extent.minLat - height * scaleFactor, - maxLon: extent.maxLon + width * scaleFactor, - maxLat: extent.maxLat + height * scaleFactor, - }; - } - } - - dispatch({ - type: MAP_EXTENT_CHANGED, - mapState: { - ...dataFilters, - ...newMapConstants, - }, - }); - const newDataFilters = { ...dataFilters, ...newMapConstants }; - await syncDataForAllLayers(dispatch, getState, newDataFilters); - }; -} - -export function closeOnClickTooltip(tooltipId) { - return (dispatch, getState) => { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips: getOpenTooltips(getState()).filter(({ id }) => { - return tooltipId !== id; - }), - }); - }; -} - -export function openOnClickTooltip(tooltipState) { - return (dispatch, getState) => { - const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => { - return ( - isLocked && - !_.isEqual(location, tooltipState.location) && - !_.isEqual(features, tooltipState.features) - ); - }); - - openTooltips.push({ - ...tooltipState, - isLocked: true, - id: uuid(), - }); - - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips, - }); - }; -} - -export function closeOnHoverTooltip() { - return (dispatch, getState) => { - if (getOpenTooltips(getState()).length) { - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips: [], - }); - } - }; -} - -export function openOnHoverTooltip(tooltipState) { - return { - type: SET_OPEN_TOOLTIPS, - openTooltips: [ - { - ...tooltipState, - isLocked: false, - id: uuid(), - }, - ], - }; -} - -export function setMouseCoordinates({ lat, lon }) { - let safeLon = lon; - if (lon > 180) { - const overlapWestOfDateLine = lon - 180; - safeLon = -180 + overlapWestOfDateLine; - } else if (lon < -180) { - const overlapEastOfDateLine = Math.abs(lon) - 180; - safeLon = 180 - overlapEastOfDateLine; - } - - return { - type: SET_MOUSE_COORDINATES, - lat, - lon: safeLon, - }; -} - -export function clearMouseCoordinates() { - return { type: CLEAR_MOUSE_COORDINATES }; -} - -export function disableScrollZoom() { - return { type: SET_SCROLL_ZOOM, scrollZoom: false }; -} - -export function fitToLayerExtent(layerId) { - return async function(dispatch, getState) { - const targetLayer = getLayerById(layerId, getState()); - - if (targetLayer) { - const dataFilters = getDataFilters(getState()); - const bounds = await targetLayer.getBounds(dataFilters); - if (bounds) { - await dispatch(setGotoWithBounds(bounds)); - } - } - }; -} - -export function setGotoWithBounds(bounds) { - return { - type: SET_GOTO, - bounds: bounds, - }; -} - -export function setGotoWithCenter({ lat, lon, zoom }) { - return { - type: SET_GOTO, - center: { lat, lon, zoom }, - }; -} - -export function clearGoto() { - return { type: CLEAR_GOTO }; -} - -export function startDataLoad(layerId, dataId, requestToken, meta = {}) { - return (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (layer) { - dispatch(cancelRequest(layer.getPrevRequestToken(dataId))); - } - - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoad) { - eventHandlers.onDataLoad({ - layerId, - dataId, - }); - } - - dispatch({ - meta, - type: LAYER_DATA_LOAD_STARTED, - layerId, - dataId, - requestToken, - }); - }; -} - -export function updateSourceDataRequest(layerId, newData) { - return dispatch => { - dispatch({ - type: UPDATE_SOURCE_DATA_REQUEST, - dataId: SOURCE_DATA_ID_ORIGIN, - layerId, - newData, - }); - - dispatch(updateStyleMeta(layerId)); - }; -} - -export function endDataLoad(layerId, dataId, requestToken, data, meta) { - return async (dispatch, getState) => { - dispatch(unregisterCancelCallback(requestToken)); - - const features = data && data.features ? data.features : []; - - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadEnd) { - const layer = getLayerById(layerId, getState()); - const resultMeta = {}; - if (layer && layer.getType() === LAYER_TYPE.VECTOR) { - resultMeta.featuresCount = features.length; - } - - eventHandlers.onDataLoadEnd({ - layerId, - dataId, - resultMeta, - }); - } - - dispatch(cleanTooltipStateForLayer(layerId, features)); - dispatch({ - type: LAYER_DATA_LOAD_ENDED, - layerId, - dataId, - data, - meta, - requestToken, - }); - - //Clear any data-load errors when there is a succesful data return. - //Co this on end-data-load iso at start-data-load to avoid blipping the error status between true/false. - //This avoids jitter in the warning icon of the TOC when the requests continues to return errors. - dispatch(setLayerDataLoadErrorStatus(layerId, null)); - - dispatch(updateStyleMeta(layerId)); - }; -} - -export function onDataLoadError(layerId, dataId, requestToken, errorMessage) { - return async (dispatch, getState) => { - dispatch(unregisterCancelCallback(requestToken)); - - const eventHandlers = getEventHandlers(getState()); - if (eventHandlers && eventHandlers.onDataLoadError) { - eventHandlers.onDataLoadError({ - layerId, - dataId, - errorMessage, - }); - } - - dispatch(cleanTooltipStateForLayer(layerId)); - dispatch({ - type: LAYER_DATA_LOAD_ERROR, - data: null, - layerId, - dataId, - requestToken, - }); - - dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); - }; -} - -export function updateSourceProp(layerId, propName, value, newLayerType) { - return async dispatch => { - dispatch({ - type: UPDATE_SOURCE_PROP, - layerId, - propName, - value, - }); - if (newLayerType) { - dispatch(updateLayerType(layerId, newLayerType)); - } - await dispatch(clearMissingStyleProperties(layerId)); - dispatch(syncDataForLayer(layerId)); - }; -} - -function updateLayerType(layerId, newLayerType) { - return (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer || layer.getType() === newLayerType) { - return; - } - dispatch(clearDataRequests(layer)); - dispatch({ - type: UPDATE_LAYER_PROP, - id: layerId, - propName: 'type', - newValue: newLayerType, - }); - }; -} - -export function syncDataForLayer(layerId) { - return async (dispatch, getState) => { - const targetLayer = getLayerById(layerId, getState()); - if (targetLayer) { - const dataFilters = getDataFilters(getState()); - const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layerId); - await targetLayer.syncData({ - ...loadingFunctions, - dataFilters, - }); - } - }; -} - -export function updateLayerLabel(id, newLabel) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'label', - newValue: newLabel, - }; -} - -export function updateLayerMinZoom(id, minZoom) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'minZoom', - newValue: minZoom, - }; -} - -export function updateLayerMaxZoom(id, maxZoom) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'maxZoom', - newValue: maxZoom, - }; -} - -export function updateLayerAlpha(id, alpha) { - return { - type: UPDATE_LAYER_PROP, - id, - propName: 'alpha', - newValue: alpha, - }; -} - -export function setLayerQuery(id, query) { - return dispatch => { - dispatch({ - type: UPDATE_LAYER_PROP, - id, - propName: 'query', - newValue: query, - }); - - dispatch(syncDataForLayer(id)); - }; -} - -export function removeSelectedLayer() { - return (dispatch, getState) => { - const state = getState(); - const layerId = getSelectedLayerId(state); - dispatch(removeLayer(layerId)); - }; -} - -export function removeLayer(layerId) { - return async (dispatch, getState) => { - const state = getState(); - const selectedLayerId = getSelectedLayerId(state); - if (layerId === selectedLayerId) { - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - await dispatch(setSelectedLayer(null)); - } - dispatch(removeLayerFromLayerList(layerId)); - }; -} - -function removeLayerFromLayerList(layerId) { - return (dispatch, getState) => { - const layerGettingRemoved = getLayerById(layerId, getState()); - if (!layerGettingRemoved) { - return; - } - - layerGettingRemoved.getInFlightRequestTokens().forEach(requestToken => { - dispatch(cancelRequest(requestToken)); - }); - dispatch(cleanTooltipStateForLayer(layerId)); - layerGettingRemoved.destroy(); - dispatch({ - type: REMOVE_LAYER, - id: layerId, - }); - }; -} - -export function setQuery({ query, timeFilters, filters = [], refresh = false }) { - function generateQueryTimestamp() { - return new Date().toISOString(); - } - return async (dispatch, getState) => { - const prevQuery = getQuery(getState()); - const prevTriggeredAt = - prevQuery && prevQuery.queryLastTriggeredAt - ? prevQuery.queryLastTriggeredAt - : generateQueryTimestamp(); - - dispatch({ - type: SET_QUERY, - timeFilters, - query: { - ...query, - // ensure query changes to trigger re-fetch when "Refresh" clicked - queryLastTriggeredAt: refresh ? generateQueryTimestamp() : prevTriggeredAt, - }, - filters, - }); - - const dataFilters = getDataFilters(getState()); - await syncDataForAllLayers(dispatch, getState, dataFilters); - }; -} - -export function setRefreshConfig({ isPaused, interval }) { - return { - type: SET_REFRESH_CONFIG, - isPaused, - interval, - }; -} - -export function triggerRefreshTimer() { - return async (dispatch, getState) => { - dispatch({ - type: TRIGGER_REFRESH_TIMER, - }); - - const dataFilters = getDataFilters(getState()); - await syncDataForAllLayers(dispatch, getState, dataFilters); - }; -} - -export function clearMissingStyleProperties(layerId) { - return async (dispatch, getState) => { - const targetLayer = getLayerById(layerId, getState()); - if (!targetLayer) { - return; - } - - const style = targetLayer.getCurrentStyle(); - if (!style) { - return; - } - - const nextFields = await targetLayer.getFields(); //take into account all fields, since labels can be driven by any field (source or join) - const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved( - nextFields - ); - if (hasChanges) { - dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); - } - }; -} - -export function updateLayerStyle(layerId, styleDescriptor) { - return dispatch => { - dispatch({ - type: UPDATE_LAYER_STYLE, - layerId, - style: { - ...styleDescriptor, - }, - }); - - // Ensure updateStyleMeta is triggered - // syncDataForLayer may not trigger endDataLoad if no re-fetch is required - dispatch(updateStyleMeta(layerId)); - - // Style update may require re-fetch, for example ES search may need to retrieve field used for dynamic styling - dispatch(syncDataForLayer(layerId)); - }; -} - -export function updateStyleMeta(layerId) { - return async (dispatch, getState) => { - const layer = getLayerById(layerId, getState()); - if (!layer) { - return; - } - const sourceDataRequest = layer.getSourceDataRequest(); - const style = layer.getCurrentStyle(); - if (!style || !sourceDataRequest) { - return; - } - const styleMeta = await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); - dispatch({ - type: SET_LAYER_STYLE_META, - layerId, - styleMeta, - }); - }; -} - -export function updateLayerStyleForSelectedLayer(styleDescriptor) { - return (dispatch, getState) => { - const selectedLayerId = getSelectedLayerId(getState()); - if (!selectedLayerId) { - return; - } - dispatch(updateLayerStyle(selectedLayerId, styleDescriptor)); - }; -} - -export function setJoinsForLayer(layer, joins) { - return async dispatch => { - await dispatch({ - type: SET_JOINS, - layer: layer, - joins: joins, - }); - - await dispatch(clearMissingStyleProperties(layer.getId())); - dispatch(syncDataForLayer(layer.getId())); - }; -} - -export function updateDrawState(drawState) { - return dispatch => { - if (drawState !== null) { - dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way - } - dispatch({ - type: UPDATE_DRAW_STATE, - drawState: drawState, - }); - }; -} - -export function disableInteractive() { - return { type: SET_INTERACTIVE, disableInteractive: true }; -} - -export function disableTooltipControl() { - return { type: DISABLE_TOOLTIP_CONTROL, disableTooltipControl: true }; -} - -export function hideToolbarOverlay() { - return { type: HIDE_TOOLBAR_OVERLAY, hideToolbarOverlay: true }; -} - -export function hideLayerControl() { - return { type: HIDE_LAYER_CONTROL, hideLayerControl: true }; -} -export function hideViewControl() { - return { type: HIDE_VIEW_CONTROL, hideViewControl: true }; -} - -export function setHiddenLayers(hiddenLayerIds) { - return (dispatch, getState) => { - const isMapReady = getMapReady(getState()); - - if (!isMapReady) { - dispatch({ type: SET_WAITING_FOR_READY_HIDDEN_LAYERS, hiddenLayerIds }); - } else { - getLayerListRaw(getState()).forEach(layer => - dispatch(setLayerVisibility(layer.id, !hiddenLayerIds.includes(layer.id))) - ); - } - }; -} diff --git a/x-pack/legacy/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/ui_actions.d.ts deleted file mode 100644 index 233918847de08..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/ui_actions.d.ts +++ /dev/null @@ -1,13 +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 { AnyAction } from 'redux'; - -export function setOpenTOCDetails(layerIds?: string[]): AnyAction; - -export function setIsLayerTOCOpen(open: boolean): AnyAction; - -export function setReadOnly(readOnly: boolean): AnyAction; diff --git a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js b/x-pack/legacy/plugins/maps/public/actions/ui_actions.js deleted file mode 100644 index 33ab2fd74122a..0000000000000 --- a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js +++ /dev/null @@ -1,91 +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 { - UPDATE_FLYOUT, - CLOSE_SET_VIEW, - OPEN_SET_VIEW, - SET_IS_LAYER_TOC_OPEN, - SET_FULL_SCREEN, - SET_READ_ONLY, - SET_OPEN_TOC_DETAILS, - SHOW_TOC_DETAILS, - HIDE_TOC_DETAILS, - UPDATE_INDEXING_STAGE, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/actions/ui_actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export * from '../../../../../plugins/maps/public/actions/ui_actions'; - -export function exitFullScreen() { - return { - type: SET_FULL_SCREEN, - isFullScreen: false, - }; -} - -export function updateFlyout(display) { - return { - type: UPDATE_FLYOUT, - display, - }; -} -export function closeSetView() { - return { - type: CLOSE_SET_VIEW, - }; -} -export function openSetView() { - return { - type: OPEN_SET_VIEW, - }; -} -export function setIsLayerTOCOpen(isLayerTOCOpen) { - return { - type: SET_IS_LAYER_TOC_OPEN, - isLayerTOCOpen, - }; -} -export function enableFullScreen() { - return { - type: SET_FULL_SCREEN, - isFullScreen: true, - }; -} -export function setReadOnly(isReadOnly) { - return { - type: SET_READ_ONLY, - isReadOnly, - }; -} - -export function setOpenTOCDetails(layerIds) { - return { - type: SET_OPEN_TOC_DETAILS, - layerIds, - }; -} - -export function showTOCDetails(layerId) { - return { - type: SHOW_TOC_DETAILS, - layerId, - }; -} - -export function hideTOCDetails(layerId) { - return { - type: HIDE_TOC_DETAILS, - layerId, - }; -} - -export function updateIndexingStage(stage) { - return { - type: UPDATE_INDEXING_STAGE, - stage, - }; -} diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js b/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js deleted file mode 100644 index 686259aeaaba4..0000000000000 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.js +++ /dev/null @@ -1,54 +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 _ from 'lodash'; -// Import each layer type, even those not used, to init in registry -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/wms_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/ems_file_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/es_search_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/kibana_regionmap_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/es_geo_grid_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import '../../../../../plugins/maps/public/layers/sources/xyz_tms_source'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { KibanaTilemapSource } from '../../../../../plugins/maps/public/layers/sources/kibana_tilemap_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { EMSTMSSource } from '../../../../../plugins/maps/public/layers/sources/ems_tms_source'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInjectedVarFunc } from '../../../../../plugins/maps/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getKibanaTileMap } from '../../../../../plugins/maps/public/meta'; - -export function getInitialLayers(layerListJSON, initialLayers = []) { - if (layerListJSON) { - return JSON.parse(layerListJSON); - } - - const tilemapSourceFromKibana = getKibanaTileMap(); - if (_.get(tilemapSourceFromKibana, 'url')) { - const sourceDescriptor = KibanaTilemapSource.createDescriptor(); - const source = new KibanaTilemapSource(sourceDescriptor); - const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor(), ...initialLayers]; - } - - const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); - if (isEmsEnabled) { - const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); - const source = new EMSTMSSource(descriptor); - const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor(), ...initialLayers]; - } - - return initialLayers; -} diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 6bc8a4d0be5ac..bb1a6b74d43a7 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -28,8 +28,10 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; import { Provider } from 'react-redux'; -import { GisMap } from '../connected_components/gis_map'; -import { addHelpMenuToAppChrome } from '../help_menu_util'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { GisMap } from '../../../../../plugins/maps/public/connected_components/gis_map'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { addHelpMenuToAppChrome } from '../../../../../plugins/maps/public/help_menu_util'; import { setSelectedLayer, setRefreshConfig, @@ -37,7 +39,9 @@ import { replaceLayerList, setQuery, clearTransientLayerStateAndCloseFlyout, -} from '../actions/map_actions'; + setMapSettings, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/actions/map_actions'; import { DEFAULT_IS_LAYER_TOC_OPEN, FLYOUT_STATE, @@ -49,22 +53,33 @@ import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails, -} from '../actions/ui_actions'; -import { getIsFullScreen } from '../selectors/ui_selectors'; + openMapSettings, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/actions/ui_actions'; +import { + getIsFullScreen, + getFlyoutDisplay, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/selectors/ui_selectors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { copyPersistentState } from '../../../../../plugins/maps/public/reducers/util'; import { getQueryableUniqueIndexPatternIds, hasDirtyState, getLayerListRaw, -} from '../selectors/map_selectors'; + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/selectors/map_selectors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; -import { getInitialLayers } from './get_initial_layers'; -import { getInitialQuery } from './get_initial_query'; -import { getInitialTimeFilters } from './get_initial_time_filters'; -import { getInitialRefreshConfig } from './get_initial_refresh_config'; -import { MAP_SAVED_OBJECT_TYPE, MAP_APP_PATH } from '../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialLayers } from '../../../../../plugins/maps/public/angular/get_initial_layers'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialQuery } from '../../../../../plugins/maps/public/angular/get_initial_query'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialTimeFilters } from '../../../../../plugins/maps/public/angular/get_initial_time_filters'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getInitialRefreshConfig } from '../../../../../plugins/maps/public/angular/get_initial_refresh_config'; +import { MAP_SAVED_OBJECT_TYPE, MAP_APP_PATH } from '../../../../../plugins/maps/common/constants'; import { npSetup, npStart } from 'ui/new_platform'; import { esFilters } from '../../../../../../src/plugins/data/public'; import { @@ -321,7 +336,7 @@ app.controller( function addFilters(newFilters) { newFilters.forEach(filter => { - filter.$state = esFilters.FilterStateStore.APP_STATE; + filter.$state = { store: esFilters.FilterStateStore.APP_STATE }; }); $scope.updateFiltersAndDispatch([...$scope.filters, ...newFilters]); } @@ -385,6 +400,9 @@ app.controller( if (mapState.filters) { savedObjectFilters = mapState.filters; } + if (mapState.settings) { + store.dispatch(setMapSettings(mapState.settings)); + } } if (savedMap.uiStateJSON) { @@ -443,6 +461,7 @@ app.controller( $scope.isFullScreen = false; $scope.isSaveDisabled = false; + $scope.isOpenSettingsDisabled = false; function handleStoreChanges(store) { const nextIsFullScreen = getIsFullScreen(store.getState()); if (nextIsFullScreen !== $scope.isFullScreen) { @@ -464,6 +483,14 @@ app.controller( $scope.isSaveDisabled = nextIsSaveDisabled; }); } + + const flyoutDisplay = getFlyoutDisplay(store.getState()); + const nextIsOpenSettingsDisabled = flyoutDisplay !== FLYOUT_STATE.NONE; + if (nextIsOpenSettingsDisabled !== $scope.isOpenSettingsDisabled) { + $scope.$evalAsync(() => { + $scope.isOpenSettingsDisabled = nextIsOpenSettingsDisabled; + }); + } } $scope.$on('$destroy', () => { @@ -581,6 +608,22 @@ app.controller( getInspector().open(inspectorAdapters, {}); }, }, + { + id: 'mapSettings', + label: i18n.translate('xpack.maps.mapController.openSettingsButtonLabel', { + defaultMessage: `Map settings`, + }), + description: i18n.translate('xpack.maps.mapController.openSettingsDescription', { + defaultMessage: `Open map settings`, + }), + testId: 'openSettingsButton', + disableButton() { + return $scope.isOpenSettingsDisabled; + }, + run() { + store.dispatch(openMapSettings()); + }, + }, ...(getMapsCapabilities().save ? [ { diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap deleted file mode 100644 index af836ceffa4b7..0000000000000 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/layer_toc_actions.test.js.snap +++ /dev/null @@ -1,343 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LayerTocActions is rendered 1`] = ` -<EuiPopover - anchorClassName="mapLayTocActions__popoverAnchor" - anchorPosition="leftUp" - button={ - <EuiToolTip - anchorClassName="mapLayTocActions__tooltipAnchor" - content={ - <React.Fragment> - simulated tooltip content at zoom: 0 - <div> - <span> - mockFootnoteIcon - </span> - - simulated footnote at isUsingSearch: true - </div> - </React.Fragment> - } - delay="regular" - position="top" - title="layer 1" - > - <EuiButtonEmpty - className="mapTocEntry__layerName eui-textLeft" - color="text" - data-test-subj="layerTocActionsPanelToggleButtonlayer1" - flush="left" - onClick={[Function]} - size="xs" - > - <span - className="mapTocEntry__layerNameIcon" - > - <span> - mockIcon - </span> - </span> - layer 1 - - <React.Fragment> - - <span> - mockFootnoteIcon - </span> - </React.Fragment> - </EuiButtonEmpty> - </EuiToolTip> - } - className="mapLayTocActions" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="contextMenu" - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - withTitle={true} -> - <EuiContextMenu - data-test-subj="layerTocActionsPanellayer1" - initialPanelId={0} - panels={ - Array [ - Object { - "id": 0, - "items": Array [ - Object { - "data-test-subj": "fitToBoundsButton", - "disabled": false, - "icon": <EuiIcon - size="m" - type="search" - />, - "name": "Fit to data", - "onClick": [Function], - "toolTipContent": null, - }, - Object { - "data-test-subj": "layerVisibilityToggleButton", - "icon": <EuiIcon - size="m" - type="eye" - />, - "name": "Hide layer", - "onClick": [Function], - }, - Object { - "data-test-subj": "editLayerButton", - "icon": <EuiIcon - size="m" - type="pencil" - />, - "name": "Edit layer", - "onClick": [Function], - }, - Object { - "data-test-subj": "cloneLayerButton", - "icon": <EuiIcon - size="m" - type="copy" - />, - "name": "Clone layer", - "onClick": [Function], - }, - Object { - "data-test-subj": "removeLayerButton", - "icon": <EuiIcon - size="m" - type="trash" - />, - "name": "Remove layer", - "onClick": [Function], - }, - ], - "title": "Layer actions", - }, - ] - } - /> -</EuiPopover> -`; - -exports[`LayerTocActions should disable fit to data when supportsFitToBounds is false 1`] = ` -<EuiPopover - anchorClassName="mapLayTocActions__popoverAnchor" - anchorPosition="leftUp" - button={ - <EuiToolTip - anchorClassName="mapLayTocActions__tooltipAnchor" - content={ - <React.Fragment> - simulated tooltip content at zoom: 0 - <div> - <span> - mockFootnoteIcon - </span> - - simulated footnote at isUsingSearch: true - </div> - </React.Fragment> - } - delay="regular" - position="top" - title="layer 1" - > - <EuiButtonEmpty - className="mapTocEntry__layerName eui-textLeft" - color="text" - data-test-subj="layerTocActionsPanelToggleButtonlayer1" - flush="left" - onClick={[Function]} - size="xs" - > - <span - className="mapTocEntry__layerNameIcon" - > - <span> - mockIcon - </span> - </span> - layer 1 - - <React.Fragment> - - <span> - mockFootnoteIcon - </span> - </React.Fragment> - </EuiButtonEmpty> - </EuiToolTip> - } - className="mapLayTocActions" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="contextMenu" - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - withTitle={true} -> - <EuiContextMenu - data-test-subj="layerTocActionsPanellayer1" - initialPanelId={0} - panels={ - Array [ - Object { - "id": 0, - "items": Array [ - Object { - "data-test-subj": "fitToBoundsButton", - "disabled": true, - "icon": <EuiIcon - size="m" - type="search" - />, - "name": "Fit to data", - "onClick": [Function], - "toolTipContent": "Layer does not support fit to data", - }, - Object { - "data-test-subj": "layerVisibilityToggleButton", - "icon": <EuiIcon - size="m" - type="eye" - />, - "name": "Hide layer", - "onClick": [Function], - }, - Object { - "data-test-subj": "editLayerButton", - "icon": <EuiIcon - size="m" - type="pencil" - />, - "name": "Edit layer", - "onClick": [Function], - }, - Object { - "data-test-subj": "cloneLayerButton", - "icon": <EuiIcon - size="m" - type="copy" - />, - "name": "Clone layer", - "onClick": [Function], - }, - Object { - "data-test-subj": "removeLayerButton", - "icon": <EuiIcon - size="m" - type="trash" - />, - "name": "Remove layer", - "onClick": [Function], - }, - ], - "title": "Layer actions", - }, - ] - } - /> -</EuiPopover> -`; - -exports[`LayerTocActions should not show edit actions in read only mode 1`] = ` -<EuiPopover - anchorClassName="mapLayTocActions__popoverAnchor" - anchorPosition="leftUp" - button={ - <EuiToolTip - anchorClassName="mapLayTocActions__tooltipAnchor" - content={ - <React.Fragment> - simulated tooltip content at zoom: 0 - <div> - <span> - mockFootnoteIcon - </span> - - simulated footnote at isUsingSearch: true - </div> - </React.Fragment> - } - delay="regular" - position="top" - title="layer 1" - > - <EuiButtonEmpty - className="mapTocEntry__layerName eui-textLeft" - color="text" - data-test-subj="layerTocActionsPanelToggleButtonlayer1" - flush="left" - onClick={[Function]} - size="xs" - > - <span - className="mapTocEntry__layerNameIcon" - > - <span> - mockIcon - </span> - </span> - layer 1 - - <React.Fragment> - - <span> - mockFootnoteIcon - </span> - </React.Fragment> - </EuiButtonEmpty> - </EuiToolTip> - } - className="mapLayTocActions" - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="contextMenu" - isOpen={false} - ownFocus={false} - panelPaddingSize="none" - withTitle={true} -> - <EuiContextMenu - data-test-subj="layerTocActionsPanellayer1" - initialPanelId={0} - panels={ - Array [ - Object { - "id": 0, - "items": Array [ - Object { - "data-test-subj": "fitToBoundsButton", - "disabled": false, - "icon": <EuiIcon - size="m" - type="search" - />, - "name": "Fit to data", - "onClick": [Function], - "toolTipContent": null, - }, - Object { - "data-test-subj": "layerVisibilityToggleButton", - "icon": <EuiIcon - size="m" - type="eye" - />, - "name": "Hide layer", - "onClick": [Function], - }, - ], - "title": "Layer actions", - }, - ] - } - /> -</EuiPopover> -`; diff --git a/x-pack/legacy/plugins/maps/public/components/_index.scss b/x-pack/legacy/plugins/maps/public/components/_index.scss deleted file mode 100644 index 0b32719442424..0000000000000 --- a/x-pack/legacy/plugins/maps/public/components/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './metric_editors'; -@import './geometry_filter'; -@import './tooltip_selector'; diff --git a/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.js b/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.js deleted file mode 100644 index d79eda16037cb..0000000000000 --- a/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.js +++ /dev/null @@ -1,197 +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 React, { Component, Fragment } from 'react'; - -import { EuiButtonEmpty, EuiPopover, EuiContextMenu, EuiIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export class LayerTocActions extends Component { - state = { - isPopoverOpen: false, - supportsFitToBounds: false, - }; - - componentDidMount() { - this._isMounted = true; - this._loadSupportsFitToBounds(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - async _loadSupportsFitToBounds() { - const supportsFitToBounds = await this.props.layer.supportsFitToBounds(); - if (this._isMounted) { - this.setState({ supportsFitToBounds }); - } - } - - _togglePopover = () => { - this.setState(prevState => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; - - _closePopover = () => { - this.setState(() => ({ - isPopoverOpen: false, - })); - }; - - _renderPopoverToggleButton() { - const { icon, tooltipContent, footnotes } = this.props.layer.getIconAndTooltipContent( - this.props.zoom, - this.props.isUsingSearch - ); - - const footnoteIcons = footnotes.map((footnote, index) => { - return ( - <Fragment key={index}> - {''} - {footnote.icon} - </Fragment> - ); - }); - const footnoteTooltipContent = footnotes.map((footnote, index) => { - return ( - <div key={index}> - {footnote.icon} {footnote.message} - </div> - ); - }); - - return ( - <EuiToolTip - anchorClassName="mapLayTocActions__tooltipAnchor" - position="top" - title={this.props.displayName} - content={ - <Fragment> - {tooltipContent} - {footnoteTooltipContent} - </Fragment> - } - > - <EuiButtonEmpty - className="mapTocEntry__layerName eui-textLeft" - size="xs" - flush="left" - color="text" - onClick={this._togglePopover} - data-test-subj={`layerTocActionsPanelToggleButton${this.props.escapedDisplayName}`} - > - <span className="mapTocEntry__layerNameIcon">{icon}</span> - {this.props.displayName} {footnoteIcons} - </EuiButtonEmpty> - </EuiToolTip> - ); - } - - _getActionsPanel() { - const actionItems = [ - { - name: i18n.translate('xpack.maps.layerTocActions.fitToDataTitle', { - defaultMessage: 'Fit to data', - }), - icon: <EuiIcon type="search" size="m" />, - 'data-test-subj': 'fitToBoundsButton', - toolTipContent: this.state.supportsFitToBounds - ? null - : i18n.translate('xpack.maps.layerTocActions.noFitSupportTooltip', { - defaultMessage: 'Layer does not support fit to data', - }), - disabled: !this.state.supportsFitToBounds, - onClick: () => { - this._closePopover(); - this.props.fitToBounds(); - }, - }, - { - name: this.props.layer.isVisible() - ? i18n.translate('xpack.maps.layerTocActions.hideLayerTitle', { - defaultMessage: 'Hide layer', - }) - : i18n.translate('xpack.maps.layerTocActions.showLayerTitle', { - defaultMessage: 'Show layer', - }), - icon: <EuiIcon type={this.props.layer.isVisible() ? 'eye' : 'eyeClosed'} size="m" />, - 'data-test-subj': 'layerVisibilityToggleButton', - onClick: () => { - this._closePopover(); - this.props.toggleVisible(); - }, - }, - ]; - - if (!this.props.isReadOnly) { - actionItems.push({ - name: i18n.translate('xpack.maps.layerTocActions.editLayerTitle', { - defaultMessage: 'Edit layer', - }), - icon: <EuiIcon type="pencil" size="m" />, - 'data-test-subj': 'editLayerButton', - onClick: () => { - this._closePopover(); - this.props.editLayer(); - }, - }); - actionItems.push({ - name: i18n.translate('xpack.maps.layerTocActions.cloneLayerTitle', { - defaultMessage: 'Clone layer', - }), - icon: <EuiIcon type="copy" size="m" />, - 'data-test-subj': 'cloneLayerButton', - onClick: () => { - this._closePopover(); - this.props.cloneLayer(); - }, - }); - actionItems.push({ - name: i18n.translate('xpack.maps.layerTocActions.removeLayerTitle', { - defaultMessage: 'Remove layer', - }), - icon: <EuiIcon type="trash" size="m" />, - 'data-test-subj': 'removeLayerButton', - onClick: () => { - this._closePopover(); - this.props.removeLayer(); - }, - }); - } - - return { - id: 0, - title: i18n.translate('xpack.maps.layerTocActions.layerActionsTitle', { - defaultMessage: 'Layer actions', - }), - items: actionItems, - }; - } - - render() { - return ( - <EuiPopover - id="contextMenu" - className="mapLayTocActions" - button={this._renderPopoverToggleButton()} - isOpen={this.state.isPopoverOpen} - closePopover={this._closePopover} - panelPaddingSize="none" - withTitle - anchorPosition="leftUp" - anchorClassName="mapLayTocActions__popoverAnchor" - > - <EuiContextMenu - initialPanelId={0} - panels={[this._getActionsPanel()]} - data-test-subj={`layerTocActionsPanel${this.props.escapedDisplayName}`} - /> - </EuiPopover> - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.test.js b/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.test.js deleted file mode 100644 index c3a8f59c4c736..0000000000000 --- a/x-pack/legacy/plugins/maps/public/components/layer_toc_actions.test.js +++ /dev/null @@ -1,80 +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 React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; - -import { LayerTocActions } from './layer_toc_actions'; - -let supportsFitToBounds; -const layerMock = { - supportsFitToBounds: () => { - return supportsFitToBounds; - }, - isVisible: () => { - return true; - }, - getIconAndTooltipContent: (zoom, isUsingSearch) => { - return { - icon: <span>mockIcon</span>, - tooltipContent: `simulated tooltip content at zoom: ${zoom}`, - footnotes: [ - { - icon: <span>mockFootnoteIcon</span>, - message: `simulated footnote at isUsingSearch: ${isUsingSearch}`, - }, - ], - }; - }, -}; - -const defaultProps = { - displayName: 'layer 1', - escapedDisplayName: 'layer1', - zoom: 0, - layer: layerMock, - isUsingSearch: true, -}; - -describe('LayerTocActions', () => { - beforeEach(() => { - supportsFitToBounds = true; - }); - - test('is rendered', async () => { - const component = shallowWithIntl(<LayerTocActions {...defaultProps} />); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component).toMatchSnapshot(); - }); - - test('should not show edit actions in read only mode', async () => { - const component = shallowWithIntl(<LayerTocActions {...defaultProps} isReadOnly={true} />); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component).toMatchSnapshot(); - }); - - test('should disable fit to data when supportsFitToBounds is false', async () => { - supportsFitToBounds = false; - const component = shallowWithIntl(<LayerTocActions {...defaultProps} />); - - // Ensure all promises resolve - await new Promise(resolve => process.nextTick(resolve)); - // Ensure the state changes are reflected - component.update(); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/_index.scss deleted file mode 100644 index 99a2e222ea6c1..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/_index.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import './gis_map/gis_map'; -@import './layer_addpanel/source_select/index'; -@import './layer_panel/index'; -@import './widget_overlay/index'; -@import './toolbar_overlay/index'; -@import './map/features_tooltip/index'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.d.ts b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.d.ts deleted file mode 100644 index 8689d88297171..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.d.ts +++ /dev/null @@ -1,15 +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 React from 'react'; -import { Filter } from 'src/plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderToolTipContent } from '../../../../../../plugins/maps/public/layers/tooltips/tooltip_property'; - -export const GisMap: React.ComponentType<{ - addFilters: ((filters: Filter[]) => void) | null; - renderTooltipContent?: RenderToolTipContent; -}>; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js deleted file mode 100644 index 2d8265bae9387..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js +++ /dev/null @@ -1,51 +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 { connect } from 'react-redux'; -import { GisMap } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../plugins/maps/public/reducers/ui'; -import { exitFullScreen } from '../../actions/ui_actions'; -import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; -import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions'; -import { - areLayersLoaded, - getRefreshConfig, - getMapInitError, - getQueryableUniqueIndexPatternIds, - isToolbarOverlayHidden, -} from '../../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getCoreChrome } from '../../../../../../plugins/maps/public/kibana_services'; - -function mapStateToProps(state = {}) { - const flyoutDisplay = getFlyoutDisplay(state); - return { - areLayersLoaded: areLayersLoaded(state), - layerDetailsVisible: flyoutDisplay === FLYOUT_STATE.LAYER_PANEL, - addLayerVisible: flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD, - noFlyoutVisible: flyoutDisplay === FLYOUT_STATE.NONE, - isFullScreen: getIsFullScreen(state), - refreshConfig: getRefreshConfig(state), - mapInitError: getMapInitError(state), - indexPatternIds: getQueryableUniqueIndexPatternIds(state), - hideToolbarOverlay: isToolbarOverlayHidden(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), - exitFullScreen: () => { - dispatch(exitFullScreen()); - getCoreChrome().setIsVisible(true); - }, - cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), - }; -} - -const connectedGisMap = connect(mapStateToProps, mapDispatchToProps)(GisMap); -export { connectedGisMap as GisMap }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js deleted file mode 100644 index 06097ebea1900..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js +++ /dev/null @@ -1,222 +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 _ from 'lodash'; -import React, { Component } from 'react'; -import { MBMapContainer } from '../map/mb'; -import { WidgetOverlay } from '../widget_overlay/index'; -import { ToolbarOverlay } from '../toolbar_overlay/index'; -import { LayerPanel } from '../layer_panel/index'; -import { AddLayerPanel } from '../layer_addpanel/index'; -import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; -import { ExitFullScreenButton } from '../../../../../../../src/plugins/kibana_react/public'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getIndexPatternsFromIds } from '../../../../../../plugins/maps/public/index_pattern_util'; -import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; -import { indexPatterns as indexPatternsUtils } from '../../../../../../../src/plugins/data/public'; -import { i18n } from '@kbn/i18n'; -import uuid from 'uuid/v4'; - -const RENDER_COMPLETE_EVENT = 'renderComplete'; - -export class GisMap extends Component { - state = { - isInitialLoadRenderTimeoutComplete: false, - domId: uuid(), - geoFields: [], - }; - - componentDidMount() { - this._isMounted = true; - this._isInitalLoadRenderTimerStarted = false; - this._setRefreshTimer(); - } - - componentDidUpdate() { - this._setRefreshTimer(); - if (this.props.areLayersLoaded && !this._isInitalLoadRenderTimerStarted) { - this._isInitalLoadRenderTimerStarted = true; - this._startInitialLoadRenderTimer(); - } - - if (!!this.props.addFilters) { - this._loadGeoFields(this.props.indexPatternIds); - } - } - - componentWillUnmount() { - this._isMounted = false; - this._clearRefreshTimer(); - this.props.cancelAllInFlightRequests(); - } - - // Reporting uses both a `data-render-complete` attribute and a DOM event listener to determine - // if a visualization is done loading. The process roughly is: - // - See if the `data-render-complete` attribute is "true". If so we're done! - // - If it's not, then reporting injects a listener into the browser for a custom "renderComplete" event. - // - When that event is fired, we snapshot the viz and move on. - // Failure to not have the dom attribute, or custom event, will timeout the job. - // See x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts for more. - _onInitialLoadRenderComplete = () => { - const el = document.querySelector(`[data-dom-id="${this.state.domId}"]`); - - if (el) { - el.dispatchEvent(new CustomEvent(RENDER_COMPLETE_EVENT, { bubbles: true })); - } - }; - - _loadGeoFields = async nextIndexPatternIds => { - if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { - // all ready loaded index pattern ids - return; - } - - this._prevIndexPatternIds = nextIndexPatternIds; - - const geoFields = []; - try { - const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); - indexPatterns.forEach(indexPattern => { - indexPattern.fields.forEach(field => { - if ( - !indexPatternsUtils.isNestedField(field) && - (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || - field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) - ) { - geoFields.push({ - geoFieldName: field.name, - geoFieldType: field.type, - indexPatternTitle: indexPattern.title, - indexPatternId: indexPattern.id, - }); - } - }); - }); - } catch (e) { - // swallow errors. - // the Layer-TOC will indicate which layers are disfunctional on a per-layer basis - } - - if (!this._isMounted) { - return; - } - - this.setState({ geoFields }); - }; - - _setRefreshTimer = () => { - const { isPaused, interval } = this.props.refreshConfig; - - if (this.isPaused === isPaused && this.interval === interval) { - // refreshConfig is the same, nothing to do - return; - } - - this.isPaused = isPaused; - this.interval = interval; - - this._clearRefreshTimer(); - - if (!isPaused && interval > 0) { - this.refreshTimerId = setInterval(() => { - this.props.triggerRefreshTimer(); - }, interval); - } - }; - - _clearRefreshTimer = () => { - if (this.refreshTimerId) { - clearInterval(this.refreshTimerId); - } - }; - - // Mapbox does not provide any feedback when rendering is complete. - // Temporary solution is just to wait set period of time after data has loaded. - _startInitialLoadRenderTimer = () => { - setTimeout(() => { - if (this._isMounted) { - this.setState({ isInitialLoadRenderTimeoutComplete: true }); - this._onInitialLoadRenderComplete(); - } - }, 5000); - }; - - render() { - const { - addFilters, - layerDetailsVisible, - addLayerVisible, - noFlyoutVisible, - isFullScreen, - exitFullScreen, - mapInitError, - renderTooltipContent, - } = this.props; - - const { domId } = this.state; - - if (mapInitError) { - return ( - <div data-render-complete data-shared-item> - <EuiCallOut - title={i18n.translate('xpack.maps.map.initializeErrorTitle', { - defaultMessage: 'Unable to initialize map', - })} - color="danger" - iconType="cross" - > - <p>{mapInitError}</p> - </EuiCallOut> - </div> - ); - } - - let currentPanel; - let currentPanelClassName; - if (noFlyoutVisible) { - currentPanel = null; - } else if (addLayerVisible) { - currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = <AddLayerPanel />; - } else if (layerDetailsVisible) { - currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = <LayerPanel />; - } - - let exitFullScreenButton; - if (isFullScreen) { - exitFullScreenButton = <ExitFullScreenButton onExitFullScreenMode={exitFullScreen} />; - } - return ( - <EuiFlexGroup - gutterSize="none" - responsive={false} - data-dom-id={domId} - data-render-complete={this.state.isInitialLoadRenderTimeoutComplete} - data-shared-item - > - <EuiFlexItem className="mapMapWrapper"> - <MBMapContainer - addFilters={addFilters} - geoFields={this.state.geoFields} - renderTooltipContent={renderTooltipContent} - /> - {!this.props.hideToolbarOverlay && ( - <ToolbarOverlay addFilters={addFilters} geoFields={this.state.geoFields} /> - )} - <WidgetOverlay /> - </EuiFlexItem> - - <EuiFlexItem className={`mapMapLayerPanel ${currentPanelClassName}`} grow={false}> - {currentPanel} - </EuiFlexItem> - - {exitFullScreenButton} - </EuiFlexGroup> - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js deleted file mode 100644 index e8192795f98ae..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js +++ /dev/null @@ -1,31 +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 { connect } from 'react-redux'; -import { ImportEditor } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { INDEXING_STAGE } from '../../../../../../../plugins/maps/public/reducers/ui'; -import { updateIndexingStage } from '../../../actions/ui_actions'; -import { getIndexingStage } from '../../../selectors/ui_selectors'; - -function mapStateToProps(state = {}) { - return { - inspectorAdapters: getInspectorAdapters(state), - isIndexingTriggered: getIndexingStage(state) === INDEXING_STAGE.TRIGGERED, - }; -} - -const mapDispatchToProps = { - onIndexReady: indexReady => - indexReady ? updateIndexingStage(INDEXING_STAGE.READY) : updateIndexingStage(null), - importSuccessHandler: () => updateIndexingStage(INDEXING_STAGE.SUCCESS), - importErrorHandler: () => updateIndexingStage(INDEXING_STAGE.ERROR), -}; - -const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(ImportEditor); -export { connectedFlyOut as ImportEditor }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js deleted file mode 100644 index cb20d80733c33..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js +++ /dev/null @@ -1,54 +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 React, { Fragment } from 'react'; -import { EuiSpacer, EuiPanel, EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { uploadLayerWizardConfig } from '../../../../../../../plugins/maps/public/layers/sources/client_file_source'; - -export const ImportEditor = ({ clearSource, isIndexingTriggered, ...props }) => { - const editorProperties = getEditorProperties({ isIndexingTriggered, ...props }); - return ( - <Fragment> - {isIndexingTriggered ? null : ( - <Fragment> - <EuiButtonEmpty size="xs" flush="left" onClick={clearSource} iconType="arrowLeft"> - <FormattedMessage - id="xpack.maps.addLayerPanel.changeDataSourceButtonLabel" - defaultMessage="Change data source" - /> - </EuiButtonEmpty> - <EuiSpacer size="s" /> - </Fragment> - )} - <EuiPanel style={{ position: 'relative' }}> - {uploadLayerWizardConfig.renderWizard(editorProperties)} - </EuiPanel> - </Fragment> - ); -}; - -function getEditorProperties({ - inspectorAdapters, - onRemove, - viewLayer, - isIndexingTriggered, - onIndexReady, - importSuccessHandler, - importErrorHandler, -}) { - return { - onPreviewSource: viewLayer, - inspectorAdapters, - onRemove, - importSuccessHandler, - importErrorHandler, - isIndexingTriggered, - addAndViewSource: viewLayer, - onIndexReady, - }; -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js deleted file mode 100644 index c4e2fa5169b0f..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/index.js +++ /dev/null @@ -1,60 +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 { connect } from 'react-redux'; -import { AddLayerPanel } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE, INDEXING_STAGE } from '../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout, updateIndexingStage } from '../../actions/ui_actions'; -import { getFlyoutDisplay, getIndexingStage } from '../../selectors/ui_selectors'; -import { getMapColors } from '../../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../plugins/maps/public/reducers/non_serializable_instances'; -import { - setTransientLayer, - addLayer, - setSelectedLayer, - removeTransientLayer, -} from '../../actions/map_actions'; - -function mapStateToProps(state = {}) { - const indexingStage = getIndexingStage(state); - return { - inspectorAdapters: getInspectorAdapters(state), - flyoutVisible: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, - mapColors: getMapColors(state), - isIndexingTriggered: indexingStage === INDEXING_STAGE.TRIGGERED, - isIndexingSuccess: indexingStage === INDEXING_STAGE.SUCCESS, - isIndexingReady: indexingStage === INDEXING_STAGE.READY, - }; -} - -function mapDispatchToProps(dispatch) { - return { - viewLayer: async layer => { - await dispatch(setSelectedLayer(null)); - await dispatch(removeTransientLayer()); - dispatch(addLayer(layer.toLayerDescriptor())); - dispatch(setSelectedLayer(layer.getId())); - dispatch(setTransientLayer(layer.getId())); - }, - removeTransientLayer: () => { - dispatch(setSelectedLayer(null)); - dispatch(removeTransientLayer()); - }, - selectLayerAndAdd: () => { - dispatch(setTransientLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); - }, - setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)), - resetIndexing: () => dispatch(updateIndexingStage(null)), - }; -} - -const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })( - AddLayerPanel -); -export { connectedFlyOut as AddLayerPanel }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js deleted file mode 100644 index 553e54ee89766..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js +++ /dev/null @@ -1,19 +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 { connect } from 'react-redux'; -import { SourceEditor } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; - -function mapStateToProps(state = {}) { - return { - inspectorAdapters: getInspectorAdapters(state), - }; -} - -const connectedFlyOut = connect(mapStateToProps)(SourceEditor); -export { connectedFlyOut as SourceEditor }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js deleted file mode 100644 index 50312b68277fa..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js +++ /dev/null @@ -1,40 +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 React, { Fragment } from 'react'; -import { EuiSpacer, EuiPanel, EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const SourceEditor = ({ - clearSource, - layerWizard, - isIndexingTriggered, - inspectorAdapters, - previewLayer, -}) => { - if (!layerWizard) { - return null; - } - - return ( - <Fragment> - {isIndexingTriggered ? null : ( - <Fragment> - <EuiButtonEmpty size="xs" flush="left" onClick={clearSource} iconType="arrowLeft"> - <FormattedMessage - id="xpack.maps.addLayerPanel.changeDataSourceButtonLabel" - defaultMessage="Change data source" - /> - </EuiButtonEmpty> - <EuiSpacer size="s" /> - </Fragment> - )} - <EuiPanel> - {layerWizard.renderWizard({ onPreviewSource: previewLayer, inspectorAdapters })} - </EuiPanel> - </Fragment> - ); -}; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss deleted file mode 100644 index 7fe1396fcca16..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './source_select'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js deleted file mode 100644 index 67cc17ebaa224..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js +++ /dev/null @@ -1,54 +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 React, { Fragment } from 'react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getLayerWizards } from '../../../../../../../plugins/maps/public/layers/layer_wizard_registry'; -import { EuiTitle, EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import _ from 'lodash'; - -export function SourceSelect({ updateSourceSelection }) { - const sourceCards = getLayerWizards().map(layerWizard => { - const icon = layerWizard.icon ? <EuiIcon type={layerWizard.icon} size="l" /> : null; - - const onClick = () => { - updateSourceSelection({ - layerWizard: layerWizard, - isIndexingSource: !!layerWizard.isIndexingSource, - }); - }; - - return ( - <Fragment key={layerWizard.title}> - <EuiSpacer size="s" /> - <EuiCard - className="mapLayerAddpanel__card" - title={layerWizard.title} - icon={icon} - onClick={onClick} - description={layerWizard.description} - layout="horizontal" - data-test-subj={_.camelCase(layerWizard.title)} - /> - </Fragment> - ); - }); - - return ( - <Fragment> - <EuiTitle size="xs"> - <h2> - <FormattedMessage - id="xpack.maps.addLayerPanel.chooseDataSourceTitle" - defaultMessage="Choose data source" - /> - </h2> - </EuiTitle> - {sourceCards} - </Fragment> - ); -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js deleted file mode 100644 index 92fcf01f3901f..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/view.js +++ /dev/null @@ -1,181 +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 React, { Component } from 'react'; -import { SourceSelect } from './source_select/source_select'; -import { FlyoutFooter } from './flyout_footer'; -import { SourceEditor } from './source_editor'; -import { ImportEditor } from './import_editor'; -import { EuiFlexGroup, EuiTitle, EuiFlyoutHeader } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export class AddLayerPanel extends Component { - state = { - layerWizard: null, - layer: null, - importView: false, - layerImportAddReady: false, - }; - - componentDidMount() { - this._isMounted = true; - } - - componentWillUnmount() { - this._isMounted = false; - } - - componentDidUpdate() { - if (!this.state.layerImportAddReady && this.props.isIndexingSuccess) { - this.setState({ layerImportAddReady: true }); - } - } - - _getPanelDescription() { - const { layerWizard, importView, layerImportAddReady } = this.state; - let panelDescription; - if (!layerWizard) { - panelDescription = i18n.translate('xpack.maps.addLayerPanel.selectSource', { - defaultMessage: 'Select source', - }); - } else if (layerImportAddReady || !importView) { - panelDescription = i18n.translate('xpack.maps.addLayerPanel.addLayer', { - defaultMessage: 'Add layer', - }); - } else { - panelDescription = i18n.translate('xpack.maps.addLayerPanel.importFile', { - defaultMessage: 'Import file', - }); - } - return panelDescription; - } - - _viewLayer = async (source, options = {}) => { - if (!this._isMounted) { - return; - } - if (!source) { - this.setState({ layer: null }); - this.props.removeTransientLayer(); - return; - } - - const styleDescriptor = - this.state.layer && this.state.layer.getCurrentStyle() - ? this.state.layer.getCurrentStyle().getDescriptor() - : null; - const layerInitProps = { - ...options, - style: styleDescriptor, - }; - const newLayer = source.createDefaultLayer(layerInitProps, this.props.mapColors); - if (!this._isMounted) { - return; - } - this.setState({ layer: newLayer }, () => this.props.viewLayer(this.state.layer)); - }; - - _clearLayerData = ({ keepSourceType = false }) => { - if (!this._isMounted) { - return; - } - - this.setState({ - layer: null, - ...(!keepSourceType ? { layerWizard: null, importView: false } : {}), - }); - this.props.removeTransientLayer(); - }; - - _onSourceSelectionChange = ({ layerWizard, isIndexingSource }) => { - this.setState({ layerWizard, importView: isIndexingSource }); - }; - - _layerAddHandler = () => { - const { - isIndexingTriggered, - setIndexingTriggered, - selectLayerAndAdd, - resetIndexing, - } = this.props; - const layerSource = this.state.layer.getSource(); - const boolIndexLayer = layerSource.shouldBeIndexed(); - this.setState({ layer: null }); - if (boolIndexLayer && !isIndexingTriggered) { - setIndexingTriggered(); - } else { - selectLayerAndAdd(); - if (this.state.importView) { - this.setState({ - layerImportAddReady: false, - }); - resetIndexing(); - } - } - }; - - _renderAddLayerPanel() { - const { layerWizard, importView } = this.state; - if (!layerWizard) { - return <SourceSelect updateSourceSelection={this._onSourceSelectionChange} />; - } - if (importView) { - return ( - <ImportEditor - clearSource={this._clearLayerData} - viewLayer={this._viewLayer} - onRemove={() => this._clearLayerData({ keepSourceType: true })} - /> - ); - } - return ( - <SourceEditor - clearSource={this._clearLayerData} - layerWizard={layerWizard} - previewLayer={this._viewLayer} - /> - ); - } - - _renderFooter(buttonDescription) { - const { importView, layer } = this.state; - const { isIndexingReady, isIndexingSuccess } = this.props; - - const buttonEnabled = importView ? isIndexingReady || isIndexingSuccess : !!layer; - - return ( - <FlyoutFooter - showNextButton={!!this.state.layerWizard} - disableNextButton={!buttonEnabled} - onClick={this._layerAddHandler} - nextButtonText={buttonDescription} - /> - ); - } - - _renderFlyout() { - const panelDescription = this._getPanelDescription(); - - return ( - <EuiFlexGroup direction="column" gutterSize="none"> - <EuiFlyoutHeader hasBorder className="mapLayerPanel__header"> - <EuiTitle size="s"> - <h2>{panelDescription}</h2> - </EuiTitle> - </EuiFlyoutHeader> - - <div className="mapLayerPanel__body" data-test-subj="layerAddForm"> - <div className="mapLayerPanel__bodyOverflow">{this._renderAddLayerPanel()}</div> - </div> - {this._renderFooter(panelDescription)} - </EuiFlexGroup> - ); - } - - render() { - return this.props.flyoutVisible ? this._renderFlyout() : null; - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss deleted file mode 100644 index fd074edf032fa..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './layer_panel'; -@import './filter_editor/filter_editor'; -@import './join_editor/resources/join'; -@import './style_settings/style_settings'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js deleted file mode 100644 index 287f0019f18ec..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js +++ /dev/null @@ -1,43 +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 { connect } from 'react-redux'; -import { FlyoutFooter } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout } from '../../../actions/ui_actions'; -import { hasDirtyState } from '../../../selectors/map_selectors'; -import { - setSelectedLayer, - removeSelectedLayer, - removeTrackedLayerStateForSelectedLayer, -} from '../../../actions/map_actions'; - -function mapStateToProps(state = {}) { - return { - hasStateChanged: hasDirtyState(state), - }; -} - -const mapDispatchToProps = dispatch => { - return { - cancelLayerPanel: () => { - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(setSelectedLayer(null)); - }, - saveLayerEdits: () => { - dispatch(updateFlyout(FLYOUT_STATE.NONE)); - dispatch(removeTrackedLayerStateForSelectedLayer()); - dispatch(setSelectedLayer(null)); - }, - removeLayer: () => { - dispatch(removeSelectedLayer()); - }, - }; -}; - -const connectedFlyoutFooter = connect(mapStateToProps, mapDispatchToProps)(FlyoutFooter); -export { connectedFlyoutFooter as FlyoutFooter }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js deleted file mode 100644 index e8f980bbbf2b4..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js +++ /dev/null @@ -1,41 +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 { connect } from 'react-redux'; -import { LayerSettings } from './layer_settings'; -import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { - updateLayerLabel, - updateLayerMaxZoom, - updateLayerMinZoom, - updateLayerAlpha, -} from '../../../actions/map_actions'; -import { MAX_ZOOM } from '../../../../../../../plugins/maps/common/constants'; - -function mapStateToProps(state = {}) { - const selectedLayer = getSelectedLayer(state); - return { - minVisibilityZoom: selectedLayer.getMinSourceZoom(), - maxVisibilityZoom: MAX_ZOOM, - alpha: selectedLayer.getAlpha(), - label: selectedLayer.getLabel(), - layerId: selectedLayer.getId(), - maxZoom: selectedLayer.getMaxZoom(), - minZoom: selectedLayer.getMinZoom(), - }; -} - -function mapDispatchToProps(dispatch) { - return { - updateLabel: (id, label) => dispatch(updateLayerLabel(id, label)), - updateMinZoom: (id, minZoom) => dispatch(updateLayerMinZoom(id, minZoom)), - updateMaxZoom: (id, maxZoom) => dispatch(updateLayerMaxZoom(id, maxZoom)), - updateAlpha: (id, alpha) => dispatch(updateLayerAlpha(id, alpha)), - }; -} - -const connectedLayerSettings = connect(mapStateToProps, mapDispatchToProps)(LayerSettings); -export { connectedLayerSettings as LayerSettings }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js deleted file mode 100644 index 1d352913e54a3..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ /dev/null @@ -1,117 +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 React, { Fragment } from 'react'; - -import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ValidatedRange } from '../../../../../../../plugins/maps/public/components/validated_range'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ValidatedDualRange } from '../../../../../../../../src/plugins/kibana_react/public'; -export function LayerSettings(props) { - const onLabelChange = event => { - const label = event.target.value; - props.updateLabel(props.layerId, label); - }; - - const onZoomChange = ([min, max]) => { - props.updateMinZoom(props.layerId, Math.max(props.minVisibilityZoom, parseInt(min, 10))); - props.updateMaxZoom(props.layerId, Math.min(props.maxVisibilityZoom, parseInt(max, 10))); - }; - - const onAlphaChange = alpha => { - const alphaDecimal = alpha / 100; - props.updateAlpha(props.layerId, alphaDecimal); - }; - - const renderZoomSliders = () => { - return ( - <ValidatedDualRange - label={i18n.translate('xpack.maps.layerPanel.settingsPanel.visibleZoomLabel', { - defaultMessage: 'Visibility', - })} - formRowDisplay="columnCompressed" - min={props.minVisibilityZoom} - max={props.maxVisibilityZoom} - value={[props.minZoom, props.maxZoom]} - showInput="inputWithPopover" - showRange - showLabels - onChange={onZoomChange} - allowEmptyRange={false} - compressed - prepend={i18n.translate('xpack.maps.layerPanel.settingsPanel.visibleZoom', { - defaultMessage: 'Zoom levels', - })} - /> - ); - }; - - const renderLabel = () => { - return ( - <EuiFormRow - label={i18n.translate('xpack.maps.layerPanel.settingsPanel.layerNameLabel', { - defaultMessage: 'Name', - })} - display="columnCompressed" - > - <EuiFieldText value={props.label} onChange={onLabelChange} compressed /> - </EuiFormRow> - ); - }; - - const renderAlphaSlider = () => { - const alphaPercent = Math.round(props.alpha * 100); - - return ( - <EuiFormRow - label={i18n.translate('xpack.maps.layerPanel.settingsPanel.layerTransparencyLabel', { - defaultMessage: 'Opacity', - })} - display="columnCompressed" - > - <ValidatedRange - min={0} - max={100} - step={1} - value={alphaPercent} - onChange={onAlphaChange} - showInput - showRange - compressed - append={i18n.translate('xpack.maps.layerPanel.settingsPanel.percentageLabel', { - defaultMessage: '%', - description: 'Percentage', - })} - /> - </EuiFormRow> - ); - }; - - return ( - <Fragment> - <EuiPanel> - <EuiTitle size="xs"> - <h5> - <FormattedMessage - id="xpack.maps.layerPanel.layerSettingsTitle" - defaultMessage="Layer settings" - /> - </h5> - </EuiTitle> - - <EuiSpacer size="m" /> - {renderLabel()} - {renderZoomSliders()} - {renderAlphaSlider()} - </EuiPanel> - - <EuiSpacer size="s" /> - </Fragment> - ); -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.d.ts b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.d.ts deleted file mode 100644 index cf4fdc7be70c6..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.d.ts +++ /dev/null @@ -1,8 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -export * from '../../../../../../plugins/maps/public/connected_components/layer_panel/view'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js deleted file mode 100644 index 2521318f0b3c9..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ /dev/null @@ -1,238 +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 React, { Fragment } from 'react'; - -import { FilterEditor } from './filter_editor'; -import { JoinEditor } from './join_editor'; -import { FlyoutFooter } from './flyout_footer'; -import { LayerErrors } from './layer_errors'; -import { LayerSettings } from './layer_settings'; -import { StyleSettings } from './style_settings'; -import { - EuiButtonIcon, - EuiFlexItem, - EuiTitle, - EuiPanel, - EuiFlexGroup, - EuiFlyoutHeader, - EuiFlyoutFooter, - EuiSpacer, - EuiAccordion, - EuiText, - EuiLink, -} from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; -import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getData, getCore } from '../../../../../../plugins/maps/public/kibana_services'; - -const localStorage = new Storage(window.localStorage); - -export class LayerPanel extends React.Component { - state = { - displayName: '', - immutableSourceProps: [], - leftJoinFields: null, - }; - - componentDidMount() { - this._isMounted = true; - this.loadDisplayName(); - this.loadImmutableSourceProperties(); - this.loadLeftJoinFields(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - loadDisplayName = async () => { - if (!this.props.selectedLayer) { - return; - } - - const displayName = await this.props.selectedLayer.getDisplayName(); - if (this._isMounted) { - this.setState({ displayName }); - } - }; - - loadImmutableSourceProperties = async () => { - if (!this.props.selectedLayer) { - return; - } - - const immutableSourceProps = await this.props.selectedLayer.getImmutableSourceProperties(); - if (this._isMounted) { - this.setState({ immutableSourceProps }); - } - }; - - async loadLeftJoinFields() { - if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) { - return; - } - - let leftJoinFields; - try { - const leftFieldsInstances = await this.props.selectedLayer.getLeftJoinFields(); - const leftFieldPromises = leftFieldsInstances.map(async field => { - return { - name: field.getName(), - label: await field.getLabel(), - }; - }); - leftJoinFields = await Promise.all(leftFieldPromises); - } catch (error) { - leftJoinFields = []; - } - if (this._isMounted) { - this.setState({ leftJoinFields }); - } - } - - _onSourceChange = ({ propName, value, newLayerType }) => { - this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); - }; - - _renderFilterSection() { - if (!this.props.selectedLayer.supportsElasticsearchFilters()) { - return null; - } - - return ( - <Fragment> - <EuiPanel> - <FilterEditor /> - </EuiPanel> - <EuiSpacer size="s" /> - </Fragment> - ); - } - - _renderJoinSection() { - if (!this.props.selectedLayer.isJoinable()) { - return null; - } - - return ( - <Fragment> - <EuiPanel> - <JoinEditor - leftJoinFields={this.state.leftJoinFields} - layerDisplayName={this.state.displayName} - /> - </EuiPanel> - <EuiSpacer size="s" /> - </Fragment> - ); - } - - _renderSourceProperties() { - return this.state.immutableSourceProps.map(({ label, value, link }) => { - function renderValue() { - if (link) { - return ( - <EuiLink href={link} target="_blank"> - {value} - </EuiLink> - ); - } - return <span>{value}</span>; - } - return ( - <p key={label} className="mapLayerPanel__sourceDetail"> - <strong>{label}</strong> {renderValue()} - </p> - ); - }); - } - - render() { - const { selectedLayer } = this.props; - - if (!selectedLayer) { - return null; - } - - return ( - <KibanaContextProvider - services={{ - appName: 'maps', - storage: localStorage, - data: getData(), - ...getCore(), - }} - > - <EuiFlexGroup direction="column" gutterSize="none"> - <EuiFlyoutHeader hasBorder className="mapLayerPanel__header"> - <EuiFlexGroup responsive={false} alignItems="center" gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiButtonIcon - aria-label={i18n.translate('xpack.maps.layerPanel.fitToBoundsAriaLabel', { - defaultMessage: 'Fit to bounds', - })} - iconType={selectedLayer.getLayerTypeIconName()} - onClick={this.props.fitToBounds} - > - <FormattedMessage - id="xpack.maps.layerPanel.fitToBoundsButtonLabel" - defaultMessage="Fit" - /> - </EuiButtonIcon> - </EuiFlexItem> - <EuiFlexItem> - <EuiTitle size="s"> - <h2>{this.state.displayName}</h2> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="xs" /> - <div className="mapLayerPanel__sourceDetails"> - <EuiAccordion - id="accordion1" - buttonContent={i18n.translate('xpack.maps.layerPanel.sourceDetailsLabel', { - defaultMessage: 'Source details', - })} - > - <EuiText color="subdued" size="s"> - <EuiSpacer size="xs" /> - {this._renderSourceProperties()} - </EuiText> - </EuiAccordion> - </div> - </EuiFlyoutHeader> - - <div className="mapLayerPanel__body"> - <div className="mapLayerPanel__bodyOverflow"> - <LayerErrors /> - - <LayerSettings /> - - {this.props.selectedLayer.renderSourceSettingsEditor({ - onChange: this._onSourceChange, - })} - - {this._renderFilterSection()} - - {this._renderJoinSection()} - - <StyleSettings /> - </div> - </div> - - <EuiFlyoutFooter className="mapLayerPanel__footer"> - <FlyoutFooter /> - </EuiFlyoutFooter> - </EuiFlexGroup> - </KibanaContextProvider> - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js deleted file mode 100644 index 350cb7028abee..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ /dev/null @@ -1,75 +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 { connect } from 'react-redux'; -import { MBMapContainer } from './view'; -import { - mapExtentChanged, - mapReady, - mapDestroyed, - setMouseCoordinates, - clearMouseCoordinates, - clearGoto, - setMapInitError, -} from '../../../actions/map_actions'; -import { - getLayerList, - getMapReady, - getGoto, - getScrollZoom, - isInteractiveDisabled, - isTooltipControlDisabled, - isViewControlHidden, -} from '../../../selectors/map_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../../../plugins/maps/public/reducers/non_serializable_instances'; - -function mapStateToProps(state = {}) { - return { - isMapReady: getMapReady(state), - layerList: getLayerList(state), - goto: getGoto(state), - inspectorAdapters: getInspectorAdapters(state), - scrollZoom: getScrollZoom(state), - disableInteractive: isInteractiveDisabled(state), - disableTooltipControl: isTooltipControlDisabled(state), - disableTooltipControl: isTooltipControlDisabled(state), - hideViewControl: isViewControlHidden(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - extentChanged: e => { - dispatch(mapExtentChanged(e)); - }, - onMapReady: e => { - dispatch(clearGoto()); - dispatch(mapExtentChanged(e)); - dispatch(mapReady()); - }, - onMapDestroyed: () => { - dispatch(mapDestroyed()); - }, - setMouseCoordinates: ({ lat, lon }) => { - dispatch(setMouseCoordinates({ lat, lon })); - }, - clearMouseCoordinates: () => { - dispatch(clearMouseCoordinates()); - }, - clearGoto: () => { - dispatch(clearGoto()); - }, - setMapInitError(errorMessage) { - dispatch(setMapInitError(errorMessage)); - }, - }; -} - -const connectedMBMapContainer = connect(mapStateToProps, mapDispatchToProps, null, { - forwardRef: true, -})(MBMapContainer); -export { connectedMBMapContainer as MBMapContainer }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js deleted file mode 100644 index a1d1341b7c4f7..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/utils.js +++ /dev/null @@ -1,107 +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 _ from 'lodash'; -import { - loadSpriteSheetImageData, - addSpriteSheetToMapFromImageData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/maps/public/connected_components/map/mb/utils'; - -export { loadSpriteSheetImageData, addSpriteSheetToMapFromImageData }; - -export function removeOrphanedSourcesAndLayers(mbMap, layerList) { - const mbStyle = mbMap.getStyle(); - - const mbLayerIdsToRemove = []; - mbStyle.layers.forEach(mbLayer => { - const layer = layerList.find(layer => { - return layer.ownsMbLayerId(mbLayer.id); - }); - if (!layer) { - mbLayerIdsToRemove.push(mbLayer.id); - } - }); - mbLayerIdsToRemove.forEach(mbLayerId => mbMap.removeLayer(mbLayerId)); - - const mbSourcesToRemove = []; - for (const mbSourceId in mbStyle.sources) { - if (mbStyle.sources.hasOwnProperty(mbSourceId)) { - const layer = layerList.find(layer => { - return layer.ownsMbSourceId(mbSourceId); - }); - if (!layer) { - mbSourcesToRemove.push(mbSourceId); - } - } - } - mbSourcesToRemove.forEach(mbSourceId => mbMap.removeSource(mbSourceId)); -} - -/** - * This is function assumes only a single layer moved in the layerList, compared to mbMap - * It is optimized to minimize the amount of mbMap.moveLayer calls. - * @param mbMap - * @param layerList - */ -export function syncLayerOrderForSingleLayer(mbMap, layerList) { - if (!layerList || layerList.length === 0) { - return; - } - - const mbLayers = mbMap.getStyle().layers.slice(); - const layerIds = mbLayers.map(mbLayer => { - const layer = layerList.find(layer => layer.ownsMbLayerId(mbLayer.id)); - return layer.getId(); - }); - - const currentLayerOrderLayerIds = _.uniq(layerIds); - - const newLayerOrderLayerIdsUnfiltered = layerList.map(l => l.getId()); - const newLayerOrderLayerIds = newLayerOrderLayerIdsUnfiltered.filter(layerId => - currentLayerOrderLayerIds.includes(layerId) - ); - - let netPos = 0; - let netNeg = 0; - const movementArr = currentLayerOrderLayerIds.reduce((accu, id, idx) => { - const movement = newLayerOrderLayerIds.findIndex(newOId => newOId === id) - idx; - movement > 0 ? netPos++ : movement < 0 && netNeg++; - accu.push({ id, movement }); - return accu; - }, []); - if (netPos === 0 && netNeg === 0) { - return; - } - const movedLayerId = - (netPos >= netNeg && movementArr.find(l => l.movement < 0).id) || - (netPos < netNeg && movementArr.find(l => l.movement > 0).id); - const nextLayerIdx = newLayerOrderLayerIds.findIndex(layerId => layerId === movedLayerId) + 1; - - let nextMbLayerId; - if (nextLayerIdx === newLayerOrderLayerIds.length) { - nextMbLayerId = null; - } else { - const foundLayer = mbLayers.find(({ id: mbLayerId }) => { - const layerId = newLayerOrderLayerIds[nextLayerIdx]; - const layer = layerList.find(layer => layer.getId() === layerId); - return layer.ownsMbLayerId(mbLayerId); - }); - nextMbLayerId = foundLayer.id; - } - - const movedLayer = layerList.find(layer => layer.getId() === movedLayerId); - mbLayers.forEach(({ id: mbLayerId }) => { - if (movedLayer.ownsMbLayerId(mbLayerId)) { - mbMap.moveLayer(mbLayerId, nextMbLayerId); - } - }); -} - -export async function addSpritesheetToMap(json, imgUrl, mbMap) { - const imgData = await loadSpriteSheetImageData(imgUrl); - addSpriteSheetToMapFromImageData(json, imgData, mbMap); -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js deleted file mode 100644 index a36e1d7048e92..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ /dev/null @@ -1,322 +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 _ from 'lodash'; -import React from 'react'; -import { ResizeChecker } from '../../../../../../../../src/plugins/kibana_utils/public'; -import { - syncLayerOrderForSingleLayer, - removeOrphanedSourcesAndLayers, - addSpritesheetToMap, -} from './utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getGlyphUrl, isRetina } from '../../../../../../../plugins/maps/public/meta'; -import { - DECIMAL_DEGREES_PRECISION, - MAX_ZOOM, - MIN_ZOOM, - ZOOM_PRECISION, -} from '../../../../common/constants'; -import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; -import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; -import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; -import { spritesheet } from '@elastic/maki'; -import sprites1 from '@elastic/maki/dist/sprite@1.png'; -import sprites2 from '@elastic/maki/dist/sprite@2.png'; -import { DrawControl } from './draw_control'; -import { TooltipControl } from './tooltip_control'; -import { - clampToLatBounds, - clampToLonBounds, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInjectedVarFunc } from '../../../../../../../plugins/maps/public/kibana_services'; - -mapboxgl.workerUrl = mbWorkerUrl; -mapboxgl.setRTLTextPlugin(mbRtlPlugin); - -export class MBMapContainer extends React.Component { - state = { - prevLayerList: undefined, - hasSyncedLayerList: false, - mbMap: undefined, - }; - - static getDerivedStateFromProps(nextProps, prevState) { - const nextLayerList = nextProps.layerList; - if (nextLayerList !== prevState.prevLayerList) { - return { - prevLayerList: nextLayerList, - hasSyncedLayerList: false, - }; - } - - return null; - } - - componentDidMount() { - this._initializeMap(); - this._isMounted = true; - } - - componentDidUpdate() { - if (this.state.mbMap) { - // do not debounce syncing of map-state - this._syncMbMapWithMapState(); - this._debouncedSync(); - } - } - - componentWillUnmount() { - this._isMounted = false; - if (this._checker) { - this._checker.destroy(); - } - if (this.state.mbMap) { - this.state.mbMap.remove(); - this.state.mbMap = null; - } - this.props.onMapDestroyed(); - } - - _debouncedSync = _.debounce(() => { - if (this._isMounted) { - if (!this.state.hasSyncedLayerList) { - this.setState( - { - hasSyncedLayerList: true, - }, - () => { - this._syncMbMapWithLayerList(); - this._syncMbMapWithInspector(); - } - ); - } - } - }, 256); - - _getMapState() { - const zoom = this.state.mbMap.getZoom(); - const mbCenter = this.state.mbMap.getCenter(); - const mbBounds = this.state.mbMap.getBounds(); - return { - zoom: _.round(zoom, ZOOM_PRECISION), - center: { - lon: _.round(mbCenter.lng, DECIMAL_DEGREES_PRECISION), - lat: _.round(mbCenter.lat, DECIMAL_DEGREES_PRECISION), - }, - extent: { - minLon: _.round(mbBounds.getWest(), DECIMAL_DEGREES_PRECISION), - minLat: _.round(mbBounds.getSouth(), DECIMAL_DEGREES_PRECISION), - maxLon: _.round(mbBounds.getEast(), DECIMAL_DEGREES_PRECISION), - maxLat: _.round(mbBounds.getNorth(), DECIMAL_DEGREES_PRECISION), - }, - }; - } - - async _createMbMapInstance() { - return new Promise(resolve => { - const mbStyle = { - version: 8, - sources: {}, - layers: [], - }; - const glyphUrl = getGlyphUrl(); - if (glyphUrl) { - mbStyle.glyphs = glyphUrl; - } - - const options = { - attributionControl: false, - container: this.refs.mapContainer, - style: mbStyle, - scrollZoom: this.props.scrollZoom, - preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false), - interactive: !this.props.disableInteractive, - minZoom: MIN_ZOOM, - maxZoom: MAX_ZOOM, - }; - const initialView = _.get(this.props.goto, 'center'); - if (initialView) { - options.zoom = initialView.zoom; - options.center = { - lng: initialView.lon, - lat: initialView.lat, - }; - } else { - options.bounds = [-170, -60, 170, 75]; - } - const mbMap = new mapboxgl.Map(options); - mbMap.dragRotate.disable(); - mbMap.touchZoomRotate.disableRotation(); - if (!this.props.disableInteractive) { - mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); - } - - let emptyImage; - mbMap.on('styleimagemissing', e => { - if (emptyImage) { - mbMap.addImage(e.id, emptyImage); - } - }); - mbMap.on('load', () => { - emptyImage = new Image(); - - emptyImage.src = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; - emptyImage.crossOrigin = 'anonymous'; - resolve(mbMap); - }); - }); - } - - async _initializeMap() { - let mbMap; - try { - mbMap = await this._createMbMapInstance(); - } catch (error) { - this.props.setMapInitError(error.message); - return; - } - - if (!this._isMounted) { - return; - } - - this.setState({ mbMap }, () => { - this._loadMakiSprites(); - this._initResizerChecker(); - this._registerMapEventListeners(); - this.props.onMapReady(this._getMapState()); - }); - } - - _registerMapEventListeners() { - // moveend callback is debounced to avoid updating map extent state while map extent is still changing - // moveend is fired while the map extent is still changing in the following scenarios - // 1) During opening/closing of layer details panel, the EUI animation results in 8 moveend events - // 2) Setting map zoom and center from goto is done in 2 API calls, resulting in 2 moveend events - this.state.mbMap.on( - 'moveend', - _.debounce(() => { - this.props.extentChanged(this._getMapState()); - }, 100) - ); - // Attach event only if view control is visible, which shows lat/lon - if (!this.props.hideViewControl) { - const throttledSetMouseCoordinates = _.throttle(e => { - this.props.setMouseCoordinates({ - lat: e.lngLat.lat, - lon: e.lngLat.lng, - }); - }, 100); - this.state.mbMap.on('mousemove', throttledSetMouseCoordinates); - this.state.mbMap.on('mouseout', () => { - throttledSetMouseCoordinates.cancel(); // cancel any delayed setMouseCoordinates invocations - this.props.clearMouseCoordinates(); - }); - } - } - - _initResizerChecker() { - this._checker = new ResizeChecker(this.refs.mapContainer); - this._checker.on('resize', () => { - this.state.mbMap.resize(); - }); - } - - _loadMakiSprites() { - const sprites = isRetina() ? sprites2 : sprites1; - const json = isRetina() ? spritesheet[2] : spritesheet[1]; - addSpritesheetToMap(json, sprites, this.state.mbMap); - } - - _syncMbMapWithMapState = () => { - const { isMapReady, goto, clearGoto } = this.props; - - if (!isMapReady || !goto) { - return; - } - - clearGoto(); - - if (goto.bounds) { - //clamping ot -89/89 latitudes since Mapboxgl does not seem to handle bounds that contain the poles (logs errors to the console when using -90/90) - const lnLatBounds = new mapboxgl.LngLatBounds( - new mapboxgl.LngLat( - clampToLonBounds(goto.bounds.minLon), - clampToLatBounds(goto.bounds.minLat) - ), - new mapboxgl.LngLat( - clampToLonBounds(goto.bounds.maxLon), - clampToLatBounds(goto.bounds.maxLat) - ) - ); - //maxZoom ensure we're not zooming in too far on single points or small shapes - //the padding is to avoid too tight of a fit around edges - this.state.mbMap.fitBounds(lnLatBounds, { maxZoom: 17, padding: 16 }); - } else if (goto.center) { - this.state.mbMap.setZoom(goto.center.zoom); - this.state.mbMap.setCenter({ - lng: goto.center.lon, - lat: goto.center.lat, - }); - } - }; - - _syncMbMapWithLayerList = () => { - if (!this.props.isMapReady) { - return; - } - - removeOrphanedSourcesAndLayers(this.state.mbMap, this.props.layerList); - this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap)); - syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList); - }; - - _syncMbMapWithInspector = () => { - if (!this.props.isMapReady || !this.props.inspectorAdapters.map) { - return; - } - - const stats = { - center: this.state.mbMap.getCenter().toArray(), - zoom: this.state.mbMap.getZoom(), - }; - this.props.inspectorAdapters.map.setMapState({ - stats, - style: this.state.mbMap.getStyle(), - }); - }; - - render() { - let drawControl; - let tooltipControl; - if (this.state.mbMap) { - drawControl = <DrawControl mbMap={this.state.mbMap} addFilters={this.props.addFilters} />; - tooltipControl = !this.props.disableTooltipControl ? ( - <TooltipControl - mbMap={this.state.mbMap} - addFilters={this.props.addFilters} - geoFields={this.props.geoFields} - renderTooltipContent={this.props.renderTooltipContent} - /> - ) : null; - } - return ( - <div - id="mapContainer" - className="mapContainer" - ref="mapContainer" - data-test-subj="mapContainer" - > - {drawControl} - {tooltipControl} - </div> - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/_index.scss deleted file mode 100644 index 01aea403b27f0..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/_index.scss +++ /dev/null @@ -1,21 +0,0 @@ -@import './tools_control/index'; - -.mapToolbarOverlay { - position: absolute; - top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin - left: $euiSizeM; - z-index: 2; // Sit on top of mapbox controls shadow -} - -.mapToolbarOverlay__button { - @include size($euiSizeXL); - // sass-lint:disable-block no-important - background-color: $euiColorEmptyShade !important; - pointer-events: all; - - &:enabled, - &:enabled:hover, - &:enabled:focus { - @include euiBottomShadowLarge; - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js deleted file mode 100644 index 2b6fae26098be..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js +++ /dev/null @@ -1,38 +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 { connect } from 'react-redux'; -import { SetViewControl } from './set_view_control'; -import { setGotoWithCenter } from '../../../actions/map_actions'; -import { getMapZoom, getMapCenter } from '../../../selectors/map_selectors'; -import { closeSetView, openSetView } from '../../../actions/ui_actions'; -import { getIsSetViewOpen } from '../../../selectors/ui_selectors'; - -function mapStateToProps(state = {}) { - return { - isSetViewOpen: getIsSetViewOpen(state), - zoom: getMapZoom(state), - center: getMapCenter(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - onSubmit: ({ lat, lon, zoom }) => { - dispatch(closeSetView()); - dispatch(setGotoWithCenter({ lat, lon, zoom })); - }, - closeSetView: () => { - dispatch(closeSetView()); - }, - openSetView: () => { - dispatch(openSetView()); - }, - }; -} - -const connectedSetViewControl = connect(mapStateToProps, mapDispatchToProps)(SetViewControl); -export { connectedSetViewControl as SetViewControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_index.scss deleted file mode 100644 index cc1ab35039dac..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_index.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import './mixins'; - -@import './widget_overlay'; -@import './attribution_control/attribution_control'; -@import './layer_control/index'; -@import './view_control/view_control'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap deleted file mode 100644 index 560ebad89c50e..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap +++ /dev/null @@ -1,189 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LayerControl is rendered 1`] = ` -<Fragment> - <EuiPanel - className="mapWidgetControl mapWidgetControl-hasShadow" - grow={false} - paddingSize="none" - > - <EuiFlexItem - className="mapWidgetControl__headerFlexItem" - grow={false} - > - <EuiFlexGroup - alignItems="center" - gutterSize="none" - justifyContent="spaceBetween" - responsive={false} - > - <EuiFlexItem> - <EuiTitle - className="mapWidgetControl__header" - size="xxxs" - > - <h2> - <FormattedMessage - defaultMessage="Layers" - id="xpack.maps.layerControl.layersTitle" - values={Object {}} - /> - </h2> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiToolTip - content="Collapse layers panel" - delay="long" - position="top" - > - <EuiButtonIcon - aria-label="Collapse layers panel" - className="mapLayerControl__closeLayerTOCButton" - color="text" - data-test-subj="mapToggleLegendButton" - iconType="menuRight" - onClick={[Function]} - /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem - className="mapLayerControl" - > - <LayerTOC /> - </EuiFlexItem> - </EuiPanel> - <EuiSpacer - size="s" - /> - <EuiButton - className="mapLayerControl__addLayerButton" - data-test-subj="addLayerButton" - fill={true} - fullWidth={true} - isDisabled={true} - onClick={[Function]} - > - <FormattedMessage - defaultMessage="Add layer" - id="xpack.maps.layerControl.addLayerButtonLabel" - values={Object {}} - /> - </EuiButton> -</Fragment> -`; - -exports[`LayerControl isLayerTOCOpen Should render expand button 1`] = ` -<EuiToolTip - content="Expand layers panel" - delay="long" - position="left" -> - <EuiButtonIcon - aria-label="Expand layers panel" - className="mapLayerControl__openLayerTOCButton" - color="text" - iconType="menuLeft" - onClick={[Function]} - /> -</EuiToolTip> -`; - -exports[`LayerControl isLayerTOCOpen Should render expand button with error icon when layer has error 1`] = ` -<EuiToolTip - content="Expand layers panel" - delay="long" - position="left" -> - <EuiButtonIcon - aria-label="Expand layers panel" - className="mapLayerControl__openLayerTOCButton" - color="text" - iconType="alert" - onClick={[Function]} - /> -</EuiToolTip> -`; - -exports[`LayerControl isLayerTOCOpen Should render expand button with loading icon when layer is loading 1`] = ` -<EuiToolTip - content="Expand layers panel" - delay="long" - position="left" -> - <button - aria-label="Expand layers panel" - className="euiButtonIcon euiButtonIcon--text mapLayerControl__openLayerTOCButton" - onClick={[Function]} - type="button" - > - <EuiLoadingSpinner - size="m" - /> - </button> -</EuiToolTip> -`; - -exports[`LayerControl isReadOnly 1`] = ` -<Fragment> - <EuiPanel - className="mapWidgetControl mapWidgetControl-hasShadow" - grow={false} - paddingSize="none" - > - <EuiFlexItem - className="mapWidgetControl__headerFlexItem" - grow={false} - > - <EuiFlexGroup - alignItems="center" - gutterSize="none" - justifyContent="spaceBetween" - responsive={false} - > - <EuiFlexItem> - <EuiTitle - className="mapWidgetControl__header" - size="xxxs" - > - <h2> - <FormattedMessage - defaultMessage="Layers" - id="xpack.maps.layerControl.layersTitle" - values={Object {}} - /> - </h2> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem - grow={false} - > - <EuiToolTip - content="Collapse layers panel" - delay="long" - position="top" - > - <EuiButtonIcon - aria-label="Collapse layers panel" - className="mapLayerControl__closeLayerTOCButton" - color="text" - data-test-subj="mapToggleLegendButton" - iconType="menuRight" - onClick={[Function]} - /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem - className="mapLayerControl" - > - <LayerTOC /> - </EuiFlexItem> - </EuiPanel> -</Fragment> -`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss deleted file mode 100644 index 761ef9d17b4c2..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './layer_control'; -@import './layer_toc/toc_entry/toc_entry'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js deleted file mode 100644 index 04de5f71f5bfc..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js +++ /dev/null @@ -1,45 +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 { connect } from 'react-redux'; -import { LayerControl } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout, setIsLayerTOCOpen } from '../../../actions/ui_actions'; -import { setSelectedLayer } from '../../../actions/map_actions'; -import { - getIsReadOnly, - getIsLayerTOCOpen, - getFlyoutDisplay, -} from '../../../selectors/ui_selectors'; -import { getLayerList } from '../../../selectors/map_selectors'; - -function mapStateToProps(state = {}) { - return { - isReadOnly: getIsReadOnly(state), - isLayerTOCOpen: getIsLayerTOCOpen(state), - layerList: getLayerList(state), - isAddButtonActive: getFlyoutDisplay(state) === FLYOUT_STATE.NONE, - }; -} - -function mapDispatchToProps(dispatch) { - return { - showAddLayerWizard: async () => { - await dispatch(setSelectedLayer(null)); - dispatch(updateFlyout(FLYOUT_STATE.ADD_LAYER_WIZARD)); - }, - closeLayerTOC: () => { - dispatch(setIsLayerTOCOpen(false)); - }, - openLayerTOC: () => { - dispatch(setIsLayerTOCOpen(true)); - }, - }; -} - -const connectedLayerControl = connect(mapStateToProps, mapDispatchToProps)(LayerControl); -export { connectedLayerControl as LayerControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap deleted file mode 100644 index 27ea52bfed044..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap +++ /dev/null @@ -1,328 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TOCEntry is rendered 1`] = ` -<div - className="mapTocEntry" - data-layerid="1" - id="1" -> - <div - className="mapTocEntry-visible" - > - <LayerTocActions - cloneLayer={[Function]} - displayName="layer 1" - editLayer={[Function]} - escapedDisplayName="layer_1" - fitToBounds={[Function]} - layer={ - Object { - "getDisplayName": [Function], - "getId": [Function], - "hasErrors": [Function], - "hasLegendDetails": [Function], - "isVisible": [Function], - "renderLegendDetails": [Function], - "showAtZoomLevel": [Function], - } - } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} - /> - <div - className="mapTocEntry__layerIcons" - > - <EuiButtonIcon - aria-label="Edit layer" - iconType="pencil" - onClick={[Function]} - title="Edit layer" - /> - <EuiButtonIcon - aria-label="Reorder layer" - className="mapTocEntry__grab" - color="subdued" - iconType="grab" - title="Reorder layer" - /> - </div> - </div> - <span - className="mapTocEntry__detailsToggle" - > - <button - aria-label="Show layer details" - className="mapTocEntry__detailsToggleButton" - onClick={[Function]} - title="Show layer details" - > - <EuiIcon - className="eui-alignBaseline" - size="s" - type="arrowDown" - /> - </button> - </span> -</div> -`; - -exports[`TOCEntry props Should shade background when not selected layer 1`] = ` -<div - className="mapTocEntry" - data-layerid="1" - id="1" -> - <div - className="mapTocEntry-visible" - > - <LayerTocActions - cloneLayer={[Function]} - displayName="layer 1" - editLayer={[Function]} - escapedDisplayName="layer_1" - fitToBounds={[Function]} - layer={ - Object { - "getDisplayName": [Function], - "getId": [Function], - "hasErrors": [Function], - "hasLegendDetails": [Function], - "isVisible": [Function], - "renderLegendDetails": [Function], - "showAtZoomLevel": [Function], - } - } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} - /> - <div - className="mapTocEntry__layerIcons" - > - <EuiButtonIcon - aria-label="Edit layer" - iconType="pencil" - onClick={[Function]} - title="Edit layer" - /> - <EuiButtonIcon - aria-label="Reorder layer" - className="mapTocEntry__grab" - color="subdued" - iconType="grab" - title="Reorder layer" - /> - </div> - </div> - <span - className="mapTocEntry__detailsToggle" - > - <button - aria-label="Show layer details" - className="mapTocEntry__detailsToggleButton" - onClick={[Function]} - title="Show layer details" - > - <EuiIcon - className="eui-alignBaseline" - size="s" - type="arrowDown" - /> - </button> - </span> -</div> -`; - -exports[`TOCEntry props Should shade background when selected layer 1`] = ` -<div - className="mapTocEntry mapTocEntry-isSelected" - data-layerid="1" - id="1" -> - <div - className="mapTocEntry-visible" - > - <LayerTocActions - cloneLayer={[Function]} - displayName="layer 1" - editLayer={[Function]} - escapedDisplayName="layer_1" - fitToBounds={[Function]} - layer={ - Object { - "getDisplayName": [Function], - "getId": [Function], - "hasErrors": [Function], - "hasLegendDetails": [Function], - "isVisible": [Function], - "renderLegendDetails": [Function], - "showAtZoomLevel": [Function], - } - } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} - /> - <div - className="mapTocEntry__layerIcons" - > - <EuiButtonIcon - aria-label="Edit layer" - iconType="pencil" - onClick={[Function]} - title="Edit layer" - /> - <EuiButtonIcon - aria-label="Reorder layer" - className="mapTocEntry__grab" - color="subdued" - iconType="grab" - title="Reorder layer" - /> - </div> - </div> - <span - className="mapTocEntry__detailsToggle" - > - <button - aria-label="Show layer details" - className="mapTocEntry__detailsToggleButton" - onClick={[Function]} - title="Show layer details" - > - <EuiIcon - className="eui-alignBaseline" - size="s" - type="arrowDown" - /> - </button> - </span> -</div> -`; - -exports[`TOCEntry props isReadOnly 1`] = ` -<div - className="mapTocEntry" - data-layerid="1" - id="1" -> - <div - className="mapTocEntry-visible" - > - <LayerTocActions - cloneLayer={[Function]} - displayName="layer 1" - editLayer={[Function]} - escapedDisplayName="layer_1" - fitToBounds={[Function]} - isReadOnly={true} - layer={ - Object { - "getDisplayName": [Function], - "getId": [Function], - "hasErrors": [Function], - "hasLegendDetails": [Function], - "isVisible": [Function], - "renderLegendDetails": [Function], - "showAtZoomLevel": [Function], - } - } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} - /> - </div> - <span - className="mapTocEntry__detailsToggle" - > - <button - aria-label="Show layer details" - className="mapTocEntry__detailsToggleButton" - onClick={[Function]} - title="Show layer details" - > - <EuiIcon - className="eui-alignBaseline" - size="s" - type="arrowDown" - /> - </button> - </span> -</div> -`; - -exports[`TOCEntry props should display layer details when isLegendDetailsOpen is true 1`] = ` -<div - className="mapTocEntry" - data-layerid="1" - id="1" -> - <div - className="mapTocEntry-visible" - > - <LayerTocActions - cloneLayer={[Function]} - displayName="layer 1" - editLayer={[Function]} - escapedDisplayName="layer_1" - fitToBounds={[Function]} - layer={ - Object { - "getDisplayName": [Function], - "getId": [Function], - "hasErrors": [Function], - "hasLegendDetails": [Function], - "isVisible": [Function], - "renderLegendDetails": [Function], - "showAtZoomLevel": [Function], - } - } - removeLayer={[Function]} - toggleVisible={[Function]} - zoom={0} - /> - <div - className="mapTocEntry__layerIcons" - > - <EuiButtonIcon - aria-label="Edit layer" - iconType="pencil" - onClick={[Function]} - title="Edit layer" - /> - <EuiButtonIcon - aria-label="Reorder layer" - className="mapTocEntry__grab" - color="subdued" - iconType="grab" - title="Reorder layer" - /> - </div> - </div> - <div - className="mapTocEntry__layerDetails" - data-test-subj="mapLayerTOCDetailslayer_1" - > - <div> - TOC details mock - </div> - </div> - <span - className="mapTocEntry__detailsToggle" - > - <button - aria-label="Hide layer details" - className="mapTocEntry__detailsToggleButton" - onClick={[Function]} - title="Hide layer details" - > - <EuiIcon - className="eui-alignBaseline" - size="s" - type="arrowUp" - /> - </button> - </span> -</div> -`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js deleted file mode 100644 index 588445d0b4992..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js +++ /dev/null @@ -1,69 +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 _ from 'lodash'; -import { connect } from 'react-redux'; -import { TOCEntry } from './view'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../../../plugins/maps/public/reducers/ui'; -import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions'; -import { getIsReadOnly, getOpenTOCDetails } from '../../../../../selectors/ui_selectors'; -import { - fitToLayerExtent, - setSelectedLayer, - toggleLayerVisible, - removeTransientLayer, - cloneLayer, - removeLayer, -} from '../../../../../actions/map_actions'; - -import { - hasDirtyState, - getSelectedLayer, - isUsingSearch, -} from '../../../../../selectors/map_selectors'; - -function mapStateToProps(state = {}, ownProps) { - return { - isReadOnly: getIsReadOnly(state), - zoom: _.get(state, 'map.mapState.zoom', 0), - selectedLayer: getSelectedLayer(state), - hasDirtyStateSelector: hasDirtyState(state), - isLegendDetailsOpen: getOpenTOCDetails(state).includes(ownProps.layer.getId()), - isUsingSearch: isUsingSearch(state), - }; -} - -function mapDispatchToProps(dispatch) { - return { - openLayerPanel: async layerId => { - await dispatch(removeTransientLayer()); - await dispatch(setSelectedLayer(layerId)); - dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); - }, - toggleVisible: layerId => { - dispatch(toggleLayerVisible(layerId)); - }, - fitToBounds: layerId => { - dispatch(fitToLayerExtent(layerId)); - }, - cloneLayer: layerId => { - dispatch(cloneLayer(layerId)); - }, - removeLayer: layerId => { - dispatch(removeLayer(layerId)); - }, - hideTOCDetails: layerId => { - dispatch(hideTOCDetails(layerId)); - }, - showTOCDetails: layerId => { - dispatch(showTOCDetails(layerId)); - }, - }; -} - -const connectedTOCEntry = connect(mapStateToProps, mapDispatchToProps)(TOCEntry); -export { connectedTOCEntry as TOCEntry }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js deleted file mode 100644 index fe56523fb2580..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ /dev/null @@ -1,284 +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 React from 'react'; -import classNames from 'classnames'; - -import { EuiIcon, EuiOverlayMask, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; -import { LayerTocActions } from '../../../../../components/layer_toc_actions'; -import { i18n } from '@kbn/i18n'; - -function escapeLayerName(name) { - return name ? name.split(' ').join('_') : ''; -} - -export class TOCEntry extends React.Component { - state = { - displayName: null, - hasLegendDetails: false, - shouldShowModal: false, - }; - - componentDidMount() { - this._isMounted = true; - this._updateDisplayName(); - this._loadHasLegendDetails(); - } - - componentWillUnmount() { - this._isMounted = false; - } - - componentDidUpdate() { - this._updateDisplayName(); - this._loadHasLegendDetails(); - } - - _toggleLayerDetailsVisibility = () => { - if (this.props.isLegendDetailsOpen) { - this.props.hideTOCDetails(this.props.layer.getId()); - } else { - this.props.showTOCDetails(this.props.layer.getId()); - } - }; - - async _loadHasLegendDetails() { - const hasLegendDetails = - (await this.props.layer.hasLegendDetails()) && - this.props.layer.isVisible() && - this.props.layer.showAtZoomLevel(this.props.zoom); - if (this._isMounted && hasLegendDetails !== this.state.hasLegendDetails) { - this.setState({ hasLegendDetails }); - } - } - - async _updateDisplayName() { - const label = await this.props.layer.getDisplayName(); - if (this._isMounted) { - if (label !== this.state.displayName) { - this.setState({ - displayName: label, - }); - } - } - } - - _openLayerPanelWithCheck = () => { - const { selectedLayer, hasDirtyStateSelector } = this.props; - if (selectedLayer && selectedLayer.getId() === this.props.layer.getId()) { - return; - } - - if (hasDirtyStateSelector) { - this.setState({ - shouldShowModal: true, - }); - return; - } - - this.props.openLayerPanel(this.props.layer.getId()); - }; - - _renderCancelModal() { - if (!this.state.shouldShowModal) { - return null; - } - - const closeModal = () => { - this.setState({ - shouldShowModal: false, - }); - }; - - const openPanel = () => { - closeModal(); - this.props.openLayerPanel(this.props.layer.getId()); - }; - - return ( - <EuiOverlayMask> - <EuiConfirmModal - title="Discard changes" - onCancel={closeModal} - onConfirm={openPanel} - cancelButtonText="Do not proceed" - confirmButtonText="Proceed and discard changes" - buttonColor="danger" - defaultFocusedButton="cancel" - > - <p>There are unsaved changes to your layer.</p> - <p>Are you sure you want to proceed?</p> - </EuiConfirmModal> - </EuiOverlayMask> - ); - } - - _renderLayerIcons() { - if (this.props.isReadOnly) { - return null; - } - - return ( - <div className="mapTocEntry__layerIcons"> - <EuiButtonIcon - iconType="pencil" - aria-label={i18n.translate('xpack.maps.layerControl.tocEntry.editButtonAriaLabel', { - defaultMessage: 'Edit layer', - })} - title={i18n.translate('xpack.maps.layerControl.tocEntry.editButtonTitle', { - defaultMessage: 'Edit layer', - })} - onClick={this._openLayerPanelWithCheck} - /> - - <EuiButtonIcon - iconType="grab" - color="subdued" - title={i18n.translate('xpack.maps.layerControl.tocEntry.grabButtonTitle', { - defaultMessage: 'Reorder layer', - })} - aria-label={i18n.translate('xpack.maps.layerControl.tocEntry.grabButtonAriaLabel', { - defaultMessage: 'Reorder layer', - })} - className="mapTocEntry__grab" - {...this.props.dragHandleProps} - /> - </div> - ); - } - - _renderDetailsToggle() { - if (!this.state.hasLegendDetails) { - return null; - } - - const { isLegendDetailsOpen } = this.props; - return ( - <span className="mapTocEntry__detailsToggle"> - <button - className="mapTocEntry__detailsToggleButton" - aria-label={ - isLegendDetailsOpen - ? i18n.translate('xpack.maps.layerControl.tocEntry.hideDetailsButtonAriaLabel', { - defaultMessage: 'Hide layer details', - }) - : i18n.translate('xpack.maps.layerControl.tocEntry.showDetailsButtonAriaLabel', { - defaultMessage: 'Show layer details', - }) - } - title={ - isLegendDetailsOpen - ? i18n.translate('xpack.maps.layerControl.tocEntry.hideDetailsButtonTitle', { - defaultMessage: 'Hide layer details', - }) - : i18n.translate('xpack.maps.layerControl.tocEntry.showDetailsButtonTitle', { - defaultMessage: 'Show layer details', - }) - } - onClick={this._toggleLayerDetailsVisibility} - > - <EuiIcon - className="eui-alignBaseline" - type={isLegendDetailsOpen ? 'arrowUp' : 'arrowDown'} - size="s" - /> - </button> - </span> - ); - } - - _renderLayerHeader() { - const { - removeLayer, - cloneLayer, - isReadOnly, - layer, - zoom, - toggleVisible, - fitToBounds, - isUsingSearch, - } = this.props; - - return ( - <div - className={ - layer.isVisible() && layer.showAtZoomLevel(zoom) && !layer.hasErrors() - ? 'mapTocEntry-visible' - : 'mapTocEntry-notVisible' - } - > - <LayerTocActions - layer={layer} - isUsingSearch={isUsingSearch} - fitToBounds={() => { - fitToBounds(layer.getId()); - }} - zoom={zoom} - toggleVisible={() => { - toggleVisible(layer.getId()); - }} - displayName={this.state.displayName} - escapedDisplayName={escapeLayerName(this.state.displayName)} - cloneLayer={() => { - cloneLayer(layer.getId()); - }} - editLayer={this._openLayerPanelWithCheck} - isReadOnly={isReadOnly} - removeLayer={() => { - removeLayer(layer.getId()); - }} - /> - - {this._renderLayerIcons()} - </div> - ); - } - - _renderLegendDetails = () => { - if (!this.props.isLegendDetailsOpen || !this.state.hasLegendDetails) { - return null; - } - - const tocDetails = this.props.layer.renderLegendDetails(); - if (!tocDetails) { - return null; - } - - return ( - <div - className="mapTocEntry__layerDetails" - data-test-subj={`mapLayerTOCDetails${escapeLayerName(this.state.displayName)}`} - > - {tocDetails} - </div> - ); - }; - - render() { - const classes = classNames('mapTocEntry', { - 'mapTocEntry-isDragging': this.props.isDragging, - 'mapTocEntry-isDraggingOver': this.props.isDraggingOver, - 'mapTocEntry-isSelected': - this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId(), - }); - - return ( - <div - className={classes} - id={this.props.layer.getId()} - data-layerid={this.props.layer.getId()} - > - {this._renderLayerHeader()} - - {this._renderLegendDetails()} - - {this._renderDetailsToggle()} - - {this._renderCancelModal()} - </div> - ); - } -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js deleted file mode 100644 index 537a676287042..0000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js +++ /dev/null @@ -1,162 +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 React, { Fragment } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiButton, - EuiTitle, - EuiSpacer, - EuiButtonIcon, - EuiToolTip, - EuiLoadingSpinner, -} from '@elastic/eui'; -import { LayerTOC } from './layer_toc'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -function renderExpandButton({ hasErrors, isLoading, onClick }) { - const expandLabel = i18n.translate('xpack.maps.layerControl.openLayerTOCButtonAriaLabel', { - defaultMessage: 'Expand layers panel', - }); - - if (isLoading) { - // Can not use EuiButtonIcon with spinner because spinner is a class and not an icon - return ( - <button - className="euiButtonIcon euiButtonIcon--text mapLayerControl__openLayerTOCButton" - type="button" - onClick={onClick} - aria-label={expandLabel} - > - <EuiLoadingSpinner size="m" /> - </button> - ); - } - - return ( - <EuiButtonIcon - className="mapLayerControl__openLayerTOCButton" - color="text" - onClick={onClick} - iconType={hasErrors ? 'alert' : 'menuLeft'} - aria-label={expandLabel} - /> - ); -} - -export function LayerControl({ - isReadOnly, - isLayerTOCOpen, - showAddLayerWizard, - closeLayerTOC, - openLayerTOC, - layerList, - isAddButtonActive, -}) { - if (!isLayerTOCOpen) { - const hasErrors = layerList.some(layer => { - return layer.hasErrors(); - }); - const isLoading = layerList.some(layer => { - return layer.isLayerLoading(); - }); - - return ( - <EuiToolTip - delay="long" - content={i18n.translate('xpack.maps.layerControl.openLayerTOCButtonAriaLabel', { - defaultMessage: 'Expand layers panel', - })} - position="left" - > - {renderExpandButton({ hasErrors, isLoading, onClick: openLayerTOC })} - </EuiToolTip> - ); - } - - let addLayer; - if (!isReadOnly) { - addLayer = ( - <Fragment> - <EuiSpacer size="s" /> - <EuiButton - isDisabled={!isAddButtonActive} - className="mapLayerControl__addLayerButton" - fill - fullWidth - onClick={showAddLayerWizard} - data-test-subj="addLayerButton" - > - <FormattedMessage - id="xpack.maps.layerControl.addLayerButtonLabel" - defaultMessage="Add layer" - /> - </EuiButton> - </Fragment> - ); - } - - return ( - <Fragment> - <EuiPanel - className="mapWidgetControl mapWidgetControl-hasShadow" - paddingSize="none" - grow={false} - > - <EuiFlexItem className="mapWidgetControl__headerFlexItem" grow={false}> - <EuiFlexGroup - justifyContent="spaceBetween" - alignItems="center" - responsive={false} - gutterSize="none" - > - <EuiFlexItem> - <EuiTitle size="xxxs" className="mapWidgetControl__header"> - <h2> - <FormattedMessage - id="xpack.maps.layerControl.layersTitle" - defaultMessage="Layers" - /> - </h2> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiToolTip - delay="long" - content={i18n.translate('xpack.maps.layerControl.closeLayerTOCButtonAriaLabel', { - defaultMessage: 'Collapse layers panel', - })} - > - <EuiButtonIcon - className="mapLayerControl__closeLayerTOCButton" - onClick={closeLayerTOC} - iconType="menuRight" - color="text" - aria-label={i18n.translate( - 'xpack.maps.layerControl.closeLayerTOCButtonAriaLabel', - { - defaultMessage: 'Collapse layers panel', - } - )} - data-test-subj="mapToggleLegendButton" - /> - </EuiToolTip> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - - <EuiFlexItem className="mapLayerControl"> - <LayerTOC /> - </EuiFlexItem> - </EuiPanel> - - {addLayer} - </Fragment> - ); -} diff --git a/x-pack/legacy/plugins/maps/public/index.scss b/x-pack/legacy/plugins/maps/public/index.scss index b2ac514299d80..b2a228f01b921 100644 --- a/x-pack/legacy/plugins/maps/public/index.scss +++ b/x-pack/legacy/plugins/maps/public/index.scss @@ -1,17 +1,3 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - /* GIS plugin styles */ -// Prefix all styles with "map" to avoid conflicts. -// Examples -// mapChart -// mapChart__legend -// mapChart__legend--small -// mapChart__legend-isLoading - -@import './main'; -@import './mapbox_hacks'; -@import './connected_components/index'; -@import './components/index'; -@import '../../../../plugins/maps/public/layers/index'; +@import '../../../../plugins/maps/public/index'; diff --git a/x-pack/legacy/plugins/maps/public/index.ts b/x-pack/legacy/plugins/maps/public/index.ts index 8555594e909d3..98db26859297b 100644 --- a/x-pack/legacy/plugins/maps/public/index.ts +++ b/x-pack/legacy/plugins/maps/public/index.ts @@ -15,15 +15,14 @@ import 'uiExports/embeddableActions'; import 'ui/autoload/all'; import 'react-vis/dist/style.css'; - -import './angular/services/gis_map_saved_object_loader'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import '../../../../plugins/maps/public/angular/services/gis_map_saved_object_loader'; import './angular/map_controller'; import './routes'; // @ts-ignore -import { PluginInitializerContext } from 'kibana/public'; import { MapsPlugin } from './plugin'; -export const plugin = (initializerContext: PluginInitializerContext) => { +export const plugin = () => { return new MapsPlugin(); }; @@ -32,4 +31,4 @@ export { RenderTooltipContentParams, ITooltipProperty, } from '../../../../plugins/maps/public/layers/tooltips/tooltip_property'; -export { MapEmbeddable, MapEmbeddableInput } from './embeddable'; +export { MapEmbeddable, MapEmbeddableInput } from '../../../../plugins/maps/public/embeddable'; diff --git a/x-pack/legacy/plugins/maps/public/legacy.ts b/x-pack/legacy/plugins/maps/public/legacy.ts index 96d9e09c1d09a..bcbfca17755fb 100644 --- a/x-pack/legacy/plugins/maps/public/legacy.ts +++ b/x-pack/legacy/plugins/maps/public/legacy.ts @@ -7,10 +7,9 @@ import { npSetup, npStart } from 'ui/new_platform'; // @ts-ignore Untyped Module import { uiModules } from 'ui/modules'; -import { PluginInitializerContext } from 'kibana/public'; // eslint-disable-line import/order import { plugin } from '.'; -const pluginInstance = plugin({} as PluginInitializerContext); +const pluginInstance = plugin(); const setupPlugins = { __LEGACY: { diff --git a/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts b/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts deleted file mode 100644 index 00f788f267d4b..0000000000000 --- a/x-pack/legacy/plugins/maps/public/legacy_register_feature.ts +++ /dev/null @@ -1,14 +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 { npSetup } from 'ui/new_platform'; -import { featureCatalogueEntry } from './feature_catalogue_entry'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register(featureCatalogueEntry); diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts index 71f1a30c1fbef..0123e32b6d3b9 100644 --- a/x-pack/legacy/plugins/maps/public/plugin.ts +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -10,7 +10,7 @@ import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; // @ts-ignore import { wrapInI18nContext } from 'ui/i18n'; // @ts-ignore -import { MapListing } from './components/map_listing'; +import { MapListing } from '../../../../plugins/maps/public/components/map_listing'; // eslint-disable-line @kbn/eslint/no-restricted-paths // @ts-ignore import { bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, @@ -18,7 +18,6 @@ import { } from '../../../../plugins/maps/public/plugin'; // eslint-disable-line @kbn/eslint/no-restricted-paths import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; -import { featureCatalogueEntry } from './feature_catalogue_entry'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -57,8 +56,6 @@ export class MapsPlugin implements Plugin<MapsPluginSetup, MapsPluginStart> { }); bindNpSetupCoreAndPlugins(core, np); - - np.home.featureCatalogue.register(featureCatalogueEntry); } public start(core: CoreStart, plugins: MapsPluginStartDependencies) { diff --git a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js b/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js deleted file mode 100644 index 9dc07bcb5dc0e..0000000000000 --- a/x-pack/legacy/plugins/maps/public/register_vis_type_alias.js +++ /dev/null @@ -1,49 +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 { i18n } from '@kbn/i18n'; -import { APP_ID, APP_ICON, MAP_BASE_URL } from '../common/constants'; -import { - getInjectedVarFunc, - getVisualizations, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../plugins/maps/public/kibana_services'; -import { npSetup } from 'ui/new_platform'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { bindSetupCoreAndPlugins } from '../../../../plugins/maps/public/plugin'; - -bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); - -const showMapVisualizationTypes = getInjectedVarFunc()('showMapVisualizationTypes', false); - -const description = i18n.translate('xpack.maps.visTypeAlias.description', { - defaultMessage: 'Create and style maps with multiple layers and indices.', -}); - -const legacyMapVisualizationWarning = i18n.translate( - 'xpack.maps.visTypeAlias.legacyMapVizWarning', - { - defaultMessage: `Use the Maps app instead of Coordinate Map and Region Map. -The Maps app offers more functionality and is easier to use.`, - } -); - -getVisualizations().registerAlias({ - aliasUrl: MAP_BASE_URL, - name: APP_ID, - title: i18n.translate('xpack.maps.visTypeAlias.title', { - defaultMessage: 'Maps', - }), - description: showMapVisualizationTypes - ? `${description} ${legacyMapVisualizationWarning}` - : description, - icon: APP_ICON, - stage: 'production', -}); - -if (!showMapVisualizationTypes) { - getVisualizations().hideTypes(['region_map', 'tile_map']); -} diff --git a/x-pack/legacy/plugins/maps/public/routes.js b/x-pack/legacy/plugins/maps/public/routes.js index c082e0e1352c0..70c1c4a50efd4 100644 --- a/x-pack/legacy/plugins/maps/public/routes.js +++ b/x-pack/legacy/plugins/maps/public/routes.js @@ -6,15 +6,18 @@ import { i18n } from '@kbn/i18n'; import routes from 'ui/routes'; -import listingTemplate from './angular/listing_ng_wrapper.html'; -import mapTemplate from './angular/map.html'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import listingTemplate from '../../../../plugins/maps/public/angular/listing_ng_wrapper.html'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import mapTemplate from '../../../../plugins/maps/public/angular/map.html'; import { getSavedObjectsClient, getCoreChrome, getMapsCapabilities, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/maps/public/kibana_services'; -import { getMapsSavedObjectLoader } from './angular/services/gis_map_saved_object_loader'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getMapsSavedObjectLoader } from '../../../../plugins/maps/public/angular/services/gis_map_saved_object_loader'; routes.enable(); diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts deleted file mode 100644 index 8c99e0adcc14f..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts +++ /dev/null @@ -1,18 +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 { AnyAction } from 'redux'; -import { MapCenter } from '../../common/descriptor_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; - -export function getHiddenLayerIds(state: MapStoreState): string[]; - -export function getMapZoom(state: MapStoreState): number; - -export function getMapCenter(state: MapStoreState): MapCenter; - -export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[]; diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js deleted file mode 100644 index 72cc748617540..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.test.js +++ /dev/null @@ -1,56 +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. - */ - -jest.mock('../../../../../plugins/maps/public/layers/vector_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/tiled_vector_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/blended_vector_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/heatmap_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/vector_tile_layer', () => {}); -jest.mock('../../../../../plugins/maps/public/layers/joins/inner_join', () => {}); -jest.mock('../../../../../plugins/maps/public/reducers/non_serializable_instances', () => ({ - getInspectorAdapters: () => { - return {}; - }, -})); -jest.mock('../../../../../plugins/maps/public/kibana_services', () => ({ - getTimeFilter: () => ({ - getTime: () => { - return { - to: 'now', - from: 'now-15m', - }; - }, - }), -})); - -import { getTimeFilters } from './map_selectors'; - -describe('getTimeFilters', () => { - it('should return timeFilters when contained in state', () => { - const state = { - map: { - mapState: { - timeFilters: { - to: '2001-01-01', - from: '2001-12-31', - }, - }, - }, - }; - expect(getTimeFilters(state)).toEqual({ to: '2001-01-01', from: '2001-12-31' }); - }); - - it('should return kibana time filters when not contained in state', () => { - const state = { - map: { - mapState: { - timeFilters: null, - }, - }, - }; - expect(getTimeFilters(state)).toEqual({ to: 'now', from: 'now-15m' }); - }); -}); diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts deleted file mode 100644 index fdf2a8ea0e4f3..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts +++ /dev/null @@ -1,19 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE, INDEXING_STAGE } from '../../../../../plugins/maps/public/reducers/ui'; - -export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; -export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; -export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; -export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; -export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; -export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; -export const getIndexingStage = ({ ui }: MapStoreState): INDEXING_STAGE | null => - ui.importIndexingStage; diff --git a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js index c5522b7ba21c5..e2a758075155a 100644 --- a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js +++ b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.js @@ -5,7 +5,10 @@ */ import _ from 'lodash'; -import { DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../common/constants'; +import { + DEFAULT_MAX_RESULT_WINDOW, + DEFAULT_MAX_INNER_RESULT_WINDOW, +} from '../../../../../plugins/maps/common/constants'; export function getIndexPatternSettings(indicesSettingsResp) { let maxResultWindow = Infinity; diff --git a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js index 01a1ba2703cba..c152f5bfffc31 100644 --- a/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js +++ b/x-pack/legacy/plugins/maps/server/lib/get_index_pattern_settings.test.js @@ -5,7 +5,10 @@ */ import { getIndexPatternSettings } from './get_index_pattern_settings'; -import { DEFAULT_MAX_RESULT_WINDOW, DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../common/constants'; +import { + DEFAULT_MAX_RESULT_WINDOW, + DEFAULT_MAX_INNER_RESULT_WINDOW, +} from '../../../../../plugins/maps/common/constants'; describe('max_result_window and max_inner_result_window are not set', () => { test('Should provide default values when values not set', () => { diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts index 652bb83a0d781..d34e306d1fff9 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/collectors/register.ts @@ -9,7 +9,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SavedObjectsClientContract } from 'src/core/server'; import { getMapsTelemetry } from '../maps_telemetry'; // @ts-ignore -import { TELEMETRY_TYPE } from '../../../common/constants'; +import { TELEMETRY_TYPE } from '../../../../../../plugins/maps/common/constants'; export function registerMapsUsageCollector( usageCollection: UsageCollectionSetup, diff --git a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 27c0211446e85..4610baabad3fe 100644 --- a/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/legacy/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -16,8 +16,8 @@ import { ES_GEO_FIELD_TYPE, MAP_SAVED_OBJECT_TYPE, TELEMETRY_TYPE, -} from '../../common/constants'; -import { LayerDescriptor } from '../../common/descriptor_types'; +} from '../../../../../plugins/maps/common/constants'; +import { LayerDescriptor } from '../../../../../plugins/maps/common/descriptor_types'; import { MapSavedObject } from '../../../../../plugins/maps/common/map_saved_object_type'; interface IStats { diff --git a/x-pack/legacy/plugins/maps/server/plugin.js b/x-pack/legacy/plugins/maps/server/plugin.js index 25c552433e9f8..79f3dcf76b82e 100644 --- a/x-pack/legacy/plugins/maps/server/plugin.js +++ b/x-pack/legacy/plugins/maps/server/plugin.js @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { APP_ID, APP_ICON, createMapPath, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { + APP_ID, + APP_ICON, + createMapPath, + MAP_SAVED_OBJECT_TYPE, +} from '../../../../plugins/maps/common/constants'; import { getEcommerceSavedObjects } from './sample_data/ecommerce_saved_objects'; import { getFlightsSavedObjects } from './sample_data/flights_saved_objects.js'; import { getWebLogsSavedObjects } from './sample_data/web_logs_saved_objects.js'; diff --git a/x-pack/legacy/plugins/maps/server/routes.js b/x-pack/legacy/plugins/maps/server/routes.js index 20e022001577a..d49f9827e3ea0 100644 --- a/x-pack/legacy/plugins/maps/server/routes.js +++ b/x-pack/legacy/plugins/maps/server/routes.js @@ -21,7 +21,7 @@ import { GIS_API_PATH, EMS_SPRITES_PATH, INDEX_SETTINGS_API_PATH, -} from '../common/constants'; +} from '../../../../plugins/maps/common/constants'; import { EMSClient } from '@elastic/ems-client'; import fetch from 'node-fetch'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/.agignore b/x-pack/legacy/plugins/monitoring/.agignore deleted file mode 100644 index 10fb4038cbdc2..0000000000000 --- a/x-pack/legacy/plugins/monitoring/.agignore +++ /dev/null @@ -1,3 +0,0 @@ -agent -node_modules -bower_components diff --git a/x-pack/legacy/plugins/monitoring/README.md b/x-pack/legacy/plugins/monitoring/README.md index 3659f4d2fa0f7..e9ececa8c6350 100644 --- a/x-pack/legacy/plugins/monitoring/README.md +++ b/x-pack/legacy/plugins/monitoring/README.md @@ -72,8 +72,8 @@ cluster. 1. Set the Kibana config: ``` % cat config/kibana.dev.yml - xpack.monitoring.elasticsearch: - url: "http://localhost:9210" + monitoring.ui.elasticsearch: + hosts: "http://localhost:9210" username: "kibana" password: "changeme" ``` diff --git a/x-pack/legacy/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js b/x-pack/legacy/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js deleted file mode 100644 index 470d596bd2bdc..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/__tests__/format_timestamp_to_duration.js +++ /dev/null @@ -1,128 +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 expect from '@kbn/expect'; -import moment from 'moment'; -import { formatTimestampToDuration } from '../format_timestamp_to_duration'; -import { CALCULATE_DURATION_SINCE, CALCULATE_DURATION_UNTIL } from '../constants'; - -const testTime = moment('2010-05-01'); // pick a date where adding/subtracting 2 months formats roundly to '2 months 0 days' -const getTestTime = () => moment(testTime); // clones the obj so it's not mutated with .adds and .subtracts - -/** - * Test the moment-duration-format template - */ -describe('formatTimestampToDuration', () => { - describe('format timestamp to duration - time since', () => { - it('should format timestamp to human-readable duration', () => { - // time inputs are a few "moments" extra from the time advertised by name - const fiftyNineSeconds = getTestTime().subtract(59, 'seconds'); - expect( - formatTimestampToDuration(fiftyNineSeconds, CALCULATE_DURATION_SINCE, getTestTime()) - ).to.be('59 seconds'); - - const fiveMins = getTestTime() - .subtract(5, 'minutes') - .subtract(30, 'seconds'); - expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '6 mins' - ); - - const sixHours = getTestTime() - .subtract(6, 'hours') - .subtract(30, 'minutes'); - expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '6 hrs 30 mins' - ); - - const sevenDays = getTestTime() - .subtract(7, 'days') - .subtract(6, 'hours') - .subtract(18, 'minutes'); - expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '7 days 6 hrs 18 mins' - ); - - const eightWeeks = getTestTime() - .subtract(8, 'weeks') - .subtract(7, 'days') - .subtract(6, 'hours') - .subtract(18, 'minutes'); - expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '2 months 2 days' - ); - - const oneHour = getTestTime().subtract(1, 'hour'); // should trim 0 min - expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '1 hr' - ); - - const oneDay = getTestTime().subtract(1, 'day'); // should trim 0 hrs - expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '1 day' - ); - - const twoMonths = getTestTime().subtract(2, 'month'); // should trim 0 days - expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_SINCE, getTestTime())).to.be( - '2 months' - ); - }); - }); - - describe('format timestamp to duration - time until', () => { - it('should format timestamp to human-readable duration', () => { - // time inputs are a few "moments" extra from the time advertised by name - const fiftyNineSeconds = getTestTime().add(59, 'seconds'); - expect( - formatTimestampToDuration(fiftyNineSeconds, CALCULATE_DURATION_UNTIL, getTestTime()) - ).to.be('59 seconds'); - - const fiveMins = getTestTime().add(10, 'minutes'); - expect(formatTimestampToDuration(fiveMins, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '10 mins' - ); - - const sixHours = getTestTime() - .add(6, 'hours') - .add(30, 'minutes'); - expect(formatTimestampToDuration(sixHours, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '6 hrs 30 mins' - ); - - const sevenDays = getTestTime() - .add(7, 'days') - .add(6, 'hours') - .add(18, 'minutes'); - expect(formatTimestampToDuration(sevenDays, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '7 days 6 hrs 18 mins' - ); - - const eightWeeks = getTestTime() - .add(8, 'weeks') - .add(7, 'days') - .add(6, 'hours') - .add(18, 'minutes'); - expect(formatTimestampToDuration(eightWeeks, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '2 months 2 days' - ); - - const oneHour = getTestTime().add(1, 'hour'); // should trim 0 min - expect(formatTimestampToDuration(oneHour, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '1 hr' - ); - - const oneDay = getTestTime().add(1, 'day'); // should trim 0 hrs - expect(formatTimestampToDuration(oneDay, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '1 day' - ); - - const twoMonths = getTestTime().add(2, 'month'); // should trim 0 days - expect(formatTimestampToDuration(twoMonths, CALCULATE_DURATION_UNTIL, getTestTime())).to.be( - '2 months' - ); - }); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/common/cancel_promise.ts b/x-pack/legacy/plugins/monitoring/common/cancel_promise.ts deleted file mode 100644 index f100edda50796..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/cancel_promise.ts +++ /dev/null @@ -1,70 +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. - */ - -export enum Status { - Canceled, - Failed, - Resolved, - Awaiting, - Idle, -} - -/** - * Simple [PromiseWithCancel] factory - */ -export class PromiseWithCancel { - private _promise: Promise<any>; - private _status: Status = Status.Idle; - - /** - * @param {Promise} promise Promise you want to cancel / track - */ - constructor(promise: Promise<any>) { - this._promise = promise; - } - - /** - * Cancel the promise in any state - */ - public cancel = (): void => { - this._status = Status.Canceled; - }; - - /** - * @returns status based on [Status] - */ - public status = (): Status => { - return this._status; - }; - - /** - * @returns promise passed in [constructor] - * This sets the state to Status.Awaiting - */ - public promise = (): Promise<any> => { - if (this._status === Status.Canceled) { - throw Error('Getting a canceled promise is not allowed'); - } else if (this._status !== Status.Idle) { - return this._promise; - } - return new Promise((resolve, reject) => { - this._status = Status.Awaiting; - return this._promise - .then(response => { - if (this._status !== Status.Canceled) { - this._status = Status.Resolved; - return resolve(response); - } - }) - .catch(error => { - if (this._status !== Status.Canceled) { - this._status = Status.Failed; - return reject(error); - } - }); - }); - }; -} diff --git a/x-pack/legacy/plugins/monitoring/common/constants.ts b/x-pack/legacy/plugins/monitoring/common/constants.ts deleted file mode 100644 index 3a4c7b71dcd03..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/constants.ts +++ /dev/null @@ -1,268 +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. - */ - -/** - * Helper string to add as a tag in every logging call - */ -export const LOGGING_TAG = 'monitoring'; -/** - * Helper string to add as a tag in every logging call related to Kibana monitoring - */ -export const KIBANA_MONITORING_LOGGING_TAG = 'kibana-monitoring'; - -/** - * The Monitoring API version is the expected API format that we export and expect to import. - * @type {string} - */ -export const MONITORING_SYSTEM_API_VERSION = '7'; -/** - * The type name used within the Monitoring index to publish Kibana ops stats. - * @type {string} - */ -export const KIBANA_STATS_TYPE_MONITORING = 'kibana_stats'; // similar to KIBANA_STATS_TYPE but rolled up into 10s stats from 5s intervals through ops_buffer -/** - * The type name used within the Monitoring index to publish Kibana stats. - * @type {string} - */ -export const KIBANA_SETTINGS_TYPE = 'kibana_settings'; -/** - * The type name used within the Monitoring index to publish Kibana usage stats. - * NOTE: this string shows as-is in the stats API as a field name for the kibana usage stats - * @type {string} - */ -export const KIBANA_USAGE_TYPE = 'kibana'; - -/* - * Key for the localStorage service - */ -export const STORAGE_KEY = 'xpack.monitoring.data'; - -/** - * Units for derivative metric values - */ -export const NORMALIZED_DERIVATIVE_UNIT = '1s'; - -/* - * Values for column sorting in table options - * @type {number} 1 or -1 - */ -export const EUI_SORT_ASCENDING = 'asc'; -export const EUI_SORT_DESCENDING = 'desc'; -export const SORT_ASCENDING = 1; -export const SORT_DESCENDING = -1; - -/* - * Chart colors - * @type {string} - */ -export const CHART_LINE_COLOR = '#d2d2d2'; -export const CHART_TEXT_COLOR = '#9c9c9c'; - -/* - * Number of cluster alerts to show on overview page - * @type {number} - */ -export const CLUSTER_ALERTS_SEARCH_SIZE = 3; - -/* - * Format for moment-duration-format timestamp-to-duration template if the time diffs are gte 1 month - * @type {string} - */ -export const FORMAT_DURATION_TEMPLATE_LONG = 'M [months] d [days]'; - -/* - * Format for moment-duration-format timestamp-to-duration template if the time diffs are lt 1 month but gt 1 minute - * @type {string} - */ -export const FORMAT_DURATION_TEMPLATE_SHORT = ' d [days] h [hrs] m [min]'; - -/* - * Format for moment-duration-format timestamp-to-duration template if the time diffs are lt 1 minute - * @type {string} - */ -export const FORMAT_DURATION_TEMPLATE_TINY = ' s [seconds]'; - -/* - * Simple unique values for Timestamp to duration flags. These are used for - * determining if calculation should be formatted as "time until" (now to - * timestamp) or "time since" (timestamp to now) - */ -export const CALCULATE_DURATION_SINCE = 'since'; -export const CALCULATE_DURATION_UNTIL = 'until'; - -/** - * In order to show ML Jobs tab in the Elasticsearch section / tab navigation, license must be supported - */ -export const ML_SUPPORTED_LICENSES = ['trial', 'platinum', 'enterprise']; - -/** - * Metadata service URLs for the different cloud services that have constant URLs (e.g., unlike GCP, which is a constant prefix). - * - * @type {Object} - */ -export const CLOUD_METADATA_SERVICES = { - // We explicitly call out the version, 2016-09-02, rather than 'latest' to avoid unexpected changes - AWS_URL: 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document', - - // 2017-04-02 is the first GA release of this API - AZURE_URL: 'http://169.254.169.254/metadata/instance?api-version=2017-04-02', - - // GCP documentation shows both 'metadata.google.internal' (mostly) and '169.254.169.254' (sometimes) - // To bypass potential DNS changes, the IP was used because it's shared with other cloud services - GCP_URL_PREFIX: 'http://169.254.169.254/computeMetadata/v1/instance', -}; - -/** - * Constants used by Logstash monitoring code - */ -export const LOGSTASH = { - MAJOR_VER_REQD_FOR_PIPELINES: 6, - - /* - * Names ES keys on for different Logstash pipeline queues. - * @type {string} - */ - QUEUE_TYPES: { - MEMORY: 'memory', - PERSISTED: 'persisted', - }, -}; - -export const DEBOUNCE_SLOW_MS = 17; // roughly how long it takes to render a frame at 60fps -export const DEBOUNCE_FAST_MS = 10; // roughly how long it takes to render a frame at 100fps - -/** - * Configuration key for setting the email address used for cluster alert notifications. - */ -export const CLUSTER_ALERTS_ADDRESS_CONFIG_KEY = 'cluster_alerts.email_notifications.email_address'; - -export const STANDALONE_CLUSTER_CLUSTER_UUID = '__standalone_cluster__'; - -export const INDEX_PATTERN = '.monitoring-*-6-*,.monitoring-*-7-*'; -export const INDEX_PATTERN_KIBANA = '.monitoring-kibana-6-*,.monitoring-kibana-7-*'; -export const INDEX_PATTERN_LOGSTASH = '.monitoring-logstash-6-*,.monitoring-logstash-7-*'; -export const INDEX_PATTERN_BEATS = '.monitoring-beats-6-*,.monitoring-beats-7-*'; -export const INDEX_ALERTS = '.monitoring-alerts-6,.monitoring-alerts-7'; -export const INDEX_PATTERN_ELASTICSEARCH = '.monitoring-es-6-*,.monitoring-es-7-*'; - -// This is the unique token that exists in monitoring indices collected by metricbeat -export const METRICBEAT_INDEX_NAME_UNIQUE_TOKEN = '-mb-'; - -// We use this for metricbeat migration to identify specific products that we do not have constants for -export const ELASTICSEARCH_SYSTEM_ID = 'elasticsearch'; - -/** - * The id of the infra source owned by the monitoring plugin. - */ -export const INFRA_SOURCE_ID = 'internal-stack-monitoring'; - -/* - * These constants represent code paths within `getClustersFromRequest` - * that an api call wants to invoke. This is meant as an optimization to - * avoid unnecessary ES queries (looking at you logstash) when the data - * is not used. In the long term, it'd be nice to have separate api calls - * instead of this path logic. - */ -export const CODE_PATH_ALL = 'all'; -export const CODE_PATH_ALERTS = 'alerts'; -export const CODE_PATH_KIBANA = 'kibana'; -export const CODE_PATH_ELASTICSEARCH = 'elasticsearch'; -export const CODE_PATH_ML = 'ml'; -export const CODE_PATH_BEATS = 'beats'; -export const CODE_PATH_LOGSTASH = 'logstash'; -export const CODE_PATH_APM = 'apm'; -export const CODE_PATH_LICENSE = 'license'; -export const CODE_PATH_LOGS = 'logs'; - -/** - * The header sent by telemetry service when hitting Elasticsearch to identify query source - * @type {string} - */ -export const TELEMETRY_QUERY_SOURCE = 'TELEMETRY'; - -/** - * The name of the Kibana System ID used to publish and look up Kibana stats through the Monitoring system. - * @type {string} - */ -export const KIBANA_SYSTEM_ID = 'kibana'; - -/** - * The name of the Beats System ID used to publish and look up Beats stats through the Monitoring system. - * @type {string} - */ -export const BEATS_SYSTEM_ID = 'beats'; - -/** - * The name of the Apm System ID used to publish and look up Apm stats through the Monitoring system. - * @type {string} - */ -export const APM_SYSTEM_ID = 'apm'; - -/** - * The name of the Kibana System ID used to look up Logstash stats through the Monitoring system. - * @type {string} - */ -export const LOGSTASH_SYSTEM_ID = 'logstash'; - -/** - * The name of the Kibana System ID used to look up Reporting stats through the Monitoring system. - * @type {string} - */ -export const REPORTING_SYSTEM_ID = 'reporting'; - -/** - * The amount of time, in milliseconds, to wait between collecting kibana stats from es. - * - * Currently 24 hours kept in sync with reporting interval. - * @type {Number} - */ -export const TELEMETRY_COLLECTION_INTERVAL = 86400000; - -/** - * We want to slowly rollout the migration from watcher-based cluster alerts to - * kibana alerts and we only want to enable the kibana alerts once all - * watcher-based cluster alerts have been migrated so this flag will serve - * as the only way to see the new UI and actually run Kibana alerts. It will - * be false until all alerts have been migrated, then it will be removed - */ -export const KIBANA_ALERTING_ENABLED = false; - -/** - * The prefix for all alert types used by monitoring - */ -export const ALERT_TYPE_PREFIX = 'monitoring_'; - -/** - * This is the alert type id for the license expiration alert - */ -export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; -/** - * This is the alert type id for the cluster state alert - */ -export const ALERT_TYPE_CLUSTER_STATE = `${ALERT_TYPE_PREFIX}alert_type_cluster_state`; - -/** - * A listing of all alert types - */ -export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_STATE]; - -/** - * Matches the id for the built-in in email action type - * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts - */ -export const ALERT_ACTION_TYPE_EMAIL = '.email'; - -/** - * The number of alerts that have been migrated - */ -export const NUMBER_OF_MIGRATED_ALERTS = 2; - -/** - * The advanced settings config name for the email address - */ -export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; - -export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/legacy/plugins/monitoring/common/format_timestamp_to_duration.js b/x-pack/legacy/plugins/monitoring/common/format_timestamp_to_duration.js deleted file mode 100644 index 46c8f7db49b0f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/format_timestamp_to_duration.js +++ /dev/null @@ -1,54 +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 moment from 'moment'; -import 'moment-duration-format'; -import { - FORMAT_DURATION_TEMPLATE_TINY, - FORMAT_DURATION_TEMPLATE_SHORT, - FORMAT_DURATION_TEMPLATE_LONG, - CALCULATE_DURATION_SINCE, - CALCULATE_DURATION_UNTIL, -} from './constants'; - -/* - * Formats a timestamp string - * @param timestamp: ISO time string - * @param calculationFlag: control "since" or "until" logic - * @param initialTime {Object} moment object (not required) - * @return string - */ -export function formatTimestampToDuration(timestamp, calculationFlag, initialTime) { - initialTime = initialTime || moment(); - let timeDuration; - if (calculationFlag === CALCULATE_DURATION_SINCE) { - timeDuration = moment.duration(initialTime - moment(timestamp)); // since: now - timestamp - } else if (calculationFlag === CALCULATE_DURATION_UNTIL) { - timeDuration = moment.duration(moment(timestamp) - initialTime); // until: timestamp - now - } else { - throw new Error( - '[formatTimestampToDuration] requires a [calculationFlag] parameter to specify format as "since" or "until" the given time.' - ); - } - - // See https://github.com/elastic/x-pack-kibana/issues/3554 - let duration; - if (Math.abs(initialTime.diff(timestamp, 'months')) >= 1) { - // time diff is greater than 1 month, show months / days - duration = moment.duration(timeDuration).format(FORMAT_DURATION_TEMPLATE_LONG); - } else if (Math.abs(initialTime.diff(timestamp, 'minutes')) >= 1) { - // time diff is less than 1 month but greater than a minute, show days / hours / minutes - duration = moment.duration(timeDuration).format(FORMAT_DURATION_TEMPLATE_SHORT); - } else { - // time diff is less than a minute, show seconds - duration = moment.duration(timeDuration).format(FORMAT_DURATION_TEMPLATE_TINY); - } - - return duration - .replace(/ 0 mins$/, '') - .replace(/ 0 hrs$/, '') - .replace(/ 0 days$/, ''); // See https://github.com/jsmreese/moment-duration-format/issues/64 -} diff --git a/x-pack/legacy/plugins/monitoring/common/formatting.js b/x-pack/legacy/plugins/monitoring/common/formatting.js deleted file mode 100644 index ed5d68f942dfd..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/formatting.js +++ /dev/null @@ -1,35 +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 moment from 'moment-timezone'; - -export const LARGE_FLOAT = '0,0.[00]'; -export const SMALL_FLOAT = '0.[00]'; -export const LARGE_BYTES = '0,0.0 b'; -export const SMALL_BYTES = '0.0 b'; -export const LARGE_ABBREVIATED = '0,0.[0]a'; - -/** - * Format the {@code date} in the user's expected date/time format using their <em>dateFormat:tz</em> defined time zone. - * @param date Either a numeric Unix timestamp or a {@code Date} object - * @returns The date formatted using 'LL LTS' - */ -export function formatDateTimeLocal(date, timezone) { - if (timezone === 'Browser') { - timezone = moment.tz.guess() || 'utc'; - } - - return moment.tz(date, timezone).format('LL LTS'); -} - -/** - * Shorten a Logstash Pipeline's hash for display purposes - * @param {string} hash The complete hash - * @return {string} The shortened hash - */ -export function shortenPipelineHash(hash) { - return hash.substr(0, 6); -} diff --git a/x-pack/legacy/plugins/monitoring/common/index.js b/x-pack/legacy/plugins/monitoring/common/index.js deleted file mode 100644 index 183396f8f0d72..0000000000000 --- a/x-pack/legacy/plugins/monitoring/common/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { formatTimestampToDuration } from './format_timestamp_to_duration'; diff --git a/x-pack/legacy/plugins/monitoring/config.js b/x-pack/legacy/plugins/monitoring/config.js deleted file mode 100644 index fd4e6512c5063..0000000000000 --- a/x-pack/legacy/plugins/monitoring/config.js +++ /dev/null @@ -1,147 +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. - */ - -/** - * User-configurable settings for xpack.monitoring via configuration schema - * @param {Object} Joi - HapiJS Joi module that allows for schema validation - * @return {Object} config schema - */ -export const config = Joi => { - const DEFAULT_REQUEST_HEADERS = ['authorization']; - - return Joi.object({ - enabled: Joi.boolean().default(true), - ui: Joi.object({ - enabled: Joi.boolean().default(true), - logs: Joi.object({ - index: Joi.string().default('filebeat-*'), - }).default(), - ccs: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - container: Joi.object({ - elasticsearch: Joi.object({ - enabled: Joi.boolean().default(false), - }).default(), - logstash: Joi.object({ - enabled: Joi.boolean().default(false), - }).default(), - }).default(), - max_bucket_size: Joi.number().default(10000), - min_interval_seconds: Joi.number().default(10), - show_license_expiration: Joi.boolean().default(true), - elasticsearch: Joi.object({ - customHeaders: Joi.object().default({}), - logQueries: Joi.boolean().default(false), - requestHeadersWhitelist: Joi.array() - .items() - .single() - .default(DEFAULT_REQUEST_HEADERS), - sniffOnStart: Joi.boolean().default(false), - sniffInterval: Joi.number() - .allow(false) - .default(false), - sniffOnConnectionFault: Joi.boolean().default(false), - hosts: Joi.array() - .items(Joi.string().uri({ scheme: ['http', 'https'] })) - .single(), // if empty, use Kibana's connection config - username: Joi.string(), - password: Joi.string(), - requestTimeout: Joi.number().default(30000), - pingTimeout: Joi.number().default(30000), - ssl: Joi.object({ - verificationMode: Joi.string() - .valid('none', 'certificate', 'full') - .default('full'), - certificateAuthorities: Joi.array() - .single() - .items(Joi.string()), - certificate: Joi.string(), - key: Joi.string(), - keyPassphrase: Joi.string(), - keystore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - truststore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - alwaysPresentCertificate: Joi.boolean().default(false), - }).default(), - apiVersion: Joi.string().default('master'), - logFetchCount: Joi.number().default(10), - }).default(), - }).default(), - kibana: Joi.object({ - collection: Joi.object({ - enabled: Joi.boolean().default(true), - interval: Joi.number().default(10000), // op status metrics get buffered at `ops.interval` and flushed to the bulk endpoint at this interval - }).default(), - }).default(), - elasticsearch: Joi.object({ - customHeaders: Joi.object().default({}), - logQueries: Joi.boolean().default(false), - requestHeadersWhitelist: Joi.array() - .items() - .single() - .default(DEFAULT_REQUEST_HEADERS), - sniffOnStart: Joi.boolean().default(false), - sniffInterval: Joi.number() - .allow(false) - .default(false), - sniffOnConnectionFault: Joi.boolean().default(false), - hosts: Joi.array() - .items(Joi.string().uri({ scheme: ['http', 'https'] })) - .single(), // if empty, use Kibana's connection config - username: Joi.string(), - password: Joi.string(), - requestTimeout: Joi.number().default(30000), - pingTimeout: Joi.number().default(30000), - ssl: Joi.object({ - verificationMode: Joi.string() - .valid('none', 'certificate', 'full') - .default('full'), - certificateAuthorities: Joi.array() - .single() - .items(Joi.string()), - certificate: Joi.string(), - key: Joi.string(), - keyPassphrase: Joi.string(), - keystore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - truststore: Joi.object({ - path: Joi.string(), - password: Joi.string(), - }).default(), - alwaysPresentCertificate: Joi.boolean().default(false), - }).default(), - apiVersion: Joi.string().default('master'), - }).default(), - cluster_alerts: Joi.object({ - enabled: Joi.boolean().default(true), - email_notifications: Joi.object({ - enabled: Joi.boolean().default(true), - email_address: Joi.string().email(), - }).default(), - }).default(), - licensing: Joi.object({ - api_polling_frequency: Joi.number().default(30001), - }), - agent: Joi.object({ - interval: Joi.string() - .regex(/[\d\.]+[yMwdhms]/) - .default('10s'), - }).default(), - tests: Joi.object({ - cloud_detector: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - }).default(), - }).default(); -}; diff --git a/x-pack/legacy/plugins/monitoring/config.ts b/x-pack/legacy/plugins/monitoring/config.ts new file mode 100644 index 0000000000000..0c664fbe1c00c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/config.ts @@ -0,0 +1,147 @@ +/* + * 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. + */ + +/** + * User-configurable settings for xpack.monitoring via configuration schema + * @param {Object} Joi - HapiJS Joi module that allows for schema validation + * @return {Object} config schema + */ +export const config = (Joi: any) => { + const DEFAULT_REQUEST_HEADERS = ['authorization']; + + return Joi.object({ + enabled: Joi.boolean().default(true), + ui: Joi.object({ + enabled: Joi.boolean().default(true), + logs: Joi.object({ + index: Joi.string().default('filebeat-*'), + }).default(), + ccs: Joi.object({ + enabled: Joi.boolean().default(true), + }).default(), + container: Joi.object({ + elasticsearch: Joi.object({ + enabled: Joi.boolean().default(false), + }).default(), + logstash: Joi.object({ + enabled: Joi.boolean().default(false), + }).default(), + }).default(), + max_bucket_size: Joi.number().default(10000), + min_interval_seconds: Joi.number().default(10), + show_license_expiration: Joi.boolean().default(true), + elasticsearch: Joi.object({ + customHeaders: Joi.object().default({}), + logQueries: Joi.boolean().default(false), + requestHeadersWhitelist: Joi.array() + .items() + .single() + .default(DEFAULT_REQUEST_HEADERS), + sniffOnStart: Joi.boolean().default(false), + sniffInterval: Joi.number() + .allow(false) + .default(false), + sniffOnConnectionFault: Joi.boolean().default(false), + hosts: Joi.array() + .items(Joi.string().uri({ scheme: ['http', 'https'] })) + .single(), // if empty, use Kibana's connection config + username: Joi.string(), + password: Joi.string(), + requestTimeout: Joi.number().default(30000), + pingTimeout: Joi.number().default(30000), + ssl: Joi.object({ + verificationMode: Joi.string() + .valid('none', 'certificate', 'full') + .default('full'), + certificateAuthorities: Joi.array() + .single() + .items(Joi.string()), + certificate: Joi.string(), + key: Joi.string(), + keyPassphrase: Joi.string(), + keystore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), + truststore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), + alwaysPresentCertificate: Joi.boolean().default(false), + }).default(), + apiVersion: Joi.string().default('master'), + logFetchCount: Joi.number().default(10), + }).default(), + }).default(), + kibana: Joi.object({ + collection: Joi.object({ + enabled: Joi.boolean().default(true), + interval: Joi.number().default(10000), // op status metrics get buffered at `ops.interval` and flushed to the bulk endpoint at this interval + }).default(), + }).default(), + elasticsearch: Joi.object({ + customHeaders: Joi.object().default({}), + logQueries: Joi.boolean().default(false), + requestHeadersWhitelist: Joi.array() + .items() + .single() + .default(DEFAULT_REQUEST_HEADERS), + sniffOnStart: Joi.boolean().default(false), + sniffInterval: Joi.number() + .allow(false) + .default(false), + sniffOnConnectionFault: Joi.boolean().default(false), + hosts: Joi.array() + .items(Joi.string().uri({ scheme: ['http', 'https'] })) + .single(), // if empty, use Kibana's connection config + username: Joi.string(), + password: Joi.string(), + requestTimeout: Joi.number().default(30000), + pingTimeout: Joi.number().default(30000), + ssl: Joi.object({ + verificationMode: Joi.string() + .valid('none', 'certificate', 'full') + .default('full'), + certificateAuthorities: Joi.array() + .single() + .items(Joi.string()), + certificate: Joi.string(), + key: Joi.string(), + keyPassphrase: Joi.string(), + keystore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), + truststore: Joi.object({ + path: Joi.string(), + password: Joi.string(), + }).default(), + alwaysPresentCertificate: Joi.boolean().default(false), + }).default(), + apiVersion: Joi.string().default('master'), + }).default(), + cluster_alerts: Joi.object({ + enabled: Joi.boolean().default(true), + email_notifications: Joi.object({ + enabled: Joi.boolean().default(true), + email_address: Joi.string().email(), + }).default(), + }).default(), + licensing: Joi.object({ + api_polling_frequency: Joi.number().default(30001), + }), + agent: Joi.object({ + interval: Joi.string() + .regex(/[\d\.]+[yMwdhms]/) + .default('10s'), + }).default(), + tests: Joi.object({ + cloud_detector: Joi.object({ + enabled: Joi.boolean().default(true), + }).default(), + }).default(), + }).default(); +}; diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js deleted file mode 100644 index ccb45dc1f446f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/index.js +++ /dev/null @@ -1,57 +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 { get } from 'lodash'; -import { resolve } from 'path'; -import { config } from './config'; -import { getUiExports } from './ui_exports'; -import { KIBANA_ALERTING_ENABLED } from './common/constants'; - -/** - * Invokes plugin modules to instantiate the Monitoring plugin for Kibana - * @param kibana {Object} Kibana plugin instance - * @return {Object} Monitoring UI Kibana plugin object - */ -const deps = ['kibana', 'elasticsearch', 'xpack_main']; -if (KIBANA_ALERTING_ENABLED) { - deps.push(...['alerting', 'actions']); -} -export const monitoring = kibana => { - return new kibana.Plugin({ - require: deps, - id: 'monitoring', - configPrefix: 'monitoring', - publicDir: resolve(__dirname, 'public'), - init(server) { - const serverConfig = server.config(); - const npMonitoring = server.newPlatform.setup.plugins.monitoring; - if (npMonitoring) { - const kbnServerStatus = this.kbnServer.status; - npMonitoring.registerLegacyAPI({ - getServerStatus: () => { - const status = kbnServerStatus.toJSON(); - return get(status, 'overall.state'); - }, - }); - } - - server.injectUiAppVars('monitoring', () => { - return { - maxBucketSize: serverConfig.get('monitoring.ui.max_bucket_size'), - minIntervalSeconds: serverConfig.get('monitoring.ui.min_interval_seconds'), - kbnIndex: serverConfig.get('kibana.index'), - showLicenseExpiration: serverConfig.get('monitoring.ui.show_license_expiration'), - showCgroupMetricsElasticsearch: serverConfig.get( - 'monitoring.ui.container.elasticsearch.enabled' - ), - showCgroupMetricsLogstash: serverConfig.get('monitoring.ui.container.logstash.enabled'), // Note, not currently used, but see https://github.com/elastic/x-pack-kibana/issues/1559 part 2 - }; - }); - }, - config, - uiExports: getUiExports(), - }); -}; diff --git a/x-pack/legacy/plugins/monitoring/index.ts b/x-pack/legacy/plugins/monitoring/index.ts new file mode 100644 index 0000000000000..1a0fecb9ef5b5 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/index.ts @@ -0,0 +1,41 @@ +/* + * 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 Hapi from 'hapi'; +import { config } from './config'; +import { KIBANA_ALERTING_ENABLED } from '../../../plugins/monitoring/common/constants'; + +/** + * Invokes plugin modules to instantiate the Monitoring plugin for Kibana + * @param kibana {Object} Kibana plugin instance + * @return {Object} Monitoring UI Kibana plugin object + */ +const deps = ['kibana', 'elasticsearch', 'xpack_main']; +if (KIBANA_ALERTING_ENABLED) { + deps.push(...['alerting', 'actions']); +} +export const monitoring = (kibana: any) => { + return new kibana.Plugin({ + require: deps, + id: 'monitoring', + configPrefix: 'monitoring', + init(server: Hapi.Server) { + const npMonitoring = server.newPlatform.setup.plugins.monitoring as object & { + registerLegacyAPI: (api: unknown) => void; + }; + if (npMonitoring) { + const kbnServerStatus = this.kbnServer.status; + npMonitoring.registerLegacyAPI({ + getServerStatus: () => { + const status = kbnServerStatus.toJSON(); + return status?.overall?.state; + }, + }); + } + }, + config, + }); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js deleted file mode 100644 index f3a888bf9e905..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/instances.js +++ /dev/null @@ -1,195 +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 React, { Fragment } from 'react'; -import moment from 'moment'; -import { uniq, get } from 'lodash'; -import { EuiMonitoringTable } from '../../table'; -import { - EuiLink, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiSpacer, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { Status } from './status'; -import { formatMetric } from '../../../lib/format_number'; -import { formatTimestampToDuration } from '../../../../common'; -import { i18n } from '@kbn/i18n'; -import { APM_SYSTEM_ID } from '../../../../common/constants'; -import { ListingCallOut } from '../../setup_mode/listing_callout'; -import { SetupModeBadge } from '../../setup_mode/badge'; -import { FormattedMessage } from '@kbn/i18n/react'; - -function getColumns(setupMode) { - return [ - { - name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', { - defaultMessage: 'Name', - }), - field: 'name', - render: (name, apm) => { - let setupModeStatus = null; - if (setupMode && setupMode.enabled) { - const list = get(setupMode, 'data.byUuid', {}); - const status = list[apm.uuid] || {}; - const instance = { - uuid: apm.uuid, - name: apm.name, - }; - - setupModeStatus = ( - <div className="monTableCell__setupModeStatus"> - <SetupModeBadge - setupMode={setupMode} - status={status} - instance={instance} - productName={APM_SYSTEM_ID} - /> - </div> - ); - } - - return ( - <Fragment> - <EuiLink href={`#/apm/instances/${apm.uuid}`} data-test-subj={`apmLink-${name}`}> - {name} - </EuiLink> - {setupModeStatus} - </Fragment> - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.outputEnabledTitle', { - defaultMessage: 'Output Enabled', - }), - field: 'output', - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.totalEventsRateTitle', { - defaultMessage: 'Total Events Rate', - }), - field: 'total_events_rate', - render: value => formatMetric(value, '', '/s'), - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.bytesSentRateTitle', { - defaultMessage: 'Bytes Sent Rate', - }), - field: 'bytes_sent_rate', - render: value => formatMetric(value, 'byte', '/s'), - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.outputErrorsTitle', { - defaultMessage: 'Output Errors', - }), - field: 'errors', - render: value => formatMetric(value, '0'), - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.lastEventTitle', { - defaultMessage: 'Last Event', - }), - field: 'time_of_last_event', - render: value => - i18n.translate('xpack.monitoring.apm.instances.lastEventValue', { - defaultMessage: '{timeOfLastEvent} ago', - values: { - timeOfLastEvent: formatTimestampToDuration(+moment(value), 'since'), - }, - }), - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.allocatedMemoryTitle', { - defaultMessage: 'Allocated Memory', - }), - field: 'memory', - render: value => formatMetric(value, 'byte'), - }, - { - name: i18n.translate('xpack.monitoring.apm.instances.versionTitle', { - defaultMessage: 'Version', - }), - field: 'version', - }, - ]; -} - -export function ApmServerInstances({ apms, setupMode }) { - const { pagination, sorting, onTableChange, data } = apms; - - let setupModeCallout = null; - if (setupMode.enabled && setupMode.data) { - setupModeCallout = ( - <ListingCallOut - setupModeData={setupMode.data} - useNodeIdentifier={false} - productName={APM_SYSTEM_ID} - /> - ); - } - - const versions = uniq(data.apms.map(item => item.version)).map(version => { - return { value: version }; - }); - - return ( - <EuiPage> - <EuiPageBody> - <EuiScreenReaderOnly> - <h1> - <FormattedMessage - id="xpack.monitoring.apm.instances.heading" - defaultMessage="APM Instances" - /> - </h1> - </EuiScreenReaderOnly> - <EuiPageContent> - <Status stats={data.stats} /> - <EuiSpacer size="m" /> - {setupModeCallout} - <EuiMonitoringTable - className="apmInstancesTable" - rows={data.apms} - columns={getColumns(setupMode)} - sorting={sorting} - pagination={pagination} - setupMode={setupMode} - productName={APM_SYSTEM_ID} - search={{ - box: { - incremental: true, - placeholder: i18n.translate( - 'xpack.monitoring.apm.instances.filterInstancesPlaceholder', - { - defaultMessage: 'Filter Instances…', - } - ), - }, - filters: [ - { - type: 'field_value_selection', - field: 'version', - name: i18n.translate('xpack.monitoring.apm.instances.versionFilter', { - defaultMessage: 'Version', - }), - options: versions, - multiSelect: 'or', - }, - ], - }} - onTableChange={onTableChange} - executeQueryOptions={{ - defaultFields: ['name'], - }} - /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - ); -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/status_icon.js b/x-pack/legacy/plugins/monitoring/public/components/apm/status_icon.js deleted file mode 100644 index 2de77b70df646..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/apm/status_icon.js +++ /dev/null @@ -1,32 +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 React from 'react'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; -import { i18n } from '@kbn/i18n'; - -export function ApmStatusIcon({ status, availability = true }) { - const type = (() => { - if (!availability) { - return StatusIcon.TYPES.GRAY; - } - - const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; - })(); - - return ( - <StatusIcon - type={type} - label={i18n.translate('xpack.monitoring.apm.healthStatusLabel', { - defaultMessage: 'Health: {status}', - values: { - status, - }, - })} - /> - ); -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js deleted file mode 100644 index dfc9117ef48bc..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/listing.js +++ /dev/null @@ -1,208 +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 React, { PureComponent } from 'react'; -import { uniq, get } from 'lodash'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiSpacer, - EuiLink, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { Stats } from 'plugins/monitoring/components/beats'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; -import { i18n } from '@kbn/i18n'; -import { BEATS_SYSTEM_ID } from '../../../../common/constants'; -import { ListingCallOut } from '../../setup_mode/listing_callout'; -import { SetupModeBadge } from '../../setup_mode/badge'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export class Listing extends PureComponent { - getColumns() { - const { kbnUrl, scope } = this.props.angular; - const setupMode = this.props.setupMode; - - return [ - { - name: i18n.translate('xpack.monitoring.beats.instances.nameTitle', { - defaultMessage: 'Name', - }), - field: 'name', - render: (name, beat) => { - let setupModeStatus = null; - if (setupMode && setupMode.enabled) { - const list = get(setupMode, 'data.byUuid', {}); - const status = list[beat.uuid] || {}; - const instance = { - uuid: beat.uuid, - name: beat.name, - }; - - setupModeStatus = ( - <div className="monTableCell__setupModeStatus"> - <SetupModeBadge - setupMode={setupMode} - status={status} - instance={instance} - productName={BEATS_SYSTEM_ID} - /> - </div> - ); - } - - return ( - <div> - <EuiLink - onClick={() => { - scope.$evalAsync(() => { - kbnUrl.changePath(`/beats/beat/${beat.uuid}`); - }); - }} - data-test-subj={`beatLink-${name}`} - > - {name} - </EuiLink> - {setupModeStatus} - </div> - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.beats.instances.typeTitle', { - defaultMessage: 'Type', - }), - field: 'type', - }, - { - name: i18n.translate('xpack.monitoring.beats.instances.outputEnabledTitle', { - defaultMessage: 'Output Enabled', - }), - field: 'output', - }, - { - name: i18n.translate('xpack.monitoring.beats.instances.totalEventsRateTitle', { - defaultMessage: 'Total Events Rate', - }), - field: 'total_events_rate', - render: value => formatMetric(value, '', '/s'), - }, - { - name: i18n.translate('xpack.monitoring.beats.instances.bytesSentRateTitle', { - defaultMessage: 'Bytes Sent Rate', - }), - field: 'bytes_sent_rate', - render: value => formatMetric(value, 'byte', '/s'), - }, - { - name: i18n.translate('xpack.monitoring.beats.instances.outputErrorsTitle', { - defaultMessage: 'Output Errors', - }), - field: 'errors', - render: value => formatMetric(value, '0'), - }, - { - name: i18n.translate('xpack.monitoring.beats.instances.allocatedMemoryTitle', { - defaultMessage: 'Allocated Memory', - }), - field: 'memory', - render: value => formatMetric(value, 'byte'), - }, - { - name: i18n.translate('xpack.monitoring.beats.instances.versionTitle', { - defaultMessage: 'Version', - }), - field: 'version', - }, - ]; - } - - render() { - const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props; - - let setupModeCallOut = null; - if (setupMode.enabled && setupMode.data) { - setupModeCallOut = ( - <ListingCallOut - setupModeData={setupMode.data} - useNodeIdentifier={false} - productName={BEATS_SYSTEM_ID} - /> - ); - } - - const types = uniq(data.map(item => item.type)).map(type => { - return { value: type }; - }); - - const versions = uniq(data.map(item => item.version)).map(version => { - return { value: version }; - }); - - return ( - <EuiPage> - <EuiPageBody> - <EuiScreenReaderOnly> - <h1> - <FormattedMessage - id="xpack.monitoring.beats.listing.heading" - defaultMessage="Beats listing" - /> - </h1> - </EuiScreenReaderOnly> - <EuiPageContent> - <Stats stats={stats} /> - <EuiSpacer size="m" /> - {setupModeCallOut} - <EuiMonitoringTable - className="beatsTable" - rows={data} - setupMode={setupMode} - productName={BEATS_SYSTEM_ID} - columns={this.getColumns()} - sorting={sorting} - pagination={pagination} - search={{ - box: { - incremental: true, - placeholder: i18n.translate('xpack.monitoring.beats.filterBeatsPlaceholder', { - defaultMessage: 'Filter Beats...', - }), - }, - filters: [ - { - type: 'field_value_selection', - field: 'type', - name: i18n.translate('xpack.monitoring.beats.instances.typeFilter', { - defaultMessage: 'Type', - }), - options: types, - multiSelect: 'or', - }, - { - type: 'field_value_selection', - field: 'version', - name: i18n.translate('xpack.monitoring.beats.instances.versionFilter', { - defaultMessage: 'Version', - }), - options: versions, - multiSelect: 'or', - }, - ], - }} - onTableChange={onTableChange} - executeQueryOptions={{ - defaultFields: ['name', 'type'], - }} - /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - ); - } -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.test.js b/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.test.js deleted file mode 100644 index d8a6f1ad6bd9e..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.test.js +++ /dev/null @@ -1,76 +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 React from 'react'; -import expect from '@kbn/expect'; -import { shallow } from 'enzyme'; -import { ChartTarget } from './chart_target'; - -const props = { - seriesToShow: ['Max Heap', 'Max Heap Used'], - series: [ - { - color: '#3ebeb0', - label: 'Max Heap', - id: 'Max Heap', - data: [ - [1562958960000, 1037959168], - [1562958990000, 1037959168], - [1562959020000, 1037959168], - ], - }, - { - color: '#3b73ac', - label: 'Max Heap Used', - id: 'Max Heap Used', - data: [ - [1562958960000, 639905768], - [1562958990000, 622312416], - [1562959020000, 555967504], - ], - }, - ], - timeRange: { - min: 1562958939851, - max: 1562962539851, - }, - hasLegend: true, - onBrush: () => void 0, - tickFormatter: () => void 0, - updateLegend: () => void 0, -}; - -jest.mock('../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - -// TODO: Skipping for now, seems flaky in New Platform (needs more investigation) -describe.skip('Test legends to toggle series: ', () => { - const ids = props.series.map(item => item.id); - - describe('props.series: ', () => { - it('should toggle based on seriesToShow array', () => { - const component = shallow(<ChartTarget {...props} />); - - const componentClass = component.instance(); - - const seriesA = componentClass.filterData(props.series, [ids[0]]); - expect(seriesA.length).to.be(1); - expect(seriesA[0].id).to.be(ids[0]); - - const seriesB = componentClass.filterData(props.series, [ids[1]]); - expect(seriesB.length).to.be(1); - expect(seriesB[0].id).to.be(ids[1]); - - const seriesAB = componentClass.filterData(props.series, ids); - expect(seriesAB.length).to.be(2); - expect(seriesAB[0].id).to.be(ids[0]); - expect(seriesAB[1].id).to.be(ids[1]); - }); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/listing.js deleted file mode 100644 index 4cf74b3595730..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/listing.js +++ /dev/null @@ -1,455 +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 React, { Fragment, Component } from 'react'; -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; -import moment from 'moment'; -import numeral from '@elastic/numeral'; -import { capitalize, partial } from 'lodash'; -import { - EuiHealth, - EuiLink, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiToolTip, - EuiCallOut, - EuiSpacer, - EuiIcon, -} from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; -import { AlertsIndicator } from 'plugins/monitoring/components/cluster/listing/alerts_indicator'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; - -const IsClusterSupported = ({ isSupported, children }) => { - return isSupported ? children : '-'; -}; - -const STANDALONE_CLUSTER_STORAGE_KEY = 'viewedStandaloneCluster'; - -/* - * This checks if alerts feature is supported via monitoring cluster - * license. If the alerts feature is not supported because the prod cluster - * license is basic, IsClusterSupported makes the status col hidden - * completely - */ -const IsAlertsSupported = props => { - const { alertsMeta = { enabled: true }, clusterMeta = { enabled: true } } = props.cluster.alerts; - if (alertsMeta.enabled && clusterMeta.enabled) { - return <span>{props.children}</span>; - } - - const message = - alertsMeta.message || - clusterMeta.message || - i18n.translate('xpack.monitoring.cluster.listing.unknownHealthMessage', { - defaultMessage: 'Unknown', - }); - - return ( - <EuiToolTip content={message} position="bottom"> - <EuiHealth color="subdued" data-test-subj="alertIcon"> - N/A - </EuiHealth> - </EuiToolTip> - ); -}; - -const getColumns = ( - showLicenseExpiration, - changeCluster, - handleClickIncompatibleLicense, - handleClickInvalidLicense -) => { - return [ - { - name: i18n.translate('xpack.monitoring.cluster.listing.nameColumnTitle', { - defaultMessage: 'Name', - }), - field: 'cluster_name', - sortable: true, - render: (value, cluster) => { - if (cluster.isSupported) { - return ( - <EuiLink - onClick={() => changeCluster(cluster.cluster_uuid, cluster.ccs)} - data-test-subj="clusterLink" - > - {value} - </EuiLink> - ); - } - - // not supported because license is basic/not compatible with multi-cluster - if (cluster.license) { - return ( - <EuiLink - onClick={() => handleClickIncompatibleLicense(cluster.cluster_name)} - data-test-subj="clusterLink" - > - {value} - </EuiLink> - ); - } - - // not supported because license is invalid - return ( - <EuiLink - onClick={() => handleClickInvalidLicense(cluster.cluster_name)} - data-test-subj="clusterLink" - > - {value} - </EuiLink> - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.cluster.listing.statusColumnTitle', { - defaultMessage: 'Status', - }), - field: 'status', - 'data-test-subj': 'alertsStatus', - sortable: true, - render: (_status, cluster) => ( - <IsClusterSupported {...cluster}> - <IsAlertsSupported cluster={cluster}> - <AlertsIndicator alerts={cluster.alerts} /> - </IsAlertsSupported> - </IsClusterSupported> - ), - }, - { - name: i18n.translate('xpack.monitoring.cluster.listing.nodesColumnTitle', { - defaultMessage: 'Nodes', - }), - field: 'elasticsearch.cluster_stats.nodes.count.total', - 'data-test-subj': 'nodesCount', - sortable: true, - render: (total, cluster) => ( - <IsClusterSupported {...cluster}>{numeral(total).format('0,0')}</IsClusterSupported> - ), - }, - { - name: i18n.translate('xpack.monitoring.cluster.listing.indicesColumnTitle', { - defaultMessage: 'Indices', - }), - field: 'elasticsearch.cluster_stats.indices.count', - 'data-test-subj': 'indicesCount', - sortable: true, - render: (count, cluster) => ( - <IsClusterSupported {...cluster}>{numeral(count).format('0,0')}</IsClusterSupported> - ), - }, - { - name: i18n.translate('xpack.monitoring.cluster.listing.dataColumnTitle', { - defaultMessage: 'Data', - }), - field: 'elasticsearch.cluster_stats.indices.store.size_in_bytes', - 'data-test-subj': 'dataSize', - sortable: true, - render: (size, cluster) => ( - <IsClusterSupported {...cluster}>{numeral(size).format('0,0[.]0 b')}</IsClusterSupported> - ), - }, - { - name: i18n.translate('xpack.monitoring.cluster.listing.logstashColumnTitle', { - defaultMessage: 'Logstash', - }), - field: 'logstash.node_count', - 'data-test-subj': 'logstashCount', - sortable: true, - render: (count, cluster) => ( - <IsClusterSupported {...cluster}>{numeral(count).format('0,0')}</IsClusterSupported> - ), - }, - { - name: i18n.translate('xpack.monitoring.cluster.listing.kibanaColumnTitle', { - defaultMessage: 'Kibana', - }), - field: 'kibana.count', - 'data-test-subj': 'kibanaCount', - sortable: true, - render: (count, cluster) => ( - <IsClusterSupported {...cluster}>{numeral(count).format('0,0')}</IsClusterSupported> - ), - }, - { - name: i18n.translate('xpack.monitoring.cluster.listing.licenseColumnTitle', { - defaultMessage: 'License', - }), - field: 'license.type', - 'data-test-subj': 'clusterLicense', - sortable: true, - render: (licenseType, cluster) => { - const license = cluster.license; - - if (!licenseType) { - return ( - <div> - <div className="monTableCell__clusterCellLiscense">N/A</div> - </div> - ); - } - - if (license) { - const licenseExpiry = () => { - if (license.expiry_date_in_millis < moment().valueOf()) { - // license is expired - return <span className="monTableCell__clusterCellExpired">Expired</span>; - } - - // license is fine - return <span>Expires {moment(license.expiry_date_in_millis).format('D MMM YY')}</span>; - }; - - return ( - <div> - <div className="monTableCell__clusterCellLiscense">{capitalize(licenseType)}</div> - <div className="monTableCell__clusterCellExpiration"> - {showLicenseExpiration ? licenseExpiry() : null} - </div> - </div> - ); - } - - // there is no license! - return ( - <EuiLink onClick={() => handleClickInvalidLicense(cluster.cluster_name)}> - <EuiHealth color="subdued" data-test-subj="alertIcon"> - N/A - </EuiHealth> - </EuiLink> - ); - }, - }, - ]; -}; - -const changeCluster = (scope, globalState, kbnUrl, clusterUuid, ccs) => { - scope.$evalAsync(() => { - globalState.cluster_uuid = clusterUuid; - globalState.ccs = ccs; - globalState.save(); - kbnUrl.changePath('/overview'); - }); -}; - -const licenseWarning = (scope, { title, text }) => { - scope.$evalAsync(() => { - toastNotifications.addWarning({ title, text, 'data-test-subj': 'monitoringLicenseWarning' }); - }); -}; - -const handleClickIncompatibleLicense = (scope, clusterName) => { - licenseWarning(scope, { - title: toMountPoint( - <FormattedMessage - id="xpack.monitoring.cluster.listing.incompatibleLicense.warningMessageTitle" - defaultMessage="You can't view the {clusterName} cluster" - values={{ clusterName: '"' + clusterName + '"' }} - /> - ), - text: toMountPoint( - <Fragment> - <p> - <FormattedMessage - id="xpack.monitoring.cluster.listing.incompatibleLicense.noMultiClusterSupportMessage" - defaultMessage="The Basic license does not support multi-cluster monitoring." - /> - </p> - <p> - <FormattedMessage - id="xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage" - defaultMessage="Need to monitor multiple clusters? {getLicenseInfoLink} to enjoy multi-cluster monitoring." - values={{ - getLicenseInfoLink: ( - <EuiLink href="https://www.elastic.co/subscriptions" target="_blank"> - <FormattedMessage - id="xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel" - defaultMessage="Get a license with full functionality" - /> - </EuiLink> - ), - }} - /> - </p> - </Fragment> - ), - }); -}; - -const handleClickInvalidLicense = (scope, clusterName) => { - const licensingPath = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management/home`; - - licenseWarning(scope, { - title: toMountPoint( - <FormattedMessage - id="xpack.monitoring.cluster.listing.invalidLicense.warningMessageTitle" - defaultMessage="You can't view the {clusterName} cluster" - values={{ clusterName: '"' + clusterName + '"' }} - /> - ), - text: toMountPoint( - <Fragment> - <p> - <FormattedMessage - id="xpack.monitoring.cluster.listing.invalidLicense.invalidInfoMessage" - defaultMessage="The license information is invalid." - /> - </p> - <p> - <FormattedMessage - id="xpack.monitoring.cluster.listing.invalidLicense.infoMessage" - defaultMessage="Need a license? {getBasicLicenseLink} or {getLicenseInfoLink} to enjoy multi-cluster monitoring." - values={{ - getBasicLicenseLink: ( - <EuiLink href={licensingPath}> - <FormattedMessage - id="xpack.monitoring.cluster.listing.invalidLicense.getBasicLicenseLinkLabel" - defaultMessage="Get a free Basic license" - /> - </EuiLink> - ), - getLicenseInfoLink: ( - <EuiLink href="https://www.elastic.co/subscriptions" target="_blank"> - <FormattedMessage - id="xpack.monitoring.cluster.listing.invalidLicense.getLicenseLinkLabel" - defaultMessage="Get a license with full functionality" - /> - </EuiLink> - ), - }} - /> - </p> - </Fragment> - ), - }); -}; - -export class Listing extends Component { - constructor(props) { - super(props); - this.state = { - [STANDALONE_CLUSTER_STORAGE_KEY]: false, - }; - } - - renderStandaloneClusterCallout(changeCluster, storage) { - if (storage.get(STANDALONE_CLUSTER_STORAGE_KEY)) { - return null; - } - - return ( - <div> - <EuiCallOut - color="warning" - title={i18n.translate('xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle', { - defaultMessage: - "It looks like you have instances that aren't connected to an Elasticsearch cluster.", - })} - iconType="link" - > - <p> - <EuiLink - onClick={() => changeCluster(STANDALONE_CLUSTER_CLUSTER_UUID)} - data-test-subj="standaloneClusterLink" - > - <FormattedMessage - id="xpack.monitoring.cluster.listing.standaloneClusterCallOutLink" - defaultMessage="View these instances." - /> - </EuiLink> -   - <FormattedMessage - id="xpack.monitoring.cluster.listing.standaloneClusterCallOutText" - defaultMessage="Or, click Standalone Cluster in the table below" - /> - </p> - <p> - <EuiLink - onClick={() => { - storage.set(STANDALONE_CLUSTER_STORAGE_KEY, true); - this.setState({ [STANDALONE_CLUSTER_STORAGE_KEY]: true }); - }} - > - <EuiIcon type="cross" /> -   - <FormattedMessage - id="xpack.monitoring.cluster.listing.standaloneClusterCallOutDismiss" - defaultMessage="Dismiss" - /> - </EuiLink> - </p> - </EuiCallOut> - <EuiSpacer /> - </div> - ); - } - - render() { - const { angular, clusters, sorting, pagination, onTableChange } = this.props; - - const _changeCluster = partial( - changeCluster, - angular.scope, - angular.globalState, - angular.kbnUrl - ); - const _handleClickIncompatibleLicense = partial(handleClickIncompatibleLicense, angular.scope); - const _handleClickInvalidLicense = partial(handleClickInvalidLicense, angular.scope); - const hasStandaloneCluster = !!clusters.find( - cluster => cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID - ); - - return ( - <EuiPage> - <EuiPageBody> - <EuiPageContent> - {hasStandaloneCluster - ? this.renderStandaloneClusterCallout(_changeCluster, angular.storage) - : null} - <EuiMonitoringTable - className="clusterTable" - rows={clusters} - columns={getColumns( - angular.showLicenseExpiration, - _changeCluster, - _handleClickIncompatibleLicense, - _handleClickInvalidLicense - )} - rowProps={item => { - return { - 'data-test-subj': `clusterRow_${item.cluster_uuid}`, - }; - }} - sorting={{ - ...sorting, - sort: { - ...sorting.sort, - field: 'cluster_name', - }, - }} - pagination={pagination} - search={{ - box: { - incremental: true, - placeholder: angular.scope.filterText, - }, - }} - onTableChange={onTableChange} - executeQueryOptions={{ - defaultFields: ['cluster_name'], - }} - /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - ); - } -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js b/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js deleted file mode 100644 index c2775713171ad..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js +++ /dev/null @@ -1,36 +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 React from 'react'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; -import { i18n } from '@kbn/i18n'; - -export function MachineLearningJobStatusIcon({ status }) { - const type = (() => { - const statusKey = status.toUpperCase(); - - if (statusKey === 'OPENED') { - return StatusIcon.TYPES.GREEN; - } else if (statusKey === 'CLOSED') { - return StatusIcon.TYPES.GRAY; - } else if (statusKey === 'FAILED') { - return StatusIcon.TYPES.RED; - } - - // basically a "changing" state like OPENING or CLOSING - return StatusIcon.TYPES.YELLOW; - })(); - - return ( - <StatusIcon - type={type} - label={i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.statusIconLabel', { - defaultMessage: 'Job Status: {status}', - values: { status }, - })} - /> - ); -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/instances/instances.js b/x-pack/legacy/plugins/monitoring/public/components/kibana/instances/instances.js deleted file mode 100644 index df817df268de4..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/kibana/instances/instances.js +++ /dev/null @@ -1,290 +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 React, { PureComponent, Fragment } from 'react'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPanel, - EuiSpacer, - EuiLink, - EuiCallOut, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { capitalize, get } from 'lodash'; -import { ClusterStatus } from '../cluster_status'; -import { EuiMonitoringTable } from '../../table'; -import { KibanaStatusIcon } from '../status_icon'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; -import { formatMetric, formatNumber } from '../../../lib/format_number'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { SetupModeBadge } from '../../setup_mode/badge'; -import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; -import { ListingCallOut } from '../../setup_mode/listing_callout'; - -const getColumns = setupMode => { - const columns = [ - { - name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', { - defaultMessage: 'Name', - }), - field: 'name', - render: (name, kibana) => { - let setupModeStatus = null; - if (setupMode && setupMode.enabled) { - const list = get(setupMode, 'data.byUuid', {}); - const uuid = get(kibana, 'kibana.uuid'); - const status = list[uuid] || {}; - const instance = { - uuid, - name: kibana.name, - }; - - setupModeStatus = ( - <div className="monTableCell__setupModeStatus"> - <SetupModeBadge - setupMode={setupMode} - status={status} - instance={instance} - productName={KIBANA_SYSTEM_ID} - /> - </div> - ); - if (status.isNetNewUser) { - return ( - <div> - {name} - {setupModeStatus} - </div> - ); - } - } - - return ( - <div> - <EuiLink - href={`#/kibana/instances/${kibana.kibana.uuid}`} - data-test-subj={`kibanaLink-${name}`} - > - {name} - </EuiLink> - {setupModeStatus} - </div> - ); - }, - }, - { - name: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', { - defaultMessage: 'Status', - }), - field: 'status', - render: (status, kibana) => ( - <div - title={i18n.translate('xpack.monitoring.kibana.listing.instanceStatusTitle', { - defaultMessage: 'Instance status: {kibanaStatus}', - values: { - kibanaStatus: status, - }, - })} - className="monTableCell__status" - > - <KibanaStatusIcon status={status} availability={kibana.availability} /> -   - {!kibana.availability ? ( - <FormattedMessage - id="xpack.monitoring.kibana.listing.instanceStatus.offlineLabel" - defaultMessage="Offline" - /> - ) : ( - capitalize(status) - )} - </div> - ), - }, - { - name: i18n.translate('xpack.monitoring.kibana.listing.loadAverageColumnTitle', { - defaultMessage: 'Load Average', - }), - field: 'os.load.1m', - render: value => <span>{formatMetric(value, '0.00')}</span>, - }, - { - name: i18n.translate('xpack.monitoring.kibana.listing.memorySizeColumnTitle', { - defaultMessage: 'Memory Size', - }), - field: 'process.memory.resident_set_size_in_bytes', - render: value => <span>{formatNumber(value, 'byte')}</span>, - }, - { - name: i18n.translate('xpack.monitoring.kibana.listing.requestsColumnTitle', { - defaultMessage: 'Requests', - }), - field: 'requests.total', - render: value => <span>{formatNumber(value, 'int_commas')}</span>, - }, - { - name: i18n.translate('xpack.monitoring.kibana.listing.responseTimeColumnTitle', { - defaultMessage: 'Response Times', - }), - // It is possible this does not exist through MB collection - field: 'response_times.average', - render: (value, kibana) => { - if (!value) { - return null; - } - - return ( - <div> - <div className="monTableCell__splitNumber"> - {formatNumber(value, 'int_commas') + ' ms avg'} - </div> - <div className="monTableCell__splitNumber"> - {formatNumber(kibana.response_times.max, 'int_commas')} ms max - </div> - </div> - ); - }, - }, - ]; - - return columns; -}; - -export class KibanaInstances extends PureComponent { - render() { - const { clusterStatus, angular, setupMode, sorting, pagination, onTableChange } = this.props; - - let setupModeCallOut = null; - // Merge the instances data with the setup data if enabled - const instances = this.props.instances || []; - if (setupMode.enabled && setupMode.data) { - // We want to create a seamless experience for the user by merging in the setup data - // and the node data from monitoring indices in the likely scenario where some instances - // are using MB collection and some are using no collection - const instancesByUuid = instances.reduce( - (byUuid, instance) => ({ - ...byUuid, - [get(instance, 'kibana.uuid')]: instance, - }), - {} - ); - - instances.push( - ...Object.entries(setupMode.data.byUuid).reduce((instances, [nodeUuid, instance]) => { - if (!instancesByUuid[nodeUuid]) { - instances.push({ - kibana: { - ...instance.instance.kibana, - status: StatusIcon.TYPES.GRAY, - }, - }); - } - return instances; - }, []) - ); - - setupModeCallOut = ( - <ListingCallOut - setupModeData={setupMode.data} - useNodeIdentifier={false} - productName={KIBANA_SYSTEM_ID} - customRenderer={() => { - const customRenderResponse = { - shouldRender: false, - componentToRender: null, - }; - - const hasInstances = setupMode.data.totalUniqueInstanceCount > 0; - if (!hasInstances) { - customRenderResponse.shouldRender = true; - customRenderResponse.componentToRender = ( - <Fragment> - <EuiCallOut - title={i18n.translate( - 'xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle', - { - defaultMessage: 'Kibana instance detected', - } - )} - color="warning" - iconType="flag" - > - <p> - {i18n.translate( - 'xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription', - { - defaultMessage: `The following instances are not monitored. - Click 'Monitor with Metricbeat' below to start monitoring.`, - } - )} - </p> - </EuiCallOut> - <EuiSpacer size="m" /> - </Fragment> - ); - } - - return customRenderResponse; - }} - /> - ); - } - - const dataFlattened = instances.map(item => ({ - ...item, - name: item.kibana.name, - status: item.kibana.status, - })); - - return ( - <EuiPage> - <EuiPageBody> - <EuiScreenReaderOnly> - <h1> - <FormattedMessage - id="xpack.monitoring.kibana.instances.heading" - defaultMessage="Kibana instances" - /> - </h1> - </EuiScreenReaderOnly> - <EuiPanel> - <ClusterStatus stats={clusterStatus} /> - </EuiPanel> - <EuiSpacer size="m" /> - {setupModeCallOut} - <EuiPageContent> - <EuiMonitoringTable - className="kibanaInstancesTable" - rows={dataFlattened} - columns={getColumns(angular.kbnUrl, angular.$scope, setupMode)} - sorting={sorting} - pagination={pagination} - setupMode={setupMode} - productName={KIBANA_SYSTEM_ID} - search={{ - box: { - incremental: true, - placeholder: i18n.translate( - 'xpack.monitoring.kibana.listing.filterInstancesPlaceholder', - { - defaultMessage: 'Filter Instances…', - } - ), - }, - }} - onTableChange={onTableChange} - executeQueryOptions={{ - defaultFields: ['name'], - }} - /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - ); - } -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/status_icon.js b/x-pack/legacy/plugins/monitoring/public/components/kibana/status_icon.js deleted file mode 100644 index 87a2ab4cc4713..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/kibana/status_icon.js +++ /dev/null @@ -1,32 +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 React from 'react'; -import { StatusIcon } from 'plugins/monitoring/components/status_icon'; -import { i18n } from '@kbn/i18n'; - -export function KibanaStatusIcon({ status, availability = true }) { - const type = (() => { - if (!availability) { - return StatusIcon.TYPES.GRAY; - } - - const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; - })(); - - return ( - <StatusIcon - type={type} - label={i18n.translate('xpack.monitoring.kibana.statusIconLabel', { - defaultMessage: 'Health: {status}', - values: { - status, - }, - })} - /> - ); -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/license/index.js b/x-pack/legacy/plugins/monitoring/public/components/license/index.js deleted file mode 100644 index d43896d5f8d84..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/license/index.js +++ /dev/null @@ -1,91 +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 React from 'react'; -import { - EuiPage, - EuiPageBody, - EuiSpacer, - EuiCodeBlock, - EuiPanel, - EuiText, - EuiLink, - EuiFlexGroup, - EuiFlexItem, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { LicenseStatus, AddLicense } from 'plugins/xpack_main/components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; - -const licenseManagement = `${chrome.getBasePath()}/app/kibana#/management/elasticsearch/license_management`; - -const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => { - if (!isPrimaryCluster) { - return null; - } - - // viewed license is for the cluster directly connected to Kibana - return <AddLicense uploadPath={uploadLicensePath} />; -}; - -const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { - if (isPrimaryCluster) { - return null; - } - - // viewed license is for a remote monitored cluster not directly connected to Kibana - return ( - <EuiPanel> - <p> - <FormattedMessage - id="xpack.monitoring.license.howToUpdateLicenseDescription" - defaultMessage="To update the license for this cluster, provide the license file through - the Elasticsearch {apiText}:" - values={{ - apiText: 'API', - }} - /> - </p> - <EuiSpacer /> - <EuiCodeBlock> - {`curl -XPUT -u <user> 'https://<host>:<port>/_license' -H 'Content-Type: application/json' -d @license.json`} - </EuiCodeBlock> - </EuiPanel> - ); -}; - -export function License(props) { - const { status, type, isExpired, expiryDate } = props; - return ( - <EuiPage> - <EuiScreenReaderOnly> - <h1> - <FormattedMessage id="xpack.monitoring.license.heading" defaultMessage="License" /> - </h1> - </EuiScreenReaderOnly> - <EuiPageBody> - <LicenseStatus isExpired={isExpired} status={status} type={type} expiryDate={expiryDate} /> - <EuiSpacer /> - - <EuiFlexGroup justifyContent="center"> - <EuiFlexItem grow={false}> - <LicenseUpdateInfoForPrimary {...props} /> - <LicenseUpdateInfoForRemote {...props} /> - </EuiFlexItem> - </EuiFlexGroup> - - <EuiSpacer /> - <EuiText size="s" textAlign="center"> - <p> - For more license options please visit  - <EuiLink href={licenseManagement}>License Management</EuiLink>. - </p> - </EuiText> - </EuiPageBody> - </EuiPage> - ); -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js deleted file mode 100644 index 9fa33802b0202..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js +++ /dev/null @@ -1,140 +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 { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; -import { Monospace } from '../components/monospace'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; - -export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { - const securitySetup = getSecurityStep( - `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-configuration.html` - ); - - const installMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatTitle', - { - defaultMessage: 'Install Metricbeat on the same server as the APM server', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const enableMetricbeatModuleStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleTitle', - { - defaultMessage: 'Enable and configure the Beat x-pack module in Metricbeat', - } - ), - children: ( - <Fragment> - <EuiCodeBlock isCopyable language="bash"> - metricbeat modules enable beat-xpack - </EuiCodeBlock> - <EuiSpacer size="s" /> - <EuiText> - <p> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleDescription" - defaultMessage="By default the module will collect APM server monitoring metrics from http://localhost:5066. If the local APM server has a different address, you must specify it via the {hosts} setting in the {file} file." - values={{ - hosts: <Monospace>hosts</Monospace>, - file: <Monospace>modules.d/beat-xpack.yml</Monospace>, - }} - /> - </p> - </EuiText> - {securitySetup} - </Fragment> - ), - }; - - const configureMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatTitle', - { - defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', - } - ), - children: ( - <Fragment> - <EuiText> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatDescription" - defaultMessage="Make these changes in your {file}." - values={{ - file: <Monospace>metricbeat.yml</Monospace>, - }} - /> - </EuiText> - <EuiSpacer size="s" /> - <EuiCodeBlock isCopyable> - {`output.elasticsearch: - hosts: [${esMonitoringUrl}] ## Monitoring cluster - - # Optional protocol and basic auth credentials. - #protocol: "https" - #username: "elastic" - #password: "changeme" -`} - </EuiCodeBlock> - {securitySetup} - </Fragment> - ), - }; - - const startMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatTitle', - { - defaultMessage: 'Start Metricbeat', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const migrationStatusStep = getMigrationStatusStep(product); - - return [ - installMetricbeatStep, - enableMetricbeatModuleStep, - configureMetricbeatStep, - startMetricbeatStep, - migrationStatusStep, - ]; -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js deleted file mode 100644 index 56e7c9b5af064..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js +++ /dev/null @@ -1,179 +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 { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import { EuiSpacer, EuiCodeBlock, EuiLink, EuiCallOut, EuiText } from '@elastic/eui'; -import { Monospace } from '../components/monospace'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { UNDETECTED_BEAT_TYPE, DEFAULT_BEAT_FOR_URLS } from './common_beats_instructions'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; - -export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { - const beatType = product.beatType; - const securitySetup = getSecurityStep( - `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-configuration.html` - ); - - const installMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatTitle', - { - defaultMessage: 'Install Metricbeat on the same server as this {beatType}', - values: { - beatType: beatType || UNDETECTED_BEAT_TYPE, - }, - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const httpEndpointUrl = - `${ELASTIC_WEBSITE_URL}guide/en/beats/${beatType || DEFAULT_BEAT_FOR_URLS}` + - `/${DOC_LINK_VERSION}/http-endpoint.html`; - - const enableMetricbeatModuleStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleTitle', - { - defaultMessage: 'Enable and configure the Beat x-pack module in Metricbeat', - } - ), - children: ( - <Fragment> - <EuiCodeBlock isCopyable language="bash"> - metricbeat modules enable beat-xpack - </EuiCodeBlock> - <EuiSpacer size="s" /> - <EuiText> - <p> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleDescription" - defaultMessage="By default the module will collect {beatType} monitoring metrics from http://localhost:5066. If the {beatType} instance being monitored has a different address, you must specify it via the {hosts} setting in the {file} file." - values={{ - hosts: <Monospace>hosts</Monospace>, - file: <Monospace>modules.d/beat-xpack.yml</Monospace>, - beatType: beatType || UNDETECTED_BEAT_TYPE, - }} - /> - </p> - </EuiText> - <EuiSpacer size="m" /> - <EuiCallOut - color="warning" - iconType="help" - title={ - <EuiText> - <p> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirections" - defaultMessage="In order for Metricbeat to collect metrics from the running {beatType}, you need to {link}." - values={{ - link: ( - <EuiLink href={httpEndpointUrl} target="_blank"> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirectionsLinkText" - defaultMessage="enable an HTTP endpoint for the {beatType} instance being monitored" - values={{ - beatType, - }} - /> - </EuiLink> - ), - beatType: beatType || UNDETECTED_BEAT_TYPE, - }} - /> - </p> - </EuiText> - } - /> - {securitySetup} - </Fragment> - ), - }; - - const configureMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatTitle', - { - defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', - } - ), - children: ( - <Fragment> - <EuiText> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatDescription" - defaultMessage="Make these changes in your {file}." - values={{ - file: <Monospace>metricbeat.yml</Monospace>, - }} - /> - </EuiText> - <EuiSpacer size="s" /> - <EuiCodeBlock isCopyable> - {`output.elasticsearch: - hosts: [${esMonitoringUrl}] ## Monitoring cluster - - # Optional protocol and basic auth credentials. - #protocol: "https" - #username: "elastic" - #password: "changeme" -`} - </EuiCodeBlock> - {securitySetup} - </Fragment> - ), - }; - - const startMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatTitle', - { - defaultMessage: 'Start Metricbeat', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const migrationStatusStep = getMigrationStatusStep(product); - - return [ - installMetricbeatStep, - enableMetricbeatModuleStep, - configureMetricbeatStep, - startMetricbeatStep, - migrationStatusStep, - ]; -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js deleted file mode 100644 index 36b3dd21ff43e..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js +++ /dev/null @@ -1,156 +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 { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; -import { Monospace } from '../components/monospace'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { getSecurityStep, getMigrationStatusStep } from '../common_instructions'; - -export function getElasticsearchInstructionsForEnablingMetricbeat( - product, - _meta, - { esMonitoringUrl } -) { - const securitySetup = getSecurityStep( - `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` - ); - - const installMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatTitle', - { - defaultMessage: 'Install Metricbeat on the same server as Elasticsearch', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatLinkText" - defaultMessage="Follow these instructions." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const enableMetricbeatModuleStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleTitle', - { - defaultMessage: 'Enable and configure the Elasticsearch x-pack module in Metricbeat', - } - ), - children: ( - <Fragment> - <EuiText> - <p> - {i18n.translate( - 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleInstallDirectory', - { - defaultMessage: 'From the installation directory, run:', - } - )} - </p> - </EuiText> - <EuiSpacer size="s" /> - <EuiCodeBlock isCopyable language="bash"> - metricbeat modules enable elasticsearch-xpack - </EuiCodeBlock> - <EuiSpacer size="s" /> - <EuiText> - <p> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleDescription" - defaultMessage="By default the module collects Elasticsearch metrics from {url}. - If the local server has a different address, add it to the hosts setting in {module}." - values={{ - module: <Monospace>modules.d/elasticsearch-xpack.yml</Monospace>, - url: <Monospace>http://localhost:9200</Monospace>, - }} - /> - </p> - </EuiText> - {securitySetup} - </Fragment> - ), - }; - - const configureMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatTitle', - { - defaultMessage: 'Configure Metricbeat to send data to the monitoring cluster', - } - ), - children: ( - <Fragment> - <EuiText> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatDescription" - defaultMessage="Modify {file} to set the connection information." - values={{ - file: <Monospace>metricbeat.yml</Monospace>, - }} - /> - </EuiText> - <EuiSpacer size="s" /> - <EuiCodeBlock isCopyable> - {`output.elasticsearch: - hosts: [${esMonitoringUrl}] ## Monitoring cluster - - # Optional protocol and basic auth credentials. - #protocol: "https" - #username: "elastic" - #password: "changeme" -`} - </EuiCodeBlock> - {securitySetup} - </Fragment> - ), - }; - - const startMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatTitle', - { - defaultMessage: 'Start Metricbeat', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatLinkText" - defaultMessage="Follow these instructions." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const migrationStatusStep = getMigrationStatusStep(product); - - return [ - installMetricbeatStep, - enableMetricbeatModuleStep, - configureMetricbeatStep, - startMetricbeatStep, - migrationStatusStep, - ]; -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js deleted file mode 100644 index 98c75dbfe4b37..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js +++ /dev/null @@ -1,140 +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 { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; -import { Monospace } from '../components/monospace'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; - -export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { - const securitySetup = getSecurityStep( - `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html` - ); - - const installMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatTitle', - { - defaultMessage: 'Install Metricbeat on the same server as Kibana', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const enableMetricbeatModuleStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleTitle', - { - defaultMessage: 'Enable and configure the Kibana x-pack module in Metricbeat', - } - ), - children: ( - <Fragment> - <EuiCodeBlock isCopyable language="bash"> - metricbeat modules enable kibana-xpack - </EuiCodeBlock> - <EuiSpacer size="s" /> - <EuiText> - <p> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleDescription" - defaultMessage="By default the module will collect Kibana monitoring metrics from http://localhost:5601. If the local Kibana instance has a different address, you must specify it via the {hosts} setting in the {file} file." - values={{ - hosts: <Monospace>hosts</Monospace>, - file: <Monospace>modules.d/kibana-xpack.yml</Monospace>, - }} - /> - </p> - </EuiText> - {securitySetup} - </Fragment> - ), - }; - - const configureMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatTitle', - { - defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', - } - ), - children: ( - <Fragment> - <EuiText> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatDescription" - defaultMessage="Make these changes in your {file}." - values={{ - file: <Monospace>metricbeat.yml</Monospace>, - }} - /> - </EuiText> - <EuiSpacer size="s" /> - <EuiCodeBlock isCopyable> - {`output.elasticsearch: - hosts: [${esMonitoringUrl}] ## Monitoring cluster - - # Optional protocol and basic auth credentials. - #protocol: "https" - #username: "elastic" - #password: "changeme" -`} - </EuiCodeBlock> - {securitySetup} - </Fragment> - ), - }; - - const startMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatTitle', - { - defaultMessage: 'Start Metricbeat', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const migrationStatusStep = getMigrationStatusStep(product); - - return [ - installMetricbeatStep, - enableMetricbeatModuleStep, - configureMetricbeatStep, - startMetricbeatStep, - migrationStatusStep, - ]; -} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js b/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js deleted file mode 100644 index 4a36f394e4bd5..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js +++ /dev/null @@ -1,140 +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 { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; -import { Monospace } from '../components/monospace'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; - -export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { - const securitySetup = getSecurityStep( - `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html` - ); - - const installMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatTitle', - { - defaultMessage: 'Install Metricbeat on the same server as Logstash', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const enableMetricbeatModuleStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleTitle', - { - defaultMessage: 'Enable and configure the Logstash x-pack module in Metricbeat', - } - ), - children: ( - <Fragment> - <EuiCodeBlock isCopyable language="bash"> - metricbeat modules enable logstash-xpack - </EuiCodeBlock> - <EuiSpacer size="s" /> - <EuiText> - <p> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleDescription" - defaultMessage="By default the module will collect Logstash monitoring metrics from http://localhost:9600. If the local Logstash instance has a different address, you must specify it via the {hosts} setting in the {file} file." - values={{ - hosts: <Monospace>hosts</Monospace>, - file: <Monospace>modules.d/logstash-xpack.yml</Monospace>, - }} - /> - </p> - </EuiText> - {securitySetup} - </Fragment> - ), - }; - - const configureMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatTitle', - { - defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', - } - ), - children: ( - <Fragment> - <EuiText> - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatDescription" - defaultMessage="Make these changes in your {file}." - values={{ - file: <Monospace>metricbeat.yml</Monospace>, - }} - /> - </EuiText> - <EuiSpacer size="s" /> - <EuiCodeBlock isCopyable> - {`output.elasticsearch: - hosts: [${esMonitoringUrl}] ## Monitoring cluster - - # Optional protocol and basic auth credentials. - #protocol: "https" - #username: "elastic" - #password: "changeme" -`} - </EuiCodeBlock> - {securitySetup} - </Fragment> - ), - }; - - const startMetricbeatStep = { - title: i18n.translate( - 'xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatTitle', - { - defaultMessage: 'Start Metricbeat', - } - ), - children: ( - <EuiText> - <p> - <EuiLink - href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} - target="_blank" - > - <FormattedMessage - id="xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatLinkText" - defaultMessage="Follow the instructions here." - /> - </EuiLink> - </p> - </EuiText> - ), - }; - - const migrationStatusStep = getMigrationStatusStep(product); - - return [ - installMetricbeatStep, - enableMetricbeatModuleStep, - configureMetricbeatStep, - startMetricbeatStep, - migrationStatusStep, - ]; -} diff --git a/x-pack/legacy/plugins/monitoring/public/directives/all.js b/x-pack/legacy/plugins/monitoring/public/directives/all.js deleted file mode 100644 index 43ad80a7a7e94..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/directives/all.js +++ /dev/null @@ -1,10 +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 './main'; -import './elasticsearch/ml_job_listing'; -import './beats/overview'; -import './beats/beat'; diff --git a/x-pack/legacy/plugins/monitoring/public/directives/beats/beat/index.js b/x-pack/legacy/plugins/monitoring/public/directives/beats/beat/index.js deleted file mode 100644 index c86315fc03482..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/directives/beats/beat/index.js +++ /dev/null @@ -1,40 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { Beat } from 'plugins/monitoring/components/beats/beat'; -import { I18nContext } from 'ui/i18n'; - -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringBeatsBeat', () => { - return { - restrict: 'E', - scope: { - data: '=', - onBrush: '<', - zoomInfo: '<', - }, - link(scope, $el) { - scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); - - scope.$watch('data', (data = {}) => { - render( - <I18nContext> - <Beat - summary={data.summary} - metrics={data.metrics} - onBrush={scope.onBrush} - zoomInfo={scope.zoomInfo} - /> - </I18nContext>, - $el[0] - ); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/directives/beats/overview/index.js b/x-pack/legacy/plugins/monitoring/public/directives/beats/overview/index.js deleted file mode 100644 index fb78b6a2e0300..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/directives/beats/overview/index.js +++ /dev/null @@ -1,35 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { BeatsOverview } from 'plugins/monitoring/components/beats/overview'; -import { I18nContext } from 'ui/i18n'; - -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringBeatsOverview', () => { - return { - restrict: 'E', - scope: { - data: '=', - onBrush: '<', - zoomInfo: '<', - }, - link(scope, $el) { - scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); - - scope.$watch('data', (data = {}) => { - render( - <I18nContext> - <BeatsOverview {...data} onBrush={scope.onBrush} zoomInfo={scope.zoomInfo} /> - </I18nContext>, - $el[0] - ); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js b/x-pack/legacy/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js deleted file mode 100644 index 8f35bd599ac49..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js +++ /dev/null @@ -1,167 +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 { capitalize } from 'lodash'; -import numeral from '@elastic/numeral'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; -import { MachineLearningJobStatusIcon } from 'plugins/monitoring/components/elasticsearch/ml_job_listing/status_icon'; -import { LARGE_ABBREVIATED, LARGE_BYTES } from '../../../../common/formatting'; -import { EuiLink, EuiPage, EuiPageContent, EuiPageBody, EuiPanel, EuiSpacer } from '@elastic/eui'; -import { ClusterStatus } from '../../../components/elasticsearch/cluster_status'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -const getColumns = (kbnUrl, scope) => [ - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.jobIdTitle', { - defaultMessage: 'Job ID', - }), - field: 'job_id', - sortable: true, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.stateTitle', { - defaultMessage: 'State', - }), - field: 'state', - sortable: true, - render: state => ( - <div> - <MachineLearningJobStatusIcon status={state} /> -   - {capitalize(state)} - </div> - ), - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.processedRecordsTitle', { - defaultMessage: 'Processed Records', - }), - field: 'data_counts.processed_record_count', - sortable: true, - render: value => <span>{numeral(value).format(LARGE_ABBREVIATED)}</span>, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.modelSizeTitle', { - defaultMessage: 'Model Size', - }), - field: 'model_size_stats.model_bytes', - sortable: true, - render: value => <span>{numeral(value).format(LARGE_BYTES)}</span>, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.forecastsTitle', { - defaultMessage: 'Forecasts', - }), - field: 'forecasts_stats.total', - sortable: true, - render: value => <span>{numeral(value).format(LARGE_ABBREVIATED)}</span>, - }, - { - name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.nodeTitle', { - defaultMessage: 'Node', - }), - field: 'node.name', - sortable: true, - render: (name, node) => { - if (node) { - return ( - <EuiLink - onClick={() => { - scope.$evalAsync(() => kbnUrl.changePath(`/elasticsearch/nodes/${node.id}`)); - }} - > - {name} - </EuiLink> - ); - } - - return ( - <FormattedMessage - id="xpack.monitoring.elasticsearch.mlJobListing.noDataLabel" - defaultMessage="N/A" - /> - ); - }, - }, -]; - -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringMlListing', kbnUrl => { - return { - restrict: 'E', - scope: { - jobs: '=', - paginationSettings: '=', - sorting: '=', - onTableChange: '=', - status: '=', - }, - link(scope, $el) { - scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); - const columns = getColumns(kbnUrl, scope); - - const filterJobsPlaceholder = i18n.translate( - 'xpack.monitoring.elasticsearch.mlJobListing.filterJobsPlaceholder', - { - defaultMessage: 'Filter Jobs…', - } - ); - - scope.$watch('jobs', (jobs = []) => { - const mlTable = ( - <I18nContext> - <EuiPage> - <EuiPageBody> - <EuiPanel> - <ClusterStatus stats={scope.status} /> - </EuiPanel> - <EuiSpacer size="m" /> - <EuiPageContent> - <EuiMonitoringTable - className="mlJobsTable" - rows={jobs} - columns={columns} - sorting={{ - ...scope.sorting, - sort: { - ...scope.sorting.sort, - field: 'job_id', - }, - }} - pagination={scope.paginationSettings} - message={i18n.translate( - 'xpack.monitoring.elasticsearch.mlJobListing.noJobsDescription', - { - defaultMessage: - 'There are no Machine Learning Jobs that match your query. Try changing the time range selection.', - } - )} - search={{ - box: { - incremental: true, - placeholder: filterJobsPlaceholder, - }, - }} - onTableChange={scope.onTableChange} - executeQueryOptions={{ - defaultFields: ['job_id'], - }} - /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </I18nContext> - ); - render(mlTable, $el[0]); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/index.html b/x-pack/legacy/plugins/monitoring/public/directives/main/index.html deleted file mode 100644 index bb34cf6f5bb63..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/directives/main/index.html +++ /dev/null @@ -1,316 +0,0 @@ -<div class="app-container"> - <div id="setupModeNav"></div> - <kbn-top-nav - name="monitoringMain.navName" - app-name="'monitoring'" - show-search-bar="true" - show-date-picker="true" - date-range-from="monitoringMain.datePicker.timeRange.from" - date-range-to="monitoringMain.datePicker.timeRange.to" - is-refresh-paused="monitoringMain.datePicker.refreshInterval.pause" - refresh-interval="monitoringMain.datePicker.refreshInterval.value" - on-refresh-change="monitoringMain.datePicker.onRefreshChange" - on-query-submit="monitoringMain.datePicker.onTimeUpdate" - > - </kbn-top-nav> - <div> - <div ng-if="monitoringMain.inElasticsearch" class="euiTabs" role="navigation"> - <a - ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')" - kbn-href="#/elasticsearch" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.esNavigation.overviewLinkText" - i18n-default-message="Overview" - ></a> - <a - ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')" - kbn-href="" - class="euiTab euiTab-isDisabled" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.esNavigation.overviewLinkText" - i18n-default-message="Overview" - ></a> - <a - ng-if="!monitoringMain.instance" - kbn-href="#/elasticsearch/nodes" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('nodes')}" - i18n-id="xpack.monitoring.esNavigation.nodesLinkText" - i18n-default-message="Nodes" - ></a> - <a - ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')" - kbn-href="#/elasticsearch/indices" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}" - i18n-id="xpack.monitoring.esNavigation.indicesLinkText" - i18n-default-message="Indices" - ></a> - <a - ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')" - kbn-href="" - class="euiTab euiTab-isDisabled" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}" - i18n-id="xpack.monitoring.esNavigation.indicesLinkText" - i18n-default-message="Indices" - ></a> - <a - ng-if="!monitoringMain.instance && monitoringMain.isMlSupported()" - kbn-href="#/elasticsearch/ml_jobs" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('ml')}" - i18n-id="xpack.monitoring.esNavigation.jobsLinkText" - i18n-default-message="Jobs" - ></a> - <a - ng-if="(monitoringMain.isCcrEnabled || monitoringMain.isActiveTab('ccr')) && !monitoringMain.instance" - kbn-href="#/elasticsearch/ccr" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('ccr')}" - i18n-id="xpack.monitoring.esNavigation.ccrLinkText" - i18n-default-message="CCR" - ></a> - <a - ng-if="monitoringMain.instance && (monitoringMain.name === 'nodes' || monitoringMain.name === 'indices')" - kbn-href="#/elasticsearch/{{ monitoringMain.name }}/{{ monitoringMain.resolver }}" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}" - > - <span - ng-if="monitoringMain.tabIconClass" - class="fa {{ monitoringMain.tabIconClass }}" - title="{{ monitoringMain.tabIconLabel }}" - ></span> - <span - i18n-id="xpack.monitoring.esNavigation.instance.overviewLinkText" - i18n-default-message="Overview" - ></span> - </a> - <a - ng-if="monitoringMain.instance && (monitoringMain.name === 'nodes' || monitoringMain.name === 'indices')" - kbn-href="#/elasticsearch/{{ monitoringMain.name }}/{{ monitoringMain.resolver }}/advanced" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.page === 'advanced'}" - i18n-id="xpack.monitoring.esNavigation.instance.advancedLinkText" - i18n-default-message="Advanced" - > - </a> - <!-- ML Instance (for use later) --> - <a - ng-if="monitoringMain.instance && monitoringMain.name !== 'nodes' && monitoringMain.name !== 'indices'" - class="euiTab" - >{{ monitoringMain.instance }}</a - > - </div> - - <div ng-if="monitoringMain.inKibana" class="euiTabs" role="navigation"> - <a - ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('kibana')" - kbn-href="#/kibana" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.kibanaNavigation.overviewLinkText" - i18n-default-message="Overview" - ></a> - <a - ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('kibana')" - kbn-href="" - class="euiTab euiTab-isDisabled" - ng-class="{ - 'euiTab-isSelected': monitoringMain.isActiveTab('overview'), - }" - i18n-id="xpack.monitoring.kibanaNavigation.overviewLinkText" - i18n-default-message="Overview" - ></a> - <a - ng-if="!monitoringMain.instance" - kbn-href="#/kibana/instances" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('kibanas')}" - i18n-id="xpack.monitoring.kibanaNavigation.instancesLinkText" - i18n-default-message="Instances" - ></a> - <a ng-if="monitoringMain.instance" class="euiTab">{{ monitoringMain.instance }}</a> - </div> - - <div ng-if="monitoringMain.inApm" class="euiTabs" role="navigation"> - <a - ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('apm')" - kbn-href="#/apm" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.apmNavigation.overviewLinkText" - i18n-default-message="Overview" - ></a> - <a - ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('apm')" - kbn-href="" - class="euiTab euiTab-isDisabled" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.apmNavigation.overviewLinkText" - i18n-default-message="Overview" - ></a> - <a - ng-if="!monitoringMain.instance" - kbn-href="#/apm/instances" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('apms')}" - i18n-id="xpack.monitoring.apmNavigation.instancesLinkText" - i18n-default-message="Instances" - ></a> - <a ng-if="monitoringMain.instance" class="euiTab">{{ monitoringMain.instance }}</a> - </div> - - <div ng-if="monitoringMain.inBeats" class="euiTabs" role="navigation"> - <a - ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('beats')" - kbn-href="#/beats" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.beatsNavigation.overviewLinkText" - i18n-default-message="Overview" - > - </a> - <a - ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('beats')" - kbn-href="" - class="euiTab euiTab-isDisabled" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.beatsNavigation.overviewLinkText" - i18n-default-message="Overview" - > - </a> - <a - ng-if="!monitoringMain.instance" - kbn-href="#/beats/beats" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('beats')}" - i18n-id="xpack.monitoring.beatsNavigation.instancesLinkText" - i18n-default-message="Instances" - > - </a> - <a - ng-if="monitoringMain.instance" - kbn-href="#/beats/beat/{{ monitoringMain.resolver }}" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}" - i18n-id="xpack.monitoring.beatsNavigation.instance.overviewLinkText" - i18n-default-message="Overview" - > - </a> - </div> - - <div ng-if="monitoringMain.inLogstash" class="euiTabs" role="navigation"> - <a - ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && !monitoringMain.isDisabledTab('logstash')" - kbn-href="#/logstash" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.logstashNavigation.overviewLinkText" - i18n-default-message="Overview" - > - </a> - <a - ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && monitoringMain.isDisabledTab('logstash')" - kbn-href="" - class="euiTab euiTab-isDisabled" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" - i18n-id="xpack.monitoring.logstashNavigation.overviewLinkText" - i18n-default-message="Overview" - > - </a> - <a - ng-if="!monitoringMain.instance && !monitoringMain.pipelineId" - kbn-href="#/logstash/nodes" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('nodes')}" - i18n-id="xpack.monitoring.logstashNavigation.nodesLinkText" - i18n-default-message="Nodes" - > - </a> - <a - ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && !monitoringMain.isDisabledTab('logstash')" - kbn-href="#/logstash/pipelines" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('pipelines')}" - > - <span - i18n-id="xpack.monitoring.logstashNavigation.pipelinesLinkText" - i18n-default-message="Pipelines" - ></span> - <span class="kuiIcon fa-flask monTabs--icon" tooltip="Beta feature" /> - </a> - <a - ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && monitoringMain.isDisabledTab('logstash')" - kbn-href="" - class="euiTab euiTab-isDisabled" - ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('pipelines')}" - > - <span - i18n-id="xpack.monitoring.logstashNavigation.pipelinesLinkText" - i18n-default-message="Pipelines" - ></span> - <span class="kuiIcon fa-flask monTabs--icon" tooltip="Beta feature" /> - </a> - <a - ng-if="monitoringMain.instance" - kbn-href="#/logstash/node/{{ monitoringMain.resolver }}" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}" - i18n-id="xpack.monitoring.logstashNavigation.instance.overviewLinkText" - i18n-default-message="Overview" - > - </a> - <a - ng-if="monitoringMain.instance" - kbn-href="#/logstash/node/{{ monitoringMain.resolver }}/pipelines" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.page === 'pipelines'}" - > - <span - i18n-id="xpack.monitoring.logstashNavigation.instance.pipelinesLinkText" - i18n-default-message="Pipelines" - ></span> - <span class="kuiIcon fa-flask fa-sm monTabs--icon" tooltip="Beta feature" /> - </a> - <a - ng-if="monitoringMain.instance" - kbn-href="#/logstash/node/{{ monitoringMain.resolver }}/advanced" - class="euiTab" - ng-class="{'euiTab-isSelected': monitoringMain.page === 'advanced'}" - i18n-id="xpack.monitoring.logstashNavigation.instance.advancedLinkText" - i18n-default-message="Advanced" - > - </a> - <div - class="euiTab" - ng-if="monitoringMain.pipelineVersions.length" - id="dropdown-elm" - ng-init="monitoringMain.dropdownLoadedHandler()" - ></div> - </div> - - <div ng-if="monitoringMain.inOverview" class="euiTabs" role="navigation"> - <a class="euiTab" data-test-subj="clusterName">{{ pageData.cluster_name }}</a> - </div> - - <div ng-if="monitoringMain.inAlerts" class="euiTabs" role="navigation"> - <a - class="euiTab" - data-test-subj="clusterAlertsListingPage" - i18n-id="xpack.monitoring.clusterAlertsNavigation.clusterAlertsLinkText" - i18n-default-message="Cluster Alerts" - ></a> - </div> - - <div ng-if="monitoringMain.inListing" class="euiTabs" role="navigation"> - <a - class="euiTab" - i18n-id="xpack.monitoring.clustersNavigation.clustersLinkText" - i18n-default-message="Clusters" - ></a> - </div> - </div> - <div ng-transclude></div> -</div> diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/index.js b/x-pack/legacy/plugins/monitoring/public/directives/main/index.js deleted file mode 100644 index 2505f651d9803..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/directives/main/index.js +++ /dev/null @@ -1,262 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { EuiSelect, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { shortenPipelineHash } from '../../../common/formatting'; -import { getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; -import { Subscription } from 'rxjs'; - -const setOptions = controller => { - if ( - !controller.pipelineVersions || - !controller.pipelineVersions.length || - !controller.pipelineDropdownElement - ) { - return; - } - - render( - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiTitle - style={{ maxWidth: 400, lineHeight: '40px', overflow: 'hidden', whiteSpace: 'nowrap' }} - > - <h2>{controller.pipelineId}</h2> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiSelect - value={controller.pipelineHash} - options={controller.pipelineVersions.map(option => { - return { - text: i18n.translate( - 'xpack.monitoring.logstashNavigation.pipelineVersionDescription', - { - defaultMessage: - 'Version active {relativeLastSeen} and first seen {relativeFirstSeen}', - values: { - relativeLastSeen: option.relativeLastSeen, - relativeFirstSeen: option.relativeFirstSeen, - }, - } - ), - value: option.hash, - }; - })} - onChange={controller.onChangePipelineHash} - /> - </EuiFlexItem> - </EuiFlexGroup>, - controller.pipelineDropdownElement - ); -}; - -/* - * Manage data and provide helper methods for the "main" directive's template - */ -export class MonitoringMainController { - // called internally by Angular - constructor() { - this.inListing = false; - this.inAlerts = false; - this.inOverview = false; - this.inElasticsearch = false; - this.inKibana = false; - this.inLogstash = false; - this.inBeats = false; - this.inApm = false; - } - - addTimerangeObservers = () => { - this.subscriptions = new Subscription(); - - const refreshIntervalUpdated = () => { - const { value: refreshInterval, pause: isPaused } = timefilter.getRefreshInterval(); - this.datePicker.onRefreshChange({ refreshInterval, isPaused }, true); - }; - - const timeUpdated = () => { - this.datePicker.onTimeUpdate({ dateRange: timefilter.getTime() }, true); - }; - - this.subscriptions.add( - timefilter.getRefreshIntervalUpdate$().subscribe(refreshIntervalUpdated) - ); - this.subscriptions.add(timefilter.getTimeUpdate$().subscribe(timeUpdated)); - }; - - dropdownLoadedHandler() { - this.pipelineDropdownElement = document.querySelector('#dropdown-elm'); - setOptions(this); - } - - // kick things off from the directive link function - setup(options) { - this._licenseService = options.licenseService; - this._breadcrumbsService = options.breadcrumbsService; - this._kbnUrlService = options.kbnUrlService; - this._executorService = options.executorService; - - Object.assign(this, options.attributes); - - this.navName = `${this.name}-nav`; - - // set the section we're navigated in - if (this.product) { - this.inElasticsearch = this.product === 'elasticsearch'; - this.inKibana = this.product === 'kibana'; - this.inLogstash = this.product === 'logstash'; - this.inBeats = this.product === 'beats'; - this.inApm = this.product === 'apm'; - } else { - this.inOverview = this.name === 'overview'; - this.inAlerts = this.name === 'alerts'; - this.inListing = this.name === 'listing'; // || this.name === 'no-data'; - } - - if (!this.inListing) { - // no breadcrumbs in cluster listing page - this.breadcrumbs = this._breadcrumbsService(options.clusterName, this); - } - - if (this.pipelineHash) { - this.pipelineHashShort = shortenPipelineHash(this.pipelineHash); - this.onChangePipelineHash = () => { - return this._kbnUrlService.changePath( - `/logstash/pipelines/${this.pipelineId}/${this.pipelineHash}` - ); - }; - } - - this.datePicker = { - timeRange: timefilter.getTime(), - refreshInterval: timefilter.getRefreshInterval(), - onRefreshChange: ({ isPaused, refreshInterval }, skipSet = false) => { - this.datePicker.refreshInterval = { - pause: isPaused, - value: refreshInterval, - }; - if (!skipSet) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : this.datePicker.refreshInterval.value, - }); - } - }, - onTimeUpdate: ({ dateRange }, skipSet = false) => { - this.datePicker.timeRange = { - ...dateRange, - }; - if (!skipSet) { - timefilter.setTime(dateRange); - } - this._executorService.cancel(); - this._executorService.run(); - }, - }; - } - - // check whether to "highlight" a tab - isActiveTab(testPath) { - return this.name === testPath; - } - - // check whether to show ML tab - isMlSupported() { - return this._licenseService.mlIsSupported(); - } - - isDisabledTab(product) { - const setupMode = getSetupModeState(); - if (!setupMode.enabled || !setupMode.data) { - return false; - } - - const data = setupMode.data[product] || {}; - if (data.totalUniqueInstanceCount === 0) { - return true; - } - if ( - data.totalUniqueInternallyCollectedCount === 0 && - data.totalUniqueFullyMigratedCount === 0 && - data.totalUniquePartiallyMigratedCount === 0 - ) { - return true; - } - return false; - } -} - -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringMain', (breadcrumbs, license, kbnUrl, $injector) => { - const $executor = $injector.get('$executor'); - - return { - restrict: 'E', - transclude: true, - template, - controller: MonitoringMainController, - controllerAs: 'monitoringMain', - bindToController: true, - link(scope, _element, attributes, controller) { - controller.addTimerangeObservers(); - initSetupModeState(scope, $injector, () => { - controller.setup(getSetupObj()); - }); - if (!scope.cluster) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - scope.cluster = ($route.current.locals.clusters || []).find( - cluster => cluster.cluster_uuid === globalState.cluster_uuid - ); - } - - function getSetupObj() { - return { - licenseService: license, - breadcrumbsService: breadcrumbs, - executorService: $executor, - kbnUrlService: kbnUrl, - attributes: { - name: attributes.name, - product: attributes.product, - instance: attributes.instance, - resolver: attributes.resolver, - page: attributes.page, - tabIconClass: attributes.tabIconClass, - tabIconLabel: attributes.tabIconLabel, - pipelineId: attributes.pipelineId, - pipelineHash: attributes.pipelineHash, - pipelineVersions: get(scope, 'pageData.versions'), - isCcrEnabled: attributes.isCcrEnabled === 'true' || attributes.isCcrEnabled === true, - }, - clusterName: get(scope, 'cluster.cluster_name'), - }; - } - - const setupObj = getSetupObj(); - controller.setup(setupObj); - Object.keys(setupObj.attributes).forEach(key => { - attributes.$observe(key, () => controller.setup(getSetupObj())); - }); - scope.$on('$destroy', () => { - controller.pipelineDropdownElement && - unmountComponentAtNode(controller.pipelineDropdownElement); - controller.subscriptions && controller.subscriptions.unsubscribe(); - }); - scope.$watch('pageData.versions', versions => { - controller.pipelineVersions = versions; - setOptions(controller); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/filters/index.js b/x-pack/legacy/plugins/monitoring/public/filters/index.js deleted file mode 100644 index a67770ff50dc8..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/filters/index.js +++ /dev/null @@ -1,30 +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 { capitalize } from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { formatNumber, formatMetric } from 'plugins/monitoring/lib/format_number'; -import { extractIp } from 'plugins/monitoring/lib/extract_ip'; - -const uiModule = uiModules.get('monitoring/filters', []); - -uiModule.filter('capitalize', function() { - return function(input) { - return capitalize(input.toLowerCase()); - }; -}); - -uiModule.filter('formatNumber', function() { - return formatNumber; -}); - -uiModule.filter('formatMetric', function() { - return formatMetric; -}); - -uiModule.filter('extractIp', function() { - return extractIp; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/hacks/__tests__/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/monitoring/public/hacks/__tests__/toggle_app_link_in_nav.js deleted file mode 100644 index 6ed3371486740..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/hacks/__tests__/toggle_app_link_in_nav.js +++ /dev/null @@ -1,9 +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 { uiModules } from 'ui/modules'; - -uiModules.get('kibana').constant('monitoringUiEnabled', true); diff --git a/x-pack/legacy/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js deleted file mode 100644 index 9448e826f3723..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/hacks/toggle_app_link_in_nav.js +++ /dev/null @@ -1,16 +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 { uiModules } from 'ui/modules'; -import { npStart } from 'ui/new_platform'; - -uiModules.get('monitoring/hacks').run(monitoringUiEnabled => { - if (monitoringUiEnabled) { - return; - } - - npStart.core.chrome.navLinks.update('monitoring', { hidden: true }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/icons/monitoring.svg b/x-pack/legacy/plugins/monitoring/public/icons/monitoring.svg deleted file mode 100644 index e00faca26a251..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/icons/monitoring.svg +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 21.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - width="20px" height="20px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> -<style type="text/css"> - .st0{fill:#FFFFFF;} -</style> -<g> - <path class="st0" d="M1.9,12.4h2.8l1.6-3c0.1-0.2,0.3-0.3,0.5-0.3c0.2,0,0.4,0.1,0.4,0.3l1.5,4.2l2.5-7.8c0.1-0.2,0.2-0.3,0.5-0.3 - c0.3,0,0.4,0.1,0.5,0.3l2.6,6.5h3.2l0.2-0.2c2.3-2.3,2.2-6-0.1-8.3C16,1.6,12.3,1.5,10,3.7c-2.3-2.3-6-2.3-8.3,0 - c-2.3,2.3-2.3,6.1,0,8.5L1.9,12.4z"/> - <path class="st0" d="M14.5,13.4c-0.2,0-0.4-0.1-0.5-0.3l-2.2-5.6l-2.6,7.9c-0.1,0.2-0.3,0.3-0.5,0.3c0,0,0,0,0,0 - c-0.2,0-0.4-0.1-0.5-0.3l-1.6-4.5l-1.2,2.3c-0.1,0.2-0.3,0.3-0.4,0.3H2.9l5.8,5.9c0,0,0,0,0,0C9,19.7,9.4,19.9,9.7,20c0,0,0,0,0,0 - c0.1,0,0.1,0,0.2,0c0.1,0,0.1,0,0.2,0c0,0,0,0,0,0c0.3,0,0.6-0.2,0.9-0.4l6-6.1H14.5z"/> -</g> -</svg> diff --git a/x-pack/legacy/plugins/monitoring/public/index.scss b/x-pack/legacy/plugins/monitoring/public/index.scss deleted file mode 100644 index 41bca7774a8b8..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/index.scss +++ /dev/null @@ -1,23 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Temporary hacks -@import 'hacks'; - -// Monitoring plugin styles - -// Prefix all styles with "mon" to avoid conflicts. -// Examples -// monChart -// monChart__legend -// monChart__legend--small -// monChart__legend-isLoading - -@import 'components/chart/index'; -@import 'components/no_data/index'; -@import 'components/sparkline/index'; -@import 'components/summary_status/index'; -@import 'components/table/index'; -@import 'components/logstash/pipeline_viewer/views/index'; -@import 'components/elasticsearch/shard_allocation/index'; -@import 'components/setup_mode/index'; diff --git a/x-pack/legacy/plugins/monitoring/public/legacy.ts b/x-pack/legacy/plugins/monitoring/public/legacy.ts deleted file mode 100644 index 293b6ac7bd821..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/legacy.ts +++ /dev/null @@ -1,27 +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 'plugins/monitoring/filters'; -import 'plugins/monitoring/services/clusters'; -import 'plugins/monitoring/services/features'; -import 'plugins/monitoring/services/executor'; -import 'plugins/monitoring/services/license'; -import 'plugins/monitoring/services/title'; -import 'plugins/monitoring/services/breadcrumbs'; -import 'plugins/monitoring/directives/all'; -import 'plugins/monitoring/views/all'; -import { npSetup, npStart } from '../public/np_imports/legacy_imports'; -import { plugin } from './np_ready'; -import { localApplicationService } from '../../../../../src/legacy/core_plugins/kibana/public/local_application_service'; - -const pluginInstance = plugin({} as any); -pluginInstance.setup(npSetup.core, npSetup.plugins); -pluginInstance.start(npStart.core, { - ...npStart.plugins, - __LEGACY: { - localApplicationService, - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/lib/get_page_data.js deleted file mode 100644 index ae04b2d8791fa..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/lib/get_page_data.js +++ /dev/null @@ -1,29 +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 { ajaxErrorHandlersProvider } from './ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector, api) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); - - return $http - .post(api, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js deleted file mode 100644 index 765909f0aa251..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ /dev/null @@ -1,309 +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 { - coreMock, - overlayServiceMock, - notificationServiceMock, -} from '../../../../../../src/core/public/mocks'; - -let toggleSetupMode; -let initSetupModeState; -let getSetupModeState; -let updateSetupModeData; -let setSetupModeMenuItem; - -jest.mock('./ajax_error_handler', () => ({ - ajaxErrorHandlersProvider: err => { - throw err; - }, -})); - -jest.mock('react-dom', () => ({ - render: jest.fn(), -})); - -let data = {}; - -const injectorModulesMock = { - globalState: { - save: jest.fn(), - }, - Private: module => module, - $http: { - post: jest.fn().mockImplementation(() => { - return { data }; - }), - }, - $executor: { - run: jest.fn(), - }, -}; - -const angularStateMock = { - injector: { - get: module => { - return injectorModulesMock[module] || {}; - }, - }, - scope: { - $apply: fn => fn && fn(), - $evalAsync: fn => fn && fn(), - }, -}; - -// We are no longer waiting for setup mode data to be fetched when enabling -// so we need to wait for the next tick for the async action to finish -function waitForSetupModeData(action) { - process.nextTick(action); -} - -function mockFilterManager() { - let subscriber; - let filters = []; - return { - getUpdates$: () => ({ - subscribe: ({ next }) => { - subscriber = next; - return jest.fn(); - }, - }), - setFilters: newFilters => { - filters = newFilters; - subscriber(); - }, - getFilters: () => filters, - removeAll: () => { - filters = []; - subscriber(); - }, - }; -} - -const pluginData = { - query: { - filterManager: mockFilterManager(), - timefilter: { - timefilter: { - getTime: jest.fn(() => ({ from: 'now-1h', to: 'now' })), - setTime: jest.fn(), - }, - }, - }, -}; - -function setModulesAndMocks(isOnCloud = false) { - jest.clearAllMocks().resetModules(); - injectorModulesMock.globalState.inSetupMode = false; - - jest.doMock('ui/new_platform', () => ({ - npSetup: { - plugins: { - cloud: isOnCloud ? { cloudId: 'test', isCloudEnabled: true } : {}, - uiActions: { - registerAction: jest.fn(), - attachAction: jest.fn(), - }, - }, - core: { - ...coreMock.createSetup(), - notifications: notificationServiceMock.createStartContract(), - }, - }, - npStart: { - plugins: { - data: pluginData, - navigation: { ui: {} }, - }, - core: { - ...coreMock.createStart(), - overlays: overlayServiceMock.createStartContract(), - }, - }, - })); - - const setupMode = require('./setup_mode'); - toggleSetupMode = setupMode.toggleSetupMode; - initSetupModeState = setupMode.initSetupModeState; - getSetupModeState = setupMode.getSetupModeState; - updateSetupModeData = setupMode.updateSetupModeData; - setSetupModeMenuItem = setupMode.setSetupModeMenuItem; -} - -describe('setup_mode', () => { - beforeEach(async () => { - setModulesAndMocks(); - }); - - describe('setup', () => { - it('should require angular state', async () => { - let error; - try { - toggleSetupMode(true); - } catch (err) { - error = err; - } - expect(error.message).toEqual( - 'Unable to interact with setup ' + - 'mode because the angular injector was not previously set. This needs to be ' + - 'set by calling `initSetupModeState`.' - ); - }); - - it('should enable toggle mode', async () => { - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - expect(injectorModulesMock.globalState.inSetupMode).toBe(true); - }); - - it('should disable toggle mode', async () => { - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(false); - expect(injectorModulesMock.globalState.inSetupMode).toBe(false); - }); - - it('should set top nav config', async () => { - const render = require('react-dom').render; - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - setSetupModeMenuItem(); - await toggleSetupMode(true); - expect(render.mock.calls.length).toBe(2); - }); - }); - - describe('in setup mode', () => { - afterEach(async () => { - data = {}; - }); - - it('should not fetch data if on cloud', async done => { - const addDanger = jest.fn(); - data = { - _meta: { - hasPermissions: true, - }, - }; - jest.doMock('ui/notify', () => ({ - toastNotifications: { - addDanger, - }, - })); - setModulesAndMocks(true); - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - waitForSetupModeData(() => { - const state = getSetupModeState(); - expect(state.enabled).toBe(false); - expect(addDanger).toHaveBeenCalledWith({ - title: 'Setup mode is not available', - text: 'This feature is not available on cloud.', - }); - done(); - }); - }); - - it('should not fetch data if the user does not have sufficient permissions', async done => { - const addDanger = jest.fn(); - jest.doMock('ui/notify', () => ({ - toastNotifications: { - addDanger, - }, - })); - data = { - _meta: { - hasPermissions: false, - }, - }; - setModulesAndMocks(); - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - waitForSetupModeData(() => { - const state = getSetupModeState(); - expect(state.enabled).toBe(false); - expect(addDanger).toHaveBeenCalledWith({ - title: 'Setup mode is not available', - text: 'You do not have the necessary permissions to do this.', - }); - done(); - }); - }); - - it('should set the newly discovered cluster uuid', async done => { - const clusterUuid = '1ajy'; - data = { - _meta: { - liveClusterUuid: clusterUuid, - hasPermissions: true, - }, - elasticsearch: { - byUuid: { - 123: { - isPartiallyMigrated: true, - }, - }, - }, - }; - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - waitForSetupModeData(() => { - expect(injectorModulesMock.globalState.cluster_uuid).toBe(clusterUuid); - done(); - }); - }); - - it('should fetch data for a given cluster', async done => { - const clusterUuid = '1ajy'; - data = { - _meta: { - liveClusterUuid: clusterUuid, - hasPermissions: true, - }, - elasticsearch: { - byUuid: { - 123: { - isPartiallyMigrated: true, - }, - }, - }, - }; - - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - waitForSetupModeData(() => { - expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( - `../api/monitoring/v1/setup/collection/cluster/${clusterUuid}`, - { - ccs: undefined, - } - ); - done(); - }); - }); - - it('should fetch data for a single node', async () => { - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - injectorModulesMock.$http.post.mockClear(); - await updateSetupModeData('45asd'); - expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( - '../api/monitoring/v1/setup/collection/node/45asd', - { - ccs: undefined, - } - ); - }); - - it('should fetch data without a cluster uuid', async () => { - initSetupModeState(angularStateMock.scope, angularStateMock.injector); - await toggleSetupMode(true); - injectorModulesMock.$http.post.mockClear(); - await updateSetupModeData(undefined, true); - const url = '../api/monitoring/v1/setup/collection/cluster'; - const args = { ccs: undefined }; - expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args); - }); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/angular_config.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/angular_config.ts deleted file mode 100644 index d1849d9247985..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/angular_config.ts +++ /dev/null @@ -1,157 +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 { - ICompileProvider, - IHttpProvider, - IHttpService, - ILocationProvider, - IModule, - IRootScopeService, -} from 'angular'; -import $ from 'jquery'; -import _, { cloneDeep, forOwn, get, set } from 'lodash'; -import * as Rx from 'rxjs'; -import { CoreStart, LegacyCoreStart } from 'kibana/public'; - -const isSystemApiRequest = (request: any) => - Boolean(request && request.headers && !!request.headers['kbn-system-api']); - -export const configureAppAngularModule = (angularModule: IModule, newPlatform: LegacyCoreStart) => { - const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata(); - - forOwn(newPlatform.injectedMetadata.getInjectedVars(), (val, name) => { - if (name !== undefined) { - // The legacy platform modifies some of these values, clone to an unfrozen object. - angularModule.value(name, cloneDeep(val)); - } - }); - - angularModule - .value('kbnVersion', newPlatform.injectedMetadata.getKibanaVersion()) - .value('buildNum', legacyMetadata.buildNum) - .value('buildSha', legacyMetadata.buildSha) - .value('serverName', legacyMetadata.serverName) - .value('esUrl', getEsUrl(newPlatform)) - .value('uiCapabilities', newPlatform.application.capabilities) - .config(setupCompileProvider(newPlatform)) - .config(setupLocationProvider()) - .config($setupXsrfRequestInterceptor(newPlatform)) - .run(capture$httpLoadingCount(newPlatform)) - .run($setupUICapabilityRedirect(newPlatform)); -}; - -const getEsUrl = (newPlatform: CoreStart) => { - const a = document.createElement('a'); - a.href = newPlatform.http.basePath.prepend('/elasticsearch'); - const protocolPort = /https/.test(a.protocol) ? 443 : 80; - const port = a.port || protocolPort; - return { - host: a.hostname, - port, - protocol: a.protocol, - pathname: a.pathname, - }; -}; - -const setupCompileProvider = (newPlatform: LegacyCoreStart) => ( - $compileProvider: ICompileProvider -) => { - if (!newPlatform.injectedMetadata.getLegacyMetadata().devMode) { - $compileProvider.debugInfoEnabled(false); - } -}; - -const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); - - $locationProvider.hashPrefix(''); -}; - -const $setupXsrfRequestInterceptor = (newPlatform: LegacyCoreStart) => { - const version = newPlatform.injectedMetadata.getLegacyMetadata().version; - - // Configure jQuery prefilter - $.ajaxPrefilter(({ kbnXsrfToken = true }: any, originalOptions, jqXHR) => { - if (kbnXsrfToken) { - jqXHR.setRequestHeader('kbn-version', version); - } - }); - - return ($httpProvider: IHttpProvider) => { - // Configure $httpProvider interceptor - $httpProvider.interceptors.push(() => { - return { - request(opts) { - const { kbnXsrfToken = true } = opts as any; - if (kbnXsrfToken) { - set(opts, ['headers', 'kbn-version'], version); - } - return opts; - }, - }; - }); - }; -}; - -/** - * Injected into angular module by ui/chrome angular integration - * and adds a root-level watcher that will capture the count of - * active $http requests on each digest loop and expose the count to - * the core.loadingCount api - * @param {Angular.Scope} $rootScope - * @param {HttpService} $http - * @return {undefined} - */ -const capture$httpLoadingCount = (newPlatform: CoreStart) => ( - $rootScope: IRootScopeService, - $http: IHttpService -) => { - newPlatform.http.addLoadingCountSource( - new Rx.Observable(observer => { - const unwatch = $rootScope.$watch(() => { - const reqs = $http.pendingRequests || []; - observer.next(reqs.filter(req => !isSystemApiRequest(req)).length); - }); - - return unwatch; - }) - ); -}; - -/** - * integrates with angular to automatically redirect to home if required - * capability is not met - */ -const $setupUICapabilityRedirect = (newPlatform: CoreStart) => ( - $rootScope: IRootScopeService, - $injector: any -) => { - const isKibanaAppRoute = window.location.pathname.endsWith('/app/kibana'); - // this feature only works within kibana app for now after everything is - // switched to the application service, this can be changed to handle all - // apps. - if (!isKibanaAppRoute) { - return; - } - $rootScope.$on( - '$routeChangeStart', - (event, { $$route: route }: { $$route?: { requireUICapability: boolean } } = {}) => { - if (!route || !route.requireUICapability) { - return; - } - - if (!get(newPlatform.application.capabilities, route.requireUICapability)) { - $injector.get('kbnUrl').change('/home'); - event.preventDefault(); - } - } - ); -}; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/index.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/index.ts deleted file mode 100644 index 8fd8d170bbb40..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/index.ts +++ /dev/null @@ -1,48 +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 angular, { IModule } from 'angular'; - -import { AppMountContext, LegacyCoreStart } from 'kibana/public'; - -// @ts-ignore TODO: change to absolute path -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -// @ts-ignore TODO: change to absolute path -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; -// @ts-ignore TODO: change to absolute path -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -// @ts-ignore TODO: change to absolute path -import { registerTimefilterWithGlobalState } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { configureAppAngularModule } from './angular_config'; - -import { localAppModule, appModuleName } from './modules'; - -export class AngularApp { - private injector?: angular.auto.IInjectorService; - - constructor({ core }: AppMountContext, { element }: { element: HTMLElement }) { - uiModules.addToModule(); - const app: IModule = localAppModule(core); - app.config(($routeProvider: any) => { - $routeProvider.eagerInstantiationEnabled(false); - uiRoutes.addToProvider($routeProvider); - }); - configureAppAngularModule(app, core as LegacyCoreStart); - registerTimefilterWithGlobalState(app); - const appElement = document.createElement('div'); - appElement.setAttribute('style', 'height: 100%'); - appElement.innerHTML = '<div ng-view style="height: 100%" id="monitoring-angular-app"></div>'; - this.injector = angular.bootstrap(appElement, [appModuleName]); - chrome.setInjector(this.injector); - angular.element(element).append(appElement); - } - - public destroy = () => { - if (this.injector) { - this.injector.get('$rootScope').$destroy(); - } - }; -} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts deleted file mode 100644 index b506784bf15ee..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/modules.ts +++ /dev/null @@ -1,144 +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 angular, { IWindowService } from 'angular'; -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; - -import { AppMountContext } from 'kibana/public'; -import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; -import { - createTopNavDirective, - createTopNavHelper, -} from '../../../../../../../src/plugins/kibana_legacy/public'; - -import { - GlobalStateProvider, - StateManagementConfigProvider, - AppStateProvider, - KbnUrlProvider, - npStart, -} from '../legacy_imports'; - -// @ts-ignore -import { PromiseServiceCreator } from './providers/promises'; -// @ts-ignore -import { PrivateProvider } from './providers/private'; - -type IPrivate = <T>(provider: (...injectable: any[]) => T) => T; - -export const appModuleName = 'monitoring'; -const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react']; - -export const localAppModule = (core: AppMountContext['core']) => { - createLocalI18nModule(); - createLocalPrivateModule(); - createLocalPromiseModule(); - createLocalStorage(); - createLocalConfigModule(core); - createLocalKbnUrlModule(); - createLocalStateModule(); - createLocalTopNavModule(npStart.plugins.navigation); - createHrefModule(core); - - const appModule = angular.module(appModuleName, [ - ...thirdPartyAngularDependencies, - 'monitoring/Config', - 'monitoring/I18n', - 'monitoring/Private', - 'monitoring/TopNav', - 'monitoring/State', - 'monitoring/Storage', - 'monitoring/href', - 'monitoring/services', - 'monitoring/filters', - 'monitoring/directives', - ]); - return appModule; -}; - -function createLocalStateModule() { - angular - .module('monitoring/State', [ - 'monitoring/Private', - 'monitoring/Config', - 'monitoring/KbnUrl', - 'monitoring/Promise', - ]) - .factory('AppState', function(Private: IPrivate) { - return Private(AppStateProvider); - }) - .service('globalState', function(Private: IPrivate) { - return Private(GlobalStateProvider); - }); -} - -function createLocalKbnUrlModule() { - angular - .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute']) - .service('kbnUrl', (Private: IPrivate) => Private(KbnUrlProvider)); -} - -function createLocalConfigModule(core: AppMountContext['core']) { - angular - .module('monitoring/Config', ['monitoring/Private']) - .provider('stateManagementConfig', StateManagementConfigProvider) - .provider('config', () => { - return { - $get: () => ({ - get: core.uiSettings.get.bind(core.uiSettings), - }), - }; - }); -} - -function createLocalPromiseModule() { - angular.module('monitoring/Promise', []).service('Promise', PromiseServiceCreator); -} - -function createLocalStorage() { - angular - .module('monitoring/Storage', []) - .service('localStorage', ($window: IWindowService) => new Storage($window.localStorage)) - .service('sessionStorage', ($window: IWindowService) => new Storage($window.sessionStorage)) - .service('sessionTimeout', () => {}); -} - -function createLocalPrivateModule() { - angular.module('monitoring/Private', []).provider('Private', PrivateProvider); -} - -function createLocalTopNavModule({ ui }: any) { - angular - .module('monitoring/TopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(ui)); -} - -function createLocalI18nModule() { - angular - .module('monitoring/I18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} - -function createHrefModule(core: AppMountContext['core']) { - const name: string = 'kbnHref'; - angular.module('monitoring/href', []).directive(name, () => { - return { - restrict: 'A', - link: { - pre: (_$scope, _$el, $attr) => { - $attr.$observe(name, val => { - if (val) { - $attr.$set('href', core.http.basePath.prepend(val as string)); - } - }); - }, - }, - }; - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/promises.js b/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/promises.js deleted file mode 100644 index 22adccaf3db7f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/promises.js +++ /dev/null @@ -1,116 +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 _ from 'lodash'; - -export function PromiseServiceCreator($q, $timeout) { - function Promise(fn) { - if (typeof this === 'undefined') - throw new Error('Promise constructor must be called with "new"'); - - const defer = $q.defer(); - try { - fn(defer.resolve, defer.reject); - } catch (e) { - defer.reject(e); - } - return defer.promise; - } - - Promise.all = Promise.props = $q.all; - Promise.resolve = function(val) { - const defer = $q.defer(); - defer.resolve(val); - return defer.promise; - }; - Promise.reject = function(reason) { - const defer = $q.defer(); - defer.reject(reason); - return defer.promise; - }; - Promise.cast = $q.when; - Promise.delay = function(ms) { - return $timeout(_.noop, ms); - }; - Promise.method = function(fn) { - return function() { - const args = Array.prototype.slice.call(arguments); - return Promise.try(fn, args, this); - }; - }; - Promise.nodeify = function(promise, cb) { - promise.then(function(val) { - cb(void 0, val); - }, cb); - }; - Promise.map = function(arr, fn) { - return Promise.all( - arr.map(function(i, el, list) { - return Promise.try(fn, [i, el, list]); - }) - ); - }; - Promise.each = function(arr, fn) { - const queue = arr.slice(0); - let i = 0; - return (function next() { - if (!queue.length) return arr; - return Promise.try(fn, [arr.shift(), i++]).then(next); - })(); - }; - Promise.is = function(obj) { - // $q doesn't create instances of any constructor, promises are just objects with a then function - // https://github.com/angular/angular.js/blob/58f5da86645990ef984353418cd1ed83213b111e/src/ng/q.js#L335 - return obj && typeof obj.then === 'function'; - }; - Promise.halt = _.once(function() { - const promise = new Promise(() => {}); - promise.then = _.constant(promise); - promise.catch = _.constant(promise); - return promise; - }); - Promise.try = function(fn, args, ctx) { - if (typeof fn !== 'function') { - return Promise.reject(new TypeError('fn must be a function')); - } - - let value; - - if (Array.isArray(args)) { - try { - value = fn.apply(ctx, args); - } catch (e) { - return Promise.reject(e); - } - } else { - try { - value = fn.call(ctx, args); - } catch (e) { - return Promise.reject(e); - } - } - - return Promise.resolve(value); - }; - Promise.fromNode = function(takesCbFn) { - return new Promise(function(resolve, reject) { - takesCbFn(function(err, ...results) { - if (err) reject(err); - else if (results.length > 1) resolve(results); - else resolve(results[0]); - }); - }); - }; - Promise.race = function(iterable) { - return new Promise((resolve, reject) => { - for (const i of iterable) { - Promise.resolve(i).then(resolve, reject); - } - }); - }; - - return Promise; -} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts deleted file mode 100644 index 208b7e2acdb0f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/legacy_imports.ts +++ /dev/null @@ -1,22 +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. - */ - -/** - * Last remaining 'ui/*' imports that will eventually be shimmed with their np alternatives - */ - -export { npSetup, npStart } from 'ui/new_platform'; -// @ts-ignore -export { GlobalStateProvider } from 'ui/state_management/global_state'; -// @ts-ignore -export { StateManagementConfigProvider } from 'ui/state_management/config_provider'; -// @ts-ignore -export { AppStateProvider } from 'ui/state_management/app_state'; -// @ts-ignore -export { EventsProvider } from 'ui/events'; -// @ts-ignore -export { KbnUrlProvider } from 'ui/url'; -export { registerTimefilterWithGlobalStateFactory } from 'ui/timefilter/setup_router'; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/capabilities.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/capabilities.ts deleted file mode 100644 index 5aff302501401..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/capabilities.ts +++ /dev/null @@ -1,8 +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 { npStart } from '../legacy_imports'; -export const capabilities = { get: () => npStart.core.application.capabilities }; diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/chrome.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/chrome.ts deleted file mode 100644 index f0c5bacabecbf..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/chrome.ts +++ /dev/null @@ -1,33 +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 angular from 'angular'; -import { npStart, npSetup } from '../legacy_imports'; - -type OptionalInjector = void | angular.auto.IInjectorService; - -class Chrome { - private injector?: OptionalInjector; - - public setInjector = (injector: OptionalInjector): void => void (this.injector = injector); - public dangerouslyGetActiveInjector = (): OptionalInjector => this.injector; - - public getBasePath = (): string => npStart.core.http.basePath.get(); - - public getInjected = (name?: string, defaultValue?: any): string | unknown => { - const { getInjectedVar, getInjectedVars } = npSetup.core.injectedMetadata; - return name ? getInjectedVar(name, defaultValue) : getInjectedVars(); - }; - - public get breadcrumbs() { - const set = (...args: any[]) => npStart.core.chrome.setBreadcrumbs.apply(this, args as any); - return { set }; - } -} - -const chrome = new Chrome(); - -export default chrome; // eslint-disable-line import/no-default-export diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/modules.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/modules.ts deleted file mode 100644 index 70201a7906110..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/modules.ts +++ /dev/null @@ -1,55 +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 angular from 'angular'; - -type PrivateProvider = (...args: any) => any; -interface Provider { - name: string; - provider: PrivateProvider; -} - -class Modules { - private _services: Provider[] = []; - private _filters: Provider[] = []; - private _directives: Provider[] = []; - - public get = (_name: string, _dep?: string[]) => { - return this; - }; - - public service = (...args: any) => { - this._services.push(args); - }; - - public filter = (...args: any) => { - this._filters.push(args); - }; - - public directive = (...args: any) => { - this._directives.push(args); - }; - - public addToModule = () => { - angular.module('monitoring/services', []); - angular.module('monitoring/filters', []); - angular.module('monitoring/directives', []); - - this._services.forEach(args => { - angular.module('monitoring/services').service.apply(null, args as any); - }); - - this._filters.forEach(args => { - angular.module('monitoring/filters').filter.apply(null, args as any); - }); - - this._directives.forEach(args => { - angular.module('monitoring/directives').directive.apply(null, args as any); - }); - }; -} - -export const uiModules = new Modules(); diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/routes.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/routes.ts deleted file mode 100644 index 22da56a8d184a..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/routes.ts +++ /dev/null @@ -1,39 +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. - */ - -type RouteObject = [string, any]; -interface Redirect { - redirectTo: string; -} - -class Routes { - private _routes: RouteObject[] = []; - private _redirect?: Redirect; - - public when = (...args: RouteObject) => { - const [, routeOptions] = args; - routeOptions.reloadOnSearch = false; - this._routes.push(args); - return this; - }; - - public otherwise = (redirect: Redirect) => { - this._redirect = redirect; - return this; - }; - - public addToProvider = ($routeProvider: any) => { - this._routes.forEach(args => { - $routeProvider.when.apply(this, args); - }); - - if (this._redirect) { - $routeProvider.otherwise(this._redirect); - } - }; -} -const uiRoutes = new Routes(); -export default uiRoutes; // eslint-disable-line import/no-default-export diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/timefilter.ts b/x-pack/legacy/plugins/monitoring/public/np_imports/ui/timefilter.ts deleted file mode 100644 index e28699bd126b9..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/timefilter.ts +++ /dev/null @@ -1,31 +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 { IModule, IRootScopeService } from 'angular'; -import { npStart, registerTimefilterWithGlobalStateFactory } from '../legacy_imports'; - -const { - core: { uiSettings }, -} = npStart; -export const { timefilter } = npStart.plugins.data.query.timefilter; - -uiSettings.overrideLocalDefault( - 'timepicker:refreshIntervalDefaults', - JSON.stringify({ value: 10000, pause: false }) -); -uiSettings.overrideLocalDefault( - 'timepicker:timeDefaults', - JSON.stringify({ from: 'now-1h', to: 'now' }) -); - -export const registerTimefilterWithGlobalState = (app: IModule) => { - app.run((globalState: any, $rootScope: IRootScopeService) => { - globalState.fetch(); - globalState.$inheritedGlobalState = true; - globalState.save(); - registerTimefilterWithGlobalStateFactory(timefilter, globalState, $rootScope); - }); -}; diff --git a/x-pack/legacy/plugins/monitoring/public/np_ready/index.ts b/x-pack/legacy/plugins/monitoring/public/np_ready/index.ts deleted file mode 100644 index 80848c497c370..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_ready/index.ts +++ /dev/null @@ -1,12 +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 { PluginInitializerContext } from 'src/core/public'; -import { MonitoringPlugin } from './plugin'; - -export function plugin(ctx: PluginInitializerContext) { - return new MonitoringPlugin(ctx); -} diff --git a/x-pack/legacy/plugins/monitoring/public/np_ready/plugin.ts b/x-pack/legacy/plugins/monitoring/public/np_ready/plugin.ts deleted file mode 100644 index 5598a7a51cf42..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/np_ready/plugin.ts +++ /dev/null @@ -1,28 +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 { App, CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; - -export class MonitoringPlugin implements Plugin { - constructor(ctx: PluginInitializerContext) {} - - public setup(core: CoreSetup, plugins: any) { - const app: App = { - id: 'monitoring', - title: 'Monitoring', - mount: async (context, params) => { - const { AngularApp } = await import('../np_imports/angular'); - const monitoringApp = new AngularApp(context, params); - return monitoringApp.destroy; - }, - }; - - core.application.register(app); - } - - public start(core: CoreStart, plugins: any) {} - public stop() {} -} diff --git a/x-pack/legacy/plugins/monitoring/public/register_feature.ts b/x-pack/legacy/plugins/monitoring/public/register_feature.ts deleted file mode 100644 index 9b72e01a19394..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/register_feature.ts +++ /dev/null @@ -1,30 +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 { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -if (chrome.getInjected('monitoringUiEnabled')) { - home.featureCatalogue.register({ - id: 'monitoring', - title: i18n.translate('xpack.monitoring.monitoringTitle', { - defaultMessage: 'Monitoring', - }), - description: i18n.translate('xpack.monitoring.monitoringDescription', { - defaultMessage: 'Track the real-time health and performance of your Elastic Stack.', - }), - icon: 'monitoringApp', - path: '/app/monitoring', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/services/__tests__/breadcrumbs.js b/x-pack/legacy/plugins/monitoring/public/services/__tests__/breadcrumbs.js deleted file mode 100644 index e5b2e01373340..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/__tests__/breadcrumbs.js +++ /dev/null @@ -1,130 +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 expect from '@kbn/expect'; -import { breadcrumbsProvider } from '../breadcrumbs_provider'; -import { MonitoringMainController } from 'plugins/monitoring/directives/main'; - -describe('Monitoring Breadcrumbs Service', () => { - it('in Cluster Alerts', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - name: 'alerts', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, - { url: '#/overview', label: 'test-cluster-foo' }, - ]); - }); - - it('in Cluster Overview', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - name: 'overview', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, - ]); - }); - - it('in ES Node - Advanced', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'elasticsearch', - name: 'nodes', - instance: 'es-node-name-01', - resolver: 'es-node-resolver-01', - page: 'advanced', - tabIconClass: 'fa star', - tabIconLabel: 'Master Node', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, - { url: '#/overview', label: 'test-cluster-foo' }, - { url: '#/elasticsearch', label: 'Elasticsearch' }, - { url: '#/elasticsearch/nodes', label: 'Nodes', testSubj: 'breadcrumbEsNodes' }, - { url: null, label: 'es-node-name-01' }, - ]); - }); - - it('in Kibana Overview', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'kibana', - name: 'overview', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, - { url: '#/overview', label: 'test-cluster-foo' }, - { url: null, label: 'Kibana' }, - ]); - }); - - /** - * <monitoring-main product="logstash" name="nodes"> - */ - it('in Logstash Listing', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'logstash', - name: 'listing', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, - { url: '#/overview', label: 'test-cluster-foo' }, - { url: null, label: 'Logstash' }, - ]); - }); - - /** - * <monitoring-main product="logstash" page="pipeline"> - */ - it('in Logstash Pipeline Viewer', () => { - const controller = new MonitoringMainController(); - controller.setup({ - clusterName: 'test-cluster-foo', - licenseService: {}, - breadcrumbsService: breadcrumbsProvider(), - attributes: { - product: 'logstash', - page: 'pipeline', - pipelineId: 'main', - pipelineHash: '42ee890af9...', - }, - }); - expect(controller.breadcrumbs).to.eql([ - { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, - { url: '#/overview', label: 'test-cluster-foo' }, - { url: '#/logstash', label: 'Logstash' }, - { url: '#/logstash/pipelines', label: 'Pipelines' }, - ]); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/services/__tests__/executor_provider.js b/x-pack/legacy/plugins/monitoring/public/services/__tests__/executor_provider.js deleted file mode 100644 index 2c4d49716406c..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/__tests__/executor_provider.js +++ /dev/null @@ -1,103 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { executorProvider } from '../executor_provider'; -import Bluebird from 'bluebird'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -describe('$executor service', () => { - let scope; - let executor; - let $timeout; - - beforeEach(ngMock.module('kibana')); - - beforeEach( - ngMock.inject(function(_$rootScope_) { - scope = _$rootScope_.$new(); - }) - ); - - beforeEach(() => { - $timeout = sinon.spy(setTimeout); - $timeout.cancel = id => clearTimeout(id); - - timefilter.setRefreshInterval({ - value: 0, - }); - - executor = executorProvider(Bluebird, $timeout); - }); - - afterEach(() => executor.destroy()); - - it('should not call $timeout if the timefilter is not paused and set to zero', () => { - executor.start(scope); - expect($timeout.callCount).to.equal(0); - }); - - it('should call $timeout if the timefilter is not paused and set to 1000ms', () => { - timefilter.setRefreshInterval({ - pause: false, - value: 1000, - }); - executor.start(scope); - expect($timeout.callCount).to.equal(1); - }); - - it('should execute function if timefilter is not paused and interval set to 1000ms', done => { - timefilter.setRefreshInterval({ - pause: false, - value: 1000, - }); - executor.register({ execute: () => Bluebird.resolve().then(() => done(), done) }); - executor.start(scope); - }); - - it('should execute function multiple times', done => { - let calls = 0; - timefilter.setRefreshInterval({ - pause: false, - value: 10, - }); - executor.register({ - execute: () => { - if (calls++ > 1) { - done(); - } - return Bluebird.resolve(); - }, - }); - executor.start(scope); - }); - - it('should call handleResponse', done => { - timefilter.setRefreshInterval({ - pause: false, - value: 10, - }); - executor.register({ - execute: () => Bluebird.resolve(), - handleResponse: () => done(), - }); - executor.start(scope); - }); - - it('should call handleError', done => { - timefilter.setRefreshInterval({ - pause: false, - value: 10, - }); - executor.register({ - execute: () => Bluebird.reject(new Error('reject test')), - handleError: () => done(), - }); - executor.start(scope); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs.js b/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs.js deleted file mode 100644 index d0fe600386307..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs.js +++ /dev/null @@ -1,10 +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 { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { breadcrumbsProvider } from './breadcrumbs_provider'; -const uiModule = uiModules.get('monitoring/breadcrumbs', []); -uiModule.service('breadcrumbs', breadcrumbsProvider); diff --git a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js b/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js deleted file mode 100644 index 7917606a5bc8e..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/breadcrumbs_provider.js +++ /dev/null @@ -1,208 +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 chrome from 'plugins/monitoring/np_imports/ui/chrome'; -import { i18n } from '@kbn/i18n'; - -// Helper for making objects to use in a link element -const createCrumb = (url, label, testSubj) => { - const crumb = { url, label }; - if (testSubj) { - crumb.testSubj = testSubj; - } - return crumb; -}; - -// generate Elasticsearch breadcrumbs -function getElasticsearchBreadcrumbs(mainInstance) { - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/elasticsearch', 'Elasticsearch')); - if (mainInstance.name === 'indices') { - breadcrumbs.push( - createCrumb( - '#/elasticsearch/indices', - i18n.translate('xpack.monitoring.breadcrumbs.es.indicesLabel', { - defaultMessage: 'Indices', - }), - 'breadcrumbEsIndices' - ) - ); - } else if (mainInstance.name === 'nodes') { - breadcrumbs.push( - createCrumb( - '#/elasticsearch/nodes', - i18n.translate('xpack.monitoring.breadcrumbs.es.nodesLabel', { defaultMessage: 'Nodes' }), - 'breadcrumbEsNodes' - ) - ); - } else if (mainInstance.name === 'ml') { - // ML Instance (for user later) - breadcrumbs.push( - createCrumb( - '#/elasticsearch/ml_jobs', - i18n.translate('xpack.monitoring.breadcrumbs.es.jobsLabel', { defaultMessage: 'Jobs' }) - ) - ); - } else if (mainInstance.name === 'ccr_shard') { - breadcrumbs.push( - createCrumb( - '#/elasticsearch/ccr', - i18n.translate('xpack.monitoring.breadcrumbs.es.ccrLabel', { defaultMessage: 'CCR' }) - ) - ); - } - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, 'Elasticsearch')); - } - return breadcrumbs; -} - -// generate Kibana breadcrumbs -function getKibanaBreadcrumbs(mainInstance) { - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/kibana', 'Kibana')); - breadcrumbs.push( - createCrumb( - '#/kibana/instances', - i18n.translate('xpack.monitoring.breadcrumbs.kibana.instancesLabel', { - defaultMessage: 'Instances', - }) - ) - ); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, 'Kibana')); - } - return breadcrumbs; -} - -// generate Logstash breadcrumbs -function getLogstashBreadcrumbs(mainInstance) { - const logstashLabel = i18n.translate('xpack.monitoring.breadcrumbs.logstashLabel', { - defaultMessage: 'Logstash', - }); - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); - if (mainInstance.name === 'nodes') { - breadcrumbs.push( - createCrumb( - '#/logstash/nodes', - i18n.translate('xpack.monitoring.breadcrumbs.logstash.nodesLabel', { - defaultMessage: 'Nodes', - }) - ) - ); - } - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else if (mainInstance.page === 'pipeline') { - breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); - breadcrumbs.push( - createCrumb( - '#/logstash/pipelines', - i18n.translate('xpack.monitoring.breadcrumbs.logstash.pipelinesLabel', { - defaultMessage: 'Pipelines', - }) - ) - ); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, logstashLabel)); - } - - return breadcrumbs; -} - -// generate Beats breadcrumbs -function getBeatsBreadcrumbs(mainInstance) { - const beatsLabel = i18n.translate('xpack.monitoring.breadcrumbs.beatsLabel', { - defaultMessage: 'Beats', - }); - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/beats', beatsLabel)); - breadcrumbs.push( - createCrumb( - '#/beats/beats', - i18n.translate('xpack.monitoring.breadcrumbs.beats.instancesLabel', { - defaultMessage: 'Instances', - }) - ) - ); - breadcrumbs.push(createCrumb(null, mainInstance.instance)); - } else { - breadcrumbs.push(createCrumb(null, beatsLabel)); - } - - return breadcrumbs; -} - -// generate Apm breadcrumbs -function getApmBreadcrumbs(mainInstance) { - const apmLabel = i18n.translate('xpack.monitoring.breadcrumbs.apmLabel', { - defaultMessage: 'APM', - }); - const breadcrumbs = []; - if (mainInstance.instance) { - breadcrumbs.push(createCrumb('#/apm', apmLabel)); - breadcrumbs.push( - createCrumb( - '#/apm/instances', - i18n.translate('xpack.monitoring.breadcrumbs.apm.instancesLabel', { - defaultMessage: 'Instances', - }) - ) - ); - } else { - // don't link to Overview when we're possibly on Overview or its sibling tabs - breadcrumbs.push(createCrumb(null, apmLabel)); - } - return breadcrumbs; -} - -export function breadcrumbsProvider() { - return function createBreadcrumbs(clusterName, mainInstance) { - const homeCrumb = i18n.translate('xpack.monitoring.breadcrumbs.clustersLabel', { - defaultMessage: 'Clusters', - }); - - let breadcrumbs = [createCrumb('#/home', homeCrumb, 'breadcrumbClusters')]; - - if (!mainInstance.inOverview && clusterName) { - breadcrumbs.push(createCrumb('#/overview', clusterName)); - } - - if (mainInstance.inElasticsearch) { - breadcrumbs = breadcrumbs.concat(getElasticsearchBreadcrumbs(mainInstance)); - } - if (mainInstance.inKibana) { - breadcrumbs = breadcrumbs.concat(getKibanaBreadcrumbs(mainInstance)); - } - if (mainInstance.inLogstash) { - breadcrumbs = breadcrumbs.concat(getLogstashBreadcrumbs(mainInstance)); - } - if (mainInstance.inBeats) { - breadcrumbs = breadcrumbs.concat(getBeatsBreadcrumbs(mainInstance)); - } - if (mainInstance.inApm) { - breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance)); - } - - chrome.breadcrumbs.set( - breadcrumbs.map(b => ({ - text: b.label, - href: b.url, - 'data-test-subj': b.testSubj, - })) - ); - - return breadcrumbs; - }; -} diff --git a/x-pack/legacy/plugins/monitoring/public/services/executor.js b/x-pack/legacy/plugins/monitoring/public/services/executor.js deleted file mode 100644 index 5004cd0238012..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/executor.js +++ /dev/null @@ -1,10 +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 { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { executorProvider } from './executor_provider'; -const uiModule = uiModules.get('monitoring/executor', []); -uiModule.service('$executor', executorProvider); diff --git a/x-pack/legacy/plugins/monitoring/public/services/executor_provider.js b/x-pack/legacy/plugins/monitoring/public/services/executor_provider.js deleted file mode 100644 index 4a0551fa5af11..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/executor_provider.js +++ /dev/null @@ -1,122 +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 { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { subscribeWithScope } from 'plugins/monitoring/np_imports/ui/utils'; -import { Subscription } from 'rxjs'; -export function executorProvider(Promise, $timeout) { - const queue = []; - const subscriptions = new Subscription(); - let executionTimer; - let ignorePaused = false; - - /** - * Resets the timer to start again - * @returns {void} - */ - function reset() { - cancel(); - start(); - } - - function killTimer() { - if (executionTimer) { - $timeout.cancel(executionTimer); - } - } - - /** - * Cancels the execution timer - * @returns {void} - */ - function cancel() { - killTimer(); - } - - /** - * Registers a service with the executor - * @param {object} service The service to register - * @returns {void} - */ - function register(service) { - queue.push(service); - } - - /** - * Stops the executor and empties the service queue - * @returns {void} - */ - function destroy() { - subscriptions.unsubscribe(); - cancel(); - ignorePaused = false; - queue.splice(0, queue.length); - } - - /** - * Runs the queue (all at once) - * @returns {Promise} a promise of all the services - */ - function run() { - const noop = () => Promise.resolve(); - return Promise.all( - queue.map(service => { - return service - .execute() - .then(service.handleResponse || noop) - .catch(service.handleError || noop); - }) - ).finally(reset); - } - - function reFetch() { - cancel(); - run(); - } - - function killIfPaused() { - if (timefilter.getRefreshInterval().pause) { - killTimer(); - } - } - - /** - * Starts the executor service if the timefilter is not paused - * @returns {void} - */ - function start() { - if ( - (ignorePaused || timefilter.getRefreshInterval().pause === false) && - timefilter.getRefreshInterval().value > 0 - ) { - executionTimer = $timeout(run, timefilter.getRefreshInterval().value); - } - } - - /** - * Expose the methods - */ - return { - register, - start($scope) { - subscriptions.add( - subscribeWithScope($scope, timefilter.getFetch$(), { - next: reFetch, - }) - ); - subscriptions.add( - subscribeWithScope($scope, timefilter.getRefreshIntervalUpdate$(), { - next: killIfPaused, - }) - ); - start(); - }, - run, - destroy, - reset, - cancel, - }; -} diff --git a/x-pack/legacy/plugins/monitoring/public/services/title.js b/x-pack/legacy/plugins/monitoring/public/services/title.js deleted file mode 100644 index 442f4fb5b4029..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/services/title.js +++ /dev/null @@ -1,26 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { docTitle } from 'ui/doc_title'; - -const uiModule = uiModules.get('monitoring/title', []); -uiModule.service('title', () => { - return function changeTitle(cluster, suffix) { - let clusterName = _.get(cluster, 'cluster_name'); - clusterName = clusterName ? `- ${clusterName}` : ''; - suffix = suffix ? `- ${suffix}` : ''; - docTitle.change( - i18n.translate('xpack.monitoring.stackMonitoringDocTitle', { - defaultMessage: 'Stack Monitoring {clusterName} {suffix}', - values: { clusterName, suffix }, - }), - true - ); - }; -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/__tests__/base_controller.js b/x-pack/legacy/plugins/monitoring/public/views/__tests__/base_controller.js deleted file mode 100644 index 6c3c73a35601c..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/__tests__/base_controller.js +++ /dev/null @@ -1,204 +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 { spy, stub } from 'sinon'; -import expect from '@kbn/expect'; -import { MonitoringViewBaseController } from '../'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { PromiseWithCancel, Status } from '../../../common/cancel_promise'; - -/* - * Mostly copied from base_table_controller test, with modifications - */ -describe('MonitoringViewBaseController', function() { - let ctrl; - let $injector; - let $scope; - let opts; - let titleService; - let executorService; - let configService; - const httpCall = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); - - before(() => { - titleService = spy(); - executorService = { - register: spy(), - start: spy(), - cancel: spy(), - run: spy(), - }; - configService = { - get: spy(), - }; - - const windowMock = () => { - const events = {}; - const targetEvent = 'popstate'; - return { - removeEventListener: stub(), - addEventListener: (name, handler) => name === targetEvent && (events[name] = handler), - history: { - back: () => events[targetEvent] && events[targetEvent](), - }, - }; - }; - - const injectorGetStub = stub(); - injectorGetStub.withArgs('title').returns(titleService); - injectorGetStub.withArgs('$executor').returns(executorService); - injectorGetStub - .withArgs('localStorage') - .throws('localStorage should not be used by this class'); - injectorGetStub.withArgs('$window').returns(windowMock()); - injectorGetStub.withArgs('config').returns(configService); - $injector = { get: injectorGetStub }; - - $scope = { - cluster: { cluster_uuid: 'foo' }, - $on: stub(), - $apply: stub(), - }; - - opts = { - title: 'testo', - getPageData: () => Promise.resolve({ data: { test: true } }), - $injector, - $scope, - }; - - ctrl = new MonitoringViewBaseController(opts); - }); - - it('show/hide zoom-out button based on interaction', done => { - const xaxis = { from: 1562089923880, to: 1562090159676 }; - const timeRange = { xaxis }; - const { zoomInfo } = ctrl; - - ctrl.onBrush(timeRange); - - expect(zoomInfo.showZoomOutBtn()).to.be(true); - - /* - Need to do this async, since we are delaying event adding - */ - setTimeout(() => { - zoomInfo.zoomOutHandler(); - expect(zoomInfo.showZoomOutBtn()).to.be(false); - done(); - }, 15); - }); - - it('creates functions for fetching data', () => { - expect(ctrl.updateData).to.be.a('function'); - expect(ctrl.onBrush).to.be.a('function'); - }); - - it('sets page title', () => { - expect(titleService.calledOnce).to.be(true); - const { args } = titleService.getCall(0); - expect(args).to.eql([{ cluster_uuid: 'foo' }, 'testo']); - }); - - it('starts data poller', () => { - expect(executorService.register.calledOnce).to.be(true); - expect(executorService.start.calledOnce).to.be(true); - }); - - it('does not allow for a new request if one is inflight', done => { - let counter = 0; - const opts = { - title: 'testo', - getPageData: ms => httpCall(ms), - $injector, - $scope, - }; - - const ctrl = new MonitoringViewBaseController(opts); - ctrl.updateData(30).then(() => ++counter); - ctrl.updateData(60).then(() => { - expect(counter).to.be(0); - done(); - }); - }); - - describe('time filter', () => { - it('enables timepicker and auto refresh #1', () => { - expect(timefilter.isTimeRangeSelectorEnabled()).to.be(true); - expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(true); - }); - - it('enables timepicker and auto refresh #2', () => { - opts = { - ...opts, - options: {}, - }; - ctrl = new MonitoringViewBaseController(opts); - - expect(timefilter.isTimeRangeSelectorEnabled()).to.be(true); - expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(true); - }); - - it('disables timepicker and enables auto refresh', () => { - opts = { - ...opts, - options: { enableTimeFilter: false }, - }; - ctrl = new MonitoringViewBaseController(opts); - - expect(timefilter.isTimeRangeSelectorEnabled()).to.be(false); - expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(true); - }); - - it('enables timepicker and disables auto refresh', () => { - opts = { - ...opts, - options: { enableAutoRefresh: false }, - }; - ctrl = new MonitoringViewBaseController(opts); - - expect(timefilter.isTimeRangeSelectorEnabled()).to.be(true); - expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(false); - }); - - it('disables timepicker and auto refresh', () => { - opts = { - ...opts, - options: { - enableTimeFilter: false, - enableAutoRefresh: false, - }, - }; - ctrl = new MonitoringViewBaseController(opts); - - expect(timefilter.isTimeRangeSelectorEnabled()).to.be(false); - expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(false); - }); - - it('disables timepicker and auto refresh', done => { - opts = { - title: 'test', - getPageData: () => httpCall(60), - $injector, - $scope, - }; - - ctrl = new MonitoringViewBaseController({ ...opts }); - ctrl.updateDataPromise = new PromiseWithCancel(httpCall(50)); - - let shouldBeFalse = false; - ctrl.updateDataPromise.promise().then(() => (shouldBeFalse = true)); - - const lastUpdateDataPromise = ctrl.updateDataPromise; - - ctrl.updateData().then(() => { - expect(shouldBeFalse).to.be(false); - expect(lastUpdateDataPromise.status()).to.be(Status.Canceled); - done(); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.js b/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.js deleted file mode 100644 index a0cfc79f001ca..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.js +++ /dev/null @@ -1,50 +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 { noop } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import uiChrome from 'plugins/monitoring/np_imports/ui/chrome'; -import template from './index.html'; - -const tryPrivilege = ($http, kbnUrl) => { - return $http - .get('../api/monitoring/v1/check_access') - .then(() => kbnUrl.redirect('/home')) - .catch(noop); -}; - -uiRoutes.when('/access-denied', { - template, - resolve: { - /* - * The user may have been granted privileges in between leaving Monitoring - * and before coming back to Monitoring. That means, they just be on this - * page because Kibana remembers the "last app URL". We check for the - * privilege one time up front (doing it in the resolve makes it happen - * before the template renders), and then keep retrying every 5 seconds. - */ - initialCheck($http, kbnUrl) { - return tryPrivilege($http, kbnUrl); - }, - }, - controllerAs: 'accessDenied', - controller($scope, $injector) { - const $window = $injector.get('$window'); - const kbnBaseUrl = $injector.get('kbnBaseUrl'); - const $http = $injector.get('$http'); - const kbnUrl = $injector.get('kbnUrl'); - const $interval = $injector.get('$interval'); - - // The template's "Back to Kibana" button click handler - this.goToKibana = () => { - $window.location.href = uiChrome.getBasePath() + kbnBaseUrl; - }; - - // keep trying to load data in the background - const accessPoller = $interval(() => tryPrivilege($http, kbnUrl), 5 * 1000); // every 5 seconds - $scope.$on('$destroy', () => $interval.cancel(accessPoller)); - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js deleted file mode 100644 index 62cc985887e9f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ /dev/null @@ -1,131 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { render } from 'react-dom'; -import { find, get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import template from './index.html'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { I18nContext } from 'ui/i18n'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { Alerts } from '../../components/alerts'; -import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui'; -import { CODE_PATH_ALERTS, KIBANA_ALERTING_ENABLED } from '../../../common/constants'; - -function getPageData($injector) { - const globalState = $injector.get('globalState'); - const $http = $injector.get('$http'); - const Private = $injector.get('Private'); - const url = KIBANA_ALERTING_ENABLED - ? `../api/monitoring/v1/alert_status` - : `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; - - const timeBounds = timefilter.getBounds(); - const data = { - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }; - - if (!KIBANA_ALERTING_ENABLED) { - data.ccs = globalState.ccs; - } - - return $http - .post(url, data) - .then(response => { - const result = get(response, 'data', []); - if (KIBANA_ALERTING_ENABLED) { - return result.alerts; - } - return result; - }) - .catch(err => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/alerts', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ALERTS] }); - }, - alerts: getPageData, - }, - controllerAs: 'alerts', - controller: class AlertsView extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const kbnUrl = $injector.get('kbnUrl'); - - // breadcrumbs + page title - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.alerts.clusterAlertsTitle', { - defaultMessage: 'Cluster Alerts', - }), - getPageData, - $scope, - $injector, - storageKey: 'alertsTable', - reactNodeId: 'monitoringAlertsApp', - }); - - this.data = $route.current.locals.alerts; - - const renderReact = data => { - const app = data.message ? ( - <p>{data.message}</p> - ) : ( - <Alerts - alerts={data} - angular={{ kbnUrl, scope: $scope }} - sorting={this.sorting} - pagination={this.pagination} - onTableChange={this.onTableChange} - /> - ); - - render( - <I18nContext> - <EuiPage> - <EuiPageBody> - <EuiPageContent> - {app} - <EuiSpacer size="m" /> - <EuiLink href="#/overview"> - <FormattedMessage - id="xpack.monitoring.alerts.clusterOverviewLinkLabel" - defaultMessage="« Cluster Overview" - /> - </EuiLink> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </I18nContext>, - document.getElementById('monitoringAlertsApp') - ); - }; - $scope.$watch( - () => this.data, - data => renderReact(data) - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/all.js b/x-pack/legacy/plugins/monitoring/public/views/all.js deleted file mode 100644 index ded378b050c2d..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/all.js +++ /dev/null @@ -1,39 +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 './loading'; -import './no_data'; -import './access_denied'; -import './alerts'; -import './license'; -import './cluster/listing'; -import './cluster/overview'; -import './elasticsearch/overview'; -import './elasticsearch/indices'; -import './elasticsearch/index'; -import './elasticsearch/index/advanced'; -import './elasticsearch/nodes'; -import './elasticsearch/node'; -import './elasticsearch/node/advanced'; -import './elasticsearch/ccr'; -import './elasticsearch/ccr/shard'; -import './elasticsearch/ml_jobs'; -import './kibana/overview'; -import './kibana/instances'; -import './kibana/instance'; -import './logstash/overview'; -import './logstash/nodes'; -import './logstash/node'; -import './logstash/node/advanced'; -import './logstash/node/pipelines'; -import './logstash/pipelines'; -import './logstash/pipeline'; -import './beats/overview'; -import './beats/listing'; -import './beats/beat'; -import './apm/overview'; -import './apm/instances'; -import './apm/instance'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.js deleted file mode 100644 index 4d0f858d28117..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.js +++ /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. - */ - -/* - * 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 { i18n } from '@kbn/i18n'; -import { find, get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { ApmServerInstance } from '../../../components/apm/instance'; -import { I18nContext } from 'ui/i18n'; -import { CODE_PATH_APM } from '../../../../common/constants'; - -uiRoutes.when('/apm/instances/:uuid', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_APM] }); - }, - }, - - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const title = $injector.get('title'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.apm.instance.routeTitle', { - defaultMessage: '{apm} - Instance', - values: { - apm: 'APM', - }, - }), - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm/${$route.current.params.uuid}`, - defaultData: {}, - reactNodeId: 'apmInstanceReact', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - title($scope.cluster, `APM - ${get(data, 'apmSummary.name')}`); - this.renderReact(data); - } - ); - } - - renderReact(data) { - const component = ( - <I18nContext> - <ApmServerInstance - summary={data.apmSummary || {}} - metrics={data.metrics || {}} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - </I18nContext> - ); - super.renderReact(component); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js deleted file mode 100644 index 317879063b6e5..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.js +++ /dev/null @@ -1,91 +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 React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { ApmServerInstances } from '../../../components/apm/instances'; -import { MonitoringViewBaseEuiTableController } from '../..'; -import { I18nContext } from 'ui/i18n'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants'; - -uiRoutes.when('/apm/instances', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_APM] }); - }, - }, - controller: class extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.apm.instances.routeTitle', { - defaultMessage: '{apm} - Instances', - values: { - apm: 'APM', - }, - }), - storageKey: 'apm.instances', - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm/instances`, - defaultData: {}, - reactNodeId: 'apmInstancesReact', - $scope, - $injector, - }); - - this.scope = $scope; - this.injector = $injector; - - $scope.$watch( - () => this.data, - data => { - this.renderReact(data); - } - ); - } - - renderReact(data) { - const { pagination, sorting, onTableChange } = this; - - const component = ( - <I18nContext> - <SetupModeRenderer - scope={this.scope} - injector={this.injector} - productName={APM_SYSTEM_ID} - render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( - <Fragment> - {flyoutComponent} - <ApmServerInstances - setupMode={setupMode} - apms={{ - pagination, - sorting, - onTableChange, - data, - }} - /> - {bottomBarComponent} - </Fragment> - )} - /> - </I18nContext> - ); - super.renderReact(component); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.js deleted file mode 100644 index e6562f428d2a0..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.js +++ /dev/null @@ -1,59 +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 React from 'react'; -import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { ApmOverview } from '../../../components/apm/overview'; -import { I18nContext } from 'ui/i18n'; -import { CODE_PATH_APM } from '../../../../common/constants'; - -uiRoutes.when('/apm', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_APM] }); - }, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: 'APM', - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm`, - defaultData: {}, - reactNodeId: 'apmOverviewReact', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - this.renderReact(data); - } - ); - } - - renderReact(data) { - const component = ( - <I18nContext> - <ApmOverview {...data} onBrush={this.onBrush} zoomInfo={this.zoomInfo} /> - </I18nContext> - ); - super.renderReact(component); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/base_controller.js b/x-pack/legacy/plugins/monitoring/public/views/base_controller.js deleted file mode 100644 index 25b4d97177a98..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/base_controller.js +++ /dev/null @@ -1,211 +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 React from 'react'; -import moment from 'moment'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { getPageData } from '../lib/get_page_data'; -import { PageLoading } from 'plugins/monitoring/components'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; -import { PromiseWithCancel } from '../../common/cancel_promise'; -import { updateSetupModeData, getSetupModeState } from '../lib/setup_mode'; - -/** - * Given a timezone, this function will calculate the offset in milliseconds - * from UTC time. - * - * @param {string} timezone - */ -const getOffsetInMS = timezone => { - if (timezone === 'Browser') { - return 0; - } - const offsetInMinutes = moment.tz(timezone).utcOffset(); - const offsetInMS = offsetInMinutes * 1 * 60 * 1000; - return offsetInMS; -}; - -/** - * Class to manage common instantiation behaviors in a view controller - * - * This is expected to be extended, and behavior enabled using super(); - * - * Example: - * uiRoutes.when('/myRoute', { - * template: importedTemplate, - * controllerAs: 'myView', - * controller: class MyView extends MonitoringViewBaseController { - * constructor($injector, $scope) { - * super({ - * title: 'Hello World', - * api: '../api/v1/monitoring/foo/bar', - * defaultData, - * reactNodeId, - * $scope, - * $injector, - * options: { - * enableTimeFilter: false // this will have just the page auto-refresh control show - * } - * }); - * } - * } - * }); - */ -export class MonitoringViewBaseController { - /** - * Create a view controller - * @param {String} title - Title of the page - * @param {String} api - Back-end API endpoint to poll for getting the page - * data using POST and time range data in the body. Whenever possible, use - * this method for data polling rather than supply the getPageData param. - * @param {Function} apiUrlFn - Function that returns a string for the back-end - * API endpoint, in case the string has dynamic query parameters (e.g. - * show_system_indices) rather than supply the getPageData param. - * @param {Function} getPageData - (Optional) Function to fetch page data, if - * simply passing the API string isn't workable. - * @param {Object} defaultData - Initial model data to populate - * @param {String} reactNodeId - DOM element ID of the element for mounting - * the view's main React component - * @param {Service} $injector - Angular dependency injection service - * @param {Service} $scope - Angular view data binding service - * @param {Boolean} options.enableTimeFilter - Whether to show the time filter - * @param {Boolean} options.enableAutoRefresh - Whether to show the auto - * refresh control - */ - constructor({ - title = '', - api = '', - apiUrlFn, - getPageData: _getPageData = getPageData, - defaultData, - reactNodeId = null, // WIP: https://github.com/elastic/x-pack-kibana/issues/5198 - $scope, - $injector, - options = {}, - fetchDataImmediately = true, - }) { - const titleService = $injector.get('title'); - const $executor = $injector.get('$executor'); - const $window = $injector.get('$window'); - const config = $injector.get('config'); - - titleService($scope.cluster, title); - - $scope.pageData = this.data = { ...defaultData }; - this._isDataInitialized = false; - this.reactNodeId = reactNodeId; - - let deferTimer; - let zoomInLevel = 0; - - const popstateHandler = () => zoomInLevel > 0 && --zoomInLevel; - const removePopstateHandler = () => $window.removeEventListener('popstate', popstateHandler); - const addPopstateHandler = () => $window.addEventListener('popstate', popstateHandler); - - this.zoomInfo = { - zoomOutHandler: () => $window.history.back(), - showZoomOutBtn: () => zoomInLevel > 0, - }; - - const { enableTimeFilter = true, enableAutoRefresh = true } = options; - - if (enableTimeFilter === false) { - timefilter.disableTimeRangeSelector(); - } else { - timefilter.enableTimeRangeSelector(); - } - - if (enableAutoRefresh === false) { - timefilter.disableAutoRefreshSelector(); - } else { - timefilter.enableAutoRefreshSelector(); - } - - this.updateData = () => { - if (this.updateDataPromise) { - // Do not sent another request if one is inflight - // See https://github.com/elastic/kibana/issues/24082 - this.updateDataPromise.cancel(); - this.updateDataPromise = null; - } - const _api = apiUrlFn ? apiUrlFn() : api; - const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; - const setupMode = getSetupModeState(); - if (setupMode.enabled) { - promises.push(updateSetupModeData()); - } - this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); - return this.updateDataPromise.promise().then(([pageData]) => { - $scope.$apply(() => { - this._isDataInitialized = true; // render will replace loading screen with the react component - $scope.pageData = this.data = pageData; // update the view's data with the fetch result - }); - }); - }; - fetchDataImmediately && this.updateData(); - - $executor.register({ - execute: () => this.updateData(), - }); - $executor.start($scope); - $scope.$on('$destroy', () => { - clearTimeout(deferTimer); - removePopstateHandler(); - if (this.reactNodeId) { - // WIP https://github.com/elastic/x-pack-kibana/issues/5198 - unmountComponentAtNode(document.getElementById(this.reactNodeId)); - } - $executor.destroy(); - }); - - // needed for chart pages - this.onBrush = ({ xaxis }) => { - removePopstateHandler(); - const { to, from } = xaxis; - const timezone = config.get('dateFormat:tz'); - const offset = getOffsetInMS(timezone); - timefilter.setTime({ - from: moment(from - offset), - to: moment(to - offset), - mode: 'absolute', - }); - $executor.cancel(); - $executor.run(); - ++zoomInLevel; - clearTimeout(deferTimer); - /* - Needed to defer 'popstate' event, so it does not fire immediately after it's added. - 10ms is to make sure the event is not added with the same code digest - */ - deferTimer = setTimeout(() => addPopstateHandler(), 10); - }; - - this.setTitle = title => titleService($scope.cluster, title); - } - - renderReact(component) { - const renderElement = document.getElementById(this.reactNodeId); - if (!renderElement) { - console.warn(`"#${this.reactNodeId}" element has not been added to the DOM yet`); - return; - } - if (this._isDataInitialized === false) { - render( - <I18nContext> - <PageLoading /> - </I18nContext>, - renderElement - ); - } else { - render(component, renderElement); - } - } - - getPaginationRouteOptions() { - return {}; - } -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/beats/beat/get_page_data.js deleted file mode 100644 index 7e77e93d52fe8..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/get_page_data.js +++ /dev/null @@ -1,31 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beat/${$route.current.params.beatUuid}`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.js deleted file mode 100644 index b3fad1b4cc3cb..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.js +++ /dev/null @@ -1,51 +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 { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { CODE_PATH_BEATS } from '../../../../common/constants'; - -uiRoutes.when('/beats/beat/:beatUuid', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_BEATS] }); - }, - pageData: getPageData, - }, - controllerAs: 'beat', - controller: class BeatDetail extends MonitoringViewBaseController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - const pageData = $route.current.locals.pageData; - super({ - title: i18n.translate('xpack.monitoring.beats.instance.routeTitle', { - defaultMessage: 'Beats - {instanceName} - Overview', - values: { - instanceName: pageData.summary.name, - }, - }), - getPageData, - $scope, - $injector, - }); - - this.data = pageData; - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/beats/listing/get_page_data.js deleted file mode 100644 index 1838011dee652..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/get_page_data.js +++ /dev/null @@ -1,30 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beats`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js deleted file mode 100644 index 48848007c9c27..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.js +++ /dev/null @@ -1,93 +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 { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import React, { Fragment } from 'react'; -import { I18nContext } from 'ui/i18n'; -import { Listing } from '../../../components/beats/listing/listing'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_BEATS, BEATS_SYSTEM_ID } from '../../../../common/constants'; - -uiRoutes.when('/beats/beats', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_BEATS] }); - }, - pageData: getPageData, - }, - controllerAs: 'beats', - controller: class BeatsListing extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.beats.routeTitle', { defaultMessage: 'Beats' }), - storageKey: 'beats.beats', - getPageData, - reactNodeId: 'monitoringBeatsInstancesApp', - $scope, - $injector, - }); - - this.data = $route.current.locals.pageData; - this.scope = $scope; - this.injector = $injector; - this.kbnUrl = $injector.get('kbnUrl'); - - //Bypassing super.updateData, since this controller loads its own data - this._isDataInitialized = true; - - $scope.$watch( - () => this.data, - () => this.renderComponent() - ); - } - - renderComponent() { - const { sorting, pagination, onTableChange } = this.scope.beats; - this.renderReact( - <I18nContext> - <SetupModeRenderer - scope={this.scope} - injector={this.injector} - productName={BEATS_SYSTEM_ID} - render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( - <Fragment> - {flyoutComponent} - <Listing - stats={this.data.stats} - data={this.data.listing} - setupMode={setupMode} - sorting={this.sorting || sorting} - pagination={this.pagination || pagination} - onTableChange={this.onTableChange || onTableChange} - angular={{ - kbnUrl: this.kbnUrl, - scope: this.scope, - }} - /> - {bottomBarComponent} - </Fragment> - )} - /> - </I18nContext> - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/beats/overview/get_page_data.js deleted file mode 100644 index a3b120b277b94..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/get_page_data.js +++ /dev/null @@ -1,30 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.js deleted file mode 100644 index aea62d5c7f78f..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.js +++ /dev/null @@ -1,47 +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 { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { CODE_PATH_BEATS } from '../../../../common/constants'; - -uiRoutes.when('/beats', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_BEATS] }); - }, - pageData: getPageData, - }, - controllerAs: 'beats', - controller: class BeatsOverview extends MonitoringViewBaseController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: i18n.translate('xpack.monitoring.beats.overview.routeTitle', { - defaultMessage: 'Beats - Overview', - }), - getPageData, - $scope, - $injector, - }); - - this.data = $route.current.locals.pageData; - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.js deleted file mode 100644 index 958226514b146..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.js +++ /dev/null @@ -1,87 +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 React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { I18nContext } from 'ui/i18n'; -import template from './index.html'; -import { Listing } from '../../../components/cluster/listing'; -import { CODE_PATH_ALL } from '../../../../common/constants'; - -const CODE_PATHS = [CODE_PATH_ALL]; - -const getPageData = $injector => { - const monitoringClusters = $injector.get('monitoringClusters'); - return monitoringClusters(undefined, undefined, CODE_PATHS); -}; - -uiRoutes - .when('/home', { - template, - resolve: { - clusters: (Private, kbnUrl) => { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: CODE_PATHS, fetchAllClusters: true }).then(clusters => { - if (!clusters || !clusters.length) { - kbnUrl.changePath('/no-data'); - return Promise.reject(); - } - if (clusters.length === 1) { - // Bypass the cluster listing if there is just 1 cluster - kbnUrl.changePath('/overview'); - return Promise.reject(); - } - return clusters; - }); - }, - }, - controllerAs: 'clusters', - controller: class ClustersList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - storageKey: 'clusters', - getPageData, - $scope, - $injector, - reactNodeId: 'monitoringClusterListingApp', - }); - - const $route = $injector.get('$route'); - const kbnUrl = $injector.get('kbnUrl'); - const globalState = $injector.get('globalState'); - const storage = $injector.get('localStorage'); - const showLicenseExpiration = $injector.get('showLicenseExpiration'); - - this.data = $route.current.locals.clusters; - - $scope.$watch( - () => this.data, - data => { - this.renderReact( - <I18nContext> - <Listing - clusters={data} - angular={{ - scope: $scope, - globalState, - kbnUrl, - storage, - showLicenseExpiration, - }} - sorting={this.sorting} - pagination={this.pagination} - onTableChange={this.onTableChange} - /> - </I18nContext> - ); - } - ); - } - }, - }) - .otherwise({ redirectTo: '/no-data' }); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js deleted file mode 100644 index e1777b8ed7b49..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js +++ /dev/null @@ -1,103 +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 React, { Fragment } from 'react'; -import { isEmpty } from 'lodash'; -import chrome from 'ui/chrome'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../'; -import { Overview } from 'plugins/monitoring/components/cluster/overview'; -import { I18nContext } from 'ui/i18n'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { - CODE_PATH_ALL, - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - KIBANA_ALERTING_ENABLED, -} from '../../../../common/constants'; - -const CODE_PATHS = [CODE_PATH_ALL]; - -uiRoutes.when('/overview', { - template, - resolve: { - clusters(Private) { - // checks license info of all monitored clusters for multi-cluster monitoring usage and capability - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: CODE_PATHS }); - }, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const kbnUrl = $injector.get('kbnUrl'); - const monitoringClusters = $injector.get('monitoringClusters'); - const globalState = $injector.get('globalState'); - const showLicenseExpiration = $injector.get('showLicenseExpiration'); - const config = $injector.get('config'); - - super({ - title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { - defaultMessage: 'Overview', - }), - defaultData: {}, - getPageData: async () => { - const clusters = await monitoringClusters( - globalState.cluster_uuid, - globalState.ccs, - CODE_PATHS - ); - return clusters[0]; - }, - reactNodeId: 'monitoringClusterOverviewApp', - $scope, - $injector, - }); - - const changeUrl = target => { - $scope.$evalAsync(() => { - kbnUrl.changePath(target); - }); - }; - - $scope.$watch( - () => this.data, - async data => { - if (isEmpty(data)) { - return; - } - - let emailAddress = chrome.getInjected('monitoringLegacyEmailAddress') || ''; - if (KIBANA_ALERTING_ENABLED) { - emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; - } - - this.renderReact( - <I18nContext> - <SetupModeRenderer - scope={$scope} - injector={$injector} - render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( - <Fragment> - {flyoutComponent} - <Overview - cluster={data} - emailAddress={emailAddress} - setupMode={setupMode} - changeUrl={changeUrl} - showLicenseExpiration={showLicenseExpiration} - /> - {bottomBarComponent} - </Fragment> - )} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js deleted file mode 100644 index 83dd24209dfe3..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js +++ /dev/null @@ -1,30 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.js deleted file mode 100644 index cf51347842f4a..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.js +++ /dev/null @@ -1,56 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { getPageData } from './get_page_data'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { Ccr } from '../../../components/elasticsearch/ccr'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { I18nContext } from 'ui/i18n'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch/ccr', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'elasticsearchCcr', - controller: class ElasticsearchCcrController extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.ccr.routeTitle', { - defaultMessage: 'Elasticsearch - Ccr', - }), - reactNodeId: 'elasticsearchCcrReact', - getPageData, - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - this.renderReact(data); - } - ); - - this.renderReact = ({ data }) => { - super.renderReact( - <I18nContext> - <Ccr data={data} /> - </I18nContext> - ); - }; - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js deleted file mode 100644 index 22ca094d28b07..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js +++ /dev/null @@ -1,31 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr/${$route.current.params.index}/shard/${$route.current.params.shardId}`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js deleted file mode 100644 index ff35f7f743f66..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ /dev/null @@ -1,65 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { getPageData } from './get_page_data'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { CcrShard } from '../../../../components/elasticsearch/ccr_shard'; -import { I18nContext } from 'ui/i18n'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; - -uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'elasticsearchCcr', - controller: class ElasticsearchCcrController extends MonitoringViewBaseController { - constructor($injector, $scope, pageData) { - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.routeTitle', { - defaultMessage: 'Elasticsearch - Ccr - Shard', - }), - reactNodeId: 'elasticsearchCcrShardReact', - getPageData, - $scope, - $injector, - }); - - $scope.instance = i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.instanceTitle', { - defaultMessage: 'Index: {followerIndex} Shard: {shardId}', - values: { - followerIndex: get(pageData, 'stat.follower_index'), - shardId: get(pageData, 'stat.shard_id'), - }, - }); - - $scope.$watch( - () => this.data, - data => { - this.renderReact(data); - } - ); - - this.renderReact = props => { - super.renderReact( - <I18nContext> - <CcrShard {...props} /> - </I18nContext> - ); - }; - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js deleted file mode 100644 index 4fc439b4e0123..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js +++ /dev/null @@ -1,94 +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. - */ - -/** - * Controller for Advanced Index Detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { AdvancedIndex } from '../../../../components/elasticsearch/index/advanced'; -import { I18nContext } from 'ui/i18n'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; - -function getPageData($injector) { - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; - const $http = $injector.get('$http'); - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: true, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/elasticsearch/indices/:index/advanced', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringElasticsearchAdvancedIndexApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const indexName = $route.current.params.index; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.indices.advanced.routeTitle', { - defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', - values: { - indexName, - }, - }), - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchAdvancedIndexApp', - $scope, - $injector, - }); - - this.indexName = indexName; - - $scope.$watch( - () => this.data, - data => { - this.renderReact( - <I18nContext> - <AdvancedIndex - indexSummary={data.indexSummary} - metrics={data.metrics} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.js deleted file mode 100644 index bbeef8294a897..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.js +++ /dev/null @@ -1,115 +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. - */ - -/** - * Controller for single index detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; -import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; -import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes'; -import { Index } from '../../../components/elasticsearch/index/index'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: false, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/elasticsearch/indices/:index', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringElasticsearchIndexApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const kbnUrl = $injector.get('kbnUrl'); - const indexName = $route.current.params.index; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.indices.overview.routeTitle', { - defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', - values: { - indexName, - }, - }), - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchIndexApp', - $scope, - $injector, - }); - - this.indexName = indexName; - const transformer = indicesByNodes(); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.shards) { - return; - } - - const shards = data.shards; - $scope.totalCount = shards.length; - $scope.showing = transformer(shards, data.nodes); - $scope.labels = labels.node; - if (shards.some(shard => shard.state === 'UNASSIGNED')) { - $scope.labels = labels.indexWithUnassigned; - } else { - $scope.labels = labels.index; - } - - this.renderReact( - <I18nContext> - <Index - scope={$scope} - kbnUrl={kbnUrl} - onBrush={this.onBrush} - indexUuid={this.indexName} - clusterUuid={$scope.cluster.cluster_uuid} - zoomInfo={this.zoomInfo} - {...data} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.js deleted file mode 100644 index f1d96557b0c1c..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.js +++ /dev/null @@ -1,89 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { ElasticsearchIndices } from '../../../components'; -import template from './index.html'; -import { I18nContext } from 'ui/i18n'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch/indices', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - }, - controllerAs: 'elasticsearchIndices', - controller: class ElasticsearchIndicesController extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const features = $injector.get('features'); - - const { cluster_uuid: clusterUuid } = globalState; - $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: clusterUuid }); - - let showSystemIndices = features.isEnabled('showSystemIndices', false); - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.indices.routeTitle', { - defaultMessage: 'Elasticsearch - Indices', - }), - storageKey: 'elasticsearch.indices', - apiUrlFn: () => - `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices?show_system_indices=${showSystemIndices}`, - reactNodeId: 'elasticsearchIndicesReact', - defaultData: {}, - $scope, - $injector, - $scope, - $injector, - }); - - this.isCcrEnabled = $scope.cluster.isCcrEnabled; - - // for binding - const toggleShowSystemIndices = isChecked => { - // flip the boolean - showSystemIndices = isChecked; - // preserve setting in localStorage - features.update('showSystemIndices', isChecked); - // update the page (resets pagination and sorting) - this.updateData(); - }; - - $scope.$watch( - () => this.data, - data => { - this.renderReact(data); - } - ); - - this.renderReact = ({ clusterStatus, indices }) => { - super.renderReact( - <I18nContext> - <ElasticsearchIndices - clusterStatus={clusterStatus} - indices={indices} - showSystemIndices={showSystemIndices} - toggleShowSystemIndices={toggleShowSystemIndices} - sorting={this.sorting} - pagination={this.pagination} - onTableChange={this.onTableChange} - /> - </I18nContext> - ); - }; - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js deleted file mode 100644 index 1943b580f7a75..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js +++ /dev/null @@ -1,29 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ml_jobs`; - const timeBounds = timefilter.getBounds(); - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js deleted file mode 100644 index 5e66a4147ab70..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js +++ /dev/null @@ -1,47 +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 { find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { CODE_PATH_ELASTICSEARCH, CODE_PATH_ML } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch/ml_jobs', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH, CODE_PATH_ML] }); - }, - pageData: getPageData, - }, - controllerAs: 'mlJobs', - controller: class MlJobsList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.mlJobs.routeTitle', { - defaultMessage: 'Elasticsearch - Machine Learning Jobs', - }), - storageKey: 'elasticsearch.mlJobs', - getPageData, - $scope, - $injector, - }); - - const $route = $injector.get('$route'); - this.data = $route.current.locals.pageData; - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - this.isCcrEnabled = Boolean($scope.cluster && $scope.cluster.isCcrEnabled); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js deleted file mode 100644 index 2bbdf604d00ce..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js +++ /dev/null @@ -1,95 +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. - */ - -/** - * Controller for Advanced Node Detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; -import { AdvancedNode } from '../../../../components/elasticsearch/node/advanced'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const timeBounds = timefilter.getBounds(); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: true, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/elasticsearch/nodes/:node/advanced', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchAdvancedNodeApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.elasticsearch.node.advanced.routeTitle', { - defaultMessage: 'Elasticsearch - Nodes - {nodeSummaryName} - Advanced', - values: { - nodeSummaryName: data.nodeSummary.name, - }, - }) - ); - - this.renderReact( - <I18nContext> - <AdvancedNode - nodeSummary={data.nodeSummary} - metrics={data.metrics} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js deleted file mode 100644 index 0d9e0b25eacd0..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js +++ /dev/null @@ -1,35 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; - const features = $injector.get('features'); - const showSystemIndices = features.isEnabled('showSystemIndices', false); - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - showSystemIndices, - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: false, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.js deleted file mode 100644 index fa76222d78e2d..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.js +++ /dev/null @@ -1,98 +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. - */ - -/** - * Controller for Node Detail - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { partial } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { Node } from '../../../components/elasticsearch/node/node'; -import { I18nContext } from 'ui/i18n'; -import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; -import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch/nodes/:node', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringElasticsearchNodeApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const kbnUrl = $injector.get('kbnUrl'); - const nodeName = $route.current.params.node; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.node.overview.routeTitle', { - defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Overview', - values: { - nodeName, - }, - }), - defaultData: {}, - getPageData, - reactNodeId: 'monitoringElasticsearchNodeApp', - $scope, - $injector, - }); - - this.nodeName = nodeName; - - const features = $injector.get('features'); - const callPageData = partial(getPageData, $injector); - // show/hide system indices in shard allocation view - $scope.showSystemIndices = features.isEnabled('showSystemIndices', false); - $scope.toggleShowSystemIndices = isChecked => { - $scope.showSystemIndices = isChecked; - // preserve setting in localStorage - features.update('showSystemIndices', isChecked); - // update the page - callPageData().then(data => (this.data = data)); - }; - - const transformer = nodesByIndices(); - $scope.$watch( - () => this.data, - data => { - if (!data || !data.shards) { - return; - } - - const shards = data.shards; - $scope.totalCount = shards.length; - $scope.showing = transformer(shards, data.nodes); - $scope.labels = labels.node; - - this.renderReact( - <I18nContext> - <Node - scope={$scope} - kbnUrl={kbnUrl} - nodeId={this.nodeName} - clusterUuid={$scope.cluster.cluster_uuid} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - {...data} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js deleted file mode 100644 index a9a6774d4c883..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.js +++ /dev/null @@ -1,119 +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 React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import template from './index.html'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { ElasticsearchNodes } from '../../../components'; -import { I18nContext } from 'ui/i18n'; -import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch/nodes', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - }, - controllerAs: 'elasticsearchNodes', - controller: class ElasticsearchNodesController extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const showCgroupMetricsElasticsearch = $injector.get('showCgroupMetricsElasticsearch'); - - $scope.cluster = - find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }) || {}; - - const getPageData = ($injector, _api = undefined, routeOptions = {}) => { - _api; // to fix eslint - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const timeBounds = timefilter.getBounds(); - - const getNodes = (clusterUuid = globalState.cluster_uuid) => - $http.post(`../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - ...routeOptions, - }); - - const promise = globalState.cluster_uuid ? getNodes() : new Promise(resolve => resolve({})); - return promise - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); - }; - - super({ - title: i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { - defaultMessage: 'Elasticsearch - Nodes', - }), - storageKey: 'elasticsearch.nodes', - reactNodeId: 'elasticsearchNodesReact', - defaultData: {}, - getPageData, - $scope, - $injector, - fetchDataImmediately: false, // We want to apply pagination before sending the first request - }); - - this.isCcrEnabled = $scope.cluster.isCcrEnabled; - - $scope.$watch( - () => this.data, - () => this.renderReact(this.data || {}) - ); - - this.renderReact = ({ clusterStatus, nodes, totalNodeCount }) => { - const pagination = { - ...this.pagination, - totalItemCount: totalNodeCount, - }; - - super.renderReact( - <I18nContext> - <SetupModeRenderer - scope={$scope} - injector={$injector} - productName={ELASTICSEARCH_SYSTEM_ID} - render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( - <Fragment> - {flyoutComponent} - <ElasticsearchNodes - clusterStatus={clusterStatus} - clusterUuid={globalState.cluster_uuid} - setupMode={setupMode} - nodes={nodes} - showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch} - {...this.getPaginationTableProps(pagination)} - /> - {bottomBarComponent} - </Fragment> - )} - /> - </I18nContext> - ); - }; - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/controller.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/controller.js deleted file mode 100644 index 9f59b4d632222..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/controller.js +++ /dev/null @@ -1,98 +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 React from 'react'; -import { find } from 'lodash'; -import { MonitoringViewBaseController } from '../../'; -import { ElasticsearchOverview } from 'plugins/monitoring/components'; -import { I18nContext } from 'ui/i18n'; - -export class ElasticsearchOverviewController extends MonitoringViewBaseController { - constructor($injector, $scope) { - // breadcrumbs + page title - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - super({ - title: 'Elasticsearch', - api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch`, - defaultData: { - clusterStatus: { status: '' }, - metrics: null, - shardActivity: null, - }, - reactNodeId: 'elasticsearchOverviewReact', - $scope, - $injector, - }); - - this.isCcrEnabled = $scope.cluster.isCcrEnabled; - this.showShardActivityHistory = false; - this.toggleShardActivityHistory = () => { - this.showShardActivityHistory = !this.showShardActivityHistory; - $scope.$evalAsync(() => { - this.renderReact(this.data, $scope.cluster); - }); - }; - - this.initScope($scope); - } - - initScope($scope) { - $scope.$watch( - () => this.data, - data => { - this.renderReact(data, $scope.cluster); - } - ); - - // HACK to force table to re-render even if data hasn't changed. This - // happens when the data remains empty after turning on showHistory. The - // button toggle needs to update the "no data" message based on the value of showHistory - $scope.$watch( - () => this.showShardActivityHistory, - () => { - const { data } = this; - const dataWithShardActivityLoading = { ...data, shardActivity: null }; - // force shard activity to rerender by manipulating and then re-setting its data prop - this.renderReact(dataWithShardActivityLoading, $scope.cluster); - this.renderReact(data, $scope.cluster); - } - ); - } - - filterShardActivityData(shardActivity) { - return shardActivity.filter(row => { - return this.showShardActivityHistory || row.stage !== 'DONE'; - }); - } - - renderReact(data, cluster) { - // All data needs to originate in this view, and get passed as a prop to the components, for statelessness - const { clusterStatus, metrics, shardActivity, logs } = data; - const shardActivityData = shardActivity && this.filterShardActivityData(shardActivity); // no filter on data = null - const component = ( - <I18nContext> - <ElasticsearchOverview - clusterStatus={clusterStatus} - metrics={metrics} - logs={logs} - cluster={cluster} - shardActivity={shardActivityData} - onBrush={this.onBrush} - showShardActivityHistory={this.showShardActivityHistory} - toggleShardActivityHistory={this.toggleShardActivityHistory} - zoomInfo={this.zoomInfo} - /> - </I18nContext> - ); - - super.renderReact(component); - } -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.js deleted file mode 100644 index 475c0fc494857..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.js +++ /dev/null @@ -1,23 +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 uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { ElasticsearchOverviewController } from './controller'; -import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; - -uiRoutes.when('/elasticsearch', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); - }, - }, - controllerAs: 'elasticsearchOverview', - controller: ElasticsearchOverviewController, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.js deleted file mode 100644 index 6535bd7410445..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.js +++ /dev/null @@ -1,153 +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. - */ - -/* - * Kibana Instance - */ -import React from 'react'; -import { get } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, - EuiPanel, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { DetailStatus } from 'plugins/monitoring/components/kibana/detail_status'; -import { I18nContext } from 'ui/i18n'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/${$route.current.params.uuid}`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/kibana/instances/:uuid', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_KIBANA] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringKibanaInstanceApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: `Kibana - ${get($scope.pageData, 'kibanaSummary.name')}`, - defaultData: {}, - getPageData, - reactNodeId: 'monitoringKibanaInstanceApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.metrics) { - return; - } - - this.setTitle(`Kibana - ${get(data, 'kibanaSummary.name')}`); - - this.renderReact( - <I18nContext> - <EuiPage> - <EuiPageBody> - <EuiPanel> - <DetailStatus stats={data.kibanaSummary} /> - </EuiPanel> - <EuiSpacer size="m" /> - <EuiPageContent> - <EuiFlexGrid columns={2} gutterSize="s"> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_requests} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - <EuiSpacer /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_response_times} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - <EuiSpacer /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_memory} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - <EuiSpacer /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_average_concurrent_connections} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - <EuiSpacer /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_os_load} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - <EuiSpacer /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_process_delay} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - <EuiSpacer /> - </EuiFlexItem> - </EuiFlexGrid> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/get_page_data.js deleted file mode 100644 index 4f8d7fa20d332..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/get_page_data.js +++ /dev/null @@ -1,30 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/instances`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js deleted file mode 100644 index 51a7e033bd0d6..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.js +++ /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 React, { Fragment } from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { KibanaInstances } from 'plugins/monitoring/components/kibana/instances'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { I18nContext } from 'ui/i18n'; -import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA } from '../../../../common/constants'; - -uiRoutes.when('/kibana/instances', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_KIBANA] }); - }, - pageData: getPageData, - }, - controllerAs: 'kibanas', - controller: class KibanaInstancesList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - title: 'Kibana Instances', - storageKey: 'kibana.instances', - getPageData, - reactNodeId: 'monitoringKibanaInstancesApp', - $scope, - $injector, - }); - - const kbnUrl = $injector.get('kbnUrl'); - - const renderReact = () => { - this.renderReact( - <I18nContext> - <SetupModeRenderer - scope={$scope} - injector={$injector} - productName={KIBANA_SYSTEM_ID} - render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( - <Fragment> - {flyoutComponent} - <KibanaInstances - instances={this.data.kibanas} - setupMode={setupMode} - sorting={this.sorting} - pagination={this.pagination} - onTableChange={this.onTableChange} - clusterStatus={this.data.clusterStatus} - angular={{ - $scope, - kbnUrl, - }} - /> - {bottomBarComponent} - </Fragment> - )} - /> - </I18nContext> - ); - }; - - $scope.$watch( - () => this.data, - data => { - if (!data) { - return; - } - - renderReact(); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.js deleted file mode 100644 index 0705e3b7f270b..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.js +++ /dev/null @@ -1,115 +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. - */ - -/** - * Kibana Overview - */ -import React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPanel, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { ClusterStatus } from '../../../components/kibana/cluster_status'; -import { I18nContext } from 'ui/i18n'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_KIBANA } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/kibana', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_KIBANA] }); - }, - pageData: getPageData, - }, - controllerAs: 'monitoringKibanaOverviewApp', - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: `Kibana`, - defaultData: {}, - getPageData, - reactNodeId: 'monitoringKibanaOverviewApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.clusterStatus) { - return; - } - - this.renderReact( - <I18nContext> - <EuiPage> - <EuiPageBody> - <EuiPanel> - <ClusterStatus stats={data.clusterStatus} /> - </EuiPanel> - <EuiSpacer size="m" /> - <EuiPageContent> - <EuiFlexGroup> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_cluster_requests} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <MonitoringTimeseriesContainer - series={data.metrics.kibana_cluster_response_times} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js b/x-pack/legacy/plugins/monitoring/public/views/license/controller.js deleted file mode 100644 index ce6e9c8fb74cd..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/license/controller.js +++ /dev/null @@ -1,82 +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 { get, find } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import chrome from 'plugins/monitoring/np_imports/ui/chrome'; -import { formatDateTimeLocal } from '../../../common/formatting'; -import { MANAGEMENT_BASE_PATH } from 'plugins/xpack_main/components'; -import { License } from 'plugins/monitoring/components'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; - -const REACT_NODE_ID = 'licenseReact'; - -export class LicenseViewController { - constructor($injector, $scope) { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - $scope.$on('$destroy', () => { - unmountComponentAtNode(document.getElementById(REACT_NODE_ID)); - }); - - this.init($injector, $scope, i18n); - } - - init($injector, $scope) { - const globalState = $injector.get('globalState'); - const title = $injector.get('title'); - const $route = $injector.get('$route'); - - const cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - $scope.cluster = cluster; - const routeTitle = i18n.translate('xpack.monitoring.license.licenseRouteTitle', { - defaultMessage: 'License', - }); - title($scope.cluster, routeTitle); - - this.license = cluster.license; - this.isExpired = Date.now() > get(cluster, 'license.expiry_date_in_millis'); - this.isPrimaryCluster = cluster.isPrimary; - - const basePath = chrome.getBasePath(); - this.uploadLicensePath = basePath + '/app/kibana#' + MANAGEMENT_BASE_PATH + 'upload_license'; - - this.renderReact($scope); - } - - renderReact($scope) { - const injector = chrome.dangerouslyGetActiveInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); - $scope.$evalAsync(() => { - const { isPrimaryCluster, license, isExpired, uploadLicensePath } = this; - let expiryDate = license.expiry_date_in_millis; - if (license.expiry_date_in_millis !== undefined) { - expiryDate = formatDateTimeLocal(license.expiry_date_in_millis, timezone); - } - - // Mount the React component to the template - render( - <I18nContext> - <License - isPrimaryCluster={isPrimaryCluster} - status={license.status} - type={license.type} - isExpired={isExpired} - expiryDate={expiryDate} - uploadLicensePath={uploadLicensePath} - /> - </I18nContext>, - document.getElementById(REACT_NODE_ID) - ); - }); - } -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/index.js b/x-pack/legacy/plugins/monitoring/public/views/license/index.js deleted file mode 100644 index e0796c85d8f85..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/license/index.js +++ /dev/null @@ -1,23 +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 uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { LicenseViewController } from './controller'; -import { CODE_PATH_LICENSE } from '../../../common/constants'; - -uiRoutes.when('/license', { - template, - resolve: { - clusters: Private => { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LICENSE] }); - }, - }, - controllerAs: 'licenseView', - controller: LicenseViewController, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/loading/index.html b/x-pack/legacy/plugins/monitoring/public/views/loading/index.html deleted file mode 100644 index 11da26a0ceed2..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/loading/index.html +++ /dev/null @@ -1,5 +0,0 @@ -<monitoring-main name="loading"> - <div data-test-subj="loadingContainer"> - <div id="monitoringLoadingReactApp"></div> - </div> -</monitoring-main> diff --git a/x-pack/legacy/plugins/monitoring/public/views/loading/index.js b/x-pack/legacy/plugins/monitoring/public/views/loading/index.js deleted file mode 100644 index 0488683845a7d..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/loading/index.js +++ /dev/null @@ -1,51 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { PageLoading } from 'plugins/monitoring/components'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { I18nContext } from 'ui/i18n'; -import template from './index.html'; -import { CODE_PATH_LICENSE } from '../../../common/constants'; - -const REACT_DOM_ID = 'monitoringLoadingReactApp'; - -uiRoutes.when('/loading', { - template, - controller: class { - constructor($injector, $scope) { - const monitoringClusters = $injector.get('monitoringClusters'); - const kbnUrl = $injector.get('kbnUrl'); - - $scope.$on('$destroy', () => { - unmountComponentAtNode(document.getElementById(REACT_DOM_ID)); - }); - - $scope.$$postDigest(() => { - this.renderReact(); - }); - - monitoringClusters(undefined, undefined, [CODE_PATH_LICENSE]).then(clusters => { - if (clusters && clusters.length) { - kbnUrl.changePath('/home'); - return; - } - kbnUrl.changePath('/no-data'); - return; - }); - } - - renderReact() { - render( - <I18nContext> - <PageLoading /> - </I18nContext>, - document.getElementById(REACT_DOM_ID) - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.js deleted file mode 100644 index 29cf4839eff94..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.js +++ /dev/null @@ -1,130 +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. - */ - -/* - * Logstash Node Advanced View - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { MonitoringViewBaseController } from '../../../base_controller'; -import { DetailStatus } from 'plugins/monitoring/components/logstash/detail_status'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPanel, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../../../components/chart'; -import { I18nContext } from 'ui/i18n'; -import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const $route = $injector.get('$route'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: true, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash/node/:uuid/advanced', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringLogstashNodeAdvancedApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.logstash.node.advanced.routeTitle', { - defaultMessage: 'Logstash - {nodeName} - Advanced', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - const metricsToShow = [ - data.metrics.logstash_node_cpu_utilization, - data.metrics.logstash_queue_events_count, - data.metrics.logstash_node_cgroup_cpu, - data.metrics.logstash_pipeline_queue_size, - data.metrics.logstash_node_cgroup_stats, - ]; - - this.renderReact( - <I18nContext> - <EuiPage> - <EuiPageBody> - <EuiPanel> - <DetailStatus stats={data.nodeSummary} /> - </EuiPanel> - <EuiSpacer size="m" /> - <EuiPageContent> - <EuiFlexGrid columns={2} gutterSize="s"> - {metricsToShow.map((metric, index) => ( - <EuiFlexItem key={index}> - <MonitoringTimeseriesContainer - series={metric} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - {...data} - /> - <EuiSpacer /> - </EuiFlexItem> - ))} - </EuiFlexGrid> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.js deleted file mode 100644 index f1777d1e46ef0..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.js +++ /dev/null @@ -1,131 +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. - */ - -/* - * Logstash Node - */ -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { DetailStatus } from 'plugins/monitoring/components/logstash/detail_status'; -import { - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPanel, - EuiSpacer, - EuiFlexGrid, - EuiFlexItem, -} from '@elastic/eui'; -import { MonitoringTimeseriesContainer } from '../../../components/chart'; -import { I18nContext } from 'ui/i18n'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const $route = $injector.get('$route'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - is_advanced: false, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash/node/:uuid', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringLogstashNodeApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.logstash.node.routeTitle', { - defaultMessage: 'Logstash - {nodeName}', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - const metricsToShow = [ - data.metrics.logstash_events_input_rate, - data.metrics.logstash_jvm_usage, - data.metrics.logstash_events_output_rate, - data.metrics.logstash_node_cpu_metric, - data.metrics.logstash_events_latency, - data.metrics.logstash_os_load, - ]; - - this.renderReact( - <I18nContext> - <EuiPage> - <EuiPageBody> - <EuiPanel> - <DetailStatus stats={data.nodeSummary} /> - </EuiPanel> - <EuiSpacer size="m" /> - <EuiPageContent> - <EuiFlexGrid columns={2} gutterSize="s"> - {metricsToShow.map((metric, index) => ( - <EuiFlexItem key={index}> - <MonitoringTimeseriesContainer - series={metric} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - {...data} - /> - <EuiSpacer /> - </EuiFlexItem> - ))} - </EuiFlexGrid> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.js deleted file mode 100644 index 017988b70bdd4..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.js +++ /dev/null @@ -1,132 +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. - */ - -/* - * Logstash Node Pipelines Listing - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { isPipelineMonitoringSupportedInVersion } from 'plugins/monitoring/lib/logstash/pipelines'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { MonitoringViewBaseEuiTableController } from '../../../'; -import { I18nContext } from 'ui/i18n'; -import { PipelineListing } from '../../../../components/logstash/pipeline_listing/pipeline_listing'; -import { DetailStatus } from '../../../../components/logstash/detail_status'; -import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; - -const getPageData = ($injector, _api = undefined, routeOptions = {}) => { - _api; // fixing eslint - const $route = $injector.get('$route'); - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const Private = $injector.get('Private'); - - const logstashUuid = $route.current.params.uuid; - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${logstashUuid}/pipelines`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - ...routeOptions, - }) - .then(response => response.data) - .catch(err => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -}; - -function makeUpgradeMessage(logstashVersion) { - if (isPipelineMonitoringSupportedInVersion(logstashVersion)) { - return null; - } - - return i18n.translate('xpack.monitoring.logstash.node.pipelines.notAvailableDescription', { - defaultMessage: - 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher. This node is running version {logstashVersion}.', - values: { - logstashVersion, - }, - }); -} - -uiRoutes.when('/logstash/node/:uuid/pipelines', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - }, - controller: class extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const kbnUrl = $injector.get('kbnUrl'); - const config = $injector.get('config'); - - super({ - defaultData: {}, - getPageData, - reactNodeId: 'monitoringLogstashNodePipelinesApp', - $scope, - $injector, - fetchDataImmediately: false, // We want to apply pagination before sending the first request - }); - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.nodeSummary) { - return; - } - - this.setTitle( - i18n.translate('xpack.monitoring.logstash.node.pipelines.routeTitle', { - defaultMessage: 'Logstash - {nodeName} - Pipelines', - values: { - nodeName: data.nodeSummary.name, - }, - }) - ); - - const pagination = { - ...this.pagination, - totalItemCount: data.totalPipelineCount, - }; - - this.renderReact( - <I18nContext> - <PipelineListing - className="monitoringLogstashPipelinesTable" - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - stats={data.nodeSummary} - statusComponent={DetailStatus} - data={data.pipelines} - {...this.getPaginationTableProps(pagination)} - dateFormat={config.get('dateFormat')} - upgradeMessage={makeUpgradeMessage(data.nodeSummary.version, i18n)} - angular={{ - kbnUrl, - scope: $scope, - }} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/get_page_data.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/get_page_data.js deleted file mode 100644 index d476f6ba5143e..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/get_page_data.js +++ /dev/null @@ -1,30 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; - -export function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/nodes`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js deleted file mode 100644 index 30f851b2a7534..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.js +++ /dev/null @@ -1,71 +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 React, { Fragment } from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { MonitoringViewBaseEuiTableController } from '../../'; -import { getPageData } from './get_page_data'; -import template from './index.html'; -import { I18nContext } from 'ui/i18n'; -import { Listing } from '../../../components/logstash/listing'; -import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; - -uiRoutes.when('/logstash/nodes', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controllerAs: 'lsNodes', - controller: class LsNodesList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - const kbnUrl = $injector.get('kbnUrl'); - - super({ - title: 'Logstash - Nodes', - storageKey: 'logstash.nodes', - getPageData, - reactNodeId: 'monitoringLogstashNodesApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - this.renderReact( - <I18nContext> - <SetupModeRenderer - scope={$scope} - injector={$injector} - productName={LOGSTASH_SYSTEM_ID} - render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( - <Fragment> - {flyoutComponent} - <Listing - data={data.nodes} - setupMode={setupMode} - stats={data.clusterStatus} - sorting={this.sorting} - pagination={this.pagination} - onTableChange={this.onTableChange} - angular={{ kbnUrl, scope: $scope }} - /> - {bottomBarComponent} - </Fragment> - )} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.js deleted file mode 100644 index f41f54555952e..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.js +++ /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. - */ - -/** - * Logstash Overview - */ -import React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; -import { Overview } from '../../../components/logstash/overview'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; - -function getPageData($injector) { - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - }) - .then(response => response.data) - .catch(err => { - const Private = $injector.get('Private'); - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash', { - template, - resolve: { - clusters: function(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - super({ - title: 'Logstash', - getPageData, - reactNodeId: 'monitoringLogstashOverviewApp', - $scope, - $injector, - }); - - $scope.$watch( - () => this.data, - data => { - this.renderReact( - <I18nContext> - <Overview - stats={data.clusterStatus} - metrics={data.metrics} - onBrush={this.onBrush} - zoomInfo={this.zoomInfo} - /> - </I18nContext> - ); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.js deleted file mode 100644 index 11cb8516847c8..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.js +++ /dev/null @@ -1,175 +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. - */ - -/* - * Logstash Node Pipeline View - */ -import React from 'react'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import moment from 'moment'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { CALCULATE_DURATION_SINCE, CODE_PATH_LOGSTASH } from '../../../../common/constants'; -import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import template from './index.html'; -import { i18n } from '@kbn/i18n'; -import { List } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/list'; -import { PipelineState } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/pipeline_state'; -import { PipelineViewer } from 'plugins/monitoring/components/logstash/pipeline_viewer'; -import { Pipeline } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/pipeline'; -import { vertexFactory } from 'plugins/monitoring/components/logstash/pipeline_viewer/models/graph/vertex_factory'; -import { MonitoringViewBaseController } from '../../base_controller'; -import { I18nContext } from 'ui/i18n'; -import { EuiPageBody, EuiPage, EuiPageContent } from '@elastic/eui'; - -let previousPipelineHash = undefined; -let detailVertexId = undefined; - -function getPageData($injector) { - const $route = $injector.get('$route'); - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const minIntervalSeconds = $injector.get('minIntervalSeconds'); - const Private = $injector.get('Private'); - - const { ccs, cluster_uuid: clusterUuid } = globalState; - const pipelineId = $route.current.params.id; - const pipelineHash = $route.current.params.hash || ''; - - // Pipeline version was changed, so clear out detailVertexId since that vertex won't - // exist in the updated pipeline version - if (pipelineHash !== previousPipelineHash) { - previousPipelineHash = pipelineHash; - detailVertexId = undefined; - } - - const url = pipelineHash - ? `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}/${pipelineHash}` - : `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}`; - return $http - .post(url, { - ccs, - detailVertexId, - }) - .then(response => response.data) - .then(data => { - data.versions = data.versions.map(version => { - const relativeFirstSeen = formatTimestampToDuration( - version.firstSeen, - CALCULATE_DURATION_SINCE - ); - const relativeLastSeen = formatTimestampToDuration( - version.lastSeen, - CALCULATE_DURATION_SINCE - ); - - const fudgeFactorSeconds = 2 * minIntervalSeconds; - const isLastSeenCloseToNow = Date.now() - version.lastSeen <= fudgeFactorSeconds * 1000; - - return { - ...version, - relativeFirstSeen: i18n.translate( - 'xpack.monitoring.logstash.pipeline.relativeFirstSeenAgoLabel', - { - defaultMessage: '{relativeFirstSeen} ago', - values: { relativeFirstSeen }, - } - ), - relativeLastSeen: isLastSeenCloseToNow - ? i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenNowLabel', { - defaultMessage: 'now', - }) - : i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenAgoLabel', { - defaultMessage: 'until {relativeLastSeen} ago', - values: { relativeLastSeen }, - }), - }; - }); - - return data; - }) - .catch(err => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -} - -uiRoutes.when('/logstash/pipelines/:id/:hash?', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - pageData: getPageData, - }, - controller: class extends MonitoringViewBaseController { - constructor($injector, $scope) { - const config = $injector.get('config'); - const dateFormat = config.get('dateFormat'); - - super({ - title: i18n.translate('xpack.monitoring.logstash.pipeline.routeTitle', { - defaultMessage: 'Logstash - Pipeline', - }), - storageKey: 'logstash.pipelines', - getPageData, - reactNodeId: 'monitoringLogstashPipelineApp', - $scope, - options: { - enableTimeFilter: false, - }, - $injector, - }); - - const timeseriesTooltipXValueFormatter = xValue => moment(xValue).format(dateFormat); - - const setDetailVertexId = vertex => { - if (!vertex) { - detailVertexId = undefined; - } else { - detailVertexId = vertex.id; - } - - return this.updateData(); - }; - - $scope.$watch( - () => this.data, - data => { - if (!data || !data.pipeline) { - return; - } - this.pipelineState = new PipelineState(data.pipeline); - this.detailVertex = data.vertex ? vertexFactory(null, data.vertex) : null; - this.renderReact( - <I18nContext> - <EuiPage> - <EuiPageBody> - <EuiPageContent> - <PipelineViewer - pipeline={List.fromPipeline( - Pipeline.fromPipelineGraph(this.pipelineState.config.graph) - )} - timeseriesTooltipXValueFormatter={timeseriesTooltipXValueFormatter} - setDetailVertexId={setDetailVertexId} - detailVertex={this.detailVertex} - /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </I18nContext> - ); - } - ); - - $scope.$on('$destroy', () => { - previousPipelineHash = undefined; - detailVertexId = undefined; - }); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.js b/x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.js deleted file mode 100644 index 75a18000c14dd..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.js +++ /dev/null @@ -1,132 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { find } from 'lodash'; -import uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; -import { isPipelineMonitoringSupportedInVersion } from 'plugins/monitoring/lib/logstash/pipelines'; -import template from './index.html'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; -import { I18nContext } from 'ui/i18n'; -import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; -import { MonitoringViewBaseEuiTableController } from '../..'; -import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; - -/* - * Logstash Pipelines Listing page - */ - -const getPageData = ($injector, _api = undefined, routeOptions = {}) => { - _api; // to fix eslint - const $http = $injector.get('$http'); - const globalState = $injector.get('globalState'); - const Private = $injector.get('Private'); - - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/pipelines`; - const timeBounds = timefilter.getBounds(); - - return $http - .post(url, { - ccs: globalState.ccs, - timeRange: { - min: timeBounds.min.toISOString(), - max: timeBounds.max.toISOString(), - }, - ...routeOptions, - }) - .then(response => response.data) - .catch(err => { - const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); - return ajaxErrorHandlers(err); - }); -}; - -function makeUpgradeMessage(logstashVersions) { - if ( - !Array.isArray(logstashVersions) || - logstashVersions.length === 0 || - logstashVersions.some(isPipelineMonitoringSupportedInVersion) - ) { - return null; - } - - return 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher.'; -} - -uiRoutes.when('/logstash/pipelines', { - template, - resolve: { - clusters(Private) { - const routeInit = Private(routeInitProvider); - return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); - }, - }, - controller: class LogstashPipelinesList extends MonitoringViewBaseEuiTableController { - constructor($injector, $scope) { - super({ - title: 'Logstash Pipelines', - storageKey: 'logstash.pipelines', - getPageData, - reactNodeId: 'monitoringLogstashPipelinesApp', - $scope, - $injector, - fetchDataImmediately: false, // We want to apply pagination before sending the first request - }); - - const $route = $injector.get('$route'); - const kbnUrl = $injector.get('kbnUrl'); - const config = $injector.get('config'); - this.data = $route.current.locals.pageData; - const globalState = $injector.get('globalState'); - $scope.cluster = find($route.current.locals.clusters, { - cluster_uuid: globalState.cluster_uuid, - }); - - const renderReact = pageData => { - if (!pageData) { - return; - } - - const upgradeMessage = pageData - ? makeUpgradeMessage(pageData.clusterStatus.versions, i18n) - : null; - - const pagination = { - ...this.pagination, - totalItemCount: pageData.totalPipelineCount, - }; - - super.renderReact( - <I18nContext> - <PipelineListing - className="monitoringLogstashPipelinesTable" - onBrush={xaxis => this.onBrush({ xaxis })} - stats={pageData.clusterStatus} - data={pageData.pipelines} - {...this.getPaginationTableProps(pagination)} - upgradeMessage={upgradeMessage} - dateFormat={config.get('dateFormat')} - angular={{ - kbnUrl, - scope: $scope, - }} - /> - </I18nContext> - ); - }; - - $scope.$watch( - () => this.data, - pageData => { - renderReact(pageData); - } - ); - } - }, -}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js b/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js deleted file mode 100644 index a914aa0155e90..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/no_data/controller.js +++ /dev/null @@ -1,116 +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 React from 'react'; -import { - ClusterSettingsChecker, - NodeSettingsChecker, - Enabler, - startChecks, -} from 'plugins/monitoring/lib/elasticsearch_settings'; -import { ModelUpdater } from './model_updater'; -import { NoData } from 'plugins/monitoring/components'; -import { I18nContext } from 'ui/i18n'; -import { CODE_PATH_LICENSE } from '../../../common/constants'; -import { MonitoringViewBaseController } from '../base_controller'; -import { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; - -export class NoDataController extends MonitoringViewBaseController { - constructor($injector, $scope) { - const monitoringClusters = $injector.get('monitoringClusters'); - const kbnUrl = $injector.get('kbnUrl'); - const $http = $injector.get('$http'); - const checkers = [new ClusterSettingsChecker($http), new NodeSettingsChecker($http)]; - - const getData = async () => { - let catchReason; - try { - const monitoringClustersData = await monitoringClusters(undefined, undefined, [ - CODE_PATH_LICENSE, - ]); - if (monitoringClustersData && monitoringClustersData.length) { - kbnUrl.redirect('/home'); - return monitoringClustersData; - } - } catch (err) { - if (err && err.status === 503) { - catchReason = { - property: 'custom', - message: err.data.message, - }; - } - } - - this.errors.length = 0; - if (catchReason) { - this.reason = catchReason; - } else if (!this.isCollectionEnabledUpdating && !this.isCollectionIntervalUpdating) { - /** - * `no-use-before-define` is fine here, since getData is an async function. - * Needs to be done this way, since there is no `this` before super is executed - * */ - await startChecks(checkers, updateModel); // eslint-disable-line no-use-before-define - } - }; - - super({ - title: i18n.translate('xpack.monitoring.noData.routeTitle', { - defaultMessage: 'Setup Monitoring', - }), - getPageData: async () => await getData(), - reactNodeId: 'noDataReact', - $scope, - $injector, - }); - Object.assign(this, this.getDefaultModel()); - - //Need to set updateModel after super since there is no `this` otherwise - const { updateModel } = new ModelUpdater($scope, this); - const enabler = new Enabler($http, updateModel); - $scope.$watch( - () => this, - () => { - if (this.isCollectionEnabledUpdated && !this.reason) { - return; - } - this.render(enabler); - }, - true - ); - - this.changePath = path => kbnUrl.changePath(path); - } - - getDefaultModel() { - return { - errors: [], // errors can happen from trying to check or set ES settings - checkMessage: null, // message to show while waiting for api response - isLoading: true, // flag for in-progress state of checking for no data reason - isCollectionEnabledUpdating: false, // flags to indicate whether to show a spinner while waiting for ajax - isCollectionEnabledUpdated: false, - isCollectionIntervalUpdating: false, - isCollectionIntervalUpdated: false, - }; - } - - render(enabler) { - const props = this; - const { cloud } = npSetup.plugins; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - - this.renderReact( - <I18nContext> - <NoData - {...props} - enabler={enabler} - changePath={this.changePath} - isCloudEnabled={isCloudEnabled} - /> - </I18nContext> - ); - } -} diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/index.js b/x-pack/legacy/plugins/monitoring/public/views/no_data/index.js deleted file mode 100644 index edade513e5ab2..0000000000000 --- a/x-pack/legacy/plugins/monitoring/public/views/no_data/index.js +++ /dev/null @@ -1,16 +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 uiRoutes from 'plugins/monitoring/np_imports/ui/routes'; -import template from './index.html'; -import { NoDataController } from './controller'; - -uiRoutes - .when('/no-data', { - template, - controller: NoDataController, - }) - .otherwise({ redirectTo: '/home' }); diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js deleted file mode 100644 index e0c04411ef46b..0000000000000 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ /dev/null @@ -1,65 +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 { i18n } from '@kbn/i18n'; -import { resolve } from 'path'; -import { - MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, - KIBANA_ALERTING_ENABLED, -} from './common/constants'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; - -/** - * Configuration of dependency objects for the UI, which are needed for the - * Monitoring UI app and views and data for outside the monitoring - * app (injectDefaultVars and hacks) - * @return {Object} data per Kibana plugin uiExport schema - */ -export const getUiExports = () => { - const uiSettingDefaults = {}; - if (KIBANA_ALERTING_ENABLED) { - uiSettingDefaults[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS] = { - name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { - defaultMessage: 'Alerting email address', - }), - value: '', - description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { - defaultMessage: `The default email address to receive alerts from Stack Monitoring`, - }), - category: ['monitoring'], - }; - } - - return { - app: { - title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { - defaultMessage: 'Stack Monitoring', - }), - order: 9002, - description: i18n.translate('xpack.monitoring.uiExportsDescription', { - defaultMessage: 'Monitoring for Elastic Stack', - }), - icon: 'plugins/monitoring/icons/monitoring.svg', - euiIconType: 'monitoringApp', - linkToLastSubUrl: false, - main: 'plugins/monitoring/legacy', - category: DEFAULT_APP_CATEGORIES.management, - }, - injectDefaultVars(server) { - const config = server.config(); - return { - monitoringUiEnabled: config.get('monitoring.ui.enabled'), - monitoringLegacyEmailAddress: config.get( - 'monitoring.cluster_alerts.email_notifications.email_address' - ), - }; - }, - uiSettingDefaults, - hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], - home: ['plugins/monitoring/register_feature'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }; -}; diff --git a/x-pack/legacy/plugins/remote_clusters/common/index.ts b/x-pack/legacy/plugins/remote_clusters/common/index.ts deleted file mode 100644 index baad348d7a136..0000000000000 --- a/x-pack/legacy/plugins/remote_clusters/common/index.ts +++ /dev/null @@ -1,9 +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. - */ - -export const PLUGIN = { - ID: 'remoteClusters', -}; diff --git a/x-pack/legacy/plugins/remote_clusters/index.ts b/x-pack/legacy/plugins/remote_clusters/index.ts deleted file mode 100644 index 439cb818d8a56..0000000000000 --- a/x-pack/legacy/plugins/remote_clusters/index.ts +++ /dev/null @@ -1,40 +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 { resolve } from 'path'; -import { Legacy } from 'kibana'; - -import { PLUGIN } from './common'; - -export function remoteClusters(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.remote_clusters', - publicDir: resolve(__dirname, 'public'), - require: ['kibana'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - // TODO: Remove once CCR has migrated to NP - config(Joi: any) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - }).default(); - }, - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.remote_clusters.enabled') && config.get('xpack.index_management.enabled') - ); - }, - init() {}, - }); -} diff --git a/x-pack/legacy/plugins/remote_clusters/public/index.scss b/x-pack/legacy/plugins/remote_clusters/public/index.scss deleted file mode 100644 index 4ae11323642d8..0000000000000 --- a/x-pack/legacy/plugins/remote_clusters/public/index.scss +++ /dev/null @@ -1,28 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Remote clusters plugin styles - -// Prefix all styles with "remoteClusters" to avoid conflicts. -// Examples -// remoteClustersChart -// remoteClustersChart__legend -// remoteClustersChart__legend--small -// remoteClustersChart__legend-isLoading - -/** - * 1. Override EuiFormRow styles. Otherwise the switch will jump around when toggled on and off, - * as the 'Reset to defaults' link is added to and removed from the DOM. - * 2. Fix the positioning. - */ -.remoteClusterSkipIfUnavailableSwitch { - justify-content: flex-start !important; /* 1 */ - padding-top: $euiSizeS !important; -} - -/** - * 1. Prevent inherited flexbox layout from compressing this element on IE. - */ - .remoteClustersConnectionStatus__message { - flex-basis: auto !important; /* 1 */ -} diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index e3d6a4274e7df..f30a7cc87f318 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -20,6 +20,7 @@ export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; export const CSV_BOM_CHARS = '\ufeff'; +export const CSV_FORMULA_CHARS = ['=', '+', '-', '@']; export const WHITELISTED_JOB_CONTENT_TYPES = [ 'application/json', diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index 5a04f1a497abf..0d75b067fbe63 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { screenshotsObservableFactory } from './observable'; +export { screenshotsObservableFactory, ScreenshotsObservableFn } from './observable'; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts index 519a3289395b9..c6861ae1d17ad 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/observable.ts @@ -21,10 +21,18 @@ import { waitForVisualizations } from './wait_for_visualizations'; const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; +export type ScreenshotsObservableFn = ({ + logger, + urls, + conditionalHeaders, + layout, + browserTimezone, +}: ScreenshotObservableOpts) => Rx.Observable<ScreenshotResults[]>; + export function screenshotsObservableFactory( captureConfig: CaptureConfig, browserDriverFactory: HeadlessChromiumDriverFactory -) { +): ScreenshotsObservableFn { return function screenshotsObservable({ logger, urls, diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts index f0afade8629ab..ad35aaf003094 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.test.ts @@ -300,7 +300,7 @@ describe('CSV Execute Job', function() { }); }); - describe('Cells with formula values', () => { + describe('Warning when cells have formulas', () => { it('returns `csv_contains_formulas` when cells contain formulas', async function() { configGetStub.withArgs('csv', 'checkForFormulas').returns(true); callAsCurrentUserStub.onFirstCall().returns({ @@ -353,6 +353,7 @@ describe('CSV Execute Job', function() { it('returns no warnings when cells have no formulas', async function() { configGetStub.withArgs('csv', 'checkForFormulas').returns(true); + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(false); callAsCurrentUserStub.onFirstCall().returns({ hits: { hits: [{ _source: { one: 'foo', two: 'bar' } }], @@ -376,6 +377,33 @@ describe('CSV Execute Job', function() { expect(csvContainsFormulas).toEqual(false); }); + it('returns no warnings when cells have formulas but are escaped', async function() { + configGetStub.withArgs('csv', 'checkForFormulas').returns(true); + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { '=SUM(A1:A2)': 'foo', two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['=SUM(A1:A2)', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + + const { csv_contains_formulas: csvContainsFormulas } = await executeJob( + 'job123', + jobParams, + cancellationToken + ); + + expect(csvContainsFormulas).toEqual(false); + }); + it('returns no warnings when configured not to', async () => { configGetStub.withArgs('csv', 'checkForFormulas').returns(false); callAsCurrentUserStub.onFirstCall().returns({ @@ -446,6 +474,50 @@ describe('CSV Execute Job', function() { }); }); + describe('Escaping cells with formulas', () => { + it('escapes values with formulas', async () => { + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(true); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: `=cmd|' /C calc'!A0`, two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual("one,two\n\"'=cmd|' /C calc'!A0\",bar\n"); + }); + + it('does not escapes values with formulas', async () => { + configGetStub.withArgs('csv', 'escapeFormulaValues').returns(false); + callAsCurrentUserStub.onFirstCall().returns({ + hits: { + hits: [{ _source: { one: `=cmd|' /C calc'!A0`, two: 'bar' } }], + }, + _scroll_id: 'scrollId', + }); + + const executeJob = await executeJobFactory(mockReportingPlugin, mockLogger); + const jobParams = getJobDocPayload({ + headers: encryptedHeaders, + fields: ['one', 'two'], + conflictedTypesFields: [], + searchRequest: { index: null, body: null }, + }); + const { content } = await executeJob('job123', jobParams, cancellationToken); + + expect(content).toEqual('one,two\n"=cmd|\' /C calc\'!A0",bar\n'); + }); + }); + describe('Elasticsearch call errors', function() { it('should reject Promise if search call errors out', async function() { callAsCurrentUserStub.rejects(new Error()); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts index 376a398da274f..dbe305bc452db 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/execute_job.ts @@ -123,7 +123,7 @@ export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn< const generateCsv = createGenerateCsv(jobLogger); const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; - const { content, maxSizeReached, size, csvContainsFormulas } = await generateCsv({ + const { content, maxSizeReached, size, csvContainsFormulas, warnings } = await generateCsv({ searchRequest, fields, metaFields, @@ -136,15 +136,18 @@ export const executeJobFactory: ExecuteJobFactory<ESQueueWorkerExecuteFn< checkForFormulas: config.get('csv', 'checkForFormulas'), maxSizeBytes: config.get('csv', 'maxSizeBytes'), scroll: config.get('csv', 'scroll'), + escapeFormulaValues: config.get('csv', 'escapeFormulaValues'), }, }); + // @TODO: Consolidate these one-off warnings into the warnings array (max-size reached and csv contains formulas) return { content_type: 'text/csv', content: bom + content, max_size_reached: maxSizeReached, size, csv_contains_formulas: csvContainsFormulas, + warnings, }; }; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/cell_has_formula.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/cell_has_formula.ts new file mode 100644 index 0000000000000..b285e546ca5e3 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/cell_has_formula.ts @@ -0,0 +1,11 @@ +/* + * 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 { startsWith } from 'lodash'; +import { CSV_FORMULA_CHARS } from '../../../../common/constants'; + +export const cellHasFormulas = (val: string) => + CSV_FORMULA_CHARS.some(formulaChar => startsWith(val, formulaChar)); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts index 09f7cd2061ffb..0ec39c527d656 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/check_cells_for_formulas.ts @@ -5,8 +5,7 @@ */ import * as _ from 'lodash'; - -const formulaValues = ['=', '+', '-', '@']; +import { cellHasFormulas } from './cell_has_formula'; interface IFlattened { [header: string]: string; @@ -14,7 +13,7 @@ interface IFlattened { export const checkIfRowsHaveFormulas = (flattened: IFlattened, fields: string[]) => { const pruned = _.pick(flattened, fields); - const csvValues = [..._.keys(pruned), ...(_.values(pruned) as string[])]; + const cells = [..._.keys(pruned), ...(_.values(pruned) as string[])]; - return _.some(csvValues, cell => _.some(formulaValues, char => _.startsWith(cell, char))); + return _.some(cells, cell => cellHasFormulas(cell)); }; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts index 64b021a2aeea8..dd0f9d08b864b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.test.ts @@ -11,7 +11,7 @@ describe('escapeValue', function() { describe('quoteValues is true', function() { let escapeValue: (val: string) => string; beforeEach(function() { - escapeValue = createEscapeValue(true); + escapeValue = createEscapeValue(true, false); }); it('should escape value with spaces', function() { @@ -46,7 +46,7 @@ describe('escapeValue', function() { describe('quoteValues is false', function() { let escapeValue: (val: string) => string; beforeEach(function() { - escapeValue = createEscapeValue(false); + escapeValue = createEscapeValue(false, false); }); it('should return the value unescaped', function() { @@ -54,4 +54,34 @@ describe('escapeValue', function() { expect(escapeValue(value)).to.be(value); }); }); + + describe('escapeValues', () => { + describe('when true', () => { + let escapeValue: (val: string) => string; + beforeEach(function() { + escapeValue = createEscapeValue(true, true); + }); + + ['@', '+', '-', '='].forEach(badChar => { + it(`should escape ${badChar} injection values`, function() { + expect(escapeValue(`${badChar}cmd|' /C calc'!A0`)).to.be( + `"'${badChar}cmd|' /C calc'!A0"` + ); + }); + }); + }); + + describe('when false', () => { + let escapeValue: (val: string) => string; + beforeEach(function() { + escapeValue = createEscapeValue(true, false); + }); + + ['@', '+', '-', '='].forEach(badChar => { + it(`should not escape ${badChar} injection values`, function() { + expect(escapeValue(`${badChar}cmd|' /C calc'!A0`)).to.be(`"${badChar}cmd|' /C calc'!A0"`); + }); + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts index 563de563350e9..60e75d74b2f98 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/escape_value.ts @@ -5,15 +5,20 @@ */ import { RawValue } from './types'; +import { cellHasFormulas } from './cell_has_formula'; const nonAlphaNumRE = /[^a-zA-Z0-9]/; const allDoubleQuoteRE = /"/g; -export function createEscapeValue(quoteValues: boolean): (val: RawValue) => string { +export function createEscapeValue( + quoteValues: boolean, + escapeFormulas: boolean +): (val: RawValue) => string { return function escapeValue(val: RawValue) { if (val && typeof val === 'string') { - if (quoteValues && nonAlphaNumRE.test(val)) { - return `"${val.replace(allDoubleQuoteRE, '""')}"`; + const formulasEscaped = escapeFormulas && cellHasFormulas(val) ? "'" + val : val; + if (quoteValues && nonAlphaNumRE.test(formulasEscaped)) { + return `"${formulasEscaped.replace(allDoubleQuoteRE, '""')}"`; } } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts index 1986e68917ba8..c7996ebf832a1 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/generate_csv.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { Logger } from '../../../../types'; import { GenerateCsvParams, SavedSearchGeneratorResult } from '../../types'; import { createFlattenHit } from './flatten_hit'; @@ -26,14 +27,17 @@ export function createGenerateCsv(logger: Logger) { cancellationToken, settings, }: GenerateCsvParams): Promise<SavedSearchGeneratorResult> { - const escapeValue = createEscapeValue(settings.quoteValues); + const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); const builder = new MaxSizeStringBuilder(settings.maxSizeBytes); const header = `${fields.map(escapeValue).join(settings.separator)}\n`; + const warnings: string[] = []; + if (!builder.tryAppend(header)) { return { size: 0, content: '', maxSizeReached: true, + warnings: [], }; } @@ -82,11 +86,20 @@ export function createGenerateCsv(logger: Logger) { const size = builder.getSizeInBytes(); logger.debug(`finished generating, total size in bytes: ${size}`); + if (csvContainsFormulas && settings.escapeFormulaValues) { + warnings.push( + i18n.translate('xpack.reporting.exportTypes.csv.generateCsv.escapedFormulaValues', { + defaultMessage: 'CSV may contain formulas whose values have been escaped', + }) + ); + } + return { content: builder.getString(), - csvContainsFormulas, + csvContainsFormulas: csvContainsFormulas && !settings.escapeFormulaValues, maxSizeReached, size, + warnings, }; }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts b/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts index 529c195486bc6..40a42db352635 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv/types.d.ts @@ -87,6 +87,7 @@ export interface SavedSearchGeneratorResult { size: number; maxSizeReached: boolean; csvContainsFormulas?: boolean; + warnings: string[]; } export interface CsvResultFromSearch { @@ -109,5 +110,6 @@ export interface GenerateCsvParams { maxSizeBytes: number; scroll: ScrollConfig; checkForFormulas?: boolean; + escapeFormulaValues: boolean; }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 9757c71c19cf4..2611b74c83de9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -173,6 +173,7 @@ export async function generateCsvSearch( ...uiSettings, maxSizeBytes: config.get('csv', 'maxSizeBytes'), scroll: config.get('csv', 'scroll'), + escapeFormulaValues: config.get('csv', 'escapeFormulaValues'), timezone, }, }; diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts index c9cba64a732b6..9d3deda5d98be 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.ts @@ -5,14 +5,14 @@ */ import * as Rx from 'rxjs'; -import { createMockReportingCore, createMockBrowserDriverFactory } from '../../../../test_helpers'; -import { cryptoFactory } from '../../../../server/lib/crypto'; -import { executeJobFactory } from './index'; -import { generatePngObservableFactory } from '../lib/generate_png'; import { CancellationToken } from '../../../../common/cancellation_token'; +import { ReportingCore } from '../../../../server'; import { LevelLogger } from '../../../../server/lib'; -import { ReportingCore, CaptureConfig } from '../../../../server/types'; +import { cryptoFactory } from '../../../../server/lib/crypto'; +import { createMockReportingCore } from '../../../../test_helpers'; import { JobDocPayloadPNG } from '../../types'; +import { generatePngObservableFactory } from '../lib/generate_png'; +import { executeJobFactory } from './index'; jest.mock('../lib/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); @@ -31,8 +31,6 @@ const mockLoggerFactory = { }; const getMockLogger = () => new LevelLogger(mockLoggerFactory); -const captureConfig = {} as CaptureConfig; - const mockEncryptionKey = 'abcabcsecuresecret'; const encryptHeaders = async (headers: Record<string, string>) => { const crypto = cryptoFactory(mockEncryptionKey); @@ -46,10 +44,13 @@ beforeEach(async () => { 'server.basePath': '/sbp', }; const reportingConfig = { + index: '.reporting-2018.10.10', encryptionKey: mockEncryptionKey, 'kibanaServer.hostname': 'localhost', 'kibanaServer.port': 5601, 'kibanaServer.protocol': 'http', + 'queue.indexInterval': 'daily', + 'queue.timeout': Infinity, }; const mockReportingConfig = { get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], @@ -74,13 +75,8 @@ afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); test(`passes browserTimezone to generatePng`, async () => { const encryptedHeaders = await encryptHeaders({}); - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePngObservable = generatePngObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; @@ -94,26 +90,43 @@ test(`passes browserTimezone to generatePng`, async () => { cancellationToken ); - expect(generatePngObservable).toBeCalledWith( - expect.any(LevelLogger), - 'http://localhost:5601/sbp/app/kibana#/something', - browserTimezone, - expect.anything(), - undefined - ); + expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + LevelLogger { + "_logger": Object { + "get": [MockFunction], + }, + "_tags": Array [ + "PNG", + "execute", + "pngJobId", + ], + "warning": [Function], + }, + "http://localhost:5601/sbp/app/kibana#/something", + "UTC", + Object { + "conditions": Object { + "basePath": "/sbp", + "hostname": "localhost", + "port": 5601, + "protocol": "http", + }, + "headers": Object {}, + }, + undefined, + ], + ] + `); }); test(`returns content_type of application/png`, async () => { const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePngObservable = generatePngObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); const { content_type: contentType } = await executeJob( 'pngJobId', @@ -126,13 +139,8 @@ test(`returns content_type of application/png`, async () => { test(`returns content of generatePng getBuffer base64 encoded`, async () => { const testContent = 'test content'; - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePngObservable = generatePngObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); - (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); + const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; + generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const executeJob = await executeJobFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 113da92d1862f..0ffd42d0b52f9 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -25,13 +25,11 @@ export const executeJobFactory: QueuedPngExecutorFactory = async function execut parentLogger: Logger ) { const config = reporting.getConfig(); - const captureConfig = config.get('capture'); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); return async function executeJob(jobId: string, job: JobDocPayloadPNG, cancellationToken: any) { - const browserDriverFactory = await reporting.getBrowserDriverFactory(); - const generatePngObservable = generatePngObservableFactory(captureConfig, browserDriverFactory); + const generatePngObservable = await generatePngObservableFactory(reporting); const jobLogger = logger.clone([jobId]); const process$: Rx.Observable<JobDocOutput> = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index a15541d99f6fb..c03ea170f76ee 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -6,19 +6,15 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; +import { ReportingCore } from '../../../../server'; import { LevelLogger } from '../../../../server/lib'; -import { CaptureConfig } from '../../../../server/types'; -import { ConditionalHeaders, HeadlessChromiumDriverFactory } from '../../../../types'; +import { ConditionalHeaders } from '../../../../types'; import { LayoutParams } from '../../../common/layouts/layout'; import { PreserveLayout } from '../../../common/layouts/preserve_layout'; -import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { ScreenshotResults } from '../../../common/lib/screenshots/types'; -export function generatePngObservableFactory( - captureConfig: CaptureConfig, - browserDriverFactory: HeadlessChromiumDriverFactory -) { - const screenshotsObservable = screenshotsObservableFactory(captureConfig, browserDriverFactory); +export async function generatePngObservableFactory(reporting: ReportingCore) { + const getScreenshots = await reporting.getScreenshotsObservable(); return function generatePngObservable( logger: LevelLogger, @@ -32,7 +28,7 @@ export function generatePngObservableFactory( } const layout = new PreserveLayout(layoutParams.dimensions); - const screenshots$ = screenshotsObservable({ + const screenshots$ = getScreenshots({ logger, urls: [url], conditionalHeaders, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts index c3c0d38584bc1..ae18c0f4f9f4b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.ts @@ -5,11 +5,11 @@ */ import * as Rx from 'rxjs'; -import { createMockReportingCore, createMockBrowserDriverFactory } from '../../../../test_helpers'; +import { createMockReportingCore } from '../../../../test_helpers'; import { cryptoFactory } from '../../../../server/lib/crypto'; import { LevelLogger } from '../../../../server/lib'; import { CancellationToken } from '../../../../types'; -import { ReportingCore, CaptureConfig } from '../../../../server/types'; +import { ReportingCore } from '../../../../server'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { JobDocPayloadPDF } from '../../types'; import { executeJobFactory } from './index'; @@ -22,8 +22,6 @@ const cancellationToken = ({ on: jest.fn(), } as unknown) as CancellationToken; -const captureConfig = {} as CaptureConfig; - const mockLoggerFactory = { get: jest.fn().mockImplementation(() => ({ error: jest.fn(), @@ -72,16 +70,64 @@ beforeEach(async () => { afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); +test(`passes browserTimezone to generatePdf`, async () => { + const encryptedHeaders = await encryptHeaders({}); + const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; + generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + + const executeJob = await executeJobFactory(mockReporting, getMockLogger()); + const browserTimezone = 'UTC'; + await executeJob( + 'pdfJobId', + getJobDocPayload({ + relativeUrl: '/app/kibana#/something', + browserTimezone, + headers: encryptedHeaders, + }), + cancellationToken + ); + + expect(generatePdfObservable.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + LevelLogger { + "_logger": Object { + "get": [MockFunction], + }, + "_tags": Array [ + "printable_pdf", + "execute", + "pdfJobId", + ], + "warning": [Function], + }, + undefined, + Array [ + "http://localhost:5601/sbp/app/kibana#/something", + ], + "UTC", + Object { + "conditions": Object { + "basePath": "/sbp", + "hostname": "localhost", + "port": 5601, + "protocol": "http", + }, + "headers": Object {}, + }, + undefined, + false, + ], + ] + `); +}); + test(`returns content_type of application/pdf`, async () => { const logger = getMockLogger(); const executeJob = await executeJobFactory(mockReporting, logger); - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(logger); const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = generatePdfObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); const { content_type: contentType } = await executeJob( @@ -94,12 +140,7 @@ test(`returns content_type of application/pdf`, async () => { test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const testContent = 'test content'; - const mockBrowserDriverFactory = await createMockBrowserDriverFactory(getMockLogger()); - - const generatePdfObservable = generatePdfObservableFactory( - captureConfig, - mockBrowserDriverFactory - ); + const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const executeJob = await executeJobFactory(mockReporting, getMockLogger()); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index dbdccb6160a6e..3d69042b6c7ab 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -26,14 +26,12 @@ export const executeJobFactory: QueuedPdfExecutorFactory = async function execut parentLogger: Logger ) { const config = reporting.getConfig(); - const captureConfig = config.get('capture'); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PDF_JOB_TYPE, 'execute']); return async function executeJob(jobId: string, job: JobDocPayloadPDF, cancellationToken: any) { - const browserDriverFactory = await reporting.getBrowserDriverFactory(); - const generatePdfObservable = generatePdfObservableFactory(captureConfig, browserDriverFactory); + const generatePdfObservable = await generatePdfObservableFactory(reporting); const jobLogger = logger.clone([jobId]); const process$: Rx.Observable<JobDocOutput> = Rx.of(1).pipe( diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index a62b7ec7013a5..c882ef682f952 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -7,12 +7,11 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; +import { ReportingCore } from '../../../../server'; import { LevelLogger } from '../../../../server/lib'; -import { CaptureConfig } from '../../../../server/types'; -import { ConditionalHeaders, HeadlessChromiumDriverFactory } from '../../../../types'; +import { ConditionalHeaders } from '../../../../types'; import { createLayout } from '../../../common/layouts'; import { LayoutInstance, LayoutParams } from '../../../common/layouts/layout'; -import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { ScreenshotResults } from '../../../common/lib/screenshots/types'; // @ts-ignore untyped module import { pdf } from './pdf'; @@ -27,11 +26,10 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { return null; }; -export function generatePdfObservableFactory( - captureConfig: CaptureConfig, - browserDriverFactory: HeadlessChromiumDriverFactory -) { - const screenshotsObservable = screenshotsObservableFactory(captureConfig, browserDriverFactory); +export async function generatePdfObservableFactory(reporting: ReportingCore) { + const config = reporting.getConfig(); + const captureConfig = config.get('capture'); + const getScreenshots = await reporting.getScreenshotsObservable(); return function generatePdfObservable( logger: LevelLogger, @@ -43,7 +41,7 @@ export function generatePdfObservableFactory( logo?: string ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { const layout = createLayout(captureConfig, layoutParams) as LayoutInstance; - const screenshots$ = screenshotsObservable({ + const screenshots$ = getScreenshots({ logger, urls, conditionalHeaders, diff --git a/x-pack/legacy/plugins/reporting/server/core.ts b/x-pack/legacy/plugins/reporting/server/core.ts index 9be61d091b00e..0b243f13adb80 100644 --- a/x-pack/legacy/plugins/reporting/server/core.ts +++ b/x-pack/legacy/plugins/reporting/server/core.ts @@ -24,6 +24,10 @@ import { ReportingConfig, ReportingConfigType } from './config'; import { checkLicenseFactory, getExportTypesRegistry, LevelLogger } from './lib'; import { registerRoutes } from './routes'; import { ReportingSetupDeps } from './types'; +import { + screenshotsObservableFactory, + ScreenshotsObservableFn, +} from '../export_types/common/lib/screenshots'; interface ReportingInternalSetup { browserDriverFactory: HeadlessChromiumDriverFactory; @@ -95,13 +99,13 @@ export class ReportingCore { return (await this.getPluginStartDeps()).enqueueJob; } - public async getBrowserDriverFactory(): Promise<HeadlessChromiumDriverFactory> { - return (await this.getPluginSetupDeps()).browserDriverFactory; - } - public getConfig(): ReportingConfig { return this.config; } + public async getScreenshotsObservable(): Promise<ScreenshotsObservableFn> { + const { browserDriverFactory } = await this.getPluginSetupDeps(); + return screenshotsObservableFactory(this.config.get('capture'), browserDriverFactory); + } /* * Outside dependencies diff --git a/x-pack/legacy/plugins/reporting/server/index.ts b/x-pack/legacy/plugins/reporting/server/index.ts index c564963e363cc..4288a37fe6adc 100644 --- a/x-pack/legacy/plugins/reporting/server/index.ts +++ b/x-pack/legacy/plugins/reporting/server/index.ts @@ -14,3 +14,5 @@ export const plugin = (context: PluginInitializerContext, config: ReportingConfi export { ReportingPlugin } from './plugin'; export { ReportingConfig, ReportingCore }; + +export { PreserveLayout, PrintLayout } from '../export_types/common/layouts'; diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 8230ee889ae05..560cc943ed45e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -16,21 +16,23 @@ export async function createQueueFactory<JobParamsType, JobPayloadType>( logger: Logger ): Promise<ESQueueInstance> { const config = reporting.getConfig(); - const queueConfig = config.get('queue'); - const index = config.get('index'); - const elasticsearch = await reporting.getElasticsearchService(); + const queueIndexInterval = config.get('queue', 'indexInterval'); + const queueTimeout = config.get('queue', 'timeout'); + const queueIndex = config.get('index'); + const isPollingEnabled = config.get('queue', 'pollEnabled'); + const elasticsearch = await reporting.getElasticsearchService(); const queueOptions = { - interval: queueConfig.indexInterval, - timeout: queueConfig.timeout, + interval: queueIndexInterval, + timeout: queueTimeout, dateSeparator: '.', client: elasticsearch.dataClient, logger: createTaggedLogger(logger, ['esqueue', 'queue-worker']), }; - const queue: ESQueueInstance = new Esqueue(index, queueOptions); + const queue: ESQueueInstance = new Esqueue(queueIndex, queueOptions); - if (queueConfig.pollEnabled) { + if (isPollingEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed const createWorker = createWorkerFactory(reporting, logger); await createWorker(queue); diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index 5a062a693b468..3e87337dc4355 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -26,12 +26,12 @@ interface ConfirmedJob { } export function enqueueJobFactory(reporting: ReportingCore, parentLogger: Logger): EnqueueJobFn { - const logger = parentLogger.clone(['queue-job']); const config = reporting.getConfig(); - const captureConfig = config.get('capture'); - const queueConfig = config.get('queue'); - const browserType = captureConfig.browser.type; - const maxAttempts = captureConfig.maxAttempts; + const queueTimeout = config.get('queue', 'timeout'); + const browserType = config.get('capture', 'browser', 'type'); + const maxAttempts = config.get('capture', 'maxAttempts'); + + const logger = parentLogger.clone(['queue-job']); return async function enqueueJob<JobParamsType>( exportTypeId: string, @@ -53,7 +53,7 @@ export function enqueueJobFactory(reporting: ReportingCore, parentLogger: Logger const payload = await createJob(jobParams, headers, request); const options = { - timeout: queueConfig.timeout, + timeout: queueTimeout, created_by: get(user, 'username', false), browser_type: browserType, max_attempts: maxAttempts, diff --git a/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap new file mode 100644 index 0000000000000..aa22c3f66df18 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`data modeling with empty data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 0, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 0, + }, + "_all": 0, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "failed": 0, + }, + "statuses": Object {}, +} +`; + +exports[`data modeling with normal looking usage data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 3, + }, + "_all": 12, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 0, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 1, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "completed_with_warnings": 1, + "failed": 0, + }, + "statuses": Object { + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 1, + "csv": Object { + "available": true, + "total": 0, + }, + "printable_pdf": Object { + "app": Object { + "dashboard": 0, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 0, + "print": 0, + }, + "total": 0, + }, + "status": Object { + "completed": 0, + "completed_with_warnings": 1, + "failed": 0, + }, + "statuses": Object { + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 6, + "dashboard": 0, + "visualization": 3, + }, + "available": true, + "layout": Object { + "preserve_layout": 9, + "print": 0, + }, + "total": 9, + }, + "status": Object { + "completed": 10, + "completed_with_warnings": 1, + "failed": 1, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "visualization": 1, + }, + "printable_pdf": Object { + "canvas workpad": 6, + "visualization": 3, + }, + }, + "completed_with_warnings": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + "failed": Object { + "PNG": Object { + "dashboard": 1, + }, + }, + }, +} +`; + +exports[`data modeling with sparse data 1`] = ` +Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "available": true, + "browser_type": undefined, + "csv": Object { + "available": true, + "total": 1, + }, + "enabled": true, + "last7Days": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "csv": Object { + "available": true, + "total": 1, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, + }, + "lastDay": Object { + "PNG": Object { + "available": true, + "total": 1, + }, + "_all": 4, + "csv": Object { + "available": true, + "total": 1, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, + }, + "printable_pdf": Object { + "app": Object { + "canvas workpad": 1, + "dashboard": 1, + "visualization": 0, + }, + "available": true, + "layout": Object { + "preserve_layout": 2, + "print": 0, + }, + "total": 2, + }, + "status": Object { + "completed": 4, + "failed": 0, + }, + "statuses": Object { + "completed": Object { + "PNG": Object { + "dashboard": 1, + }, + "csv": Object {}, + "printable_pdf": Object { + "canvas workpad": 1, + "dashboard": 1, + }, + }, + }, +} +`; diff --git a/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts b/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts index 359bcc45230c3..ef985d2dd1cf3 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/decorate_range_stats.ts @@ -6,7 +6,7 @@ import { uniq } from 'lodash'; import { CSV_JOB_TYPE, PDF_JOB_TYPE, PNG_JOB_TYPE } from '../../common/constants'; -import { AvailableTotal, FeatureAvailabilityMap, RangeStats, ExportType } from './types'; +import { AvailableTotal, ExportType, FeatureAvailabilityMap, RangeStats } from './types'; function getForFeature( range: Partial<RangeStats>, @@ -47,6 +47,7 @@ export const decorateRangeStats = ( const { _all: rangeAll, status: rangeStatus, + statuses: rangeStatusByApp, [PDF_JOB_TYPE]: rangeStatsPdf, ...rangeStatsBasic } = rangeStats; @@ -73,6 +74,7 @@ export const decorateRangeStats = ( const resultStats = { _all: rangeAll || 0, status: { completed: 0, failed: 0, ...rangeStatus }, + statuses: rangeStatusByApp, ...rangePdf, ...rangeBasic, } as RangeStats; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts index e9523d9e70202..2c3bb8f4bf71c 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts @@ -11,13 +11,13 @@ import { ReportingConfig } from '../types'; import { decorateRangeStats } from './decorate_range_stats'; import { getExportTypesHandler } from './get_export_type_handler'; import { - AggregationBuckets, - AggregationResults, + AggregationResultBuckets, FeatureAvailabilityMap, JobTypes, KeyCountBucket, - RangeAggregationResults, RangeStats, + SearchResponse, + StatusByAppBucket, } from './types'; type XPackInfo = XPackMainPlugin['info']; @@ -29,6 +29,7 @@ const LAYOUT_TYPES_FIELD = 'meta.layout.keyword'; const OBJECT_TYPES_KEY = 'objectTypes'; const OBJECT_TYPES_FIELD = 'meta.objectType.keyword'; const STATUS_TYPES_KEY = 'statusTypes'; +const STATUS_BY_APP_KEY = 'statusByApp'; const STATUS_TYPES_FIELD = 'status'; const DEFAULT_TERMS_SIZE = 10; @@ -38,16 +39,30 @@ const PRINTABLE_PDF_JOBTYPE = 'printable_pdf'; const getKeyCount = (buckets: KeyCountBucket[]): { [key: string]: number } => buckets.reduce((accum, { key, doc_count: count }) => ({ ...accum, [key]: count }), {}); -function getAggStats(aggs: AggregationResults) { - const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY] as AggregationBuckets; - const jobTypes: JobTypes = jobBuckets.reduce( +// indexes some key/count buckets by statusType > jobType > appName: statusCount +const getAppStatuses = (buckets: StatusByAppBucket[]) => + buckets.reduce((statuses, statusBucket) => { + return { + ...statuses, + [statusBucket.key]: statusBucket.jobTypes.buckets.reduce((jobTypes, job) => { + return { + ...jobTypes, + [job.key]: job.appNames.buckets.reduce((apps, app) => { + return { + ...apps, + [app.key]: app.doc_count, + }; + }, {}), + }; + }, {}), + }; + }, {}); + +function getAggStats(aggs: AggregationResultBuckets): RangeStats { + const { buckets: jobBuckets } = aggs[JOB_TYPES_KEY]; + const jobTypes = jobBuckets.reduce( (accum: JobTypes, { key, doc_count: count }: { key: string; doc_count: number }) => { - return { - ...accum, - [key]: { - total: count, - }, - }; + return { ...accum, [key]: { total: count } }; }, {} as JobTypes ); @@ -55,8 +70,8 @@ function getAggStats(aggs: AggregationResults) { // merge pdf stats into pdf jobtype key const pdfJobs = jobTypes[PRINTABLE_PDF_JOBTYPE]; if (pdfJobs) { - const pdfAppBuckets = get(aggs[OBJECT_TYPES_KEY], '.pdf.buckets', []); - const pdfLayoutBuckets = get(aggs[LAYOUT_TYPES_KEY], '.pdf.buckets', []); + const pdfAppBuckets = get<KeyCountBucket[]>(aggs[OBJECT_TYPES_KEY], '.pdf.buckets', []); + const pdfLayoutBuckets = get<KeyCountBucket[]>(aggs[LAYOUT_TYPES_KEY], '.pdf.buckets', []); pdfJobs.app = getKeyCount(pdfAppBuckets) as { visualization: number; dashboard: number; @@ -69,26 +84,35 @@ function getAggStats(aggs: AggregationResults) { const all = aggs.doc_count as number; let statusTypes = {}; - const statusBuckets = get(aggs[STATUS_TYPES_KEY], 'buckets', []); + const statusBuckets = get<KeyCountBucket[]>(aggs[STATUS_TYPES_KEY], 'buckets', []); if (statusBuckets) { statusTypes = getKeyCount(statusBuckets); } - return { _all: all, status: statusTypes, ...jobTypes }; + let statusByApp = {}; + const statusAppBuckets = get<StatusByAppBucket[]>(aggs[STATUS_BY_APP_KEY], 'buckets', []); + if (statusAppBuckets) { + statusByApp = getAppStatuses(statusAppBuckets); + } + + return { _all: all, status: statusTypes, statuses: statusByApp, ...jobTypes }; } +type SearchAggregation = SearchResponse['aggregations']['ranges']['buckets']; + type RangeStatSets = Partial< RangeStats & { lastDay: RangeStats; last7Days: RangeStats; } >; -async function handleResponse(response: AggregationResults): Promise<RangeStatSets> { - const buckets = get(response, 'aggregations.ranges.buckets'); + +async function handleResponse(response: SearchResponse): Promise<RangeStatSets> { + const buckets = get<SearchAggregation>(response, 'aggregations.ranges.buckets'); if (!buckets) { return {}; } - const { lastDay, last7Days, all } = buckets as RangeAggregationResults; + const { lastDay, last7Days, all } = buckets; const lastDayUsage = lastDay ? getAggStats(lastDay) : ({} as RangeStats); const last7DaysUsage = last7Days ? getAggStats(last7Days) : ({} as RangeStats); @@ -126,6 +150,17 @@ export async function getReportingUsage( aggs: { [JOB_TYPES_KEY]: { terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, [STATUS_TYPES_KEY]: { terms: { field: STATUS_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, + [STATUS_BY_APP_KEY]: { + terms: { field: 'status', size: DEFAULT_TERMS_SIZE }, + aggs: { + jobTypes: { + terms: { field: JOB_TYPES_FIELD, size: DEFAULT_TERMS_SIZE }, + aggs: { + appNames: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } }, // NOTE Discover/CSV export is missing the 'meta.objectType' field, so Discover/CSV results are missing for this agg + }, + }, + }, + }, [OBJECT_TYPES_KEY]: { filter: { term: { jobtype: PRINTABLE_PDF_JOBTYPE } }, aggs: { pdf: { terms: { field: OBJECT_TYPES_FIELD, size: DEFAULT_TERMS_SIZE } } }, @@ -141,7 +176,7 @@ export async function getReportingUsage( }; return callCluster('search', params) - .then((response: AggregationResults) => handleResponse(response)) + .then((response: SearchResponse) => handleResponse(response)) .then((usage: RangeStatSets) => { // Allow this to explicitly throw an exception if/when this config is deprecated, // because we shouldn't collect browserType in that case! diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts index dbc674ce36ec8..61b736a3e4d8c 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -12,6 +12,7 @@ import { getReportingUsageCollector, } from './reporting_usage_collector'; import { ReportingConfig } from '../types'; +import { SearchResponse } from './types'; const exportTypesRegistry = getExportTypesRegistry(); @@ -61,7 +62,7 @@ const getMockReportingConfig = () => ({ get: () => {}, kbnConfig: { get: () => '' }, }); -const getResponseMock = (customization = {}) => customization; +const getResponseMock = (base = {}) => base; describe('license checks', () => { let mockConfig: ReportingConfig; @@ -206,212 +207,143 @@ describe('data modeling', () => { ranges: { buckets: { all: { - doc_count: 54, - layoutTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'preserve_layout', doc_count: 13 }, - { key: 'print', doc_count: 10 }, - ], - }, - }, - objectTypes: { - doc_count: 23, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 23 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'pending', doc_count: 33 }, - { key: 'completed', doc_count: 20 }, - { key: 'processing', doc_count: 1 }, - ], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 27 }, - { key: 'printable_pdf', doc_count: 23 }, - { key: 'PNG', doc_count: 4 }, - ], - }, - }, - lastDay: { - doc_count: 11, - layoutTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'print', doc_count: 2 }], - }, - }, - objectTypes: { - doc_count: 2, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 2 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 11 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'csv', doc_count: 5 }, - { key: 'PNG', doc_count: 4 }, - { key: 'printable_pdf', doc_count: 2 }, - ], - }, + doc_count: 12, + jobTypes: { buckets: [ { doc_count: 9, key: 'printable_pdf' }, { doc_count: 3, key: 'PNG' }, ], }, + layoutTypes: { doc_count: 9, pdf: { buckets: [{ doc_count: 9, key: 'preserve_layout' }] }, }, + objectTypes: { doc_count: 9, pdf: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, }, + statusByApp: { buckets: [ { doc_count: 10, jobTypes: { buckets: [ { appNames: { buckets: [ { doc_count: 6, key: 'canvas workpad' }, { doc_count: 3, key: 'visualization' }, ], }, doc_count: 9, key: 'printable_pdf', }, { appNames: { buckets: [{ doc_count: 1, key: 'visualization' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'failed', }, ], }, + statusTypes: { buckets: [ { doc_count: 10, key: 'completed' }, { doc_count: 1, key: 'completed_with_warnings' }, { doc_count: 1, key: 'failed' }, ], }, }, last7Days: { - doc_count: 27, - layoutTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'print', doc_count: 10 }, - { key: 'preserve_layout', doc_count: 3 }, - ], - }, - }, - objectTypes: { - doc_count: 13, - pdf: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'dashboard', doc_count: 13 }], - }, - }, - statusTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'pending', doc_count: 27 }], - }, - jobTypes: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'printable_pdf', doc_count: 13 }, - { key: 'csv', doc_count: 10 }, - { key: 'PNG', doc_count: 4 }, - ], - }, + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, + }, + lastDay: { + doc_count: 1, + jobTypes: { buckets: [{ doc_count: 1, key: 'PNG' }] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [ { doc_count: 1, jobTypes: { buckets: [ { appNames: { buckets: [{ doc_count: 1, key: 'dashboard' }] }, doc_count: 1, key: 'PNG', }, ], }, key: 'completed_with_warnings', }, ], }, + statusTypes: { buckets: [{ doc_count: 1, key: 'completed_with_warnings' }] }, }, }, }, }, - }) + } as SearchResponse) // prettier-ignore ) ); const usageStats = await fetch(callClusterMock as any); - expect(usageStats).toMatchInlineSnapshot(` - Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 54, - "available": true, - "browser_type": undefined, - "csv": Object { - "available": true, - "total": 27, - }, - "enabled": true, - "last7Days": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 27, - "csv": Object { - "available": true, - "total": 10, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 13, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 3, - "print": 10, + expect(usageStats).toMatchSnapshot(); + }); + + test('with sparse data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + last7Days: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + lastDay: { + doc_count: 4, + layoutTypes: { doc_count: 2, pdf: { buckets: [{ key: 'preserve_layout', doc_count: 2 }] }, }, + statusByApp: { buckets: [ { key: 'completed', doc_count: 4, jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2, appNames: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, { key: 'PNG', doc_count: 1, appNames: { buckets: [{ key: 'dashboard', doc_count: 1 }] }, }, { key: 'csv', doc_count: 1, appNames: { buckets: [] } }, ], }, }, ], }, + objectTypes: { doc_count: 2, pdf: { buckets: [ { key: 'canvas workpad', doc_count: 1 }, { key: 'dashboard', doc_count: 1 }, ], }, }, + statusTypes: { buckets: [{ key: 'completed', doc_count: 4 }] }, + jobTypes: { buckets: [ { key: 'printable_pdf', doc_count: 2 }, { key: 'PNG', doc_count: 1 }, { key: 'csv', doc_count: 1 }, ], }, + }, + }, }, - "total": 13, }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 27, - }, - }, - "lastDay": Object { - "PNG": Object { - "available": true, - "total": 4, - }, - "_all": 11, - "csv": Object { - "available": true, - "total": 5, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 2, - "visualization": 0, - }, - "available": true, - "layout": Object { - "preserve_layout": 0, - "print": 2, + } as SearchResponse) // prettier-ignore + ) + ); + + const usageStats = await fetch(callClusterMock as any); + expect(usageStats).toMatchSnapshot(); + }); + + test('with empty data', async () => { + const mockConfig = getMockReportingConfig(); + const plugins = getPluginsMock(); + const { fetch } = getReportingUsageCollector( + mockConfig, + plugins.usageCollection, + plugins.__LEGACY.plugins.xpack_main.info, + exportTypesRegistry, + function isReady() { + return Promise.resolve(true); + } + ); + const callClusterMock = jest.fn(() => + Promise.resolve( + getResponseMock({ + aggregations: { + ranges: { + buckets: { + all: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + last7Days: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + lastDay: { + doc_count: 0, + jobTypes: { buckets: [] }, + layoutTypes: { doc_count: 0, pdf: { buckets: [] } }, + objectTypes: { doc_count: 0, pdf: { buckets: [] } }, + statusByApp: { buckets: [] }, + statusTypes: { buckets: [] }, + }, + }, }, - "total": 2, - }, - "status": Object { - "completed": 0, - "failed": 0, - "pending": 11, - }, - }, - "printable_pdf": Object { - "app": Object { - "dashboard": 23, - "visualization": 0, }, - "available": true, - "layout": Object { - "preserve_layout": 13, - "print": 10, - }, - "total": 23, - }, - "status": Object { - "completed": 20, - "failed": 0, - "pending": 33, - "processing": 1, - }, - } - `); + } as SearchResponse) + ) + ); + const usageStats = await fetch(callClusterMock as any); + expect(usageStats).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts index 98e025ccf661e..83f1701863355 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/types.d.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/types.d.ts @@ -4,15 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface AvailableTotal { - available: boolean; - total: number; -} - -interface StatusCounts { - [statusType: string]: number; -} - export interface KeyCountBucket { key: string; doc_count: number; @@ -20,22 +11,53 @@ export interface KeyCountBucket { export interface AggregationBuckets { buckets: KeyCountBucket[]; - pdf?: { - buckets: KeyCountBucket[]; - }; } -/* - * Mapped Types and Intersection Types - */ +export interface StatusByAppBucket { + key: string; + doc_count: number; + jobTypes: { + buckets: Array<{ + doc_count: number; + key: string; + appNames: AggregationBuckets; + }>; + }; +} -type AggregationKeys = 'jobTypes' | 'layoutTypes' | 'objectTypes' | 'statusTypes'; -export type AggregationResults = { [K in AggregationKeys]: AggregationBuckets } & { +export interface AggregationResultBuckets { + jobTypes: AggregationBuckets; + layoutTypes: { + doc_count: number; + pdf: AggregationBuckets; + }; + objectTypes: { + doc_count: number; + pdf: AggregationBuckets; + }; + statusTypes: AggregationBuckets; + statusByApp: { + buckets: StatusByAppBucket[]; + }; doc_count: number; -}; +} -type RangeAggregationKeys = 'all' | 'lastDay' | 'last7Days'; -export type RangeAggregationResults = { [K in RangeAggregationKeys]?: AggregationResults }; +export interface SearchResponse { + aggregations: { + ranges: { + buckets: { + all: AggregationResultBuckets; + last7Days: AggregationResultBuckets; + lastDay: AggregationResultBuckets; + }; + }; + }; +} + +export interface AvailableTotal { + available: boolean; + total: number; +} type BaseJobTypeKeys = 'csv' | 'PNG'; export type JobTypes = { [K in BaseJobTypeKeys]: AvailableTotal } & { @@ -51,9 +73,22 @@ export type JobTypes = { [K in BaseJobTypeKeys]: AvailableTotal } & { }; }; +interface StatusCounts { + [statusType: string]: number; +} + +interface StatusByAppCounts { + [statusType: string]: { + [jobType: string]: { + [appName: string]: number; + }; + }; +} + export type RangeStats = JobTypes & { _all: number; status: StatusCounts; + statuses: StatusByAppCounts; }; export type ExportType = 'csv' | 'printable_pdf' | 'PNG'; diff --git a/x-pack/legacy/plugins/rollup/README.md b/x-pack/legacy/plugins/rollup/README.md deleted file mode 100644 index 3647be38b6a09..0000000000000 --- a/x-pack/legacy/plugins/rollup/README.md +++ /dev/null @@ -1,52 +0,0 @@ -## Summary -Welcome to the Kibana rollup plugin! This plugin provides Kibana support for [Elasticsearch's rollup feature](https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-rollup.html). Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. - -This plugin allows Kibana to: - -* Create and manage rollup jobs -* Create rollup index patterns -* Create visualizations from rollup index patterns -* Identify rollup indices in Index Management - -The rest of this doc dives into the implementation details of each of the above functionality. - ---- - -## Create and manage rollup jobs - -The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives [here](../../../plugins/rollup/public/crud_app) and uses endpoints registered [here](server/routes/api/jobs.js). - -Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-getting-started.html) to understand rollup indices and how to create rollup jobs. - -## Create rollup index patterns - -Kibana uses index patterns to consume and visualize rollup indices. Typically, Kibana can inspect the indices captured by an index pattern, identify its aggregations and fields, and determine how to consume the data. Rollup indices don't contain this type of information, so we predefine how to consume a rollup index pattern with the type and typeMeta fields on the index pattern saved object. All rollup index patterns have `type` defined as "rollup" and `typeMeta` defined as an object of the index pattern's capabilities. - -In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](../../../plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard: - -1. Adds a `Rollup` badge to rollup indices using `getIndexTags()`. -2. Enforces index pattern rules using `checkIndicesForErrors()`. Rollup index patterns must match **one** rollup index, and optionally, any number of regular indices. A rollup index pattern configured with one or more regular indices is known as a "hybrid" index pattern. This allows the user to visualize historical (rollup) data and live (regular) data in the same visualization. -3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns.js), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names. -4. Writes additional information about aggregations, fields, histogram interval, and date histogram interval and timezone to the rollup index pattern saved object using `getIndexPatternMappings()`. This collection of information is referred to as its "capabilities". - -Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](../../../plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js) extension points. - -## Create visualizations from rollup index patterns - -This plugin enables the user to create visualizations from rollup data using the Visualize app, excluding TSVB, Vega, and Timelion. When Visualize sends search requests, this plugin routes the requests to the [Elasticsearch rollup search endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html), which searches the special document structure within rollup indices. The visualization options available to users are based on the capabilities of the rollup index pattern they're visualizing. - -Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](../../../plugins/rollup/public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js). - -Limiting visualization editor options is done by [registering configs](../../../plugins/rollup/public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit: -* Available aggregation types -* Available fields for a particular aggregation -* Default and base interval for histogram aggregation -* Default and base interval, and time zone, for date histogram aggregation - -## Identify rollup indices in Index Management - -In Index Management, similar to system indices, rollup indices are hidden by default. A toggle is provided to show rollup indices and add a badge to the table rows. This is done by using Index Management's extension points. - -The toggle and badge are registered on client-side [here](../../../plugins/rollup/public/extend_index_management/index.js). - -Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.js). diff --git a/x-pack/legacy/plugins/rollup/common/index.ts b/x-pack/legacy/plugins/rollup/common/index.ts deleted file mode 100644 index 526af055a3ef6..0000000000000 --- a/x-pack/legacy/plugins/rollup/common/index.ts +++ /dev/null @@ -1,19 +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 { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants'; - -export const PLUGIN = { - ID: 'rollup', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, - getI18nName: (i18n: any): string => { - return i18n.translate('xpack.rollupJobs.appName', { - defaultMessage: 'Rollup jobs', - }); - }, -}; - -export * from '../../../../plugins/rollup/common'; diff --git a/x-pack/legacy/plugins/rollup/index.ts b/x-pack/legacy/plugins/rollup/index.ts deleted file mode 100644 index f33ae7cfee0a2..0000000000000 --- a/x-pack/legacy/plugins/rollup/index.ts +++ /dev/null @@ -1,43 +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 { PluginInitializerContext } from 'src/core/server'; -import { RollupSetup } from '../../../plugins/rollup/server'; -import { PLUGIN } from './common'; -import { plugin } from './server'; - -export function rollup(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.rollup', - require: ['kibana', 'elasticsearch', 'xpack_main'], - init(server: any) { - const { core: coreSetup, plugins } = server.newPlatform.setup; - const { usageCollection, visTypeTimeseries, indexManagement } = plugins; - - const rollupSetup = (plugins.rollup as unknown) as RollupSetup; - - const initContext = ({ - config: rollupSetup.__legacy.config, - logger: rollupSetup.__legacy.logger, - } as unknown) as PluginInitializerContext; - - const rollupPluginInstance = plugin(initContext); - - rollupPluginInstance.setup(coreSetup, { - usageCollection, - visTypeTimeseries, - indexManagement, - __LEGACY: { - plugins: { - xpack_main: server.plugins.xpack_main, - rollup: server.plugins[PLUGIN.ID], - }, - }, - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json deleted file mode 100644 index 78458c9218be3..0000000000000 --- a/x-pack/legacy/plugins/rollup/kibana.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "rollup", - "version": "kibana", - "requiredPlugins": [ - "home", - "index_management", - "visTypeTimeseries", - "indexPatternManagement" - ], - "optionalPlugins": [ - "usageCollection" - ], - "server": true, - "ui": false -} diff --git a/x-pack/legacy/plugins/rollup/server/index.ts b/x-pack/legacy/plugins/rollup/server/index.ts deleted file mode 100644 index 6bbd00ac6576e..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/index.ts +++ /dev/null @@ -1,9 +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 { PluginInitializerContext } from 'src/core/server'; -import { RollupsServerPlugin } from './plugin'; - -export const plugin = (ctx: PluginInitializerContext) => new RollupsServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/jobs.js b/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/jobs.js deleted file mode 100644 index eb16b211da3fd..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/jobs.js +++ /dev/null @@ -1,98 +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. - */ - -export const jobs = [ - { - "job_id" : "foo1", - "rollup_index" : "foo_rollup", - "index_pattern" : "foo-*", - "fields" : { - "node" : [ - { - "agg" : "terms" - } - ], - "temperature" : [ - { - "agg" : "min" - }, - { - "agg" : "max" - }, - { - "agg" : "sum" - } - ], - "timestamp" : [ - { - "agg" : "date_histogram", - "time_zone" : "UTC", - "interval" : "1h", - "delay": "7d" - } - ], - "voltage" : [ - { - "agg" : "histogram", - "interval": 5 - }, - { - "agg" : "sum" - } - ] - } - }, - { - "job_id" : "foo2", - "rollup_index" : "foo_rollup", - "index_pattern" : "foo-*", - "fields" : { - "host" : [ - { - "agg" : "terms" - } - ], - "timestamp" : [ - { - "agg" : "date_histogram", - "time_zone" : "UTC", - "interval" : "1h", - "delay": "7d" - } - ], - "voltage" : [ - { - "agg" : "histogram", - "interval": 20 - } - ] - } - }, - { - "job_id" : "foo3", - "rollup_index" : "foo_rollup", - "index_pattern" : "foo-*", - "fields" : { - "timestamp" : [ - { - "agg" : "date_histogram", - "time_zone" : "PST", - "interval" : "1h", - "delay": "7d" - } - ], - "voltage" : [ - { - "agg" : "histogram", - "interval": 5 - }, - { - "agg" : "sum" - } - ] - } - } -]; diff --git a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts deleted file mode 100644 index 883b3552a7c02..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts +++ /dev/null @@ -1,28 +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 { ElasticsearchServiceSetup } from 'kibana/server'; -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../../client/elasticsearch_rollup'; - -const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { - const config = { plugins: [elasticsearchJsPlugin] }; - return elasticsearchService.createClient('rollup', config); -}); - -export const callWithRequestFactory = ( - elasticsearchService: ElasticsearchServiceSetup, - request: any -) => { - return (...args: any[]) => { - return ( - callWithRequest(elasticsearchService) - .asScoped(request) - // @ts-ignore - .callAsCurrentUser(...args) - ); - }; -}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts b/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts deleted file mode 100644 index 0743e443955f4..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js deleted file mode 100644 index b6cea09e0ea3c..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js +++ /dev/null @@ -1,62 +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 expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '.'; -import { - LICENSE_STATUS_VALID, - LICENSE_STATUS_INVALID, -} from '../../../../../common/constants/license_status'; -import { kibanaResponseFactory } from '../../../../../../../src/core/server'; - -describe('licensePreRoutingFactory()', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - describe('status is invalid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_INVALID, - }; - }); - - it('replies with 403', () => { - const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => {}); - const stubRequest = {}; - const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); - expect(response.status).to.be(403); - }); - }); - - describe('status is valid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_VALID, - }; - }); - - it('replies with nothing', () => { - const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => null); - const stubRequest = {}; - const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); - expect(response).to.be(null); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index 353510d96a00d..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ /dev/null @@ -1,43 +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 { - KibanaRequest, - KibanaResponseFactory, - RequestHandler, - RequestHandlerContext, -} from 'src/core/server'; -import { PLUGIN } from '../../../common'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; -import { ServerShim } from '../../types'; - -export const licensePreRoutingFactory = ( - server: ServerShim, - handler: RequestHandler<any, any, any> -): RequestHandler<any, any, any> => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - return function licensePreRouting( - ctx: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory - ) { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - const { status } = licenseCheckResults; - - if (status !== LICENSE_STATUS_VALID) { - return response.customError({ - body: { - message: licenseCheckResults.messsage, - }, - statusCode: 403, - }); - } - - return handler(ctx, request, response); - }; -}; diff --git a/x-pack/legacy/plugins/rollup/server/plugin.ts b/x-pack/legacy/plugins/rollup/server/plugin.ts deleted file mode 100644 index 05c22b030fff9..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/plugin.ts +++ /dev/null @@ -1,95 +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 { CoreSetup, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; -import { first } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; - -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; -import { IndexManagementPluginSetup } from '../../../../plugins/index_management/server'; -import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; -import { PLUGIN } from '../common'; -import { ServerShim, RouteDependencies } from './types'; - -import { - registerIndicesRoute, - registerFieldsForWildcardRoute, - registerSearchRoute, - registerJobsRoute, -} from './routes/api'; - -import { registerRollupUsageCollector } from './collectors'; - -import { rollupDataEnricher } from './rollup_data_enricher'; -import { registerRollupSearchStrategy } from './lib/search_strategies'; - -export class RollupsServerPlugin implements Plugin<void, void, any, any> { - log: Logger; - - constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = initializerContext.logger.get(); - } - - async setup( - { http, elasticsearch: elasticsearchService }: CoreSetup, - { - __LEGACY: serverShim, - usageCollection, - visTypeTimeseries, - indexManagement, - }: { - __LEGACY: ServerShim; - usageCollection?: UsageCollectionSetup; - visTypeTimeseries?: VisTypeTimeseriesSetup; - indexManagement?: IndexManagementPluginSetup; - } - ) { - const elasticsearch = await elasticsearchService.adminClient; - const router = http.createRouter(); - const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, - router, - }; - - registerLicenseChecker( - serverShim as any, - PLUGIN.ID, - PLUGIN.getI18nName(i18n), - PLUGIN.MINIMUM_LICENSE_REQUIRED - ); - - registerIndicesRoute(routeDependencies, serverShim); - registerFieldsForWildcardRoute(routeDependencies, serverShim); - registerSearchRoute(routeDependencies, serverShim); - registerJobsRoute(routeDependencies, serverShim); - - if (usageCollection) { - this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise() - .then(config => { - registerRollupUsageCollector(usageCollection, config.kibana.index); - }) - .catch(e => { - this.log.warn(`Registering Rollup collector failed: ${e}`); - }); - } - - if (indexManagement && indexManagement.indexDataEnricher) { - indexManagement.indexDataEnricher.add(rollupDataEnricher); - } - - if (visTypeTimeseries) { - const { addSearchStrategy } = visTypeTimeseries; - registerRollupSearchStrategy(routeDependencies, addSearchStrategy); - } - } - - start() {} - - stop() {} -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/index.ts b/x-pack/legacy/plugins/rollup/server/routes/api/index.ts deleted file mode 100644 index 146c3e973f9ea..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export { registerIndicesRoute } from './indices'; -export { registerFieldsForWildcardRoute } from './index_patterns'; -export { registerSearchRoute } from './search'; -export { registerJobsRoute } from './jobs'; diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts b/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts deleted file mode 100644 index 2516840bd9537..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts +++ /dev/null @@ -1,131 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; - -import { indexBy } from 'lodash'; -import { IndexPatternsFetcher } from '../../shared_imports'; -import { RouteDependencies, ServerShim } from '../../types'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; -import { mergeCapabilitiesWithFields, Field } from '../../lib/merge_capabilities_with_fields'; - -const parseMetaFields = (metaFields: string | string[]) => { - let parsedFields: string[] = []; - if (typeof metaFields === 'string') { - parsedFields = JSON.parse(metaFields); - } else { - parsedFields = metaFields; - } - return parsedFields; -}; - -const getFieldsForWildcardRequest = async (context: any, request: any, response: any) => { - const { callAsCurrentUser } = context.core.elasticsearch.dataClient; - const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); - const { pattern, meta_fields: metaFields } = request.query; - - let parsedFields: string[] = []; - try { - parsedFields = parseMetaFields(metaFields); - } catch (error) { - return response.badRequest({ - body: error, - }); - } - - try { - const fields = await indexPatterns.getFieldsForWildcard({ - pattern, - metaFields: parsedFields, - }); - - return response.ok({ - body: { fields }, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - return response.notFound(); - } -}; - -/** - * Get list of fields for rollup index pattern, in the format of regular index pattern fields - */ -export function registerFieldsForWildcardRoute(deps: RouteDependencies, legacy: ServerShim) { - const handler: RequestHandler<any, any, any> = async (ctx, request, response) => { - const { params, meta_fields: metaFields } = request.query; - - try { - // Make call and use field information from response - const { payload } = await getFieldsForWildcardRequest(ctx, request, response); - const fields = payload.fields; - const parsedParams = JSON.parse(params); - const rollupIndex = parsedParams.rollup_index; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const rollupFields: Field[] = []; - const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name'); - const rollupIndexCapabilities = getCapabilitiesForRollupIndices( - await callWithRequest('rollup.rollupIndexCapabilities', { - indexPattern: rollupIndex, - }) - )[rollupIndex].aggs; - // Keep meta fields - metaFields.forEach( - (field: string) => - fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) - ); - const mergedRollupFields = mergeCapabilitiesWithFields( - rollupIndexCapabilities, - fieldsFromFieldCapsApi, - rollupFields - ); - return response.ok({ body: { fields: mergedRollupFields } }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - deps.router.get( - { - path: '/api/index_patterns/rollup/_fields_for_wildcard', - validate: { - query: schema.object({ - pattern: schema.string(), - meta_fields: schema.arrayOf(schema.string(), { - defaultValue: [], - }), - params: schema.string({ - validate(value) { - try { - const params = JSON.parse(value); - const keys = Object.keys(params); - const { rollup_index: rollupIndex } = params; - - if (!rollupIndex) { - return '[request query.params]: "rollup_index" is required'; - } else if (keys.length > 1) { - const invalidParams = keys.filter(key => key !== 'rollup_index'); - return `[request query.params]: ${invalidParams.join(', ')} is not allowed`; - } - } catch (err) { - return '[request query.params]: expected JSON string'; - } - }, - }), - }), - }, - }, - licensePreRoutingFactory(legacy, handler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts b/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts deleted file mode 100644 index e78f09a71876b..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts +++ /dev/null @@ -1,175 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; -import { API_BASE_PATH } from '../../../common'; -import { RouteDependencies, ServerShim } from '../../types'; - -type NumericField = - | 'long' - | 'integer' - | 'short' - | 'byte' - | 'scaled_float' - | 'double' - | 'float' - | 'half_float'; - -interface FieldCapability { - date?: any; - keyword?: any; - long?: any; - integer?: any; - short?: any; - byte?: any; - double?: any; - float?: any; - half_float?: any; - scaled_float?: any; -} - -interface FieldCapabilities { - fields: FieldCapability[]; -} - -function isNumericField(fieldCapability: FieldCapability) { - const numericTypes = [ - 'long', - 'integer', - 'short', - 'byte', - 'double', - 'float', - 'half_float', - 'scaled_float', - ]; - return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null); -} - -export function registerIndicesRoute(deps: RouteDependencies, legacy: ServerShim) { - const getIndicesHandler: RequestHandler<any, any, any> = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const data = await callWithRequest('rollup.rollupIndexCapabilities', { - indexPattern: '_all', - }); - return response.ok({ body: getCapabilitiesForRollupIndices(data) }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const validateIndexPatternHandler: RequestHandler<any, any, any> = async ( - ctx, - request, - response - ) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const { indexPattern } = request.params; - const [fieldCapabilities, rollupIndexCapabilities]: [ - FieldCapabilities, - { [key: string]: any } - ] = await Promise.all([ - callWithRequest('rollup.fieldCapabilities', { indexPattern }), - callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }), - ]); - - const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; - const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0; - - const dateFields: string[] = []; - const numericFields: string[] = []; - const keywordFields: string[] = []; - - const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); - - fieldCapabilitiesEntries.forEach( - ([fieldName, fieldCapability]: [string, FieldCapability]) => { - if (fieldCapability.date) { - dateFields.push(fieldName); - return; - } - - if (isNumericField(fieldCapability)) { - numericFields.push(fieldName); - return; - } - - if (fieldCapability.keyword) { - keywordFields.push(fieldName); - } - } - ); - - const body = { - doesMatchIndices, - doesMatchRollupIndices, - dateFields, - numericFields, - keywordFields, - }; - - return response.ok({ body }); - } catch (err) { - // 404s are still valid results. - if (err.statusCode === 404) { - const notFoundBody = { - doesMatchIndices: false, - doesMatchRollupIndices: false, - dateFields: [], - numericFields: [], - keywordFields: [], - }; - return response.ok({ body: notFoundBody }); - } - - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - - return response.internalError({ body: err }); - } - }; - - /** - * Returns a list of all rollup index names - */ - deps.router.get( - { - path: `${API_BASE_PATH}/indices`, - validate: false, - }, - licensePreRoutingFactory(legacy, getIndicesHandler) - ); - - /** - * Returns information on validity of an index pattern for creating a rollup job: - * - Does the index pattern match any indices? - * - Does the index pattern match rollup indices? - * - Which date fields, numeric fields, and keyword fields are available in the matching indices? - */ - deps.router.get( - { - path: `${API_BASE_PATH}/index_pattern_validity/{indexPattern}`, - validate: { - params: schema.object({ - indexPattern: schema.string(), - }), - }, - }, - licensePreRoutingFactory(legacy, validateIndexPatternHandler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts deleted file mode 100644 index e45713e2b807c..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts +++ /dev/null @@ -1,178 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../common'; -import { RouteDependencies, ServerShim } from '../../types'; - -export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) { - const getJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const data = await callWithRequest('rollup.jobs'); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const createJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => { - try { - const { id, ...rest } = request.body.job; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - // Create job. - await callWithRequest('rollup.createJob', { - id, - body: rest, - }); - // Then request the newly created job. - const results = await callWithRequest('rollup.job', { id }); - return response.ok({ body: results.jobs[0] }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const startJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => { - try { - const { jobIds } = request.body; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const data = await Promise.all( - jobIds.map((id: string) => callWithRequest('rollup.startJob', { id })) - ).then(() => ({ success: true })); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const stopJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => { - try { - const { jobIds } = request.body; - // For our API integration tests we need to wait for the jobs to be stopped - // in order to be able to delete them sequencially. - const { waitForCompletion } = request.query; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const stopRollupJob = (id: string) => - callWithRequest('rollup.stopJob', { - id, - waitForCompletion: waitForCompletion === 'true', - }); - const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const deleteJobsHandler: RequestHandler<any, any, any> = async (ctx, request, response) => { - try { - const { jobIds } = request.body; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const data = await Promise.all( - jobIds.map((id: string) => callWithRequest('rollup.deleteJob', { id })) - ).then(() => ({ success: true })); - return response.ok({ body: data }); - } catch (err) { - // There is an issue opened on ES to handle the following error correctly - // https://github.com/elastic/elasticsearch/issues/42908 - // Until then we'll modify the response here. - if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { - err.status = 400; - err.statusCode = 400; - err.displayName = 'Bad request'; - err.message = JSON.parse(err.response).task_failures[0].reason.reason; - } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - deps.router.get( - { - path: `${API_BASE_PATH}/jobs`, - validate: false, - }, - licensePreRoutingFactory(legacy, getJobsHandler) - ); - - deps.router.put( - { - path: `${API_BASE_PATH}/create`, - validate: { - body: schema.object({ - job: schema.object( - { - id: schema.string(), - }, - { unknowns: 'allow' } - ), - }), - }, - }, - licensePreRoutingFactory(legacy, createJobsHandler) - ); - - deps.router.post( - { - path: `${API_BASE_PATH}/start`, - validate: { - body: schema.object({ - jobIds: schema.arrayOf(schema.string()), - }), - query: schema.maybe( - schema.object({ - waitForCompletion: schema.maybe(schema.string()), - }) - ), - }, - }, - licensePreRoutingFactory(legacy, startJobsHandler) - ); - - deps.router.post( - { - path: `${API_BASE_PATH}/stop`, - validate: { - body: schema.object({ - jobIds: schema.arrayOf(schema.string()), - }), - }, - }, - licensePreRoutingFactory(legacy, stopJobsHandler) - ); - - deps.router.post( - { - path: `${API_BASE_PATH}/delete`, - validate: { - body: schema.object({ - jobIds: schema.arrayOf(schema.string()), - }), - }, - }, - licensePreRoutingFactory(legacy, deleteJobsHandler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/search.ts b/x-pack/legacy/plugins/rollup/server/routes/api/search.ts deleted file mode 100644 index 97999a4b5ce8d..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/search.ts +++ /dev/null @@ -1,50 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../common'; -import { RouteDependencies, ServerShim } from '../../types'; - -export function registerSearchRoute(deps: RouteDependencies, legacy: ServerShim) { - const handler: RequestHandler<any, any, any> = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - try { - const requests = request.body.map(({ index, query }: { index: string; query: any }) => - callWithRequest('rollup.search', { - index, - rest_total_hits_as_int: true, - body: query, - }) - ); - const data = await Promise.all(requests); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - deps.router.post( - { - path: `${API_BASE_PATH}/search`, - validate: { - body: schema.arrayOf( - schema.object({ - index: schema.string(), - query: schema.any(), - }) - ), - }, - }, - licensePreRoutingFactory(legacy, handler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/shared_imports.ts b/x-pack/legacy/plugins/rollup/server/shared_imports.ts deleted file mode 100644 index 941610b97707f..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/shared_imports.ts +++ /dev/null @@ -1,7 +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. - */ - -export { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; diff --git a/x-pack/legacy/plugins/rollup/server/types.ts b/x-pack/legacy/plugins/rollup/server/types.ts deleted file mode 100644 index bcc6770e9b8ee..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/types.ts +++ /dev/null @@ -1,21 +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 { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; - -export interface ServerShim { - plugins: { - xpack_main: XPackMainPlugin; - rollup: any; - }; -} - -export interface RouteDependencies { - router: IRouter; - elasticsearchService: ElasticsearchServiceSetup; - elasticsearch: IClusterClient; -} diff --git a/x-pack/legacy/plugins/rollup/tsconfig.json b/x-pack/legacy/plugins/rollup/tsconfig.json deleted file mode 100644 index 618c6c3e97b57..0000000000000 --- a/x-pack/legacy/plugins/rollup/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../tsconfig.json" -} diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts index 5b2218af1fd52..b1dec2ce82c52 100644 --- a/x-pack/legacy/plugins/security/index.ts +++ b/x-pack/legacy/plugins/security/index.ts @@ -78,9 +78,7 @@ export const security = (kibana: Record<string, any>) => // features are up to date. xpackInfo .feature(this.id) - .registerLicenseCheckResultsGenerator(() => - securityPlugin.__legacyCompat.license.getFeatures() - ); + .registerLicenseCheckResultsGenerator(() => securityPlugin.license.getFeatures()); server.expose({ getUser: async (request: LegacyRequest) => diff --git a/x-pack/legacy/plugins/siem/.gitattributes b/x-pack/legacy/plugins/siem/.gitattributes deleted file mode 100644 index a4071d39e63c0..0000000000000 --- a/x-pack/legacy/plugins/siem/.gitattributes +++ /dev/null @@ -1,5 +0,0 @@ -# Auto-collapse generated files in GitHub -# https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github -x-pack/legacy/plugins/siem/public/graphql/types.ts linguist-generated=true -x-pack/legacy/plugins/siem/public/graphql/introspection.json linguist-generated=true - diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts deleted file mode 100644 index 6e03583dda69f..0000000000000 --- a/x-pack/legacy/plugins/siem/index.ts +++ /dev/null @@ -1,171 +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 { i18n } from '@kbn/i18n'; -import { resolve } from 'path'; -import { Root } from 'joi'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { savedObjectMappings } from '../../../plugins/siem/server/saved_objects'; - -import { - APP_ID, - APP_NAME, - DEFAULT_INDEX_KEY, - DEFAULT_ANOMALY_SCORE, - DEFAULT_SIEM_TIME_RANGE, - DEFAULT_SIEM_REFRESH_INTERVAL, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_FROM, - DEFAULT_TO, - ENABLE_NEWS_FEED_SETTING, - NEWS_FEED_URL_SETTING, - NEWS_FEED_URL_SETTING_DEFAULT, - IP_REPUTATION_LINKS_SETTING, - IP_REPUTATION_LINKS_SETTING_DEFAULT, - DEFAULT_INDEX_PATTERN, -} from '../../../plugins/siem/common/constants'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const siem = (kibana: any) => { - return new kibana.Plugin({ - id: APP_ID, - configPrefix: 'xpack.siem', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'alerting', 'actions', 'triggers_actions_ui'], - uiExports: { - app: { - description: i18n.translate('xpack.siem.securityDescription', { - defaultMessage: 'Explore your SIEM App', - }), - main: 'plugins/siem/legacy', - euiIconType: 'securityAnalyticsApp', - title: APP_NAME, - listed: false, - url: `/app/${APP_ID}`, - }, - home: ['plugins/siem/register_feature'], - links: [ - { - description: i18n.translate('xpack.siem.linkSecurityDescription', { - defaultMessage: 'Explore your SIEM App', - }), - euiIconType: 'securityAnalyticsApp', - id: 'siem', - order: 9000, - title: APP_NAME, - url: `/app/${APP_ID}`, - category: DEFAULT_APP_CATEGORIES.security, - }, - ], - uiSettingDefaults: { - [DEFAULT_SIEM_REFRESH_INTERVAL]: { - type: 'json', - name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', { - defaultMessage: 'Time filter refresh interval', - }), - value: `{ - "pause": ${DEFAULT_INTERVAL_PAUSE}, - "value": ${DEFAULT_INTERVAL_VALUE} -}`, - description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', { - defaultMessage: - '<p>Default refresh interval for the SIEM time filter, in milliseconds.</p>', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_SIEM_TIME_RANGE]: { - type: 'json', - name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', { - defaultMessage: 'Time filter period', - }), - value: `{ - "from": "${DEFAULT_FROM}", - "to": "${DEFAULT_TO}" -}`, - description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', { - defaultMessage: '<p>Default period of time in the SIEM time filter.</p>', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_INDEX_KEY]: { - name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { - defaultMessage: 'Elasticsearch indices', - }), - value: DEFAULT_INDEX_PATTERN, - description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', { - defaultMessage: - '<p>Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.</p>', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_ANOMALY_SCORE]: { - name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', { - defaultMessage: 'Anomaly threshold', - }), - value: 50, - type: 'number', - description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', { - defaultMessage: - '<p>Value above which Machine Learning job anomalies are displayed in the SIEM app.</p><p>Valid values: 0 to 100.</p>', - }), - category: ['siem'], - requiresPageReload: true, - }, - [ENABLE_NEWS_FEED_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', { - defaultMessage: 'News feed', - }), - value: true, - description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', { - defaultMessage: '<p>Enables the News feed</p>', - }), - type: 'boolean', - category: ['siem'], - requiresPageReload: true, - }, - [NEWS_FEED_URL_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', { - defaultMessage: 'News feed URL', - }), - value: NEWS_FEED_URL_SETTING_DEFAULT, - description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', { - defaultMessage: '<p>News feed content will be retrieved from this URL</p>', - }), - category: ['siem'], - requiresPageReload: true, - }, - [IP_REPUTATION_LINKS_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', { - defaultMessage: 'IP Reputation Links', - }), - value: IP_REPUTATION_LINKS_SETTING_DEFAULT, - type: 'json', - description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', { - defaultMessage: - 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', - }), - category: ['siem'], - requiresPageReload: true, - }, - }, - mappings: savedObjectMappings, - }, - config(Joi: Root) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - }) - .unknown(true) - .default(); - }, - }); -}; diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json deleted file mode 100644 index 3a93beef963a0..0000000000000 --- a/x-pack/legacy/plugins/siem/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "author": "Elastic", - "name": "siem-legacy-ui", - "version": "8.0.0", - "private": true, - "license": "Elastic-License", - "scripts": {}, - "devDependencies": { - "@types/lodash": "^4.14.110", - "@types/js-yaml": "^3.12.1", - "@types/react-beautiful-dnd": "^12.1.1" - }, - "dependencies": { - "lodash": "^4.17.15", - "react-beautiful-dnd": "^12.2.0", - "react-markdown": "^4.0.6" - } -} diff --git a/x-pack/legacy/plugins/siem/public/app/app.tsx b/x-pack/legacy/plugins/siem/public/app/app.tsx deleted file mode 100644 index 44c1c923cd6ee..0000000000000 --- a/x-pack/legacy/plugins/siem/public/app/app.tsx +++ /dev/null @@ -1,116 +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 { createHashHistory, History } from 'history'; -import React, { memo, useMemo, FC } from 'react'; -import { ApolloProvider } from 'react-apollo'; -import { Store } from 'redux'; -import { Provider as ReduxStoreProvider } from 'react-redux'; -import { ThemeProvider } from 'styled-components'; - -import { EuiErrorBoundary } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { BehaviorSubject } from 'rxjs'; -import { pluck } from 'rxjs/operators'; - -import { KibanaContextProvider, useKibana, useUiSetting$ } from '../lib/kibana'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; - -import { DEFAULT_DARK_MODE } from '../../../../../plugins/siem/common/constants'; -import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; -import { compose } from '../lib/compose/kibana_compose'; -import { AppFrontendLibs, AppApolloClient } from '../lib/lib'; -import { CoreStart, StartPlugins } from '../plugin'; -import { PageRouter } from '../routes'; -import { createStore, createInitialState } from '../store'; -import { GlobalToaster, ManageGlobalToaster } from '../components/toasters'; -import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabilities_provider'; - -import { ApolloClientContext } from '../utils/apollo_context'; - -interface AppPluginRootComponentProps { - apolloClient: AppApolloClient; - history: History; - store: Store; - theme: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -const AppPluginRootComponent: React.FC<AppPluginRootComponentProps> = ({ - theme, - store, - apolloClient, - history, -}) => ( - <ManageGlobalToaster> - <ReduxStoreProvider store={store}> - <ApolloProvider client={apolloClient}> - <ApolloClientContext.Provider value={apolloClient}> - <ThemeProvider theme={theme}> - <MlCapabilitiesProvider> - <PageRouter history={history} /> - </MlCapabilitiesProvider> - </ThemeProvider> - <ErrorToastDispatcher /> - <GlobalToaster /> - </ApolloClientContext.Provider> - </ApolloProvider> - </ReduxStoreProvider> - </ManageGlobalToaster> -); - -const AppPluginRoot = memo(AppPluginRootComponent); - -const StartAppComponent: FC<AppFrontendLibs> = libs => { - const { i18n } = useKibana().services; - const history = createHashHistory(); - const libs$ = new BehaviorSubject(libs); - const store = createStore(createInitialState(), libs$.pipe(pluck('apolloClient'))); - const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); - const theme = useMemo( - () => ({ - eui: darkMode ? euiDarkVars : euiLightVars, - darkMode, - }), - [darkMode] - ); - - return ( - <EuiErrorBoundary> - <i18n.Context> - <AppPluginRoot - store={store} - apolloClient={libs.apolloClient} - history={history} - theme={theme} - /> - </i18n.Context> - </EuiErrorBoundary> - ); -}; - -const StartApp = memo(StartAppComponent); - -interface SiemAppComponentProps { - core: CoreStart; - plugins: StartPlugins; -} - -const SiemAppComponent: React.FC<SiemAppComponentProps> = ({ core, plugins }) => ( - <KibanaContextProvider - services={{ - appName: 'siem', - storage: new Storage(localStorage), - ...core, - ...plugins, - savedObjects: core.savedObjects, - }} - > - <StartApp {...compose(core)} /> - </KibanaContextProvider> -); - -export const SiemApp = memo(SiemAppComponent); diff --git a/x-pack/legacy/plugins/siem/public/app/index.tsx b/x-pack/legacy/plugins/siem/public/app/index.tsx deleted file mode 100644 index 01175a98d1e44..0000000000000 --- a/x-pack/legacy/plugins/siem/public/app/index.tsx +++ /dev/null @@ -1,20 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -import { CoreStart, StartPlugins, AppMountParameters } from '../plugin'; -import { SiemApp } from './app'; - -export const renderApp = ( - core: CoreStart, - plugins: StartPlugins, - { element }: AppMountParameters -) => { - render(<SiemApp core={core} plugins={plugins} />, element); - return () => unmountComponentAtNode(element); -}; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx deleted file mode 100644 index 778adc708d901..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx +++ /dev/null @@ -1,67 +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 React, { useEffect, useCallback, useMemo } from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../plugins/siem/common/constants'; -import { AlertsComponentsQueryProps } from './types'; -import { AlertsTable } from './alerts_table'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../lib/kibana'; -import { MatrixHistogramContainer } from '../matrix_histogram'; -import { histogramConfigs } from './histogram_configs'; -import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; -const ID = 'alertsOverTimeQuery'; - -export const AlertsView = ({ - deleteQuery, - endDate, - filterQuery, - pageFilters, - setQuery, - startDate, - type, -}: AlertsComponentsQueryProps) => { - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const getSubtitle = useCallback( - (totalCount: number) => - `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( - totalCount - )}`, - [] - ); - const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - subtitle: getSubtitle, - }), - [getSubtitle] - ); - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, [deleteQuery]); - - return ( - <> - <MatrixHistogramContainer - endDate={endDate} - filterQuery={filterQuery} - id={ID} - setQuery={setQuery} - sourceId="default" - startDate={startDate} - type={type} - {...alertsHistogramConfigs} - /> - <AlertsTable endDate={endDate} startDate={startDate} pageFilters={pageFilters} /> - </> - ); -}; -AlertsView.displayName = 'AlertsView'; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts deleted file mode 100644 index a24c66e31e670..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/types.ts +++ /dev/null @@ -1,22 +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 { Filter } from '../../../../../../../src/plugins/data/public'; -import { HostsComponentsQueryProps } from '../../pages/hosts/navigation/types'; -import { NetworkComponentQueryProps } from '../../pages/network/navigation/types'; -import { MatrixHistogramOption } from '../matrix_histogram/types'; - -type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; -export interface AlertsComponentsQueryProps - extends Pick< - CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' - > { - pageFilters: Filter[]; - stackByOptions?: MatrixHistogramOption[]; - defaultFilters?: Filter[]; - defaultStackByOption?: MatrixHistogramOption; -} diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx deleted file mode 100644 index 2fb270c284000..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx +++ /dev/null @@ -1,49 +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 { EuiBadge } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import * as i18n from './translations'; - -const RoundedBadge = styled(EuiBadge)` - align-items: center; - border-radius: 100%; - display: inline-flex; - font-size: 9px; - height: 34px; - justify-content: center; - margin: 0 5px 0 5px; - padding: 7px 6px 4px 6px; - user-select: none; - width: 34px; - - .euiBadge__content { - position: relative; - top: -1px; - } - - .euiBadge__text { - text-overflow: clip; - } -`; - -RoundedBadge.displayName = 'RoundedBadge'; - -export type AndOr = 'and' | 'or'; - -/** Displays AND / OR in a round badge */ -// Ref: https://github.com/elastic/eui/issues/1655 -export const AndOrBadge = React.memo<{ type: AndOr }>(({ type }) => { - return ( - <RoundedBadge data-test-subj="and-or-badge" color="hollow"> - {type === 'and' ? i18n.AND : i18n.OR} - </RoundedBadge> - ); -}); - -AndOrBadge.displayName = 'AndOrBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx deleted file mode 100644 index 8f261da629f94..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx +++ /dev/null @@ -1,34 +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 * as React from 'react'; -import { storiesOf } from '@storybook/react'; -import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { - QuerySuggestion, - QuerySuggestionTypes, -} from '../../../../../../../../src/plugins/data/public'; -import { SuggestionItem } from '../suggestion_item'; - -const suggestion: QuerySuggestion = { - description: 'Description...', - end: 3, - start: 1, - text: 'Text...', - type: QuerySuggestionTypes.Value, -}; - -storiesOf('components/SuggestionItem', module).add('example', () => ( - <ThemeProvider - theme={() => ({ - eui: euiLightVars, - darkMode: false, - })} - > - <SuggestionItem suggestion={suggestion} /> - </ThemeProvider> -)); diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx deleted file mode 100644 index 55e114818ffea..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.test.tsx +++ /dev/null @@ -1,388 +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 { EuiFieldSearch } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; -import { noop } from 'lodash/fp'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { - QuerySuggestion, - QuerySuggestionTypes, -} from '../../../../../../../src/plugins/data/public'; - -import { TestProviders } from '../../mock'; - -import { AutocompleteField } from '.'; - -const mockAutoCompleteData: QuerySuggestion[] = [ - { - type: QuerySuggestionTypes.Field, - text: 'agent.ephemeral_id ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.ephemeral_id</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.hostname ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.hostname</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.id ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.id</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.name ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.name</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.type ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.type</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.version ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.version</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test1 ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.test1</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test2 ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.test2</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test3 ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.test3</span></p>', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test4 ', - description: - '<p>Filter results that contain <span class="suggestionItem__callout">agent.test4</span></p>', - start: 0, - end: 1, - }, -]; - -describe('Autocomplete', () => { - describe('rendering', () => { - test('it renders against snapshot', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = shallow( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={noop} - onSubmit={noop} - placeholder={placeholder} - suggestions={[]} - value={''} - /> - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it is rendering with placeholder', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={noop} - onSubmit={noop} - placeholder={placeholder} - suggestions={[]} - value={''} - /> - ); - const input = wrapper.find('input[type="search"]'); - expect(input.find('[placeholder]').props().placeholder).toEqual(placeholder); - }); - - test('Rendering suggested items', () => { - const wrapper = mount( - <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={noop} - onSubmit={noop} - placeholder="" - suggestions={mockAutoCompleteData} - value={''} - /> - </ThemeProvider> - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - wrapper.update(); - - expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(10); - }); - - test('Should Not render suggested items if loading new suggestions', () => { - const wrapper = mount( - <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> - <AutocompleteField - isLoadingSuggestions={true} - isValid={false} - loadSuggestions={noop} - onChange={noop} - onSubmit={noop} - placeholder="" - suggestions={mockAutoCompleteData} - value={''} - /> - </ThemeProvider> - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - wrapper.update(); - - expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(0); - }); - }); - - describe('events', () => { - test('OnChange should have been called', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={onChange} - onSubmit={noop} - placeholder="" - suggestions={[]} - value={''} - /> - ); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('change', { target: { value: 'test' } }); - expect(onChange).toHaveBeenCalled(); - }); - }); - - test('OnSubmit should have been called by keying enter on the search input', () => { - const onSubmit = jest.fn((value: string) => value); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={true} - loadSuggestions={noop} - onChange={noop} - onSubmit={onSubmit} - placeholder="" - suggestions={mockAutoCompleteData} - value={'filter: query'} - /> - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: null }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); - expect(onSubmit).toHaveBeenCalled(); - }); - - test('OnSubmit should have been called by onSearch event on the input', () => { - const onSubmit = jest.fn((value: string) => value); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={true} - loadSuggestions={noop} - onChange={noop} - onSubmit={onSubmit} - placeholder="" - suggestions={mockAutoCompleteData} - value={'filter: query'} - /> - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: null }); - const wrapperFixedEuiFieldSearch = wrapper.find(EuiFieldSearch); - // TODO: FixedEuiFieldSearch fails to import - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (wrapperFixedEuiFieldSearch as any).props().onSearch(); - expect(onSubmit).toHaveBeenCalled(); - }); - - test('OnChange should have been called if keying enter on a suggested item selected', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={onChange} - onSubmit={noop} - placeholder="" - suggestions={mockAutoCompleteData} - value={''} - /> - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: 1 }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should be called if tab is pressed when a suggested item is selected', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={onChange} - onSubmit={noop} - placeholder="" - suggestions={mockAutoCompleteData} - value={''} - /> - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: 1 }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should NOT be called if tab is pressed when more than one item is suggested, and no selection has been made', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={onChange} - onSubmit={noop} - placeholder="" - suggestions={mockAutoCompleteData} - value={''} - /> - ); - - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).not.toHaveBeenCalled(); - }); - - test('OnChange should be called if tab is pressed when only one item is suggested, even though that item is NOT selected', () => { - const onChange = jest.fn((value: string) => value); - const onlyOneSuggestion = [mockAutoCompleteData[0]]; - - const wrapper = mount( - <TestProviders> - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={onChange} - onSubmit={noop} - placeholder="" - suggestions={onlyOneSuggestion} - value={''} - /> - </TestProviders> - ); - - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should NOT be called if tab is pressed when 0 items are suggested', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={noop} - onChange={onChange} - onSubmit={noop} - placeholder="" - suggestions={[]} - value={''} - /> - ); - - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).not.toHaveBeenCalled(); - }); - - test('Load more suggestions when arrowdown on the search bar', () => { - const loadSuggestions = jest.fn(noop); - - const wrapper = mount( - <AutocompleteField - isLoadingSuggestions={false} - isValid={false} - loadSuggestions={loadSuggestions} - onChange={noop} - onSubmit={noop} - placeholder="" - suggestions={[]} - value={''} - /> - ); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'ArrowDown', preventDefault: noop }); - expect(loadSuggestions).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx b/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx deleted file mode 100644 index f051e18f8acab..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/index.tsx +++ /dev/null @@ -1,333 +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 { - EuiFieldSearch, - EuiFieldSearchProps, - EuiOutsideClickDetector, - EuiPanel, -} from '@elastic/eui'; -import React from 'react'; -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; - -import euiStyled from '../../../../../common/eui_styled_components'; - -import { SuggestionItem } from './suggestion_item'; - -interface AutocompleteFieldProps { - 'data-test-subj'?: string; - isLoadingSuggestions: boolean; - isValid: boolean; - loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; - onSubmit?: (value: string) => void; - onChange?: (value: string) => void; - placeholder?: string; - suggestions: QuerySuggestion[]; - value: string; -} - -interface AutocompleteFieldState { - areSuggestionsVisible: boolean; - isFocused: boolean; - selectedIndex: number | null; -} - -export class AutocompleteField extends React.PureComponent< - AutocompleteFieldProps, - AutocompleteFieldState -> { - public readonly state: AutocompleteFieldState = { - areSuggestionsVisible: false, - isFocused: false, - selectedIndex: null, - }; - - private inputElement: HTMLInputElement | null = null; - - public render() { - const { - 'data-test-subj': dataTestSubj, - suggestions, - isLoadingSuggestions, - isValid, - placeholder, - value, - } = this.props; - const { areSuggestionsVisible, selectedIndex } = this.state; - return ( - <EuiOutsideClickDetector onOutsideClick={this.handleBlur}> - <AutocompleteContainer> - <FixedEuiFieldSearch - data-test-subj={dataTestSubj} - fullWidth - inputRef={this.handleChangeInputRef} - isLoading={isLoadingSuggestions} - isInvalid={!isValid} - onChange={this.handleChange} - onFocus={this.handleFocus} - onKeyDown={this.handleKeyDown} - onKeyUp={this.handleKeyUp} - onSearch={this.submit} - placeholder={placeholder} - value={value} - /> - {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( - <SuggestionsPanel> - {suggestions.map((suggestion, suggestionIndex) => ( - <SuggestionItem - key={suggestion.text} - suggestion={suggestion} - isSelected={suggestionIndex === selectedIndex} - onMouseEnter={this.selectSuggestionAt(suggestionIndex)} - onClick={this.applySuggestionAt(suggestionIndex)} - /> - ))} - </SuggestionsPanel> - ) : null} - </AutocompleteContainer> - </EuiOutsideClickDetector> - ); - } - - public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { - const hasNewValue = prevProps.value !== this.props.value; - const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; - - if (hasNewValue) { - this.updateSuggestions(); - } - - if (hasNewSuggestions && this.state.isFocused) { - this.showSuggestions(); - } - } - - private handleChangeInputRef = (element: HTMLInputElement | null) => { - this.inputElement = element; - }; - - private handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => { - this.changeValue(evt.currentTarget.value); - }; - - private handleKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => { - const { suggestions } = this.props; - switch (evt.key) { - case 'ArrowUp': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState( - composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) - ); - } - break; - case 'ArrowDown': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); - } else { - this.updateSuggestions(); - } - break; - case 'Enter': - evt.preventDefault(); - if (this.state.selectedIndex !== null) { - this.applySelectedSuggestion(); - } else { - this.submit(); - } - break; - case 'Tab': - evt.preventDefault(); - if (this.state.areSuggestionsVisible && this.props.suggestions.length === 1) { - this.applySuggestionAt(0)(); - } else if (this.state.selectedIndex !== null) { - this.applySelectedSuggestion(); - } - break; - case 'Escape': - evt.preventDefault(); - evt.stopPropagation(); - this.setState(withSuggestionsHidden); - break; - } - }; - - private handleKeyUp = (evt: React.KeyboardEvent<HTMLInputElement>) => { - switch (evt.key) { - case 'ArrowLeft': - case 'ArrowRight': - case 'Home': - case 'End': - this.updateSuggestions(); - break; - } - }; - - private handleFocus = () => { - this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); - }; - - private handleBlur = () => { - this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); - }; - - private selectSuggestionAt = (index: number) => () => { - this.setState(withSuggestionAtIndexSelected(index)); - }; - - private applySelectedSuggestion = () => { - if (this.state.selectedIndex !== null) { - this.applySuggestionAt(this.state.selectedIndex)(); - } - }; - - private applySuggestionAt = (index: number) => () => { - const { value, suggestions } = this.props; - const selectedSuggestion = suggestions[index]; - - if (!selectedSuggestion) { - return; - } - - const newValue = - value.substr(0, selectedSuggestion.start) + - selectedSuggestion.text + - value.substr(selectedSuggestion.end); - - this.setState(withSuggestionsHidden); - this.changeValue(newValue); - this.focusInputElement(); - }; - - private changeValue = (value: string) => { - const { onChange } = this.props; - - if (onChange) { - onChange(value); - } - }; - - private focusInputElement = () => { - if (this.inputElement) { - this.inputElement.focus(); - } - }; - - private showSuggestions = () => { - this.setState(withSuggestionsVisible); - }; - - private submit = () => { - const { isValid, onSubmit, value } = this.props; - - if (isValid && onSubmit) { - onSubmit(value); - } - - this.setState(withSuggestionsHidden); - }; - - private updateSuggestions = () => { - const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; - this.props.loadSuggestions(this.props.value, inputCursorPosition, 10); - }; -} - -type StateUpdater<State, Props = {}> = ( - prevState: Readonly<State>, - prevProps: Readonly<Props> -) => State | null; - -function composeStateUpdaters<State, Props>(...updaters: Array<StateUpdater<State, Props>>) { - return (state: State, props: Props) => - updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); -} - -const withPreviousSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length - : Math.max(props.suggestions.length - 1, 0), -}); - -const withNextSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + 1) % props.suggestions.length - : 0, -}); - -const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length - ? suggestionIndex - : 0, -}); - -const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: true, -}); - -const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: false, - selectedIndex: null, -}); - -const withFocused = (state: AutocompleteFieldState) => ({ - ...state, - isFocused: true, -}); - -const withUnfocused = (state: AutocompleteFieldState) => ({ - ...state, - isFocused: false, -}); - -export const FixedEuiFieldSearch: React.FC<React.InputHTMLAttributes<HTMLInputElement> & - EuiFieldSearchProps & { - inputRef?: (element: HTMLInputElement | null) => void; - onSearch: (value: string) => void; - }> = EuiFieldSearch as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -const AutocompleteContainer = euiStyled.div` - position: relative; -`; - -AutocompleteContainer.displayName = 'AutocompleteContainer'; - -const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ - paddingSize: 'none', - hasShadow: true, -}))` - position: absolute; - width: 100%; - margin-top: 2px; - overflow: hidden; - z-index: ${props => props.theme.eui.euiZLevel1}; -`; - -SuggestionsPanel.displayName = 'SuggestionsPanel'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx deleted file mode 100644 index b3811d05eea04..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ /dev/null @@ -1,176 +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 { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; -import { getEmptyStringTag } from '../empty_value'; -import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; -import { Provider } from '../timeline/data_providers/provider'; - -export interface DefaultDraggableType { - id: string; - field: string; - value?: string | null; - name?: string | null; - queryValue?: string | null; - children?: React.ReactNode; - tooltipContent?: React.ReactNode; -} - -/** - * Only returns true if the specified tooltipContent is exactly `null`. - * Example input / output: - * `bob -> false` - * `undefined -> false` - * `<span>thing</span> -> false` - * `null -> true` - */ -export const tooltipContentIsExplicitlyNull = (tooltipContent?: React.ReactNode): boolean => - tooltipContent === null; // an explicit / exact null check - -/** - * Derives the tooltip content from the field name if no tooltip was specified - */ -export const getDefaultWhenTooltipIsUnspecified = ({ - field, - tooltipContent, -}: { - field: string; - tooltipContent?: React.ReactNode; -}): React.ReactNode => (tooltipContent != null ? tooltipContent : field); - -/** - * Renders the content of the draggable, wrapped in a tooltip - */ -const Content = React.memo<{ - children?: React.ReactNode; - field: string; - tooltipContent?: React.ReactNode; - value?: string | null; -}>(({ children, field, tooltipContent, value }) => - !tooltipContentIsExplicitlyNull(tooltipContent) ? ( - <EuiToolTip - data-test-subj={`${field}-tooltip`} - content={getDefaultWhenTooltipIsUnspecified({ tooltipContent, field })} - > - <>{children ? children : value}</> - </EuiToolTip> - ) : ( - <>{children ? children : value}</> - ) -); - -Content.displayName = 'Content'; - -/** - * Draggable text (or an arbitrary visualization specified by `children`) - * that's only displayed when the specified value is non-`null`. - * - * @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}` - * @param field - the name of the field, e.g. `network.transport` - * @param value - value of the field e.g. `tcp` - * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data - * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior - * @param tooltipContent - defaults to displaying `field`, pass `null` to - * prevent a tooltip from being displayed, or pass arbitrary content - * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data - */ -export const DefaultDraggable = React.memo<DefaultDraggableType>( - ({ id, field, value, name, children, tooltipContent, queryValue }) => - value != null ? ( - <DraggableWrapper - dataProvider={{ - and: [], - enabled: true, - id: escapeDataProviderId(id), - name: name ? name : value, - excluded: false, - kqlQuery: '', - queryMatch: { - field, - value: queryValue ? queryValue : value, - operator: IS_OPERATOR, - }, - }} - render={(dataProvider, _, snapshot) => - snapshot.isDragging ? ( - <DragEffects> - <Provider dataProvider={dataProvider} /> - </DragEffects> - ) : ( - <Content field={field} tooltipContent={tooltipContent} value={value}> - {children} - </Content> - ) - } - /> - ) : null -); - -DefaultDraggable.displayName = 'DefaultDraggable'; - -export const Badge = styled(EuiBadge)` - vertical-align: top; -`; - -Badge.displayName = 'Badge'; - -export type BadgeDraggableType = Omit<DefaultDraggableType, 'id'> & { - contextId: string; - eventId: string; - iconType?: IconType; - color?: string; -}; - -/** - * A draggable badge that's only displayed when the specified value is non-`null`. - * - * @param contextId - used as part of the formula to derive a unique draggable id, this describes the context e.g. `event-fields-browser` in which the badge is displayed - * @param eventId - uniquely identifies an event, as specified in the `_id` field of the document - * @param field - the name of the field, e.g. `network.transport` - * @param value - value of the field e.g. `tcp` - * @param iconType -the (optional) type of icon e.g. `snowflake` to display on the badge - * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data - * @param color - defaults to `hollow`, optionally overwrite the color of the badge icon - * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior - * @param tooltipContent - defaults to displaying `field`, pass `null` to - * prevent a tooltip from being displayed, or pass arbitrary content - * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data - */ -export const DraggableBadge = React.memo<BadgeDraggableType>( - ({ - contextId, - eventId, - field, - value, - iconType, - name, - color = 'hollow', - children, - tooltipContent, - queryValue, - }) => - value != null ? ( - <DefaultDraggable - id={`draggable-badge-default-draggable-${contextId}-${eventId}-${field}-${value}`} - field={field} - name={name} - value={value} - tooltipContent={tooltipContent} - queryValue={queryValue} - > - <Badge iconType={iconType} color={color} title=""> - {children ? children : value !== '' ? value : getEmptyStringTag()} - </Badge> - </DefaultDraggable> - ) : null -); - -DraggableBadge.displayName = 'DraggableBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx deleted file mode 100644 index a7272593c2b27..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ /dev/null @@ -1,227 +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 { EuiLink, EuiText } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; -import { createPortalNode, InPortal } from 'react-reverse-portal'; -import styled, { css } from 'styled-components'; -import { npStart } from 'ui/new_platform'; - -import { - EmbeddablePanel, - ErrorEmbeddable, -} from '../../../../../../../src/plugins/embeddable/public'; -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; -import { useIndexPatterns } from '../../hooks/use_index_patterns'; -import { Loader } from '../loader'; -import { displayErrorToast, useStateToaster } from '../toasters'; -import { Embeddable } from './embeddable'; -import { EmbeddableHeader } from './embeddable_header'; -import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; -import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; -import { MapToolTip } from './map_tool_tip/map_tool_tip'; -import * as i18n from './translations'; -import { SetQuery } from './types'; -import { MapEmbeddable } from '../../../../../plugins/maps/public'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { useKibana, useUiSetting$ } from '../../lib/kibana'; -import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; - -interface EmbeddableMapProps { - maintainRatio?: boolean; -} - -const EmbeddableMap = styled.div.attrs(() => ({ - className: 'siemEmbeddable__map', -}))<EmbeddableMapProps>` - .embPanel { - border: none; - box-shadow: none; - } - - .mapToolbarOverlay__button { - display: none; - } - - ${({ maintainRatio }) => - maintainRatio && - css` - padding-top: calc(3 / 4 * 100%); /* 4:3 (standard) ratio */ - position: relative; - - @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { - padding-top: calc(9 / 32 * 100%); /* 32:9 (ultra widescreen) ratio */ - } - - @media only screen and (min-width: 1441px) and (min-height: 901px) { - padding-top: calc(9 / 21 * 100%); /* 21:9 (ultrawide) ratio */ - } - - .embPanel { - bottom: 0; - left: 0; - position: absolute; - right: 0; - top: 0; - } - `} -`; -EmbeddableMap.displayName = 'EmbeddableMap'; - -export interface EmbeddedMapProps { - query: Query; - filters: Filter[]; - startDate: number; - endDate: number; - setQuery: SetQuery; -} - -export const EmbeddedMapComponent = ({ - endDate, - filters, - query, - setQuery, - startDate, -}: EmbeddedMapProps) => { - const [embeddable, setEmbeddable] = React.useState<MapEmbeddable | undefined | ErrorEmbeddable>( - undefined - ); - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(false); - const [isIndexError, setIsIndexError] = useState(false); - - const [, dispatchToaster] = useStateToaster(); - const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns(); - const [siemDefaultIndices] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - - // This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our - // own component tree instead of the embeddables (default). This is necessary to have access to - // the Redux store, theme provider, etc, which is required to register and un-register the draggable - // Search InPortal/OutPortal for implementation touch points - const portalNode = React.useMemo(() => createPortalNode(), []); - - const { services } = useKibana(); - - // Initial Load useEffect - useEffect(() => { - let isSubscribed = true; - async function setupEmbeddable() { - // Ensure at least one `siem:defaultIndex` kibana index pattern exists before creating embeddable - const matchingIndexPatterns = findMatchingIndexPatterns({ - kibanaIndexPatterns, - siemDefaultIndices, - }); - - if (matchingIndexPatterns.length === 0 && isSubscribed) { - setIsLoading(false); - setIsIndexError(true); - return; - } - - // Create & set Embeddable - try { - const embeddableObject = await createEmbeddable( - filters, - getIndexPatternTitleIdMapping(matchingIndexPatterns), - query, - startDate, - endDate, - setQuery, - portalNode, - services.embeddable - ); - if (isSubscribed) { - setEmbeddable(embeddableObject); - } - } catch (e) { - if (isSubscribed) { - displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, [e.message], dispatchToaster); - setIsError(true); - } - } - if (isSubscribed) { - setIsLoading(false); - } - } - - if (!loadingKibanaIndexPatterns) { - setupEmbeddable(); - } - return () => { - isSubscribed = false; - }; - }, [loadingKibanaIndexPatterns, kibanaIndexPatterns]); - - // queryExpression updated useEffect - useEffect(() => { - if (embeddable != null) { - embeddable.updateInput({ query }); - } - }, [query]); - - useEffect(() => { - if (embeddable != null) { - embeddable.updateInput({ filters }); - } - }, [filters]); - - // DateRange updated useEffect - useEffect(() => { - if (embeddable != null && startDate != null && endDate != null) { - const timeRange = { - from: new Date(startDate).toISOString(), - to: new Date(endDate).toISOString(), - }; - embeddable.updateInput({ timeRange }); - } - }, [startDate, endDate]); - - return isError ? null : ( - <Embeddable> - <EmbeddableHeader title={i18n.EMBEDDABLE_HEADER_TITLE}> - <EuiText size="xs"> - <EuiLink - href={`${services.docLinks.ELASTIC_WEBSITE_URL}guide/en/siem/guide/${services.docLinks.DOC_LINK_VERSION}/conf-map-ui.html`} - target="_blank" - > - {i18n.EMBEDDABLE_HEADER_HELP} - </EuiLink> - </EuiText> - </EmbeddableHeader> - - <InPortal node={portalNode}> - <MapToolTip /> - </InPortal> - - <EmbeddableMap maintainRatio={!isIndexError}> - {embeddable != null ? ( - <EmbeddablePanel - data-test-subj="embeddable-panel" - embeddable={embeddable} - getActions={services.uiActions.getTriggerCompatibleActions} - getEmbeddableFactory={npStart.plugins.embeddable.getEmbeddableFactory} - getAllEmbeddableFactories={npStart.plugins.embeddable.getEmbeddableFactories} - notifications={services.notifications} - overlays={services.overlays} - inspector={services.inspector} - SavedObjectFinder={getSavedObjectFinder(services.savedObjects, services.uiSettings)} - /> - ) : !isLoading && isIndexError ? ( - <IndexPatternsMissingPrompt data-test-subj="missing-prompt" /> - ) : ( - <Loader data-test-subj="loading-panel" overlay size="xl" /> - )} - </EmbeddableMap> - </Embeddable> - ); -}; - -EmbeddedMapComponent.displayName = 'EmbeddedMapComponent'; - -export const EmbeddedMap = React.memo(EmbeddedMapComponent); - -EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts deleted file mode 100644 index 216fe9105327c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/types.ts +++ /dev/null @@ -1,58 +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 { RenderTooltipContentParams } from '../../../../maps/public'; -import { inputsModel } from '../../store/inputs'; - -export interface IndexPatternMapping { - title: string; - id: string; -} - -export interface LayerMappingDetails { - metricField: string; - geoField: string; - tooltipProperties: string[]; - label: string; -} - -export interface LayerMapping { - source: LayerMappingDetails; - destination: LayerMappingDetails; -} - -export interface LayerMappingCollection { - [indexPatternTitle: string]: LayerMapping; -} - -export type SetQuery = (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; -}) => void; - -export interface MapFeature { - id: number; - layerId: string; -} - -export interface LoadFeatureProps { - layerId: string; - featureId: number; -} - -export interface FeatureProperty { - _propertyKey: string; - _rawValue: string | string[]; -} - -export interface FeatureGeometry { - coordinates: [number]; - type: string; -} - -export type MapToolTipProps = Partial<RenderTooltipContentParams>; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx deleted file mode 100644 index c6d9dbc2fcfc8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ /dev/null @@ -1,218 +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 React, { useCallback, useMemo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { inputsActions, timelineActions } from '../../store/actions'; -import { - ColumnHeaderOptions, - SubsetTimelineModel, - TimelineModel, -} from '../../store/timeline/model'; -import { OnChangeItemsPerPage } from '../timeline/events'; -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { useUiSetting } from '../../lib/kibana'; -import { EventsViewer } from './events_viewer'; -import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; -import { TimelineTypeContextProps } from '../timeline/timeline_context'; -import { InspectButtonContainer } from '../inspect'; -import * as i18n from './translations'; - -export interface OwnProps { - defaultIndices?: string[]; - defaultModel: SubsetTimelineModel; - end: number; - id: string; - start: number; - headerFilterGroup?: React.ReactNode; - pageFilters?: Filter[]; - timelineTypeContext?: TimelineTypeContextProps; - utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; -} - -type Props = OwnProps & PropsFromRedux; - -const defaultTimelineTypeContext = { - loadingText: i18n.LOADING_EVENTS, -}; - -const StatefulEventsViewerComponent: React.FC<Props> = ({ - createTimeline, - columns, - dataProviders, - deletedEventIds, - defaultIndices, - deleteEventQuery, - end, - filters, - headerFilterGroup, - id, - isLive, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - pageFilters, - query, - removeColumn, - start, - showCheckboxes, - showRowRenderers, - sort, - timelineTypeContext = defaultTimelineTypeContext, - updateItemsPerPage, - upsertColumn, - utilityBar, -}) => { - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( - defaultIndices ?? useUiSetting<string[]>(DEFAULT_INDEX_KEY) - ); - - useEffect(() => { - if (createTimeline != null) { - createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); - } - return () => { - deleteEventQuery({ id, inputId: 'global' }); - }; - }, []); - - const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( - itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), - [id, updateItemsPerPage] - ); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - const exists = columns.findIndex(c => c.id === column.id) !== -1; - - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } - - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }, - [columns, id, upsertColumn, removeColumn] - ); - - const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); - - return ( - <InspectButtonContainer> - <EventsViewer - browserFields={browserFields} - columns={columns} - id={id} - dataProviders={dataProviders!} - deletedEventIds={deletedEventIds} - end={end} - filters={globalFilters} - headerFilterGroup={headerFilterGroup} - indexPattern={indexPatterns} - isLive={isLive} - itemsPerPage={itemsPerPage!} - itemsPerPageOptions={itemsPerPageOptions!} - kqlMode={kqlMode} - onChangeItemsPerPage={onChangeItemsPerPage} - query={query} - start={start} - sort={sort!} - timelineTypeContext={timelineTypeContext} - toggleColumn={toggleColumn} - utilityBar={utilityBar} - /> - </InspectButtonContainer> - ); -}; - -const makeMapStateToProps = () => { - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getEvents = timelineSelectors.getEventsByIdSelector(); - const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { - const input: inputsModel.InputsRange = getInputsTimeline(state); - const events: TimelineModel = getEvents(state, id) ?? defaultModel; - const { - columns, - dataProviders, - deletedEventIds, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - sort, - showCheckboxes, - showRowRenderers, - } = events; - - return { - columns, - dataProviders, - deletedEventIds, - filters: getGlobalFiltersQuerySelector(state), - id, - isLive: input.policy.kind === 'interval', - itemsPerPage, - itemsPerPageOptions, - kqlMode, - query: getGlobalQuerySelector(state), - sort, - showCheckboxes, - showRowRenderers, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - createTimeline: timelineActions.createTimeline, - deleteEventQuery: inputsActions.deleteOneQuery, - updateItemsPerPage: timelineActions.updateItemsPerPage, - removeColumn: timelineActions.removeColumn, - upsertColumn: timelineActions.upsertColumn, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const StatefulEventsViewer = connector( - React.memo( - StatefulEventsViewerComponent, - (prevProps, nextProps) => - prevProps.id === nextProps.id && - deepEqual(prevProps.columns, nextProps.columns) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - prevProps.deletedEventIds === nextProps.deletedEventIds && - prevProps.end === nextProps.end && - deepEqual(prevProps.filters, nextProps.filters) && - prevProps.isLive === nextProps.isLive && - prevProps.itemsPerPage === nextProps.itemsPerPage && - deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - prevProps.kqlMode === nextProps.kqlMode && - deepEqual(prevProps.query, nextProps.query) && - deepEqual(prevProps.sort, nextProps.sort) && - prevProps.start === nextProps.start && - deepEqual(prevProps.pageFilters, nextProps.pageFilters) && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && - prevProps.start === nextProps.start && - deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && - prevProps.utilityBar === nextProps.utilityBar - ) -); diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx deleted file mode 100644 index 44abe5b679c8e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ /dev/null @@ -1,111 +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 { EuiBadge } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import styled from 'styled-components'; - -import { State, timelineSelectors } from '../../store'; -import { DataProvider } from '../timeline/data_providers/data_provider'; -import { FlyoutButton } from './button'; -import { Pane } from './pane'; -import { timelineActions } from '../../store/actions'; -import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; -import { StatefulTimeline } from '../timeline'; -import { TimelineById } from '../../store/timeline/types'; - -export const Badge = styled(EuiBadge)` - position: absolute; - padding-left: 4px; - padding-right: 4px; - right: 0%; - top: 0%; - border-bottom-left-radius: 5px; -`; - -Badge.displayName = 'Badge'; - -const Visible = styled.div<{ show?: boolean }>` - visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; -`; - -Visible.displayName = 'Visible'; - -interface OwnProps { - flyoutHeight: number; - timelineId: string; - usersViewing: string[]; -} - -type Props = OwnProps & ProsFromRedux; - -export const FlyoutComponent = React.memo<Props>( - ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { - const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ - showTimeline, - timelineId, - ]); - const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ - showTimeline, - timelineId, - ]); - - return ( - <> - <Visible show={show}> - <Pane - flyoutHeight={flyoutHeight} - onClose={handleClose} - timelineId={timelineId} - width={width} - > - <StatefulTimeline onClose={handleClose} usersViewing={usersViewing} id={timelineId} /> - </Pane> - </Visible> - <FlyoutButton - dataProviders={dataProviders} - show={!show} - timelineId={timelineId} - onOpen={handleOpen} - /> - </> - ); - } -); - -FlyoutComponent.displayName = 'FlyoutComponent'; - -const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; -const DEFAULT_TIMELINE_BY_ID = {}; - -const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timelineById: TimelineById = - timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; - /* - In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender - of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS - */ - const dataProviders = timelineById[timelineId]?.dataProviders.length - ? timelineById[timelineId]?.dataProviders - : DEFAULT_DATA_PROVIDERS; - const show = timelineById[timelineId]?.show ?? false; - const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; - - return { dataProviders, show, width }; -}; - -const mapDispatchToProps = { - showTimeline: timelineActions.showTimeline, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -type ProsFromRedux = ConnectedProps<typeof connector>; - -export const Flyout = connector(FlyoutComponent); - -Flyout.displayName = 'Flyout'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx deleted file mode 100644 index abde602c1bdac..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.tsx +++ /dev/null @@ -1,33 +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 React from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_BYTES_FORMAT } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; - -type Bytes = string | number; - -export const formatBytes = (value: Bytes, format: string) => { - return numeral(value).format(format); -}; - -export const useFormatBytes = () => { - const [bytesFormat] = useUiSetting$<string>(DEFAULT_BYTES_FORMAT); - - return (value: Bytes) => formatBytes(value, bytesFormat); -}; - -export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => ( - <>{useFormatBytes()(value)}</> -); - -PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; - -export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); - -PreferenceFormattedBytes.displayName = 'PreferenceFormattedBytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx deleted file mode 100644 index 56fa0d56f3c3a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx +++ /dev/null @@ -1,41 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import '../../mock/match_media'; -import { HeaderGlobal } from './index'; - -jest.mock('react-router-dom', () => ({ - useLocation: () => ({ - pathname: '/app/siem#/hosts/allHosts', - hash: '', - search: '', - state: '', - }), - withRouter: () => jest.fn(), -})); - -jest.mock('ui/new_platform'); - -// Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar -jest.mock('../search_bar', () => ({ - SiemSearchBar: () => null, -})); - -describe('HeaderGlobal', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - test('it renders', () => { - const wrapper = shallow(<HeaderGlobal />); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx deleted file mode 100644 index f88ffc3f3c6c4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx +++ /dev/null @@ -1,122 +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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; -import React from 'react'; -import styled, { css } from 'styled-components'; - -import { LinkIcon, LinkIconProps } from '../link_icon'; -import { Subtitle, SubtitleProps } from '../subtitle'; -import { Title } from './title'; -import { DraggableArguments, BadgeOptions, TitleProp } from './types'; - -interface HeaderProps { - border?: boolean; - isLoading?: boolean; -} - -const Header = styled.header.attrs({ - className: 'siemHeaderPage', -})<HeaderProps>` - ${({ border, theme }) => css` - margin-bottom: ${theme.eui.euiSizeL}; - - ${border && - css` - border-bottom: ${theme.eui.euiBorderThin}; - padding-bottom: ${theme.eui.paddingSizes.l}; - .euiProgress { - top: ${theme.eui.paddingSizes.l}; - } - `} - `} -`; -Header.displayName = 'Header'; - -const FlexItem = styled(EuiFlexItem)` - display: block; -`; -FlexItem.displayName = 'FlexItem'; - -const LinkBack = styled.div.attrs({ - className: 'siemHeaderPage__linkBack', -})` - ${({ theme }) => css` - font-size: ${theme.eui.euiFontSizeXS}; - line-height: ${theme.eui.euiLineHeight}; - margin-bottom: ${theme.eui.euiSizeS}; - `} -`; -LinkBack.displayName = 'LinkBack'; - -const Badge = styled(EuiBadge)` - letter-spacing: 0; -`; -Badge.displayName = 'Badge'; - -interface BackOptions { - href: LinkIconProps['href']; - text: LinkIconProps['children']; -} - -export interface HeaderPageProps extends HeaderProps { - backOptions?: BackOptions; - badgeOptions?: BadgeOptions; - children?: React.ReactNode; - draggableArguments?: DraggableArguments; - subtitle?: SubtitleProps['items']; - subtitle2?: SubtitleProps['items']; - title: TitleProp; - titleNode?: React.ReactElement; -} - -const HeaderPageComponent: React.FC<HeaderPageProps> = ({ - backOptions, - badgeOptions, - border, - children, - draggableArguments, - isLoading, - subtitle, - subtitle2, - title, - titleNode, - ...rest -}) => ( - <Header border={border} {...rest}> - <EuiFlexGroup alignItems="center"> - <FlexItem> - {backOptions && ( - <LinkBack> - <LinkIcon href={backOptions.href} iconType="arrowLeft"> - {backOptions.text} - </LinkIcon> - </LinkBack> - )} - - {titleNode || ( - <Title - draggableArguments={draggableArguments} - title={title} - badgeOptions={badgeOptions} - /> - )} - - {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} - {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} - {border && isLoading && <EuiProgress size="xs" color="accent" />} - </FlexItem> - - {children && ( - <FlexItem data-test-subj="header-page-supplements" grow={false}> - {children} - </FlexItem> - )} - </EuiFlexGroup> - </Header> -); - -export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx deleted file mode 100644 index ba5874d42d515..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx +++ /dev/null @@ -1,81 +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 { EuiIcon, EuiLink, IconSize, IconType } from '@elastic/eui'; -import { LinkAnchorProps } from '@elastic/eui/src/components/link/link'; -import React from 'react'; -import styled, { css } from 'styled-components'; - -interface LinkProps { - color?: LinkAnchorProps['color']; - disabled?: boolean; - href?: string; - iconSide?: 'left' | 'right'; - onClick?: Function; - ariaLabel?: string; -} - -const Link = styled(({ iconSide, children, ...rest }) => <EuiLink {...rest}>{children}</EuiLink>)< - LinkProps ->` - ${({ iconSide, theme }) => css` - align-items: center; - display: inline-flex; - vertical-align: top; - white-space: nowrap; - - ${iconSide === 'left' && - css` - .euiIcon { - margin-right: ${theme.eui.euiSizeXS}; - } - `} - - ${iconSide === 'right' && - css` - flex-direction: row-reverse; - - .euiIcon { - margin-left: ${theme.eui.euiSizeXS}; - } - `} - `} -`; -Link.displayName = 'Link'; - -export interface LinkIconProps extends LinkProps { - children: string; - iconSize?: IconSize; - iconType: IconType; -} - -export const LinkIcon = React.memo<LinkIconProps>( - ({ - children, - color, - disabled, - href, - iconSide = 'left', - iconSize = 's', - iconType, - onClick, - ariaLabel, - }) => ( - <Link - className="siemLinkIcon" - color={color} - disabled={disabled} - href={href} - iconSide={iconSide} - onClick={onClick} - aria-label={ariaLabel ?? children} - > - <EuiIcon size={iconSize} type={iconType} /> - <span className="siemLinkIcon__label">{children}</span> - </Link> - ) -); -LinkIcon.displayName = 'LinkIcon'; diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.tsx b/x-pack/legacy/plugins/siem/public/components/links/index.tsx deleted file mode 100644 index 45225e31e9ac8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/links/index.tsx +++ /dev/null @@ -1,312 +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 { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { isNil } from 'lodash/fp'; -import styled from 'styled-components'; - -import { IP_REPUTATION_LINKS_SETTING } from '../../../../../../plugins/siem/common/constants'; -import { - DefaultFieldRendererOverflow, - DEFAULT_MORE_MAX_HEIGHT, -} from '../field_renderers/field_renderers'; -import { encodeIpv6 } from '../../lib/helpers'; -import { - getCaseDetailsUrl, - getHostDetailsUrl, - getIPDetailsUrl, - getCreateCaseUrl, -} from '../link_to'; -import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; -import { useUiSetting$ } from '../../lib/kibana'; -import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; -import { ExternalLinkIcon } from '../external_link_icon'; -import { navTabs } from '../../pages/home/home_navigations'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; - -import * as i18n from './translations'; - -export const DEFAULT_NUMBER_OF_LINK = 5; - -// Internal Links -const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ - children, - hostName, -}) => ( - <EuiLink href={getHostDetailsUrl(encodeURIComponent(hostName))}> - {children ? children : hostName} - </EuiLink> -); - -const whitelistUrlSchemes = ['http://', 'https://']; -export const ExternalLink = React.memo<{ - url: string; - children?: React.ReactNode; - idx?: number; - overflowIndexStart?: number; - allItemsLimit?: number; -}>( - ({ - url, - children, - idx, - overflowIndexStart = DEFAULT_NUMBER_OF_LINK, - allItemsLimit = DEFAULT_NUMBER_OF_LINK, - }) => { - const lastVisibleItemIndex = overflowIndexStart - 1; - const lastItemIndex = allItemsLimit - 1; - const lastIndexToShow = Math.max(0, Math.min(lastVisibleItemIndex, lastItemIndex)); - const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); - return url && inWhitelist && !isUrlInvalid(url) && children ? ( - <EuiToolTip content={url} position="top" data-test-subj="externalLinkTooltip"> - <EuiLink href={url} target="_blank" rel="noopener" data-test-subj="externalLink"> - {children} - <ExternalLinkIcon data-test-subj="externalLinkIcon" /> - {!isNil(idx) && idx < lastIndexToShow && <Comma data-test-subj="externalLinkComma" />} - </EuiLink> - </EuiToolTip> - ) : null; - } -); - -ExternalLink.displayName = 'ExternalLink'; - -export const HostDetailsLink = React.memo(HostDetailsLinkComponent); - -const IPDetailsLinkComponent: React.FC<{ - children?: React.ReactNode; - ip: string; - flowTarget?: FlowTarget | FlowTargetSourceDest; -}> = ({ children, ip, flowTarget = FlowTarget.source }) => ( - <EuiLink href={`${getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget)}`}> - {children ? children : ip} - </EuiLink> -); - -export const IPDetailsLink = React.memo(IPDetailsLinkComponent); - -const CaseDetailsLinkComponent: React.FC<{ - children?: React.ReactNode; - detailName: string; - title?: string; -}> = ({ children, detailName, title }) => { - const search = useGetUrlSearch(navTabs.case); - - return ( - <EuiLink - href={getCaseDetailsUrl({ id: detailName, search })} - data-test-subj="case-details-link" - aria-label={i18n.CASE_DETAILS_LINK_ARIA(title ?? detailName)} - > - {children ? children : detailName} - </EuiLink> - ); -}; -export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); -CaseDetailsLink.displayName = 'CaseDetailsLink'; - -export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { - const search = useGetUrlSearch(navTabs.case); - return <EuiLink href={getCreateCaseUrl(search)}>{children}</EuiLink>; -}); - -CreateCaseLink.displayName = 'CreateCaseLink'; - -// External Links -export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( - ({ children, link }) => ( - <ExternalLink url={`https://www.google.com/search?q=${encodeURIComponent(link)}`}> - {children ? children : link} - </ExternalLink> - ) -); - -GoogleLink.displayName = 'GoogleLink'; - -export const PortOrServiceNameLink = React.memo<{ - children?: React.ReactNode; - portOrServiceName: number | string; -}>(({ children, portOrServiceName }) => ( - <EuiLink - data-test-subj="port-or-service-name-link" - href={`https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=${encodeURIComponent( - String(portOrServiceName) - )}`} - target="_blank" - > - {children ? children : portOrServiceName} - </EuiLink> -)); - -PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; - -export const Ja3FingerprintLink = React.memo<{ - children?: React.ReactNode; - ja3Fingerprint: string; -}>(({ children, ja3Fingerprint }) => ( - <EuiLink - data-test-subj="ja3-fingerprint-link" - href={`https://sslbl.abuse.ch/ja3-fingerprints/${encodeURIComponent(ja3Fingerprint)}`} - target="_blank" - > - {children ? children : ja3Fingerprint} - </EuiLink> -)); - -Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; - -export const CertificateFingerprintLink = React.memo<{ - children?: React.ReactNode; - certificateFingerprint: string; -}>(({ children, certificateFingerprint }) => ( - <EuiLink - data-test-subj="certificate-fingerprint-link" - href={`https://sslbl.abuse.ch/ssl-certificates/sha1/${encodeURIComponent( - certificateFingerprint - )}`} - target="_blank" - > - {children ? children : certificateFingerprint} - </EuiLink> -)); - -CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; - -enum DefaultReputationLink { - 'virustotal.com' = 'virustotal.com', - 'talosIntelligence.com' = 'talosIntelligence.com', -} - -export interface ReputationLinkSetting { - name: string; - url_template: string; -} - -function isDefaultReputationLink(name: string): name is DefaultReputationLink { - return ( - name === DefaultReputationLink['virustotal.com'] || - name === DefaultReputationLink['talosIntelligence.com'] - ); -} -const isReputationLink = ( - rowItem: string | ReputationLinkSetting -): rowItem is ReputationLinkSetting => - (rowItem as ReputationLinkSetting).url_template !== undefined && - (rowItem as ReputationLinkSetting).name !== undefined; - -export const Comma = styled('span')` - margin-right: 5px; - margin-left: 5px; - &::after { - content: ' ,'; - } -`; - -Comma.displayName = 'Comma'; - -const defaultNameMapping: Record<DefaultReputationLink, string> = { - [DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL, - [DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE, -}; - -const ReputationLinkComponent: React.FC<{ - overflowIndexStart?: number; - allItemsLimit?: number; - showDomain?: boolean; - domain: string; - direction?: 'row' | 'column'; -}> = ({ - overflowIndexStart = DEFAULT_NUMBER_OF_LINK, - allItemsLimit = DEFAULT_NUMBER_OF_LINK, - showDomain = false, - domain, - direction = 'row', -}) => { - const [ipReputationLinksSetting] = useUiSetting$<ReputationLinkSetting[]>( - IP_REPUTATION_LINKS_SETTING - ); - - const ipReputationLinks: ReputationLinkSetting[] = useMemo( - () => - ipReputationLinksSetting - ?.slice(0, allItemsLimit) - .filter( - ({ url_template, name }) => - !isNil(url_template) && !isNil(name) && !isUrlInvalid(url_template) - ) - .map(({ name, url_template }: { name: string; url_template: string }) => ({ - name: isDefaultReputationLink(name) ? defaultNameMapping[name] : name, - url_template: url_template.replace(`{{ip}}`, encodeURIComponent(domain)), - })), - [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] - ); - - return ipReputationLinks?.length > 0 ? ( - <section> - <EuiFlexGroup - gutterSize="none" - justifyContent="center" - direction={direction} - alignItems="center" - data-test-subj="reputationLinkGroup" - > - <EuiFlexItem grow={true}> - {ipReputationLinks - ?.slice(0, overflowIndexStart) - .map(({ name, url_template: urlTemplate }: ReputationLinkSetting, id) => ( - <ExternalLink - allItemsLimit={ipReputationLinks.length} - idx={id} - overflowIndexStart={overflowIndexStart} - url={urlTemplate} - data-test-subj="externalLinkComponent" - key={`reputationLink-${id}`} - > - <>{showDomain ? domain : name ?? domain}</> - </ExternalLink> - ))} - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <DefaultFieldRendererOverflow - rowItems={ipReputationLinks} - idPrefix="moreReputationLink" - render={rowItem => { - return ( - isReputationLink(rowItem) && ( - <ExternalLink - url={rowItem.url_template} - overflowIndexStart={overflowIndexStart} - allItemsLimit={allItemsLimit} - > - <>{rowItem.name ?? domain}</> - </ExternalLink> - ) - ); - }} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={overflowIndexStart} - /> - </EuiFlexItem> - </EuiFlexGroup> - </section> - ) : null; -}; - -ReputationLinkComponent.displayName = 'ReputationLinkComponent'; - -export const ReputationLink = React.memo(ReputationLinkComponent); - -export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( - ({ children, domain }) => ( - <ExternalLink url={`https://www.iana.org/whois?q=${encodeURIComponent(domain)}`}> - {children ? children : domain} - </ExternalLink> - ) -); - -WhoIsLink.displayName = 'WhoIsLink'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx b/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx deleted file mode 100644 index ee004a3c572bb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/markdown_editor/index.tsx +++ /dev/null @@ -1,164 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiPanel, - EuiTabbedContent, - EuiTextArea, -} from '@elastic/eui'; -import React, { useMemo, useCallback, ChangeEvent } from 'react'; -import styled, { css } from 'styled-components'; - -import { Markdown } from '../markdown'; -import * as i18n from './translations'; -import { MARKDOWN_HELP_LINK } from './constants'; - -const TextArea = styled(EuiTextArea)` - width: 100%; -`; - -const Container = styled(EuiPanel)` - ${({ theme }) => css` - padding: 0; - background: ${theme.eui.euiColorLightestShade}; - position: relative; - .markdown-tabs-header { - position: absolute; - top: ${theme.eui.euiSizeS}; - right: ${theme.eui.euiSizeS}; - z-index: ${theme.eui.euiZContentMenu}; - } - .euiTab { - padding: 10px; - } - .markdown-tabs { - width: 100%; - } - .markdown-tabs-footer { - height: 41px; - padding: 0 ${theme.eui.euiSizeM}; - .euiLink { - font-size: ${theme.eui.euiSizeM}; - } - } - .euiFormRow__labelWrapper { - position: absolute; - top: -${theme.eui.euiSizeL}; - } - .euiFormErrorText { - padding: 0 ${theme.eui.euiSizeM}; - } - `} -`; - -const MarkdownContainer = styled(EuiPanel)` - min-height: 150px; - overflow: auto; -`; - -export interface CursorPosition { - start: number; - end: number; -} - -/** An input for entering a new case description */ -export const MarkdownEditor = React.memo<{ - bottomRightContent?: React.ReactNode; - topRightContent?: React.ReactNode; - content: string; - isDisabled?: boolean; - onChange: (description: string) => void; - onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void; - placeholder?: string; -}>( - ({ - bottomRightContent, - topRightContent, - content, - isDisabled = false, - onChange, - placeholder, - onCursorPositionUpdate, - }) => { - const handleOnChange = useCallback( - (evt: ChangeEvent<HTMLTextAreaElement>) => { - onChange(evt.target.value); - }, - [onChange] - ); - - const setCursorPosition = (e: React.ChangeEvent<HTMLTextAreaElement>) => { - if (onCursorPositionUpdate) { - onCursorPositionUpdate({ - start: e!.target!.selectionStart ?? 0, - end: e!.target!.selectionEnd ?? 0, - }); - } - return false; - }; - - const tabs = useMemo( - () => [ - { - id: 'comment', - name: i18n.MARKDOWN, - content: ( - <TextArea - onChange={handleOnChange} - onBlur={setCursorPosition} - aria-label={`markdown-editor-comment`} - fullWidth={true} - disabled={isDisabled} - placeholder={placeholder ?? ''} - spellCheck={false} - value={content} - /> - ), - }, - { - id: 'preview', - name: i18n.PREVIEW, - content: ( - <MarkdownContainer data-test-subj="markdown-container" paddingSize="s"> - <Markdown raw={content} /> - </MarkdownContainer> - ), - }, - ], - [content, isDisabled, placeholder] - ); - return ( - <Container> - {topRightContent && <div className={`markdown-tabs-header`}>{topRightContent}</div>} - <EuiTabbedContent - className={`markdown-tabs`} - data-test-subj={`markdown-tabs`} - size="s" - tabs={tabs} - initialSelectedTab={tabs[0]} - /> - <EuiFlexGroup - className={`markdown-tabs-footer`} - alignItems="center" - gutterSize="none" - justifyContent="spaceBetween" - > - <EuiFlexItem grow={false}> - <EuiLink href={MARKDOWN_HELP_LINK} external target="_blank"> - {i18n.MARKDOWN_SYNTAX_HELP} - </EuiLink> - </EuiFlexItem> - {bottomRightContent && <EuiFlexItem grow={false}>{bottomRightContent}</EuiFlexItem>} - </EuiFlexGroup> - </Container> - ); - } -); - -MarkdownEditor.displayName = 'MarkdownEditor'; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 5aa846d15b684..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,5 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKF jbBKkl\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div>"`; - -exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKF hneqJM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKH iNPult\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\" data-test-subj=\\"spacer\\"></div>"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts deleted file mode 100644 index 98437845a3ab7..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/types.ts +++ /dev/null @@ -1,143 +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 { EuiTitleSize } from '@elastic/eui'; -import { ScaleType, Position, TickFormatter } from '@elastic/charts'; -import { ActionCreator } from 'redux'; -import { ESQuery } from '../../../../../../plugins/siem/common/typed_json'; -import { SetQuery } from '../../pages/hosts/navigation/types'; -import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../graphql/types'; -import { UpdateDateRange } from '../charts/common'; - -export type MatrixHistogramMappingTypes = Record< - string, - { key: string; value: null; color?: string | undefined } ->; -export interface MatrixHistogramOption { - text: string; - value: string; -} - -export type GetSubTitle = (count: number) => string; -export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; - -export interface MatrixHisrogramConfigs { - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string | GetTitle; - titleSize?: EuiTitleSize; -} - -interface MatrixHistogramBasicProps { - chartHeight?: number; - defaultIndex: string[]; - defaultStackByOption: MatrixHistogramOption; - dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; - endDate: number; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - id: string; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - panelHeight?: number; - setQuery: SetQuery; - startDate: number; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title?: string | GetTitle; - titleSize?: EuiTitleSize; -} - -export interface MatrixHistogramQueryProps { - endDate: number; - errorMessage: string; - filterQuery?: ESQuery | string | undefined; - setAbsoluteRangeDatePicker?: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - stackByField: string; - startDate: number; - indexToAdd?: string[] | null; - isInspected: boolean; - histogramType: HistogramType; -} - -export interface MatrixHistogramProps extends MatrixHistogramBasicProps { - scaleType?: ScaleType; - yTickFormatter?: (value: number) => string; - showLegend?: boolean; - showSpacer?: boolean; - legendPosition?: Position; -} - -export interface HistogramBucket { - key_as_string: string; - key: number; - doc_count: number; -} -export interface GroupBucket { - key: string; - signals: { - buckets: HistogramBucket[]; - }; -} - -export interface HistogramAggregation { - histogramAgg: { - buckets: GroupBucket[]; - }; -} - -export interface BarchartConfigs { - series: { - xScaleType: ScaleType; - yScaleType: ScaleType; - stackAccessors: string[]; - }; - axis: { - xTickFormatter: TickFormatter; - yTickFormatter: TickFormatter; - tickSize: number; - }; - settings: { - legendPosition: Position; - onBrushEnd: UpdateDateRange; - showLegend: boolean; - showLegendExtra: boolean; - theme: { - scales: { - barsPadding: number; - }; - chartMargins: { - left: number; - right: number; - top: number; - bottom: number; - }; - chartPaddings: { - left: number; - right: number; - top: number; - bottom: number; - }; - }; - }; - customHeight: number; -} diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts deleted file mode 100644 index 991c82cf701e8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts +++ /dev/null @@ -1,203 +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 { MlError } from '../ml/types'; -import { AuditMessageBase } from '../../../../../../plugins/ml/common/types/audit_message'; - -export interface Group { - id: string; - jobIds: string[]; - calendarIds: string[]; -} - -export interface CheckRecognizerProps { - indexPatternName: string[]; - signal: AbortSignal; -} - -export interface RecognizerModule { - id: string; - title: string; - query: Record<string, object>; - description: string; - logo: { - icon: string; - }; -} - -export interface GetModulesProps { - moduleId?: string; - signal: AbortSignal; -} - -export interface Module { - id: string; - title: string; - description: string; - type: string; - logoFile: string; - defaultIndexPattern: string; - query: Record<string, object>; - jobs: ModuleJob[]; - datafeeds: ModuleDatafeed[]; - kibana: object; -} - -/** - * Representation of an ML Job as returned from `the ml/modules/get_module` API - */ -export interface ModuleJob { - id: string; - config: { - groups: string[]; - description: string; - analysis_config: { - bucket_span: string; - summary_count_field_name?: string; - detectors: Detector[]; - influencers: string[]; - }; - analysis_limits: { - model_memory_limit: string; - }; - data_description: { - time_field: string; - time_format?: string; - }; - model_plot_config?: { - enabled: boolean; - }; - custom_settings: { - created_by: string; - custom_urls: CustomURL[]; - }; - job_type: string; - }; -} - -// TODO: Speak to ML team about why the get_module API will sometimes return indexes and other times indices -// See mockGetModuleResponse for examples -export interface ModuleDatafeed { - id: string; - config: { - job_id: string; - indexes?: string[]; - indices?: string[]; - query: Record<string, object>; - }; -} - -export interface MlSetupArgs { - configTemplate: string; - indexPatternName: string; - jobIdErrorFilter: string[]; - groups: string[]; - prefix?: string; -} - -/** - * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API - */ -export interface JobSummary { - auditMessage?: AuditMessageBase; - datafeedId: string; - datafeedIndices: string[]; - datafeedState: string; - description: string; - earliestTimestampMs?: number; - latestResultsTimestampMs?: number; - groups: string[]; - hasDatafeed: boolean; - id: string; - isSingleMetricViewerJob: boolean; - jobState: string; - latestTimestampMs?: number; - memory_status: string; - nodeName?: string; - processed_record_count: number; -} - -export interface Detector { - detector_description: string; - function: string; - by_field_name: string; - partition_field_name?: string; -} - -export interface CustomURL { - url_name: string; - url_value: string; -} - -/** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary - * that includes necessary metadata like moduleName, defaultIndexPattern, etc. - */ -export interface SiemJob extends JobSummary { - moduleId: string; - defaultIndexPattern: string; - isCompatible: boolean; - isInstalled: boolean; - isElasticJob: boolean; -} - -export interface AugmentedSiemJobFields { - moduleId: string; - defaultIndexPattern: string; - isCompatible: boolean; - isElasticJob: boolean; -} - -export interface SetupMlResponseJob { - id: string; - success: boolean; - error?: MlError; -} - -export interface SetupMlResponseDatafeed { - id: string; - success: boolean; - started: boolean; - error?: MlError; -} - -export interface SetupMlResponse { - jobs: SetupMlResponseJob[]; - datafeeds: SetupMlResponseDatafeed[]; - kibana: {}; -} - -export interface StartDatafeedResponse { - [key: string]: { - started: boolean; - error?: string; - }; -} - -export interface ErrorResponse { - statusCode?: number; - error?: string; - message?: string; -} - -export interface StopDatafeedResponse { - [key: string]: { - stopped: boolean; - }; -} - -export interface CloseJobsResponse { - [key: string]: { - closed: boolean; - }; -} - -export interface JobsFilters { - filterQuery: string; - showCustomJobs: boolean; - showElasticJobs: boolean; - selectedGroups: string[]; -} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts deleted file mode 100644 index 5407eba8b5b29..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ /dev/null @@ -1,143 +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 { getOr, omit } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { APP_NAME } from '../../../../../../../plugins/siem/common/constants'; -import { StartServices } from '../../../plugin'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; -import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; -import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; -import { SiemPageName } from '../../../pages/home/types'; -import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; -import { getOverviewUrl } from '../../link_to'; - -import { TabNavigationProps } from '../tab_navigation/types'; -import { getSearch } from '../helpers'; -import { SearchNavTab } from '../types'; - -export const setBreadcrumbs = ( - spyState: RouteSpyState & TabNavigationProps, - chrome: StartServices['chrome'] -) => { - const breadcrumbs = getBreadcrumbsForRoute(spyState); - if (breadcrumbs) { - chrome.setBreadcrumbs(breadcrumbs); - } -}; - -export const siemRootBreadcrumb: ChromeBreadcrumb[] = [ - { - text: APP_NAME, - href: getOverviewUrl(), - }, -]; - -const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.network; - -const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.hosts; - -const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => - spyState != null && spyState.pageName === SiemPageName.case; - -const isDetectionsRoutes = (spyState: RouteSpyState) => - spyState != null && spyState.pageName === SiemPageName.detections; - -export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps -): ChromeBreadcrumb[] | null => { - const spyState: RouteSpyState = omit('navTabs', object); - if (isHostsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - ...siemRootBreadcrumb, - ...getHostDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isNetworkRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - ...siemRootBreadcrumb, - ...getIPDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isDetectionsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - ...siemRootBreadcrumb, - ...getDetectionRulesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isCaseRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - ...siemRootBreadcrumb, - ...getCaseDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if ( - spyState != null && - object.navTabs && - spyState.pageName && - object.navTabs[spyState.pageName] - ) { - return [ - ...siemRootBreadcrumb, - { - text: object.navTabs[spyState.pageName].name, - href: '', - }, - ]; - } - - return null; -}; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts b/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts deleted file mode 100644 index 899d108fe246d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/helpers.ts +++ /dev/null @@ -1,68 +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 { isEmpty } from 'lodash/fp'; -import { Location } from 'history'; - -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { CONSTANTS } from '../url_state/constants'; -import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; -import { - replaceQueryStringInLocation, - replaceStateKeyInQueryString, - getQueryStringFromLocation, -} from '../url_state/helpers'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; - -import { SearchNavTab } from './types'; - -export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { - if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { - return URL_STATE_KEYS[tab.urlKey].reduce<Location>( - (myLocation: Location, urlKey: KeyUrlState) => { - let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = ''; - - if (urlKey === CONSTANTS.appQuery && urlState.query != null) { - if (urlState.query.query === '') { - urlStateToReplace = ''; - } else { - urlStateToReplace = urlState.query; - } - } else if (urlKey === CONSTANTS.filters && urlState.filters != null) { - if (isEmpty(urlState.filters)) { - urlStateToReplace = ''; - } else { - urlStateToReplace = urlState.filters; - } - } else if (urlKey === CONSTANTS.timerange) { - urlStateToReplace = urlState[CONSTANTS.timerange]; - } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { - const timeline = urlState[CONSTANTS.timeline]; - if (timeline.id === '') { - urlStateToReplace = ''; - } else { - urlStateToReplace = timeline; - } - } - return replaceQueryStringInLocation( - myLocation, - replaceStateKeyInQueryString( - urlKey, - urlStateToReplace - )(getQueryStringFromLocation(myLocation.search)) - ); - }, - { - pathname: '', - hash: '', - search: '', - state: '', - } - ).search; - } - return ''; -}; diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx deleted file mode 100644 index a821d310344d8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ /dev/null @@ -1,239 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { CONSTANTS } from '../url_state/constants'; -import { SiemNavigationComponent } from './'; -import { setBreadcrumbs } from './breadcrumbs'; -import { navTabs } from '../../pages/home/home_navigations'; -import { HostsTableType } from '../../store/hosts/model'; -import { RouteSpyState } from '../../utils/route/types'; -import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; - -jest.mock('ui/new_platform'); -jest.mock('./breadcrumbs', () => ({ - setBreadcrumbs: jest.fn(), -})); - -describe('SIEM Navigation', () => { - const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { - pageName: 'hosts', - pathName: '/hosts', - detailName: undefined, - search: '', - tabName: HostsTableType.authentications, - navTabs, - urlState: { - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }, - }; - const wrapper = mount(<SiemNavigationComponent {...mockProps} />); - test('it calls setBreadcrumbs with correct path on mount', () => { - expect(setBreadcrumbs).toHaveBeenNthCalledWith( - 1, - { - detailName: undefined, - navTabs: { - case: { - disabled: false, - href: '#/link-to/case', - id: 'case', - name: 'Cases', - urlKey: 'case', - }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, - hosts: { - disabled: false, - href: '#/link-to/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '#/link-to/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '#/link-to/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '#/link-to/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - }, - pageName: 'hosts', - pathName: '/hosts', - search: '', - tabName: 'authentications', - query: { query: '', language: 'kuery' }, - filters: [], - savedQuery: undefined, - timeline: { - id: '', - isOpen: false, - }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - }, - }, - undefined - ); - }); - test('it calls setBreadcrumbs with correct path on update', () => { - wrapper.setProps({ - pageName: 'network', - pathName: '/network', - tabName: undefined, - }); - wrapper.update(); - expect(setBreadcrumbs).toHaveBeenNthCalledWith( - 1, - { - detailName: undefined, - filters: [], - navTabs: { - case: { - disabled: false, - href: '#/link-to/case', - id: 'case', - name: 'Cases', - urlKey: 'case', - }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, - hosts: { - disabled: false, - href: '#/link-to/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '#/link-to/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '#/link-to/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '#/link-to/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - }, - pageName: 'hosts', - pathName: '/hosts', - query: { language: 'kuery', query: '' }, - savedQuery: undefined, - search: '', - state: undefined, - tabName: 'authentications', - timeline: { id: '', isOpen: false }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - }, - }, - undefined - ); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx deleted file mode 100644 index b9563b60f301b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ /dev/null @@ -1,159 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { navTabs } from '../../../pages/home/home_navigations'; -import { SiemPageName } from '../../../pages/home/types'; -import { navTabsHostDetails } from '../../../pages/hosts/details/nav_tabs'; -import { HostsTableType } from '../../../store/hosts/model'; -import { RouteSpyState } from '../../../utils/route/types'; -import { CONSTANTS } from '../../url_state/constants'; -import { TabNavigationComponent } from './'; -import { TabNavigationProps } from './types'; - -jest.mock('ui/new_platform'); - -describe('Tab Navigation', () => { - const pageName = SiemPageName.hosts; - const hostName = 'siem-window'; - const tabName = HostsTableType.authentications; - const pathName = `/${pageName}/${hostName}/${tabName}`; - - describe('Page Navigation', () => { - const mockProps: TabNavigationProps & RouteSpyState = { - pageName, - pathName, - detailName: undefined, - search: '', - tabName, - navTabs, - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }; - test('it mounts with correct tab highlighted', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const hostsTab = wrapper.find('EuiTab[data-test-subj="navigation-hosts"]'); - expect(hostsTab.prop('isSelected')).toBeTruthy(); - }); - test('it changes active tab when nav changes by props', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const networkTab = () => wrapper.find('EuiTab[data-test-subj="navigation-network"]').first(); - expect(networkTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ - pageName: 'network', - pathName: '/network', - tabName: undefined, - }); - wrapper.update(); - expect(networkTab().prop('isSelected')).toBeTruthy(); - }); - test('it carries the url state in the link', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const firstTab = wrapper.find('EuiTab[data-test-subj="navigation-network"]'); - expect(firstTab.props().href).toBe( - "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" - ); - }); - }); - - describe('Table Navigation', () => { - const mockHasMlUserPermissions = true; - const mockProps: TabNavigationProps & RouteSpyState = { - pageName: 'hosts', - pathName: '/hosts', - detailName: undefined, - search: '', - tabName: HostsTableType.authentications, - navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions), - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }; - test('it mounts with correct tab highlighted', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const tableNavigationTab = wrapper.find( - `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` - ); - - expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); - }); - test('it changes active tab when nav changes by props', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const tableNavigationTab = () => - wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); - expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ - pageName: SiemPageName.hosts, - pathName: `/${SiemPageName.hosts}`, - tabName: HostsTableType.events, - }); - wrapper.update(); - expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); - }); - test('it carries the url state in the link', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const firstTab = wrapper.find( - `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` - ); - expect(firstTab.props().href).toBe( - `#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))` - ); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts deleted file mode 100644 index fe701ad115d17..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/types.ts +++ /dev/null @@ -1,33 +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 { UrlInputsModel } from '../../../store/inputs/model'; -import { CONSTANTS } from '../../url_state/constants'; -import { HostsTableType } from '../../../store/hosts/model'; -import { TimelineUrl } from '../../../store/timeline/model'; -import { Filter, Query } from '../../../../../../../../src/plugins/data/public'; - -import { SiemNavigationProps } from '../types'; - -export interface TabNavigationProps extends SiemNavigationProps { - pathName: string; - pageName: string; - tabName: HostsTableType | undefined; - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; -} - -export interface TabNavigationItemProps { - href: string; - hrefWithSearch: string; - id: string; - disabled: boolean; - name: string; - isSelected: boolean; -} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/types.ts b/x-pack/legacy/plugins/siem/public/components/navigation/types.ts deleted file mode 100644 index 96a70e0bc70cc..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/navigation/types.ts +++ /dev/null @@ -1,40 +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 { Filter, Query } from '../../../../../../../src/plugins/data/public'; -import { HostsTableType } from '../../store/hosts/model'; -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { CONSTANTS, UrlStateType } from '../url_state/constants'; - -export interface SiemNavigationProps { - display?: 'default' | 'condensed'; - navTabs: Record<string, NavTab>; -} - -export interface SiemNavigationComponentProps { - pathName: string; - pageName: string; - tabName: HostsTableType | undefined; - urlState: { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; - }; -} - -export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; - -export interface NavTab { - id: string; - name: string; - href: string; - disabled: boolean; - urlKey: UrlStateType; - isDetailPage?: boolean; -} diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts deleted file mode 100644 index 686ec4e86e785..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.test.ts +++ /dev/null @@ -1,492 +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 { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../../../../plugins/siem/common/constants'; -import { KibanaServices } from '../../lib/kibana'; -import { rawNewsApiResponse } from '../../mock/news'; -import { rawNewsJSON } from '../../mock/raw_news'; - -import { - fetchNews, - getLocale, - getNewsFeedUrl, - getNewsItemsFromApiResponse, - removeSnapshotFromVersion, - showNewsItem, -} from './helpers'; -import { NewsItem, RawNewsApiResponse } from './types'; - -jest.mock('../../lib/kibana'); - -describe('helpers', () => { - describe('removeSnapshotFromVersion', () => { - test('it should remove an all-caps `-SNAPSHOT`', () => { - const version = '8.0.0-SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should remove a mixed-case `-SnApShoT`', () => { - const version = '8.0.0-SnApShoT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should remove all occurrences of `-SNAPSHOT`, regardless of where they appear in the version', () => { - const version = '-SNAPSHOT8.0.0-SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should NOT transform a version when it does not contain a `-SNAPSHOT`', () => { - const version = '8.0.0'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should NOT transform a version if it omits the dash in `SNAPSHOT`', () => { - const version = '8.0.0SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0SNAPSHOT'); - }); - - test('it should NOT transform a version if has only a partial `-SNAPSHOT`', () => { - const version = '8.0.0-SNAP'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0-SNAP'); - }); - - test('it should NOT transform an undefined version', () => { - const version = undefined; - - expect(removeSnapshotFromVersion(version)).toBeUndefined(); - }); - - test('it should NOT transform an empty version', () => { - const version = ''; - - expect(removeSnapshotFromVersion(version)).toEqual(''); - }); - }); - - describe('getNewsFeedUrl', () => { - const getKibanaVersion = () => '8.0.0'; - - test('it combines the (default) base URL from settings and the Kibana version to return the expected URL', () => { - expect( - getNewsFeedUrl({ newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, getKibanaVersion }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - - test('it combines a URL with extra whitespace and the Kibana version to return the expected URL', () => { - const withExtraWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT} `; - - expect(getNewsFeedUrl({ newsFeedUrlSetting: withExtraWhitespace, getKibanaVersion })).toEqual( - 'https://feeds.elastic.co/security-solution/v8.0.0.json' - ); - }); - - test('it combines a URL with a trailing slash and the Kibana version to return the expected URL', () => { - const withTrailingSlash = `${NEWS_FEED_URL_SETTING_DEFAULT}/`; - - expect(getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlash, getKibanaVersion })).toEqual( - 'https://feeds.elastic.co/security-solution/v8.0.0.json' - ); - }); - - test('it combines a URL with a trailing slash plus whitespace and the Kibana version to return the expected URL', () => { - const withTrailingSlashPlusWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT}/ `; - - expect( - getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlashPlusWhitespace, getKibanaVersion }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - - test('it combines a URL and a Kibana version with a `-SNAPSHOT` to return the expected URL', () => { - const getKibanaVersionWithSnapshot = () => '8.0.0-SNAPSHOT'; - - expect( - getNewsFeedUrl({ - newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, - getKibanaVersion: getKibanaVersionWithSnapshot, - }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - }); - - describe('getLocale', () => { - const fallback = 'wowzers'; - - test('it returns language specified in the document', () => { - const lang = 'ja'; - - document.documentElement.lang = lang; - - expect(getLocale(fallback)).toEqual(lang); - }); - - test('it returns the fallback when the language in the document is an empty string', () => { - document.documentElement.lang = ''; - - expect(getLocale(fallback)).toEqual(fallback); - }); - }); - - describe('getNewsItemsFromApiResponse', () => { - const expectedNewsItems: NewsItem[] = [ - { - description: - "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", - expireOn: expect.any(Date), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: - 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', - linkUrl: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Got SIEM Questions?', - }, - { - description: - 'Elastic Security combines the threat hunting and analytics of Elastic SIEM with the prevention and response provided by Elastic Endpoint Security.', - expireOn: expect.any(Date), - hash: 'edcb2d396ffdd80bfd5a97fbc0dc9f4b73477f9be556863fe0a1caf086679420', - imageUrl: - 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1caa35177420c61b/5d0d0394d8ff351753cbf2c5/illustrated-screenshot-hero-siem.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/elastic-security-7-5-0-released?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Elastic Security 7.5.0 released', - }, - { - description: - 'At Elastic, we’re bringing endpoint protection and SIEM together into the same experience to streamline how you secure your organization.', - expireOn: expect.any(Date), - hash: 'ec970adc85e9eede83f77e4cc6a6fea00cd7822cbe48a71dc2c5f1df10939196', - imageUrl: - 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd0eb8689eafe398a/5d970ecc1970e80e85277925/illustration-endpoint-hero.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/webinars/elastic-endpoint-security-overview-security-starts-at-the-endpoint?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Elastic Endpoint Security Overview Webinar', - }, - { - description: - 'For small businesses and homes, having access to effective security analytics can come at a high cost of either time or money. Well, until now!', - expireOn: expect.any(Date), - hash: 'aa243fd5845356a5ccd54a7a11b208ed307e0d88158873b1fcf7d1164b739bac', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt024c26b7636cb24f/5daf4e293a326d6df6c0e025/home-siem-blog-1-map.jpg?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/elastic-siem-for-small-business-and-home-1-getting-started?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Trying Elastic SIEM at Home?', - }, - { - description: - 'Elastic is excited to announce the introduction of Elastic Endpoint Security, based on Elastic’s acquisition of Endgame, a pioneer and industry-recognized leader in endpoint threat prevention, detection, and response.', - expireOn: expect.any(Date), - hash: '3c64576c9749d33ff98726d641cdf2fb2bfde3dd9a6f99ff2573ac8d8c5b2c02', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt1f87637fb7870298/5d9fe27bf8ca980f8717f6f8/screenshot-resolver-trickbot-enrichments-showing-defender-shutdown-endgame-2-optimized.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/introducing-elastic-endpoint-security?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Introducing Elastic Endpoint Security', - }, - { - description: - 'Elastic SIEM is powered by Elastic Common Schema. With ECS, analytics content such as dashboards, rules, and machine learning jobs can be applied more broadly, searches can be crafted more narrowly, and field names are easier to remember.', - expireOn: expect.any(Date), - hash: 'b8a0d3d21e9638bde891ab5eb32594b3d7a3daacc7f0900c6dd506d5d7b42410', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt71256f06dc672546/5c98d595975fd58f4d12646d/ecs-intro-dashboard-1360.jpg?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/introducing-the-elastic-common-schema?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'What is Elastic Common Schema (ECS)?', - }, - ]; - - test('it returns an empty collection of news items when the response is undefined', () => { - expect(getNewsItemsFromApiResponse(undefined)).toEqual([]); - }); - - test('it returns an empty collection of news items when the response is null', () => { - expect(getNewsItemsFromApiResponse(null)).toEqual([]); - }); - - test('it returns an empty collection of news items when the response items are undefined', () => { - expect(getNewsItemsFromApiResponse({ items: undefined })).toEqual([]); - }); - - test('it returns an empty collection of news items when the response items are null', () => { - expect(getNewsItemsFromApiResponse({ items: null })).toEqual([]); - }); - - test('it returns the expected news items when the browser language matches the i18n values in the response', () => { - const lang = 'en'; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when an ALL CAPS the browser language matches the i18n values in the response', () => { - const allCapsLang = 'EN'; - - document.documentElement.lang = allCapsLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when the browser language does NOT match the i18n values in the response', () => { - const nonMatchingLang = 'ja'; - - document.documentElement.lang = nonMatchingLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when the browser language is an empty string', () => { - const emptyLang = ''; - - document.documentElement.lang = emptyLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news item when parsing a raw JSON response', () => { - const lang = 'en'; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(JSON.parse(rawNewsJSON))).toEqual(expectedNewsItems); - }); - - describe('translated items', () => { - const translatedDescription = - 'Elastic SIEMユーザーの素晴らしいコミュニティがそこにあります。 Elastic SIEMアプリの設定、学習、使用、および脅威の検出に関するディスカッションに参加してください!'; - const translatedImageUrl = 'https://aws1.discourse-cdn.com/elastic/translated-image-url'; - const translatedLinkUrl = 'https://discuss.elastic.co/translated-link-url'; - const translatedTitle = 'SIEMに関する質問はありますか?'; - - const withNonDefaultTranslations: RawNewsApiResponse = { - items: [ - { - title: { en: 'Got SIEM Questions?', ja: translatedTitle }, - description: { - en: - "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", - ja: translatedDescription, - }, - link_text: null, - link_url: { - en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', - ja: translatedLinkUrl, - }, - languages: null, - badge: { en: '7.6' }, - image_url: { - en: - 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', - ja: translatedImageUrl, - }, - publish_on: new Date('2020-01-01T00:00:00'), - expire_on: new Date('2020-12-31T00:00:00'), - }, - ], - }; - - test('it returns a translated description when the browser language matches additional translated content', () => { - const lang = 'ja'; // an additional translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].description).toEqual( - translatedDescription - ); - }); - - test('it returns a translated imageUrl when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].imageUrl).toEqual( - translatedImageUrl - ); - }); - - test('it returns a translated linkUrl when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].linkUrl).toEqual( - translatedLinkUrl - ); - }); - - test('it returns a translated title when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - translatedTitle - ); - }); - - test('it returns the default translated title when the browser language matches additional translated content', () => { - const lang = 'fr'; // no translation for this language - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - 'Got SIEM Questions?' - ); - }); - - test('it returns the default translated title when the browser language is an empty string', () => { - const lang = ''; // just an empty string - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - 'Got SIEM Questions?' - ); - }); - }); - - test('it generates a news item hash when an item does NOT include it', () => { - const lang = 'en'; - - const itemHasNoHash: RawNewsApiResponse = { - items: [ - { - title: { en: 'Got SIEM Questions?' }, - description: { - en: 'some description', - }, - link_text: null, - link_url: { en: 'https://example.com/link-url' }, - languages: null, - badge: { en: '7.6' }, - image_url: { - en: 'https://example.com/image-url', - }, - publish_on: new Date('2020-01-01T00:00:00'), - expire_on: new Date('2020-12-31T00:00:00'), - }, - ], - }; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(itemHasNoHash)[0].hash.length).toBeGreaterThan(0); - }); - }); - - describe('fetchNews', () => { - const mockKibanaServices = KibanaServices.get as jest.Mock; - const fetchMock = jest.fn(); - mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); - - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rawNewsApiResponse); - }); - - test('it returns the raw API response from the news feed', async () => { - const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; - expect(await fetchNews({ newsFeedUrl })).toEqual(rawNewsApiResponse); - }); - }); - - describe('showNewsItem', () => { - const MOCK_DATE_NOW = 1579848101395; // 2020-01-24T06:41:41.395Z - - let dateNowSpy: { mockRestore: () => void }; - - beforeAll(() => { - dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_NOW); - }); - - afterAll(() => { - dateNowSpy.mockRestore(); - }); - - test('it should return true when the article has already been published, and will expire in the future', () => { - const alreadyPublishedAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 1000), - title: 'Show this post', - }; - - expect(showNewsItem(alreadyPublishedAndNotExpired)).toEqual(true); - }); - - test('it should return false when the article was published exactly "now", and will expire in the future', () => { - const publishedJustNowAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(publishedJustNowAndNotExpired)).toEqual(false); - }); - - test('it should return false when the article has not been published yet, and has not expired yet', () => { - const notPublishedAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 5000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW + 1000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(notPublishedAndNotExpired)).toEqual(false); - }); - - test('it should return false when the article was published in the past, and will expire exactly now', () => { - const alreadyPublishedAndExpiredNow: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 1000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(alreadyPublishedAndExpiredNow)).toEqual(false); - }); - - test('it should return false when the article was published in the past, and it already expired', () => { - const articleJustExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW - 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 5000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(articleJustExpired)).toEqual(false); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx b/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx deleted file mode 100644 index 6a5e08b287f96..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/news_feed/index.tsx +++ /dev/null @@ -1,61 +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 React, { useEffect, useState } from 'react'; -import chrome from 'ui/chrome'; - -import { fetchNews, getNewsFeedUrl, getNewsItemsFromApiResponse } from './helpers'; -import { useKibana, useUiSetting$ } from '../../lib/kibana'; -import { NewsFeed } from './news_feed'; -import { NewsItem } from './types'; - -export const StatefulNewsFeed = React.memo<{ - enableNewsFeedSetting: string; - newsFeedSetting: string; -}>(({ enableNewsFeedSetting, newsFeedSetting }) => { - const kibanaNewsfeedEnabled = useKibana().services.newsfeed; - const [enableNewsFeed] = useUiSetting$<boolean>(enableNewsFeedSetting); - const [newsFeedUrlSetting] = useUiSetting$<string>(newsFeedSetting); - const [news, setNews] = useState<NewsItem[] | null>(null); - - // respect kibana's global newsfeed.enabled setting - const newsfeedEnabled = kibanaNewsfeedEnabled && enableNewsFeed; - - const newsFeedUrl = getNewsFeedUrl({ - newsFeedUrlSetting, - getKibanaVersion: chrome.getKibanaVersion, - }); - - useEffect(() => { - let canceled = false; - - const fetchData = async () => { - try { - const apiResponse = await fetchNews({ newsFeedUrl }); - - if (!canceled) { - setNews(getNewsItemsFromApiResponse(apiResponse)); - } - } catch { - if (!canceled) { - setNews([]); - } - } - }; - - if (newsfeedEnabled) { - fetchData(); - } - - return () => { - canceled = true; - }; - }, [newsfeedEnabled, newsFeedUrl]); - - return <>{newsfeedEnabled ? <NewsFeed news={news} /> : null}</>; -}); - -StatefulNewsFeed.displayName = 'StatefulNewsFeed'; diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.tsx deleted file mode 100644 index 946c4b3a612dd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState, useCallback } from 'react'; -import { DeleteTimelines } from '../types'; - -import { TimelineDownloader } from './export_timeline'; -import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; -import { exportSelectedTimeline } from '../../../containers/timeline/all/api'; - -export interface ExportTimeline { - disableExportTimelineDownloader: () => void; - enableExportTimelineDownloader: () => void; - isEnableDownloader: boolean; -} - -export const useExportTimeline = (): ExportTimeline => { - const [isEnableDownloader, setIsEnableDownloader] = useState(false); - - const enableExportTimelineDownloader = useCallback(() => { - setIsEnableDownloader(true); - }, []); - - const disableExportTimelineDownloader = useCallback(() => { - setIsEnableDownloader(false); - }, []); - - return { - disableExportTimelineDownloader, - enableExportTimelineDownloader, - isEnableDownloader, - }; -}; - -const EditTimelineActionsComponent: React.FC<{ - deleteTimelines: DeleteTimelines | undefined; - ids: string[]; - isEnableDownloader: boolean; - isDeleteTimelineModalOpen: boolean; - onComplete: () => void; - title: string; -}> = ({ - deleteTimelines, - ids, - isEnableDownloader, - isDeleteTimelineModalOpen, - onComplete, - title, -}) => ( - <> - <TimelineDownloader - exportedIds={ids} - getExportedData={exportSelectedTimeline} - isEnableDownloader={isEnableDownloader} - onComplete={onComplete} - /> - {deleteTimelines != null && ( - <DeleteTimelineModalOverlay - deleteTimelines={deleteTimelines} - isModalOpen={isDeleteTimelineModalOpen} - onComplete={onComplete} - savedObjectIds={ids} - title={title} - /> - )} - </> -); - -export const EditTimelineActions = React.memo(EditTimelineActionsComponent); -export const EditOneTimelineAction = React.memo(EditTimelineActionsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx deleted file mode 100644 index 04f0abe0d00d1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.test.tsx +++ /dev/null @@ -1,628 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount } from 'enzyme'; -import { MockedProvider } from 'react-apollo/test-utils'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { wait } from '../../lib/helpers'; -import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../mock/timeline_results'; -import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; - -import { StatefulOpenTimeline } from '.'; -import { NotePreviews } from './note_previews'; -import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; - -jest.mock('../../lib/kibana'); - -describe('StatefulOpenTimeline', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const title = 'All Timelines / Open Timelines'; - - test('it has the expected initial state', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - const componentProps = wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .props(); - - expect(componentProps).toEqual({ - ...componentProps, - itemIdToExpandedNotesRowMap: {}, - onlyFavorites: false, - pageIndex: 0, - pageSize: 10, - query: '', - selectedItems: [], - sortDirection: 'desc', - sortField: 'updated', - }); - }); - - describe('#onQueryChange', () => { - test('it updates the query state with the expected trimmed value when the user enters a query', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - expect( - wrapper - .find('[data-test-subj="search-row"]') - .first() - .prop('query') - ).toEqual('abcd'); - }); - - test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - - expect( - wrapper - .find('[data-test-subj="query-message"]') - .first() - .text() - ).toContain('Showing: 11 timelines with'); - }); - - test('echos (renders) the query when the user enters a query', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - - expect( - wrapper - .find('[data-test-subj="selectable-query-text"]') - .first() - .text() - ).toEqual('with "abcd"'); - }); - }); - - describe('#focusInput', () => { - test('focuses the input when the component mounts', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - expect( - wrapper - .find(`.${OPEN_TIMELINE_CLASS_NAME} input`) - .first() - .getDOMNode().id === document.activeElement!.id - ).toBe(true); - }); - }); - - describe('#onAddTimelinesToFavorites', () => { - // This functionality is hiding for now and waiting to see the light in the near future - test.skip('it invokes addTimelinesToFavorites with the selected timelines when the button is clicked', async () => { - const addTimelinesToFavorites = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper - .find('[data-test-subj="favorite-selected"]') - .first() - .simulate('click'); - - expect(addTimelinesToFavorites).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); - }); - }); - - describe('#onDeleteSelected', () => { - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes deleteTimelines with the selected timelines when the button is clicked', async () => { - const deleteTimelines = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper - .find('[data-test-subj="delete-selected"]') - .first() - .simulate('click'); - - expect(deleteTimelines).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); - }); - }); - - describe('#onSelectionChange', () => { - test('it updates the selection state when timelines are selected', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - const selectedItems: [] = wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('selectedItems'); - - expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query - }); - }); - - describe('#onTableChange', () => { - test('it updates the sort state when the user clicks on a column to sort it', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('sortDirection') - ).toEqual('desc'); - - wrapper - .find('thead tr th button') - .at(0) - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('sortDirection') - ).toEqual('asc'); - }); - }); - - describe('#onToggleOnlyFavorites', () => { - test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('onlyFavorites') - ).toEqual(false); - - wrapper - .find('[data-test-subj="only-favorites-toggle"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('onlyFavorites') - ).toEqual(true); - }); - }); - - describe('#onToggleShowNotes', () => { - test('it updates the itemIdToExpandedNotesRowMap state when the user clicks the expand notes button', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('itemIdToExpandedNotesRowMap') - ).toEqual({}); - - wrapper - .find('[data-test-subj="expand-notes"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('itemIdToExpandedNotesRowMap') - ).toEqual({ - '10849df0-7b44-11e9-a608-ab3d811609': ( - <NotePreviews - notes={ - mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes != null - ? mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes.map( - note => ({ ...note, savedObjectId: note.noteId }) - ) - : [] - } - /> - ), - }); - }); - - test('it renders the expanded notes when the expand button is clicked', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper.update(); - - wrapper - .find('[data-test-subj="expand-notes"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="note-previews-container"]') - .find('[data-test-subj="updated-by"]') - .first() - .text() - ).toEqual('elastic'); - }); - }); - - test('it renders the title', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - expect( - wrapper - .find('[data-test-subj="header-section-title"]') - .first() - .text() - ).toEqual(title); - }); - - describe('#resetSelectionState', () => { - test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - const getSelectedItem = (): [] => - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('selectedItems'); - await wait(); - expect(getSelectedItem().length).toEqual(0); - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - expect(getSelectedItem().length).toEqual(13); - }); - }); - - test('it renders the expected count of matching timelines when no query has been entered', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <TestProviderWithoutDragAndDrop> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </TestProviderWithoutDragAndDrop> - </MockedProvider> - </ThemeProvider> - ); - - await wait(); - - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="query-message"]') - .first() - .text() - ).toContain('Showing: 11 timelines '); - }); - - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes onOpenTimeline with the expected parameters when the hyperlink is clicked', async () => { - const onOpenTimeline = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find( - `[data-test-subj="title-${ - mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId - }"]` - ) - .first() - .simulate('click'); - - expect(onOpenTimeline).toHaveBeenCalledWith({ - duplicate: false, - timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] - .savedObjectId, - }); - }); - - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes onOpenTimeline with the expected params when the button is clicked', async () => { - const onOpenTimeline = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <StatefulOpenTimeline - data-test-subj="stateful-timeline" - apolloClient={apolloClient} - isModal={false} - defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} - title={title} - /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper - .find('[data-test-subj="open-duplicate"]') - .first() - .simulate('click'); - - expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx deleted file mode 100644 index c27a6039da29d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/index.tsx +++ /dev/null @@ -1,358 +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 ApolloClient from 'apollo-client'; -import React, { useEffect, useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { Dispatch } from 'redux'; -import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; -import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; -import { AllTimelinesVariables, AllTimelinesQuery } from '../../containers/timeline/all'; -import { allTimelinesQuery } from '../../containers/timeline/all/index.gql_query'; -import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types'; -import { State, timelineSelectors } from '../../store'; -import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { - createTimeline as dispatchCreateNewTimeline, - updateIsLoading as dispatchUpdateIsLoading, -} from '../../store/timeline/actions'; -import { OpenTimeline } from './open_timeline'; -import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; -import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; -import { - ActionTimelineToShow, - DeleteTimelines, - EuiSearchBarQuery, - OnDeleteSelected, - OnOpenTimeline, - OnQueryChange, - OnSelectionChange, - OnTableChange, - OnTableChangeParams, - OpenTimelineProps, - OnToggleOnlyFavorites, - OpenTimelineResult, - OnToggleShowNotes, - OnDeleteOneTimeline, -} from './types'; -import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; - -interface OwnProps<TCache = object> { - apolloClient: ApolloClient<TCache>; - /** Displays open timeline in modal */ - isModal: boolean; - closeModalTimeline?: () => void; - hideActions?: ActionTimelineToShow[]; - onOpenTimeline?: (timeline: TimelineModel) => void; -} - -export type OpenTimelineOwnProps = OwnProps & - Pick< - OpenTimelineProps, - 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' - > & - PropsFromRedux; - -/** Returns a collection of selected timeline ids */ -export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): string[] => - selectedItems.reduce<string[]>( - (validSelections, timelineResult) => - timelineResult.savedObjectId != null - ? [...validSelections, timelineResult.savedObjectId] - : validSelections, - [] - ); - -/** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ -export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( - ({ - apolloClient, - closeModalTimeline, - createNewTimeline, - defaultPageSize, - hideActions = [], - isModal = false, - importDataModalToggle, - onOpenTimeline, - setImportDataModalToggle, - timeline, - title, - updateTimeline, - updateIsLoading, - }) => { - /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ - const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< - Record<string, JSX.Element> - >({}); - /** Only query for favorite timelines when true */ - const [onlyFavorites, setOnlyFavorites] = useState(false); - /** The requested page of results */ - const [pageIndex, setPageIndex] = useState(0); - /** The requested size of each page of search results */ - const [pageSize, setPageSize] = useState(defaultPageSize); - /** The current search criteria */ - const [search, setSearch] = useState(''); - /** The currently-selected timelines in the table */ - const [selectedItems, setSelectedItems] = useState<OpenTimelineResult[]>([]); - /** The requested sort direction of the query results */ - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); - /** The requested field to sort on */ - const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); - - /** Invoked when the user presses enters to submit the text in the search input */ - const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { - setSearch(query.queryText.trim()); - }, []); - - /** Focuses the input that filters the field browser */ - const focusInput = () => { - const elements = document.querySelector<HTMLElement>(`.${OPEN_TIMELINE_CLASS_NAME} input`); - - if (elements != null) { - elements.focus(); - } - }; - - /* This feature will be implemented in the near future, so we are keeping it to know what to do */ - - /** Invoked when the user clicks the action to add the selected timelines to favorites */ - // const onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { - // const { addTimelinesToFavorites } = this.props; - // const { selectedItems } = this.state; - // if (addTimelinesToFavorites != null) { - // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); - // TODO: it's not possible to clear the selection state of the newly-favorited - // items, because we can't pass the selection state as props to the table. - // See: https://github.com/elastic/eui/issues/1077 - // TODO: the query must re-execute to show the results of the mutation - // } - // }; - - const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( - (timelineIds: string[]) => { - deleteTimelines(timelineIds, { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); - }, - [search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites] - ); - - /** Invoked when the user clicks the action to delete the selected timelines */ - const onDeleteSelected: OnDeleteSelected = useCallback(() => { - deleteTimelines(getSelectedTimelineIds(selectedItems), { - search, - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - sort: { - sortField: sortField as SortFieldTimeline, - sortOrder: sortDirection as Direction, - }, - onlyUserFavorite: onlyFavorites, - }); - - // NOTE: we clear the selection state below, but if the server fails to - // delete a timeline, it will remain selected in the table: - resetSelectionState(); - - // TODO: the query must re-execute to show the results of the deletion - }, [selectedItems, search, pageIndex, pageSize, sortField, sortDirection, onlyFavorites]); - - /** Invoked when the user selects (or de-selects) timelines */ - const onSelectionChange: OnSelectionChange = useCallback( - (newSelectedItems: OpenTimelineResult[]) => { - setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 - }, - [] - ); - - /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ - const onTableChange: OnTableChange = useCallback(({ page, sort }: OnTableChangeParams) => { - const { index, size } = page; - const { field, direction } = sort; - setPageIndex(index); - setPageSize(size); - setSortDirection(direction); - setSortField(field); - }, []); - - /** Invoked when the user toggles the option to only view favorite timelines */ - const onToggleOnlyFavorites: OnToggleOnlyFavorites = useCallback(() => { - setOnlyFavorites(!onlyFavorites); - }, [onlyFavorites]); - - /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - const onToggleShowNotes: OnToggleShowNotes = useCallback( - (newItemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => { - setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); - }, - [] - ); - - /** Resets the selection state such that all timelines are unselected */ - const resetSelectionState = useCallback(() => { - setSelectedItems([]); - }, []); - - const openTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { - if (isModal && closeModalTimeline != null) { - closeModalTimeline(); - } - - queryTimelineById({ - apolloClient, - duplicate, - onOpenTimeline, - timelineId, - updateIsLoading, - updateTimeline, - }); - }, - [apolloClient, updateIsLoading, updateTimeline] - ); - - const deleteTimelines: DeleteTimelines = useCallback( - (timelineIds: string[], variables?: AllTimelinesVariables) => { - if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); - } - apolloClient.mutate<DeleteTimelineMutation.Mutation, DeleteTimelineMutation.Variables>({ - mutation: deleteTimelineMutation, - fetchPolicy: 'no-cache', - variables: { id: timelineIds }, - refetchQueries: [ - { - query: allTimelinesQuery, - variables, - }, - ], - }); - }, - [apolloClient, createNewTimeline, timeline] - ); - - useEffect(() => { - focusInput(); - }, []); - - return ( - <AllTimelinesQuery - pageInfo={{ - pageIndex: pageIndex + 1, - pageSize, - }} - search={search} - sort={{ sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }} - onlyUserFavorite={onlyFavorites} - > - {({ timelines, loading, totalCount, refetch }) => { - return !isModal ? ( - <OpenTimeline - data-test-subj={'open-timeline'} - deleteTimelines={onDeleteOneTimeline} - defaultPageSize={defaultPageSize} - isLoading={loading} - itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} - importDataModalToggle={importDataModalToggle} - onAddTimelinesToFavorites={undefined} - onDeleteSelected={onDeleteSelected} - onlyFavorites={onlyFavorites} - onOpenTimeline={openTimeline} - onQueryChange={onQueryChange} - onSelectionChange={onSelectionChange} - onTableChange={onTableChange} - onToggleOnlyFavorites={onToggleOnlyFavorites} - onToggleShowNotes={onToggleShowNotes} - pageIndex={pageIndex} - pageSize={pageSize} - query={search} - refetch={refetch} - searchResults={timelines} - setImportDataModalToggle={setImportDataModalToggle} - selectedItems={selectedItems} - sortDirection={sortDirection} - sortField={sortField} - title={title} - totalSearchResultsCount={totalCount} - /> - ) : ( - <OpenTimelineModalBody - data-test-subj={'open-timeline-modal'} - deleteTimelines={onDeleteOneTimeline} - defaultPageSize={defaultPageSize} - hideActions={hideActions} - isLoading={loading} - itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} - onAddTimelinesToFavorites={undefined} - onlyFavorites={onlyFavorites} - onOpenTimeline={openTimeline} - onQueryChange={onQueryChange} - onSelectionChange={onSelectionChange} - onTableChange={onTableChange} - onToggleOnlyFavorites={onToggleOnlyFavorites} - onToggleShowNotes={onToggleShowNotes} - pageIndex={pageIndex} - pageSize={pageSize} - query={search} - searchResults={timelines} - selectedItems={selectedItems} - sortDirection={sortDirection} - sortField={sortField} - title={title} - totalSearchResultsCount={totalCount} - /> - ); - }} - </AllTimelinesQuery> - ); - } -); - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; - - return { - timeline, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - createNewTimeline: ({ - id, - columns, - show, - }: { - id: string; - columns: ColumnHeaderOptions[]; - show?: boolean; - }) => dispatch(dispatchCreateNewTimeline({ id, columns, show })), - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const StatefulOpenTimeline = connector(StatefulOpenTimelineComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx deleted file mode 100644 index ca8fa50c572fe..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ /dev/null @@ -1,44 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount } from 'enzyme'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { ThemeProvider } from 'styled-components'; - -import { wait } from '../../../lib/helpers'; -import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results'; - -import { OpenTimelineModal } from '.'; - -jest.mock('../../../lib/kibana'); -jest.mock('../../../utils/apollo_context', () => ({ - useApolloClient: () => ({}), -})); - -describe('OpenTimelineModal', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - - test('it renders the expected modal', async () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <TestProviderWithoutDragAndDrop> - <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> - <OpenTimelineModal onClose={jest.fn()} /> - </MockedProvider> - </TestProviderWithoutDragAndDrop> - </ThemeProvider> - ); - - await wait(); - - wrapper.update(); - - expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts deleted file mode 100644 index b7cc92ebd183f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts +++ /dev/null @@ -1,187 +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 { SetStateAction, Dispatch } from 'react'; -import { AllTimelinesVariables } from '../../containers/timeline/all'; -import { TimelineModel } from '../../store/timeline/model'; -import { NoteResult } from '../../graphql/types'; -import { Refetch } from '../../store/inputs/model'; - -/** The users who added a timeline to favorites */ -export interface FavoriteTimelineResult { - userId?: number | null; - userName?: string | null; - favoriteDate?: number | null; -} - -export interface TimelineResultNote { - savedObjectId?: string | null; - note?: string | null; - noteId?: string | null; - updated?: number | null; - updatedBy?: string | null; -} - -export interface TimelineActionsOverflowColumns { - width: string; - actions: Array<{ - name: string; - icon?: string; - onClick?: (timeline: OpenTimelineResult) => void; - description: string; - render?: (timeline: OpenTimelineResult) => JSX.Element; - } | null>; -} - -/** The results of the query run by the OpenTimeline component */ -export interface OpenTimelineResult { - created?: number | null; - description?: string | null; - eventIdToNoteIds?: Readonly<Record<string, string[]>> | null; - favorite?: FavoriteTimelineResult[] | null; - noteIds?: string[] | null; - notes?: TimelineResultNote[] | null; - pinnedEventIds?: Readonly<Record<string, boolean>> | null; - savedObjectId?: string | null; - title?: string | null; - updated?: number | null; - updatedBy?: string | null; -} - -/** - * EuiSearchBar returns this object when the user changes the query. At the - * time of this writing, there is no typescript definition for this type, so - * only the properties used by the Open Timeline component are exposed. - */ -export interface EuiSearchBarQuery { - queryText: string; -} - -/** Performs IO to delete the specified timelines */ -export type DeleteTimelines = (timelineIds: string[], variables?: AllTimelinesVariables) => void; - -/** Invoked when the user clicks the action make the selected timelines favorites */ -export type OnAddTimelinesToFavorites = () => void; - -/** Invoked when the user clicks the action to delete the selected timelines */ -export type OnDeleteSelected = () => void; -export type OnDeleteOneTimeline = (timelineIds: string[]) => void; - -/** Invoked when the user clicks on the name of a timeline to open it */ -export type OnOpenTimeline = ({ - duplicate, - timelineId, -}: { - duplicate: boolean; - timelineId: string; -}) => void; - -export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; -export type SetActionTimeline = Dispatch<SetStateAction<OpenTimelineResult | undefined>>; -export type EnableExportTimelineDownloader = (selectedItem: OpenTimelineResult) => void; -/** Invoked when the user presses enters to submit the text in the search input */ -export type OnQueryChange = (query: EuiSearchBarQuery) => void; - -/** Invoked when the user selects (or de-selects) timelines in the table */ -export type OnSelectionChange = (selectedItems: OpenTimelineResult[]) => void; - -/** Invoked when the user toggles the option to only view favorite timelines */ -export type OnToggleOnlyFavorites = () => void; - -/** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ -export type OnToggleShowNotes = (itemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => void; - -/** Parameters to the OnTableChange callback */ -export interface OnTableChangeParams { - page: { - index: number; - size: number; - }; - sort: { - field: string; - direction: 'asc' | 'desc'; - }; -} - -/** Invoked by the EUI table implementation when the user interacts with the table */ -export type OnTableChange = (tableChange: OnTableChangeParams) => void; - -export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable'; - -export interface OpenTimelineProps { - /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ - deleteTimelines?: DeleteTimelines; - /** The default requested size of each page of search results */ - defaultPageSize: number; - /** Displays an indicator that data is loading when true */ - isLoading: boolean; - /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ - itemIdToExpandedNotesRowMap: Record<string, JSX.Element>; - /** Display import timelines modal*/ - importDataModalToggle?: boolean; - /** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */ - onAddTimelinesToFavorites?: OnAddTimelinesToFavorites; - /** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */ - onDeleteSelected?: OnDeleteSelected; - /** Only show favorite timelines when true */ - onlyFavorites: boolean; - /** Invoked when the user presses enter after typing in the search bar */ - onQueryChange: OnQueryChange; - /** Invoked when the user selects (or de-selects) timelines in the table */ - onSelectionChange: OnSelectionChange; - /** Invoked when the user clicks on the name of a timeline to open it */ - onOpenTimeline: OnOpenTimeline; - /** Invoked by the EUI table implementation when the user interacts with the table */ - onTableChange: OnTableChange; - /** Invoked when the user toggles the option to only show favorite timelines */ - onToggleOnlyFavorites: OnToggleOnlyFavorites; - /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - onToggleShowNotes: OnToggleShowNotes; - /** the requested page of results */ - pageIndex: number; - /** the requested size of each page of search results */ - pageSize: number; - /** The currently applied search criteria */ - query: string; - /** Refetch table */ - refetch?: Refetch; - /** The results of executing a search */ - searchResults: OpenTimelineResult[]; - /** the currently-selected timelines in the table */ - selectedItems: OpenTimelineResult[]; - /** Toggle export timelines modal*/ - setImportDataModalToggle?: React.Dispatch<React.SetStateAction<boolean>>; - /** the requested sort direction of the query results */ - sortDirection: 'asc' | 'desc'; - /** the requested field to sort on */ - sortField: string; - /** The title of the Open Timeline component */ - title: string; - /** The total (server-side) count of the search results */ - totalSearchResultsCount: number; - /** Hide action on timeline if needed it */ - hideActions?: ActionTimelineToShow[]; -} - -export interface UpdateTimeline { - duplicate: boolean; - id: string; - from: number; - notes: NoteResult[] | null | undefined; - timeline: TimelineModel; - to: number; - ruleNote?: string; -} - -export type DispatchUpdateTimeline = ({ - duplicate, - id, - from, - notes, - timeline, - to, - ruleNote, -}: UpdateTimeline) => () => void; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts deleted file mode 100644 index d88bc2bf3b7e6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts +++ /dev/null @@ -1,50 +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 { Filter } from '../../../../../../../../src/plugins/data/public'; - -export const createFilter = ( - key: string, - value: string[] | string | null | undefined, - negate: boolean = false -): Filter => { - const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null; - return queryValue != null - ? { - meta: { - alias: null, - negate, - disabled: false, - type: 'phrase', - key, - value: queryValue, - params: { - query: queryValue, - }, - }, - query: { - match: { - [key]: { - query: queryValue, - type: 'phrase', - }, - }, - }, - } - : ({ - exists: { - field: key, - }, - meta: { - alias: null, - disabled: false, - key, - negate: value === undefined, - type: 'exists', - value: 'exists', - }, - } as Filter); -}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx deleted file mode 100644 index 127eb3bae0284..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx +++ /dev/null @@ -1,81 +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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React, { useCallback } from 'react'; - -import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { WithHoverActions } from '../../with_hover_actions'; -import { useKibana } from '../../../lib/kibana'; - -import * as i18n from './translations'; - -export * from './helpers'; - -interface OwnProps { - children: JSX.Element; - filter: Filter; - onFilterAdded?: () => void; -} - -export const AddFilterToGlobalSearchBar = React.memo<OwnProps>( - ({ children, filter, onFilterAdded }) => { - const { filterManager } = useKibana().services.data.query; - - const filterForValue = useCallback(() => { - filterManager.addFilters(filter); - - if (onFilterAdded != null) { - onFilterAdded(); - } - }, [filterManager, filter, onFilterAdded]); - - const filterOutValue = useCallback(() => { - filterManager.addFilters({ - ...filter, - meta: { - ...filter.meta, - negate: true, - }, - }); - - if (onFilterAdded != null) { - onFilterAdded(); - } - }, [filterManager, filter, onFilterAdded]); - - return ( - <WithHoverActions - hoverContent={ - <div data-test-subj="hover-actions-container"> - <EuiToolTip content={i18n.FILTER_FOR_VALUE}> - <EuiButtonIcon - aria-label={i18n.FILTER_FOR_VALUE} - color="text" - data-test-subj="add-to-filter" - iconType="magnifyWithPlus" - onClick={filterForValue} - /> - </EuiToolTip> - - <EuiToolTip content={i18n.FILTER_OUT_VALUE}> - <EuiButtonIcon - aria-label={i18n.FILTER_OUT_VALUE} - color="text" - data-test-subj="filter-out-value" - iconType="magnifyWithMinus" - onClick={filterOutValue} - /> - </EuiToolTip> - </div> - } - render={() => children} - /> - ); - } -); - -AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx deleted file mode 100644 index a0ca5f855237c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.tsx +++ /dev/null @@ -1,191 +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 { EuiFlexItem } from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { getOr } from 'lodash/fp'; -import React from 'react'; - -import { DEFAULT_DARK_MODE } from '../../../../../../../../plugins/siem/common/constants'; -import { DescriptionList } from '../../../../../../../../plugins/siem/common/utility_types'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { getEmptyTagValue } from '../../../empty_value'; -import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/field_renderers'; -import { InspectButton, InspectButtonContainer } from '../../../inspect'; -import { HostItem } from '../../../../graphql/types'; -import { Loader } from '../../../loader'; -import { IPDetailsLink } from '../../../links'; -import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; -import { AnomalyScores } from '../../../ml/score/anomaly_scores'; -import { Anomalies, NarrowDateRange } from '../../../ml/types'; -import { DescriptionListStyled, OverviewWrapper } from '../../index'; -import { FirstLastSeenHost, FirstLastSeenHostType } from '../first_last_seen_host'; - -import * as i18n from './translations'; - -interface HostSummaryProps { - data: HostItem; - id: string; - loading: boolean; - isLoadingAnomaliesData: boolean; - anomaliesData: Anomalies | null; - startDate: number; - endDate: number; - narrowDateRange: NarrowDateRange; -} - -const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( - <EuiFlexItem key={key}> - <DescriptionListStyled listItems={descriptionList} /> - </EuiFlexItem> -); - -export const HostOverview = React.memo<HostSummaryProps>( - ({ - data, - loading, - id, - startDate, - endDate, - isLoadingAnomaliesData, - anomaliesData, - narrowDateRange, - }) => { - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); - - const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( - <DefaultFieldRenderer - rowItems={getOr([], fieldName, fieldData)} - attrName={fieldName} - idPrefix="host-overview" - /> - ); - - const column: DescriptionList[] = [ - { - title: i18n.HOST_ID, - description: data.host - ? hostIdRenderer({ host: data.host, noLink: true }) - : getEmptyTagValue(), - }, - { - title: i18n.FIRST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - <FirstLastSeenHost - hostname={data.host.name[0]} - type={FirstLastSeenHostType.FIRST_SEEN} - /> - ) : ( - getEmptyTagValue() - ), - }, - { - title: i18n.LAST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - <FirstLastSeenHost - hostname={data.host.name[0]} - type={FirstLastSeenHostType.LAST_SEEN} - /> - ) : ( - getEmptyTagValue() - ), - }, - ]; - const firstColumn = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - <AnomalyScores - anomalies={anomaliesData} - startDate={startDate} - endDate={endDate} - isLoading={isLoadingAnomaliesData} - narrowDateRange={narrowDateRange} - /> - ), - }, - ] - : column; - - const descriptionLists: Readonly<DescriptionList[][]> = [ - firstColumn, - [ - { - title: i18n.IP_ADDRESSES, - description: ( - <DefaultFieldRenderer - rowItems={getOr([], 'host.ip', data)} - attrName={'host.ip'} - idPrefix="host-overview" - render={ip => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())} - /> - ), - }, - { - title: i18n.MAC_ADDRESSES, - description: getDefaultRenderer('host.mac', data), - }, - { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, - ], - [ - { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, - { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, - { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, - { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, - ], - [ - { - title: i18n.CLOUD_PROVIDER, - description: getDefaultRenderer('cloud.provider', data), - }, - { - title: i18n.REGION, - description: getDefaultRenderer('cloud.region', data), - }, - { - title: i18n.INSTANCE_ID, - description: getDefaultRenderer('cloud.instance.id', data), - }, - { - title: i18n.MACHINE_TYPE, - description: getDefaultRenderer('cloud.machine.type', data), - }, - ], - ]; - - return ( - <InspectButtonContainer> - <OverviewWrapper> - <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> - - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} - - {loading && ( - <Loader - overlay - overlayBackground={ - darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor - } - size="xl" - /> - )} - </OverviewWrapper> - </InspectButtonContainer> - ); - } -); - -HostOverview.displayName = 'HostOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/index.tsx deleted file mode 100644 index 3a36a2dce476b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/index.tsx +++ /dev/null @@ -1,203 +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 { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; -import styled, { createGlobalStyle } from 'styled-components'; - -/* - SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly - and `EuiPopover`, `EuiToolTip` global styles -*/ -export const AppGlobalStyle = createGlobalStyle` - /* dirty hack to fix draggables with tooltip on FF */ - body#siem-app { - position: static; - } - /* end of dirty hack to fix draggables with tooltip on FF */ - - div.app-wrapper { - background-color: rgba(0,0,0,0); - } - - div.application { - background-color: rgba(0,0,0,0); - } - - .euiPopover__panel.euiPopover__panel-isOpen { - z-index: 9900 !important; - min-width: 24px; - } - .euiToolTip { - z-index: 9950 !important; - } - - /* - overrides the default styling of euiComboBoxOptionsList because it's implemented - as a popover, so it's not selectable as a child of the styled component - */ - .euiComboBoxOptionsList { - z-index: 9999; - } - - /* overrides default styling in angular code that was not theme-friendly */ - .euiPanel-loading-hide-border { - border: none; - } - - /* hide open popovers when a modal is being displayed to prevent them from covering the modal */ - body.euiBody-hasOverlayMask .euiPopover__panel-isOpen { - visibility: hidden !important; - } - - /* ensure elastic charts tooltips appear above open euiPopovers */ - .echTooltip { - z-index: 9950; - } - -`; - -export const DescriptionListStyled = styled(EuiDescriptionList)` - ${({ theme }) => ` - dt { - font-size: ${theme.eui.euiFontSizeXS} !important; - } - dd { - width: fit-content; - } - dd > div { - width: fit-content; - } - `} -`; - -DescriptionListStyled.displayName = 'DescriptionListStyled'; - -export const PageContainer = styled.div` - display: flex; - flex-direction: column; - align-items: stretch; - background-color: ${props => props.theme.eui.euiColorEmptyShade}; - height: 100%; - padding: 1rem; - overflow: hidden; - margin: 0px; -`; - -PageContainer.displayName = 'PageContainer'; - -export const PageContent = styled.div` - flex: 1 1 auto; - height: 100%; - position: relative; - overflow-y: hidden; - background-color: ${props => props.theme.eui.euiColorEmptyShade}; - margin-top: 62px; -`; - -PageContent.displayName = 'PageContent'; - -export const FlexPage = styled(EuiPage)` - flex: 1 0 0; -`; - -FlexPage.displayName = 'FlexPage'; - -export const PageHeader = styled.div` - background-color: ${props => props.theme.eui.euiColorEmptyShade}; - display: flex; - user-select: none; - padding: 1rem 1rem 0rem 1rem; - width: 100vw; - position: fixed; -`; - -PageHeader.displayName = 'PageHeader'; - -export const FooterContainer = styled.div` - flex: 0; - bottom: 0; - color: #666; - left: 0; - position: fixed; - text-align: left; - user-select: none; - width: 100%; - background-color: #f5f7fa; - padding: 16px; - border-top: 1px solid #d3dae6; -`; - -FooterContainer.displayName = 'FooterContainer'; - -export const PaneScrollContainer = styled.div` - height: 100%; - overflow-y: scroll; - > div:last-child { - margin-bottom: 3rem; - } -`; - -PaneScrollContainer.displayName = 'PaneScrollContainer'; - -export const Pane = styled.div` - height: 100%; - overflow: hidden; - user-select: none; -`; - -Pane.displayName = 'Pane'; - -export const PaneHeader = styled.div` - display: flex; -`; - -PaneHeader.displayName = 'PaneHeader'; - -export const Pane1FlexContent = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - height: 100%; -`; - -Pane1FlexContent.displayName = 'Pane1FlexContent'; - -export const CountBadge = styled(EuiBadge)` - margin-left: 5px; -`; - -CountBadge.displayName = 'CountBadge'; - -export const Spacer = styled.span` - margin-left: 5px; -`; - -Spacer.displayName = 'Spacer'; - -export const Badge = styled(EuiBadge)` - vertical-align: top; -`; - -Badge.displayName = 'Badge'; - -export const MoreRowItems = styled(EuiIcon)` - margin-left: 5px; -`; - -MoreRowItems.displayName = 'MoreRowItems'; - -export const OverviewWrapper = styled(EuiFlexGroup)` - position: relative; - - .euiButtonIcon { - position: absolute; - right: ${props => props.theme.eui.euiSizeM}; - top: 6px; - z-index: 2; - } -`; - -OverviewWrapper.displayName = 'OverviewWrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx deleted file mode 100644 index a652fef5508fc..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.tsx +++ /dev/null @@ -1,166 +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 { EuiFlexItem } from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import React from 'react'; - -import { DEFAULT_DARK_MODE } from '../../../../../../../../plugins/siem/common/constants'; -import { DescriptionList } from '../../../../../../../../plugins/siem/common/utility_types'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { FlowTarget, IpOverviewData, Overview } from '../../../../graphql/types'; -import { networkModel } from '../../../../store'; -import { getEmptyTagValue } from '../../../empty_value'; - -import { - autonomousSystemRenderer, - dateRenderer, - hostIdRenderer, - hostNameRenderer, - locationRenderer, - reputationRenderer, - whoisRenderer, -} from '../../../field_renderers/field_renderers'; -import * as i18n from './translations'; -import { DescriptionListStyled, OverviewWrapper } from '../../index'; -import { Loader } from '../../../loader'; -import { Anomalies, NarrowDateRange } from '../../../ml/types'; -import { AnomalyScores } from '../../../ml/score/anomaly_scores'; -import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; -import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; -import { InspectButton, InspectButtonContainer } from '../../../inspect'; - -interface OwnProps { - data: IpOverviewData; - flowTarget: FlowTarget; - id: string; - ip: string; - loading: boolean; - isLoadingAnomaliesData: boolean; - anomaliesData: Anomalies | null; - startDate: number; - endDate: number; - type: networkModel.NetworkType; - narrowDateRange: NarrowDateRange; -} - -export type IpOverviewProps = OwnProps; - -const getDescriptionList = (descriptionList: DescriptionList[], key: number) => { - return ( - <EuiFlexItem key={key}> - <DescriptionListStyled listItems={descriptionList} /> - </EuiFlexItem> - ); -}; - -export const IpOverview = React.memo<IpOverviewProps>( - ({ - id, - ip, - data, - loading, - flowTarget, - startDate, - endDate, - isLoadingAnomaliesData, - anomaliesData, - narrowDateRange, - }) => { - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); - const typeData: Overview = data[flowTarget]!; - const column: DescriptionList[] = [ - { - title: i18n.LOCATION, - description: locationRenderer( - [`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`], - data - ), - }, - { - title: i18n.AUTONOMOUS_SYSTEM, - description: typeData - ? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget) - : getEmptyTagValue(), - }, - ]; - - const firstColumn: DescriptionList[] = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - <AnomalyScores - anomalies={anomaliesData} - startDate={startDate} - endDate={endDate} - isLoading={isLoadingAnomaliesData} - narrowDateRange={narrowDateRange} - /> - ), - }, - ] - : column; - - const descriptionLists: Readonly<DescriptionList[][]> = [ - firstColumn, - [ - { - title: i18n.FIRST_SEEN, - description: typeData ? dateRenderer(typeData.firstSeen) : getEmptyTagValue(), - }, - { - title: i18n.LAST_SEEN, - description: typeData ? dateRenderer(typeData.lastSeen) : getEmptyTagValue(), - }, - ], - [ - { - title: i18n.HOST_ID, - description: typeData - ? hostIdRenderer({ host: data.host, ipFilter: ip }) - : getEmptyTagValue(), - }, - { - title: i18n.HOST_NAME, - description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), - }, - ], - [ - { title: i18n.WHOIS, description: whoisRenderer(ip) }, - { title: i18n.REPUTATION, description: reputationRenderer(ip) }, - ], - ]; - - return ( - <InspectButtonContainer> - <OverviewWrapper> - <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> - - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} - - {loading && ( - <Loader - overlay - overlayBackground={ - darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor - } - size="xl" - /> - )} - </OverviewWrapper> - </InspectButtonContainer> - ); - } -); - -IpOverview.displayName = 'IpOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx deleted file mode 100644 index b43efbbde51b3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ /dev/null @@ -1,129 +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 { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; -import { ESQuery } from '../../../../../../../../plugins/siem/common/typed_json'; -import { - ID as OverviewHostQueryId, - OverviewHostQuery, -} from '../../../../containers/overview/overview_host'; -import { HeaderSection } from '../../../header_section'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { getHostsUrl } from '../../../link_to'; -import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; -import { manageQuery } from '../../../page/manage_query'; -import { inputsModel } from '../../../../store/inputs'; -import { InspectButtonContainer } from '../../../inspect'; -import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; -import { navTabs } from '../../../../pages/home/home_navigations'; - -export interface OwnProps { - startDate: number; - endDate: number; - filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; -} - -const OverviewHostStatsManage = manageQuery(OverviewHostStats); -export type OverviewHostProps = OwnProps; - -const OverviewHostComponent: React.FC<OverviewHostProps> = ({ - endDate, - filterQuery, - startDate, - setQuery, -}) => { - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); - const hostPageButton = useMemo( - () => ( - <EuiButton href={getHostsUrl(urlSearch)}> - <FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View hosts" /> - </EuiButton> - ), - [urlSearch] - ); - return ( - <EuiFlexItem> - <InspectButtonContainer> - <EuiPanel> - <OverviewHostQuery - data-test-subj="overview-host-query" - endDate={endDate} - filterQuery={filterQuery} - sourceId="default" - startDate={startDate} - > - {({ overviewHost, loading, id, inspect, refetch }) => { - const hostEventsCount = getOverviewHostStats(overviewHost).reduce( - (total, stat) => total + stat.count, - 0 - ); - const formattedHostEventsCount = numeral(hostEventsCount).format(defaultNumberFormat); - - return ( - <> - <HeaderSection - id={OverviewHostQueryId} - subtitle={ - !isEmpty(overviewHost) ? ( - <FormattedMessage - defaultMessage="Showing: {formattedHostEventsCount} {hostEventsCount, plural, one {event} other {events}}" - id="xpack.siem.overview.overviewHost.hostsSubtitle" - values={{ - formattedHostEventsCount, - hostEventsCount, - }} - /> - ) : ( - <>{''}</> - ) - } - title={ - <FormattedMessage - id="xpack.siem.overview.hostsTitle" - defaultMessage="Host events" - /> - } - > - {hostPageButton} - </HeaderSection> - - <OverviewHostStatsManage - loading={loading} - data={overviewHost} - setQuery={setQuery} - id={id} - inspect={inspect} - refetch={refetch} - /> - </> - ); - }} - </OverviewHostQuery> - </EuiPanel> - </InspectButtonContainer> - </EuiFlexItem> - ); -}; - -export const OverviewHost = React.memo(OverviewHostComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx deleted file mode 100644 index af50fa88e5fe8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.tsx +++ /dev/null @@ -1,132 +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 { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; -import { ESQuery } from '../../../../../../../../plugins/siem/common/typed_json'; -import { HeaderSection } from '../../../header_section'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { manageQuery } from '../../../page/manage_query'; -import { - ID as OverviewNetworkQueryId, - OverviewNetworkQuery, -} from '../../../../containers/overview/overview_network'; -import { inputsModel } from '../../../../store/inputs'; -import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; -import { getNetworkUrl } from '../../../link_to'; -import { InspectButtonContainer } from '../../../inspect'; -import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; -import { navTabs } from '../../../../pages/home/home_navigations'; - -export interface OverviewNetworkProps { - startDate: number; - endDate: number; - filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; -} - -const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); - -const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({ - endDate, - filterQuery, - startDate, - setQuery, -}) => { - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.network); - const networkPageButton = useMemo( - () => ( - <EuiButton href={getNetworkUrl(urlSearch)}> - <FormattedMessage id="xpack.siem.overview.networkAction" defaultMessage="View network" /> - </EuiButton> - ), - [urlSearch] - ); - return ( - <EuiFlexItem> - <InspectButtonContainer> - <EuiPanel> - <OverviewNetworkQuery - data-test-subj="overview-network-query" - endDate={endDate} - filterQuery={filterQuery} - sourceId="default" - startDate={startDate} - > - {({ overviewNetwork, loading, id, inspect, refetch }) => { - const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce( - (total, stat) => total + stat.count, - 0 - ); - const formattedNetworkEventsCount = numeral(networkEventsCount).format( - defaultNumberFormat - ); - - return ( - <> - <HeaderSection - id={OverviewNetworkQueryId} - subtitle={ - !isEmpty(overviewNetwork) ? ( - <FormattedMessage - defaultMessage="Showing: {formattedNetworkEventsCount} {networkEventsCount, plural, one {event} other {events}}" - id="xpack.siem.overview.overviewNetwork.networkSubtitle" - values={{ - formattedNetworkEventsCount, - networkEventsCount, - }} - /> - ) : ( - <>{''}</> - ) - } - title={ - <FormattedMessage - id="xpack.siem.overview.networkTitle" - defaultMessage="Network events" - /> - } - > - {networkPageButton} - </HeaderSection> - - <OverviewNetworkStatsManage - loading={loading} - data={overviewNetwork} - id={id} - inspect={inspect} - setQuery={setQuery} - refetch={refetch} - /> - </> - ); - }} - </OverviewNetworkQuery> - </EuiPanel> - </InspectButtonContainer> - </EuiFlexItem> - ); -}; - -OverviewNetworkComponent.displayName = 'OverviewNetworkComponent'; - -export const OverviewNetwork = React.memo(OverviewNetworkComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap deleted file mode 100644 index a32ebf07f1680..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,735 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Paginated Table Component rendering it renders the default load more table 1`] = ` -<ContextProvider - value={ - Object { - "darkMode": true, - "eui": Object { - "avatarSizing": Object { - "l": Object { - "font-size": "19.200000000000003px", - "size": "40px", - }, - "m": Object { - "font-size": "14.4px", - "size": "32px", - }, - "s": Object { - "font-size": "12px", - "size": "24px", - }, - "xl": Object { - "font-size": "25.6px", - "size": "64px", - }, - }, - "euiAnimSlightBounce": "cubic-bezier(0.34, 1.61, 0.7, 1)", - "euiAnimSlightResistance": "cubic-bezier(0.694, 0.0482, 0.335, 1)", - "euiAnimSpeedExtraFast": "90ms", - "euiAnimSpeedExtraSlow": "500ms", - "euiAnimSpeedFast": "150ms", - "euiAnimSpeedNormal": "250ms", - "euiAnimSpeedSlow": "350ms", - "euiBadgeGroupGutterTypes": Object { - "gutterExtraSmall": "4px", - "gutterSmall": "8px", - }, - "euiBorderColor": "#343741", - "euiBorderEditable": "2px dotted #343741", - "euiBorderRadius": "4px", - "euiBorderThick": "2px solid #343741", - "euiBorderThin": "1px solid #343741", - "euiBorderWidthThick": "2px", - "euiBorderWidthThin": "1px", - "euiBreadcrumbSpacing": "8px", - "euiBreadcrumbTruncateWidth": "160px", - "euiBreakpointKeys": "'xs', 's', 'm', 'l', 'xl'", - "euiBreakpoints": Object { - "l": "992px", - "m": "768px", - "s": "575px", - "xl": "1200px", - "xs": 0, - }, - "euiButtonColorDisabled": "#434548", - "euiButtonColorDisabledText": "#757678", - "euiButtonColorGhostDisabled": "#343741", - "euiButtonEmptyTypes": Object { - "danger": "#ff6666", - "disabled": "#757678", - "ghost": "#ffffff", - "primary": "#1ba9f5", - "text": "#dfe5ef", - }, - "euiButtonHeight": "40px", - "euiButtonHeightSmall": "32px", - "euiButtonIconTypes": Object { - "danger": "#ff6666", - "disabled": "#757678", - "ghost": "#ffffff", - "primary": "#1ba9f5", - "subdued": "#98a2b3", - "success": "#7de2d1", - "text": "#dfe5ef", - "warning": "#ffce7a", - }, - "euiButtonMinWidth": "112px", - "euiButtonToggleBorderColor": "#343741", - "euiButtonToggleTypes": Object { - "danger": "#ff6666", - "ghost": "#ffffff", - "primary": "#1ba9f5", - "secondary": "#7de2d1", - "text": "#98a2b3", - "warning": "#ffce7a", - }, - "euiButtonTypes": Object { - "danger": "#ff6666", - "ghost": "#ffffff", - "primary": "#1ba9f5", - "secondary": "#7de2d1", - "text": "#98a2b3", - "warning": "#ffce7a", - }, - "euiCallOutTypes": Object { - "danger": "#ff6666", - "primary": "#1ba9f5", - "success": "#7de2d1", - "warning": "#ffce7a", - }, - "euiCardBottomNodeHeight": "40px", - "euiCardSelectButtonBackgrounds": Object { - "danger": "#4d1f1f", - "ghost": "#98a2b3", - "primary": "#08334a", - "success": "#26443f", - "text": "#25262e", - }, - "euiCardSelectButtonBorders": Object { - "danger": "#ff6666", - "ghost": "#98a2b3", - "primary": "#1ba9f5", - "success": "#7de2d1", - "text": "#7de2d1", - }, - "euiCardSpacing": "16px", - "euiCheckBoxSize": "16px", - "euiCodeBlockAdditionBackgroundColor": "#144212", - "euiCodeBlockAdditionColor": "#e6e1dc", - "euiCodeBlockAttributeColor": "#80cbbf", - "euiCodeBlockBackgroundColor": "#25262e", - "euiCodeBlockBuiltInColor": "#0086b3", - "euiCodeBlockColor": "#dfe5ef", - "euiCodeBlockCommentColor": "#656565", - "euiCodeBlockDeletionBackgroundColor": "#660000", - "euiCodeBlockDeletionColor": "#e6e1dc", - "euiCodeBlockFunctionTitleColor": "#75a5ff", - "euiCodeBlockKeywordColor": "#c792ea", - "euiCodeBlockMetaColor": "#75a5ff", - "euiCodeBlockNameColor": "#e06c75", - "euiCodeBlockNumberColor": "#f77669", - "euiCodeBlockParamsColor": "#eefff7", - "euiCodeBlockRegexpColor": "#009926", - "euiCodeBlockSectionColor": "#ffc66d", - "euiCodeBlockSelectedBackgroundColor": "inherit", - "euiCodeBlockSelectorClassColor": "#ffcb68", - "euiCodeBlockSelectorIdColor": "#f77669", - "euiCodeBlockSelectorTagColor": "#c792ea", - "euiCodeBlockStringColor": "#c3e88d", - "euiCodeBlockSymbolColor": "#c792ea", - "euiCodeBlockTagColor": "#abb2bf", - "euiCodeBlockTitleColor": "#75a5ff", - "euiCodeBlockTypeColor": "#da4939", - "euiCodeFontFamily": "'Roboto Mono', 'Consolas', 'Menlo', 'Courier', monospace", - "euiColorAccent": "#f990c0", - "euiColorAccentText": "#f990c0", - "euiColorChartBand": "#2a2c35", - "euiColorChartLines": "#343741", - "euiColorDanger": "#ff6666", - "euiColorDangerText": "#ff6666", - "euiColorDarkShade": "#98a2b3", - "euiColorDarkestShade": "#d4dae5", - "euiColorEmptyShade": "#1d1e24", - "euiColorFullShade": "#ffffff", - "euiColorGhost": "#ffffff", - "euiColorHighlight": "#2e2d25", - "euiColorInk": "#000000", - "euiColorLightShade": "#343741", - "euiColorLightestShade": "#25262e", - "euiColorMediumShade": "#535966", - "euiColorPickerIndicatorSize": "12px", - "euiColorPickerSaturationRange0": "#000000", - "euiColorPickerSaturationRange1": "rgba(0, 0, 0, 0)", - "euiColorPickerValueRange0": "#ffffff", - "euiColorPickerValueRange1": "rgba(255, 255, 255, 0)", - "euiColorPickerWidth": "152px", - "euiColorPrimary": "#1ba9f5", - "euiColorPrimaryText": "#1ba9f5", - "euiColorSecondary": "#7de2d1", - "euiColorSecondaryText": "#7de2d1", - "euiColorSuccess": "#7de2d1", - "euiColorSuccessText": "#7de2d1", - "euiColorVis0": "#54b399", - "euiColorVis0_behindText": "#6dccb1", - "euiColorVis1": "#6092c0", - "euiColorVis1_behindText": "#79aad9", - "euiColorVis2": "#d36086", - "euiColorVis2_behindText": "#ee789d", - "euiColorVis3": "#9170b8", - "euiColorVis3_behindText": "#a987d1", - "euiColorVis4": "#ca8eae", - "euiColorVis4_behindText": "#e4a6c7", - "euiColorVis5": "#d6bf57", - "euiColorVis5_behindText": "#f1d86f", - "euiColorVis6": "#b9a888", - "euiColorVis6_behindText": "#d2c0a0", - "euiColorVis7": "#da8b45", - "euiColorVis7_behindText": "#f5a35c", - "euiColorVis8": "#aa6556", - "euiColorVis8_behindText": "#c47c6c", - "euiColorVis9": "#e7664c", - "euiColorVis9_behindText": "#ff7e62", - "euiColorWarning": "#ffce7a", - "euiColorWarningText": "#ffce7a", - "euiContextMenuWidth": "256px", - "euiControlBarBackground": "#000000", - "euiControlBarBorderColor": "rgba(255, 255, 255, 0.19999999999999996)", - "euiControlBarHeights": Object { - "l": "100vh", - "m": "480px", - "s": "240px", - }, - "euiControlBarInitialHeight": "40px", - "euiControlBarMaxHeight": "calc(100vh - 80px)", - "euiControlBarText": "#a9aaad", - "euiDataGridCellPaddingL": "8px", - "euiDataGridCellPaddingM": "6px", - "euiDataGridCellPaddingS": "4px", - "euiDataGridColumnResizerWidth": "3px", - "euiDataGridPrefix": ".euiDataGrid--", - "euiDataGridStyles": "'bordersAll', 'bordersNone', 'bordersHorizontal', 'paddingSmall', 'paddingMedium', 'paddingLarge', 'stripes', 'rowHoverNone', 'rowHoverHighlight', 'headerShade', 'headerUnderline', 'fontSizeSmall', 'fontSizeLarge', 'noControls'", - "euiDataGridVerticalBorder": "solid 1px #24272e", - "euiDatePickerCalendarWidth": "284px", - "euiDragAndDropSpacing": Object { - "l": "8px", - "m": "4px", - "s": "2px", - }, - "euiExpressionColors": Object { - "accent": "#f990c0", - "danger": "#ff6666", - "primary": "#1ba9f5", - "secondary": "#7de2d1", - "subdued": "#98a2b3", - "warning": "#ffce7a", - }, - "euiFilePickerTallHeight": "128px", - "euiFocusBackgroundColor": "#232635", - "euiFocusRingAnimStartColor": "rgba(27, 169, 245, 0)", - "euiFocusRingColor": "rgba(27, 169, 245, 0.3)", - "euiFocusRingSize": "3px", - "euiFocusRingSizeLarge": "4px", - "euiFontFamily": "'Inter UI', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica', 'Arial', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", - "euiFontFeatureSettings": "calt 1 kern 1 liga 1", - "euiFontSize": "16px", - "euiFontSizeL": "20px", - "euiFontSizeM": "18px", - "euiFontSizeS": "14px", - "euiFontSizeXL": "28px", - "euiFontSizeXS": "12px", - "euiFontSizeXXL": "36px", - "euiFontWeightBold": 700, - "euiFontWeightLight": 300, - "euiFontWeightMedium": 500, - "euiFontWeightRegular": 400, - "euiFontWeightSemiBold": 600, - "euiFormBackgroundColor": "#16171c", - "euiFormBackgroundDisabledColor": "#202128", - "euiFormBackgroundReadOnlyColor": "rgba(0, 0, 0, 0.050000000000000044)", - "euiFormBorderColor": "rgba(255, 255, 255, 0.09999999999999998)", - "euiFormBorderDisabledColor": "rgba(255, 255, 255, 0.09999999999999998)", - "euiFormBorderOpaqueColor": "#ffffff", - "euiFormControlBoxShadow": "0 1px 1px -1px rgba(0, 0, 0, 0.19999999999999996) 0 3px 2px -2px rgba(0, 0, 0, 0.19999999999999996)", - "euiFormControlCompressedBorderRadius": "2px", - "euiFormControlCompressedHeight": "32px", - "euiFormControlCompressedPadding": "8px", - "euiFormControlDisabledColor": "#535966", - "euiFormControlHeight": "40px", - "euiFormControlLayoutGroupInputCompressedBorderRadius": "1px", - "euiFormControlLayoutGroupInputCompressedHeight": "30px", - "euiFormControlLayoutGroupInputHeight": "38px", - "euiFormControlPadding": "12px", - "euiFormCustomControlBorderColor": "#66676d", - "euiFormCustomControlDisabledIconColor": "#a6aab0", - "euiFormInputGroupBorder": "1px solid #282a30", - "euiFormInputGroupLabelBackground": "#1f2127", - "euiFormMaxWidth": "400px", - "euiGradientMiddle": "#282a31", - "euiGradientStartStop": "#2e3039", - "euiHeaderBackgroundColor": "#1d1e24", - "euiHeaderBreadcrumbColor": "#d4dae5", - "euiHeaderChildSize": "48px", - "euiIconColors": Object { - "accent": "#f990c0", - "danger": "#ff6666", - "ghost": "#ffffff", - "primary": "#1ba9f5", - "secondary": "#7de2d1", - "subdued": "#98a2b3", - "success": "#7de2d1", - "text": "#dfe5ef", - "warning": "#ffce7a", - }, - "euiIconLoadingOpacity": 0.05, - "euiIconSizes": Object { - "large": "24px", - "medium": "16px", - "small": "12px", - "xLarge": "32px", - "xxLarge": "40px", - }, - "euiKeyPadMenuItemBetaBadgeSize": "20px", - "euiKeyPadMenuSize": "96px", - "euiLineHeight": 1.5, - "euiLinkColor": "#1ba9f5", - "euiListGroupGutterTypes": Object { - "gutterM": "16px", - "gutterS": "8px", - }, - "euiListGroupItemColorTypes": Object { - "primary": "#1ba9f5", - "subdued": "#98a2b3", - "text": "#dfe5ef", - }, - "euiListGroupItemSizeTypes": Object { - "large": "20px", - "medium": "16px", - "small": "14px", - "xSmall": "12px", - }, - "euiNavDrawerBackgroundColor": "#1d1e24", - "euiNavDrawerContractingDelay": "150ms", - "euiNavDrawerExpandingDelay": "250ms", - "euiNavDrawerExtendedDelay": "1000ms", - "euiNavDrawerMenuAddedDelay": "90ms", - "euiNavDrawerSideShadow": "2px 0 2px -1px rgba(0, 0, 0, 0.3)", - "euiNavDrawerTopPosition": "49px", - "euiNavDrawerWidthCollapsed": "48px", - "euiNavDrawerWidthExpanded": "240px", - "euiPageBackgroundColor": "#1a1b20", - "euiPaletteColorBlind": Object { - "euiColorVis0": Object { - "behindText": "#6dccb1", - "graphic": "#54b399", - }, - "euiColorVis1": Object { - "behindText": "#79aad9", - "graphic": "#6092c0", - }, - "euiColorVis2": Object { - "behindText": "#ee789d", - "graphic": "#d36086", - }, - "euiColorVis3": Object { - "behindText": "#a987d1", - "graphic": "#9170b8", - }, - "euiColorVis4": Object { - "behindText": "#e4a6c7", - "graphic": "#ca8eae", - }, - "euiColorVis5": Object { - "behindText": "#f1d86f", - "graphic": "#d6bf57", - }, - "euiColorVis6": Object { - "behindText": "#d2c0a0", - "graphic": "#b9a888", - }, - "euiColorVis7": Object { - "behindText": "#f5a35c", - "graphic": "#da8b45", - }, - "euiColorVis8": Object { - "behindText": "#c47c6c", - "graphic": "#aa6556", - }, - "euiColorVis9": Object { - "behindText": "#ff7e62", - "graphic": "#e7664c", - }, - }, - "euiPaletteColorBlindKeys": "'euiColorVis0', 'euiColorVis1', 'euiColorVis2', 'euiColorVis3', 'euiColorVis4', 'euiColorVis5', 'euiColorVis6', 'euiColorVis7', 'euiColorVis8', 'euiColorVis9'", - "euiPanelPaddingModifiers": Object { - "paddingLarge": "24px", - "paddingMedium": "16px", - "paddingSmall": "8px", - }, - "euiPopoverArrowSize": "12px", - "euiPopoverTranslateDistance": "8px", - "euiProgressColors": Object { - "accent": "#f990c0", - "danger": "#ff6666", - "primary": "#1ba9f5", - "secondary": "#7de2d1", - "subdued": "#535966", - "warning": "#ffce7a", - }, - "euiProgressSizes": Object { - "l": "16px", - "m": "8px", - "s": "4px", - "xs": "2px", - }, - "euiRadioSize": "16px", - "euiRangeDisabledOpacity": 0.25, - "euiRangeHighlightHeight": "4px", - "euiRangeLevelColors": Object { - "danger": "#ff6666", - "primary": "#1ba9f5", - "success": "#7de2d1", - "warning": "#ffce7a", - }, - "euiRangeThumbBorderColor": "#98a2b3", - "euiRangeThumbHeight": "16px", - "euiRangeThumbRadius": "50%", - "euiRangeThumbWidth": "16px", - "euiRangeTrackBorderColor": "#98a2b3", - "euiRangeTrackBorderWidth": 0, - "euiRangeTrackColor": "#98a2b3", - "euiRangeTrackHeight": "2px", - "euiRangeTrackRadius": "4px", - "euiRangeTrackWidth": "100%", - "euiScrollBar": "16px", - "euiScrollBarCorner": "6px", - "euiSelectableListItemBorder": "1px solid #202128", - "euiSelectableListItemPadding": "4px 12px", - "euiShadowColor": "#000000", - "euiShadowColorLarge": "#000000", - "euiSize": "16px", - "euiSizeL": "24px", - "euiSizeM": "12px", - "euiSizeS": "8px", - "euiSizeXL": "32px", - "euiSizeXS": "4px", - "euiSizeXXL": "40px", - "euiStepNumberMargin": "16px", - "euiStepNumberSize": "32px", - "euiStepStatusColorsToFade": Object { - "danger": "#ff6666", - "disabled": "#98a2b3", - "incomplete": "#98a2b3", - "warning": "#ffce7a", - }, - "euiSuggestItemColors": Object { - "tint0": "#54b399", - "tint1": "#6092c0", - "tint10": "#98a2b3", - "tint2": "#d36086", - "tint3": "#9170b8", - "tint4": "#ca8eae", - "tint5": "#d6bf57", - "tint6": "#b9a888", - "tint7": "#da8b45", - "tint8": "#aa6556", - "tint9": "#e7664c", - }, - "euiSuperDatePickerButtonWidth": "118px", - "euiSuperDatePickerWidth": "480px", - "euiSwitchHeight": "20px", - "euiSwitchHeightCompressed": "16px", - "euiSwitchHeightMini": "10px", - "euiSwitchIconHeight": "16px", - "euiSwitchOffColor": "rgba(83, 89, 102, 0.7)", - "euiSwitchThumbSize": "20px", - "euiSwitchThumbSizeCompressed": "16px", - "euiSwitchThumbSizeMini": "10px", - "euiSwitchWidth": "44px", - "euiSwitchWidthCompressed": "28px", - "euiSwitchWidthMini": "22px", - "euiTabFontSize": "16px", - "euiTabFontSizeS": "14px", - "euiTableActionsAreaWidth": "40px", - "euiTableActionsBorderColor": "rgba(83, 89, 102, 0.09999999999999998)", - "euiTableCellCheckboxWidth": "32px", - "euiTableCellContentPadding": "8px", - "euiTableCellContentPaddingCompressed": "4px", - "euiTableFocusClickableColor": "rgba(27, 169, 245, 0.09999999999999998)", - "euiTableHoverClickableColor": "rgba(27, 169, 245, 0.050000000000000044)", - "euiTableHoverColor": "#1e1e25", - "euiTableHoverSelectedColor": "#202230", - "euiTableSelectedColor": "#232635", - "euiTextColor": "#dfe5ef", - "euiTextConstrainedMaxWidth": "36em", - "euiTextScale": "2.25 1.75 1.25 1.125 1 0.875 0.75", - "euiTitleColor": "#dfe5ef", - "euiToastTypes": Object { - "danger": "#ff6666", - "primary": "#1ba9f5", - "success": "#7de2d1", - "warning": "#ffce7a", - }, - "euiToastWidth": "320px", - "euiTokenGrayColor": "#535966", - "euiTokenTypeKeys": "'euiColorVis0', 'euiColorVis1', 'euiColorVis2', 'euiColorVis3', 'euiColorVis4', 'euiColorVis5', 'euiColorVis6', 'euiColorVis7', 'euiColorVis8', 'euiColorVis9', 'gray'", - "euiTokenTypes": Object { - "euiColorVis0": Object { - "behindText": "#6dccb1", - "graphic": "#54b399", - }, - "euiColorVis1": Object { - "behindText": "#79aad9", - "graphic": "#6092c0", - }, - "euiColorVis2": Object { - "behindText": "#ee789d", - "graphic": "#d36086", - }, - "euiColorVis3": Object { - "behindText": "#a987d1", - "graphic": "#9170b8", - }, - "euiColorVis4": Object { - "behindText": "#e4a6c7", - "graphic": "#ca8eae", - }, - "euiColorVis5": Object { - "behindText": "#f1d86f", - "graphic": "#d6bf57", - }, - "euiColorVis6": Object { - "behindText": "#d2c0a0", - "graphic": "#b9a888", - }, - "euiColorVis7": Object { - "behindText": "#f5a35c", - "graphic": "#da8b45", - }, - "euiColorVis8": Object { - "behindText": "#c47c6c", - "graphic": "#aa6556", - }, - "euiColorVis9": Object { - "behindText": "#ff7e62", - "graphic": "#e7664c", - }, - "gray": Object { - "behindText": "#535966", - "graphic": "#535966", - }, - }, - "euiTooltipAnimations": Object { - "bottom": "euiToolTipLeft", - "left": "euiToolTipBottom", - "right": "euiToolTipRight", - "top": "euiToolTipTop", - }, - "euiTooltipBackgroundColor": "#000000", - "euiZComboBox": 8001, - "euiZContent": 0, - "euiZContentMenu": 2000, - "euiZHeader": 1000, - "euiZLevel0": 0, - "euiZLevel1": 1000, - "euiZLevel2": 2000, - "euiZLevel3": 3000, - "euiZLevel4": 4000, - "euiZLevel5": 5000, - "euiZLevel6": 6000, - "euiZLevel7": 7000, - "euiZLevel8": 8000, - "euiZLevel9": 9000, - "euiZMask": 6000, - "euiZModal": 8000, - "euiZNavigation": 4000, - "euiZToastList": 9000, - "flyoutSizes": Object { - "large": Object { - "max": "992px", - "min": "691px", - "width": "75vw", - }, - "medium": Object { - "max": "768px", - "min": "424px", - "width": "50vw", - }, - "small": Object { - "max": "403px", - "min": "384px", - "width": "25vw", - }, - }, - "fractions": Object { - "fourths": Object { - "count": 4, - "percentage": "25%", - }, - "halves": Object { - "count": 2, - "percentage": "50%", - }, - "single": Object { - "count": 1, - "percentage": "100%", - }, - "thirds": Object { - "count": 3, - "percentage": "33.3%", - }, - }, - "gutterTypes": Object { - "gutterExtraLarge": "40px", - "gutterExtraSmall": "4px", - "gutterLarge": "24px", - "gutterMedium": "16px", - "gutterSmall": "8px", - }, - "paddingSizes": Object { - "l": "24px", - "m": "16px", - "s": "8px", - "xl": "32px", - "xs": "4px", - }, - "ruleMargins": Object { - "marginLarge": "24px", - "marginMedium": "16px", - "marginSmall": "12px", - "marginXLarge": "32px", - "marginXSmall": "8px", - "marginXXLarge": "40px", - }, - "spacerSizes": Object { - "l": "24px", - "m": "16px", - "s": "8px", - "xl": "32px", - "xs": "4px", - "xxl": "40px", - }, - "textColors": Object { - "accent": "#f990c0", - "danger": "#ff6666", - "default": "#dfe5ef", - "ghost": "#ffffff", - "secondary": "#7de2d1", - "subdued": "#98a2b3", - "warning": "#ffce7a", - }, - "textareaResizing": Object { - "both": "resizeBoth", - "horizontal": "resizeHorizontal", - "none": "resizeNone", - "vertical": "resizeVertical", - }, - }, - } - } -> - <PaginatedTableComponent - activePage={0} - columns={ - Array [ - Object { - "field": "node.host.name", - "hideForMobile": false, - "name": "Host", - "render": [Function], - "truncateText": false, - }, - Object { - "field": "node.host.firstSeen", - "hideForMobile": false, - "name": "First seen", - "render": [Function], - "truncateText": false, - }, - Object { - "field": "node.host.os", - "hideForMobile": false, - "name": "OS", - "render": [Function], - "truncateText": false, - }, - Object { - "field": "node.host.version", - "hideForMobile": false, - "name": "Version", - "render": [Function], - "truncateText": false, - }, - ] - } - headerCount={1} - headerSupplement={ - <p> - My test supplement. - </p> - } - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={ - Array [ - Object { - "numberOfRow": 2, - "text": "2 rows", - }, - Object { - "numberOfRow": 5, - "text": "5 rows", - }, - Object { - "numberOfRow": 10, - "text": "10 rows", - }, - Object { - "numberOfRow": 20, - "text": "20 rows", - }, - Object { - "numberOfRow": 50, - "text": "50 rows", - }, - ] - } - limit={1} - loadPage={[MockFunction]} - loading={false} - pageOfItems={ - Array [ - Object { - "cursor": Object { - "value": "98966fa2013c396155c460d35c0902be", - }, - "host": Object { - "_id": "cPsuhGcB0WOhS6qyTKC0", - "firstSeen": "2018-12-06T15:40:53.319Z", - "name": "elrond.elstc.co", - "os": "Ubuntu", - "version": "18.04.1 LTS (Bionic Beaver)", - }, - }, - Object { - "cursor": Object { - "value": "aa7ca589f1b8220002f2fc61c64cfbf1", - }, - "host": Object { - "_id": "KwQDiWcB0WOhS6qyXmrW", - "firstSeen": "2018-12-07T14:12:38.560Z", - "name": "siem-kibana", - "os": "Debian GNU/Linux", - "version": "9 (stretch)", - }, - }, - ] - } - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={[MockFunction]} - updateLimitPagination={[Function]} - /> -</ContextProvider> -`; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx deleted file mode 100644 index 2f743c3387209..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx +++ /dev/null @@ -1,522 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../plugins/siem/common/constants'; -import { Direction } from '../../graphql/types'; - -import { BasicTableProps, PaginatedTable } from './index'; -import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; - -jest.mock('react', () => { - const r = jest.requireActual('react'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { ...r, memo: (x: any) => x }; -}); - -describe('Paginated Table Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - let loadPage: jest.Mock<number>; - let updateLimitPagination: jest.Mock<number>; - let updateActivePage: jest.Mock<number>; - beforeEach(() => { - loadPage = jest.fn(); - updateLimitPagination = jest.fn(); - updateActivePage = jest.fn(); - }); - - describe('rendering', () => { - test('it renders the default load more table', () => { - const wrapper = shallow( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the loading panel at the beginning ', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={-1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={[]} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect( - wrapper.find('[data-test-subj="initialLoadingPanelPaginatedTable"]').exists() - ).toBeTruthy(); - }); - - test('it renders the over loading panel after data has been in the table ', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="loadingPanelPaginatedTable"]').exists()).toBeTruthy(); - }); - - test('it renders the correct amount of pages and starts at activePage: 0', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - const paginiationProps = wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .props(); - - const expectedPaginationProps = { - 'data-test-subj': 'numberedPagination', - pageCount: 10, - activePage: 0, - }; - expect(JSON.stringify(paginiationProps)).toEqual(JSON.stringify(expectedPaginationProps)); - }); - - test('it render popover to select new limit in table', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy(); - }); - - test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={[]} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); - }); - - test('It should render a sort icon if sorting is defined', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={sortedHosts} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy(); - }); - - test('Should display toast when user reaches end of results max', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls.length).toEqual(0); - }); - - test('Should show items per row if totalCount is greater than items', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={30} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); - }); - - test('Should hide items per row if totalCount is less than items', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={1} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); - }); - }); - - describe('Events', () => { - test('should call updateActivePage with 1 when clicking to the first page', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls[0][0]).toEqual(1); - }); - - test('Should call updateActivePage with 0 when you pick a new limit', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMorePickSizeRow"] button') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls[1][0]).toEqual(0); - }); - - test('should update the page when the activePage is changed from redux', () => { - const ourProps: BasicTableProps<unknown> = { - activePage: 3, - columns: getHostsColumns(), - headerCount: 1, - headerSupplement: <p>{'My test supplement.'}</p>, - headerTitle: 'Hosts', - headerTooltip: 'My test tooltip', - headerUnit: 'Test Unit', - itemsPerRow: rowItems, - limit: 1, - loading: false, - loadPage, - pageOfItems: mockData.Hosts.edges, - showMorePagesIndicator: true, - totalCount: 10, - updateActivePage, - updateLimitPagination: limit => updateLimitPagination({ limit }), - }; - - // enzyme does not allow us to pass props to child of HOC - // so we make a component to pass it the props context - // ComponentWithContext will pass the changed props to Component - // https://github.com/airbnb/enzyme/issues/1853#issuecomment-443475903 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ComponentWithContext = (props: BasicTableProps<any>) => { - return ( - <ThemeProvider theme={theme}> - <PaginatedTable {...props} /> - </ThemeProvider> - ); - }; - - const wrapper = mount(<ComponentWithContext {...ourProps} />); - expect( - wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .prop('activePage') - ).toEqual(3); - wrapper.setProps({ activePage: 0 }); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .prop('activePage') - ).toEqual(0); - }); - - test('Should call updateLimitPagination when you pick a new limit', () => { - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={getHostsColumns()} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMorePickSizeRow"] button') - .first() - .simulate('click'); - expect(updateLimitPagination).toBeCalled(); - }); - - test('Should call onChange when you choose a new sort in the table', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - <ThemeProvider theme={theme}> - <PaginatedTable - activePage={0} - columns={sortedHosts} - headerCount={1} - headerSupplement={<p>{'My test supplement.'}</p>} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> - </ThemeProvider> - ); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - expect(mockOnChange).toBeCalled(); - expect(mockOnChange.mock.calls[0]).toEqual([ - { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } }, - ]); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx deleted file mode 100644 index e481fe7245201..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx +++ /dev/null @@ -1,350 +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 { - EuiBasicTable, - EuiBasicTableProps, - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiGlobalToastListToast as Toast, - EuiLoadingContent, - EuiPagination, - EuiPopover, - Direction, -} from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; -import styled from 'styled-components'; - -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../plugins/siem/common/constants'; -import { AuthTableColumns } from '../page/hosts/authentications_table'; -import { HostsTableColumns } from '../page/hosts/hosts_table'; -import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; -import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; -import { - NetworkTopNFlowColumns, - NetworkTopNFlowColumnsIpDetails, -} from '../page/network/network_top_n_flow_table/columns'; -import { - NetworkTopCountriesColumns, - NetworkTopCountriesColumnsIpDetails, -} from '../page/network/network_top_countries_table/columns'; -import { TlsColumns } from '../page/network/tls_table/columns'; -import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table'; -import { UsersColumns } from '../page/network/users_table/columns'; -import { HeaderSection } from '../header_section'; -import { Loader } from '../loader'; -import { useStateToaster } from '../toasters'; - -import * as i18n from './translations'; -import { Panel } from '../panel'; -import { InspectButtonContainer } from '../inspect'; - -const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; - -export interface ItemsPerRow { - text: string; - numberOfRow: number; -} - -export interface SortingBasicTable { - field: string; - direction: Direction; - allowNeutralSort?: boolean; -} - -export interface Criteria { - page?: { index: number; size: number }; - sort?: SortingBasicTable; -} - -declare type HostsTableColumnsTest = [ - Columns<string>, - Columns<string>, - Columns<string>, - Columns<string> -]; - -declare type BasicTableColumns = - | AuthTableColumns - | HostsTableColumns - | HostsTableColumnsTest - | NetworkDnsColumns - | NetworkHttpColumns - | NetworkTopCountriesColumns - | NetworkTopCountriesColumnsIpDetails - | NetworkTopNFlowColumns - | NetworkTopNFlowColumnsIpDetails - | TlsColumns - | UncommonProcessTableColumns - | UsersColumns; - -declare type SiemTables = BasicTableProps<BasicTableColumns>; - -// Using telescoping templates to remove 'any' that was polluting downstream column type checks -export interface BasicTableProps<T> { - activePage: number; - columns: T; - dataTestSubj?: string; - headerCount: number; - headerSupplement?: React.ReactElement; - headerTitle: string | React.ReactElement; - headerTooltip?: string; - headerUnit: string | React.ReactElement; - id?: string; - itemsPerRow?: ItemsPerRow[]; - isInspect?: boolean; - limit: number; - loading: boolean; - loadPage: (activePage: number) => void; - onChange?: (criteria: Criteria) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pageOfItems: any[]; - showMorePagesIndicator: boolean; - sorting?: SortingBasicTable; - totalCount: number; - updateActivePage: (activePage: number) => void; - updateLimitPagination: (limit: number) => void; -} -type Func<T> = (arg: T) => string | number; - -export interface Columns<T, U = T> { - align?: string; - field?: string; - hideForMobile?: boolean; - isMobileHeader?: boolean; - name: string | React.ReactNode; - render?: (item: T, node: U) => React.ReactNode; - sortable?: boolean | Func<T>; - truncateText?: boolean; - width?: string; -} - -const PaginatedTableComponent: FC<SiemTables> = ({ - activePage, - columns, - dataTestSubj = DEFAULT_DATA_TEST_SUBJ, - headerCount, - headerSupplement, - headerTitle, - headerTooltip, - headerUnit, - id, - isInspect, - itemsPerRow, - limit, - loading, - loadPage, - onChange = noop, - pageOfItems, - showMorePagesIndicator, - sorting = null, - totalCount, - updateActivePage, - updateLimitPagination, -}) => { - const [myLoading, setMyLoading] = useState(loading); - const [myActivePage, setActivePage] = useState(activePage); - const [loadingInitial, setLoadingInitial] = useState(headerCount === -1); - const [isPopoverOpen, setPopoverOpen] = useState(false); - - const pageCount = Math.ceil(totalCount / limit); - const dispatchToaster = useStateToaster()[1]; - - useEffect(() => { - setActivePage(activePage); - }, [activePage]); - - useEffect(() => { - if (headerCount >= 0 && loadingInitial) { - setLoadingInitial(false); - } - }, [loadingInitial, headerCount]); - - useEffect(() => { - setMyLoading(loading); - }, [loading]); - - const onButtonClick = () => { - setPopoverOpen(!isPopoverOpen); - }; - - const closePopover = () => { - setPopoverOpen(false); - }; - - const goToPage = (newActivePage: number) => { - if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - const toast: Toast = { - id: 'PaginationWarningMsg', - title: headerTitle + i18n.TOAST_TITLE, - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 10000, - text: i18n.TOAST_TEXT, - }; - return dispatchToaster({ - type: 'addToaster', - toast, - }); - } - setActivePage(newActivePage); - loadPage(newActivePage); - updateActivePage(newActivePage); - }; - - const button = ( - <EuiButtonEmpty - size="xs" - color="text" - iconType="arrowDown" - iconSide="right" - onClick={onButtonClick} - > - {`${i18n.ROWS}: ${limit}`} - </EuiButtonEmpty> - ); - - const rowItems = - itemsPerRow && - itemsPerRow.map((item: ItemsPerRow) => ( - <EuiContextMenuItem - key={item.text} - icon={limit === item.numberOfRow ? 'check' : 'empty'} - onClick={() => { - closePopover(); - updateLimitPagination(item.numberOfRow); - updateActivePage(0); // reset results to first page - }} - > - {item.text} - </EuiContextMenuItem> - )); - const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; - - return ( - <InspectButtonContainer show={!loadingInitial}> - <Panel data-test-subj={`${dataTestSubj}-loading-${loading}`} loading={loading}> - <HeaderSection - id={id} - subtitle={ - !loadingInitial && - `${i18n.SHOWING}: ${headerCount >= 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` - } - title={headerTitle} - tooltip={headerTooltip} - > - {!loadingInitial && headerSupplement} - </HeaderSection> - - {loadingInitial ? ( - <EuiLoadingContent data-test-subj="initialLoadingPanelPaginatedTable" lines={10} /> - ) : ( - <> - <BasicTable - columns={columns} - compressed - items={pageOfItems} - onChange={onChange} - sorting={ - sorting - ? { - sort: { - field: sorting.field, - direction: sorting.direction, - }, - } - : undefined - } - /> - <FooterAction> - <EuiFlexItem> - {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( - <EuiPopover - id="customizablePagination" - data-test-subj="loadingMoreSizeRowPopover" - button={button} - isOpen={isPopoverOpen} - closePopover={closePopover} - panelPaddingSize="none" - > - <EuiContextMenuPanel items={rowItems} data-test-subj="loadingMorePickSizeRow" /> - </EuiPopover> - )} - </EuiFlexItem> - - <PaginationWrapper grow={false}> - <EuiPagination - data-test-subj="numberedPagination" - pageCount={pageCount} - activePage={myActivePage} - onPageClick={goToPage} - /> - </PaginationWrapper> - </FooterAction> - {(isInspect || myLoading) && ( - <Loader data-test-subj="loadingPanelPaginatedTable" overlay size="xl" /> - )} - </> - )} - </Panel> - </InspectButtonContainer> - ); -}; - -export const PaginatedTable = memo(PaginatedTableComponent); - -type BasicTableType = ComponentType<EuiBasicTableProps<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any -const BasicTable = styled(EuiBasicTable as BasicTableType)` - tbody { - th, - td { - vertical-align: top; - } - - .euiTableCellContent { - display: block; - } - } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -BasicTable.displayName = 'BasicTable'; - -const FooterAction = styled(EuiFlexGroup).attrs(() => ({ - alignItems: 'center', - responsive: false, -}))` - margin-top: ${({ theme }) => theme.eui.euiSizeXS}; -`; - -FooterAction.displayName = 'FooterAction'; - -const PaginationEuiFlexItem = styled(EuiFlexItem)` - @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { - .euiButtonIcon:last-child { - margin-left: 28px; - } - - .euiPagination { - position: relative; - } - - .euiPagination::before { - bottom: 0; - color: ${({ theme }) => theme.eui.euiButtonColorDisabled}; - content: '\\2026'; - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; - padding: 5px ${({ theme }) => theme.eui.euiSizeS}; - position: absolute; - right: ${({ theme }) => theme.eui.euiSizeL}; - } - } -`; - -PaginationEuiFlexItem.displayName = 'PaginationEuiFlexItem'; diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx deleted file mode 100644 index 49afc8d5ef68b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ /dev/null @@ -1,338 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../../plugins/siem/common/constants'; -import { TestProviders, mockIndexPattern } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; -import { QueryBar, QueryBarComponentProps } from '.'; -import { createKibanaContextProviderMock } from '../../mock/kibana_react'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -describe('QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - const mockOnChangeQuery = jest.fn(); - const mockOnSubmitQuery = jest.fn(); - const mockOnSavedQuery = jest.fn(); - - beforeEach(() => { - mockOnChangeQuery.mockClear(); - mockOnSubmitQuery.mockClear(); - mockOnSavedQuery.mockClear(); - }); - - test('check if we format the appropriate props to QueryBar', () => { - const wrapper = mount( - <TestProviders> - <QueryBar - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - </TestProviders> - ); - const { - customSubmitButton, - timeHistory, - onClearSavedQuery, - onFiltersUpdated, - onQueryChange, - onQuerySubmit, - onSaved, - onSavedQueryUpdated, - ...searchBarProps - } = wrapper.find(SearchBar).props(); - - expect(searchBarProps).toEqual({ - dataTestSubj: undefined, - dateRangeFrom: 'now-24h', - dateRangeTo: 'now', - filters: [], - indexPatterns: [ - { - fields: [ - { - aggregatable: true, - name: '@timestamp', - searchable: true, - type: 'date', - }, - { - aggregatable: true, - name: '@version', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.id', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test1', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test2', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test3', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test4', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test5', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test6', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test7', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test8', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'host.name', - searchable: true, - type: 'string', - }, - ], - title: 'filebeat-*,auditbeat-*,packetbeat-*', - }, - ], - isLoading: false, - isRefreshPaused: true, - query: { - language: 'kuery', - query: 'here: query', - }, - refreshInterval: undefined, - showAutoRefreshOnly: false, - showDatePicker: false, - showFilterBar: true, - showQueryBar: true, - showQueryInput: true, - showSaveQuery: true, - }); - }); - - describe('#onQueryChange', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <KibanaWithStorageProvider services={{ storage: { get: jest.fn() } }}> - <QueryBar {...props} /> - </KibanaWithStorageProvider> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); - queryInput.simulate('change', { target: { value: 'hello: world' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - }); - - describe('#onQuerySubmit', () => { - test(' is the only reference that changed when filterQuery props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <QueryBar {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - - test(' is only reference that changed when timelineId props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <QueryBar {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ onSubmitQuery: jest.fn() }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - }); - - describe('#onSavedQueryUpdated', () => { - test('is only reference that changed when dataProviders props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - <TestProviders> - <QueryBar {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - dateRangeFrom={DEFAULT_FROM} - dateRangeTo={DEFAULT_TO} - hideSavedQuery={false} - indexPattern={mockIndexPattern} - isRefreshPaused={true} - filterQuery={{ query: 'here: query', language: 'kuery' }} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filters={[]} - onChangedQuery={mockOnChangeQuery} - onSubmitQuery={mockOnSubmitQuery} - onSavedQuery={mockOnSavedQuery} - /> - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ onSavedQuery: jest.fn() }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx deleted file mode 100644 index 557d389aefee9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ /dev/null @@ -1,153 +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 React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - IIndexPattern, - FilterManager, - Query, - TimeHistory, - TimeRange, - SavedQuery, - SearchBar, - SavedQueryTimeFilter, -} from '../../../../../../../src/plugins/data/public'; -import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; - -export interface QueryBarComponentProps { - dataTestSubj?: string; - dateRangeFrom?: string; - dateRangeTo?: string; - hideSavedQuery?: boolean; - indexPattern: IIndexPattern; - isLoading?: boolean; - isRefreshPaused?: boolean; - filterQuery: Query; - filterManager: FilterManager; - filters: Filter[]; - onChangedQuery: (query: Query) => void; - onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; - refreshInterval?: number; - savedQuery?: SavedQuery | null; - onSavedQuery: (savedQuery: SavedQuery | null) => void; -} - -export const QueryBar = memo<QueryBarComponentProps>( - ({ - dateRangeFrom, - dateRangeTo, - hideSavedQuery = false, - indexPattern, - isLoading = false, - isRefreshPaused, - filterQuery, - filterManager, - filters, - onChangedQuery, - onSubmitQuery, - refreshInterval, - savedQuery, - onSavedQuery, - dataTestSubj, - }) => { - const [draftQuery, setDraftQuery] = useState(filterQuery); - - useEffect(() => { - setDraftQuery(filterQuery); - }, [filterQuery]); - - const onQuerySubmit = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !deepEqual(payload.query, filterQuery)) { - onSubmitQuery(payload.query); - } - }, - [filterQuery, onSubmitQuery] - ); - - const onQueryChange = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !deepEqual(payload.query, draftQuery)) { - setDraftQuery(payload.query); - onChangedQuery(payload.query); - } - }, - [draftQuery, onChangedQuery, setDraftQuery] - ); - - const onSaved = useCallback( - (newSavedQuery: SavedQuery) => { - onSavedQuery(newSavedQuery); - }, - [onSavedQuery] - ); - - const onSavedQueryUpdated = useCallback( - (savedQueryUpdated: SavedQuery) => { - const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; - onSubmitQuery(newQuery, timefilter); - filterManager.setFilters(newFilters || []); - onSavedQuery(savedQueryUpdated); - }, - [filterManager, onSubmitQuery, onSavedQuery] - ); - - const onClearSavedQuery = useCallback(() => { - if (savedQuery != null) { - onSubmitQuery({ - query: '', - language: savedQuery.attributes.query.language, - }); - filterManager.setFilters([]); - onSavedQuery(null); - } - }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); - - const onFiltersUpdated = useCallback( - (newFilters: Filter[]) => { - filterManager.setFilters(newFilters); - }, - [filterManager] - ); - - const CustomButton = <>{null}</>; - const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); - - const searchBarProps = savedQuery != null ? { savedQuery } : {}; - - return ( - <SearchBar - customSubmitButton={CustomButton} - dateRangeFrom={dateRangeFrom} - dateRangeTo={dateRangeTo} - filters={filters} - indexPatterns={indexPatterns} - isLoading={isLoading} - isRefreshPaused={isRefreshPaused} - query={draftQuery} - onClearSavedQuery={onClearSavedQuery} - onFiltersUpdated={onFiltersUpdated} - onQueryChange={onQueryChange} - onQuerySubmit={onQuerySubmit} - onSaved={onSaved} - onSavedQueryUpdated={onSavedQueryUpdated} - refreshInterval={refreshInterval} - showAutoRefreshOnly={false} - showFilterBar={!hideSavedQuery} - showDatePicker={false} - showQueryBar={true} - showQueryInput={true} - showSaveQuery={true} - timeHistory={new TimeHistory(new Storage(localStorage))} - dataTestSubj={dataTestSubj} - {...searchBarProps} - /> - ); - } -); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx deleted file mode 100644 index 5b851701b973c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/recent_timelines/index.tsx +++ /dev/null @@ -1,110 +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 ApolloClient from 'apollo-client'; -import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { AllTimelinesQuery } from '../../containers/timeline/all'; -import { SortFieldTimeline, Direction } from '../../graphql/types'; -import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; -import { OnOpenTimeline } from '../open_timeline/types'; -import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; -import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions'; - -import { RecentTimelines } from './recent_timelines'; -import * as i18n from './translations'; -import { FilterMode } from './types'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { navTabs } from '../../pages/home/home_navigations'; -import { getTimelinesUrl } from '../link_to/redirect_to_timelines'; - -interface OwnProps { - apolloClient: ApolloClient<{}>; - filterBy: FilterMode; -} - -export type Props = OwnProps & PropsFromRedux; - -const PAGE_SIZE = 3; - -const StatefulRecentTimelinesComponent = React.memo<Props>( - ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { - const onOpenTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { - queryTimelineById({ - apolloClient, - duplicate, - timelineId, - updateIsLoading, - updateTimeline, - }); - }, - [apolloClient, updateIsLoading, updateTimeline] - ); - - const noTimelinesMessage = - filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; - const urlSearch = useGetUrlSearch(navTabs.timelines); - const linkAllTimelines = useMemo( - () => <EuiLink href={getTimelinesUrl(urlSearch)}>{i18n.VIEW_ALL_TIMELINES}</EuiLink>, - [urlSearch] - ); - const loadingPlaceholders = useMemo( - () => ( - <LoadingPlaceholders lines={2} placeholders={filterBy === 'favorites' ? 1 : PAGE_SIZE} /> - ), - [filterBy] - ); - - return ( - <AllTimelinesQuery - pageInfo={{ - pageIndex: 1, - pageSize: PAGE_SIZE, - }} - search={''} - sort={{ - sortField: SortFieldTimeline.updated, - sortOrder: Direction.desc, - }} - onlyUserFavorite={filterBy === 'favorites'} - > - {({ timelines, loading }) => ( - <> - {loading ? ( - loadingPlaceholders - ) : ( - <RecentTimelines - noTimelinesMessage={noTimelinesMessage} - onOpenTimeline={onOpenTimeline} - timelines={timelines} - /> - )} - <EuiHorizontalRule margin="s" /> - <EuiText size="xs">{linkAllTimelines}</EuiText> - </> - )} - </AllTimelinesQuery> - ); - } -); - -StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const StatefulRecentTimelines = connector(StatefulRecentTimelinesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx deleted file mode 100644 index d64ddb9bb40b1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.test.tsx +++ /dev/null @@ -1,443 +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 { mount } from 'enzyme'; -import React from 'react'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; -import { apolloClientObservable, mockGlobalState } from '../../mock'; -import { createUseUiSetting$Mock } from '../../mock/kibana_react'; -import { createStore, State } from '../../store'; - -import { SuperDatePicker, makeMapStateToProps } from '.'; -import { cloneDeep } from 'lodash/fp'; - -jest.mock('../../lib/kibana'); -const mockUseUiSetting$ = useUiSetting$ as jest.Mock; -const timepickerRanges = [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - }, - { - from: 'now-15m', - to: 'now', - display: 'Last 15 minutes', - }, - { - from: 'now-30m', - to: 'now', - display: 'Last 30 minutes', - }, - { - from: 'now-1h', - to: 'now', - display: 'Last 1 hour', - }, - { - from: 'now-24h', - to: 'now', - display: 'Last 24 hours', - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - }, -]; - -describe('SIEM Super Date Picker', () => { - describe('#SuperDatePicker', () => { - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - jest.clearAllMocks(); - store = createStore(state, apolloClientObservable); - mockUseUiSetting$.mockImplementation((key, defaultValue) => { - const useUiSetting$Mock = createUseUiSetting$Mock(); - - return key === DEFAULT_TIMEPICKER_QUICK_RANGES - ? [timepickerRanges, jest.fn()] - : useUiSetting$Mock(key, defaultValue); - }); - }); - - describe('Pick Relative Date', () => { - let wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - beforeEach(() => { - wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.euiQuickSelect__applyButton') - .first() - .simulate('click'); - wrapper.update(); - }); - - test('Make Sure it is relative date', () => { - expect(store.getState().inputs.global.timerange.kind).toBe('relative'); - }); - - test('Make Sure it is last 15 minutes date', () => { - expect(store.getState().inputs.global.timerange.fromStr).toBe('now-15m'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now'); - }); - - test('Make Sure it is Today date', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') - .first() - .simulate('click'); - wrapper.update(); - expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); - }); - - test('Make Sure to (end date) is superior than from (start date)', () => { - expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( - store.getState().inputs.global.timerange.from - ); - }); - }); - - describe('Recently used date ranges', () => { - let wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - beforeEach(() => { - wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') - .first() - .simulate('click'); - wrapper.update(); - }); - - test('Today is in Recently used date ranges', () => { - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Today'); - }); - - test('Today and Last 15 minutes are in Recently used date ranges', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.euiQuickSelect__applyButton') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Last 15 minutesToday'); - }); - - test('Make sure that it does not add any duplicate if you click again on today', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Today'); - }); - }); - - describe('Refresh Every', () => { - let wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - beforeEach(() => { - wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - const wrapperFixedEuiFieldSearch = wrapper.find( - 'input[data-test-subj="superDatePickerRefreshIntervalInput"]' - ); - - wrapperFixedEuiFieldSearch.simulate('change', { target: { value: '2' } }); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerToggleRefreshButton"]') - .first() - .simulate('click'); - wrapper.update(); - }); - - test('Make sure the duration get updated to 2 minutes === 120000ms', () => { - expect(store.getState().inputs.global.policy.duration).toEqual(120000); - }); - - test('Make sure the stream live started', () => { - expect(store.getState().inputs.global.policy.kind).toBe('interval'); - }); - - test('Make sure we can stop the stream live', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerToggleRefreshButton"]') - .first() - .simulate('click'); - wrapper.update(); - - expect(store.getState().inputs.global.policy.kind).toBe('manual'); - }); - }); - - describe('Pick Absolute Date', () => { - let wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - beforeEach(() => { - wrapper = mount( - <ReduxStoreProvider store={store}> - <SuperDatePicker id="global" /> - </ReduxStoreProvider> - ); - wrapper - .find('[data-test-subj="superDatePickerShowDatesButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerstartDatePopoverButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerAbsoluteTab"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.react-datepicker__navigation--previous') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('div.react-datepicker__day') - .at(1) - .simulate('click'); - wrapper.update(); - - wrapper - .find('button[data-test-subj="superDatePickerApplyTimeButton"]') - .first() - .simulate('click'); - wrapper.update(); - }); - }); - - describe('#makeMapStateToProps', () => { - test('it should return the same shallow references given the same input twice', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const props2 = mapStateToProps(state, { id: 'global' }); - Object.keys(props1).forEach(key => { - expect((props1 as Record<string, {}>)[key]).toBe((props2 as Record<string, {}>)[key]); - }); - }); - - test('it should not return the same reference if policy kind is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.policy.kind = 'interval'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.policy).not.toBe(props2.policy); - }); - - test('it should not return the same reference if duration is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.policy.duration = 99999; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.duration).not.toBe(props2.duration); - }); - - test('it should not return the same reference if timerange kind is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.kind = 'absolute'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.kind).not.toBe(props2.kind); - }); - - test('it should not return the same reference if timerange from is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.from = 999; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.start).not.toBe(props2.start); - }); - - test('it should not return the same reference if timerange to is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.to = 999; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.end).not.toBe(props2.end); - }); - - test('it should not return the same reference of toStr if toStr different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.toStr = 'some other string'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.toStr).not.toBe(props2.toStr); - }); - - test('it should not return the same reference of fromStr if fromStr different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.fromStr = 'some other string'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.fromStr).not.toBe(props2.fromStr); - }); - - test('it should not return the same reference of isLoadingSelector if the query different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.queries = [ - { - loading: true, - id: '1', - inspect: { dsl: [], response: [] }, - isInspected: false, - refetch: null, - selectedInspectIndex: 0, - }, - ]; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.isLoading).not.toBe(props2.isLoading); - }); - - test('it should not return the same reference of refetchSelector if the query different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.queries = [ - { - loading: true, - id: '1', - inspect: { dsl: [], response: [] }, - isInspected: false, - refetch: null, - selectedInspectIndex: 0, - }, - ]; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.queries).not.toBe(props2.queries); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx deleted file mode 100644 index cf350b3993a4b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/super_date_picker/index.tsx +++ /dev/null @@ -1,313 +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 dateMath from '@elastic/datemath'; -import { - EuiSuperDatePicker, - OnRefreshChangeProps, - EuiSuperDatePickerRecentRange, - OnRefreshProps, - OnTimeChangeProps, -} from '@elastic/eui'; -import { getOr, take, isEmpty } from 'lodash/fp'; -import React, { useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; -import { inputsModel, State } from '../../store'; -import { inputsActions, timelineActions } from '../../store/actions'; -import { InputsModelId } from '../../store/inputs/constants'; -import { - policySelector, - durationSelector, - kindSelector, - startSelector, - endSelector, - fromStrSelector, - toStrSelector, - isLoadingSelector, - queriesSelector, - kqlQuerySelector, -} from './selectors'; -import { InputsRange } from '../../store/inputs/model'; - -const MAX_RECENTLY_USED_RANGES = 9; - -interface Range { - from: string; - to: string; - display: string; -} - -interface UpdateReduxTime extends OnTimeChangeProps { - id: InputsModelId; - kql?: inputsModel.GlobalKqlQuery | undefined; - timelineId?: string; -} - -interface ReturnUpdateReduxTime { - kqlHaveBeenUpdated: boolean; -} - -export type DispatchUpdateReduxTime = ({ - end, - id, - isQuickSelection, - kql, - start, - timelineId, -}: UpdateReduxTime) => ReturnUpdateReduxTime; - -interface OwnProps { - disabled?: boolean; - id: InputsModelId; - timelineId?: string; -} - -export type SuperDatePickerProps = OwnProps & PropsFromRedux; - -export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>( - ({ - duration, - end, - fromStr, - id, - isLoading, - kind, - kqlQuery, - policy, - queries, - setDuration, - start, - startAutoReload, - stopAutoReload, - timelineId, - toStr, - updateReduxTime, - }) => { - const [isQuickSelection, setIsQuickSelection] = useState(true); - const [recentlyUsedRanges, setRecentlyUsedRanges] = useState<EuiSuperDatePickerRecentRange[]>( - [] - ); - const onRefresh = useCallback( - ({ start: newStart, end: newEnd }: OnRefreshProps): void => { - const { kqlHaveBeenUpdated } = updateReduxTime({ - end: newEnd, - id, - isInvalid: false, - isQuickSelection, - kql: kqlQuery, - start: newStart, - timelineId, - }); - const currentStart = formatDate(newStart); - const currentEnd = isQuickSelection - ? formatDate(newEnd, { roundUp: true }) - : formatDate(newEnd); - if ( - !kqlHaveBeenUpdated && - (!isQuickSelection || (start === currentStart && end === currentEnd)) - ) { - refetchQuery(queries); - } - }, - [end, id, isQuickSelection, kqlQuery, start, timelineId] - ); - - const onRefreshChange = useCallback( - ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { - if (duration !== refreshInterval) { - setDuration({ id, duration: refreshInterval }); - } - - if (isPaused && policy === 'interval') { - stopAutoReload({ id }); - } else if (!isPaused && policy === 'manual') { - startAutoReload({ id }); - } - - if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { - refetchQuery(queries); - } - }, - [id, isQuickSelection, duration, policy, toStr] - ); - - const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { - newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - }; - - const onTimeChange = useCallback( - ({ - start: newStart, - end: newEnd, - isQuickSelection: newIsQuickSelection, - isInvalid, - }: OnTimeChangeProps) => { - if (!isInvalid) { - updateReduxTime({ - end: newEnd, - id, - isInvalid, - isQuickSelection: newIsQuickSelection, - kql: kqlQuery, - start: newStart, - timelineId, - }); - const newRecentlyUsedRanges = [ - { start: newStart, end: newEnd }, - ...take( - MAX_RECENTLY_USED_RANGES, - recentlyUsedRanges.filter( - recentlyUsedRange => - !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) - ) - ), - ]; - - setRecentlyUsedRanges(newRecentlyUsedRanges); - setIsQuickSelection(newIsQuickSelection); - } - }, - [recentlyUsedRanges, kqlQuery] - ); - - const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); - const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); - - const [quickRanges] = useUiSetting$<Range[]>(DEFAULT_TIMEPICKER_QUICK_RANGES); - const commonlyUsedRanges = isEmpty(quickRanges) - ? [] - : quickRanges.map(({ from, to, display }) => ({ - start: from, - end: to, - label: display, - })); - - return ( - <EuiSuperDatePicker - commonlyUsedRanges={commonlyUsedRanges} - end={endDate} - isLoading={isLoading} - isPaused={policy === 'manual'} - onRefresh={onRefresh} - onRefreshChange={onRefreshChange} - onTimeChange={onTimeChange} - recentlyUsedRanges={recentlyUsedRanges} - refreshInterval={duration} - showUpdateButton={true} - start={startDate} - /> - ); - } -); - -export const formatDate = ( - date: string, - options?: { - roundUp?: boolean; - } -) => { - const momentDate = dateMath.parse(date, options); - return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; -}; - -export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ - end, - id, - isQuickSelection, - kql, - start, - timelineId, -}: UpdateReduxTime): ReturnUpdateReduxTime => { - const fromDate = formatDate(start); - let toDate = formatDate(end, { roundUp: true }); - if (isQuickSelection) { - dispatch( - inputsActions.setRelativeRangeDatePicker({ - id, - fromStr: start, - toStr: end, - from: fromDate, - to: toDate, - }) - ); - } else { - toDate = formatDate(end); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - id, - from: formatDate(start), - to: formatDate(end), - }) - ); - } - if (timelineId != null) { - dispatch( - timelineActions.updateRange({ - id: timelineId, - start: fromDate, - end: toDate, - }) - ); - } - if (kql) { - return { - kqlHaveBeenUpdated: kql.refetch(dispatch), - }; - } - - return { - kqlHaveBeenUpdated: false, - }; -}; - -export const makeMapStateToProps = () => { - const getDurationSelector = durationSelector(); - const getEndSelector = endSelector(); - const getFromStrSelector = fromStrSelector(); - const getIsLoadingSelector = isLoadingSelector(); - const getKindSelector = kindSelector(); - const getKqlQuerySelector = kqlQuerySelector(); - const getPolicySelector = policySelector(); - const getQueriesSelector = queriesSelector(); - const getStartSelector = startSelector(); - const getToStrSelector = toStrSelector(); - return (state: State, { id }: OwnProps) => { - const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); - return { - duration: getDurationSelector(inputsRange), - end: getEndSelector(inputsRange), - fromStr: getFromStrSelector(inputsRange), - isLoading: getIsLoadingSelector(inputsRange), - kind: getKindSelector(inputsRange), - kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery, - policy: getPolicySelector(inputsRange), - queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[], - start: getStartSelector(inputsRange), - toStr: getToStrSelector(inputsRange), - }; - }; -}; - -SuperDatePickerComponent.displayName = 'SuperDatePickerComponent'; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - startAutoReload: ({ id }: { id: InputsModelId }) => - dispatch(inputsActions.startAutoReload({ id })), - stopAutoReload: ({ id }: { id: InputsModelId }) => dispatch(inputsActions.stopAutoReload({ id })), - setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => - dispatch(inputsActions.setDuration({ id, duration })), - updateReduxTime: dispatchUpdateReduxTime(dispatch), -}); - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const SuperDatePicker = connector(SuperDatePickerComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx deleted file mode 100644 index c43c69da64ba4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; - -import { mockIndexPattern } from '../../../mock'; -import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; -import { TestProviders } from '../../../mock/test_providers'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; -import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; - -import { TimelineHeader } from '.'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -jest.mock('../../../lib/kibana'); - -describe('Header', () => { - const indexPattern = mockIndexPattern; - const mount = useMountAppended(); - - describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - <TimelineHeader - browserFields={{}} - dataProviders={mockDataProviders} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - id="foo" - indexPattern={indexPattern} - onChangeDataProviderKqlQuery={jest.fn()} - onChangeDroppableAndProvider={jest.fn()} - onDataProviderEdited={jest.fn()} - onDataProviderRemoved={jest.fn()} - onToggleDataProviderEnabled={jest.fn()} - onToggleDataProviderExcluded={jest.fn()} - show={true} - showCallOutUnauthorizedMsg={false} - /> - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the data providers', () => { - const wrapper = mount( - <TestProviders> - <TimelineHeader - browserFields={{}} - dataProviders={mockDataProviders} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - id="foo" - indexPattern={indexPattern} - onChangeDataProviderKqlQuery={jest.fn()} - onChangeDroppableAndProvider={jest.fn()} - onDataProviderEdited={jest.fn()} - onDataProviderRemoved={jest.fn()} - onToggleDataProviderEnabled={jest.fn()} - onToggleDataProviderExcluded={jest.fn()} - show={true} - showCallOutUnauthorizedMsg={false} - /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); - }); - - test('it renders the unauthorized call out providers', () => { - const wrapper = mount( - <TestProviders> - <TimelineHeader - browserFields={{}} - dataProviders={mockDataProviders} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - id="foo" - indexPattern={indexPattern} - onChangeDataProviderKqlQuery={jest.fn()} - onChangeDroppableAndProvider={jest.fn()} - onDataProviderEdited={jest.fn()} - onDataProviderRemoved={jest.fn()} - onToggleDataProviderEnabled={jest.fn()} - onToggleDataProviderExcluded={jest.fn()} - show={true} - showCallOutUnauthorizedMsg={true} - /> - </TestProviders> - ); - - expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx deleted file mode 100644 index 9fd71f071ec60..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.test.tsx +++ /dev/null @@ -1,398 +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 { cloneDeep } from 'lodash/fp'; -import { mockIndexPattern } from '../../mock'; - -import { mockDataProviders } from './data_providers/mock/mock_data_providers'; -import { buildGlobalQuery, combineQueries } from './helpers'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { EsQueryConfig, Filter, esFilters } from '../../../../../../../src/plugins/data/public'; - -const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); -const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); - -describe('Build KQL Query', () => { - test('Build KQL query with one data provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); - }); - - test('Build KQL query with one data provider as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Buld KQL query with one data provider as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Buld KQL query with one data provider as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider', () => { - const dataProviders = mockDataProviders.slice(0, 2); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); - }); - - test('Build KQL query with one data provider and one and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = mockDataProviders.slice(1, 2); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); - }); - - test('Build KQL query with one data provider and one and as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider and multiple and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' - ); - }); -}); - -describe('Combined Queries', () => { - const config: EsQueryConfig = { - allowLeadingWildcards: true, - queryStringOptions: {}, - ignoreFilterIfFieldNotInIndex: true, - dateFormatTZ: 'America/New_York', - }; - test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - }) - ).toBeNull(); - }); - - test('No Data Provider & No kqlQuery & isEventViewer is true', () => { - const isEventViewer = true; - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - isEventViewer, - }) - ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', - }); - }); - - test('No Data Provider & No kqlQuery & with Filters', () => { - const isEventViewer = true; - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [ - { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { query: 'file' }, - type: 'phrase', - }, - query: { match_phrase: { 'event.category': 'file' } }, - }, - { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'host.name', - negate: false, - type: 'exists', - value: 'exists', - }, - exists: { field: 'host.name' }, - } as Filter, - ], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - isEventViewer, - }) - ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', - }); - }); - - test('Only Data Provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with a date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only KQL search/filter query', () => { - const { filterQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL filter query', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL filter query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx deleted file mode 100644 index f051bbe5b1af6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/helpers.tsx +++ /dev/null @@ -1,160 +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 { isEmpty, isNumber, get } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; - -import { escapeQueryValue, convertToBuildEsQuery } from '../../lib/keury'; - -import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider'; -import { BrowserFields } from '../../containers/source'; -import { - IIndexPattern, - Query, - EsQueryConfig, - Filter, -} from '../../../../../../../src/plugins/data/public'; - -const convertDateFieldToQuery = (field: string, value: string | number) => - `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; - -const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { - const baseFields = get('base', browserFields); - if (baseFields != null && baseFields.fields != null) { - return Object.keys(baseFields.fields); - } - return []; -}); - -const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { - const splitFields = field.split('.'); - const baseFields = getBaseFields(browserFields); - if (baseFields.includes(field)) { - return ['base', 'fields', field]; - } - return [splitFields[0], 'fields', field]; -}; - -const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { - const pathBrowserField = getBrowserFieldPath(field, browserFields); - const browserField = get(pathBrowserField, browserFields); - if (browserField != null && browserField.type === 'date') { - return true; - } - return false; -}; - -const buildQueryMatch = ( - dataProvider: DataProvider | DataProvidersAnd, - browserFields: BrowserFields -) => - `${dataProvider.excluded ? 'NOT ' : ''}${ - dataProvider.queryMatch.operator !== EXISTS_OPERATOR - ? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) - ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) - : `${dataProvider.queryMatch.field} : ${ - isNumber(dataProvider.queryMatch.value) - ? dataProvider.queryMatch.value - : escapeQueryValue(dataProvider.queryMatch.value) - }` - : `${dataProvider.queryMatch.field} ${EXISTS_OPERATOR}` - }`.trim(); - -const buildQueryForAndProvider = ( - dataAndProviders: DataProvidersAnd[], - browserFields: BrowserFields -) => - dataAndProviders - .reduce((andQuery, andDataProvider) => { - const prepend = (q: string) => `${q !== '' ? `${q} and ` : ''}`; - return andDataProvider.enabled - ? `${prepend(andQuery)} ${buildQueryMatch(andDataProvider, browserFields)}` - : andQuery; - }, '') - .trim(); - -export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => - dataProviders - .reduce((query, dataProvider: DataProvider, i) => { - const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; - const openParen = i > 0 ? '(' : ''; - const closeParen = i > 0 ? ')' : ''; - return dataProvider.enabled - ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} - ${ - dataProvider.and.length > 0 - ? ` and ${buildQueryForAndProvider(dataProvider.and, browserFields)}` - : '' - }${closeParen}`.trim() - : query; - }, '') - .trim(); - -export const combineQueries = ({ - config, - dataProviders, - indexPattern, - browserFields, - filters = [], - kqlQuery, - kqlMode, - start, - end, - isEventViewer, -}: { - config: EsQueryConfig; - dataProviders: DataProvider[]; - indexPattern: IIndexPattern; - browserFields: BrowserFields; - filters: Filter[]; - kqlQuery: Query; - kqlMode: string; - start: number; - end: number; - isEventViewer?: boolean; -}): { filterQuery: string } | null => { - const kuery: Query = { query: '', language: kqlQuery.language }; - if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { - return null; - } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { - kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { - kuery.query = `(${buildGlobalQuery( - dataProviders, - browserFields - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } - const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; - const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; - kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( - kqlQuery.query as string - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; -}; - -/** - * The CSS class name of a "stateful event", which appears in both - * the `Timeline` and the `Events Viewer` widget - */ -export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx deleted file mode 100644 index e63bce388ae80..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx +++ /dev/null @@ -1,61 +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 React from 'react'; -import { mount } from 'enzyme'; -/* eslint-disable @kbn/eslint/module_migration */ -import routeData from 'react-router'; -/* eslint-enable @kbn/eslint/module_migration */ -import { InsertTimelinePopoverComponent } from './'; - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => ({ - useDispatch: () => mockDispatch, -})); -const mockLocation = { - pathname: '/apath', - hash: '', - search: '', - state: '', -}; -const mockLocationWithState = { - ...mockLocation, - state: { - insertTimeline: { - timelineId: 'timeline-id', - timelineSavedObjectId: '34578-3497-5893-47589-34759', - timelineTitle: 'Timeline title', - }, - }, -}; - -const onTimelineChange = jest.fn(); -const defaultProps = { - isDisabled: false, - onTimelineChange, -}; - -describe('Insert timeline popover ', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should insert a timeline when passed in the router state', () => { - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocationWithState); - mount(<InsertTimelinePopoverComponent {...defaultProps} />); - expect(mockDispatch).toBeCalledWith({ - payload: { id: 'timeline-id', show: false }, - type: 'x-pack/siem/local/timeline/SHOW_TIMELINE', - }); - expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); - }); - it('should do nothing when router state', () => { - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - mount(<InsertTimelinePopoverComponent {...defaultProps} />); - expect(mockDispatch).toHaveBeenCalledTimes(0); - expect(onTimelineChange).toHaveBeenCalledTimes(0); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx deleted file mode 100644 index 6f7e1f782d3f6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/properties/helpers.tsx +++ /dev/null @@ -1,327 +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 { - EuiBadge, - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiModal, - EuiOverlayMask, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; - -import { Note } from '../../../lib/note'; -import { Notes } from '../../notes'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; -import { NOTES_PANEL_WIDTH } from './notes_size'; -import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; -import * as i18n from './translations'; -import { SiemPageName } from '../../../pages/home/types'; -import { timelineSelectors } from '../../../store/timeline'; -import { State } from '../../../store'; - -export const historyToolTip = 'The chronological history of actions related to this timeline'; -export const streamLiveToolTip = 'Update the Timeline as new data arrives'; -export const newTimelineToolTip = 'Create a new timeline'; - -const NotesCountBadge = styled(EuiBadge)` - margin-left: 5px; -`; - -NotesCountBadge.displayName = 'NotesCountBadge'; - -type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; - -export const StarIcon = React.memo<{ - isFavorite: boolean; - timelineId: string; - updateIsFavorite: UpdateIsFavorite; -}>(({ isFavorite, timelineId: id, updateIsFavorite }) => ( - // TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener - // TODO: 2 error is: Elements with the 'button' interactive role must be focusable - // TODO: Investigate this error - // eslint-disable-next-line - <div role="button" onClick={() => updateIsFavorite({ id, isFavorite: !isFavorite })}> - {isFavorite ? ( - <EuiToolTip data-test-subj="timeline-favorite-filled-star-tool-tip" content={i18n.FAVORITE}> - <StyledStar data-test-subj="timeline-favorite-filled-star" type="starFilled" size="l" /> - </EuiToolTip> - ) : ( - <EuiToolTip content={i18n.NOT_A_FAVORITE}> - <StyledStar data-test-subj="timeline-favorite-empty-star" type="starEmpty" size="l" /> - </EuiToolTip> - )} - </div> -)); -StarIcon.displayName = 'StarIcon'; - -interface DescriptionProps { - description: string; - timelineId: string; - updateDescription: UpdateDescription; -} - -export const Description = React.memo<DescriptionProps>( - ({ description, timelineId, updateDescription }) => ( - <EuiToolTip data-test-subj="timeline-description-tool-tip" content={i18n.DESCRIPTION_TOOL_TIP}> - <DescriptionContainer data-test-subj="description-container"> - <EuiFieldText - aria-label={i18n.TIMELINE_DESCRIPTION} - data-test-subj="timeline-description" - fullWidth={true} - onChange={e => updateDescription({ id: timelineId, description: e.target.value })} - placeholder={i18n.DESCRIPTION} - spellCheck={true} - value={description} - /> - </DescriptionContainer> - </EuiToolTip> - ) -); -Description.displayName = 'Description'; - -interface NameProps { - timelineId: string; - title: string; - updateTitle: UpdateTitle; -} - -export const Name = React.memo<NameProps>(({ timelineId, title, updateTitle }) => ( - <EuiToolTip data-test-subj="timeline-title-tool-tip" content={i18n.TITLE}> - <NameField - aria-label={i18n.TIMELINE_TITLE} - data-test-subj="timeline-title" - onChange={e => updateTitle({ id: timelineId, title: e.target.value })} - placeholder={i18n.UNTITLED_TIMELINE} - spellCheck={true} - value={title} - /> - </EuiToolTip> -)); -Name.displayName = 'Name'; - -interface NewCaseProps { - onClosePopover: () => void; - timelineId: string; - timelineTitle: string; -} - -export const NewCase = React.memo<NewCaseProps>(({ onClosePopover, timelineId, timelineTitle }) => { - const history = useHistory(); - const { savedObjectId } = useSelector((state: State) => - timelineSelectors.selectTimeline(state, timelineId) - ); - const handleClick = useCallback(() => { - onClosePopover(); - history.push({ - pathname: `/${SiemPageName.case}/create`, - state: { - insertTimeline: { - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, - }, - }, - }); - }, [onClosePopover, history, timelineId, timelineTitle]); - - return ( - <EuiButtonEmpty - data-test-subj="attach-timeline-case" - color="text" - iconSide="left" - iconType="paperClip" - onClick={handleClick} - > - {i18n.ATTACH_TIMELINE_TO_NEW_CASE} - </EuiButtonEmpty> - ); -}); -NewCase.displayName = 'NewCase'; - -interface NewTimelineProps { - createTimeline: CreateTimeline; - onClosePopover: () => void; - timelineId: string; -} - -export const NewTimeline = React.memo<NewTimelineProps>( - ({ createTimeline, onClosePopover, timelineId }) => { - const handleClick = useCallback(() => { - createTimeline({ id: timelineId, show: true }); - onClosePopover(); - }, [createTimeline, timelineId, onClosePopover]); - - return ( - <EuiButtonEmpty - data-test-subj="timeline-new" - color="text" - iconSide="left" - iconType="plusInCircle" - onClick={handleClick} - > - {i18n.NEW_TIMELINE} - </EuiButtonEmpty> - ); - } -); -NewTimeline.displayName = 'NewTimeline'; - -interface NotesButtonProps { - animate?: boolean; - associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - noteIds: string[]; - size: 's' | 'l'; - showNotes: boolean; - toggleShowNotes: () => void; - text?: string; - toolTip?: string; - updateNote: UpdateNote; -} - -const getNewNoteId = (): string => uuid.v4(); - -interface LargeNotesButtonProps { - noteIds: string[]; - text?: string; - toggleShowNotes: () => void; -} - -const LargeNotesButton = React.memo<LargeNotesButtonProps>(({ noteIds, text, toggleShowNotes }) => ( - <EuiButton - data-test-subj="timeline-notes-button-large" - onClick={() => toggleShowNotes()} - size="m" - > - <EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center"> - <EuiFlexItem grow={false}> - <EuiIcon color="subdued" size="m" type="editorComment" /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - {text && text.length ? <LabelText>{text}</LabelText> : null} - </EuiFlexItem> - <EuiFlexItem grow={false}> - <NotesCountBadge data-test-subj="timeline-notes-count" color="hollow"> - {noteIds.length} - </NotesCountBadge> - </EuiFlexItem> - </EuiFlexGroup> - </EuiButton> -)); -LargeNotesButton.displayName = 'LargeNotesButton'; - -interface SmallNotesButtonProps { - noteIds: string[]; - toggleShowNotes: () => void; -} - -const SmallNotesButton = React.memo<SmallNotesButtonProps>(({ noteIds, toggleShowNotes }) => ( - <EuiButtonIcon - aria-label={i18n.NOTES} - data-test-subj="timeline-notes-button-small" - iconType="editorComment" - onClick={() => toggleShowNotes()} - /> -)); -SmallNotesButton.displayName = 'SmallNotesButton'; - -/** - * The internal implementation of the `NotesButton` - */ -const NotesButtonComponent = React.memo<NotesButtonProps>( - ({ - animate = true, - associateNote, - getNotesByIds, - noteIds, - showNotes, - size, - toggleShowNotes, - text, - updateNote, - }) => ( - <ButtonContainer animate={animate} data-test-subj="timeline-notes-button-container"> - <> - {size === 'l' ? ( - <LargeNotesButton noteIds={noteIds} text={text} toggleShowNotes={toggleShowNotes} /> - ) : ( - <SmallNotesButton noteIds={noteIds} toggleShowNotes={toggleShowNotes} /> - )} - {size === 'l' && showNotes ? ( - <EuiOverlayMask> - <EuiModal maxWidth={NOTES_PANEL_WIDTH} onClose={toggleShowNotes}> - <Notes - associateNote={associateNote} - getNotesByIds={getNotesByIds} - noteIds={noteIds} - getNewNoteId={getNewNoteId} - updateNote={updateNote} - /> - </EuiModal> - </EuiOverlayMask> - ) : null} - </> - </ButtonContainer> - ) -); -NotesButtonComponent.displayName = 'NotesButtonComponent'; - -export const NotesButton = React.memo<NotesButtonProps>( - ({ - animate = true, - associateNote, - getNotesByIds, - noteIds, - showNotes, - size, - toggleShowNotes, - toolTip, - text, - updateNote, - }) => - showNotes ? ( - <NotesButtonComponent - animate={animate} - associateNote={associateNote} - getNotesByIds={getNotesByIds} - noteIds={noteIds} - showNotes={showNotes} - size={size} - toggleShowNotes={toggleShowNotes} - text={text} - updateNote={updateNote} - /> - ) : ( - <EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip"> - <NotesButtonComponent - animate={animate} - associateNote={associateNote} - getNotesByIds={getNotesByIds} - noteIds={noteIds} - showNotes={showNotes} - size={size} - toggleShowNotes={toggleShowNotes} - text={text} - updateNote={updateNote} - /> - </EuiToolTip> - ) -); -NotesButton.displayName = 'NotesButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx deleted file mode 100644 index a49f6cc930abd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.test.tsx +++ /dev/null @@ -1,409 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../../../plugins/siem/common/constants'; -import { mockBrowserFields } from '../../../containers/source/mock'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { mockIndexPattern, TestProviders } from '../../../mock'; -import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; -import { QueryBar } from '../../query_bar'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; -import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; -import { buildGlobalQuery } from '../helpers'; - -import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -jest.mock('../../../lib/kibana'); - -describe('Timeline QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - const mockApplyKqlFilterQuery = jest.fn(); - const mockSetFilters = jest.fn(); - const mockSetKqlFilterQueryDraft = jest.fn(); - const mockSetSavedQueryId = jest.fn(); - const mockUpdateReduxTime = jest.fn(); - - beforeEach(() => { - mockApplyKqlFilterQuery.mockClear(); - mockSetFilters.mockClear(); - mockSetKqlFilterQueryDraft.mockClear(); - mockSetSavedQueryId.mockClear(); - mockUpdateReduxTime.mockClear(); - }); - - test('check if we format the appropriate props to QueryBar', () => { - const wrapper = mount( - <TestProviders> - <QueryBarTimeline - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - </TestProviders> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - - expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); - expect(queryBarProps.dateRangeTo).toEqual('now'); - expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); - expect(queryBarProps.savedQuery).toEqual(null); - }); - - describe('#onChangeQuery', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - }); - - describe('#onSubmitQuery', () => { - test(' is the only reference that changed when filterQuery props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - - test(' is only reference that changed when timelineId props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ timelineId: 'new-timeline' }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - }); - - describe('#onSavedQuery', () => { - test('is only reference that changed when dataProviders props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - }); - - test('is only reference that changed when savedQueryId props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - <TestProviders> - <QueryBarTimeline {...props} /> - </TestProviders> - ); - - const wrapper = mount( - <Proxy - applyKqlFilterQuery={mockApplyKqlFilterQuery} - browserFields={mockBrowserFields} - dataProviders={mockDataProviders} - filters={[]} - filterManager={new FilterManager(mockUiSettingsForFilterManager)} - filterQuery={{ expression: 'here: query', kind: 'kuery' }} - filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} - from={0} - fromStr={DEFAULT_FROM} - to={1} - toStr={DEFAULT_TO} - kqlMode="search" - indexPattern={mockIndexPattern} - isRefreshPaused={true} - refreshInterval={3000} - savedQueryId={null} - setFilters={mockSetFilters} - setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} - setSavedQueryId={mockSetSavedQueryId} - timelineId="timeline-real-id" - updateReduxTime={mockUpdateReduxTime} - /> - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ - savedQueryId: 'new', - }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - }); - }); - - describe('#getDataProviderFilter', () => { - test('returns valid data provider filter with a simple bool data provider', () => { - const dataProvidersDsl = convertKueryToElasticSearchQuery( - buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), - mockIndexPattern - ); - const filter = getDataProviderFilter(dataProvidersDsl); - expect(filter).toEqual({ - $state: { - store: 'appState', - }, - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - name: 'Provider 1', - }, - }, - ], - }, - meta: { - alias: 'timeline-filter-drop-area', - controlledBy: 'timeline-filter-drop-area', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', - }, - }); - }); - - test('returns valid data provider filter with an exists operator', () => { - const dataProvidersDsl = convertKueryToElasticSearchQuery( - buildGlobalQuery( - [ - { - id: `id-exists`, - name, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: '', - operator: ':*', - }, - and: [], - }, - ], - mockBrowserFields - ), - mockIndexPattern - ); - const filter = getDataProviderFilter(dataProvidersDsl); - expect(filter).toEqual({ - $state: { - store: 'appState', - }, - bool: { - minimum_should_match: 1, - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - }, - meta: { - alias: 'timeline-filter-drop-area', - controlledBy: 'timeline-filter-drop-area', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', - }, - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx deleted file mode 100644 index f53f7bb56e2f4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/query_bar/index.tsx +++ /dev/null @@ -1,319 +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 { isEmpty } from 'lodash/fp'; -import React, { memo, useCallback, useState, useEffect } from 'react'; -import { Subscription } from 'rxjs'; -import deepEqual from 'fast-deep-equal'; - -import { - IIndexPattern, - Query, - Filter, - esFilters, - FilterManager, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../../../../../src/plugins/data/public'; - -import { BrowserFields } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; -import { KqlMode } from '../../../store/timeline/model'; -import { useSavedQueryServices } from '../../../utils/saved_query_services'; -import { DispatchUpdateReduxTime } from '../../super_date_picker'; -import { QueryBar } from '../../query_bar'; -import { DataProvider } from '../data_providers/data_provider'; -import { buildGlobalQuery } from '../helpers'; - -export interface QueryBarTimelineComponentProps { - applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; - browserFields: BrowserFields; - dataProviders: DataProvider[]; - filters: Filter[]; - filterManager: FilterManager; - filterQuery: KueryFilterQuery; - filterQueryDraft: KueryFilterQuery; - from: number; - fromStr: string; - kqlMode: KqlMode; - indexPattern: IIndexPattern; - isRefreshPaused: boolean; - refreshInterval: number; - savedQueryId: string | null; - setFilters: (filters: Filter[]) => void; - setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; - setSavedQueryId: (savedQueryId: string | null) => void; - timelineId: string; - to: number; - toStr: string; - updateReduxTime: DispatchUpdateReduxTime; -} - -const timelineFilterDropArea = 'timeline-filter-drop-area'; - -export const QueryBarTimeline = memo<QueryBarTimelineComponentProps>( - ({ - applyKqlFilterQuery, - browserFields, - dataProviders, - filters, - filterManager, - filterQuery, - filterQueryDraft, - from, - fromStr, - kqlMode, - indexPattern, - isRefreshPaused, - savedQueryId, - setFilters, - setKqlFilterQueryDraft, - setSavedQueryId, - refreshInterval, - timelineId, - to, - toStr, - updateReduxTime, - }) => { - const [dateRangeFrom, setDateRangeFrom] = useState<string>( - fromStr != null ? fromStr : new Date(from).toISOString() - ); - const [dateRangeTo, setDateRangTo] = useState<string>( - toStr != null ? toStr : new Date(to).toISOString() - ); - - const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); - const [filterQueryConverted, setFilterQueryConverted] = useState<Query>({ - query: filterQuery != null ? filterQuery.expression : '', - language: filterQuery != null ? filterQuery.kind : 'kuery', - }); - const [queryBarFilters, setQueryBarFilters] = useState<Filter[]>([]); - const [dataProvidersDsl, setDataProvidersDsl] = useState<string>( - convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) - ); - const savedQueryServices = useSavedQueryServices(); - - useEffect(() => { - let isSubscribed = true; - const subscriptions = new Subscription(); - filterManager.setFilters(filters); - - subscriptions.add( - filterManager.getUpdates$().subscribe({ - next: () => { - if (isSubscribed) { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - setFilters(filterWithoutDropArea); - setQueryBarFilters(filterWithoutDropArea); - } - }, - }) - ); - - return () => { - isSubscribed = false; - subscriptions.unsubscribe(); - }; - }, []); - - useEffect(() => { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - if (!deepEqual(filters, filterWithoutDropArea)) { - filterManager.setFilters(filters); - } - }, [filters]); - - useEffect(() => { - setFilterQueryConverted({ - query: filterQuery != null ? filterQuery.expression : '', - language: filterQuery != null ? filterQuery.kind : 'kuery', - }); - }, [filterQuery]); - - useEffect(() => { - setDataProvidersDsl( - convertKueryToElasticSearchQuery( - buildGlobalQuery(dataProviders, browserFields), - indexPattern - ) - ); - }, [dataProviders, browserFields, indexPattern]); - - useEffect(() => { - if (fromStr != null && toStr != null) { - setDateRangeFrom(fromStr); - setDateRangTo(toStr); - } else if (from != null && to != null) { - setDateRangeFrom(new Date(from).toISOString()); - setDateRangTo(new Date(to).toISOString()); - } - }, [from, fromStr, to, toStr]); - - useEffect(() => { - let isSubscribed = true; - async function setSavedQueryByServices() { - if (savedQueryId != null && savedQueryServices != null) { - try { - // The getSavedQuery function will throw a promise rejection in - // src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts - // if the savedObjectsClient is undefined. This is happening in a test - // so I wrapped this in a try catch to keep the unhandled promise rejection - // warning from appearing in tests. - const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); - if (isSubscribed && mySavedQuery != null) { - setSavedQuery({ - ...mySavedQuery, - attributes: { - ...mySavedQuery.attributes, - filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), - }, - }); - } - } catch (exc) { - setSavedQuery(null); - } - } else if (isSubscribed) { - setSavedQuery(null); - } - } - setSavedQueryByServices(); - return () => { - isSubscribed = false; - }; - }, [savedQueryId]); - - const onChangedQuery = useCallback( - (newQuery: Query) => { - if ( - filterQueryDraft == null || - (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || - filterQueryDraft.kind !== newQuery.language - ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); - } - }, - [filterQueryDraft] - ); - - const onSubmitQuery = useCallback( - (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { - if ( - filterQuery == null || - (filterQuery != null && filterQuery.expression !== newQuery.query) || - filterQuery.kind !== newQuery.language - ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); - applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); - } - if (timefilter != null) { - const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); - - updateReduxTime({ - id: 'timeline', - end: timefilter.to, - start: timefilter.from, - isInvalid: false, - isQuickSelection, - timelineId, - }); - } - }, - [filterQuery, timelineId] - ); - - const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { - if (newSavedQuery != null) { - if (newSavedQuery.id !== savedQueryId) { - setSavedQueryId(newSavedQuery.id); - } - if (savedQueryServices != null && dataProvidersDsl !== '') { - const dataProviderFilterExists = - newSavedQuery.attributes.filters != null - ? newSavedQuery.attributes.filters.findIndex( - f => f.meta.controlledBy === timelineFilterDropArea - ) - : -1; - savedQueryServices.saveQuery( - { - ...newSavedQuery.attributes, - filters: - newSavedQuery.attributes.filters != null - ? dataProviderFilterExists > -1 - ? [ - ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), - getDataProviderFilter(dataProvidersDsl), - ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), - ] - : [ - ...newSavedQuery.attributes.filters, - getDataProviderFilter(dataProvidersDsl), - ] - : [], - }, - { - overwrite: true, - } - ); - } - } else { - setSavedQueryId(null); - } - }, - [dataProvidersDsl, savedQueryId, savedQueryServices] - ); - - return ( - <QueryBar - dateRangeFrom={dateRangeFrom} - dateRangeTo={dateRangeTo} - hideSavedQuery={kqlMode === 'search'} - indexPattern={indexPattern} - isRefreshPaused={isRefreshPaused} - filterQuery={filterQueryConverted} - filterManager={filterManager} - filters={queryBarFilters} - onChangedQuery={onChangedQuery} - onSubmitQuery={onSubmitQuery} - refreshInterval={refreshInterval} - savedQuery={savedQuery} - onSavedQuery={onSavedQuery} - dataTestSubj={'timelineQueryInput'} - /> - ); - } -); - -export const getDataProviderFilter = (dataProviderDsl: string): Filter => { - const dslObject = JSON.parse(dataProviderDsl); - const key = Object.keys(dslObject); - return { - ...dslObject, - meta: { - alias: timelineFilterDropArea, - controlledBy: timelineFilterDropArea, - negate: false, - disabled: false, - type: 'custom', - key: isEmpty(key) ? 'bool' : key[0], - value: dataProviderDsl, - }, - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - }; -}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx deleted file mode 100644 index a0a0ac4c2b85c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ /dev/null @@ -1,243 +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 { getOr } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - FilterManager, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/public'; -import { BrowserFields } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { - KueryFilterQuery, - SerializedFilterQuery, - State, - timelineSelectors, - inputsModel, - inputsSelectors, -} from '../../../store'; -import { timelineActions } from '../../../store/actions'; -import { KqlMode, TimelineModel, EventType } from '../../../store/timeline/model'; -import { timelineDefaults } from '../../../store/timeline/defaults'; -import { dispatchUpdateReduxTime } from '../../super_date_picker'; -import { SearchOrFilter } from './search_or_filter'; - -interface OwnProps { - browserFields: BrowserFields; - filterManager: FilterManager; - indexPattern: IIndexPattern; - timelineId: string; -} - -type Props = OwnProps & PropsFromRedux; - -const StatefulSearchOrFilterComponent = React.memo<Props>( - ({ - applyKqlFilterQuery, - browserFields, - dataProviders, - eventType, - filters, - filterManager, - filterQuery, - filterQueryDraft, - from, - fromStr, - indexPattern, - isRefreshPaused, - kqlMode, - refreshInterval, - savedQueryId, - setFilters, - setKqlFilterQueryDraft, - setSavedQueryId, - timelineId, - to, - toStr, - updateEventType, - updateKqlMode, - updateReduxTime, - }) => { - const applyFilterQueryFromKueryExpression = useCallback( - (expression: string, kind) => - applyKqlFilterQuery({ - id: timelineId, - filterQuery: { - kuery: { - kind, - expression, - }, - serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), - }, - }), - [indexPattern, timelineId] - ); - - const setFilterQueryDraftFromKueryExpression = useCallback( - (expression: string, kind) => - setKqlFilterQueryDraft({ - id: timelineId, - filterQueryDraft: { - kind, - expression, - }, - }), - [timelineId] - ); - - const setFiltersInTimeline = useCallback( - (newFilters: Filter[]) => - setFilters({ - id: timelineId, - filters: newFilters, - }), - [timelineId] - ); - - const setSavedQueryInTimeline = useCallback( - (newSavedQueryId: string | null) => - setSavedQueryId({ - id: timelineId, - savedQueryId: newSavedQueryId, - }), - [timelineId] - ); - - const handleUpdateEventType = useCallback( - (newEventType: EventType) => - updateEventType({ - id: timelineId, - eventType: newEventType, - }), - [timelineId] - ); - - return ( - <SearchOrFilter - applyKqlFilterQuery={applyFilterQueryFromKueryExpression} - browserFields={browserFields} - dataProviders={dataProviders} - eventType={eventType} - filters={filters} - filterManager={filterManager} - filterQuery={filterQuery} - filterQueryDraft={filterQueryDraft} - from={from} - fromStr={fromStr} - indexPattern={indexPattern} - isRefreshPaused={isRefreshPaused} - kqlMode={kqlMode!} - refreshInterval={refreshInterval} - savedQueryId={savedQueryId} - setFilters={setFiltersInTimeline} - setKqlFilterQueryDraft={setFilterQueryDraftFromKueryExpression!} - setSavedQueryId={setSavedQueryInTimeline} - timelineId={timelineId} - to={to} - toStr={toStr} - updateEventType={handleUpdateEventType} - updateKqlMode={updateKqlMode!} - updateReduxTime={updateReduxTime} - /> - ); - }, - (prevProps, nextProps) => { - return ( - prevProps.eventType === nextProps.eventType && - prevProps.filterManager === nextProps.filterManager && - prevProps.from === nextProps.from && - prevProps.fromStr === nextProps.fromStr && - prevProps.to === nextProps.to && - prevProps.toStr === nextProps.toStr && - prevProps.isRefreshPaused === nextProps.isRefreshPaused && - prevProps.refreshInterval === nextProps.refreshInterval && - prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.browserFields, nextProps.browserFields) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - deepEqual(prevProps.filters, nextProps.filters) && - deepEqual(prevProps.filterQuery, nextProps.filterQuery) && - deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && - deepEqual(prevProps.indexPattern, nextProps.indexPattern) && - deepEqual(prevProps.kqlMode, nextProps.kqlMode) && - deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && - deepEqual(prevProps.timelineId, nextProps.timelineId) - ); - } -); -StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); - const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); - const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; - const input: inputsModel.InputsRange = getInputsTimeline(state); - const policy: inputsModel.Policy = getInputsPolicy(state); - return { - dataProviders: timeline.dataProviders, - eventType: timeline.eventType ?? 'raw', - filterQuery: getKqlFilterQuery(state, timelineId)!, - filterQueryDraft: getKqlFilterQueryDraft(state, timelineId)!, - filters: timeline.filters!, - from: input.timerange.from, - fromStr: input.timerange.fromStr!, - isRefreshPaused: policy.kind === 'manual', - kqlMode: getOr('filter', 'kqlMode', timeline), - refreshInterval: policy.duration, - savedQueryId: getOr(null, 'savedQueryId', timeline), - to: input.timerange.to, - toStr: input.timerange.toStr!, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => - dispatch( - timelineActions.applyKqlFilterQuery({ - id, - filterQuery, - }) - ), - updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => - dispatch(timelineActions.updateEventType({ id, eventType })), - updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => - dispatch(timelineActions.updateKqlMode({ id, kqlMode })), - setKqlFilterQueryDraft: ({ - id, - filterQueryDraft, - }: { - id: string; - filterQueryDraft: KueryFilterQuery; - }) => - dispatch( - timelineActions.setKqlFilterQueryDraft({ - id, - filterQueryDraft, - }) - ), - setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => - dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), - setFilters: ({ id, filters }: { id: string; filters: Filter[] }) => - dispatch(timelineActions.setFilters({ id, filters })), - updateReduxTime: dispatchUpdateReduxTime(dispatch), -}); - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const StatefulSearchOrFilter = connector(StatefulSearchOrFilterComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx deleted file mode 100644 index 49e3bc4017a10..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ /dev/null @@ -1,276 +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 { - EuiSelectable, - EuiHighlight, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiTextColor, - EuiSelectableOption, - EuiPortal, - EuiFilterGroup, - EuiFilterButton, -} from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { memo, useCallback, useMemo, useState } from 'react'; -import { ListProps } from 'react-virtualized'; -import styled from 'styled-components'; - -import { AllTimelinesQuery } from '../../../containers/timeline/all'; -import { SortFieldTimeline, Direction } from '../../../graphql/types'; -import { isUntitled } from '../../open_timeline/helpers'; -import * as i18nTimeline from '../../open_timeline/translations'; -import { OpenTimelineResult } from '../../open_timeline/types'; -import { getEmptyTagValue } from '../../empty_value'; - -import * as i18n from '../translations'; - -const MyEuiFlexItem = styled(EuiFlexItem)` - display: inline-block; - max-width: 296px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const MyEuiFlexGroup = styled(EuiFlexGroup)` - padding 0px 4px; -`; - -const EuiSelectableContainer = styled.div<{ isLoading: boolean }>` - .euiSelectable { - .euiFormControlLayout__childrenWrapper { - display: flex; - } - ${({ isLoading }) => `${ - isLoading - ? ` - .euiFormControlLayoutIcons { - display: none; - } - .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { - display: block; - left: 12px; - top: 12px; - }` - : '' - } - `} - } -`; - -const ORIGINAL_PAGE_SIZE = 50; -const POPOVER_HEIGHT = 260; -const TIMELINE_ITEM_HEIGHT = 50; - -export interface GetSelectableOptions { - timelines: OpenTimelineResult[]; - onlyFavorites: boolean; - searchTimelineValue: string; -} - -interface SelectableTimelineProps { - hideUntitled?: boolean; - getSelectableOptions: ({ - timelines, - onlyFavorites, - searchTimelineValue, - }: GetSelectableOptions) => EuiSelectableOption[]; - onClosePopover: () => void; - onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; -} - -const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({ - hideUntitled = false, - getSelectableOptions, - onClosePopover, - onTimelineChange, -}) => { - const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); - const [heightTrigger, setHeightTrigger] = useState(0); - const [searchTimelineValue, setSearchTimelineValue] = useState(''); - const [onlyFavorites, setOnlyFavorites] = useState(false); - const [searchRef, setSearchRef] = useState<HTMLElement | null>(null); - - const onSearchTimeline = useCallback(val => { - setSearchTimelineValue(val); - }, []); - - const handleOnToggleOnlyFavorites = useCallback(() => { - setOnlyFavorites(!onlyFavorites); - }, [onlyFavorites]); - - const handleOnScroll = useCallback( - ( - totalTimelines: number, - totalCount: number, - { - clientHeight, - scrollHeight, - scrollTop, - }: { - clientHeight: number; - scrollHeight: number; - scrollTop: number; - } - ) => { - if (totalTimelines < totalCount) { - const clientHeightTrigger = clientHeight * 1.2; - if ( - scrollTop > 10 && - scrollHeight - scrollTop < clientHeightTrigger && - scrollHeight > heightTrigger - ) { - setHeightTrigger(scrollHeight); - setPageSize(pageSize + ORIGINAL_PAGE_SIZE); - } - } - }, - [heightTrigger, pageSize] - ); - - const renderTimelineOption = useCallback((option, searchValue) => { - return ( - <EuiFlexGroup - gutterSize="s" - justifyContent="spaceBetween" - alignItems="center" - responsive={false} - > - <EuiFlexItem grow={false}> - <EuiIcon type={`${option.checked === 'on' ? 'check' : 'none'}`} color="primary" /> - </EuiFlexItem> - <EuiFlexItem grow={true}> - <EuiFlexGroup gutterSize="none" direction="column"> - <MyEuiFlexItem grow={false}> - <EuiHighlight search={searchValue}> - {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} - </EuiHighlight> - </MyEuiFlexItem> - <MyEuiFlexItem grow={false}> - <EuiTextColor color="subdued" component="span"> - <small> - {option.description != null && option.description.trim().length > 0 - ? option.description - : getEmptyTagValue()} - </small> - </EuiTextColor> - </MyEuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiIcon - type={`${ - option.favorite != null && isEmpty(option.favorite) ? 'starEmpty' : 'starFilled' - }`} - /> - </EuiFlexItem> - </EuiFlexGroup> - ); - }, []); - - const handleTimelineChange = useCallback( - options => { - const selectedTimeline = options.filter( - (option: { checked: string }) => option.checked === 'on' - ); - if (selectedTimeline != null && selectedTimeline.length > 0) { - onTimelineChange( - isEmpty(selectedTimeline[0].title) - ? i18nTimeline.UNTITLED_TIMELINE - : selectedTimeline[0].title, - selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id - ); - } - onClosePopover(); - }, - [onClosePopover, onTimelineChange] - ); - - const favoritePortal = useMemo( - () => - searchRef != null ? ( - <EuiPortal insert={{ sibling: searchRef, position: 'after' }}> - <MyEuiFlexGroup gutterSize="xs" justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiFilterGroup> - <EuiFilterButton - size="l" - data-test-subj="only-favorites-toggle" - hasActiveFilters={onlyFavorites} - onClick={handleOnToggleOnlyFavorites} - > - {i18nTimeline.ONLY_FAVORITES} - </EuiFilterButton> - </EuiFilterGroup> - </EuiFlexItem> - </MyEuiFlexGroup> - </EuiPortal> - ) : null, - [searchRef, onlyFavorites, handleOnToggleOnlyFavorites] - ); - - return ( - <> - <AllTimelinesQuery - pageInfo={{ - pageIndex: 1, - pageSize, - }} - search={searchTimelineValue} - sort={{ sortField: SortFieldTimeline.updated, sortOrder: Direction.desc }} - onlyUserFavorite={onlyFavorites} - > - {({ timelines, loading, totalCount }) => ( - <EuiSelectableContainer isLoading={loading}> - <EuiSelectable - height={POPOVER_HEIGHT} - isLoading={loading && timelines.length === 0} - listProps={{ - rowHeight: TIMELINE_ITEM_HEIGHT, - showIcons: false, - virtualizedProps: ({ - onScroll: handleOnScroll.bind( - null, - timelines.filter(t => !hideUntitled || t.title !== '').length, - totalCount - ), - } as unknown) as ListProps, - }} - renderOption={renderTimelineOption} - onChange={handleTimelineChange} - searchable - searchProps={{ - 'data-test-subj': 'timeline-super-select-search-box', - isLoading: loading, - placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER, - onSearch: onSearchTimeline, - incremental: false, - inputRef: (ref: HTMLElement) => { - setSearchRef(ref); - }, - }} - singleSelection={true} - options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })} - > - {(list, search) => ( - <> - {search} - {favoritePortal} - {list} - </> - )} - </EuiSelectable> - </EuiSelectableContainer> - )} - </AllTimelinesQuery> - </> - ); -}; - -export const SelectableTimeline = memo(SelectableTimelineComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx deleted file mode 100644 index 61772f1dc7a7a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/top_n/index.test.tsx +++ /dev/null @@ -1,379 +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 { mount, ReactWrapper } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../containers/source/mock'; -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { createStore, State } from '../../store'; -import { TimelineContext, TimelineTypeContext } from '../timeline/timeline_context'; - -import { Props } from './top_n'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; - -jest.mock('../../lib/kibana'); - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -const field = 'process.name'; -const value = 'nice'; - -const state: State = { - ...mockGlobalState, - inputs: { - ...mockGlobalState.inputs, - global: { - ...mockGlobalState.inputs.global, - query: { - query: 'host.name : end*', - language: 'kuery', - }, - filters: [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.os.name', - params: { - query: 'Linux', - }, - }, - query: { - match: { - 'host.os.name': { - query: 'Linux', - type: 'phrase', - }, - }, - }, - }, - ], - }, - timeline: { - ...mockGlobalState.inputs.timeline, - timerange: { - kind: 'relative', - fromStr: 'now-24h', - toStr: 'now', - from: 1586835969047, - to: 1586922369047, - }, - }, - }, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - [ACTIVE_TIMELINE_REDUX_ID]: { - ...mockGlobalState.timeline.timelineById.test, - id: ACTIVE_TIMELINE_REDUX_ID, - dataProviders: [ - { - id: - 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', - name: 'tcp', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'network.transport', - value: 'tcp', - operator: ':', - }, - and: [], - }, - ], - eventType: 'all', - filters: [ - { - meta: { - alias: null, - disabled: false, - key: 'source.port', - negate: false, - params: { - query: '30045', - }, - type: 'phrase', - }, - query: { - match: { - 'source.port': { - query: '30045', - type: 'phrase', - }, - }, - }, - }, - ], - kqlMode: 'filter', - kqlQuery: { - filterQuery: { - kuery: { - kind: 'kuery', - expression: 'host.name : *', - }, - serializedQuery: - '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', - }, - filterQueryDraft: { - kind: 'kuery', - expression: 'host.name : *', - }, - }, - }, - }, - }, -}; -const store = createStore(state, apolloClientObservable); - -describe('StatefulTopN', () => { - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - - describe('rendering in a global NON-timeline context', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - <TestProviders store={store}> - <StatefulTopN - browserFields={mockBrowserFields} - field={field} - toggleTopN={jest.fn()} - onFilterAdded={jest.fn()} - value={value} - /> - </TestProviders> - ); - }); - - test('it has undefined combinedQueries when rendering in a global context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.combinedQueries).toBeUndefined(); - }); - - test(`defaults to the 'Raw events' view when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('raw'); - }); - - test(`provides a 'deleteQuery' when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.deleteQuery).toBeDefined(); - }); - - test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.filters).toEqual([ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.os.name', - params: { query: 'Linux' }, - }, - query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, - }, - ]); - }); - - test(`provides 'from' via GlobalTime when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.from).toEqual(0); - }); - - test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); - }); - - test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); - }); - - test(`provides 'to' via GlobalTime when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.to).toEqual(1); - }); - }); - - describe('rendering in a timeline context', () => { - let filterManager: FilterManager; - let wrapper: ReactWrapper; - - beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - - wrapper = mount( - <TestProviders store={store}> - <TimelineContext.Provider value={{ filterManager, isLoading: false }}> - <TimelineTypeContext.Provider value={{ id: ACTIVE_TIMELINE_REDUX_ID }}> - <StatefulTopN - browserFields={mockBrowserFields} - field={field} - toggleTopN={jest.fn()} - onFilterAdded={jest.fn()} - value={value} - /> - </TimelineTypeContext.Provider> - </TimelineContext.Provider> - </TestProviders> - ); - }); - - test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.combinedQueries).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' - ); - }); - - test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('all'); - }); - - test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.deleteQuery).toBeUndefined(); - }); - - test(`provides empty filters when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.filters).toEqual([]); - }); - - test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.from).toEqual(1586835969047); - }); - - test('provides an empty query when rendering in a timeline context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.query).toEqual({ query: '', language: 'kuery' }); - }); - - test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); - }); - - test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.to).toEqual(1586922369047); - }); - }); - - test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { - const filterManager = new FilterManager(mockUiSettingsForFilterManager); - const wrapper = mount( - <TestProviders store={store}> - <TimelineContext.Provider value={{ filterManager, isLoading: false }}> - <TimelineTypeContext.Provider - value={{ documentType: 'signals', id: ACTIVE_TIMELINE_REDUX_ID }} - > - <StatefulTopN - browserFields={mockBrowserFields} - field={field} - toggleTopN={jest.fn()} - onFilterAdded={jest.fn()} - value={value} - /> - </TimelineTypeContext.Provider> - </TimelineContext.Provider> - </TestProviders> - ); - - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('signal'); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx b/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx deleted file mode 100644 index 983b234a04eaa..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/top_n/index.tsx +++ /dev/null @@ -1,166 +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 React from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { GlobalTime } from '../../containers/global_time'; -import { BrowserFields, WithSource } from '../../containers/source'; -import { useKibana } from '../../lib/kibana'; -import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/public'; -import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { TimelineModel } from '../../store/timeline/model'; -import { combineQueries } from '../timeline/helpers'; -import { useTimelineTypeContext } from '../timeline/timeline_context'; - -import { getOptions } from './helpers'; -import { TopN } from './top_n'; - -/** The currently active timeline always has this Redux ID */ -export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; - -const EMPTY_FILTERS: Filter[] = []; -const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); - - // The mapped Redux state provided to this component includes the global - // filters that appear at the top of most views in the app, and all the - // filters in the active timeline: - const mapStateToProps = (state: State) => { - const activeTimeline: TimelineModel = - getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; - const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; - const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); - - return { - activeTimelineEventType: activeTimeline.eventType, - activeTimelineFilters, - activeTimelineFrom: activeTimelineInput.timerange.from, - activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), - activeTimelineTo: activeTimelineInput.timerange.to, - dataProviders: activeTimeline.dataProviders, - globalQuery: getGlobalQuerySelector(state), - globalFilters: getGlobalFiltersQuerySelector(state), - kqlMode: activeTimeline.kqlMode, - }; - }; - - return mapStateToProps; -}; - -const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -interface OwnProps { - browserFields: BrowserFields; - field: string; - toggleTopN: () => void; - onFilterAdded?: () => void; - value?: string[] | string | null; -} -type PropsFromRedux = ConnectedProps<typeof connector>; -type Props = OwnProps & PropsFromRedux; - -const StatefulTopNComponent: React.FC<Props> = ({ - activeTimelineEventType, - activeTimelineFilters, - activeTimelineFrom, - activeTimelineKqlQueryExpression, - activeTimelineTo, - browserFields, - dataProviders, - field, - globalFilters = EMPTY_FILTERS, - globalQuery = EMPTY_QUERY, - kqlMode, - onFilterAdded, - setAbsoluteRangeDatePicker, - toggleTopN, - value, -}) => { - const kibana = useKibana(); - - // Regarding data from useTimelineTypeContext: - // * `documentType` (e.g. 'signals') may only be populated in some views, - // e.g. the `Signals` view on the `Detections` page. - // * `id` (`timelineId`) may only be populated when we are rendered in the - // context of the active timeline. - // * `indexToAdd`, which enables the signals index to be appended to - // the `indexPattern` returned by `WithSource`, may only be populated when - // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the signals index - // to the index pattern. - const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); - - const options = getOptions( - timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined - ); - - return ( - <GlobalTime> - {({ from, deleteQuery, setQuery, to }) => ( - <WithSource sourceId="default" indexToAdd={indexToAdd}> - {({ indexPattern }) => ( - <TopN - combinedQueries={ - timelineId === ACTIVE_TIMELINE_REDUX_ID - ? combineQueries({ - browserFields, - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders, - end: activeTimelineTo, - filters: activeTimelineFilters, - indexPattern, - kqlMode, - kqlQuery: { - language: 'kuery', - query: activeTimelineKqlQueryExpression ?? '', - }, - start: activeTimelineFrom, - })?.filterQuery - : undefined - } - data-test-subj="top-n" - defaultView={ - documentType?.toLocaleLowerCase() === 'signals' ? 'signal' : options[0].value - } - deleteQuery={timelineId === ACTIVE_TIMELINE_REDUX_ID ? undefined : deleteQuery} - field={field} - filters={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_FILTERS : globalFilters} - from={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineFrom : from} - indexPattern={indexPattern} - indexToAdd={indexToAdd} - options={options} - query={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_QUERY : globalQuery} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} - setAbsoluteRangeDatePickerTarget={ - timelineId === ACTIVE_TIMELINE_REDUX_ID ? 'timeline' : 'global' - } - setQuery={setQuery} - to={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineTo : to} - toggleTopN={toggleTopN} - onFilterAdded={onFilterAdded} - value={value} - /> - )} - </WithSource> - )} - </GlobalTime> - ); -}; - -StatefulTopNComponent.displayName = 'StatefulTopNComponent'; - -export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts deleted file mode 100644 index b30244e57d0f1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ /dev/null @@ -1,260 +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 { isEmpty } from 'lodash/fp'; -import { parse, stringify } from 'query-string'; -import { decode, encode } from 'rison-node'; -import * as H from 'history'; - -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { url } from '../../../../../../../src/plugins/kibana_utils/public'; - -import { SiemPageName } from '../../pages/home/types'; -import { inputsSelectors, State, timelineSelectors } from '../../store'; -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { formatDate } from '../super_date_picker'; -import { NavTab } from '../navigation/types'; -import { CONSTANTS, UrlStateType } from './constants'; -import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; - -export const decodeRisonUrlState = <T>(value: string | undefined): T | null => { - try { - return value ? ((decode(value) as unknown) as T) : null; - } catch (error) { - if (error instanceof Error && error.message.startsWith('rison decoder error')) { - return null; - } - throw error; - } -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const encodeRisonUrlState = (state: any) => encode(state); - -export const getQueryStringFromLocation = (search: string) => search.substring(1); - -export const getParamFromQueryString = (queryString: string, key: string) => { - const parsedQueryString = parse(queryString, { sort: false }); - const queryParam = parsedQueryString[key]; - - return Array.isArray(queryParam) ? queryParam[0] : queryParam; -}; - -export const replaceStateKeyInQueryString = <T>(stateKey: string, urlState: T) => ( - queryString: string -): string => { - const previousQueryValues = parse(queryString, { sort: false }); - if (urlState == null || (typeof urlState === 'string' && urlState === '')) { - delete previousQueryValues[stateKey]; - - return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); - } - - // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ - // Remove this if these utilities are promoted to kibana core - const encodedUrlState = - typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - - return stringify( - url.encodeQuery({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }), - { sort: false, encode: false } - ); -}; - -export const replaceQueryStringInLocation = ( - location: H.Location, - queryString: string -): H.Location => { - if (queryString === getQueryStringFromLocation(location.search)) { - return location; - } else { - return { - ...location, - search: `?${queryString}`, - }; - } -}; - -export const getUrlType = (pageName: string): UrlStateType => { - if (pageName === SiemPageName.overview) { - return 'overview'; - } else if (pageName === SiemPageName.hosts) { - return 'host'; - } else if (pageName === SiemPageName.network) { - return 'network'; - } else if (pageName === SiemPageName.detections) { - return 'detections'; - } else if (pageName === SiemPageName.timelines) { - return 'timeline'; - } else if (pageName === SiemPageName.case) { - return 'case'; - } - return 'overview'; -}; - -export const getTitle = ( - pageName: string, - detailName: string | undefined, - navTabs: Record<string, NavTab> -): string => { - if (detailName != null) return detailName; - return navTabs[pageName] != null ? navTabs[pageName].name : ''; -}; - -export const makeMapStateToProps = () => { - const getInputsSelector = inputsSelectors.inputsSelector(); - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); - const getTimelines = timelineSelectors.getTimelines(); - const mapStateToProps = (state: State) => { - const inputState = getInputsSelector(state); - const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; - const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - - const timeline = Object.entries(getTimelines(state)).reduce( - (obj, [timelineId, timelineObj]) => ({ - id: timelineObj.savedObjectId != null ? timelineObj.savedObjectId : '', - isOpen: timelineObj.show, - }), - { id: '', isOpen: false } - ); - - let searchAttr: { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - } = { - [CONSTANTS.appQuery]: getGlobalQuerySelector(state), - [CONSTANTS.filters]: getGlobalFiltersQuerySelector(state), - }; - const savedQuery = getGlobalSavedQuerySelector(state); - if (savedQuery != null && savedQuery.id !== '') { - searchAttr = { - [CONSTANTS.savedQuery]: savedQuery.id, - }; - } - - return { - urlState: { - ...searchAttr, - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: globalTimerange, - linkTo: globalLinkTo, - }, - timeline: { - [CONSTANTS.timerange]: timelineTimerange, - linkTo: timelineLinkTo, - }, - }, - [CONSTANTS.timeline]: timeline, - }, - }; - }; - - return mapStateToProps; -}; - -export const updateTimerangeUrl = ( - timeRange: UrlInputsModel, - isInitializing: boolean -): UrlInputsModel => { - if (timeRange.global.timerange.kind === 'relative') { - timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); - timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); - } - if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { - timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); - timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { - roundUp: true, - }); - } - return timeRange; -}; - -export const updateUrlStateString = ({ - isInitializing, - history, - newUrlStateString, - pathName, - search, - updateTimerange, - urlKey, -}: UpdateUrlStateString): string => { - if (urlKey === CONSTANTS.appQuery) { - const queryState = decodeRisonUrlState<Query>(newUrlStateString); - if (queryState != null && queryState.query === '') { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.timerange && updateTimerange) { - const queryState = decodeRisonUrlState<UrlInputsModel>(newUrlStateString); - if (queryState != null && queryState.global != null) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.filters) { - const queryState = decodeRisonUrlState<Filter[]>(newUrlStateString); - if (isEmpty(queryState)) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.timeline) { - const queryState = decodeRisonUrlState<TimelineUrl>(newUrlStateString); - if (queryState != null && queryState.id === '') { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } - return search; -}; - -export const replaceStateInLocation = <T>({ - history, - urlStateToReplace, - urlStateKey, - pathName, - search, -}: ReplaceStateInLocation<T>) => { - const newLocation = replaceQueryStringInLocation( - { - hash: '', - pathname: pathName, - search, - state: '', - }, - replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) - ); - if (history) { - history.replace(newLocation); - } - return newLocation.search; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx deleted file mode 100644 index 83c38f2a76175..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx +++ /dev/null @@ -1,77 +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 React, { useEffect } from 'react'; - -import { DEFAULT_ANOMALY_SCORE } from '../../../../../../../plugins/siem/common/constants'; -import { AnomaliesQueryTabBodyProps } from './types'; -import { getAnomaliesFilterQuery } from './utils'; -import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { histogramConfigs } from './histogram_configs'; -const ID = 'anomaliesOverTimeQuery'; - -export const AnomaliesQueryTabBody = ({ - deleteQuery, - endDate, - setQuery, - skip, - startDate, - type, - narrowDateRange, - filterQuery, - anomaliesFilterQuery, - AnomaliesTableComponent, - flowTarget, - ip, -}: AnomaliesQueryTabBodyProps) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - - const [, siemJobs] = useSiemJobs(true); - const [anomalyScore] = useUiSetting$<number>(DEFAULT_ANOMALY_SCORE); - - const mergedFilterQuery = getAnomaliesFilterQuery( - filterQuery, - anomaliesFilterQuery, - siemJobs, - anomalyScore, - flowTarget, - ip - ); - - return ( - <> - <MatrixHistogramContainer - endDate={endDate} - filterQuery={mergedFilterQuery} - id={ID} - setQuery={setQuery} - sourceId="default" - startDate={startDate} - type={type} - {...histogramConfigs} - /> - <AnomaliesTableComponent - startDate={startDate} - endDate={endDate} - skip={skip} - type={type as never} - narrowDateRange={narrowDateRange} - flowTarget={flowTarget} - ip={ip} - /> - </> - ); -}; - -AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts deleted file mode 100644 index d17eadc68d04b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts +++ /dev/null @@ -1,35 +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 { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { NarrowDateRange } from '../../../components/ml/types'; -import { UpdateDateRange } from '../../../components/charts/common'; -import { SetQuery } from '../../../pages/hosts/navigation/types'; -import { FlowTarget } from '../../../graphql/types'; -import { HostsType } from '../../../store/hosts/model'; -import { NetworkType } from '../../../store/network/model'; -import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; - -interface QueryTabBodyProps { - type: HostsType | NetworkType; - filterQuery?: string | ESTermQuery; -} - -export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { - anomaliesFilterQuery?: object; - AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; - deleteQuery?: ({ id }: { id: string }) => void; - endDate: number; - flowTarget?: FlowTarget; - narrowDateRange: NarrowDateRange; - setQuery: SetQuery; - startDate: number; - skip: boolean; - updateDateRange?: UpdateDateRange; - hideHistogramIfEmpty?: boolean; - ip?: string; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts deleted file mode 100644 index f698e302d3423..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts +++ /dev/null @@ -1,69 +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 deepmerge from 'deepmerge'; - -import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { createFilter } from '../../helpers'; -import { SiemJob } from '../../../components/ml_popover/types'; -import { FlowTarget } from '../../../graphql/types'; - -export const getAnomaliesFilterQuery = ( - filterQuery: string | ESTermQuery | undefined, - anomaliesFilterQuery: object = {}, - siemJobs: SiemJob[] = [], - anomalyScore: number, - flowTarget?: FlowTarget, - ip?: string -): string => { - const siemJobIds = siemJobs - .filter(job => job.isInstalled) - .map(job => job.id) - .map(jobId => ({ - match_phrase: { - job_id: jobId, - }, - })); - - const filterQueryString = createFilter(filterQuery); - const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; - const mergedFilterQuery = deepmerge.all([ - filterQueryObject, - anomaliesFilterQuery, - { - bool: { - filter: [ - { - bool: { - should: siemJobIds, - minimum_should_match: 1, - }, - }, - { - match_phrase: { - result_type: 'record', - }, - }, - flowTarget && - ip && { - match_phrase: { - [`${flowTarget}.ip`]: ip, - }, - }, - { - range: { - record_score: { - gte: anomalyScore, - }, - }, - }, - ], - }, - }, - ]); - - return JSON.stringify(mergedFilterQuery); -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx b/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx deleted file mode 100644 index 13bb40dad04bd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx +++ /dev/null @@ -1,150 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - AuthenticationsEdges, - GetAuthenticationsQuery, - PageInfoPaginated, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { authenticationsQuery } from './index.gql_query'; - -const ID = 'authenticationQuery'; - -export interface AuthenticationArgs { - authentications: AuthenticationsEdges[]; - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: AuthenticationArgs) => React.ReactNode; - type: hostsModel.HostsType; -} - -export interface AuthenticationsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; -} - -type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; - -class AuthenticationsComponentQuery extends QueryTemplatePaginated< - AuthenticationsProps, - GetAuthenticationsQuery.Query, - GetAuthenticationsQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetAuthenticationsQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }; - return ( - <Query<GetAuthenticationsQuery.Query, GetAuthenticationsQuery.Variables> - query={authenticationsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const authentications = getOr([], 'source.Authentications.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Authentications: { - ...fetchMoreResult.source.Authentications, - edges: [...fetchMoreResult.source.Authentications.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - authentications, - id, - inspect: getOr(null, 'source.Authentications.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Authentications.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Authentications.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getAuthenticationsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -export const AuthenticationsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(AuthenticationsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts deleted file mode 100644 index 6d2cfb7147537..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/__mocks__/api.ts +++ /dev/null @@ -1,122 +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 { - ActionLicense, - AllCases, - BulkUpdateStatus, - Case, - CasesStatus, - CaseUserActions, - FetchCasesProps, - SortFieldCase, -} from '../types'; -import { - actionLicenses, - allCases, - basicCase, - basicCaseCommentPatch, - basicCasePost, - casesStatus, - caseUserActions, - pushedCase, - respReporters, - serviceConnector, - tags, -} from '../mock'; -import { - CaseExternalServiceRequest, - CasePatchRequest, - CasePostRequest, - CommentRequest, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, - User, -} from '../../../../../../../plugins/case/common/api'; - -export const getCase = async ( - caseId: string, - includeComments: boolean = true, - signal: AbortSignal -): Promise<Case> => { - return Promise.resolve(basicCase); -}; - -export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => - Promise.resolve(casesStatus); - -export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); - -export const getReporters = async (signal: AbortSignal): Promise<User[]> => - Promise.resolve(respReporters); - -export const getCaseUserActions = async ( - caseId: string, - signal: AbortSignal -): Promise<CaseUserActions[]> => Promise.resolve(caseUserActions); - -export const getCases = async ({ - filterOptions = { - search: '', - reporters: [], - status: 'open', - tags: [], - }, - queryParams = { - page: 1, - perPage: 5, - sortField: SortFieldCase.createdAt, - sortOrder: 'desc', - }, - signal, -}: FetchCasesProps): Promise<AllCases> => Promise.resolve(allCases); - -export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => - Promise.resolve(basicCasePost); - -export const patchCase = async ( - caseId: string, - updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, - version: string, - signal: AbortSignal -): Promise<Case[]> => Promise.resolve([basicCase]); - -export const patchCasesStatus = async ( - cases: BulkUpdateStatus[], - signal: AbortSignal -): Promise<Case[]> => Promise.resolve(allCases.cases); - -export const postComment = async ( - newComment: CommentRequest, - caseId: string, - signal: AbortSignal -): Promise<Case> => Promise.resolve(basicCase); - -export const patchComment = async ( - caseId: string, - commentId: string, - commentUpdate: string, - version: string, - signal: AbortSignal -): Promise<Case> => Promise.resolve(basicCaseCommentPatch); - -export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<boolean> => - Promise.resolve(true); - -export const pushCase = async ( - caseId: string, - push: CaseExternalServiceRequest, - signal: AbortSignal -): Promise<Case> => Promise.resolve(pushedCase); - -export const pushToService = async ( - connectorId: string, - casePushParams: ServiceConnectorCaseParams, - signal: AbortSignal -): Promise<ServiceConnectorCaseResponse> => Promise.resolve(serviceConnector); - -export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => - Promise.resolve(actionLicenses); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts deleted file mode 100644 index 12b4c80a2dd89..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ /dev/null @@ -1,259 +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 { - CaseResponse, - CasesResponse, - CasesFindResponse, - CasePatchRequest, - CasePostRequest, - CasesStatusResponse, - CommentRequest, - User, - CaseUserActionsResponse, - CaseExternalServiceRequest, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, - ActionTypeExecutorResult, -} from '../../../../../../plugins/case/common/api'; - -import { KibanaServices } from '../../lib/kibana'; - -import { - ActionLicense, - AllCases, - BulkUpdateStatus, - Case, - CasesStatus, - FetchCasesProps, - SortFieldCase, - CaseUserActions, -} from './types'; - -import { CASES_URL } from './constants'; - -import { - convertToCamelCase, - convertAllCasesToCamel, - convertArrayToCamelCase, - decodeCaseResponse, - decodeCasesResponse, - decodeCasesFindResponse, - decodeCasesStatusResponse, - decodeCaseUserActionsResponse, - decodeServiceConnectorCaseResponse, -} from './utils'; - -import * as i18n from './translations'; - -export const getCase = async ( - caseId: string, - includeComments: boolean = true, - signal: AbortSignal -): Promise<Case> => { - const response = await KibanaServices.get().http.fetch<CaseResponse>(`${CASES_URL}/${caseId}`, { - method: 'GET', - query: { - includeComments, - }, - signal, - }); - return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); -}; - -export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => { - const response = await KibanaServices.get().http.fetch<CasesStatusResponse>( - `${CASES_URL}/status`, - { - method: 'GET', - signal, - } - ); - return convertToCamelCase<CasesStatusResponse, CasesStatus>(decodeCasesStatusResponse(response)); -}; - -export const getTags = async (signal: AbortSignal): Promise<string[]> => { - const response = await KibanaServices.get().http.fetch<string[]>(`${CASES_URL}/tags`, { - method: 'GET', - signal, - }); - return response ?? []; -}; - -export const getReporters = async (signal: AbortSignal): Promise<User[]> => { - const response = await KibanaServices.get().http.fetch<User[]>(`${CASES_URL}/reporters`, { - method: 'GET', - signal, - }); - return response ?? []; -}; - -export const getCaseUserActions = async ( - caseId: string, - signal: AbortSignal -): Promise<CaseUserActions[]> => { - const response = await KibanaServices.get().http.fetch<CaseUserActionsResponse>( - `${CASES_URL}/${caseId}/user_actions`, - { - method: 'GET', - signal, - } - ); - return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; -}; - -export const getCases = async ({ - filterOptions = { - search: '', - reporters: [], - status: 'open', - tags: [], - }, - queryParams = { - page: 1, - perPage: 20, - sortField: SortFieldCase.createdAt, - sortOrder: 'desc', - }, - signal, -}: FetchCasesProps): Promise<AllCases> => { - const query = { - reporters: filterOptions.reporters.map(r => r.username ?? '').filter(r => r !== ''), - tags: filterOptions.tags, - ...(filterOptions.status !== '' ? { status: filterOptions.status } : {}), - ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), - ...queryParams, - }; - const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, { - method: 'GET', - query, - signal, - }); - return convertAllCasesToCamel(decodeCasesFindResponse(response)); -}; - -export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => { - const response = await KibanaServices.get().http.fetch<CaseResponse>(CASES_URL, { - method: 'POST', - body: JSON.stringify(newCase), - signal, - }); - return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); -}; - -export const patchCase = async ( - caseId: string, - updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, - version: string, - signal: AbortSignal -): Promise<Case[]> => { - const response = await KibanaServices.get().http.fetch<CasesResponse>(CASES_URL, { - method: 'PATCH', - body: JSON.stringify({ cases: [{ ...updatedCase, id: caseId, version }] }), - signal, - }); - return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response)); -}; - -export const patchCasesStatus = async ( - cases: BulkUpdateStatus[], - signal: AbortSignal -): Promise<Case[]> => { - const response = await KibanaServices.get().http.fetch<CasesResponse>(CASES_URL, { - method: 'PATCH', - body: JSON.stringify({ cases }), - signal, - }); - return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response)); -}; - -export const postComment = async ( - newComment: CommentRequest, - caseId: string, - signal: AbortSignal -): Promise<Case> => { - const response = await KibanaServices.get().http.fetch<CaseResponse>( - `${CASES_URL}/${caseId}/comments`, - { - method: 'POST', - body: JSON.stringify(newComment), - signal, - } - ); - return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); -}; - -export const patchComment = async ( - caseId: string, - commentId: string, - commentUpdate: string, - version: string, - signal: AbortSignal -): Promise<Case> => { - const response = await KibanaServices.get().http.fetch<CaseResponse>( - `${CASES_URL}/${caseId}/comments`, - { - method: 'PATCH', - body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), - signal, - } - ); - return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); -}; - -export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<string> => { - const response = await KibanaServices.get().http.fetch<string>(CASES_URL, { - method: 'DELETE', - query: { ids: JSON.stringify(caseIds) }, - signal, - }); - return response; -}; - -export const pushCase = async ( - caseId: string, - push: CaseExternalServiceRequest, - signal: AbortSignal -): Promise<Case> => { - const response = await KibanaServices.get().http.fetch<CaseResponse>( - `${CASES_URL}/${caseId}/_push`, - { - method: 'POST', - body: JSON.stringify(push), - signal, - } - ); - return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); -}; - -export const pushToService = async ( - connectorId: string, - casePushParams: ServiceConnectorCaseParams, - signal: AbortSignal -): Promise<ServiceConnectorCaseResponse> => { - const response = await KibanaServices.get().http.fetch<ActionTypeExecutorResult>( - `/api/action/${connectorId}/_execute`, - { - method: 'POST', - body: JSON.stringify({ params: casePushParams }), - signal, - } - ); - - if (response.status === 'error') { - throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); - } - - return decodeServiceConnectorCaseResponse(response.data); -}; - -export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => { - const response = await KibanaServices.get().http.fetch<ActionLicense[]>(`/api/action/types`, { - method: 'GET', - signal, - }); - return response; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts deleted file mode 100644 index 03f7d241e5dff..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/__mocks__/api.ts +++ /dev/null @@ -1,31 +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 { - CasesConfigurePatch, - CasesConfigureRequest, - Connector, -} from '../../../../../../../../plugins/case/common/api'; - -import { ApiProps } from '../../types'; -import { CaseConfigure } from '../types'; -import { connectorsMock, caseConfigurationCamelCaseResponseMock } from '../mock'; - -export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => - Promise.resolve(connectorsMock); - -export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure> => - Promise.resolve(caseConfigurationCamelCaseResponseMock); - -export const postCaseConfigure = async ( - caseConfiguration: CasesConfigureRequest, - signal: AbortSignal -): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); - -export const patchCaseConfigure = async ( - caseConfiguration: CasesConfigurePatch, - signal: AbortSignal -): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts deleted file mode 100644 index c24081c777a96..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.ts +++ /dev/null @@ -1,81 +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 { isEmpty } from 'lodash/fp'; -import { - Connector, - CasesConfigurePatch, - CasesConfigureResponse, - CasesConfigureRequest, -} from '../../../../../../../plugins/case/common/api'; -import { KibanaServices } from '../../../lib/kibana'; - -import { CASES_CONFIGURE_URL } from '../constants'; -import { ApiProps } from '../types'; -import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; -import { CaseConfigure } from './types'; - -export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => { - const response = await KibanaServices.get().http.fetch( - `${CASES_CONFIGURE_URL}/connectors/_find`, - { - method: 'GET', - signal, - } - ); - - return response; -}; - -export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure | null> => { - const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( - CASES_CONFIGURE_URL, - { - method: 'GET', - signal, - } - ); - - return !isEmpty(response) - ? convertToCamelCase<CasesConfigureResponse, CaseConfigure>( - decodeCaseConfigureResponse(response) - ) - : null; -}; - -export const postCaseConfigure = async ( - caseConfiguration: CasesConfigureRequest, - signal: AbortSignal -): Promise<CaseConfigure> => { - const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( - CASES_CONFIGURE_URL, - { - method: 'POST', - body: JSON.stringify(caseConfiguration), - signal, - } - ); - return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( - decodeCaseConfigureResponse(response) - ); -}; - -export const patchCaseConfigure = async ( - caseConfiguration: CasesConfigurePatch, - signal: AbortSignal -): Promise<CaseConfigure> => { - const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( - CASES_CONFIGURE_URL, - { - method: 'PATCH', - body: JSON.stringify(caseConfiguration), - signal, - } - ); - return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( - decodeCaseConfigureResponse(response) - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts deleted file mode 100644 index d2491b39fdf56..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/mock.ts +++ /dev/null @@ -1,99 +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 { - Connector, - CasesConfigureResponse, - CasesConfigureRequest, -} from '../../../../../../../plugins/case/common/api'; -import { CaseConfigure } from './types'; - -export const connectorsMock: Connector[] = [ - { - id: '123', - actionTypeId: '.servicenow', - name: 'My Connector', - config: { - apiUrl: 'https://instance1.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - isPreconfigured: true, - }, - { - id: '456', - actionTypeId: '.servicenow', - name: 'My Connector 2', - config: { - apiUrl: 'https://instance2.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - isPreconfigured: true, - }, -]; - -export const caseConfigurationResposeMock: CasesConfigureResponse = { - created_at: '2020-04-06T13:03:18.657Z', - created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, - connector_id: '123', - connector_name: 'My Connector', - closure_type: 'close-by-user', - updated_at: '2020-04-06T14:03:18.657Z', - updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, - version: 'WzHJ12', -}; - -export const caseConfigurationMock: CasesConfigureRequest = { - connector_id: '123', - connector_name: 'My Connector', - closure_type: 'close-by-user', -}; - -export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { - createdAt: '2020-04-06T13:03:18.657Z', - createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, - connectorId: '123', - connectorName: 'My Connector', - closureType: 'close-by-user', - updatedAt: '2020-04-06T14:03:18.657Z', - updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, - version: 'WzHJ12', -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts deleted file mode 100644 index d69c23fe02ec9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/types.ts +++ /dev/null @@ -1,38 +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 { ElasticUser } from '../types'; -import { - ActionType, - CasesConfigurationMaps, - CaseField, - ClosureType, - Connector, - ThirdPartyField, -} from '../../../../../../../plugins/case/common/api'; - -export { ActionType, CasesConfigurationMaps, CaseField, ClosureType, Connector, ThirdPartyField }; - -export interface CasesConfigurationMapping { - source: CaseField; - target: ThirdPartyField; - actionType: ActionType; -} - -export interface CaseConfigure { - createdAt: string; - createdBy: ElasticUser; - connectorId: string; - connectorName: string; - closureType: ClosureType; - updatedAt: string; - updatedBy: ElasticUser; - version: string; -} - -export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps { - actionType?: ActionType; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx deleted file mode 100644 index 3ee16e19eaf9f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.test.tsx +++ /dev/null @@ -1,299 +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 { renderHook, act } from '@testing-library/react-hooks'; -import { useCaseConfigure, ReturnUseCaseConfigure, PersistCaseConfigure } from './use_configure'; -import { caseConfigurationCamelCaseResponseMock } from './mock'; -import * as api from './api'; - -jest.mock('./api'); - -const configuration: PersistCaseConfigure = { - connectorId: '456', - connectorName: 'My Connector 2', - closureType: 'close-by-pushing', -}; - -describe('useConfigure', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - const args = { - setConnector: jest.fn(), - setClosureType: jest.fn(), - setCurrentConfiguration: jest.fn(), - }; - - test('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: true, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('fetch case configuration', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('fetch case configuration - setConnector', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(args.setConnector).toHaveBeenCalledWith('123', 'My Connector'); - }); - }); - - test('fetch case configuration - setClosureType', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(args.setClosureType).toHaveBeenCalledWith('close-by-user'); - }); - }); - - test('fetch case configuration - setCurrentConfiguration', async () => { - await act(async () => { - const { waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(args.setCurrentConfiguration).toHaveBeenCalledWith({ - connectorId: '123', - closureType: 'close-by-user', - }); - }); - }); - - test('fetch case configuration - only setConnector', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure({ setConnector: jest.fn() }) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('refetch case configuration', async () => { - const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - result.current.refetchCaseConfigure(); - expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); - }); - }); - - test('set isLoading to true when fetching case configuration', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - result.current.refetchCaseConfigure(); - - expect(result.current.loading).toBe(true); - }); - }); - - test('persist case configuration', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - expect(result.current).toEqual({ - loading: false, - persistLoading: true, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('save case configuration - postCaseConfigure', async () => { - // When there is no version, a configuration is created. Otherwise is updated. - const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); - spyOnGetCaseConfigure.mockImplementation(() => - Promise.resolve({ - ...caseConfigurationCamelCaseResponseMock, - version: '', - }) - ); - - const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); - spyOnPostCaseConfigure.mockImplementation(() => - Promise.resolve({ - ...caseConfigurationCamelCaseResponseMock, - ...configuration, - }) - ); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); - expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); - expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { - connectorId: '456', - closureType: 'close-by-pushing', - }); - }); - }); - - test('save case configuration - patchCaseConfigure', async () => { - const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); - spyOnPatchCaseConfigure.mockImplementation(() => - Promise.resolve({ - ...caseConfigurationCamelCaseResponseMock, - ...configuration, - }) - ); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(args.setConnector).toHaveBeenNthCalledWith(2, '456'); - expect(args.setClosureType).toHaveBeenNthCalledWith(2, 'close-by-pushing'); - expect(args.setCurrentConfiguration).toHaveBeenNthCalledWith(2, { - connectorId: '456', - closureType: 'close-by-pushing', - }); - }); - }); - - test('save case configuration - only setConnector', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure({ setConnector: jest.fn() }) - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('unhappy path - fetch case configuration', async () => { - const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); - spyOnGetCaseConfigure.mockImplementation(() => { - throw new Error('Something went wrong'); - }); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); - - test('unhappy path - persist case configuration', async () => { - const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); - spyOnPostCaseConfigure.mockImplementation(() => { - throw new Error('Something went wrong'); - }); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => - useCaseConfigure(args) - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - result.current.persistCaseConfigure(configuration); - - await waitForNextUpdate(); - - expect(result.current).toEqual({ - loading: false, - persistLoading: false, - refetchCaseConfigure: result.current.refetchCaseConfigure, - persistCaseConfigure: result.current.persistCaseConfigure, - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx deleted file mode 100644 index 1c03a09a8c2ea..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ /dev/null @@ -1,165 +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 { useState, useEffect, useCallback } from 'react'; -import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; - -import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; -import * as i18n from './translations'; -import { ClosureType } from './types'; -import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; - -export interface PersistCaseConfigure { - connectorId: string; - connectorName: string; - closureType: ClosureType; -} - -export interface ReturnUseCaseConfigure { - loading: boolean; - refetchCaseConfigure: () => void; - persistCaseConfigure: ({ - connectorId, - connectorName, - closureType, - }: PersistCaseConfigure) => unknown; - persistLoading: boolean; -} - -interface UseCaseConfigure { - setConnector: (newConnectorId: string, newConnectorName?: string) => void; - setClosureType?: (newClosureType: ClosureType) => void; - setCurrentConfiguration?: (configuration: CurrentConfiguration) => void; -} - -export const useCaseConfigure = ({ - setConnector, - setClosureType, - setCurrentConfiguration, -}: UseCaseConfigure): ReturnUseCaseConfigure => { - const [, dispatchToaster] = useStateToaster(); - const [loading, setLoading] = useState(true); - const [firstLoad, setFirstLoad] = useState(false); - const [persistLoading, setPersistLoading] = useState(false); - const [version, setVersion] = useState(''); - - const refetchCaseConfigure = useCallback(() => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchCaseConfiguration = async () => { - try { - setLoading(true); - const res = await getCaseConfigure({ signal: abortCtrl.signal }); - if (!didCancel) { - if (res != null) { - setConnector(res.connectorId, res.connectorName); - if (setClosureType != null) { - setClosureType(res.closureType); - } - setVersion(res.version); - - if (!firstLoad) { - setFirstLoad(true); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - connectorId: res.connectorId, - closureType: res.closureType, - }); - } - } - } - setLoading(false); - } - } catch (error) { - if (!didCancel) { - setLoading(false); - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - } - } - }; - - fetchCaseConfiguration(); - - return () => { - didCancel = true; - abortCtrl.abort(); - }; - }, []); - - const persistCaseConfigure = useCallback( - async ({ connectorId, connectorName, closureType }: PersistCaseConfigure) => { - let didCancel = false; - const abortCtrl = new AbortController(); - const saveCaseConfiguration = async () => { - try { - setPersistLoading(true); - const connectorObj = { - connector_id: connectorId, - connector_name: connectorName, - closure_type: closureType, - }; - const res = - version.length === 0 - ? await postCaseConfigure(connectorObj, abortCtrl.signal) - : await patchCaseConfigure( - { - ...connectorObj, - version, - }, - abortCtrl.signal - ); - if (!didCancel) { - setConnector(res.connectorId); - if (setClosureType) { - setClosureType(res.closureType); - } - setVersion(res.version); - if (setCurrentConfiguration != null) { - setCurrentConfiguration({ - connectorId: res.connectorId, - closureType: res.closureType, - }); - } - - displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); - setPersistLoading(false); - } - } catch (error) { - if (!didCancel) { - setPersistLoading(false); - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - } - } - }; - saveCaseConfiguration(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; - }, - [version] - ); - - useEffect(() => { - refetchCaseConfigure(); - }, []); - - return { - loading, - refetchCaseConfigure, - persistCaseConfigure, - persistLoading, - }; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts deleted file mode 100644 index ab8dc98db4f64..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ /dev/null @@ -1,10 +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. - */ - -export const CASES_URL = `/api/cases`; -export const CASES_CONFIGURE_URL = `/api/cases/configure`; -export const DEFAULT_TABLE_ACTIVE_PAGE = 1; -export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/mock.ts b/x-pack/legacy/plugins/siem/public/containers/case/mock.ts deleted file mode 100644 index 0bda75e5bc9e0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/mock.ts +++ /dev/null @@ -1,307 +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 { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; - -import { - CommentResponse, - ServiceConnectorCaseResponse, - Status, - UserAction, - UserActionField, - CaseResponse, - CasesStatusResponse, - CaseUserActionsResponse, - CasesResponse, - CasesFindResponse, -} from '../../../../../../plugins/case/common/api/cases'; -import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; - -export const basicCaseId = 'basic-case-id'; -const basicCommentId = 'basic-comment-id'; -const basicCreatedAt = '2020-02-19T23:06:33.798Z'; -const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; -const laterTime = '2020-02-28T15:02:57.995Z'; -export const elasticUser = { - fullName: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', -}; - -export const tags: string[] = ['coke', 'pepsi']; - -export const basicComment: Comment = { - comment: 'Solve this fast!', - id: basicCommentId, - createdAt: basicCreatedAt, - createdBy: elasticUser, - pushedAt: null, - pushedBy: null, - updatedAt: null, - updatedBy: null, - version: 'WzQ3LDFc', -}; - -export const basicCase: Case = { - closedAt: null, - closedBy: null, - id: basicCaseId, - comments: [basicComment], - createdAt: basicCreatedAt, - createdBy: elasticUser, - description: 'Security banana Issue', - externalService: null, - status: 'open', - tags, - title: 'Another horrible breach!!', - totalComment: 1, - updatedAt: basicUpdatedAt, - updatedBy: elasticUser, - version: 'WzQ3LDFd', -}; - -export const basicCasePost: Case = { - ...basicCase, - updatedAt: null, - updatedBy: null, -}; - -export const basicCommentPatch: Comment = { - ...basicComment, - updatedAt: basicUpdatedAt, - updatedBy: { - username: 'elastic', - }, -}; - -export const basicCaseCommentPatch = { - ...basicCase, - comments: [basicCommentPatch], -}; - -export const casesStatus: CasesStatus = { - countClosedCases: 130, - countOpenCases: 20, -}; - -const basicPush = { - connectorId: 'connector_id', - connectorName: 'connector name', - externalId: 'external_id', - externalTitle: 'external title', - externalUrl: 'basicPush.com', - pushedAt: basicUpdatedAt, - pushedBy: elasticUser, -}; - -export const pushedCase: Case = { - ...basicCase, - externalService: basicPush, -}; - -export const serviceConnector: ServiceConnectorCaseResponse = { - number: '123', - incidentId: '444', - pushedDate: basicUpdatedAt, - url: 'connector.com', - comments: [ - { - commentId: basicCommentId, - pushedDate: basicUpdatedAt, - }, - ], -}; - -const basicAction = { - actionAt: basicCreatedAt, - actionBy: elasticUser, - oldValue: null, - newValue: 'what a cool value', - caseId: basicCaseId, - commentId: null, -}; - -export const casePushParams = { - actionBy: elasticUser, - caseId: basicCaseId, - createdAt: basicCreatedAt, - createdBy: elasticUser, - incidentId: null, - title: 'what a cool value', - commentId: null, - updatedAt: basicCreatedAt, - updatedBy: elasticUser, - description: 'nice', -}; -export const actionTypeExecutorResult = { - actionId: 'string', - status: 'ok', - data: serviceConnector, -}; - -export const cases: Case[] = [ - basicCase, - { ...pushedCase, id: '1', totalComment: 0, comments: [] }, - { ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] }, - { ...basicCase, id: '3', totalComment: 0, comments: [] }, - { ...basicCase, id: '4', totalComment: 0, comments: [] }, -]; - -export const allCases: AllCases = { - cases, - page: 1, - perPage: 5, - total: 10, - ...casesStatus, -}; -export const actionLicenses: ActionLicense[] = [ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }, -]; - -// Snake case for mock api responses -export const elasticUserSnake = { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', -}; -export const basicCommentSnake: CommentResponse = { - ...basicComment, - comment: 'Solve this fast!', - id: basicCommentId, - created_at: basicCreatedAt, - created_by: elasticUserSnake, - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, -}; - -export const basicCaseSnake: CaseResponse = { - ...basicCase, - status: 'open' as Status, - closed_at: null, - closed_by: null, - comments: [basicCommentSnake], - created_at: basicCreatedAt, - created_by: elasticUserSnake, - external_service: null, - updated_at: basicUpdatedAt, - updated_by: elasticUserSnake, -}; - -export const casesStatusSnake: CasesStatusResponse = { - count_closed_cases: 130, - count_open_cases: 20, -}; - -export const pushSnake = { - connector_id: 'connector_id', - connector_name: 'connector name', - external_id: 'external_id', - external_title: 'external title', - external_url: 'basicPush.com', -}; -const basicPushSnake = { - ...pushSnake, - pushed_at: basicUpdatedAt, - pushed_by: elasticUserSnake, -}; -export const pushedCaseSnake = { - ...basicCaseSnake, - external_service: basicPushSnake, -}; - -export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; -export const respReporters = [ - { username: 'alexis', full_name: null, email: null }, - { username: 'kim', full_name: null, email: null }, - { username: 'maria', full_name: null, email: null }, - { username: 'steph', full_name: null, email: null }, -]; -export const casesSnake: CasesResponse = [ - basicCaseSnake, - { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] }, - { ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] }, - { ...basicCaseSnake, id: '3', totalComment: 0, comments: [] }, - { ...basicCaseSnake, id: '4', totalComment: 0, comments: [] }, -]; - -export const allCasesSnake: CasesFindResponse = { - cases: casesSnake, - page: 1, - per_page: 5, - total: 10, - ...casesStatusSnake, -}; - -const basicActionSnake = { - action_at: basicCreatedAt, - action_by: elasticUserSnake, - old_value: null, - new_value: 'what a cool value', - case_id: basicCaseId, - comment_id: null, -}; -export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ - ...basicActionSnake, - action_id: `${af[0]}-${a}`, - action_field: af, - action: a, - comment_id: af[0] === 'comment' ? basicCommentId : null, - new_value: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); - -export const caseUserActionsSnake: CaseUserActionsResponse = [ - getUserActionSnake(['description'], 'create'), - getUserActionSnake(['comment'], 'create'), - getUserActionSnake(['description'], 'update'), -]; - -// user actions - -export const getUserAction = (af: UserActionField, a: UserAction) => ({ - ...basicAction, - actionId: `${af[0]}-${a}`, - actionField: af, - action: a, - commentId: af[0] === 'comment' ? basicCommentId : null, - newValue: - a === 'push-to-service' && af[0] === 'pushed' - ? JSON.stringify(basicPushSnake) - : basicAction.newValue, -}); - -export const caseUserActions: CaseUserActions[] = [ - getUserAction(['description'], 'create'), - getUserAction(['comment'], 'create'), - getUserAction(['description'], 'update'), -]; - -// components tests -export const useGetCasesMockState: UseGetCasesState = { - data: allCases, - loading: [], - selectedCases: [], - isError: false, - queryParams: DEFAULT_QUERY_PARAMS, - filterOptions: DEFAULT_FILTER_OPTIONS, -}; - -export const basicCaseClosed: Case = { - ...basicCase, - closedAt: '2020-02-25T23:06:33.798Z', - closedBy: elasticUser, - status: 'closed', -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts deleted file mode 100644 index e552f22b55fa4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ /dev/null @@ -1,121 +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 { User, UserActionField, UserAction } from '../../../../../../plugins/case/common/api'; - -export interface Comment { - id: string; - createdAt: string; - createdBy: ElasticUser; - comment: string; - pushedAt: string | null; - pushedBy: string | null; - updatedAt: string | null; - updatedBy: ElasticUser | null; - version: string; -} -export interface CaseUserActions { - actionId: string; - actionField: UserActionField; - action: UserAction; - actionAt: string; - actionBy: ElasticUser; - caseId: string; - commentId: string | null; - newValue: string | null; - oldValue: string | null; -} - -export interface CaseExternalService { - pushedAt: string; - pushedBy: ElasticUser; - connectorId: string; - connectorName: string; - externalId: string; - externalTitle: string; - externalUrl: string; -} -export interface Case { - id: string; - closedAt: string | null; - closedBy: ElasticUser | null; - comments: Comment[]; - createdAt: string; - createdBy: ElasticUser; - description: string; - externalService: CaseExternalService | null; - status: string; - tags: string[]; - title: string; - totalComment: number; - updatedAt: string | null; - updatedBy: ElasticUser | null; - version: string; -} - -export interface QueryParams { - page: number; - perPage: number; - sortField: SortFieldCase; - sortOrder: 'asc' | 'desc'; -} - -export interface FilterOptions { - search: string; - status: string; - tags: string[]; - reporters: User[]; -} - -export interface CasesStatus { - countClosedCases: number | null; - countOpenCases: number | null; -} - -export interface AllCases extends CasesStatus { - cases: Case[]; - page: number; - perPage: number; - total: number; -} - -export enum SortFieldCase { - createdAt = 'createdAt', - closedAt = 'closedAt', -} - -export interface ElasticUser { - readonly email?: string | null; - readonly fullName?: string | null; - readonly username?: string | null; -} - -export interface FetchCasesProps extends ApiProps { - queryParams?: QueryParams; - filterOptions?: FilterOptions; -} - -export interface ApiProps { - signal: AbortSignal; -} - -export interface BulkUpdateStatus { - status: string; - id: string; - version: string; -} -export interface ActionLicense { - id: string; - name: string; - enabled: boolean; - enabledInConfig: boolean; - enabledInLicense: boolean; -} - -export interface DeleteCase { - id: string; - title?: string; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts b/x-pack/legacy/plugins/siem/public/containers/case/utils.ts deleted file mode 100644 index 1ec98bf5b5f1f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/case/utils.ts +++ /dev/null @@ -1,99 +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 { camelCase, isArray, isObject, set } from 'lodash'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { - CasesFindResponse, - CasesFindResponseRt, - CaseResponse, - CaseResponseRt, - CasesResponse, - CasesResponseRt, - CasesStatusResponseRt, - CasesStatusResponse, - throwErrors, - CasesConfigureResponse, - CaseConfigureResponseRt, - CaseUserActionsResponse, - CaseUserActionsResponseRt, - ServiceConnectorCaseResponseRt, - ServiceConnectorCaseResponse, -} from '../../../../../../plugins/case/common/api'; -import { ToasterError } from '../../components/toasters'; -import { AllCases, Case } from './types'; - -export const getTypedPayload = <T>(a: unknown): T => a as T; - -export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => - arrayOfSnakes.reduce((acc: unknown[], value) => { - if (isArray(value)) { - return [...acc, convertArrayToCamelCase(value)]; - } else if (isObject(value)) { - return [...acc, convertToCamelCase(value)]; - } else { - return [...acc, value]; - } - }, []); - -export const convertToCamelCase = <T, U extends {}>(snakeCase: T): U => - Object.entries(snakeCase).reduce((acc, [key, value]) => { - if (isArray(value)) { - set(acc, camelCase(key), convertArrayToCamelCase(value)); - } else if (isObject(value)) { - set(acc, camelCase(key), convertToCamelCase(value)); - } else { - set(acc, camelCase(key), value); - } - return acc; - }, {} as U); - -export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({ - cases: snakeCases.cases.map(snakeCase => convertToCamelCase<CaseResponse, Case>(snakeCase)), - countClosedCases: snakeCases.count_closed_cases, - countOpenCases: snakeCases.count_open_cases, - page: snakeCases.page, - perPage: snakeCases.per_page, - total: snakeCases.total, -}); - -export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => - pipe( - CasesStatusResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const createToasterPlainError = (message: string) => new ToasterError([message]); - -export const decodeCaseResponse = (respCase?: CaseResponse) => - pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCasesResponse = (respCase?: CasesResponse) => - pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => - pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => - pipe( - CaseConfigureResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) => - pipe( - CaseUserActionsResponseRt.decode(respUserActions), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => - pipe( - ServiceConnectorCaseResponseRt.decode(respPushCase), - fold(throwErrors(createToasterPlainError), identity) - ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts deleted file mode 100644 index 69f4c93a82e2c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ /dev/null @@ -1,331 +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 { - DETECTION_ENGINE_RULES_URL, - DETECTION_ENGINE_PREPACKAGED_URL, - DETECTION_ENGINE_RULES_STATUS_URL, - DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, - DETECTION_ENGINE_TAGS_URL, -} from '../../../../../../../plugins/siem/common/constants'; -import { - AddRulesProps, - DeleteRulesProps, - DuplicateRulesProps, - EnableRulesProps, - FetchRulesProps, - FetchRulesResponse, - NewRule, - Rule, - FetchRuleProps, - BasicFetchProps, - ImportDataProps, - ExportDocumentsProps, - RuleStatusResponse, - ImportDataResponse, - PrePackagedRulesStatusResponse, - BulkRuleResponse, -} from './types'; -import { KibanaServices } from '../../../lib/kibana'; -import * as i18n from '../../../pages/detection_engine/rules/translations'; - -/** - * Add provided Rule - * - * @param rule to add - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> => - KibanaServices.get().http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, { - method: rule.id != null ? 'PUT' : 'POST', - body: JSON.stringify(rule), - signal, - }); - -/** - * Fetches all rules from the Detection Engine API - * - * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) - * @param pagination desired pagination options (e.g. page/perPage) - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchRules = async ({ - filterOptions = { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: false, - showElasticRules: false, - tags: [], - }, - pagination = { - page: 1, - perPage: 20, - total: 0, - }, - signal, -}: FetchRulesProps): Promise<FetchRulesResponse> => { - const filters = [ - ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), - ...(filterOptions.showCustomRules - ? [`alert.attributes.tags: "__internal_immutable:false"`] - : []), - ...(filterOptions.showElasticRules - ? [`alert.attributes.tags: "__internal_immutable:true"`] - : []), - ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), - ]; - - const query = { - page: pagination.page, - per_page: pagination.perPage, - sort_field: filterOptions.sortField, - sort_order: filterOptions.sortOrder, - ...(filters.length ? { filter: filters.join(' AND ') } : {}), - }; - - return KibanaServices.get().http.fetch<FetchRulesResponse>( - `${DETECTION_ENGINE_RULES_URL}/_find`, - { - method: 'GET', - query, - signal, - } - ); -}; - -/** - * Fetch a Rule by providing a Rule ID - * - * @param id Rule ID's (not rule_id) - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> => - KibanaServices.get().http.fetch<Rule>(DETECTION_ENGINE_RULES_URL, { - method: 'GET', - query: { id }, - signal, - }); - -/** - * Enables/Disables provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to enable/disable - * @param enabled to enable or disable - * - * @throws An error if response is not OK - */ -export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<BulkRuleResponse> => - KibanaServices.get().http.fetch<BulkRuleResponse>(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { - method: 'PATCH', - body: JSON.stringify(ids.map(id => ({ id, enabled }))), - }); - -/** - * Deletes provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to delete - * - * @throws An error if response is not OK - */ -export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<BulkRuleResponse> => - KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'DELETE', - body: JSON.stringify(ids.map(id => ({ id }))), - }); - -/** - * Duplicates provided Rules - * - * @param rules to duplicate - * - * @throws An error if response is not OK - */ -export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<BulkRuleResponse> => - KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { - method: 'POST', - body: JSON.stringify( - rules.map(rule => ({ - ...rule, - name: `${rule.name} [${i18n.DUPLICATE}]`, - created_at: undefined, - created_by: undefined, - id: undefined, - rule_id: undefined, - updated_at: undefined, - updated_by: undefined, - enabled: rule.enabled, - immutable: undefined, - last_success_at: undefined, - last_success_message: undefined, - last_failure_at: undefined, - last_failure_message: undefined, - status: undefined, - status_date: undefined, - })) - ), - }); - -/** - * Create Prepackaged Rules - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> => { - await KibanaServices.get().http.fetch<unknown>(DETECTION_ENGINE_PREPACKAGED_URL, { - method: 'PUT', - signal, - }); - - return true; -}; - -/** - * Imports rules in the same format as exported via the _export API - * - * @param fileToImport File to upload containing rules to import - * @param overwrite whether or not to overwrite rules with the same ruleId - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const importRules = async ({ - fileToImport, - overwrite = false, - signal, -}: ImportDataProps): Promise<ImportDataResponse> => { - const formData = new FormData(); - formData.append('file', fileToImport); - - return KibanaServices.get().http.fetch<ImportDataResponse>( - `${DETECTION_ENGINE_RULES_URL}/_import`, - { - method: 'POST', - headers: { 'Content-Type': undefined }, - query: { overwrite }, - body: formData, - signal, - } - ); -}; - -/** - * Export rules from the server as a file download - * - * @param excludeExportDetails whether or not to exclude additional details at bottom of exported file (defaults to false) - * @param filename of exported rules. Be sure to include `.ndjson` extension! (defaults to localized `rules_export.ndjson`) - * @param ruleIds array of rule_id's (not id!) to export (empty array exports _all_ rules) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const exportRules = async ({ - excludeExportDetails = false, - filename = `${i18n.EXPORT_FILENAME}.ndjson`, - ids = [], - signal, -}: ExportDocumentsProps): Promise<Blob> => { - const body = - ids.length > 0 ? JSON.stringify({ objects: ids.map(rule => ({ rule_id: rule })) }) : undefined; - - return KibanaServices.get().http.fetch<Blob>(`${DETECTION_ENGINE_RULES_URL}/_export`, { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - }); -}; - -/** - * Get Rule Status provided Rule ID - * - * @param id string of Rule ID's (not rule_id) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getRuleStatusById = async ({ - id, - signal, -}: { - id: string; - signal: AbortSignal; -}): Promise<RuleStatusResponse> => - KibanaServices.get().http.fetch<RuleStatusResponse>(DETECTION_ENGINE_RULES_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ ids: [id] }), - signal, - }); - -/** - * Return rule statuses given list of alert ids - * - * @param ids array of string of Rule ID's (not rule_id) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getRulesStatusByIds = async ({ - ids, - signal, -}: { - ids: string[]; - signal: AbortSignal; -}): Promise<RuleStatusResponse> => { - const res = await KibanaServices.get().http.fetch<RuleStatusResponse>( - DETECTION_ENGINE_RULES_STATUS_URL, - { - method: 'POST', - body: JSON.stringify({ ids }), - signal, - } - ); - return res; -}; - -/** - * Fetch all unique Tags used by Rules - * - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> => - KibanaServices.get().http.fetch<string[]>(DETECTION_ENGINE_TAGS_URL, { - method: 'GET', - signal, - }); - -/** - * Get pre packaged rules Status - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getPrePackagedRulesStatus = async ({ - signal, -}: { - signal: AbortSignal; -}): Promise<PrePackagedRulesStatusResponse> => - KibanaServices.get().http.fetch<PrePackagedRulesStatusResponse>( - DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, - { - method: 'GET', - signal, - } - ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts deleted file mode 100644 index 2f2de2e151664..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ /dev/null @@ -1,246 +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 * as t from 'io-ts'; - -import { RuleTypeSchema } from '../../../../../../../plugins/siem/common/detection_engine/types'; - -/** - * Params is an "record", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts - */ -export const action = t.exact( - t.type({ - group: t.string, - id: t.string, - action_type_id: t.string, - params: t.record(t.string, t.any), - }) -); - -export const NewRuleSchema = t.intersection([ - t.type({ - description: t.string, - enabled: t.boolean, - interval: t.string, - name: t.string, - risk_score: t.number, - severity: t.string, - type: RuleTypeSchema, - }), - t.partial({ - actions: t.array(action), - anomaly_threshold: t.number, - created_by: t.string, - false_positives: t.array(t.string), - filters: t.array(t.unknown), - from: t.string, - id: t.string, - index: t.array(t.string), - language: t.string, - machine_learning_job_id: t.string, - max_signals: t.number, - query: t.string, - references: t.array(t.string), - rule_id: t.string, - saved_id: t.string, - tags: t.array(t.string), - threat: t.array(t.unknown), - throttle: t.union([t.string, t.null]), - to: t.string, - updated_by: t.string, - note: t.string, - }), -]); - -export const NewRulesSchema = t.array(NewRuleSchema); -export type NewRule = t.TypeOf<typeof NewRuleSchema>; - -export interface AddRulesProps { - rule: NewRule; - signal: AbortSignal; -} - -const MetaRule = t.intersection([ - t.type({ - from: t.string, - }), - t.partial({ - throttle: t.string, - kibana_siem_app_url: t.string, - }), -]); - -export const RuleSchema = t.intersection([ - t.type({ - created_at: t.string, - created_by: t.string, - description: t.string, - enabled: t.boolean, - false_positives: t.array(t.string), - from: t.string, - id: t.string, - interval: t.string, - immutable: t.boolean, - name: t.string, - max_signals: t.number, - references: t.array(t.string), - risk_score: t.number, - rule_id: t.string, - severity: t.string, - tags: t.array(t.string), - type: RuleTypeSchema, - to: t.string, - threat: t.array(t.unknown), - updated_at: t.string, - updated_by: t.string, - actions: t.array(action), - throttle: t.union([t.string, t.null]), - }), - t.partial({ - anomaly_threshold: t.number, - filters: t.array(t.unknown), - index: t.array(t.string), - language: t.string, - last_failure_at: t.string, - last_failure_message: t.string, - meta: MetaRule, - machine_learning_job_id: t.string, - output_index: t.string, - query: t.string, - saved_id: t.string, - status: t.string, - status_date: t.string, - timeline_id: t.string, - timeline_title: t.string, - note: t.string, - version: t.number, - }), -]); - -export const RulesSchema = t.array(RuleSchema); - -export type Rule = t.TypeOf<typeof RuleSchema>; -export type Rules = t.TypeOf<typeof RulesSchema>; - -export interface RuleError { - id?: string; - rule_id?: string; - error: { status_code: number; message: string }; -} - -export type BulkRuleResponse = Array<Rule | RuleError>; - -export interface RuleResponseBuckets { - rules: Rule[]; - errors: RuleError[]; -} - -export interface PaginationOptions { - page: number; - perPage: number; - total: number; -} - -export interface FetchRulesProps { - pagination?: PaginationOptions; - filterOptions?: FilterOptions; - signal: AbortSignal; -} - -export interface FilterOptions { - filter: string; - sortField: string; - sortOrder: 'asc' | 'desc'; - showCustomRules?: boolean; - showElasticRules?: boolean; - tags?: string[]; -} - -export interface FetchRulesResponse { - page: number; - perPage: number; - total: number; - data: Rule[]; -} - -export interface FetchRuleProps { - id: string; - signal: AbortSignal; -} - -export interface EnableRulesProps { - ids: string[]; - enabled: boolean; -} - -export interface DeleteRulesProps { - ids: string[]; -} - -export interface DuplicateRulesProps { - rules: Rule[]; -} - -export interface BasicFetchProps { - signal: AbortSignal; -} - -export interface ImportDataProps { - fileToImport: File; - overwrite?: boolean; - signal: AbortSignal; -} - -export interface ImportRulesResponseError { - rule_id: string; - error: { - status_code: number; - message: string; - }; -} - -export interface ImportDataResponse { - success: boolean; - success_count: number; - errors: ImportRulesResponseError[]; -} - -export interface ExportDocumentsProps { - ids: string[]; - filename?: string; - excludeExportDetails?: boolean; - signal: AbortSignal; -} - -export interface RuleStatus { - current_status: RuleInfoStatus; - failures: RuleInfoStatus[]; -} - -export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; -export interface RuleInfoStatus { - alert_id: string; - status_date: string; - status: RuleStatusType | null; - last_failure_at: string | null; - last_success_at: string | null; - last_failure_message: string | null; - last_success_message: string | null; - last_look_back_date: string | null | undefined; - gap: string | null | undefined; - bulk_create_time_durations: string[] | null | undefined; - search_after_time_durations: string[] | null | undefined; -} - -export type RuleStatusResponse = Record<string, RuleStatus>; - -export interface PrePackagedRulesStatusResponse { - rules_custom_installed: number; - rules_installed: number; - rules_not_installed: number; - rules_not_updated: number; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx deleted file mode 100644 index 0a30329baf68d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx +++ /dev/null @@ -1,176 +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 { renderHook, act, cleanup } from '@testing-library/react-hooks'; -import { - useRuleStatus, - ReturnRuleStatus, - useRulesStatuses, - ReturnRulesStatuses, -} from './use_rule_status'; -import * as api from './api'; -import { Rule } from '../rules/types'; - -jest.mock('./api'); - -const testRule: Rule = { - actions: [ - { - group: 'fake group', - id: 'fake id', - action_type_id: 'fake action_type_id', - params: { - someKey: 'someVal', - }, - }, - ], - created_at: 'mm/dd/yyyyTHH:MM:sssz', - created_by: 'mockUser', - description: 'some desc', - enabled: true, - false_positives: [], - filters: [], - from: 'now-360s', - id: '12345678987654321', - immutable: false, - index: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - interval: '5m', - language: 'kuery', - name: 'Test rule', - max_signals: 100, - query: "user.email: 'root@elastic.co'", - references: [], - risk_score: 75, - rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', - severity: 'high', - tags: ['APM'], - threat: [], - throttle: null, - to: 'now', - type: 'query', - updated_at: 'mm/dd/yyyyTHH:MM:sssz', - updated_by: 'mockUser', -}; - -describe('useRuleStatus', () => { - beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - }); - afterEach(async () => { - cleanup(); - }); - - test('init', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - expect(result.current).toEqual([true, null, null]); - }); - }); - - test('fetch rule status', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual([ - false, - { - current_status: { - alert_id: 'alertId', - last_failure_at: null, - last_failure_message: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_success_message: 'it is a success', - status: 'succeeded', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - gap: null, - bulk_create_time_durations: ['2235.01'], - search_after_time_durations: ['616.97'], - last_look_back_date: '2020-03-19T00:32:07.996Z', - }, - failures: [], - }, - result.current[2], - ]); - }); - }); - - test('re-fetch rule status', async () => { - const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById'); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() => - useRuleStatus('myOwnRuleID') - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - if (result.current[2]) { - result.current[2]('myOwnRuleID'); - } - await waitForNextUpdate(); - expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2); - }); - }); - - test('init rules statuses', async () => { - const payload = [testRule]; - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnRulesStatuses>(() => - useRulesStatuses(payload) - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ loading: false, rulesStatuses: [] }); - }); - }); - - test('fetch rules statuses', async () => { - const payload = [testRule]; - await act(async () => { - const { result, waitForNextUpdate } = renderHook<string, ReturnRulesStatuses>(() => - useRulesStatuses(payload) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - rulesStatuses: [ - { - current_status: { - alert_id: 'alertId', - bulk_create_time_durations: ['2235.01'], - gap: null, - last_failure_at: null, - last_failure_message: null, - last_look_back_date: '2020-03-19T00:32:07.996Z', - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_success_message: 'it is a success', - search_after_time_durations: ['616.97'], - status: 'succeeded', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - }, - failures: [], - id: '12345678987654321', - activate: true, - name: 'Test rule', - }, - ], - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts deleted file mode 100644 index ece2483adde3a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ /dev/null @@ -1,101 +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 { - DETECTION_ENGINE_QUERY_SIGNALS_URL, - DETECTION_ENGINE_SIGNALS_STATUS_URL, - DETECTION_ENGINE_INDEX_URL, - DETECTION_ENGINE_PRIVILEGES_URL, -} from '../../../../../../../plugins/siem/common/constants'; -import { KibanaServices } from '../../../lib/kibana'; -import { - BasicSignals, - Privilege, - QuerySignals, - SignalSearchResponse, - SignalsIndex, - UpdateSignalStatusProps, -} from './types'; - -/** - * Fetch Signals by providing a query - * - * @param query String to match a dsl - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchQuerySignals = async <Hit, Aggregations>({ - query, - signal, -}: QuerySignals): Promise<SignalSearchResponse<Hit, Aggregations>> => - KibanaServices.get().http.fetch<SignalSearchResponse<Hit, Aggregations>>( - DETECTION_ENGINE_QUERY_SIGNALS_URL, - { - method: 'POST', - body: JSON.stringify(query), - signal, - } - ); - -/** - * Update signal status by query - * - * @param query of signals to update - * @param status to update to('open' / 'closed') - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const updateSignalStatus = async ({ - query, - status, - signal, -}: UpdateSignalStatusProps): Promise<unknown> => - KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ status, ...query }), - signal, - }); - -/** - * Fetch Signal Index - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => - KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { - method: 'GET', - signal, - }); - -/** - * Get User Privileges - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege> => - KibanaServices.get().http.fetch<Privilege>(DETECTION_ENGINE_PRIVILEGES_URL, { - method: 'GET', - signal, - }); - -/** - * Create Signal Index if needed it - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => - KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { - method: 'POST', - signal, - }); diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts deleted file mode 100644 index 8628ba502f081..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/index.ts +++ /dev/null @@ -1,86 +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 { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { GetLastEventTimeQuery, LastEventIndexKey, LastTimeDetails } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; -import { QueryTemplateProps } from '../../query_template'; -import { useUiSetting$ } from '../../../lib/kibana'; - -import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; -import { useApolloClient } from '../../../utils/apollo_context'; - -export interface LastEventTimeArgs { - id: string; - errorMessage: string; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: LastEventTimeArgs) => React.ReactNode; - indexKey: LastEventIndexKey; -} - -export function useLastEventTimeQuery<TCache = object>( - indexKey: LastEventIndexKey, - details: LastTimeDetails, - sourceId: string -) { - const [loading, updateLoading] = useState(false); - const [lastSeen, updateLastSeen] = useState<number | null>(null); - const [errorMessage, updateErrorMessage] = useState<string | null>(null); - const [currentIndexKey, updateCurrentIndexKey] = useState<LastEventIndexKey | null>(null); - const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const apolloClient = useApolloClient(); - async function fetchLastEventTime(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query<GetLastEventTimeQuery.Query, GetLastEventTimeQuery.Variables>({ - query: LastEventTimeGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - indexKey, - details, - defaultIndex, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateLastSeen(get('data.source.LastEventTime.lastSeen', result)); - updateErrorMessage(null); - updateCurrentIndexKey(currentIndexKey); - }, - error => { - updateLoading(false); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchLastEventTime(signal); - return () => abortCtrl.abort(); - }, [apolloClient, indexKey, details.hostName, details.ip]); - - return { lastSeen, loading, errorMessage }; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts b/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts deleted file mode 100644 index 5ef8e67dedddb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/mock.ts +++ /dev/null @@ -1,61 +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 { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; -import { GetLastEventTimeQuery, LastEventIndexKey } from '../../../graphql/types'; - -import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; - -interface MockLastEventTimeQuery { - request: { - query: GetLastEventTimeQuery.Query; - variables: GetLastEventTimeQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - LastEventTime: { - lastSeen: string | null; - errorMessage: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} - -const getTimeTwelveMinutesAgo = () => { - const d = new Date(); - const ts = d.getTime(); - const twelveMinutes = ts - 12 * 60 * 1000; - return new Date(twelveMinutes).toISOString(); -}; - -export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ - { - request: { - query: LastEventTimeGqlQuery, - variables: { - sourceId: 'default', - indexKey: LastEventIndexKey.hosts, - details: {}, - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - LastEventTime: { - lastSeen: getTimeTwelveMinutesAgo(), - errorMessage: null, - }, - }, - }, - }, - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts b/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts deleted file mode 100644 index 67cfe259927ab..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/helpers.test.ts +++ /dev/null @@ -1,29 +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 { ESQuery } from '../../../../../plugins/siem/common/typed_json'; - -import { createFilter } from './helpers'; - -describe('Helpers', () => { - describe('#createFilter', () => { - test('if it is a string it returns untouched', () => { - const filter = createFilter('even invalid strings return the same'); - expect(filter).toBe('even invalid strings return the same'); - }); - - test('if it is an ESQuery object it will be returned as a string', () => { - const query: ESQuery = { term: { 'host.id': 'host-value' } }; - const filter = createFilter(query); - expect(filter).toBe(JSON.stringify(query)); - }); - - test('if it is undefined, then undefined is returned', () => { - const filter = createFilter(undefined); - expect(filter).toBe(undefined); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/containers/helpers.ts b/x-pack/legacy/plugins/siem/public/containers/helpers.ts deleted file mode 100644 index 7ff9577bfb05e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/helpers.ts +++ /dev/null @@ -1,15 +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 { FetchPolicy } from 'apollo-client'; -import { isString } from 'lodash/fp'; - -import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; - -export const createFilter = (filterQuery: ESQuery | string | undefined) => - isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); - -export const getDefaultFetchPolicy = (): FetchPolicy => 'cache-and-network'; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts deleted file mode 100644 index 5806125f2397b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/index.ts +++ /dev/null @@ -1,85 +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 ApolloClient from 'apollo-client'; -import { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; -import { QueryTemplateProps } from '../../query_template'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -export interface FirstLastSeenHostArgs { - id: string; - errorMessage: string; - firstSeen: Date; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: FirstLastSeenHostArgs) => React.ReactNode; - hostName: string; -} - -export function useFirstLastSeenHostQuery<TCache = object>( - hostName: string, - sourceId: string, - apolloClient: ApolloClient<TCache> -) { - const [loading, updateLoading] = useState(false); - const [firstSeen, updateFirstSeen] = useState<Date | null>(null); - const [lastSeen, updateLastSeen] = useState<Date | null>(null); - const [errorMessage, updateErrorMessage] = useState<string | null>(null); - const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - - async function fetchFirstLastSeenHost(signal: AbortSignal) { - updateLoading(true); - return apolloClient - .query<GetHostFirstLastSeenQuery.Query, GetHostFirstLastSeenQuery.Variables>({ - query: HostFirstLastSeenGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - hostName, - defaultIndex, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); - updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); - updateErrorMessage(null); - }, - error => { - updateLoading(false); - updateFirstSeen(null); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchFirstLastSeenHost(signal); - return () => abortCtrl.abort(); - }, []); - - return { firstSeen, lastSeen, loading, errorMessage }; -} diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts b/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts deleted file mode 100644 index 7376f38ae8d0f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/mock.ts +++ /dev/null @@ -1,52 +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 { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; -import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -interface MockedProvidedQuery { - request: { - query: GetHostFirstLastSeenQuery.Query; - variables: GetHostFirstLastSeenQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - HostFirstLastSeen: { - firstSeen: string | null; - lastSeen: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} -export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ - { - request: { - query: HostFirstLastSeenGqlQuery, - variables: { - sourceId: 'default', - hostName: 'kibana-siem', - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - HostFirstLastSeen: { - firstSeen: '2019-04-08T16:09:40.692Z', - lastSeen: '2019-04-08T18:35:45.064Z', - }, - }, - }, - }, - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx b/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx deleted file mode 100644 index edf3f6855f955..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/index.tsx +++ /dev/null @@ -1,183 +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 { get, getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - Direction, - GetHostsTableQuery, - HostsEdges, - HostsFields, - PageInfoPaginated, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; - -import { HostsTableQuery } from './hosts_table.gql_query'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; - -const ID = 'hostsQuery'; - -export interface HostsArgs { - endDate: number; - hosts: HostsEdges[]; - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - startDate: number; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: HostsArgs) => React.ReactNode; - type: hostsModel.HostsType; - startDate: number; - endDate: number; -} - -export interface HostsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sortField: HostsFields; - direction: Direction; -} - -type HostsProps = OwnProps & HostsComponentReduxProps & WithKibanaProps; - -class HostsComponentQuery extends QueryTemplatePaginated< - HostsProps, - GetHostsTableQuery.Query, - GetHostsTableQuery.Variables -> { - private memoizedHosts: ( - variables: string, - data: GetHostsTableQuery.Source | undefined - ) => HostsEdges[]; - - constructor(props: HostsProps) { - super(props); - this.memoizedHosts = memoizeOne(this.getHosts); - } - - public render() { - const { - activePage, - id = ID, - isInspected, - children, - direction, - filterQuery, - endDate, - kibana, - limit, - startDate, - skip, - sourceId, - sortField, - } = this.props; - const defaultIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); - - const variables: GetHostsTableQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - sort: { - direction, - field: sortField, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex, - inspect: isInspected, - }; - return ( - <Query<GetHostsTableQuery.Query, GetHostsTableQuery.Variables> - query={HostsTableQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={variables} - skip={skip} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Hosts: { - ...fetchMoreResult.source.Hosts, - edges: [...fetchMoreResult.source.Hosts.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - endDate, - hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)), - id, - inspect: getOr(null, 'source.Hosts.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Hosts.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - startDate, - totalCount: getOr(-1, 'source.Hosts.totalCount', data), - }); - }} - </Query> - ); - } - - private getHosts = ( - variables: string, - source: GetHostsTableQuery.Source | undefined - ): HostsEdges[] => getOr([], 'Hosts.edges', source); -} - -const makeMapStateToProps = () => { - const getHostsSelector = hostsSelectors.hostsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHostsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -export const HostsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(HostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx b/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx deleted file mode 100644 index 405c45348b54d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/index.tsx +++ /dev/null @@ -1,113 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { inputsModel, inputsSelectors, State } from '../../../store'; -import { getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplate, QueryTemplateProps } from '../../query_template'; -import { withKibana, WithKibanaProps } from '../../../lib/kibana'; - -import { HostOverviewQuery } from './host_overview.gql_query'; -import { GetHostOverviewQuery, HostItem } from '../../../graphql/types'; - -const ID = 'hostOverviewQuery'; - -export interface HostOverviewArgs { - id: string; - inspect: inputsModel.InspectQuery; - hostOverview: HostItem; - loading: boolean; - refetch: inputsModel.Refetch; - startDate: number; - endDate: number; -} - -export interface HostOverviewReduxProps { - isInspected: boolean; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: HostOverviewArgs) => React.ReactNode; - hostName: string; - startDate: number; - endDate: number; -} - -type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; - -class HostOverviewByNameComponentQuery extends QueryTemplate< - HostsOverViewProps, - GetHostOverviewQuery.Query, - GetHostOverviewQuery.Variables -> { - public render() { - const { - id = ID, - isInspected, - children, - hostName, - kibana, - skip, - sourceId, - startDate, - endDate, - } = this.props; - return ( - <Query<GetHostOverviewQuery.Query, GetHostOverviewQuery.Variables> - query={HostOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - hostName, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const hostOverview = getOr([], 'source.HostOverview', data); - return children({ - id, - inspect: getOr(null, 'source.HostOverview.inspect', data), - refetch, - loading, - hostOverview, - startDate, - endDate, - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const HostOverviewByNameQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(HostOverviewByNameComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx b/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx deleted file mode 100644 index 954bfede07139..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.tsx +++ /dev/null @@ -1,84 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetIpOverviewQuery, IpOverviewData } from '../../graphql/types'; -import { networkModel, inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { ipOverviewQuery } from './index.gql_query'; - -const ID = 'ipOverviewQuery'; - -export interface IpOverviewArgs { - id: string; - inspect: inputsModel.InspectQuery; - ipOverviewData: IpOverviewData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface IpOverviewProps extends QueryTemplateProps { - children: (args: IpOverviewArgs) => React.ReactNode; - type: networkModel.NetworkType; - ip: string; -} - -const IpOverviewComponentQuery = React.memo<IpOverviewProps & PropsFromRedux>( - ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( - <Query<GetIpOverviewQuery.Query, GetIpOverviewQuery.Variables> - query={ipOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - filterQuery: createFilter(filterQuery), - ip, - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const init: IpOverviewData = { host: {} }; - const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data); - return children({ - id, - inspect: getOr(null, 'source.IpOverview.inspect', data), - ipOverviewData, - loading, - refetch, - }); - }} - </Query> - ) -); - -IpOverviewComponentQuery.displayName = 'IpOverviewComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: IpOverviewProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const IpOverviewQuery = connector(IpOverviewComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx deleted file mode 100644 index 3933aefa60483..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.tsx +++ /dev/null @@ -1,85 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiHostDetailsQuery } from './index.gql_query'; - -const ID = 'kpiHostDetailsQuery'; - -export interface KpiHostDetailsArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiHostDetails: KpiHostDetailsData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface QueryKpiHostDetailsProps extends QueryTemplateProps { - children: (args: KpiHostDetailsArgs) => React.ReactNode; -} - -const KpiHostDetailsComponentQuery = React.memo<QueryKpiHostDetailsProps & PropsFromRedux>( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( - <Query<GetKpiHostDetailsQuery.Query, GetKpiHostDetailsQuery.Variables> - query={kpiHostDetailsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiHostDetails = getOr({}, `source.KpiHostDetails`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiHostDetails.inspect', data), - kpiHostDetails, - loading, - refetch, - }); - }} - </Query> - ) -); - -KpiHostDetailsComponentQuery.displayName = 'KpiHostDetailsComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: QueryKpiHostDetailsProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const KpiHostDetailsQuery = connector(KpiHostDetailsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx deleted file mode 100644 index 7035d63193118..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.tsx +++ /dev/null @@ -1,85 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiHostsQuery } from './index.gql_query'; - -const ID = 'kpiHostsQuery'; - -export interface KpiHostsArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiHosts: KpiHostsData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface KpiHostsProps extends QueryTemplateProps { - children: (args: KpiHostsArgs) => React.ReactNode; -} - -const KpiHostsComponentQuery = React.memo<KpiHostsProps & PropsFromRedux>( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( - <Query<GetKpiHostsQuery.Query, GetKpiHostsQuery.Variables> - query={kpiHostsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiHosts = getOr({}, `source.KpiHosts`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiHosts.inspect', data), - kpiHosts, - loading, - refetch, - }); - }} - </Query> - ) -); - -KpiHostsComponentQuery.displayName = 'KpiHostsComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: KpiHostsProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const KpiHostsQuery = connector(KpiHostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx deleted file mode 100644 index 002a819417df6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.tsx +++ /dev/null @@ -1,85 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetKpiNetworkQuery, KpiNetworkData } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiNetworkQuery } from './index.gql_query'; - -const ID = 'kpiNetworkQuery'; - -export interface KpiNetworkArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiNetwork: KpiNetworkData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface KpiNetworkProps extends QueryTemplateProps { - children: (args: KpiNetworkArgs) => React.ReactNode; -} - -const KpiNetworkComponentQuery = React.memo<KpiNetworkProps & PropsFromRedux>( - ({ id = ID, children, filterQuery, isInspected, skip, sourceId, startDate, endDate }) => ( - <Query<GetKpiNetworkQuery.Query, GetKpiNetworkQuery.Variables> - query={kpiNetworkQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiNetwork = getOr({}, `source.KpiNetwork`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiNetwork.inspect', data), - kpiNetwork, - loading, - refetch, - }); - }} - </Query> - ) -); - -KpiNetworkComponentQuery.displayName = 'KpiNetworkComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: KpiNetworkProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const KpiNetworkQuery = connector(KpiNetworkComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx b/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx deleted file mode 100644 index af4eb1ff7a5e1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/kuery_autocompletion/index.tsx +++ /dev/null @@ -1,84 +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 React, { useState } from 'react'; -import { QuerySuggestion, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { useKibana } from '../../lib/kibana'; - -type RendererResult = React.ReactElement<JSX.Element> | null; -type RendererFunction<RenderArgs, Result = RendererResult> = (args: RenderArgs) => Result; - -interface KueryAutocompletionLifecycleProps { - children: RendererFunction<{ - isLoadingSuggestions: boolean; - loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: QuerySuggestion[]; - }>; - indexPattern: IIndexPattern; -} - -interface KueryAutocompletionCurrentRequest { - expression: string; - cursorPosition: number; -} - -export const KueryAutocompletion = React.memo<KueryAutocompletionLifecycleProps>( - ({ children, indexPattern }) => { - const [currentRequest, setCurrentRequest] = useState<KueryAutocompletionCurrentRequest | null>( - null - ); - const [suggestions, setSuggestions] = useState<QuerySuggestion[]>([]); - const kibana = useKibana(); - const loadSuggestions = async ( - expression: string, - cursorPosition: number, - maxSuggestions?: number - ) => { - const language = 'kuery'; - - if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { - return; - } - - const futureRequest = { - expression, - cursorPosition, - }; - setCurrentRequest({ - expression, - cursorPosition, - }); - setSuggestions([]); - - if ( - futureRequest && - futureRequest.expression !== (currentRequest && currentRequest.expression) && - futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) - ) { - const newSuggestions = - (await kibana.services.data.autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - boolFilter: [], - query: expression, - selectionStart: cursorPosition, - selectionEnd: cursorPosition, - })) || []; - - setCurrentRequest(null); - setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); - } - }; - - return children({ - isLoadingSuggestions: currentRequest !== null, - loadSuggestions, - suggestions, - }); - } -); - -KueryAutocompletion.displayName = 'KueryAutocompletion'; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts deleted file mode 100644 index 55d7e7cdc6e54..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.ts +++ /dev/null @@ -1,119 +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 { isEmpty } from 'lodash/fp'; -import { useEffect, useMemo, useState, useRef } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { useUiSetting$ } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { useApolloClient } from '../../utils/apollo_context'; -import { inputsModel } from '../../store'; -import { MatrixHistogramGqlQuery } from './index.gql_query'; -import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../graphql/types'; - -export const useQuery = <Hit, Aggs, TCache = object>({ - endDate, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, -}: MatrixHistogramQueryProps) => { - const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo<string[]>(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; - } - return configIndex; - }, [configIndex, indexToAdd]); - - const [, dispatchToaster] = useStateToaster(); - const refetch = useRef<inputsModel.Refetch>(); - const [loading, setLoading] = useState<boolean>(false); - const [data, setData] = useState<MatrixOverTimeHistogramData[] | null>(null); - const [inspect, setInspect] = useState<inputsModel.InspectQuery | null>(null); - const [totalCount, setTotalCount] = useState<number>(-1); - const apolloClient = useApolloClient(); - - useEffect(() => { - const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { - filterQuery: createFilter(filterQuery), - sourceId: 'default', - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - defaultIndex, - inspect: isInspected, - stackByField, - histogramType, - }; - let isSubscribed = true; - const abortCtrl = new AbortController(); - const abortSignal = abortCtrl.signal; - - async function fetchData() { - if (!apolloClient) return null; - setLoading(true); - return apolloClient - .query<GetMatrixHistogramQuery.Query, GetMatrixHistogramQuery.Variables>({ - query: MatrixHistogramGqlQuery, - fetchPolicy: 'network-only', - variables: matrixHistogramVariables, - context: { - fetchOptions: { - abortSignal, - }, - }, - }) - .then( - result => { - if (isSubscribed) { - const source = result?.data?.source?.MatrixHistogram ?? {}; - setData(source?.matrixHistogramData ?? []); - setTotalCount(source?.totalCount ?? -1); - setInspect(source?.inspect ?? null); - setLoading(false); - } - }, - error => { - if (isSubscribed) { - setData(null); - setTotalCount(-1); - setInspect(null); - setLoading(false); - errorToToaster({ title: errorMessage, error, dispatchToaster }); - } - } - ); - } - refetch.current = fetchData; - fetchData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [ - defaultIndex, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, - endDate, - data, - ]); - - return { data, loading, inspect, totalCount, refetch: refetch.current }; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx deleted file mode 100644 index 060b66fc3cbbe..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.tsx +++ /dev/null @@ -1,209 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DocumentNode } from 'graphql'; -import { ScaleType } from '@elastic/charts'; -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - GetNetworkDnsQuery, - NetworkDnsEdges, - NetworkDnsSortField, - PageInfoPaginated, - MatrixOverOrdinalHistogramData, -} from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkDnsQuery } from './index.gql_query'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; -import { MatrixHistogram } from '../../components/matrix_histogram'; -import { MatrixHistogramOption, GetSubTitle } from '../../components/matrix_histogram/types'; -import { UpdateDateRange } from '../../components/charts/common'; -import { SetQuery } from '../../pages/hosts/navigation/types'; - -const ID = 'networkDnsQuery'; -export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; -export interface NetworkDnsArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkDns: NetworkDnsEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - stackByField?: string; - totalCount: number; - histogram: MatrixOverOrdinalHistogramData[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkDnsArgs) => React.ReactNode; - type: networkModel.NetworkType; -} - -interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { - dataKey: string | string[]; - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - isDnsHistogram?: boolean; - query: DocumentNode; - scaleType: ScaleType; - setQuery: SetQuery; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string; - type: networkModel.NetworkType; - updateDateRange: UpdateDateRange; - yTickFormatter?: (value: number) => string; -} - -export interface NetworkDnsComponentReduxProps { - activePage: number; - sort: NetworkDnsSortField; - isInspected: boolean; - isPtrIncluded: boolean; - limit: number; -} - -type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; - -export class NetworkDnsComponentQuery extends QueryTemplatePaginated< - NetworkDnsProps, - GetNetworkDnsQuery.Query, - GetNetworkDnsQuery.Variables -> { - public render() { - const { - activePage, - children, - sort, - endDate, - filterQuery, - id = ID, - isInspected, - isPtrIncluded, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetNetworkDnsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - - return ( - <Query<GetNetworkDnsQuery.Query, GetNetworkDnsQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkDnsQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkDns = getOr([], `source.NetworkDns.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkDns: { - ...fetchMoreResult.source.NetworkDns, - edges: [...fetchMoreResult.source.NetworkDns.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkDns.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkDns, - pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), - histogram: getOr(null, 'source.NetworkDns.histogram', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - isInspected, - id, - }; - }; - - return mapStateToProps; -}; - -const makeMapHistogramStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - isInspected, - id, - }; - }; - - return mapStateToProps; -}; - -export const NetworkDnsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkDnsComponentQuery); - -export const NetworkDnsHistogramQuery = compose<React.ComponentClass<DnsHistogramOwnProps>>( - connect(makeMapHistogramStateToProps), - withKibana -)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx deleted file mode 100644 index b13637fa88d07..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_http/index.tsx +++ /dev/null @@ -1,156 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - GetNetworkHttpQuery, - NetworkHttpEdges, - NetworkHttpSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkHttpQuery } from './index.gql_query'; - -const ID = 'networkHttpQuery'; - -export interface NetworkHttpArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkHttp: NetworkHttpEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkHttpArgs) => React.ReactNode; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkHttpComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkHttpSortField; -} - -type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; - -class NetworkHttpComponentQuery extends QueryTemplatePaginated< - NetworkHttpProps, - GetNetworkHttpQuery.Query, - GetNetworkHttpQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - sort, - startDate, - } = this.props; - const variables: GetNetworkHttpQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetNetworkHttpQuery.Query, GetNetworkHttpQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkHttpQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkHttp = getOr([], `source.NetworkHttp.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkHttp: { - ...fetchMoreResult.source.NetworkHttp, - edges: [...fetchMoreResult.source.NetworkHttp.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkHttp.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkHttp, - pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getHttpSelector = networkSelectors.httpSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHttpSelector(state, type), - isInspected, - }; - }; -}; - -export const NetworkHttpQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkHttpComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx deleted file mode 100644 index 17a14ce3a1120..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.tsx +++ /dev/null @@ -1,160 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - FlowTargetSourceDest, - GetNetworkTopCountriesQuery, - NetworkTopCountriesEdges, - NetworkTopTablesSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkTopCountriesQuery } from './index.gql_query'; - -const ID = 'networkTopCountriesQuery'; - -export interface NetworkTopCountriesArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkTopCountries: NetworkTopCountriesEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopCountriesArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkTopCountriesComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} - -type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; - -class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< - NetworkTopCountriesProps, - GetNetworkTopCountriesQuery.Query, - GetNetworkTopCountriesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopCountriesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetNetworkTopCountriesQuery.Query, GetNetworkTopCountriesQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopCountriesQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopCountries: { - ...fetchMoreResult.source.NetworkTopCountries, - edges: [...fetchMoreResult.source.NetworkTopCountries.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopCountries, - pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopCountriesSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const NetworkTopCountriesQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopCountriesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx deleted file mode 100644 index fdac282292a4b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.tsx +++ /dev/null @@ -1,160 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - FlowTargetSourceDest, - GetNetworkTopNFlowQuery, - NetworkTopNFlowEdges, - NetworkTopTablesSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkTopNFlowQuery } from './index.gql_query'; - -const ID = 'networkTopNFlowQuery'; - -export interface NetworkTopNFlowArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkTopNFlow: NetworkTopNFlowEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopNFlowArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkTopNFlowComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} - -type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps & WithKibanaProps; - -class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated< - NetworkTopNFlowProps, - GetNetworkTopNFlowQuery.Query, - GetNetworkTopNFlowQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopNFlowQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetNetworkTopNFlowQuery.Query, GetNetworkTopNFlowQuery.Variables> - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopNFlowQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopNFlow = getOr([], `source.NetworkTopNFlow.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopNFlow: { - ...fetchMoreResult.source.NetworkTopNFlow, - edges: [...fetchMoreResult.source.NetworkTopNFlow.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopNFlow.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopNFlow, - pageInfo: getOr({}, 'source.NetworkTopNFlow.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopNFlow.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getTopNFlowSelector = networkSelectors.topNFlowSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopNFlowSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const NetworkTopNFlowQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopNFlowComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx deleted file mode 100644 index e7b68bf557a21..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.tsx +++ /dev/null @@ -1,89 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; -import { inputsModel, inputsSelectors } from '../../../store/inputs'; -import { State } from '../../../store'; -import { createFilter, getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { overviewHostQuery } from './index.gql_query'; - -export const ID = 'overviewHostQuery'; - -export interface OverviewHostArgs { - id: string; - inspect: inputsModel.InspectQuery; - loading: boolean; - overviewHost: OverviewHostData; - refetch: inputsModel.Refetch; -} - -export interface OverviewHostProps extends QueryTemplateProps { - children: (args: OverviewHostArgs) => React.ReactNode; - sourceId: string; - endDate: number; - startDate: number; -} - -const OverviewHostComponentQuery = React.memo<OverviewHostProps & PropsFromRedux>( - ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => { - return ( - <Query<GetOverviewHostQuery.Query, GetOverviewHostQuery.Variables> - query={overviewHostQuery} - fetchPolicy={getDefaultFetchPolicy()} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const overviewHost = getOr({}, `source.OverviewHost`, data); - return children({ - id, - inspect: getOr(null, 'source.OverviewHost.inspect', data), - overviewHost, - loading, - refetch, - }); - }} - </Query> - ); - } -); - -OverviewHostComponentQuery.displayName = 'OverviewHostComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OverviewHostProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const OverviewHostQuery = connector(OverviewHostComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx b/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx deleted file mode 100644 index c7f72ac6193f4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.tsx +++ /dev/null @@ -1,88 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { GetOverviewNetworkQuery, OverviewNetworkData } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; -import { State } from '../../../store'; -import { inputsModel, inputsSelectors } from '../../../store/inputs'; -import { createFilter, getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { overviewNetworkQuery } from './index.gql_query'; - -export const ID = 'overviewNetworkQuery'; - -export interface OverviewNetworkArgs { - id: string; - inspect: inputsModel.InspectQuery; - overviewNetwork: OverviewNetworkData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OverviewNetworkProps extends QueryTemplateProps { - children: (args: OverviewNetworkArgs) => React.ReactNode; - sourceId: string; - endDate: number; - startDate: number; -} - -export const OverviewNetworkComponentQuery = React.memo<OverviewNetworkProps & PropsFromRedux>( - ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( - <Query<GetOverviewNetworkQuery.Query, GetOverviewNetworkQuery.Variables> - query={overviewNetworkQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); - return children({ - id, - inspect: getOr(null, 'source.OverviewNetwork.inspect', data), - overviewNetwork, - loading, - refetch, - }); - }} - </Query> - ) -); - -OverviewNetworkComponentQuery.displayName = 'OverviewNetworkComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OverviewNetworkProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const OverviewNetworkQuery = connector(OverviewNetworkComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx b/x-pack/legacy/plugins/siem/public/containers/source/index.tsx deleted file mode 100644 index 3467e2b5f18d8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/source/index.tsx +++ /dev/null @@ -1,177 +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 { isUndefined } from 'lodash'; -import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; -import { Query } from 'react-apollo'; -import React, { useEffect, useMemo, useState } from 'react'; -import memoizeOne from 'memoize-one'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; - -import { IndexField, SourceQuery } from '../../graphql/types'; - -import { sourceQuery } from './index.gql_query'; -import { useApolloClient } from '../../utils/apollo_context'; - -export { sourceQuery }; - -export interface BrowserField { - aggregatable: boolean; - category: string; - description: string | null; - example: string | number | null; - fields: Readonly<Record<string, Partial<BrowserField>>>; - format: string; - indexes: string[]; - name: string; - searchable: boolean; - type: string; -} - -export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>; - -export const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> => - Object.values(browserFields).reduce<Array<Partial<BrowserField>>>( - (acc, namespace) => [ - ...acc, - ...Object.values(namespace.fields != null ? namespace.fields : {}), - ], - [] - ); - -export const getAllFieldsByName = ( - browserFields: BrowserFields -): { [fieldName: string]: Partial<BrowserField> } => - keyBy('name', getAllBrowserFields(browserFields)); - -interface WithSourceArgs { - indicesExist: boolean; - browserFields: BrowserFields; - indexPattern: IIndexPattern; -} - -interface WithSourceProps { - children: (args: WithSourceArgs) => React.ReactNode; - indexToAdd?: string[] | null; - sourceId: string; -} - -export const getIndexFields = memoizeOne( - (title: string, fields: IndexField[]): IIndexPattern => - fields && fields.length > 0 - ? { - fields: fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field)), - title, - } - : { fields: [], title } -); - -export const getBrowserFields = memoizeOne( - (title: string, fields: IndexField[]): BrowserFields => - fields && fields.length > 0 - ? fields.reduce<BrowserFields>( - (accumulator: BrowserFields, field: IndexField) => - set([field.category, 'fields', field.name], field, accumulator), - {} - ) - : {} -); - -export const WithSource = React.memo<WithSourceProps>(({ children, indexToAdd, sourceId }) => { - const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo<string[]>(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; - } - return configIndex; - }, [configIndex, indexToAdd]); - - return ( - <Query<SourceQuery.Query, SourceQuery.Variables> - query={sourceQuery} - fetchPolicy="cache-first" - notifyOnNetworkStatusChange - variables={{ - sourceId, - defaultIndex, - }} - > - {({ data }) => - children({ - indicesExist: get('source.status.indicesExist', data), - browserFields: getBrowserFields( - defaultIndex.join(), - get('source.status.indexFields', data) - ), - indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), - }) - } - </Query> - ); -}); - -WithSource.displayName = 'WithSource'; - -export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => - indicesExist || isUndefined(indicesExist); - -export const useWithSource = (sourceId: string, indices: string[]) => { - const [loading, updateLoading] = useState(false); - const [indicesExist, setIndicesExist] = useState<boolean | undefined | null>(undefined); - const [browserFields, setBrowserFields] = useState<BrowserFields | null>(null); - const [indexPattern, setIndexPattern] = useState<IIndexPattern | null>(null); - const [errorMessage, updateErrorMessage] = useState<string | null>(null); - - const apolloClient = useApolloClient(); - async function fetchSource(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query<SourceQuery.Query, SourceQuery.Variables>({ - query: sourceQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - defaultIndex: indices, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateErrorMessage(null); - setIndicesExist(get('data.source.status.indicesExist', result)); - setBrowserFields( - getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) - ); - setIndexPattern( - getIndexFields(indices.join(), get('data.source.status.indexFields', result)) - ); - }, - error => { - updateLoading(false); - updateErrorMessage(error.message); - } - ); - } - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchSource(signal); - return () => abortCtrl.abort(); - }, [apolloClient, sourceId, indices]); - - return { indicesExist, browserFields, indexPattern, loading, errorMessage }; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/mock.ts b/x-pack/legacy/plugins/siem/public/containers/source/mock.ts deleted file mode 100644 index 805c69f7fcc12..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/source/mock.ts +++ /dev/null @@ -1,699 +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 { DEFAULT_INDEX_PATTERN } from '../../../../../../plugins/siem/common/constants'; - -import { BrowserFields } from '.'; -import { sourceQuery } from './index.gql_query'; - -export const mocksSource = [ - { - request: { - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - configuration: {}, - status: { - indicesExist: true, - winlogbeatIndices: [ - 'winlogbeat-7.0.0-2019.02.17', - 'winlogbeat-7.0.0-2019.02.18', - 'winlogbeat-7.0.0-2019.02.19', - 'winlogbeat-7.0.0-2019.02.20', - 'winlogbeat-7.0.0-2019.02.21', - 'winlogbeat-7.0.0-2019.02.21-000001', - 'winlogbeat-7.0.0-2019.02.22', - 'winlogbeat-8.0.0-2019.02.19-000001', - ], - auditbeatIndices: [ - 'auditbeat-7.0.0-2019.02.17', - 'auditbeat-7.0.0-2019.02.18', - 'auditbeat-7.0.0-2019.02.19', - 'auditbeat-7.0.0-2019.02.20', - 'auditbeat-7.0.0-2019.02.21', - 'auditbeat-7.0.0-2019.02.21-000001', - 'auditbeat-7.0.0-2019.02.22', - 'auditbeat-8.0.0-2019.02.19-000001', - ], - filebeatIndices: [ - 'filebeat-7.0.0-iot-2019.06', - 'filebeat-7.0.0-iot-2019.07', - 'filebeat-7.0.0-iot-2019.08', - 'filebeat-7.0.0-iot-2019.09', - 'filebeat-7.0.0-iot-2019.10', - 'filebeat-8.0.0-2019.02.19-000001', - ], - indexFields: [ - { - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'source', - description: - 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: DEFAULT_INDEX_PATTERN, - name: 'event.end', - searchable: true, - type: 'date', - }, - ], - }, - }, - }, - }, - }, -]; - -export const mockIndexFields = [ - { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, - { aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.name', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a0', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a1', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a2', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.address', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.bytes', searchable: true, type: 'number' }, - { aggregatable: true, name: 'client.domain', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.geo.country_iso_code', searchable: true, type: 'string' }, - { aggregatable: true, name: 'cloud.account.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'cloud.availability_zone', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.image.name', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.image.tag', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.address', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.bytes', searchable: true, type: 'number' }, - { aggregatable: true, name: 'destination.domain', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.ip', searchable: true, type: 'ip' }, - { aggregatable: true, name: 'destination.port', searchable: true, type: 'long' }, - { aggregatable: true, name: 'source.ip', searchable: true, type: 'ip' }, - { aggregatable: true, name: 'source.port', searchable: true, type: 'long' }, - { aggregatable: true, name: 'event.end', searchable: true, type: 'date' }, -]; - -export const mockBrowserFields: BrowserFields = { - agent: { - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - 'agent.id': { - aggregatable: true, - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - }, - 'agent.name': { - aggregatable: true, - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - }, - }, - }, - auditd: { - fields: { - 'auditd.data.a0': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - }, - 'auditd.data.a1': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - }, - 'auditd.data.a2': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - }, - }, - }, - base: { - fields: { - '@timestamp': { - aggregatable: true, - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - }, - }, - }, - client: { - fields: { - 'client.address': { - aggregatable: true, - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - }, - 'client.bytes': { - aggregatable: true, - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - 'client.geo.country_iso_code': { - aggregatable: true, - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - }, - }, - }, - cloud: { - fields: { - 'cloud.account.id': { - aggregatable: true, - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - }, - 'cloud.availability_zone': { - aggregatable: true, - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - }, - }, - }, - container: { - fields: { - 'container.id': { - aggregatable: true, - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - }, - 'container.image.name': { - aggregatable: true, - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - }, - 'container.image.tag': { - aggregatable: true, - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - }, - }, - }, - destination: { - fields: { - 'destination.address': { - aggregatable: true, - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - }, - 'destination.bytes': { - aggregatable: true, - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - }, - 'destination.domain': { - aggregatable: true, - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - }, - 'destination.ip': { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - 'destination.port': { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - }, - }, - event: { - fields: { - 'event.end': { - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: DEFAULT_INDEX_PATTERN, - name: 'event.end', - searchable: true, - type: 'date', - aggregatable: true, - }, - }, - }, - source: { - fields: { - 'source.ip': { - aggregatable: true, - category: 'source', - description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - 'source.port': { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - }, - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts deleted file mode 100644 index 32ac62d594e1c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/api.ts +++ /dev/null @@ -1,51 +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 { - TIMELINE_IMPORT_URL, - TIMELINE_EXPORT_URL, -} from '../../../../../../../plugins/siem/common/constants'; -import { ImportDataProps, ImportDataResponse } from '../../detection_engine/rules'; -import { KibanaServices } from '../../../lib/kibana'; -import { ExportSelectedData } from '../../../components/generic_downloader'; - -export const importTimelines = async ({ - fileToImport, - overwrite = false, - signal, -}: ImportDataProps): Promise<ImportDataResponse> => { - const formData = new FormData(); - formData.append('file', fileToImport); - - return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, { - method: 'POST', - headers: { 'Content-Type': undefined }, - query: { overwrite }, - body: formData, - signal, - }); -}; - -export const exportSelectedTimeline: ExportSelectedData = async ({ - excludeExportDetails = false, - filename = `timelines_export.ndjson`, - ids = [], - signal, -}): Promise<Blob> => { - const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; - const response = await KibanaServices.get().http.fetch<Blob>(`${TIMELINE_EXPORT_URL}`, { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - asResponse: true, - }); - - return response.body!; -}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx deleted file mode 100644 index b5c91ca287f0b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.tsx +++ /dev/null @@ -1,116 +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 React, { useCallback } from 'react'; -import { getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; - -import { Query } from 'react-apollo'; - -import { ApolloQueryResult } from 'apollo-client'; -import { OpenTimelineResult } from '../../../components/open_timeline/types'; -import { - GetAllTimeline, - PageInfoTimeline, - SortTimeline, - TimelineResult, -} from '../../../graphql/types'; -import { allTimelinesQuery } from './index.gql_query'; - -export interface AllTimelinesArgs { - timelines: OpenTimelineResult[]; - loading: boolean; - totalCount: number; - refetch: () => void; -} - -export interface AllTimelinesVariables { - onlyUserFavorite: boolean; - pageInfo: PageInfoTimeline; - search: string; - sort: SortTimeline; -} - -interface OwnProps extends AllTimelinesVariables { - children?: (args: AllTimelinesArgs) => React.ReactNode; -} - -type Refetch = ( - variables: GetAllTimeline.Variables | undefined -) => Promise<ApolloQueryResult<GetAllTimeline.Query>>; - -const getAllTimeline = memoizeOne( - (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => - timelines.map(timeline => ({ - created: timeline.created, - description: timeline.description, - eventIdToNoteIds: - timeline.eventIdToNoteIds != null - ? timeline.eventIdToNoteIds.reduce((acc, note) => { - if (note.eventId != null) { - const notes = getOr([], note.eventId, acc); - return { ...acc, [note.eventId]: [...notes, note.noteId] }; - } - return acc; - }, {}) - : null, - favorite: timeline.favorite, - noteIds: timeline.noteIds, - notes: - timeline.notes != null - ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) - : null, - pinnedEventIds: - timeline.pinnedEventIds != null - ? timeline.pinnedEventIds.reduce( - (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), - {} - ) - : null, - savedObjectId: timeline.savedObjectId, - title: timeline.title, - updated: timeline.updated, - updatedBy: timeline.updatedBy, - })) -); - -const AllTimelinesQueryComponent: React.FC<OwnProps> = ({ - children, - onlyUserFavorite, - pageInfo, - search, - sort, -}) => { - const variables: GetAllTimeline.Variables = { - onlyUserFavorite, - pageInfo, - search, - sort, - }; - const handleRefetch = useCallback((refetch: Refetch) => refetch(variables), [variables]); - - return ( - <Query<GetAllTimeline.Query, GetAllTimeline.Variables> - query={allTimelinesQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => - children!({ - loading, - refetch: handleRefetch.bind(null, refetch), - totalCount: getOr(0, 'getAllTimeline.totalCount', data), - timelines: getAllTimeline( - JSON.stringify(variables), - getOr([], 'getAllTimeline.timeline', data) - ), - }) - } - </Query> - ); -}; - -export const AllTimelinesQuery = React.memo(AllTimelinesQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx deleted file mode 100644 index 0debed9c5f9aa..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.tsx +++ /dev/null @@ -1,70 +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 { getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; -import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; - -import { timelineDetailsQuery } from './index.gql_query'; - -export interface EventsArgs { - detailsData: DetailItem[] | null; - loading: boolean; -} - -export interface TimelineDetailsProps { - children?: (args: EventsArgs) => React.ReactElement; - indexName: string; - eventId: string; - executeQuery: boolean; - sourceId: string; -} - -const getDetailsEvent = memoizeOne( - (variables: string, detail: DetailItem[]): DetailItem[] => detail -); - -const TimelineDetailsQueryComponent: React.FC<TimelineDetailsProps> = ({ - children, - indexName, - eventId, - executeQuery, - sourceId, -}) => { - const variables: GetTimelineDetailsQuery.Variables = { - sourceId, - indexName, - eventId, - defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), - }; - return executeQuery ? ( - <Query<GetTimelineDetailsQuery.Query, GetTimelineDetailsQuery.Variables> - query={timelineDetailsQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => - children!({ - loading, - detailsData: getDetailsEvent( - JSON.stringify(variables), - getOr([], 'source.TimelineDetails.data', data) - ), - }) - } - </Query> - ) : ( - children!({ loading: false, detailsData: null }) - ); -}; - -export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx b/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx deleted file mode 100644 index 3c089ef6926dd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.tsx +++ /dev/null @@ -1,199 +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 { getOr, uniqBy } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { compose, Dispatch } from 'redux'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; -import { - GetTimelineQuery, - PageInfo, - SortField, - TimelineEdges, - TimelineItem, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { QueryTemplate, QueryTemplateProps } from '../query_template'; -import { EventType } from '../../store/timeline/model'; -import { timelineQuery } from './index.gql_query'; -import { timelineActions } from '../../store/timeline'; -import { SIGNALS_PAGE_TIMELINE_ID } from '../../pages/detection_engine/components/signals'; - -export interface TimelineArgs { - events: TimelineItem[]; - id: string; - inspect: inputsModel.InspectQuery; - loading: boolean; - loadMore: (cursor: string, tieBreaker: string) => void; - pageInfo: PageInfo; - refetch: inputsModel.Refetch; - totalCount: number; - getUpdatedAt: () => number; -} - -export interface CustomReduxProps { - clearSignalsState: ({ id }: { id?: string }) => void; -} - -export interface OwnProps extends QueryTemplateProps { - children?: (args: TimelineArgs) => React.ReactNode; - eventType?: EventType; - id: string; - indexPattern?: IIndexPattern; - indexToAdd?: string[]; - limit: number; - sortField: SortField; - fields: string[]; -} - -type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; - -class TimelineQueryComponent extends QueryTemplate< - TimelineQueryProps, - GetTimelineQuery.Query, - GetTimelineQuery.Variables -> { - private updatedDate: number = Date.now(); - private memoizedTimelineEvents: (variables: string, events: TimelineEdges[]) => TimelineItem[]; - - constructor(props: TimelineQueryProps) { - super(props); - this.memoizedTimelineEvents = memoizeOne(this.getTimelineEvents); - } - - public render() { - const { - children, - clearSignalsState, - eventType = 'raw', - id, - indexPattern, - indexToAdd = [], - isInspected, - kibana, - limit, - fields, - filterQuery, - sourceId, - sortField, - } = this.props; - const defaultKibanaIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); - const defaultIndex = - indexPattern == null || (indexPattern != null && indexPattern.title === '') - ? [ - ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), - ...(['all', 'signal'].includes(eventType) ? indexToAdd : []), - ] - : indexPattern?.title.split(',') ?? []; - const variables: GetTimelineQuery.Variables = { - fieldRequested: fields, - filterQuery: createFilter(filterQuery), - sourceId, - pagination: { limit, cursor: null, tiebreaker: null }, - sortField, - defaultIndex, - inspect: isInspected, - }; - - return ( - <Query<GetTimelineQuery.Query, GetTimelineQuery.Variables> - query={timelineQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, fetchMore, refetch }) => { - this.setRefetch(refetch); - this.setExecuteBeforeRefetch(clearSignalsState); - this.setExecuteBeforeFetchMore(clearSignalsState); - - const timelineEdges = getOr([], 'source.Timeline.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newCursor: string, tiebreaker?: string) => ({ - variables: { - pagination: { - cursor: newCursor, - tiebreaker, - limit, - }, - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Timeline: { - ...fetchMoreResult.source.Timeline, - edges: uniqBy('node._id', [ - ...prev.source.Timeline.edges, - ...fetchMoreResult.source.Timeline.edges, - ]), - }, - }, - }; - }, - })); - this.updatedDate = Date.now(); - return children!({ - id, - inspect: getOr(null, 'source.Timeline.inspect', data), - refetch: this.wrappedRefetch, - loading, - totalCount: getOr(0, 'source.Timeline.totalCount', data), - pageInfo: getOr({}, 'source.Timeline.pageInfo', data), - events: this.memoizedTimelineEvents(JSON.stringify(variables), timelineEdges), - loadMore: this.wrappedLoadMore, - getUpdatedAt: this.getUpdatedAt, - }); - }} - </Query> - ); - } - - private getUpdatedAt = () => this.updatedDate; - - private getTimelineEvents = (variables: string, timelineEdges: TimelineEdges[]): TimelineItem[] => - timelineEdges.map((e: TimelineEdges) => e.node); -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.timelineQueryByIdSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSignalsState: ({ id }: { id?: string }) => { - if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) { - dispatch(timelineActions.clearEventsLoading({ id })); - dispatch(timelineActions.clearEventsDeleted({ id })); - } - }, -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const TimelineQuery = compose<React.ComponentClass<OwnProps>>( - connector, - withKibana -)(TimelineQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx b/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx deleted file mode 100644 index 20617b88bda94..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/tls/index.tsx +++ /dev/null @@ -1,159 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - PageInfoPaginated, - TlsEdges, - TlsSortField, - GetTlsQuery, - FlowTargetSourceDest, -} from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { tlsQuery } from './index.gql_query'; - -const ID = 'tlsQuery'; - -export interface TlsArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - tls: TlsEdges[]; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: TlsArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip: string; - type: networkModel.NetworkType; -} - -export interface TlsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: TlsSortField; -} - -type TlsProps = OwnProps & TlsComponentReduxProps & WithKibanaProps; - -class TlsComponentQuery extends QueryTemplatePaginated< - TlsProps, - GetTlsQuery.Query, - GetTlsQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetTlsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate ? startDate : 0, - to: endDate ? endDate : Date.now(), - }, - }; - return ( - <Query<GetTlsQuery.Query, GetTlsQuery.Variables> - query={tlsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const tls = getOr([], 'source.Tls.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Tls: { - ...fetchMoreResult.source.Tls, - edges: [...fetchMoreResult.source.Tls.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Tls.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Tls.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - tls, - totalCount: getOr(-1, 'source.Tls.totalCount', data), - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTlsSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const TlsQuery = compose<React.ComponentClass<OwnProps>>( - connect(makeMapStateToProps), - withKibana -)(TlsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx b/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx deleted file mode 100644 index 72e4e46bc6ae0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.tsx +++ /dev/null @@ -1,148 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { - GetUncommonProcessesQuery, - PageInfoPaginated, - UncommonProcessesEdges, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { uncommonProcessesQuery } from './index.gql_query'; - -const ID = 'uncommonProcessesQuery'; - -export interface UncommonProcessesArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; - uncommonProcesses: UncommonProcessesEdges[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UncommonProcessesArgs) => React.ReactNode; - type: hostsModel.HostsType; -} - -type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UncommonProcessesComponentQuery extends QueryTemplatePaginated< - UncommonProcessesProps, - GetUncommonProcessesQuery.Query, - GetUncommonProcessesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetUncommonProcessesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - pagination: generateTablePaginationOptions(activePage, limit), - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetUncommonProcessesQuery.Query, GetUncommonProcessesQuery.Variables> - query={uncommonProcessesQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - UncommonProcesses: { - ...fetchMoreResult.source.UncommonProcesses, - edges: [...fetchMoreResult.source.UncommonProcesses.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.UncommonProcesses.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), - uncommonProcesses, - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUncommonProcessesSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const UncommonProcessesQuery = compose<React.ComponentClass<OwnProps>>( - connector, - withKibana -)(UncommonProcessesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/users/index.tsx b/x-pack/legacy/plugins/siem/public/containers/users/index.tsx deleted file mode 100644 index 658cb5785b54c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/containers/users/index.tsx +++ /dev/null @@ -1,153 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../plugins/siem/common/constants'; -import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { usersQuery } from './index.gql_query'; - -const ID = 'usersQuery'; - -export interface UsersArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; - users: UsersEdges[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UsersArgs) => React.ReactNode; - flowTarget: FlowTarget; - ip: string; - type: networkModel.NetworkType; -} - -type UsersProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UsersComponentQuery extends QueryTemplatePaginated< - UsersProps, - GetUsersQuery.Query, - GetUsersQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetUsersQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - <Query<GetUsersQuery.Query, GetUsersQuery.Variables> - query={usersQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const users = getOr([], `source.Users.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Users: { - ...fetchMoreResult.source.Users, - edges: [...fetchMoreResult.source.Users.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Users.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Users.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Users.totalCount', data), - users, - }); - }} - </Query> - ); - } -} - -const makeMapStateToProps = () => { - const getUsersSelector = networkSelectors.usersSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUsersSelector(state), - isInspected, - }; - }; - - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const UsersQuery = compose<React.ComponentClass<OwnProps>>( - connector, - withKibana -)(UsersComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts deleted file mode 100644 index e15c099a007ad..0000000000000 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ /dev/null @@ -1,5943 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/* - * 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 Maybe<T> = T | null; - -export interface PageInfoNote { - pageIndex: number; - - pageSize: number; -} - -export interface SortNote { - sortField: SortFieldNote; - - sortOrder: Direction; -} - -export interface TimerangeInput { - /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ - interval: string; - /** The end of the timerange */ - to: number; - /** The beginning of the timerange */ - from: number; -} - -export interface PaginationInputPaginated { - /** The activePage parameter defines the page of results you want to fetch */ - activePage: number; - /** The cursorStart parameter defines the start of the results to be displayed */ - cursorStart: number; - /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ - fakePossibleCount: number; - /** The querySize parameter is the number of items to be returned */ - querySize: number; -} - -export interface PaginationInput { - /** The limit parameter allows you to configure the maximum amount of items to be returned */ - limit: number; - /** The cursor parameter defines the next result you want to fetch */ - cursor?: Maybe<string>; - /** The tiebreaker parameter allow to be more precise to fetch the next item */ - tiebreaker?: Maybe<string>; -} - -export interface SortField { - sortFieldId: string; - - direction: Direction; -} - -export interface LastTimeDetails { - hostName?: Maybe<string>; - - ip?: Maybe<string>; -} - -export interface HostsSortField { - field: HostsFields; - - direction: Direction; -} - -export interface UsersSortField { - field: UsersFields; - - direction: Direction; -} - -export interface NetworkTopTablesSortField { - field: NetworkTopTablesFields; - - direction: Direction; -} - -export interface NetworkDnsSortField { - field: NetworkDnsFields; - - direction: Direction; -} - -export interface NetworkHttpSortField { - direction: Direction; -} - -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - -export interface PageInfoTimeline { - pageIndex: number; - - pageSize: number; -} - -export interface SortTimeline { - sortField: SortFieldTimeline; - - sortOrder: Direction; -} - -export interface NoteInput { - eventId?: Maybe<string>; - - note?: Maybe<string>; - - timelineId?: Maybe<string>; -} - -export interface TimelineInput { - columns?: Maybe<ColumnHeaderInput[]>; - - dataProviders?: Maybe<DataProviderInput[]>; - - description?: Maybe<string>; - - eventType?: Maybe<string>; - - filters?: Maybe<FilterTimelineInput[]>; - - kqlMode?: Maybe<string>; - - kqlQuery?: Maybe<SerializedFilterQueryInput>; - - title?: Maybe<string>; - - dateRange?: Maybe<DateRangePickerInput>; - - savedQueryId?: Maybe<string>; - - sort?: Maybe<SortTimelineInput>; -} - -export interface ColumnHeaderInput { - aggregatable?: Maybe<boolean>; - - category?: Maybe<string>; - - columnHeaderType?: Maybe<string>; - - description?: Maybe<string>; - - example?: Maybe<string>; - - indexes?: Maybe<string[]>; - - id?: Maybe<string>; - - name?: Maybe<string>; - - placeholder?: Maybe<string>; - - searchable?: Maybe<boolean>; - - type?: Maybe<string>; -} - -export interface DataProviderInput { - id?: Maybe<string>; - - name?: Maybe<string>; - - enabled?: Maybe<boolean>; - - excluded?: Maybe<boolean>; - - kqlQuery?: Maybe<string>; - - queryMatch?: Maybe<QueryMatchInput>; - - and?: Maybe<DataProviderInput[]>; -} - -export interface QueryMatchInput { - field?: Maybe<string>; - - displayField?: Maybe<string>; - - value?: Maybe<string>; - - displayValue?: Maybe<string>; - - operator?: Maybe<string>; -} - -export interface FilterTimelineInput { - exists?: Maybe<string>; - - meta?: Maybe<FilterMetaTimelineInput>; - - match_all?: Maybe<string>; - - missing?: Maybe<string>; - - query?: Maybe<string>; - - range?: Maybe<string>; - - script?: Maybe<string>; -} - -export interface FilterMetaTimelineInput { - alias?: Maybe<string>; - - controlledBy?: Maybe<string>; - - disabled?: Maybe<boolean>; - - field?: Maybe<string>; - - formattedValue?: Maybe<string>; - - index?: Maybe<string>; - - key?: Maybe<string>; - - negate?: Maybe<boolean>; - - params?: Maybe<string>; - - type?: Maybe<string>; - - value?: Maybe<string>; -} - -export interface SerializedFilterQueryInput { - filterQuery?: Maybe<SerializedKueryQueryInput>; -} - -export interface SerializedKueryQueryInput { - kuery?: Maybe<KueryFilterQueryInput>; - - serializedQuery?: Maybe<string>; -} - -export interface KueryFilterQueryInput { - kind?: Maybe<string>; - - expression?: Maybe<string>; -} - -export interface DateRangePickerInput { - start?: Maybe<number>; - - end?: Maybe<number>; -} - -export interface SortTimelineInput { - columnId?: Maybe<string>; - - sortDirection?: Maybe<string>; -} - -export interface FavoriteTimelineInput { - fullName?: Maybe<string>; - - userName?: Maybe<string>; - - favoriteDate?: Maybe<number>; -} - -export enum SortFieldNote { - updatedBy = 'updatedBy', - updated = 'updated', -} - -export enum Direction { - asc = 'asc', - desc = 'desc', -} - -export enum LastEventIndexKey { - hostDetails = 'hostDetails', - hosts = 'hosts', - ipDetails = 'ipDetails', - network = 'network', -} - -export enum HostsFields { - hostName = 'hostName', - lastSeen = 'lastSeen', -} - -export enum UsersFields { - name = 'name', - count = 'count', -} - -export enum FlowTarget { - client = 'client', - destination = 'destination', - server = 'server', - source = 'source', -} - -export enum HistogramType { - authentications = 'authentications', - anomalies = 'anomalies', - events = 'events', - alerts = 'alerts', - dns = 'dns', -} - -export enum FlowTargetSourceDest { - destination = 'destination', - source = 'source', -} - -export enum NetworkTopTablesFields { - bytes_in = 'bytes_in', - bytes_out = 'bytes_out', - flows = 'flows', - destination_ips = 'destination_ips', - source_ips = 'source_ips', -} - -export enum NetworkDnsFields { - dnsName = 'dnsName', - queryCount = 'queryCount', - uniqueDomains = 'uniqueDomains', - dnsBytesIn = 'dnsBytesIn', - dnsBytesOut = 'dnsBytesOut', -} - -export enum TlsFields { - _id = '_id', -} - -export enum SortFieldTimeline { - title = 'title', - description = 'description', - updated = 'updated', - created = 'created', -} - -export enum NetworkDirectionEcs { - inbound = 'inbound', - outbound = 'outbound', - internal = 'internal', - external = 'external', - incoming = 'incoming', - outgoing = 'outgoing', - listening = 'listening', - unknown = 'unknown', -} - -export enum NetworkHttpFields { - domains = 'domains', - lastHost = 'lastHost', - lastSourceIp = 'lastSourceIp', - methods = 'methods', - path = 'path', - requestCount = 'requestCount', - statuses = 'statuses', -} - -export enum FlowDirection { - uniDirectional = 'uniDirectional', - biDirectional = 'biDirectional', -} - -export type ToStringArray = string[]; - -export type Date = string; - -export type ToNumberArray = number[]; - -export type ToDateArray = string[]; - -export type ToBooleanArray = boolean[]; - -export type ToAny = any; - -export type EsValue = any; - -// ==================================================== -// Scalars -// ==================================================== - -// ==================================================== -// Types -// ==================================================== - -export interface Query { - getNote: NoteResult; - - getNotesByTimelineId: NoteResult[]; - - getNotesByEventId: NoteResult[]; - - getAllNotes: ResponseNotes; - - getAllPinnedEventsByTimelineId: PinnedEvent[]; - /** Get a security data source by id */ - source: Source; - /** Get a list of all security data sources */ - allSources: Source[]; - - getOneTimeline: TimelineResult; - - getAllTimeline: ResponseTimelines; -} - -export interface NoteResult { - eventId?: Maybe<string>; - - note?: Maybe<string>; - - timelineId?: Maybe<string>; - - noteId: string; - - created?: Maybe<number>; - - createdBy?: Maybe<string>; - - timelineVersion?: Maybe<string>; - - updated?: Maybe<number>; - - updatedBy?: Maybe<string>; - - version?: Maybe<string>; -} - -export interface ResponseNotes { - notes: NoteResult[]; - - totalCount?: Maybe<number>; -} - -export interface PinnedEvent { - code?: Maybe<number>; - - message?: Maybe<string>; - - pinnedEventId: string; - - eventId?: Maybe<string>; - - timelineId?: Maybe<string>; - - timelineVersion?: Maybe<string>; - - created?: Maybe<number>; - - createdBy?: Maybe<string>; - - updated?: Maybe<number>; - - updatedBy?: Maybe<string>; - - version?: Maybe<string>; -} - -export interface Source { - /** The id of the source */ - id: string; - /** The raw configuration of the source */ - configuration: SourceConfiguration; - /** The status of the source */ - status: SourceStatus; - /** Gets Authentication success and failures based on a timerange */ - Authentications: AuthenticationsData; - - Timeline: TimelineData; - - TimelineDetails: TimelineDetailsData; - - LastEventTime: LastEventTimeData; - /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ - Hosts: HostsData; - - HostOverview: HostItem; - - HostFirstLastSeen: FirstLastSeenHost; - - IpOverview?: Maybe<IpOverviewData>; - - Users: UsersData; - - KpiNetwork?: Maybe<KpiNetworkData>; - - KpiHosts: KpiHostsData; - - KpiHostDetails: KpiHostDetailsData; - - MatrixHistogram: MatrixHistogramOverTimeData; - - NetworkTopCountries: NetworkTopCountriesData; - - NetworkTopNFlow: NetworkTopNFlowData; - - NetworkDns: NetworkDnsData; - - NetworkDnsHistogram: NetworkDsOverTimeData; - - NetworkHttp: NetworkHttpData; - - OverviewNetwork?: Maybe<OverviewNetworkData>; - - OverviewHost?: Maybe<OverviewHostData>; - - Tls: TlsData; - /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ - UncommonProcesses: UncommonProcessesData; - /** Just a simple example to get the app name */ - whoAmI?: Maybe<SayMyName>; -} - -/** A set of configuration options for a security data source */ -export interface SourceConfiguration { - /** The field mapping to use for this source */ - fields: SourceFields; -} - -/** A mapping of semantic fields to their document counterparts */ -export interface SourceFields { - /** The field to identify a container by */ - container: string; - /** The fields to identify a host by */ - host: string; - /** The fields that may contain the log event message. The first field found win. */ - message: string[]; - /** The field to identify a pod by */ - pod: string; - /** The field to use as a tiebreaker for log events that have identical timestamps */ - tiebreaker: string; - /** The field to use as a timestamp for metrics and logs */ - timestamp: string; -} - -/** The status of an infrastructure data source */ -export interface SourceStatus { - /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */ - indicesExist: boolean; - /** The list of fields defined in the index mappings */ - indexFields: IndexField[]; -} - -/** A descriptor of a field in an index */ -export interface IndexField { - /** Where the field belong */ - category: string; - /** Example of field's value */ - example?: Maybe<string>; - /** whether the field's belong to an alias index */ - indexes: (Maybe<string>)[]; - /** The name of the field */ - name: string; - /** The type of the field's values as recognized by Kibana */ - type: string; - /** Whether the field's values can be efficiently searched for */ - searchable: boolean; - /** Whether the field's values can be aggregated */ - aggregatable: boolean; - /** Description of the field */ - description?: Maybe<string>; - - format?: Maybe<string>; -} - -export interface AuthenticationsData { - edges: AuthenticationsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface AuthenticationsEdges { - node: AuthenticationItem; - - cursor: CursorType; -} - -export interface AuthenticationItem { - _id: string; - - failures: number; - - successes: number; - - user: UserEcsFields; - - lastSuccess?: Maybe<LastSourceHost>; - - lastFailure?: Maybe<LastSourceHost>; -} - -export interface UserEcsFields { - domain?: Maybe<string[]>; - - id?: Maybe<string[]>; - - name?: Maybe<string[]>; - - full_name?: Maybe<string[]>; - - email?: Maybe<string[]>; - - hash?: Maybe<string[]>; - - group?: Maybe<string[]>; -} - -export interface LastSourceHost { - timestamp?: Maybe<string>; - - source?: Maybe<SourceEcsFields>; - - host?: Maybe<HostEcsFields>; -} - -export interface SourceEcsFields { - bytes?: Maybe<number[]>; - - ip?: Maybe<string[]>; - - port?: Maybe<number[]>; - - domain?: Maybe<string[]>; - - geo?: Maybe<GeoEcsFields>; - - packets?: Maybe<number[]>; -} - -export interface GeoEcsFields { - city_name?: Maybe<string[]>; - - continent_name?: Maybe<string[]>; - - country_iso_code?: Maybe<string[]>; - - country_name?: Maybe<string[]>; - - location?: Maybe<Location>; - - region_iso_code?: Maybe<string[]>; - - region_name?: Maybe<string[]>; -} - -export interface Location { - lon?: Maybe<number[]>; - - lat?: Maybe<number[]>; -} - -export interface HostEcsFields { - architecture?: Maybe<string[]>; - - id?: Maybe<string[]>; - - ip?: Maybe<string[]>; - - mac?: Maybe<string[]>; - - name?: Maybe<string[]>; - - os?: Maybe<OsEcsFields>; - - type?: Maybe<string[]>; -} - -export interface OsEcsFields { - platform?: Maybe<string[]>; - - name?: Maybe<string[]>; - - full?: Maybe<string[]>; - - family?: Maybe<string[]>; - - version?: Maybe<string[]>; - - kernel?: Maybe<string[]>; -} - -export interface CursorType { - value?: Maybe<string>; - - tiebreaker?: Maybe<string>; -} - -export interface PageInfoPaginated { - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; -} - -export interface Inspect { - dsl: string[]; - - response: string[]; -} - -export interface TimelineData { - edges: TimelineEdges[]; - - totalCount: number; - - pageInfo: PageInfo; - - inspect?: Maybe<Inspect>; -} - -export interface TimelineEdges { - node: TimelineItem; - - cursor: CursorType; -} - -export interface TimelineItem { - _id: string; - - _index?: Maybe<string>; - - data: TimelineNonEcsData[]; - - ecs: Ecs; -} - -export interface TimelineNonEcsData { - field: string; - - value?: Maybe<string[]>; -} - -export interface Ecs { - _id: string; - - _index?: Maybe<string>; - - auditd?: Maybe<AuditdEcsFields>; - - destination?: Maybe<DestinationEcsFields>; - - dns?: Maybe<DnsEcsFields>; - - endgame?: Maybe<EndgameEcsFields>; - - event?: Maybe<EventEcsFields>; - - geo?: Maybe<GeoEcsFields>; - - host?: Maybe<HostEcsFields>; - - network?: Maybe<NetworkEcsField>; - - rule?: Maybe<RuleEcsField>; - - signal?: Maybe<SignalField>; - - source?: Maybe<SourceEcsFields>; - - suricata?: Maybe<SuricataEcsFields>; - - tls?: Maybe<TlsEcsFields>; - - zeek?: Maybe<ZeekEcsFields>; - - http?: Maybe<HttpEcsFields>; - - url?: Maybe<UrlEcsFields>; - - timestamp?: Maybe<string>; - - message?: Maybe<string[]>; - - user?: Maybe<UserEcsFields>; - - winlog?: Maybe<WinlogEcsFields>; - - process?: Maybe<ProcessEcsFields>; - - file?: Maybe<FileFields>; - - system?: Maybe<SystemEcsField>; -} - -export interface AuditdEcsFields { - result?: Maybe<string[]>; - - session?: Maybe<string[]>; - - data?: Maybe<AuditdData>; - - summary?: Maybe<Summary>; - - sequence?: Maybe<string[]>; -} - -export interface AuditdData { - acct?: Maybe<string[]>; - - terminal?: Maybe<string[]>; - - op?: Maybe<string[]>; -} - -export interface Summary { - actor?: Maybe<PrimarySecondary>; - - object?: Maybe<PrimarySecondary>; - - how?: Maybe<string[]>; - - message_type?: Maybe<string[]>; - - sequence?: Maybe<string[]>; -} - -export interface PrimarySecondary { - primary?: Maybe<string[]>; - - secondary?: Maybe<string[]>; - - type?: Maybe<string[]>; -} - -export interface DestinationEcsFields { - bytes?: Maybe<number[]>; - - ip?: Maybe<string[]>; - - port?: Maybe<number[]>; - - domain?: Maybe<string[]>; - - geo?: Maybe<GeoEcsFields>; - - packets?: Maybe<number[]>; -} - -export interface DnsEcsFields { - question?: Maybe<DnsQuestionData>; - - resolved_ip?: Maybe<string[]>; - - response_code?: Maybe<string[]>; -} - -export interface DnsQuestionData { - name?: Maybe<string[]>; - - type?: Maybe<string[]>; -} - -export interface EndgameEcsFields { - exit_code?: Maybe<number[]>; - - file_name?: Maybe<string[]>; - - file_path?: Maybe<string[]>; - - logon_type?: Maybe<number[]>; - - parent_process_name?: Maybe<string[]>; - - pid?: Maybe<number[]>; - - process_name?: Maybe<string[]>; - - subject_domain_name?: Maybe<string[]>; - - subject_logon_id?: Maybe<string[]>; - - subject_user_name?: Maybe<string[]>; - - target_domain_name?: Maybe<string[]>; - - target_logon_id?: Maybe<string[]>; - - target_user_name?: Maybe<string[]>; -} - -export interface EventEcsFields { - action?: Maybe<string[]>; - - category?: Maybe<string[]>; - - code?: Maybe<string[]>; - - created?: Maybe<string[]>; - - dataset?: Maybe<string[]>; - - duration?: Maybe<number[]>; - - end?: Maybe<string[]>; - - hash?: Maybe<string[]>; - - id?: Maybe<string[]>; - - kind?: Maybe<string[]>; - - module?: Maybe<string[]>; - - original?: Maybe<string[]>; - - outcome?: Maybe<string[]>; - - risk_score?: Maybe<number[]>; - - risk_score_norm?: Maybe<number[]>; - - severity?: Maybe<number[]>; - - start?: Maybe<string[]>; - - timezone?: Maybe<string[]>; - - type?: Maybe<string[]>; -} - -export interface NetworkEcsField { - bytes?: Maybe<number[]>; - - community_id?: Maybe<string[]>; - - direction?: Maybe<string[]>; - - packets?: Maybe<number[]>; - - protocol?: Maybe<string[]>; - - transport?: Maybe<string[]>; -} - -export interface RuleEcsField { - reference?: Maybe<string[]>; -} - -export interface SignalField { - rule?: Maybe<RuleField>; - - original_time?: Maybe<string[]>; -} - -export interface RuleField { - id?: Maybe<string[]>; - - rule_id?: Maybe<string[]>; - - false_positives: string[]; - - saved_id?: Maybe<string[]>; - - timeline_id?: Maybe<string[]>; - - timeline_title?: Maybe<string[]>; - - max_signals?: Maybe<number[]>; - - risk_score?: Maybe<string[]>; - - output_index?: Maybe<string[]>; - - description?: Maybe<string[]>; - - from?: Maybe<string[]>; - - immutable?: Maybe<boolean[]>; - - index?: Maybe<string[]>; - - interval?: Maybe<string[]>; - - language?: Maybe<string[]>; - - query?: Maybe<string[]>; - - references?: Maybe<string[]>; - - severity?: Maybe<string[]>; - - tags?: Maybe<string[]>; - - threat?: Maybe<ToAny>; - - type?: Maybe<string[]>; - - size?: Maybe<string[]>; - - to?: Maybe<string[]>; - - enabled?: Maybe<boolean[]>; - - filters?: Maybe<ToAny>; - - created_at?: Maybe<string[]>; - - updated_at?: Maybe<string[]>; - - created_by?: Maybe<string[]>; - - updated_by?: Maybe<string[]>; - - version?: Maybe<string[]>; - - note?: Maybe<string[]>; -} - -export interface SuricataEcsFields { - eve?: Maybe<SuricataEveData>; -} - -export interface SuricataEveData { - alert?: Maybe<SuricataAlertData>; - - flow_id?: Maybe<number[]>; - - proto?: Maybe<string[]>; -} - -export interface SuricataAlertData { - signature?: Maybe<string[]>; - - signature_id?: Maybe<number[]>; -} - -export interface TlsEcsFields { - client_certificate?: Maybe<TlsClientCertificateData>; - - fingerprints?: Maybe<TlsFingerprintsData>; - - server_certificate?: Maybe<TlsServerCertificateData>; -} - -export interface TlsClientCertificateData { - fingerprint?: Maybe<FingerprintData>; -} - -export interface FingerprintData { - sha1?: Maybe<string[]>; -} - -export interface TlsFingerprintsData { - ja3?: Maybe<TlsJa3Data>; -} - -export interface TlsJa3Data { - hash?: Maybe<string[]>; -} - -export interface TlsServerCertificateData { - fingerprint?: Maybe<FingerprintData>; -} - -export interface ZeekEcsFields { - session_id?: Maybe<string[]>; - - connection?: Maybe<ZeekConnectionData>; - - notice?: Maybe<ZeekNoticeData>; - - dns?: Maybe<ZeekDnsData>; - - http?: Maybe<ZeekHttpData>; - - files?: Maybe<ZeekFileData>; - - ssl?: Maybe<ZeekSslData>; -} - -export interface ZeekConnectionData { - local_resp?: Maybe<boolean[]>; - - local_orig?: Maybe<boolean[]>; - - missed_bytes?: Maybe<number[]>; - - state?: Maybe<string[]>; - - history?: Maybe<string[]>; -} - -export interface ZeekNoticeData { - suppress_for?: Maybe<number[]>; - - msg?: Maybe<string[]>; - - note?: Maybe<string[]>; - - sub?: Maybe<string[]>; - - dst?: Maybe<string[]>; - - dropped?: Maybe<boolean[]>; - - peer_descr?: Maybe<string[]>; -} - -export interface ZeekDnsData { - AA?: Maybe<boolean[]>; - - qclass_name?: Maybe<string[]>; - - RD?: Maybe<boolean[]>; - - qtype_name?: Maybe<string[]>; - - rejected?: Maybe<boolean[]>; - - qtype?: Maybe<string[]>; - - query?: Maybe<string[]>; - - trans_id?: Maybe<number[]>; - - qclass?: Maybe<string[]>; - - RA?: Maybe<boolean[]>; - - TC?: Maybe<boolean[]>; -} - -export interface ZeekHttpData { - resp_mime_types?: Maybe<string[]>; - - trans_depth?: Maybe<string[]>; - - status_msg?: Maybe<string[]>; - - resp_fuids?: Maybe<string[]>; - - tags?: Maybe<string[]>; -} - -export interface ZeekFileData { - session_ids?: Maybe<string[]>; - - timedout?: Maybe<boolean[]>; - - local_orig?: Maybe<boolean[]>; - - tx_host?: Maybe<string[]>; - - source?: Maybe<string[]>; - - is_orig?: Maybe<boolean[]>; - - overflow_bytes?: Maybe<number[]>; - - sha1?: Maybe<string[]>; - - duration?: Maybe<number[]>; - - depth?: Maybe<number[]>; - - analyzers?: Maybe<string[]>; - - mime_type?: Maybe<string[]>; - - rx_host?: Maybe<string[]>; - - total_bytes?: Maybe<number[]>; - - fuid?: Maybe<string[]>; - - seen_bytes?: Maybe<number[]>; - - missing_bytes?: Maybe<number[]>; - - md5?: Maybe<string[]>; -} - -export interface ZeekSslData { - cipher?: Maybe<string[]>; - - established?: Maybe<boolean[]>; - - resumed?: Maybe<boolean[]>; - - version?: Maybe<string[]>; -} - -export interface HttpEcsFields { - version?: Maybe<string[]>; - - request?: Maybe<HttpRequestData>; - - response?: Maybe<HttpResponseData>; -} - -export interface HttpRequestData { - method?: Maybe<string[]>; - - body?: Maybe<HttpBodyData>; - - referrer?: Maybe<string[]>; - - bytes?: Maybe<number[]>; -} - -export interface HttpBodyData { - content?: Maybe<string[]>; - - bytes?: Maybe<number[]>; -} - -export interface HttpResponseData { - status_code?: Maybe<number[]>; - - body?: Maybe<HttpBodyData>; - - bytes?: Maybe<number[]>; -} - -export interface UrlEcsFields { - domain?: Maybe<string[]>; - - original?: Maybe<string[]>; - - username?: Maybe<string[]>; - - password?: Maybe<string[]>; -} - -export interface WinlogEcsFields { - event_id?: Maybe<number[]>; -} - -export interface ProcessEcsFields { - hash?: Maybe<ProcessHashData>; - - pid?: Maybe<number[]>; - - name?: Maybe<string[]>; - - ppid?: Maybe<number[]>; - - args?: Maybe<string[]>; - - executable?: Maybe<string[]>; - - title?: Maybe<string[]>; - - thread?: Maybe<Thread>; - - working_directory?: Maybe<string[]>; -} - -export interface ProcessHashData { - md5?: Maybe<string[]>; - - sha1?: Maybe<string[]>; - - sha256?: Maybe<string[]>; -} - -export interface Thread { - id?: Maybe<number[]>; - - start?: Maybe<string[]>; -} - -export interface FileFields { - name?: Maybe<string[]>; - - path?: Maybe<string[]>; - - target_path?: Maybe<string[]>; - - extension?: Maybe<string[]>; - - type?: Maybe<string[]>; - - device?: Maybe<string[]>; - - inode?: Maybe<string[]>; - - uid?: Maybe<string[]>; - - owner?: Maybe<string[]>; - - gid?: Maybe<string[]>; - - group?: Maybe<string[]>; - - mode?: Maybe<string[]>; - - size?: Maybe<number[]>; - - mtime?: Maybe<string[]>; - - ctime?: Maybe<string[]>; -} - -export interface SystemEcsField { - audit?: Maybe<AuditEcsFields>; - - auth?: Maybe<AuthEcsFields>; -} - -export interface AuditEcsFields { - package?: Maybe<PackageEcsFields>; -} - -export interface PackageEcsFields { - arch?: Maybe<string[]>; - - entity_id?: Maybe<string[]>; - - name?: Maybe<string[]>; - - size?: Maybe<number[]>; - - summary?: Maybe<string[]>; - - version?: Maybe<string[]>; -} - -export interface AuthEcsFields { - ssh?: Maybe<SshEcsFields>; -} - -export interface SshEcsFields { - method?: Maybe<string[]>; - - signature?: Maybe<string[]>; -} - -export interface PageInfo { - endCursor?: Maybe<CursorType>; - - hasNextPage?: Maybe<boolean>; -} - -export interface TimelineDetailsData { - data?: Maybe<DetailItem[]>; - - inspect?: Maybe<Inspect>; -} - -export interface DetailItem { - field: string; - - values?: Maybe<string[]>; - - originalValue?: Maybe<EsValue>; -} - -export interface LastEventTimeData { - lastSeen?: Maybe<string>; - - inspect?: Maybe<Inspect>; -} - -export interface HostsData { - edges: HostsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface HostsEdges { - node: HostItem; - - cursor: CursorType; -} - -export interface HostItem { - _id?: Maybe<string>; - - lastSeen?: Maybe<string>; - - host?: Maybe<HostEcsFields>; - - cloud?: Maybe<CloudFields>; - - inspect?: Maybe<Inspect>; -} - -export interface CloudFields { - instance?: Maybe<CloudInstance>; - - machine?: Maybe<CloudMachine>; - - provider?: Maybe<(Maybe<string>)[]>; - - region?: Maybe<(Maybe<string>)[]>; -} - -export interface CloudInstance { - id?: Maybe<(Maybe<string>)[]>; -} - -export interface CloudMachine { - type?: Maybe<(Maybe<string>)[]>; -} - -export interface FirstLastSeenHost { - inspect?: Maybe<Inspect>; - - firstSeen?: Maybe<string>; - - lastSeen?: Maybe<string>; -} - -export interface IpOverviewData { - client?: Maybe<Overview>; - - destination?: Maybe<Overview>; - - host: HostEcsFields; - - server?: Maybe<Overview>; - - source?: Maybe<Overview>; - - inspect?: Maybe<Inspect>; -} - -export interface Overview { - firstSeen?: Maybe<string>; - - lastSeen?: Maybe<string>; - - autonomousSystem: AutonomousSystem; - - geo: GeoEcsFields; -} - -export interface AutonomousSystem { - number?: Maybe<number>; - - organization?: Maybe<AutonomousSystemOrganization>; -} - -export interface AutonomousSystemOrganization { - name?: Maybe<string>; -} - -export interface UsersData { - edges: UsersEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface UsersEdges { - node: UsersNode; - - cursor: CursorType; -} - -export interface UsersNode { - _id?: Maybe<string>; - - timestamp?: Maybe<string>; - - user?: Maybe<UsersItem>; -} - -export interface UsersItem { - name?: Maybe<string>; - - id?: Maybe<string[]>; - - groupId?: Maybe<string[]>; - - groupName?: Maybe<string[]>; - - count?: Maybe<number>; -} - -export interface KpiNetworkData { - networkEvents?: Maybe<number>; - - uniqueFlowId?: Maybe<number>; - - uniqueSourcePrivateIps?: Maybe<number>; - - uniqueSourcePrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; - - uniqueDestinationPrivateIps?: Maybe<number>; - - uniqueDestinationPrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; - - dnsQueries?: Maybe<number>; - - tlsHandshakes?: Maybe<number>; - - inspect?: Maybe<Inspect>; -} - -export interface KpiNetworkHistogramData { - x?: Maybe<number>; - - y?: Maybe<number>; -} - -export interface KpiHostsData { - hosts?: Maybe<number>; - - hostsHistogram?: Maybe<KpiHostHistogramData[]>; - - authSuccess?: Maybe<number>; - - authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; - - authFailure?: Maybe<number>; - - authFailureHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueSourceIps?: Maybe<number>; - - uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueDestinationIps?: Maybe<number>; - - uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - inspect?: Maybe<Inspect>; -} - -export interface KpiHostHistogramData { - x?: Maybe<number>; - - y?: Maybe<number>; -} - -export interface KpiHostDetailsData { - authSuccess?: Maybe<number>; - - authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; - - authFailure?: Maybe<number>; - - authFailureHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueSourceIps?: Maybe<number>; - - uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - uniqueDestinationIps?: Maybe<number>; - - uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; - - inspect?: Maybe<Inspect>; -} - -export interface MatrixHistogramOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface MatrixOverTimeHistogramData { - x?: Maybe<number>; - - y?: Maybe<number>; - - g?: Maybe<string>; -} - -export interface NetworkTopCountriesData { - edges: NetworkTopCountriesEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface NetworkTopCountriesEdges { - node: NetworkTopCountriesItem; - - cursor: CursorType; -} - -export interface NetworkTopCountriesItem { - _id?: Maybe<string>; - - source?: Maybe<TopCountriesItemSource>; - - destination?: Maybe<TopCountriesItemDestination>; - - network?: Maybe<TopNetworkTablesEcsField>; -} - -export interface TopCountriesItemSource { - country?: Maybe<string>; - - destination_ips?: Maybe<number>; - - flows?: Maybe<number>; - - location?: Maybe<GeoItem>; - - source_ips?: Maybe<number>; -} - -export interface GeoItem { - geo?: Maybe<GeoEcsFields>; - - flowTarget?: Maybe<FlowTargetSourceDest>; -} - -export interface TopCountriesItemDestination { - country?: Maybe<string>; - - destination_ips?: Maybe<number>; - - flows?: Maybe<number>; - - location?: Maybe<GeoItem>; - - source_ips?: Maybe<number>; -} - -export interface TopNetworkTablesEcsField { - bytes_in?: Maybe<number>; - - bytes_out?: Maybe<number>; -} - -export interface NetworkTopNFlowData { - edges: NetworkTopNFlowEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface NetworkTopNFlowEdges { - node: NetworkTopNFlowItem; - - cursor: CursorType; -} - -export interface NetworkTopNFlowItem { - _id?: Maybe<string>; - - source?: Maybe<TopNFlowItemSource>; - - destination?: Maybe<TopNFlowItemDestination>; - - network?: Maybe<TopNetworkTablesEcsField>; -} - -export interface TopNFlowItemSource { - autonomous_system?: Maybe<AutonomousSystemItem>; - - domain?: Maybe<string[]>; - - ip?: Maybe<string>; - - location?: Maybe<GeoItem>; - - flows?: Maybe<number>; - - destination_ips?: Maybe<number>; -} - -export interface AutonomousSystemItem { - name?: Maybe<string>; - - number?: Maybe<number>; -} - -export interface TopNFlowItemDestination { - autonomous_system?: Maybe<AutonomousSystemItem>; - - domain?: Maybe<string[]>; - - ip?: Maybe<string>; - - location?: Maybe<GeoItem>; - - flows?: Maybe<number>; - - source_ips?: Maybe<number>; -} - -export interface NetworkDnsData { - edges: NetworkDnsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; - - histogram?: Maybe<MatrixOverOrdinalHistogramData[]>; -} - -export interface NetworkDnsEdges { - node: NetworkDnsItem; - - cursor: CursorType; -} - -export interface NetworkDnsItem { - _id?: Maybe<string>; - - dnsBytesIn?: Maybe<number>; - - dnsBytesOut?: Maybe<number>; - - dnsName?: Maybe<string>; - - queryCount?: Maybe<number>; - - uniqueDomains?: Maybe<number>; -} - -export interface MatrixOverOrdinalHistogramData { - x: string; - - y: number; - - g: string; -} - -export interface NetworkDsOverTimeData { - inspect?: Maybe<Inspect>; - - matrixHistogramData: MatrixOverTimeHistogramData[]; - - totalCount: number; -} - -export interface NetworkHttpData { - edges: NetworkHttpEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface NetworkHttpEdges { - node: NetworkHttpItem; - - cursor: CursorType; -} - -export interface NetworkHttpItem { - _id?: Maybe<string>; - - domains: string[]; - - lastHost?: Maybe<string>; - - lastSourceIp?: Maybe<string>; - - methods: string[]; - - path?: Maybe<string>; - - requestCount?: Maybe<number>; - - statuses: string[]; -} - -export interface OverviewNetworkData { - auditbeatSocket?: Maybe<number>; - - filebeatCisco?: Maybe<number>; - - filebeatNetflow?: Maybe<number>; - - filebeatPanw?: Maybe<number>; - - filebeatSuricata?: Maybe<number>; - - filebeatZeek?: Maybe<number>; - - packetbeatDNS?: Maybe<number>; - - packetbeatFlow?: Maybe<number>; - - packetbeatTLS?: Maybe<number>; - - inspect?: Maybe<Inspect>; -} - -export interface OverviewHostData { - auditbeatAuditd?: Maybe<number>; - - auditbeatFIM?: Maybe<number>; - - auditbeatLogin?: Maybe<number>; - - auditbeatPackage?: Maybe<number>; - - auditbeatProcess?: Maybe<number>; - - auditbeatUser?: Maybe<number>; - - endgameDns?: Maybe<number>; - - endgameFile?: Maybe<number>; - - endgameImageLoad?: Maybe<number>; - - endgameNetwork?: Maybe<number>; - - endgameProcess?: Maybe<number>; - - endgameRegistry?: Maybe<number>; - - endgameSecurity?: Maybe<number>; - - filebeatSystemModule?: Maybe<number>; - - winlogbeatSecurity?: Maybe<number>; - - winlogbeatMWSysmonOperational?: Maybe<number>; - - inspect?: Maybe<Inspect>; -} - -export interface TlsData { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export interface TlsNode { - _id?: Maybe<string>; - - timestamp?: Maybe<string>; - - notAfter?: Maybe<string[]>; - - subjects?: Maybe<string[]>; - - ja3?: Maybe<string[]>; - - issuers?: Maybe<string[]>; -} - -export interface UncommonProcessesData { - edges: UncommonProcessesEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe<Inspect>; -} - -export interface UncommonProcessesEdges { - node: UncommonProcessItem; - - cursor: CursorType; -} - -export interface UncommonProcessItem { - _id: string; - - instances: number; - - process: ProcessEcsFields; - - hosts: HostEcsFields[]; - - user?: Maybe<UserEcsFields>; -} - -export interface SayMyName { - /** The id of the source */ - appName: string; -} - -export interface TimelineResult { - columns?: Maybe<ColumnHeaderResult[]>; - - created?: Maybe<number>; - - createdBy?: Maybe<string>; - - dataProviders?: Maybe<DataProviderResult[]>; - - dateRange?: Maybe<DateRangePickerResult>; - - description?: Maybe<string>; - - eventIdToNoteIds?: Maybe<NoteResult[]>; - - eventType?: Maybe<string>; - - favorite?: Maybe<FavoriteTimelineResult[]>; - - filters?: Maybe<FilterTimelineResult[]>; - - kqlMode?: Maybe<string>; - - kqlQuery?: Maybe<SerializedFilterQueryResult>; - - notes?: Maybe<NoteResult[]>; - - noteIds?: Maybe<string[]>; - - pinnedEventIds?: Maybe<string[]>; - - pinnedEventsSaveObject?: Maybe<PinnedEvent[]>; - - savedQueryId?: Maybe<string>; - - savedObjectId: string; - - sort?: Maybe<SortTimelineResult>; - - title?: Maybe<string>; - - updated?: Maybe<number>; - - updatedBy?: Maybe<string>; - - version: string; -} - -export interface ColumnHeaderResult { - aggregatable?: Maybe<boolean>; - - category?: Maybe<string>; - - columnHeaderType?: Maybe<string>; - - description?: Maybe<string>; - - example?: Maybe<string>; - - indexes?: Maybe<string[]>; - - id?: Maybe<string>; - - name?: Maybe<string>; - - placeholder?: Maybe<string>; - - searchable?: Maybe<boolean>; - - type?: Maybe<string>; -} - -export interface DataProviderResult { - id?: Maybe<string>; - - name?: Maybe<string>; - - enabled?: Maybe<boolean>; - - excluded?: Maybe<boolean>; - - kqlQuery?: Maybe<string>; - - queryMatch?: Maybe<QueryMatchResult>; - - and?: Maybe<DataProviderResult[]>; -} - -export interface QueryMatchResult { - field?: Maybe<string>; - - displayField?: Maybe<string>; - - value?: Maybe<string>; - - displayValue?: Maybe<string>; - - operator?: Maybe<string>; -} - -export interface DateRangePickerResult { - start?: Maybe<number>; - - end?: Maybe<number>; -} - -export interface FavoriteTimelineResult { - fullName?: Maybe<string>; - - userName?: Maybe<string>; - - favoriteDate?: Maybe<number>; -} - -export interface FilterTimelineResult { - exists?: Maybe<string>; - - meta?: Maybe<FilterMetaTimelineResult>; - - match_all?: Maybe<string>; - - missing?: Maybe<string>; - - query?: Maybe<string>; - - range?: Maybe<string>; - - script?: Maybe<string>; -} - -export interface FilterMetaTimelineResult { - alias?: Maybe<string>; - - controlledBy?: Maybe<string>; - - disabled?: Maybe<boolean>; - - field?: Maybe<string>; - - formattedValue?: Maybe<string>; - - index?: Maybe<string>; - - key?: Maybe<string>; - - negate?: Maybe<boolean>; - - params?: Maybe<string>; - - type?: Maybe<string>; - - value?: Maybe<string>; -} - -export interface SerializedFilterQueryResult { - filterQuery?: Maybe<SerializedKueryQueryResult>; -} - -export interface SerializedKueryQueryResult { - kuery?: Maybe<KueryFilterQueryResult>; - - serializedQuery?: Maybe<string>; -} - -export interface KueryFilterQueryResult { - kind?: Maybe<string>; - - expression?: Maybe<string>; -} - -export interface SortTimelineResult { - columnId?: Maybe<string>; - - sortDirection?: Maybe<string>; -} - -export interface ResponseTimelines { - timeline: (Maybe<TimelineResult>)[]; - - totalCount?: Maybe<number>; -} - -export interface Mutation { - /** Persists a note */ - persistNote: ResponseNote; - - deleteNote?: Maybe<boolean>; - - deleteNoteByTimelineId?: Maybe<boolean>; - /** Persists a pinned event in a timeline */ - persistPinnedEventOnTimeline?: Maybe<PinnedEvent>; - /** Remove a pinned events in a timeline */ - deletePinnedEventOnTimeline: boolean; - /** Remove all pinned events in a timeline */ - deleteAllPinnedEventsOnTimeline: boolean; - /** Persists a timeline */ - persistTimeline: ResponseTimeline; - - persistFavorite: ResponseFavoriteTimeline; - - deleteTimeline: boolean; -} - -export interface ResponseNote { - code?: Maybe<number>; - - message?: Maybe<string>; - - note: NoteResult; -} - -export interface ResponseTimeline { - code?: Maybe<number>; - - message?: Maybe<string>; - - timeline: TimelineResult; -} - -export interface ResponseFavoriteTimeline { - code?: Maybe<number>; - - message?: Maybe<string>; - - savedObjectId: string; - - version: string; - - favorite?: Maybe<FavoriteTimelineResult[]>; -} - -export interface EcsEdges { - node: Ecs; - - cursor: CursorType; -} - -export interface EventsTimelineData { - edges: EcsEdges[]; - - totalCount: number; - - pageInfo: PageInfo; - - inspect?: Maybe<Inspect>; -} - -export interface OsFields { - platform?: Maybe<string>; - - name?: Maybe<string>; - - full?: Maybe<string>; - - family?: Maybe<string>; - - version?: Maybe<string>; - - kernel?: Maybe<string>; -} - -export interface HostFields { - architecture?: Maybe<string>; - - id?: Maybe<string>; - - ip?: Maybe<(Maybe<string>)[]>; - - mac?: Maybe<(Maybe<string>)[]>; - - name?: Maybe<string>; - - os?: Maybe<OsFields>; - - type?: Maybe<string>; -} - -// ==================================================== -// Arguments -// ==================================================== - -export interface GetNoteQueryArgs { - id: string; -} -export interface GetNotesByTimelineIdQueryArgs { - timelineId: string; -} -export interface GetNotesByEventIdQueryArgs { - eventId: string; -} -export interface GetAllNotesQueryArgs { - pageInfo?: Maybe<PageInfoNote>; - - search?: Maybe<string>; - - sort?: Maybe<SortNote>; -} -export interface GetAllPinnedEventsByTimelineIdQueryArgs { - timelineId: string; -} -export interface SourceQueryArgs { - /** The id of the source */ - id: string; -} -export interface GetOneTimelineQueryArgs { - id: string; -} -export interface GetAllTimelineQueryArgs { - pageInfo?: Maybe<PageInfoTimeline>; - - search?: Maybe<string>; - - sort?: Maybe<SortTimeline>; - - onlyUserFavorite?: Maybe<boolean>; -} -export interface AuthenticationsSourceArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface TimelineSourceArgs { - pagination: PaginationInput; - - sortField: SortField; - - fieldRequested: string[]; - - timerange?: Maybe<TimerangeInput>; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface TimelineDetailsSourceArgs { - eventId: string; - - indexName: string; - - defaultIndex: string[]; -} -export interface LastEventTimeSourceArgs { - id?: Maybe<string>; - - indexKey: LastEventIndexKey; - - details: LastTimeDetails; - - defaultIndex: string[]; -} -export interface HostsSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - sort: HostsSortField; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface HostOverviewSourceArgs { - id?: Maybe<string>; - - hostName: string; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface HostFirstLastSeenSourceArgs { - id?: Maybe<string>; - - hostName: string; - - defaultIndex: string[]; -} -export interface IpOverviewSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip: string; - - defaultIndex: string[]; -} -export interface UsersSourceArgs { - filterQuery?: Maybe<string>; - - id?: Maybe<string>; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: UsersSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface KpiNetworkSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface KpiHostsSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface KpiHostDetailsSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface MatrixHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField: string; - - histogramType: HistogramType; -} -export interface NetworkTopCountriesSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip?: Maybe<string>; - - flowTarget: FlowTargetSourceDest; - - pagination: PaginationInputPaginated; - - sort: NetworkTopTablesSortField; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface NetworkTopNFlowSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip?: Maybe<string>; - - flowTarget: FlowTargetSourceDest; - - pagination: PaginationInputPaginated; - - sort: NetworkTopTablesSortField; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface NetworkDnsSourceArgs { - filterQuery?: Maybe<string>; - - id?: Maybe<string>; - - isPtrIncluded: boolean; - - pagination: PaginationInputPaginated; - - sort: NetworkDnsSortField; - - stackByField?: Maybe<string>; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface NetworkDnsHistogramSourceArgs { - filterQuery?: Maybe<string>; - - defaultIndex: string[]; - - timerange: TimerangeInput; - - stackByField?: Maybe<string>; -} -export interface NetworkHttpSourceArgs { - id?: Maybe<string>; - - filterQuery?: Maybe<string>; - - ip?: Maybe<string>; - - pagination: PaginationInputPaginated; - - sort: NetworkHttpSortField; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface OverviewNetworkSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface OverviewHostSourceArgs { - id?: Maybe<string>; - - timerange: TimerangeInput; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface TlsSourceArgs { - filterQuery?: Maybe<string>; - - id?: Maybe<string>; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTargetSourceDest; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface UncommonProcessesSourceArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe<string>; - - defaultIndex: string[]; -} -export interface IndicesExistSourceStatusArgs { - defaultIndex: string[]; -} -export interface IndexFieldsSourceStatusArgs { - defaultIndex: string[]; -} -export interface PersistNoteMutationArgs { - noteId?: Maybe<string>; - - version?: Maybe<string>; - - note: NoteInput; -} -export interface DeleteNoteMutationArgs { - id: string[]; -} -export interface DeleteNoteByTimelineIdMutationArgs { - timelineId: string; - - version?: Maybe<string>; -} -export interface PersistPinnedEventOnTimelineMutationArgs { - pinnedEventId?: Maybe<string>; - - eventId: string; - - timelineId?: Maybe<string>; -} -export interface DeletePinnedEventOnTimelineMutationArgs { - id: string[]; -} -export interface DeleteAllPinnedEventsOnTimelineMutationArgs { - timelineId: string; -} -export interface PersistTimelineMutationArgs { - id?: Maybe<string>; - - version?: Maybe<string>; - - timeline: TimelineInput; -} -export interface PersistFavoriteMutationArgs { - timelineId?: Maybe<string>; -} -export interface DeleteTimelineMutationArgs { - id: string[]; -} - -// ==================================================== -// Documents -// ==================================================== - -export namespace GetAuthenticationsQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Authentications: Authentications; - }; - - export type Authentications = { - __typename?: 'AuthenticationsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'AuthenticationsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'AuthenticationItem'; - - _id: string; - - failures: number; - - successes: number; - - user: User; - - lastSuccess: Maybe<LastSuccess>; - - lastFailure: Maybe<LastFailure>; - }; - - export type User = { - __typename?: 'UserEcsFields'; - - name: Maybe<string[]>; - }; - - export type LastSuccess = { - __typename?: 'LastSourceHost'; - - timestamp: Maybe<string>; - - source: Maybe<_Source>; - - host: Maybe<Host>; - }; - - export type _Source = { - __typename?: 'SourceEcsFields'; - - ip: Maybe<string[]>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type LastFailure = { - __typename?: 'LastSourceHost'; - - timestamp: Maybe<string>; - - source: Maybe<__Source>; - - host: Maybe<_Host>; - }; - - export type __Source = { - __typename?: 'SourceEcsFields'; - - ip: Maybe<string[]>; - }; - - export type _Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetLastEventTimeQuery { - export type Variables = { - sourceId: string; - indexKey: LastEventIndexKey; - details: LastTimeDetails; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - LastEventTime: LastEventTime; - }; - - export type LastEventTime = { - __typename?: 'LastEventTimeData'; - - lastSeen: Maybe<string>; - }; -} - -export namespace GetHostFirstLastSeenQuery { - export type Variables = { - sourceId: string; - hostName: string; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - HostFirstLastSeen: HostFirstLastSeen; - }; - - export type HostFirstLastSeen = { - __typename?: 'FirstLastSeenHost'; - - firstSeen: Maybe<string>; - - lastSeen: Maybe<string>; - }; -} - -export namespace GetHostsTableQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - sort: HostsSortField; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Hosts: Hosts; - }; - - export type Hosts = { - __typename?: 'HostsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'HostsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'HostItem'; - - _id: Maybe<string>; - - lastSeen: Maybe<string>; - - host: Maybe<Host>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - - os: Maybe<Os>; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - name: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetHostOverviewQuery { - export type Variables = { - sourceId: string; - hostName: string; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - HostOverview: HostOverview; - }; - - export type HostOverview = { - __typename?: 'HostItem'; - - _id: Maybe<string>; - - host: Maybe<Host>; - - cloud: Maybe<Cloud>; - - inspect: Maybe<Inspect>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - architecture: Maybe<string[]>; - - id: Maybe<string[]>; - - ip: Maybe<string[]>; - - mac: Maybe<string[]>; - - name: Maybe<string[]>; - - os: Maybe<Os>; - - type: Maybe<string[]>; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - family: Maybe<string[]>; - - name: Maybe<string[]>; - - platform: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Cloud = { - __typename?: 'CloudFields'; - - instance: Maybe<Instance>; - - machine: Maybe<Machine>; - - provider: Maybe<(Maybe<string>)[]>; - - region: Maybe<(Maybe<string>)[]>; - }; - - export type Instance = { - __typename?: 'CloudInstance'; - - id: Maybe<(Maybe<string>)[]>; - }; - - export type Machine = { - __typename?: 'CloudMachine'; - - type: Maybe<(Maybe<string>)[]>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetIpOverviewQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe<string>; - ip: string; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - IpOverview: Maybe<IpOverview>; - }; - - export type IpOverview = { - __typename?: 'IpOverviewData'; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - host: Host; - - inspect: Maybe<Inspect>; - }; - - export type _Source = { - __typename?: 'Overview'; - - firstSeen: Maybe<string>; - - lastSeen: Maybe<string>; - - autonomousSystem: AutonomousSystem; - - geo: Geo; - }; - - export type AutonomousSystem = { - __typename?: 'AutonomousSystem'; - - number: Maybe<number>; - - organization: Maybe<Organization>; - }; - - export type Organization = { - __typename?: 'AutonomousSystemOrganization'; - - name: Maybe<string>; - }; - - export type Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - city_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - country_name: Maybe<string[]>; - - location: Maybe<Location>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Location = { - __typename?: 'Location'; - - lat: Maybe<number[]>; - - lon: Maybe<number[]>; - }; - - export type Destination = { - __typename?: 'Overview'; - - firstSeen: Maybe<string>; - - lastSeen: Maybe<string>; - - autonomousSystem: _AutonomousSystem; - - geo: _Geo; - }; - - export type _AutonomousSystem = { - __typename?: 'AutonomousSystem'; - - number: Maybe<number>; - - organization: Maybe<_Organization>; - }; - - export type _Organization = { - __typename?: 'AutonomousSystemOrganization'; - - name: Maybe<string>; - }; - - export type _Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - city_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - country_name: Maybe<string[]>; - - location: Maybe<_Location>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type _Location = { - __typename?: 'Location'; - - lat: Maybe<number[]>; - - lon: Maybe<number[]>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - architecture: Maybe<string[]>; - - id: Maybe<string[]>; - - ip: Maybe<string[]>; - - mac: Maybe<string[]>; - - name: Maybe<string[]>; - - os: Maybe<Os>; - - type: Maybe<string[]>; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - family: Maybe<string[]>; - - name: Maybe<string[]>; - - platform: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetKpiHostDetailsQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - KpiHostDetails: KpiHostDetails; - }; - - export type KpiHostDetails = { - __typename?: 'KpiHostDetailsData'; - - authSuccess: Maybe<number>; - - authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; - - authFailure: Maybe<number>; - - authFailureHistogram: Maybe<AuthFailureHistogram[]>; - - uniqueSourceIps: Maybe<number>; - - uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; - - uniqueDestinationIps: Maybe<number>; - - uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; - - inspect: Maybe<Inspect>; - }; - - export type AuthSuccessHistogram = KpiHostDetailsChartFields.Fragment; - - export type AuthFailureHistogram = KpiHostDetailsChartFields.Fragment; - - export type UniqueSourceIpsHistogram = KpiHostDetailsChartFields.Fragment; - - export type UniqueDestinationIpsHistogram = KpiHostDetailsChartFields.Fragment; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetKpiHostsQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - KpiHosts: KpiHosts; - }; - - export type KpiHosts = { - __typename?: 'KpiHostsData'; - - hosts: Maybe<number>; - - hostsHistogram: Maybe<HostsHistogram[]>; - - authSuccess: Maybe<number>; - - authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; - - authFailure: Maybe<number>; - - authFailureHistogram: Maybe<AuthFailureHistogram[]>; - - uniqueSourceIps: Maybe<number>; - - uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; - - uniqueDestinationIps: Maybe<number>; - - uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; - - inspect: Maybe<Inspect>; - }; - - export type HostsHistogram = KpiHostChartFields.Fragment; - - export type AuthSuccessHistogram = KpiHostChartFields.Fragment; - - export type AuthFailureHistogram = KpiHostChartFields.Fragment; - - export type UniqueSourceIpsHistogram = KpiHostChartFields.Fragment; - - export type UniqueDestinationIpsHistogram = KpiHostChartFields.Fragment; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetKpiNetworkQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - KpiNetwork: Maybe<KpiNetwork>; - }; - - export type KpiNetwork = { - __typename?: 'KpiNetworkData'; - - networkEvents: Maybe<number>; - - uniqueFlowId: Maybe<number>; - - uniqueSourcePrivateIps: Maybe<number>; - - uniqueSourcePrivateIpsHistogram: Maybe<UniqueSourcePrivateIpsHistogram[]>; - - uniqueDestinationPrivateIps: Maybe<number>; - - uniqueDestinationPrivateIpsHistogram: Maybe<UniqueDestinationPrivateIpsHistogram[]>; - - dnsQueries: Maybe<number>; - - tlsHandshakes: Maybe<number>; - - inspect: Maybe<Inspect>; - }; - - export type UniqueSourcePrivateIpsHistogram = KpiNetworkChartFields.Fragment; - - export type UniqueDestinationPrivateIpsHistogram = KpiNetworkChartFields.Fragment; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetMatrixHistogramQuery { - export type Variables = { - defaultIndex: string[]; - filterQuery?: Maybe<string>; - histogramType: HistogramType; - inspect: boolean; - sourceId: string; - stackByField: string; - timerange: TimerangeInput; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - MatrixHistogram: MatrixHistogram; - }; - - export type MatrixHistogram = { - __typename?: 'MatrixHistogramOverTimeData'; - - matrixHistogramData: MatrixHistogramData[]; - - totalCount: number; - - inspect: Maybe<Inspect>; - }; - - export type MatrixHistogramData = { - __typename?: 'MatrixOverTimeHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - - g: Maybe<string>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkDnsQuery { - export type Variables = { - defaultIndex: string[]; - filterQuery?: Maybe<string>; - inspect: boolean; - isPtrIncluded: boolean; - pagination: PaginationInputPaginated; - sort: NetworkDnsSortField; - sourceId: string; - stackByField?: Maybe<string>; - timerange: TimerangeInput; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkDns: NetworkDns; - }; - - export type NetworkDns = { - __typename?: 'NetworkDnsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkDnsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkDnsItem'; - - _id: Maybe<string>; - - dnsBytesIn: Maybe<number>; - - dnsBytesOut: Maybe<number>; - - dnsName: Maybe<string>; - - queryCount: Maybe<number>; - - uniqueDomains: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkHttpQuery { - export type Variables = { - sourceId: string; - ip?: Maybe<string>; - filterQuery?: Maybe<string>; - pagination: PaginationInputPaginated; - sort: NetworkHttpSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkHttp: NetworkHttp; - }; - - export type NetworkHttp = { - __typename?: 'NetworkHttpData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkHttpEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkHttpItem'; - - domains: string[]; - - lastHost: Maybe<string>; - - lastSourceIp: Maybe<string>; - - methods: string[]; - - path: Maybe<string>; - - requestCount: Maybe<number>; - - statuses: string[]; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkTopCountriesQuery { - export type Variables = { - sourceId: string; - ip?: Maybe<string>; - filterQuery?: Maybe<string>; - pagination: PaginationInputPaginated; - sort: NetworkTopTablesSortField; - flowTarget: FlowTargetSourceDest; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkTopCountries: NetworkTopCountries; - }; - - export type NetworkTopCountries = { - __typename?: 'NetworkTopCountriesData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkTopCountriesEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkTopCountriesItem'; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - network: Maybe<Network>; - }; - - export type _Source = { - __typename?: 'TopCountriesItemSource'; - - country: Maybe<string>; - - destination_ips: Maybe<number>; - - flows: Maybe<number>; - - source_ips: Maybe<number>; - }; - - export type Destination = { - __typename?: 'TopCountriesItemDestination'; - - country: Maybe<string>; - - destination_ips: Maybe<number>; - - flows: Maybe<number>; - - source_ips: Maybe<number>; - }; - - export type Network = { - __typename?: 'TopNetworkTablesEcsField'; - - bytes_in: Maybe<number>; - - bytes_out: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetNetworkTopNFlowQuery { - export type Variables = { - sourceId: string; - ip?: Maybe<string>; - filterQuery?: Maybe<string>; - pagination: PaginationInputPaginated; - sort: NetworkTopTablesSortField; - flowTarget: FlowTargetSourceDest; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - NetworkTopNFlow: NetworkTopNFlow; - }; - - export type NetworkTopNFlow = { - __typename?: 'NetworkTopNFlowData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'NetworkTopNFlowEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'NetworkTopNFlowItem'; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - network: Maybe<Network>; - }; - - export type _Source = { - __typename?: 'TopNFlowItemSource'; - - autonomous_system: Maybe<AutonomousSystem>; - - domain: Maybe<string[]>; - - ip: Maybe<string>; - - location: Maybe<Location>; - - flows: Maybe<number>; - - destination_ips: Maybe<number>; - }; - - export type AutonomousSystem = { - __typename?: 'AutonomousSystemItem'; - - name: Maybe<string>; - - number: Maybe<number>; - }; - - export type Location = { - __typename?: 'GeoItem'; - - geo: Maybe<Geo>; - - flowTarget: Maybe<FlowTargetSourceDest>; - }; - - export type Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Destination = { - __typename?: 'TopNFlowItemDestination'; - - autonomous_system: Maybe<_AutonomousSystem>; - - domain: Maybe<string[]>; - - ip: Maybe<string>; - - location: Maybe<_Location>; - - flows: Maybe<number>; - - source_ips: Maybe<number>; - }; - - export type _AutonomousSystem = { - __typename?: 'AutonomousSystemItem'; - - name: Maybe<string>; - - number: Maybe<number>; - }; - - export type _Location = { - __typename?: 'GeoItem'; - - geo: Maybe<_Geo>; - - flowTarget: Maybe<FlowTargetSourceDest>; - }; - - export type _Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Network = { - __typename?: 'TopNetworkTablesEcsField'; - - bytes_in: Maybe<number>; - - bytes_out: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetOverviewHostQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - OverviewHost: Maybe<OverviewHost>; - }; - - export type OverviewHost = { - __typename?: 'OverviewHostData'; - - auditbeatAuditd: Maybe<number>; - - auditbeatFIM: Maybe<number>; - - auditbeatLogin: Maybe<number>; - - auditbeatPackage: Maybe<number>; - - auditbeatProcess: Maybe<number>; - - auditbeatUser: Maybe<number>; - - endgameDns: Maybe<number>; - - endgameFile: Maybe<number>; - - endgameImageLoad: Maybe<number>; - - endgameNetwork: Maybe<number>; - - endgameProcess: Maybe<number>; - - endgameRegistry: Maybe<number>; - - endgameSecurity: Maybe<number>; - - filebeatSystemModule: Maybe<number>; - - winlogbeatSecurity: Maybe<number>; - - winlogbeatMWSysmonOperational: Maybe<number>; - - inspect: Maybe<Inspect>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetOverviewNetworkQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - OverviewNetwork: Maybe<OverviewNetwork>; - }; - - export type OverviewNetwork = { - __typename?: 'OverviewNetworkData'; - - auditbeatSocket: Maybe<number>; - - filebeatCisco: Maybe<number>; - - filebeatNetflow: Maybe<number>; - - filebeatPanw: Maybe<number>; - - filebeatSuricata: Maybe<number>; - - filebeatZeek: Maybe<number>; - - packetbeatDNS: Maybe<number>; - - packetbeatFlow: Maybe<number>; - - packetbeatTLS: Maybe<number>; - - inspect: Maybe<Inspect>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace SourceQuery { - export type Variables = { - sourceId?: Maybe<string>; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - status: Status; - }; - - export type Status = { - __typename?: 'SourceStatus'; - - indicesExist: boolean; - - indexFields: IndexFields[]; - }; - - export type IndexFields = { - __typename?: 'IndexField'; - - category: string; - - description: Maybe<string>; - - example: Maybe<string>; - - indexes: (Maybe<string>)[]; - - name: string; - - searchable: boolean; - - type: string; - - aggregatable: boolean; - - format: Maybe<string>; - }; -} - -export namespace GetAllTimeline { - export type Variables = { - pageInfo: PageInfoTimeline; - search?: Maybe<string>; - sort?: Maybe<SortTimeline>; - onlyUserFavorite?: Maybe<boolean>; - }; - - export type Query = { - __typename?: 'Query'; - - getAllTimeline: GetAllTimeline; - }; - - export type GetAllTimeline = { - __typename?: 'ResponseTimelines'; - - totalCount: Maybe<number>; - - timeline: (Maybe<Timeline>)[]; - }; - - export type Timeline = { - __typename?: 'TimelineResult'; - - savedObjectId: string; - - description: Maybe<string>; - - favorite: Maybe<Favorite[]>; - - eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; - - notes: Maybe<Notes[]>; - - noteIds: Maybe<string[]>; - - pinnedEventIds: Maybe<string[]>; - - title: Maybe<string>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: string; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; - - export type EventIdToNoteIds = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - timelineVersion: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type Notes = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; -} - -export namespace DeleteTimelineMutation { - export type Variables = { - id: string[]; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - deleteTimeline: boolean; - }; -} - -export namespace GetTimelineDetailsQuery { - export type Variables = { - sourceId: string; - eventId: string; - indexName: string; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - TimelineDetails: TimelineDetails; - }; - - export type TimelineDetails = { - __typename?: 'TimelineDetailsData'; - - data: Maybe<Data[]>; - }; - - export type Data = { - __typename?: 'DetailItem'; - - field: string; - - values: Maybe<string[]>; - - originalValue: Maybe<EsValue>; - }; -} - -export namespace PersistTimelineFavoriteMutation { - export type Variables = { - timelineId?: Maybe<string>; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistFavorite: PersistFavorite; - }; - - export type PersistFavorite = { - __typename?: 'ResponseFavoriteTimeline'; - - savedObjectId: string; - - version: string; - - favorite: Maybe<Favorite[]>; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; -} - -export namespace GetTimelineQuery { - export type Variables = { - sourceId: string; - fieldRequested: string[]; - pagination: PaginationInput; - sortField: SortField; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Timeline: Timeline; - }; - - export type Timeline = { - __typename?: 'TimelineData'; - - totalCount: number; - - inspect: Maybe<Inspect>; - - pageInfo: PageInfo; - - edges: Edges[]; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type PageInfo = { - __typename?: 'PageInfo'; - - endCursor: Maybe<EndCursor>; - - hasNextPage: Maybe<boolean>; - }; - - export type EndCursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - - tiebreaker: Maybe<string>; - }; - - export type Edges = { - __typename?: 'TimelineEdges'; - - node: Node; - }; - - export type Node = { - __typename?: 'TimelineItem'; - - _id: string; - - _index: Maybe<string>; - - data: Data[]; - - ecs: Ecs; - }; - - export type Data = { - __typename?: 'TimelineNonEcsData'; - - field: string; - - value: Maybe<string[]>; - }; - - export type Ecs = { - __typename?: 'ECS'; - - _id: string; - - _index: Maybe<string>; - - timestamp: Maybe<string>; - - message: Maybe<string[]>; - - system: Maybe<System>; - - event: Maybe<Event>; - - auditd: Maybe<Auditd>; - - file: Maybe<File>; - - host: Maybe<Host>; - - rule: Maybe<Rule>; - - source: Maybe<_Source>; - - destination: Maybe<Destination>; - - dns: Maybe<Dns>; - - endgame: Maybe<Endgame>; - - geo: Maybe<__Geo>; - - signal: Maybe<Signal>; - - suricata: Maybe<Suricata>; - - network: Maybe<Network>; - - http: Maybe<Http>; - - tls: Maybe<Tls>; - - url: Maybe<Url>; - - user: Maybe<User>; - - winlog: Maybe<Winlog>; - - process: Maybe<Process>; - - zeek: Maybe<Zeek>; - }; - - export type System = { - __typename?: 'SystemEcsField'; - - auth: Maybe<Auth>; - - audit: Maybe<Audit>; - }; - - export type Auth = { - __typename?: 'AuthEcsFields'; - - ssh: Maybe<Ssh>; - }; - - export type Ssh = { - __typename?: 'SshEcsFields'; - - signature: Maybe<string[]>; - - method: Maybe<string[]>; - }; - - export type Audit = { - __typename?: 'AuditEcsFields'; - - package: Maybe<Package>; - }; - - export type Package = { - __typename?: 'PackageEcsFields'; - - arch: Maybe<string[]>; - - entity_id: Maybe<string[]>; - - name: Maybe<string[]>; - - size: Maybe<number[]>; - - summary: Maybe<string[]>; - - version: Maybe<string[]>; - }; - - export type Event = { - __typename?: 'EventEcsFields'; - - action: Maybe<string[]>; - - category: Maybe<string[]>; - - code: Maybe<string[]>; - - created: Maybe<string[]>; - - dataset: Maybe<string[]>; - - duration: Maybe<number[]>; - - end: Maybe<string[]>; - - hash: Maybe<string[]>; - - id: Maybe<string[]>; - - kind: Maybe<string[]>; - - module: Maybe<string[]>; - - original: Maybe<string[]>; - - outcome: Maybe<string[]>; - - risk_score: Maybe<number[]>; - - risk_score_norm: Maybe<number[]>; - - severity: Maybe<number[]>; - - start: Maybe<string[]>; - - timezone: Maybe<string[]>; - - type: Maybe<string[]>; - }; - - export type Auditd = { - __typename?: 'AuditdEcsFields'; - - result: Maybe<string[]>; - - session: Maybe<string[]>; - - data: Maybe<_Data>; - - summary: Maybe<Summary>; - }; - - export type _Data = { - __typename?: 'AuditdData'; - - acct: Maybe<string[]>; - - terminal: Maybe<string[]>; - - op: Maybe<string[]>; - }; - - export type Summary = { - __typename?: 'Summary'; - - actor: Maybe<Actor>; - - object: Maybe<Object>; - - how: Maybe<string[]>; - - message_type: Maybe<string[]>; - - sequence: Maybe<string[]>; - }; - - export type Actor = { - __typename?: 'PrimarySecondary'; - - primary: Maybe<string[]>; - - secondary: Maybe<string[]>; - }; - - export type Object = { - __typename?: 'PrimarySecondary'; - - primary: Maybe<string[]>; - - secondary: Maybe<string[]>; - - type: Maybe<string[]>; - }; - - export type File = { - __typename?: 'FileFields'; - - name: Maybe<string[]>; - - path: Maybe<string[]>; - - target_path: Maybe<string[]>; - - extension: Maybe<string[]>; - - type: Maybe<string[]>; - - device: Maybe<string[]>; - - inode: Maybe<string[]>; - - uid: Maybe<string[]>; - - owner: Maybe<string[]>; - - gid: Maybe<string[]>; - - group: Maybe<string[]>; - - mode: Maybe<string[]>; - - size: Maybe<number[]>; - - mtime: Maybe<string[]>; - - ctime: Maybe<string[]>; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - - ip: Maybe<string[]>; - }; - - export type Rule = { - __typename?: 'RuleEcsField'; - - reference: Maybe<string[]>; - }; - - export type _Source = { - __typename?: 'SourceEcsFields'; - - bytes: Maybe<number[]>; - - ip: Maybe<string[]>; - - packets: Maybe<number[]>; - - port: Maybe<number[]>; - - geo: Maybe<Geo>; - }; - - export type Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Destination = { - __typename?: 'DestinationEcsFields'; - - bytes: Maybe<number[]>; - - ip: Maybe<string[]>; - - packets: Maybe<number[]>; - - port: Maybe<number[]>; - - geo: Maybe<_Geo>; - }; - - export type _Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe<string[]>; - - country_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - - city_name: Maybe<string[]>; - - region_iso_code: Maybe<string[]>; - - region_name: Maybe<string[]>; - }; - - export type Dns = { - __typename?: 'DnsEcsFields'; - - question: Maybe<Question>; - - resolved_ip: Maybe<string[]>; - - response_code: Maybe<string[]>; - }; - - export type Question = { - __typename?: 'DnsQuestionData'; - - name: Maybe<string[]>; - - type: Maybe<string[]>; - }; - - export type Endgame = { - __typename?: 'EndgameEcsFields'; - - exit_code: Maybe<number[]>; - - file_name: Maybe<string[]>; - - file_path: Maybe<string[]>; - - logon_type: Maybe<number[]>; - - parent_process_name: Maybe<string[]>; - - pid: Maybe<number[]>; - - process_name: Maybe<string[]>; - - subject_domain_name: Maybe<string[]>; - - subject_logon_id: Maybe<string[]>; - - subject_user_name: Maybe<string[]>; - - target_domain_name: Maybe<string[]>; - - target_logon_id: Maybe<string[]>; - - target_user_name: Maybe<string[]>; - }; - - export type __Geo = { - __typename?: 'GeoEcsFields'; - - region_name: Maybe<string[]>; - - country_iso_code: Maybe<string[]>; - }; - - export type Signal = { - __typename?: 'SignalField'; - - original_time: Maybe<string[]>; - - rule: Maybe<_Rule>; - }; - - export type _Rule = { - __typename?: 'RuleField'; - - id: Maybe<string[]>; - - saved_id: Maybe<string[]>; - - timeline_id: Maybe<string[]>; - - timeline_title: Maybe<string[]>; - - output_index: Maybe<string[]>; - - from: Maybe<string[]>; - - index: Maybe<string[]>; - - language: Maybe<string[]>; - - query: Maybe<string[]>; - - to: Maybe<string[]>; - - filters: Maybe<ToAny>; - - note: Maybe<string[]>; - }; - - export type Suricata = { - __typename?: 'SuricataEcsFields'; - - eve: Maybe<Eve>; - }; - - export type Eve = { - __typename?: 'SuricataEveData'; - - proto: Maybe<string[]>; - - flow_id: Maybe<number[]>; - - alert: Maybe<Alert>; - }; - - export type Alert = { - __typename?: 'SuricataAlertData'; - - signature: Maybe<string[]>; - - signature_id: Maybe<number[]>; - }; - - export type Network = { - __typename?: 'NetworkEcsField'; - - bytes: Maybe<number[]>; - - community_id: Maybe<string[]>; - - direction: Maybe<string[]>; - - packets: Maybe<number[]>; - - protocol: Maybe<string[]>; - - transport: Maybe<string[]>; - }; - - export type Http = { - __typename?: 'HttpEcsFields'; - - version: Maybe<string[]>; - - request: Maybe<Request>; - - response: Maybe<Response>; - }; - - export type Request = { - __typename?: 'HttpRequestData'; - - method: Maybe<string[]>; - - body: Maybe<Body>; - - referrer: Maybe<string[]>; - }; - - export type Body = { - __typename?: 'HttpBodyData'; - - bytes: Maybe<number[]>; - - content: Maybe<string[]>; - }; - - export type Response = { - __typename?: 'HttpResponseData'; - - status_code: Maybe<number[]>; - - body: Maybe<_Body>; - }; - - export type _Body = { - __typename?: 'HttpBodyData'; - - bytes: Maybe<number[]>; - - content: Maybe<string[]>; - }; - - export type Tls = { - __typename?: 'TlsEcsFields'; - - client_certificate: Maybe<ClientCertificate>; - - fingerprints: Maybe<Fingerprints>; - - server_certificate: Maybe<ServerCertificate>; - }; - - export type ClientCertificate = { - __typename?: 'TlsClientCertificateData'; - - fingerprint: Maybe<Fingerprint>; - }; - - export type Fingerprint = { - __typename?: 'FingerprintData'; - - sha1: Maybe<string[]>; - }; - - export type Fingerprints = { - __typename?: 'TlsFingerprintsData'; - - ja3: Maybe<Ja3>; - }; - - export type Ja3 = { - __typename?: 'TlsJa3Data'; - - hash: Maybe<string[]>; - }; - - export type ServerCertificate = { - __typename?: 'TlsServerCertificateData'; - - fingerprint: Maybe<_Fingerprint>; - }; - - export type _Fingerprint = { - __typename?: 'FingerprintData'; - - sha1: Maybe<string[]>; - }; - - export type Url = { - __typename?: 'UrlEcsFields'; - - original: Maybe<string[]>; - - domain: Maybe<string[]>; - - username: Maybe<string[]>; - - password: Maybe<string[]>; - }; - - export type User = { - __typename?: 'UserEcsFields'; - - domain: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type Winlog = { - __typename?: 'WinlogEcsFields'; - - event_id: Maybe<number[]>; - }; - - export type Process = { - __typename?: 'ProcessEcsFields'; - - hash: Maybe<Hash>; - - pid: Maybe<number[]>; - - name: Maybe<string[]>; - - ppid: Maybe<number[]>; - - args: Maybe<string[]>; - - executable: Maybe<string[]>; - - title: Maybe<string[]>; - - working_directory: Maybe<string[]>; - }; - - export type Hash = { - __typename?: 'ProcessHashData'; - - md5: Maybe<string[]>; - - sha1: Maybe<string[]>; - - sha256: Maybe<string[]>; - }; - - export type Zeek = { - __typename?: 'ZeekEcsFields'; - - session_id: Maybe<string[]>; - - connection: Maybe<Connection>; - - notice: Maybe<Notice>; - - dns: Maybe<_Dns>; - - http: Maybe<_Http>; - - files: Maybe<Files>; - - ssl: Maybe<Ssl>; - }; - - export type Connection = { - __typename?: 'ZeekConnectionData'; - - local_resp: Maybe<boolean[]>; - - local_orig: Maybe<boolean[]>; - - missed_bytes: Maybe<number[]>; - - state: Maybe<string[]>; - - history: Maybe<string[]>; - }; - - export type Notice = { - __typename?: 'ZeekNoticeData'; - - suppress_for: Maybe<number[]>; - - msg: Maybe<string[]>; - - note: Maybe<string[]>; - - sub: Maybe<string[]>; - - dst: Maybe<string[]>; - - dropped: Maybe<boolean[]>; - - peer_descr: Maybe<string[]>; - }; - - export type _Dns = { - __typename?: 'ZeekDnsData'; - - AA: Maybe<boolean[]>; - - qclass_name: Maybe<string[]>; - - RD: Maybe<boolean[]>; - - qtype_name: Maybe<string[]>; - - rejected: Maybe<boolean[]>; - - qtype: Maybe<string[]>; - - query: Maybe<string[]>; - - trans_id: Maybe<number[]>; - - qclass: Maybe<string[]>; - - RA: Maybe<boolean[]>; - - TC: Maybe<boolean[]>; - }; - - export type _Http = { - __typename?: 'ZeekHttpData'; - - resp_mime_types: Maybe<string[]>; - - trans_depth: Maybe<string[]>; - - status_msg: Maybe<string[]>; - - resp_fuids: Maybe<string[]>; - - tags: Maybe<string[]>; - }; - - export type Files = { - __typename?: 'ZeekFileData'; - - session_ids: Maybe<string[]>; - - timedout: Maybe<boolean[]>; - - local_orig: Maybe<boolean[]>; - - tx_host: Maybe<string[]>; - - source: Maybe<string[]>; - - is_orig: Maybe<boolean[]>; - - overflow_bytes: Maybe<number[]>; - - sha1: Maybe<string[]>; - - duration: Maybe<number[]>; - - depth: Maybe<number[]>; - - analyzers: Maybe<string[]>; - - mime_type: Maybe<string[]>; - - rx_host: Maybe<string[]>; - - total_bytes: Maybe<number[]>; - - fuid: Maybe<string[]>; - - seen_bytes: Maybe<number[]>; - - missing_bytes: Maybe<number[]>; - - md5: Maybe<string[]>; - }; - - export type Ssl = { - __typename?: 'ZeekSslData'; - - cipher: Maybe<string[]>; - - established: Maybe<boolean[]>; - - resumed: Maybe<boolean[]>; - - version: Maybe<string[]>; - }; -} - -export namespace PersistTimelineNoteMutation { - export type Variables = { - noteId?: Maybe<string>; - version?: Maybe<string>; - note: NoteInput; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistNote: PersistNote; - }; - - export type PersistNote = { - __typename?: 'ResponseNote'; - - code: Maybe<number>; - - message: Maybe<string>; - - note: Note; - }; - - export type Note = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; -} - -export namespace GetOneTimeline { - export type Variables = { - id: string; - }; - - export type Query = { - __typename?: 'Query'; - - getOneTimeline: GetOneTimeline; - }; - - export type GetOneTimeline = { - __typename?: 'TimelineResult'; - - savedObjectId: string; - - columns: Maybe<Columns[]>; - - dataProviders: Maybe<DataProviders[]>; - - dateRange: Maybe<DateRange>; - - description: Maybe<string>; - - eventType: Maybe<string>; - - eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; - - favorite: Maybe<Favorite[]>; - - filters: Maybe<Filters[]>; - - kqlMode: Maybe<string>; - - kqlQuery: Maybe<KqlQuery>; - - notes: Maybe<Notes[]>; - - noteIds: Maybe<string[]>; - - pinnedEventIds: Maybe<string[]>; - - pinnedEventsSaveObject: Maybe<PinnedEventsSaveObject[]>; - - title: Maybe<string>; - - savedQueryId: Maybe<string>; - - sort: Maybe<Sort>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: string; - }; - - export type Columns = { - __typename?: 'ColumnHeaderResult'; - - aggregatable: Maybe<boolean>; - - category: Maybe<string>; - - columnHeaderType: Maybe<string>; - - description: Maybe<string>; - - example: Maybe<string>; - - indexes: Maybe<string[]>; - - id: Maybe<string>; - - name: Maybe<string>; - - searchable: Maybe<boolean>; - - type: Maybe<string>; - }; - - export type DataProviders = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<QueryMatch>; - - and: Maybe<And[]>; - }; - - export type QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type And = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<_QueryMatch>; - }; - - export type _QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type DateRange = { - __typename?: 'DateRangePickerResult'; - - start: Maybe<number>; - - end: Maybe<number>; - }; - - export type EventIdToNoteIds = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - timelineVersion: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; - - export type Filters = { - __typename?: 'FilterTimelineResult'; - - meta: Maybe<Meta>; - - query: Maybe<string>; - - exists: Maybe<string>; - - match_all: Maybe<string>; - - missing: Maybe<string>; - - range: Maybe<string>; - - script: Maybe<string>; - }; - - export type Meta = { - __typename?: 'FilterMetaTimelineResult'; - - alias: Maybe<string>; - - controlledBy: Maybe<string>; - - disabled: Maybe<boolean>; - - field: Maybe<string>; - - formattedValue: Maybe<string>; - - index: Maybe<string>; - - key: Maybe<string>; - - negate: Maybe<boolean>; - - params: Maybe<string>; - - type: Maybe<string>; - - value: Maybe<string>; - }; - - export type KqlQuery = { - __typename?: 'SerializedFilterQueryResult'; - - filterQuery: Maybe<FilterQuery>; - }; - - export type FilterQuery = { - __typename?: 'SerializedKueryQueryResult'; - - kuery: Maybe<Kuery>; - - serializedQuery: Maybe<string>; - }; - - export type Kuery = { - __typename?: 'KueryFilterQueryResult'; - - kind: Maybe<string>; - - expression: Maybe<string>; - }; - - export type Notes = { - __typename?: 'NoteResult'; - - eventId: Maybe<string>; - - note: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - noteId: string; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type PinnedEventsSaveObject = { - __typename?: 'PinnedEvent'; - - pinnedEventId: string; - - eventId: Maybe<string>; - - timelineId: Maybe<string>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe<string>; - - sortDirection: Maybe<string>; - }; -} - -export namespace PersistTimelineMutation { - export type Variables = { - timelineId?: Maybe<string>; - version?: Maybe<string>; - timeline: TimelineInput; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistTimeline: PersistTimeline; - }; - - export type PersistTimeline = { - __typename?: 'ResponseTimeline'; - - code: Maybe<number>; - - message: Maybe<string>; - - timeline: Timeline; - }; - - export type Timeline = { - __typename?: 'TimelineResult'; - - savedObjectId: string; - - version: string; - - columns: Maybe<Columns[]>; - - dataProviders: Maybe<DataProviders[]>; - - description: Maybe<string>; - - eventType: Maybe<string>; - - favorite: Maybe<Favorite[]>; - - filters: Maybe<Filters[]>; - - kqlMode: Maybe<string>; - - kqlQuery: Maybe<KqlQuery>; - - title: Maybe<string>; - - dateRange: Maybe<DateRange>; - - savedQueryId: Maybe<string>; - - sort: Maybe<Sort>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - }; - - export type Columns = { - __typename?: 'ColumnHeaderResult'; - - aggregatable: Maybe<boolean>; - - category: Maybe<string>; - - columnHeaderType: Maybe<string>; - - description: Maybe<string>; - - example: Maybe<string>; - - indexes: Maybe<string[]>; - - id: Maybe<string>; - - name: Maybe<string>; - - searchable: Maybe<boolean>; - - type: Maybe<string>; - }; - - export type DataProviders = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<QueryMatch>; - - and: Maybe<And[]>; - }; - - export type QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type And = { - __typename?: 'DataProviderResult'; - - id: Maybe<string>; - - name: Maybe<string>; - - enabled: Maybe<boolean>; - - excluded: Maybe<boolean>; - - kqlQuery: Maybe<string>; - - queryMatch: Maybe<_QueryMatch>; - }; - - export type _QueryMatch = { - __typename?: 'QueryMatchResult'; - - field: Maybe<string>; - - displayField: Maybe<string>; - - value: Maybe<string>; - - displayValue: Maybe<string>; - - operator: Maybe<string>; - }; - - export type Favorite = { - __typename?: 'FavoriteTimelineResult'; - - fullName: Maybe<string>; - - userName: Maybe<string>; - - favoriteDate: Maybe<number>; - }; - - export type Filters = { - __typename?: 'FilterTimelineResult'; - - meta: Maybe<Meta>; - - query: Maybe<string>; - - exists: Maybe<string>; - - match_all: Maybe<string>; - - missing: Maybe<string>; - - range: Maybe<string>; - - script: Maybe<string>; - }; - - export type Meta = { - __typename?: 'FilterMetaTimelineResult'; - - alias: Maybe<string>; - - controlledBy: Maybe<string>; - - disabled: Maybe<boolean>; - - field: Maybe<string>; - - formattedValue: Maybe<string>; - - index: Maybe<string>; - - key: Maybe<string>; - - negate: Maybe<boolean>; - - params: Maybe<string>; - - type: Maybe<string>; - - value: Maybe<string>; - }; - - export type KqlQuery = { - __typename?: 'SerializedFilterQueryResult'; - - filterQuery: Maybe<FilterQuery>; - }; - - export type FilterQuery = { - __typename?: 'SerializedKueryQueryResult'; - - kuery: Maybe<Kuery>; - - serializedQuery: Maybe<string>; - }; - - export type Kuery = { - __typename?: 'KueryFilterQueryResult'; - - kind: Maybe<string>; - - expression: Maybe<string>; - }; - - export type DateRange = { - __typename?: 'DateRangePickerResult'; - - start: Maybe<number>; - - end: Maybe<number>; - }; - - export type Sort = { - __typename?: 'SortTimelineResult'; - - columnId: Maybe<string>; - - sortDirection: Maybe<string>; - }; -} - -export namespace PersistTimelinePinnedEventMutation { - export type Variables = { - pinnedEventId?: Maybe<string>; - eventId: string; - timelineId?: Maybe<string>; - }; - - export type Mutation = { - __typename?: 'Mutation'; - - persistPinnedEventOnTimeline: Maybe<PersistPinnedEventOnTimeline>; - }; - - export type PersistPinnedEventOnTimeline = { - __typename?: 'PinnedEvent'; - - pinnedEventId: string; - - eventId: Maybe<string>; - - timelineId: Maybe<string>; - - timelineVersion: Maybe<string>; - - created: Maybe<number>; - - createdBy: Maybe<string>; - - updated: Maybe<number>; - - updatedBy: Maybe<string>; - - version: Maybe<string>; - }; -} - -export namespace GetTlsQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe<string>; - flowTarget: FlowTargetSourceDest; - ip: string; - pagination: PaginationInputPaginated; - sort: TlsSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Tls: Tls; - }; - - export type Tls = { - __typename?: 'TlsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'TlsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'TlsNode'; - - _id: Maybe<string>; - - subjects: Maybe<string[]>; - - ja3: Maybe<string[]>; - - issuers: Maybe<string[]>; - - notAfter: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetUncommonProcessesQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - filterQuery?: Maybe<string>; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - UncommonProcesses: UncommonProcesses; - }; - - export type UncommonProcesses = { - __typename?: 'UncommonProcessesData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'UncommonProcessesEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'UncommonProcessItem'; - - _id: string; - - instances: number; - - process: Process; - - user: Maybe<User>; - - hosts: Hosts[]; - }; - - export type Process = { - __typename?: 'ProcessEcsFields'; - - args: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type User = { - __typename?: 'UserEcsFields'; - - id: Maybe<string[]>; - - name: Maybe<string[]>; - }; - - export type Hosts = { - __typename?: 'HostEcsFields'; - - name: Maybe<string[]>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetUsersQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe<string>; - flowTarget: FlowTarget; - ip: string; - pagination: PaginationInputPaginated; - sort: UsersSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Users: Users; - }; - - export type Users = { - __typename?: 'UsersData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe<Inspect>; - }; - - export type Edges = { - __typename?: 'UsersEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'UsersNode'; - - user: Maybe<User>; - }; - - export type User = { - __typename?: 'UsersItem'; - - name: Maybe<string>; - - id: Maybe<string[]>; - - groupId: Maybe<string[]>; - - groupName: Maybe<string[]>; - - count: Maybe<number>; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe<string>; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace KpiHostDetailsChartFields { - export type Fragment = { - __typename?: 'KpiHostHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - }; -} - -export namespace KpiHostChartFields { - export type Fragment = { - __typename?: 'KpiHostHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - }; -} - -export namespace KpiNetworkChartFields { - export type Fragment = { - __typename?: 'KpiNetworkHistogramData'; - - x: Maybe<number>; - - y: Maybe<number>; - }; -} diff --git a/x-pack/legacy/plugins/siem/public/hooks/types.ts b/x-pack/legacy/plugins/siem/public/hooks/types.ts deleted file mode 100644 index 301b8bd655333..0000000000000 --- a/x-pack/legacy/plugins/siem/public/hooks/types.ts +++ /dev/null @@ -1,15 +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 { SimpleSavedObject } from '../../../../../../src/core/public'; - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type IndexPatternSavedObjectAttributes = { title: string }; - -export type IndexPatternSavedObject = Pick< - SimpleSavedObject<IndexPatternSavedObjectAttributes>, - 'type' | 'id' | 'attributes' | '_version' ->; diff --git a/x-pack/legacy/plugins/siem/public/index.ts b/x-pack/legacy/plugins/siem/public/index.ts deleted file mode 100644 index 3a396a0637ea1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/index.ts +++ /dev/null @@ -1,9 +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 { Plugin, PluginInitializerContext } from './plugin'; - -export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); diff --git a/x-pack/legacy/plugins/siem/public/legacy.ts b/x-pack/legacy/plugins/siem/public/legacy.ts deleted file mode 100644 index b3a06a170bb80..0000000000000 --- a/x-pack/legacy/plugins/siem/public/legacy.ts +++ /dev/null @@ -1,16 +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 { npSetup, npStart } from 'ui/new_platform'; - -import { PluginInitializerContext } from '../../../../../src/core/public'; -import { plugin } from './'; -import { SetupPlugins, StartPlugins } from './plugin'; - -const pluginInstance = plugin({} as PluginInitializerContext); - -pluginInstance.setup(npSetup.core, (npSetup.plugins as unknown) as SetupPlugins); -pluginInstance.start(npStart.core, (npStart.plugins as unknown) as StartPlugins); diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/config.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/config.ts deleted file mode 100644 index baeb69b3f6943..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/config.ts +++ /dev/null @@ -1,36 +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 { CasesConfigurationMapping } from '../../containers/case/configure/types'; -import serviceNowLogo from './logos/servicenow.svg'; -import { Connector } from './types'; - -const connectors: Record<string, Connector> = { - '.servicenow': { - actionTypeId: '.servicenow', - logo: serviceNowLogo, - }, -}; - -const defaultMapping: CasesConfigurationMapping[] = [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, -]; - -export { connectors, defaultMapping }; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/index.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/index.ts deleted file mode 100644 index fdf337b5ef120..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { getActionType as serviceNowActionType } from './servicenow'; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx deleted file mode 100644 index 536798ffad41b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ /dev/null @@ -1,246 +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 React, { useCallback, ChangeEvent, useEffect } from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import { isEmpty, get } from 'lodash/fp'; - -import { - ActionConnectorFieldsProps, - ActionTypeModel, - ValidationResult, - ActionParamsProps, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/triggers_actions_ui/public/types'; - -import { FieldMapping } from '../../pages/case/components/configure_cases/field_mapping'; - -import * as i18n from './translations'; - -import { ServiceNowActionConnector } from './types'; -import { isUrlInvalid } from './validators'; - -import { connectors, defaultMapping } from './config'; -import { CasesConfigurationMapping } from '../../containers/case/configure/types'; - -const serviceNowDefinition = connectors['.servicenow']; - -interface ServiceNowActionParams { - message: string; -} - -interface Errors { - apiUrl: string[]; - username: string[]; - password: string[]; -} - -export function getActionType(): ActionTypeModel { - return { - id: serviceNowDefinition.actionTypeId, - iconClass: serviceNowDefinition.logo, - selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: i18n.SERVICENOW_TITLE, - validateConnector: (action: ServiceNowActionConnector): ValidationResult => { - const errors: Errors = { - apiUrl: [], - username: [], - password: [], - }; - - if (!action.config.apiUrl) { - errors.apiUrl = [...errors.apiUrl, i18n.SERVICENOW_API_URL_REQUIRED]; - } - - if (isUrlInvalid(action.config.apiUrl)) { - errors.apiUrl = [...errors.apiUrl, i18n.SERVICENOW_API_URL_INVALID]; - } - - if (!action.secrets.username) { - errors.username = [...errors.username, i18n.SERVICENOW_USERNAME_REQUIRED]; - } - - if (!action.secrets.password) { - errors.password = [...errors.password, i18n.SERVICENOW_PASSWORD_REQUIRED]; - } - - return { errors }; - }, - validateParams: (actionParams: ServiceNowActionParams): ValidationResult => { - return { errors: {} }; - }, - actionConnectorFields: ServiceNowConnectorFields, - actionParamsFields: ServiceNowParamsFields, - }; -} - -const ServiceNowConnectorFields: React.FunctionComponent<ActionConnectorFieldsProps< - ServiceNowActionConnector ->> = ({ action, editActionConfig, editActionSecrets, errors }) => { - /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. - * If we do, errors will be shown the first time the flyout is open even though the user did not - * interact with the form. Also, we would like to show errors for empty fields provided by the user. - /*/ - const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; - const { username, password } = action.secrets; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; - const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; - - /** - * We need to distinguish between the add flyout and the edit flyout. - * useEffect will run only once on component mount. - * This guarantees that the function below will run only once. - * On the first render of the component the apiUrl can be either undefined or filled. - * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. - */ - - useEffect(() => { - if (!isEmpty(apiUrl)) { - editActionSecrets('username', ''); - editActionSecrets('password', ''); - } - }, []); - - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: defaultMapping, - }); - } - - const handleOnChangeActionConfig = useCallback( - (key: string, evt: ChangeEvent<HTMLInputElement>) => editActionConfig(key, evt.target.value), - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (key === 'apiUrl' && action.config[key] == null) { - editActionConfig(key, ''); - } - }, - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, evt: ChangeEvent<HTMLInputElement>) => editActionSecrets(key, evt.target.value), - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (['username', 'password'].includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - [action.config] - ); - - return ( - <> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - id="apiUrl" - fullWidth - error={errors.apiUrl} - isInvalid={isApiUrlInvalid} - label={i18n.SERVICENOW_API_URL_LABEL} - > - <EuiFieldText - fullWidth - isInvalid={isApiUrlInvalid} - name="apiUrl" - value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined - data-test-subj="apiUrlFromInput" - placeholder="https://<instance>.service-now.com" - onChange={handleOnChangeActionConfig.bind(null, 'apiUrl')} - onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} - /> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="m" /> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - id="connector-servicenow-username" - fullWidth - error={errors.username} - isInvalid={isUsernameInvalid} - label={i18n.SERVICENOW_USERNAME_LABEL} - > - <EuiFieldText - fullWidth - isInvalid={isUsernameInvalid} - name="connector-servicenow-username" - value={username || ''} // Needed to prevent uncontrolled input error when value is undefined - data-test-subj="usernameFromInput" - onChange={handleOnChangeSecretConfig.bind(null, 'username')} - onBlur={handleOnBlurSecretConfig.bind(null, 'username')} - /> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="m" /> - <EuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - id="connector-servicenow-password" - fullWidth - error={errors.password} - isInvalid={isPasswordInvalid} - label={i18n.SERVICENOW_PASSWORD_LABEL} - > - <EuiFieldPassword - fullWidth - isInvalid={isPasswordInvalid} - name="connector-servicenow-password" - value={password || ''} // Needed to prevent uncontrolled input error when value is undefined - data-test-subj="passwordFromInput" - onChange={handleOnChangeSecretConfig.bind(null, 'password')} - onBlur={handleOnBlurSecretConfig.bind(null, 'password')} - /> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - <EuiSpacer size="l" /> - <EuiFlexGroup> - <EuiFlexItem> - <FieldMapping - disabled={true} - mapping={mapping as CasesConfigurationMapping[]} - onChangeMapping={handleOnChangeMappingConfig} - /> - </EuiFlexItem> - </EuiFlexGroup> - </> - ); -}; - -const ServiceNowParamsFields: React.FunctionComponent<ActionParamsProps< - ServiceNowActionParams ->> = ({ actionParams, editAction, index, errors }) => { - return null; -}; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts deleted file mode 100644 index ae2084120255c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/translations.ts +++ /dev/null @@ -1,70 +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 { i18n } from '@kbn/i18n'; - -export const SERVICENOW_DESC = i18n.translate( - 'xpack.siem.case.connectors.servicenow.selectMessageText', - { - defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', - } -); - -export const SERVICENOW_TITLE = i18n.translate( - 'xpack.siem.case.connectors.servicenow.actionTypeTitle', - { - defaultMessage: 'ServiceNow', - } -); - -export const SERVICENOW_API_URL_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel', - { - defaultMessage: 'URL', - } -); - -export const SERVICENOW_API_URL_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredApiUrlTextField', - { - defaultMessage: 'URL is required', - } -); - -export const SERVICENOW_API_URL_INVALID = i18n.translate( - 'xpack.siem.case.connectors.servicenow.invalidApiUrlTextField', - { - defaultMessage: 'URL is invalid', - } -); - -export const SERVICENOW_USERNAME_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.usernameTextFieldLabel', - { - defaultMessage: 'Username', - } -); - -export const SERVICENOW_USERNAME_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredUsernameTextField', - { - defaultMessage: 'Username is required', - } -); - -export const SERVICENOW_PASSWORD_LABEL = i18n.translate( - 'xpack.siem.case.connectors.servicenow.passwordTextFieldLabel', - { - defaultMessage: 'Password', - } -); - -export const SERVICENOW_PASSWORD_REQUIRED = i18n.translate( - 'xpack.siem.case.connectors.servicenow.requiredPasswordTextField', - { - defaultMessage: 'Password is required', - } -); diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts b/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts deleted file mode 100644 index 66326a6590deb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/types.ts +++ /dev/null @@ -1,23 +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. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - ConfigType, - SecretsType, -} from '../../../../../../plugins/actions/server/builtin_action_types/servicenow/types'; - -export interface ServiceNowActionConnector { - config: ConfigType; - secrets: SecretsType; -} - -export interface Connector { - actionTypeId: string; - logo: string; -} diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts b/x-pack/legacy/plugins/siem/public/lib/keury/index.ts deleted file mode 100644 index 53f845de48fb3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/keury/index.ts +++ /dev/null @@ -1,113 +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 { isEmpty, isString, flow } from 'lodash/fp'; -import { - EsQueryConfig, - Query, - Filter, - esQuery, - esKuery, - IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; - -import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; - -import { KueryFilterQuery } from '../../store'; - -export const convertKueryToElasticSearchQuery = ( - kueryExpression: string, - indexPattern?: IIndexPattern -) => { - try { - return kueryExpression - ? JSON.stringify( - esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) - ) - : ''; - } catch (err) { - return ''; - } -}; - -export const convertKueryToDslFilter = ( - kueryExpression: string, - indexPattern: IIndexPattern -): JsonObject => { - try { - return kueryExpression - ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) - : {}; - } catch (err) { - return {}; - } -}; - -export const escapeQueryValue = (val: number | string = ''): string | number => { - if (isString(val)) { - if (isEmpty(val)) { - return '""'; - } - return `"${escapeKuery(val)}"`; - } - - return val; -}; - -export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { - if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { - try { - esKuery.fromKueryExpression(kqlFilterQuery.expression); - } catch (err) { - return false; - } - } - return true; -}; - -const escapeWhitespace = (val: string) => - val - .replace(/\t/g, '\\t') - .replace(/\r/g, '\\r') - .replace(/\n/g, '\\n'); - -// See the SpecialCharacter rule in kuery.peg -const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string - -// See the Keyword rule in kuery.peg -const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); - -const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); - -export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); - -export const convertToBuildEsQuery = ({ - config, - indexPattern, - queries, - filters, -}: { - config: EsQueryConfig; - indexPattern: IIndexPattern; - queries: Query[]; - filters: Filter[]; -}) => { - try { - return JSON.stringify( - esQuery.buildEsQuery( - indexPattern, - queries, - filters.filter(f => f.meta.disabled === false), - { - ...config, - dateFormatTZ: undefined, - } - ) - ); - } catch (exp) { - return ''; - } -}; diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/__mocks__/index.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/__mocks__/index.ts deleted file mode 100644 index 227680d79912f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/__mocks__/index.ts +++ /dev/null @@ -1,23 +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 { - createKibanaContextProviderMock, - createUseUiSettingMock, - createUseUiSetting$Mock, - createUseKibanaMock, - createWithKibanaMock, -} from '../../../mock/kibana_react'; - -export const KibanaServices = { get: jest.fn() }; -export const useKibana = jest.fn(createUseKibanaMock()); -export const useUiSetting = jest.fn(createUseUiSettingMock()); -export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); -export const useTimeZone = jest.fn(); -export const useDateFormat = jest.fn(); -export const useBasePath = jest.fn(() => '/test/base/path'); -export const withKibana = jest.fn(createWithKibanaMock()); -export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/kibana_react.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/kibana_react.ts deleted file mode 100644 index 012a1cfef5da2..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/kibana_react.ts +++ /dev/null @@ -1,31 +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 { - KibanaContextProvider, - KibanaReactContextValue, - useKibana, - useUiSetting, - useUiSetting$, - withKibana, -} from '../../../../../../../src/plugins/kibana_react/public'; -import { StartServices } from '../../plugin'; - -export type KibanaContext = KibanaReactContextValue<StartServices>; -export interface WithKibanaProps { - kibana: KibanaContext; -} - -// eslint-disable-next-line react-hooks/rules-of-hooks -const typedUseKibana = () => useKibana<StartServices>(); - -export { - KibanaContextProvider, - typedUseKibana as useKibana, - useUiSetting, - useUiSetting$, - withKibana, -}; diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/services.ts b/x-pack/legacy/plugins/siem/public/lib/kibana/services.ts deleted file mode 100644 index 3a6a3f13dc5ce..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/services.ts +++ /dev/null @@ -1,27 +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 { StartServices } from '../../plugin'; - -type GlobalServices = Pick<StartServices, 'http' | 'uiSettings'>; - -export class KibanaServices { - private static services?: GlobalServices; - - public static init({ http, uiSettings }: StartServices) { - this.services = { http, uiSettings }; - } - - public static get(): GlobalServices { - if (!this.services) { - throw new Error( - 'Kibana services not set - are you trying to import this module from outside of the SIEM app?' - ); - } - - return this.services; - } -} diff --git a/x-pack/legacy/plugins/siem/public/lib/telemetry/index.ts b/x-pack/legacy/plugins/siem/public/lib/telemetry/index.ts deleted file mode 100644 index 856b7783a5367..0000000000000 --- a/x-pack/legacy/plugins/siem/public/lib/telemetry/index.ts +++ /dev/null @@ -1,56 +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 { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; - -import { SetupPlugins } from '../../plugin'; -export { telemetryMiddleware } from './middleware'; - -export { METRIC_TYPE }; - -type TrackFn = (type: UiStatsMetricType, event: string | string[], count?: number) => void; - -let _track: TrackFn; - -export const track: TrackFn = (type, event, count) => { - try { - _track(type, event, count); - } catch (error) { - // ignore failed tracking call - } -}; - -export const initTelemetry = (usageCollection: SetupPlugins['usageCollection'], appId: string) => { - try { - _track = usageCollection.reportUiStats.bind(null, appId); - } catch (error) { - // ignore failed setup here, as we'll just have an inert tracker - } -}; - -export enum TELEMETRY_EVENT { - // Detections - SIEM_RULE_ENABLED = 'siem_rule_enabled', - SIEM_RULE_DISABLED = 'siem_rule_disabled', - CUSTOM_RULE_ENABLED = 'custom_rule_enabled', - CUSTOM_RULE_DISABLED = 'custom_rule_disabled', - - // ML - SIEM_JOB_ENABLED = 'siem_job_enabled', - SIEM_JOB_DISABLED = 'siem_job_disabled', - CUSTOM_JOB_ENABLED = 'custom_job_enabled', - CUSTOM_JOB_DISABLED = 'custom_job_disabled', - JOB_ENABLE_FAILURE = 'job_enable_failure', - JOB_DISABLE_FAILURE = 'job_disable_failure', - - // Timeline - TIMELINE_OPENED = 'open_timeline', - TIMELINE_SAVED = 'timeline_saved', - TIMELINE_NAMED = 'timeline_named', - - // UI Interactions - TAB_CLICKED = 'tab_', -} diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_core.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_core.ts deleted file mode 100644 index cf3523a6260bb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_core.ts +++ /dev/null @@ -1,13 +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 { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers'; - -export const createKibanaCoreSetupMock = () => createUiNewPlatformMock().npSetup.core; -export const createKibanaPluginsSetupMock = () => createUiNewPlatformMock().npSetup.plugins; - -export const createKibanaCoreStartMock = () => createUiNewPlatformMock().npStart.core; -export const createKibanaPluginsStartMock = () => createUiNewPlatformMock().npStart.plugins; diff --git a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts b/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts deleted file mode 100644 index db7a931b3fb15..0000000000000 --- a/x-pack/legacy/plugins/siem/public/mock/kibana_react.ts +++ /dev/null @@ -1,108 +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. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; - -import { - DEFAULT_SIEM_TIME_RANGE, - DEFAULT_SIEM_REFRESH_INTERVAL, - DEFAULT_INDEX_KEY, - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, - DEFAULT_DARK_MODE, - DEFAULT_TIME_RANGE, - DEFAULT_REFRESH_RATE_INTERVAL, - DEFAULT_FROM, - DEFAULT_TO, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_BYTES_FORMAT, - DEFAULT_INDEX_PATTERN, -} from '../../../../../plugins/siem/common/constants'; -import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const mockUiSettings: Record<string, any> = { - [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, - [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, - [DEFAULT_SIEM_TIME_RANGE]: { - from: DEFAULT_FROM, - to: DEFAULT_TO, - }, - [DEFAULT_SIEM_REFRESH_INTERVAL]: { - pause: DEFAULT_INTERVAL_PAUSE, - value: DEFAULT_INTERVAL_VALUE, - }, - [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, - [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', - [DEFAULT_DARK_MODE]: false, -}; - -export const createUseUiSettingMock = () => <T extends unknown = string>( - key: string, - defaultValue?: T -): T => { - const result = mockUiSettings[key]; - - if (typeof result != null) return result; - - if (defaultValue != null) { - return defaultValue; - } - - throw new Error(`Unexpected config key: ${key}`); -}; - -export const createUseUiSetting$Mock = () => { - const useUiSettingMock = createUseUiSettingMock(); - - return <T extends unknown = string>( - key: string, - defaultValue?: T - ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; -}; - -export const createUseKibanaMock = () => { - const core = createKibanaCoreStartMock(); - const plugins = createKibanaPluginsStartMock(); - const useUiSetting = createUseUiSettingMock(); - - const services = { - ...core, - ...plugins, - uiSettings: { - ...core.uiSettings, - get: useUiSetting, - }, - }; - - return () => ({ services }); -}; - -export const createWithKibanaMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (Component: any) => (props: any) => { - return React.createElement(Component, { ...props, kibana }); - }; -}; - -export const createKibanaContextProviderMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ({ services, ...rest }: any) => - React.createElement(KibanaContextProvider, { - ...rest, - services: { ...kibana.services, ...services }, - }); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx deleted file mode 100644 index 74f6411f17fa0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.test.tsx +++ /dev/null @@ -1,144 +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 React from 'react'; -import { mount } from 'enzyme'; - -import { AddComment } from './'; -import { TestProviders } from '../../../../mock'; -import { getFormMock } from '../__mock__/form'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; - -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { usePostComment } from '../../../../containers/case/use_post_comment'; -import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { wait } from '../../../../lib/helpers'; -jest.mock( - '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); -jest.mock('../../../../containers/case/use_post_comment'); - -export const useFormMock = useForm as jest.Mock; - -const useInsertTimelineMock = useInsertTimeline as jest.Mock; -const usePostCommentMock = usePostComment as jest.Mock; - -const onCommentSaving = jest.fn(); -const onCommentPosted = jest.fn(); -const postComment = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); - -const addCommentProps = { - caseId: '1234', - disabled: false, - insertQuote: null, - onCommentSaving, - onCommentPosted, - showLoading: false, -}; - -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; - -const defaultPostCommment = { - isLoading: false, - isError: false, - postComment, -}; -const sampleData = { - comment: 'what a cool comment', -}; -describe('AddComment ', () => { - const formHookMock = getFormMock(sampleData); - - beforeEach(() => { - jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); - usePostCommentMock.mockImplementation(() => defaultPostCommment); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - }); - - it('should post comment on submit click', async () => { - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...addCommentProps} /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); - - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .simulate('click'); - await wait(); - expect(onCommentSaving).toBeCalled(); - expect(postComment).toBeCalledWith(sampleData, onCommentPosted); - expect(formHookMock.reset).toBeCalled(); - }); - - it('should render spinner and disable submit when loading', () => { - usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...{ ...addCommentProps, showLoading: true }} /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); - expect( - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .prop('isDisabled') - ).toBeTruthy(); - }); - - it('should disable submit button when disabled prop passed', () => { - usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...{ ...addCommentProps, disabled: true }} /> - </Router> - </TestProviders> - ); - expect( - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .prop('isDisabled') - ).toBeTruthy(); - }); - - it('should insert a quote if one is available', () => { - const sampleQuote = 'what a cool quote'; - mount( - <TestProviders> - <Router history={mockHistory}> - <AddComment {...{ ...addCommentProps, insertQuote: sampleQuote }} /> - </Router> - </TestProviders> - ); - - expect(formHookMock.setFieldValue).toBeCalledWith( - 'comment', - `${sampleData.comment}\n\n${sampleQuote}` - ); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx deleted file mode 100644 index eaba708948a99..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ /dev/null @@ -1,114 +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 { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import styled from 'styled-components'; - -import { CommentRequest } from '../../../../../../../../plugins/case/common/api'; -import { usePostComment } from '../../../../containers/case/use_post_comment'; -import { Case } from '../../../../containers/case/types'; -import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; -import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { Form, useForm, UseField } from '../../../../shared_imports'; - -import * as i18n from '../../translations'; -import { schema } from './schema'; - -const MySpinner = styled(EuiLoadingSpinner)` - position: absolute; - top: 50%; - left: 50%; -`; - -const initialCommentValue: CommentRequest = { - comment: '', -}; - -interface AddCommentProps { - caseId: string; - disabled?: boolean; - insertQuote: string | null; - onCommentSaving?: () => void; - onCommentPosted: (newCase: Case) => void; - showLoading?: boolean; -} - -export const AddComment = React.memo<AddCommentProps>( - ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { - const { isLoading, postComment } = usePostComment(caseId); - const { form } = useForm<CommentRequest>({ - defaultValue: initialCommentValue, - options: { stripEmptyFields: false }, - schema, - }); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CommentRequest>( - form, - 'comment' - ); - - useEffect(() => { - if (insertQuote !== null) { - const { comment } = form.getFormData(); - form.setFieldValue( - 'comment', - `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` - ); - } - }, [insertQuote]); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - if (onCommentSaving != null) { - onCommentSaving(); - } - await postComment(data, onCommentPosted); - form.reset(); - } - }, [form, onCommentPosted, onCommentSaving]); - return ( - <span id="add-comment-permLink"> - {isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />} - <Form form={form}> - <UseField - path="comment" - component={MarkdownEditorForm} - componentProps={{ - idAria: 'caseComment', - isDisabled: isLoading, - dataTestSubj: 'add-comment', - placeholder: i18n.ADD_COMMENT_HELP_TEXT, - onCursorPositionUpdate: handleCursorChange, - bottomRightContent: ( - <EuiButton - data-test-subj="submit-comment" - iconType="plusInCircle" - isDisabled={isLoading || disabled} - isLoading={isLoading} - onClick={onSubmit} - size="s" - > - {i18n.ADD_COMMENT} - </EuiButton> - ), - topRightContent: ( - <InsertTimelinePopover - hideUntitled={true} - isDisabled={isLoading} - onTimelineChange={handleOnTimelineChange} - /> - ), - }} - /> - </Form> - </span> - ); - } -); - -AddComment.displayName = 'AddComment'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx deleted file mode 100644 index c61874a8dabfc..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/schema.tsx +++ /dev/null @@ -1,22 +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 { CommentRequest } from '../../../../../../../../plugins/case/common/api'; -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; -import * as i18n from '../../translations'; - -const { emptyField } = fieldValidators; - -export const schema: FormSchema<CommentRequest> = { - comment: { - type: FIELD_TYPES.TEXTAREA, - validations: [ - { - validator: emptyField(i18n.COMMENT_REQUIRED), - }, - ], - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx deleted file mode 100644 index e48e5cb0c5959..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ /dev/null @@ -1,205 +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 React, { useCallback } from 'react'; -import { - EuiBadge, - EuiTableFieldDataColumnType, - EuiTableComputedColumnType, - EuiTableActionsColumnType, - EuiAvatar, - EuiLink, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; -import { getEmptyTagValue } from '../../../../components/empty_value'; -import { Case } from '../../../../containers/case/types'; -import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; -import { CaseDetailsLink } from '../../../../components/links'; -import { TruncatableText } from '../../../../components/truncatable_text'; -import * as i18n from './translations'; - -export type CasesColumns = - | EuiTableFieldDataColumnType<Case> - | EuiTableComputedColumnType<Case> - | EuiTableActionsColumnType<Case>; - -const MediumShadeText = styled.p` - color: ${({ theme }) => theme.eui.euiColorMediumShade}; -`; - -const Spacer = styled.span` - margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; -`; - -const renderStringField = (field: string, dataTestSubj: string) => - field != null ? <span data-test-subj={dataTestSubj}>{field}</span> : getEmptyTagValue(); - -export const getCasesColumns = ( - actions: Array<DefaultItemIconButtonAction<Case>>, - filterStatus: string -): CasesColumns[] => [ - { - name: i18n.NAME, - render: (theCase: Case) => { - if (theCase.id != null && theCase.title != null) { - const caseDetailsLinkComponent = ( - <CaseDetailsLink detailName={theCase.id} title={theCase.title}> - {theCase.title} - </CaseDetailsLink> - ); - return theCase.status === 'open' ? ( - caseDetailsLinkComponent - ) : ( - <> - <MediumShadeText> - {caseDetailsLinkComponent} - <Spacer>{i18n.CLOSED}</Spacer> - </MediumShadeText> - </> - ); - } - return getEmptyTagValue(); - }, - }, - { - field: 'createdBy', - name: i18n.REPORTER, - render: (createdBy: Case['createdBy']) => { - if (createdBy != null) { - return ( - <> - <EuiAvatar - className="userAction__circle" - name={createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} - size="s" - /> - <Spacer data-test-subj="case-table-column-createdBy"> - {createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} - </Spacer> - </> - ); - } - return getEmptyTagValue(); - }, - }, - { - field: 'tags', - name: i18n.TAGS, - render: (tags: Case['tags']) => { - if (tags != null && tags.length > 0) { - return ( - <TruncatableText> - {tags.map((tag: string, i: number) => ( - <EuiBadge - color="hollow" - key={`${tag}-${i}`} - data-test-subj={`case-table-column-tags-${i}`} - > - {tag} - </EuiBadge> - ))} - </TruncatableText> - ); - } - return getEmptyTagValue(); - }, - truncateText: true, - }, - { - align: 'right', - field: 'totalComment', - name: i18n.COMMENTS, - sortable: true, - render: (totalComment: Case['totalComment']) => - totalComment != null - ? renderStringField(`${totalComment}`, `case-table-column-commentCount`) - : getEmptyTagValue(), - }, - filterStatus === 'open' - ? { - field: 'createdAt', - name: i18n.OPENED_ON, - sortable: true, - render: (createdAt: Case['createdAt']) => { - if (createdAt != null) { - return ( - <FormattedRelativePreferenceDate - value={createdAt} - data-test-subj={`case-table-column-createdAt`} - /> - ); - } - return getEmptyTagValue(); - }, - } - : { - field: 'closedAt', - name: i18n.CLOSED_ON, - sortable: true, - render: (closedAt: Case['closedAt']) => { - if (closedAt != null) { - return ( - <FormattedRelativePreferenceDate - value={closedAt} - data-test-subj={`case-table-column-closedAt`} - /> - ); - } - return getEmptyTagValue(); - }, - }, - { - name: i18n.SERVICENOW_INCIDENT, - render: (theCase: Case) => { - if (theCase.id != null) { - return <ServiceNowColumn theCase={theCase} />; - } - return getEmptyTagValue(); - }, - }, - { - name: i18n.ACTIONS, - actions, - }, -]; - -interface Props { - theCase: Case; -} - -export const ServiceNowColumn: React.FC<Props> = ({ theCase }) => { - const handleRenderDataToPush = useCallback(() => { - const lastCaseUpdate = theCase.updatedAt != null ? new Date(theCase.updatedAt) : null; - const lastCasePush = - theCase.externalService?.pushedAt != null - ? new Date(theCase.externalService?.pushedAt) - : null; - const hasDataToPush = - lastCasePush === null || - (lastCasePush != null && - lastCaseUpdate != null && - lastCasePush.getTime() < lastCaseUpdate?.getTime()); - return ( - <p> - <EuiLink - data-test-subj={`case-table-column-external`} - href={theCase.externalService?.externalUrl} - target="_blank" - aria-label={i18n.SERVICENOW_LINK_ARIA} - > - {theCase.externalService?.externalTitle} - </EuiLink> - {hasDataToPush - ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) - : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} - </p> - ); - }, [theCase]); - if (theCase.externalService !== null) { - return handleRenderDataToPush(); - } - return renderStringField(i18n.NOT_PUSHED, `case-table-column-external-notPushed`); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx deleted file mode 100644 index 58d0c1b0faaf3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ /dev/null @@ -1,346 +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 React from 'react'; -import { mount } from 'enzyme'; -import moment from 'moment-timezone'; -import { AllCases } from './'; -import { TestProviders } from '../../../../mock'; -import { useGetCasesMockState } from '../../../../containers/case/mock'; -import * as i18n from './translations'; - -import { getEmptyTagValue } from '../../../../components/empty_value'; -import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; -import { useGetCases } from '../../../../containers/case/use_get_cases'; -import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; -import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; -import { getCasesColumns } from './columns'; -jest.mock('../../../../containers/case/use_bulk_update_case'); -jest.mock('../../../../containers/case/use_delete_cases'); -jest.mock('../../../../containers/case/use_get_cases'); -jest.mock('../../../../containers/case/use_get_cases_status'); -const useDeleteCasesMock = useDeleteCases as jest.Mock; -const useGetCasesMock = useGetCases as jest.Mock; -const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; -const useUpdateCasesMock = useUpdateCases as jest.Mock; - -describe('AllCases', () => { - const dispatchResetIsDeleted = jest.fn(); - const dispatchResetIsUpdated = jest.fn(); - const dispatchUpdateCaseProperty = jest.fn(); - const handleOnDeleteConfirm = jest.fn(); - const handleToggleModal = jest.fn(); - const refetchCases = jest.fn(); - const setFilters = jest.fn(); - const setQueryParams = jest.fn(); - const setSelectedCases = jest.fn(); - const updateBulkStatus = jest.fn(); - const fetchCasesStatus = jest.fn(); - const emptyTag = getEmptyTagValue().props.children; - - const defaultGetCases = { - ...useGetCasesMockState, - dispatchUpdateCaseProperty, - refetchCases, - setFilters, - setQueryParams, - setSelectedCases, - }; - const defaultDeleteCases = { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isDeleted: false, - isDisplayConfirmDeleteModal: false, - isLoading: false, - }; - const defaultCasesStatus = { - countClosedCases: 0, - countOpenCases: 5, - fetchCasesStatus, - isError: false, - isLoading: true, - }; - const defaultUpdateCases = { - isUpdated: false, - isLoading: false, - isError: false, - dispatchResetIsUpdated, - updateBulkStatus, - }; - /* eslint-disable no-console */ - // Silence until enzyme fixed to use ReactTestUtils.act() - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ - beforeEach(() => { - jest.resetAllMocks(); - useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); - useGetCasesMock.mockImplementation(() => defaultGetCases); - useDeleteCasesMock.mockImplementation(() => defaultDeleteCases); - useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); - moment.tz.setDefault('UTC'); - }); - it('should render AllCases', () => { - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - expect( - wrapper - .find(`a[data-test-subj="case-details-link"]`) - .first() - .prop('href') - ).toEqual( - `#/link-to/case/${useGetCasesMockState.data.cases[0].id}?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))` - ); - expect( - wrapper - .find(`a[data-test-subj="case-details-link"]`) - .first() - .text() - ).toEqual(useGetCasesMockState.data.cases[0].title); - expect( - wrapper - .find(`span[data-test-subj="case-table-column-tags-0"]`) - .first() - .prop('title') - ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); - expect( - wrapper - .find(`[data-test-subj="case-table-column-createdBy"]`) - .first() - .text() - ).toEqual(useGetCasesMockState.data.cases[0].createdBy.fullName); - expect( - wrapper - .find(`[data-test-subj="case-table-column-createdAt"]`) - .first() - .prop('value') - ).toEqual(useGetCasesMockState.data.cases[0].createdAt); - expect( - wrapper - .find(`[data-test-subj="case-table-case-count"]`) - .first() - .text() - ).toEqual('Showing 10 cases'); - }); - it('should render empty fields', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - data: { - ...defaultGetCases.data, - cases: [ - { - ...defaultGetCases.data.cases[0], - id: null, - createdAt: null, - createdBy: null, - tags: null, - title: null, - totalComment: null, - }, - ], - }, - })); - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - const checkIt = (columnName: string, key: number) => { - const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key); - if (columnName === i18n.ACTIONS) { - return; - } - expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName); - expect(column.find('span').text()).toEqual(emptyTag); - }; - getCasesColumns([], 'open').map((i, key) => i.name != null && checkIt(`${i.name}`, key)); - }); - it('should tableHeaderSortButton AllCases', () => { - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - wrapper - .find('[data-test-subj="tableHeaderSortButton"]') - .first() - .simulate('click'); - expect(setQueryParams).toBeCalledWith({ - page: 1, - perPage: 5, - sortField: 'createdAt', - sortOrder: 'asc', - }); - }); - it('closes case when row action icon clicked', () => { - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - wrapper - .find('[data-test-subj="action-close"]') - .first() - .simulate('click'); - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty).toBeCalledWith({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: 'closed', - refetchCasesStatus: fetchCasesStatus, - version: firstCase.version, - }); - }); - it('opens case when row action icon clicked', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' }, - })); - - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - wrapper - .find('[data-test-subj="action-open"]') - .first() - .simulate('click'); - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty).toBeCalledWith({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: 'open', - refetchCasesStatus: fetchCasesStatus, - version: firstCase.version, - }); - }); - it('Bulk delete', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - selectedCases: useGetCasesMockState.data.cases, - })); - useDeleteCasesMock - .mockReturnValueOnce({ - ...defaultDeleteCases, - isDisplayConfirmDeleteModal: false, - }) - .mockReturnValue({ - ...defaultDeleteCases, - isDisplayConfirmDeleteModal: true, - }); - - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - wrapper - .find('[data-test-subj="case-table-bulk-actions"] button') - .first() - .simulate('click'); - wrapper - .find('[data-test-subj="cases-bulk-delete-button"]') - .first() - .simulate('click'); - expect(handleToggleModal).toBeCalled(); - - wrapper - .find( - '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' - ) - .last() - .simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(({ id }) => ({ id })) - ); - }); - it('Bulk close status update', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - selectedCases: useGetCasesMockState.data.cases, - })); - - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - wrapper - .find('[data-test-subj="case-table-bulk-actions"] button') - .first() - .simulate('click'); - wrapper - .find('[data-test-subj="cases-bulk-close-button"]') - .first() - .simulate('click'); - expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); - }); - it('Bulk open status update', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - selectedCases: useGetCasesMockState.data.cases, - filterOptions: { - ...defaultGetCases.filterOptions, - status: 'closed', - }, - })); - - const wrapper = mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - wrapper - .find('[data-test-subj="case-table-bulk-actions"] button') - .first() - .simulate('click'); - wrapper - .find('[data-test-subj="cases-bulk-open-button"]') - .first() - .simulate('click'); - expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); - }); - it('isDeleted is true, refetch', () => { - useDeleteCasesMock.mockImplementation(() => ({ - ...defaultDeleteCases, - isDeleted: true, - })); - - mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - expect(refetchCases).toBeCalled(); - expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsDeleted).toBeCalled(); - }); - it('isUpdated is true, refetch', () => { - useUpdateCasesMock.mockImplementation(() => ({ - ...defaultUpdateCases, - isUpdated: true, - })); - - mount( - <TestProviders> - <AllCases userCanCrud={true} /> - </TestProviders> - ); - expect(refetchCases).toBeCalled(); - expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsUpdated).toBeCalled(); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx deleted file mode 100644 index a6eca717a82a3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ /dev/null @@ -1,436 +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 React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - EuiBasicTable, - EuiButton, - EuiContextMenuPanel, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingContent, - EuiProgress, - EuiTableSortingType, -} from '@elastic/eui'; -import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; -import { isEmpty } from 'lodash/fp'; -import styled, { css } from 'styled-components'; -import * as i18n from './translations'; - -import { getCasesColumns } from './columns'; -import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; -import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases'; -import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; -import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; -import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { Panel } from '../../../../components/panel'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../components/utility_bar'; -import { getCreateCaseUrl } from '../../../../components/link_to'; -import { getBulkItems } from '../bulk_actions'; -import { CaseHeaderPage } from '../case_header_page'; -import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; -import { OpenClosedStats } from '../open_closed_stats'; -import { navTabs } from '../../../home/home_navigations'; - -import { getActions } from './actions'; -import { CasesTableFilters } from './table_filters'; -import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { getActionLicenseError } from '../use_push_to_service/helpers'; -import { CaseCallOut } from '../callout'; -import { ConfigureCaseButton } from '../configure_cases/button'; -import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; - -const Div = styled.div` - margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; -`; -const FlexItemDivider = styled(EuiFlexItem)` - ${({ theme }) => css` - .euiFlexGroup--gutterMedium > &.euiFlexItem { - border-right: ${theme.eui.euiBorderThin}; - padding-right: ${theme.eui.euiSize}; - margin-right: ${theme.eui.euiSize}; - } - `} -`; - -const ProgressLoader = styled(EuiProgress)` - ${({ theme }) => css` - top: 2px; - border-radius: ${theme.eui.euiBorderRadius}; - z-index: ${theme.eui.euiZHeader}; - `} -`; - -const getSortField = (field: string): SortFieldCase => { - if (field === SortFieldCase.createdAt) { - return SortFieldCase.createdAt; - } else if (field === SortFieldCase.closedAt) { - return SortFieldCase.closedAt; - } - return SortFieldCase.createdAt; -}; - -interface AllCasesProps { - userCanCrud: boolean; -} -export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => { - const urlSearch = useGetUrlSearch(navTabs.case); - const { actionLicense } = useGetActionLicense(); - const { - countClosedCases, - countOpenCases, - isLoading: isCasesStatusLoading, - fetchCasesStatus, - } = useGetCasesStatus(); - const { - data, - dispatchUpdateCaseProperty, - filterOptions, - loading, - queryParams, - selectedCases, - refetchCases, - setFilters, - setQueryParams, - setSelectedCases, - } = useGetCases(); - - // Delete case - const { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isLoading: isDeleting, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); - - // Update case - const { - dispatchResetIsUpdated, - isLoading: isUpdating, - isUpdated, - updateBulkStatus, - } = useUpdateCases(); - const [deleteThisCase, setDeleteThisCase] = useState({ - title: '', - id: '', - }); - const [deleteBulk, setDeleteBulk] = useState<DeleteCase[]>([]); - const filterRefetch = useRef<() => void>(); - const setFilterRefetch = useCallback( - (refetchFilter: () => void) => { - filterRefetch.current = refetchFilter; - }, - [filterRefetch.current] - ); - const refreshCases = useCallback( - (dataRefresh = true) => { - if (dataRefresh) refetchCases(); - fetchCasesStatus(); - setSelectedCases([]); - setDeleteBulk([]); - if (filterRefetch.current != null) { - filterRefetch.current(); - } - }, - [filterOptions, queryParams, filterRefetch.current] - ); - - useEffect(() => { - if (isDeleted) { - refreshCases(); - dispatchResetIsDeleted(); - } - if (isUpdated) { - refreshCases(); - dispatchResetIsUpdated(); - } - }, [isDeleted, isUpdated]); - const confirmDeleteModal = useMemo( - () => ( - <ConfirmDeleteCaseModal - caseTitle={deleteThisCase.title} - isModalVisible={isDisplayConfirmDeleteModal} - isPlural={deleteBulk.length > 0} - onCancel={handleToggleModal} - onConfirm={handleOnDeleteConfirm.bind( - null, - deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] - )} - /> - ), - [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] - ); - - const toggleDeleteModal = useCallback((deleteCase: Case) => { - handleToggleModal(); - setDeleteThisCase(deleteCase); - }, []); - - const toggleBulkDeleteModal = useCallback( - (caseIds: string[]) => { - handleToggleModal(); - if (caseIds.length === 1) { - const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); - if (singleCase) { - return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); - } - } - const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); - setDeleteBulk(convertToDeleteCases); - }, - [selectedCases] - ); - - const handleUpdateCaseStatus = useCallback( - (status: string) => { - updateBulkStatus(selectedCases, status); - }, - [selectedCases] - ); - - const selectedCaseIds = useMemo( - (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), - [selectedCases] - ); - - const getBulkItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - <EuiContextMenuPanel - data-test-subj="cases-bulk-actions" - items={getBulkItems({ - caseStatus: filterOptions.status, - closePopover, - deleteCasesAction: toggleBulkDeleteModal, - selectedCaseIds, - updateCaseStatus: handleUpdateCaseStatus, - })} - /> - ), - [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal] - ); - const handleDispatchUpdate = useCallback( - (args: Omit<UpdateCase, 'refetchCasesStatus'>) => { - dispatchUpdateCaseProperty({ ...args, refetchCasesStatus: fetchCasesStatus }); - }, - [dispatchUpdateCaseProperty, fetchCasesStatus] - ); - - const actions = useMemo( - () => - getActions({ - caseStatus: filterOptions.status, - deleteCaseOnClick: toggleDeleteModal, - dispatchUpdate: handleDispatchUpdate, - }), - [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] - ); - - const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); - - const tableOnChangeCallback = useCallback( - ({ page, sort }: EuiBasicTableOnChange) => { - let newQueryParams = queryParams; - if (sort) { - newQueryParams = { - ...newQueryParams, - sortField: getSortField(sort.field), - sortOrder: sort.direction, - }; - } - if (page) { - newQueryParams = { - ...newQueryParams, - page: page.index + 1, - perPage: page.size, - }; - } - setQueryParams(newQueryParams); - refreshCases(false); - }, - [queryParams] - ); - - const onFilterChangedCallback = useCallback( - (newFilterOptions: Partial<FilterOptions>) => { - if (newFilterOptions.status && newFilterOptions.status === 'closed') { - setQueryParams({ sortField: SortFieldCase.closedAt }); - } else if (newFilterOptions.status && newFilterOptions.status === 'open') { - setQueryParams({ sortField: SortFieldCase.createdAt }); - } - setFilters(newFilterOptions); - refreshCases(false); - }, - [filterOptions, queryParams] - ); - - const memoizedGetCasesColumns = useMemo( - () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status), - [actions, filterOptions.status, userCanCrud] - ); - const memoizedPagination = useMemo( - () => ({ - pageIndex: queryParams.page - 1, - pageSize: queryParams.perPage, - totalItemCount: data.total, - pageSizeOptions: [5, 10, 15, 20, 25], - }), - [data, queryParams] - ); - - const sorting: EuiTableSortingType<Case> = { - sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, - }; - const euiBasicTableSelectionProps = useMemo<EuiTableSelectionType<Case>>( - () => ({ onSelectionChange: setSelectedCases }), - [selectedCases] - ); - const isCasesLoading = useMemo( - () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, - [loading] - ); - const isDataEmpty = useMemo(() => data.total === 0, [data]); - - return ( - <> - {!isEmpty(actionsErrors) && ( - <CaseCallOut title={ERROR_PUSH_SERVICE_CALLOUT_TITLE} messages={actionsErrors} /> - )} - <CaseHeaderPage title={i18n.PAGE_TITLE}> - <EuiFlexGroup alignItems="center" gutterSize="m" responsive={false} wrap={true}> - <EuiFlexItem grow={false}> - <OpenClosedStats - caseCount={countOpenCases} - caseStatus={'open'} - isLoading={isCasesStatusLoading} - /> - </EuiFlexItem> - <FlexItemDivider grow={false}> - <OpenClosedStats - caseCount={countClosedCases} - caseStatus={'closed'} - isLoading={isCasesStatusLoading} - /> - </FlexItemDivider> - <EuiFlexItem grow={false}> - <ConfigureCaseButton - label={i18n.CONFIGURE_CASES_BUTTON} - isDisabled={!isEmpty(actionsErrors) || !userCanCrud} - showToolTip={!isEmpty(actionsErrors)} - msgTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].description : <></>} - titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} - urlSearch={urlSearch} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - isDisabled={!userCanCrud} - fill - href={getCreateCaseUrl(urlSearch)} - iconType="plusInCircle" - > - {i18n.CREATE_TITLE} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </CaseHeaderPage> - {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( - <ProgressLoader size="xs" color="accent" className="essentialAnimation" /> - )} - <Panel loading={isCasesLoading}> - <CasesTableFilters - countClosedCases={data.countClosedCases} - countOpenCases={data.countOpenCases} - onFilterChanged={onFilterChangedCallback} - initial={{ - search: filterOptions.search, - reporters: filterOptions.reporters, - tags: filterOptions.tags, - status: filterOptions.status, - }} - setFilterRefetch={setFilterRefetch} - /> - {isCasesLoading && isDataEmpty ? ( - <Div> - <EuiLoadingContent data-test-subj="initialLoadingPanelAllCases" lines={10} /> - </Div> - ) : ( - <Div> - <UtilityBar border> - <UtilityBarSection> - <UtilityBarGroup> - <UtilityBarText data-test-subj="case-table-case-count"> - {i18n.SHOWING_CASES(data.total ?? 0)} - </UtilityBarText> - </UtilityBarGroup> - <UtilityBarGroup> - <UtilityBarText data-test-subj="case-table-selected-case-count"> - {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} - </UtilityBarText> - {userCanCrud && ( - <UtilityBarAction - data-test-subj="case-table-bulk-actions" - iconSide="right" - iconType="arrowDown" - popoverContent={getBulkItemsPopoverContent} - > - {i18n.BULK_ACTIONS} - </UtilityBarAction> - )} - <UtilityBarAction iconSide="left" iconType="refresh" onClick={refreshCases}> - {i18n.REFRESH} - </UtilityBarAction> - </UtilityBarGroup> - </UtilityBarSection> - </UtilityBar> - <EuiBasicTable - columns={memoizedGetCasesColumns} - data-test-subj="cases-table" - isSelectable={userCanCrud} - itemId="id" - items={data.cases} - noItemsMessage={ - <EuiEmptyPrompt - title={<h3>{i18n.NO_CASES}</h3>} - titleSize="xs" - body={i18n.NO_CASES_BODY} - actions={ - <EuiButton - isDisabled={!userCanCrud} - fill - size="s" - href={getCreateCaseUrl(urlSearch)} - iconType="plusInCircle" - > - {i18n.ADD_NEW_CASE} - </EuiButton> - } - /> - } - onChange={tableOnChangeCallback} - pagination={memoizedPagination} - selection={userCanCrud ? euiBasicTableSelectionProps : {}} - sorting={sorting} - /> - </Div> - )} - </Panel> - {confirmDeleteModal} - </> - ); -}); - -AllCases.displayName = 'AllCases'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx deleted file mode 100644 index 3cf0405f40637..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ /dev/null @@ -1,341 +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 { - EuiButtonToggle, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingContent, - EuiLoadingSpinner, - EuiHorizontalRule, -} from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; - -import * as i18n from './translations'; -import { Case } from '../../../../containers/case/types'; -import { getCaseUrl } from '../../../../components/link_to'; -import { HeaderPage } from '../../../../components/header_page'; -import { EditableTitle } from '../../../../components/header_page/editable_title'; -import { TagList } from '../tag_list'; -import { useGetCase } from '../../../../containers/case/use_get_case'; -import { UserActionTree } from '../user_action_tree'; -import { UserList } from '../user_list'; -import { useUpdateCase } from '../../../../containers/case/use_update_case'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { WrapperPage } from '../../../../components/wrapper_page'; -import { getTypedPayload } from '../../../../containers/case/utils'; -import { WhitePageWrapper } from '../wrappers'; -import { useBasePath } from '../../../../lib/kibana'; -import { CaseStatus } from '../case_status'; -import { navTabs } from '../../../home/home_navigations'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; -import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; -import { usePushToService } from '../use_push_to_service'; - -interface Props { - caseId: string; - userCanCrud: boolean; -} - -const MyWrapper = styled(WrapperPage)` - padding-bottom: 0; -`; - -const MyEuiFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; - -const MyEuiHorizontalRule = styled(EuiHorizontalRule)` - margin-left: 48px; - &.euiHorizontalRule--full { - width: calc(100% - 48px); - } -`; - -export interface CaseProps extends Props { - fetchCase: () => void; - caseData: Case; - updateCase: (newCase: Case) => void; -} - -export const CaseComponent = React.memo<CaseProps>( - ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { - const basePath = window.location.origin + useBasePath(); - const caseLink = `${basePath}/app/siem#/case/${caseId}`; - const search = useGetUrlSearch(navTabs.case); - - const [initLoadingData, setInitLoadingData] = useState(true); - const { - caseUserActions, - fetchCaseUserActions, - firstIndexPushToService, - hasDataToPush, - isLoading: isLoadingUserActions, - lastIndexPushToService, - participants, - } = useGetCaseUserActions(caseId); - const { isLoading, updateKey, updateCaseProperty } = useUpdateCase({ - caseId, - }); - - // Update Fields - const onUpdateField = useCallback( - (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { - const handleUpdateNewCase = (newCase: Case) => - updateCase({ ...newCase, comments: caseData.comments }); - switch (newUpdateKey) { - case 'title': - const titleUpdate = getTypedPayload<string>(updateValue); - if (titleUpdate.length > 0) { - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'title', - updateValue: titleUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - } - break; - case 'description': - const descriptionUpdate = getTypedPayload<string>(updateValue); - if (descriptionUpdate.length > 0) { - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'description', - updateValue: descriptionUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - } - break; - case 'tags': - const tagsUpdate = getTypedPayload<string[]>(updateValue); - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'tags', - updateValue: tagsUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - break; - case 'status': - const statusUpdate = getTypedPayload<string>(updateValue); - if (caseData.status !== updateValue) { - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'status', - updateValue: statusUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - } - default: - return null; - } - }, - [fetchCaseUserActions, updateCaseProperty, updateCase, caseData] - ); - const handleUpdateCase = useCallback( - (newCase: Case) => { - updateCase(newCase); - fetchCaseUserActions(newCase.id); - }, - [updateCase, fetchCaseUserActions] - ); - - const { pushButton, pushCallouts } = usePushToService({ - caseId: caseData.id, - caseStatus: caseData.status, - isNew: caseUserActions.filter(cua => cua.action === 'push-to-service').length === 0, - updateCase: handleUpdateCase, - userCanCrud, - }); - - const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); - const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [ - onUpdateField, - ]); - const toggleStatusCase = useCallback( - e => onUpdateField('status', e.target.checked ? 'closed' : 'open'), - [onUpdateField] - ); - const handleRefresh = useCallback(() => { - fetchCaseUserActions(caseData.id); - fetchCase(); - }, [caseData.id, fetchCase, fetchCaseUserActions]); - - const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); - - const caseStatusData = useMemo( - () => - caseData.status === 'open' - ? { - 'data-test-subj': 'case-view-createdAt', - value: caseData.createdAt, - title: i18n.CASE_OPENED, - buttonLabel: i18n.CLOSE_CASE, - status: caseData.status, - icon: 'folderCheck', - badgeColor: 'secondary', - isSelected: false, - } - : { - 'data-test-subj': 'case-view-closedAt', - value: caseData.closedAt ?? '', - title: i18n.CASE_CLOSED, - buttonLabel: i18n.REOPEN_CASE, - status: caseData.status, - icon: 'folderExclamation', - badgeColor: 'danger', - isSelected: true, - }, - [caseData.closedAt, caseData.createdAt, caseData.status] - ); - const emailContent = useMemo( - () => ({ - subject: i18n.EMAIL_SUBJECT(caseData.title), - body: i18n.EMAIL_BODY(caseLink), - }), - [caseLink, caseData.title] - ); - - useEffect(() => { - if (initLoadingData && !isLoadingUserActions) { - setInitLoadingData(false); - } - }, [initLoadingData, isLoadingUserActions]); - - return ( - <> - <MyWrapper> - <HeaderPage - backOptions={{ - href: getCaseUrl(search), - text: i18n.BACK_TO_ALL, - }} - data-test-subj="case-view-title" - titleNode={ - <EditableTitle - disabled={!userCanCrud} - isLoading={isLoading && updateKey === 'title'} - title={caseData.title} - onSubmit={onSubmitTitle} - /> - } - title={caseData.title} - > - <CaseStatus - caseData={caseData} - disabled={!userCanCrud} - isLoading={isLoading && updateKey === 'status'} - onRefresh={handleRefresh} - toggleStatusCase={toggleStatusCase} - {...caseStatusData} - /> - </HeaderPage> - </MyWrapper> - <WhitePageWrapper> - <MyWrapper> - {pushCallouts != null && pushCallouts} - <EuiFlexGroup> - <EuiFlexItem grow={6}> - {initLoadingData && <EuiLoadingContent lines={8} />} - {!initLoadingData && ( - <> - <UserActionTree - caseUserActions={caseUserActions} - data={caseData} - fetchUserActions={fetchCaseUserActions.bind(null, caseData.id)} - firstIndexPushToService={firstIndexPushToService} - isLoadingDescription={isLoading && updateKey === 'description'} - isLoadingUserActions={isLoadingUserActions} - lastIndexPushToService={lastIndexPushToService} - onUpdateField={onUpdateField} - updateCase={updateCase} - userCanCrud={userCanCrud} - /> - <MyEuiHorizontalRule margin="s" /> - <EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd"> - <EuiFlexItem grow={false}> - <EuiButtonToggle - data-test-subj={caseStatusData['data-test-subj']} - iconType={caseStatusData.icon} - isDisabled={!userCanCrud} - isSelected={caseStatusData.isSelected} - isLoading={isLoading && updateKey === 'status'} - label={caseStatusData.buttonLabel} - onChange={toggleStatusCase} - /> - </EuiFlexItem> - {hasDataToPush && ( - <EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}> - {pushButton} - </EuiFlexItem> - )} - </EuiFlexGroup> - </> - )} - </EuiFlexItem> - <EuiFlexItem grow={2}> - <UserList - data-test-subj="case-view-user-list-reporter" - email={emailContent} - headline={i18n.REPORTER} - users={[caseData.createdBy]} - /> - <UserList - data-test-subj="case-view-user-list-participants" - email={emailContent} - headline={i18n.PARTICIPANTS} - loading={isLoadingUserActions} - users={participants} - /> - <TagList - data-test-subj="case-view-tag-list" - disabled={!userCanCrud} - tags={caseData.tags} - onSubmit={onSubmitTags} - isLoading={isLoading && updateKey === 'tags'} - /> - </EuiFlexItem> - </EuiFlexGroup> - </MyWrapper> - </WhitePageWrapper> - <SpyRoute state={spyState} /> - </> - ); - } -); - -export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { - const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId); - if (isError) { - return null; - } - if (isLoading) { - return ( - <MyEuiFlexGroup justifyContent="center" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiLoadingSpinner data-test-subj="case-view-loading" size="xl" /> - </EuiFlexItem> - </MyEuiFlexGroup> - ); - } - - return ( - <CaseComponent - caseId={caseId} - fetchCase={fetchCase} - caseData={data} - updateCase={updateCase} - userCanCrud={userCanCrud} - /> - ); -}); - -CaseComponent.displayName = 'CaseComponent'; -CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx deleted file mode 100644 index 135f0f2a7e26d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx +++ /dev/null @@ -1,124 +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 { - Connector, - CasesConfigurationMapping, -} from '../../../../../containers/case/configure/types'; -import { State } from '../reducer'; -import { ReturnConnectors } from '../../../../../containers/case/configure/use_connectors'; -import { ReturnUseCaseConfigure } from '../../../../../containers/case/configure/use_configure'; -import { createUseKibanaMock } from '../../../../../mock/kibana_react'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { actionTypeRegistryMock } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/action_type_registry.mock'; - -export const connectors: Connector[] = [ - { - id: '123', - actionTypeId: '.servicenow', - name: 'My Connector', - isPreconfigured: false, - config: { - apiUrl: 'https://instance1.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - }, - { - id: '456', - actionTypeId: '.servicenow', - name: 'My Connector 2', - isPreconfigured: false, - config: { - apiUrl: 'https://instance2.service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, - }, -]; - -export const mapping: CasesConfigurationMapping[] = [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'append', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, -]; - -export const searchURL = - '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; - -export const initialState: State = { - connectorId: 'none', - closureType: 'close-by-user', - mapping: null, - currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, -}; - -export const useCaseConfigureResponse: ReturnUseCaseConfigure = { - loading: false, - persistLoading: false, - refetchCaseConfigure: jest.fn(), - persistCaseConfigure: jest.fn(), -}; - -export const useConnectorsResponse: ReturnConnectors = { - loading: false, - connectors, - refetchConnectors: jest.fn(), -}; - -export const kibanaMockImplementationArgs = { - services: { - ...createUseKibanaMock()().services, - triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx deleted file mode 100644 index 5ea3f500c0349..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx +++ /dev/null @@ -1,748 +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 React, { useEffect } from 'react'; -import { ReactWrapper, mount } from 'enzyme'; - -import { useKibana } from '../../../../lib/kibana'; -import { useConnectors } from '../../../../containers/case/configure/use_connectors'; -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; - -import { - connectors, - searchURL, - useCaseConfigureResponse, - useConnectorsResponse, - kibanaMockImplementationArgs, -} from './__mock__'; - -jest.mock('../../../../lib/kibana'); -jest.mock('../../../../containers/case/configure/use_connectors'); -jest.mock('../../../../containers/case/configure/use_configure'); -jest.mock('../../../../components/navigation/use_get_url_search'); - -const useKibanaMock = useKibana as jest.Mock; -const useConnectorsMock = useConnectors as jest.Mock; -const useCaseConfigureMock = useCaseConfigure as jest.Mock; -const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; - -import { ConfigureCases } from './'; -import { TestProviders } from '../../../../mock'; -import { Connectors } from './connectors'; -import { ClosureOptions } from './closure_options'; -import { Mapping } from './mapping'; -import { - ActionsConnectorsContextProvider, - ConnectorAddFlyout, - ConnectorEditFlyout, -} from '../../../../../../../../plugins/triggers_actions_ui/public'; -import { EuiBottomBar } from '@elastic/eui'; - -describe('rendering', () => { - let wrapper: ReactWrapper; - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - - wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - }); - - test('it renders the Connectors', () => { - expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').exists()).toBeTruthy(); - }); - - test('it renders the ClosureType', () => { - expect( - wrapper.find('[data-test-subj="case-closure-options-form-group"]').exists() - ).toBeTruthy(); - }); - - test('it renders the Mapping', () => { - expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').exists()).toBeTruthy(); - }); - - test('it renders the ActionsConnectorsContextProvider', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy(); - }); - - test('it renders the ConnectorAddFlyout', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy(); - }); - - test('it does NOT render the ConnectorEditFlyout', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); - }); - - test('it does NOT render the EuiCallOut', () => { - expect(wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()).toBeFalsy(); - }); - - test('it does NOT render the EuiBottomBar', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); -}); - -describe('ConfigureCases - Unhappy path', () => { - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - }); - - test('it shows the warning callout when configuration is invalid', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('not-id'), []); - return useCaseConfigureResponse; - } - ); - - const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() - ).toBeTruthy(); - }); -}); - -describe('ConfigureCases - Happy path', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('123'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return useCaseConfigureResponse; - } - ); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - - wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - }); - - test('it renders the ConnectorEditFlyout', () => { - expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy(); - }); - - test('it renders with correct props', () => { - // Connector - expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); - expect(wrapper.find(Connectors).prop('disabled')).toBe(false); - expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); - expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('123'); - - // ClosureOptions - expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); - expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); - - // Mapping - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false); - expect(wrapper.find(Mapping).prop('mapping')).toEqual( - connectors[0].config.casesConfiguration.mapping - ); - - // Flyouts - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); - expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, - ]); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); - expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); - }); - - test('it disables correctly when the user cannot crud', () => { - const newWrapper = mount(<ConfigureCases userCanCrud={false} />, { - wrappingComponent: TestProviders, - }); - - expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - expect(newWrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(true); - }); - - test('it disables correctly Connector when loading connectors', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); - }); - - test('it disables correctly Connector when saving configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - persistLoading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); - }); - - test('it pass the correct value to isLoading attribute on Connector', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('isLoading')).toBe(true); - }); - - test('it set correctly the selected connector', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Connectors).prop('selectedConnector')).toBe('456'); - }); - - test('it show the add flyout when pressing the add connector button', () => { - wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); - }); - - test('it disables correctly ClosureOptions when loading connectors', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - - test('it disables correctly ClosureOptions when saving configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - persistLoading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - - test('it disables correctly ClosureOptions when the connector is set to none', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('none'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - - test('it disables the mapping permanently', () => { - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when loading the connectors', () => { - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - expect(wrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when loading the configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when saving the configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - persistLoading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when the connectorId is invalid', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('not-id'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it disables the update connector button when the connectorId is set to none', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('none'), []); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); - }); - - test('it show the edit flyout when pressing the update connector button', () => { - wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); - }); - - test('it sets the mapping of a connector correctly', () => { - expect(wrapper.find(Mapping).prop('mapping')).toEqual( - connectors[0].config.casesConfiguration.mapping - ); - }); - - // TODO: When mapping is enabled the test.todo should be implemented. - test.todo('the mapping is changed successfully when changing the third party'); - test.todo('the mapping is changed successfully when changing the action type'); - - test('it does not shows the action bar when there is no change', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it shows the action bar when the connector is changed', () => { - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it shows the action bar when the closure type is changed', () => { - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it tracks the changes successfully', () => { - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('2 unsaved changes'); - }); - - test('it tracks and reverts the changes successfully ', () => { - // change settings - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // revert back to initial settings - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-123"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-user"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it close and restores the action bar when the add connector button is pressed', () => { - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // Press add connector button - wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - - // Close the add flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it close and restores the action bar when the update connector button is pressed', () => { - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // Press update connector button - wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - - // Close the edit flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it disables the buttons of action bar when loading connectors', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return useCaseConfigureResponse; - } - ); - - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it disables the buttons of action bar when loading configuration', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, loading: true }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it disables the buttons of action bar when saving configuration', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, persistLoading: true }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it shows the loading spinner when saving configuration', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, persistLoading: true }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isLoading') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isLoading') - ).toBe(true); - }); - - test('it closes the action bar when pressing save', () => { - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return useCaseConfigureResponse; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); - - newWrapper.update(); - - expect( - newWrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it submits the configuration correctly', () => { - const persistCaseConfigure = jest.fn(); - - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-pushing', - }), - [] - ); - return { ...useCaseConfigureResponse, persistCaseConfigure }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); - - newWrapper.update(); - - expect(persistCaseConfigure).toHaveBeenCalled(); - expect(persistCaseConfigure).toHaveBeenCalledWith({ - connectorId: '456', - connectorName: 'My Connector 2', - closureType: 'close-by-user', - }); - }); - - test('it has the correct url on cancel button', () => { - const persistCaseConfigure = jest.fn(); - - useCaseConfigureMock.mockImplementation( - ({ setConnector, setClosureType, setCurrentConfiguration }) => { - useEffect(() => setConnector('456'), []); - useEffect(() => setClosureType('close-by-user'), []); - useEffect( - () => - setCurrentConfiguration({ - connectorId: '123', - closureType: 'close-by-user', - }), - [] - ); - return { ...useCaseConfigureResponse, persistCaseConfigure }; - } - ); - - const newWrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('href') - ).toBe(`#/link-to/case${searchURL}`); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx deleted file mode 100644 index 241dcef14a145..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ /dev/null @@ -1,364 +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 React, { - useReducer, - useCallback, - useEffect, - useState, - Dispatch, - SetStateAction, -} from 'react'; -import styled, { css } from 'styled-components'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiCallOut, - EuiBottomBar, - EuiButtonEmpty, - EuiText, -} from '@elastic/eui'; -import { isEmpty, difference } from 'lodash/fp'; -import { useKibana } from '../../../../lib/kibana'; -import { useConnectors } from '../../../../containers/case/configure/use_connectors'; -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { - ActionsConnectorsContextProvider, - ActionType, - ConnectorAddFlyout, - ConnectorEditFlyout, -} from '../../../../../../../../plugins/triggers_actions_ui/public'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorTableItem } from '../../../../../../../../plugins/triggers_actions_ui/public/types'; -import { getCaseUrl } from '../../../../components/link_to'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { - ClosureType, - CasesConfigurationMapping, - CCMapsCombinedActionAttributes, -} from '../../../../containers/case/configure/types'; -import { Connectors } from '../configure_cases/connectors'; -import { ClosureOptions } from '../configure_cases/closure_options'; -import { Mapping } from '../configure_cases/mapping'; -import { SectionWrapper } from '../wrappers'; -import { navTabs } from '../../../../pages/home/home_navigations'; -import { configureCasesReducer, State, CurrentConfiguration } from './reducer'; -import * as i18n from './translations'; - -const FormWrapper = styled.div` - ${({ theme }) => css` - & > * { - margin-top 40px; - } - - & > :first-child { - margin-top: 0; - } - - padding-top: ${theme.eui.paddingSizes.xl}; - padding-bottom: ${theme.eui.paddingSizes.xl}; - `} -`; - -const initialState: State = { - connectorId: 'none', - closureType: 'close-by-user', - mapping: null, - currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, -}; - -const actionTypes: ActionType[] = [ - { - id: '.servicenow', - name: 'ServiceNow', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', - }, -]; - -interface ConfigureCasesComponentProps { - userCanCrud: boolean; -} - -const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userCanCrud }) => { - const search = useGetUrlSearch(navTabs.case); - const { http, triggers_actions_ui, notifications, application } = useKibana().services; - - const [connectorIsValid, setConnectorIsValid] = useState(true); - const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false); - const [editedConnectorItem, setEditedConnectorItem] = useState<ActionConnectorTableItem | null>( - null - ); - - const [actionBarVisible, setActionBarVisible] = useState(false); - const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0); - - const [{ connectorId, closureType, mapping, currentConfiguration }, dispatch] = useReducer( - configureCasesReducer(), - initialState - ); - - const setCurrentConfiguration = useCallback((configuration: CurrentConfiguration) => { - dispatch({ - type: 'setCurrentConfiguration', - currentConfiguration: { ...configuration }, - }); - }, []); - - const setConnectorId = useCallback((newConnectorId: string) => { - dispatch({ - type: 'setConnectorId', - connectorId: newConnectorId, - }); - }, []); - - const setClosureType = useCallback((newClosureType: ClosureType) => { - dispatch({ - type: 'setClosureType', - closureType: newClosureType, - }); - }, []); - - const setMapping = useCallback((newMapping: CasesConfigurationMapping[]) => { - dispatch({ - type: 'setMapping', - mapping: newMapping, - }); - }, []); - - const { loading: loadingCaseConfigure, persistLoading, persistCaseConfigure } = useCaseConfigure({ - setConnector: setConnectorId, - setClosureType, - setCurrentConfiguration, - }); - - const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); - - // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise<void>. - // TODO: Fix it if reloadConnectors type change. - const reloadConnectors = useCallback(async () => refetchConnectors(), []); - const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; - const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none'; - - const handleSubmit = useCallback( - // TO DO give a warning/error to user when field are not mapped so they have chance to do it - () => { - setActionBarVisible(false); - persistCaseConfigure({ - connectorId, - connectorName: connectors.find(c => c.id === connectorId)?.name ?? '', - closureType, - }); - }, - [connectorId, connectors, closureType, mapping] - ); - - const onClickAddConnector = useCallback(() => { - setActionBarVisible(false); - setAddFlyoutVisibility(true); - }, []); - - const onClickUpdateConnector = useCallback(() => { - setActionBarVisible(false); - setEditFlyoutVisibility(true); - }, []); - - const handleActionBar = useCallback(() => { - const unsavedChanges = difference(Object.values(currentConfiguration), [ - connectorId, - closureType, - ]).length; - - if (unsavedChanges === 0) { - setActionBarVisible(false); - } else { - setActionBarVisible(true); - } - - setTotalConfigurationChanges(unsavedChanges); - }, [currentConfiguration, connectorId, closureType]); - - const handleSetAddFlyoutVisibility = useCallback( - (isVisible: boolean) => { - handleActionBar(); - setAddFlyoutVisibility(isVisible); - }, - [currentConfiguration, connectorId, closureType] - ); - - const handleSetEditFlyoutVisibility = useCallback( - (isVisible: boolean) => { - handleActionBar(); - setEditFlyoutVisibility(isVisible); - }, - [currentConfiguration, connectorId, closureType] - ); - - useEffect(() => { - if ( - !isEmpty(connectors) && - connectorId !== 'none' && - connectors.some(c => c.id === connectorId) - ) { - const myConnector = connectors.find(c => c.id === connectorId); - const myMapping = myConnector?.config?.casesConfiguration?.mapping ?? []; - setMapping( - myMapping.map((m: CCMapsCombinedActionAttributes) => ({ - source: m.source, - target: m.target, - actionType: m.action_type ?? m.actionType, - })) - ); - } - }, [connectors, connectorId]); - - useEffect(() => { - if ( - !isLoadingConnectors && - connectorId !== 'none' && - !connectors.some(c => c.id === connectorId) - ) { - setConnectorIsValid(false); - } else if ( - !isLoadingConnectors && - (connectorId === 'none' || connectors.some(c => c.id === connectorId)) - ) { - setConnectorIsValid(true); - } - }, [connectors, connectorId]); - - useEffect(() => { - if (!isLoadingConnectors && connectorId !== 'none') { - setEditedConnectorItem( - connectors.find(c => c.id === connectorId) as ActionConnectorTableItem - ); - } - }, [connectors, connectorId]); - - useEffect(() => { - handleActionBar(); - }, [connectors, connectorId, closureType, currentConfiguration]); - - return ( - <FormWrapper> - {!connectorIsValid && ( - <SectionWrapper style={{ marginTop: 0 }}> - <EuiCallOut - title={i18n.WARNING_NO_CONNECTOR_TITLE} - color="warning" - iconType="help" - data-test-subj="configure-cases-warning-callout" - > - {i18n.WARNING_NO_CONNECTOR_MESSAGE} - </EuiCallOut> - </SectionWrapper> - )} - <SectionWrapper> - <Connectors - connectors={connectors ?? []} - disabled={persistLoading || isLoadingConnectors || !userCanCrud} - isLoading={isLoadingConnectors} - onChangeConnector={setConnectorId} - handleShowAddFlyout={onClickAddConnector} - selectedConnector={connectorId} - /> - </SectionWrapper> - <SectionWrapper> - <ClosureOptions - closureTypeSelected={closureType} - disabled={persistLoading || isLoadingConnectors || connectorId === 'none' || !userCanCrud} - onChangeClosureType={setClosureType} - /> - </SectionWrapper> - <SectionWrapper> - <Mapping - disabled - updateConnectorDisabled={updateConnectorDisabled || !userCanCrud} - mapping={mapping} - onChangeMapping={setMapping} - setEditFlyoutVisibility={onClickUpdateConnector} - /> - </SectionWrapper> - {actionBarVisible && ( - <EuiBottomBar data-test-subj="case-configure-action-bottom-bar"> - <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiText data-test-subj="case-configure-action-bottom-bar-total-changes"> - {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} - </EuiText> - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - color="ghost" - iconType="cross" - isDisabled={isLoadingAny} - isLoading={persistLoading} - aria-label={i18n.CANCEL} - href={getCaseUrl(search)} - data-test-subj="case-configure-action-bottom-bar-cancel-button" - > - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - color="secondary" - iconType="save" - aria-label={i18n.SAVE_CHANGES} - isDisabled={isLoadingAny} - isLoading={persistLoading} - onClick={handleSubmit} - data-test-subj="case-configure-action-bottom-bar-save-button" - > - {i18n.SAVE_CHANGES} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - </EuiBottomBar> - )} - <ActionsConnectorsContextProvider - value={{ - http, - actionTypeRegistry: triggers_actions_ui.actionTypeRegistry, - toastNotifications: notifications.toasts, - capabilities: application.capabilities, - reloadConnectors, - }} - > - <ConnectorAddFlyout - addFlyoutVisible={addFlyoutVisible} - setAddFlyoutVisibility={handleSetAddFlyoutVisibility as Dispatch<SetStateAction<boolean>>} - actionTypes={actionTypes} - /> - {editedConnectorItem && ( - <ConnectorEditFlyout - key={editedConnectorItem.id} - initialConnector={editedConnectorItem} - editFlyoutVisible={editFlyoutVisible} - setEditFlyoutVisibility={ - handleSetEditFlyoutVisibility as Dispatch<SetStateAction<boolean>> - } - /> - )} - </ActionsConnectorsContextProvider> - </FormWrapper> - ); -}; - -export const ConfigureCases = React.memo(ConfigureCasesComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts deleted file mode 100644 index df958b75dc6b8..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts +++ /dev/null @@ -1,68 +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 { configureCasesReducer, Action, State } from './reducer'; -import { initialState, mapping } from './__mock__'; - -describe('Reducer', () => { - let reducer: (state: State, action: Action) => State; - - beforeAll(() => { - reducer = configureCasesReducer(); - }); - - test('it should set the correct configuration', () => { - const action: Action = { - type: 'setCurrentConfiguration', - currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - currentConfiguration: action.currentConfiguration, - }); - }); - - test('it should set the correct connector id', () => { - const action: Action = { - type: 'setConnectorId', - connectorId: '456', - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - connectorId: action.connectorId, - }); - }); - - test('it should set the closure type', () => { - const action: Action = { - type: 'setClosureType', - closureType: 'close-by-pushing', - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - closureType: action.closureType, - }); - }); - - test('it should set the mapping', () => { - const action: Action = { - type: 'setMapping', - mapping, - }; - const state = reducer(initialState, action); - - expect(state).toEqual({ - ...state, - mapping: action.mapping, - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts deleted file mode 100644 index f6b9d38a76de3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.ts +++ /dev/null @@ -1,71 +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 { - ClosureType, - CasesConfigurationMapping, -} from '../../../../containers/case/configure/types'; - -export interface State { - mapping: CasesConfigurationMapping[] | null; - connectorId: string; - closureType: ClosureType; - currentConfiguration: CurrentConfiguration; -} - -export interface CurrentConfiguration { - connectorId: State['connectorId']; - closureType: State['closureType']; -} - -export type Action = - | { - type: 'setCurrentConfiguration'; - currentConfiguration: CurrentConfiguration; - } - | { - type: 'setConnectorId'; - connectorId: string; - } - | { - type: 'setClosureType'; - closureType: ClosureType; - } - | { - type: 'setMapping'; - mapping: CasesConfigurationMapping[]; - }; - -export const configureCasesReducer = () => (state: State, action: Action) => { - switch (action.type) { - case 'setCurrentConfiguration': { - return { - ...state, - currentConfiguration: { ...action.currentConfiguration }, - }; - } - case 'setConnectorId': { - return { - ...state, - connectorId: action.connectorId, - }; - } - case 'setClosureType': { - return { - ...state, - closureType: action.closureType, - }; - } - case 'setMapping': { - return { - ...state, - mapping: action.mapping, - }; - } - default: - return state; - } -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx deleted file mode 100644 index d480744fc932a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx +++ /dev/null @@ -1,121 +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 React from 'react'; -import { mount } from 'enzyme'; - -import { Create } from './'; -import { TestProviders } from '../../../../mock'; -import { getFormMock } from '../__mock__/form'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; - -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { usePostCase } from '../../../../containers/case/use_post_case'; -jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); -jest.mock('../../../../containers/case/use_post_case'); -import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; -import { wait } from '../../../../lib/helpers'; -import { SiemPageName } from '../../../home/types'; -jest.mock( - '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); - -export const useFormMock = useForm as jest.Mock; - -const useInsertTimelineMock = useInsertTimeline as jest.Mock; -const usePostCaseMock = usePostCase as jest.Mock; - -const postCase = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); - -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; -const sampleData = { - description: 'what a great description', - tags: ['coke', 'pepsi'], - title: 'what a cool title', -}; -const defaultPostCase = { - isLoading: false, - isError: false, - caseData: null, - postCase, -}; -describe('Create case', () => { - const formHookMock = getFormMock(sampleData); - - beforeEach(() => { - jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); - usePostCaseMock.mockImplementation(() => defaultPostCase); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - }); - - it('should post case on submit click', async () => { - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <Create /> - </Router> - </TestProviders> - ); - wrapper - .find(`[data-test-subj="create-case-submit"]`) - .first() - .simulate('click'); - await wait(); - expect(postCase).toBeCalledWith(sampleData); - }); - - it('should redirect to all cases on cancel click', () => { - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <Create /> - </Router> - </TestProviders> - ); - wrapper - .find(`[data-test-subj="create-case-cancel"]`) - .first() - .simulate('click'); - expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`); - }); - it('should redirect to new case when caseData is there', () => { - const sampleId = '777777'; - usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } })); - mount( - <TestProviders> - <Router history={mockHistory}> - <Create /> - </Router> - </TestProviders> - ); - expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual( - `/${SiemPageName.case}/${sampleId}` - ); - }); - - it('should render spinner when loading', () => { - usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <Create /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx deleted file mode 100644 index 53b792bb9b5eb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ /dev/null @@ -1,170 +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 React, { useCallback, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPanel, -} from '@elastic/eui'; -import styled, { css } from 'styled-components'; -import { Redirect } from 'react-router-dom'; - -import { CasePostRequest } from '../../../../../../../../plugins/case/common/api'; -import { Field, Form, getUseField, useForm, UseField } from '../../../../shared_imports'; -import { usePostCase } from '../../../../containers/case/use_post_case'; -import { schema } from './schema'; -import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import * as i18n from '../../translations'; -import { SiemPageName } from '../../../home/types'; -import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; - -export const CommonUseField = getUseField({ component: Field }); - -const ContainerBig = styled.div` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSizeXL}; - `} -`; - -const Container = styled.div` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSize}; - `} -`; -const MySpinner = styled(EuiLoadingSpinner)` - position: absolute; - top: 50%; - left: 50%; - z-index: 99; -`; - -const initialCaseValue: CasePostRequest = { - description: '', - tags: [], - title: '', -}; - -export const Create = React.memo(() => { - const { caseData, isLoading, postCase } = usePostCase(); - const [isCancel, setIsCancel] = useState(false); - const { form } = useForm<CasePostRequest>({ - defaultValue: initialCaseValue, - options: { stripEmptyFields: false }, - schema, - }); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CasePostRequest>( - form, - 'description' - ); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - await postCase(data); - } - }, [form]); - - const handleSetIsCancel = useCallback(() => { - setIsCancel(true); - }, []); - - if (caseData != null && caseData.id) { - return <Redirect to={`/${SiemPageName.case}/${caseData.id}`} />; - } - - if (isCancel) { - return <Redirect to={`/${SiemPageName.case}`} />; - } - - return ( - <EuiPanel> - {isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} - <Form form={form}> - <CommonUseField - path="title" - componentProps={{ - idAria: 'caseTitle', - 'data-test-subj': 'caseTitle', - euiFieldProps: { - fullWidth: false, - disabled: isLoading, - }, - }} - /> - <Container> - <CommonUseField - path="tags" - componentProps={{ - idAria: 'caseTags', - 'data-test-subj': 'caseTags', - euiFieldProps: { - fullWidth: true, - placeholder: '', - disabled: isLoading, - }, - }} - /> - </Container> - <ContainerBig> - <UseField - path="description" - component={MarkdownEditorForm} - componentProps={{ - dataTestSubj: 'caseDescription', - idAria: 'caseDescription', - isDisabled: isLoading, - onCursorPositionUpdate: handleCursorChange, - topRightContent: ( - <InsertTimelinePopover - hideUntitled={true} - isDisabled={isLoading} - onTimelineChange={handleOnTimelineChange} - /> - ), - }} - /> - </ContainerBig> - </Form> - <Container> - <EuiFlexGroup - alignItems="center" - justifyContent="flexEnd" - gutterSize="xs" - responsive={false} - > - <EuiFlexItem grow={false}> - <EuiButtonEmpty - data-test-subj="create-case-cancel" - size="s" - onClick={handleSetIsCancel} - iconType="cross" - > - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - data-test-subj="create-case-submit" - fill - iconType="plusInCircle" - isDisabled={isLoading} - isLoading={isLoading} - onClick={onSubmit} - > - {i18n.CREATE_CASE} - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </Container> - </EuiPanel> - ); -}); - -Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx deleted file mode 100644 index 4653dbc67d5a1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/schema.tsx +++ /dev/null @@ -1,40 +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 { CasePostRequest } from '../../../../../../../../plugins/case/common/api'; -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; -import * as i18n from '../../translations'; - -import { OptionalFieldLabel } from './optional_field_label'; -const { emptyField } = fieldValidators; - -export const schemaTags = { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.TAGS, - helpText: i18n.TAGS_HELP, - labelAppend: OptionalFieldLabel, -}; - -export const schema: FormSchema<CasePostRequest> = { - title: { - type: FIELD_TYPES.TEXT, - label: i18n.NAME, - validations: [ - { - validator: emptyField(i18n.TITLE_REQUIRED), - }, - ], - }, - description: { - label: i18n.DESCRIPTION, - validations: [ - { - validator: emptyField(i18n.DESCRIPTION_REQUIRED), - }, - ], - }, - tags: schemaTags, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx deleted file mode 100644 index 75f1d4d911518..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx +++ /dev/null @@ -1,30 +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 React, { useMemo } from 'react'; -import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; -import * as i18n from '../all_cases/translations'; - -export interface Props { - caseCount: number | null; - caseStatus: 'open' | 'closed'; - isLoading: boolean; -} - -export const OpenClosedStats = React.memo<Props>(({ caseCount, caseStatus, isLoading }) => { - const openClosedStats = useMemo( - () => [ - { - title: caseStatus === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, - description: isLoading ? <EuiLoadingSpinner /> : caseCount ?? 'N/A', - }, - ], - [caseCount, caseStatus, isLoading] - ); - return <EuiDescriptionList textStyle="reverse" listItems={openClosedStats} />; -}); - -OpenClosedStats.displayName = 'OpenClosedStats'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx deleted file mode 100644 index 8ad2f8f8cb737..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx +++ /dev/null @@ -1,138 +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 React from 'react'; -import { mount } from 'enzyme'; - -import { TagList } from './'; -import { getFormMock } from '../__mock__/form'; -import { TestProviders } from '../../../../mock'; -import { wait } from '../../../../lib/helpers'; -import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; -import { act } from 'react-dom/test-utils'; - -jest.mock( - '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -const onSubmit = jest.fn(); -const defaultProps = { - disabled: false, - isLoading: false, - onSubmit, - tags: [], -}; - -describe('TagList ', () => { - const sampleTags = ['coke', 'pepsi']; - const formHookMock = getFormMock({ tags: sampleTags }); - beforeEach(() => { - jest.resetAllMocks(); - (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); - }); - it('Renders no tags, and then edit', () => { - const wrapper = mount( - <TestProviders> - <TagList {...defaultProps} /> - </TestProviders> - ); - expect( - wrapper - .find(`[data-test-subj="no-tags"]`) - .last() - .exists() - ).toBeTruthy(); - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .simulate('click'); - expect( - wrapper - .find(`[data-test-subj="no-tags"]`) - .last() - .exists() - ).toBeFalsy(); - expect( - wrapper - .find(`[data-test-subj="edit-tags"]`) - .last() - .exists() - ).toBeTruthy(); - }); - it('Edit tag on submit', async () => { - const wrapper = mount( - <TestProviders> - <TagList {...defaultProps} /> - </TestProviders> - ); - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .simulate('click'); - await act(async () => { - wrapper - .find(`[data-test-subj="edit-tags-submit"]`) - .last() - .simulate('click'); - await wait(); - expect(onSubmit).toBeCalledWith(sampleTags); - }); - }); - it('Cancels on cancel', async () => { - const props = { - ...defaultProps, - tags: ['pepsi'], - }; - const wrapper = mount( - <TestProviders> - <TagList {...props} /> - </TestProviders> - ); - expect( - wrapper - .find(`[data-test-subj="case-tag"]`) - .last() - .exists() - ).toBeTruthy(); - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .simulate('click'); - await act(async () => { - expect( - wrapper - .find(`[data-test-subj="case-tag"]`) - .last() - .exists() - ).toBeFalsy(); - wrapper - .find(`[data-test-subj="edit-tags-cancel"]`) - .last() - .simulate('click'); - await wait(); - wrapper.update(); - expect( - wrapper - .find(`[data-test-subj="case-tag"]`) - .last() - .exists() - ).toBeTruthy(); - }); - }); - it('Renders disabled button', () => { - const props = { ...defaultProps, disabled: true }; - const wrapper = mount( - <TestProviders> - <TagList {...props} /> - </TestProviders> - ); - expect( - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .prop('disabled') - ).toBeTruthy(); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx deleted file mode 100644 index 9bac000b93235..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ /dev/null @@ -1,140 +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 React, { useCallback, useState } from 'react'; -import { - EuiText, - EuiHorizontalRule, - EuiFlexGroup, - EuiFlexItem, - EuiBadge, - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiLoadingSpinner, -} from '@elastic/eui'; -import styled, { css } from 'styled-components'; -import * as i18n from './translations'; -import { Form, useForm } from '../../../../shared_imports'; -import { schema } from './schema'; -import { CommonUseField } from '../create'; - -interface TagListProps { - disabled?: boolean; - isLoading: boolean; - onSubmit: (a: string[]) => void; - tags: string[]; -} - -const MyFlexGroup = styled(EuiFlexGroup)` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSizeM}; - p { - font-size: ${theme.eui.euiSizeM}; - } - `} -`; - -export const TagList = React.memo( - ({ disabled = false, isLoading, onSubmit, tags }: TagListProps) => { - const { form } = useForm({ - defaultValue: { tags }, - options: { stripEmptyFields: false }, - schema, - }); - const [isEditTags, setIsEditTags] = useState(false); - - const onSubmitTags = useCallback(async () => { - const { isValid, data: newData } = await form.submit(); - if (isValid && newData.tags) { - onSubmit(newData.tags); - setIsEditTags(false); - } - }, [form, onSubmit]); - - return ( - <EuiText> - <EuiFlexGroup alignItems="center" gutterSize="xs" justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <h4>{i18n.TAGS}</h4> - </EuiFlexItem> - {isLoading && <EuiLoadingSpinner data-test-subj="tag-list-loading" />} - {!isLoading && ( - <EuiFlexItem data-test-subj="tag-list-edit" grow={false}> - <EuiButtonIcon - data-test-subj="tag-list-edit-button" - isDisabled={disabled} - aria-label={i18n.EDIT_TAGS_ARIA} - iconType={'pencil'} - onClick={setIsEditTags.bind(null, true)} - /> - </EuiFlexItem> - )} - </EuiFlexGroup> - <EuiHorizontalRule margin="xs" /> - <MyFlexGroup gutterSize="xs" data-test-subj="grr"> - {tags.length === 0 && !isEditTags && <p data-test-subj="no-tags">{i18n.NO_TAGS}</p>} - {tags.length > 0 && - !isEditTags && - tags.map((tag, key) => ( - <EuiFlexItem grow={false} key={`${tag}${key}`}> - <EuiBadge data-test-subj="case-tag" color="hollow"> - {tag} - </EuiBadge> - </EuiFlexItem> - ))} - {isEditTags && ( - <EuiFlexGroup data-test-subj="edit-tags" direction="column"> - <EuiFlexItem> - <Form form={form}> - <CommonUseField - path="tags" - componentProps={{ - idAria: 'caseTags', - 'data-test-subj': 'caseTags', - euiFieldProps: { - fullWidth: true, - placeholder: '', - }, - }} - /> - </Form> - </EuiFlexItem> - <EuiFlexItem> - <EuiFlexGroup gutterSize="s" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiButton - color="secondary" - data-test-subj="edit-tags-submit" - fill - iconType="save" - onClick={onSubmitTags} - size="s" - > - {i18n.SAVE} - </EuiButton> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - data-test-subj="edit-tags-cancel" - iconType="cross" - onClick={setIsEditTags.bind(null, false)} - size="s" - > - {i18n.CANCEL} - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - )} - </MyFlexGroup> - </EuiText> - ); - } -); - -TagList.displayName = 'TagList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx deleted file mode 100644 index 77215e2318ded..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx +++ /dev/null @@ -1,192 +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. - */ -/* eslint-disable react/display-name */ -import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { usePushToService, ReturnUsePushToService, UsePushToService } from './'; -import { TestProviders } from '../../../../mock'; -import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; -import { ClosureType } from '../../../../../../../../plugins/case/common/api/cases'; -import * as i18n from './translations'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { getKibanaConfigError, getLicenseError } from './helpers'; -import * as api from '../../../../containers/case/configure/api'; -jest.mock('../../../../containers/case/use_get_action_license'); -jest.mock('../../../../containers/case/use_post_push_to_service'); -jest.mock('../../../../containers/case/configure/api'); - -describe('usePushToService', () => { - const caseId = '12345'; - const updateCase = jest.fn(); - const postPushToService = jest.fn(); - const mockPostPush = { - isLoading: false, - postPushToService, - }; - const closureType: ClosureType = 'close-by-user'; - const mockConnector = { - connectorId: 'c00l', - connectorName: 'name', - }; - const mockCaseConfigure = { - ...mockConnector, - createdAt: 'string', - createdBy: {}, - closureType, - updatedAt: 'string', - updatedBy: {}, - version: 'string', - }; - const getConfigureMock = jest.spyOn(api, 'getCaseConfigure'); - const actionLicense = { - id: '.servicenow', - name: 'ServiceNow', - minimumLicenseRequired: 'platinum', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }; - beforeEach(() => { - jest.resetAllMocks(); - (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense, - })); - getConfigureMock.mockImplementation(() => Promise.resolve(mockCaseConfigure)); - }); - it('push case button posts the push with correct args', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(getConfigureMock).toBeCalled(); - result.current.pushButton.props.children.props.onClick(); - expect(postPushToService).toBeCalledWith({ ...mockConnector, caseId, updateCase }); - expect(result.current.pushCallouts).toBeNull(); - }); - }); - it('Displays message when user does not have premium license', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense: { - ...actionLicense, - enabledInLicense: false, - }, - })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(getLicenseError().title); - }); - }); - it('Displays message when user does not have case enabled in config', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense: { - ...actionLicense, - enabledInConfig: false, - }, - })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); - }); - }); - it('Displays message when user does not have a connector configured', async () => { - getConfigureMock.mockImplementation(() => - Promise.resolve({ - ...mockCaseConfigure, - connectorId: 'none', - }) - ); - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'open', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); - }); - }); - it('Displays message when case is closed', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( - () => - usePushToService({ - caseId, - caseStatus: 'closed', - isNew: false, - updateCase, - userCanCrud: true, - }), - { - wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, - } - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx deleted file mode 100644 index 5092cba6872e3..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx +++ /dev/null @@ -1,174 +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 { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useState, useMemo } from 'react'; - -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { Case } from '../../../../containers/case/types'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; -import { getConfigureCasesUrl } from '../../../../components/link_to'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../../home/home_navigations'; -import { CaseCallOut } from '../callout'; -import { getLicenseError, getKibanaConfigError } from './helpers'; -import * as i18n from './translations'; - -export interface UsePushToService { - caseId: string; - caseStatus: string; - isNew: boolean; - updateCase: (newCase: Case) => void; - userCanCrud: boolean; -} - -interface Connector { - connectorId: string; - connectorName: string; -} - -export interface ReturnUsePushToService { - pushButton: JSX.Element; - pushCallouts: JSX.Element | null; -} - -export const usePushToService = ({ - caseId, - caseStatus, - isNew, - updateCase, - userCanCrud, -}: UsePushToService): ReturnUsePushToService => { - const urlSearch = useGetUrlSearch(navTabs.case); - const [connector, setConnector] = useState<Connector | null>(null); - - const { isLoading, postPushToService } = usePostPushToService(); - - const handleSetConnector = useCallback((connectorId: string, connectorName?: string) => { - setConnector({ connectorId, connectorName: connectorName ?? '' }); - }, []); - - const { loading: loadingCaseConfigure } = useCaseConfigure({ - setConnector: handleSetConnector, - }); - - const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); - - const handlePushToService = useCallback(() => { - if (connector != null) { - postPushToService({ - caseId, - ...connector, - updateCase, - }); - } - }, [caseId, connector, postPushToService, updateCase]); - - const errorsMsg = useMemo(() => { - let errors: Array<{ title: string; description: JSX.Element }> = []; - if (actionLicense != null && !actionLicense.enabledInLicense) { - errors = [...errors, getLicenseError()]; - } - if ( - (connector == null || (connector != null && connector.connectorId === 'none')) && - !loadingCaseConfigure && - !loadingLicense - ) { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, - description: ( - <FormattedMessage - defaultMessage="To open and update cases in external systems, you must configure a {link}." - id="xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription" - values={{ - link: ( - <EuiLink href={getConfigureCasesUrl(urlSearch)} target="_blank"> - {i18n.LINK_CONNECTOR_CONFIGURE} - </EuiLink> - ), - }} - /> - ), - }, - ]; - } - if (caseStatus === 'closed') { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE, - description: ( - <FormattedMessage - defaultMessage="Closed cases cannot be sent to external systems. Reopen the case if you want to open or update it in an external system." - id="xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedDescription" - /> - ), - }, - ]; - } - if (actionLicense != null && !actionLicense.enabledInConfig) { - errors = [...errors, getKibanaConfigError()]; - } - return errors; - }, [actionLicense, caseStatus, connector, loadingCaseConfigure, loadingLicense, urlSearch]); - - const pushToServiceButton = useMemo( - () => ( - <EuiButton - data-test-subj="push-to-service-now" - fill - iconType="importAction" - onClick={handlePushToService} - disabled={ - isLoading || - loadingLicense || - loadingCaseConfigure || - errorsMsg.length > 0 || - !userCanCrud - } - isLoading={isLoading} - > - {isNew ? i18n.PUSH_SERVICENOW : i18n.UPDATE_PUSH_SERVICENOW} - </EuiButton> - ), - [ - isNew, - handlePushToService, - isLoading, - loadingLicense, - loadingCaseConfigure, - errorsMsg, - userCanCrud, - ] - ); - - const objToReturn = useMemo( - () => ({ - pushButton: - errorsMsg.length > 0 ? ( - <EuiToolTip - position="top" - title={errorsMsg[0].title} - content={<p>{errorsMsg[0].description}</p>} - > - {pushToServiceButton} - </EuiToolTip> - ) : ( - <>{pushToServiceButton}</> - ), - pushCallouts: - errorsMsg.length > 0 ? ( - <CaseCallOut title={i18n.ERROR_PUSH_SERVICE_CALLOUT_TITLE} messages={errorsMsg} /> - ) : null, - }), - [errorsMsg, pushToServiceButton] - ); - return objToReturn; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx deleted file mode 100644 index d6016e540bdc0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx +++ /dev/null @@ -1,77 +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 { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui'; -import React from 'react'; - -import { CaseFullExternalService } from '../../../../../../../../plugins/case/common/api'; -import { CaseUserActions } from '../../../../containers/case/types'; -import * as i18n from '../case_view/translations'; - -interface LabelTitle { - action: CaseUserActions; - field: string; - firstIndexPushToService: number; - index: number; -} - -export const getLabelTitle = ({ action, field, firstIndexPushToService, index }: LabelTitle) => { - if (field === 'tags') { - return getTagsLabelTitle(action); - } else if (field === 'title' && action.action === 'update') { - return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ - action.newValue - }"`; - } else if (field === 'description' && action.action === 'update') { - return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; - } else if (field === 'status' && action.action === 'update') { - return `${ - action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase() - } ${i18n.CASE}`; - } else if (field === 'comment' && action.action === 'update') { - return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; - } else if (field === 'pushed' && action.action === 'push-to-service' && action.newValue != null) { - return getPushedServiceLabelTitle(action, firstIndexPushToService, index); - } - return ''; -}; - -const getTagsLabelTitle = (action: CaseUserActions) => ( - <EuiFlexGroup alignItems="baseline" gutterSize="xs" component="span"> - <EuiFlexItem data-test-subj="ua-tags-label"> - {action.action === 'add' && i18n.ADDED_FIELD} - {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} - </EuiFlexItem> - {action.newValue != null && - action.newValue.split(',').map(tag => ( - <EuiFlexItem grow={false} key={tag}> - <EuiBadge data-test-subj={`ua-tag`} color="default"> - {tag} - </EuiBadge> - </EuiFlexItem> - ))} - </EuiFlexGroup> -); - -const getPushedServiceLabelTitle = ( - action: CaseUserActions, - firstIndexPushToService: number, - index: number -) => { - const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; - return ( - <EuiFlexGroup alignItems="baseline" gutterSize="xs" data-test-subj="pushed-service-label-title"> - <EuiFlexItem data-test-subj="pushed-label"> - {firstIndexPushToService === index ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiLink data-test-subj="pushed-value" href={pushedVal?.external_url} target="_blank"> - {pushedVal?.connector_name} {pushedVal?.external_title} - </EuiLink> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx deleted file mode 100644 index 1c71260422d4b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx +++ /dev/null @@ -1,331 +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 React from 'react'; -import { mount } from 'enzyme'; - -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { getFormMock } from '../__mock__/form'; -import { useUpdateComment } from '../../../../containers/case/use_update_comment'; -import { basicCase, getUserAction } from '../../../../containers/case/mock'; -import { UserActionTree } from './'; -import { TestProviders } from '../../../../mock'; -import { useFormMock } from '../create/index.test'; -import { wait } from '../../../../lib/helpers'; -import { act } from 'react-dom/test-utils'; -jest.mock( - '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); - -const fetchUserActions = jest.fn(); -const onUpdateField = jest.fn(); -const updateCase = jest.fn(); -const defaultProps = { - data: basicCase, - caseUserActions: [], - firstIndexPushToService: -1, - isLoadingDescription: false, - isLoadingUserActions: false, - lastIndexPushToService: -1, - userCanCrud: true, - fetchUserActions, - onUpdateField, - updateCase, -}; -const useUpdateCommentMock = useUpdateComment as jest.Mock; -jest.mock('../../../../containers/case/use_update_comment'); - -const patchComment = jest.fn(); -describe('UserActionTree ', () => { - const sampleData = { - content: 'what a great comment update', - }; - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - useUpdateCommentMock.mockImplementation(() => ({ - isLoadingIds: [], - patchComment, - })); - const formHookMock = getFormMock(sampleData); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - }); - - it('Loading spinner when user actions loading and displays fullName/username', () => { - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...{ ...defaultProps, isLoadingUserActions: true }} /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="user-actions-loading"]`).exists()).toBeTruthy(); - - expect( - wrapper - .find(`[data-test-subj="user-action-avatar"]`) - .first() - .prop('name') - ).toEqual(defaultProps.data.createdBy.fullName); - expect( - wrapper - .find(`[data-test-subj="user-action-title"] strong`) - .first() - .text() - ).toEqual(defaultProps.data.createdBy.username); - }); - it('Renders service now update line with top and bottom when push is required', () => { - const ourActions = [ - getUserAction(['comment'], 'push-to-service'), - getUserAction(['comment'], 'update'), - ]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - lastIndexPushToService: 0, - }; - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy(); - }); - it('Renders service now update line with top only when push is up to date', () => { - const ourActions = [getUserAction(['comment'], 'push-to-service')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - lastIndexPushToService: 0, - }; - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy(); - }); - - it('Outlines comment when update move to link is clicked', () => { - const ourActions = [getUserAction(['comment'], 'create'), getUserAction(['comment'], 'update')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - expect( - wrapper - .find(`[data-test-subj="comment-create-action"]`) - .first() - .prop('idToOutline') - ).toEqual(''); - wrapper - .find(`[data-test-subj="comment-update-action"] [data-test-subj="move-to-link"]`) - .first() - .simulate('click'); - expect( - wrapper - .find(`[data-test-subj="comment-create-action"]`) - .first() - .prop('idToOutline') - ).toEqual(ourActions[0].commentId); - }); - - it('Switches to markdown when edit is clicked and back to panel when canceled', () => { - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) - .first() - .simulate('click'); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(true); - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]` - ) - .first() - .simulate('click'); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - }); - - it('calls update comment when comment markdown is saved', async () => { - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) - .first() - .simulate('click'); - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]` - ) - .first() - .simulate('click'); - await act(async () => { - await wait(); - wrapper.update(); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(patchComment).toBeCalledWith({ - commentUpdate: sampleData.content, - caseId: props.data.id, - commentId: props.data.comments[0].id, - fetchUserActions, - updateCase, - version: props.data.comments[0].version, - }); - }); - }); - - it('calls update description when description markdown is saved', async () => { - const props = defaultProps; - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`) - .first() - .simulate('click'); - wrapper - .find( - `[data-test-subj="user-action-description"] [data-test-subj="user-action-save-markdown"]` - ) - .first() - .simulate('click'); - await act(async () => { - await wait(); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(onUpdateField).toBeCalledWith('description', sampleData.content); - }); - }); - - it('quotes', async () => { - const commentData = { - comment: '', - }; - const formHookMock = getFormMock(commentData); - const setFieldValue = jest.fn(); - useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } })); - const props = defaultProps; - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) - .first() - .simulate('click'); - expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); - }); - it('Outlines comment when url param is provided', () => { - const commentId = 'neat-comment-id'; - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId }); - const wrapper = mount( - <TestProviders> - <Router history={mockHistory}> - <UserActionTree {...props} /> - </Router> - </TestProviders> - ); - expect( - wrapper - .find(`[data-test-subj="comment-create-action"]`) - .first() - .prop('idToOutline') - ).toEqual(commentId); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/utils.ts deleted file mode 100644 index df9f0d08e728c..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/case/utils.ts +++ /dev/null @@ -1,41 +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 { isEmpty } from 'lodash/fp'; -import { Breadcrumb } from 'ui/chrome'; - -import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; -import { RouteSpyState } from '../../utils/route/types'; -import * as i18n from './translations'; - -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => { - const queryParameters = !isEmpty(search[0]) ? search[0] : null; - - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getCaseUrl(queryParameters), - }, - ]; - if (params.detailName === 'create') { - breadcrumb = [ - ...breadcrumb, - { - text: i18n.CREATE_BC_TITLE, - href: getCreateCaseUrl(queryParameters), - }, - ]; - } else if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state?.caseTitle ?? '', - href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), - }, - ]; - } - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts deleted file mode 100644 index 7e6778ca4fb4f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts +++ /dev/null @@ -1,273 +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 { - getStringArray, - replaceTemplateFieldFromQuery, - replaceTemplateFieldFromMatchFilters, - reformatDataProviderWithNewValue, -} from './helpers'; -import { mockEcsData } from '../../../../mock/mock_ecs'; -import { Filter } from '../../../../../../../../../src/plugins/data/public'; -import { DataProvider } from '../../../../components/timeline/data_providers/data_provider'; -import { mockDataProviders } from '../../../../components/timeline/data_providers/mock/mock_data_providers'; -import { cloneDeep } from 'lodash/fp'; - -describe('helpers', () => { - let mockEcsDataClone = cloneDeep(mockEcsData); - beforeEach(() => { - mockEcsDataClone = cloneDeep(mockEcsData); - }); - describe('getStringOrStringArray', () => { - test('it should correctly return a string array', () => { - const value = getStringArray('x', { - x: 'The nickname of the developer we all :heart:', - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with a single element', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:'], - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with two elements of strings', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], - }); - expect(value).toEqual([ - 'The nickname of the developer we all :heart:', - 'We are all made of stars', - ]); - }); - - test('it should correctly return a string array with deep elements', () => { - const value = getStringArray('x.y.z', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual(['zed']); - }); - - test('it should correctly return a string array with a non-existent value', () => { - const value = getStringArray('non.existent', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual([]); - }); - - test('it should trace an error if the value is not a string', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: 5 }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - 5, - 'when trying to access field:', - 'a', - 'from data object of:', - { a: 5 } - ); - }); - - test('it should trace an error if the value is an array of mixed values', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - ['hi', 5], - 'when trying to access field:', - 'a', - 'from data object of:', - { a: ['hi', 5] } - ); - }); - }); - - describe('replaceTemplateFieldFromQuery', () => { - test('given an empty query string this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('given a query string with spaces this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('it should replace a query with a template value such as apache from a mock template', () => { - const replacement = replaceTemplateFieldFromQuery( - 'host.name: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('host.name: apache'); - }); - - test('it should replace a template field with an ECS value that is not an array', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); - expect(replacement).toEqual('host.name: *'); - }); - - test('it should NOT replace a query with a template value that is not part of the template fields array', () => { - const replacement = replaceTemplateFieldFromQuery( - 'user.id: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('user.id: placeholdertext'); - }); - }); - - describe('replaceTemplateFieldFromMatchFilters', () => { - test('given an empty query filter this will return an empty filter', () => { - const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); - expect(replacement).toEqual([]); - }); - - test('given a query filter this will return that filter with the placeholder replaced', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Braden' }, - }, - query: { match_phrase: { 'host.name': 'Braden' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'apache' }, - }, - query: { match_phrase: { 'host.name': 'apache' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - - test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - }); - - describe('reformatDataProviderWithNewValue', () => { - test('it should replace a query with a template value such as apache from a mock data provider', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'user.id'; - mockDataProvider.id = 'my-id'; - mockDataProvider.name = 'Rebecca'; - mockDataProvider.queryMatch.value = 'Rebecca'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'my-id', - name: 'Rebecca', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.id', - value: 'Rebecca', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts deleted file mode 100644 index d80ef066d86ac..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts +++ /dev/null @@ -1,165 +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 { get, isEmpty } from 'lodash/fp'; -import { Filter, esKuery, KueryNode } from '../../../../../../../../../src/plugins/data/public'; -import { - DataProvider, - DataProvidersAnd, -} from '../../../../components/timeline/data_providers/data_provider'; -import { Ecs } from '../../../../graphql/types'; - -interface FindValueToChangeInQuery { - field: string; - valueToChange: string; -} - -/** - * Fields that will be replaced with the template strings from a a saved timeline template. - * This is used for the signals detection engine feature when you save a timeline template - * and are the fields you can replace when creating a template. - */ -const templateFields = [ - 'host.name', - 'host.hostname', - 'host.domain', - 'host.id', - 'host.ip', - 'client.ip', - 'destination.ip', - 'server.ip', - 'source.ip', - 'network.community_id', - 'user.name', - 'process.name', -]; - -/** - * This will return an unknown as a string array if it exists from an unknown data type and a string - * that represents the path within the data object the same as lodash's "get". If the value is non-existent - * we will return an empty array. If it is a non string value then this will log a trace to the console - * that it encountered an error and return an empty array. - * @param field string of the field to access - * @param data The unknown data that is typically a ECS value to get the value - * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console - */ -export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { - const value: unknown | undefined = get(field, data); - if (value == null) { - return []; - } else if (typeof value === 'string') { - return [value]; - } else if (Array.isArray(value) && value.every(element => typeof element === 'string')) { - return value; - } else { - localConsole.trace( - 'Data type that is not a string or string array detected:', - value, - 'when trying to access field:', - field, - 'from data object of:', - data - ); - return []; - } -}; - -export const findValueToChangeInQuery = ( - kueryNode: KueryNode, - valueToChange: FindValueToChangeInQuery[] = [] -): FindValueToChangeInQuery[] => { - let localValueToChange = valueToChange; - if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { - localValueToChange = [ - ...localValueToChange, - { - field: kueryNode.arguments[0].value, - valueToChange: kueryNode.arguments[1].value, - }, - ]; - } - return kueryNode.arguments.reduce( - (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { - if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { - return [ - ...addValueToChange, - { - field: ast.arguments[0].value, - valueToChange: ast.arguments[1].value, - }, - ]; - } - if (ast.arguments) { - return findValueToChangeInQuery(ast, addValueToChange); - } - return addValueToChange; - }, - localValueToChange - ); -}; - -export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { - if (query.trim() !== '') { - const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); - return valueToChange.reduce((newQuery, vtc) => { - const newValue = getStringArray(vtc.field, ecsData); - if (newValue.length) { - return newQuery.replace(vtc.valueToChange, newValue[0]); - } else { - return newQuery; - } - }, query); - } else { - return ''; - } -}; - -export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => - filters.map(filter => { - if ( - filter.meta.type === 'phrase' && - filter.meta.key != null && - templateFields.includes(filter.meta.key) - ) { - const newValue = getStringArray(filter.meta.key, ecsData); - if (newValue.length) { - filter.meta.params = { query: newValue[0] }; - filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; - } - } - return filter; - }); - -export const reformatDataProviderWithNewValue = <T extends DataProvider | DataProvidersAnd>( - dataProvider: T, - ecsData: Ecs -): T => { - if (templateFields.includes(dataProvider.queryMatch.field)) { - const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); - if (newValue.length) { - dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); - dataProvider.name = newValue[0]; - dataProvider.queryMatch.value = newValue[0]; - dataProvider.queryMatch.displayField = undefined; - dataProvider.queryMatch.displayValue = undefined; - } - } - return dataProvider; -}; - -export const replaceTemplateFieldFromDataProviders = ( - dataProviders: DataProvider[], - ecsData: Ecs -): DataProvider[] => - dataProviders.map(dataProvider => { - const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); - if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { - newDataProvider.and = newDataProvider.and.map(andDataProvider => - reformatDataProviderWithNewValue(andDataProvider, ecsData) - ); - } - return newDataProvider; - }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx deleted file mode 100644 index ce8ae2054b2c7..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ /dev/null @@ -1,369 +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 { EuiPanel, EuiLoadingContent } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { Filter, esQuery } from '../../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; -import { StatefulEventsViewer } from '../../../../components/events_viewer'; -import { HeaderSection } from '../../../../components/header_section'; -import { combineQueries } from '../../../../components/timeline/helpers'; -import { useKibana } from '../../../../lib/kibana'; -import { inputsSelectors, State, inputsModel } from '../../../../store'; -import { timelineActions, timelineSelectors } from '../../../../store/timeline'; -import { TimelineModel } from '../../../../store/timeline/model'; -import { timelineDefaults } from '../../../../store/timeline/defaults'; -import { useApolloClient } from '../../../../utils/apollo_context'; - -import { updateSignalStatusAction } from './actions'; -import { - getSignalsActions, - requiredFieldsForActions, - signalsClosedFilters, - signalsDefaultModel, - signalsOpenFilters, -} from './default_config'; -import { - FILTER_CLOSED, - FILTER_OPEN, - SignalFilterOption, - SignalsTableFilterGroup, -} from './signals_filter_group'; -import { SignalsUtilityBar } from './signals_utility_bar'; -import * as i18n from './translations'; -import { - CreateTimelineProps, - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateSignalsStatusCallback, - UpdateSignalsStatusProps, -} from './types'; -import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers'; - -export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; - -interface OwnProps { - canUserCRUD: boolean; - defaultFilters?: Filter[]; - hasIndexWrite: boolean; - from: number; - loading: boolean; - signalsIndex: string; - to: number; -} - -type SignalsTableComponentProps = OwnProps & PropsFromRedux; - -export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({ - canUserCRUD, - clearEventsDeleted, - clearEventsLoading, - clearSelected, - defaultFilters, - from, - globalFilters, - globalQuery, - hasIndexWrite, - isSelectAllChecked, - loading, - loadingEventIds, - selectedEventIds, - setEventsDeleted, - setEventsLoading, - signalsIndex, - to, - updateTimeline, - updateTimelineIsLoading, -}) => { - const [selectAll, setSelectAll] = useState(false); - const apolloClient = useApolloClient(); - - const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); - const [filterGroup, setFilterGroup] = useState<SignalFilterOption>(FILTER_OPEN); - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( - signalsIndex !== '' ? [signalsIndex] : [] - ); - const kibana = useKibana(); - - const getGlobalQuery = useCallback(() => { - if (browserFields != null && indexPatterns != null) { - return combineQueries({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders: [], - indexPattern: indexPatterns, - browserFields, - filters: isEmpty(defaultFilters) - ? globalFilters - : [...(defaultFilters ?? []), ...globalFilters], - kqlQuery: globalQuery, - kqlMode: globalQuery.language, - start: from, - end: to, - isEventViewer: true, - }); - } - return null; - }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); - - // Callback for creating a new timeline -- utilized by row/batch actions - const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); - updateTimeline({ - duplicate: true, - from: fromTimeline, - id: 'timeline-1', - notes: [], - timeline: { - ...timeline, - show: true, - }, - to: toTimeline, - ruleNote, - })(); - }, - [updateTimeline, updateTimelineIsLoading] - ); - - const setEventsLoadingCallback = useCallback( - ({ eventIds, isLoading }: SetEventsLoadingProps) => { - setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading }); - }, - [setEventsLoading, SIGNALS_PAGE_TIMELINE_ID] - ); - - const setEventsDeletedCallback = useCallback( - ({ eventIds, isDeleted }: SetEventsDeletedProps) => { - setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted }); - }, - [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] - ); - - // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar - useEffect(() => { - if (!isSelectAllChecked) { - setShowClearSelectionAction(false); - } else { - setSelectAll(false); - } - }, [isSelectAllChecked]); - - // Callback for when open/closed filter changes - const onFilterGroupChangedCallback = useCallback( - (newFilterGroup: SignalFilterOption) => { - clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); - setFilterGroup(newFilterGroup); - }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] - ); - - // Callback for clearing entire selection from utility bar - const clearSelectionCallback = useCallback(() => { - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); - setSelectAll(false); - setShowClearSelectionAction(false); - }, [clearSelected, setSelectAll, setShowClearSelectionAction]); - - // Callback for selecting all events on all pages from utility bar - // Dispatches to stateful_body's selectAll via TimelineTypeContext props - // as scope of response data required to actually set selectedEvents - const selectAllCallback = useCallback(() => { - setSelectAll(true); - setShowClearSelectionAction(true); - }, [setSelectAll, setShowClearSelectionAction]); - - const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback( - async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => { - await updateSignalStatusAction({ - query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, - signalIds: Object.keys(selectedEventIds), - status, - setEventsDeleted: setEventsDeletedCallback, - setEventsLoading: setEventsLoadingCallback, - }); - refetchQuery(); - }, - [ - getGlobalQuery, - selectedEventIds, - setEventsDeletedCallback, - setEventsLoadingCallback, - showClearSelectionAction, - ] - ); - - // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component - const utilityBarCallback = useCallback( - (refetchQuery: inputsModel.Refetch, totalCount: number) => { - return ( - <SignalsUtilityBar - canUserCRUD={canUserCRUD} - areEventsLoading={loadingEventIds.length > 0} - clearSelection={clearSelectionCallback} - hasIndexWrite={hasIndexWrite} - isFilteredToOpen={filterGroup === FILTER_OPEN} - selectAll={selectAllCallback} - selectedEventIds={selectedEventIds} - showClearSelection={showClearSelectionAction} - totalCount={totalCount} - updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} - /> - ); - }, - [ - canUserCRUD, - hasIndexWrite, - clearSelectionCallback, - filterGroup, - loadingEventIds.length, - selectAllCallback, - selectedEventIds, - showClearSelectionAction, - updateSignalsStatusCallback, - ] - ); - - // Send to Timeline / Update Signal Status Actions for each table row - const additionalActions = useMemo( - () => - getSignalsActions({ - apolloClient, - canUserCRUD, - hasIndexWrite, - createTimeline: createTimelineCallback, - setEventsLoading: setEventsLoadingCallback, - setEventsDeleted: setEventsDeletedCallback, - status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, - updateTimelineIsLoading, - }), - [ - apolloClient, - canUserCRUD, - createTimelineCallback, - hasIndexWrite, - filterGroup, - setEventsLoadingCallback, - setEventsDeletedCallback, - updateTimelineIsLoading, - ] - ); - - const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); - const defaultFiltersMemo = useMemo(() => { - if (isEmpty(defaultFilters)) { - return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters; - } else if (defaultFilters != null && !isEmpty(defaultFilters)) { - return [ - ...defaultFilters, - ...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters), - ]; - } - }, [defaultFilters, filterGroup]); - - const timelineTypeContext = useMemo( - () => ({ - documentType: i18n.SIGNALS_DOCUMENT_TYPE, - footerText: i18n.TOTAL_COUNT_OF_SIGNALS, - loadingText: i18n.LOADING_SIGNALS, - queryFields: requiredFieldsForActions, - timelineActions: additionalActions, - title: i18n.SIGNALS_TABLE_TITLE, - selectAll: canUserCRUD ? selectAll : false, - }), - [additionalActions, canUserCRUD, selectAll] - ); - - const headerFilterGroup = useMemo( - () => <SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />, - [onFilterGroupChangedCallback] - ); - - if (loading || isEmpty(signalsIndex)) { - return ( - <EuiPanel> - <HeaderSection title={i18n.SIGNALS_TABLE_TITLE} /> - <EuiLoadingContent data-test-subj="loading-signals-panel" /> - </EuiPanel> - ); - } - - return ( - <StatefulEventsViewer - defaultIndices={defaultIndices} - pageFilters={defaultFiltersMemo} - defaultModel={signalsDefaultModel} - end={to} - headerFilterGroup={headerFilterGroup} - id={SIGNALS_PAGE_TIMELINE_ID} - start={from} - timelineTypeContext={timelineTypeContext} - utilityBar={utilityBarCallback} - /> - ); -}; - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getGlobalInputs = inputsSelectors.globalSelector(); - const mapStateToProps = (state: State) => { - const timeline: TimelineModel = - getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults; - const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; - - const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); - const { query, filters } = globalInputs; - return { - globalQuery: query, - globalFilters: filters, - deletedEventIds, - isSelectAllChecked, - loadingEventIds, - selectedEventIds, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), - setEventsLoading: ({ - id, - eventIds, - isLoading, - }: { - id: string; - eventIds: string[]; - isLoading: boolean; - }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), - clearEventsLoading: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsLoading({ id })), - setEventsDeleted: ({ - id, - eventIds, - isDeleted, - }: { - id: string; - eventIds: string[]; - isDeleted: boolean; - }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), - clearEventsDeleted: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsDeleted({ id })), - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const SignalsTable = connector(React.memo(SignalsTableComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx deleted file mode 100644 index 847fcc7860085..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ /dev/null @@ -1,125 +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 { isEmpty } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../../plugins/siem/common/constants'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../../components/utility_bar'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../../../../lib/kibana'; -import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { UpdateSignalsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; - -interface SignalsUtilityBarProps { - canUserCRUD: boolean; - hasIndexWrite: boolean; - areEventsLoading: boolean; - clearSelection: () => void; - isFilteredToOpen: boolean; - selectAll: () => void; - selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; - showClearSelection: boolean; - totalCount: number; - updateSignalsStatus: UpdateSignalsStatus; -} - -const SignalsUtilityBarComponent: React.FC<SignalsUtilityBarProps> = ({ - canUserCRUD, - hasIndexWrite, - areEventsLoading, - clearSelection, - totalCount, - selectedEventIds, - isFilteredToOpen, - selectAll, - showClearSelection, - updateSignalsStatus, -}) => { - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - - const handleUpdateStatus = useCallback(async () => { - await updateSignalsStatus({ - signalIds: Object.keys(selectedEventIds), - status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, - }); - }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); - - const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); - const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( - defaultNumberFormat - ); - - return ( - <> - <UtilityBar> - <UtilityBarSection> - <UtilityBarGroup> - <UtilityBarText dataTestSubj="showingSignals"> - {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} - </UtilityBarText> - </UtilityBarGroup> - - <UtilityBarGroup> - {canUserCRUD && hasIndexWrite && ( - <> - <UtilityBarText dataTestSubj="selectedSignals"> - {i18n.SELECTED_SIGNALS( - showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, - showClearSelection ? totalCount : Object.keys(selectedEventIds).length - )} - </UtilityBarText> - - <UtilityBarAction - dataTestSubj="openCloseSignal" - disabled={areEventsLoading || isEmpty(selectedEventIds)} - iconType={isFilteredToOpen ? 'securitySignalResolved' : 'securitySignalDetected'} - onClick={handleUpdateStatus} - > - {isFilteredToOpen - ? i18n.BATCH_ACTION_CLOSE_SELECTED - : i18n.BATCH_ACTION_OPEN_SELECTED} - </UtilityBarAction> - - <UtilityBarAction - iconType={showClearSelection ? 'cross' : 'pagesSelect'} - onClick={() => { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }} - > - {showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} - </UtilityBarAction> - </> - )} - </UtilityBarGroup> - </UtilityBarSection> - </UtilityBar> - </> - ); -}; - -export const SignalsUtilityBar = React.memo( - SignalsUtilityBarComponent, - (prevProps, nextProps) => - prevProps.areEventsLoading === nextProps.areEventsLoading && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection -); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx deleted file mode 100644 index 90bdd39e4a6fa..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx +++ /dev/null @@ -1,101 +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 { showAllOthersBucket } from '../../../../../../../../plugins/siem/common/constants'; -import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; -import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types'; -import * as i18n from './translations'; - -export const formatSignalsData = ( - signalsData: SignalSearchResponse<{}, SignalsAggregation> | null -) => { - const groupBuckets: SignalsGroupBucket[] = - signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; - return groupBuckets.reduce<HistogramData[]>((acc, { key: group, signals }) => { - const signalsBucket: SignalsBucket[] = signals.buckets ?? []; - - return [ - ...acc, - ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ - x: key, - y: doc_count, - g: group, - })), - ]; - }, []); -}; - -export const getSignalsHistogramQuery = ( - stackByField: string, - from: number, - to: number, - additionalFilters: Array<{ - bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; - }> -) => { - const missing = showAllOthersBucket.includes(stackByField) - ? { - missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, - } - : {}; - - return { - aggs: { - signalsByGrouping: { - terms: { - field: stackByField, - ...missing, - order: { - _count: 'desc', - }, - size: 10, - }, - aggs: { - signals: { - date_histogram: { - field: '@timestamp', - fixed_interval: `${Math.floor((to - from) / 32)}ms`, - min_doc_count: 0, - extended_bounds: { - min: from, - max: to, - }, - }, - }, - }, - }, - }, - query: { - bool: { - filter: [ - ...additionalFilters, - { - range: { - '@timestamp': { - gte: from, - lte: to, - }, - }, - }, - ], - }, - }, - }; -}; - -/** - * Returns `true` when the signals histogram initial loading spinner should be shown - * - * @param isInitialLoading The loading spinner will only be displayed if this value is `true`, because after initial load, a different, non-spinner loading indicator is displayed - * @param isLoadingSignals When `true`, IO is being performed to request signals (for rendering in the histogram) - */ -export const showInitialLoadingSpinner = ({ - isInitialLoading, - isLoadingSignals, -}: { - isInitialLoading: boolean; - isLoadingSignals: boolean; -}): boolean => isInitialLoading && isLoadingSignals; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx deleted file mode 100644 index e70ba804ec018..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ /dev/null @@ -1,283 +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 { Position } from '@elastic/charts'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash/fp'; -import uuid from 'uuid'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../../plugins/siem/common/constants'; -import { LegendItem } from '../../../../components/charts/draggable_legend_item'; -import { escapeDataProviderId } from '../../../../components/drag_and_drop/helpers'; -import { HeaderSection } from '../../../../components/header_section'; -import { Filter, esQuery, Query } from '../../../../../../../../../src/plugins/data/public'; -import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; -import { getDetectionEngineUrl } from '../../../../components/link_to'; -import { defaultLegendColors } from '../../../../components/matrix_histogram/utils'; -import { InspectButtonContainer } from '../../../../components/inspect'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; -import { MatrixHistogramOption } from '../../../../components/matrix_histogram/types'; -import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; -import { navTabs } from '../../../home/home_navigations'; -import { signalsHistogramOptions } from './config'; -import { formatSignalsData, getSignalsHistogramQuery, showInitialLoadingSpinner } from './helpers'; -import { SignalsHistogram } from './signals_histogram'; -import * as i18n from './translations'; -import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; - -const DEFAULT_PANEL_HEIGHT = 300; - -const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` - display: flex; - flex-direction: column; - ${({ height }) => (height != null ? `height: ${height}px;` : '')} - position: relative; -`; - -const defaultTotalSignalsObj: SignalsTotal = { - value: 0, - relation: 'eq', -}; - -export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; - -const ViewSignalsFlexItem = styled(EuiFlexItem)` - margin-left: 24px; -`; - -interface SignalsHistogramPanelProps { - chartHeight?: number; - defaultStackByOption?: SignalsHistogramOption; - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - /** Override all defaults, and only display this field */ - onlyField?: string; - query?: Query; - legendPosition?: Position; - panelHeight?: number; - signalIndexName: string | null; - setQuery: (params: RegisterQuery) => void; - showLinkToSignals?: boolean; - showTotalSignalsCount?: boolean; - stackByOptions?: SignalsHistogramOption[]; - title?: string; - to: number; - updateDateRange: (min: number, max: number) => void; -} - -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - -const NO_LEGEND_DATA: LegendItem[] = []; - -export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( - ({ - chartHeight, - defaultStackByOption = signalsHistogramOptions[0], - deleteQuery, - filters, - headerChildren, - onlyField, - query, - from, - legendPosition = 'right', - panelHeight = DEFAULT_PANEL_HEIGHT, - setQuery, - signalIndexName, - showLinkToSignals = false, - showTotalSignalsCount = false, - stackByOptions, - to, - title = i18n.HISTOGRAM_HEADER, - updateDateRange, - }) => { - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); - const [isInitialLoading, setIsInitialLoading] = useState(true); - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const [totalSignalsObj, setTotalSignalsObj] = useState<SignalsTotal>(defaultTotalSignalsObj); - const [selectedStackByOption, setSelectedStackByOption] = useState<SignalsHistogramOption>( - onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) - ); - const { - loading: isLoadingSignals, - data: signalsData, - setQuery: setSignalsQuery, - response, - request, - refetch, - } = useQuerySignals<{}, SignalsAggregation>( - getSignalsHistogramQuery(selectedStackByOption.value, from, to, []), - signalIndexName - ); - const kibana = useKibana(); - const urlSearch = useGetUrlSearch(navTabs.detections); - - const totalSignals = useMemo( - () => - i18n.SHOWING_SIGNALS( - numeral(totalSignalsObj.value).format(defaultNumberFormat), - totalSignalsObj.value, - totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' - ), - [totalSignalsObj] - ); - - const setSelectedOptionCallback = useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { - setSelectedStackByOption( - stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption - ); - }, []); - - const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); - - const legendItems: LegendItem[] = useMemo( - () => - signalsData?.aggregations?.signalsByGrouping?.buckets != null - ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ - color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, - dataProviderId: escapeDataProviderId( - `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` - ), - field: selectedStackByOption.value, - value: bucket.key, - })) - : NO_LEGEND_DATA, - [signalsData, selectedStackByOption.value] - ); - - useEffect(() => { - let canceled = false; - - if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingSignals })) { - setIsInitialLoading(false); - } - - return () => { - canceled = true; // prevent long running data fetches from updating state after unmounting - }; - }, [isInitialLoading, isLoadingSignals, setIsInitialLoading]); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - }, []); - - useEffect(() => { - if (refetch != null && setQuery != null) { - setQuery({ - id: uniqueQueryId, - inspect: { - dsl: [request], - response: [response], - }, - loading: isLoadingSignals, - refetch, - }); - } - }, [setQuery, isLoadingSignals, signalsData, response, request, refetch]); - - useEffect(() => { - setTotalSignalsObj( - signalsData?.hits.total ?? { - value: 0, - relation: 'eq', - } - ); - }, [signalsData]); - - useEffect(() => { - const converted = esQuery.buildEsQuery( - undefined, - query != null ? [query] : [], - filters?.filter(f => f.meta.disabled === false) ?? [], - { - ...esQuery.getEsQueryConfig(kibana.services.uiSettings), - dateFormatTZ: undefined, - } - ); - - setSignalsQuery( - getSignalsHistogramQuery( - selectedStackByOption.value, - from, - to, - !isEmpty(converted) ? [converted] : [] - ) - ); - }, [selectedStackByOption.value, from, to, query, filters]); - - const linkButton = useMemo(() => { - if (showLinkToSignals) { - return ( - <ViewSignalsFlexItem grow={false}> - <EuiButton href={getDetectionEngineUrl(urlSearch)}>{i18n.VIEW_SIGNALS}</EuiButton> - </ViewSignalsFlexItem> - ); - } - }, [showLinkToSignals, urlSearch]); - - const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ - onlyField, - title, - ]); - - return ( - <InspectButtonContainer data-test-subj="signals-histogram-panel" show={!isInitialLoading}> - <StyledEuiPanel height={panelHeight}> - <HeaderSection - id={uniqueQueryId} - title={titleText} - titleSize={onlyField == null ? 'm' : 's'} - subtitle={!isInitialLoading && showTotalSignalsCount && totalSignals} - > - <EuiFlexGroup alignItems="center" gutterSize="none"> - <EuiFlexItem grow={false}> - {stackByOptions && ( - <EuiSelect - onChange={setSelectedOptionCallback} - options={stackByOptions} - prepend={i18n.STACK_BY_LABEL} - value={selectedStackByOption.value} - /> - )} - {headerChildren != null && headerChildren} - </EuiFlexItem> - {linkButton} - </EuiFlexGroup> - </HeaderSection> - - {isInitialLoading ? ( - <MatrixLoader /> - ) : ( - <SignalsHistogram - chartHeight={chartHeight} - data={formattedSignalsData} - from={from} - legendItems={legendItems} - legendPosition={legendPosition} - loading={isLoadingSignals} - to={to} - updateDateRange={updateDateRange} - /> - )} - </StyledEuiPanel> - </InspectButtonContainer> - ); - } -); - -SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts deleted file mode 100644 index 5e0293325289b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ /dev/null @@ -1,218 +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 { esFilters } from '../../../../../../../../../../src/plugins/data/public'; -import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; -import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; -import { FieldValueQueryBar } from '../../components/query_bar'; - -export const mockQueryBar: FieldValueQueryBar = { - query: { - query: 'test query', - language: 'kuery', - }, - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', -}; - -export const mockRule = (id: string): Rule => ({ - actions: [], - created_at: '2020-01-10T21:11:45.839Z', - updated_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: [], - filters: [], - from: 'now-300s', - id, - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - language: 'kuery', - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 21, - name: 'Home Grown!', - query: '', - references: [], - saved_id: "Garrett's IP", - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Untitled timeline', - meta: { from: '0m' }, - severity: 'low', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'saved_query', - threat: [], - throttle: 'no_actions', - note: '# this is some markdown documentation', - version: 1, -}); - -export const mockRuleWithEverything = (id: string): Rule => ({ - actions: [], - created_at: '2020-01-10T21:11:45.839Z', - updated_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: ['test'], - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - from: 'now-300s', - id, - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - language: 'kuery', - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 21, - name: 'Query with rule-id', - query: 'user.name: root or user.name: admin', - references: ['www.test.co'], - saved_id: 'test123', - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Titled timeline', - meta: { from: '0m' }, - severity: 'low', - updated_by: 'elastic', - tags: ['tag1', 'tag2'], - to: 'now', - type: 'saved_query', - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - throttle: 'no_actions', - note: '# this is some markdown documentation', - version: 1, -}); - -export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ - isNew, - name: 'Query with rule-id', - description: '24/7', - severity: 'low', - riskScore: 21, - references: ['www.test.co'], - falsePositives: ['test'], - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - note: '# this is some markdown documentation', -}); - -export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({ - isNew, - actions: [], - kibanaSiemAppUrl: 'http://localhost:5601/app/siem', - enabled, - throttle: 'no_actions', -}); - -export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ - isNew, - ruleType: 'query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['filebeat-'], - queryBar: mockQueryBar, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Titled timeline', - }, -}); - -export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ - isNew, - interval: '5m', - from: '6m', - to: 'now', -}); - -export const mockRuleError = (id: string): RuleError => ({ - rule_id: id, - error: { status_code: 404, message: `id: "${id}" not found` }, -}); - -export const mockRules: Rule[] = [ - mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), - mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx deleted file mode 100644 index 8bea504f84206..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ /dev/null @@ -1,332 +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. - */ - -/* eslint-disable react/display-name */ - -import { - EuiBadge, - EuiLink, - EuiBasicTableColumn, - EuiTableActionsColumnType, - EuiText, - EuiHealth, - EuiToolTip, -} from '@elastic/eui'; -import { FormattedRelative } from '@kbn/i18n/react'; -import * as H from 'history'; -import React, { Dispatch } from 'react'; - -import { isMlRule } from '../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules'; -import { getEmptyTagValue } from '../../../../components/empty_value'; -import { FormattedDate } from '../../../../components/formatted_date'; -import { getRuleDetailsUrl } from '../../../../components/link_to/redirect_to_detection_engine'; -import { ActionToaster } from '../../../../components/toasters'; -import { TruncatableText } from '../../../../components/truncatable_text'; -import { getStatusColor } from '../components/rule_status/helpers'; -import { RuleSwitch } from '../components/rule_switch'; -import { SeverityBadge } from '../components/severity_badge'; -import * as i18n from '../translations'; -import { - deleteRulesAction, - duplicateRulesAction, - editRuleAction, - exportRulesAction, -} from './actions'; -import { Action } from './reducer'; -import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip'; -import * as detectionI18n from '../../translations'; - -export const getActions = ( - dispatch: React.Dispatch<Action>, - dispatchToaster: Dispatch<ActionToaster>, - history: H.History, - reFetchRules: (refreshPrePackagedRule?: boolean) => void -) => [ - { - description: i18n.EDIT_RULE_SETTINGS, - icon: 'controlsHorizontal', - name: i18n.EDIT_RULE_SETTINGS, - onClick: (rowItem: Rule) => editRuleAction(rowItem, history), - }, - { - description: i18n.DUPLICATE_RULE, - icon: 'copy', - name: i18n.DUPLICATE_RULE, - onClick: async (rowItem: Rule) => { - await duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster); - await reFetchRules(true); - }, - }, - { - description: i18n.EXPORT_RULE, - icon: 'exportAction', - name: i18n.EXPORT_RULE, - onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch), - enabled: (rowItem: Rule) => !rowItem.immutable, - }, - { - 'data-test-subj': 'deleteRuleAction', - description: i18n.DELETE_RULE, - icon: 'trash', - name: i18n.DELETE_RULE, - onClick: async (rowItem: Rule) => { - await deleteRulesAction([rowItem.id], dispatch, dispatchToaster); - await reFetchRules(true); - }, - }, -]; - -export type RuleStatusRowItemType = RuleStatus & { - name: string; - id: string; -}; -export type RulesColumns = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>; -export type RulesStatusesColumns = EuiBasicTableColumn<RuleStatusRowItemType>; - -interface GetColumns { - dispatch: React.Dispatch<Action>; - dispatchToaster: Dispatch<ActionToaster>; - history: H.History; - hasMlPermissions: boolean; - hasNoPermissions: boolean; - loadingRuleIds: string[]; - reFetchRules: (refreshPrePackagedRule?: boolean) => void; -} - -// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? -export const getColumns = ({ - dispatch, - dispatchToaster, - history, - hasMlPermissions, - hasNoPermissions, - loadingRuleIds, - reFetchRules, -}: GetColumns): RulesColumns[] => { - const cols: RulesColumns[] = [ - { - field: 'name', - name: i18n.COLUMN_RULE, - render: (value: Rule['name'], item: Rule) => ( - <EuiLink data-test-subj="ruleName" href={getRuleDetailsUrl(item.id)}> - {value} - </EuiLink> - ), - truncateText: true, - width: '24%', - }, - { - field: 'risk_score', - name: i18n.COLUMN_RISK_SCORE, - render: (value: Rule['risk_score']) => ( - <EuiText data-test-subj="riskScore" size="s"> - {value} - </EuiText> - ), - truncateText: true, - width: '14%', - }, - { - field: 'severity', - name: i18n.COLUMN_SEVERITY, - render: (value: Rule['severity']) => <SeverityBadge value={value} />, - truncateText: true, - width: '16%', - }, - { - field: 'status_date', - name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: Rule['status_date']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - <LocalizedDateTooltip fieldName={i18n.COLUMN_LAST_COMPLETE_RUN} date={new Date(value)}> - <FormattedRelative value={value} /> - </LocalizedDateTooltip> - ); - }, - truncateText: true, - width: '20%', - }, - { - field: 'status', - name: i18n.COLUMN_LAST_RESPONSE, - render: (value: Rule['status']) => { - return ( - <> - <EuiHealth color={getStatusColor(value ?? null)}> - {value ?? getEmptyTagValue()} - </EuiHealth> - </> - ); - }, - width: '16%', - truncateText: true, - }, - { - field: 'tags', - name: i18n.COLUMN_TAGS, - render: (value: Rule['tags']) => ( - <TruncatableText data-test-subj="tags"> - {value.map((tag, i) => ( - <EuiBadge color="hollow" key={`${tag}-${i}`}> - {tag} - </EuiBadge> - ))} - </TruncatableText> - ), - truncateText: true, - width: '20%', - }, - { - align: 'center', - field: 'enabled', - name: i18n.COLUMN_ACTIVATE, - render: (value: Rule['enabled'], item: Rule) => ( - <EuiToolTip - position="top" - content={ - isMlRule(item.type) && !hasMlPermissions - ? detectionI18n.ML_RULES_DISABLED_MESSAGE - : undefined - } - > - <RuleSwitch - data-test-subj="enabled" - dispatch={dispatch} - id={item.id} - enabled={item.enabled} - isDisabled={ - hasNoPermissions || (isMlRule(item.type) && !hasMlPermissions && !item.enabled) - } - isLoading={loadingRuleIds.includes(item.id)} - /> - </EuiToolTip> - ), - sortable: true, - width: '95px', - }, - ]; - const actions: RulesColumns[] = [ - { - actions: getActions(dispatch, dispatchToaster, history, reFetchRules), - width: '40px', - } as EuiTableActionsColumnType<Rule>, - ]; - - return hasNoPermissions ? cols : [...cols, ...actions]; -}; - -export const getMonitoringColumns = (): RulesStatusesColumns[] => { - const cols: RulesStatusesColumns[] = [ - { - field: 'name', - name: i18n.COLUMN_RULE, - render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => { - return ( - <EuiLink data-test-subj="ruleName" href={getRuleDetailsUrl(item.id)}> - {value} - </EuiLink> - ); - }, - truncateText: true, - width: '24%', - }, - { - field: 'current_status.bulk_create_time_durations', - name: i18n.COLUMN_INDEXING_TIMES, - render: (value: RuleStatus['current_status']['bulk_create_time_durations']) => ( - <EuiText data-test-subj="bulk_create_time_durations" size="s"> - {value != null && value.length > 0 - ? Math.max(...value?.map(item => Number.parseFloat(item))) - : null} - </EuiText> - ), - truncateText: true, - width: '14%', - }, - { - field: 'current_status.search_after_time_durations', - name: i18n.COLUMN_QUERY_TIMES, - render: (value: RuleStatus['current_status']['search_after_time_durations']) => ( - <EuiText data-test-subj="search_after_time_durations" size="s"> - {value != null && value.length > 0 - ? Math.max(...value?.map(item => Number.parseFloat(item))) - : null} - </EuiText> - ), - truncateText: true, - width: '14%', - }, - { - field: 'current_status.gap', - name: i18n.COLUMN_GAP, - render: (value: RuleStatus['current_status']['gap']) => ( - <EuiText data-test-subj="gap" size="s"> - {value} - </EuiText> - ), - truncateText: true, - width: '14%', - }, - { - field: 'current_status.last_look_back_date', - name: i18n.COLUMN_LAST_LOOKBACK_DATE, - render: (value: RuleStatus['current_status']['last_look_back_date']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - <FormattedDate value={value} fieldName={'last look back date'} /> - ); - }, - truncateText: true, - width: '16%', - }, - { - field: 'current_status.status_date', - name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: RuleStatus['current_status']['status_date']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - <LocalizedDateTooltip fieldName={i18n.COLUMN_LAST_COMPLETE_RUN} date={new Date(value)}> - <FormattedRelative value={value} /> - </LocalizedDateTooltip> - ); - }, - truncateText: true, - width: '20%', - }, - { - field: 'current_status.status', - name: i18n.COLUMN_LAST_RESPONSE, - render: (value: RuleStatus['current_status']['status']) => { - return ( - <> - <EuiHealth color={getStatusColor(value ?? null)}> - {value ?? getEmptyTagValue()} - </EuiHealth> - </> - ); - }, - width: '16%', - truncateText: true, - }, - { - field: 'activate', - name: i18n.COLUMN_ACTIVATE, - render: (value: Rule['enabled']) => ( - <EuiText data-test-subj="search_after_time_durations" size="s"> - {value ? i18n.ACTIVE : i18n.INACTIVE} - </EuiText> - ), - width: '95px', - }, - ]; - - return cols; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx deleted file mode 100644 index f4955c2a93b8d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx +++ /dev/null @@ -1,40 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { AllRules } from './index'; - -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - - return { - ...originalModule, - useHistory: jest.fn(), - }; -}); - -describe('AllRules', () => { - it('renders correctly', () => { - const wrapper = shallow( - <AllRules - createPrePackagedRules={jest.fn()} - hasNoPermissions={false} - loading={false} - loadingCreatePrePackagedRules={false} - refetchPrePackagedRulesStatus={jest.fn()} - rulesCustomInstalled={0} - rulesInstalled={0} - rulesNotInstalled={0} - rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} - /> - ); - - expect(wrapper.find('[title="All rules"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx deleted file mode 100644 index e96ed856208bd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ /dev/null @@ -1,375 +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 { EuiBasicTable, EuiContextMenuPanel, EuiLoadingContent, EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import uuid from 'uuid'; - -import { - useRules, - useRulesStatuses, - CreatePreBuiltRules, - FilterOptions, - Rule, - PaginationOptions, - exportRules, -} from '../../../../containers/detection_engine/rules'; -import { HeaderSection } from '../../../../components/header_section'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../components/utility_bar'; -import { useStateToaster } from '../../../../components/toasters'; -import { Loader } from '../../../../components/loader'; -import { Panel } from '../../../../components/panel'; -import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_empty_prompt'; -import { GenericDownloader } from '../../../../components/generic_downloader'; -import { AllRulesTables, SortingType } from '../components/all_rules_tables'; -import { getPrePackagedRuleStatus } from '../helpers'; -import * as i18n from '../translations'; -import { EuiBasicTableOnChange } from '../types'; -import { getBatchItems } from './batch_actions'; -import { getColumns, getMonitoringColumns } from './columns'; -import { showRulesTable } from './helpers'; -import { allRulesReducer, State } from './reducer'; -import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; -import { useMlCapabilities } from '../../../../components/ml_popover/hooks/use_ml_capabilities'; -import { hasMlAdminPermissions } from '../../../../components/ml/permissions/has_ml_admin_permissions'; - -const SORT_FIELD = 'enabled'; -const initialState: State = { - exportRuleIds: [], - filterOptions: { - filter: '', - sortField: SORT_FIELD, - sortOrder: 'desc', - }, - loadingRuleIds: [], - loadingRulesAction: null, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - rules: [], - selectedRuleIds: [], -}; - -interface AllRulesProps { - createPrePackagedRules: CreatePreBuiltRules | null; - hasNoPermissions: boolean; - loading: boolean; - loadingCreatePrePackagedRules: boolean; - refetchPrePackagedRulesStatus: () => void; - rulesCustomInstalled: number | null; - rulesInstalled: number | null; - rulesNotInstalled: number | null; - rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; -} - -/** - * Table Component for displaying all Rules for a given cluster. Provides the ability to filter - * by name, sort by enabled, and perform the following actions: - * * Enable/Disable - * * Duplicate - * * Delete - * * Import/Export - */ -export const AllRules = React.memo<AllRulesProps>( - ({ - createPrePackagedRules, - hasNoPermissions, - loading, - loadingCreatePrePackagedRules, - refetchPrePackagedRulesStatus, - rulesCustomInstalled, - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated, - setRefreshRulesData, - }) => { - const [initLoading, setInitLoading] = useState(true); - const tableRef = useRef<EuiBasicTable>(); - const [ - { - exportRuleIds, - filterOptions, - loadingRuleIds, - loadingRulesAction, - pagination, - rules, - selectedRuleIds, - }, - dispatch, - ] = useReducer(allRulesReducer(tableRef), initialState); - const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); - const history = useHistory(); - const [, dispatchToaster] = useStateToaster(); - const mlCapabilities = useMlCapabilities(); - - // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); - - const setRules = useCallback((newRules: Rule[], newPagination: Partial<PaginationOptions>) => { - dispatch({ - type: 'setRules', - rules: newRules, - pagination: newPagination, - }); - }, []); - - const [isLoadingRules, , reFetchRulesData] = useRules({ - pagination, - filterOptions, - refetchPrePackagedRulesStatus, - dispatchRulesInReducer: setRules, - }); - - const sorting = useMemo( - (): SortingType => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }), - [filterOptions.sortOrder] - ); - - const prePackagedRuleStatus = getPrePackagedRuleStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - const getBatchItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - <EuiContextMenuPanel - items={getBatchItems({ - closePopover, - dispatch, - dispatchToaster, - hasMlPermissions, - loadingRuleIds, - selectedRuleIds, - reFetchRules: reFetchRulesData, - rules, - })} - /> - ), - [ - dispatch, - dispatchToaster, - hasMlPermissions, - loadingRuleIds, - reFetchRulesData, - rules, - selectedRuleIds, - ] - ); - - const paginationMemo = useMemo( - () => ({ - pageIndex: pagination.page - 1, - pageSize: pagination.perPage, - totalItemCount: pagination.total, - pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], - }), - [pagination] - ); - - const tableOnChangeCallback = useCallback( - ({ page, sort }: EuiBasicTableOnChange) => { - dispatch({ - type: 'updateFilterOptions', - filterOptions: { - sortField: SORT_FIELD, // Only enabled is supported for sorting currently - sortOrder: sort?.direction ?? 'desc', - }, - pagination: { page: page.index + 1, perPage: page.size }, - }); - }, - [dispatch] - ); - - const rulesColumns = useMemo(() => { - return getColumns({ - dispatch, - dispatchToaster, - history, - hasMlPermissions, - hasNoPermissions, - loadingRuleIds: - loadingRulesAction != null && - (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') - ? loadingRuleIds - : [], - reFetchRules: reFetchRulesData, - }); - }, [ - dispatch, - dispatchToaster, - hasMlPermissions, - history, - loadingRuleIds, - loadingRulesAction, - reFetchRulesData, - ]); - - const monitoringColumns = useMemo(() => getMonitoringColumns(), []); - - useEffect(() => { - if (reFetchRulesData != null) { - setRefreshRulesData(reFetchRulesData); - } - }, [reFetchRulesData, setRefreshRulesData]); - - useEffect(() => { - if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) { - setInitLoading(false); - } - }, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]); - - const handleCreatePrePackagedRules = useCallback(async () => { - if (createPrePackagedRules != null && reFetchRulesData != null) { - await createPrePackagedRules(); - reFetchRulesData(true); - } - }, [createPrePackagedRules, reFetchRulesData]); - - const euiBasicTableSelectionProps = useMemo( - () => ({ - selectable: (item: Rule) => !loadingRuleIds.includes(item.id), - onSelectionChange: (selected: Rule[]) => - dispatch({ type: 'selectedRuleIds', ids: selected.map(r => r.id) }), - }), - [loadingRuleIds] - ); - - const onFilterChangedCallback = useCallback((newFilterOptions: Partial<FilterOptions>) => { - dispatch({ - type: 'updateFilterOptions', - filterOptions: { - ...newFilterOptions, - }, - pagination: { page: 1 }, - }); - }, []); - - const isLoadingAnActionOnRule = useMemo(() => { - if ( - loadingRuleIds.length > 0 && - (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') - ) { - return false; - } else if (loadingRuleIds.length > 0) { - return true; - } - return false; - }, [loadingRuleIds, loadingRulesAction]); - - return ( - <> - <GenericDownloader - filename={`${i18n.EXPORT_FILENAME}.ndjson`} - ids={exportRuleIds} - onExportSuccess={exportCount => { - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), - color: 'success', - iconType: 'check', - }, - }); - }} - exportSelectedData={exportRules} - /> - <EuiSpacer /> - - <Panel loading={loading || isLoadingRules || isLoadingRulesStatuses}> - <> - <HeaderSection split title={i18n.ALL_RULES}> - <RulesTableFilters - onFilterChanged={onFilterChangedCallback} - rulesCustomInstalled={rulesCustomInstalled} - rulesInstalled={rulesInstalled} - /> - </HeaderSection> - - {(loading || isLoadingRules || isLoadingAnActionOnRule || isLoadingRulesStatuses) && - !initLoading && ( - <Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" /> - )} - {rulesCustomInstalled != null && - rulesCustomInstalled === 0 && - prePackagedRuleStatus === 'ruleNotInstalled' && - !initLoading && ( - <PrePackagedRulesPrompt - createPrePackagedRules={handleCreatePrePackagedRules} - loading={loadingCreatePrePackagedRules} - userHasNoPermissions={hasNoPermissions} - /> - )} - {initLoading && ( - <EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} /> - )} - {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && ( - <> - <UtilityBar border> - <UtilityBarSection> - <UtilityBarGroup> - <UtilityBarText dataTestSubj="showingRules"> - {i18n.SHOWING_RULES(pagination.total ?? 0)} - </UtilityBarText> - </UtilityBarGroup> - - <UtilityBarGroup> - <UtilityBarText>{i18n.SELECTED_RULES(selectedRuleIds.length)}</UtilityBarText> - {!hasNoPermissions && ( - <UtilityBarAction - dataTestSubj="bulkActions" - iconSide="right" - iconType="arrowDown" - popoverContent={getBatchItemsPopoverContent} - > - {i18n.BATCH_ACTIONS} - </UtilityBarAction> - )} - <UtilityBarAction - iconSide="left" - iconType="refresh" - onClick={() => reFetchRulesData(true)} - > - {i18n.REFRESH} - </UtilityBarAction> - </UtilityBarGroup> - </UtilityBarSection> - </UtilityBar> - <AllRulesTables - euiBasicTableSelectionProps={euiBasicTableSelectionProps} - hasNoPermissions={hasNoPermissions} - monitoringColumns={monitoringColumns} - pagination={paginationMemo} - rules={rules} - rulesColumns={rulesColumns} - rulesStatuses={rulesStatuses} - sorting={sorting} - tableOnChangeCallback={tableOnChangeCallback} - tableRef={tableRef} - /> - </> - )} - </> - </Panel> - </> - ); - } -); - -AllRules.displayName = 'AllRules'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx deleted file mode 100644 index 3dab83bca2946..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx +++ /dev/null @@ -1,46 +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 React, { useRef } from 'react'; -import { shallow } from 'enzyme'; - -import { AllRulesTables } from './index'; - -describe('AllRulesTables', () => { - it('renders correctly', () => { - const Component = () => { - const ref = useRef(); - - return ( - <AllRulesTables - euiBasicTableSelectionProps={{}} - hasNoPermissions={false} - monitoringColumns={[]} - rules={[]} - rulesColumns={[]} - rulesStatuses={[]} - tableOnChangeCallback={jest.fn()} - tableRef={ref} - pagination={{ - pageIndex: 0, - pageSize: 0, - totalItemCount: 0, - pageSizeOptions: [0], - }} - sorting={{ - sort: { - field: 'enabled', - direction: 'asc', - }, - }} - /> - ); - }; - const wrapper = shallow(<Component />); - - expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx deleted file mode 100644 index 31aaa426e4f3b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx +++ /dev/null @@ -1,150 +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 { - EuiBasicTable, - EuiBasicTableColumn, - EuiTab, - EuiTabs, - EuiEmptyPrompt, - Direction, - EuiTableSelectionType, -} from '@elastic/eui'; -import React, { useMemo, memo, useState } from 'react'; -import styled from 'styled-components'; - -import { EuiBasicTableOnChange } from '../../types'; -import * as i18n from '../../translations'; -import { - RulesColumns, - RuleStatusRowItemType, -} from '../../../../../pages/detection_engine/rules/all/columns'; -import { Rule, Rules } from '../../../../../containers/detection_engine/rules'; - -// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way -// after few hours of fight with typescript !!!! I lost :( -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; - -export interface SortingType { - sort: { - field: 'enabled'; - direction: Direction; - }; -} - -interface AllRulesTablesProps { - euiBasicTableSelectionProps: EuiTableSelectionType<Rule>; - hasNoPermissions: boolean; - monitoringColumns: Array<EuiBasicTableColumn<RuleStatusRowItemType>>; - pagination: { - pageIndex: number; - pageSize: number; - totalItemCount: number; - pageSizeOptions: number[]; - }; - rules: Rules; - rulesColumns: RulesColumns[]; - rulesStatuses: RuleStatusRowItemType[]; - sorting: { - sort: { - field: 'enabled'; - direction: Direction; - }; - }; - tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; - tableRef?: React.MutableRefObject<EuiBasicTable | undefined>; -} - -enum AllRulesTabs { - rules = 'rules', - monitoring = 'monitoring', -} - -const allRulesTabs = [ - { - id: AllRulesTabs.rules, - name: i18n.RULES_TAB, - disabled: false, - }, - { - id: AllRulesTabs.monitoring, - name: i18n.MONITORING_TAB, - disabled: false, - }, -]; - -const AllRulesTablesComponent: React.FC<AllRulesTablesProps> = ({ - euiBasicTableSelectionProps, - hasNoPermissions, - monitoringColumns, - pagination, - rules, - rulesColumns, - rulesStatuses, - sorting, - tableOnChangeCallback, - tableRef, -}) => { - const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); - const emptyPrompt = useMemo(() => { - return ( - <EuiEmptyPrompt title={<h3>{i18n.NO_RULES}</h3>} titleSize="xs" body={i18n.NO_RULES_BODY} /> - ); - }, []); - const tabs = useMemo( - () => ( - <EuiTabs> - {allRulesTabs.map(tab => ( - <EuiTab - onClick={() => setAllRulesTab(tab.id)} - isSelected={tab.id === allRulesTab} - disabled={tab.disabled} - key={tab.id} - > - {tab.name} - </EuiTab> - ))} - </EuiTabs> - ), - [allRulesTabs, allRulesTab, setAllRulesTab] - ); - return ( - <> - {tabs} - {allRulesTab === AllRulesTabs.rules && ( - <MyEuiBasicTable - data-test-subj="rules-table" - columns={rulesColumns} - isSelectable={!hasNoPermissions ?? false} - itemId="id" - items={rules ?? []} - noItemsMessage={emptyPrompt} - onChange={tableOnChangeCallback} - pagination={pagination} - ref={tableRef} - sorting={sorting} - selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} - /> - )} - {allRulesTab === AllRulesTabs.monitoring && ( - <MyEuiBasicTable - data-test-subj="monitoring-table" - columns={monitoringColumns} - isSelectable={!hasNoPermissions ?? false} - itemId="id" - items={rulesStatuses} - noItemsMessage={emptyPrompt} - onChange={tableOnChangeCallback} - pagination={pagination} - sorting={sorting} - /> - )} - </> - ); -}; - -export const AllRulesTables = memo(AllRulesTablesComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx deleted file mode 100644 index af946c6f02cbb..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx +++ /dev/null @@ -1,415 +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 React from 'react'; -import { shallow } from 'enzyme'; -import { EuiLoadingSpinner } from '@elastic/eui'; - -import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; -import { esFilters, FilterManager } from '../../../../../../../../../../src/plugins/data/public'; -import { SeverityBadge } from '../severity_badge'; - -import * as i18n from './translations'; -import { - isNotEmptyArray, - buildQueryBarDescription, - buildThreatDescription, - buildUnorderedListArrayDescription, - buildStringArrayDescription, - buildSeverityDescription, - buildUrlsDescription, - buildNoteDescription, - buildRuleTypeDescription, -} from './helpers'; -import { ListItems } from './types'; - -const setupMock = coreMock.createSetup(); -const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { - switch (key) { - case 'filters:pinnedByDefault': - return pinnedByDefault; - default: - throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); - } -}; -setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); -const mockFilterManager = new FilterManager(setupMock.uiSettings); - -const mockQueryBar = { - query: 'test query', - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', -}; - -describe('helpers', () => { - describe('isNotEmptyArray', () => { - test('returns false if empty array', () => { - const result = isNotEmptyArray([]); - expect(result).toBeFalsy(); - }); - - test('returns false if array of empty strings', () => { - const result = isNotEmptyArray(['', '']); - expect(result).toBeFalsy(); - }); - - test('returns true if array of string with space', () => { - const result = isNotEmptyArray([' ']); - expect(result).toBeTruthy(); - }); - - test('returns true if array with at least one non-empty string', () => { - const result = isNotEmptyArray(['', 'abc']); - expect(result).toBeTruthy(); - }); - }); - - describe('buildQueryBarDescription', () => { - test('returns empty array if no filters, query or savedId exist', () => { - const emptyMockQueryBar = { - query: '', - filters: [], - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: emptyMockQueryBar.filters, - filterManager: mockFilterManager, - query: emptyMockQueryBar.query, - savedId: emptyMockQueryBar.saved_id, - }); - expect(result).toEqual([]); - }); - - test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { - const mockQueryBarWithFilters = { - ...mockQueryBar, - query: '', - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithFilters.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithFilters.query, - savedId: mockQueryBarWithFilters.saved_id, - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); - expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); - }); - - test('returns expected array of ListItems when filters AND indexPatterns exist', () => { - const mockQueryBarWithFilters = { - ...mockQueryBar, - query: '', - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithFilters.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithFilters.query, - savedId: mockQueryBarWithFilters.saved_id, - indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); - - expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); - expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); - expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); - }); - - test('returns expected array of ListItems when "query.query" exists', () => { - const mockQueryBarWithQuery = { - ...mockQueryBar, - filters: [], - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithQuery.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithQuery.query, - savedId: mockQueryBarWithQuery.saved_id, - }); - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); - expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} </>); - }); - - test('returns expected array of ListItems when "savedId" exists', () => { - const mockQueryBarWithSavedId = { - ...mockQueryBar, - query: '', - filters: [], - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithSavedId.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithSavedId.query, - savedId: mockQueryBarWithSavedId.saved_id, - }); - expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} </>); - expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} </>); - }); - }); - - describe('buildThreatDescription', () => { - test('returns empty array if no threats', () => { - const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); - expect(result).toHaveLength(0); - }); - - test('returns empty tactic link if no corresponding tactic id found', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( - 'Audio Capture (T1123)' - ); - }); - - test('returns empty technique link if no corresponding technique id found', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( - 'Collection (TA0009)' - ); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); - }); - - test('returns with corresponding tactic and technique link text', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( - 'Collection (TA0009)' - ); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( - 'Audio Capture (T1123)' - ); - }); - - test('returns corresponding number of tactic and technique links', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [ - { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, - { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, - ], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - { - framework: 'MITRE ATTACK', - technique: [ - { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, - ], - tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, - }, - ], - }); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); - }); - }); - - describe('buildUnorderedListArrayDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildUnorderedListArrayDescription( - 'Test label', - 'falsePositives', - [] - ); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildUnorderedListArrayDescription( - 'Test label', - 'falsePositives', - ['', 'falsePositive1', 'falsePositive2'] - ); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); - }); - }); - - describe('buildStringArrayDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ - '', - 'tag1', - 'tag2', - ]); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); - expect( - wrapper - .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') - .first() - .text() - ).toEqual('tag1'); - expect( - wrapper - .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') - .at(1) - .text() - ).toEqual('tag2'); - }); - }); - - describe('buildSeverityDescription', () => { - test('returns ListItem with passed in label and SeverityBadge component', () => { - const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); - - expect(result[0].title).toEqual('Test label'); - expect(result[0].description).toEqual(<SeverityBadge value="Test description value" />); - }); - }); - - describe('buildUrlsDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildUrlsDescription('Test label', []); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildUrlsDescription('Test label', [ - 'www.test.com', - 'www.test2.com', - ]); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); - expect( - wrapper - .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') - .first() - .text() - ).toEqual('www.test.com'); - expect( - wrapper - .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') - .at(1) - .text() - ).toEqual('www.test2.com'); - }); - }); - - describe('buildNoteDescription', () => { - test('returns ListItem with passed in label and note content', () => { - const noteSample = - 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; - const result: ListItems[] = buildNoteDescription('Test label', noteSample); - const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); - const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); - - expect(result[0].title).toEqual('Test label'); - expect(noteElement.exists()).toBeTruthy(); - expect(noteElement.text()).toEqual(noteSample); - }); - - test('returns empty array if passed in note is empty string', () => { - const result: ListItems[] = buildNoteDescription('Test label', ''); - - expect(result).toHaveLength(0); - }); - }); - - describe('buildRuleTypeDescription', () => { - it('returns the label for a machine_learning type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); - - expect(result.title).toEqual('Test label'); - }); - - it('returns a humanized description for a machine_learning type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); - - expect(result.description).toEqual('Machine Learning'); - }); - - it('returns the label for a query type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); - - expect(result.title).toEqual('Test label'); - }); - - it('returns a humanized description for a query type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); - - expect(result.description).toEqual('Query'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx deleted file mode 100644 index 5b7a85e23834d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ /dev/null @@ -1,294 +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 { - EuiBadge, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - EuiLink, - EuiText, -} from '@elastic/eui'; - -import { isEmpty } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; -import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; - -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; - -import * as i18n from './translations'; -import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; -import { SeverityBadge } from '../severity_badge'; -import ListTreeIcon from './assets/list_tree_icon.svg'; -import { assertUnreachable } from '../../../../../lib/helpers'; - -const NoteDescriptionContainer = styled(EuiFlexItem)` - height: 105px; - overflow-y: hidden; -`; - -export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); - -const EuiBadgeWrap = styled(EuiBadge)` - .euiBadge__text { - white-space: pre-wrap !important; - } -`; - -export const buildQueryBarDescription = ({ - field, - filters, - filterManager, - query, - savedId, - indexPatterns, -}: BuildQueryBarDescription): ListItems[] => { - let items: ListItems[] = []; - if (!isEmpty(filters)) { - filterManager.setFilters(filters); - items = [ - ...items, - { - title: <>{i18n.FILTERS_LABEL} </>, - description: ( - <EuiFlexGroup wrap responsive={false} gutterSize="xs"> - {filterManager.getFilters().map((filter, index) => ( - <EuiFlexItem grow={false} key={`${field}-filter-${index}`}> - <EuiBadgeWrap color="hollow"> - {indexPatterns != null ? ( - <esFilters.FilterLabel - filter={filter} - valueLabel={esFilters.getDisplayValueFromFilter(filter, [indexPatterns])} - /> - ) : ( - <EuiLoadingSpinner size="m" /> - )} - </EuiBadgeWrap> - </EuiFlexItem> - ))} - </EuiFlexGroup> - ), - }, - ]; - } - if (!isEmpty(query)) { - items = [ - ...items, - { - title: <>{i18n.QUERY_LABEL} </>, - description: <>{query} </>, - }, - ]; - } - if (!isEmpty(savedId)) { - items = [ - ...items, - { - title: <>{i18n.SAVED_ID_LABEL} </>, - description: <>{savedId} </>, - }, - ]; - } - return items; -}; - -const ThreatEuiFlexGroup = styled(EuiFlexGroup)` - .euiFlexItem { - margin-bottom: 0px; - } -`; - -const TechniqueLinkItem = styled(EuiButtonEmpty)` - .euiIcon { - width: 8px; - height: 8px; - } -`; - -export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { - if (threat.length > 0) { - return [ - { - title: label, - description: ( - <ThreatEuiFlexGroup direction="column"> - {threat.map((singleThreat, index) => { - const tactic = tacticsOptions.find(t => t.id === singleThreat.tactic.id); - return ( - <EuiFlexItem key={`${singleThreat.tactic.name}-${index}`}> - <EuiLink - data-test-subj="threatTacticLink" - href={singleThreat.tactic.reference} - target="_blank" - > - {tactic != null ? tactic.text : ''} - </EuiLink> - <EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column"> - {singleThreat.technique.map(technique => { - const myTechnique = techniquesOptions.find(t => t.id === technique.id); - return ( - <EuiFlexItem> - <TechniqueLinkItem - data-test-subj="threatTechniqueLink" - href={technique.reference} - target="_blank" - iconType={ListTreeIcon} - size="xs" - flush="left" - > - {myTechnique != null ? myTechnique.label : ''} - </TechniqueLinkItem> - </EuiFlexItem> - ); - })} - </EuiFlexGroup> - </EuiFlexItem> - ); - })} - <EuiSpacer /> - </ThreatEuiFlexGroup> - ), - }, - ]; - } - return []; -}; - -export const buildUnorderedListArrayDescription = ( - label: string, - field: string, - values: string[] -): ListItems[] => { - if (isNotEmptyArray(values)) { - return [ - { - title: label, - description: ( - <EuiText size="s"> - <ul> - {values.map(val => - isEmpty(val) ? null : ( - <li data-test-subj="unorderedListArrayDescriptionItem" key={`${field}-${val}`}> - {val} - </li> - ) - )} - </ul> - </EuiText> - ), - }, - ]; - } - return []; -}; - -export const buildStringArrayDescription = ( - label: string, - field: string, - values: string[] -): ListItems[] => { - if (isNotEmptyArray(values)) { - return [ - { - title: label, - description: ( - <EuiFlexGroup responsive={false} gutterSize="xs" wrap> - {values.map((val: string) => - isEmpty(val) ? null : ( - <EuiFlexItem grow={false} key={`${field}-${val}`}> - <EuiBadgeWrap data-test-subj="stringArrayDescriptionBadgeItem" color="hollow"> - {val} - </EuiBadgeWrap> - </EuiFlexItem> - ) - )} - </EuiFlexGroup> - ), - }, - ]; - } - return []; -}; - -export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ - { - title: label, - description: <SeverityBadge value={value} />, - }, -]; - -export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { - if (isNotEmptyArray(values)) { - return [ - { - title: label, - description: ( - <EuiText size="s"> - <ul> - {values - .filter(v => !isEmpty(v)) - .map((val, index) => ( - <li data-test-subj="urlsDescriptionReferenceLinkItem" key={`${index}-${val}`}> - <EuiLink href={val} external target="_blank"> - {val} - </EuiLink> - </li> - ))} - </ul> - </EuiText> - ), - }, - ]; - } - return []; -}; - -export const buildNoteDescription = (label: string, note: string): ListItems[] => { - if (note.trim() !== '') { - return [ - { - title: label, - description: ( - <NoteDescriptionContainer> - <div data-test-subj="noteDescriptionItem" className="eui-yScrollWithShadows"> - {note} - </div> - </NoteDescriptionContainer> - ), - }, - ]; - } - return []; -}; - -export const buildRuleTypeDescription = (label: string, ruleType: RuleType): ListItems[] => { - switch (ruleType) { - case 'machine_learning': { - return [ - { - title: label, - description: i18n.ML_TYPE_DESCRIPTION, - }, - ]; - } - case 'query': - case 'saved_query': { - return [ - { - title: label, - description: i18n.QUERY_TYPE_DESCRIPTION, - }, - ]; - } - default: - return assertUnreachable(ruleType); - } -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx deleted file mode 100644 index 8e8927cb7bbd1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ /dev/null @@ -1,474 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { - StepRuleDescriptionComponent, - addFilterStateIfNotThere, - buildListItems, - getDescriptionItem, -} from './'; - -import { - esFilters, - Filter, - FilterManager, -} from '../../../../../../../../../../src/plugins/data/public'; -import { mockAboutStepRule, mockDefineStepRule } from '../../all/__mocks__/mock'; -import { coreMock } from '../../../../../../../../../../src/core/public/mocks'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import * as i18n from './translations'; - -import { schema } from '../step_about_rule/schema'; -import { ListItems } from './types'; -import { AboutStepRule } from '../../types'; - -jest.mock('../../../../../lib/kibana'); - -describe('description_step', () => { - const setupMock = coreMock.createSetup(); - const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { - switch (key) { - case 'filters:pinnedByDefault': - return pinnedByDefault; - default: - throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); - } - }; - let mockFilterManager: FilterManager; - let mockAboutStep: AboutStepRule; - - beforeEach(() => { - setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); - mockFilterManager = new FilterManager(setupMock.uiSettings); - mockAboutStep = mockAboutStepRule(); - }); - - describe('StepRuleDescriptionComponent', () => { - test('renders correctly against snapshot when columns is "multi"', () => { - const wrapper = shallow( - <StepRuleDescriptionComponent columns="multi" data={mockAboutStep} schema={schema} /> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); - }); - - test('renders correctly against snapshot when columns is "single"', () => { - const wrapper = shallow( - <StepRuleDescriptionComponent columns="single" data={mockAboutStep} schema={schema} /> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); - }); - - test('renders correctly against snapshot when columns is "singleSplit', () => { - const wrapper = shallow( - <StepRuleDescriptionComponent columns="singleSplit" data={mockAboutStep} schema={schema} /> - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); - expect( - wrapper - .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') - .at(0) - .prop('type') - ).toEqual('column'); - }); - }); - - describe('addFilterStateIfNotThere', () => { - test('it does not change the state if it is global', () => { - const filters: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - const output = addFilterStateIfNotThere(filters); - const expected: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - expect(output).toEqual(expected); - }); - - test('it adds the state if it does not exist as local', () => { - const filters: Filter[] = [ - { - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - const output = addFilterStateIfNotThere(filters); - const expected: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - expect(output).toEqual(expected); - }); - }); - - describe('buildListItems', () => { - test('returns expected ListItems array when given valid inputs', () => { - const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); - - expect(result.length).toEqual(9); - }); - }); - - describe('getDescriptionItem', () => { - test('returns ListItem with all values enumerated when value[field] is an array', () => { - const result: ListItems[] = getDescriptionItem( - 'tags', - 'Tags label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Tags label'); - expect(typeof result[0].description).toEqual('object'); - }); - - test('returns ListItem with description of value[field] when value[field] is a string', () => { - const result: ListItems[] = getDescriptionItem( - 'description', - 'Description label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Description label'); - expect(result[0].description).toEqual('24/7'); - }); - - test('returns empty array when "value" is a non-existant property in "field"', () => { - const result: ListItems[] = getDescriptionItem( - 'jibberjabber', - 'JibberJabber label', - mockAboutStep, - mockFilterManager - ); - - expect(result.length).toEqual(0); - }); - - describe('queryBar', () => { - test('returns array of ListItems when queryBar exist', () => { - const mockQueryBar = { - isNew: false, - queryBar: { - query: { - query: 'user.name: root or user.name: admin', - language: 'kuery', - }, - filters: null, - saved_id: null, - }, - }; - const result: ListItems[] = getDescriptionItem( - 'queryBar', - 'Query bar label', - mockQueryBar, - mockFilterManager - ); - - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); - expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} </>); - }); - }); - - describe('threat', () => { - test('returns array of ListItems when threat exist', () => { - const result: ListItems[] = getDescriptionItem( - 'threat', - 'Threat label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Threat label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - - test('filters out threats with tactic.name of "none"', () => { - const mockStep = { - ...mockAboutStep, - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'none', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - const result: ListItems[] = getDescriptionItem( - 'threat', - 'Threat label', - mockStep, - mockFilterManager - ); - - expect(result.length).toEqual(0); - }); - }); - - describe('references', () => { - test('returns array of ListItems when references exist', () => { - const result: ListItems[] = getDescriptionItem( - 'references', - 'Reference label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Reference label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('falsePositives', () => { - test('returns array of ListItems when falsePositives exist', () => { - const result: ListItems[] = getDescriptionItem( - 'falsePositives', - 'False positives label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('False positives label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('severity', () => { - test('returns array of ListItems when severity exist', () => { - const result: ListItems[] = getDescriptionItem( - 'severity', - 'Severity label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Severity label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('riskScore', () => { - test('returns array of ListItems when riskScore exist', () => { - const result: ListItems[] = getDescriptionItem( - 'riskScore', - 'Risk score label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Risk score label'); - expect(result[0].description).toEqual(21); - }); - }); - - describe('timeline', () => { - test('returns timeline title if one exists', () => { - const mockDefineStep = mockDefineStepRule(); - const result: ListItems[] = getDescriptionItem( - 'timeline', - 'Timeline label', - mockDefineStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Timeline label'); - expect(result[0].description).toEqual('Titled timeline'); - }); - - test('returns default timeline title if none exists', () => { - const mockStep = { - ...mockDefineStepRule(), - timeline: { - id: '12345', - }, - }; - const result: ListItems[] = getDescriptionItem( - 'timeline', - 'Timeline label', - mockStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Timeline label'); - expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); - }); - }); - - describe('note', () => { - test('returns default "note" description', () => { - const result: ListItems[] = getDescriptionItem( - 'note', - 'Investigation guide', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Investigation guide'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx deleted file mode 100644 index 49977713a585a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ /dev/null @@ -1,205 +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 { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; -import React, { memo, useState } from 'react'; -import styled from 'styled-components'; - -import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; -import { - IIndexPattern, - Filter, - esFilters, - FilterManager, -} from '../../../../../../../../../../src/plugins/data/public'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import { useKibana } from '../../../../../lib/kibana'; -import { IMitreEnterpriseAttack } from '../../types'; -import { FieldValueTimeline } from '../pick_timeline'; -import { FormSchema } from '../../../../../shared_imports'; -import { ListItems } from './types'; -import { - buildQueryBarDescription, - buildSeverityDescription, - buildStringArrayDescription, - buildThreatDescription, - buildUnorderedListArrayDescription, - buildUrlsDescription, - buildNoteDescription, - buildRuleTypeDescription, -} from './helpers'; -import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; -import { buildMlJobDescription } from './ml_job_description'; - -const DescriptionListContainer = styled(EuiDescriptionList)` - &.euiDescriptionList--column .euiDescriptionList__title { - width: 30%; - } - &.euiDescriptionList--column .euiDescriptionList__description { - width: 70%; - } -`; - -interface StepRuleDescriptionProps { - columns?: 'multi' | 'single' | 'singleSplit'; - data: unknown; - indexPatterns?: IIndexPattern; - schema: FormSchema; -} - -export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({ - data, - columns = 'multi', - indexPatterns, - schema, -}) => { - const kibana = useKibana(); - const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); - const [, siemJobs] = useSiemJobs(true); - - const keys = Object.keys(schema); - const listItems = keys.reduce((acc: ListItems[], key: string) => { - if (key === 'machineLearningJobId') { - return [ - ...acc, - buildMlJobDescription( - get(key, data) as string, - (get(key, schema) as { label: string }).label, - siemJobs - ), - ]; - } - return [...acc, ...buildListItems(data, pick(key, schema), filterManager, indexPatterns)]; - }, []); - - if (columns === 'multi') { - return ( - <EuiFlexGroup> - {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( - <EuiFlexItem - data-test-subj="listItemColumnStepRuleDescription" - key={`description-step-rule-${index}`} - > - <EuiDescriptionList listItems={chunkListItems} /> - </EuiFlexItem> - ))} - </EuiFlexGroup> - ); - } - - return ( - <EuiFlexGroup> - <EuiFlexItem data-test-subj="listItemColumnStepRuleDescription"> - {columns === 'single' ? ( - <EuiDescriptionList listItems={listItems} /> - ) : ( - <DescriptionListContainer - data-test-subj="singleSplitStepRuleDescriptionList" - type="column" - listItems={listItems} - /> - )} - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -export const StepRuleDescription = memo(StepRuleDescriptionComponent); - -export const buildListItems = ( - data: unknown, - schema: FormSchema, - filterManager: FilterManager, - indexPatterns?: IIndexPattern -): ListItems[] => - Object.keys(schema).reduce<ListItems[]>( - (acc, field) => [ - ...acc, - ...getDescriptionItem( - field, - get([field, 'label'], schema), - data, - filterManager, - indexPatterns - ), - ], - [] - ); - -export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { - return filters.map(filter => { - if (filter.$state == null) { - return { $state: { store: esFilters.FilterStateStore.APP_STATE }, ...filter }; - } else { - return filter; - } - }); -}; - -export const getDescriptionItem = ( - field: string, - label: string, - data: unknown, - filterManager: FilterManager, - indexPatterns?: IIndexPattern -): ListItems[] => { - if (field === 'queryBar') { - const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); - const query = get('queryBar.query.query', data); - const savedId = get('queryBar.saved_id', data); - return buildQueryBarDescription({ - field, - filters, - filterManager, - query, - savedId, - indexPatterns, - }); - } else if (field === 'threat') { - const threat: IMitreEnterpriseAttack[] = get(field, data).filter( - (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' - ); - return buildThreatDescription({ label, threat }); - } else if (field === 'references') { - const urls: string[] = get(field, data); - return buildUrlsDescription(label, urls); - } else if (field === 'falsePositives') { - const values: string[] = get(field, data); - return buildUnorderedListArrayDescription(label, field, values); - } else if (Array.isArray(get(field, data))) { - const values: string[] = get(field, data); - return buildStringArrayDescription(label, field, values); - } else if (field === 'severity') { - const val: string = get(field, data); - return buildSeverityDescription(label, val); - } else if (field === 'timeline') { - const timeline = get(field, data) as FieldValueTimeline; - return [ - { - title: label, - description: timeline.title ?? DEFAULT_TIMELINE_TITLE, - }, - ]; - } else if (field === 'note') { - const val: string = get(field, data); - return buildNoteDescription(label, val); - } else if (field === 'ruleType') { - const ruleType: RuleType = get(field, data); - return buildRuleTypeDescription(label, ruleType); - } - - const description: string = get(field, data); - if (isNumber(description) || !isEmpty(description)) { - return [ - { - title: label, - description, - }, - ]; - } - return []; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts deleted file mode 100644 index bfca6b2068443..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts +++ /dev/null @@ -1,32 +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 { ReactNode } from 'react'; - -import { - IIndexPattern, - Filter, - FilterManager, -} from '../../../../../../../../../../src/plugins/data/public'; -import { IMitreEnterpriseAttack } from '../../types'; - -export interface ListItems { - title: NonNullable<ReactNode>; - description: NonNullable<ReactNode>; -} - -export interface BuildQueryBarDescription { - field: string; - filters: Filter[]; - filterManager: FilterManager; - query: string; - savedId: string; - indexPatterns?: IIndexPattern; -} - -export interface BuildThreatDescription { - label: string; - threat: IMitreEnterpriseAttack[]; -} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx deleted file mode 100644 index 82350150488d0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx +++ /dev/null @@ -1,140 +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 React, { useCallback, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; - -import styled from 'styled-components'; -import { isJobStarted } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; -import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; -import { useKibana } from '../../../../../lib/kibana'; -import { - ML_JOB_SELECT_PLACEHOLDER_TEXT, - ENABLE_ML_JOB_WARNING, -} from '../step_define_rule/translations'; - -const HelpTextWarningContainer = styled.div` - margin-top: 10px; -`; - -const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` - margin-bottom: 5px; -`; - -const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ - href, - showEnableWarning = false, -}) => ( - <> - <FormattedMessage - id="xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText" - defaultMessage="We've provided a few common jobs to get you started. To add your own custom jobs, assign a group of “siem” to those jobs in the {machineLearning} application to make them appear here." - values={{ - machineLearning: ( - <EuiLink href={href} target="_blank"> - <FormattedMessage - id="xpack.siem.components.mlJobSelect.machineLearningLink" - defaultMessage="Machine Learning" - /> - </EuiLink> - ), - }} - /> - {showEnableWarning && ( - <HelpTextWarningContainer> - <EuiText size="xs" color="warning"> - <EuiIcon type="alert" /> - <span>{ENABLE_ML_JOB_WARNING}</span> - </EuiText> - </HelpTextWarningContainer> - )} - </> -); - -const JobDisplay: React.FC<{ title: string; description: string }> = ({ title, description }) => ( - <> - <strong>{title}</strong> - <EuiText size="xs" color="subdued"> - <p>{description}</p> - </EuiText> - </> -); - -interface MlJobSelectProps { - describedByIds: string[]; - field: FieldHook; -} - -export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], field }) => { - const jobId = field.value as string; - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [isLoading, siemJobs] = useSiemJobs(false); - const mlUrl = useKibana().services.application.getUrlForApp('ml'); - const handleJobChange = useCallback( - (machineLearningJobId: string) => { - field.setValue(machineLearningJobId); - }, - [field] - ); - const placeholderOption = { - value: 'placeholder', - inputDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, - dropdownDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, - disabled: true, - }; - - const jobOptions = siemJobs.map(job => ({ - value: job.id, - inputDisplay: job.id, - dropdownDisplay: <JobDisplay title={job.id} description={job.description} />, - })); - - const options = [placeholderOption, ...jobOptions]; - - const isJobRunning = useMemo(() => { - // If the selected job is not found in the list, it means the placeholder is selected - // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' - const job = siemJobs.find(j => j.id === jobId); - return job == null || isJobStarted(job.jobState, job.datafeedState); - }, [siemJobs, jobId]); - - return ( - <MlJobSelectEuiFlexGroup> - <EuiFlexItem> - <EuiFormRow - label={field.label} - helpText={<HelpText href={mlUrl} showEnableWarning={!isJobRunning} />} - isInvalid={isInvalid} - error={errorMessage} - data-test-subj="mlJobSelect" - describedByIds={describedByIds} - > - <EuiFlexGroup> - <EuiFlexItem> - <EuiSuperSelect - hasDividers - isLoading={isLoading} - onChange={handleJobChange} - options={options} - valueOfSelected={jobId || 'placeholder'} - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFormRow> - </EuiFlexItem> - </MlJobSelectEuiFlexGroup> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx deleted file mode 100644 index d232c86c19e6f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ /dev/null @@ -1,285 +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 { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Subscription } from 'rxjs'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - IIndexPattern, - Query, - FilterManager, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../../../../../../../src/plugins/data/public'; - -import { BrowserFields } from '../../../../../containers/source'; -import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal'; -import { ActionTimelineToShow } from '../../../../../components/open_timeline/types'; -import { QueryBar } from '../../../../../components/query_bar'; -import { buildGlobalQuery } from '../../../../../components/timeline/helpers'; -import { getDataProviderFilter } from '../../../../../components/timeline/query_bar'; -import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; -import { useKibana } from '../../../../../lib/kibana'; -import { TimelineModel } from '../../../../../store/timeline/model'; -import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; -import * as i18n from './translations'; - -export interface FieldValueQueryBar { - filters: Filter[]; - query: Query; - saved_id?: string; -} -interface QueryBarDefineRuleProps { - browserFields: BrowserFields; - dataTestSubj: string; - field: FieldHook; - idAria: string; - isLoading: boolean; - indexPattern: IIndexPattern; - onCloseTimelineSearch: () => void; - openTimelineSearch: boolean; - resizeParentContainer?: (height: number) => void; -} - -const StyledEuiFormRow = styled(EuiFormRow)` - .kbnTypeahead__items { - max-height: 45vh !important; - } - .globalQueryBar { - padding: 4px 0px 0px 0px; - .kbnQueryBar { - & > div:first-child { - margin: 0px 0px 0px 4px; - } - } - } -`; - -// TODO need to add disabled in the SearchBar - -export const QueryBarDefineRule = ({ - browserFields, - dataTestSubj, - field, - idAria, - indexPattern, - isLoading = false, - onCloseTimelineSearch, - openTimelineSearch = false, - resizeParentContainer, -}: QueryBarDefineRuleProps) => { - const [originalHeight, setOriginalHeight] = useState(-1); - const [loadingTimeline, setLoadingTimeline] = useState(false); - const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); - const [queryDraft, setQueryDraft] = useState<Query>({ query: '', language: 'kuery' }); - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - const kibana = useKibana(); - const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); - - const savedQueryServices = useSavedQueryServices(); - - useEffect(() => { - let isSubscribed = true; - const subscriptions = new Subscription(); - filterManager.setFilters([]); - - subscriptions.add( - filterManager.getUpdates$().subscribe({ - next: () => { - if (isSubscribed) { - const newFilters = filterManager.getFilters(); - const { filters } = field.value as FieldValueQueryBar; - - if (!deepEqual(filters, newFilters)) { - field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); - } - } - }, - }) - ); - - return () => { - isSubscribed = false; - subscriptions.unsubscribe(); - }; - }, [field.value]); - - useEffect(() => { - let isSubscribed = true; - async function updateFilterQueryFromValue() { - const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; - if (!deepEqual(query, queryDraft)) { - setQueryDraft(query); - } - if (!deepEqual(filters, filterManager.getFilters())) { - filterManager.setFilters(filters); - } - if ( - (savedId != null && savedQuery != null && savedId !== savedQuery.id) || - (savedId != null && savedQuery == null) - ) { - try { - const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); - if (isSubscribed && mySavedQuery != null) { - setSavedQuery(mySavedQuery); - } - } catch { - setSavedQuery(null); - } - } else if (savedId == null && savedQuery != null) { - setSavedQuery(null); - } - } - updateFilterQueryFromValue(); - return () => { - isSubscribed = false; - }; - }, [field.value]); - - const onSubmitQuery = useCallback( - (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { - const { query } = field.value as FieldValueQueryBar; - if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); - } - }, - [field] - ); - - const onChangedQuery = useCallback( - (newQuery: Query) => { - const { query } = field.value as FieldValueQueryBar; - if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); - } - }, - [field] - ); - - const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { - if (newSavedQuery != null) { - const { saved_id: savedId } = field.value as FieldValueQueryBar; - if (newSavedQuery.id !== savedId) { - setSavedQuery(newSavedQuery); - field.setValue({ - filters: newSavedQuery.attributes.filters, - query: newSavedQuery.attributes.query, - saved_id: newSavedQuery.id, - }); - } - } - }, - [field.value] - ); - - const onCloseTimelineModal = useCallback(() => { - setLoadingTimeline(true); - onCloseTimelineSearch(); - }, [onCloseTimelineSearch]); - - const onOpenTimeline = useCallback( - (timeline: TimelineModel) => { - setLoadingTimeline(false); - const newQuery = { - query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', - language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', - }; - const dataProvidersDsl = - timeline.dataProviders != null && timeline.dataProviders.length > 0 - ? convertKueryToElasticSearchQuery( - buildGlobalQuery(timeline.dataProviders, browserFields), - indexPattern - ) - : ''; - const newFilters = timeline.filters ?? []; - field.setValue({ - filters: - dataProvidersDsl !== '' - ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] - : newFilters, - query: newQuery, - saved_id: '', - }); - }, - [browserFields, field, indexPattern] - ); - - const onMutation = (event: unknown, observer: unknown) => { - if (resizeParentContainer != null) { - const suggestionContainer = document.getElementById('kbnTypeahead__items'); - if (suggestionContainer != null) { - const box = suggestionContainer.getBoundingClientRect(); - const accordionContainer = document.getElementById('define-rule'); - if (accordionContainer != null) { - const accordionBox = accordionContainer.getBoundingClientRect(); - if (originalHeight === -1 || accordionBox.height < originalHeight + box.height) { - resizeParentContainer(originalHeight + box.height - 100); - } - if (originalHeight === -1) { - setOriginalHeight(accordionBox.height); - } - } - } else { - resizeParentContainer(-1); - } - } - }; - - const actionTimelineToHide = useMemo<ActionTimelineToShow[]>(() => ['duplicate'], []); - - return ( - <> - <StyledEuiFormRow - label={field.label} - labelAppend={field.labelAppend} - helpText={field.helpText} - error={errorMessage} - isInvalid={isInvalid} - fullWidth - data-test-subj={dataTestSubj} - describedByIds={idAria ? [idAria] : undefined} - > - <EuiMutationObserver - observerOptions={{ subtree: true, attributes: true, childList: true }} - onMutation={onMutation} - > - {mutationRef => ( - <div ref={mutationRef}> - <QueryBar - indexPattern={indexPattern} - isLoading={isLoading || loadingTimeline} - isRefreshPaused={false} - filterQuery={queryDraft} - filterManager={filterManager} - filters={filterManager.getFilters() || []} - onChangedQuery={onChangedQuery} - onSubmitQuery={onSubmitQuery} - savedQuery={savedQuery} - onSavedQuery={onSavedQuery} - hideSavedQuery={false} - /> - </div> - )} - </EuiMutationObserver> - </StyledEuiFormRow> - {openTimelineSearch ? ( - <OpenTimelineModal - hideActions={actionTimelineToHide} - modalTitle={i18n.IMPORT_TIMELINE_MODAL} - onClose={onCloseTimelineModal} - onOpen={onOpenTimeline} - /> - ) : null} - </> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx deleted file mode 100644 index b4d813c48b43f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx +++ /dev/null @@ -1,86 +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 React, { useCallback, useEffect, useState } from 'react'; -import deepMerge from 'deepmerge'; - -import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../../../../plugins/siem/common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loadActionTypes } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/lib/action_connector_api'; -import { SelectField } from '../../../../../shared_imports'; -import { - ActionForm, - ActionType, -} from '../../../../../../../../../plugins/triggers_actions_ui/public'; -import { AlertAction } from '../../../../../../../../../plugins/alerting/common'; -import { useKibana } from '../../../../../lib/kibana'; - -type ThrottleSelectField = typeof SelectField; - -const DEFAULT_ACTION_GROUP_ID = 'default'; -const DEFAULT_ACTION_MESSAGE = - 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; - -export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { - const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>(); - const { - http, - triggers_actions_ui: { actionTypeRegistry }, - notifications, - } = useKibana().services; - - const setActionIdByIndex = useCallback( - (id: string, index: number) => { - const updatedActions = [...(field.value as Array<Partial<AlertAction>>)]; - updatedActions[index] = deepMerge(updatedActions[index], { id }); - field.setValue(updatedActions); - }, - [field] - ); - - const setAlertProperty = useCallback( - (updatedActions: AlertAction[]) => field.setValue(updatedActions), - [field] - ); - - const setActionParamsProperty = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (key: string, value: any, index: number) => { - const updatedActions = [...(field.value as AlertAction[])]; - updatedActions[index].params[key] = value; - field.setValue(updatedActions); - }, - [field] - ); - - useEffect(() => { - (async function() { - const actionTypes = await loadActionTypes({ http }); - const supportedTypes = actionTypes.filter(actionType => - NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) - ); - setSupportedActionTypes(supportedTypes); - })(); - }, []); - - if (!supportedActionTypes) return <></>; - - return ( - <ActionForm - actions={field.value as AlertAction[]} - messageVariables={messageVariables} - defaultActionGroupId={DEFAULT_ACTION_GROUP_ID} - setActionIdByIndex={setActionIdByIndex} - setAlertProperty={setAlertProperty} - setActionParamsProperty={setActionParamsProperty} - http={http} - actionTypeRegistry={actionTypeRegistry} - actionTypes={supportedActionTypes} - defaultActionMessage={DEFAULT_ACTION_MESSAGE} - toastNotifications={notifications.toasts} - /> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx deleted file mode 100644 index 2b1e5a367a965..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx +++ /dev/null @@ -1,123 +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 React, { useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCard, - EuiFlexGrid, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiText, -} from '@elastic/eui'; - -import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { RuleType } from '../../../../../../../../../plugins/siem/common/detection_engine/types'; -import { FieldHook } from '../../../../../shared_imports'; -import { useKibana } from '../../../../../lib/kibana'; -import * as i18n from './translations'; - -const MlCardDescription = ({ - subscriptionUrl, - hasValidLicense = false, -}: { - subscriptionUrl: string; - hasValidLicense?: boolean; -}) => ( - <EuiText size="s"> - {hasValidLicense ? ( - i18n.ML_TYPE_DESCRIPTION - ) : ( - <FormattedMessage - id="xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription" - defaultMessage="Access to ML requires a {subscriptionsLink}." - values={{ - subscriptionsLink: ( - <EuiLink href={subscriptionUrl} target="_blank"> - <FormattedMessage - id="xpack.siem.components.stepDefineRule.ruleTypeField.subscriptionsLink" - defaultMessage="Platinum subscription" - /> - </EuiLink> - ), - }} - /> - )} - </EuiText> -); - -interface SelectRuleTypeProps { - describedByIds?: string[]; - field: FieldHook; - hasValidLicense?: boolean; - isMlAdmin?: boolean; - isReadOnly?: boolean; -} - -export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ - describedByIds = [], - field, - isReadOnly = false, - hasValidLicense = false, - isMlAdmin = false, -}) => { - const ruleType = field.value as RuleType; - const setType = useCallback( - (type: RuleType) => { - field.setValue(type); - }, - [field] - ); - const setMl = useCallback(() => setType('machine_learning'), [setType]); - const setQuery = useCallback(() => setType('query'), [setType]); - const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; - const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { - path: '#/management/elasticsearch/license_management', - }); - - return ( - <EuiFormRow - fullWidth - data-test-subj="selectRuleType" - describedByIds={describedByIds} - label={field.label} - > - <EuiFlexGrid columns={4}> - <EuiFlexItem> - <EuiCard - data-test-subj="customRuleType" - title={i18n.QUERY_TYPE_TITLE} - description={i18n.QUERY_TYPE_DESCRIPTION} - icon={<EuiIcon size="l" type="search" />} - selectable={{ - isDisabled: isReadOnly, - onClick: setQuery, - isSelected: !isMlRule(ruleType), - }} - /> - </EuiFlexItem> - <EuiFlexItem> - <EuiCard - data-test-subj="machineLearningRuleType" - title={i18n.ML_TYPE_TITLE} - description={ - <MlCardDescription subscriptionUrl={licensingUrl} hasValidLicense={hasValidLicense} /> - } - icon={<EuiIcon size="l" type="machineLearningApp" />} - isDisabled={mlCardDisabled} - selectable={{ - isDisabled: mlCardDisabled, - onClick: setMl, - isSelected: isMlRule(ruleType), - }} - /> - </EuiFlexItem> - </EuiFlexGrid> - </EuiFormRow> - ); -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx deleted file mode 100644 index be9e919b806b5..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ /dev/null @@ -1,276 +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 { EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; -import React, { FC, memo, useCallback, useState, useEffect } from 'react'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../../../../plugins/siem/common/constants'; -import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import { useMlCapabilities } from '../../../../../components/ml_popover/hooks/use_ml_capabilities'; -import { useUiSetting$ } from '../../../../../lib/kibana'; -import { setFieldValue } from '../../helpers'; -import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; -import { StepRuleDescription } from '../description_step'; -import { QueryBarDefineRule } from '../query_bar'; -import { SelectRuleType } from '../select_rule_type'; -import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; -import { MlJobSelect } from '../ml_job_select'; -import { PickTimeline } from '../pick_timeline'; -import { StepContentWrapper } from '../step_content_wrapper'; -import { NextStep } from '../next_step'; -import { - Field, - Form, - FormDataProvider, - getUseField, - UseField, - useForm, - FormSchema, -} from '../../../../../shared_imports'; -import { schema } from './schema'; -import * as i18n from './translations'; -import { filterRuleFieldsForType, RuleFields } from '../../create/helpers'; -import { hasMlAdminPermissions } from '../../../../../components/ml/permissions/has_ml_admin_permissions'; - -const CommonUseField = getUseField({ component: Field }); - -interface StepDefineRuleProps extends RuleStepProps { - defaultValues?: DefineStepRule | null; -} - -const stepDefineDefaultValue: DefineStepRule = { - anomalyThreshold: 50, - index: [], - isNew: true, - machineLearningJobId: '', - ruleType: 'query', - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: undefined, - }, - timeline: { - id: null, - title: DEFAULT_TIMELINE_TITLE, - }, -}; - -const MyLabelButton = styled(EuiButtonEmpty)` - height: 18px; - font-size: 12px; - - .euiIcon { - width: 14px; - height: 14px; - } -`; - -MyLabelButton.defaultProps = { - flush: 'right', -}; - -const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ - addPadding = false, - defaultValues, - descriptionColumns = 'singleSplit', - isReadOnlyView, - isLoading, - isUpdateView = false, - setForm, - setStepData, -}) => { - const mlCapabilities = useMlCapabilities(); - const [openTimelineSearch, setOpenTimelineSearch] = useState(false); - const [indexModified, setIndexModified] = useState(false); - const [localIsMlRule, setIsMlRule] = useState(false); - const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); - const [myStepData, setMyStepData] = useState<DefineStepRule>({ - ...stepDefineDefaultValue, - index: indicesConfig ?? [], - }); - const [ - { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, - ] = useFetchIndexPatterns(myStepData.index); - - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); - - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.defineRule, null, false); - const { isValid, data } = await form.submit(); - if (isValid && setStepData) { - setStepData(RuleStep.defineRule, data, isValid); - setMyStepData({ ...data, isNew: false } as DefineStepRule); - } - } - }, [form]); - - useEffect(() => { - const { isNew, ...values } = myStepData; - if (defaultValues != null && !deepEqual(values, defaultValues)) { - const newValues = { ...values, ...defaultValues, isNew: false }; - setMyStepData(newValues); - setFieldValue(form, schema, newValues); - } - }, [defaultValues, setMyStepData, setFieldValue]); - - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.defineRule, form); - } - }, [form]); - - const handleResetIndices = useCallback(() => { - const indexField = form.getFields().index; - indexField.setValue(indicesConfig); - }, [form, indicesConfig]); - - const handleOpenTimelineSearch = useCallback(() => { - setOpenTimelineSearch(true); - }, []); - - const handleCloseTimelineSearch = useCallback(() => { - setOpenTimelineSearch(false); - }, []); - - return isReadOnlyView ? ( - <StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}> - <StepRuleDescription - columns={descriptionColumns} - indexPatterns={indexPatternQueryBar as IIndexPattern} - schema={filterRuleFieldsForType(schema as FormSchema & RuleFields, myStepData.ruleType)} - data={filterRuleFieldsForType(myStepData, myStepData.ruleType)} - /> - </StepContentWrapper> - ) : ( - <> - <StepContentWrapper addPadding={!isUpdateView}> - <Form form={form} data-test-subj="stepDefineRule"> - <UseField - path="ruleType" - component={SelectRuleType} - componentProps={{ - describedByIds: ['detectionEngineStepDefineRuleType'], - isReadOnly: isUpdateView, - hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense, - isMlAdmin: hasMlAdminPermissions(mlCapabilities), - }} - /> - <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'none' : 'flex' }}> - <> - <CommonUseField - path="index" - config={{ - ...schema.index, - labelAppend: indexModified ? ( - <MyLabelButton onClick={handleResetIndices} iconType="refresh"> - {i18n.RESET_DEFAULT_INDEX} - </MyLabelButton> - ) : null, - }} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleIndices', - 'data-test-subj': 'detectionEngineStepDefineRuleIndices', - euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, - placeholder: '', - }, - }} - /> - <UseField - path="queryBar" - config={{ - ...schema.queryBar, - labelAppend: ( - <MyLabelButton onClick={handleOpenTimelineSearch}> - {i18n.IMPORT_TIMELINE_QUERY} - </MyLabelButton> - ), - }} - component={QueryBarDefineRule} - componentProps={{ - browserFields, - idAria: 'detectionEngineStepDefineRuleQueryBar', - indexPattern: indexPatternQueryBar, - isDisabled: isLoading, - isLoading: indexPatternLoadingQueryBar, - dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', - openTimelineSearch, - onCloseTimelineSearch: handleCloseTimelineSearch, - }} - /> - </> - </EuiFormRow> - <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'flex' : 'none' }}> - <> - <UseField - path="machineLearningJobId" - component={MlJobSelect} - componentProps={{ - describedByIds: ['detectionEngineStepDefineRulemachineLearningJobId'], - }} - /> - <UseField - path="anomalyThreshold" - component={AnomalyThresholdSlider} - componentProps={{ - describedByIds: ['detectionEngineStepDefineRuleAnomalyThreshold'], - }} - /> - </> - </EuiFormRow> - <UseField - path="timeline" - component={PickTimeline} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleTimeline', - isDisabled: isLoading, - dataTestSubj: 'detectionEngineStepDefineRuleTimeline', - }} - /> - <FormDataProvider pathsToWatch={['index', 'ruleType']}> - {({ index, ruleType }) => { - if (index != null) { - if (deepEqual(index, indicesConfig) && indexModified) { - setIndexModified(false); - } else if (!deepEqual(index, indicesConfig) && !indexModified) { - setIndexModified(true); - } - } - - if (isMlRule(ruleType) && !localIsMlRule) { - setIsMlRule(true); - clearErrors(); - } else if (!isMlRule(ruleType) && localIsMlRule) { - setIsMlRule(false); - clearErrors(); - } - - return null; - }} - </FormDataProvider> - </Form> - </StepContentWrapper> - - {!isUpdateView && ( - <NextStep dataTestSubj="define-continue" onClick={onSubmit} isDisabled={isLoading} /> - )} - </> - ); -}; - -export const StepDefineRule = memo(StepDefineRuleComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx deleted file mode 100644 index 629c6758a1414..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ /dev/null @@ -1,176 +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 { i18n } from '@kbn/i18n'; -import { EuiText } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React from 'react'; - -import { isMlRule } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; -import { FieldValueQueryBar } from '../query_bar'; -import { - ERROR_CODE, - FIELD_TYPES, - fieldValidators, - FormSchema, - ValidationFunc, -} from '../../../../../shared_imports'; -import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; - -export const schema: FormSchema = { - index: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', - { - defaultMessage: 'Index patterns', - } - ), - helpText: <EuiText size="xs">{INDEX_HELPER_TEXT}</EuiText>, - validations: [ - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ formData }] = args; - const needsValidation = !isMlRule(formData.ruleType); - - if (!needsValidation) { - return; - } - - return fieldValidators.emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', - { - defaultMessage: 'A minimum of one index pattern is required.', - } - ) - )(...args); - }, - }, - ], - }, - queryBar: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', - { - defaultMessage: 'Custom query', - } - ), - validations: [ - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ value, path, formData }] = args; - const { query, filters } = value as FieldValueQueryBar; - const needsValidation = !isMlRule(formData.ruleType); - if (!needsValidation) { - return; - } - - return isEmpty(query.query as string) && isEmpty(filters) - ? { - code: 'ERR_FIELD_MISSING', - path, - message: CUSTOM_QUERY_REQUIRED, - } - : undefined; - }, - }, - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ value, path, formData }] = args; - const { query } = value as FieldValueQueryBar; - const needsValidation = !isMlRule(formData.ruleType); - if (!needsValidation) { - return; - } - - if (!isEmpty(query.query as string) && query.language === 'kuery') { - try { - esKuery.fromKueryExpression(query.query); - } catch (err) { - return { - code: 'ERR_FIELD_FORMAT', - path, - message: INVALID_CUSTOM_QUERY, - }; - } - } - }, - }, - ], - }, - ruleType: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', - { - defaultMessage: 'Rule type', - } - ), - validations: [], - }, - anomalyThreshold: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', - { - defaultMessage: 'Anomaly score threshold', - } - ), - validations: [], - }, - machineLearningJobId: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', - { - defaultMessage: 'Machine Learning job', - } - ), - validations: [ - { - validator: ( - ...args: Parameters<ValidationFunc> - ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { - const [{ formData }] = args; - const needsValidation = isMlRule(formData.ruleType); - - if (!needsValidation) { - return; - } - - return fieldValidators.emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', - { - defaultMessage: 'A Machine Learning job is required.', - } - ) - )(...args); - }, - }, - ], - }, - timeline: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', - { - defaultMessage: 'Timeline template', - } - ), - helpText: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', - { - defaultMessage: - 'Select an existing timeline to use as a template when investigating generated signals.', - } - ), - }, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx deleted file mode 100644 index 3b297a623e34d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx +++ /dev/null @@ -1,36 +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 React, { useCallback } from 'react'; - -import { - NOTIFICATION_THROTTLE_RULE, - NOTIFICATION_THROTTLE_NO_ACTIONS, -} from '../../../../../../../../../plugins/siem/common/constants'; -import { SelectField } from '../../../../../shared_imports'; - -export const THROTTLE_OPTIONS = [ - { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, - { value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' }, - { value: '1h', text: 'Hourly' }, - { value: '1d', text: 'Daily' }, - { value: '7d', text: 'Weekly' }, -]; - -type ThrottleSelectField = typeof SelectField; - -export const ThrottleSelectField: ThrottleSelectField = props => { - const onChange = useCallback( - e => { - const throttle = e.target.value; - props.field.setValue(throttle); - props.handleChange(throttle); - }, - [props.field.setValue, props.handleChange] - ); - const newEuiFieldProps = { ...props.euiFieldProps, onChange }; - return <SelectField {...props} euiFieldProps={newEuiFieldProps} />; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts deleted file mode 100644 index a65e8178f75c4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ /dev/null @@ -1,174 +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 { has, isEmpty } from 'lodash/fp'; -import moment from 'moment'; -import deepmerge from 'deepmerge'; - -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../../../plugins/siem/common/constants'; -import { transformAlertToRuleAction } from '../../../../../../../../plugins/siem/common/detection_engine/transform_actions'; -import { RuleType } from '../../../../../../../../plugins/siem/common/detection_engine/types'; -import { isMlRule } from '../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { NewRule } from '../../../../containers/detection_engine/rules'; - -import { - AboutStepRule, - DefineStepRule, - ScheduleStepRule, - ActionsStepRule, - DefineStepRuleJson, - ScheduleStepRuleJson, - AboutStepRuleJson, - ActionsStepRuleJson, -} from '../types'; - -export const getTimeTypeValue = (time: string): { unit: string; value: number } => { - const timeObj = { - unit: '', - value: 0, - }; - const filterTimeVal = (time as string).match(/\d+/g); - const filterTimeType = (time as string).match(/[a-zA-Z]+/g); - if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { - timeObj.value = Number(filterTimeVal[0]); - } - if ( - !isEmpty(filterTimeType) && - filterTimeType != null && - ['s', 'm', 'h'].includes(filterTimeType[0]) - ) { - timeObj.unit = filterTimeType[0]; - } - return timeObj; -}; - -export interface RuleFields { - anomalyThreshold: unknown; - machineLearningJobId: unknown; - queryBar: unknown; - index: unknown; - ruleType: unknown; -} -type QueryRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId'>; -type MlRuleFields<T> = Omit<T, 'queryBar' | 'index'>; - -const isMlFields = <T>(fields: QueryRuleFields<T> | MlRuleFields<T>): fields is MlRuleFields<T> => - has('anomalyThreshold', fields); - -export const filterRuleFieldsForType = <T extends RuleFields>(fields: T, type: RuleType) => { - if (isMlRule(type)) { - const { index, queryBar, ...mlRuleFields } = fields; - return mlRuleFields; - } else { - const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; - return queryRuleFields; - } -}; - -export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); - const { ruleType, timeline } = ruleFields; - const baseFields = { - type: ruleType, - ...(timeline.id != null && - timeline.title != null && { - timeline_id: timeline.id, - timeline_title: timeline.title, - }), - }; - - const typeFields = isMlFields(ruleFields) - ? { - anomaly_threshold: ruleFields.anomalyThreshold, - machine_learning_job_id: ruleFields.machineLearningJobId, - } - : { - index: ruleFields.index, - filters: ruleFields.queryBar?.filters, - language: ruleFields.queryBar?.query?.language, - query: ruleFields.queryBar?.query?.query as string, - saved_id: ruleFields.queryBar?.saved_id, - ...(ruleType === 'query' && - ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), - }; - - return { - ...baseFields, - ...typeFields, - }; -}; - -export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { - const { isNew, ...formatScheduleData } = scheduleData; - if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { - const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( - formatScheduleData.interval - ); - const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); - const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); - duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); - formatScheduleData.from = `now-${duration.asSeconds()}s`; - formatScheduleData.to = 'now'; - } - return { - ...formatScheduleData, - meta: { - from: scheduleData.from, - }, - }; -}; - -export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threat, isNew, note, ...rest } = aboutStepData; - return { - false_positives: falsePositives.filter(item => !isEmpty(item)), - references: references.filter(item => !isEmpty(item)), - risk_score: riskScore, - threat: threat - .filter(singleThreat => singleThreat.tactic.name !== 'none') - .map(singleThreat => ({ - ...singleThreat, - framework: 'MITRE ATT&CK', - technique: singleThreat.technique.map(technique => { - const { id, name, reference } = technique; - return { id, name, reference }; - }), - })), - ...(!isEmpty(note) ? { note } : {}), - ...rest, - }; -}; - -export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { - const { - actions = [], - enabled, - kibanaSiemAppUrl, - throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, - } = actionsStepData; - - return { - actions: actions.map(transformAlertToRuleAction), - enabled, - throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, - meta: { - kibana_siem_app_url: kibanaSiemAppUrl, - }, - }; -}; - -export const formatRule = ( - defineStepData: DefineStepRule, - aboutStepData: AboutStepRule, - scheduleData: ScheduleStepRule, - actionsData: ActionsStepRule -): NewRule => - deepmerge.all([ - formatDefineStepData(defineStepData), - formatAboutStepData(aboutStepData), - formatScheduleStepData(scheduleData), - formatActionsStepData(actionsData), - ]) as NewRule; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx deleted file mode 100644 index 1c01a19573cd6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ /dev/null @@ -1,378 +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 { - GetStepsData, - getDefineStepsData, - getScheduleStepsData, - getStepsData, - getAboutStepsData, - getActionsStepsData, - getHumanizedDuration, - getModifiedAboutDetailsData, - determineDetailsValue, - userHasNoPermissions, -} from './helpers'; -import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; -import { esFilters } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; -import { - AboutStepRule, - AboutStepRuleDetails, - DefineStepRule, - ScheduleStepRule, - ActionsStepRule, -} from './types'; - -describe('rule helpers', () => { - describe('getStepsData', () => { - test('returns object with about, define, schedule and actions step properties formatted', () => { - const { - defineRuleData, - modifiedAboutRuleDetailsData, - aboutRuleData, - scheduleRuleData, - ruleActionsData, - }: GetStepsData = getStepsData({ - rule: mockRuleWithEverything('test-id'), - }); - const defineRuleStepData = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - index: ['auditbeat-*'], - machineLearningJobId: '', - queryBar: { - query: { - query: 'user.name: root or user.name: admin', - language: 'kuery', - }, - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Titled timeline', - }, - }; - const aboutRuleStepData = { - description: '24/7', - falsePositives: ['test'], - isNew: false, - name: 'Query with rule-id', - note: '# this is some markdown documentation', - references: ['www.test.co'], - riskScore: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; - const ruleActionsStepData = { - enabled: true, - throttle: 'no_actions', - isNew: false, - actions: [], - }; - const aboutRuleDataDetailsData = { - note: '# this is some markdown documentation', - description: '24/7', - }; - - expect(defineRuleData).toEqual(defineRuleStepData); - expect(aboutRuleData).toEqual(aboutRuleStepData); - expect(scheduleRuleData).toEqual(scheduleRuleStepData); - expect(ruleActionsData).toEqual(ruleActionsStepData); - expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); - }); - }); - - describe('getAboutStepsData', () => { - test('returns name, description, and note as empty string if detailsView is true', () => { - const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); - - expect(result.name).toEqual(''); - expect(result.description).toEqual(''); - expect(result.note).toEqual(''); - }); - - test('returns note as empty string if property does not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.note; - const result: AboutStepRule = getAboutStepsData(mockedRule, false); - - expect(result.note).toEqual(''); - }); - }); - - describe('determineDetailsValue', () => { - test('returns name, description, and note as empty string if detailsView is true', () => { - const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( - mockRuleWithEverything('test-id'), - true - ); - const expected = { name: '', description: '', note: '' }; - - expect(result).toEqual(expected); - }); - - test('returns name, description, and note values if detailsView is false', () => { - const mockedRule = mockRuleWithEverything('test-id'); - const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( - mockedRule, - false - ); - const expected = { - name: mockedRule.name, - description: mockedRule.description, - note: mockedRule.note, - }; - - expect(result).toEqual(expected); - }); - - test('returns note as empty string if property does not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.note; - const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( - mockedRule, - false - ); - const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; - - expect(result).toEqual(expected); - }); - }); - - describe('getDefineStepsData', () => { - test('returns with saved_id if value exists on rule', () => { - const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); - const expected = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: "Garrett's IP", - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Untitled timeline', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns with saved_id of undefined if value does not exist on rule', () => { - const mockedRule = { - ...mockRule('test-id'), - }; - delete mockedRule.saved_id; - const result: DefineStepRule = getDefineStepsData(mockedRule); - const expected = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: undefined, - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Untitled timeline', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns timeline id and title of null if they do not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.timeline_id; - delete mockedRule.timeline_title; - const result: DefineStepRule = getDefineStepsData(mockedRule); - - expect(result.timeline.id).toBeNull(); - expect(result.timeline.title).toBeNull(); - }); - }); - - describe('getHumanizedDuration', () => { - test('returns from as seconds if from duration is less than a minute', () => { - const result = getHumanizedDuration('now-62s', '1m'); - - expect(result).toEqual('2s'); - }); - - test('returns from as minutes if from duration is less than an hour', () => { - const result = getHumanizedDuration('now-660s', '5m'); - - expect(result).toEqual('6m'); - }); - - test('returns from as hours if from duration is more than 60 minutes', () => { - const result = getHumanizedDuration('now-7400s', '5m'); - - expect(result).toEqual('1h'); - }); - - test('returns from as if from is not parsable as dateMath', () => { - const result = getHumanizedDuration('randomstring', '5m'); - - expect(result).toEqual('NaNh'); - }); - - test('returns from as 5m if interval is not parsable as dateMath', () => { - const result = getHumanizedDuration('now-300s', 'randomstring'); - - expect(result).toEqual('5m'); - }); - }); - - describe('getScheduleStepsData', () => { - test('returns expected ScheduleStep rule object', () => { - const mockedRule = { - ...mockRule('test-id'), - }; - const result: ScheduleStepRule = getScheduleStepsData(mockedRule); - const expected = { - isNew: false, - interval: mockedRule.interval, - from: '0s', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('getActionsStepsData', () => { - test('returns expected ActionsStepRule rule object', () => { - const mockedRule = { - ...mockRule('test-id'), - actions: [ - { - id: 'id', - group: 'group', - params: {}, - action_type_id: 'action_type_id', - }, - ], - }; - const result: ActionsStepRule = getActionsStepsData(mockedRule); - const expected = { - actions: [ - { - id: 'id', - group: 'group', - params: {}, - actionTypeId: 'action_type_id', - }, - ], - enabled: mockedRule.enabled, - isNew: false, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('getModifiedAboutDetailsData', () => { - test('returns object with "note" and "description" being those of passed in rule', () => { - const result: AboutStepRuleDetails = getModifiedAboutDetailsData( - mockRuleWithEverything('test-id') - ); - const aboutRuleDataDetailsData = { - note: '# this is some markdown documentation', - description: '24/7', - }; - - expect(result).toEqual(aboutRuleDataDetailsData); - }); - - test('returns "note" with empty string if "note" does not exist', () => { - const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; - const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); - - const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; - - expect(result).toEqual(aboutRuleDetailsData); - }); - }); - - describe('userHasNoPermissions', () => { - test("returns false when user's CRUD operations are null", () => { - const result: boolean = userHasNoPermissions(null); - const userHasNoPermissionsExpectedResult = false; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - - test('returns true when user cannot CRUD', () => { - const result: boolean = userHasNoPermissions(false); - const userHasNoPermissionsExpectedResult = true; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - - test('returns false when user can CRUD', () => { - const result: boolean = userHasNoPermissions(true); - const userHasNoPermissionsExpectedResult = false; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx deleted file mode 100644 index 7bea41c2ab4d5..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ /dev/null @@ -1,276 +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 dateMath from '@elastic/datemath'; -import { get } from 'lodash/fp'; -import moment from 'moment'; -import memoizeOne from 'memoize-one'; -import { useLocation } from 'react-router-dom'; - -import { - RuleAlertAction, - RuleType, -} from '../../../../../../../plugins/siem/common/detection_engine/types'; -import { isMlRule } from '../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; -import { transformRuleToAlertAction } from '../../../../../../../plugins/siem/common/detection_engine/transform_actions'; -import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; -import { FormData, FormHook, FormSchema } from '../../../shared_imports'; -import { - AboutStepRule, - AboutStepRuleDetails, - DefineStepRule, - IMitreEnterpriseAttack, - ScheduleStepRule, - ActionsStepRule, -} from './types'; - -export interface GetStepsData { - aboutRuleData: AboutStepRule; - modifiedAboutRuleDetailsData: AboutStepRuleDetails; - defineRuleData: DefineStepRule; - scheduleRuleData: ScheduleStepRule; - ruleActionsData: ActionsStepRule; -} - -export const getStepsData = ({ - rule, - detailsView = false, -}: { - rule: Rule; - detailsView?: boolean; -}): GetStepsData => { - const defineRuleData: DefineStepRule = getDefineStepsData(rule); - const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); - const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); - const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); - const ruleActionsData: ActionsStepRule = getActionsStepsData(rule); - - return { - aboutRuleData, - modifiedAboutRuleDetailsData, - defineRuleData, - scheduleRuleData, - ruleActionsData, - }; -}; - -export const getActionsStepsData = ( - rule: Omit<Rule, 'actions'> & { actions: RuleAlertAction[] } -): ActionsStepRule => { - const { enabled, throttle, meta, actions = [] } = rule; - - return { - actions: actions?.map(transformRuleToAlertAction), - isNew: false, - throttle, - kibanaSiemAppUrl: meta?.kibana_siem_app_url, - enabled, - }; -}; - -export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ - isNew: false, - ruleType: rule.type, - anomalyThreshold: rule.anomaly_threshold ?? 50, - machineLearningJobId: rule.machine_learning_job_id ?? '', - index: rule.index ?? [], - queryBar: { - query: { query: rule.query ?? '', language: rule.language ?? '' }, - filters: (rule.filters ?? []) as Filter[], - saved_id: rule.saved_id, - }, - timeline: { - id: rule.timeline_id ?? null, - title: rule.timeline_title ?? null, - }, -}); - -export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { - const { interval, from } = rule; - const fromHumanizedValue = getHumanizedDuration(from, interval); - - return { - isNew: false, - interval, - from: fromHumanizedValue, - }; -}; - -export const getHumanizedDuration = (from: string, interval: string): string => { - const fromValue = dateMath.parse(from) ?? moment(); - const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); - - const fromDuration = moment.duration(intervalValue.diff(fromValue)); - const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; - - if (fromDuration.asSeconds() < 60) { - return `${Math.floor(fromDuration.asSeconds())}s`; - } else if (fromDuration.asMinutes() < 60) { - return `${Math.floor(fromDuration.asMinutes())}m`; - } - - return fromHumanize; -}; - -export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { - const { name, description, note } = determineDetailsValue(rule, detailsView); - const { - references, - severity, - false_positives: falsePositives, - risk_score: riskScore, - tags, - threat, - } = rule; - - return { - isNew: false, - name, - description, - note: note!, - references, - severity, - tags, - riskScore, - falsePositives, - threat: threat as IMitreEnterpriseAttack[], - }; -}; - -export const determineDetailsValue = ( - rule: Rule, - detailsView: boolean -): Pick<Rule, 'name' | 'description' | 'note'> => { - const { name, description, note } = rule; - if (detailsView) { - return { name: '', description: '', note: '' }; - } - - return { name, description, note: note ?? '' }; -}; - -export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ - note: rule.note ?? '', - description: rule.description, -}); - -export const useQuery = () => new URLSearchParams(useLocation().search); - -export type PrePackagedRuleStatus = - | 'ruleInstalled' - | 'ruleNotInstalled' - | 'ruleNeedUpdate' - | 'someRuleUninstall' - | 'unknown'; - -export const getPrePackagedRuleStatus = ( - rulesInstalled: number | null, - rulesNotInstalled: number | null, - rulesNotUpdated: number | null -): PrePackagedRuleStatus => { - if ( - rulesNotInstalled != null && - rulesInstalled === 0 && - rulesNotInstalled > 0 && - rulesNotUpdated === 0 - ) { - return 'ruleNotInstalled'; - } else if ( - rulesInstalled != null && - rulesInstalled > 0 && - rulesNotInstalled === 0 && - rulesNotUpdated === 0 - ) { - return 'ruleInstalled'; - } else if ( - rulesInstalled != null && - rulesNotInstalled != null && - rulesInstalled > 0 && - rulesNotInstalled > 0 && - rulesNotUpdated === 0 - ) { - return 'someRuleUninstall'; - } else if ( - rulesInstalled != null && - rulesNotInstalled != null && - rulesNotUpdated != null && - rulesInstalled > 0 && - rulesNotInstalled >= 0 && - rulesNotUpdated > 0 - ) { - return 'ruleNeedUpdate'; - } - return 'unknown'; -}; -export const setFieldValue = ( - form: FormHook<FormData>, - schema: FormSchema<FormData>, - defaultValues: unknown -) => - Object.keys(schema).forEach(key => { - const val = get(key, defaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - -export const redirectToDetections = ( - isSignalIndexExists: boolean | null, - isAuthenticated: boolean | null, - hasEncryptionKey: boolean | null -) => - isSignalIndexExists != null && - isAuthenticated != null && - hasEncryptionKey != null && - (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); - -export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { - const commonRuleParamsKeys = [ - 'id', - 'name', - 'description', - 'false_positives', - 'rule_id', - 'max_signals', - 'risk_score', - 'output_index', - 'references', - 'severity', - 'timeline_id', - 'timeline_title', - 'threat', - 'type', - 'version', - // 'lists', - ]; - - const ruleParamsKeys = [ - ...commonRuleParamsKeys, - ...(isMlRule(ruleType) - ? ['anomaly_threshold', 'machine_learning_job_id'] - : ['index', 'filters', 'language', 'query', 'saved_id']), - ].sort(); - - return ruleParamsKeys; -}; - -export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => { - if (!ruleType) { - return []; - } - const actionMessageRuleParams = getActionMessageRuleParams(ruleType); - - return [ - 'state.signals_count', - '{context.results_link}', - ...actionMessageRuleParams.map(param => `context.rule.${param}`), - ]; -}); - -// typed as null not undefined as the initial state for this value is null. -export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => - canUserCRUD != null ? !canUserCRUD : false; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts deleted file mode 100644 index 380ef52190349..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ /dev/null @@ -1,144 +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 { - RuleAlertAction, - RuleType, -} from '../../../../../../../plugins/siem/common/detection_engine/types'; -import { AlertAction } from '../../../../../../../plugins/alerting/common'; -import { Filter } from '../../../../../../../../src/plugins/data/common'; -import { FieldValueQueryBar } from './components/query_bar'; -import { FormData, FormHook } from '../../../shared_imports'; -import { FieldValueTimeline } from './components/pick_timeline'; - -export interface EuiBasicTableSortTypes { - field: string; - direction: 'asc' | 'desc'; -} - -export interface EuiBasicTableOnChange { - page: { - index: number; - size: number; - }; - sort?: EuiBasicTableSortTypes; -} - -export enum RuleStep { - defineRule = 'define-rule', - aboutRule = 'about-rule', - scheduleRule = 'schedule-rule', - ruleActions = 'rule-actions', -} -export type RuleStatusType = 'passive' | 'active' | 'valid'; - -export interface RuleStepData { - data: unknown; - isValid: boolean; -} - -export interface RuleStepProps { - addPadding?: boolean; - descriptionColumns?: 'multi' | 'single' | 'singleSplit'; - setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; - isReadOnlyView: boolean; - isUpdateView?: boolean; - isLoading: boolean; - resizeParentContainer?: (height: number) => void; - setForm?: (step: RuleStep, form: FormHook<FormData>) => void; -} - -interface StepRuleData { - isNew: boolean; -} -export interface AboutStepRule extends StepRuleData { - name: string; - description: string; - severity: string; - riskScore: number; - references: string[]; - falsePositives: string[]; - tags: string[]; - threat: IMitreEnterpriseAttack[]; - note: string; -} - -export interface AboutStepRuleDetails { - note: string; - description: string; -} - -export interface DefineStepRule extends StepRuleData { - anomalyThreshold: number; - index: string[]; - machineLearningJobId: string; - queryBar: FieldValueQueryBar; - ruleType: RuleType; - timeline: FieldValueTimeline; -} - -export interface ScheduleStepRule extends StepRuleData { - interval: string; - from: string; - to?: string; -} - -export interface ActionsStepRule extends StepRuleData { - actions: AlertAction[]; - enabled: boolean; - kibanaSiemAppUrl?: string; - throttle?: string | null; -} - -export interface DefineStepRuleJson { - anomaly_threshold?: number; - index?: string[]; - filters?: Filter[]; - machine_learning_job_id?: string; - saved_id?: string; - query?: string; - language?: string; - timeline_id?: string; - timeline_title?: string; - type: RuleType; -} - -export interface AboutStepRuleJson { - name: string; - description: string; - severity: string; - risk_score: number; - references: string[]; - false_positives: string[]; - tags: string[]; - threat: IMitreEnterpriseAttack[]; - note?: string; -} - -export interface ScheduleStepRuleJson { - interval: string; - from: string; - to?: string; - meta?: unknown; -} - -export interface ActionsStepRuleJson { - actions: RuleAlertAction[]; - enabled: boolean; - throttle?: string | null; - meta?: unknown; -} - -export interface IMitreAttack { - id: string; - name: string; - reference: string; -} -export interface IMitreEnterpriseAttack { - framework: string; - tactic: IMitreAttack; - technique: IMitreAttack[]; -} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts deleted file mode 100644 index 11942c57a4d27..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.ts +++ /dev/null @@ -1,98 +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 { isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { - getDetectionEngineUrl, - getDetectionEngineTabUrl, - getRulesUrl, - getRuleDetailsUrl, - getCreateRuleUrl, - getEditRuleUrl, -} from '../../../components/link_to/redirect_to_detection_engine'; -import * as i18nDetections from '../translations'; -import * as i18nRules from './translations'; -import { RouteSpyState } from '../../../utils/route/types'; - -const getTabBreadcrumb = (pathname: string, search: string[]) => { - const tabPath = pathname.split('/')[2]; - - if (tabPath === 'alerts') { - return { - text: i18nDetections.ALERT, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } - - if (tabPath === 'signals') { - return { - text: i18nDetections.SIGNAL, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } - - if (tabPath === 'rules') { - return { - text: i18nRules.PAGE_TITLE, - href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } -}; - -const isRuleCreatePage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/create'); - -const isRuleEditPage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/edit'); - -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18nDetections.PAGE_TITLE, - href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - - const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); - - if (tabBreadcrumb) { - breadcrumb = [...breadcrumb, tabBreadcrumb]; - } - - if (params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state.ruleName, - href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - if (isRuleCreatePage(params.pathName)) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.ADD_PAGE_TITLE, - href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.EDIT_PAGE_TITLE, - href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx deleted file mode 100644 index a8a34383585c6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ /dev/null @@ -1,150 +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 React, { useMemo } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import styled from 'styled-components'; - -import { useThrottledResizeObserver } from '../../components/utils'; -import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; -import { Flyout } from '../../components/flyout'; -import { HeaderGlobal } from '../../components/header_global'; -import { HelpMenu } from '../../components/help_menu'; -import { LinkToPage } from '../../components/link_to'; -import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container'; -import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; -import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning'; -import { UseUrlState } from '../../components/url_state'; -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { useShowTimeline } from '../../utils/timeline/use_show_timeline'; -import { NotFoundPage } from '../404'; -import { DetectionEngineContainer } from '../detection_engine'; -import { HostsContainer } from '../hosts'; -import { NetworkContainer } from '../network'; -import { Overview } from '../overview'; -import { Case } from '../case'; -import { Timelines } from '../timelines'; -import { navTabs } from './home_navigations'; -import { SiemPageName } from './types'; - -/* - * This is import is important to keep because if we do not have it - * we will loose the map embeddable until they move to the New Platform - * we need to have it - */ -import 'uiExports/embeddableFactories'; - -const WrappedByAutoSizer = styled.div` - height: 100%; -`; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; - -const Main = styled.main` - height: 100%; -`; -Main.displayName = 'Main'; - -const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) - -/** the global Kibana navigation at the top of every page */ -const globalHeaderHeightPx = 48; - -const calculateFlyoutHeight = ({ - globalHeaderSize, - windowHeight, -}: { - globalHeaderSize: number; - windowHeight: number; -}): number => Math.max(0, windowHeight - globalHeaderSize); - -export const HomePage: React.FC = () => { - const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); - const flyoutHeight = useMemo( - () => - calculateFlyoutHeight({ - globalHeaderSize: globalHeaderHeightPx, - windowHeight, - }), - [windowHeight] - ); - - const [showTimeline] = useShowTimeline(); - - return ( - <WrappedByAutoSizer data-test-subj="wrapped-by-auto-sizer" ref={measureRef}> - <HeaderGlobal /> - - <Main data-test-subj="pageContainer"> - <WithSource sourceId="default"> - {({ browserFields, indexPattern, indicesExist }) => ( - <DragDropContextWrapper browserFields={browserFields}> - <UseUrlState indexPattern={indexPattern} navTabs={navTabs} /> - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( - <> - <AutoSaveWarningMsg /> - <Flyout - flyoutHeight={flyoutHeight} - timelineId="timeline-1" - usersViewing={usersViewing} - /> - </> - )} - - <Switch> - <Redirect exact from="/" to={`/${SiemPageName.overview}`} /> - <Route path={`/:pageName(${SiemPageName.overview})`} render={() => <Overview />} /> - <Route - path={`/:pageName(${SiemPageName.hosts})`} - render={({ match }) => <HostsContainer url={match.url} />} - /> - <Route - path={`/:pageName(${SiemPageName.network})`} - render={({ location, match }) => ( - <NetworkContainer location={location} url={match.url} /> - )} - /> - <Route - path={`/:pageName(${SiemPageName.detections})`} - render={({ location, match }) => ( - <DetectionEngineContainer location={location} url={match.url} /> - )} - /> - <Route - path={`/:pageName(${SiemPageName.timelines})`} - render={() => <Timelines />} - /> - <Route path="/link-to" render={props => <LinkToPage {...props} />} /> - <Route - path="/ml-hosts" - render={({ location, match }) => ( - <MlHostConditionalContainer location={location} url={match.url} /> - )} - /> - <Route - path="/ml-network" - render={({ location, match }) => ( - <MlNetworkConditionalContainer location={location} url={match.url} /> - )} - /> - <Route path={`/:pageName(${SiemPageName.case})`}> - <Case /> - </Route> - <Route render={() => <NotFoundPage />} /> - </Switch> - </DragDropContextWrapper> - )} - </WithSource> - </Main> - - <HelpMenu /> - - <SpyRoute /> - </WrappedByAutoSizer> - ); -}; - -HomePage.displayName = 'HomePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.test.ts deleted file mode 100644 index 69268d841e12e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.test.ts +++ /dev/null @@ -1,68 +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 { getHostDetailsEventsKqlQueryExpression, getHostDetailsPageFilters } from './helpers'; -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; - -describe('hosts page helpers', () => { - describe('getHostDetailsEventsKqlQueryExpression', () => { - const filterQueryExpression = 'user.name: "root"'; - const hostName = 'foo'; - - it('combines the filterQueryExpression and hostname when both are NOT empty', () => { - expect(getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName })).toEqual( - 'user.name: "root" and host.name: "foo"' - ); - }); - - it('returns just the filterQueryExpression when it is NOT empty, but hostname is empty', () => { - expect( - getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName: '' }) - ).toEqual('user.name: "root"'); - }); - - it('returns just the hostname when filterQueryExpression is empty, but hostname is NOT empty', () => { - expect( - getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName }) - ).toEqual('host.name: "foo"'); - }); - - it('returns an empty string when both the filterQueryExpression and hostname are empty', () => { - expect( - getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName: '' }) - ).toEqual(''); - }); - }); - - describe('getHostDetailsPageFilters', () => { - it('correctly constructs pageFilters for the given hostName', () => { - const expected: Filter[] = [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.name', - value: 'host-1', - params: { - query: 'host-1', - }, - }, - query: { - match: { - 'host.name': { - query: 'host-1', - type: 'phrase', - }, - }, - }, - }, - ]; - expect(getHostDetailsPageFilters('host-1')).toEqual(expected); - }); - }); -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.ts deleted file mode 100644 index 461fde2bba0ca..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/helpers.ts +++ /dev/null @@ -1,49 +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 { escapeQueryValue } from '../../../lib/keury'; -import { Filter } from '../../../../../../../../src/plugins/data/public'; - -/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ -export const getHostDetailsEventsKqlQueryExpression = ({ - filterQueryExpression, - hostName, -}: { - filterQueryExpression: string; - hostName: string; -}): string => { - if (filterQueryExpression.length) { - return `${filterQueryExpression}${ - hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' - }`; - } else { - return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; - } -}; - -export const getHostDetailsPageFilters = (hostName: string): Filter[] => [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.name', - value: hostName, - params: { - query: hostName, - }, - }, - query: { - match: { - 'host.name': { - query: hostName, - type: 'phrase', - }, - }, - }, - }, -]; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx deleted file mode 100644 index a12c95b3b5a6f..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/index.tsx +++ /dev/null @@ -1,229 +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 { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import React, { useEffect, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; - -import { FiltersGlobal } from '../../../components/filters_global'; -import { HeaderPage } from '../../../components/header_page'; -import { LastEventTime } from '../../../components/last_event_time'; -import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; -import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria'; -import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../components/ml_popover/hooks/use_ml_capabilities'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { SiemNavigation } from '../../../components/navigation'; -import { KpiHostsComponent } from '../../../components/page/hosts'; -import { HostOverview } from '../../../components/page/hosts/host_overview'; -import { manageQuery } from '../../../components/page/manage_query'; -import { SiemSearchBar } from '../../../components/search_bar'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { HostOverviewByNameQuery } from '../../../containers/hosts/overview'; -import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { LastEventIndexKey } from '../../../graphql/types'; -import { useKibana } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { inputsSelectors, State } from '../../../store'; -import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import { esQuery, Filter } from '../../../../../../../../src/plugins/data/public'; - -import { HostsEmptyPage } from '../hosts_empty_page'; -import { HostDetailsTabs } from './details_tabs'; -import { navTabsHostDetails } from './nav_tabs'; -import { HostDetailsProps } from './types'; -import { type } from './utils'; -import { getHostDetailsPageFilters } from './helpers'; - -const HostOverviewManage = manageQuery(HostOverview); -const KpiHostDetailsManage = manageQuery(KpiHostsComponent); - -const HostDetailsComponent = React.memo<HostDetailsProps & PropsFromRedux>( - ({ - filters, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero, - setQuery, - to, - detailName, - deleteQuery, - hostDetailsPagePath, - }) => { - useEffect(() => { - setHostDetailsTablesActivePageToZero(); - }, [setHostDetailsTablesActivePageToZero, detailName]); - const capabilities = useMlCapabilities(); - const kibana = useKibana(); - const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ - detailName, - ]); - const getFilters = () => [...hostDetailsPageFilters, ...filters]; - const narrowDateRange = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - return ( - <> - <WithSource sourceId="default"> - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: getFilters(), - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - <StickyContainer> - <FiltersGlobal> - <SiemSearchBar indexPattern={indexPattern} id="global" /> - </FiltersGlobal> - - <WrapperPage> - <HeaderPage - border - subtitle={ - <LastEventTime - indexKey={LastEventIndexKey.hostDetails} - hostName={detailName} - /> - } - title={detailName} - /> - - <HostOverviewByNameQuery - sourceId="default" - hostName={detailName} - skip={isInitializing} - startDate={from} - endDate={to} - > - {({ hostOverview, loading, id, inspect, refetch }) => ( - <AnomalyTableProvider - criteriaFields={hostToCriteria(hostOverview)} - startDate={from} - endDate={to} - skip={isInitializing} - > - {({ isLoadingAnomaliesData, anomaliesData }) => ( - <HostOverviewManage - id={id} - inspect={inspect} - refetch={refetch} - setQuery={setQuery} - data={hostOverview} - anomaliesData={anomaliesData} - isLoadingAnomaliesData={isLoadingAnomaliesData} - loading={loading} - startDate={from} - endDate={to} - narrowDateRange={(score, interval) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - </AnomalyTableProvider> - )} - </HostOverviewByNameQuery> - - <EuiHorizontalRule /> - - <KpiHostDetailsQuery - sourceId="default" - filterQuery={filterQuery} - skip={isInitializing} - startDate={from} - endDate={to} - > - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - <KpiHostDetailsManage - data={kpiHostDetails} - from={from} - id={id} - inspect={inspect} - loading={loading} - refetch={refetch} - setQuery={setQuery} - to={to} - narrowDateRange={narrowDateRange} - /> - )} - </KpiHostDetailsQuery> - - <EuiSpacer /> - - <SiemNavigation - navTabs={navTabsHostDetails(detailName, hasMlUserPermissions(capabilities))} - /> - - <EuiSpacer /> - - <HostDetailsTabs - isInitializing={isInitializing} - deleteQuery={deleteQuery} - pageFilters={hostDetailsPageFilters} - to={to} - from={from} - detailName={detailName} - type={type} - setQuery={setQuery} - filterQuery={filterQuery} - hostDetailsPagePath={hostDetailsPagePath} - indexPattern={indexPattern} - setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} - /> - </WrapperPage> - </StickyContainer> - ) : ( - <WrapperPage> - <HeaderPage border title={detailName} /> - - <HostsEmptyPage /> - </WrapperPage> - ); - }} - </WithSource> - - <SpyRoute /> - </> - ); - } -); -HostDetailsComponent.displayName = 'HostDetailsComponent'; - -export const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero: dispatchHostDetailsTablesActivePageToZero, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const HostDetails = connector(HostDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts deleted file mode 100644 index c4680dc3d795e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/utils.ts +++ /dev/null @@ -1,58 +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 { get, isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { hostsModel } from '../../../store'; -import { HostsTableType } from '../../../store/hosts/model'; -import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; - -import * as i18n from '../translations'; -import { HostRouteSpyState } from '../../../utils/route/types'; - -export const type = hostsModel.HostsType.details; - -const TabNameMappedToI18nKey: Record<HostsTableType, string> = { - [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, - [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, - [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, -}; - -export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.detailName, - href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - if (params.tabName != null) { - const tabName = get('tabName', params); - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - } - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index ebcb07131bb24..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,54 +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 React, { useMemo } from 'react'; - -import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { AlertsView } from '../../../components/alerts_viewer'; -import { AlertsComponentQueryProps } from './types'; - -export const filterHostData: Filter[] = [ - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - meta: { - alias: '', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "host.name"}}],"minimum_should_match": 1}}]}}}', - }, - }, -]; -export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { - const { pageFilters, ...rest } = alertsProps; - const hostPageFilters = useMemo( - () => (pageFilters != null ? [...filterHostData, ...pageFilters] : filterHostData), - [pageFilters] - ); - - return <AlertsView {...rest} pageFilters={hostPageFilters} />; -}); - -HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts deleted file mode 100644 index 207b86fee02b9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/types.ts +++ /dev/null @@ -1,61 +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 { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { Filter, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; -import { NarrowDateRange } from '../../../components/ml/types'; -import { InspectQuery, Refetch } from '../../../store/inputs/model'; - -import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { NavTab } from '../../../components/navigation/types'; -import { UpdateDateRange } from '../../../components/charts/common'; - -export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & - HostsTableType.authentications & - HostsTableType.uncommonProcesses & - HostsTableType.events; - -type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies; - -type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission; - -export type HostsNavTab = Record<KeyHostsNavTab, NavTab>; - -export type SetQuery = ({ - id, - inspect, - loading, - refetch, -}: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; -}) => void; - -export interface QueryTabBodyProps { - type: HostsType; - startDate: number; - endDate: number; - filterQuery?: string | ESTermQuery; -} - -export type HostsComponentsQueryProps = QueryTabBodyProps & { - deleteQuery?: ({ id }: { id: string }) => void; - indexPattern: IIndexPattern; - pageFilters?: Filter[]; - skip: boolean; - setQuery: SetQuery; - updateDateRange?: UpdateDateRange; - narrowDateRange?: NarrowDateRange; -}; - -export type AlertsComponentQueryProps = HostsComponentsQueryProps & { - filterQuery: string; - pageFilters?: Filter[]; -}; - -export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx deleted file mode 100644 index e796eaca0cd28..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.tsx +++ /dev/null @@ -1,298 +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 { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; - -import { FiltersGlobal } from '../../../components/filters_global'; -import { HeaderPage } from '../../../components/header_page'; -import { LastEventTime } from '../../../components/last_event_time'; -import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; -import { networkToCriteria } from '../../../components/ml/criteria/network_to_criteria'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; -import { manageQuery } from '../../../components/page/manage_query'; -import { FlowTargetSelectConnected } from '../../../components/page/network/flow_target_select_connected'; -import { IpOverview } from '../../../components/page/network/ip_overview'; -import { SiemSearchBar } from '../../../components/search_bar'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { IpOverviewQuery } from '../../../containers/ip_overview'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; -import { useKibana } from '../../../lib/kibana'; -import { decodeIpv6 } from '../../../lib/helpers'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { ConditionalFlexGroup } from '../../../pages/network/navigation/conditional_flex_group'; -import { networkModel, State, inputsSelectors } from '../../../store'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; -import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../../store/network/actions'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import { NetworkEmptyPage } from '../network_empty_page'; -import { NetworkHttpQueryTable } from './network_http_query_table'; -import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table'; -import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; -import { TlsQueryTable } from './tls_query_table'; -import { IPDetailsComponentProps } from './types'; -import { UsersQueryTable } from './users_query_table'; -import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; -import { esQuery } from '../../../../../../../../src/plugins/data/public'; - -export { getBreadcrumbs } from './utils'; - -const IpOverviewManage = manageQuery(IpOverview); - -export const IPDetailsComponent: React.FC<IPDetailsComponentProps & PropsFromRedux> = ({ - detailName, - filters, - flowTarget, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero, - setQuery, - to, -}) => { - const type = networkModel.NetworkType.details; - const narrowDateRange = useCallback( - (score, interval) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - [setAbsoluteRangeDatePicker] - ); - const kibana = useKibana(); - - useEffect(() => { - setIpDetailsTablesActivePageToZero(); - }, [detailName, setIpDetailsTablesActivePageToZero]); - - return ( - <> - <WithSource sourceId="default" data-test-subj="ip-details-page"> - {({ indicesExist, indexPattern }) => { - const ip = decodeIpv6(detailName); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - <StickyContainer> - <FiltersGlobal> - <SiemSearchBar indexPattern={indexPattern} id="global" /> - </FiltersGlobal> - - <WrapperPage> - <HeaderPage - border - data-test-subj="ip-details-headline" - draggableArguments={{ field: `${flowTarget}.ip`, value: ip }} - subtitle={<LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />} - title={ip} - > - <FlowTargetSelectConnected flowTarget={flowTarget} /> - </HeaderPage> - - <IpOverviewQuery - skip={isInitializing} - sourceId="default" - filterQuery={filterQuery} - type={type} - ip={ip} - > - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - <AnomalyTableProvider - criteriaFields={networkToCriteria(detailName, flowTarget)} - startDate={from} - endDate={to} - skip={isInitializing} - > - {({ isLoadingAnomaliesData, anomaliesData }) => ( - <IpOverviewManage - id={id} - inspect={inspect} - ip={ip} - data={ipOverviewData} - anomaliesData={anomaliesData} - loading={loading} - isLoadingAnomaliesData={isLoadingAnomaliesData} - type={type} - flowTarget={flowTarget} - refetch={refetch} - setQuery={setQuery} - startDate={from} - endDate={to} - narrowDateRange={narrowDateRange} - /> - )} - </AnomalyTableProvider> - )} - </IpOverviewQuery> - - <EuiHorizontalRule /> - - <ConditionalFlexGroup direction="column"> - <EuiFlexItem> - <NetworkTopNFlowQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={FlowTargetSourceDest.source} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - - <EuiFlexItem> - <NetworkTopNFlowQueryTable - endDate={to} - flowTarget={FlowTargetSourceDest.destination} - filterQuery={filterQuery} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - </ConditionalFlexGroup> - - <EuiSpacer /> - - <ConditionalFlexGroup direction="column"> - <EuiFlexItem> - <NetworkTopCountriesQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={FlowTargetSourceDest.source} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - - <EuiFlexItem> - <NetworkTopCountriesQueryTable - endDate={to} - flowTarget={FlowTargetSourceDest.destination} - filterQuery={filterQuery} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - indexPattern={indexPattern} - /> - </EuiFlexItem> - </ConditionalFlexGroup> - - <EuiSpacer /> - - <UsersQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={flowTarget} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - /> - - <EuiSpacer /> - - <NetworkHttpQueryTable - endDate={to} - filterQuery={filterQuery} - ip={ip} - skip={isInitializing} - startDate={from} - type={type} - setQuery={setQuery} - /> - - <EuiSpacer /> - - <TlsQueryTable - endDate={to} - filterQuery={filterQuery} - flowTarget={(flowTarget as unknown) as FlowTargetSourceDest} - ip={ip} - setQuery={setQuery} - skip={isInitializing} - startDate={from} - type={type} - /> - - <EuiSpacer /> - - <AnomaliesQueryTabBody - filterQuery={filterQuery} - setQuery={setQuery} - startDate={from} - endDate={to} - skip={isInitializing} - ip={ip} - type={type} - flowTarget={flowTarget} - narrowDateRange={narrowDateRange} - hideHistogramIfEmpty={true} - AnomaliesTableComponent={AnomaliesNetworkTable} - /> - </WrapperPage> - </StickyContainer> - ) : ( - <WrapperPage> - <HeaderPage border title={ip} /> - - <NetworkEmptyPage /> - </WrapperPage> - ); - }} - </WithSource> - - <SpyRoute /> - </> - ); -}; -IPDetailsComponent.displayName = 'IPDetailsComponent'; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, -}; - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps<typeof connector>; - -export const IPDetails = connector(React.memo(IPDetailsComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts deleted file mode 100644 index efd9c644ec6b6..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/types.ts +++ /dev/null @@ -1,53 +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 { IIndexPattern } from 'src/plugins/data/public'; - -import { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { NetworkType } from '../../../store/network/model'; -import { InspectQuery, Refetch } from '../../../store/inputs/model'; -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; -import { GlobalTimeArgs } from '../../../containers/global_time'; - -export const type = NetworkType.details; - -export type IPDetailsComponentProps = GlobalTimeArgs & { - detailName: string; - flowTarget: FlowTarget; -}; - -export interface OwnProps { - type: NetworkType; - startDate: number; - endDate: number; - filterQuery: string | ESTermQuery; - ip: string; - skip: boolean; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; - }) => void; -} - -export type NetworkComponentsQueryProps = OwnProps & { - flowTarget: FlowTarget; -}; - -export type TlsQueryTableComponentProps = OwnProps & { - flowTarget: FlowTargetSourceDest; -}; - -export type NetworkWithIndexComponentsQueryTableProps = OwnProps & { - flowTarget: FlowTargetSourceDest; - indexPattern: IIndexPattern; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts deleted file mode 100644 index 1488dd556c0b9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/utils.ts +++ /dev/null @@ -1,60 +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 { get, isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; -import { decodeIpv6 } from '../../../lib/helpers'; -import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network'; -import { networkModel } from '../../../store/network'; -import * as i18n from '../translations'; -import { NetworkRouteType } from '../navigation/types'; -import { NetworkRouteSpyState } from '../../../utils/route/types'; - -export const type = networkModel.NetworkType.details; -const TabNameMappedToI18nKey: Record<NetworkRouteType, string> = { - [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, - [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, - [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, - [NetworkRouteType.http]: i18n.NAVIGATION_HTTP_TITLE, - [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, -}; - -export const getBreadcrumbs = ( - params: NetworkRouteSpyState, - search: string[] -): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: decodeIpv6(params.detailName), - href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ - !isEmpty(search[1]) ? search[1] : '' - }`, - }, - ]; - } - - const tabName = get('tabName', params); - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - return breadcrumb; -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index a5d0207f526d1..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,68 +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 React from 'react'; - -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; -import { AlertsView } from '../../../components/alerts_viewer'; -import { NetworkComponentQueryProps } from './types'; - -export const filterNetworkData: Filter[] = [ - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - bool: { - should: [ - { - exists: { - field: 'source.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - exists: { - field: 'destination.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - meta: { - alias: '', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field": "source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field": "destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}', - }, - }, -]; - -export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( - <AlertsView {...alertsProps} pageFilters={filterNetworkData} /> -)); - -NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts deleted file mode 100644 index 90c18b6ff69f4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/network/navigation/types.ts +++ /dev/null @@ -1,77 +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 { ESTermQuery } from '../../../../../../../plugins/siem/common/typed_json'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/'; - -import { NavTab } from '../../../components/navigation/types'; -import { FlowTargetSourceDest } from '../../../graphql/types'; -import { networkModel } from '../../../store'; -import { GlobalTimeArgs } from '../../../containers/global_time'; - -import { SetAbsoluteRangeDatePicker } from '../types'; -import { NarrowDateRange } from '../../../components/ml/types'; - -interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQuery'> { - skip: boolean; - type: networkModel.NetworkType; - startDate: number; - endDate: number; - filterQuery?: string | ESTermQuery; - narrowDateRange?: NarrowDateRange; -} - -export type NetworkComponentQueryProps = QueryTabBodyProps; - -export type IPsQueryTabBodyProps = QueryTabBodyProps & { - indexPattern: IIndexPattern; - flowTarget: FlowTargetSourceDest; -}; - -export type TlsQueryTabBodyProps = QueryTabBodyProps & { - flowTarget: FlowTargetSourceDest; - ip?: string; -}; - -export type HttpQueryTabBodyProps = QueryTabBodyProps & { - ip?: string; -}; - -export type NetworkRoutesProps = GlobalTimeArgs & { - networkPagePath: string; - type: networkModel.NetworkType; - filterQuery?: string | ESTermQuery; - indexPattern: IIndexPattern; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; -}; - -export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & - NetworkRouteType.flows & - NetworkRouteType.http & - NetworkRouteType.tls & - NetworkRouteType.alerts; - -type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & - NetworkRouteType.anomalies; - -type KeyNetworkNavTab = KeyNetworkNavTabWithoutMlPermission | KeyNetworkNavTabWithMlPermission; - -export type NetworkNavTab = Record<KeyNetworkNavTab, NavTab>; - -export enum NetworkRouteType { - flows = 'flows', - dns = 'dns', - anomalies = 'anomalies', - tls = 'tls', - http = 'http', - alerts = 'alerts', -} - -export type GetNetworkRoutePath = ( - pagePath: string, - capabilitiesFetched: boolean, - hasMlUserPermission: boolean -) => string; diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx deleted file mode 100644 index 8e09572cb2796..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.tsx +++ /dev/null @@ -1,123 +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 { EuiButton } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; -import { Position } from '@elastic/charts'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; -import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; -import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { useKibana, useUiSetting$ } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { HostsType } from '../../../store/hosts/model'; - -import * as i18n from '../translations'; -import { - alertsStackByOptions, - histogramConfigs, -} from '../../../components/alerts_viewer/histogram_configs'; -import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; - -const ID = 'alertsByCategoryOverview'; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'event.module'; - -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - hideHeaderChildren?: boolean; - indexPattern: IIndexPattern; - query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const AlertsByCategoryComponent: React.FC<Props> = ({ - deleteQuery, - filters = NO_FILTERS, - from, - hideHeaderChildren = false, - indexPattern, - query = DEFAULT_QUERY, - setQuery, - to, -}) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - - const kibana = useKibana(); - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.detections); - - const alertsCountViewAlertsButton = useMemo( - () => ( - <EuiButton data-test-subj="view-alerts" href={getDetectionEngineAlertUrl(urlSearch)}> - {i18n.VIEW_ALERTS} - </EuiButton> - ), - [urlSearch] - ); - - const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - defaultStackByOption: - alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], - subtitle: (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - legendPosition: Position.Right, - }), - [] - ); - - return ( - <MatrixHistogramContainer - endDate={to} - filterQuery={convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - })} - headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} - id={ID} - setQuery={setQuery} - sourceId="default" - startDate={from} - type={HostsType.page} - {...alertsByCategoryHistogramConfigs} - /> - ); -}; - -AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent'; - -export const AlertsByCategory = React.memo(AlertsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx deleted file mode 100644 index 0fc37935b6062..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.tsx +++ /dev/null @@ -1,91 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { OverviewHost } from '../../../components/page/overview/overview_host'; -import { OverviewNetwork } from '../../../components/page/overview/overview_network'; -import { filterHostData } from '../../hosts/navigation/alerts_query_tab_body'; -import { useKibana } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { filterNetworkData } from '../../network/navigation/alerts_query_tab_body'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; - -const HorizontalSpacer = styled(EuiFlexItem)` - width: 24px; -`; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; - -interface Props { - filters?: Filter[]; - from: number; - indexPattern: IIndexPattern; - query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const EventCountsComponent: React.FC<Props> = ({ - filters = NO_FILTERS, - from, - indexPattern, - query = DEFAULT_QUERY, - setQuery, - to, -}) => { - const kibana = useKibana(); - - return ( - <EuiFlexGroup gutterSize="none" justifyContent="spaceBetween"> - <EuiFlexItem grow={true}> - <OverviewHost - endDate={to} - filterQuery={convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: [...filters, ...filterHostData], - })} - startDate={from} - setQuery={setQuery} - /> - </EuiFlexItem> - - <HorizontalSpacer grow={false} /> - - <EuiFlexItem grow={true}> - <OverviewNetwork - endDate={to} - filterQuery={convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: [...filters, ...filterNetworkData], - })} - startDate={from} - setQuery={setQuery} - /> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -export const EventCounts = React.memo(EventCountsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx deleted file mode 100644 index 14cc29adb505a..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ /dev/null @@ -1,174 +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 { Position } from '@elastic/charts'; -import { EuiButton } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; -import uuid from 'uuid'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; -import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; -import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { - MatrixHisrogramConfigs, - MatrixHistogramOption, -} from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; -import { eventsStackByOptions } from '../../hosts/navigation'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { useKibana, useUiSetting$ } from '../../../lib/kibana'; -import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { InputsModelId } from '../../../store/inputs/constants'; - -import * as i18n from '../translations'; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'event.dataset'; - -const ID = 'eventsByDatasetOverview'; - -interface Props { - combinedQueries?: string; - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - indexPattern: IIndexPattern; - indexToAdd?: string[] | null; - onlyField?: string; - query?: Query; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - showSpacer?: boolean; - to: number; -} - -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - -const EventsByDatasetComponent: React.FC<Props> = ({ - combinedQueries, - deleteQuery, - filters = NO_FILTERS, - from, - headerChildren, - indexPattern, - indexToAdd, - onlyField, - query = DEFAULT_QUERY, - setAbsoluteRangeDatePickerTarget, - setQuery, - showSpacer = true, - to, -}) => { - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - }, [deleteQuery, uniqueQueryId]); - - const kibana = useKibana(); - const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); - - const eventsCountViewEventsButton = useMemo( - () => ( - <EuiButton href={getTabsOnHostsUrl(HostsTableType.events, urlSearch)}> - {i18n.VIEW_EVENTS} - </EuiButton> - ), - [urlSearch] - ); - - const filterQuery = useMemo( - () => - combinedQueries == null - ? convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }) - : combinedQueries, - [combinedQueries, kibana, indexPattern, query, filters] - ); - - const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - stackByOptions: - onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, - defaultStackByOption: - onlyField != null - ? getHistogramOption(onlyField) - : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], - legendPosition: Position.Right, - subtitle: (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - titleSize: onlyField == null ? 'm' : 's', - }), - [onlyField, defaultNumberFormat] - ); - - const headerContent = useMemo(() => { - if (onlyField == null || headerChildren != null) { - return ( - <> - {headerChildren} - {onlyField == null && eventsCountViewEventsButton} - </> - ); - } else { - return null; - } - }, [onlyField, headerChildren, eventsCountViewEventsButton]); - - return ( - <MatrixHistogramContainer - endDate={to} - filterQuery={filterQuery} - headerChildren={headerContent} - id={uniqueQueryId} - indexToAdd={indexToAdd} - setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} - setQuery={setQuery} - showSpacer={showSpacer} - sourceId="default" - startDate={from} - type={HostsType.page} - {...eventsByDatasetHistogramConfigs} - title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} - /> - ); -}; - -EventsByDatasetComponent.displayName = 'EventsByDatasetComponent'; - -export const EventsByDataset = React.memo(EventsByDatasetComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx deleted file mode 100644 index feba80539a11b..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/overview/signals_by_category/index.tsx +++ /dev/null @@ -1,89 +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 React, { useCallback } from 'react'; - -import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel'; -import { signalsHistogramOptions } from '../../detection_engine/components/signals_histogram_panel/config'; -import { useSignalIndex } from '../../../containers/detection_engine/signals/use_signal_index'; -import { SetAbsoluteRangeDatePicker } from '../../network/types'; -import { Filter, IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { InputsModelId } from '../../../store/inputs/constants'; -import * as i18n from '../translations'; - -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; -const NO_FILTERS: Filter[] = []; - -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - indexPattern: IIndexPattern; - /** Override all defaults, and only display this field */ - onlyField?: string; - query?: Query; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const SignalsByCategoryComponent: React.FC<Props> = ({ - deleteQuery, - filters = NO_FILTERS, - from, - headerChildren, - onlyField, - query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, - setAbsoluteRangeDatePickerTarget = 'global', - setQuery, - to, -}) => { - const { signalIndexName } = useSignalIndex(); - const updateDateRangeCallback = useCallback( - (min: number, max: number) => { - setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - const defaultStackByOption = - signalsHistogramOptions.find(o => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0]; - - return ( - <SignalsHistogramPanel - deleteQuery={deleteQuery} - defaultStackByOption={defaultStackByOption} - filters={filters} - from={from} - headerChildren={headerChildren} - onlyField={onlyField} - query={query} - signalIndexName={signalIndexName} - setQuery={setQuery} - showTotalSignalsCount={true} - showLinkToSignals={onlyField == null ? true : false} - stackByOptions={onlyField == null ? signalsHistogramOptions : undefined} - legendPosition={'right'} - to={to} - title={i18n.SIGNAL_COUNT} - updateDateRange={updateDateRangeCallback} - /> - ); -}; - -SignalsByCategoryComponent.displayName = 'SignalsByCategoryComponent'; - -export const SignalsByCategory = React.memo(SignalsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/translations.ts b/x-pack/legacy/plugins/siem/public/pages/timelines/translations.ts deleted file mode 100644 index 723d164ad2cdd..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/translations.ts +++ /dev/null @@ -1,25 +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 { i18n } from '@kbn/i18n'; - -export const PAGE_TITLE = i18n.translate('xpack.siem.timelines.pageTitle', { - defaultMessage: 'Timelines', -}); - -export const ALL_TIMELINES_PANEL_TITLE = i18n.translate( - 'xpack.siem.timelines.allTimelines.panelTitle', - { - defaultMessage: 'All timelines', - } -); - -export const ALL_TIMELINES_IMPORT_TIMELINE_TITLE = i18n.translate( - 'xpack.siem.timelines.allTimelines.importTimelineTitle', - { - defaultMessage: 'Import Timeline', - } -); diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx deleted file mode 100644 index da4aad97e5b48..0000000000000 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ /dev/null @@ -1,93 +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 { - AppMountParameters, - CoreSetup, - CoreStart, - PluginInitializerContext, - Plugin as IPlugin, -} from '../../../../../src/core/public'; -import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; -import { Start as NewsfeedStart } from '../../../../../src/plugins/newsfeed/public'; -import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; -import { initTelemetry } from './lib/telemetry'; -import { KibanaServices } from './lib/kibana'; - -import { serviceNowActionType } from './lib/connectors'; - -import { - TriggersAndActionsUIPublicPluginSetup, - TriggersAndActionsUIPublicPluginStart, -} from '../../../../plugins/triggers_actions_ui/public'; -import { SecurityPluginSetup } from '../../../../plugins/security/public'; - -export { AppMountParameters, CoreSetup, CoreStart, PluginInitializerContext }; - -export interface SetupPlugins { - home: HomePublicPluginSetup; - security: SecurityPluginSetup; - triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; - usageCollection: UsageCollectionSetup; -} -export interface StartPlugins { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - inspector: InspectorStart; - newsfeed?: NewsfeedStart; - security: SecurityPluginSetup; - triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; - uiActions: UiActionsStart; -} -export type StartServices = CoreStart & StartPlugins; - -export type Setup = ReturnType<Plugin['setup']>; -export type Start = ReturnType<Plugin['start']>; - -export class Plugin implements IPlugin<Setup, Start> { - public id = 'siem'; - public name = 'SIEM'; - - constructor( - // @ts-ignore this is added to satisfy the New Platform typing constraint, - // but we're not leveraging any of its functionality yet. - private readonly initializerContext: PluginInitializerContext - ) {} - - public setup(core: CoreSetup, plugins: SetupPlugins) { - initTelemetry(plugins.usageCollection, this.id); - - const security = plugins.security; - - core.application.register({ - id: this.id, - title: this.name, - async mount(context, params) { - const [coreStart, startPlugins] = await core.getStartServices(); - const { renderApp } = await import('./app'); - - plugins.triggers_actions_ui.actionTypeRegistry.register(serviceNowActionType()); - return renderApp(coreStart, { ...startPlugins, security } as StartPlugins, params); - }, - }); - - return {}; - } - - public start(core: CoreStart, plugins: StartPlugins) { - KibanaServices.init({ ...core, ...plugins }); - - return {}; - } - - public stop() { - return {}; - } -} diff --git a/x-pack/legacy/plugins/siem/public/register_feature.ts b/x-pack/legacy/plugins/siem/public/register_feature.ts deleted file mode 100644 index b5e8f78ebc560..0000000000000 --- a/x-pack/legacy/plugins/siem/public/register_feature.ts +++ /dev/null @@ -1,20 +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 { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -import { APP_ID } from '../../../../plugins/siem/common/constants'; - -// TODO(rylnd): move this into Plugin.setup once we're on NP -npSetup.plugins.home.featureCatalogue.register({ - id: APP_ID, - title: 'SIEM', - description: 'Explore security metrics and logs for events and alerts', - icon: 'securityAnalyticsApp', - path: `/app/${APP_ID}`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, -}); diff --git a/x-pack/legacy/plugins/siem/public/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts deleted file mode 100644 index c83433ef129c9..0000000000000 --- a/x-pack/legacy/plugins/siem/public/shared_imports.ts +++ /dev/null @@ -1,26 +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. - */ - -export { - getUseField, - getFieldValidityAndErrorMessage, - FieldHook, - FIELD_TYPES, - Form, - FormData, - FormDataProvider, - FormHook, - FormSchema, - UseField, - useForm, - ValidationFunc, -} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; -export { - Field, - SelectField, -} from '../../../../../src/plugins/es_ui_shared/static/forms/components'; -export { fieldValidators } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers'; -export { ERROR_CODE } from '../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts b/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts deleted file mode 100644 index 5b26957843f08..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts +++ /dev/null @@ -1,86 +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 actionCreatorFactory from 'typescript-fsa'; - -import { InspectQuery, Refetch, RefetchKql } from './model'; -import { InputsModelId } from './constants'; -import { Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); - -export const setAbsoluteRangeDatePicker = actionCreator<{ - id: InputsModelId; - from: number; - to: number; -}>('SET_ABSOLUTE_RANGE_DATE_PICKER'); - -export const setTimelineRangeDatePicker = actionCreator<{ - from: number; - to: number; -}>('SET_TIMELINE_RANGE_DATE_PICKER'); - -export const setRelativeRangeDatePicker = actionCreator<{ - id: InputsModelId; - fromStr: string; - toStr: string; - from: number; - to: number; -}>('SET_RELATIVE_RANGE_DATE_PICKER'); - -export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); - -export const startAutoReload = actionCreator<{ id: InputsModelId }>('START_KQL_AUTO_RELOAD'); - -export const stopAutoReload = actionCreator<{ id: InputsModelId }>('STOP_KQL_AUTO_RELOAD'); - -export const setQuery = actionCreator<{ - inputId: InputsModelId; - id: string; - loading: boolean; - refetch: Refetch | RefetchKql; - inspect: InspectQuery | null; -}>('SET_QUERY'); - -export const deleteOneQuery = actionCreator<{ - inputId: InputsModelId; - id: string; -}>('DELETE_QUERY'); - -export const setInspectionParameter = actionCreator<{ - id: string; - inputId: InputsModelId; - isInspected: boolean; - selectedInspectIndex: number; -}>('SET_INSPECTION_PARAMETER'); - -export const deleteAllQuery = actionCreator<{ id: InputsModelId }>('DELETE_ALL_QUERY'); - -export const toggleTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>( - 'TOGGLE_TIMELINE_LINK_TO' -); - -export const removeTimelineLinkTo = actionCreator('REMOVE_TIMELINE_LINK_TO'); -export const addTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_TIMELINE_LINK_TO'); - -export const removeGlobalLinkTo = actionCreator('REMOVE_GLOBAL_LINK_TO'); -export const addGlobalLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_GLOBAL_LINK_TO'); - -export const setFilterQuery = actionCreator<{ - id: InputsModelId; - query: string | { [key: string]: unknown }; - language: string; -}>('SET_FILTER_QUERY'); - -export const setSavedQuery = actionCreator<{ - id: InputsModelId; - savedQuery: SavedQuery | undefined; -}>('SET_SAVED_QUERY'); - -export const setSearchBarFilter = actionCreator<{ - id: InputsModelId; - filters: Filter[]; -}>('SET_SEARCH_BAR_FILTER'); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts deleted file mode 100644 index e851caf523eb4..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts +++ /dev/null @@ -1,103 +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 { Dispatch } from 'redux'; -import { InputsModelId } from './constants'; -import { CONSTANTS } from '../../components/url_state/constants'; -import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; - -export interface AbsoluteTimeRange { - kind: 'absolute'; - fromStr: undefined; - toStr: undefined; - from: number; - to: number; -} - -export interface RelativeTimeRange { - kind: 'relative'; - fromStr: string; - toStr: string; - from: number; - to: number; -} - -export const isRelativeTimeRange = ( - timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange -): timeRange is RelativeTimeRange => timeRange.kind === 'relative'; - -export const isAbsoluteTimeRange = ( - timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange -): timeRange is AbsoluteTimeRange => timeRange.kind === 'absolute'; - -export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; - -export type URLTimeRange = Omit<TimeRange, 'from' | 'to'> & { - from: string | TimeRange['from']; - to: string | TimeRange['to']; -}; - -export interface Policy { - kind: 'manual' | 'interval'; - duration: number; // in ms -} - -interface InspectVariables { - inspect: boolean; -} -export type RefetchWithParams = ({ inspect }: InspectVariables) => void; -export type RefetchKql = (dispatch: Dispatch) => boolean; -export type Refetch = () => void; - -export interface InspectQuery { - dsl: string[]; - response: string[]; -} - -export interface GlobalGenericQuery { - inspect: InspectQuery | null; - isInspected: boolean; - loading: boolean; - selectedInspectIndex: number; -} - -export interface GlobalGraphqlQuery extends GlobalGenericQuery { - id: string; - refetch: null | Refetch | RefetchWithParams; -} -export interface GlobalKqlQuery extends GlobalGenericQuery { - id: 'kql'; - refetch: RefetchKql; -} - -export type GlobalQuery = GlobalGraphqlQuery | GlobalKqlQuery; - -export interface InputsRange { - timerange: TimeRange; - policy: Policy; - queries: GlobalQuery[]; - linkTo: InputsModelId[]; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; -} - -export interface LinkTo { - linkTo: InputsModelId[]; -} - -export interface InputsModel { - global: InputsRange; - timeline: InputsRange; -} -export interface UrlInputsModelInputs { - linkTo: InputsModelId[]; - [CONSTANTS.timerange]: TimeRange; -} -export interface UrlInputsModel { - global: UrlInputsModelInputs; - timeline: UrlInputsModelInputs; -} diff --git a/x-pack/legacy/plugins/siem/public/store/model.ts b/x-pack/legacy/plugins/siem/public/store/model.ts deleted file mode 100644 index 9e9e663a59fe0..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/model.ts +++ /dev/null @@ -1,23 +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. - */ - -export { appModel } from './app'; -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; -} diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts deleted file mode 100644 index 51043b999c27e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts +++ /dev/null @@ -1,249 +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 actionCreatorFactory from 'typescript-fsa'; - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { Sort } from '../../components/timeline/body/sort'; -import { - DataProvider, - QueryOperator, -} from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; -import { TimelineNonEcsData } from '../../graphql/types'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline'); - -export const addHistory = actionCreator<{ id: string; historyId: string }>('ADD_HISTORY'); - -export const addNote = actionCreator<{ id: string; noteId: string }>('ADD_NOTE'); - -export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventId: string }>( - 'ADD_NOTE_TO_EVENT' -); - -export const upsertColumn = actionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>('UPSERT_COLUMN'); - -export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); - -export const applyDeltaToWidth = actionCreator<{ - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; -}>('APPLY_DELTA_TO_WIDTH'); - -export const applyDeltaToColumnWidth = actionCreator<{ - id: string; - columnId: string; - delta: number; -}>('APPLY_DELTA_TO_COLUMN_WIDTH'); - -export const createTimeline = actionCreator<{ - id: string; - dataProviders?: DataProvider[]; - dateRange?: { - start: number; - end: number; - }; - filters?: Filter[]; - columns: ColumnHeaderOptions[]; - itemsPerPage?: number; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - show?: boolean; - sort?: Sort; - showCheckboxes?: boolean; - showRowRenderers?: boolean; -}>('CREATE_TIMELINE'); - -export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); - -export const removeColumn = actionCreator<{ - id: string; - columnId: string; -}>('REMOVE_COLUMN'); - -export const removeProvider = actionCreator<{ - id: string; - providerId: string; - andProviderId?: string; -}>('REMOVE_PROVIDER'); - -export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE'); - -export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT'); - -export const updateTimeline = actionCreator<{ - id: string; - timeline: TimelineModel; -}>('UPDATE_TIMELINE'); - -export const addTimeline = actionCreator<{ - id: string; - timeline: TimelineModel; -}>('ADD_TIMELINE'); - -export const startTimelineSaving = actionCreator<{ - id: string; -}>('START_TIMELINE_SAVING'); - -export const endTimelineSaving = actionCreator<{ - id: string; -}>('END_TIMELINE_SAVING'); - -export const updateIsLoading = actionCreator<{ - id: string; - isLoading: boolean; -}>('UPDATE_LOADING'); - -export const updateColumns = actionCreator<{ - id: string; - columns: ColumnHeaderOptions[]; -}>('UPDATE_COLUMNS'); - -export const updateDataProviderEnabled = actionCreator<{ - id: string; - enabled: boolean; - providerId: string; - andProviderId?: string; -}>('TOGGLE_PROVIDER_ENABLED'); - -export const updateDataProviderExcluded = actionCreator<{ - id: string; - excluded: boolean; - providerId: string; - andProviderId?: string; -}>('TOGGLE_PROVIDER_EXCLUDED'); - -export const dataProviderEdited = actionCreator<{ - andProviderId?: string; - excluded: boolean; - field: string; - id: string; - operator: QueryOperator; - providerId: string; - value: string | number; -}>('DATA_PROVIDER_EDITED'); - -export const updateDataProviderKqlQuery = actionCreator<{ - id: string; - kqlQuery: string; - providerId: string; -}>('PROVIDER_EDIT_KQL_QUERY'); - -export const updateHighlightedDropAndProviderId = actionCreator<{ - id: string; - providerId: string; -}>('UPDATE_DROP_AND_PROVIDER'); - -export const updateDescription = actionCreator<{ id: string; description: string }>( - 'UPDATE_DESCRIPTION' -); - -export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); - -export const setKqlFilterQueryDraft = actionCreator<{ - id: string; - filterQueryDraft: KueryFilterQuery; -}>('SET_KQL_FILTER_QUERY_DRAFT'); - -export const applyKqlFilterQuery = actionCreator<{ - id: string; - filterQuery: SerializedFilterQuery; -}>('APPLY_KQL_FILTER_QUERY'); - -export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean }>( - 'UPDATE_IS_FAVORITE' -); - -export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE'); - -export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( - 'UPDATE_ITEMS_PER_PAGE' -); - -export const updateItemsPerPageOptions = actionCreator<{ - id: string; - itemsPerPageOptions: number[]; -}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); - -export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); - -export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( - 'UPDATE_PAGE_INDEX' -); - -export const updateProviders = actionCreator<{ id: string; providers: DataProvider[] }>( - 'UPDATE_PROVIDERS' -); - -export const updateRange = actionCreator<{ id: string; start: number; end: number }>( - 'UPDATE_RANGE' -); - -export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); - -export const updateAutoSaveMsg = actionCreator<{ - timelineId: string | null; - newTimelineModel: TimelineModel | null; -}>('UPDATE_AUTO_SAVE'); - -export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); - -export const setSavedQueryId = actionCreator<{ - id: string; - savedQueryId: string | null; -}>('SET_TIMELINE_SAVED_QUERY'); - -export const setFilters = actionCreator<{ - id: string; - filters: Filter[]; -}>('SET_TIMELINE_FILTERS'); - -export const setSelected = actionCreator<{ - id: string; - eventIds: Readonly<Record<string, TimelineNonEcsData[]>>; - isSelected: boolean; - isSelectAllChecked: boolean; -}>('SET_TIMELINE_SELECTED'); - -export const clearSelected = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_SELECTED'); - -export const setEventsLoading = actionCreator<{ - id: string; - eventIds: string[]; - isLoading: boolean; -}>('SET_TIMELINE_EVENTS_LOADING'); - -export const clearEventsLoading = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_LOADING'); - -export const setEventsDeleted = actionCreator<{ - id: string; - eventIds: string[]; - isDeleted: boolean; -}>('SET_TIMELINE_EVENTS_DELETED'); - -export const clearEventsDeleted = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_DELETED'); - -export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( - 'UPDATE_EVENT_TYPE' -); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts deleted file mode 100644 index fa70c1b04608d..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ /dev/null @@ -1,1324 +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 { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers'; -import { Sort } from '../../components/timeline/body/sort'; -import { - DataProvider, - QueryOperator, - QueryMatch, -} from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -import { timelineDefaults } from './defaults'; -import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; -import { TimelineById, TimelineState } from './types'; -import { TimelineNonEcsData } from '../../graphql/types'; - -const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference - -export const isNotNull = <T>(value: T | null): value is T => value !== null; - -export const initialTimelineState: TimelineState = { - timelineById: EMPTY_TIMELINE_BY_ID, - autoSavedWarningMsg: { - timelineId: null, - newTimelineModel: null, - }, - showCallOutUnauthorizedMsg: false, -}; - -interface AddTimelineHistoryParams { - id: string; - historyId: string; - timelineById: TimelineById; -} - -export const addTimelineHistory = ({ - id, - historyId, - timelineById, -}: AddTimelineHistoryParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - historyIds: uniq([...timeline.historyIds, historyId]), - }, - }; -}; - -interface AddTimelineNoteParams { - id: string; - noteId: string; - timelineById: TimelineById; -} - -export const addTimelineNote = ({ - id, - noteId, - timelineById, -}: AddTimelineNoteParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - noteIds: [...timeline.noteIds, noteId], - }, - }; -}; - -interface AddTimelineNoteToEventParams { - id: string; - noteId: string; - eventId: string; - timelineById: TimelineById; -} - -export const addTimelineNoteToEvent = ({ - id, - noteId, - eventId, - timelineById, -}: AddTimelineNoteToEventParams): TimelineById => { - const timeline = timelineById[id]; - const existingNoteIds = getOr([], `eventIdToNoteIds.${eventId}`, timeline); - - return { - ...timelineById, - [id]: { - ...timeline, - eventIdToNoteIds: { - ...timeline.eventIdToNoteIds, - ...{ [eventId]: uniq([...existingNoteIds, noteId]) }, - }, - }, - }; -}; - -interface AddTimelineParams { - id: string; - timeline: TimelineModel; - timelineById: TimelineById; -} - -/** - * Add a saved object timeline to the store - * and default the value to what need to be if values are null - */ -export const addTimelineToStore = ({ - id, - timeline, - timelineById, -}: AddTimelineParams): TimelineById => ({ - ...timelineById, - [id]: { - ...timeline, - isLoading: timelineById[id].isLoading, - }, -}); - -interface AddNewTimelineParams { - columns: ColumnHeaderOptions[]; - dataProviders?: DataProvider[]; - dateRange?: { - start: number; - end: number; - }; - filters?: Filter[]; - id: string; - itemsPerPage?: number; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - show?: boolean; - sort?: Sort; - showCheckboxes?: boolean; - showRowRenderers?: boolean; - timelineById: TimelineById; -} - -/** Adds a new `Timeline` to the provided collection of `TimelineById` */ -export const addNewTimeline = ({ - columns, - dataProviders = [], - dateRange = { start: 0, end: 0 }, - filters = timelineDefaults.filters, - id, - itemsPerPage = timelineDefaults.itemsPerPage, - kqlQuery = { filterQuery: null, filterQueryDraft: null }, - sort = timelineDefaults.sort, - show = false, - showCheckboxes = false, - showRowRenderers = true, - timelineById, -}: AddNewTimelineParams): TimelineById => ({ - ...timelineById, - [id]: { - id, - ...timelineDefaults, - columns, - dataProviders, - dateRange, - filters, - itemsPerPage, - kqlQuery, - sort, - show, - savedObjectId: null, - version: null, - isSaving: false, - isLoading: false, - showCheckboxes, - showRowRenderers, - }, -}); - -interface PinTimelineEventParams { - id: string; - eventId: string; - timelineById: TimelineById; -} - -export const pinTimelineEvent = ({ - id, - eventId, - timelineById, -}: PinTimelineEventParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - pinnedEventIds: { - ...timeline.pinnedEventIds, - ...{ [eventId]: true }, - }, - }, - }; -}; - -interface UpdateShowTimelineProps { - id: string; - show: boolean; - timelineById: TimelineById; -} - -export const updateTimelineShowTimeline = ({ - id, - show, - timelineById, -}: UpdateShowTimelineProps): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - show, - }, - }; -}; - -interface ApplyDeltaToCurrentWidthParams { - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; - timelineById: TimelineById; -} - -export const applyDeltaToCurrentWidth = ({ - id, - delta, - bodyClientWidthPixels, - minWidthPixels, - maxWidthPercent, - timelineById, -}: ApplyDeltaToCurrentWidthParams): TimelineById => { - const timeline = timelineById[id]; - - const requestedWidth = timeline.width + delta * -1; // raw change in width - const maxWidthPixels = (maxWidthPercent / 100) * bodyClientWidthPixels; - const clampedWidth = Math.min(requestedWidth, maxWidthPixels); - const width = Math.max(minWidthPixels, clampedWidth); // if the clamped width is smaller than the min, use the min - - return { - ...timelineById, - [id]: { - ...timeline, - width, - }, - }; -}; - -const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { - if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { - return true; - } - return false; -}; - -const addAndToProviderInTimeline = ( - id: string, - provider: DataProvider, - timeline: TimelineModel, - timelineById: TimelineById -): TimelineById => { - const alreadyExistsProviderIndex = timeline.dataProviders.findIndex( - p => p.id === timeline.highlightedDropAndProviderId - ); - const newProvider = timeline.dataProviders[alreadyExistsProviderIndex]; - const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id); - const { and, ...andProvider } = provider; - - if ( - isEqualWith(queryMatchCustomizer, newProvider.queryMatch, andProvider.queryMatch) || - (alreadyExistsAndProviderIndex === -1 && - newProvider.and.filter(itemAndProvider => - isEqualWith(queryMatchCustomizer, itemAndProvider.queryMatch, andProvider.queryMatch) - ).length > 0) - ) { - return timelineById; - } - - const dataProviders = [ - ...timeline.dataProviders.slice(0, alreadyExistsProviderIndex), - { - ...timeline.dataProviders[alreadyExistsProviderIndex], - and: - alreadyExistsAndProviderIndex > -1 - ? [ - ...newProvider.and.slice(0, alreadyExistsAndProviderIndex), - andProvider, - ...newProvider.and.slice(alreadyExistsAndProviderIndex + 1), - ] - : [...newProvider.and, andProvider], - }, - ...timeline.dataProviders.slice(alreadyExistsProviderIndex + 1), - ]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders, - }, - }; -}; - -const addProviderToTimeline = ( - id: string, - provider: DataProvider, - timeline: TimelineModel, - timelineById: TimelineById -): TimelineById => { - const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id); - - if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { - provider.id = `${provider.id}-${ - timeline.dataProviders.filter(p => p.id === provider.id).length - }`; - } - - const dataProviders = - alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) - ? [ - ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), - provider, - ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), - ] - : [...timeline.dataProviders, provider]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders, - }, - }; -}; - -interface AddTimelineColumnParams { - column: ColumnHeaderOptions; - id: string; - index: number; - timelineById: TimelineById; -} - -/** - * Adds or updates a column. When updating a column, it will be moved to the - * new index - */ -export const upsertTimelineColumn = ({ - column, - id, - index, - timelineById, -}: AddTimelineColumnParams): TimelineById => { - const timeline = timelineById[id]; - const alreadyExistsAtIndex = timeline.columns.findIndex(c => c.id === column.id); - - if (alreadyExistsAtIndex !== -1) { - // remove the existing entry and add the new one at the specified index - const reordered = timeline.columns.filter(c => c.id !== column.id); - reordered.splice(index, 0, column); // ⚠️ mutation - - return { - ...timelineById, - [id]: { - ...timeline, - columns: reordered, - }, - }; - } - - // add the new entry at the specified index - const columns = [...timeline.columns]; - columns.splice(index, 0, column); // ⚠️ mutation - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface RemoveTimelineColumnParams { - id: string; - columnId: string; - timelineById: TimelineById; -} - -export const removeTimelineColumn = ({ - id, - columnId, - timelineById, -}: RemoveTimelineColumnParams): TimelineById => { - const timeline = timelineById[id]; - - const columns = timeline.columns.filter(c => c.id !== columnId); - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface ApplyDeltaToTimelineColumnWidth { - id: string; - columnId: string; - delta: number; - timelineById: TimelineById; -} - -export const applyDeltaToTimelineColumnWidth = ({ - id, - columnId, - delta, - timelineById, -}: ApplyDeltaToTimelineColumnWidth): TimelineById => { - const timeline = timelineById[id]; - - const columnIndex = timeline.columns.findIndex(c => c.id === columnId); - if (columnIndex === -1) { - // the column was not found - return { - ...timelineById, - [id]: { - ...timeline, - }, - }; - } - const minWidthPixels = getColumnWidthFromType(timeline.columns[columnIndex].type!); - const requestedWidth = timeline.columns[columnIndex].width + delta; // raw change in width - const width = Math.max(minWidthPixels, requestedWidth); // if the requested width is smaller than the min, use the min - - const columnWithNewWidth = { - ...timeline.columns[columnIndex], - width, - }; - - const columns = [ - ...timeline.columns.slice(0, columnIndex), - columnWithNewWidth, - ...timeline.columns.slice(columnIndex + 1), - ]; - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface AddTimelineProviderParams { - id: string; - provider: DataProvider; - timelineById: TimelineById; -} - -export const addTimelineProvider = ({ - id, - provider, - timelineById, -}: AddTimelineProviderParams): TimelineById => { - const timeline = timelineById[id]; - - if (timeline.highlightedDropAndProviderId !== '') { - return addAndToProviderInTimeline(id, provider, timeline, timelineById); - } else { - return addProviderToTimeline(id, provider, timeline, timelineById); - } -}; - -interface ApplyKqlFilterQueryDraftParams { - id: string; - filterQuery: SerializedFilterQuery; - timelineById: TimelineById; -} - -export const applyKqlFilterQueryDraft = ({ - id, - filterQuery, - timelineById, -}: ApplyKqlFilterQueryDraftParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlQuery: { - ...timeline.kqlQuery, - filterQuery, - }, - }, - }; -}; - -interface UpdateTimelineKqlModeParams { - id: string; - kqlMode: KqlMode; - timelineById: TimelineById; -} - -export const updateTimelineKqlMode = ({ - id, - kqlMode, - timelineById, -}: UpdateTimelineKqlModeParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlMode, - }, - }; -}; - -interface UpdateKqlFilterQueryDraftParams { - id: string; - filterQueryDraft: KueryFilterQuery; - timelineById: TimelineById; -} - -export const updateKqlFilterQueryDraft = ({ - id, - filterQueryDraft, - timelineById, -}: UpdateKqlFilterQueryDraftParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlQuery: { - ...timeline.kqlQuery, - filterQueryDraft, - }, - }, - }; -}; - -interface UpdateTimelineColumnsParams { - id: string; - columns: ColumnHeaderOptions[]; - timelineById: TimelineById; -} - -export const updateTimelineColumns = ({ - id, - columns, - timelineById, -}: UpdateTimelineColumnsParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface UpdateTimelineDescriptionParams { - id: string; - description: string; - timelineById: TimelineById; -} - -export const updateTimelineDescription = ({ - id, - description, - timelineById, -}: UpdateTimelineDescriptionParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - description: description.endsWith(' ') ? `${description.trim()} ` : description.trim(), - }, - }; -}; - -interface UpdateTimelineTitleParams { - id: string; - title: string; - timelineById: TimelineById; -} - -export const updateTimelineTitle = ({ - id, - title, - timelineById, -}: UpdateTimelineTitleParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - title: title.endsWith(' ') ? `${title.trim()} ` : title.trim(), - }, - }; -}; - -interface UpdateTimelineEventTypeParams { - id: string; - eventType: EventType; - timelineById: TimelineById; -} - -export const updateTimelineEventType = ({ - id, - eventType, - timelineById, -}: UpdateTimelineEventTypeParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - eventType, - }, - }; -}; - -interface UpdateTimelineIsFavoriteParams { - id: string; - isFavorite: boolean; - timelineById: TimelineById; -} - -export const updateTimelineIsFavorite = ({ - id, - isFavorite, - timelineById, -}: UpdateTimelineIsFavoriteParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - isFavorite, - }, - }; -}; - -interface UpdateTimelineIsLiveParams { - id: string; - isLive: boolean; - timelineById: TimelineById; -} - -export const updateTimelineIsLive = ({ - id, - isLive, - timelineById, -}: UpdateTimelineIsLiveParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - isLive, - }, - }; -}; - -interface UpdateTimelineProvidersParams { - id: string; - providers: DataProvider[]; - timelineById: TimelineById; -} - -export const updateTimelineProviders = ({ - id, - providers, - timelineById, -}: UpdateTimelineProvidersParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: providers, - }, - }; -}; - -interface UpdateTimelineRangeParams { - id: string; - start: number; - end: number; - timelineById: TimelineById; -} - -export const updateTimelineRange = ({ - id, - start, - end, - timelineById, -}: UpdateTimelineRangeParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dateRange: { - start, - end, - }, - }, - }; -}; - -interface UpdateTimelineSortParams { - id: string; - sort: Sort; - timelineById: TimelineById; -} - -export const updateTimelineSort = ({ - id, - sort, - timelineById, -}: UpdateTimelineSortParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - sort, - }, - }; -}; - -const updateEnabledAndProvider = ( - andProviderId: string, - enabled: boolean, - providerId: string, - timeline: TimelineModel -) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId ? { ...andProvider, enabled } : andProvider - ), - } - : provider - ); - -const updateEnabledProvider = (enabled: boolean, providerId: string, timeline: TimelineModel) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - enabled, - } - : provider - ); - -interface UpdateTimelineProviderEnabledParams { - id: string; - providerId: string; - enabled: boolean; - timelineById: TimelineById; - andProviderId?: string; -} - -export const updateTimelineProviderEnabled = ({ - id, - providerId, - enabled, - timelineById, - andProviderId, -}: UpdateTimelineProviderEnabledParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateEnabledAndProvider(andProviderId, enabled, providerId, timeline) - : updateEnabledProvider(enabled, providerId, timeline), - }, - }; -}; - -const updateExcludedAndProvider = ( - andProviderId: string, - excluded: boolean, - providerId: string, - timeline: TimelineModel -) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId ? { ...andProvider, excluded } : andProvider - ), - } - : provider - ); - -const updateExcludedProvider = (excluded: boolean, providerId: string, timeline: TimelineModel) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - excluded, - } - : provider - ); - -interface UpdateTimelineProviderExcludedParams { - id: string; - providerId: string; - excluded: boolean; - timelineById: TimelineById; - andProviderId?: string; -} - -export const updateTimelineProviderExcluded = ({ - id, - providerId, - excluded, - timelineById, - andProviderId, -}: UpdateTimelineProviderExcludedParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateExcludedAndProvider(andProviderId, excluded, providerId, timeline) - : updateExcludedProvider(excluded, providerId, timeline), - }, - }; -}; - -const updateProviderProperties = ({ - excluded, - field, - operator, - providerId, - timeline, - value, -}: { - excluded: boolean; - field: string; - operator: QueryOperator; - providerId: string; - timeline: TimelineModel; - value: string | number; -}) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - excluded, - queryMatch: { - ...provider.queryMatch, - field, - displayField: field, - value, - displayValue: value, - operator, - }, - } - : provider - ); - -const updateAndProviderProperties = ({ - andProviderId, - excluded, - field, - operator, - providerId, - timeline, - value, -}: { - andProviderId: string; - excluded: boolean; - field: string; - operator: QueryOperator; - providerId: string; - timeline: TimelineModel; - value: string | number; -}) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId - ? { - ...andProvider, - excluded, - queryMatch: { - ...andProvider.queryMatch, - field, - displayField: field, - value, - displayValue: value, - operator, - }, - } - : andProvider - ), - } - : provider - ); - -interface UpdateTimelineProviderEditPropertiesParams { - andProviderId?: string; - excluded: boolean; - field: string; - id: string; - operator: QueryOperator; - providerId: string; - timelineById: TimelineById; - value: string | number; -} - -export const updateTimelineProviderProperties = ({ - andProviderId, - excluded, - field, - id, - operator, - providerId, - timelineById, - value, -}: UpdateTimelineProviderEditPropertiesParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateAndProviderProperties({ - andProviderId, - excluded, - field, - operator, - providerId, - timeline, - value, - }) - : updateProviderProperties({ - excluded, - field, - operator, - providerId, - timeline, - value, - }), - }, - }; -}; - -interface UpdateTimelineProviderKqlQueryParams { - id: string; - providerId: string; - kqlQuery: string; - timelineById: TimelineById; -} - -export const updateTimelineProviderKqlQuery = ({ - id, - providerId, - kqlQuery, - timelineById, -}: UpdateTimelineProviderKqlQueryParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: timeline.dataProviders.map(provider => - provider.id === providerId ? { ...provider, ...{ kqlQuery } } : provider - ), - }, - }; -}; - -interface UpdateTimelineItemsPerPageParams { - id: string; - itemsPerPage: number; - timelineById: TimelineById; -} - -export const updateTimelineItemsPerPage = ({ - id, - itemsPerPage, - timelineById, -}: UpdateTimelineItemsPerPageParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - itemsPerPage, - }, - }; -}; - -interface UpdateTimelinePageIndexParams { - id: string; - activePage: number; - timelineById: TimelineById; -} - -export const updateTimelinePageIndex = ({ - id, - activePage, - timelineById, -}: UpdateTimelinePageIndexParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - activePage, - }, - }; -}; - -interface UpdateTimelinePerPageOptionsParams { - id: string; - itemsPerPageOptions: number[]; - timelineById: TimelineById; -} - -export const updateTimelinePerPageOptions = ({ - id, - itemsPerPageOptions, - timelineById, -}: UpdateTimelinePerPageOptionsParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - itemsPerPageOptions, - }, - }; -}; - -const removeAndProvider = (andProviderId: string, providerId: string, timeline: TimelineModel) => { - const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); - const providerAndIndex = timeline.dataProviders[providerIndex].and.findIndex( - p => p.id === andProviderId - ); - return [ - ...timeline.dataProviders.slice(0, providerIndex), - { - ...timeline.dataProviders[providerIndex], - and: [ - ...timeline.dataProviders[providerIndex].and.slice(0, providerAndIndex), - ...timeline.dataProviders[providerIndex].and.slice(providerAndIndex + 1), - ], - }, - ...timeline.dataProviders.slice(providerIndex + 1), - ]; -}; - -const removeProvider = (providerId: string, timeline: TimelineModel) => { - const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); - return [ - ...timeline.dataProviders.slice(0, providerIndex), - ...(timeline.dataProviders[providerIndex].and.length - ? [ - { - ...timeline.dataProviders[providerIndex].and.slice(0, 1)[0], - and: [...timeline.dataProviders[providerIndex].and.slice(1)], - }, - ] - : []), - ...timeline.dataProviders.slice(providerIndex + 1), - ]; -}; - -interface RemoveTimelineProviderParams { - id: string; - providerId: string; - timelineById: TimelineById; - andProviderId?: string; -} - -export const removeTimelineProvider = ({ - id, - providerId, - timelineById, - andProviderId, -}: RemoveTimelineProviderParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? removeAndProvider(andProviderId, providerId, timeline) - : removeProvider(providerId, timeline), - }, - }; -}; - -interface SetDeletedTimelineEventsParams { - id: string; - eventIds: string[]; - isDeleted: boolean; - timelineById: TimelineById; -} - -export const setDeletedTimelineEvents = ({ - id, - eventIds, - isDeleted, - timelineById, -}: SetDeletedTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const deletedEventIds = isDeleted - ? union(timeline.deletedEventIds, eventIds) - : timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); - - const selectedEventIds = Object.fromEntries( - Object.entries(timeline.selectedEventIds).filter( - ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) - ) - ); - - const isSelectAllChecked = - Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false; - - return { - ...timelineById, - [id]: { - ...timeline, - deletedEventIds, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -interface SetLoadingTimelineEventsParams { - id: string; - eventIds: string[]; - isLoading: boolean; - timelineById: TimelineById; -} - -export const setLoadingTimelineEvents = ({ - id, - eventIds, - isLoading, - timelineById, -}: SetLoadingTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const loadingEventIds = isLoading - ? union(timeline.loadingEventIds, eventIds) - : timeline.loadingEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); - - return { - ...timelineById, - [id]: { - ...timeline, - loadingEventIds, - }, - }; -}; - -interface SetSelectedTimelineEventsParams { - id: string; - eventIds: Record<string, TimelineNonEcsData[]>; - isSelectAllChecked: boolean; - isSelected: boolean; - timelineById: TimelineById; -} - -export const setSelectedTimelineEvents = ({ - id, - eventIds, - isSelectAllChecked = false, - isSelected, - timelineById, -}: SetSelectedTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const selectedEventIds = isSelected - ? { ...timeline.selectedEventIds, ...eventIds } - : omit(Object.keys(eventIds), timeline.selectedEventIds); - - return { - ...timelineById, - [id]: { - ...timeline, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -interface UnPinTimelineEventParams { - id: string; - eventId: string; - timelineById: TimelineById; -} - -export const unPinTimelineEvent = ({ - id, - eventId, - timelineById, -}: UnPinTimelineEventParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - pinnedEventIds: omit(eventId, timeline.pinnedEventIds), - }, - }; -}; - -interface UpdateHighlightedDropAndProviderIdParams { - id: string; - providerId: string; - timelineById: TimelineById; -} - -export const updateHighlightedDropAndProvider = ({ - id, - providerId, - timelineById, -}: UpdateHighlightedDropAndProviderIdParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - highlightedDropAndProviderId: providerId, - }, - }; -}; - -interface UpdateSavedQueryParams { - id: string; - savedQueryId: string | null; - timelineById: TimelineById; -} - -export const updateSavedQuery = ({ - id, - savedQueryId, - timelineById, -}: UpdateSavedQueryParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - savedQueryId, - }, - }; -}; - -interface UpdateFiltersParams { - id: string; - filters: Filter[]; - timelineById: TimelineById; -} - -export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - filters, - }, - }; -}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts b/x-pack/legacy/plugins/siem/public/store/timeline/model.ts deleted file mode 100644 index ef46a8d061c2e..0000000000000 --- a/x-pack/legacy/plugins/siem/public/store/timeline/model.ts +++ /dev/null @@ -1,149 +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 { Filter } from '../../../../../../../src/plugins/data/public'; -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; -import { Sort } from '../../components/timeline/body/sort'; -import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages -export type KqlMode = 'filter' | 'search'; -export type EventType = 'all' | 'raw' | 'signal'; - -export type ColumnHeaderType = 'not-filtered' | 'text-filter'; - -/** Uniquely identifies a column */ -export type ColumnId = string; - -/** The specification of a column header */ -export interface ColumnHeaderOptions { - aggregatable?: boolean; - category?: string; - columnHeaderType: ColumnHeaderType; - description?: string; - example?: string; - format?: string; - id: ColumnId; - label?: string; - linkField?: string; - placeholder?: string; - type?: string; - width: number; -} - -export interface TimelineModel { - /** The columns displayed in the timeline */ - columns: ColumnHeaderOptions[]; - /** The sources of the event data shown in the timeline */ - dataProviders: DataProvider[]; - /** Events to not be rendered **/ - deletedEventIds: string[]; - /** A summary of the events and notes in this timeline */ - description: string; - /** Typoe of event you want to see in this timeline */ - eventType?: EventType; - /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ - eventIdToNoteIds: Record<string, string[]>; - filters?: Filter[]; - /** The chronological history of actions related to this timeline */ - historyIds: string[]; - /** The chronological history of actions related to this timeline */ - highlightedDropAndProviderId: string; - /** Uniquely identifies the timeline */ - id: string; - /** If selectAll checkbox in header is checked **/ - isSelectAllChecked: boolean; - /** Events to be rendered as loading **/ - loadingEventIds: string[]; - savedObjectId: string | null; - /** When true, this timeline was marked as "favorite" by the user */ - isFavorite: boolean; - /** When true, the timeline will update as new data arrives */ - isLive: boolean; - /** The number of items to show in a single page of results */ - itemsPerPage: number; - /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ - itemsPerPageOptions: number[]; - /** determines the behavior of the KQL bar */ - kqlMode: KqlMode; - /** the KQL query in the KQL bar */ - kqlQuery: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - /** Title */ - title: string; - /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ - noteIds: string[]; - /** Events pinned to this timeline */ - pinnedEventIds: Record<string, boolean>; - pinnedEventsSaveObject: Record<string, PinnedEvent>; - /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ - dateRange: { - start: number; - end: number; - }; - savedQueryId?: string | null; - /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ - selectedEventIds: Record<string, TimelineNonEcsData[]>; - /** When true, show the timeline flyover */ - show: boolean; - /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ - showCheckboxes: boolean; - /** When true, shows additional rowRenderers below the PlainRowRenderer **/ - showRowRenderers: boolean; - /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: Sort; - /** Persists the UI state (width) of the timeline flyover */ - width: number; - /** timeline is saving */ - isSaving: boolean; - isLoading: boolean; - version: string | null; -} - -export type SubsetTimelineModel = Readonly< - Pick< - TimelineModel, - | 'columns' - | 'dataProviders' - | 'deletedEventIds' - | 'description' - | 'eventType' - | 'eventIdToNoteIds' - | 'highlightedDropAndProviderId' - | 'historyIds' - | 'isFavorite' - | 'isLive' - | 'isSelectAllChecked' - | 'itemsPerPage' - | 'itemsPerPageOptions' - | 'kqlMode' - | 'kqlQuery' - | 'title' - | 'loadingEventIds' - | 'noteIds' - | 'pinnedEventIds' - | 'pinnedEventsSaveObject' - | 'dateRange' - | 'selectedEventIds' - | 'show' - | 'showCheckboxes' - | 'showRowRenderers' - | 'sort' - | 'width' - | 'isSaving' - | 'isLoading' - | 'savedObjectId' - | 'version' - > ->; - -export interface TimelineUrl { - id: string; - isOpen: boolean; -} diff --git a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx deleted file mode 100644 index a8ee10ba2b801..0000000000000 --- a/x-pack/legacy/plugins/siem/public/utils/saved_query_services/index.tsx +++ /dev/null @@ -1,27 +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 { useState, useEffect } from 'react'; -import { - SavedQueryService, - createSavedQueryService, -} from '../../../../../../../src/plugins/data/public'; - -import { useKibana } from '../../lib/kibana'; - -export const useSavedQueryServices = () => { - const kibana = useKibana(); - const client = kibana.services.savedObjects.client; - - const [savedQueryService, setSavedQueryService] = useState<SavedQueryService>( - createSavedQueryService(client) - ); - - useEffect(() => { - setSavedQueryService(createSavedQueryService(client)); - }, [client]); - return savedQueryService; -}; diff --git a/x-pack/legacy/plugins/siem/yarn.lock b/x-pack/legacy/plugins/siem/yarn.lock deleted file mode 120000 index 4b16253de2abe..0000000000000 --- a/x-pack/legacy/plugins/siem/yarn.lock +++ /dev/null @@ -1 +0,0 @@ -../../../../yarn.lock \ No newline at end of file diff --git a/x-pack/legacy/plugins/task_manager/server/index.ts b/x-pack/legacy/plugins/task_manager/server/index.ts index ff25d8a1e0e5d..a3167920efa06 100644 --- a/x-pack/legacy/plugins/task_manager/server/index.ts +++ b/x-pack/legacy/plugins/task_manager/server/index.ts @@ -6,8 +6,6 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import mappings from './mappings.json'; -import { migrations } from './migrations'; import { createLegacyApi, getTaskManagerSetup } from './legacy'; export { LegacyTaskManagerApi, getTaskManagerSetup, getTaskManagerStart } from './legacy'; @@ -15,19 +13,13 @@ export { LegacyTaskManagerApi, getTaskManagerSetup, getTaskManagerStart } from ' // Once all plugins are migrated to NP, this can be removed // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TaskManager } from '../../../../plugins/task_manager/server/task_manager'; +import { + LegacyPluginApi, + LegacyPluginSpec, + ArrayOrItem, +} from '../../../../../src/legacy/plugin_discovery/types'; -const savedObjectSchemas = { - task: { - hidden: true, - isNamespaceAgnostic: true, - convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, - indexPattern(config: any) { - return config.get('xpack.task_manager.index'); - }, - }, -}; - -export function taskManager(kibana: any) { +export function taskManager(kibana: LegacyPluginApi): ArrayOrItem<LegacyPluginSpec> { return new kibana.Plugin({ id: 'task_manager', require: ['kibana', 'elasticsearch', 'xpack_main'], @@ -51,14 +43,18 @@ export function taskManager(kibana: any) { server.expose( createLegacyApi( getTaskManagerSetup(server)! - .registerLegacyAPI({}) + .registerLegacyAPI() .then((taskManagerPlugin: TaskManager) => { // we can't tell the Kibana Platform Task Manager plugin to // to wait to `start` as that happens before legacy plugins // instead we will start the internal Task Manager plugin when // all legacy plugins have finished initializing // Once all plugins are migrated to NP, this can be removed - this.kbnServer.afterPluginsInit(() => { + + // the typing for the lagcy server isn't quite correct, so + // we'll bypase it for now + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this as any).kbnServer.afterPluginsInit(() => { taskManagerPlugin.start(); }); return taskManagerPlugin; @@ -66,10 +62,5 @@ export function taskManager(kibana: any) { ) ); }, - uiExports: { - mappings, - migrations, - savedObjectSchemas, - }, - }); + } as Legacy.PluginSpecOptions); } diff --git a/x-pack/legacy/plugins/task_manager/server/legacy.ts b/x-pack/legacy/plugins/task_manager/server/legacy.ts index cd2047b757e61..0d50828004a94 100644 --- a/x-pack/legacy/plugins/task_manager/server/legacy.ts +++ b/x-pack/legacy/plugins/task_manager/server/legacy.ts @@ -49,10 +49,10 @@ export function createLegacyApi(legacyTaskManager: Promise<TaskManager>): Legacy fetch: (opts: SearchOpts) => legacyTaskManager.then((tm: TaskManager) => tm.fetch(opts)), get: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.get(id)), remove: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.remove(id)), - schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: any) => + schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: object) => legacyTaskManager.then((tm: TaskManager) => tm.schedule(taskInstance, options)), runNow: (taskId: string) => legacyTaskManager.then((tm: TaskManager) => tm.runNow(taskId)), - ensureScheduled: (taskInstance: TaskInstanceWithId, options?: any) => + ensureScheduled: (taskInstance: TaskInstanceWithId, options?: object) => legacyTaskManager.then((tm: TaskManager) => tm.ensureScheduled(taskInstance, options)), }; } diff --git a/x-pack/legacy/plugins/task_manager/server/migrations.ts b/x-pack/legacy/plugins/task_manager/server/migrations.ts deleted file mode 100644 index 97c4f97f59c58..0000000000000 --- a/x-pack/legacy/plugins/task_manager/server/migrations.ts +++ /dev/null @@ -1,35 +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 { SavedObject } from '../../../../../src/core/server'; - -export const migrations = { - task: { - '7.4.0': (doc: SavedObject<Record<any, any>>) => ({ - ...doc, - updated_at: new Date().toISOString(), - }), - '7.6.0': moveIntervalIntoSchedule, - }, -}; - -function moveIntervalIntoSchedule({ - attributes: { interval, ...attributes }, - ...doc -}: SavedObject<Record<any, any>>) { - return { - ...doc, - attributes: { - ...attributes, - ...(interval - ? { - schedule: { - interval, - }, - } - : {}), - }, - }; -} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/index.ts b/x-pack/legacy/plugins/triggers_actions_ui/index.ts deleted file mode 100644 index eb74290c84682..0000000000000 --- a/x-pack/legacy/plugins/triggers_actions_ui/index.ts +++ /dev/null @@ -1,34 +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 { Legacy } from 'kibana'; -import { Root } from 'joi'; -import { resolve } from 'path'; - -export function triggersActionsUI(kibana: any) { - return new kibana.Plugin({ - id: 'triggers_actions_ui', - configPrefix: 'xpack.triggers_actions_ui', - isEnabled(config: Legacy.KibanaConfig) { - return ( - config.get('xpack.triggers_actions_ui.enabled') && - (config.get('xpack.actions.enabled') || config.get('xpack.alerting.enabled')) - ); - }, - publicDir: resolve(__dirname, 'public'), - require: ['kibana'], - config(Joi: Root) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - }) - .default(); - }, - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - }); -} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss b/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss deleted file mode 100644 index 8c83c0a571f28..0000000000000 --- a/x-pack/legacy/plugins/triggers_actions_ui/public/index.scss +++ /dev/null @@ -1,2 +0,0 @@ -// Imported EUI -@import 'src/legacy/ui/public/styles/_styling_constants'; diff --git a/x-pack/legacy/plugins/upgrade_assistant/index.ts b/x-pack/legacy/plugins/upgrade_assistant/index.ts deleted file mode 100644 index b5e8ce4750215..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/index.ts +++ /dev/null @@ -1,28 +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 { Legacy } from 'kibana'; -import mappings from './mappings.json'; - -export function upgradeAssistant(kibana: any) { - const config: Legacy.PluginSpecOptions = { - id: 'upgrade_assistant', - uiExports: { - // @ts-ignore - savedObjectSchemas: { - 'upgrade-assistant-reindex-operation': { - isNamespaceAgnostic: true, - }, - 'upgrade-assistant-telemetry': { - isNamespaceAgnostic: true, - }, - }, - mappings, - }, - - init() {}, - }; - return new kibana.Plugin(config); -} diff --git a/x-pack/legacy/plugins/upgrade_assistant/mappings.json b/x-pack/legacy/plugins/upgrade_assistant/mappings.json deleted file mode 100644 index 885ac4d5a9b44..0000000000000 --- a/x-pack/legacy/plugins/upgrade_assistant/mappings.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "upgrade-assistant-reindex-operation": { - "dynamic": true, - "properties": { - "indexName": { - "type": "keyword" - }, - "status": { - "type": "integer" - } - } - }, - "upgrade-assistant-telemetry": { - "properties": { - "ui_open": { - "properties": { - "overview": { - "type": "long", - "null_value": 0 - }, - "cluster": { - "type": "long", - "null_value": 0 - }, - "indices": { - "type": "long", - "null_value": 0 - } - } - }, - "ui_reindex": { - "properties": { - "close": { - "type": "long", - "null_value": 0 - }, - "open": { - "type": "long", - "null_value": 0 - }, - "start": { - "type": "long", - "null_value": 0 - }, - "stop": { - "type": "long", - "null_value": 0 - } - } - }, - "features": { - "properties": { - "deprecation_logging": { - "properties": { - "enabled": { - "type": "boolean", - "null_value": true - } - } - } - } - } - } - } -} diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts deleted file mode 100644 index 74783cf46550f..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ /dev/null @@ -1,15 +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. - */ - -export { ACTION_GROUP_DEFINITIONS } from './alerts'; -export { CHART_FORMAT_LIMITS } from './chart_format_limits'; -export { CLIENT_DEFAULTS } from './client_defaults'; -export { CONTEXT_DEFAULTS } from './context_defaults'; -export * from './capabilities'; -export { PLUGIN } from './plugin'; -export { QUERY, STATES } from './query'; -export * from './ui'; -export * from './rest_api'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts b/x-pack/legacy/plugins/uptime/common/constants/plugin.ts deleted file mode 100644 index 00781726941d5..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/constants/plugin.ts +++ /dev/null @@ -1,19 +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 { i18n } from '@kbn/i18n'; - -export const PLUGIN = { - APP_ROOT_ID: 'react-uptime-root', - DESCRIPTION: 'Uptime monitoring', - ID: 'uptime', - LOCAL_STORAGE_KEY: 'xpack.uptime', - NAME: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { - defaultMessage: 'Uptime', - }), - ROUTER_BASE_NAME: '/app/uptime#', - TITLE: 'uptime', -}; diff --git a/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts b/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts deleted file mode 100644 index 22df610d2d516..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/graphql/resolver_types.ts +++ /dev/null @@ -1,13 +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. - */ - -type UMResolverResult<T> = Promise<T> | T; - -export type UMResolver<Result, Parent, Args, Context> = ( - parent: Parent, - args: Args, - context: Context -) => UMResolverResult<Result>; diff --git a/x-pack/legacy/plugins/uptime/common/graphql/types.ts b/x-pack/legacy/plugins/uptime/common/graphql/types.ts deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts deleted file mode 100644 index e8be67abf3a44..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/certs.ts +++ /dev/null @@ -1,42 +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 * as t from 'io-ts'; - -export const GetCertsParamsType = t.intersection([ - t.type({ - from: t.string, - to: t.string, - index: t.number, - size: t.number, - }), - t.partial({ - search: t.string, - }), -]); - -export type GetCertsParams = t.TypeOf<typeof GetCertsParamsType>; - -export const CertType = t.intersection([ - t.type({ - monitors: t.array( - t.partial({ - name: t.string, - id: t.string, - }) - ), - sha256: t.string, - }), - t.partial({ - certificate_not_valid_after: t.string, - certificate_not_valid_before: t.string, - common_name: t.string, - issuer: t.string, - sha1: t.string, - }), -]); - -export type Cert = t.TypeOf<typeof CertType>; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts deleted file mode 100644 index 985b51891da99..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ /dev/null @@ -1,42 +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 * as t from 'io-ts'; - -export const CertificatesStatesThresholdType = t.interface({ - warningState: t.number, - errorState: t.number, -}); - -export const DynamicSettingsType = t.intersection([ - t.type({ - heartbeatIndices: t.string, - }), - t.partial({ - certificatesThresholds: CertificatesStatesThresholdType, - }), -]); - -export const DynamicSettingsSaveType = t.intersection([ - t.type({ - success: t.boolean, - }), - t.partial({ - error: t.string, - }), -]); - -export type DynamicSettings = t.TypeOf<typeof DynamicSettingsType>; -export type DynamicSettingsSaveResponse = t.TypeOf<typeof DynamicSettingsSaveType>; -export type CertificatesStatesThreshold = t.TypeOf<typeof CertificatesStatesThresholdType>; - -export const defaultDynamicSettings: DynamicSettings = { - heartbeatIndices: 'heartbeat-8*', - certificatesThresholds: { - errorState: 7, - warningState: 30, - }, -}; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts deleted file mode 100644 index 78aab3806ae04..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ /dev/null @@ -1,14 +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. - */ - -export * from './alerts'; -export * from './certs'; -export * from './common'; -export * from './monitor'; -export * from './overview_filters'; -export * from './ping'; -export * from './snapshot'; -export * from './dynamic_settings'; diff --git a/x-pack/legacy/plugins/uptime/common/types/index.ts b/x-pack/legacy/plugins/uptime/common/types/index.ts deleted file mode 100644 index a32eabd49a3e5..0000000000000 --- a/x-pack/legacy/plugins/uptime/common/types/index.ts +++ /dev/null @@ -1,41 +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. - */ - -/** Represents a bucket of monitor status information. */ -export interface StatusData { - /** The timeseries point for this status data. */ - x: number; - /** The value of up counts for this point. */ - up?: number | null; - /** The value for down counts for this point. */ - down?: number | null; - /** The total down counts for this point. */ - total?: number | null; -} - -/** Represents the average monitor duration ms at a point in time. */ -export interface MonitorDurationAveragePoint { - /** The timeseries value for this point. */ - x: number; - /** The average duration ms for the monitor. */ - y?: number | null; -} - -export interface LocationDurationLine { - name: string; - - line: MonitorDurationAveragePoint[]; -} - -/** The data used to populate the monitor charts. */ -export interface MonitorDurationResult { - /** The average values for the monitor duration. */ - locationDurationLines: LocationDurationLine[]; -} - -export interface MonitorIdParam { - monitorId: string; -} diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts deleted file mode 100644 index f52ad8ce867b6..0000000000000 --- a/x-pack/legacy/plugins/uptime/index.ts +++ /dev/null @@ -1,36 +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 { i18n } from '@kbn/i18n'; -import { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; - -export const uptime = (kibana: any) => - new kibana.Plugin({ - configPrefix: 'xpack.uptime', - id: PLUGIN.ID, - publicDir: resolve(__dirname, 'public'), - require: ['alerting', 'kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - app: { - description: i18n.translate('xpack.uptime.pluginDescription', { - defaultMessage: 'Uptime monitoring', - description: 'The description text that will be shown to users in Kibana', - }), - icon: 'plugins/uptime/icons/heartbeat_white.svg', - euiIconType: 'uptimeApp', - title: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { - defaultMessage: 'Uptime', - }), - main: 'plugins/uptime/app', - order: 8900, - url: '/app/uptime#/', - category: DEFAULT_APP_CATEGORIES.observability, - }, - home: ['plugins/uptime/register_feature'], - }, - }); diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts deleted file mode 100644 index d58bf8398fcde..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/apps/index.ts +++ /dev/null @@ -1,16 +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 { npSetup } from 'ui/new_platform'; -import { Plugin } from './plugin'; -import 'uiExports/embeddableFactories'; - -const plugin = new Plugin({ - opaqueId: Symbol('uptime'), - env: {} as any, - config: { get: () => ({} as any) }, -}); -plugin.setup(npSetup); diff --git a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts b/x-pack/legacy/plugins/uptime/public/apps/plugin.ts deleted file mode 100644 index e73598c44c9f0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/apps/plugin.ts +++ /dev/null @@ -1,56 +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 { LegacyCoreSetup, PluginInitializerContext, AppMountParameters } from 'src/core/public'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; -import { UMFrontendLibs } from '../lib/lib'; -import { PLUGIN } from '../../common/constants'; -import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; - -export interface SetupObject { - core: LegacyCoreSetup; - plugins: PluginsSetup; -} - -export class Plugin { - constructor( - // @ts-ignore this is added to satisfy the New Platform typing constraint, - // but we're not leveraging any of its functionality yet. - private readonly initializerContext: PluginInitializerContext - ) {} - - public setup(setup: SetupObject) { - const { core, plugins } = setup; - const { home } = plugins; - - home.featureCatalogue.register({ - category: FeatureCatalogueCategory.DATA, - description: PLUGIN.DESCRIPTION, - icon: 'uptimeApp', - id: PLUGIN.ID, - path: '/app/uptime#/', - showOnHomePage: true, - title: PLUGIN.TITLE, - }); - - core.application.register({ - id: PLUGIN.ID, - euiIconType: 'uptimeApp', - order: 8900, - title: 'Uptime', - async mount(params: AppMountParameters) { - const [coreStart] = await core.getStartServices(); - const { element } = params; - const libs: UMFrontendLibs = { - framework: getKibanaFrameworkAdapter(coreStart, plugins), - }; - libs.framework.render(element); - return () => {}; - }, - }); - } -} diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx deleted file mode 100644 index 85d0b1b593704..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx +++ /dev/null @@ -1,133 +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 React, { useEffect, useState, useContext, useRef } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { npStart } from 'ui/new_platform'; - -import { - ViewMode, - EmbeddableOutput, - ErrorEmbeddable, - isErrorEmbeddable, -} from '../../../../../../../../../src/plugins/embeddable/public'; -import * as i18n from './translations'; -import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../maps/public'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../../plugins/maps/public'; -import { Location } from '../../../../../common/runtime_types'; - -import { getLayerList } from './map_config'; -import { UptimeThemeContext } from '../../../../contexts'; - -export interface EmbeddedMapProps { - upPoints: LocationPoint[]; - downPoints: LocationPoint[]; -} - -export type LocationPoint = Required<Location>; - -const EmbeddedPanel = styled.div` - z-index: auto; - flex: 1; - display: flex; - flex-direction: column; - height: 100%; - position: relative; - .embPanel__content { - display: flex; - flex: 1 1 100%; - z-index: 1; - min-height: 0; // Absolute must for Firefox to scroll contents - } - &&& .mapboxgl-canvas { - animation: none !important; - } -`; - -export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { - const { colors } = useContext(UptimeThemeContext); - const [embeddable, setEmbeddable] = useState<MapEmbeddable | ErrorEmbeddable | undefined>(); - const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null); - const factory = npStart.plugins.embeddable.getEmbeddableFactory< - MapEmbeddableInput, - EmbeddableOutput, - MapEmbeddable - >(MAP_SAVED_OBJECT_TYPE); - - const input: MapEmbeddableInput = { - id: uuid.v4(), - filters: [], - hidePanelTitles: true, - refreshConfig: { - value: 0, - pause: false, - }, - viewMode: ViewMode.VIEW, - isLayerTOCOpen: false, - hideFilterActions: true, - // Zoom Lat/Lon values are set to make sure map is in center in the panel - // It wil also omit Greenland/Antarctica etc - mapCenter: { - lon: 11, - lat: 20, - zoom: 0, - }, - disableInteractive: true, - disableTooltipControl: true, - hideToolbarOverlay: true, - hideLayerControl: true, - hideViewControl: true, - }; - - useEffect(() => { - async function setupEmbeddable() { - if (!factory) { - throw new Error('Map embeddable not found.'); - } - const embeddableObject = await factory.create({ - ...input, - title: i18n.MAP_TITLE, - }); - - if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { - embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - - setEmbeddable(embeddableObject); - } - setupEmbeddable(); - - // we want this effect to execute exactly once after the component mounts - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // update map layers based on points - useEffect(() => { - if (embeddable && !isErrorEmbeddable(embeddable)) { - embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); - } - }, [upPoints, downPoints, embeddable, colors]); - - // We can only render after embeddable has already initialized - useEffect(() => { - if (embeddableRoot.current && embeddable) { - embeddable.render(embeddableRoot.current); - } - }, [embeddable, embeddableRoot]); - - return ( - <EmbeddedPanel> - <div - data-test-subj="xpack.uptime.locationMap.embeddedPanel" - className="embPanel__content" - ref={embeddableRoot} - /> - </EmbeddedPanel> - ); -}); - -EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap deleted file mode 100644 index 605fc3cdb6b38..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_ssl_certificate.test.tsx.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MonitorStatusBar component renders 1`] = ` -Array [ - <div - class="euiSpacer euiSpacer--s" - />, - <div - aria-label="SSL certificate expires in 2 months" - class="euiText euiText--small euiText--constrainedWidth" - > - SSL certificate expires - <span - class="euiBadge euiBadge--iconLeft" - style="background-color:#d3dae6;color:#000" - > - <span - class="euiBadge__content" - > - <span - class="euiBadge__text" - > - in 2 months - </span> - </span> - </span> - </div>, -] -`; - -exports[`MonitorStatusBar component renders null if invalid date 1`] = `null`; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx deleted file mode 100644 index 57ed09cc30ef1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_ssl_certificate.test.tsx +++ /dev/null @@ -1,76 +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 React from 'react'; -import moment from 'moment'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { EuiBadge } from '@elastic/eui'; -import { renderWithIntl } from 'test_utils/enzyme_helpers'; -import { Tls } from '../../../../../common/runtime_types'; -import { MonitorSSLCertificate } from '../monitor_status_bar'; - -describe('MonitorStatusBar component', () => { - let monitorTls: Tls; - - beforeEach(() => { - const dateInTwoMonths = moment() - .add(2, 'month') - .toString(); - - monitorTls = { - certificate_not_valid_after: dateInTwoMonths, - }; - }); - - it('renders', () => { - const component = renderWithIntl(<MonitorSSLCertificate tls={monitorTls} />); - expect(component).toMatchSnapshot(); - }); - - it('renders null if invalid date', () => { - monitorTls = { - certificate_not_valid_after: 'i am so invalid date', - }; - const component = renderWithIntl(<MonitorSSLCertificate tls={monitorTls} />); - expect(component).toMatchSnapshot(); - }); - - it('renders expiration date with a warning state if ssl expiry date is less than 30 days', () => { - const dateIn15Days = moment() - .add(15, 'day') - .toString(); - monitorTls = { - certificate_not_valid_after: dateIn15Days, - }; - const component = mountWithIntl(<MonitorSSLCertificate tls={monitorTls} />); - - const badgeComponent = component.find(EuiBadge); - expect(badgeComponent.props().color).toBe('warning'); - - const badgeComponentText = component.find('.euiBadge__text'); - expect(badgeComponentText.text()).toBe(moment(dateIn15Days).fromNow()); - - expect(badgeComponent.find('span.euiBadge--warning')).toBeTruthy(); - }); - - it('does not render the expiration date with a warning state if expiry date is greater than a month', () => { - const dateIn40Days = moment() - .add(40, 'day') - .toString(); - monitorTls = { - certificate_not_valid_after: dateIn40Days, - }; - const component = mountWithIntl(<MonitorSSLCertificate tls={monitorTls} />); - - const badgeComponent = component.find(EuiBadge); - expect(badgeComponent.props().color).toBe('default'); - - const badgeComponentText = component.find('.euiBadge__text'); - expect(badgeComponentText.text()).toBe(moment(dateIn40Days).fromNow()); - - expect(badgeComponent.find('span.euiBadge--warning')).toHaveLength(0); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx deleted file mode 100644 index d92534aecd175..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx +++ /dev/null @@ -1,58 +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 React from 'react'; -import moment from 'moment'; -import { EuiSpacer, EuiText, EuiBadge } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { Tls } from '../../../../../common/runtime_types'; - -interface Props { - /** - * TLS information coming from monitor in ES heartbeat index - */ - tls: Tls | null | undefined; -} - -export const MonitorSSLCertificate = ({ tls }: Props) => { - const certValidityDate = new Date(tls?.certificate_not_valid_after ?? ''); - - const isValidDate = !isNaN(certValidityDate.valueOf()); - - const dateIn30Days = moment().add('30', 'days'); - - const isExpiringInMonth = isValidDate && dateIn30Days > moment(certValidityDate); - - return isValidDate ? ( - <> - <EuiSpacer size="s" /> - <EuiText - grow={false} - size="s" - aria-label={i18n.translate( - 'xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel', - { - defaultMessage: 'SSL certificate expires {validityDate}', - values: { validityDate: moment(certValidityDate).fromNow() }, - } - )} - > - <FormattedMessage - id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent" - defaultMessage="SSL certificate expires {emphasizedText}" - values={{ - emphasizedText: ( - <EuiBadge color={isExpiringInMonth ? 'warning' : 'default'}> - {moment(certValidityDate).fromNow()} - </EuiBadge> - ), - }} - /> - </EuiText> - </> - ) : null; -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx deleted file mode 100644 index 63aceed2be636..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx +++ /dev/null @@ -1,156 +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 React, { useState, useEffect } from 'react'; -import { uniqueId, startsWith } from 'lodash'; -import { EuiCallOut } from '@elastic/eui'; -import styled from 'styled-components'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { Typeahead } from './typeahead'; -import { useUrlParams } from '../../../hooks'; -import { - esKuery, - IIndexPattern, - QuerySuggestion, - DataPublicPluginSetup, -} from '../../../../../../../../src/plugins/data/public'; - -const Container = styled.div` - margin-bottom: 10px; -`; - -interface State { - suggestions: QuerySuggestion[]; - isLoadingIndexPattern: boolean; -} - -function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { - const ast = esKuery.fromKueryExpression(kuery); - return esKuery.toElasticsearchQuery(ast, indexPattern); -} - -interface Props { - 'aria-label': string; - autocomplete: DataPublicPluginSetup['autocomplete']; - 'data-test-subj': string; - loadIndexPattern: () => void; - indexPattern: IIndexPattern | null; - loading: boolean; -} - -export function KueryBarComponent({ - 'aria-label': ariaLabel, - autocomplete: autocompleteService, - 'data-test-subj': dataTestSubj, - loadIndexPattern, - indexPattern, - loading, -}: Props) { - useEffect(() => { - if (!indexPattern) { - loadIndexPattern(); - } - }, [indexPattern, loadIndexPattern]); - - const [state, setState] = useState<State>({ - suggestions: [], - isLoadingIndexPattern: true, - }); - const [isLoadingSuggestions, setIsLoadingSuggestions] = useState<boolean>(false); - let currentRequestCheck: string; - - const [getUrlParams, updateUrlParams] = useUrlParams(); - const { search: kuery } = getUrlParams(); - - const indexPatternMissing = loading && !indexPattern; - - async function onChange(inputValue: string, selectionStart: number) { - if (!indexPattern) { - return; - } - - setIsLoadingSuggestions(true); - setState({ ...state, suggestions: [] }); - - const currentRequest = uniqueId(); - currentRequestCheck = currentRequest; - - try { - const suggestions = ( - (await autocompleteService.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - query: inputValue, - selectionStart, - selectionEnd: selectionStart, - })) || [] - ) - .filter(suggestion => !startsWith(suggestion.text, 'span.')) - .slice(0, 15); - - if (currentRequest !== currentRequestCheck) { - return; - } - - setIsLoadingSuggestions(false); - setState({ ...state, suggestions }); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error while fetching suggestions', e); - } - } - - function onSubmit(inputValue: string) { - if (indexPattern === null) { - return; - } - - try { - const res = convertKueryToEsQuery(inputValue, indexPattern); - if (!res) { - return; - } - - updateUrlParams({ search: inputValue.trim() }); - } catch (e) { - console.log('Invalid kuery syntax'); // eslint-disable-line no-console - } - } - - return ( - <Container> - <Typeahead - aria-label={ariaLabel} - data-test-subj={dataTestSubj} - disabled={indexPatternMissing} - isLoading={isLoadingSuggestions || loading} - initialValue={kuery} - onChange={onChange} - onSubmit={onSubmit} - suggestions={state.suggestions} - queryExample="" - /> - - {indexPatternMissing && ( - <EuiCallOut - style={{ display: 'inline-block', marginTop: '10px' }} - title={ - <div> - <FormattedMessage - id="xpack.uptime.kueryBar.indexPatternMissingWarningMessage" - // TODO: we need to determine the best instruction to provide if the index pattern is missing - defaultMessage="There was an error retrieving the index pattern." - /> - </div> - } - color="warning" - iconType="alert" - size="s" - /> - )} - </Container> - ); -} diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts b/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts deleted file mode 100644 index 7b9b2d07f2a76..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/translations.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const STATUS_COLUMN_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumnLabel', { - defaultMessage: 'Status', -}); - -export const NAME_COLUMN_LABEL = i18n.translate('xpack.uptime.monitorList.nameColumnLabel', { - defaultMessage: 'Name', -}); - -export const HISTORY_COLUMN_LABEL = i18n.translate( - 'xpack.uptime.monitorList.monitorHistoryColumnLabel', - { - defaultMessage: 'Downtime history', - } -); - -export const getExpandDrawerLabel = (id: string) => { - return i18n.translate('xpack.uptime.monitorList.expandDrawerButton.ariaLabel', { - defaultMessage: 'Expand row for monitor with ID {id}', - description: 'The user can click a button on this table and expand further details.', - values: { - id, - }, - }); -}; - -export const getDescriptionLabel = (itemsLength: number) => { - return i18n.translate('xpack.uptime.monitorList.table.description', { - defaultMessage: - 'Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying {length} items.', - values: { length: itemsLength }, - }); -}; - -export const NO_MONITOR_ITEM_SELECTED = i18n.translate( - 'xpack.uptime.monitorList.noItemForSelectedFiltersMessage', - { - defaultMessage: 'No monitors found for selected filter criteria', - description: - 'This message is show if there are no monitors in the table and some filter or search criteria exists', - } -); - -export const NO_DATA_MESSAGE = i18n.translate('xpack.uptime.monitorList.noItemMessage', { - defaultMessage: 'No uptime monitors found', - description: 'This message is shown if the monitors table is rendered but has no items.', -}); - -export const URL = i18n.translate('xpack.uptime.monitorList.table.url.name', { - defaultMessage: 'Url', -}); - -export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { - defaultMessage: 'Up', -}); - -export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { - defaultMessage: 'Down', -}); - -export const RESPONSE_ANOMALY_SCORE = i18n.translate( - 'xpack.uptime.monitorList.anomalyColumn.label', - { - defaultMessage: 'Response Anomaly Score', - } -); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx deleted file mode 100644 index 5103caee1e1c0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx +++ /dev/null @@ -1,160 +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 React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useSelector } from 'react-redux'; -import { - EuiDescribedFormGroup, - EuiFormRow, - EuiCode, - EuiFieldNumber, - EuiTitle, - EuiSpacer, - EuiSelect, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import { defaultDynamicSettings, DynamicSettings } from '../../../common/runtime_types'; -import { selectDynamicSettings } from '../../state/selectors'; - -type NumStr = string | number; - -export type OnFieldChangeType = (field: string, value?: NumStr) => void; - -export interface SettingsFormProps { - onChange: OnFieldChangeType; - formFields: DynamicSettings | null; - fieldErrors: any; - isDisabled: boolean; -} - -export const CertificateExpirationForm: React.FC<SettingsFormProps> = ({ - onChange, - formFields, - fieldErrors, - isDisabled, -}) => { - const dss = useSelector(selectDynamicSettings); - - return ( - <> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.uptime.sourceConfiguration.certificationSectionTitle" - defaultMessage="Certificate Expiration" - /> - </h3> - </EuiTitle> - <EuiSpacer size="m" /> - <EuiDescribedFormGroup - title={ - <h4> - <FormattedMessage - id="xpack.uptime.sourceConfiguration.stateThresholds" - defaultMessage="Expiration State Thresholds" - /> - </h4> - } - description={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.stateThresholdsDescription" - defaultMessage="Set certificate expiration warning/error thresholds" - /> - } - > - <EuiFormRow - describedByIds={['errorState']} - error={fieldErrors?.certificatesThresholds?.errorState} - fullWidth - helpText={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.errorStateDefaultValue" - defaultMessage="The default value is {defaultValue}" - values={{ - defaultValue: ( - <EuiCode>{defaultDynamicSettings?.certificatesThresholds?.errorState}</EuiCode> - ), - }} - /> - } - isInvalid={!!fieldErrors?.certificatesThresholds?.errorState} - label={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.errorStateLabel" - defaultMessage="Error state" - /> - } - > - <EuiFlexGroup> - <EuiFlexItem grow={2}> - <EuiFieldNumber - data-test-subj={`error-state-threshold-input-${dss.loading ? 'loading' : 'loaded'}`} - fullWidth - disabled={isDisabled} - isLoading={dss.loading} - value={formFields?.certificatesThresholds?.errorState || ''} - onChange={({ currentTarget: { value } }: any) => - onChange( - 'certificatesThresholds.errorState', - value === '' ? undefined : Number(value) - ) - } - /> - </EuiFlexItem> - <EuiFlexItem grow={1}> - <EuiSelect options={[{ value: 'day', text: 'Days' }]} /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFormRow> - <EuiFormRow - describedByIds={['warningState']} - error={fieldErrors?.certificatesThresholds?.warningState} - fullWidth - helpText={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.warningStateDefaultValue" - defaultMessage="The default value is {defaultValue}" - values={{ - defaultValue: ( - <EuiCode>{defaultDynamicSettings?.certificatesThresholds?.warningState}</EuiCode> - ), - }} - /> - } - isInvalid={!!fieldErrors?.certificatesThresholds?.warningState} - label={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.warningStateLabel" - defaultMessage="Warning state" - /> - } - > - <EuiFlexGroup> - <EuiFlexItem grow={2}> - <EuiFieldNumber - data-test-subj={`warning-state-threshold-input-${ - dss.loading ? 'loading' : 'loaded' - }`} - fullWidth - disabled={isDisabled} - isLoading={dss.loading} - value={formFields?.certificatesThresholds?.warningState || ''} - onChange={(event: any) => - onChange('certificatesThresholds.warningState', Number(event.currentTarget.value)) - } - /> - </EuiFlexItem> - <EuiFlexItem grow={1}> - <EuiSelect options={[{ value: 'day', text: 'Days' }]} /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFormRow> - </EuiDescribedFormGroup> - </> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx deleted file mode 100644 index c28eca2ea229e..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx +++ /dev/null @@ -1,90 +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 React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useSelector } from 'react-redux'; -import { - EuiDescribedFormGroup, - EuiFormRow, - EuiCode, - EuiFieldText, - EuiTitle, - EuiSpacer, -} from '@elastic/eui'; -import { defaultDynamicSettings } from '../../../common/runtime_types'; -import { selectDynamicSettings } from '../../state/selectors'; -import { SettingsFormProps } from './certificate_form'; - -export const IndicesForm: React.FC<SettingsFormProps> = ({ - onChange, - formFields, - fieldErrors, - isDisabled, -}) => { - const dss = useSelector(selectDynamicSettings); - - return ( - <> - <EuiTitle size="s"> - <h3> - <FormattedMessage - id="xpack.uptime.sourceConfiguration.indicesSectionTitle" - defaultMessage="Indices" - /> - </h3> - </EuiTitle> - <EuiSpacer size="m" /> - <EuiDescribedFormGroup - title={ - <h4> - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesTitle" - defaultMessage="Uptime indices" - /> - </h4> - } - description={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesDescription" - defaultMessage="Index pattern for matching indices that contain Heartbeat data" - /> - } - > - <EuiFormRow - describedByIds={['heartbeatIndices']} - error={fieldErrors?.heartbeatIndices} - fullWidth - helpText={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesDefaultValue" - defaultMessage="The default value is {defaultValue}" - values={{ - defaultValue: <EuiCode>{defaultDynamicSettings.heartbeatIndices}</EuiCode>, - }} - /> - } - isInvalid={!!fieldErrors?.heartbeatIndices} - label={ - <FormattedMessage - id="xpack.uptime.sourceConfiguration.heartbeatIndicesLabel" - defaultMessage="Heartbeat indices" - /> - } - > - <EuiFieldText - data-test-subj={`heartbeat-indices-input-${dss.loading ? 'loading' : 'loaded'}`} - fullWidth - disabled={isDisabled} - isLoading={dss.loading} - value={formFields?.heartbeatIndices || ''} - onChange={(event: any) => onChange('heartbeatIndices', event.currentTarget.value)} - /> - </EuiFormRow> - </EuiDescribedFormGroup> - </> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/index.ts b/x-pack/legacy/plugins/uptime/public/contexts/index.ts deleted file mode 100644 index 2b27fcfe907ab..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/contexts/index.ts +++ /dev/null @@ -1,13 +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. - */ - -export { UptimeRefreshContext, UptimeRefreshContextProvider } from './uptime_refresh_context'; -export { - UptimeSettingsContextValues, - UptimeSettingsContext, - UptimeSettingsContextProvider, -} from './uptime_settings_context'; -export { UptimeThemeContextProvider, UptimeThemeContext } from './uptime_theme_context'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/index.ts b/x-pack/legacy/plugins/uptime/public/hooks/index.ts deleted file mode 100644 index 1f50e995eda49..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/hooks/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export * from './use_monitor'; -export * from './use_url_params'; -export * from './use_telemetry'; -export * from './update_kuery_string'; diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts deleted file mode 100644 index 74160577cb0b1..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/index.ts +++ /dev/null @@ -1,14 +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. - */ - -// TODO: after NP migration is complete we should be able to remove this lint ignore comment -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../../../plugins/triggers_actions_ui/public'; -import { initMonitorStatusAlertType } from './monitor_status'; - -export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel; - -export const alertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType]; diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx deleted file mode 100644 index 6defb96e0da3d..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx +++ /dev/null @@ -1,192 +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 React, { useEffect, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiPanel, - EuiSpacer, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useDispatch, useSelector } from 'react-redux'; -import { cloneDeep, isEqual, set } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { Link } from 'react-router-dom'; -import { selectDynamicSettings } from '../state/selectors'; -import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; -import { DynamicSettings } from '../../common/runtime_types'; -import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; -import { OVERVIEW_ROUTE } from '../../common/constants'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { UptimePage, useUptimeTelemetry } from '../hooks'; -import { IndicesForm } from '../components/settings/indices_form'; -import { - CertificateExpirationForm, - OnFieldChangeType, -} from '../components/settings/certificate_form'; - -const getFieldErrors = (formFields: DynamicSettings | null) => { - if (formFields) { - const blankStr = 'May not be blank'; - const { certificatesThresholds, heartbeatIndices } = formFields; - const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; - const errorStateErr = certificatesThresholds?.errorState ? null : blankStr; - const warningStateErr = certificatesThresholds?.warningState ? null : blankStr; - return { - heartbeatIndices: heartbeatIndErr, - certificatesThresholds: - errorStateErr || warningStateErr - ? { - errorState: errorStateErr, - warningState: warningStateErr, - } - : null, - }; - } - return null; -}; - -export const SettingsPage = () => { - const dss = useSelector(selectDynamicSettings); - - const settingsBreadcrumbText = i18n.translate('xpack.uptime.settingsBreadcrumbText', { - defaultMessage: 'Settings', - }); - useBreadcrumbs([{ text: settingsBreadcrumbText }]); - - useUptimeTelemetry(UptimePage.Settings); - - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(getDynamicSettings()); - }, [dispatch]); - - const [formFields, setFormFields] = useState<DynamicSettings | null>(dss.settings || null); - - if (!dss.loadError && formFields == null && dss.settings) { - setFormFields({ ...dss.settings }); - } - - const fieldErrors = getFieldErrors(formFields); - - const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); - - const onChangeFormField: OnFieldChangeType = (field, value) => { - if (formFields) { - const newFormFields = cloneDeep(formFields); - set(newFormFields, field, value); - setFormFields(cloneDeep(newFormFields)); - } - }; - - const onApply = (event: React.FormEvent) => { - event.preventDefault(); - if (formFields) { - dispatch(setDynamicSettings(formFields)); - } - }; - - const resetForm = () => { - if (formFields && dss.settings) { - setFormFields({ ...dss.settings }); - } - }; - - const isFormDirty = dss.settings ? !isEqual(dss.settings, formFields) : true; - const canEdit: boolean = - !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; - const isFormDisabled = dss.loading || !canEdit; - - const editNoticeTitle = i18n.translate('xpack.uptime.settings.cannotEditTitle', { - defaultMessage: 'You do not have permission to edit settings.', - }); - const editNoticeText = i18n.translate('xpack.uptime.settings.cannotEditText', { - defaultMessage: - "Your user currently has 'Read' permissions for the Uptime app. Enable a permissions-level of 'All' to edit these settings.", - }); - const cannotEditNotice = canEdit ? null : ( - <> - <EuiCallOut title={editNoticeTitle}>{editNoticeText}</EuiCallOut> - <EuiSpacer size="s" /> - </> - ); - - return ( - <> - <Link to={OVERVIEW_ROUTE} data-test-subj="uptimeSettingsToOverviewLink"> - <EuiButtonEmpty size="s" color="primary" iconType="arrowLeft"> - {i18n.translate('xpack.uptime.settings.returnToOverviewLinkLabel', { - defaultMessage: 'Return to overview', - })} - </EuiButtonEmpty> - </Link> - <EuiSpacer size="s" /> - <EuiPanel> - <EuiFlexGroup> - <EuiFlexItem grow={false}>{cannotEditNotice}</EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <form onSubmit={onApply}> - <EuiForm> - <IndicesForm - onChange={onChangeFormField} - formFields={formFields} - fieldErrors={fieldErrors} - isDisabled={isFormDisabled} - /> - <CertificateExpirationForm - onChange={onChangeFormField} - formFields={formFields} - fieldErrors={fieldErrors} - isDisabled={isFormDisabled} - /> - - <EuiSpacer size="m" /> - <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - data-test-subj="discardSettingsButton" - isDisabled={!isFormDirty || isFormDisabled} - onClick={() => { - resetForm(); - }} - > - <FormattedMessage - id="xpack.uptime.sourceConfiguration.discardSettingsButtonLabel" - defaultMessage="Cancel" - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - data-test-subj="apply-settings-button" - type="submit" - color="primary" - isDisabled={!isFormDirty || !isFormValid || isFormDisabled} - fill - > - <FormattedMessage - id="xpack.uptime.sourceConfiguration.applySettingsButtonLabel" - defaultMessage="Apply changes" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiForm> - </form> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> - </> - ); -}; diff --git a/x-pack/legacy/plugins/uptime/public/register_feature.ts b/x-pack/legacy/plugins/uptime/public/register_feature.ts deleted file mode 100644 index 2f83fa33ba4bc..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/register_feature.ts +++ /dev/null @@ -1,25 +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 { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -const { - plugins: { home }, -} = npSetup; - -home.featureCatalogue.register({ - id: 'uptime', - title: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { defaultMessage: 'Uptime' }), - description: i18n.translate('xpack.uptime.featureCatalogueDescription', { - defaultMessage: 'Perform endpoint health checks and uptime monitoring.', - }), - icon: 'uptimeApp', - path: `uptime#/`, - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA, -}); diff --git a/x-pack/legacy/plugins/uptime/public/routes.tsx b/x-pack/legacy/plugins/uptime/public/routes.tsx deleted file mode 100644 index b5e20ef8a70a9..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/routes.tsx +++ /dev/null @@ -1,37 +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 React, { FC } from 'react'; -import { Route, Switch } from 'react-router-dom'; -import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { OverviewPage } from './components/overview/overview_container'; -import { MONITOR_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../common/constants'; -import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; - -interface RouterProps { - autocomplete: DataPublicPluginSetup['autocomplete']; -} - -export const PageRouter: FC<RouterProps> = ({ autocomplete }) => ( - <Switch> - <Route path={MONITOR_ROUTE}> - <div data-test-subj="uptimeMonitorPage"> - <MonitorPage /> - </div> - </Route> - <Route path={SETTINGS_ROUTE}> - <div data-test-subj="uptimeSettingsPage"> - <SettingsPage /> - </div> - </Route> - <Route path={OVERVIEW_ROUTE}> - <div data-test-subj="uptimeOverviewPage"> - <OverviewPage autocomplete={autocomplete} /> - </div> - </Route> - <Route component={NotFoundPage} /> - </Switch> -); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts deleted file mode 100644 index 2e83490b71b54..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/actions/ml_anomaly.ts +++ /dev/null @@ -1,52 +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 { createAction } from 'redux-actions'; -import { createAsyncAction } from './utils'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; -import { AnomaliesTableRecord } from '../../../../../../plugins/ml/common/types/anomalies'; -import { - CreateMLJobSuccess, - DeleteJobResults, - MonitorIdParam, - HeartbeatIndicesParam, -} from './types'; -import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; - -export const resetMLState = createAction('RESET_ML_STATE'); - -export const getExistingMLJobAction = createAsyncAction<MonitorIdParam, JobExistResult>( - 'GET_EXISTING_ML_JOB' -); - -export const createMLJobAction = createAsyncAction< - MonitorIdParam & HeartbeatIndicesParam, - CreateMLJobSuccess | null ->('CREATE_ML_JOB'); - -export const getMLCapabilitiesAction = createAsyncAction<any, PrivilegesResponse>( - 'GET_ML_CAPABILITIES' -); - -export const deleteMLJobAction = createAsyncAction<MonitorIdParam, DeleteJobResults>( - 'DELETE_ML_JOB' -); - -export interface AnomalyRecordsParams { - dateStart: number; - dateEnd: number; - listOfMonitorIds: string[]; - anomalyThreshold?: number; -} - -export interface AnomalyRecords { - anomalies: AnomaliesTableRecord[]; - interval: string; -} - -export const getAnomalyRecordsAction = createAsyncAction<AnomalyRecordsParams, AnomalyRecords>( - 'GET_ANOMALY_RECORDS' -); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts b/x-pack/legacy/plugins/uptime/public/state/actions/types.ts deleted file mode 100644 index 41381afd31453..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/actions/types.ts +++ /dev/null @@ -1,55 +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 { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; - -export interface AsyncAction<Payload, SuccessPayload> { - get: (payload: Payload) => Action<Payload>; - success: (payload: SuccessPayload) => Action<SuccessPayload>; - fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; -} -export interface AsyncAction1<Payload, SuccessPayload> { - get: (payload?: Payload) => Action<Payload>; - success: (payload: SuccessPayload) => Action<SuccessPayload>; - fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; -} - -export interface MonitorIdParam { - monitorId: string; -} - -export interface HeartbeatIndicesParam { - heartbeatIndices: string; -} - -export interface QueryParams { - monitorId: string; - dateStart: string; - dateEnd: string; - filters?: string; - statusFilter?: string; - location?: string; -} - -export interface MonitorDetailsActionPayload { - monitorId: string; - dateStart: string; - dateEnd: string; - location?: string; -} - -export interface CreateMLJobSuccess { - count: number; - jobId: string; -} - -export interface DeleteJobResults { - [id: string]: { - [status: string]: boolean; - error?: any; - }; -} diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts b/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts deleted file mode 100644 index a5adb96731f33..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/actions/utils.ts +++ /dev/null @@ -1,22 +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 { createAction } from 'redux-actions'; -import { AsyncAction, AsyncAction1 } from './types'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; - -export function createAsyncAction<Payload, SuccessPayload>( - actionStr: string -): AsyncAction1<Payload, SuccessPayload>; -export function createAsyncAction<Payload, SuccessPayload>( - actionStr: string -): AsyncAction<Payload, SuccessPayload> { - return { - get: createAction<Payload>(actionStr), - success: createAction<SuccessPayload>(`${actionStr}_SUCCESS`), - fail: createAction<IHttpFetchError>(`${actionStr}_FAIL`), - }; -} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts deleted file mode 100644 index f10745a50f56a..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ /dev/null @@ -1,100 +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 moment from 'moment'; -import { apiService } from './utils'; -import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; -import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; -import { - CreateMLJobSuccess, - DeleteJobResults, - MonitorIdParam, - HeartbeatIndicesParam, -} from '../actions/types'; -import { DataRecognizerConfigResponse } from '../../../../../../plugins/ml/common/types/modules'; -import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; - -export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase(); - -export const getMLCapabilities = async (): Promise<PrivilegesResponse> => { - return await apiService.get(API_URLS.ML_CAPABILITIES); -}; - -export const getExistingJobs = async (): Promise<JobExistResult> => { - return await apiService.get(API_URLS.ML_MODULE_JOBS + ML_MODULE_ID); -}; - -export const createMLJob = async ({ - monitorId, - heartbeatIndices, -}: MonitorIdParam & HeartbeatIndicesParam): Promise<CreateMLJobSuccess | null> => { - const url = API_URLS.ML_SETUP_MODULE + ML_MODULE_ID; - - // ML App doesn't support upper case characters in job name - const lowerCaseMonitorId = monitorId.toLowerCase(); - - const data = { - prefix: `${lowerCaseMonitorId}_`, - useDedicatedIndex: false, - startDatafeed: true, - start: moment() - .subtract(24, 'h') - .valueOf(), - indexPatternName: heartbeatIndices, - query: { - bool: { - filter: [ - { term: { 'monitor.id': lowerCaseMonitorId } }, - { range: { 'monitor.duration.us': { gt: 0 } } }, - ], - }, - }, - }; - - const response: DataRecognizerConfigResponse = await apiService.post(url, data); - if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) { - const jobResponse = response.jobs[0]; - if (jobResponse.success) { - return { - count: 1, - jobId: jobResponse.id, - }; - } else { - const { error } = jobResponse; - throw new Error(error?.msg); - } - } else { - return null; - } -}; - -export const deleteMLJob = async ({ monitorId }: MonitorIdParam): Promise<DeleteJobResults> => { - const data = { jobIds: [getMLJobId(monitorId)] }; - - return await apiService.post(API_URLS.ML_DELETE_JOB, data); -}; - -export const fetchAnomalyRecords = async ({ - dateStart, - dateEnd, - listOfMonitorIds, - anomalyThreshold, -}: AnomalyRecordsParams): Promise<AnomalyRecords> => { - const data = { - jobIds: listOfMonitorIds.map((monitorId: string) => getMLJobId(monitorId)), - criteriaFields: [], - influencers: [], - aggregationInterval: 'auto', - threshold: anomalyThreshold ?? 25, - earliestMs: dateStart, - latestMs: dateEnd, - dateFormatTz: Intl.DateTimeFormat().resolvedOptions().timeZone, - maxRecords: 500, - maxExamples: 10, - }; - return apiService.post(API_URLS.ML_ANOMALIES_RESULT, data); -}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/utils.ts b/x-pack/legacy/plugins/uptime/public/state/api/utils.ts deleted file mode 100644 index e67efa8570c11..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/api/utils.ts +++ /dev/null @@ -1,80 +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 { PathReporter } from 'io-ts/lib/PathReporter'; -import { isRight } from 'fp-ts/lib/Either'; -import { HttpFetchQuery, HttpSetup } from '../../../../../../../target/types/core/public'; - -class ApiService { - private static instance: ApiService; - private _http!: HttpSetup; - - public get http() { - return this._http; - } - - public set http(httpSetup: HttpSetup) { - this._http = httpSetup; - } - - private constructor() {} - - static getInstance(): ApiService { - if (!ApiService.instance) { - ApiService.instance = new ApiService(); - } - - return ApiService.instance; - } - - public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any) { - const response = await this._http!.get(apiUrl, { query: params }); - - if (decodeType) { - const decoded = decodeType.decode(response); - if (isRight(decoded)) { - return decoded.right; - } else { - // eslint-disable-next-line no-console - console.error( - `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` - ); - } - } - - return response; - } - - public async post(apiUrl: string, data?: any, decodeType?: any) { - const response = await this._http!.post(apiUrl, { - method: 'POST', - body: JSON.stringify(data), - }); - - if (decodeType) { - const decoded = decodeType.decode(response); - if (isRight(decoded)) { - return decoded.right; - } else { - // eslint-disable-next-line no-console - console.warn( - `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` - ); - } - } - return response; - } - - public async delete(apiUrl: string) { - const response = await this._http!.delete(apiUrl); - if (response instanceof Error) { - throw response; - } - return response; - } -} - -export const apiService = ApiService.getInstance(); diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts b/x-pack/legacy/plugins/uptime/public/state/effects/index.ts deleted file mode 100644 index 739179c5bbeae..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/effects/index.ts +++ /dev/null @@ -1,34 +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 { fork } from 'redux-saga/effects'; -import { fetchMonitorDetailsEffect } from './monitor'; -import { fetchOverviewFiltersEffect } from './overview_filters'; -import { fetchSnapshotCountEffect } from './snapshot'; -import { fetchMonitorListEffect } from './monitor_list'; -import { fetchMonitorStatusEffect } from './monitor_status'; -import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_settings'; -import { fetchIndexPatternEffect } from './index_pattern'; -import { fetchPingsEffect, fetchPingHistogramEffect } from './ping'; -import { fetchMonitorDurationEffect } from './monitor_duration'; -import { fetchMLJobEffect } from './ml_anomaly'; -import { fetchIndexStatusEffect } from './index_status'; - -export function* rootEffect() { - yield fork(fetchMonitorDetailsEffect); - yield fork(fetchSnapshotCountEffect); - yield fork(fetchOverviewFiltersEffect); - yield fork(fetchMonitorListEffect); - yield fork(fetchMonitorStatusEffect); - yield fork(fetchDynamicSettingsEffect); - yield fork(setDynamicSettingsEffect); - yield fork(fetchIndexPatternEffect); - yield fork(fetchPingsEffect); - yield fork(fetchPingHistogramEffect); - yield fork(fetchMLJobEffect); - yield fork(fetchMonitorDurationEffect); - yield fork(fetchIndexStatusEffect); -} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts deleted file mode 100644 index f003565e9873e..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { handleActions, Action } from 'redux-actions'; -import { - getDynamicSettings, - getDynamicSettingsSuccess, - getDynamicSettingsFail, - setDynamicSettings, - setDynamicSettingsSuccess, - setDynamicSettingsFail, -} from '../actions/dynamic_settings'; -import { DynamicSettings } from '../../../common/runtime_types'; - -export interface DynamicSettingsState { - settings?: DynamicSettings; - loadError?: Error; - saveError?: Error; - loading: boolean; -} - -const initialState: DynamicSettingsState = { - loading: true, -}; - -export const dynamicSettingsReducer = handleActions<DynamicSettingsState, any>( - { - [String(getDynamicSettings)]: state => ({ - ...state, - loading: true, - }), - [String(getDynamicSettingsSuccess)]: (state, action: Action<DynamicSettings>) => { - return { - loading: false, - settings: action.payload, - }; - }, - [String(getDynamicSettingsFail)]: (state, action: Action<Error>) => { - return { - loading: false, - loadError: action.payload, - }; - }, - [String(setDynamicSettings)]: state => ({ - ...state, - loading: true, - }), - [String(setDynamicSettingsSuccess)]: (state, action: Action<DynamicSettings>) => ({ - settings: action.payload, - saveSucceded: true, - loading: false, - }), - [String(setDynamicSettingsFail)]: (state, action: Action<Error>) => ({ - ...state, - loading: false, - saveSucceeded: false, - saveError: action.payload, - }), - }, - initialState -); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts deleted file mode 100644 index 294bde2f277ec..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index.ts +++ /dev/null @@ -1,36 +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 { combineReducers } from 'redux'; -import { monitorReducer } from './monitor'; -import { overviewFiltersReducer } from './overview_filters'; -import { snapshotReducer } from './snapshot'; -import { uiReducer } from './ui'; -import { monitorStatusReducer } from './monitor_status'; -import { monitorListReducer } from './monitor_list'; -import { dynamicSettingsReducer } from './dynamic_settings'; -import { indexPatternReducer } from './index_pattern'; -import { pingReducer } from './ping'; -import { pingListReducer } from './ping_list'; -import { monitorDurationReducer } from './monitor_duration'; -import { indexStatusReducer } from './index_status'; -import { mlJobsReducer } from './ml_anomaly'; - -export const rootReducer = combineReducers({ - monitor: monitorReducer, - overviewFilters: overviewFiltersReducer, - snapshot: snapshotReducer, - ui: uiReducer, - monitorList: monitorListReducer, - monitorStatus: monitorStatusReducer, - dynamicSettings: dynamicSettingsReducer, - indexPattern: indexPatternReducer, - ping: pingReducer, - pingList: pingListReducer, - ml: mlJobsReducer, - monitorDuration: monitorDurationReducer, - indexStatus: indexStatusReducer, -}); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts deleted file mode 100644 index bc482e2f35c45..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/index_pattern.ts +++ /dev/null @@ -1,42 +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 { handleActions, Action } from 'redux-actions'; -import { getIndexPattern, getIndexPatternSuccess, getIndexPatternFail } from '../actions'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; - -export interface IndexPatternState { - index_pattern: IIndexPattern | null; - errors: any[]; - loading: boolean; -} - -const initialState: IndexPatternState = { - index_pattern: null, - loading: false, - errors: [], -}; - -export const indexPatternReducer = handleActions<IndexPatternState>( - { - [String(getIndexPattern)]: state => ({ - ...state, - loading: true, - }), - - [String(getIndexPatternSuccess)]: (state, action: Action<any>) => ({ - ...state, - loading: false, - index_pattern: { ...action.payload }, - }), - - [String(getIndexPatternFail)]: (state, action: Action<any>) => ({ - ...state, - errors: [...state.errors, action.payload], - loading: false, - }), - }, - initialState -); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts deleted file mode 100644 index 9d59d39634327..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ /dev/null @@ -1,71 +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 { handleActions } from 'redux-actions'; -import { - getExistingMLJobAction, - createMLJobAction, - getAnomalyRecordsAction, - deleteMLJobAction, - resetMLState, - AnomalyRecords, - getMLCapabilitiesAction, -} from '../actions'; -import { getAsyncInitialState, handleAsyncAction } from './utils'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; -import { AsyncInitialState } from './types'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; -import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; -import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; - -export interface MLJobState { - mlJob: AsyncInitialState<JobExistResult>; - createJob: AsyncInitialState<CreateMLJobSuccess>; - deleteJob: AsyncInitialState<DeleteJobResults>; - anomalies: AsyncInitialState<AnomalyRecords>; - mlCapabilities: AsyncInitialState<PrivilegesResponse>; -} - -const initialState: MLJobState = { - mlJob: getAsyncInitialState(), - createJob: getAsyncInitialState(), - deleteJob: getAsyncInitialState(), - anomalies: getAsyncInitialState(), - mlCapabilities: getAsyncInitialState(), -}; - -type Payload = IHttpFetchError; - -export const mlJobsReducer = handleActions<MLJobState>( - { - ...handleAsyncAction<MLJobState, Payload>('mlJob', getExistingMLJobAction), - ...handleAsyncAction<MLJobState, Payload>('mlCapabilities', getMLCapabilitiesAction), - ...handleAsyncAction<MLJobState, Payload>('createJob', createMLJobAction), - ...handleAsyncAction<MLJobState, Payload>('deleteJob', deleteMLJobAction), - ...handleAsyncAction<MLJobState, Payload>('anomalies', getAnomalyRecordsAction), - ...{ - [String(resetMLState)]: state => ({ - ...state, - mlJob: { - loading: false, - data: null, - error: null, - }, - createJob: { - data: null, - error: null, - loading: false, - }, - deleteJob: { - data: null, - error: null, - loading: false, - }, - }), - }, - }, - initialState -); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts deleted file mode 100644 index 88995a2f5dd70..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/types.ts +++ /dev/null @@ -1,13 +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 { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; - -export interface AsyncInitialState<ReduceStateType> { - data: ReduceStateType | null; - loading: boolean; - error?: IHttpFetchError | null; -} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts deleted file mode 100644 index d7a7f237c1154..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/utils.ts +++ /dev/null @@ -1,50 +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 { Action } from 'redux-actions'; -import { AsyncAction } from '../actions/types'; - -export function handleAsyncAction<ReducerState, Payload>( - storeKey: string, - asyncAction: AsyncAction<any, any> -) { - return { - [String(asyncAction.get)]: (state: ReducerState) => ({ - ...state, - [storeKey]: { - ...(state as any)[storeKey], - loading: true, - }, - }), - - [String(asyncAction.success)]: (state: ReducerState, action: Action<any>) => ({ - ...state, - [storeKey]: { - ...(state as any)[storeKey], - data: action.payload === null ? action.payload : { ...action.payload }, - loading: false, - }, - }), - - [String(asyncAction.fail)]: (state: ReducerState, action: Action<any>) => ({ - ...state, - [storeKey]: { - ...(state as any)[storeKey], - data: null, - error: action.payload, - loading: false, - }, - }), - }; -} - -export function getAsyncInitialState(initialData = null) { - return { - data: initialData, - loading: false, - error: null, - }; -} diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts deleted file mode 100644 index 2b7c04178e9b4..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ /dev/null @@ -1,116 +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 { getBasePath, isIntegrationsPopupOpen } from '../index'; -import { AppState } from '../../../state'; - -describe('state selectors', () => { - const state: AppState = { - overviewFilters: { - filters: { - locations: [], - ports: [], - schemes: [], - tags: [], - }, - errors: [], - loading: false, - }, - dynamicSettings: { - loading: false, - }, - monitor: { - monitorDetailsList: [], - monitorLocationsList: new Map(), - loading: false, - errors: [], - }, - snapshot: { - count: { - up: 2, - down: 0, - total: 2, - }, - errors: [], - loading: false, - }, - ui: { - alertFlyoutVisible: false, - basePath: 'yyz', - esKuery: '', - integrationsPopoverOpen: null, - lastRefresh: 125, - }, - monitorStatus: { - status: null, - loading: false, - }, - indexPattern: { - index_pattern: null, - loading: false, - errors: [], - }, - ping: { - pingHistogram: null, - loading: false, - errors: [], - }, - pingList: { - loading: false, - pingList: { - total: 0, - locations: [], - pings: [], - }, - }, - monitorDuration: { - durationLines: null, - loading: false, - errors: [], - }, - monitorList: { - list: { - prevPagePagination: null, - nextPagePagination: null, - summaries: [], - totalSummaryCount: 0, - }, - loading: false, - }, - ml: { - mlJob: { - data: null, - loading: false, - }, - createJob: { data: null, loading: false }, - deleteJob: { data: null, loading: false }, - mlCapabilities: { data: null, loading: false }, - anomalies: { - data: null, - loading: false, - }, - }, - indexStatus: { - indexStatus: { - data: null, - loading: false, - }, - }, - }; - - it('selects base path from state', () => { - expect(getBasePath(state)).toBe('yyz'); - }); - - it('gets integrations popup state', () => { - const integrationsPopupOpen = { - id: 'popup-id', - open: true, - }; - state.ui.integrationsPopoverOpen = integrationsPopupOpen; - expect(isIntegrationsPopupOpen(state)).toBe(integrationsPopupOpen); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts deleted file mode 100644 index 7260c61f44147..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ /dev/null @@ -1,109 +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 { createSelector } from 'reselect'; -import { AppState } from '../../state'; - -// UI Selectors -export const getBasePath = ({ ui: { basePath } }: AppState) => basePath; - -export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: AppState) => - integrationsPopoverOpen; - -// Monitor Selectors -export const monitorDetailsSelector = (state: AppState, summary: any) => { - return state.monitor.monitorDetailsList[summary.monitor_id]; -}; - -export const monitorLocationsSelector = (state: AppState, monitorId: string) => { - return state.monitor.monitorLocationsList?.get(monitorId); -}; - -export const monitorStatusSelector = (state: AppState) => state.monitorStatus.status; - -export const selectDynamicSettings = (state: AppState) => { - return state.dynamicSettings; -}; - -export const selectIndexPattern = ({ indexPattern }: AppState) => { - return { indexPattern: indexPattern.index_pattern, loading: indexPattern.loading }; -}; - -export const selectPingHistogram = ({ ping, ui }: AppState) => { - return { - data: ping.pingHistogram, - loading: ping.loading, - lastRefresh: ui.lastRefresh, - esKuery: ui.esKuery, - }; -}; - -export const selectPingList = ({ pingList, ui: { lastRefresh } }: AppState) => ({ - pings: pingList, - lastRefresh, -}); - -export const snapshotDataSelector = ({ - snapshot: { count, loading }, - ui: { lastRefresh, esKuery }, -}: AppState) => ({ - count, - lastRefresh, - loading, - esKuery, -}); - -const mlCapabilitiesSelector = (state: AppState) => state.ml.mlCapabilities.data; - -export const hasMLFeatureAvailable = createSelector( - mlCapabilitiesSelector, - mlCapabilities => - mlCapabilities?.isPlatinumOrTrialLicense && mlCapabilities?.mlFeatureEnabledInSpace -); - -export const canCreateMLJobSelector = createSelector( - mlCapabilitiesSelector, - mlCapabilities => mlCapabilities?.capabilities.canCreateJob -); - -export const canDeleteMLJobSelector = createSelector( - mlCapabilitiesSelector, - mlCapabilities => mlCapabilities?.capabilities.canDeleteJob -); - -export const hasMLJobSelector = ({ ml }: AppState) => ml.mlJob; - -export const hasNewMLJobSelector = ({ ml }: AppState) => ml.createJob; - -export const isMLJobCreatingSelector = ({ ml }: AppState) => ml.createJob.loading; - -export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading; - -export const isMLJobDeletedSelector = ({ ml }: AppState) => ml.deleteJob; - -export const anomaliesSelector = ({ ml }: AppState) => ml.anomalies.data; - -export const selectDurationLines = ({ monitorDuration }: AppState) => { - return monitorDuration; -}; - -export const selectAlertFlyoutVisibility = ({ ui: { alertFlyoutVisible } }: AppState) => - alertFlyoutVisible; - -export const selectMonitorStatusAlert = ({ indexPattern, overviewFilters, ui }: AppState) => ({ - filters: ui.esKuery, - indexPattern: indexPattern.index_pattern, - locations: overviewFilters.filters.locations, -}); - -export const indexStatusSelector = ({ indexStatus }: AppState) => { - return indexStatus.indexStatus; -}; - -export const monitorListSelector = ({ monitorList, ui: { lastRefresh } }: AppState) => ({ - monitorList, - lastRefresh, -}); diff --git a/x-pack/legacy/plugins/uptime/tsconfig.json b/x-pack/legacy/plugins/uptime/tsconfig.json deleted file mode 100644 index 53425909db3e8..0000000000000 --- a/x-pack/legacy/plugins/uptime/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "exclude": ["**/node_modules/**"], - "paths": { - "react": ["../../../node_modules/@types/react"] - } -} \ No newline at end of file diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js index c830fc9fcd483..99071a2f85e13 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js +++ b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js @@ -6,7 +6,7 @@ import { boomify } from 'boom'; import { get } from 'lodash'; -import { KIBANA_SETTINGS_TYPE } from '../../../../../monitoring/common/constants'; +import { KIBANA_SETTINGS_TYPE } from '../../../../../../../plugins/monitoring/common/constants'; const getClusterUuid = async callCluster => { const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); diff --git a/x-pack/package.json b/x-pack/package.json index a4fdb17f52fe5..dcc9b8c61cb96 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -20,6 +20,11 @@ "build": { "intermediateBuildDirectory": "build/plugin/kibana/x-pack", "oss": false + }, + "clean": { + "extraPatterns": [ + "plugins/*/target" + ] } }, "resolutions": { @@ -85,12 +90,13 @@ "@types/node-fetch": "^2.5.0", "@types/nodemailer": "^6.2.1", "@types/object-hash": "^1.3.0", - "@types/papaparse": "^4.5.11", + "@types/papaparse": "^5.0.3", "@types/pngjs": "^3.3.2", "@types/prop-types": "^15.5.3", "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", "@types/react": "^16.9.19", + "@types/react-beautiful-dnd": "^12.1.1", "@types/react-dom": "^16.9.5", "@types/react-redux": "^7.1.7", "@types/react-router-dom": "^5.1.3", @@ -99,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", @@ -120,7 +127,7 @@ "cheerio": "0.22.0", "commander": "3.0.2", "copy-webpack-plugin": "^5.0.4", - "cypress": "^4.2.0", + "cypress": "^4.4.1", "cypress-multi-reporters": "^1.2.3", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", @@ -182,9 +189,9 @@ "@elastic/apm-rum-react": "^1.1.1", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", - "@elastic/eui": "21.0.1", + "@elastic/eui": "22.3.0", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.2.0", + "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.1.1", "@elastic/numeral": "2.4.0", "@kbn/babel-preset": "1.0.0", @@ -249,7 +256,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", - "handlebars": "4.5.3", + "handlebars": "4.7.6", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", @@ -290,7 +297,7 @@ "oboe": "^2.1.4", "oppsy": "^2.0.0", "p-retry": "^4.2.0", - "papaparse": "^4.6.3", + "papaparse": "^5.2.0", "pdfmake": "^0.1.63", "pluralize": "3.1.0", "pngjs": "3.4.0", @@ -305,7 +312,7 @@ "re-resizable": "^6.1.1", "react": "^16.12.0", "react-apollo": "^2.1.4", - "react-beautiful-dnd": "^8.0.7", + "react-beautiful-dnd": "^12.2.0", "react-datetime": "^2.14.0", "react-dom": "^16.12.0", "react-dropzone": "^4.2.9", @@ -338,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/actions/README.md b/x-pack/plugins/actions/README.md index 82cc09f5e9eca..4c8cc3aa503e6 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -28,7 +28,7 @@ Table of Contents - [RESTful API](#restful-api) - [`POST /api/action`: Create action](#post-apiaction-create-action) - [`DELETE /api/action/{id}`: Delete action](#delete-apiactionid-delete-action) - - [`GET /api/action/_getAll`: Get all actions](#get-apiaction-get-all-actions) + - [`GET /api/action/_getAll`: Get all actions](#get-apiactiongetall-get-all-actions) - [`GET /api/action/{id}`: Get action](#get-apiactionid-get-action) - [`GET /api/action/types`: List action types](#get-apiactiontypes-list-action-types) - [`PUT /api/action/{id}`: Update action](#put-apiactionid-update-action) @@ -64,6 +64,12 @@ Table of Contents - [`config`](#config-6) - [`secrets`](#secrets-6) - [`params`](#params-6) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) + - [Jira](#jira) + - [`config`](#config-7) + - [`secrets`](#secrets-7) + - [`params`](#params-7) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) - [Command Line Utility](#command-line-utility) ## Terminology @@ -143,7 +149,8 @@ This is the primary function for an action type. Whenever the action needs to ex | actionId | The action saved object id that the action type is executing for. | | config | The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | | params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | -| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.<br><br>**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR. | +| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but runs in the context of the user who is calling the action when security is enabled. | +| services.getScopedCallCluster | This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who is calling the action when security is enabled. This must only be called with instances of CallCluster provided by core. | | services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | | services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | @@ -263,7 +270,7 @@ Kibana ships with a set of built-in action types: | Type | Id | Description | | ------------------------- | ------------- | ------------------------------------------------------------------ | -| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` | +| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | | [Email](#email) | `.email` | Sends an email using SMTP | | [Slack](#slack) | `.slack` | Posts a message to a slack channel | | [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | @@ -482,13 +489,59 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `params` +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (pushToService)` + | Property | Description | Type | | ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | | caseId | The case id | string | | title | The title of the case | string _(optional)_ | | description | The description of the case | string _(optional)_ | | comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| incidentID | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| externalId | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | + +--- + +## Jira + +ID: `.jira` + +The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/platform/rest/v2/) to create and update Jira incidents. + +### `config` + +| Property | Description | Type | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| apiUrl | ServiceNow instance URL. | string | +| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object | + +### `secrets` + +| Property | Description | Type | +| -------- | --------------------------------------- | ------ | +| email | email for HTTP Basic authentication | string | +| apiToken | API token for HTTP Basic authentication | string | + +### `params` + +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | +| caseId | The case id | string | +| title | The title of the case | string _(optional)_ | +| description | The description of the case | string _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | # Command Line Utility diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 61b338d47b9f5..49e8f3e80b14a 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -19,6 +19,8 @@ export interface ActionResult { id: string; actionTypeId: string; name: string; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record<string, any>; isPreconfigured: boolean; } diff --git a/x-pack/plugins/actions/server/action_type_registry.mock.ts b/x-pack/plugins/actions/server/action_type_registry.mock.ts index 6a806d1fa531c..d14d0ca2ddf84 100644 --- a/x-pack/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/plugins/actions/server/action_type_registry.mock.ts @@ -14,6 +14,7 @@ const createActionTypeRegistryMock = () => { list: jest.fn(), ensureActionTypeEnabled: jest.fn(), isActionTypeEnabled: jest.fn(), + isActionExecutable: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 26bd68adfc4b6..3be2f26557079 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -28,6 +28,16 @@ beforeEach(() => { ), actionsConfigUtils: mockedActionsConfig, licenseState: mockedLicenseState, + preconfiguredActions: [ + { + actionTypeId: 'foo', + config: {}, + id: 'my-slack1', + name: 'Slack #xyz', + secrets: {}, + isPreconfigured: true, + }, + ], }; }); @@ -194,6 +204,19 @@ describe('isActionTypeEnabled', () => { expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); }); + test('should call isActionExecutable of the actions config', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('my-slack1', 'foo'); + expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); + }); + + test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has preconfigured connectors', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); + }); + test('should call isLicenseValidForActionType of the license state', async () => { mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); actionTypeRegistry.isActionTypeEnabled('foo'); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index c1d979feacc1d..723982b11e1cc 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; -import { ActionType } from './types'; +import { ActionType, PreConfiguredAction } from './types'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; @@ -17,6 +17,7 @@ export interface ActionTypeRegistryOpts { taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; licenseState: ILicenseState; + preconfiguredActions: PreConfiguredAction[]; } export class ActionTypeRegistry { @@ -25,12 +26,14 @@ export class ActionTypeRegistry { private readonly taskRunnerFactory: TaskRunnerFactory; private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; + private readonly preconfiguredActions: PreConfiguredAction[]; constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; this.taskRunnerFactory = constructorParams.taskRunnerFactory; this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; + this.preconfiguredActions = constructorParams.preconfiguredActions; } /** @@ -58,6 +61,19 @@ export class ActionTypeRegistry { ); } + /** + * Returns true if action type is enabled or it is a preconfigured action type. + */ + public isActionExecutable(actionId: string, actionTypeId: string) { + return ( + this.isActionTypeEnabled(actionTypeId) || + (!this.isActionTypeEnabled(actionTypeId) && + this.preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === actionId + ) !== undefined) + ); + } + /** * Registers an action type to the action type registry */ @@ -81,7 +97,7 @@ export class ActionTypeRegistry { title: actionType.name, type: `actions:${actionType.id}`, maxAttempts: actionType.maxAttempts || 1, - getRetry(attempts: number, error: any) { + getRetry(attempts: number, error: unknown) { if (error instanceof ExecutorError) { return error.retry == null ? false : error.retry; } diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 431bfb1e99c3b..64b43e1ab6bbc 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -7,9 +7,10 @@ import { ActionsClient } from './actions_client'; type ActionsClientContract = PublicMethodsOf<ActionsClient>; +export type ActionsClientMock = jest.Mocked<ActionsClientContract>; const createActionsClientMock = () => { - const mocked: jest.Mocked<ActionsClientContract> = { + const mocked: ActionsClientMock = { create: jest.fn(), get: jest.fn(), delete: jest.fn(), @@ -19,6 +20,8 @@ const createActionsClientMock = () => { return mocked; }; -export const actionsClientMock = { +export const actionsClientMock: { + create: () => ActionsClientMock; +} = { create: createActionsClientMock, }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 955e1569380a5..c96c993fef606 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -44,6 +44,7 @@ beforeEach(() => { ), actionsConfigUtils: actionsConfigMock.create(), licenseState: mockedLicenseState, + preconfiguredActions: [], }; actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionsClient = new ActionsClient({ @@ -221,6 +222,7 @@ describe('create()', () => { ), actionsConfigUtils: localConfigUtils, licenseState: licenseStateMock.create(), + preconfiguredActions: [], }; actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams); @@ -348,9 +350,6 @@ describe('get()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, }); expect(savedObjectsClient.get).not.toHaveBeenCalled(); }); @@ -418,9 +417,6 @@ describe('getAll()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, referencedByCount: 2, }, ]); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 8f73bfb31ea4d..618bc8a85e856 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -135,7 +135,7 @@ export class ActionsClient { id, actionTypeId: result.attributes.actionTypeId as string, name: result.attributes.name as string, - config: result.attributes.config as Record<string, any>, + config: result.attributes.config as Record<string, unknown>, isPreconfigured: false, }; } @@ -152,7 +152,6 @@ export class ActionsClient { id, actionTypeId: preconfiguredActionsList.actionTypeId, name: preconfiguredActionsList.name, - config: preconfiguredActionsList.config, isPreconfigured: true, }; } @@ -184,7 +183,6 @@ export class ActionsClient { id: preconfiguredAction.id, actionTypeId: preconfiguredAction.actionTypeId, name: preconfiguredAction.name, - config: preconfiguredAction.config, isPreconfigured: true, })), ].sort((a, b) => a.name.localeCompare(b.name)); @@ -230,7 +228,7 @@ async function injectExtraFindData( scopedClusterClient: IScopedClusterClient, actionResults: ActionResult[] ): Promise<FindActionResult[]> { - const aggs: Record<string, any> = {}; + const aggs: Record<string, unknown> = {}; for (const actionResult of actionResults) { aggs[actionResult.id] = { filter: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts new file mode 100644 index 0000000000000..6dc8a9cc9af6a --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ExternalServiceApi, + ExternalServiceParams, + PushToServiceResponse, + GetIncidentApiHandlerArgs, + HandshakeApiHandlerArgs, + PushToServiceApiHandlerArgs, +} from './types'; +import { prepareFieldsForTransformation, transformFields, transformComments } from './utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, +}: PushToServiceApiHandlerArgs): Promise<PushToServiceResponse> => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + currentIncident = await externalService.getIncident(externalId); + } + + const fields = prepareFieldsForTransformation({ + params, + mapping, + defaultPipes, + }); + + const incident = transformFields({ + params, + fields, + currentIncident, + }); + + if (updateIncident) { + res = await externalService.updateIncident({ incidentId: externalId, incident }); + } else { + res = await externalService.createIncident({ incident }); + } + + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping.get('comments')?.actionType !== 'nothing' + ) { + const commentsTransformed = transformComments(comments, ['informationAdded']); + + res.comments = []; + for (const currentComment of commentsTransformed) { + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + field: mapping.get('comments')?.target ?? 'comments', + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } + } + + return res; +}; + +export const api: ExternalServiceApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts new file mode 100644 index 0000000000000..1f2bc7f5e8e53 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/constants.ts @@ -0,0 +1,7 @@ +/* + * 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 const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts new file mode 100644 index 0000000000000..33b2ad6d18684 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts @@ -0,0 +1,98 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const MappingActionType = schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), +]); + +export const MapRecordSchema = schema.object({ + source: schema.string(), + target: schema.string(), + actionType: MappingActionType, +}); + +export const CaseConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapRecordSchema), +}); + +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), + casesConfiguration: CaseConfigurationSchema, +}; + +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); + +export const ExternalIncidentServiceSecretConfiguration = { + password: schema.string(), + username: schema.string(), +}; + +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const UserSchema = schema.object({ + fullName: schema.nullable(schema.string()), + username: schema.nullable(schema.string()), +}); + +const EntityInformation = { + createdAt: schema.string(), + createdBy: UserSchema, + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(UserSchema), +}; + +export const EntityInformationSchema = schema.object(EntityInformation); + +export const CommentSchema = schema.object({ + commentId: schema.string(), + comment: schema.string(), + ...EntityInformation, +}); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + caseId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + comments: schema.nullable(schema.arrayOf(CommentSchema)), + externalId: schema.nullable(schema.string()), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getIncident'), + subActionParams: ExecutorSubActionGetIncidentParamsSchema, + }), + schema.object({ + subAction: schema.literal('handshake'), + subActionParams: ExecutorSubActionHandshakeParamsSchema, + }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts new file mode 100644 index 0000000000000..75dcab65ee9f2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { transformers } from './transformers'; + +const { informationCreated, informationUpdated, informationAdded, append } = transformers; + +describe('informationCreated', () => { + test('transforms correctly', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationCreated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (created at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationCreated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (created at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('informationUpdated', () => { + test('transforms correctly', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationUpdated({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (updated at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationUpdated({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (updated at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('informationAdded', () => { + test('transforms correctly', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + }); + expect(res).toEqual({ value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)' }); + }); + + test('transforms correctly without optional fields', () => { + const res = informationAdded({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value (added at by )' }); + }); + + test('returns correctly rest fields', () => { + const res = informationAdded({ + value: 'a value', + date: '2020-04-15T08:19:27.400Z', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'a value (added at 2020-04-15T08:19:27.400Z by elastic)', + previousValue: 'previous value', + }); + }); +}); + +describe('append', () => { + test('transforms correctly', () => { + const res = append({ + value: 'a value', + previousValue: 'previous value', + }); + expect(res).toEqual({ value: 'previous value \r\na value' }); + }); + + test('transforms correctly without optional fields', () => { + const res = append({ + value: 'a value', + }); + expect(res).toEqual({ value: 'a value' }); + }); + + test('returns correctly rest fields', () => { + const res = append({ + value: 'a value', + user: 'elastic', + previousValue: 'previous value', + }); + expect(res).toEqual({ + value: 'previous value \r\na value', + user: 'elastic', + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts new file mode 100644 index 0000000000000..3dca1fd703430 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/transformers.ts @@ -0,0 +1,29 @@ +/* + * 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 { TransformerArgs } from './types'; +import * as i18n from './translations'; + +export type Transformer = (args: TransformerArgs) => TransformerArgs; + +export const transformers: Record<string, Transformer> = { + informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, + ...rest, + }), + informationUpdated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, + ...rest, + }), + informationAdded: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, + ...rest, + }), + append: ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ + value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, + ...rest, + }), +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts new file mode 100644 index 0000000000000..4842728b0e4e7 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/translations.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const API_URL_REQUIRED = i18n.translate('xpack.actions.builtin.case.connectorApiNullError', { + defaultMessage: 'connector [apiUrl] is required', +}); + +export const FIELD_INFORMATION = ( + mode: string, + date: string | undefined, + user: string | undefined +) => { + switch (mode) { + case 'create': + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentCreated', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + case 'update': + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentUpdated', { + values: { date, user }, + defaultMessage: '(updated at {date} by {user})', + }); + case 'add': + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentAdded', { + values: { date, user }, + defaultMessage: '(added at {date} by {user})', + }); + default: + return i18n.translate('xpack.actions.builtin.case.common.externalIncidentDefault', { + values: { date, user }, + defaultMessage: '(created at {date} by {user})', + }); + } +}; + +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.case.configuration.emptyMapping', + { + defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', + } +); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.case.configuration.apiWhitelistError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts new file mode 100644 index 0000000000000..459e9d2b03f92 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -0,0 +1,161 @@ +/* + * 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. + */ + +// This will have to remain `any` until we can extend connectors with generics +// and circular dependencies eliminated. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { TypeOf } from '@kbn/config-schema'; + +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + CaseConfigurationSchema, + MapRecordSchema, + CommentSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, +} from './schema'; + +export interface AnyParams { + [index: string]: string | number | object | undefined | null; +} + +export type ExternalIncidentServiceConfiguration = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type ExternalIncidentServiceSecretConfiguration = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; + +export type ExecutorParams = TypeOf<typeof ExecutorParamsSchema>; +export type ExecutorSubActionPushParams = TypeOf<typeof ExecutorSubActionPushParamsSchema>; + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export type CaseConfiguration = TypeOf<typeof CaseConfigurationSchema>; +export type MapRecord = TypeOf<typeof MapRecordSchema>; +export type Comment = TypeOf<typeof CommentSchema>; + +export interface ExternalServiceConfiguration { + id: string; + name: string; +} + +export interface ExternalServiceCredentials { + config: Record<string, any>; + secrets: Record<string, any>; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: any, configObject: any) => void; + secrets: (configurationUtilities: any, secrets: any) => void; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export interface ExternalServiceParams { + [index: string]: any; +} + +export interface ExternalService { + getIncident: (id: string) => Promise<any>; + createIncident: (params: ExternalServiceParams) => Promise<ExternalServiceIncidentResponse>; + updateIncident: (params: ExternalServiceParams) => Promise<ExternalServiceIncidentResponse>; + createComment: (params: ExternalServiceParams) => Promise<ExternalServiceCommentResponse>; +} + +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalCase: Record<string, any>; +} + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map<string, any>; +} + +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; +} + +export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetIncidentParams; +} + +export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionHandshakeParams; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise<void>; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise<PushToServiceResponse>; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise<void>; +} + +export interface CreateExternalServiceBasicArgs { + api: ExternalServiceApi; + createExternalService: (credentials: ExternalServiceCredentials) => ExternalService; +} + +export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { + config: ExternalServiceConfiguration; + validate: ExternalServiceValidation; + validationSchema: { config: any; secrets: any }; +} + +export interface CreateActionTypeArgs { + configurationUtilities: any; + executor?: any; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface PrepareFieldsForTransformArgs { + params: PushToServiceApiParams; + mapping: Map<string, MapRecord>; + defaultPipes?: string[]; +} + +export interface TransformFieldsArgs { + params: PushToServiceApiParams; + fields: PipedField[]; + currentIncident?: ExternalServiceParams; +} + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts new file mode 100644 index 0000000000000..1e8cc3eda20e5 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts @@ -0,0 +1,576 @@ +/* + * 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 axios from 'axios'; + +import { + normalizeMapping, + buildMap, + mapParams, + prepareFieldsForTransformation, + transformFields, + transformComments, + addTimeZoneToDate, + throwIfNotAlive, + request, + patch, + getErrorMessage, +} from './utils'; + +import { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { Comment, MapRecord, PushToServiceApiParams } from './types'; + +jest.mock('axios'); +const axiosMock = (axios as unknown) as jest.Mock; + +const mapping: MapRecord[] = [ + { source: 'title', target: 'short_description', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'append' }, + { source: 'comments', target: 'comments', actionType: 'append' }, +]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const finalMapping: Map<string, any> = new Map(); + +finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); + +finalMapping.set('description', { + target: 'description', + actionType: 'append', +}); + +finalMapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const maliciousMapping: MapRecord[] = [ + { source: '__proto__', target: 'short_description', actionType: 'nothing' }, + { source: 'description', target: '__proto__', actionType: 'nothing' }, + { source: 'comments', target: 'comments', actionType: 'nothing' }, + { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, +]; + +const fullParams: PushToServiceApiParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + externalCase: { + short_description: 'a title', + description: 'a description', + }, + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'second comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], +}; + +describe('normalizeMapping', () => { + test('remove malicious fields', () => { + const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( + true + ); + }); + + test('remove unsuppported source fields', () => { + const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(normalizedMapping).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + source: 'unsupportedSource', + target: 'comments', + actionType: 'nothing', + }), + ]) + ); + }); +}); + +describe('buildMap', () => { + test('builds sanitized Map', () => { + const finalMap = buildMap(maliciousMapping); + expect(finalMap.get('__proto__')).not.toBeDefined(); + }); + + test('builds Map correct', () => { + const final = buildMap(mapping); + expect(final).toEqual(finalMapping); + }); +}); + +describe('mapParams', () => { + test('maps params correctly', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + + const fields = mapParams(params, finalMapping); + + expect(fields).toEqual({ + short_description: 'Incident title', + description: 'Incident description', + }); + }); + + test('do not add fields not in mapping', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + const fields = mapParams(params, finalMapping); + + const { title, description, ...unexpectedFields } = params; + + expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); + }); +}); + +describe('prepareFieldsForTransformation', () => { + test('prepare fields with defaults', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['informationCreated'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['informationCreated', 'append'], + }, + ]); + }); + + test('prepare fields with default pipes', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['myTestPipe'], + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['myTestPipe'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['myTestPipe', 'append'], + }, + ]); + }); +}); + +describe('transformFields', () => { + test('transform fields for creation correctly', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: fullParams, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('transform fields for update correctly', () => { + const fields = prepareFieldsForTransformation({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, + }, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, + }, + fields, + currentIncident: { + short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', + description: + 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', + }); + }); + + test('add newline character to descripton', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: fullParams, + fields, + currentIncident: { + short_description: 'first title', + description: 'first description', + }, + }); + expect(res.description?.includes('\r\n')).toBe(true); + }); + + test('append username if fullname is undefined when create', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: { + ...fullParams, + createdBy: { fullName: '', username: 'elastic' }, + }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', + }); + }); + + test('append username if fullname is undefined when update', () => { + const fields = prepareFieldsForTransformation({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + username: 'anotherUser', + fullName: 'Another User', + }, + }, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: { + ...fullParams, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { username: 'anotherUser', fullName: '' }, + }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + }); + }); +}); + +describe('transformComments', () => { + test('transform creation comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, ['informationCreated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('transform update comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + fullName: 'Another User', + username: 'anotherUser', + }, + }, + ]; + const res = transformComments(comments, ['informationUpdated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-15T08:34:53.450Z', + updatedBy: { + fullName: 'Another User', + username: 'anotherUser', + }, + }, + ]); + }); + + test('transform added comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('transform comments without fullname', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: '', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by elastic)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: '', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); + + test('adds update user correctly', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (added at 2020-04-13T08:34:53.450Z by Elastic2)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + }, + ]); + }); + + test('adds update user with empty fullname correctly', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: '', username: 'elastic2' }, + }, + ]; + const res = transformComments(comments, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + comment: 'first comment (added at 2020-04-13T08:34:53.450Z by elastic2)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic', username: 'elastic' }, + updatedAt: '2020-04-13T08:34:53.450Z', + updatedBy: { fullName: '', username: 'elastic2' }, + }, + ]); + }); +}); + +describe('addTimeZoneToDate', () => { + test('adds timezone with default', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z'); + expect(date).toBe('2020-04-14T15:01:55.456Z GMT'); + }); + + test('adds timezone correctly', () => { + const date = addTimeZoneToDate('2020-04-14T15:01:55.456Z', 'PST'); + expect(date).toBe('2020-04-14T15:01:55.456Z PST'); + }); +}); + +describe('throwIfNotAlive ', () => { + test('throws correctly when status is invalid', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json'); + }).toThrow('Instance is not alive.'); + }); + + test('throws correctly when content is invalid', () => { + expect(() => { + throwIfNotAlive(200, 'application/html'); + }).toThrow('Instance is not alive.'); + }); + + test('do NOT throws with custom validStatusCodes', async () => { + expect(() => { + throwIfNotAlive(404, 'application/json', [404]); + }).not.toThrow('Instance is not alive.'); + }); +}); + +describe('request', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + }); + + test('it fetch correctly with defaults', async () => { + const res = await request({ axios, url: '/test' }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(res).toEqual({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + }); + }); + + test('it throws correctly', async () => { + axiosMock.mockImplementation(() => ({ + status: 404, + headers: { 'content-type': 'application/json' }, + data: { incidentId: '123' }, + })); + + await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); +}); + +describe('patch', () => { + beforeEach(() => { + axiosMock.mockImplementation(() => ({ + status: 200, + headers: { 'content-type': 'application/json' }, + })); + }); + + test('it fetch correctly', async () => { + await patch({ axios, url: '/test', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + }); +}); + +describe('getErrorMessage', () => { + test('it returns the correct error message', () => { + const msg = getErrorMessage('My connector name', 'An error has occurred'); + expect(msg).toBe('[Action][My connector name]: An error has occurred'); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts new file mode 100644 index 0000000000000..7d69b2791f624 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -0,0 +1,249 @@ +/* + * 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 { curry, flow, get } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { AxiosInstance, Method, AxiosResponse } from 'axios'; + +import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; + +import { ExecutorParamsSchema } from './schema'; + +import { + CreateExternalServiceArgs, + CreateActionTypeArgs, + ExecutorParams, + MapRecord, + AnyParams, + CreateExternalServiceBasicArgs, + PrepareFieldsForTransformArgs, + PipedField, + TransformFieldsArgs, + Comment, + ExecutorSubActionPushParams, +} from './types'; + +import { transformers, Transformer } from './transformers'; + +import { SUPPORTED_SOURCE_FIELDS } from './constants'; + +export const normalizeMapping = (supportedFields: string[], mapping: MapRecord[]): MapRecord[] => { + // Prevent prototype pollution and remove unsupported fields + return mapping.filter( + m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) + ); +}; + +export const buildMap = (mapping: MapRecord[]): Map<string, MapRecord> => { + return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { + const { source, target, actionType } = field; + fieldsMap.set(source, { target, actionType }); + fieldsMap.set(target, { target: source, actionType }); + return fieldsMap; + }, new Map()); +}; + +export const mapParams = ( + params: Partial<ExecutorSubActionPushParams>, + mapping: Map<string, MapRecord> +): AnyParams => { + return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { + const field = mapping.get(curr); + if (field) { + prev[field.target] = get(params, curr); + } + return prev; + }, {}); +}; + +export const createConnectorExecutor = ({ + api, + createExternalService, +}: CreateExternalServiceBasicArgs) => async ( + execOptions: ActionTypeExecutorOptions +): Promise<ActionTypeExecutorResult> => { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data = {}; + + const res: Pick<ActionTypeExecutorResult, 'status'> & + Pick<ActionTypeExecutorResult, 'actionId'> = { + status: 'ok', + actionId, + }; + + const externalService = createExternalService({ + config, + secrets, + }); + + if (!api[subAction]) { + throw new Error('[Action][ExternalService] Unsupported subAction type.'); + } + + if (subAction !== 'pushToService') { + throw new Error('[Action][ExternalService] subAction not implemented.'); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + const { comments, externalId, ...restParams } = pushToServiceParams; + + const mapping = buildMap(config.casesConfiguration.mapping); + const externalCase = mapParams(restParams, mapping); + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalCase }, + }); + } + + return { + ...res, + data, + }; +}; + +export const createConnector = ({ + api, + config, + validate, + createExternalService, + validationSchema, +}: CreateExternalServiceArgs) => { + return ({ + configurationUtilities, + executor = createConnectorExecutor({ api, createExternalService }), + }: CreateActionTypeArgs): ActionType => ({ + id: config.id, + name: config.name, + minimumLicenseRequired: 'platinum', + validate: { + config: schema.object(validationSchema.config, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(validationSchema.secrets, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor, + }); +}; + +export const throwIfNotAlive = ( + status: number, + contentType: string, + validStatusCodes: number[] = [200, 201, 204] +) => { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('Instance is not alive.'); + } +}; + +export const request = async <T = unknown>({ + axios, + url, + method = 'get', + data, +}: { + axios: AxiosInstance; + url: string; + method?: Method; + data?: T; +}): Promise<AxiosResponse> => { + const res = await axios(url, { method, data: data ?? {} }); + throwIfNotAlive(res.status, res.headers['content-type']); + return res; +}; + +export const patch = async <T = unknown>({ + axios, + url, + data, +}: { + axios: AxiosInstance; + url: string; + data: T; +}): Promise<AxiosResponse> => { + return request({ + axios, + url, + method: 'patch', + data, + }); +}; + +export const addTimeZoneToDate = (date: string, timezone = 'GMT'): string => { + return `${date} ${timezone}`; +}; + +export const prepareFieldsForTransformation = ({ + params, + mapping, + defaultPipes = ['informationCreated'], +}: PrepareFieldsForTransformArgs): PipedField[] => { + return Object.keys(params.externalCase) + .filter(p => mapping.get(p)?.actionType != null && mapping.get(p)?.actionType !== 'nothing') + .map(p => { + const actionType = mapping.get(p)?.actionType ?? 'nothing'; + return { + key: p, + value: params.externalCase[p], + actionType, + pipes: actionType === 'append' ? [...defaultPipes, 'append'] : defaultPipes, + }; + }); +}; + +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Record<string, string> => { + return fields.reduce((prev, cur) => { + const transform = flow<Transformer>(...cur.pipes.map(p => transformers[p])); + return { + ...prev, + [cur.key]: transform({ + value: cur.value, + date: params.updatedAt ?? params.createdAt, + user: + (params.updatedBy != null + ? params.updatedBy.fullName + ? params.updatedBy.fullName + : params.updatedBy.username + : params.createdBy.fullName + ? params.createdBy.fullName + : params.createdBy.username) ?? '', + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value, + }; + }, {}); +}; + +export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { + return comments.map(c => ({ + ...c, + comment: flow<Transformer>(...pipes.map(p => transformers[p]))({ + value: c.comment, + date: c.updatedAt ?? c.createdAt, + user: + (c.updatedBy != null + ? c.updatedBy.fullName + ? c.updatedBy.fullName + : c.updatedBy.username + : c.createdBy.fullName + ? c.createdBy.fullName + : c.createdBy.username) ?? '', + }).value, + })); +}; + +export const getErrorMessage = (connector: string, msg: string) => { + return `[Action][${connector}]: ${msg}`; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts new file mode 100644 index 0000000000000..80e301e5be082 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts @@ -0,0 +1,35 @@ +/* + * 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 { isEmpty } from 'lodash'; + +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ExternalIncidentServiceConfiguration +) => { + try { + if (isEmpty(configObject.casesConfiguration.mapping)) { + return i18n.MAPPING_EMPTY; + } + + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ExternalIncidentServiceSecretConfiguration +) => {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 658f8f3fd8cf9..18b434e980eb9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -9,13 +9,13 @@ jest.mock('./lib/send_email', () => ({ })); import { Logger } from '../../../../../src/core/server'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { ActionType, ActionTypeExecutorOptions } from '../types'; import { actionsConfigMock } from '../actions_config.mock'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { createActionTypeRegistry } from './index.test'; import { sendEmail } from './lib/send_email'; +import { actionsMock } from '../mocks'; import { ActionParamsType, ActionTypeConfigType, @@ -26,13 +26,8 @@ import { const sendEmailMock = sendEmail as jest.Mock; const ACTION_TYPE_ID = '.email'; -const NO_OP_FN = () => {}; -const services = { - log: NO_OP_FN, - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; @@ -52,7 +47,7 @@ describe('actionTypeRegistry.get() works', () => { describe('config validation', () => { test('config validation succeeds when config is valid', () => { - const config: Record<string, any> = { + const config: Record<string, unknown> = { service: 'gmail', from: 'bob@example.com', }; @@ -74,7 +69,7 @@ describe('config validation', () => { }); test('config validation fails when config is not valid', () => { - const baseConfig: Record<string, any> = { + const baseConfig: Record<string, unknown> = { from: 'bob@example.com', }; @@ -177,7 +172,7 @@ describe('config validation', () => { describe('secrets validation', () => { test('secrets validation succeeds when secrets is valid', () => { - const secrets: Record<string, any> = { + const secrets: Record<string, unknown> = { user: 'bob', password: 'supersecret', }; @@ -185,7 +180,7 @@ describe('secrets validation', () => { }); test('secrets validation succeeds when secrets props are null/undefined', () => { - const secrets: Record<string, any> = { + const secrets: Record<string, unknown> = { user: null, password: null, }; @@ -197,7 +192,7 @@ describe('secrets validation', () => { describe('params validation', () => { test('params validation succeeds when params is valid', () => { - const params: Record<string, any> = { + const params: Record<string, unknown> = { to: ['bob@example.com'], subject: 'this is a test', message: 'this is the message', diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index ca8d089ad2946..7ddb123a4d780 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -30,10 +30,10 @@ const ConfigSchema = schema.object(ConfigSchemaProps); function validateConfig( configurationUtilities: ActionsConfigurationUtilities, - configObject: any + configObject: unknown ): string | void { // avoids circular reference ... - const config: ActionTypeConfigType = configObject; + const config = configObject as ActionTypeConfigType; // Make sure service is set, or if not, both host/port must be set. // If service is set, host/port are ignored, when the email is sent. @@ -95,9 +95,9 @@ const ParamsSchema = schema.object( } ); -function validateParams(paramsObject: any): string | void { +function validateParams(paramsObject: unknown): string | void { // avoids circular reference ... - const params: ActionParamsType = paramsObject; + const params = paramsObject as ActionParamsType; const { to, cc, bcc } = params; const addrs = to.length + cc.length + bcc.length; diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index ec495aed7675a..be60f4c2f28af 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -10,18 +10,13 @@ jest.mock('./lib/send_email', () => ({ import { ActionType, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from './index.test'; import { ActionParamsType, ActionTypeConfigType } from './es_index'; +import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.index'; -const NO_OP_FN = () => {}; -const services = { - log: NO_OP_FN, - callCluster: jest.fn(), - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services = actionsMock.createServices(); let actionType: ActionType; @@ -43,7 +38,7 @@ describe('actionTypeRegistry.get() works', () => { describe('config validation', () => { test('config validation succeeds when config is valid', () => { - const config: Record<string, any> = { + const config: Record<string, unknown> = { index: 'testing-123', refresh: false, }; @@ -97,7 +92,7 @@ describe('config validation', () => { }); test('config validation fails when config is not valid', () => { - const baseConfig: Record<string, any> = { + const baseConfig: Record<string, unknown> = { indeX: 'bob', }; @@ -111,7 +106,7 @@ describe('config validation', () => { describe('params validation', () => { test('params validation succeeds when params is valid', () => { - const params: Record<string, any> = { + const params: Record<string, unknown> = { documents: [{ rando: 'thing' }], }; expect(validateParams(actionType, params)).toMatchInlineSnapshot(` @@ -196,9 +191,9 @@ describe('execute()', () => { await actionType.executor(executorOptions); const calls = services.callCluster.mock.calls; - const timeValue = calls[0][1].body[1].field_to_use_for_time; + const timeValue = calls[0][1]?.body[1].field_to_use_for_time; expect(timeValue).toBeInstanceOf(Date); - delete calls[0][1].body[1].field_to_use_for_time; + delete calls[0][1]?.body[1].field_to_use_for_time; expect(calls).toMatchInlineSnapshot(` Array [ Array [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index ff7b27b3f51fc..899684367d52d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -72,13 +72,12 @@ async function executor( bulkBody.push(document); } - const bulkParams: any = { + const bulkParams = { index, body: bulkBody, + refresh: config.refresh, }; - bulkParams.refresh = config.refresh; - let result; try { result = await services.callCluster('bulk', bulkParams); diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index ac21905ede11c..9150633f06117 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -27,6 +27,7 @@ export function createActionTypeRegistry(): { ), actionsConfigUtils: actionsConfigMock.create(), licenseState: licenseStateMock.create(), + preconfiguredActions: [], }); registerBuiltInActionTypes({ logger, diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index a92a279d08439..6ba4d7cfc7de0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -12,9 +12,10 @@ import { getActionType as getEmailActionType } from './email'; import { getActionType as getIndexActionType } from './es_index'; import { getActionType as getPagerDutyActionType } from './pagerduty'; import { getActionType as getServerLogActionType } from './server_log'; -import { getActionType as getServiceNowActionType } from './servicenow'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; +import { getActionType as getServiceNowActionType } from './servicenow'; +import { getActionType as getJiraActionType } from './jira'; export function registerBuiltInActionTypes({ actionsConfigUtils: configurationUtilities, @@ -29,7 +30,8 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); - actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getServiceNowActionType({ configurationUtilities })); + actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts new file mode 100644 index 0000000000000..bcfb82077d286 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -0,0 +1,517 @@ +/* + * 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 { api } from '../case/api'; +import { externalServiceMock, mapping, apiParams } from './mocks'; +import { ExternalService } from '../case/types'; + +describe('api', () => { + let externalService: jest.Mocked<ExternalService>; + + beforeEach(() => { + externalService = externalServiceMock.create(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('pushToService', () => { + describe('create incident', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + ], + }); + }); + + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: + 'Incident description (created at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (created at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ externalService, mapping, params: apiParams }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + }, + ], + }); + }); + + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-04-27T10:59:46.202Z by Elastic User)', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'description from jira \r\nIncident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: {}, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: 'Incident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + description: + 'Incident description (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + summary: + 'title from jira \r\nIncident title (updated at 2020-04-27T10:59:46.202Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'summary', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('summary', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.createComment).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts new file mode 100644 index 0000000000000..3db66e5884af4 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -0,0 +1,7 @@ +/* + * 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 { api } from '../case/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts new file mode 100644 index 0000000000000..7e415109f1bd9 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExternalServiceConfiguration } from '../case/types'; +import * as i18n from './translations'; + +export const config: ExternalServiceConfiguration = { + id: '.jira', + name: i18n.NAME, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts new file mode 100644 index 0000000000000..a2d7bb5930a75 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createConnector } from '../case/utils'; + +import { api } from './api'; +import { config } from './config'; +import { validate } from './validators'; +import { createExternalService } from './service'; +import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; + +export const getActionType = createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: JiraPublicConfiguration, + secrets: JiraSecretConfiguration, + }, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts new file mode 100644 index 0000000000000..3ae0e9db36de0 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -0,0 +1,124 @@ +/* + * 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 { + ExternalService, + PushToServiceApiParams, + ExecutorSubActionPushParams, + MapRecord, +} from '../case/types'; + +const createMock = (): jest.Mocked<ExternalService> => { + const service = { + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + key: 'CK-1', + summary: 'title from jira', + description: 'description from jira', + created: '2020-04-27T10:59:46.202Z', + updated: '2020-04-27T10:59:46.202Z', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'CK-1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }) + ), + createComment: jest.fn(), + }; + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '1', + }) + ); + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-2', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '2', + }) + ); + + return service; +}; + +const externalServiceMock = { + create: createMock, +}; + +const mapping: Map<string, Partial<MapRecord>> = new Map(); + +mapping.set('title', { + target: 'summary', + actionType: 'overwrite', +}); + +mapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +mapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +mapping.set('summary', { + target: 'title', + actionType: 'overwrite', +}); + +const executorParams: ExecutorSubActionPushParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + externalId: 'incident-3', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + ], +}; + +const apiParams: PushToServiceApiParams = { + ...executorParams, + externalCase: { summary: 'Incident title', description: 'Incident description' }, +}; + +export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts new file mode 100644 index 0000000000000..9c831e75d91c1 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { ExternalIncidentServiceConfiguration } from '../case/schema'; + +export const JiraPublicConfiguration = { + projectKey: schema.string(), + ...ExternalIncidentServiceConfiguration, +}; + +export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration); + +export const JiraSecretConfiguration = { + email: schema.string(), + apiToken: schema.string(), +}; + +export const JiraSecretConfigurationSchema = schema.object(JiraSecretConfiguration); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts new file mode 100644 index 0000000000000..b9225b043d526 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -0,0 +1,297 @@ +/* + * 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 axios from 'axios'; + +import { createExternalService } from './service'; +import * as utils from '../case/utils'; +import { ExternalService } from '../case/types'; + +jest.mock('axios'); +jest.mock('../case/utils', () => { + const originalUtils = jest.requireActual('../case/utils'); + return { + ...originalUtils, + request: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; + +describe('Jira service', () => { + let service: ExternalService; + + beforeAll(() => { + service = createExternalService({ + config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService({ + config: { apiUrl: null, projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without projectKey', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com', projectKey: null }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without username', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: 'elastic@elastic.com' }, + }) + ).toThrow(); + }); + + test('throws without password', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: undefined }, + }) + ).toThrow(); + }); + }); + + describe('getIncident', () => { + test('it returns the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + const res = await service.getIncident('1'); + expect(res).toEqual({ id: '1', key: 'CK-1', summary: 'title', description: 'description' }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { id: '1', key: 'CK-1' }, + })); + + await service.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + expect(service.getIncident('1')).rejects.toThrow( + 'Unable to get incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createIncident', () => { + test('it creates the incident correctly', async () => { + // The response from Jira when creating an issue contains only the key and the id. + // The service makes two calls when creating an issue. One to create and one to get + // the created incident with all the necessary fields. + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + })); + + const res = await service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { created: '2020-04-27T10:59:46.202Z' }, + }, + })); + + await service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + method: 'post', + data: { + fields: { + summary: 'title', + description: 'desc', + project: { key: 'CK' }, + issuetype: { name: 'Task' }, + }, + }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createIncident({ + incident: { summary: 'title', description: 'desc' }, + }) + ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred'); + }); + }); + + describe('updateIncident', () => { + test('it updates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + })); + + const res = await service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + fields: { updated: '2020-04-27T10:59:46.202Z' }, + }, + })); + + await service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + method: 'put', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + data: { fields: { summary: 'title', description: 'desc' } }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.updateIncident({ + incidentId: '1', + incident: { summary: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createComment', () => { + test('it creates the comment correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + })); + + const res = await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }); + + expect(res).toEqual({ + commentId: 'comment-1', + pushedDate: '2020-04-27T10:59:46.202Z', + externalCommentId: '1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + id: '1', + key: 'CK-1', + created: '2020-04-27T10:59:46.202Z', + }, + })); + + await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'my_field', + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + method: 'post', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', + data: { body: 'comment' }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }) + ).rejects.toThrow( + '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred' + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts new file mode 100644 index 0000000000000..ff22b8368e7dd --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -0,0 +1,156 @@ +/* + * 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 axios from 'axios'; + +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, + CreateIncidentRequest, + UpdateIncidentRequest, + CreateCommentRequest, +} from './types'; + +import * as i18n from './translations'; +import { getErrorMessage, request } from '../case/utils'; + +const VERSION = '2'; +const BASE_URL = `rest/api/${VERSION}`; +const INCIDENT_URL = `issue`; +const COMMENT_URL = `comment`; + +const VIEW_INCIDENT_URL = `browse`; + +export const createExternalService = ({ + config, + secrets, +}: ExternalServiceCredentials): ExternalService => { + const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; + const { apiToken, email } = secrets as JiraSecretConfigurationType; + + if (!url || !projectKey || !apiToken || !email) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`; + const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; + const axiosInstance = axios.create({ + auth: { username: email, password: apiToken }, + }); + + const getIncidentViewURL = (key: string) => { + return `${url}/${VIEW_INCIDENT_URL}/${key}`; + }; + + const getCommentsURL = (issueId: string) => { + return commentUrl.replace('{issueId}', issueId); + }; + + const getIncident = async (id: string) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}/${id}`, + }); + + const { fields, ...rest } = res.data; + + return { ...rest, ...fields }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + ); + } + }; + + const createIncident = async ({ incident }: ExternalServiceParams) => { + // The response from Jira when creating an issue contains only the key and the id. + // The function makes two calls when creating an issue. One to create the issue and one to get + // the created issue with all the necessary fields. + try { + const res = await request<CreateIncidentRequest>({ + axios: axiosInstance, + url: `${incidentUrl}`, + method: 'post', + data: { + fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, + }, + }); + + const updatedIncident = await getIncident(res.data.id); + + return { + title: updatedIncident.key, + id: updatedIncident.id, + pushedDate: new Date(updatedIncident.created).toISOString(), + url: getIncidentViewURL(updatedIncident.key), + }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + ); + } + }; + + const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + try { + await request<UpdateIncidentRequest>({ + axios: axiosInstance, + method: 'put', + url: `${incidentUrl}/${incidentId}`, + data: { fields: { ...incident } }, + }); + + const updatedIncident = await getIncident(incidentId); + + return { + title: updatedIncident.key, + id: updatedIncident.id, + pushedDate: new Date(updatedIncident.updated).toISOString(), + url: getIncidentViewURL(updatedIncident.key), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + try { + const res = await request<CreateCommentRequest>({ + axios: axiosInstance, + method: 'post', + url: getCommentsURL(incidentId), + data: { body: comment.comment }, + }); + + return { + commentId: comment.commentId, + externalCommentId: res.data.id, + pushedDate: new Date(res.data.created).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + return { + getIncident, + createIncident, + updateIncident, + createComment, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts new file mode 100644 index 0000000000000..dae0d75952e11 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts @@ -0,0 +1,11 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', { + defaultMessage: 'Jira', +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts new file mode 100644 index 0000000000000..8d9c6b92abb3b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; +import { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './schema'; + +export type JiraPublicConfigurationType = TypeOf<typeof JiraPublicConfigurationSchema>; +export type JiraSecretConfigurationType = TypeOf<typeof JiraSecretConfigurationSchema>; + +interface CreateIncidentBasicRequestArgs { + summary: string; + description: string; +} +interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs { + project: { key: string }; + issuetype: { name: string }; +} + +export interface CreateIncidentRequest { + fields: CreateIncidentRequestArgs; +} + +export interface UpdateIncidentRequest { + fields: Partial<CreateIncidentBasicRequestArgs>; +} + +export interface CreateCommentRequest { + body: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts new file mode 100644 index 0000000000000..7226071392bc6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; +import { ExternalServiceValidation } from '../case/types'; + +export const validate: ExternalServiceValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts index cc9d36ff86342..92f88ebe0be22 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts @@ -9,7 +9,7 @@ import { Services } from '../../types'; interface PostPagerdutyOptions { apiUrl: string; - data: any; + data: unknown; headers: Record<string, string>; services: Services; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 42160dc2fc22b..a02f79a49e8e2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -63,12 +63,15 @@ describe('send_email module', () => { }); test('handles unauthenticated email using not secure host/port', async () => { - const sendEmailOptions = getSendEmailOptions(); + const sendEmailOptions = getSendEmailOptions({ + transport: { + host: 'example.com', + port: 1025, + }, + }); delete sendEmailOptions.transport.service; delete sendEmailOptions.transport.user; delete sendEmailOptions.transport.password; - sendEmailOptions.transport.host = 'example.com'; - sendEmailOptions.transport.port = 1025; const result = await sendEmail(mockLogger, sendEmailOptions); expect(result).toBe(sendMailMockResult); expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -105,13 +108,17 @@ describe('send_email module', () => { }); test('handles unauthenticated email using secure host/port', async () => { - const sendEmailOptions = getSendEmailOptions(); + const sendEmailOptions = getSendEmailOptions({ + transport: { + host: 'example.com', + port: 1025, + secure: true, + }, + }); delete sendEmailOptions.transport.service; delete sendEmailOptions.transport.user; delete sendEmailOptions.transport.password; - sendEmailOptions.transport.host = 'example.com'; - sendEmailOptions.transport.port = 1025; - sendEmailOptions.transport.secure = true; + const result = await sendEmail(mockLogger, sendEmailOptions); expect(result).toBe(sendMailMockResult); expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -154,19 +161,22 @@ describe('send_email module', () => { }); }); -function getSendEmailOptions(): any { +function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}) { return { content: { + ...content, message: 'a message', subject: 'a subject', }, routing: { + ...routing, from: 'fred@example.com', to: ['jim@example.com'], cc: ['bob@example.com', 'robert@example.com'], bcc: [], }, transport: { + ...transport, service: 'whatever', user: 'elastic', password: 'changeme', diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index ffbf7485a8b0b..869db34f034ae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -43,13 +43,13 @@ export interface Content { } // send an email -export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise<any> { +export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise<unknown> { const { transport, routing, content } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; - const transportConfig: Record<string, any> = {}; + const transportConfig: Record<string, unknown> = {}; if (user != null && password != null) { transportConfig.auth = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index 514c9759d7b56..1bca7c18e4e1b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -11,20 +11,17 @@ jest.mock('./lib/post_pagerduty', () => ({ import { getActionType } from './pagerduty'; import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { postPagerduty } from './lib/post_pagerduty'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; import { actionsConfigMock } from '../actions_config.mock'; +import { actionsMock } from '../mocks'; const postPagerdutyMock = postPagerduty as jest.Mock; const ACTION_TYPE_ID = '.pagerduty'; -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; @@ -142,6 +139,25 @@ describe('validateParams()', () => { - [eventAction.2]: expected value to equal [acknowledge]" `); }); + + test('should validate and throw error when timestamp has spaces', () => { + const randoDate = new Date('1963-09-23T01:23:45Z').toISOString(); + const timestamp = ` ${randoDate}`; + expect(() => { + validateParams(actionType, { + timestamp, + }); + }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); + }); + + test('should validate and throw error when timestamp is invalid', () => { + const timestamp = `1963-09-55 90:23:45`; + expect(() => { + validateParams(actionType, { + timestamp, + }); + }).toThrowError(`error validating action params: error parsing timestamp "${timestamp}"`); + }); }); describe('execute()', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 2b607d0dd41ba..0c8802060164d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -68,20 +68,27 @@ const ParamsSchema = schema.object( { validate: validateParams } ); -function validateParams(paramsObject: any): string | void { - const params: ActionParamsType = paramsObject; - - const { timestamp } = params; +function validateParams(paramsObject: unknown): string | void { + const { timestamp } = paramsObject as ActionParamsType; if (timestamp != null) { - let date; try { - date = Date.parse(timestamp); + const date = Date.parse(timestamp); + if (isNaN(date)) { + return i18n.translate('xpack.actions.builtin.pagerduty.invalidTimestampErrorMessage', { + defaultMessage: `error parsing timestamp "{timestamp}"`, + values: { + timestamp, + }, + }); + } } catch (err) { - return 'error parsing timestamp: ${err.message}'; - } - - if (isNaN(date)) { - return 'error parsing timestamp'; + return i18n.translate('xpack.actions.builtin.pagerduty.timestampParsingFailedErrorMessage', { + defaultMessage: `error parsing timestamp "{timestamp}": {message}`, + values: { + timestamp, + message: err.message, + }, + }); } } } @@ -210,11 +217,23 @@ async function executor( const AcknowledgeOrResolve = new Set([EVENT_ACTION_ACKNOWLEDGE, EVENT_ACTION_RESOLVE]); -function getBodyForEventAction(actionId: string, params: ActionParamsType): any { +function getBodyForEventAction(actionId: string, params: ActionParamsType): unknown { const eventAction = params.eventAction || EVENT_ACTION_TRIGGER; const dedupKey = params.dedupKey || `action:${actionId}`; - const data: any = { + const data: { + event_action: ActionParamsType['eventAction']; + dedup_key: string; + payload?: { + summary: string; + source: string; + severity: string; + timestamp?: string; + component?: string; + group?: string; + class?: string; + }; + } = { event_action: eventAction, dedup_key: dedupKey, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index bb806f8ae36fc..d5a9c0cc1ccd2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -7,8 +7,8 @@ import { ActionType } from '../types'; import { validateParams } from '../lib'; import { Logger } from '../../../../../src/core/server'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createActionTypeRegistry } from './index.test'; +import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.server-log'; @@ -90,10 +90,7 @@ describe('execute()', () => { const actionId = 'some-id'; await actionType.executor({ actionId, - services: { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), - }, + services: actionsMock.createServices(), params: { message: 'message text here', level: 'info' }, config: {}, secrets: {}, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts deleted file mode 100644 index aa9b1dcfcf239..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ /dev/null @@ -1,850 +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 { - handleCreateIncident, - handleUpdateIncident, - handleIncident, - createComments, -} from './action_handlers'; -import { ServiceNow } from './lib'; -import { Mapping } from './types'; - -jest.mock('./lib'); - -const ServiceNowMock = ServiceNow as jest.Mock; - -const finalMapping: Mapping = new Map(); - -finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', -}); - -finalMapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - -const params = { - caseId: '123', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - incident: { - short_description: 'a title', - description: 'a description', - }, - comments: [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], -}; - -beforeAll(() => { - ServiceNowMock.mockImplementation(() => { - return { - serviceNow: { - getUserID: jest.fn().mockResolvedValue('1234'), - getIncident: jest.fn().mockResolvedValue({ - short_description: 'servicenow title', - description: 'servicenow desc', - }), - createIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }), - updateIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }), - batchCreateComments: jest - .fn() - .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), - }, - }; - }); -}); - -describe('handleIncident', () => { - test('create an incident', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleIncident({ - incidentId: null, - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); - test('update an incident', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleIncident({ - incidentId: '123', - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleCreateIncident', () => { - test('create an incident without comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleCreateIncident({ - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.createIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('create an incident with comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleCreateIncident({ - serviceNow, - params, - comments: params.comments, - mapping: finalMapping, - }); - - expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith({ - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.createIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleUpdateIncident', () => { - test('update an incident without comments', async () => { - const { serviceNow } = new ServiceNowMock(); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params: { - ...params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('update an incident with comments', async () => { - const { serviceNow } = new ServiceNowMock(); - serviceNow.batchCreateComments.mockResolvedValue([ - { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, - ]); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params: { - ...params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - comments: [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-16T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - version: 'WzU3LDFd', - }, - ], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: 'a description (updated at 2020-03-15T08:34:53.450Z by Another User)', - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment (added at 2020-03-16T08:34:53.450Z by Another User)', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-16T08:34:53.450Z', - updatedBy: { - fullName: 'Another User', - username: 'anotherUser', - }, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - comments: [ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: '789', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ], - }); - }); -}); - -describe('handleUpdateIncident: different action types', () => { - test('overwrite & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & append', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'append', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {}); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('overwrite & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('overwrite & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('nothing & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'nothing', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'nothing', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & overwrite', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'overwrite', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('append & nothing', async () => { - const { serviceNow } = new ServiceNowMock(); - finalMapping.set('title', { - target: 'short_description', - actionType: 'append', - }); - - finalMapping.set('description', { - target: 'description', - actionType: 'nothing', - }); - - finalMapping.set('comments', { - target: 'comments', - actionType: 'append', - }); - - finalMapping.set('short_description', { - target: 'title', - actionType: 'append', - }); - - const res = await handleUpdateIncident({ - incidentId: '123', - serviceNow, - params, - comments: [], - mapping: finalMapping, - }); - - expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { - short_description: - 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); -}); - -describe('createComments', () => { - test('create comments correctly', async () => { - const { serviceNow } = new ServiceNowMock(); - serviceNow.batchCreateComments.mockResolvedValue([ - { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, - ]); - - const comments = [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - version: 'WzU3LDFd', - }, - ]; - - const res = await createComments(serviceNow, '123', 'comments', comments); - - expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( - '123', - [ - { - comment: 'first comment', - commentId: '456', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: null, - updatedBy: null, - version: 'WzU3LDFd', - }, - { - comment: 'second comment', - commentId: '789', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { - fullName: 'Elastic User', - username: 'elastic', - }, - version: 'WzU3LDFd', - }, - ], - 'comments' - ); - expect(res).toEqual([ - { - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - { - commentId: '789', - pushedDate: '2020-03-10T12:24:20.000Z', - }, - ]); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts deleted file mode 100644 index fb296089e9ec5..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ /dev/null @@ -1,129 +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 { zipWith } from 'lodash'; -import { CommentResponse } from './lib/types'; -import { - HandlerResponse, - Comment, - SimpleComment, - CreateHandlerArguments, - UpdateHandlerArguments, - IncidentHandlerArguments, -} from './types'; -import { ServiceNow } from './lib'; -import { transformFields, prepareFieldsForTransformation, transformComments } from './helpers'; - -export const createComments = async ( - serviceNow: ServiceNow, - incidentId: string, - key: string, - comments: Comment[] -): Promise<SimpleComment[]> => { - const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key); - - return zipWith(comments, createdComments, (a: Comment, b: CommentResponse) => ({ - commentId: a.commentId, - pushedDate: b.pushedDate, - })); -}; - -export const handleCreateIncident = async ({ - serviceNow, - params, - comments, - mapping, -}: CreateHandlerArguments): Promise<HandlerResponse> => { - const fields = prepareFieldsForTransformation({ - params, - mapping, - }); - - const incident = transformFields({ - params, - fields, - }); - - const createdIncident = await serviceNow.createIncident({ - ...incident, - }); - - const res: HandlerResponse = { ...createdIncident }; - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments').actionType !== 'nothing' - ) { - comments = transformComments(comments, params, ['informationAdded']); - res.comments = [ - ...(await createComments( - serviceNow, - res.incidentId, - mapping.get('comments').target, - comments - )), - ]; - } - - return { ...res }; -}; - -export const handleUpdateIncident = async ({ - incidentId, - serviceNow, - params, - comments, - mapping, -}: UpdateHandlerArguments): Promise<HandlerResponse> => { - const currentIncident = await serviceNow.getIncident(incidentId); - const fields = prepareFieldsForTransformation({ - params, - mapping, - defaultPipes: ['informationUpdated'], - }); - - const incident = transformFields({ - params, - fields, - currentIncident, - }); - - const updatedIncident = await serviceNow.updateIncident(incidentId, { - ...incident, - }); - - const res: HandlerResponse = { ...updatedIncident }; - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments').actionType !== 'nothing' - ) { - comments = transformComments(comments, params, ['informationAdded']); - res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), - ]; - } - - return { ...res }; -}; - -export const handleIncident = async ({ - incidentId, - serviceNow, - params, - comments, - mapping, -}: IncidentHandlerArguments): Promise<HandlerResponse> => { - if (!incidentId) { - return await handleCreateIncident({ serviceNow, params, comments, mapping }); - } else { - return await handleUpdateIncident({ incidentId, serviceNow, params, comments, mapping }); - } -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts new file mode 100644 index 0000000000000..86a8318841271 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -0,0 +1,523 @@ +/* + * 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 { api } from '../case/api'; +import { externalServiceMock, mapping, apiParams } from './mocks'; +import { ExternalService } from '../case/types'; + +describe('api', () => { + let externalService: jest.Mocked<ExternalService>; + + beforeEach(() => { + externalService = externalServiceMock.create(); + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('pushToService', () => { + describe('create incident', () => { + test('it creates an incident', async () => { + const params = { ...apiParams, externalId: null }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('it creates an incident without comments', async () => { + const params = { ...apiParams, externalId: null, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + + test('it calls createIncident correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: + 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('update incident', () => { + test('it updates an incident', async () => { + const res = await api.pushToService({ externalService, mapping, params: apiParams }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('it updates an incident without comments', async () => { + const params = { ...apiParams, comments: [] }; + const res = await api.pushToService({ externalService, mapping, params }); + + expect(res).toEqual({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + + test('it calls updateIncident correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + + test('it calls createComment correctly', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping, params }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-1', + comment: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-2', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + field: 'comments', + }); + }); + }); + + describe('mapping variations', () => { + test('overwrite & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & append', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'append', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'description from servicenow \r\nIncident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: {}, + }); + }); + + test('overwrite & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('overwrite & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'Incident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('nothing & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & overwrite', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('append & nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + short_description: + 'title from servicenow \r\nIncident title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }, + }); + }); + + test('comment nothing', async () => { + mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + mapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + mapping.set('comments', { + target: 'comments', + actionType: 'nothing', + }); + + mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + await api.pushToService({ externalService, mapping, params: apiParams }); + expect(externalService.createComment).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts new file mode 100644 index 0000000000000..3db66e5884af4 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -0,0 +1,7 @@ +/* + * 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 { api } from '../case/api'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts new file mode 100644 index 0000000000000..4ad8108c3b137 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ExternalServiceConfiguration } from '../case/types'; +import * as i18n from './translations'; + +export const config: ExternalServiceConfiguration = { + id: '.servicenow', + name: i18n.NAME, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts deleted file mode 100644 index a0ffd859e14ca..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts +++ /dev/null @@ -1,8 +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. - */ - -export const ACTION_TYPE_ID = '.servicenow'; -export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts deleted file mode 100644 index cbcefe6364e8f..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ /dev/null @@ -1,409 +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 { - normalizeMapping, - buildMap, - mapParams, - appendField, - appendInformationToField, - prepareFieldsForTransformation, - transformFields, - transformComments, -} from './helpers'; -import { mapping, finalMapping } from './mock'; -import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapEntry, Params, Comment } from './types'; - -const maliciousMapping: MapEntry[] = [ - { source: '__proto__', target: 'short_description', actionType: 'nothing' }, - { source: 'description', target: '__proto__', actionType: 'nothing' }, - { source: 'comments', target: 'comments', actionType: 'nothing' }, - { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, -]; - -const fullParams: Params = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - incident: { - short_description: 'a title', - description: 'a description', - }, - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], -}; - -describe('sanitizeMapping', () => { - test('remove malicious fields', () => { - const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); - expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( - true - ); - }); - - test('remove unsuppported source fields', () => { - const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); - expect(normalizedMapping).not.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - source: 'unsupportedSource', - target: 'comments', - actionType: 'nothing', - }), - ]) - ); - }); -}); - -describe('buildMap', () => { - test('builds sanitized Map', () => { - const finalMap = buildMap(maliciousMapping); - expect(finalMap.get('__proto__')).not.toBeDefined(); - }); - - test('builds Map correct', () => { - const final = buildMap(mapping); - expect(final).toEqual(finalMapping); - }); -}); - -describe('mapParams', () => { - test('maps params correctly', () => { - const params = { - caseId: '123', - incidentId: '456', - title: 'Incident title', - description: 'Incident description', - }; - - const fields = mapParams(params, finalMapping); - - expect(fields).toEqual({ - short_description: 'Incident title', - description: 'Incident description', - }); - }); - - test('do not add fields not in mapping', () => { - const params = { - caseId: '123', - incidentId: '456', - title: 'Incident title', - description: 'Incident description', - }; - const fields = mapParams(params, finalMapping); - - const { title, description, ...unexpectedFields } = params; - - expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); - }); -}); - -describe('prepareFieldsForTransformation', () => { - test('prepare fields with defaults', () => { - const res = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - }); - expect(res).toEqual([ - { - key: 'short_description', - value: 'a title', - actionType: 'overwrite', - pipes: ['informationCreated'], - }, - { - key: 'description', - value: 'a description', - actionType: 'append', - pipes: ['informationCreated', 'append'], - }, - ]); - }); - - test('prepare fields with default pipes', () => { - const res = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - defaultPipes: ['myTestPipe'], - }); - expect(res).toEqual([ - { - key: 'short_description', - value: 'a title', - actionType: 'overwrite', - pipes: ['myTestPipe'], - }, - { - key: 'description', - value: 'a description', - actionType: 'append', - pipes: ['myTestPipe', 'append'], - }, - ]); - }); -}); - -describe('transformFields', () => { - test('transform fields for creation correctly', () => { - const fields = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - }); - - const res = transformFields({ - params: fullParams, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - }); - - test('transform fields for update correctly', () => { - const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, - }, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, - }, - fields, - currentIncident: { - short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }, - }); - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', - description: - 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', - }); - }); - - test('add newline character to descripton', () => { - const fields = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields({ - params: fullParams, - fields, - currentIncident: { - short_description: 'first title', - description: 'first description', - }, - }); - expect(res.description?.includes('\r\n')).toBe(true); - }); - - test('append username if fullname is undefined when create', () => { - const fields = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - }); - - const res = transformFields({ - params: { ...fullParams, createdBy: { fullName: null, username: 'elastic' } }, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', - }); - }); - - test('append username if fullname is undefined when update', () => { - const fields = prepareFieldsForTransformation({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: 'Another User' }, - }, - mapping: finalMapping, - defaultPipes: ['informationUpdated'], - }); - - const res = transformFields({ - params: { - ...fullParams, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: null }, - }, - fields, - }); - - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', - description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', - }); - }); -}); - -describe('appendField', () => { - test('prefix correctly', () => { - expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' })); - }); - - test('suffix correctly', () => { - expect('my_value my_suffix').toEqual(appendField({ value: 'my_value', suffix: 'my_suffix' })); - }); - - test('prefix and suffix correctly', () => { - expect('my_prefixmy_value my_suffix').toEqual( - appendField({ value: 'my_value', prefix: 'my_prefix', suffix: 'my_suffix' }) - ); - }); -}); - -describe('appendInformationToField', () => { - test('creation mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'create', - }); - expect(res).toEqual('my value (created at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); - - test('update mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'update', - }); - expect(res).toEqual('my value (updated at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); - - test('add mode', () => { - const res = appendInformationToField({ - value: 'my value', - user: 'Elastic Test User', - date: '2020-03-13T08:34:53.450Z', - mode: 'add', - }); - expect(res).toEqual('my value (added at 2020-03-13T08:34:53.450Z by Elastic Test User)'); - }); -}); - -describe('transformComments', () => { - test('transform creation comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = transformComments(comments, fullParams, ['informationCreated']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]); - }); - - test('transform update comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - ]; - const res = transformComments(comments, fullParams, ['informationUpdated']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment (updated at 2020-03-15T08:34:53.450Z by Another User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - ]); - }); - test('transform added comments', () => { - const comments: Comment[] = [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = transformComments(comments, fullParams, ['informationAdded']); - expect(res).toEqual([ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts deleted file mode 100644 index 750fda93b60d6..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ /dev/null @@ -1,125 +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 { flow } from 'lodash'; - -import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { - MapEntry, - Mapping, - AppendFieldArgs, - AppendInformationFieldArgs, - Params, - Comment, - TransformFieldsArgs, - PipedField, - PrepareFieldsForTransformArgs, - KeyAny, -} from './types'; -import { Incident } from './lib/types'; - -import * as transformers from './transformers'; -import * as i18n from './translations'; - -export const normalizeMapping = (supportedFields: string[], mapping: MapEntry[]): MapEntry[] => { - // Prevent prototype pollution and remove unsupported fields - return mapping.filter( - m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) - ); -}; - -export const buildMap = (mapping: MapEntry[]): Mapping => { - return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { - const { source, target, actionType } = field; - fieldsMap.set(source, { target, actionType }); - fieldsMap.set(target, { target: source, actionType }); - return fieldsMap; - }, new Map()); -}; - -export const mapParams = (params: any, mapping: Mapping) => { - return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { - const field = mapping.get(curr); - if (field) { - prev[field.target] = params[curr]; - } - return prev; - }, {}); -}; - -export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs): string => { - return `${prefix}${value} ${suffix}`; -}; - -const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution exists. - -export const prepareFieldsForTransformation = ({ - params, - mapping, - defaultPipes = ['informationCreated'], -}: PrepareFieldsForTransformArgs): PipedField[] => { - return Object.keys(params.incident) - .filter(p => mapping.get(p).actionType !== 'nothing') - .map(p => ({ - key: p, - value: params.incident[p], - actionType: mapping.get(p).actionType, - pipes: [...defaultPipes], - })) - .map(p => ({ - ...p, - pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, - })); -}; - -export const transformFields = ({ - params, - fields, - currentIncident, -}: TransformFieldsArgs): Incident => { - return fields.reduce((prev: Incident, cur) => { - const transform = flow(...cur.pipes.map(p => t[p])); - prev[cur.key] = transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: - params.updatedBy != null - ? params.updatedBy.fullName ?? params.updatedBy.username - : params.createdBy.fullName ?? params.createdBy.username, - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value; - return prev; - }, {} as Incident); -}; - -export const appendInformationToField = ({ - value, - user, - date, - mode = 'create', -}: AppendInformationFieldArgs): string => { - return appendField({ - value, - suffix: i18n.FIELD_INFORMATION(mode, date, user), - }); -}; - -export const transformComments = ( - comments: Comment[], - params: Params, - pipes: string[] -): Comment[] => { - return comments.map(c => ({ - ...c, - comment: flow(...pipes.map(p => t[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: - c.updatedBy != null - ? c.updatedBy.fullName ?? c.updatedBy.username - : c.createdBy.fullName ?? c.createdBy.username, - }).value, - })); -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts deleted file mode 100644 index 1a23354e6490d..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ /dev/null @@ -1,271 +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 { getActionType } from '.'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; -import { validateConfig, validateSecrets, validateParams } from '../../lib'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { createActionTypeRegistry } from '../index.test'; -import { actionsConfigMock } from '../../actions_config.mock'; - -import { ACTION_TYPE_ID } from './constants'; -import * as i18n from './translations'; - -import { handleIncident } from './action_handlers'; -import { incidentResponse } from './mock'; - -jest.mock('./action_handlers'); - -const handleIncidentMock = handleIncident as jest.Mock; - -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; - -let actionType: ActionType; - -const mockOptions = { - name: 'servicenow-connector', - actionTypeId: '.servicenow', - secrets: { - username: 'secret-username', - password: 'secret-password', - }, - config: { - apiUrl: 'https://service-now.com', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'work_notes', - actionType: 'append', - }, - ], - }, - }, - params: { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', - title: 'Incident title', - description: 'Incident description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], - }, -}; - -beforeAll(() => { - const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); -}); - -describe('get()', () => { - test('should return correct action type', () => { - expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual(i18n.NAME); - }); -}); - -describe('validateConfig()', () => { - test('should validate and pass when config is valid', () => { - const { config } = mockOptions; - expect(validateConfig(actionType, config)).toEqual(config); - }); - - test('should validate and throw error when config is invalid', () => { - expect(() => { - validateConfig(actionType, { shouldNotBeHere: true }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"` - ); - }); - - test('should validate and pass when the servicenow url is whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...actionsConfigMock.create(), - ensureWhitelistedUri: url => { - expect(url).toEqual(mockOptions.config.apiUrl); - }, - }, - }); - - expect(validateConfig(actionType, mockOptions.config)).toEqual(mockOptions.config); - }); - - test('config validation returns an error if the specified URL isnt whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...actionsConfigMock.create(), - ensureWhitelistedUri: _ => { - throw new Error(`target url is not whitelisted`); - }, - }, - }); - - expect(() => { - validateConfig(actionType, mockOptions.config); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` - ); - }); -}); - -describe('validateSecrets()', () => { - test('should validate and pass when secrets is valid', () => { - const { secrets } = mockOptions; - expect(validateSecrets(actionType, secrets)).toEqual(secrets); - }); - - test('should validate and throw error when secrets is invalid', () => { - expect(() => { - validateSecrets(actionType, { username: false }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"` - ); - - expect(() => { - validateSecrets(actionType, { username: false, password: 'hello' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"` - ); - }); -}); - -describe('validateParams()', () => { - test('should validate and pass when params is valid', () => { - const { params } = mockOptions; - expect(validateParams(actionType, params)).toEqual(params); - }); - - test('should validate and throw error when params is invalid', () => { - expect(() => { - validateParams(actionType, {}); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [caseId]: expected value of type [string] but got [undefined]"` - ); - }); -}); - -describe('execute()', () => { - beforeEach(() => { - handleIncidentMock.mockReset(); - }); - - test('should create an incident', async () => { - const actionId = 'some-id'; - const { incidentId, ...rest } = mockOptions.params; - - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...rest }, - secrets: mockOptions.secrets, - services, - }; - - handleIncidentMock.mockImplementation(() => incidentResponse); - - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); - }); - - test('should throw an error when failed to create incident', async () => { - expect.assertions(1); - const { incidentId, ...rest } = mockOptions.params; - - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { ...rest }, - secrets: mockOptions.secrets, - services, - }; - const errorMessage = 'Failed to create incident'; - - handleIncidentMock.mockImplementation(() => { - throw new Error(errorMessage); - }); - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); - - test('should update an incident', async () => { - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { - ...mockOptions.params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - secrets: mockOptions.secrets, - services, - }; - - handleIncidentMock.mockImplementation(() => incidentResponse); - - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); - }); - - test('should throw an error when failed to update an incident', async () => { - expect.assertions(1); - - const actionId = 'some-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config: mockOptions.config, - params: { - ...mockOptions.params, - updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'anotherUser' }, - }, - secrets: mockOptions.secrets, - services, - }; - const errorMessage = 'Failed to update incident'; - - handleIncidentMock.mockImplementation(() => { - throw new Error(errorMessage); - }); - - try { - await actionType.executor(executorOptions); - } catch (error) { - expect(error.message).toEqual(errorMessage); - } - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index a63c2fd3a6ceb..dbb536d2fa53d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -4,108 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry, isEmpty } from 'lodash'; -import { schema } from '@kbn/config-schema'; -import { - ActionType, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, - ExecutorType, -} from '../../types'; -import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ServiceNow } from './lib'; - -import * as i18n from './translations'; - -import { ACTION_TYPE_ID } from './constants'; -import { ConfigType, SecretsType, Comment, ExecutorParams } from './types'; - -import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; - -import { buildMap, mapParams } from './helpers'; -import { handleIncident } from './action_handlers'; - -function validateConfig( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ConfigType -) { - try { - if (isEmpty(configObject.casesConfiguration.mapping)) { - return i18n.MAPPING_EMPTY; - } - - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.WHITE_LISTED_ERROR(whitelistError.message); - } -} - -function validateSecrets( - configurationUtilities: ActionsConfigurationUtilities, - secrets: SecretsType -) {} +import { createConnector } from '../case/utils'; -// action type definition -export function getActionType({ - configurationUtilities, - executor = serviceNowExecutor, -}: { - configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { - return { - id: ACTION_TYPE_ID, - name: i18n.NAME, - minimumLicenseRequired: 'platinum', - validate: { - config: schema.object(ConfigSchemaProps, { - validate: curry(validateConfig)(configurationUtilities), - }), - secrets: schema.object(SecretsSchemaProps, { - validate: curry(validateSecrets)(configurationUtilities), - }), - params: ParamsSchema, - }, - executor, - }; -} - -// action executor - -async function serviceNowExecutor( - execOptions: ActionTypeExecutorOptions -): Promise<ActionTypeExecutorResult> { - const actionId = execOptions.actionId; - const { - apiUrl, - casesConfiguration: { mapping: configurationMapping }, - } = execOptions.config as ConfigType; - const { username, password } = execOptions.secrets as SecretsType; - const params = execOptions.params as ExecutorParams; - const { comments, incidentId, ...restParams } = params; - - const mapping = buildMap(configurationMapping); - const incident = mapParams(restParams, mapping); - const serviceNow = new ServiceNow({ url: apiUrl, username, password }); - - const handlerInput = { - incidentId, - serviceNow, - params: { ...params, incident }, - comments: comments as Comment[], - mapping, - }; - - const res: Pick<ActionTypeExecutorResult, 'status'> & - Pick<ActionTypeExecutorResult, 'actionId'> = { - status: 'ok', - actionId, - }; - - const data = await handleIncident(handlerInput); - - return { - ...res, - data, - }; -} +import { api } from './api'; +import { config } from './config'; +import { validate } from './validators'; +import { createExternalService } from './service'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, +} from '../case/schema'; + +export const getActionType = createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: ExternalIncidentServiceConfiguration, + secrets: ExternalIncidentServiceSecretConfiguration, + }, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts deleted file mode 100644 index 3f102ae19f437..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts +++ /dev/null @@ -1,13 +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. - */ - -export const API_VERSION = 'v2'; -export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; -export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; -export const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; - -// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html -export const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts deleted file mode 100644 index 40eeb0f920f82..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ /dev/null @@ -1,334 +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 axios from 'axios'; -import { ServiceNow } from '.'; -import { instance, params } from '../mock'; - -jest.mock('axios'); - -axios.create = jest.fn(() => axios); -const axiosMock = (axios as unknown) as jest.Mock; - -let serviceNow: ServiceNow; - -const testMissingConfiguration = (field: string) => { - expect.assertions(1); - try { - new ServiceNow({ ...instance, [field]: '' }); - } catch (error) { - expect(error.message).toEqual('[Action][ServiceNow]: Wrong configuration.'); - } -}; - -const prependInstanceUrl = (url: string): string => `${instance.url}/${url}`; - -describe('ServiceNow lib', () => { - beforeEach(() => { - serviceNow = new ServiceNow(instance); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - test('should thrown an error if url is missing', () => { - testMissingConfiguration('url'); - }); - - test('should thrown an error if username is missing', () => { - testMissingConfiguration('username'); - }); - - test('should thrown an error if password is missing', () => { - testMissingConfiguration('password'); - }); - - test('get user id', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: [{ sys_id: '123' }] }, - }); - - const res = await serviceNow.getUserID(); - const [url, { method }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/sys_user?user_name=username')); - expect(method).toEqual('get'); - expect(res).toEqual('123'); - }); - - test('create incident', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, - }); - - const res = await serviceNow.createIncident({ - short_description: 'A title', - description: 'A description', - caller_id: '123', - }); - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident')); - expect(method).toEqual('post'); - expect(data).toEqual({ - short_description: 'A title', - description: 'A description', - caller_id: '123', - }); - - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('update incident', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - const res = await serviceNow.updateIncident('123', { - short_description: params.title, - }); - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); - expect(method).toEqual('patch'); - expect(data).toEqual({ short_description: params.title }); - expect(res).toEqual({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', - }); - }); - - test('create comment', async () => { - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - const comment = { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }; - - const res = await serviceNow.createComment('123', comment, 'comments'); - - const [url, { method, data }] = axiosMock.mock.calls[0]; - - expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); - expect(method).toEqual('patch'); - expect(data).toEqual({ - comments: 'A comment', - }); - - expect(res).toEqual({ - commentId: '456', - pushedDate: '2020-03-10T12:24:20.000Z', - }); - }); - - test('create batch comment', async () => { - axiosMock.mockReturnValueOnce({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, - }); - - axiosMock.mockReturnValueOnce({ - status: 200, - headers: { - 'content-type': 'application/json', - }, - data: { result: { sys_updated_on: '2020-03-10 12:25:20' } }, - }); - - const comments = [ - { - commentId: '123', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ]; - const res = await serviceNow.batchCreateComments('000', comments, 'comments'); - - comments.forEach((comment, index) => { - const [url, { method, data }] = axiosMock.mock.calls[index]; - expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident/000')); - expect(method).toEqual('patch'); - expect(data).toEqual({ - comments: comment.comment, - }); - expect(res).toEqual([ - { commentId: '123', pushedDate: '2020-03-10T12:24:20.000Z' }, - { commentId: '456', pushedDate: '2020-03-10T12:25:20.000Z' }, - ]); - }); - }); - - test('throw if not status is not ok', async () => { - expect.assertions(1); - - axiosMock.mockResolvedValue({ - status: 401, - headers: { - 'content-type': 'application/json', - }, - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' - ); - } - }); - - test('throw if not content-type is not application/json', async () => { - expect.assertions(1); - - axiosMock.mockResolvedValue({ - status: 200, - headers: { - 'content-type': 'application/html', - }, - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' - ); - } - }); - - test('check error when getting user', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.getUserID(); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get user id. Error: Bad request.' - ); - } - }); - - test('check error when getting incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.getIncident('123'); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to get incident with id 123. Error: Bad request.' - ); - } - }); - - test('check error when creating incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.createIncident({ short_description: 'title' }); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to create incident. Error: Bad request.' - ); - } - }); - - test('check error when updating incident', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.updateIncident('123', { short_description: 'title' }); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to update incident with id 123. Error: Bad request.' - ); - } - }); - - test('check error when creating comment', async () => { - expect.assertions(1); - - axiosMock.mockImplementationOnce(() => { - throw new Error('Bad request.'); - }); - try { - await serviceNow.createComment( - '123', - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A second comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - 'comment' - ); - } catch (error) { - expect(error.message).toEqual( - '[Action][ServiceNow]: Unable to create comment at incident with id 123. Error: Bad request.' - ); - } - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts deleted file mode 100644 index cc07a0b90330d..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ /dev/null @@ -1,186 +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 axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; - -import { INCIDENT_URL, USER_URL, COMMENT_URL, VIEW_INCIDENT_URL } from './constants'; -import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; -import { Comment } from '../types'; - -const validStatusCodes = [200, 201]; - -class ServiceNow { - private readonly incidentUrl: string; - private readonly commentUrl: string; - private readonly userUrl: string; - private readonly axios: AxiosInstance; - - constructor(private readonly instance: Instance) { - if ( - !this.instance || - !this.instance.url || - !this.instance.username || - !this.instance.password - ) { - throw Error('[Action][ServiceNow]: Wrong configuration.'); - } - - this.incidentUrl = `${this.instance.url}/${INCIDENT_URL}`; - this.commentUrl = `${this.instance.url}/${COMMENT_URL}`; - this.userUrl = `${this.instance.url}/${USER_URL}`; - this.axios = axios.create({ - auth: { username: this.instance.username, password: this.instance.password }, - }); - } - - private _throwIfNotAlive(status: number, contentType: string) { - if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { - throw new Error('[ServiceNow]: Instance is not alive.'); - } - } - - private async _request({ - url, - method = 'get', - data = {}, - }: { - url: string; - method?: Method; - data?: any; - }): Promise<AxiosResponse> { - const res = await this.axios(url, { method, data }); - this._throwIfNotAlive(res.status, res.headers['content-type']); - return res; - } - - private _patch({ url, data }: { url: string; data: any }): Promise<AxiosResponse> { - return this._request({ - url, - method: 'patch', - data, - }); - } - - private _addTimeZoneToDate(date: string, timezone = 'GMT'): string { - return `${date} GMT`; - } - - private _getErrorMessage(msg: string) { - return `[Action][ServiceNow]: ${msg}`; - } - - private _getIncidentViewURL(id: string) { - return `${this.instance.url}/${VIEW_INCIDENT_URL}${id}`; - } - - async getUserID(): Promise<string> { - try { - const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); - return res.data.result[0].sys_id; - } catch (error) { - throw new Error(this._getErrorMessage(`Unable to get user id. Error: ${error.message}`)); - } - } - - async getIncident(incidentId: string) { - try { - const res = await this._request({ - url: `${this.incidentUrl}/${incidentId}`, - }); - - return { ...res.data.result }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to get incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } - - async createIncident(incident: Incident): Promise<IncidentResponse> { - try { - const res = await this._request({ - url: `${this.incidentUrl}`, - method: 'post', - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), - url: this._getIncidentViewURL(res.data.result.sys_id), - }; - } catch (error) { - throw new Error(this._getErrorMessage(`Unable to create incident. Error: ${error.message}`)); - } - } - - async updateIncident(incidentId: string, incident: UpdateIncident): Promise<IncidentResponse> { - try { - const res = await this._patch({ - url: `${this.incidentUrl}/${incidentId}`, - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - url: this._getIncidentViewURL(res.data.result.sys_id), - }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to update incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } - - async batchCreateComments( - incidentId: string, - comments: Comment[], - field: string - ): Promise<CommentResponse[]> { - // Create comments sequentially. - const promises = comments.reduce(async (prevPromise, currentComment) => { - const totalComments = await prevPromise; - const res = await this.createComment(incidentId, currentComment, field); - return [...totalComments, res]; - }, Promise.resolve([] as CommentResponse[])); - - const res = await promises; - return res; - } - - async createComment( - incidentId: string, - comment: Comment, - field: string - ): Promise<CommentResponse> { - try { - const res = await this._patch({ - url: `${this.commentUrl}/${incidentId}`, - data: { [field]: comment.comment }, - }); - - return { - commentId: comment.commentId, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; - } catch (error) { - throw new Error( - this._getErrorMessage( - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` - ) - ); - } - } -} - -export { ServiceNow }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts deleted file mode 100644 index a65e417dbc486..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts +++ /dev/null @@ -1,32 +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. - */ - -export interface Instance { - url: string; - username: string; - password: string; -} - -export interface Incident { - short_description: string; - description?: string; - caller_id?: string; - [index: string]: string | undefined; -} - -export interface IncidentResponse { - number: string; - incidentId: string; - pushedDate: string; - url: string; -} - -export interface CommentResponse { - commentId: string; - pushedDate: string; -} - -export type UpdateIncident = Partial<Incident>; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts deleted file mode 100644 index 06c006fb37825..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ /dev/null @@ -1,115 +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 { MapEntry, Mapping, ExecutorParams } from './types'; -import { Incident } from './lib/types'; - -const mapping: MapEntry[] = [ - { source: 'title', target: 'short_description', actionType: 'overwrite' }, - { source: 'description', target: 'description', actionType: 'append' }, - { source: 'comments', target: 'comments', actionType: 'append' }, -]; - -const finalMapping: Mapping = new Map(); - -finalMapping.set('title', { - target: 'short_description', - actionType: 'overwrite', -}); - -finalMapping.set('description', { - target: 'description', - actionType: 'append', -}); - -finalMapping.set('comments', { - target: 'comments', - actionType: 'append', -}); - -finalMapping.set('short_description', { - target: 'title', - actionType: 'overwrite', -}); - -const params: ExecutorParams = { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - title: 'Incident title', - description: 'Incident description', - comments: [ - { - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - version: 'WzU3LDFd', - comment: 'A comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - }, - { - commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', - version: 'WlK3LDFd', - comment: 'Another comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: 'elastic' }, - }, - ], -}; - -const incidentResponse = { - incidentId: 'c816f79cc0a8016401c5a33be04be441', - number: 'INC0010001', - pushedDate: '2020-03-13T08:34:53.450Z', - url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', -}; - -const userId = '2e9a0a5e2f79001016ab51172799b670'; - -const axiosResponse = { - status: 200, - headers: { - 'content-type': 'application/json', - }, -}; -const userIdResponse = { - result: [{ sys_id: userId }], -}; - -const incidentAxiosResponse = { - result: { sys_id: incidentResponse.incidentId, number: incidentResponse.number }, -}; - -const instance = { - url: 'https://instance.service-now.com', - username: 'username', - password: 'password', -}; - -const incident: Incident = { - short_description: params.title, - description: params.description, - caller_id: userId, -}; - -export { - mapping, - finalMapping, - params, - incidentResponse, - incidentAxiosResponse, - userId, - userIdResponse, - axiosResponse, - instance, - incident, -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts new file mode 100644 index 0000000000000..37228380910b3 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ExternalService, + PushToServiceApiParams, + ExecutorSubActionPushParams, + MapRecord, +} from '../case/types'; + +const createMock = (): jest.Mocked<ExternalService> => { + const service = { + getIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + short_description: 'title from servicenow', + description: 'description from servicenow', + }) + ), + createIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + updateIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + id: 'incident-2', + title: 'INC02', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }) + ), + createComment: jest.fn(), + }; + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }) + ); + + service.createComment.mockImplementationOnce(() => + Promise.resolve({ + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }) + ); + return service; +}; + +const externalServiceMock = { + create: createMock, +}; + +const mapping: Map<string, Partial<MapRecord>> = new Map(); + +mapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); + +mapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +mapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +mapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const executorParams: ExecutorSubActionPushParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + externalId: 'incident-3', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, + }, + ], +}; + +const apiParams: PushToServiceApiParams = { + ...executorParams, + externalCase: { short_description: 'Incident title', description: 'Incident description' }, +}; + +export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts deleted file mode 100644 index 889b57c8e92e2..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ /dev/null @@ -1,70 +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 { schema } from '@kbn/config-schema'; - -export const MapEntrySchema = schema.object({ - source: schema.string(), - target: schema.string(), - actionType: schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), - ]), -}); - -export const CasesConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapEntrySchema), -}); - -export const ConfigSchemaProps = { - apiUrl: schema.string(), - casesConfiguration: CasesConfigurationSchema, -}; - -export const ConfigSchema = schema.object(ConfigSchemaProps); - -export const SecretsSchemaProps = { - password: schema.string(), - username: schema.string(), -}; - -export const SecretsSchema = schema.object(SecretsSchemaProps); - -export const UserSchema = schema.object({ - fullName: schema.nullable(schema.string()), - username: schema.string(), -}); - -const EntityInformationSchemaProps = { - createdAt: schema.string(), - createdBy: UserSchema, - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(UserSchema), -}; - -export const EntityInformationSchema = schema.object(EntityInformationSchemaProps); - -export const CommentSchema = schema.object({ - commentId: schema.string(), - comment: schema.string(), - version: schema.maybe(schema.string()), - ...EntityInformationSchemaProps, -}); - -export const ExecutorAction = schema.oneOf([ - schema.literal('newIncident'), - schema.literal('updateIncident'), -]); - -export const ParamsSchema = schema.object({ - caseId: schema.string(), - title: schema.string(), - comments: schema.maybe(schema.arrayOf(CommentSchema)), - description: schema.maybe(schema.string()), - incidentId: schema.nullable(schema.string()), - ...EntityInformationSchemaProps, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts new file mode 100644 index 0000000000000..f65cd5430560e --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -0,0 +1,255 @@ +/* + * 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 axios from 'axios'; + +import { createExternalService } from './service'; +import * as utils from '../case/utils'; +import { ExternalService } from '../case/types'; + +jest.mock('axios'); +jest.mock('../case/utils', () => { + const originalUtils = jest.requireActual('../case/utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; +const patchMock = utils.patch as jest.Mock; + +describe('ServiceNow service', () => { + let service: ExternalService; + + beforeAll(() => { + service = createExternalService({ + config: { apiUrl: 'https://dev102283.service-now.com' }, + secrets: { username: 'admin', password: 'admin' }, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService({ + config: { apiUrl: null }, + secrets: { username: 'admin', password: 'admin' }, + }) + ).toThrow(); + }); + + test('throws without username', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: 'admin' }, + }) + ).toThrow(); + }); + + test('throws without password', () => { + expect(() => + createExternalService({ + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: undefined }, + }) + ).toThrow(); + }); + }); + + describe('getIncident', () => { + test('it returns the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01' } }, + })); + const res = await service.getIncident('1'); + expect(res).toEqual({ sys_id: '1', number: 'INC01' }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01' } }, + })); + + await service.getIncident('1'); + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + expect(service.getIncident('1')).rejects.toThrow( + 'Unable to get incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createIncident', () => { + test('it creates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + })); + + await service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident', + method: 'post', + data: { short_description: 'title', description: 'desc' }, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createIncident({ + incident: { short_description: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred' + ); + }); + }); + + describe('updateIncident', () => { + test('it updates the incident correctly', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + }); + }); + + test('it should call request with correct arguments', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }); + + expect(patchMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + data: { short_description: 'title', description: 'desc' }, + }); + }); + + test('it should throw an error', async () => { + patchMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' }, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred' + ); + }); + }); + + describe('createComment', () => { + test('it creates the comment correctly', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + const res = await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }); + + expect(res).toEqual({ + commentId: 'comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('it should call request with correct arguments', async () => { + patchMock.mockImplementation(() => ({ + data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + })); + + await service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'my_field', + }); + + expect(patchMock).toHaveBeenCalledWith({ + axios, + url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + data: { my_field: 'comment' }, + }); + }); + + test('it should throw an error', async () => { + patchMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect( + service.createComment({ + incidentId: '1', + comment: { comment: 'comment', commentId: 'comment-1' }, + field: 'comments', + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create comment at incident with id 1. Error: An error has occurred' + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts new file mode 100644 index 0000000000000..541fefce2f2ff --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -0,0 +1,138 @@ +/* + * 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 axios from 'axios'; + +import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { addTimeZoneToDate, patch, request, getErrorMessage } from '../case/utils'; + +import * as i18n from './translations'; +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + CreateIncidentRequest, + UpdateIncidentRequest, + CreateCommentRequest, +} from './types'; + +const API_VERSION = 'v2'; +const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; + +// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html +const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; + +export const createExternalService = ({ + config, + secrets, +}: ExternalServiceCredentials): ExternalService => { + const { apiUrl: url } = config as ServiceNowPublicConfigurationType; + const { username, password } = secrets as ServiceNowSecretConfigurationType; + + if (!url || !username || !password) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const incidentUrl = `${url}/${INCIDENT_URL}`; + const commentUrl = `${url}/${COMMENT_URL}`; + const axiosInstance = axios.create({ + auth: { username, password }, + }); + + const getIncidentViewURL = (id: string) => { + return `${url}/${VIEW_INCIDENT_URL}${id}`; + }; + + const getIncident = async (id: string) => { + try { + const res = await request({ + axios: axiosInstance, + url: `${incidentUrl}/${id}`, + }); + + return { ...res.data.result }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + ); + } + }; + + const createIncident = async ({ incident }: ExternalServiceParams) => { + try { + const res = await request<CreateIncidentRequest>({ + axios: axiosInstance, + url: `${incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { + title: res.data.result.number, + id: res.data.result.sys_id, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + url: getIncidentViewURL(res.data.result.sys_id), + }; + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + ); + } + }; + + const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + try { + const res = await patch<UpdateIncidentRequest>({ + axios: axiosInstance, + url: `${incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + + return { + title: res.data.result.number, + id: res.data.result.sys_id, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + url: getIncidentViewURL(res.data.result.sys_id), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + try { + const res = await patch<CreateCommentRequest>({ + axios: axiosInstance, + url: `${commentUrl}/${incidentId}`, + data: { [field]: comment.comment }, + }); + + return { + commentId: comment.commentId, + pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } + }; + + return { + getIncident, + createIncident, + updateIncident, + createComment, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts deleted file mode 100644 index dc0a03fab8c71..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts +++ /dev/null @@ -1,43 +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 { TransformerArgs } from './types'; -import * as i18n from './translations'; - -export const informationCreated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, - ...rest, -}); - -export const informationUpdated = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, - ...rest, -}); - -export const informationAdded = ({ - value, - date, - user, - ...rest -}: TransformerArgs): TransformerArgs => ({ - value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, - ...rest, -}); - -export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ - value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, - ...rest, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 3b216a6c3260a..3d6138169c4cc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -6,77 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const API_URL_REQUIRED = i18n.translate( - 'xpack.actions.builtin.servicenow.servicenowApiNullError', - { - defaultMessage: 'ServiceNow [apiUrl] is required', - } -); - -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { - defaultMessage: 'error configuring servicenow action: {message}', - values: { - message, - }, - }); - -export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { +export const NAME = i18n.translate('xpack.actions.builtin.case.servicenowTitle', { defaultMessage: 'ServiceNow', }); - -export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', { - defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', -}); - -export const ERROR_POSTING = i18n.translate( - 'xpack.actions.builtin.servicenow.postingErrorMessage', - { - defaultMessage: 'error posting servicenow event', - } -); - -export const RETRY_POSTING = (status: number) => - i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { - defaultMessage: 'error posting servicenow event: http status {status}, retry later', - values: { - status, - }, - }); - -export const UNEXPECTED_STATUS = (status: number) => - i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { - defaultMessage: 'error posting servicenow event: unexpected status {status}', - values: { - status, - }, - }); - -export const FIELD_INFORMATION = ( - mode: string, - date: string | undefined, - user: string | undefined -) => { - switch (mode) { - case 'create': - return i18n.translate('xpack.actions.builtin.servicenow.informationCreated', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - case 'update': - return i18n.translate('xpack.actions.builtin.servicenow.informationUpdated', { - values: { date, user }, - defaultMessage: '(updated at {date} by {user})', - }); - case 'add': - return i18n.translate('xpack.actions.builtin.servicenow.informationAdded', { - values: { date, user }, - defaultMessage: '(added at {date} by {user})', - }); - default: - return i18n.translate('xpack.actions.builtin.servicenow.informationDefault', { - values: { date, user }, - defaultMessage: '(created at {date} by {user})', - }); - } -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 71b05be8f3e4d..d8476b7dca54a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -4,100 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TypeOf } from '@kbn/config-schema'; +export { + ExternalIncidentServiceConfiguration as ServiceNowPublicConfigurationType, + ExternalIncidentServiceSecretConfiguration as ServiceNowSecretConfigurationType, +} from '../case/types'; -import { - ConfigSchema, - SecretsSchema, - ParamsSchema, - CasesConfigurationSchema, - MapEntrySchema, - CommentSchema, -} from './schema'; - -import { ServiceNow } from './lib'; -import { Incident, IncidentResponse } from './lib/types'; - -// config definition -export type ConfigType = TypeOf<typeof ConfigSchema>; - -// secrets definition -export type SecretsType = TypeOf<typeof SecretsSchema>; - -export type ExecutorParams = TypeOf<typeof ParamsSchema>; - -export type CasesConfigurationType = TypeOf<typeof CasesConfigurationSchema>; -export type MapEntry = TypeOf<typeof MapEntrySchema>; -export type Comment = TypeOf<typeof CommentSchema>; - -export type Mapping = Map<string, any>; - -export interface Params extends ExecutorParams { - incident: Record<string, any>; +export interface CreateIncidentRequest { + summary: string; + description: string; } -export interface CreateHandlerArguments { - serviceNow: ServiceNow; - params: Params; - comments: Comment[]; - mapping: Mapping; -} - -export type UpdateHandlerArguments = CreateHandlerArguments & { - incidentId: string; -}; - -export type IncidentHandlerArguments = CreateHandlerArguments & { - incidentId: string | null; -}; -export interface HandlerResponse extends IncidentResponse { - comments?: SimpleComment[]; -} - -export interface SimpleComment { - commentId: string; - pushedDate: string; -} - -export interface AppendFieldArgs { - value: string; - prefix?: string; - suffix?: string; -} - -export interface KeyAny { - [index: string]: string; -} - -export interface AppendInformationFieldArgs { - value: string; - user: string; - date: string; - mode: string; -} - -export interface TransformerArgs { - value: string; - date?: string; - user?: string; - previousValue?: string; -} - -export interface PrepareFieldsForTransformArgs { - params: Params; - mapping: Mapping; - defaultPipes?: string[]; -} - -export interface PipedField { - key: string; - value: string; - actionType: string; - pipes: string[]; -} +export type UpdateIncidentRequest = Partial<CreateIncidentRequest>; -export interface TransformFieldsArgs { - params: Params; - fields: PipedField[]; - currentIncident?: Incident; +export interface CreateCommentRequest { + [key: string]: string; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts new file mode 100644 index 0000000000000..7226071392bc6 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; +import { ExternalServiceValidation } from '../case/types'; + +export const validate: ExternalServiceValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 49b0b84e9dbb5..cbcd4b2954518 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,24 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; +import { + ActionType, + Services, + ActionTypeExecutorOptions, + ActionTypeExecutorResult, +} from '../types'; import { validateParams, validateSecrets } from '../lib'; import { getActionType } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; +import { actionsMock } from '../mocks'; const ACTION_TYPE_ID = '.slack'; -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; beforeAll(() => { actionType = getActionType({ - async executor(options: ActionTypeExecutorOptions): Promise<any> {}, + async executor() {}, configurationUtilities: actionsConfigMock.create(), }); }); @@ -117,7 +119,7 @@ describe('validateActionTypeSecrets()', () => { describe('execute()', () => { beforeAll(() => { - async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise<any> { + async function mockSlackExecutor(options: ActionTypeExecutorOptions) { const { params } = options; const { message } = params; if (message == null) throw new Error('message property required in parameter'); @@ -130,7 +132,9 @@ describe('execute()', () => { return { text: `slack mockExecutor success: ${message}`, - }; + actionId: '', + status: 'ok', + } as ActionTypeExecutorResult; } actionType = getActionType({ @@ -148,10 +152,12 @@ describe('execute()', () => { params: { message: 'this invocation should succeed' }, }); expect(response).toMatchInlineSnapshot(` -Object { - "text": "slack mockExecutor success: this invocation should succeed", -} -`); + Object { + "actionId": "", + "status": "ok", + "text": "slack mockExecutor success: this invocation should succeed", + } + `); }); test('calls the mock executor with failure', async () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index e51ef3f67bd65..edf3e485f3c57 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -156,7 +156,7 @@ async function slackExecutor( return successResult(actionId, result); } -function successResult(actionId: string, data: any): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 03658b3b1dd85..d28856954cca5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -11,20 +11,17 @@ jest.mock('axios', () => ({ import { getActionType } from './webhook'; import { ActionType, Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../actions_config.mock'; import { createActionTypeRegistry } from './index.test'; import { Logger } from '../../../../../src/core/server'; +import { actionsMock } from '../mocks'; import axios from 'axios'; const axiosRequestMock = axios.request as jest.Mock; const ACTION_TYPE_ID = '.webhook'; -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; +const services: Services = actionsMock.createServices(); let actionType: ActionType; let mockedLogger: jest.Mocked<Logger>; @@ -44,7 +41,7 @@ describe('actionType', () => { describe('secrets validation', () => { test('succeeds when secrets is valid', () => { - const secrets: Record<string, any> = { + const secrets: Record<string, string> = { user: 'bob', password: 'supersecret', }; @@ -60,20 +57,18 @@ describe('secrets validation', () => { }); test('succeeds when basic authentication credentials are omitted', () => { - expect(() => { - validateSecrets(actionType, {}).toEqual({}); - }); + expect(validateSecrets(actionType, {})).toEqual({ password: null, user: null }); }); }); describe('config validation', () => { - const defaultValues: Record<string, any> = { + const defaultValues: Record<string, string | null> = { headers: null, method: 'post', }; test('config validation passes when only required fields are provided', () => { - const config: Record<string, any> = { + const config: Record<string, string> = { url: 'http://mylisteningserver:9200/endpoint', }; expect(validateConfig(actionType, config)).toEqual({ @@ -84,7 +79,7 @@ describe('config validation', () => { test('config validation passes when valid methods are provided', () => { ['post', 'put'].forEach(method => { - const config: Record<string, any> = { + const config: Record<string, string> = { url: 'http://mylisteningserver:9200/endpoint', method, }; @@ -96,7 +91,7 @@ describe('config validation', () => { }); test('should validate and throw error when method on config is invalid', () => { - const config: Record<string, any> = { + const config: Record<string, string> = { url: 'http://mylisteningserver:9200/endpoint', method: 'https', }; @@ -110,7 +105,7 @@ describe('config validation', () => { }); test('config validation passes when a url is specified', () => { - const config: Record<string, any> = { + const config: Record<string, string> = { url: 'http://mylisteningserver:9200/endpoint', }; expect(validateConfig(actionType, config)).toEqual({ @@ -120,6 +115,8 @@ describe('config validation', () => { }); test('config validation passes when valid headers are provided', () => { + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record<string, any> = { url: 'http://mylisteningserver:9200/endpoint', headers: { @@ -133,7 +130,7 @@ describe('config validation', () => { }); test('should validate and throw error when headers on config is invalid', () => { - const config: Record<string, any> = { + const config: Record<string, string> = { url: 'http://mylisteningserver:9200/endpoint', headers: 'application/json', }; @@ -147,6 +144,8 @@ describe('config validation', () => { }); test('config validation passes when kibana config whitelists the url', () => { + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record<string, any> = { url: 'http://mylisteningserver.com:9200/endpoint', headers: { @@ -171,6 +170,8 @@ describe('config validation', () => { }, }); + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record<string, any> = { url: 'http://mylisteningserver.com:9200/endpoint', headers: { @@ -188,12 +189,12 @@ describe('config validation', () => { describe('params validation', () => { test('param validation passes when no fields are provided as none are required', () => { - const params: Record<string, any> = {}; + const params: Record<string, string> = {}; expect(validateParams(actionType, params)).toEqual({}); }); test('params validation passes when a valid body is provided', () => { - const params: Record<string, any> = { + const params: Record<string, string> = { body: 'count: {{ctx.payload.hits.total}}', }; expect(validateParams(actionType, params)).toEqual({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 6173edc2df15a..0191ff1d9dbc1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -160,7 +160,7 @@ export async function executor( } // Action Executor Result w/ internationalisation -function successResult(actionId: string, data: any): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 51e87dbd75b48..161a6c31d4e59 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -7,7 +7,7 @@ import { configSchema } from './config'; describe('config validation', () => { test('action defaults', () => { - const config: Record<string, any> = {}; + const config: Record<string, unknown> = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "enabled": true, @@ -23,7 +23,7 @@ describe('config validation', () => { }); test('action with preconfigured actions', () => { - const config: Record<string, any> = { + const config: Record<string, unknown> = { preconfigured: [ { id: 'my-slack1', diff --git a/x-pack/plugins/actions/server/constants/plugin.ts b/x-pack/plugins/actions/server/constants/plugin.ts index 68082ccaa1399..7d20eb6990247 100644 --- a/x-pack/plugins/actions/server/constants/plugin.ts +++ b/x-pack/plugins/actions/server/constants/plugin.ts @@ -9,6 +9,7 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/const export const PLUGIN = { ID: 'actions', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => i18n.translate('xpack.actions.appName', { defaultMessage: 'Actions', diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 6bdd30848e4b7..1b7752588e3d3 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -282,4 +282,65 @@ describe('execute()', () => { }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); + + test('should skip ensure action type if action type is preconfigured and license is valid', async () => { + const mockedActionTypeRegistry = actionTypeRegistryMock.create(); + const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: mockedActionTypeRegistry, + preconfiguredActions: [ + { + actionTypeId: 'mock-action', + config: {}, + id: 'my-slack1', + name: 'Slack #xyz', + secrets: {}, + isPreconfigured: true, + }, + ], + }); + mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + + await executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }); + expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ + getBasePath: expect.anything(), + headers: {}, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 4a9ddf412b7cc..db38431b02cac 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from '../../../../src/core/server'; +import { SavedObjectsClientContract, KibanaRequest } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { GetBasePathFunction, @@ -15,7 +15,7 @@ import { interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; - getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract; + getScopedSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract; getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; actionTypeRegistry: ActionTypeRegistryContract; @@ -24,7 +24,7 @@ interface CreateExecuteFunctionOptions { export interface ExecuteOptions { id: string; - params: Record<string, any>; + params: Record<string, unknown>; spaceId: string; apiKey: string | null; } @@ -52,7 +52,7 @@ export function createExecuteFunction({ // Since we're using API keys and accessing elasticsearch can only be done // via a request, we're faking one with the proper authorization headers. - const fakeRequest: any = { + const fakeRequest: unknown = { headers: requestHeaders, getBasePath: () => getBasePath(spaceId), path: '/', @@ -67,10 +67,12 @@ export function createExecuteFunction({ }, }; - const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); + const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest as KibanaRequest); const actionTypeId = await getActionTypeId(id); - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 124e5951c714b..4594fc1ddf6d9 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -9,21 +9,15 @@ import { schema } from '@kbn/config-schema'; import { ActionExecutor } from './action_executor'; import { actionTypeRegistryMock } from '../action_type_registry.mock'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; -import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { eventLoggerMock } from '../../../event_log/server/mocks'; import { spacesServiceMock } from '../../../spaces/server/spaces_service/spaces_service.mock'; import { ActionType } from '../types'; +import { actionsMock } from '../mocks'; const actionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: false }); -const savedObjectsClient = savedObjectsClientMock.create(); - -function getServices() { - return { - savedObjectsClient, - log: jest.fn(), - callCluster: jest.fn(), - }; -} +const services = actionsMock.createServices(); +const savedObjectsClient = services.savedObjectsClient; const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart(); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -39,7 +33,7 @@ const spacesMock = spacesServiceMock.createSetupContract(); actionExecutor.initialize({ logger: loggingServiceMock.create().get(), spaces: spacesMock, - getServices, + getServices: () => services, actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), @@ -224,12 +218,56 @@ test('throws an error if actionType is not enabled', async () => { expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test'); }); +test('should not throws an error if actionType is preconfigured', async () => { + const actionType: jest.Mocked<ActionType> = { + id: 'test', + name: 'Test', + minimumLicenseRequired: 'basic', + executor: jest.fn(), + }; + const actionSavedObject = { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + config: { + bar: true, + }, + secrets: { + baz: true, + }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject); + encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); + actionTypeRegistry.get.mockReturnValueOnce(actionType); + actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { + throw new Error('not enabled for test'); + }); + actionTypeRegistry.isActionExecutable.mockImplementationOnce(() => true); + await actionExecutor.execute(executeParams); + + expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledTimes(0); + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: '1', + services: expect.anything(), + config: { + bar: true, + }, + secrets: { + baz: true, + }, + params: { foo: true }, + }); +}); + test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => { const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true }); customActionExecutor.initialize({ logger: loggingServiceMock.create().get(), spaces: spacesMock, - getServices, + getServices: () => services, actionTypeRegistry, encryptedSavedObjectsPlugin, eventLogger: eventLoggerMock.create(), diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index a33fb8830a930..3e9262c05efac 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; @@ -32,7 +32,7 @@ export interface ActionExecutorContext { export interface ExecuteOptions { actionId: string; request: KibanaRequest; - params: Record<string, any>; + params: Record<string, unknown>; } export type ActionExecutorContract = PublicMethodsOf<ActionExecutor>; @@ -90,12 +90,14 @@ export class ActionExecutor { namespace.namespace ); - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) { + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } const actionType = actionTypeRegistry.get(actionTypeId); - let validatedParams: Record<string, any>; - let validatedConfig: Record<string, any>; - let validatedSecrets: Record<string, any>; + let validatedParams: Record<string, unknown>; + let validatedConfig: Record<string, unknown>; + let validatedSecrets: Record<string, unknown>; try { validatedParams = validateParams(actionType, params); @@ -108,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); @@ -138,13 +149,18 @@ export class ActionExecutor { status: 'ok', }; + event.event = event.event || {}; + if (result.status === 'ok') { + event.event.outcome = 'success'; event.message = `action executed: ${actionLabel}`; } else if (result.status === 'error') { + event.event.outcome = 'failure'; event.message = `action execution failure: ${actionLabel}`; event.error = event.error || {}; event.error.message = actionErrorToMessage(result); } else { + event.event.outcome = 'failure'; event.message = `action execution returned unexpected result: ${actionLabel}`; event.error = event.error || {}; event.error.message = 'action execution returned unexpected result'; @@ -174,8 +190,8 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string { interface ActionInfo { actionTypeId: string; name: string; - config: any; - secrets: any; + config: unknown; + secrets: unknown; } async function getActionInfo( diff --git a/x-pack/plugins/actions/server/lib/executor_error.ts b/x-pack/plugins/actions/server/lib/executor_error.ts index 56ccbf14e6a45..c25033b2a27a2 100644 --- a/x-pack/plugins/actions/server/lib/executor_error.ts +++ b/x-pack/plugins/actions/server/lib/executor_error.ts @@ -5,9 +5,9 @@ */ export class ExecutorError extends Error { - readonly data?: any; + readonly data?: unknown; readonly retry: boolean | Date; - constructor(message?: string, data?: any, retry: boolean | Date = false) { + constructor(message?: string, data?: unknown, retry: boolean | Date = false) { super(message); this.data = data; this.retry = retry; diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index eb10e69a444e8..0a474ec3ae3ea 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -5,16 +5,16 @@ */ import { ActionType } from '../types'; -import { BehaviorSubject } from 'rxjs'; +import { Subject } from 'rxjs'; import { LicenseState, ILicenseState } from './license_state'; import { licensingMock } from '../../../licensing/server/mocks'; import { ILicense } from '../../../licensing/server'; describe('checkLicense()', () => { - let getRawLicense: any; + const getRawLicense = jest.fn(); beforeEach(() => { - getRawLicense = jest.fn(); + jest.resetAllMocks(); }); describe('status is LICENSE_STATUS_INVALID', () => { @@ -53,7 +53,7 @@ describe('checkLicense()', () => { }); describe('isLicenseValidForActionType', () => { - let license: BehaviorSubject<ILicense>; + let license: Subject<ILicense>; let licenseState: ILicenseState; const fooActionType: ActionType = { id: 'foo', @@ -63,7 +63,7 @@ describe('isLicenseValidForActionType', () => { }; beforeEach(() => { - license = new BehaviorSubject(null as any); + license = new Subject(); licenseState = new LicenseState(license); }); @@ -75,7 +75,7 @@ describe('isLicenseValidForActionType', () => { }); test('should return false when license not available', () => { - license.next({ isAvailable: false } as any); + license.next(createUnavailableLicense()); expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ isValid: false, reason: 'unavailable', @@ -114,7 +114,7 @@ describe('isLicenseValidForActionType', () => { }); describe('ensureLicenseForActionType()', () => { - let license: BehaviorSubject<ILicense>; + let license: Subject<ILicense>; let licenseState: ILicenseState; const fooActionType: ActionType = { id: 'foo', @@ -124,7 +124,7 @@ describe('ensureLicenseForActionType()', () => { }; beforeEach(() => { - license = new BehaviorSubject(null as any); + license = new Subject(); licenseState = new LicenseState(license); }); @@ -137,7 +137,7 @@ describe('ensureLicenseForActionType()', () => { }); test('should throw when license not available', () => { - license.next({ isAvailable: false } as any); + license.next(createUnavailableLicense()); expect(() => licenseState.ensureLicenseForActionType(fooActionType) ).toThrowErrorMatchingInlineSnapshot( @@ -175,3 +175,9 @@ describe('ensureLicenseForActionType()', () => { licenseState.ensureLicenseForActionType(fooActionType); }); }); + +function createUnavailableLicense() { + const unavailableLicense = licensingMock.createLicenseMock(); + unavailableLicense.isAvailable = false; + return unavailableLicense; +} diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index e2a6128aea203..08c5b90edbcb7 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -6,7 +6,7 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; -import { Logger, CoreStart } from '../../../../../src/core/server'; +import { Logger, CoreStart, KibanaRequest } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTypeDisabledError } from './errors'; @@ -60,7 +60,7 @@ export class TaskRunnerFactory { return { async run() { - const { spaceId, actionTaskParamsId } = taskInstance.params; + const { spaceId, actionTaskParamsId } = taskInstance.params as Record<string, string>; const namespace = spaceIdToNamespace(spaceId); const { @@ -78,7 +78,7 @@ export class TaskRunnerFactory { // Since we're using API keys and accessing elasticsearch can only be done // via a request, we're faking one with the proper authorization headers. - const fakeRequest: any = { + const fakeRequest = ({ headers: requestHeaders, getBasePath: () => getBasePath(spaceId), path: '/', @@ -91,7 +91,7 @@ export class TaskRunnerFactory { url: '/', }, }, - }; + } as unknown) as KibanaRequest; let executorResult: ActionTypeExecutorResult; try { diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index b7d408985ed9f..1ccd25664374d 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -49,7 +49,7 @@ test('should validate when there are no individual validators', () => { }); test('should validate when validators return incoming value', () => { - const selfValidator = { validate: (value: any) => value }; + const selfValidator = { validate: (value: Record<string, unknown>) => value }; const actionType: ActionType = { id: 'foo', name: 'bar', @@ -76,8 +76,8 @@ test('should validate when validators return incoming value', () => { }); test('should validate when validators return different values', () => { - const returnedValue: any = { something: { shaped: 'differently' } }; - const selfValidator = { validate: (value: any) => returnedValue }; + const returnedValue = { something: { shaped: 'differently' } }; + const selfValidator = { validate: () => returnedValue }; const actionType: ActionType = { id: 'foo', name: 'bar', @@ -105,7 +105,7 @@ test('should validate when validators return different values', () => { test('should throw with expected error when validators fail', () => { const erroringValidator = { - validate: (value: any) => { + validate: () => { throw new Error('test error'); }, }; diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.ts index 2306a726229e3..021c460f4c815 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.ts @@ -7,15 +7,15 @@ import Boom from 'boom'; import { ActionType } from '../types'; -export function validateParams(actionType: ActionType, value: any) { +export function validateParams(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'params', value); } -export function validateConfig(actionType: ActionType, value: any) { +export function validateConfig(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'config', value); } -export function validateSecrets(actionType: ActionType, value: any) { +export function validateSecrets(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'secrets', value); } @@ -24,33 +24,40 @@ type ValidKeys = 'params' | 'config' | 'secrets'; function validateWithSchema( actionType: ActionType, key: ValidKeys, - value: any -): Record<string, any> { - if (actionType.validate == null) return value; - - let name; - try { - switch (key) { - case 'params': - name = 'action params'; - if (actionType.validate.params == null) return value; - return actionType.validate.params.validate(value); - - case 'config': - name = 'action type config'; - if (actionType.validate.config == null) return value; - return actionType.validate.config.validate(value); - - case 'secrets': - name = 'action type secrets'; - if (actionType.validate.secrets == null) return value; - return actionType.validate.secrets.validate(value); + value: unknown +): Record<string, unknown> { + if (actionType.validate) { + let name; + try { + switch (key) { + case 'params': + name = 'action params'; + if (actionType.validate.params) { + return actionType.validate.params.validate(value); + } + break; + case 'config': + name = 'action type config'; + if (actionType.validate.config) { + return actionType.validate.config.validate(value); + } + + break; + case 'secrets': + name = 'action type secrets'; + if (actionType.validate.secrets) { + return actionType.validate.secrets.validate(value); + } + break; + default: + // should never happen, but left here for future-proofing + throw new Error(`invalid actionType validate key: ${key}`); + } + } catch (err) { + // we can't really i18n this yet, since the err.message isn't i18n'd itself + throw Boom.badRequest(`error validating ${name}: ${err.message}`); } - } catch (err) { - // we can't really i18n this yet, since the err.message isn't i18n'd itself - throw Boom.badRequest(`error validating ${name}: ${err.message}`); } - // should never happen, but left here for future-proofing - throw new Error(`invalid actionType validate key: ${key}`); + return value as Record<string, unknown>; } diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index bc4268bb69872..4160ace50f491 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -6,6 +6,11 @@ import { actionsClientMock } from './actions_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; +import { Services } from './types'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../src/core/server/mocks'; export { actionsClientMock }; @@ -20,13 +25,26 @@ const createStartMock = () => { const mock: jest.Mocked<PluginStartContract> = { execute: jest.fn(), isActionTypeEnabled: jest.fn(), + isActionExecutable: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), preconfiguredActions: [], }; return mock; }; +const createServicesMock = () => { + const mock: jest.Mocked<Services & { + savedObjectsClient: ReturnType<typeof savedObjectsClientMock.create>; + }> = { + callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser, + getScopedCallCluster: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + return mock; +}; + export const actionsMock = { + createServices: createServicesMock, createSetup: createSetupMock, createStart: createStartMock, }; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index fa5b2f9399a4d..2b334953063d1 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; @@ -72,7 +72,9 @@ describe('Actions Plugin', () => { }); it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); expect(context.logger.get().warn).toHaveBeenCalledWith( 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' @@ -81,7 +83,9 @@ describe('Actions Plugin', () => { describe('routeHandlerContext.getActionsClient()', () => { it('should not throw error when ESO plugin not using a generated key', async () => { - await plugin.setup(coreSetup, { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, @@ -93,8 +97,8 @@ describe('Actions Plugin', () => { const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; expect(handler[0]).toEqual('actions'); - const actionsContextHandler = (await handler[1]( - { + const actionsContextHandler = ((await handler[1]( + ({ core: { savedObjects: { client: {}, @@ -103,32 +107,34 @@ describe('Actions Plugin', () => { adminClient: jest.fn(), }, }, - } as any, + } as unknown) as RequestHandlerContext, httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() - )) as any; - actionsContextHandler.getActionsClient(); + )) as unknown) as RequestHandlerContext['actions']; + actionsContextHandler!.getActionsClient(); }); it('should throw error when ESO plugin using a generated key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1); const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; expect(handler[0]).toEqual('actions'); - const actionsContextHandler = (await handler[1]( - { + const actionsContextHandler = ((await handler[1]( + ({ core: { savedObjects: { client: {}, }, }, - } as any, + } as unknown) as RequestHandlerContext, httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() - )) as any; - expect(() => actionsContextHandler.getActionsClient()).toThrowErrorMatchingInlineSnapshot( + )) as unknown) as RequestHandlerContext['actions']; + expect(() => actionsContextHandler!.getActionsClient()).toThrowErrorMatchingInlineSnapshot( `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); }); @@ -144,13 +150,17 @@ describe('Actions Plugin', () => { }; beforeEach(async () => { - setup = await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setup = await plugin.setup(coreSetup as any, pluginsSetup); }); it('should throw error when license type is invalid', async () => { expect(() => setup.registerType({ ...sampleActionType, + // we're faking an invalid value, this requires stripping the typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any minimumLicenseRequired: 'foo' as any, }) ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); @@ -211,7 +221,9 @@ describe('Actions Plugin', () => { describe('getActionsClientWithRequest()', () => { it('should not throw error when ESO plugin not using a generated key', async () => { - await plugin.setup(coreSetup, { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, @@ -224,7 +236,9 @@ describe('Actions Plugin', () => { }); it('should throw error when ESO plugin using generated key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); const pluginStart = plugin.start(coreStart, pluginsStart); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a8ab3bbb2fad2..a6cc1fb5463bb 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -18,6 +18,7 @@ import { IContextProvider, SavedObjectsServiceStart, ElasticsearchServiceStart, + IClusterClient, } from '../../../../src/core/server'; import { @@ -52,6 +53,7 @@ import { } from './routes'; import { IEventLogger, IEventLogService } from '../../event_log/server'; import { initializeActionsTelemetry, scheduleActionsTelemetry } from './usage/task'; +import { setupSavedObjects } from './saved_objects'; const EVENT_LOG_PROVIDER = 'actions'; export const EVENT_LOG_ACTIONS = { @@ -65,6 +67,7 @@ export interface PluginSetupContract { export interface PluginStartContract { isActionTypeEnabled(id: string): boolean; + isActionExecutable(actionId: string, actionTypeId: string): boolean; execute(options: ExecuteOptions): Promise<void>; getActionsClientWithRequest(request: KibanaRequest): Promise<PublicMethodsOf<ActionsClient>>; preconfiguredActions: PreConfiguredAction[]; @@ -117,7 +120,10 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi this.preconfiguredActions = []; } - public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise<PluginSetupContract> { + public async setup( + core: CoreSetup<ActionsPluginsStart>, + plugins: ActionsPluginsSetup + ): Promise<PluginSetupContract> { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -128,19 +134,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi ); } - // Encrypted attributes - // - `secrets` properties will be encrypted - // - `config` will be included in AAD - // - everything else excluded from AAD - plugins.encryptedSavedObjects.registerType({ - type: 'action', - attributesToEncrypt: new Set(['secrets']), - attributesToExcludeFromAAD: new Set(['name']), - }); - plugins.encryptedSavedObjects.registerType({ - type: 'action_task_params', - attributesToEncrypt: new Set(['apiKey']), - }); + setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); this.eventLogger = plugins.eventLog.getLogger({ @@ -167,6 +161,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi taskManager: plugins.taskManager, actionsConfigUtils, licenseState: this.licenseState, + preconfiguredActions: this.preconfiguredActions, }); this.taskRunnerFactory = taskRunnerFactory; this.actionTypeRegistry = actionTypeRegistry; @@ -182,7 +177,7 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi const usageCollection = plugins.usageCollection; if (usageCollection) { - core.getStartServices().then(async ([, startPlugins]: [CoreStart, any, any]) => { + core.getStartServices().then(async ([, startPlugins]) => { registerActionsUsageCollector(usageCollection, startPlugins.taskManager); initializeActionsTelemetry( @@ -268,6 +263,9 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi isActionTypeEnabled: id => { return this.actionTypeRegistry!.isActionTypeEnabled(id); }, + isActionExecutable: (actionId: string, actionTypeId: string) => { + return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId); + }, // Ability to get an actions client from legacy code async getActionsClientWithRequest(request: KibanaRequest) { if (isESOUsingEphemeralEncryptionKey === true) { @@ -294,12 +292,15 @@ export class ActionsPlugin implements Plugin<Promise<PluginSetupContract>, Plugi return request => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), + getScopedCallCluster(clusterClient: IClusterClient) { + return clusterClient.asScoped(request).callAsCurrentUser; + }, }); } private createRouteHandlerContext = ( defaultKibanaIndex: string - ): IContextProvider<RequestHandler<any, any, any>, 'actions'> => { + ): IContextProvider<RequestHandler<unknown, unknown, unknown>, 'actions'> => { const { actionTypeRegistry, isESOUsingEphemeralEncryptionKey, preconfiguredActions } = this; return async function actionsRouteHandlerContext(context, request) { diff --git a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts index ec525adb8eab6..06f65ff23106c 100644 --- a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts @@ -7,12 +7,17 @@ import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { identity } from 'lodash'; import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { ActionType } from '../../common'; +import { ActionsClientMock, actionsClientMock } from '../actions_client.mock'; export function mockHandlerArguments( - { actionsClient, listTypes: listTypesRes = [] }: any, - req: any, + { + actionsClient = actionsClientMock.create(), + listTypes: listTypesRes = [], + }: { actionsClient?: ActionsClientMock; listTypes?: ActionType[] }, + req: unknown, res?: Array<MethodKeysOf<KibanaResponseFactory>> -): [RequestHandlerContext, KibanaRequest<any, any, any, any>, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest<unknown, unknown, unknown>, KibanaResponseFactory] { const listTypes = jest.fn(() => listTypesRes); return [ ({ @@ -31,7 +36,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest<any, any, any, any>, + req as KibanaRequest<unknown, unknown, unknown>, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 22cf0dd7f8ace..1fa85c86e0651 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { createActionRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('createActionRoute', () => { it('creates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); @@ -40,10 +41,11 @@ describe('createActionRoute', () => { name: 'My name', actionTypeId: 'abc', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce(createResult), - }; + + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce(createResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -83,22 +85,22 @@ describe('createActionRoute', () => { it('ensures the license allows creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce({ - id: '1', - name: 'My name', - actionTypeId: 'abc', - config: { foo: true }, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); - const [context, req, res] = mockHandlerArguments(actionsClient, {}); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); await handler(context, req, res); @@ -107,7 +109,7 @@ describe('createActionRoute', () => { it('ensures the license check prevents creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -117,16 +119,16 @@ describe('createActionRoute', () => { const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce({ - id: '1', - name: 'My name', - actionTypeId: 'abc', - config: { foo: true }, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); - const [context, req, res] = mockHandlerArguments(actionsClient, {}); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); @@ -135,15 +137,14 @@ describe('createActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok', 'forbidden']); diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 0cd3143dc48d1..7f239bda41a60 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -36,9 +36,9 @@ export const createActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<any, any, TypeOf<typeof bodySchema>, any>, + req: KibanaRequest<unknown, unknown, TypeOf<typeof bodySchema>>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.actions) { diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts index 6fb526628cb02..e63989e27a57c 100644 --- a/x-pack/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/delete.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteActionRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../mocks'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('deleteActionRoute', () => { it('deletes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); @@ -35,9 +36,8 @@ describe('deleteActionRoute', () => { } `); - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -65,19 +65,21 @@ describe('deleteActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); const [, handler] = router.delete.mock.calls[0]; - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(actionsClient, { - params: { id: '1' }, - }); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + } + ); await handler(context, req, res); @@ -86,7 +88,7 @@ describe('deleteActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -96,13 +98,15 @@ describe('deleteActionRoute', () => { const [, handler] = router.delete.mock.calls[0]; - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(actionsClient, { - id: '1', - }); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + id: '1', + } + ); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index ffd1f0faabbab..b76aa27fa5d9b 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -37,9 +37,9 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 3a3ed1257f576..1cd6343a39dcf 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -5,7 +5,7 @@ */ import { executeActionRoute } from './execute'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { ActionExecutorContract, verifyApiAccess, ActionTypeDisabledError } from '../lib'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('executeActionRoute', () => { it('executes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -77,7 +77,7 @@ describe('executeActionRoute', () => { it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -115,7 +115,7 @@ describe('executeActionRoute', () => { it('ensures the license allows action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -147,7 +147,7 @@ describe('executeActionRoute', () => { it('ensures the license check prevents action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -183,7 +183,7 @@ describe('executeActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 52d0aa706e703..79d3c1dfb8d22 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -43,9 +43,9 @@ export const executeActionRoute = ( }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, TypeOf<typeof bodySchema>, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, TypeOf<typeof bodySchema>>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); const { params } = req.body; const { id } = req.params; diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index f4e834a5b767c..f701be579d99d 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -5,10 +5,11 @@ */ import { getActionRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('getActionRoute', () => { it('gets an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); @@ -41,10 +42,11 @@ describe('getActionRoute', () => { actionTypeId: '2', name: 'action name', config: {}, + isPreconfigured: false, }; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce(getResult), - }; + + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce(getResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -60,6 +62,7 @@ describe('getActionRoute', () => { "actionTypeId": "2", "config": Object {}, "id": "1", + "isPreconfigured": false, "name": "action name", }, } @@ -75,23 +78,23 @@ describe('getActionRoute', () => { it('ensures the license allows getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); const [, handler] = router.get.mock.calls[0]; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce({ - id: '1', - actionTypeId: '2', - name: 'action name', - config: {}, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); const [context, req, res] = mockHandlerArguments( - actionsClient, + { actionsClient }, { params: { id: '1' }, }, @@ -105,7 +108,7 @@ describe('getActionRoute', () => { it('ensures the license check prevents getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -115,17 +118,17 @@ describe('getActionRoute', () => { const [, handler] = router.get.mock.calls[0]; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce({ - id: '1', - actionTypeId: '2', - name: 'action name', - config: {}, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); const [context, req, res] = mockHandlerArguments( - actionsClient, + { actionsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index cd29e54556b02..30e4289ed30c8 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -32,9 +32,9 @@ export const getActionRoute = (router: IRouter, licenseState: ILicenseState) => }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts index 6499427b8c1a5..e00054fd3746f 100644 --- a/x-pack/plugins/actions/server/routes/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -5,10 +5,11 @@ */ import { getAllActionRoute } from './get_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('getAllActionRoute', () => { it('get all actions with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -36,9 +37,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); @@ -57,7 +57,7 @@ describe('getAllActionRoute', () => { it('ensures the license allows getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -72,9 +72,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); @@ -85,7 +84,7 @@ describe('getAllActionRoute', () => { it('ensures the license check prevents getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -104,9 +103,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts index c70a13bc01c9f..f6745427f74c7 100644 --- a/x-pack/plugins/actions/server/routes/get_all.ts +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -25,9 +25,9 @@ export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<any, any, any, any>, + req: KibanaRequest<unknown, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts index 76fb636a75be7..205752d5a49d1 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts @@ -5,10 +5,11 @@ */ import { listActionTypesRoute } from './list_action_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { LicenseType } from '../../../../plugins/licensing/server'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('listActionTypesRoute', () => { it('lists action types with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -41,6 +42,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; @@ -51,7 +55,10 @@ describe('listActionTypesRoute', () => { "body": Array [ Object { "enabled": true, + "enabledInConfig": true, + "enabledInLicense": true, "id": "1", + "minimumLicenseRequired": "gold", "name": "name", }, ], @@ -67,7 +74,7 @@ describe('listActionTypesRoute', () => { it('ensures the license allows listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -87,6 +94,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; @@ -105,7 +115,7 @@ describe('listActionTypesRoute', () => { it('ensures the license check prevents listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -129,6 +139,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts index 71dcbd2e19bf7..50bc7405f4d6b 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -25,9 +25,9 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<any, any, any, any>, + req: KibanaRequest<unknown, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index 161fb4398af1d..6459a34bf0737 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { updateActionRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('updateActionRoute', () => { it('updates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -40,11 +41,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -86,7 +87,7 @@ describe('updateActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -97,11 +98,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -125,7 +126,7 @@ describe('updateActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -140,11 +141,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -168,15 +169,14 @@ describe('updateActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); const [, handler] = router.put.mock.calls[0]; - const actionsClient = { - update: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: {}, body: {} }, [ 'ok', diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index 263df678f293d..45ced77be922e 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -39,9 +39,9 @@ export const updateActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, TypeOf<typeof bodySchema>, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, TypeOf<typeof bodySchema>>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts new file mode 100644 index 0000000000000..dbd7925f96871 --- /dev/null +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + savedObjects.registerType({ + name: 'action', + hidden: false, + namespaceType: 'single', + mappings: mappings.action, + }); + + // Encrypted attributes + // - `secrets` properties will be encrypted + // - `config` will be included in AAD + // - everything else excluded from AAD + encryptedSavedObjects.registerType({ + type: 'action', + attributesToEncrypt: new Set(['secrets']), + attributesToExcludeFromAAD: new Set(['name']), + }); + + savedObjects.registerType({ + name: 'action_task_params', + hidden: false, + namespaceType: 'single', + mappings: mappings.action_task_params, + }); + encryptedSavedObjects.registerType({ + type: 'action_task_params', + attributesToEncrypt: new Set(['apiKey']), + }); +} diff --git a/x-pack/legacy/plugins/actions/server/mappings.json b/x-pack/plugins/actions/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/actions/server/mappings.json rename to x-pack/plugins/actions/server/saved_objects/mappings.json diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 92e38d77314f8..093d22c2c1a71 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,21 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectAttributes } from '../../../../src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { ActionsClient } from './actions_client'; import { LicenseType } from '../../licensing/common/types'; +import { + IClusterClient, + IScopedClusterClient, + KibanaRequest, + SavedObjectsClientContract, + SavedObjectAttributes, +} from '../../../../src/core/server'; export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>; -export type GetServicesFunction = (request: any) => Services; +export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf<ActionTypeRegistry>; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export interface Services { - callCluster(path: string, opts: any): Promise<any>; + callCluster: IScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; + getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } declare module 'src/core/server' { @@ -45,8 +52,12 @@ export interface ActionsConfigType { export interface ActionTypeExecutorOptions { actionId: string; services: Services; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record<string, any>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record<string, any>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record<string, any>; } @@ -54,11 +65,15 @@ export interface ActionResult { id: string; actionTypeId: string; name: string; - config: Record<string, any>; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config?: Record<string, any>; isPreconfigured: boolean; } export interface PreConfiguredAction extends ActionResult { + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record<string, any>; } @@ -72,6 +87,8 @@ export interface ActionTypeExecutorResult { status: 'ok' | 'error'; message?: string; serviceMessage?: string; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any data?: any; retry?: null | boolean | Date; } @@ -82,7 +99,7 @@ export type ExecutorType = ( ) => Promise<ActionTypeExecutorResult | null | undefined | void>; interface ValidatorType { - validate<T>(value: any): any; + validate(value: unknown): Record<string, unknown>; } export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; @@ -108,6 +125,13 @@ export interface RawAction extends SavedObjectAttributes { export interface ActionTaskParams extends SavedObjectAttributes { actionId: string; + // Saved Objects won't allow us to enforce unknown rather than any + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record<string, any>; apiKey?: string; } + +export interface ActionTaskExecutorParams { + spaceId: string; + actionTaskParamsId: string; +} diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index eabb38e61d17d..6996ba629f8da 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -55,6 +55,8 @@ export async function getTotalCount(callCluster: APICaller, kibanaIndex: string) 0 ), countByType: Object.keys(searchResult.aggregations.byActionTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: searchResult.aggregations.byActionTypeId.value.types[key], diff --git a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx index 41ef863c00e44..ef4a0f76de9ed 100644 --- a/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/advanced_ui_actions/public/components/action_wizard/action_wizard.tsx @@ -12,7 +12,7 @@ import { EuiIcon, EuiSpacer, EuiText, - EuiKeyPadMenuItemButton, + EuiKeyPadMenuItem, } from '@elastic/eui'; import { txtChangeButton } from './i18n'; import './action_wizard.scss'; @@ -180,7 +180,7 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({ return ( <EuiFlexGroup wrap> {actionFactories.map(actionFactory => ( - <EuiKeyPadMenuItemButton + <EuiKeyPadMenuItem className="auaActionWizard__actionFactoryItem" key={actionFactory.type} label={actionFactory.displayName} @@ -189,7 +189,7 @@ const ActionFactorySelector: React.FC<ActionFactorySelectorProps> = ({ onClick={() => onActionFactorySelected(actionFactory)} > {actionFactory.iconType && <EuiIcon type={actionFactory.iconType} size="m" />} - </EuiKeyPadMenuItemButton> + </EuiKeyPadMenuItem> ))} </EuiFlexGroup> ); diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 177e42de5a95b..62c2caed669af 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -101,6 +101,7 @@ This is the primary function for an alert type. Whenever the alert needs to exec |---|---| |services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana but in the context of the user who created the alert when security is enabled.| |services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.<br><br>The scope of the saved objects client is tied to the user who created the alert (only when security isenabled).| +|services.getScopedCallCluster|This function scopes an instance of CallCluster by returning a `callCluster(path, opts)` function that runs in the context of the user who created the alert when security is enabled. This must only be called with instances of CallCluster provided by core.| |services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| |startedAt|The date and time the alert type started execution.| |previousStartedAt|The previous date and time the alert type started a successful execution.| diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 8f28c8fbaed7f..b410b6aa0187e 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -28,6 +28,8 @@ export interface Alert { consumer: string; schedule: IntervalSchedule; actions: AlertAction[]; + // This will have to remain `any` until we can extend Alert Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record<string, any>; scheduledTaskId?: string; createdBy: string | null; diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerting/server/alert_type_registry.test.ts index b51286281571e..f9df390242cd4 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.test.ts @@ -231,7 +231,7 @@ function alertTypeWithVariables(id: string, context: string, state: string): Ale name: `${id}-name`, actionGroups: [], defaultActionGroupId: id, - executor: (params: any): any => {}, + async executor() {}, }; if (!context && !state) { diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerting/server/alert_type_registry.ts index a2be43f9dacbd..55e39b6a817db 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.ts @@ -77,7 +77,7 @@ export class AlertTypeRegistry { } } -function normalizedActionVariables(actionVariables: any) { +function normalizedActionVariables(actionVariables: AlertType['actionVariables']) { return { context: actionVariables?.context ?? [], state: actionVariables?.state ?? [], diff --git a/x-pack/plugins/alerting/server/alerts_client.mock.ts b/x-pack/plugins/alerting/server/alerts_client.mock.ts index 3189fa214d5f7..1848b3432ae5a 100644 --- a/x-pack/plugins/alerting/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerting/server/alerts_client.mock.ts @@ -7,9 +7,10 @@ import { AlertsClient } from './alerts_client'; type Schema = PublicMethodsOf<AlertsClient>; +export type AlertsClientMock = jest.Mocked<Schema>; const createAlertsClientMock = () => { - const mocked: jest.Mocked<Schema> = { + const mocked: AlertsClientMock = { create: jest.fn(), get: jest.fn(), getAlertState: jest.fn(), @@ -27,6 +28,8 @@ const createAlertsClientMock = () => { return mocked; }; -export const alertsClientMock = { +export const alertsClientMock: { + create: () => AlertsClientMock; +} = { create: createAlertsClientMock, }; diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index a9ff5ee8ecdc6..6c7b93aa64003 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -5,7 +5,7 @@ */ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { AlertsClient } from './alerts_client'; +import { AlertsClient, CreateOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; @@ -45,6 +45,7 @@ beforeEach(() => { }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date { constructor() { return mockedDate; @@ -54,7 +55,7 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); } }; -function getMockData(overwrites: Record<string, any> = {}) { +function getMockData(overwrites: Record<string, unknown> = {}): CreateOptions['data'] { return { enabled: true, name: 'abc', diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 3e4c26d3444c9..ff501055ba9fe 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -24,6 +24,7 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, + RawAlertAction, } from './types'; import { validateAlertTypeParams } from './lib'; import { @@ -83,7 +84,7 @@ export interface FindResult { data: SanitizedAlert[]; } -interface CreateOptions { +export interface CreateOptions { data: Omit< Alert, | 'id' @@ -109,7 +110,7 @@ interface UpdateOptions { tags: string[]; schedule: IntervalSchedule; actions: NormalizedAlertAction[]; - params: Record<string, any>; + params: Record<string, unknown>; throttle: string | null; }; } @@ -172,7 +173,7 @@ export class AlertsClient { createdBy: username, updatedBy: username, createdAt: new Date().toISOString(), - params: validatedAlertTypeParams, + params: validatedAlertTypeParams as RawAlert['params'], muteAll: false, mutedInstanceIds: [], }; @@ -337,7 +338,7 @@ export class AlertsClient { ...attributes, ...data, ...apiKeyAttributes, - params: validatedAlertTypeParams, + params: validatedAlertTypeParams as RawAlert['params'], actions, updatedBy: username, }, @@ -667,7 +668,7 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const actionMap = new Map<string, any>(); + const actionMap = new Map<string, unknown>(); // map preconfigured actions for (const alertAction of alertActions) { const action = this.preconfiguredActions.find( @@ -712,8 +713,8 @@ export class AlertsClient { // if action is a save object, than actionTypeId should be under attributes property // if action is a preconfigured, than actionTypeId is the action property const actionTypeId = actionIds.find(actionId => actionId === id) - ? actionMapValue.attributes.actionTypeId - : actionMapValue.actionTypeId; + ? (actionMapValue as SavedObject<Record<string, string>>).attributes.actionTypeId + : (actionMapValue as RawAlertAction).actionTypeId; return { ...alertAction, actionRef, diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index 951d18a33b35f..e5aa0a674eccf 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -11,16 +11,13 @@ import { taskManagerMock } from '../../../plugins/task_manager/server/task_manag import { KibanaRequest } from '../../../../src/core/server'; import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { AuthenticatedUser } from '../../../plugins/security/public'; +import { securityMock } from '../../../plugins/security/server/mocks'; jest.mock('./alerts_client'); const savedObjectsClient = savedObjectsClientMock.create(); -const securityPluginSetup = { - authc: { - grantAPIKeyAsInternalUser: jest.fn(), - getCurrentUser: jest.fn(), - }, -}; +const securityPluginSetup = securityMock.createSetup(); const alertsClientFactoryParams: jest.Mocked<AlertsClientFactoryOpts> = { logger: loggingServiceMock.create().get(), taskManager: taskManagerMock.start(), @@ -30,7 +27,7 @@ const alertsClientFactoryParams: jest.Mocked<AlertsClientFactoryOpts> = { encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.createStart(), preconfiguredActions: [], }; -const fakeRequest: Request = { +const fakeRequest = ({ headers: {}, getBasePath: () => '', path: '/', @@ -44,7 +41,7 @@ const fakeRequest: Request = { }, }, getSavedObjectsClient: () => savedObjectsClient, -} as any; +} as unknown) as Request; beforeEach(() => { jest.resetAllMocks(); @@ -86,12 +83,14 @@ test('getUserName() returns a name when security is enabled', async () => { const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce({ username: 'bob' }); + securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({ + username: 'bob', + } as unknown) as AuthenticatedUser); const userNameResult = await constructorCall.getUserName(); expect(userNameResult).toEqual('bob'); }); @@ -121,7 +120,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => { const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; @@ -129,11 +128,12 @@ test('createAPIKey() returns an API key when security is enabled', async () => { securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({ api_key: '123', id: 'abc', + name: '', }); const createAPIKeyResult = await constructorCall.createAPIKey(); expect(createAPIKeyResult).toEqual({ apiKeysEnabled: true, - result: { api_key: '123', id: 'abc' }, + result: { api_key: '123', id: 'abc', name: '' }, }); }); @@ -141,7 +141,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error', const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; diff --git a/x-pack/plugins/alerting/server/constants/plugin.ts b/x-pack/plugins/alerting/server/constants/plugin.ts index 173aa50013b40..9c276ed1d75de 100644 --- a/x-pack/plugins/alerting/server/constants/plugin.ts +++ b/x-pack/plugins/alerting/server/constants/plugin.ts @@ -9,6 +9,8 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/const export const PLUGIN = { ID: 'alerting', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + // all plugins seem to use getI18nName with any + // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => i18n.translate('xpack.alerting.appName', { defaultMessage: 'Alerting', diff --git a/x-pack/plugins/alerting/server/lib/license_state.test.ts b/x-pack/plugins/alerting/server/lib/license_state.test.ts index 9bbd619dc4868..cbab98a6311dd 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerting/server/lib/license_state.test.ts @@ -9,10 +9,10 @@ import { LicenseState } from './license_state'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; describe('license_state', () => { - let getRawLicense: any; + const getRawLicense = jest.fn(); beforeEach(() => { - getRawLicense = jest.fn(); + jest.resetAllMocks(); }); describe('status is LICENSE_STATUS_INVALID', () => { diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts index 248d896c06ac2..b6dcb522f0925 100644 --- a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts +++ b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts @@ -5,15 +5,15 @@ */ import Boom from 'boom'; -import { AlertType } from '../types'; +import { AlertType, AlertExecutorOptions } from '../types'; -export function validateAlertTypeParams<T extends Record<string, any>>( +export function validateAlertTypeParams( alertType: AlertType, - params: T -): T { + params: Record<string, unknown> +): AlertExecutorOptions['params'] { const validator = alertType.validate && alertType.validate.params; if (!validator) { - return params; + return params as AlertExecutorOptions['params']; } try { diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index a9e224142a632..c94a7aba46cfa 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -6,8 +6,11 @@ import { alertsClientMock } from './alerts_client.mock'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { AlertInstance } from './alert_instance'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../src/core/server/mocks'; export { alertsClientMock }; @@ -55,7 +58,8 @@ const createAlertServicesMock = () => { alertInstanceFactory: jest .fn<jest.Mocked<AlertInstance>, [string]>() .mockReturnValue(alertInstanceFactoryMock), - callCluster: jest.fn(), + callCluster: elasticsearchServiceMock.createScopedClusterClient().callAsCurrentUser, + getScopedCallCluster: jest.fn(), savedObjectsClient: savedObjectsClientMock.create(), }; }; diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 74a1f2349180e..267e68930a5d7 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertingPlugin } from './plugin'; +import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../../plugins/licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; +import { KibanaRequest, CoreSetup } from 'kibana/server'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -20,19 +21,19 @@ describe('Alerting Plugin', () => { const coreSetup = coreMock.createSetup(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup<AlertingPluginsStart, unknown>, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); @@ -58,34 +59,34 @@ describe('Alerting Plugin', () => { const coreSetup = coreMock.createSetup(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup<AlertingPluginsStart, unknown>, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); const startContract = plugin.start( - coreMock.createStart() as any, - { + coreMock.createStart() as ReturnType<typeof coreMock.createStart>, + ({ actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), }, - } as any + } as unknown) as AlertingPluginsStart ); expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); expect(() => - startContract.getAlertsClientWithRequest({} as any) + startContract.getAlertsClientWithRequest({} as KibanaRequest) ).toThrowErrorMatchingInlineSnapshot( `"Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); @@ -101,33 +102,33 @@ describe('Alerting Plugin', () => { usingEphemeralEncryptionKey: false, }; await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup<AlertingPluginsStart, unknown>, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); const startContract = plugin.start( - coreMock.createStart() as any, - { + coreMock.createStart() as ReturnType<typeof coreMock.createStart>, + ({ actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), }, spaces: () => null, - } as any + } as unknown) as AlertingPluginsStart ); - const fakeRequest = { + const fakeRequest = ({ headers: {}, getBasePath: () => '', path: '/', @@ -141,8 +142,8 @@ describe('Alerting Plugin', () => { }, }, getSavedObjectsClient: jest.fn(), - }; - await startContract.getAlertsClientWithRequest(fakeRequest as any); + } as unknown) as KibanaRequest; + await startContract.getAlertsClientWithRequest(fakeRequest); }); }); }); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index ad39d09bd6d3d..8cdde2eeb9877 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -29,6 +29,7 @@ import { RequestHandler, SharedGlobalConfig, ElasticsearchServiceStart, + IClusterClient, } from '../../../../src/core/server'; import { @@ -57,6 +58,7 @@ import { Services } from './types'; import { registerAlertsUsageCollector } from './usage'; import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task'; import { IEventLogger, IEventLogService } from '../../event_log/server'; +import { setupSavedObjects } from './saved_objects'; const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -117,7 +119,10 @@ export class AlertingPlugin { .toPromise(); } - public async setup(core: CoreSetup, plugins: AlertingPluginsSetup): Promise<PluginSetupContract> { + public async setup( + core: CoreSetup<AlertingPluginsStart, unknown>, + plugins: AlertingPluginsSetup + ): Promise<PluginSetupContract> { this.licenseState = new LicenseState(plugins.licensing.license$); this.spaces = plugins.spaces?.spacesService; this.security = plugins.security; @@ -130,17 +135,7 @@ export class AlertingPlugin { ); } - // Encrypted attributes - plugins.encryptedSavedObjects.registerType({ - type: 'alert', - attributesToEncrypt: new Set(['apiKey']), - attributesToExcludeFromAAD: new Set([ - 'scheduledTaskId', - 'muteAll', - 'mutedInstanceIds', - 'updatedBy', - ]), - }); + setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS)); this.eventLogger = plugins.eventLog.getLogger({ @@ -157,7 +152,7 @@ export class AlertingPlugin { const usageCollection = plugins.usageCollection; if (usageCollection) { - core.getStartServices().then(async ([, startPlugins]: [CoreStart, any, any]) => { + core.getStartServices().then(async ([, startPlugins]) => { registerAlertsUsageCollector(usageCollection, startPlugins.taskManager); initializeAlertingTelemetry( @@ -246,7 +241,7 @@ export class AlertingPlugin { } private createRouteHandlerContext = (): IContextProvider< - RequestHandler<any, any, any>, + RequestHandler<unknown, unknown, unknown>, 'alerting' > => { const { alertTypeRegistry, alertsClientFactory } = this; @@ -267,6 +262,9 @@ export class AlertingPlugin { return request => ({ callCluster: elasticsearch.legacy.client.asScoped(request).callAsCurrentUser, savedObjectsClient: savedObjects.getScopedClient(request), + getScopedCallCluster(clusterClient: IClusterClient) { + return clusterClient.asScoped(request).callAsCurrentUser; + }, }); } diff --git a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts index 5a1d680eb06f3..6fb3df8446f5c 100644 --- a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts @@ -4,16 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IClusterClient, +} from 'kibana/server'; import { identity } from 'lodash'; import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../alerts_client.mock'; +import { alertsClientMock, AlertsClientMock } from '../alerts_client.mock'; +import { AlertType } from '../../common'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; export function mockHandlerArguments( - { alertsClient, listTypes: listTypesRes = [], elasticsearch }: any, - req: any, + { + alertsClient = alertsClientMock.create(), + listTypes: listTypesRes = [], + elasticsearch = elasticsearchServiceMock.createSetup(), + }: { + alertsClient?: AlertsClientMock; + listTypes?: AlertType[]; + elasticsearch?: jest.Mocked<{ + adminClient: jest.Mocked<IClusterClient>; + }>; + }, + req: unknown, res?: Array<MethodKeysOf<KibanaResponseFactory>> -): [RequestHandlerContext, KibanaRequest<any, any, any, any>, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest<unknown, unknown, unknown>, KibanaResponseFactory] { const listTypes = jest.fn(() => listTypesRes); return [ ({ @@ -25,7 +42,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest<any, any, any, any>, + req as KibanaRequest<unknown, unknown, unknown>, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerting/server/routes/create.test.ts index c6a0da2bd9191..a4910495c8a40 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerting/server/routes/create.test.ts @@ -5,7 +5,7 @@ */ import { createAlertRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -68,7 +68,7 @@ describe('createAlertRoute', () => { it('creates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -134,7 +134,7 @@ describe('createAlertRoute', () => { it('ensures the license allows creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -142,7 +142,7 @@ describe('createAlertRoute', () => { alertsClient.create.mockResolvedValueOnce(createResult); - const [context, req, res] = mockHandlerArguments(alertsClient, {}); + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}); await handler(context, req, res); @@ -151,7 +151,7 @@ describe('createAlertRoute', () => { it('ensures the license check prevents creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -163,7 +163,7 @@ describe('createAlertRoute', () => { alertsClient.create.mockResolvedValueOnce(createResult); - const [context, req, res] = mockHandlerArguments(alertsClient, {}); + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/alerting/server/routes/create.ts b/x-pack/plugins/alerting/server/routes/create.ts index f08460ffcb453..0c038b6490483 100644 --- a/x-pack/plugins/alerting/server/routes/create.ts +++ b/x-pack/plugins/alerting/server/routes/create.ts @@ -54,9 +54,9 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<any, any, TypeOf<typeof bodySchema>, any>, + req: KibanaRequest<unknown, unknown, TypeOf<typeof bodySchema>>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerting/server/routes/delete.test.ts index 36bb485817c15..416628d015b5a 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerting/server/routes/delete.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteAlertRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('deleteAlertRoute', () => { it('deletes an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -66,7 +66,7 @@ describe('deleteAlertRoute', () => { it('ensures the license allows deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -74,9 +74,12 @@ describe('deleteAlertRoute', () => { alertsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(alertsClient, { - params: { id: '1' }, - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + } + ); await handler(context, req, res); @@ -85,7 +88,7 @@ describe('deleteAlertRoute', () => { it('ensures the license check prevents deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -97,9 +100,12 @@ describe('deleteAlertRoute', () => { alertsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(alertsClient, { - id: '1', - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + id: '1', + } + ); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/alerting/server/routes/delete.ts b/x-pack/plugins/alerting/server/routes/delete.ts index 8d77c9b395e59..7f6600b1ec48e 100644 --- a/x-pack/plugins/alerting/server/routes/delete.ts +++ b/x-pack/plugins/alerting/server/routes/delete.ts @@ -33,9 +33,9 @@ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerting/server/routes/disable.test.ts index 622b562ec6911..fde095e9145b6 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerting/server/routes/disable.test.ts @@ -5,7 +5,7 @@ */ import { disableAlertRoute } from './disable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('disableAlertRoute', () => { it('disables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); disableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/disable.ts b/x-pack/plugins/alerting/server/routes/disable.ts index fcc7116a697b1..c7e7b1001f82d 100644 --- a/x-pack/plugins/alerting/server/routes/disable.ts +++ b/x-pack/plugins/alerting/server/routes/disable.ts @@ -33,9 +33,9 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) = }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerting/server/routes/enable.test.ts index 5a7b027e8ef54..e4e89e3f06380 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerting/server/routes/enable.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { enableAlertRoute } from './enable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('enableAlertRoute', () => { it('enables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); enableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/enable.ts b/x-pack/plugins/alerting/server/routes/enable.ts index 2283ae4a4c765..3ed4fb0739d3d 100644 --- a/x-pack/plugins/alerting/server/routes/enable.ts +++ b/x-pack/plugins/alerting/server/routes/enable.ts @@ -35,9 +35,9 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/find.test.ts index 391d6df3f9931..cc601bd42b8ca 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findAlertRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -24,7 +24,7 @@ beforeEach(() => { describe('findAlertRoute', () => { it('finds alerts with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -95,7 +95,7 @@ describe('findAlertRoute', () => { it('ensures the license allows finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -108,13 +108,16 @@ describe('findAlertRoute', () => { data: [], }); - const [context, req, res] = mockHandlerArguments(alertsClient, { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + per_page: 1, + page: 1, + default_search_operator: 'OR', + }, + } + ); await handler(context, req, res); @@ -123,7 +126,7 @@ describe('findAlertRoute', () => { it('ensures the license check prevents finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerting/server/routes/find.ts index 0787f5c6b5ad6..c723419a965c5 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerting/server/routes/find.ts @@ -55,9 +55,9 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<any, TypeOf<typeof querySchema>, any, any>, + req: KibanaRequest<unknown, TypeOf<typeof querySchema>, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerting/server/routes/get.test.ts index 4506700c6d9cc..7335f13c85a4d 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerting/server/routes/get.test.ts @@ -5,7 +5,7 @@ */ import { getAlertRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -55,7 +55,7 @@ describe('getAlertRoute', () => { it('gets an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; @@ -90,7 +90,7 @@ describe('getAlertRoute', () => { it('ensures the license allows getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); @@ -99,7 +99,7 @@ describe('getAlertRoute', () => { alertsClient.get.mockResolvedValueOnce(mockedAlert); const [context, req, res] = mockHandlerArguments( - alertsClient, + { alertsClient }, { params: { id: '1' }, }, @@ -113,7 +113,7 @@ describe('getAlertRoute', () => { it('ensures the license check prevents getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -126,7 +126,7 @@ describe('getAlertRoute', () => { alertsClient.get.mockResolvedValueOnce(mockedAlert); const [context, req, res] = mockHandlerArguments( - alertsClient, + { alertsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/alerting/server/routes/get.ts b/x-pack/plugins/alerting/server/routes/get.ts index 39fbe64b62182..6d652d1304f65 100644 --- a/x-pack/plugins/alerting/server/routes/get.ts +++ b/x-pack/plugins/alerting/server/routes/get.ts @@ -33,9 +33,9 @@ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts index eb51c96b88e5e..20a420ca00986 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts @@ -5,10 +5,10 @@ */ import { getAlertStateRoute } from './get_alert_state'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { SavedObjectsErrorHelpers } from 'src/core/server/saved_objects'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; import { alertsClientMock } from '../alerts_client.mock'; const alertsClient = alertsClientMock.create(); @@ -41,7 +41,7 @@ describe('getAlertStateRoute', () => { it('gets alert state', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -84,7 +84,7 @@ describe('getAlertStateRoute', () => { it('returns NO-CONTENT when alert exists but has no task state yet', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -127,7 +127,7 @@ describe('getAlertStateRoute', () => { it('returns NOT-FOUND when alert is not found', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.ts index c5493c4abf57a..552bfea22a42b 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.ts @@ -33,9 +33,9 @@ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 42c83a7c04deb..7c50fbf561e59 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -5,7 +5,7 @@ */ import { healthRoute } from './health'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { verifyApiAccess } from '../lib/license_api_access'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('healthRoute', () => { it('registers the route', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -35,7 +35,7 @@ describe('healthRoute', () => { }); it('queries the usage api', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -64,7 +64,7 @@ describe('healthRoute', () => { }); it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -88,7 +88,7 @@ describe('healthRoute', () => { }); it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -112,7 +112,7 @@ describe('healthRoute', () => { }); it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -136,7 +136,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -162,7 +162,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -188,7 +188,7 @@ describe('healthRoute', () => { }); it('evaluates security and tls enabled to mean that the user can generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index fa2358a1f181c..bfdbc95a7d2da 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -39,9 +39,9 @@ export function healthRoute( }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<any, any, any, any>, + req: KibanaRequest<unknown, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); try { const { diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts index 723fd86fca8b5..37b52f1ec7923 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts @@ -5,7 +5,7 @@ */ import { listAlertTypesRoute } from './list_alert_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('listAlertTypesRoute', () => { it('lists alert types with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -47,6 +47,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + actionVariables: [], }, ]; @@ -62,6 +63,7 @@ describe('listAlertTypesRoute', () => { "name": "Default", }, ], + "actionVariables": Array [], "defaultActionGroupId": "default", "id": "1", "name": "name", @@ -79,7 +81,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license allows listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -99,6 +101,14 @@ describe('listAlertTypesRoute', () => { id: '1', name: 'name', enabled: true, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: [], }, ]; @@ -117,7 +127,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license check prevents listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -147,6 +157,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + actionVariables: [], }, ]; diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.ts index 455bc5e378b6d..7ab64cf932051 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.ts @@ -26,9 +26,9 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<any, any, any, any>, + req: KibanaRequest<unknown, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/mute_all.test.ts index 4c880e176d2df..5ef9e3694f8f4 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.test.ts @@ -5,7 +5,7 @@ */ import { muteAllAlertRoute } from './mute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAllAlertRoute', () => { it('mute an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.ts b/x-pack/plugins/alerting/server/routes/mute_all.ts index 29ef7d6b6b03b..d1b4322bd1ccb 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.ts @@ -33,9 +33,9 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) = }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts index 939864972c35d..2e6adedb76df9 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts @@ -5,7 +5,7 @@ */ import { muteAlertInstanceRoute } from './mute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAlertInstanceRoute', () => { it('mutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.ts b/x-pack/plugins/alerting/server/routes/mute_instance.ts index 7a071b1535dc7..fbdda62836d74 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.ts @@ -34,9 +34,9 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts index cd14e9b2e7172..1756dbd3fb41d 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { unmuteAllAlertRoute } from './unmute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('unmuteAllAlertRoute', () => { it('unmutes an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.ts b/x-pack/plugins/alerting/server/routes/unmute_all.ts index 81e28a81874cd..e09f2fe6b8b93 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.ts @@ -33,9 +33,9 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts index d74934f691710..9b9542c606741 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts @@ -5,7 +5,7 @@ */ import { unmuteAlertInstanceRoute } from './unmute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('unmuteAlertInstanceRoute', () => { it('unmutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.ts index de081ae7f1fcb..64ba22dc3ea0b 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.ts @@ -34,9 +34,9 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/update.test.ts index c3628617f861f..cd96f289b8714 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/update.test.ts @@ -5,7 +5,7 @@ */ import { updateAlertRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -45,7 +45,7 @@ describe('updateAlertRoute', () => { it('updates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -128,7 +128,7 @@ describe('updateAlertRoute', () => { it('ensures the license allows updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -171,7 +171,7 @@ describe('updateAlertRoute', () => { it('ensures the license check prevents updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerting/server/routes/update.ts index 45f7b26b521d4..7f07749311598 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerting/server/routes/update.ts @@ -56,9 +56,9 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, TypeOf<typeof bodySchema>, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, TypeOf<typeof bodySchema>>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts index 5e9821ac005e2..0347feb24a235 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts @@ -5,7 +5,7 @@ */ import { updateApiKeyRoute } from './update_api_key'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('updateApiKeyRoute', () => { it('updates api key for an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateApiKeyRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.ts b/x-pack/plugins/alerting/server/routes/update_api_key.ts index f70d30f0bb5da..9d0c34fc1a015 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.ts @@ -35,9 +35,9 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) = handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, unknown, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts new file mode 100644 index 0000000000000..4efec2fe55ef0 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + savedObjects.registerType({ + name: 'alert', + hidden: false, + namespaceType: 'single', + mappings: mappings.alert, + }); + + // Encrypted attributes + encryptedSavedObjects.registerType({ + type: 'alert', + attributesToEncrypt: new Set(['apiKey']), + attributesToExcludeFromAAD: new Set([ + 'scheduledTaskId', + 'muteAll', + 'mutedInstanceIds', + 'updatedBy', + ]), + }); +} diff --git a/x-pack/legacy/plugins/alerting/server/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/alerting/server/mappings.json rename to x-pack/plugins/alerting/server/saved_objects/mappings.json diff --git a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts index 9f5e3851aa29d..4be506b78493b 100644 --- a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts +++ b/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts @@ -7,10 +7,17 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; -import { SanitizedAlert, AlertTaskState, alertParamsSchema, alertStateSchema } from '../../common'; +import { + SanitizedAlert, + AlertTaskState, + alertParamsSchema, + alertStateSchema, + AlertTaskParams, +} from '../../common'; export interface AlertTaskInstance extends ConcreteTaskInstance { state: AlertTaskState; + params: AlertTaskParams; } const enumerateErrorFields = (e: t.Errors) => 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 756080baba626..a564b87f2ca50 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 @@ -51,6 +51,7 @@ const createExecutionHandlerParams = { beforeEach(() => { jest.resetAllMocks(); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); }); test('calls actionsPlugin.execute per selected action', async () => { @@ -94,6 +95,7 @@ test('calls actionsPlugin.execute per selected action', async () => { "saved_objects": Array [ Object { "id": "1", + "rel": "primary", "type": "alert", }, Object { @@ -111,6 +113,7 @@ test('calls actionsPlugin.execute per selected action', async () => { test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { // Mock two calls, one for check against actions[0] and the second for actions[1] + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValueOnce(false); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); const executionHandler = createExecutionHandler({ @@ -148,6 +151,50 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => }); }); +test('trow error error message when action type is disabled', async () => { + createExecutionHandlerParams.actionsPlugin.preconfiguredActions = []; + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(false); + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(false); + const executionHandler = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [ + ...createExecutionHandlerParams.actions, + { + id: '2', + group: 'default', + actionTypeId: '.slack', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, + ], + }); + + await executionHandler({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(0); + + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockImplementation(() => true); + const executionHandlerForPreconfiguredAction = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [...createExecutionHandlerParams.actions], + }); + await executionHandlerForPreconfiguredAction({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); +}); + test('limits actionsPlugin.execute per action group', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ 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 72f9e70905dc2..16fadc8b06cd5 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 { @@ -71,7 +71,7 @@ export function createExecutionHandler({ const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`; for (const action of actions) { - if (!actionsPlugin.isActionTypeEnabled(action.actionTypeId)) { + if (!actionsPlugin.isActionExecutable(action.id, action.actionTypeId)) { logger.warn( `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` ); @@ -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/get_next_run_at.test.ts b/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts index 1c4d8a42d2830..f5914fdf01a16 100644 --- a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts @@ -7,6 +7,7 @@ import { getNextRunAt } from './get_next_run_at'; const mockedNow = new Date('2019-06-03T18:55:25.982Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date extends global.Date { static now() { return mockedNow.getTime(); 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 31cc893f785cb..35a0018049c33 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 @@ -11,9 +11,10 @@ import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manag import { TaskRunnerContext } from './task_runner_factory'; import { TaskRunner } from './task_runner'; import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; -import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; import { actionsMock } from '../../../actions/server/mocks'; +import { alertsMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; import { IEventLogger } from '../../../event_log/server'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; @@ -52,13 +53,9 @@ describe('Task Runner', () => { afterAll(() => fakeTimer.restore()); - const savedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart(); - const services = { - log: jest.fn(), - callCluster: jest.fn(), - savedObjectsClient, - }; + const services = alertsMock.createAlertServices(); + const savedObjectsClient = services.savedObjectsClient; const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> & { actionsPlugin: jest.Mocked<ActionsPluginStart>; @@ -168,12 +165,14 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -185,6 +184,7 @@ describe('Task Runner', () => { test('actionsPlugin.execute is called per alert instance that is scheduled', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( ({ services: executorServices }: AlertExecutorOptions) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); @@ -228,12 +228,14 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ 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 { @@ -344,12 +348,14 @@ describe('Task Runner', () => { Object { "event": Object { "action": "execute", + "outcome": "success", }, "kibana": Object { "saved_objects": Array [ Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -370,6 +376,7 @@ describe('Task Runner', () => { Object { "id": "1", "namespace": undefined, + "rel": "primary", "type": "alert", }, ], @@ -560,12 +567,14 @@ describe('Task Runner', () => { }, "event": Object { "action": "execute", + "outcome": "failure", }, "kibana": Object { "saved_objects": Array [ 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 1d4b12e96bc76..bf005301adc07 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -5,7 +5,7 @@ */ import { pick, mapValues, omit, without } from 'lodash'; -import { Logger, SavedObject } from '../../../../../src/core/server'; +import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; @@ -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' }; @@ -93,7 +93,7 @@ export class TaskRunner { }, }; - return this.context.getServices(fakeRequest); + return this.context.getServices((fakeRequest as unknown) as KibanaRequest); } private getExecutionHandler( @@ -174,11 +174,20 @@ 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); - let updatedAlertTypeState: void | Record<string, any>; + let updatedAlertTypeState: void | Record<string, unknown>; try { updatedAlertTypeState = await this.alertType.executor({ alertId, @@ -202,12 +211,16 @@ export class TaskRunner { event.message = `alert execution failure: ${alertLabel}`; event.error = event.error || {}; event.error.message = err.message; + event.event = event.event || {}; + event.event.outcome = 'failure'; eventLogger.logEvent(event); throw err; } eventLogger.stopTiming(event); event.message = `alert executed: ${alertLabel}`; + event.event = event.event || {}; + event.event.outcome = 'success'; eventLogger.logEvent(event); // Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object @@ -389,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/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 1d220f97f127a..563664d3544ac 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -8,8 +8,9 @@ import sinon from 'sinon'; import { ConcreteTaskInstance, TaskStatus } from '../../../../plugins/task_manager/server'; import { TaskRunnerContext, TaskRunnerFactory } from './task_runner_factory'; import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks'; -import { savedObjectsClientMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; +import { loggingServiceMock } from '../../../../../src/core/server/mocks'; import { actionsMock } from '../../../actions/server/mocks'; +import { alertsMock } from '../mocks'; import { eventLoggerMock } from '../../../event_log/server/event_logger.mock'; const alertType = { @@ -48,13 +49,8 @@ describe('Task Runner Factory', () => { afterAll(() => fakeTimer.restore()); - const savedObjectsClient = savedObjectsClientMock.create(); const encryptedSavedObjectsPlugin = encryptedSavedObjectsMock.createStart(); - const services = { - log: jest.fn(), - callCluster: jest.fn(), - savedObjectsClient, - }; + const services = alertsMock.createAlertServices(); const taskRunnerFactoryInitializerParams: jest.Mocked<TaskRunnerContext> = { getServices: jest.fn().mockReturnValue(services), diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index e0cff58c4d40a..64f846d13c0bf 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -29,7 +29,7 @@ export function transformActionParams({ actionParams, state, }: TransformActionParamsOptions): AlertActionParams { - const result = cloneDeep(actionParams, (value: any) => { + const result = cloneDeep(actionParams, (value: unknown) => { if (!isString(value)) return; // when the list of variables we pass in here changes, diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerting/server/test_utils/index.ts index be9c5493ccf2b..e8089984a786a 100644 --- a/x-pack/plugins/alerting/server/test_utils/index.ts +++ b/x-pack/plugins/alerting/server/test_utils/index.ts @@ -5,7 +5,7 @@ */ interface Resolvable<T> { - resolve: (arg?: T) => void; + resolve: (arg: T) => void; } /** @@ -13,12 +13,10 @@ interface Resolvable<T> { * coordinating async tests. */ export function resolvable<T>(): Promise<T> & Resolvable<T> { - let resolve: (arg?: T) => void; - const result = new Promise<T>(r => { - resolve = r; - }) as any; - - result.resolve = (arg: T) => resolve(arg); - - return result; + let resolve: (arg: T) => void; + return Object.assign(new Promise<T>(r => (resolve = r)), { + resolve(arg: T) { + return setTimeout(() => resolve(arg), 0); + }, + }); } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 739a0d0aece24..b733b23dd71e6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,15 +7,24 @@ import { AlertInstance } from './alert_instance'; import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../src/core/server'; import { Alert, AlertActionParams, ActionGroup } from '../common'; import { AlertsClient } from './alerts_client'; export * from '../common'; +import { + IClusterClient, + IScopedClusterClient, + KibanaRequest, + SavedObjectAttributes, + SavedObjectsClientContract, +} from '../../../../src/core/server'; +// This will have to remain `any` until we can extend Alert Executors with generics +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type State = Record<string, any>; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type Context = Record<string, any>; export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>; -export type GetServicesFunction = (request: any) => Services; +export type GetServicesFunction = (request: KibanaRequest) => Services; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; @@ -29,8 +38,9 @@ declare module 'src/core/server' { } export interface Services { - callCluster(path: string, opts: any): Promise<any>; + callCluster: IScopedClusterClient['callAsCurrentUser']; savedObjectsClient: SavedObjectsClientContract; + getScopedCallCluster(clusterClient: IClusterClient): IScopedClusterClient['callAsCurrentUser']; } export interface AlertServices extends Services { @@ -42,6 +52,8 @@ export interface AlertExecutorOptions { startedAt: Date; previousStartedAt: Date | null; services: AlertServices; + // This will have to remain `any` until we can extend Alert Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record<string, any>; state: State; spaceId: string; @@ -61,7 +73,7 @@ export interface AlertType { id: string; name: string; validate?: { - params?: { validate: (object: any) => any }; + params?: { validate: (object: unknown) => AlertExecutorOptions['params'] }; }; actionGroups: ActionGroup[]; defaultActionGroupId: ActionGroup['id']; diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 9c710fa3b3b8e..2edf1c1061864 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -5,6 +5,7 @@ */ import { APICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; const alertTypeMetric = { scripted_metric: { @@ -246,6 +247,8 @@ export async function getTotalCountAggregations(callCluster: APICaller, kibanaIn return { count_total: totalAlertsCount, count_by_type: Object.keys(results.aggregations.byAlertTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: results.aggregations.byAlertTypeId.value.types[key], @@ -284,7 +287,7 @@ export async function getTotalCountAggregations(callCluster: APICaller, kibanaIn } export async function getTotalCountInUse(callCluster: APICaller, kibanaInex: string) { - const searchResult = await callCluster('search', { + const searchResult: SearchResponse<unknown> = await callCluster('search', { index: kibanaInex, rest_total_hits_as_int: true, body: { @@ -305,6 +308,8 @@ export async function getTotalCountInUse(callCluster: APICaller, kibanaInex: str 0 ), countByType: Object.keys(searchResult.aggregations.byAlertTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: searchResult.aggregations.byAlertTypeId.value.types[key], diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts index 33d1e1897e943..c209894fb6e89 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts @@ -6,6 +6,7 @@ import { ParamsSchema, Params } from './alert_type_params'; import { runTests } from './lib/core_query_types.test'; +import { TypeOf } from '@kbn/config-schema'; const DefaultParams: Writable<Partial<Params>> = { index: 'index-name', @@ -21,6 +22,7 @@ const DefaultParams: Writable<Partial<Params>> = { describe('alertType Params validate()', () => { runTests(ParamsSchema, DefaultParams); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; beforeEach(() => { params = { ...DefaultParams }; @@ -64,7 +66,7 @@ describe('alertType Params validate()', () => { return () => validate(); } - function validate(): any { + function validate(): TypeOf<typeof ParamsSchema> { return ParamsSchema.validate(params); } }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts index f83d7fa07cd2a..4a822156ebd06 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts @@ -30,12 +30,12 @@ export const ParamsSchema = schema.object( const betweenComparators = new Set(['between', 'notBetween']); // using direct type not allowed, circular reference, so body is typed to any -function validateParams(anyParams: any): string | undefined { +function validateParams(anyParams: unknown): string | undefined { // validate core query parts, return if it fails validation (returning string) const coreQueryValidated = validateCoreQueryBody(anyParams); if (coreQueryValidated) return coreQueryValidated; - const { thresholdComparator, threshold }: Params = anyParams; + const { thresholdComparator, threshold }: Params = anyParams as Params; if (betweenComparators.has(thresholdComparator) && threshold.length === 1) { return i18n.translate('xpack.alertingBuiltins.indexThreshold.invalidThreshold2ErrorMessage', { diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts index 109785b835bdf..6c9c3542aea03 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts @@ -19,7 +19,8 @@ const DefaultParams: Writable<Partial<CoreQueryParams>> = { timeWindowUnit: 'm', }; -export function runTests(schema: ObjectType, defaultTypeParams: Record<string, any>): void { +export function runTests(schema: ObjectType, defaultTypeParams: Record<string, unknown>): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; describe('coreQueryTypes', () => { @@ -186,7 +187,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record<string, a return () => validate(); } - function validate(): any { + function validate(): unknown { return schema.validate(params); } } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts index 6e9c0072bf7b6..c8da61fb56d21 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts @@ -41,9 +41,15 @@ export type CoreQueryParams = TypeOf<typeof CoreQueryParamsSchema>; // Meant to be used in a "subclass"'s schema body validator, so the // anyParams object is assumed to have been validated with the schema // above. -// Using direct type not allowed, circular reference, so body is typed to any. -export function validateCoreQueryBody(anyParams: any): string | undefined { - const { aggType, aggField, groupBy, termField, termSize }: CoreQueryParams = anyParams; +// Using direct type not allowed, circular reference, so body is typed to unknown. +export function validateCoreQueryBody(anyParams: unknown): string | undefined { + const { + aggType, + aggField, + groupBy, + termField, + termSize, + }: CoreQueryParams = anyParams as CoreQueryParams; if (aggType !== 'count' && !aggField) { return i18n.translate('xpack.alertingBuiltins.indexThreshold.aggTypeRequiredErrorMessage', { defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"', diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts index 1d9cc1c98bc01..9c4133be6f483 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; import { DEFAULT_GROUPS } from '../index'; import { getDateRangeInfo } from './date_range_info'; import { Logger, CallCluster } from '../../../types'; @@ -35,6 +36,8 @@ export async function timeSeriesQuery( const dateRangeInfo = getDateRangeInfo({ dateStart, dateEnd, window, interval }); // core query + // Constructing a typesafe ES query in JS is problematic, use any escapehatch for now + // eslint-disable-next-line @typescript-eslint/no-explicit-any const esQuery: any = { index, body: { @@ -122,7 +125,7 @@ export async function timeSeriesQuery( }; } - let esResult: any; + let esResult: SearchResponse<unknown>; const logPrefix = 'indexThreshold timeSeriesQuery: callCluster'; logger.debug(`${logPrefix} call: ${JSON.stringify(esQuery)}`); @@ -147,7 +150,7 @@ export async function timeSeriesQuery( function getResultFromEs( isCountAgg: boolean, isGroupAgg: boolean, - esResult: Record<string, any> + esResult: SearchResponse<unknown> ): TimeSeriesResult { const aggregations = esResult?.aggregations || {}; diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts index fcbd49b26ffd0..ec164122032cb 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts @@ -6,6 +6,7 @@ import { TimeSeriesQuerySchema, TimeSeriesQuery } from './time_series_types'; import { runTests } from './core_query_types.test'; +import { TypeOf } from '@kbn/config-schema'; const DefaultParams: Writable<Partial<TimeSeriesQuery>> = { index: 'index-name', @@ -19,6 +20,7 @@ const DefaultParams: Writable<Partial<TimeSeriesQuery>> = { describe('TimeSeriesParams validate()', () => { runTests(TimeSeriesQuerySchema, DefaultParams); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; beforeEach(() => { params = { ...DefaultParams }; @@ -102,7 +104,7 @@ describe('TimeSeriesParams validate()', () => { return () => validate(); } - function validate(): any { + function validate(): TypeOf<typeof TimeSeriesQuerySchema> { return TimeSeriesQuerySchema.validate(params); } }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts index abe5d562027eb..40e6f187ce18f 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts @@ -48,13 +48,13 @@ export const TimeSeriesQuerySchema = schema.object( } ); -// using direct type not allowed, circular reference, so body is typed to any -function validateBody(anyParams: any): string | undefined { +// using direct type not allowed, circular reference, so body is typed to unknown +function validateBody(anyParams: unknown): string | undefined { // validate core query parts, return if it fails validation (returning string) const coreQueryValidated = validateCoreQueryBody(anyParams); if (coreQueryValidated) return coreQueryValidated; - const { dateStart, dateEnd, interval }: TimeSeriesQuery = anyParams; + const { dateStart, dateEnd, interval } = anyParams as TimeSeriesQuery; // dates already validated in validateDate(), if provided const epochStart = dateStart ? Date.parse(dateStart) : undefined; diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts index 32d6409d9c9fb..5cc41671f6167 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts @@ -38,7 +38,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest<any, any, RequestBody, any>, + req: KibanaRequest<unknown, unknown, RequestBody>, res: KibanaResponseFactory ): Promise<IKibanaResponse> { service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts index c08450448b44c..ebcf6b4f0e45a 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts @@ -18,6 +18,7 @@ import { KibanaResponseFactory, IScopedClusterClient, } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Service } from '../../../types'; const bodySchema = schema.object({ @@ -40,7 +41,7 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute: ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest<any, any, RequestBody, any>, + req: KibanaRequest<unknown, unknown, RequestBody>, res: KibanaResponseFactory ): Promise<IKibanaResponse> { const pattern = req.body.pattern; @@ -102,12 +103,14 @@ async function getIndicesFromPattern( }, }, }; - const response = await dataClient.callAsCurrentUser('search', params); - if (response.status === 404 || !response.aggregations) { + const response: SearchResponse<unknown> = await dataClient.callAsCurrentUser('search', params); + // TODO: Investigate when the status field might appear here, type suggests it shouldn't ever happen + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((response as any).status === 404 || !response.aggregations) { return []; } - return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + return (response.aggregations as IndiciesAggregation).indices.buckets.map(bucket => bucket.key); } async function getAliasesFromPattern( @@ -137,3 +140,9 @@ async function getAliasesFromPattern( return result; } + +interface IndiciesAggregation { + indices: { + buckets: Array<{ key: string }>; + }; +} diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts index c8129c2428ee4..201c82060f386 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts @@ -30,7 +30,7 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest<any, any, TimeSeriesQuery, any>, + req: KibanaRequest<unknown, unknown, TimeSeriesQuery>, res: KibanaResponseFactory ): Promise<IKibanaResponse> { service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); diff --git a/x-pack/legacy/plugins/apm/CONTRIBUTING.md b/x-pack/plugins/apm/CONTRIBUTING.md similarity index 100% rename from x-pack/legacy/plugins/apm/CONTRIBUTING.md rename to x-pack/plugins/apm/CONTRIBUTING.md diff --git a/x-pack/plugins/apm/common/apm_saved_object_constants.ts b/x-pack/plugins/apm/common/apm_saved_object_constants.ts index 0529d90fe940a..eb16db7715fed 100644 --- a/x-pack/plugins/apm/common/apm_saved_object_constants.ts +++ b/x-pack/plugins/apm/common/apm_saved_object_constants.ts @@ -5,7 +5,7 @@ */ // the types have to match the names of the saved object mappings -// in /x-pack/legacy/plugins/apm/mappings.json +// in /x-pack/plugins/apm/mappings.json // APM indices export const APM_INDICES_SAVED_OBJECT_TYPE = 'apm-indices'; diff --git a/x-pack/legacy/plugins/apm/public/utils/pickKeys.ts b/x-pack/plugins/apm/common/utils/pick_keys.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/pickKeys.ts rename to x-pack/plugins/apm/common/utils/pick_keys.ts diff --git a/x-pack/legacy/plugins/apm/dev_docs/github_commands.md b/x-pack/plugins/apm/dev_docs/github_commands.md similarity index 100% rename from x-pack/legacy/plugins/apm/dev_docs/github_commands.md rename to x-pack/plugins/apm/dev_docs/github_commands.md diff --git a/x-pack/legacy/plugins/apm/dev_docs/typescript.md b/x-pack/plugins/apm/dev_docs/typescript.md similarity index 83% rename from x-pack/legacy/plugins/apm/dev_docs/typescript.md rename to x-pack/plugins/apm/dev_docs/typescript.md index 6858e93ec09e0..6de61b665a1b1 100644 --- a/x-pack/legacy/plugins/apm/dev_docs/typescript.md +++ b/x-pack/plugins/apm/dev_docs/typescript.md @@ -4,8 +4,8 @@ Kibana and X-Pack are very large TypeScript projects, and it comes at a cost. Ed To run the optimization: -`$ node x-pack/legacy/plugins/apm/scripts/optimize-tsconfig` +`$ node x-pack/plugins/apm/scripts/optimize-tsconfig` To undo the optimization: -`$ node x-pack/legacy/plugins/apm/scripts/unoptimize-tsconfig` +`$ node x-pack/plugins/apm/scripts/unoptimize-tsconfig` diff --git a/x-pack/legacy/plugins/apm/dev_docs/vscode_setup.md b/x-pack/plugins/apm/dev_docs/vscode_setup.md similarity index 83% rename from x-pack/legacy/plugins/apm/dev_docs/vscode_setup.md rename to x-pack/plugins/apm/dev_docs/vscode_setup.md index e1901b3855f73..1c80d1476520d 100644 --- a/x-pack/legacy/plugins/apm/dev_docs/vscode_setup.md +++ b/x-pack/plugins/apm/dev_docs/vscode_setup.md @@ -1,6 +1,6 @@ ### Visual Studio Code -When using [Visual Studio Code](https://code.visualstudio.com/) with APM it's best to set up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) and add the `x-pack/legacy/plugins/apm` directory, the `x-pack` directory, and the root of the Kibana repository to the workspace. This makes it so you can navigate and search within APM and use the wider workspace roots when you need to widen your search. +When using [Visual Studio Code](https://code.visualstudio.com/) with APM it's best to set up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) and add the `x-pack/plugins/apm` directory, the `x-pack` directory, and the root of the Kibana repository to the workspace. This makes it so you can navigate and search within APM and use the wider workspace roots when you need to widen your search. #### Using the Jest extension @@ -25,7 +25,7 @@ If you have a workspace configured as described above you should have: in your Workspace settings, and: ```json -"jest.pathToJest": "node scripts/jest.js --testPathPattern=legacy/plugins/apm", +"jest.pathToJest": "node scripts/jest.js --testPathPattern=plugins/apm", "jest.rootPath": "../../.." ``` @@ -40,7 +40,7 @@ To make the [VSCode debugger](https://vscode.readthedocs.io/en/latest/editor/deb "type": "node", "name": "APM Jest", "request": "launch", - "args": ["--runInBand", "--testPathPattern=legacy/plugins/apm"], + "args": ["--runInBand", "--testPathPattern=plugins/apm"], "cwd": "${workspaceFolder}/../../..", "console": "internalConsole", "internalConsoleOptions": "openOnSessionStart", diff --git a/x-pack/legacy/plugins/apm/e2e/.gitignore b/x-pack/plugins/apm/e2e/.gitignore similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/.gitignore rename to x-pack/plugins/apm/e2e/.gitignore diff --git a/x-pack/plugins/apm/e2e/README.md b/x-pack/plugins/apm/e2e/README.md new file mode 100644 index 0000000000000..cf29b14d19541 --- /dev/null +++ b/x-pack/plugins/apm/e2e/README.md @@ -0,0 +1,9 @@ +# End-To-End (e2e) Test for APM UI + +**Run E2E tests** + +```sh +x-pack/plugins/apm/e2e/run-e2e.sh +``` + +_Starts APM Server, Elasticsearch (with sample data) and runs the tests_ diff --git a/x-pack/plugins/apm/e2e/ci/entrypoint.sh b/x-pack/plugins/apm/e2e/ci/entrypoint.sh new file mode 100755 index 0000000000000..3349aa74dadb9 --- /dev/null +++ b/x-pack/plugins/apm/e2e/ci/entrypoint.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -xe + +## host.docker.internal is not available in native docker installations +kibana=$(dig +short host.docker.internal) +if [ -z "${kibana}" ] ; then + kibana=127.0.0.1 +fi + +export CYPRESS_BASE_URL=http://${kibana}:5701 + +## To avoid issues with the home and caching artifacts +export HOME=/tmp +npm config set cache ${HOME} + +## To avoid issues with volumes. +#rsync -rv --exclude=.git --exclude=docs \ +# --exclude=.cache --exclude=node_modules \ +# --exclude=test/ \ +# --exclude=src/ \ +# --exclude=packages/ \ +# --exclude=built_assets --exclude=target \ +# --exclude=data /app ${HOME}/ +#cd ${HOME}/app/x-pack/plugins/apm/e2e/cypress + +cd /app/x-pack/plugins/apm/e2e +## Install dependencies for cypress +CI=true npm install +yarn install + +# Wait for the kibana to be up and running +npm install wait-on +./node_modules/.bin/wait-on ${CYPRESS_BASE_URL}/status && echo 'Kibana is up and running' + +# Run cypress +npm run cypress:run diff --git a/x-pack/legacy/plugins/apm/e2e/ci/kibana.e2e.yml b/x-pack/plugins/apm/e2e/ci/kibana.e2e.yml similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/ci/kibana.e2e.yml rename to x-pack/plugins/apm/e2e/ci/kibana.e2e.yml diff --git a/x-pack/legacy/plugins/apm/e2e/ci/prepare-kibana.sh b/x-pack/plugins/apm/e2e/ci/prepare-kibana.sh similarity index 93% rename from x-pack/legacy/plugins/apm/e2e/ci/prepare-kibana.sh rename to x-pack/plugins/apm/e2e/ci/prepare-kibana.sh index 95e933cbdeec0..f383dd6d16f7f 100755 --- a/x-pack/legacy/plugins/apm/e2e/ci/prepare-kibana.sh +++ b/x-pack/plugins/apm/e2e/ci/prepare-kibana.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -ex -E2E_DIR=x-pack/legacy/plugins/apm/e2e +E2E_DIR=x-pack/plugins/apm/e2e echo "1/2 Install dependencies ..." # shellcheck disable=SC1091 source src/dev/ci_setup/setup_env.sh true diff --git a/x-pack/legacy/plugins/apm/e2e/ci/rerun-e2e.sh b/x-pack/plugins/apm/e2e/ci/rerun-e2e.sh similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/ci/rerun-e2e.sh rename to x-pack/plugins/apm/e2e/ci/rerun-e2e.sh diff --git a/x-pack/legacy/plugins/apm/e2e/ci/run-e2e.sh b/x-pack/plugins/apm/e2e/ci/run-e2e.sh similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/ci/run-e2e.sh rename to x-pack/plugins/apm/e2e/ci/run-e2e.sh diff --git a/x-pack/legacy/plugins/apm/e2e/cypress.json b/x-pack/plugins/apm/e2e/cypress.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress.json rename to x-pack/plugins/apm/e2e/cypress.json diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/fixtures/example.json b/x-pack/plugins/apm/e2e/cypress/fixtures/example.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/fixtures/example.json rename to x-pack/plugins/apm/e2e/cypress/fixtures/example.json diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/integration/apm.feature rename to x-pack/plugins/apm/e2e/cypress/integration/apm.feature diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/integration/helpers.ts b/x-pack/plugins/apm/e2e/cypress/integration/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/integration/helpers.ts rename to x-pack/plugins/apm/e2e/cypress/integration/helpers.ts diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js new file mode 100644 index 0000000000000..a462f4a504145 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -0,0 +1,16 @@ +/* + * 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. + */ + +module.exports = { + APM: { + 'Transaction duration charts': { + '1': '500 ms', + '2': '250 ms', + '3': '0 ms' + } + }, + __version: '4.2.0' +}; diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/plugins/index.js b/x-pack/plugins/apm/e2e/cypress/plugins/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/plugins/index.js rename to x-pack/plugins/apm/e2e/cypress/plugins/index.js diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/support/commands.js b/x-pack/plugins/apm/e2e/cypress/support/commands.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/support/commands.js rename to x-pack/plugins/apm/e2e/cypress/support/commands.js diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/support/index.ts b/x-pack/plugins/apm/e2e/cypress/support/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/support/index.ts rename to x-pack/plugins/apm/e2e/cypress/support/index.ts diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/support/step_definitions/apm.ts rename to x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/typings/index.d.ts b/x-pack/plugins/apm/e2e/cypress/typings/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/typings/index.d.ts rename to x-pack/plugins/apm/e2e/cypress/typings/index.d.ts diff --git a/x-pack/legacy/plugins/apm/e2e/cypress/webpack.config.js b/x-pack/plugins/apm/e2e/cypress/webpack.config.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/cypress/webpack.config.js rename to x-pack/plugins/apm/e2e/cypress/webpack.config.js diff --git a/x-pack/legacy/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/ingest-data/replay.js rename to x-pack/plugins/apm/e2e/ingest-data/replay.js diff --git a/x-pack/legacy/plugins/apm/e2e/package.json b/x-pack/plugins/apm/e2e/package.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/package.json rename to x-pack/plugins/apm/e2e/package.json diff --git a/x-pack/legacy/plugins/apm/e2e/run-e2e.sh b/x-pack/plugins/apm/e2e/run-e2e.sh similarity index 98% rename from x-pack/legacy/plugins/apm/e2e/run-e2e.sh rename to x-pack/plugins/apm/e2e/run-e2e.sh index f1a0c21408803..c46ae261d3af5 100755 --- a/x-pack/legacy/plugins/apm/e2e/run-e2e.sh +++ b/x-pack/plugins/apm/e2e/run-e2e.sh @@ -27,7 +27,7 @@ cd ${E2E_DIR} # Ask user to start Kibana ################################################## echo "\n${bold}To start Kibana please run the following command:${normal} -node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/legacy/plugins/apm/e2e/ci/kibana.e2e.yml" +node ./scripts/kibana --no-base-path --dev --no-dev-config --config x-pack/plugins/apm/e2e/ci/kibana.e2e.yml" # # Create tmp folder diff --git a/x-pack/legacy/plugins/apm/e2e/tsconfig.json b/x-pack/plugins/apm/e2e/tsconfig.json similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/tsconfig.json rename to x-pack/plugins/apm/e2e/tsconfig.json diff --git a/x-pack/legacy/plugins/apm/e2e/yarn.lock b/x-pack/plugins/apm/e2e/yarn.lock similarity index 100% rename from x-pack/legacy/plugins/apm/e2e/yarn.lock rename to x-pack/plugins/apm/e2e/yarn.lock diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 7ffdb676c740f..1a0ad67c7b696 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -1,13 +1,23 @@ { "id": "apm", - "server": true, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "apm" + "requiredPlugins": [ + "features", + "apm_oss", + "data", + "home", + "licensing", + "triggers_actions_ui" + ], + "optionalPlugins": [ + "cloud", + "usageCollection", + "taskManager", + "actions", + "alerting" ], - "ui": false, - "requiredPlugins": ["apm_oss", "data", "home", "licensing"], - "optionalPlugins": ["cloud", "usageCollection", "taskManager","actions", "alerting"] + "server": true, + "ui": true, + "configPath": ["xpack", "apm"] } diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx new file mode 100644 index 0000000000000..c3738329219a8 --- /dev/null +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -0,0 +1,123 @@ +/* + * 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 { ApmRoute } from '@elastic/apm-rum-react'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Route, Router, Switch } from 'react-router-dom'; +import styled from 'styled-components'; +import { CoreStart, AppMountParameters } from '../../../../../src/core/public'; +import { ApmPluginSetupDeps } from '../plugin'; +import { ApmPluginContext } from '../context/ApmPluginContext'; +import { LicenseProvider } from '../context/LicenseContext'; +import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; +import { LocationProvider } from '../context/LocationContext'; +import { MatchedRouteProvider } from '../context/MatchedRouteContext'; +import { UrlParamsProvider } from '../context/UrlParamsContext'; +import { AlertsContextProvider } from '../../../triggers_actions_ui/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { px, unit, units } from '../style/variables'; +import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; +import { APMIndicesPermission } from '../components/app/APMIndicesPermission'; +import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; +import { routes } from '../components/app/Main/route_config'; +import { history } from '../utils/history'; +import { ConfigSchema } from '..'; +import 'react-vis/dist/style.css'; + +const MainContainer = styled.div` + min-width: ${px(unit * 50)}; + padding: ${px(units.plus)}; + height: 100%; +`; + +const App = () => { + return ( + <MainContainer data-test-subj="apmMainContainer" role="main"> + <UpdateBreadcrumbs routes={routes} /> + <Route component={ScrollToTopOnPathChange} /> + <APMIndicesPermission> + <Switch> + {routes.map((route, i) => ( + <ApmRoute key={i} {...route} /> + ))} + </Switch> + </APMIndicesPermission> + </MainContainer> + ); +}; + +const ApmAppRoot = ({ + core, + deps, + routerHistory, + config +}: { + core: CoreStart; + deps: ApmPluginSetupDeps; + routerHistory: typeof history; + config: ConfigSchema; +}) => { + const i18nCore = core.i18n; + const plugins = deps; + const apmPluginContextValue = { + config, + core, + plugins + }; + return ( + <ApmPluginContext.Provider value={apmPluginContextValue}> + <AlertsContextProvider + value={{ + http: core.http, + docLinks: core.docLinks, + toastNotifications: core.notifications.toasts, + actionTypeRegistry: plugins.triggers_actions_ui.actionTypeRegistry, + alertTypeRegistry: plugins.triggers_actions_ui.alertTypeRegistry + }} + > + <KibanaContextProvider services={{ ...core, ...plugins }}> + <i18nCore.Context> + <Router history={routerHistory}> + <LocationProvider> + <MatchedRouteProvider routes={routes}> + <UrlParamsProvider> + <LoadingIndicatorProvider> + <LicenseProvider> + <App /> + </LicenseProvider> + </LoadingIndicatorProvider> + </UrlParamsProvider> + </MatchedRouteProvider> + </LocationProvider> + </Router> + </i18nCore.Context> + </KibanaContextProvider> + </AlertsContextProvider> + </ApmPluginContext.Provider> + ); +}; + +/** + * This module is rendered asynchronously in the Kibana platform. + */ +export const renderApp = ( + core: CoreStart, + deps: ApmPluginSetupDeps, + { element }: AppMountParameters, + config: ConfigSchema +) => { + ReactDOM.render( + <ApmAppRoot + core={core} + deps={deps} + routerHistory={history} + config={config} + />, + element + ); + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx b/x-pack/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx rename to x-pack/plugins/apm/public/components/app/APMIndicesPermission/__test__/APMIndicesPermission.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/index.tsx b/x-pack/plugins/apm/public/components/app/APMIndicesPermission/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/APMIndicesPermission/index.tsx rename to x-pack/plugins/apm/public/components/app/APMIndicesPermission/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx index 33774c941ffd6..5982346d97b89 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ErrorTabs.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; export interface ErrorTab { key: 'log_stacktrace' | 'exception_stacktrace' | 'metadata'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx index 75e518a278aea..faec93013886c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/ExceptionStacktrace.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { EuiTitle } from '@elastic/eui'; -import { Exception } from '../../../../../../../../plugins/apm/typings/es_schemas/raw/error_raw'; +import { Exception } from '../../../../../typings/es_schemas/raw/error_raw'; import { Stacktrace } from '../../../shared/Stacktrace'; import { CauseStacktrace } from '../../../shared/Stacktrace/CauseStacktrace'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx new file mode 100644 index 0000000000000..9e2fd776e67a3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -0,0 +1,209 @@ +/* + * 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 { + EuiButtonEmpty, + EuiPanel, + EuiSpacer, + EuiTab, + EuiTabs, + EuiTitle, + EuiIcon, + EuiToolTip +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Location } from 'history'; +import React from 'react'; +import styled from 'styled-components'; +import { first } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { px, unit, units } from '../../../../style/variables'; +import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { history } from '../../../../utils/history'; +import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata'; +import { Stacktrace } from '../../../shared/Stacktrace'; +import { + ErrorTab, + exceptionStacktraceTab, + getTabs, + logStacktraceTab +} from './ErrorTabs'; +import { Summary } from '../../../shared/Summary'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem'; +import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; +import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem'; +import { ExceptionStacktrace } from './ExceptionStacktrace'; + +const HeaderContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: ${px(unit)}; +`; + +const TransactionLinkName = styled.div` + margin-left: ${px(units.half)}; + display: inline-block; + vertical-align: middle; +`; + +interface Props { + errorGroup: ErrorGroupAPIResponse; + urlParams: IUrlParams; + location: Location; +} + +// TODO: Move query-string-based tabs into a re-usable component? +function getCurrentTab( + tabs: ErrorTab[] = [], + currentTabKey: string | undefined +) { + const selectedTab = tabs.find(({ key }) => key === currentTabKey); + return selectedTab ? selectedTab : first(tabs) || {}; +} + +export function DetailView({ errorGroup, urlParams, location }: Props) { + const { transaction, error, occurrencesCount } = errorGroup; + + if (!error) { + return null; + } + + const tabs = getTabs(error); + const currentTab = getCurrentTab(tabs, urlParams.detailTab); + + const errorUrl = error.error.page?.url || error.url?.full; + + const method = error.http?.request?.method; + const status = error.http?.response?.status_code; + + return ( + <EuiPanel> + <HeaderContainer> + <EuiTitle size="s"> + <h3> + {i18n.translate( + 'xpack.apm.errorGroupDetails.errorOccurrenceTitle', + { + defaultMessage: 'Error occurrence' + } + )} + </h3> + </EuiTitle> + <DiscoverErrorLink error={error} kuery={urlParams.kuery}> + <EuiButtonEmpty iconType="discoverApp"> + {i18n.translate( + 'xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel', + { + defaultMessage: + 'View {occurrencesCount} {occurrencesCount, plural, one {occurrence} other {occurrences}} in Discover.', + values: { occurrencesCount } + } + )} + </EuiButtonEmpty> + </DiscoverErrorLink> + </HeaderContainer> + + <Summary + items={[ + <TimestampTooltip time={error.timestamp.us / 1000} />, + errorUrl && method ? ( + <HttpInfoSummaryItem + url={errorUrl} + method={method} + status={status} + /> + ) : null, + transaction && transaction.user_agent ? ( + <UserAgentSummaryItem {...transaction.user_agent} /> + ) : null, + transaction && ( + <EuiToolTip + content={i18n.translate( + 'xpack.apm.errorGroupDetails.relatedTransactionSample', + { + defaultMessage: 'Related transaction sample' + } + )} + > + <TransactionDetailLink + traceId={transaction.trace.id} + transactionId={transaction.transaction.id} + transactionName={transaction.transaction.name} + transactionType={transaction.transaction.type} + serviceName={transaction.service.name} + > + <EuiIcon type="merge" /> + <TransactionLinkName> + {transaction.transaction.name} + </TransactionLinkName> + </TransactionDetailLink> + </EuiToolTip> + ) + ]} + /> + + <EuiSpacer /> + + <EuiTabs> + {tabs.map(({ key, label }) => { + return ( + <EuiTab + onClick={() => { + history.replace({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + detailTab: key + }) + }); + }} + isSelected={currentTab.key === key} + key={key} + > + {label} + </EuiTab> + ); + })} + </EuiTabs> + <EuiSpacer /> + <TabContent error={error} currentTab={currentTab} /> + </EuiPanel> + ); +} + +function TabContent({ + error, + currentTab +}: { + error: APMError; + currentTab: ErrorTab; +}) { + const codeLanguage = error.service.language?.name; + const exceptions = error.error.exception || []; + const logStackframes = error.error.log?.stacktrace; + + switch (currentTab.key) { + case logStacktraceTab.key: + return ( + <Stacktrace stackframes={logStackframes} codeLanguage={codeLanguage} /> + ); + case exceptionStacktraceTab.key: + return ( + <ExceptionStacktrace + codeLanguage={codeLanguage} + exceptions={exceptions} + /> + ); + default: + return <ErrorMetadata error={error} />; + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx new file mode 100644 index 0000000000000..c40c711a590be --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -0,0 +1,209 @@ +/* + * 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 { + EuiBadge, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle +} from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import styled from 'styled-components'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables'; +import { ApmHeader } from '../../shared/ApmHeader'; +import { DetailView } from './DetailView'; +import { ErrorDistribution } from './Distribution'; +import { useLocation } from '../../../hooks/useLocation'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useTrackPageview } from '../../../../../observability/public'; + +const Titles = styled.div` + margin-bottom: ${px(units.plus)}; +`; + +const Label = styled.div` + margin-bottom: ${px(units.quarter)}; + font-size: ${fontSizes.small}; + color: ${theme.euiColorMediumShade}; +`; + +const Message = styled.div` + font-family: ${fontFamilyCode}; + font-weight: bold; + font-size: ${fontSizes.large}; + margin-bottom: ${px(units.half)}; +`; + +const Culprit = styled.div` + font-family: ${fontFamilyCode}; +`; + +function getShortGroupId(errorGroupId?: string) { + if (!errorGroupId) { + return NOT_AVAILABLE_LABEL; + } + + return errorGroupId.slice(0, 5); +} + +export function ErrorGroupDetails() { + const location = useLocation(); + const { urlParams, uiFilters } = useUrlParams(); + const { serviceName, start, end, errorGroupId } = urlParams; + + const { data: errorGroupData } = useFetcher( + callApmApi => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/{groupId}', + params: { + path: { + serviceName, + groupId: errorGroupId + }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, + [serviceName, start, end, errorGroupId, uiFilters] + ); + + const { data: errorDistributionData } = useFetcher( + callApmApi => { + if (serviceName && start && end && errorGroupId) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName + }, + query: { + start, + end, + groupId: errorGroupId, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, + [serviceName, start, end, errorGroupId, uiFilters] + ); + + useTrackPageview({ app: 'apm', path: 'error_group_details' }); + useTrackPageview({ app: 'apm', path: 'error_group_details', delay: 15000 }); + + if (!errorGroupData || !errorDistributionData) { + return null; + } + + // If there are 0 occurrences, show only distribution chart w. empty message + const showDetails = errorGroupData.occurrencesCount !== 0; + const logMessage = errorGroupData.error?.error.log?.message; + const excMessage = errorGroupData.error?.error.exception?.[0].message; + const culprit = errorGroupData.error?.error.culprit; + const isUnhandled = + errorGroupData.error?.error.exception?.[0].handled === false; + + return ( + <div> + <ApmHeader> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle> + <h1> + {i18n.translate('xpack.apm.errorGroupDetails.errorGroupTitle', { + defaultMessage: 'Error group {errorGroupId}', + values: { + errorGroupId: getShortGroupId(urlParams.errorGroupId) + } + })} + </h1> + </EuiTitle> + </EuiFlexItem> + {isUnhandled && ( + <EuiFlexItem grow={false}> + <EuiBadge color="warning"> + {i18n.translate('xpack.apm.errorGroupDetails.unhandledLabel', { + defaultMessage: 'Unhandled' + })} + </EuiBadge> + </EuiFlexItem> + )} + </EuiFlexGroup> + </ApmHeader> + + <EuiSpacer size="s" /> + + <EuiPanel> + {showDetails && ( + <Titles> + <EuiText> + {logMessage && ( + <Fragment> + <Label> + {i18n.translate( + 'xpack.apm.errorGroupDetails.logMessageLabel', + { + defaultMessage: 'Log message' + } + )} + </Label> + <Message>{logMessage}</Message> + </Fragment> + )} + <Label> + {i18n.translate( + 'xpack.apm.errorGroupDetails.exceptionMessageLabel', + { + defaultMessage: 'Exception message' + } + )} + </Label> + <Message>{excMessage || NOT_AVAILABLE_LABEL}</Message> + <Label> + {i18n.translate('xpack.apm.errorGroupDetails.culpritLabel', { + defaultMessage: 'Culprit' + })} + </Label> + <Culprit>{culprit || NOT_AVAILABLE_LABEL}</Culprit> + </EuiText> + </Titles> + )} + + <ErrorDistribution + distribution={errorDistributionData} + title={i18n.translate( + 'xpack.apm.errorGroupDetails.occurrencesChartLabel', + { + defaultMessage: 'Occurrences' + } + )} + /> + </EuiPanel> + <EuiSpacer size="s" /> + {showDetails && ( + <DetailView + errorGroup={errorGroupData} + urlParams={urlParams} + location={location} + /> + )} + </div> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap similarity index 99% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index afa0cb51cd108..0fbf0a5c7a27d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -141,6 +141,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > <span className="euiTableCellContent__text" + title="Group ID" > Group ID </span> @@ -162,6 +163,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > <span className="euiTableCellContent__text" + title="Type" > Type </span> @@ -183,6 +185,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > <span className="euiTableCellContent__text" + title="Error message and culprit" > Error message and culprit </span> @@ -231,6 +234,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > <span className="euiTableCellContent__text" + title="Occurrences; Sorted in descending order" > Occurrences </span> @@ -272,6 +276,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` > <span className="euiTableCellContent__text" + title="Latest occurrence" > Latest occurrence </span> @@ -519,6 +524,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > <span className="euiTableCellContent__text" + title="Group ID" > Group ID </span> @@ -540,6 +546,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > <span className="euiTableCellContent__text" + title="Type" > Type </span> @@ -561,6 +568,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > <span className="euiTableCellContent__text" + title="Error message and culprit" > Error message and culprit </span> @@ -609,6 +617,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > <span className="euiTableCellContent__text" + title="Occurrences; Sorted in descending order" > Occurrences </span> @@ -650,6 +659,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` > <span className="euiTableCellContent__text" + title="Latest occurrence" > Latest occurrence </span> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/props.json diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx new file mode 100644 index 0000000000000..695d3463d3b3d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.tsx @@ -0,0 +1,199 @@ +/* + * 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 { EuiBadge, EuiToolTip } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ErrorGroupListAPIResponse } from '../../../../../server/lib/errors/get_error_groups'; +import { + fontFamilyCode, + fontSizes, + px, + truncate, + unit +} from '../../../../style/variables'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { ManagedTable } from '../../../shared/ManagedTable'; +import { ErrorDetailLink } from '../../../shared/Links/apm/ErrorDetailLink'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; +import { APMQueryParams } from '../../../shared/Links/url_helpers'; + +const GroupIdLink = styled(ErrorDetailLink)` + font-family: ${fontFamilyCode}; +`; + +const MessageAndCulpritCell = styled.div` + ${truncate('100%')}; +`; + +const ErrorLink = styled(ErrorOverviewLink)` + ${truncate('100%')}; +`; + +const MessageLink = styled(ErrorDetailLink)` + font-family: ${fontFamilyCode}; + font-size: ${fontSizes.large}; + ${truncate('100%')}; +`; + +const Culprit = styled.div` + font-family: ${fontFamilyCode}; +`; + +interface Props { + items: ErrorGroupListAPIResponse; +} + +const ErrorGroupList: React.FC<Props> = props => { + const { items } = props; + const { urlParams } = useUrlParams(); + const { serviceName } = urlParams; + + if (!serviceName) { + throw new Error('Service name is required'); + } + + const columns = useMemo( + () => [ + { + name: i18n.translate('xpack.apm.errorsTable.groupIdColumnLabel', { + defaultMessage: 'Group ID' + }), + field: 'groupId', + sortable: false, + width: px(unit * 6), + render: (groupId: string) => { + return ( + <GroupIdLink serviceName={serviceName} errorGroupId={groupId}> + {groupId.slice(0, 5) || NOT_AVAILABLE_LABEL} + </GroupIdLink> + ); + } + }, + { + name: i18n.translate('xpack.apm.errorsTable.typeColumnLabel', { + defaultMessage: 'Type' + }), + field: 'type', + sortable: false, + render: (type: string, item: ErrorGroupListAPIResponse[0]) => { + return ( + <ErrorLink + title={type} + serviceName={serviceName} + query={ + { + ...urlParams, + kuery: `error.exception.type:${type}` + } as APMQueryParams + } + > + {type} + </ErrorLink> + ); + } + }, + { + name: i18n.translate( + 'xpack.apm.errorsTable.errorMessageAndCulpritColumnLabel', + { + defaultMessage: 'Error message and culprit' + } + ), + field: 'message', + sortable: false, + width: '50%', + render: (message: string, item: ErrorGroupListAPIResponse[0]) => { + return ( + <MessageAndCulpritCell> + <EuiToolTip + id="error-message-tooltip" + content={message || NOT_AVAILABLE_LABEL} + > + <MessageLink + serviceName={serviceName} + errorGroupId={item.groupId} + > + {message || NOT_AVAILABLE_LABEL} + </MessageLink> + </EuiToolTip> + <br /> + <EuiToolTip + id="error-culprit-tooltip" + content={item.culprit || NOT_AVAILABLE_LABEL} + > + <Culprit>{item.culprit || NOT_AVAILABLE_LABEL}</Culprit> + </EuiToolTip> + </MessageAndCulpritCell> + ); + } + }, + { + name: '', + field: 'handled', + sortable: false, + align: 'right', + render: (isUnhandled: boolean) => + isUnhandled === false && ( + <EuiBadge color="warning"> + {i18n.translate('xpack.apm.errorsTable.unhandledLabel', { + defaultMessage: 'Unhandled' + })} + </EuiBadge> + ) + }, + { + name: i18n.translate('xpack.apm.errorsTable.occurrencesColumnLabel', { + defaultMessage: 'Occurrences' + }), + field: 'occurrenceCount', + sortable: true, + dataType: 'number', + render: (value?: number) => + value ? numeral(value).format('0.[0]a') : NOT_AVAILABLE_LABEL + }, + { + field: 'latestOccurrenceAt', + sortable: true, + name: i18n.translate( + 'xpack.apm.errorsTable.latestOccurrenceColumnLabel', + { + defaultMessage: 'Latest occurrence' + } + ), + align: 'right', + render: (value?: number) => + value ? ( + <TimestampTooltip time={value} timeUnit="minutes" /> + ) : ( + NOT_AVAILABLE_LABEL + ) + } + ], + [serviceName, urlParams] + ); + + return ( + <ManagedTable + noItemsMessage={i18n.translate('xpack.apm.errorsTable.noErrorsLabel', { + defaultMessage: 'No errors were found' + })} + items={items} + columns={columns} + initialPageSize={25} + initialSortField="occurrenceCount" + initialSortDirection="desc" + sortItems={false} + /> + ); +}; + +export { ErrorGroupList }; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx new file mode 100644 index 0000000000000..604893952d9d6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/index.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { ErrorDistribution } from '../ErrorGroupDetails/Distribution'; +import { ErrorGroupList } from './List'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useTrackPageview } from '../../../../../observability/public'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; + +const ErrorGroupOverview: React.FC = () => { + const { urlParams, uiFilters } = useUrlParams(); + + const { serviceName, start, end, sortField, sortDirection } = urlParams; + + const { data: errorDistributionData } = useFetcher( + callApmApi => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors/distribution', + params: { + path: { + serviceName + }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, + [serviceName, start, end, uiFilters] + ); + + const { data: errorGroupListData } = useFetcher( + callApmApi => { + const normalizedSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; + + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/errors', + params: { + path: { + serviceName + }, + query: { + start, + end, + sortField, + sortDirection: normalizedSortDirection, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, + [serviceName, start, end, sortField, sortDirection, uiFilters] + ); + + useTrackPageview({ + app: 'apm', + path: 'error_group_overview' + }); + useTrackPageview({ app: 'apm', path: 'error_group_overview', delay: 15000 }); + + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps<typeof LocalUIFilters> = { + filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], + params: { + serviceName + }, + projection: PROJECTION.ERROR_GROUPS + }; + + return config; + }, [serviceName]); + + if (!errorDistributionData || !errorGroupListData) { + return null; + } + + return ( + <> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <LocalUIFilters {...localUIFiltersConfig} /> + </EuiFlexItem> + <EuiFlexItem grow={7}> + <EuiFlexGroup> + <EuiFlexItem> + <EuiPanel> + <ErrorDistribution + distribution={errorDistributionData} + title={i18n.translate( + 'xpack.apm.serviceDetails.metrics.errorOccurrencesChartTitle', + { + defaultMessage: 'Error occurrences' + } + )} + /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EuiPanel> + <EuiTitle size="xs"> + <h3>Errors</h3> + </EuiTitle> + <EuiSpacer size="s" /> + + <ErrorGroupList items={errorGroupListData} /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +export { ErrorGroupOverview }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/plugins/apm/public/components/app/Home/Home.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx rename to x-pack/plugins/apm/public/components/app/Home/Home.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 2b1f835a14f4a..9f461eeb5b6fc 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Home component should render services 1`] = ` value={ Object { "config": Object { - "indexPatternTitle": "apm-*", "serviceMapEnabled": true, "ui": Object { "enabled": false, @@ -46,7 +45,6 @@ exports[`Home component should render traces 1`] = ` value={ Object { "config": Object { - "indexPatternTitle": "apm-*", "serviceMapEnabled": true, "ui": Object { "enabled": false, diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx b/x-pack/plugins/apm/public/components/app/Home/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx rename to x-pack/plugins/apm/public/components/app/Home/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx rename to x-pack/plugins/apm/public/components/app/Main/ProvideBreadcrumbs.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx b/x-pack/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx rename to x-pack/plugins/apm/public/components/app/Main/ScrollToTopOnPathChange.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx rename to x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx rename to x-pack/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/Main/__snapshots__/UpdateBreadcrumbs.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx b/x-pack/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx rename to x-pack/plugins/apm/public/components/app/Main/__test__/ProvideBreadcrumbs.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx new file mode 100644 index 0000000000000..6d1db8c5dc6d4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -0,0 +1,253 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; +import { ErrorGroupDetails } from '../../ErrorGroupDetails'; +import { ServiceDetails } from '../../ServiceDetails'; +import { TransactionDetails } from '../../TransactionDetails'; +import { Home } from '../../Home'; +import { BreadcrumbRoute } from '../ProvideBreadcrumbs'; +import { RouteName } from './route_names'; +import { Settings } from '../../Settings'; +import { AgentConfigurations } from '../../Settings/AgentConfigurations'; +import { ApmIndices } from '../../Settings/ApmIndices'; +import { toQuery } from '../../../shared/Links/url_helpers'; +import { ServiceNodeMetrics } from '../../ServiceNodeMetrics'; +import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams'; +import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; +import { TraceLink } from '../../TraceLink'; +import { CustomizeUI } from '../../Settings/CustomizeUI'; +import { + EditAgentConfigurationRouteHandler, + CreateAgentConfigurationRouteHandler +} from './route_handlers/agent_configuration'; + +const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', { + defaultMessage: 'Metrics' +}); + +interface RouteParams { + serviceName: string; +} + +const renderAsRedirectTo = (to: string) => { + return ({ location }: RouteComponentProps<RouteParams>) => ( + <Redirect + to={{ + ...location, + pathname: to + }} + /> + ); +}; + +export const routes: BreadcrumbRoute[] = [ + { + exact: true, + path: '/', + render: renderAsRedirectTo('/services'), + breadcrumb: 'APM', + name: RouteName.HOME + }, + { + exact: true, + path: '/services', + component: () => <Home tab="services" />, + breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { + defaultMessage: 'Services' + }), + name: RouteName.SERVICES + }, + { + exact: true, + path: '/traces', + component: () => <Home tab="traces" />, + breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { + defaultMessage: 'Traces' + }), + name: RouteName.TRACES + }, + { + exact: true, + path: '/settings', + render: renderAsRedirectTo('/settings/agent-configuration'), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', { + defaultMessage: 'Settings' + }), + name: RouteName.SETTINGS + }, + { + exact: true, + path: '/settings/apm-indices', + component: () => ( + <Settings> + <ApmIndices /> + </Settings> + ), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', { + defaultMessage: 'Indices' + }), + name: RouteName.INDICES + }, + { + exact: true, + path: '/settings/agent-configuration', + component: () => ( + <Settings> + <AgentConfigurations /> + </Settings> + ), + breadcrumb: i18n.translate( + 'xpack.apm.breadcrumb.settings.agentConfigurationTitle', + { defaultMessage: 'Agent Configuration' } + ), + name: RouteName.AGENT_CONFIGURATION + }, + + { + exact: true, + path: '/settings/agent-configuration/create', + breadcrumb: i18n.translate( + 'xpack.apm.breadcrumb.settings.createAgentConfigurationTitle', + { defaultMessage: 'Create Agent Configuration' } + ), + name: RouteName.AGENT_CONFIGURATION_CREATE, + component: () => <CreateAgentConfigurationRouteHandler /> + }, + { + exact: true, + path: '/settings/agent-configuration/edit', + breadcrumb: i18n.translate( + 'xpack.apm.breadcrumb.settings.editAgentConfigurationTitle', + { defaultMessage: 'Edit Agent Configuration' } + ), + name: RouteName.AGENT_CONFIGURATION_EDIT, + component: () => <EditAgentConfigurationRouteHandler /> + }, + { + exact: true, + path: '/services/:serviceName', + breadcrumb: ({ match }) => match.params.serviceName, + render: (props: RouteComponentProps<RouteParams>) => + renderAsRedirectTo( + `/services/${props.match.params.serviceName}/transactions` + )(props), + name: RouteName.SERVICE + }, + // errors + { + exact: true, + path: '/services/:serviceName/errors/:groupId', + component: ErrorGroupDetails, + breadcrumb: ({ match }) => match.params.groupId, + name: RouteName.ERROR + }, + { + exact: true, + path: '/services/:serviceName/errors', + component: () => <ServiceDetails tab="errors" />, + breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', { + defaultMessage: 'Errors' + }), + name: RouteName.ERRORS + }, + // transactions + { + exact: true, + path: '/services/:serviceName/transactions', + component: () => <ServiceDetails tab="transactions" />, + breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', { + defaultMessage: 'Transactions' + }), + name: RouteName.TRANSACTIONS + }, + // metrics + { + exact: true, + path: '/services/:serviceName/metrics', + component: () => <ServiceDetails tab="metrics" />, + breadcrumb: metricsBreadcrumb, + name: RouteName.METRICS + }, + // service nodes, only enabled for java agents for now + { + exact: true, + path: '/services/:serviceName/nodes', + component: () => <ServiceDetails tab="nodes" />, + breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', { + defaultMessage: 'JVMs' + }), + name: RouteName.SERVICE_NODES + }, + // node metrics + { + exact: true, + path: '/services/:serviceName/nodes/:serviceNodeName/metrics', + component: () => <ServiceNodeMetrics />, + breadcrumb: ({ location }) => { + const { serviceNodeName } = resolveUrlParams(location, {}); + + if (serviceNodeName === SERVICE_NODE_NAME_MISSING) { + return UNIDENTIFIED_SERVICE_NODES_LABEL; + } + + return serviceNodeName || ''; + }, + name: RouteName.SERVICE_NODE_METRICS + }, + { + exact: true, + path: '/services/:serviceName/transactions/view', + component: TransactionDetails, + breadcrumb: ({ location }) => { + const query = toQuery(location.search); + return query.transactionName as string; + }, + name: RouteName.TRANSACTION_NAME + }, + { + exact: true, + path: '/link-to/trace/:traceId', + component: TraceLink, + breadcrumb: null, + name: RouteName.LINK_TO_TRACE + }, + + { + exact: true, + path: '/service-map', + component: () => <Home tab="service-map" />, + breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { + defaultMessage: 'Service Map' + }), + name: RouteName.SERVICE_MAP + }, + { + exact: true, + path: '/services/:serviceName/service-map', + component: () => <ServiceDetails tab="service-map" />, + breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { + defaultMessage: 'Service Map' + }), + name: RouteName.SINGLE_SERVICE_MAP + }, + { + exact: true, + path: '/settings/customize-ui', + component: () => ( + <Settings> + <CustomizeUI /> + </Settings> + ), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.customizeUI', { + defaultMessage: 'Customize UI' + }), + name: RouteName.CUSTOMIZE_UI + } +]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx rename to x-pack/plugins/apm/public/components/app/Main/route_config/route_handlers/agent_configuration.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx rename to x-pack/plugins/apm/public/components/app/Main/route_config/route_names.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx new file mode 100644 index 0000000000000..a1ccb04e3c42a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/AlertingFlyout/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { AlertType } from '../../../../../../common/alert_types'; +import { AlertAdd } from '../../../../../../../triggers_actions_ui/public'; + +type AlertAddProps = React.ComponentProps<typeof AlertAdd>; + +interface Props { + addFlyoutVisible: AlertAddProps['addFlyoutVisible']; + setAddFlyoutVisibility: AlertAddProps['setAddFlyoutVisibility']; + alertType: AlertType | null; +} + +export function AlertingFlyout(props: Props) { + const { addFlyoutVisible, setAddFlyoutVisibility, alertType } = props; + + return alertType ? ( + <AlertAdd + addFlyoutVisible={addFlyoutVisible} + setAddFlyoutVisibility={setAddFlyoutVisibility} + consumer="apm" + alertTypeId={alertType} + canChangeTrigger={false} + /> + ) : null; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx new file mode 100644 index 0000000000000..75c6c79bc804a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -0,0 +1,146 @@ +/* + * 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 { + EuiButtonEmpty, + EuiContextMenu, + EuiPopover, + EuiContextMenuPanelDescriptor +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { AlertType } from '../../../../../common/alert_types'; +import { AlertingFlyout } from './AlertingFlyout'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; + +const alertLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.alerts', + { + defaultMessage: 'Alerts' + } +); + +const createThresholdAlertLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert', + { + defaultMessage: 'Create threshold alert' + } +); + +const CREATE_THRESHOLD_ALERT_PANEL_ID = 'create_threshold'; + +interface Props { + canReadAlerts: boolean; + canSaveAlerts: boolean; +} + +export function AlertIntegrations(props: Props) { + const { canSaveAlerts, canReadAlerts } = props; + + const plugin = useApmPluginContext(); + + const [popoverOpen, setPopoverOpen] = useState(false); + + const [alertType, setAlertType] = useState<AlertType | null>(null); + + const button = ( + <EuiButtonEmpty + iconType="arrowDown" + iconSide="right" + onClick={() => setPopoverOpen(true)} + > + {i18n.translate('xpack.apm.serviceDetails.alertsMenu.alerts', { + defaultMessage: 'Alerts' + })} + </EuiButtonEmpty> + ); + + const panels: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + title: alertLabel, + items: [ + ...(canSaveAlerts + ? [ + { + name: createThresholdAlertLabel, + panel: CREATE_THRESHOLD_ALERT_PANEL_ID, + icon: 'bell' + } + ] + : []), + ...(canReadAlerts + ? [ + { + name: i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.viewActiveAlerts', + { + defaultMessage: 'View active alerts' + } + ), + href: plugin.core.http.basePath.prepend( + '/app/kibana#/management/kibana/triggersActions/alerts' + ), + icon: 'tableOfContents' + } + ] + : []) + ] + }, + { + id: CREATE_THRESHOLD_ALERT_PANEL_ID, + title: createThresholdAlertLabel, + items: [ + { + name: i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', + { + defaultMessage: 'Transaction duration' + } + ), + onClick: () => { + setAlertType(AlertType.TransactionDuration); + } + }, + { + name: i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.errorRate', + { + defaultMessage: 'Error rate' + } + ), + onClick: () => { + setAlertType(AlertType.ErrorRate); + } + } + ] + } + ]; + + return ( + <> + <EuiPopover + id="integrations-menu" + button={button} + isOpen={popoverOpen} + closePopover={() => setPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downRight" + > + <EuiContextMenu initialPanelId={0} panels={panels} /> + </EuiPopover> + <AlertingFlyout + alertType={alertType} + addFlyoutVisible={!!alertType} + setAddFlyoutVisibility={visible => { + if (!visible) { + setAlertType(null); + } + }} + /> + </> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index 131bb7f65d4b3..7ab2f7bac8ae2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -7,10 +7,7 @@ import { EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { - isJavaAgentName, - isRumAgentName -} from '../../../../../../../plugins/apm/common/agent_name'; +import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; import { useAgentName } from '../../../hooks/useAgentName'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useUrlParams } from '../../../hooks/useUrlParams'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/TransactionSelect.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx new file mode 100644 index 0000000000000..b7480a42ba94b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx @@ -0,0 +1,159 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { Component } from 'react'; +import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { startMLJob } from '../../../../../services/rest/ml'; +import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; +import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; +import { MachineLearningFlyoutView } from './view'; +import { ApmPluginContext } from '../../../../../context/ApmPluginContext'; + +interface Props { + isOpen: boolean; + onClose: () => void; + urlParams: IUrlParams; +} + +interface State { + isCreatingJob: boolean; +} + +export class MachineLearningFlyout extends Component<Props, State> { + static contextType = ApmPluginContext; + + public state: State = { + isCreatingJob: false + }; + + public onClickCreate = async ({ + transactionType + }: { + transactionType: string; + }) => { + this.setState({ isCreatingJob: true }); + try { + const { http } = this.context.core; + const { serviceName } = this.props.urlParams; + if (!serviceName) { + throw new Error('Service name is required to create this ML job'); + } + const res = await startMLJob({ http, serviceName, transactionType }); + const didSucceed = res.datafeeds[0].success && res.jobs[0].success; + if (!didSucceed) { + throw new Error('Creating ML job failed'); + } + this.addSuccessToast({ transactionType }); + } catch (e) { + this.addErrorToast(); + } + + this.setState({ isCreatingJob: false }); + this.props.onClose(); + }; + + public addErrorToast = () => { + const { core } = this.context; + + const { urlParams } = this.props; + const { serviceName } = urlParams; + + if (!serviceName) { + return; + } + + core.notifications.toasts.addWarning({ + title: i18n.translate( + 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationTitle', + { + defaultMessage: 'Job creation failed' + } + ), + text: toMountPoint( + <p> + {i18n.translate( + 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreationFailedNotificationText', + { + defaultMessage: + 'Your current license may not allow for creating machine learning jobs, or this job may already exist.' + } + )} + </p> + ) + }); + }; + + public addSuccessToast = ({ + transactionType + }: { + transactionType: string; + }) => { + const { core } = this.context; + const { urlParams } = this.props; + const { serviceName } = urlParams; + + if (!serviceName) { + return; + } + + core.notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationTitle', + { + defaultMessage: 'Job successfully created' + } + ), + text: toMountPoint( + <p> + {i18n.translate( + 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText', + { + defaultMessage: + 'The analysis is now running for {serviceName} ({transactionType}). It might take a while before results are added to the response times graph.', + values: { + serviceName, + transactionType + } + } + )}{' '} + <ApmPluginContext.Provider value={this.context}> + <MLJobLink + serviceName={serviceName} + transactionType={transactionType} + > + {i18n.translate( + 'xpack.apm.serviceDetails.enableAnomalyDetectionPanel.jobCreatedNotificationText.viewJobLinkText', + { + defaultMessage: 'View job' + } + )} + </MLJobLink> + </ApmPluginContext.Provider> + </p> + ) + }); + }; + + public render() { + const { isOpen, onClose, urlParams } = this.props; + const { serviceName } = urlParams; + const { isCreatingJob } = this.state; + + if (!isOpen || !serviceName) { + return null; + } + + return ( + <MachineLearningFlyoutView + isCreatingJob={isCreatingJob} + onClickCreate={this.onClickCreate} + onClose={onClose} + urlParams={urlParams} + /> + ); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 85254bee12e13..3bbd8a01d0549 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -30,12 +30,13 @@ import { padLeft, range } from 'lodash'; import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; -import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { KibanaLink } from '../../../shared/Links/KibanaLink'; import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; import { ApmPluginContext } from '../../../../context/ApmPluginContext'; +import { getApmIndexPatternTitle } from '../../../../services/rest/index_pattern'; type ScheduleKey = keyof Schedule; @@ -149,11 +150,7 @@ export class WatcherFlyout extends Component< this.setState({ slackUrl: event.target.value }); }; - public createWatch = ({ - indexPatternTitle - }: { - indexPatternTitle: string; - }) => () => { + public createWatch = () => { const { serviceName } = this.props.urlParams; const { core } = this.context; @@ -190,19 +187,21 @@ export class WatcherFlyout extends Component< unit: 'h' }; - return createErrorGroupWatch({ - http: core.http, - emails, - schedule, - serviceName, - slackUrl, - threshold: this.state.threshold, - timeRange, - apmIndexPatternTitle: indexPatternTitle - }) - .then((id: string) => { - this.props.onClose(); - this.addSuccessToast(id); + return getApmIndexPatternTitle() + .then(indexPatternTitle => { + return createErrorGroupWatch({ + http: core.http, + emails, + schedule, + serviceName, + slackUrl, + threshold: this.state.threshold, + timeRange, + apmIndexPatternTitle: indexPatternTitle + }).then((id: string) => { + this.props.onClose(); + this.addSuccessToast(id); + }); }) .catch(e => { // eslint-disable-next-line @@ -613,26 +612,20 @@ export class WatcherFlyout extends Component< <EuiFlyoutFooter> <EuiFlexGroup justifyContent="flexEnd"> <EuiFlexItem grow={false}> - <ApmPluginContext.Consumer> - {({ config }) => { - return ( - <EuiButton - onClick={this.createWatch(config)} - fill - disabled={ - !this.state.actions.email && !this.state.actions.slack - } - > - {i18n.translate( - 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', - { - defaultMessage: 'Create watch' - } - )} - </EuiButton> - ); - }} - </ApmPluginContext.Consumer> + <EuiButton + onClick={() => this.createWatch()} + fill + disabled={ + !this.state.actions.email && !this.state.actions.slack + } + > + {i18n.translate( + 'xpack.apm.serviceDetails.enableErrorReportsPanel.createWatchButtonLabel', + { + defaultMessage: 'Create watch' + } + )} + </EuiButton> </EuiFlexItem> </EuiFlexGroup> </EuiFlyoutFooter> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/__snapshots__/createErrorGroupWatch.test.ts.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/esResponse.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts index 690db9fcdd8d6..d45453e24f1c9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/createErrorGroupWatch.ts @@ -17,7 +17,7 @@ import { ERROR_LOG_MESSAGE, PROCESSOR_EVENT, SERVICE_NAME -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../../common/elasticsearch_fieldnames'; import { createWatch } from '../../../../services/rest/watcher'; function getSlackPathUrl(slackUrl?: string) { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/index.tsx rename to x-pack/plugins/apm/public/components/app/ServiceDetails/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx new file mode 100644 index 0000000000000..de775dbc8162a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -0,0 +1,306 @@ +/* + * 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 { EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { storiesOf } from '@storybook/react'; +import cytoscape from 'cytoscape'; +import React from 'react'; +import { Cytoscape } from './Cytoscape'; +import serviceMapResponse from './cytoscape-layout-test-response.json'; +import { iconForNode } from './icons'; + +const elementsFromResponses = serviceMapResponse.elements; + +storiesOf('app/ServiceMap/Cytoscape', module).add( + 'example', + () => { + const elements: cytoscape.ElementDefinition[] = [ + { + data: { + id: 'opbeans-python', + 'service.name': 'opbeans-python', + 'agent.name': 'python' + } + }, + { + data: { + id: 'opbeans-node', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs' + } + }, + { + data: { + id: 'opbeans-ruby', + 'service.name': 'opbeans-ruby', + 'agent.name': 'ruby' + } + }, + { data: { source: 'opbeans-python', target: 'opbeans-node' } }, + { + data: { + bidirectional: true, + source: 'opbeans-python', + target: 'opbeans-ruby' + } + } + ]; + const height = 300; + const width = 1340; + const serviceName = 'opbeans-python'; + return ( + <Cytoscape + elements={elements} + height={height} + width={width} + serviceName={serviceName} + /> + ); + }, + { + info: { + propTables: false, + source: false + } + } +); + +storiesOf('app/ServiceMap/Cytoscape', module) + .add( + 'node icons', + () => { + const cy = cytoscape(); + const elements = [ + { data: { id: 'default' } }, + { + data: { + id: 'aws', + 'span.type': 'aws', + 'span.subtype': 'servicename' + } + }, + { data: { id: 'cache', 'span.type': 'cache' } }, + { data: { id: 'database', 'span.type': 'db' } }, + { + data: { + id: 'cassandra', + 'span.type': 'db', + 'span.subtype': 'cassandra' + } + }, + { + data: { + id: 'elasticsearch', + 'span.type': 'db', + 'span.subtype': 'elasticsearch' + } + }, + { + data: { + id: 'mongodb', + 'span.type': 'db', + 'span.subtype': 'mongodb' + } + }, + { + data: { + id: 'mysql', + 'span.type': 'db', + 'span.subtype': 'mysql' + } + }, + { + data: { + id: 'postgresql', + 'span.type': 'db', + 'span.subtype': 'postgresql' + } + }, + { + data: { + id: 'redis', + 'span.type': 'db', + 'span.subtype': 'redis' + } + }, + { data: { id: 'external', 'span.type': 'external' } }, + { data: { id: 'ext', 'span.type': 'ext' } }, + { + data: { + id: 'graphql', + 'span.type': 'external', + 'span.subtype': 'graphql' + } + }, + { + data: { + id: 'grpc', + 'span.type': 'external', + 'span.subtype': 'grpc' + } + }, + { + data: { + id: 'websocket', + 'span.type': 'external', + 'span.subtype': 'websocket' + } + }, + { data: { id: 'messaging', 'span.type': 'messaging' } }, + { + data: { + id: 'jms', + 'span.type': 'messaging', + 'span.subtype': 'jms' + } + }, + { + data: { + id: 'kafka', + 'span.type': 'messaging', + 'span.subtype': 'kafka' + } + }, + { data: { id: 'template', 'span.type': 'template' } }, + { + data: { + id: 'handlebars', + 'span.type': 'template', + 'span.subtype': 'handlebars' + } + }, + { + data: { + id: 'dark', + 'service.name': 'dark service', + 'agent.name': 'dark' + } + }, + { + data: { + id: 'dotnet', + 'service.name': 'dotnet service', + 'agent.name': 'dotnet' + } + }, + { + data: { + id: 'go', + 'service.name': 'go service', + 'agent.name': 'go' + } + }, + { + data: { + id: 'java', + 'service.name': 'java service', + 'agent.name': 'java' + } + }, + { + data: { + id: 'RUM (js-base)', + 'service.name': 'RUM service', + 'agent.name': 'js-base' + } + }, + { + data: { + id: 'RUM (rum-js)', + 'service.name': 'RUM service', + 'agent.name': 'rum-js' + } + }, + { + data: { + id: 'nodejs', + 'service.name': 'nodejs service', + 'agent.name': 'nodejs' + } + }, + { + data: { + id: 'php', + 'service.name': 'php service', + 'agent.name': 'php' + } + }, + { + data: { + id: 'python', + 'service.name': 'python service', + 'agent.name': 'python' + } + }, + { + data: { + id: 'ruby', + 'service.name': 'ruby service', + 'agent.name': 'ruby' + } + } + ]; + cy.add(elements); + + return ( + <EuiFlexGroup gutterSize="l" wrap={true}> + {cy.nodes().map(node => ( + <EuiFlexItem key={node.data('id')}> + <EuiCard + description={ + <code style={{ textAlign: 'left', whiteSpace: 'nowrap' }}> + agent.name: {node.data('agent.name') || 'undefined'} + <br /> + span.type: {node.data('span.type') || 'undefined'} + <br /> + span.subtype: {node.data('span.subtype') || 'undefined'} + </code> + } + icon={ + <img + alt={node.data('label')} + src={iconForNode(node)} + height={80} + width={80} + /> + } + title={node.data('id')} + /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ); + }, + { + info: { + propTables: false, + source: false + } + } + ) + .add( + 'layout', + () => { + const height = 640; + const width = 1340; + const serviceName = undefined; // global service map + + return ( + <Cytoscape + elements={elementsFromResponses} + height={height} + width={width} + serviceName={serviceName} + /> + ); + }, + { + info: { + source: false + } + } + ) + .addParameters({ options: { showPanel: false } }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 53c86f92ee557..ad77434bca9f4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -19,7 +19,7 @@ import { cytoscapeOptions, nodeHeight } from './cytoscapeOptions'; -import { useUiTracker } from '../../../../../../../plugins/observability/public'; +import { useUiTracker } from '../../../../../observability/public'; export const CytoscapeContext = createContext<cytoscape.Core | undefined>( undefined diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/LoadingOverlay.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index 491ebdc5aad15..bc3434f277d1c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui'; import cytoscape from 'cytoscape'; import React from 'react'; -import { SERVICE_FRAMEWORK_NAME } from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { SERVICE_FRAMEWORK_NAME } from '../../../../../common/elasticsearch_fieldnames'; import { Buttons } from './Buttons'; import { Info } from './Info'; import { ServiceMetricFetcher } from './ServiceMetricFetcher'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index e1df3b474e9de..541f4f6a1e775 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { SPAN_SUBTYPE, SPAN_TYPE -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../../common/elasticsearch_fieldnames'; const ItemRow = styled.div` line-height: 2; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index 697aa6a1b652b..5e6412333a2e1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; +import { ServiceNodeMetrics } from '../../../../../common/service_map'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 056af68cc8173..3cee986261a68 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; -import { ServiceNodeMetrics } from '../../../../../../../../plugins/apm/common/service_map'; +import { ServiceNodeMetrics } from '../../../../../common/service_map'; import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; function LoadingSpinner() { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx new file mode 100644 index 0000000000000..1c9d5092bfcf5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPopover } from '@elastic/eui'; +import cytoscape from 'cytoscape'; +import React, { + CSSProperties, + useCallback, + useContext, + useEffect, + useRef, + useState +} from 'react'; +import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; +import { CytoscapeContext } from '../Cytoscape'; +import { Contents } from './Contents'; +import { animationOptions } from '../cytoscapeOptions'; + +interface PopoverProps { + focusedServiceName?: string; +} + +export function Popover({ focusedServiceName }: PopoverProps) { + const cy = useContext(CytoscapeContext); + const [selectedNode, setSelectedNode] = useState< + cytoscape.NodeSingular | undefined + >(undefined); + const deselect = useCallback(() => { + if (cy) { + cy.elements().unselect(); + } + setSelectedNode(undefined); + }, [cy, setSelectedNode]); + const renderedHeight = selectedNode?.renderedHeight() ?? 0; + const renderedWidth = selectedNode?.renderedWidth() ?? 0; + const { x, y } = selectedNode?.renderedPosition() ?? { x: -10000, y: -10000 }; + const isOpen = !!selectedNode; + const isService = selectedNode?.data(SERVICE_NAME) !== undefined; + const triggerStyle: CSSProperties = { + background: 'transparent', + height: renderedHeight, + position: 'absolute', + width: renderedWidth + }; + const trigger = <div style={triggerStyle} />; + const zoom = cy?.zoom() ?? 1; + const height = selectedNode?.height() ?? 0; + const translateY = y - ((zoom + 1) * height) / 4; + const popoverStyle: CSSProperties = { + position: 'absolute', + transform: `translate(${x}px, ${translateY}px)` + }; + const selectedNodeData = selectedNode?.data() ?? {}; + const selectedNodeServiceName = selectedNodeData.id; + const label = selectedNodeData.label || selectedNodeServiceName; + const popoverRef = useRef<EuiPopover>(null); + + // Set up Cytoscape event handlers + useEffect(() => { + const selectHandler: cytoscape.EventHandler = event => { + setSelectedNode(event.target); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', deselect); + cy.on('data viewport', deselect); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', deselect); + cy.removeListener('data viewport', undefined, deselect); + } + }; + }, [cy, deselect]); + + // Handle positioning of popover. This makes it so the popover positions + // itself correctly and the arrows are always pointing to where they should. + useEffect(() => { + if (popoverRef.current) { + popoverRef.current.positionPopoverFluid(); + } + }, [popoverRef, x, y]); + + const centerSelectedNode = useCallback(() => { + if (cy) { + cy.animate({ + ...animationOptions, + center: { eles: cy.getElementById(selectedNodeServiceName) } + }); + } + }, [cy, selectedNodeServiceName]); + + const isAlreadyFocused = focusedServiceName === selectedNodeServiceName; + + return ( + <EuiPopover + anchorPosition={'upCenter'} + button={trigger} + closePopover={() => {}} + isOpen={isOpen} + ref={popoverRef} + style={popoverStyle} + > + <Contents + isService={isService} + label={label} + onFocusClick={isAlreadyFocused ? centerSelectedNode : deselect} + selectedNodeData={selectedNodeData} + selectedNodeServiceName={selectedNodeServiceName} + /> + </EuiPopover> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json rename to x-pack/plugins/apm/public/components/app/ServiceMap/cytoscape-layout-test-response.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts rename to x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index bf0e052b951ae..554f84f0ad236 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -9,7 +9,7 @@ import { CSSProperties } from 'react'; import { SERVICE_NAME, SPAN_DESTINATION_SERVICE_RESOURCE -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../common/elasticsearch_fieldnames'; import { defaultIcon, iconForNode } from './icons'; // IE 11 does not properly load some SVGs or draw certain shapes. This causes @@ -60,7 +60,10 @@ const style: cytoscape.Stylesheet[] = [ ? theme.euiColorPrimary : theme.euiColorMediumShade, 'border-width': 2, - color: theme.textColors.default, + color: (el: cytoscape.NodeSingular) => + el.hasClass('primary') || el.selected() + ? theme.euiColorPrimaryText + : theme.textColors.text, // theme.euiFontFamily doesn't work here for some reason, so we're just // specifying a subset of the fonts for the label text. 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', @@ -78,8 +81,9 @@ const style: cytoscape.Stylesheet[] = [ 'overlay-opacity': 0, shape: (el: cytoscape.NodeSingular) => isService(el) ? (isIE11 ? 'rectangle' : 'ellipse') : 'diamond', - 'text-background-color': theme.euiColorLightestShade, - 'text-background-opacity': 0, + 'text-background-color': theme.euiColorPrimary, + 'text-background-opacity': (el: cytoscape.NodeSingular) => + el.hasClass('primary') || el.selected() ? 0.1 : 0, 'text-background-padding': theme.paddingSizes.xs, 'text-background-shape': 'roundrectangle', 'text-margin-y': parseInt(theme.paddingSizes.s, 10), diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts new file mode 100644 index 0000000000000..9fe5cbd23b07c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -0,0 +1,128 @@ +/* + * 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 cytoscape from 'cytoscape'; +import { isRumAgentName } from '../../../../common/agent_name'; +import { + AGENT_NAME, + SPAN_SUBTYPE, + SPAN_TYPE +} from '../../../../common/elasticsearch_fieldnames'; +import awsIcon from './icons/aws.svg'; +import cassandraIcon from './icons/cassandra.svg'; +import darkIcon from './icons/dark.svg'; +import databaseIcon from './icons/database.svg'; +import defaultIconImport from './icons/default.svg'; +import documentsIcon from './icons/documents.svg'; +import dotNetIcon from './icons/dot-net.svg'; +import elasticsearchIcon from './icons/elasticsearch.svg'; +import globeIcon from './icons/globe.svg'; +import goIcon from './icons/go.svg'; +import graphqlIcon from './icons/graphql.svg'; +import grpcIcon from './icons/grpc.svg'; +import handlebarsIcon from './icons/handlebars.svg'; +import javaIcon from './icons/java.svg'; +import kafkaIcon from './icons/kafka.svg'; +import mongodbIcon from './icons/mongodb.svg'; +import mysqlIcon from './icons/mysql.svg'; +import nodeJsIcon from './icons/nodejs.svg'; +import phpIcon from './icons/php.svg'; +import postgresqlIcon from './icons/postgresql.svg'; +import pythonIcon from './icons/python.svg'; +import redisIcon from './icons/redis.svg'; +import rubyIcon from './icons/ruby.svg'; +import rumJsIcon from './icons/rumjs.svg'; +import websocketIcon from './icons/websocket.svg'; + +export const defaultIcon = defaultIconImport; + +const defaultTypeIcons: { [key: string]: string } = { + cache: databaseIcon, + db: databaseIcon, + ext: globeIcon, + external: globeIcon, + messaging: documentsIcon, + resource: globeIcon +}; + +const typeIcons: { [key: string]: { [key: string]: string } } = { + aws: { + servicename: awsIcon + }, + db: { + cassandra: cassandraIcon, + elasticsearch: elasticsearchIcon, + mongodb: mongodbIcon, + mysql: mysqlIcon, + postgresql: postgresqlIcon, + redis: redisIcon + }, + external: { + graphql: graphqlIcon, + grpc: grpcIcon, + websocket: websocketIcon + }, + messaging: { + jms: javaIcon, + kafka: kafkaIcon + }, + template: { + handlebars: handlebarsIcon + } +}; + +const agentIcons: { [key: string]: string } = { + dark: darkIcon, + dotnet: dotNetIcon, + go: goIcon, + java: javaIcon, + 'js-base': rumJsIcon, + nodejs: nodeJsIcon, + php: phpIcon, + python: pythonIcon, + ruby: rubyIcon +}; + +function getAgentIcon(agentName?: string) { + // RUM can have multiple names. Normalize it + const normalizedAgentName = isRumAgentName(agentName) ? 'js-base' : agentName; + return normalizedAgentName && agentIcons[normalizedAgentName]; +} + +function getSpanIcon(type?: string, subtype?: string) { + if (!type) { + return; + } + + const types = type ? typeIcons[type] : {}; + if (subtype && types && subtype in types) { + return types[subtype]; + } + return defaultTypeIcons[type] || defaultIcon; +} + +// IE 11 does not properly load some SVGs, which causes a runtime error and the +// map to not work at all. We would prefer to do some kind of feature detection +// rather than browser detection, but IE 11 does support SVG, just not well +// enough for our use in loading icons. +// +// This method of detecting IE is from a Stack Overflow answer: +// https://stackoverflow.com/a/21825207 +// +// @ts-ignore `documentMode` is not recognized as a valid property of `document`. +const isIE11 = !!window.MSInputMethodContext && !!document.documentMode; + +export function iconForNode(node: cytoscape.NodeSingular) { + const agentName = node.data(AGENT_NAME); + const subtype = node.data(SPAN_SUBTYPE); + const type = node.data(SPAN_TYPE); + + if (isIE11) { + return defaultIcon; + } + + return getAgentIcon(agentName) || getSpanIcon(type, subtype) || defaultIcon; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/aws.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/aws.svg new file mode 100644 index 0000000000000..8dd2c589014ea --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/aws.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4.50943 6.93932C4.50943 7.13389 4.53072 7.29165 4.56797 7.40734C4.61053 7.52304 4.66374 7.64925 4.73824 7.78597C4.76484 7.82804 4.77548 7.87011 4.77548 7.90692C4.77548 7.95951 4.74356 8.0121 4.67439 8.06469L4.33916 8.28555C4.29127 8.3171 4.24339 8.33288 4.20082 8.33288C4.14761 8.33288 4.0944 8.30659 4.04119 8.25926C3.96669 8.18038 3.90284 8.09624 3.84963 8.0121C3.79642 7.9227 3.74321 7.82278 3.68468 7.70183C3.26964 8.18564 2.74818 8.42754 2.12031 8.42754C1.67334 8.42754 1.31684 8.30133 1.05611 8.04891C0.795378 7.79649 0.662354 7.45993 0.662354 7.03923C0.662354 6.59224 0.821983 6.22939 1.14656 5.95594C1.47115 5.68248 1.90215 5.54576 2.45021 5.54576C2.63112 5.54576 2.81736 5.56153 3.01423 5.58783C3.21111 5.61412 3.41331 5.65619 3.62615 5.70352V5.31963C3.62615 4.91997 3.54101 4.64126 3.37606 4.47824C3.20579 4.31521 2.91846 4.23633 2.50874 4.23633C2.3225 4.23633 2.13095 4.25737 1.93407 4.3047C1.7372 4.35203 1.54564 4.40987 1.3594 4.48349C1.27427 4.5203 1.21042 4.54134 1.17317 4.55186C1.13592 4.56237 1.10932 4.56763 1.08803 4.56763C1.01354 4.56763 0.976292 4.51505 0.976292 4.40461V4.14694C0.976292 4.0628 0.986934 3.99969 1.01354 3.96288C1.04014 3.92607 1.08803 3.88926 1.16253 3.85245C1.34876 3.75779 1.57224 3.67891 1.83297 3.6158C2.0937 3.54744 2.37039 3.51589 2.66305 3.51589C3.29625 3.51589 3.75917 3.65787 4.05715 3.94185C4.34981 4.22582 4.49879 4.65703 4.49879 5.23549V6.93932H4.50943ZM2.34911 7.73864C2.5247 7.73864 2.70562 7.70709 2.89717 7.64399C3.08873 7.58088 3.259 7.46519 3.40267 7.30743C3.4878 7.20751 3.55166 7.09708 3.58358 6.97087C3.61551 6.84466 3.63679 6.69216 3.63679 6.51336V6.2925C3.48248 6.25568 3.31753 6.22413 3.14726 6.2031C2.97699 6.18206 2.81204 6.17154 2.64709 6.17154C2.29058 6.17154 2.02985 6.23991 1.85426 6.38189C1.67866 6.52388 1.59353 6.72371 1.59353 6.98665C1.59353 7.23381 1.65738 7.41786 1.7904 7.54407C1.91811 7.67554 2.10434 7.73864 2.34911 7.73864ZM6.62187 8.30659C6.52609 8.30659 6.46224 8.29081 6.41967 8.254C6.37711 8.22245 6.33986 8.14882 6.30793 8.04891L5.0575 3.98392C5.02557 3.87874 5.00961 3.81038 5.00961 3.77357C5.00961 3.68943 5.05218 3.6421 5.13731 3.6421H5.65877C5.75987 3.6421 5.82904 3.65787 5.86629 3.69469C5.90886 3.72624 5.94078 3.79986 5.97271 3.89978L6.86664 7.38105L7.69671 3.89978C7.72332 3.7946 7.75524 3.72624 7.79781 3.69469C7.84038 3.66313 7.91487 3.6421 8.01065 3.6421H8.43633C8.53743 3.6421 8.6066 3.65787 8.64917 3.69469C8.69174 3.72624 8.72899 3.79986 8.75027 3.89978L9.59099 7.42312L10.5115 3.89978C10.5434 3.7946 10.5807 3.72624 10.6179 3.69469C10.6605 3.66313 10.7297 3.6421 10.8255 3.6421H11.3203C11.4054 3.6421 11.4533 3.68417 11.4533 3.77357C11.4533 3.79986 11.448 3.82615 11.4427 3.85771C11.4374 3.88926 11.4267 3.93133 11.4054 3.98917L10.1231 8.05417C10.0912 8.15934 10.0539 8.22771 10.0113 8.25926C9.96878 8.29081 9.89961 8.31185 9.80915 8.31185H9.35154C9.25044 8.31185 9.18127 8.29607 9.1387 8.25926C9.09613 8.22245 9.05889 8.15408 9.0376 8.04891L8.21285 4.65703L7.39342 8.04365C7.36681 8.14883 7.33488 8.21719 7.29232 8.254C7.24975 8.29081 7.17525 8.30659 7.07948 8.30659H6.62187ZM13.4594 8.44857C13.1827 8.44857 12.906 8.41702 12.6399 8.35392C12.3739 8.29081 12.1664 8.22245 12.028 8.14357C11.9429 8.09624 11.8843 8.04365 11.8631 7.99632C11.8418 7.94899 11.8311 7.89641 11.8311 7.84908V7.58088C11.8311 7.47045 11.8737 7.41786 11.9535 7.41786C11.9854 7.41786 12.0174 7.42312 12.0493 7.43364C12.0812 7.44416 12.1291 7.46519 12.1823 7.48623C12.3632 7.56511 12.5601 7.62821 12.7676 7.67028C12.9805 7.71235 13.188 7.73339 13.4008 7.73339C13.736 7.73339 13.9968 7.67554 14.1777 7.55985C14.3586 7.44416 14.4544 7.27588 14.4544 7.06027C14.4544 6.91302 14.4065 6.79207 14.3107 6.69216C14.2149 6.59224 14.034 6.50284 13.7733 6.4187L13.0017 6.18206C12.6133 6.06111 12.326 5.88231 12.1504 5.64567C11.9748 5.41429 11.8843 5.15661 11.8843 4.88316C11.8843 4.66229 11.9322 4.46772 12.028 4.29944C12.1238 4.13116 12.2515 3.98392 12.4111 3.86822C12.5707 3.74727 12.7517 3.65787 12.9645 3.59477C13.1773 3.53166 13.4008 3.50537 13.6349 3.50537C13.752 3.50537 13.8744 3.51063 13.9915 3.52641C14.1138 3.54218 14.2256 3.56322 14.3373 3.58425C14.4437 3.61055 14.5448 3.63684 14.6406 3.66839C14.7364 3.69994 14.8109 3.7315 14.8641 3.76305C14.9386 3.80512 14.9918 3.84719 15.0237 3.89452C15.0557 3.93659 15.0716 3.99443 15.0716 4.06805V4.31521C15.0716 4.42565 15.029 4.48349 14.9492 4.48349C14.9067 4.48349 14.8375 4.46246 14.747 4.42039C14.4437 4.28366 14.1032 4.2153 13.7254 4.2153C13.4221 4.2153 13.1827 4.26263 13.0177 4.36254C12.8528 4.46246 12.7676 4.61496 12.7676 4.83057C12.7676 4.97781 12.8208 5.10402 12.9273 5.20394C13.0337 5.30385 13.2306 5.40377 13.5126 5.49317L14.2681 5.72981C14.6513 5.85076 14.9279 6.01904 15.0929 6.23465C15.2579 6.45026 15.3377 6.69742 15.3377 6.97087C15.3377 7.197 15.2898 7.40209 15.1993 7.58088C15.1035 7.75968 14.9758 7.91744 14.8109 8.04365C14.6459 8.17512 14.4491 8.26978 14.2203 8.33814C13.9808 8.41176 13.7307 8.44857 13.4594 8.44857Z" fill="#252F3E"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M14.465 11.0045C12.7144 12.2824 10.171 12.9608 7.98406 12.9608C4.91916 12.9608 2.15756 11.8407 0.0717311 9.97907C-0.0932198 9.83183 0.0557681 9.632 0.252645 9.74769C2.50875 11.0413 5.29163 11.8249 8.17029 11.8249C10.1125 11.8249 12.2462 11.4252 14.2096 10.6049C14.5023 10.4734 14.7524 10.7942 14.465 11.0045Z" fill="#FF9900"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1941 10.1841C14.9706 9.90013 13.7148 10.0474 13.1455 10.1157C12.9752 10.1368 12.9486 9.98953 13.1029 9.8791C14.1033 9.18495 15.7474 9.38478 15.939 9.61616C16.1306 9.8528 15.8858 11.4777 14.9493 12.256C14.8056 12.377 14.6673 12.3139 14.7311 12.1561C14.944 11.6355 15.4175 10.4628 15.1941 10.1841Z" fill="#FF9900"/> +</svg> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg new file mode 100644 index 0000000000000..0cc2710563958 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/cassandra.svg @@ -0,0 +1,48 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path d="M1.90925 11.855C2.03991 11.855 2.1637 11.8859 2.22567 11.9478C2.23598 12.0442 2.14995 12.1989 2.09493 12.1989C2.0296 12.1748 1.9677 12.161 1.89205 12.161C1.57914 12.161 1.42433 12.4843 1.42433 12.8041C1.42433 13.0036 1.4931 13.124 1.65135 13.124C1.81984 13.124 1.99177 13.0105 2.08805 12.9245C2.11212 12.9417 2.14307 12.9967 2.14307 13.0586C2.14307 13.1274 2.12244 13.1927 2.06054 13.2546C1.95051 13.3647 1.76482 13.4575 1.49654 13.4575C1.18707 13.4575 0.973877 13.2822 0.973877 12.8729C0.973877 12.3365 1.32805 11.855 1.90581 11.855H1.90925Z" fill="#373535"/> +<path d="M2.92014 13.107C3.08863 13.107 3.39473 12.8112 3.4807 12.2473C3.48414 12.2129 3.48758 12.2026 3.49445 12.1682C3.45663 12.1545 3.40505 12.1442 3.3534 12.1442C3.22617 12.1442 3.09551 12.182 2.97859 12.3402C2.85824 12.5086 2.80667 12.7184 2.80667 12.887C2.80667 13.0313 2.84793 13.107 2.9167 13.107H2.92014ZM2.36646 12.9523C2.36646 12.7494 2.43523 12.3952 2.69319 12.1373C2.91326 11.9103 3.18147 11.8552 3.42568 11.8552C3.5873 11.8552 3.80736 11.9068 3.94491 11.9344C3.91052 12.0857 3.828 12.6565 3.79017 12.9866C3.77298 13.1277 3.7661 13.3237 3.77298 13.3959C3.65951 13.444 3.44631 13.4578 3.38779 13.4578C3.35691 13.4578 3.34996 13.3168 3.36035 13.1827C3.36372 13.138 3.37066 13.0726 3.37403 13.0451C3.23993 13.2686 2.97516 13.4578 2.69663 13.4578C2.49719 13.4578 2.36646 13.2927 2.36646 12.9557V12.9523Z" fill="#373535"/> +<path d="M4.80095 11.8552C4.94194 11.8552 5.07604 11.9068 5.13801 11.9721C5.13457 12.0685 5.05892 12.23 4.9385 12.1853C4.88692 12.1681 4.83534 12.1545 4.76657 12.1545C4.67373 12.1545 4.59464 12.1957 4.59464 12.2816C4.59464 12.347 4.64278 12.3952 4.87317 12.5568C5.03829 12.6771 5.10362 12.7837 5.10362 12.935C5.10362 13.1861 4.86629 13.4578 4.45366 13.4578C4.28517 13.4578 4.13723 13.3924 4.08909 13.3237C4.0272 13.2205 4.07534 13.0176 4.14418 13.0519C4.23359 13.0966 4.38145 13.1449 4.5018 13.1449C4.61527 13.1449 4.68748 13.0932 4.68748 13.0244C4.68748 12.966 4.6359 12.9144 4.42615 12.7665C4.25071 12.6359 4.19569 12.5155 4.19569 12.378C4.19569 12.0926 4.4399 11.8552 4.79752 11.8552H4.80095Z" fill="#373535"/> +<path d="M5.98729 11.8552C6.12834 11.8552 6.26245 11.9068 6.32434 11.9721C6.3209 12.0685 6.24532 12.23 6.1249 12.1853C6.07332 12.1681 6.02167 12.1545 5.95297 12.1545C5.86013 12.1545 5.78104 12.1957 5.78104 12.2816C5.78104 12.347 5.82918 12.3952 6.05957 12.5568C6.22462 12.6771 6.28995 12.7837 6.28995 12.935C6.28995 13.1861 6.05262 13.4578 5.63999 13.4578C5.4715 13.4578 5.32364 13.3924 5.2755 13.3237C5.2136 13.2205 5.26174 13.0176 5.33052 13.0519C5.41992 13.0966 5.56778 13.1449 5.68813 13.1449C5.8016 13.1449 5.87381 13.0932 5.87381 13.0244C5.87381 12.966 5.82223 12.9144 5.61248 12.7665C5.43711 12.6359 5.38209 12.5155 5.38209 12.378C5.38209 12.0926 5.62623 11.8552 5.98392 11.8552H5.98729Z" fill="#373535"/> +<path d="M7.07041 13.107C7.23897 13.107 7.54501 12.8112 7.63097 12.2473C7.63441 12.2129 7.63792 12.2026 7.6448 12.1682C7.6069 12.1545 7.5554 12.1442 7.50375 12.1442C7.37652 12.1442 7.24585 12.182 7.12887 12.3402C7.00859 12.5086 6.95694 12.7184 6.95694 12.887C6.95694 13.0313 6.9982 13.107 7.06704 13.107H7.07041ZM6.51672 12.9523C6.51672 12.7494 6.58557 12.3952 6.84346 12.1373C7.06353 11.9103 7.33174 11.8552 7.57596 11.8552C7.73757 11.8552 7.95771 11.9068 8.09526 11.9344C8.06087 12.0857 7.97834 12.6565 7.94052 12.9866C7.92325 13.1277 7.91645 13.3237 7.92325 13.3959C7.80978 13.444 7.59666 13.4578 7.53813 13.4578C7.50726 13.4578 7.50031 13.3168 7.51062 13.1827C7.51406 13.138 7.52094 13.0726 7.52438 13.0451C7.39027 13.2686 7.1255 13.4578 6.84697 13.4578C6.64746 13.4578 6.51672 13.2927 6.51672 12.9557V12.9523Z" fill="#373535"/> +<path d="M8.8721 12.3023C9.03034 12.0478 9.23659 11.8552 9.5255 11.8552C9.76964 11.8552 9.84872 12.0822 9.80409 12.4054C9.78002 12.5568 9.73876 12.7734 9.71118 12.9728C9.68711 13.1414 9.66992 13.2926 9.67343 13.3959C9.58403 13.4406 9.32262 13.4578 9.26417 13.4578C9.2401 13.4578 9.23659 13.2686 9.2676 13.0691C9.29504 12.9041 9.35357 12.6049 9.38108 12.4329C9.39834 12.3333 9.39483 12.2128 9.29855 12.2128C9.17125 12.2128 8.86873 12.4158 8.71736 13.2479C8.70697 13.3167 8.67954 13.358 8.64164 13.382C8.59013 13.4131 8.48003 13.4406 8.25659 13.444C8.29785 13.2343 8.36319 12.8044 8.41133 12.488C8.45596 12.1992 8.47322 12.0134 8.46283 11.9412C8.52136 11.9206 8.82747 11.8552 8.86171 11.8552C8.89968 11.8552 8.90648 11.9618 8.8721 12.2198C8.86873 12.2439 8.86522 12.2816 8.86171 12.3023H8.8721Z" fill="#373535"/> +<path d="M10.619 13.1036C10.7702 13.1036 11.0764 12.8216 11.1623 12.323C11.1658 12.2955 11.1761 12.2439 11.183 12.2129C11.1382 12.1716 11.0798 12.1441 10.987 12.1441C10.6395 12.1441 10.4848 12.5602 10.4848 12.8595C10.4848 13.0211 10.5399 13.1036 10.6155 13.1036H10.619ZM10.392 13.4578C10.172 13.4578 10.0344 13.2618 10.0344 12.9248C10.0344 12.3677 10.3816 11.8552 10.9663 11.8552C11.0695 11.8552 11.1623 11.8828 11.2207 11.9171C11.2517 11.759 11.3239 11.2706 11.3274 11.1262C11.4236 11.109 11.63 11.0781 11.7228 11.0781C11.7607 11.0781 11.7711 11.1159 11.7572 11.2018C11.6746 11.7177 11.5269 12.7081 11.4993 12.9282C11.4786 13.1139 11.4753 13.2962 11.4786 13.3993C11.3687 13.4474 11.1451 13.4578 11.0867 13.4578C11.0625 13.4578 11.0488 13.2996 11.0557 13.1586C11.0592 13.1139 11.066 13.0485 11.066 13.0383C10.8941 13.3202 10.6567 13.4578 10.3954 13.4578H10.392Z" fill="#373535"/> +<path d="M12.407 12.3505C12.5893 11.9447 12.8059 11.8553 12.9469 11.8553C12.9917 11.8553 13.0604 11.8828 13.0913 11.9172C13.1052 12.0307 13.0158 12.2576 12.9366 12.3436C12.8953 12.323 12.8438 12.3023 12.7853 12.3023C12.6683 12.3023 12.4379 12.4847 12.2935 13.2687C12.2831 13.3341 12.2626 13.3616 12.2248 13.3788C12.1663 13.4131 11.9359 13.4407 11.8259 13.4441C11.8774 13.1621 11.9668 12.5878 12.0012 12.2508C12.0115 12.1648 12.0149 12.0066 12.0081 11.9447C12.0734 11.9138 12.3314 11.8553 12.3898 11.8553C12.4208 11.8553 12.4449 12.0445 12.3966 12.3505H12.407Z" fill="#373535"/> +<path d="M13.6208 13.107C13.7895 13.107 14.0954 12.8112 14.1815 12.2473C14.1848 12.2129 14.1883 12.2026 14.1952 12.1682C14.1574 12.1545 14.1058 12.1442 14.0542 12.1442C13.9269 12.1442 13.7963 12.182 13.6793 12.3402C13.559 12.5086 13.5073 12.7184 13.5073 12.887C13.5073 13.0313 13.5486 13.107 13.6175 13.107H13.6208ZM13.0671 12.9523C13.0671 12.7494 13.136 12.3952 13.3939 12.1373C13.6139 11.9103 13.8822 11.8552 14.1264 11.8552C14.288 11.8552 14.5081 11.9068 14.6457 11.9344C14.6113 12.0857 14.5288 12.6565 14.4909 12.9866C14.4737 13.1277 14.4668 13.3237 14.4737 13.3959C14.3603 13.444 14.1471 13.4578 14.0885 13.4578C14.0577 13.4578 14.0508 13.3168 14.061 13.1827C14.0645 13.138 14.0713 13.0726 14.0749 13.0451C13.9407 13.2686 13.676 13.4578 13.3974 13.4578C13.1979 13.4578 13.0671 13.2927 13.0671 12.9557V12.9523Z" fill="#373535"/> +<path d="M12.6107 6.02788C12.7242 7.34973 10.6439 8.60782 7.96441 8.83771C5.28488 9.06759 3.02071 8.18215 2.90738 6.86037C2.79391 5.53831 4.87419 4.28028 7.55371 4.0504C10.2332 3.82059 12.4974 4.70581 12.6107 6.02788Z" fill="#BBE6FB"/> +<g opacity="0.350006"> +<g opacity="0.350006"> +<path opacity="0.350006" d="M7.63233 4.20532C6.61021 4.1887 5.41415 4.59453 4.5242 4.97456C4.43157 5.24148 4.38171 5.52538 4.38171 5.81967C4.38171 7.38766 5.79756 8.65894 7.54415 8.65894C9.29074 8.65894 10.7066 7.38766 10.7066 5.81967C10.7066 5.32344 10.5645 4.85722 10.3152 4.45132C9.60322 4.34065 8.62394 4.22144 7.63233 4.20532Z" fill="white"/> +</g> +</g> +<path d="M7.54427 4.20512C6.9784 4.1706 6.00306 4.46581 5.18976 4.76031C5.06619 5.05195 4.99756 5.37109 4.99756 5.706C4.99756 7.07986 6.24319 8.1936 7.56089 8.1936C8.86992 8.1936 10.0281 7.07807 10.1242 5.706C10.1555 5.2592 10.0016 4.83818 9.7881 4.4752C8.94492 4.32669 8.0501 4.236 7.54427 4.20512Z" fill="white"/> +<path d="M6.09213 5.52568C6.16155 5.37195 6.26671 5.29666 6.36858 5.16778C6.35726 5.13727 6.33248 5.03748 6.33248 5.00287C6.33248 4.84842 6.45777 4.72306 6.61229 4.72306C6.64875 4.72306 6.68357 4.73029 6.71545 4.74297C7.17858 4.39002 7.78829 4.23614 8.401 4.37311C8.45695 4.38572 8.51183 4.40098 8.5657 4.41774C7.96015 4.33378 7.27759 4.54826 6.81331 4.923C6.82749 4.95645 6.86682 5.05209 6.86682 5.09056C6.86682 5.24508 6.76681 5.28269 6.61229 5.28269C6.57719 5.28269 6.54366 5.27596 6.51264 5.26414C6.40777 5.40498 6.27925 5.67326 6.21635 5.84096C6.4731 6.07063 6.70506 6.15387 7.00737 6.26685C7.00658 6.25245 7.01776 6.23912 7.01776 6.22444C7.01776 5.75858 7.42788 5.37997 7.89381 5.37997C8.31131 5.37997 8.65704 5.68357 8.72452 6.08188C8.93284 5.94935 9.13428 5.84777 9.2878 5.65134C9.25198 5.60764 9.21337 5.48392 9.21337 5.42303C9.21337 5.28283 9.32713 5.16915 9.46733 5.16915C9.48717 5.16915 9.50622 5.17201 9.52478 5.17631C9.60272 5.01613 9.66153 4.84635 9.69914 4.67127C8.97059 4.13191 7.56098 4.20548 7.56098 4.20548C7.56098 4.20548 6.43929 4.14695 5.67993 4.51703C5.73058 4.88317 5.87665 5.23126 6.09213 5.52568Z" fill="#373535"/> +<path d="M9.57436 5.31561C9.62587 5.36203 9.65395 5.40279 9.67608 5.44248C9.74421 5.56498 9.60753 5.67709 9.46734 5.67709C9.45308 5.67709 9.43933 5.67515 9.42564 5.67286C9.26768 5.91972 8.99102 6.23629 8.74524 6.39561C9.12212 6.45736 9.47751 6.6083 9.76033 6.84155C9.96628 6.47255 10.1242 5.93971 10.1242 5.48711C10.1242 5.1426 9.97216 4.89008 9.74256 4.70483C9.73053 4.88936 9.63203 5.14747 9.57436 5.31561Z" fill="#373535"/> +<path d="M6.51481 7.65283C6.64039 7.30604 6.94929 7.03732 7.24149 6.80092C7.19873 6.74239 7.13461 6.59747 7.10789 6.52884C6.7515 6.42046 6.43615 6.21758 6.19158 5.94901C6.18979 5.95682 6.18757 5.96441 6.18571 5.97222C6.13965 6.17861 6.13119 6.38542 6.15412 6.58716C6.32476 6.61896 6.45392 6.76847 6.45392 6.94835C6.45392 7.05502 6.36043 7.20961 6.28786 7.27688C6.36674 7.44709 6.42054 7.52919 6.51481 7.65283Z" fill="#373535"/> +<path d="M6.49285 7.94394C6.34305 7.79501 6.23946 7.49793 6.14225 7.31138C6.12406 7.31418 6.10543 7.31611 6.08638 7.31611C5.88321 7.31611 5.67038 7.14561 5.71852 6.94832C5.74517 6.83915 5.82053 6.76773 5.94324 6.70812C5.91531 6.48196 5.90771 6.15687 5.96022 5.92176C5.97527 5.85449 6.00714 5.82297 6.02828 5.75893C5.80412 5.4309 5.65927 5.00036 5.65927 4.57319C5.65927 4.55757 5.66006 4.54217 5.66042 4.52669C5.2816 4.71668 4.99756 5.01598 4.99756 5.48714C4.99756 6.52932 5.59781 7.55165 6.49091 7.95239C6.49156 7.9496 6.49213 7.94673 6.49285 7.94394Z" fill="#373535"/> +<path d="M8.68892 6.50315C8.57351 6.8314 8.26146 7.06715 7.89374 7.06715C7.69703 7.06715 7.51657 6.99931 7.37316 6.88656C7.07751 7.1168 6.8093 7.52126 6.67297 7.87214C6.73415 7.93798 6.77678 7.99214 6.8454 8.05052C7.05222 8.10439 7.33705 8.05052 7.56092 8.05052C8.4614 8.05052 9.25893 7.64992 9.7162 6.94745C9.41217 6.73691 9.05355 6.53654 8.68892 6.50315Z" fill="#373535"/> +<path d="M6.06882 5.64751C6.13823 5.49378 6.22527 5.3515 6.32707 5.22263C6.31575 5.19218 6.30916 5.1593 6.30916 5.1247C6.30916 4.97025 6.43445 4.84488 6.58898 4.84488C6.62544 4.84488 6.66026 4.85212 6.69213 4.8648C7.15527 4.51184 7.76497 4.35797 8.37769 4.49494C8.43364 4.50754 8.48851 4.5228 8.54238 4.53957C7.93683 4.45561 7.31115 4.64122 6.84687 5.01603C6.86105 5.04941 6.86879 5.08623 6.86879 5.1247C6.86879 5.27922 6.7435 5.40452 6.58898 5.40452C6.55387 5.40452 6.52035 5.39778 6.48933 5.38596C6.38445 5.5268 6.2987 5.6824 6.2358 5.85017C6.4563 6.08779 6.72695 6.2757 7.02919 6.38867C7.02847 6.37427 7.02697 6.36002 7.02697 6.34533C7.02697 5.87947 7.40457 5.5018 7.8705 5.5018C8.288 5.5018 8.63372 5.8054 8.7012 6.2037C8.90952 6.07117 9.09464 5.90096 9.24808 5.70453C9.21226 5.66084 9.19006 5.60575 9.19006 5.54485C9.19006 5.40466 9.30382 5.29097 9.44401 5.29097C9.46385 5.29097 9.48291 5.29384 9.50146 5.29814C9.5794 5.13795 9.63822 4.96817 9.67583 4.79309C8.94728 4.25373 7.53767 4.3273 7.53767 4.3273C7.53767 4.3273 6.41597 4.26878 5.65662 4.63886C5.70726 5.00499 5.85333 5.35308 6.06882 5.64751Z" fill="#1287B1"/> +<path d="M9.61357 5.35735C9.66507 5.40377 9.6981 5.47032 9.6981 5.54511C9.6981 5.6853 9.58434 5.79906 9.44414 5.79906C9.42989 5.79906 9.41613 5.79713 9.40245 5.79484C9.24449 6.0417 9.03517 6.25217 8.78931 6.41149C9.15265 6.4683 9.49465 6.62246 9.7774 6.85571C9.98336 6.48671 10.1011 6.06168 10.1011 5.60908C10.1011 5.26458 9.94897 5.01205 9.71937 4.8268C9.70734 5.01134 9.67123 5.18921 9.61357 5.35735Z" fill="#1287B1"/> +<path d="M6.55349 7.73633C6.67907 7.38953 6.89728 7.07834 7.18948 6.84201C7.14672 6.78341 7.11147 6.71943 7.08475 6.65081C6.72828 6.54242 6.41301 6.33954 6.16844 6.07097C6.16665 6.07878 6.16436 6.08637 6.16257 6.09418C6.11643 6.30057 6.10798 6.50739 6.13097 6.70912C6.30154 6.74093 6.43078 6.89043 6.43078 7.07031C6.43078 7.17698 6.385 7.27291 6.31243 7.34017C6.37827 7.47972 6.45922 7.61268 6.55349 7.73633Z" fill="#1287B1"/> +<path d="M6.49179 7.93954C6.342 7.79061 6.21627 7.61954 6.11913 7.43307C6.10087 7.43586 6.08224 7.43773 6.06326 7.43773C5.86009 7.43773 5.6954 7.27303 5.6954 7.06994C5.6954 6.92437 5.78022 6.79886 5.90293 6.73926C5.87506 6.51317 5.88452 6.27849 5.9371 6.04338C5.95215 5.97618 5.97049 5.91056 5.99162 5.84645C5.76746 5.51849 5.63615 5.12198 5.63615 4.6948C5.63615 4.67919 5.63687 4.66379 5.63723 4.64838C5.25841 4.83829 4.97437 5.13767 4.97437 5.60875C4.97437 6.651 5.59675 7.54726 6.48986 7.948C6.4905 7.9452 6.49108 7.94234 6.49179 7.93954Z" fill="#1287B1"/> +<path d="M8.66577 6.625C8.55036 6.95324 8.2383 7.189 7.87059 7.189C7.67387 7.189 7.49342 7.12116 7.35 7.0084C7.05435 7.23864 6.8312 7.55249 6.69495 7.90344C6.75605 7.96927 6.82139 8.03131 6.89002 8.08962C7.0969 8.14349 7.31389 8.17236 7.53776 8.17236C8.43824 8.17236 9.22991 7.70744 9.68717 7.00504C9.38314 6.79442 9.0304 6.65838 8.66577 6.625Z" fill="#1287B1"/> +<path d="M8.69551 6.40713L9.47485 6.22123L8.69257 6.18133L9.32921 5.69477L8.60682 5.95374L9.09775 5.2734L8.4197 5.72407L8.65675 4.92768L8.19111 5.58409L8.18051 4.7546L7.87411 5.49347L7.61866 4.74579L7.63248 5.62657L7.19098 4.83706L7.42989 5.67536L6.71373 5.15455L7.27472 5.85946L6.38563 5.53251L7.08732 6.05167L6.18756 6.07932L7.10752 6.31673L6.18677 6.51581L7.09563 6.58171L6.32918 7.0725L7.11841 6.78638L6.57082 7.4799L7.26276 6.92185L7.01361 7.85249L7.54781 7.09514L7.47295 7.9896L7.77619 7.17874L8.0343 8.03101L8.09705 7.17616L8.48017 7.9055L8.25379 7.07128L8.95534 7.61472L8.50245 6.92185L9.27212 7.22172L8.68362 6.64339L9.47277 6.70543L8.69551 6.40713Z" fill="white"/> +<g opacity="0.350006"> +<g opacity="0.350006"> +<path opacity="0.350006" d="M0.398438 7.89844C2.81548 5.48147 4.74589 4.73529 6.49785 4.39466C6.72967 4.3496 6.77351 3.84305 6.77351 3.84305C6.77351 3.84305 6.80597 4.21614 6.96822 4.26478C7.13041 4.31349 7.34124 3.68079 7.34124 3.68079C7.34124 3.68079 7.1466 4.24859 7.34124 4.28104C7.53595 4.31349 7.89278 3.7295 7.89278 3.7295C7.89278 3.7295 7.74685 4.23233 7.8442 4.26478C7.94142 4.29723 8.42805 3.53487 8.42805 3.53487C8.42805 3.53487 8.13606 4.03769 8.41186 4.0864C8.68773 4.13504 9.09005 3.74319 9.09005 3.74319C9.09005 3.74319 8.7737 4.10173 8.91468 4.15123C9.51493 4.36221 10.0006 3.58208 10.0006 3.58208C10.0006 3.58208 9.88809 3.92414 9.5961 4.29723C10.2287 4.45942 10.6985 3.50643 10.6985 3.50643L10.2287 4.45942C10.472 4.5893 11.4454 3.46996 11.4454 3.46996C11.4454 3.46996 10.9425 4.34594 10.6505 4.52432C10.8127 4.65413 11.3967 4.13504 11.3967 4.13504C11.3967 4.13504 10.9263 4.71896 11.1047 4.75141C11.3642 4.96231 12.3213 3.6646 12.3213 3.6646C12.3213 3.6646 11.9482 4.42697 11.3967 5.02721C11.8577 5.25781 13.0027 3.77815 13.0027 3.77815C13.0027 3.77815 12.9702 4.24859 12.1429 4.99476C12.7593 4.91367 13.5542 3.74569 13.5542 3.74569C13.5542 3.74569 13.2622 4.62168 12.5646 5.23804C13.1699 5.17507 14.138 3.68079 14.138 3.68079C14.138 3.68079 13.765 4.71896 13.0027 5.31921C13.8462 5.50575 15.0628 4.24859 15.0628 4.24859C15.0628 4.24859 14.5843 5.14083 13.9597 5.50575C14.6573 5.77339 15.6143 4.67032 15.6143 4.67032C15.6143 4.67032 14.6573 6.14655 13.1325 6.08164C12.6322 6.06037 11.1188 4.56379 7.87659 4.68658C3.59411 4.84884 2.68575 6.66563 0.398438 7.89844Z" fill="#373535"/> +</g> +</g> +<path d="M0.497437 7.71484C2.91448 5.29787 4.84482 4.55177 6.59678 4.21106C6.82867 4.166 6.87251 3.65953 6.87251 3.65953C6.87251 3.65953 6.90489 4.03269 7.06722 4.08126C7.22941 4.1299 7.44024 3.49734 7.44024 3.49734C7.44024 3.49734 7.2456 4.06514 7.44024 4.09752C7.63495 4.1299 7.99177 3.54598 7.99177 3.54598C7.99177 3.54598 7.84585 4.0488 7.94306 4.08126C8.04042 4.11371 8.52705 3.35127 8.52705 3.35127C8.52705 3.35127 8.23505 3.85417 8.51086 3.90281C8.78659 3.95152 9.18905 3.55966 9.18905 3.55966C9.18905 3.55966 8.8727 3.91821 9.01368 3.96771C9.61393 4.17861 10.0995 3.39855 10.0995 3.39855C10.0995 3.39855 9.98695 3.74062 9.6951 4.11371C10.3277 4.27597 10.7975 3.32283 10.7975 3.32283L10.3277 4.27597C10.571 4.4057 11.5443 3.28651 11.5443 3.28651C11.5443 3.28651 11.0415 4.16235 10.7495 4.3408C10.9117 4.4706 11.4957 3.95152 11.4957 3.95152C11.4957 3.95152 11.0253 4.53551 11.2037 4.56789C11.4632 4.77886 12.4202 3.48108 12.4202 3.48108C12.4202 3.48108 12.0471 4.24351 11.4957 4.84369C11.9567 5.07436 13.1017 3.59462 13.1017 3.59462C13.1017 3.59462 13.0692 4.06514 12.2419 4.81131C12.8583 4.73015 13.6532 3.56217 13.6532 3.56217C13.6532 3.56217 13.3612 4.43815 12.6636 5.05459C13.2689 4.99162 14.237 3.49734 14.237 3.49734C14.237 3.49734 13.864 4.53551 13.1017 5.13569C13.9452 5.32223 15.1618 4.06514 15.1618 4.06514C15.1618 4.06514 14.6833 4.95724 14.0587 5.32223C14.7563 5.58987 15.7133 4.48679 15.7133 4.48679C15.7133 4.48679 14.7563 5.96302 13.2313 5.89812C12.7312 5.87677 11.2178 4.3802 7.97551 4.50306C3.69311 4.66524 2.78467 6.48204 0.497437 7.71484Z" fill="#373535"/> +<g opacity="0.350006"> +<g opacity="0.350006"> +<path opacity="0.350006" d="M0.335083 8.1629C1.61252 8.80369 2.78057 8.06561 4.25264 8.48046C5.37183 8.79596 6.73452 9.18487 8.58369 8.91717C10.4329 8.6496 12.0146 7.84662 12.769 6.41093C13.0042 5.77028 13.9612 6.557 13.9612 6.557C13.9612 6.557 13.2638 6.26501 13.28 6.40291C13.2962 6.54074 14.3263 7.01928 14.3263 7.01928C14.3263 7.01928 13.3935 6.72728 13.4503 6.94628C13.5071 7.16528 14.6181 8.04126 14.6181 8.04126C14.6181 8.04126 13.361 7.08418 13.2557 7.21392C13.1501 7.34372 13.7909 7.87097 13.7909 7.87097C13.7909 7.87097 12.7284 7.21392 12.3553 7.4077C12.0869 7.54689 13.4584 8.44687 13.4584 8.44687C13.4584 8.44687 12.5662 7.76545 12.3553 7.9439C12.1444 8.12242 13.361 9.58232 13.361 9.58232C13.361 9.58232 11.9821 8.0899 11.8443 8.1629C11.7064 8.2359 12.2254 8.94969 12.2254 8.94969C12.2254 8.94969 11.5442 8.20344 11.3577 8.33325C11.1712 8.46306 12.331 10.4015 12.331 10.4015C12.331 10.4015 11.0738 8.41442 10.5547 8.74681C11.3699 10.5353 11.0657 10.7908 11.0657 10.7908C11.0657 10.7908 10.9617 8.96624 9.94637 9.01452C9.43539 9.03888 10.1897 10.3771 10.1897 10.3771C10.1897 10.3771 9.49542 9.15486 9.03307 9.17922C9.8852 10.7918 9.58145 11.4478 9.58145 11.4478C9.58145 11.4478 9.72236 10.0705 8.56664 9.30401C9.00456 9.69321 8.29184 11.4478 8.29184 11.4478C8.29184 11.4478 8.91881 8.86229 7.82942 9.35523C7.65728 9.43295 7.81724 10.6448 7.81724 10.6448C7.81724 10.6448 7.59825 9.2578 7.26986 9.33087C7.07837 9.37335 6.05317 11.2531 6.05317 11.2531C6.05317 11.2531 6.89269 9.22134 6.63723 9.30651C6.45112 9.36862 6.10189 10.2069 6.10189 10.2069C6.10189 10.2069 6.27217 9.37951 6.10189 9.33087C5.9316 9.28216 4.98255 10.2069 4.98255 10.2069C4.98255 10.2069 5.68825 9.37951 5.56654 9.20916C5.38215 8.95105 5.16602 8.93894 4.90955 9.03888C4.5364 9.1843 4.03365 9.86615 4.03365 9.86615C4.03365 9.86615 4.53482 9.17578 4.44721 8.91717C4.30658 8.50167 2.86567 9.72015 2.86567 9.72015C2.86567 9.72015 3.7173 8.91717 3.52266 8.74681C3.32802 8.57653 2.27108 8.62918 1.94105 8.62825C0.967784 8.62524 0.630515 8.39694 0.335083 8.1629Z" fill="#373535"/> +</g> +</g> +<path d="M0.286499 8.0169C1.56394 8.6577 2.73191 7.91962 4.20399 8.33454C5.32317 8.64989 6.68593 9.03888 8.53518 8.77117C10.3844 8.50353 11.966 7.70055 12.7204 6.26501C12.9556 5.62429 13.9126 6.41093 13.9126 6.41093C13.9126 6.41093 13.2151 6.11901 13.2314 6.25691C13.2476 6.39474 14.2776 6.87328 14.2776 6.87328C14.2776 6.87328 13.3448 6.58136 13.4016 6.80036C13.4585 7.01928 14.5696 7.89526 14.5696 7.89526C14.5696 7.89526 13.3124 6.93811 13.207 7.06799C13.1016 7.19773 13.7423 7.72491 13.7423 7.72491C13.7423 7.72491 12.6798 7.06799 12.3067 7.26163C12.0382 7.40089 13.4097 8.3008 13.4097 8.3008C13.4097 8.3008 12.5175 7.61946 12.3067 7.7979C12.0958 7.97643 13.3125 9.43632 13.3125 9.43632C13.3125 9.43632 11.9335 7.9439 11.7957 8.0169C11.6578 8.08997 12.1769 8.80362 12.1769 8.80362C12.1769 8.80362 11.4955 8.05745 11.309 8.18725C11.1225 8.31699 12.2823 10.2555 12.2823 10.2555C12.2823 10.2555 11.0251 8.26835 10.5061 8.60089C11.3212 10.3893 11.0171 10.6448 11.0171 10.6448C11.0171 10.6448 10.913 8.82024 9.89772 8.8686C9.38673 8.89295 10.141 10.2311 10.141 10.2311C10.141 10.2311 9.44676 9.00879 8.98449 9.03315C9.83668 10.6458 9.5328 11.3018 9.5328 11.3018C9.5328 11.3018 9.67371 9.92439 8.51798 9.15794C8.9559 9.54722 8.24318 11.3018 8.24318 11.3018C8.24318 11.3018 8.8703 8.7163 7.78091 9.20923C7.60876 9.28703 7.76873 10.4988 7.76873 10.4988C7.76873 10.4988 7.54959 9.11188 7.22128 9.18488C7.02972 9.22736 6.00452 11.1071 6.00452 11.1071C6.00452 11.1071 6.84404 9.07534 6.58858 9.16059C6.40246 9.22256 6.05323 10.0608 6.05323 10.0608C6.05323 10.0608 6.22358 9.23352 6.05323 9.18488C5.88295 9.13623 4.93397 10.0608 4.93397 10.0608C4.93397 10.0608 5.63967 9.23352 5.51789 9.06316C5.33349 8.80505 5.11736 8.79302 4.86097 8.89295C4.48774 9.03831 3.98499 9.72015 3.98499 9.72015C3.98499 9.72015 4.48616 9.02971 4.39862 8.77117C4.25793 8.35574 2.81709 9.57415 2.81709 9.57415C2.81709 9.57415 3.66871 8.77117 3.474 8.60089C3.27936 8.43053 2.22243 8.48326 1.89239 8.48226C0.919128 8.47918 0.581931 8.25094 0.286499 8.0169Z" fill="#373535"/> +</g> +<defs> +<clipPath id="clip0"> +<rect y="3" width="16" height="10.7443" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg new file mode 100644 index 0000000000000..9ae4b31c1a0d6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg @@ -0,0 +1 @@ +<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0, 0, 400,400"><g id="svgg"><path id="path0" d="M262.901 28.177 C 262.047 28.895,261.166 29.186,259.570 29.277 C 257.669 29.385,257.384 29.511,257.272 30.300 C 257.159 31.094,256.913 31.200,255.172 31.200 C 253.348 31.200,253.200 31.273,253.200 32.177 C 253.200 33.040,253.002 33.168,251.500 33.277 C 250.198 33.371,249.709 33.607,249.412 34.284 C 249.199 34.770,248.659 35.220,248.212 35.284 C 247.721 35.354,247.349 35.756,247.272 36.300 C 247.180 36.951,246.876 37.200,246.172 37.200 C 245.402 37.200,245.200 37.408,245.200 38.200 C 245.200 39.000,245.000 39.200,244.200 39.200 C 243.400 39.200,243.200 39.400,243.200 40.200 C 243.200 41.000,243.000 41.200,242.200 41.200 C 241.408 41.200,241.200 41.402,241.200 42.172 C 241.200 42.876,240.951 43.180,240.300 43.272 C 239.756 43.349,239.354 43.721,239.284 44.212 C 239.220 44.659,238.770 45.199,238.284 45.412 C 237.607 45.709,237.371 46.198,237.277 47.500 C 237.168 49.002,237.040 49.200,236.177 49.200 C 235.273 49.200,235.200 49.348,235.200 51.172 C 235.200 52.913,235.094 53.159,234.300 53.272 C 233.511 53.384,233.385 53.669,233.277 55.570 C 233.186 57.166,232.895 58.047,232.177 58.901 L 231.200 60.061 231.200 142.497 C 231.200 187.837,231.314 224.819,231.453 224.680 C 232.048 224.086,233.160 226.374,233.277 228.434 C 233.385 230.331,233.512 230.616,234.300 230.728 C 235.094 230.841,235.200 231.087,235.200 232.828 C 235.200 234.665,235.269 234.800,236.200 234.800 C 237.141 234.800,237.584 236.367,237.247 238.500 C 237.221 238.665,237.335 238.779,237.500 238.753 C 238.548 238.587,239.161 238.915,239.272 239.700 C 239.354 240.278,239.722 240.646,240.300 240.728 C 240.951 240.820,241.200 241.124,241.200 241.828 C 241.200 242.598,241.408 242.800,242.200 242.800 C 243.000 242.800,243.200 243.000,243.200 243.800 C 243.200 244.600,243.400 244.800,244.200 244.800 C 245.000 244.800,245.200 245.000,245.200 245.800 C 245.200 246.592,245.402 246.800,246.172 246.800 C 246.876 246.800,247.180 247.049,247.272 247.700 C 247.354 248.278,247.722 248.646,248.300 248.728 C 249.085 248.839,249.413 249.452,249.247 250.500 C 249.221 250.665,249.335 250.779,249.500 250.753 C 251.633 250.416,253.200 250.859,253.200 251.800 C 253.200 252.350,253.335 252.779,253.500 252.753 C 255.453 252.444,257.155 252.872,257.272 253.700 C 257.385 254.497,257.663 254.614,259.686 254.720 C 261.540 254.816,262.115 255.021,262.727 255.798 L 263.481 256.757 276.088 256.789 L 288.695 256.821 289.344 255.829 C 289.903 254.977,290.343 254.822,292.497 254.719 C 294.727 254.613,295.014 254.504,295.125 253.721 C 295.231 252.979,295.544 252.823,297.125 252.721 C 298.258 252.648,299.020 252.402,299.050 252.100 C 299.078 251.825,299.123 251.420,299.150 251.200 C 299.178 250.980,299.802 250.800,300.538 250.800 C 301.852 250.800,303.486 249.619,302.934 249.068 C 302.787 248.920,303.147 248.800,303.733 248.800 C 304.604 248.800,304.800 248.617,304.800 247.800 C 304.800 247.000,305.000 246.800,305.800 246.800 C 306.600 246.800,306.800 246.600,306.800 245.800 C 306.800 245.000,307.000 244.800,307.800 244.800 C 308.600 244.800,308.800 244.600,308.800 243.800 C 308.800 243.000,309.000 242.800,309.800 242.800 C 310.600 242.800,310.800 242.600,310.800 241.800 C 310.800 241.000,311.000 240.800,311.800 240.800 C 312.572 240.800,312.800 240.594,312.800 239.895 C 312.800 239.340,313.148 238.837,313.700 238.595 C 314.395 238.290,314.628 237.813,314.723 236.500 C 314.832 234.998,314.960 234.800,315.823 234.800 C 316.729 234.800,316.800 234.654,316.800 232.800 C 316.800 230.933,316.867 230.800,317.800 230.800 C 318.754 230.800,318.800 230.691,318.800 228.429 C 318.800 226.375,318.934 225.953,319.802 225.270 L 320.804 224.482 320.798 141.996 L 320.792 59.509 319.816 58.741 C 319.018 58.114,318.817 57.553,318.720 55.686 C 318.614 53.663,318.497 53.385,317.700 53.272 C 316.906 53.159,316.800 52.913,316.800 51.172 C 316.800 49.335,316.731 49.200,315.800 49.200 C 314.859 49.200,314.416 47.633,314.753 45.500 C 314.779 45.335,314.665 45.221,314.500 45.247 C 313.452 45.413,312.839 45.085,312.728 44.300 C 312.646 43.722,312.278 43.354,311.700 43.272 C 311.049 43.180,310.800 42.876,310.800 42.172 C 310.800 41.402,310.592 41.200,309.800 41.200 C 309.000 41.200,308.800 41.000,308.800 40.200 C 308.800 39.400,308.600 39.200,307.800 39.200 C 307.000 39.200,306.800 39.000,306.800 38.200 C 306.800 37.408,306.598 37.200,305.828 37.200 C 305.130 37.200,304.820 36.950,304.728 36.316 C 304.649 35.774,304.214 35.326,303.600 35.156 C 303.050 35.004,302.757 34.862,302.949 34.840 C 303.141 34.818,303.097 34.558,302.852 34.263 C 302.607 33.968,302.540 33.593,302.703 33.430 C 302.867 33.267,302.820 33.170,302.600 33.214 C 300.796 33.578,299.273 33.145,299.036 32.200 C 298.815 31.319,298.571 31.200,296.992 31.200 C 295.386 31.200,295.115 31.037,295.234 30.145 C 295.310 29.579,294.089 29.212,292.109 29.206 C 290.324 29.201,289.923 29.054,289.363 28.200 L 288.708 27.200 276.385 27.200 L 264.061 27.200 262.901 28.177 M110.400 32.600 C 109.992 33.091,110.020 33.200,110.551 33.200 C 110.908 33.200,111.200 32.930,111.200 32.600 C 111.200 32.270,111.132 32.000,111.049 32.000 C 110.966 32.000,110.674 32.270,110.400 32.600 M136.800 32.600 C 136.800 32.930,137.092 33.200,137.449 33.200 C 137.980 33.200,138.008 33.091,137.600 32.600 C 137.326 32.270,137.034 32.000,136.951 32.000 C 136.868 32.000,136.800 32.270,136.800 32.600 M80.400 62.600 C 79.992 63.091,80.020 63.200,80.551 63.200 C 80.908 63.200,81.200 62.930,81.200 62.600 C 81.200 62.270,81.132 62.000,81.049 62.000 C 80.966 62.000,80.674 62.270,80.400 62.600 M166.800 62.600 C 166.800 62.930,167.092 63.200,167.449 63.200 C 167.980 63.200,168.008 63.091,167.600 62.600 C 167.326 62.270,167.034 62.000,166.951 62.000 C 166.868 62.000,166.800 62.270,166.800 62.600 M80.000 88.951 C 80.000 89.034,80.270 89.326,80.600 89.600 C 81.091 90.008,81.200 89.980,81.200 89.449 C 81.200 89.092,80.930 88.800,80.600 88.800 C 80.270 88.800,80.000 88.868,80.000 88.951 M166.800 89.449 C 166.800 89.980,166.909 90.008,167.400 89.600 C 167.730 89.326,168.000 89.034,168.000 88.951 C 168.000 88.868,167.730 88.800,167.400 88.800 C 167.070 88.800,166.800 89.092,166.800 89.449 M110.000 118.951 C 110.000 119.034,110.270 119.326,110.600 119.600 C 111.091 120.008,111.200 119.980,111.200 119.449 C 111.200 119.092,110.930 118.800,110.600 118.800 C 110.270 118.800,110.000 118.868,110.000 118.951 M136.800 119.449 C 136.800 119.980,136.909 120.008,137.400 119.600 C 137.730 119.326,138.000 119.034,138.000 118.951 C 138.000 118.868,137.730 118.800,137.400 118.800 C 137.070 118.800,136.800 119.092,136.800 119.449 M110.400 136.600 C 109.992 137.091,110.020 137.200,110.551 137.200 C 110.908 137.200,111.200 136.930,111.200 136.600 C 111.200 136.270,111.132 136.000,111.049 136.000 C 110.966 136.000,110.674 136.270,110.400 136.600 M136.800 136.600 C 136.800 136.930,137.092 137.200,137.449 137.200 C 137.980 137.200,138.008 137.091,137.600 136.600 C 137.326 136.270,137.034 136.000,136.951 136.000 C 136.868 136.000,136.800 136.270,136.800 136.600 M80.400 166.600 C 79.992 167.091,80.020 167.200,80.551 167.200 C 80.908 167.200,81.200 166.930,81.200 166.600 C 81.200 166.270,81.132 166.000,81.049 166.000 C 80.966 166.000,80.674 166.270,80.400 166.600 M166.800 166.600 C 166.800 166.930,167.092 167.200,167.449 167.200 C 167.980 167.200,168.008 167.091,167.600 166.600 C 167.326 166.270,167.034 166.000,166.951 166.000 C 166.868 166.000,166.800 166.270,166.800 166.600 M80.000 192.951 C 80.000 193.034,80.270 193.326,80.600 193.600 C 81.091 194.008,81.200 193.980,81.200 193.449 C 81.200 193.092,80.930 192.800,80.600 192.800 C 80.270 192.800,80.000 192.868,80.000 192.951 M166.800 193.449 C 166.800 193.980,166.909 194.008,167.400 193.600 C 167.730 193.326,168.000 193.034,168.000 192.951 C 168.000 192.868,167.730 192.800,167.400 192.800 C 167.070 192.800,166.800 193.092,166.800 193.449 M110.000 222.951 C 110.000 223.034,110.270 223.326,110.600 223.600 C 111.091 224.008,111.200 223.980,111.200 223.449 C 111.200 223.092,110.930 222.800,110.600 222.800 C 110.270 222.800,110.000 222.868,110.000 222.951 M136.800 223.449 C 136.800 223.980,136.909 224.008,137.400 223.600 C 137.730 223.326,138.000 223.034,138.000 222.951 C 138.000 222.868,137.730 222.800,137.400 222.800 C 137.070 222.800,136.800 223.092,136.800 223.449 M91.476 247.457 C 91.324 247.609,91.200 248.063,91.200 248.467 C 91.200 248.982,90.903 249.200,90.200 249.200 C 89.408 249.200,89.200 249.402,89.200 250.172 C 89.200 250.876,88.951 251.180,88.300 251.272 C 87.700 251.357,87.354 251.722,87.264 252.365 C 87.189 252.896,86.739 253.645,86.264 254.030 C 84.836 255.185,84.743 261.396,86.128 263.042 C 86.680 263.697,87.191 264.766,87.265 265.417 C 87.365 266.288,87.637 266.634,88.300 266.728 C 88.953 266.821,89.200 267.124,89.200 267.832 C 89.200 268.779,90.288 270.089,95.340 275.226 C 96.406 276.311,97.427 277.107,97.607 276.995 C 98.053 276.720,99.200 277.914,99.200 278.652 C 99.200 278.979,99.365 279.145,99.568 279.020 C 100.102 278.690,101.200 279.923,101.200 280.852 C 101.200 281.289,101.360 281.549,101.555 281.428 C 101.995 281.156,102.892 282.175,102.530 282.537 C 102.385 282.682,102.561 282.800,102.921 282.800 C 103.317 282.800,103.490 283.021,103.360 283.360 C 103.124 283.974,103.773 284.510,104.979 284.698 C 105.825 284.830,107.320 286.013,107.020 286.314 C 106.918 286.415,107.450 287.044,108.203 287.710 C 108.956 288.377,109.517 289.030,109.449 289.161 C 109.305 289.443,110.665 290.798,111.100 290.805 C 111.265 290.808,111.405 291.213,111.412 291.705 C 111.421 292.390,111.626 292.572,112.286 292.479 C 113.150 292.358,114.305 293.745,114.052 294.600 C 113.987 294.820,114.067 294.867,114.230 294.703 C 114.703 294.230,115.577 295.195,115.308 295.895 C 115.146 296.317,115.222 296.434,115.532 296.242 C 115.789 296.083,116.000 296.143,116.000 296.376 C 116.000 296.609,116.135 296.779,116.300 296.753 C 117.011 296.640,117.635 296.823,117.487 297.100 C 117.316 297.420,118.631 298.822,119.050 298.767 C 119.188 298.749,119.607 299.200,119.980 299.770 C 120.354 300.340,120.765 300.702,120.892 300.575 C 121.020 300.447,121.243 300.883,121.388 301.543 C 121.588 302.454,121.838 302.708,122.426 302.595 C 122.852 302.513,123.200 302.614,123.200 302.820 C 123.200 303.026,123.742 303.677,124.405 304.267 C 125.268 305.037,125.538 305.564,125.359 306.129 C 125.153 306.777,125.257 306.878,125.941 306.700 C 126.565 306.536,126.972 306.796,127.570 307.741 C 128.009 308.433,128.510 308.910,128.684 308.800 C 128.858 308.690,129.055 309.171,129.123 309.869 C 129.226 310.934,129.098 311.159,128.323 311.269 C 127.722 311.354,127.355 311.714,127.272 312.300 C 127.180 312.951,126.876 313.200,126.172 313.200 C 125.402 313.200,125.200 313.408,125.200 314.200 C 125.200 315.000,125.000 315.200,124.200 315.200 C 123.400 315.200,123.200 315.400,123.200 316.200 C 123.200 317.000,123.000 317.200,122.200 317.200 C 121.400 317.200,121.200 317.400,121.200 318.200 C 121.200 319.000,121.000 319.200,120.200 319.200 C 119.400 319.200,119.200 319.400,119.200 320.200 C 119.200 321.000,119.000 321.200,118.200 321.200 C 117.400 321.200,117.200 321.400,117.200 322.200 C 117.200 323.000,117.000 323.200,116.200 323.200 C 115.408 323.200,115.200 323.402,115.200 324.172 C 115.200 324.876,114.951 325.180,114.300 325.272 C 113.722 325.354,113.354 325.722,113.272 326.300 C 113.180 326.951,112.876 327.200,112.172 327.200 C 111.402 327.200,111.200 327.408,111.200 328.200 C 111.200 329.000,111.000 329.200,110.200 329.200 C 109.408 329.200,109.200 329.402,109.200 330.172 C 109.200 330.876,108.951 331.180,108.300 331.272 C 107.730 331.353,107.354 331.721,107.275 332.275 C 107.198 332.821,106.821 333.198,106.275 333.275 C 105.730 333.353,105.353 333.730,105.275 334.275 C 105.198 334.821,104.821 335.198,104.275 335.275 C 103.730 335.353,103.353 335.730,103.275 336.275 C 103.198 336.821,102.821 337.198,102.275 337.275 C 101.730 337.353,101.353 337.730,101.275 338.275 C 101.198 338.821,100.821 339.198,100.275 339.275 C 99.730 339.353,99.353 339.730,99.275 340.275 C 99.198 340.821,98.821 341.198,98.275 341.275 C 97.736 341.352,97.353 341.730,97.278 342.259 C 97.210 342.732,96.792 343.213,96.348 343.330 C 95.904 343.446,95.443 343.914,95.324 344.370 C 95.181 344.917,94.782 345.200,94.153 345.200 C 93.410 345.200,93.200 345.415,93.200 346.172 C 93.200 346.876,92.951 347.180,92.300 347.272 C 91.730 347.353,91.354 347.721,91.275 348.275 C 91.198 348.821,90.821 349.198,90.275 349.275 C 89.739 349.352,89.353 349.730,89.279 350.250 C 89.212 350.718,88.717 351.514,88.179 352.020 C 87.536 352.623,87.200 353.379,87.200 354.218 C 87.200 355.288,87.055 355.488,86.307 355.448 C 85.461 355.403,85.407 355.576,85.272 358.751 C 85.082 363.242,85.385 364.796,86.413 364.597 C 87.049 364.475,87.200 364.668,87.200 365.604 C 87.200 367.017,87.987 368.855,88.466 368.559 C 88.658 368.441,89.052 368.909,89.343 369.599 C 89.633 370.289,89.990 370.842,90.135 370.827 C 91.916 370.644,93.200 370.878,93.200 371.386 C 93.200 373.128,102.482 373.434,102.728 371.700 C 102.841 370.906,103.087 370.800,104.828 370.800 C 106.650 370.800,106.800 370.726,106.800 369.828 C 106.800 369.124,107.049 368.820,107.700 368.728 C 108.270 368.647,108.646 368.279,108.725 367.725 C 108.802 367.179,109.179 366.802,109.725 366.725 C 110.279 366.646,110.647 366.270,110.728 365.700 C 110.820 365.049,111.124 364.800,111.828 364.800 C 112.598 364.800,112.800 364.592,112.800 363.800 C 112.800 363.000,113.000 362.800,113.800 362.800 C 114.592 362.800,114.800 362.598,114.800 361.828 C 114.800 361.124,115.049 360.820,115.700 360.728 C 116.270 360.647,116.646 360.279,116.725 359.725 C 116.802 359.179,117.179 358.802,117.725 358.725 C 118.264 358.648,118.647 358.270,118.722 357.741 C 118.790 357.268,119.208 356.787,119.652 356.670 C 120.096 356.554,120.553 356.103,120.667 355.667 C 120.781 355.231,121.231 354.781,121.667 354.667 C 122.103 354.553,122.553 354.103,122.667 353.667 C 122.781 353.231,123.231 352.781,123.667 352.667 C 124.103 352.553,124.557 352.086,124.676 351.630 C 124.819 351.083,125.218 350.800,125.847 350.800 C 126.597 350.800,126.800 350.587,126.800 349.800 C 126.800 349.000,127.000 348.800,127.800 348.800 C 128.600 348.800,128.800 348.600,128.800 347.800 C 128.800 346.841,129.147 346.614,130.354 346.783 C 130.702 346.831,130.820 346.630,130.671 346.242 C 130.382 345.487,131.502 344.277,132.231 344.557 C 132.550 344.679,132.679 344.550,132.557 344.231 C 132.294 343.546,133.546 342.294,134.231 342.557 C 134.541 342.675,134.679 342.551,134.566 342.255 C 134.364 341.728,137.177 338.542,137.700 338.706 C 137.865 338.758,137.955 338.575,137.900 338.300 C 137.845 338.025,138.038 337.849,138.330 337.910 C 138.621 337.970,138.753 337.848,138.623 337.638 C 138.352 337.198,139.118 336.626,140.000 336.611 C 140.330 336.605,140.605 336.330,140.611 336.000 C 140.628 335.039,141.207 334.370,141.921 334.489 C 142.284 334.550,142.603 334.330,142.630 334.000 C 142.740 332.640,142.838 332.506,143.698 332.552 C 144.376 332.588,144.596 332.380,144.598 331.700 C 144.599 331.205,144.735 330.800,144.900 330.800 C 145.385 330.800,146.688 329.427,146.513 329.100 C 146.365 328.823,146.989 328.640,147.700 328.753 C 147.865 328.779,147.955 328.575,147.900 328.300 C 147.845 328.025,148.038 327.849,148.330 327.910 C 148.621 327.970,148.758 327.856,148.634 327.655 C 148.342 327.183,149.380 326.314,149.770 326.703 C 149.933 326.867,150.007 326.730,149.933 326.400 C 149.860 326.068,150.036 325.849,150.330 325.910 C 150.621 325.970,150.762 325.862,150.643 325.669 C 150.523 325.476,150.703 324.976,151.042 324.559 C 151.382 324.142,151.535 324.042,151.382 324.338 C 151.189 324.713,151.331 324.833,151.848 324.734 C 152.257 324.655,152.655 324.257,152.734 323.848 C 152.837 323.314,152.726 323.196,152.338 323.429 C 151.896 323.696,151.897 323.634,152.345 323.081 C 152.645 322.711,153.050 322.507,153.245 322.628 C 153.440 322.749,153.600 322.567,153.600 322.224 C 153.600 321.881,153.881 321.600,154.224 321.600 C 154.567 321.600,154.736 321.420,154.600 321.200 C 154.464 320.980,154.633 320.800,154.976 320.800 C 155.319 320.800,155.600 320.609,155.600 320.376 C 155.600 320.143,155.375 320.084,155.100 320.245 C 154.825 320.406,154.943 320.147,155.362 319.669 C 156.151 318.769,156.633 318.500,156.200 319.200 C 156.064 319.420,156.143 319.600,156.376 319.600 C 156.609 319.600,156.800 319.420,156.800 319.200 C 156.800 318.980,156.993 318.800,157.229 318.800 C 157.791 318.800,158.800 317.779,158.800 317.210 C 158.800 316.965,159.236 316.328,159.769 315.795 C 160.302 315.262,160.851 314.469,160.989 314.033 C 161.128 313.597,161.592 313.130,162.020 312.994 C 162.449 312.857,162.800 312.568,162.800 312.349 C 162.800 312.131,162.960 312.051,163.155 312.172 C 163.350 312.293,163.732 312.123,164.004 311.796 C 164.276 311.468,164.350 311.200,164.169 311.200 C 163.988 311.200,164.056 310.984,164.320 310.720 C 165.235 309.805,164.914 307.644,163.823 307.370 C 163.228 307.221,162.798 306.787,162.723 306.262 C 162.647 305.730,162.265 305.352,161.725 305.275 C 161.179 305.198,160.802 304.821,160.725 304.275 C 160.647 303.730,160.270 303.353,159.725 303.275 C 159.179 303.198,158.802 302.821,158.725 302.275 C 158.647 301.730,158.270 301.353,157.725 301.275 C 157.179 301.198,156.802 300.821,156.725 300.275 C 156.647 299.730,156.270 299.353,155.725 299.275 C 155.179 299.198,154.802 298.821,154.725 298.275 C 154.647 297.730,154.270 297.353,153.725 297.275 C 153.179 297.198,152.802 296.821,152.725 296.275 C 152.646 295.721,152.270 295.353,151.700 295.272 C 151.049 295.180,150.800 294.876,150.800 294.172 C 150.800 293.402,150.592 293.200,149.800 293.200 C 149.000 293.200,148.800 293.000,148.800 292.200 C 148.800 291.400,148.600 291.200,147.800 291.200 C 147.000 291.200,146.800 291.000,146.800 290.200 C 146.800 289.400,146.600 289.200,145.800 289.200 C 145.000 289.200,144.800 289.000,144.800 288.200 C 144.800 287.400,144.600 287.200,143.800 287.200 C 143.000 287.200,142.800 287.000,142.800 286.200 C 142.800 285.400,142.600 285.200,141.800 285.200 C 141.000 285.200,140.800 285.000,140.800 284.200 C 140.800 283.408,140.598 283.200,139.828 283.200 C 139.124 283.200,138.820 282.951,138.728 282.300 C 138.647 281.730,138.279 281.354,137.725 281.275 C 137.179 281.198,136.802 280.821,136.725 280.275 C 136.646 279.721,136.270 279.353,135.700 279.272 C 135.049 279.180,134.800 278.876,134.800 278.172 C 134.800 277.402,134.592 277.200,133.800 277.200 C 133.000 277.200,132.800 277.000,132.800 276.200 C 132.800 275.400,132.600 275.200,131.800 275.200 C 131.000 275.200,130.800 275.000,130.800 274.200 C 130.800 273.400,130.600 273.200,129.800 273.200 C 129.000 273.200,128.800 273.000,128.800 272.200 C 128.800 271.400,128.600 271.200,127.800 271.200 C 127.000 271.200,126.800 271.000,126.800 270.200 C 126.800 269.400,126.600 269.200,125.800 269.200 C 125.000 269.200,124.800 269.000,124.800 268.200 C 124.800 267.400,124.600 267.200,123.800 267.200 C 123.086 267.200,122.800 266.984,122.800 266.443 C 122.800 266.027,121.855 264.686,120.700 263.463 C 119.545 262.239,117.748 260.285,116.707 259.119 C 112.577 254.494,111.587 253.554,110.417 253.146 C 109.748 252.913,109.178 252.559,109.150 252.361 C 109.123 252.162,109.055 251.820,109.000 251.600 C 108.945 251.380,108.878 250.990,108.850 250.733 C 108.822 250.477,108.682 250.385,108.537 250.530 C 108.175 250.892,107.156 249.995,107.428 249.555 C 107.549 249.360,107.019 249.200,106.251 249.200 C 105.108 249.200,104.832 249.037,104.728 248.300 C 104.599 247.394,92.320 246.613,91.476 247.457 M262.400 284.600 C 261.992 285.091,262.020 285.200,262.551 285.200 C 262.908 285.200,263.200 284.930,263.200 284.600 C 263.200 284.270,263.132 284.000,263.049 284.000 C 262.966 284.000,262.674 284.270,262.400 284.600 M232.400 314.600 C 231.992 315.091,232.020 315.200,232.551 315.200 C 232.908 315.200,233.200 314.930,233.200 314.600 C 233.200 314.270,233.132 314.000,233.049 314.000 C 232.966 314.000,232.674 314.270,232.400 314.600 M318.800 314.600 C 318.800 314.930,319.092 315.200,319.449 315.200 C 319.980 315.200,320.008 315.091,319.600 314.600 C 319.326 314.270,319.034 314.000,318.951 314.000 C 318.868 314.000,318.800 314.270,318.800 314.600 M232.000 340.951 C 232.000 341.034,232.270 341.326,232.600 341.600 C 233.091 342.008,233.200 341.980,233.200 341.449 C 233.200 341.092,232.930 340.800,232.600 340.800 C 232.270 340.800,232.000 340.868,232.000 340.951 M318.800 341.449 C 318.800 341.980,318.909 342.008,319.400 341.600 C 319.730 341.326,320.000 341.034,320.000 340.951 C 320.000 340.868,319.730 340.800,319.400 340.800 C 319.070 340.800,318.800 341.092,318.800 341.449 M262.000 370.951 C 262.000 371.034,262.270 371.326,262.600 371.600 C 263.091 372.008,263.200 371.980,263.200 371.449 C 263.200 371.092,262.930 370.800,262.600 370.800 C 262.270 370.800,262.000 370.868,262.000 370.951 " stroke="none" fill="#646cb4" fill-rule="evenodd"></path><path id="path1" d="M111.200 32.200 C 111.200 33.178,111.133 33.200,108.200 33.200 C 105.267 33.200,105.200 33.222,105.200 34.200 C 105.200 35.133,105.067 35.200,103.200 35.200 C 101.333 35.200,101.200 35.267,101.200 36.200 C 101.200 37.133,101.067 37.200,99.200 37.200 C 97.333 37.200,97.200 37.267,97.200 38.200 C 97.200 39.000,97.000 39.200,96.200 39.200 C 95.400 39.200,95.200 39.400,95.200 40.200 C 95.200 41.000,95.000 41.200,94.200 41.200 C 93.400 41.200,93.200 41.400,93.200 42.200 C 93.200 43.000,93.000 43.200,92.200 43.200 C 91.400 43.200,91.200 43.400,91.200 44.200 C 91.200 45.000,91.000 45.200,90.200 45.200 C 89.400 45.200,89.200 45.400,89.200 46.200 C 89.200 47.000,89.000 47.200,88.200 47.200 C 87.400 47.200,87.200 47.400,87.200 48.200 C 87.200 49.000,87.000 49.200,86.200 49.200 C 85.267 49.200,85.200 49.333,85.200 51.200 C 85.200 53.067,85.133 53.200,84.200 53.200 C 83.267 53.200,83.200 53.333,83.200 55.200 C 83.200 57.067,83.133 57.200,82.200 57.200 C 81.222 57.200,81.200 57.267,81.200 60.200 C 81.200 63.133,81.178 63.200,80.200 63.200 L 79.200 63.200 79.200 76.000 L 79.200 88.800 80.200 88.800 C 81.178 88.800,81.200 88.867,81.200 91.800 C 81.200 94.733,81.222 94.800,82.200 94.800 C 83.133 94.800,83.200 94.933,83.200 96.800 C 83.200 98.667,83.267 98.800,84.200 98.800 C 85.133 98.800,85.200 98.933,85.200 100.800 C 85.200 102.667,85.267 102.800,86.200 102.800 C 87.000 102.800,87.200 103.000,87.200 103.800 C 87.200 104.600,87.400 104.800,88.200 104.800 C 89.000 104.800,89.200 105.000,89.200 105.800 C 89.200 106.600,89.400 106.800,90.200 106.800 C 91.000 106.800,91.200 107.000,91.200 107.800 C 91.200 108.600,91.400 108.800,92.200 108.800 C 93.000 108.800,93.200 109.000,93.200 109.800 C 93.200 110.600,93.400 110.800,94.200 110.800 C 95.000 110.800,95.200 111.000,95.200 111.800 C 95.200 112.600,95.400 112.800,96.200 112.800 C 97.000 112.800,97.200 113.000,97.200 113.800 C 97.200 114.733,97.333 114.800,99.200 114.800 C 101.067 114.800,101.200 114.867,101.200 115.800 C 101.200 116.733,101.333 116.800,103.200 116.800 C 105.067 116.800,105.200 116.867,105.200 117.800 C 105.200 118.778,105.267 118.800,108.200 118.800 C 111.133 118.800,111.200 118.822,111.200 119.800 L 111.200 120.800 124.000 120.800 L 136.800 120.800 136.800 119.800 C 136.800 118.822,136.867 118.800,139.800 118.800 C 142.733 118.800,142.800 118.778,142.800 117.800 C 142.800 116.867,142.933 116.800,144.800 116.800 C 146.667 116.800,146.800 116.733,146.800 115.800 C 146.800 114.867,146.933 114.800,148.800 114.800 C 150.667 114.800,150.800 114.733,150.800 113.800 C 150.800 113.000,151.000 112.800,151.800 112.800 C 152.600 112.800,152.800 112.600,152.800 111.800 C 152.800 111.000,153.000 110.800,153.800 110.800 C 154.600 110.800,154.800 110.600,154.800 109.800 C 154.800 109.000,155.000 108.800,155.800 108.800 C 156.600 108.800,156.800 108.600,156.800 107.800 C 156.800 107.000,157.000 106.800,157.800 106.800 C 158.600 106.800,158.800 106.600,158.800 105.800 C 158.800 105.000,159.000 104.800,159.800 104.800 C 160.600 104.800,160.800 104.600,160.800 103.800 C 160.800 103.000,161.000 102.800,161.800 102.800 C 162.733 102.800,162.800 102.667,162.800 100.800 C 162.800 98.933,162.867 98.800,163.800 98.800 C 164.733 98.800,164.800 98.667,164.800 96.800 C 164.800 94.933,164.867 94.800,165.800 94.800 C 166.778 94.800,166.800 94.733,166.800 91.800 C 166.800 88.867,166.822 88.800,167.800 88.800 L 168.800 88.800 168.800 76.000 L 168.800 63.200 167.800 63.200 C 166.822 63.200,166.800 63.133,166.800 60.200 C 166.800 57.267,166.778 57.200,165.800 57.200 C 164.867 57.200,164.800 57.067,164.800 55.200 C 164.800 53.333,164.733 53.200,163.800 53.200 C 162.867 53.200,162.800 53.067,162.800 51.200 C 162.800 49.333,162.733 49.200,161.800 49.200 C 161.000 49.200,160.800 49.000,160.800 48.200 C 160.800 47.400,160.600 47.200,159.800 47.200 C 159.000 47.200,158.800 47.000,158.800 46.200 C 158.800 45.400,158.600 45.200,157.800 45.200 C 157.000 45.200,156.800 45.000,156.800 44.200 C 156.800 43.400,156.600 43.200,155.800 43.200 C 155.000 43.200,154.800 43.000,154.800 42.200 C 154.800 41.400,154.600 41.200,153.800 41.200 C 153.000 41.200,152.800 41.000,152.800 40.200 C 152.800 39.400,152.600 39.200,151.800 39.200 C 151.000 39.200,150.800 39.000,150.800 38.200 C 150.800 37.267,150.667 37.200,148.800 37.200 C 146.933 37.200,146.800 37.133,146.800 36.200 C 146.800 35.267,146.667 35.200,144.800 35.200 C 142.933 35.200,142.800 35.133,142.800 34.200 C 142.800 33.222,142.733 33.200,139.800 33.200 C 136.867 33.200,136.800 33.178,136.800 32.200 L 136.800 31.200 124.000 31.200 L 111.200 31.200 111.200 32.200 M111.200 136.200 C 111.200 137.178,111.133 137.200,108.200 137.200 C 105.267 137.200,105.200 137.222,105.200 138.200 C 105.200 139.133,105.067 139.200,103.200 139.200 C 101.333 139.200,101.200 139.267,101.200 140.200 C 101.200 141.133,101.067 141.200,99.200 141.200 C 97.333 141.200,97.200 141.267,97.200 142.200 C 97.200 143.000,97.000 143.200,96.200 143.200 C 95.400 143.200,95.200 143.400,95.200 144.200 C 95.200 145.000,95.000 145.200,94.200 145.200 C 93.400 145.200,93.200 145.400,93.200 146.200 C 93.200 147.000,93.000 147.200,92.200 147.200 C 91.400 147.200,91.200 147.400,91.200 148.200 C 91.200 149.000,91.000 149.200,90.200 149.200 C 89.400 149.200,89.200 149.400,89.200 150.200 C 89.200 151.000,89.000 151.200,88.200 151.200 C 87.400 151.200,87.200 151.400,87.200 152.200 C 87.200 153.000,87.000 153.200,86.200 153.200 C 85.267 153.200,85.200 153.333,85.200 155.200 C 85.200 157.067,85.133 157.200,84.200 157.200 C 83.267 157.200,83.200 157.333,83.200 159.200 C 83.200 161.067,83.133 161.200,82.200 161.200 C 81.222 161.200,81.200 161.267,81.200 164.200 C 81.200 167.133,81.178 167.200,80.200 167.200 L 79.200 167.200 79.200 180.000 L 79.200 192.800 80.200 192.800 C 81.178 192.800,81.200 192.867,81.200 195.800 C 81.200 198.733,81.222 198.800,82.200 198.800 C 83.133 198.800,83.200 198.933,83.200 200.800 C 83.200 202.667,83.267 202.800,84.200 202.800 C 85.133 202.800,85.200 202.933,85.200 204.800 C 85.200 206.667,85.267 206.800,86.200 206.800 C 87.000 206.800,87.200 207.000,87.200 207.800 C 87.200 208.600,87.400 208.800,88.200 208.800 C 89.000 208.800,89.200 209.000,89.200 209.800 C 89.200 210.600,89.400 210.800,90.200 210.800 C 91.000 210.800,91.200 211.000,91.200 211.800 C 91.200 212.600,91.400 212.800,92.200 212.800 C 93.000 212.800,93.200 213.000,93.200 213.800 C 93.200 214.600,93.400 214.800,94.200 214.800 C 95.000 214.800,95.200 215.000,95.200 215.800 C 95.200 216.600,95.400 216.800,96.200 216.800 C 97.000 216.800,97.200 217.000,97.200 217.800 C 97.200 218.733,97.333 218.800,99.200 218.800 C 101.067 218.800,101.200 218.867,101.200 219.800 C 101.200 220.733,101.333 220.800,103.200 220.800 C 105.067 220.800,105.200 220.867,105.200 221.800 C 105.200 222.778,105.267 222.800,108.200 222.800 C 111.133 222.800,111.200 222.822,111.200 223.800 L 111.200 224.800 124.000 224.800 L 136.800 224.800 136.800 223.800 C 136.800 222.822,136.867 222.800,139.800 222.800 C 142.733 222.800,142.800 222.778,142.800 221.800 C 142.800 220.867,142.933 220.800,144.800 220.800 C 146.667 220.800,146.800 220.733,146.800 219.800 C 146.800 218.867,146.933 218.800,148.800 218.800 C 150.667 218.800,150.800 218.733,150.800 217.800 C 150.800 217.000,151.000 216.800,151.800 216.800 C 152.600 216.800,152.800 216.600,152.800 215.800 C 152.800 215.000,153.000 214.800,153.800 214.800 C 154.600 214.800,154.800 214.600,154.800 213.800 C 154.800 213.000,155.000 212.800,155.800 212.800 C 156.600 212.800,156.800 212.600,156.800 211.800 C 156.800 211.000,157.000 210.800,157.800 210.800 C 158.600 210.800,158.800 210.600,158.800 209.800 C 158.800 209.000,159.000 208.800,159.800 208.800 C 160.600 208.800,160.800 208.600,160.800 207.800 C 160.800 207.000,161.000 206.800,161.800 206.800 C 162.733 206.800,162.800 206.667,162.800 204.800 C 162.800 202.933,162.867 202.800,163.800 202.800 C 164.733 202.800,164.800 202.667,164.800 200.800 C 164.800 198.933,164.867 198.800,165.800 198.800 C 166.778 198.800,166.800 198.733,166.800 195.800 C 166.800 192.867,166.822 192.800,167.800 192.800 L 168.800 192.800 168.800 180.000 L 168.800 167.200 167.800 167.200 C 166.822 167.200,166.800 167.133,166.800 164.200 C 166.800 161.267,166.778 161.200,165.800 161.200 C 164.867 161.200,164.800 161.067,164.800 159.200 C 164.800 157.333,164.733 157.200,163.800 157.200 C 162.867 157.200,162.800 157.067,162.800 155.200 C 162.800 153.333,162.733 153.200,161.800 153.200 C 161.000 153.200,160.800 153.000,160.800 152.200 C 160.800 151.400,160.600 151.200,159.800 151.200 C 159.000 151.200,158.800 151.000,158.800 150.200 C 158.800 149.400,158.600 149.200,157.800 149.200 C 157.000 149.200,156.800 149.000,156.800 148.200 C 156.800 147.400,156.600 147.200,155.800 147.200 C 155.000 147.200,154.800 147.000,154.800 146.200 C 154.800 145.400,154.600 145.200,153.800 145.200 C 153.000 145.200,152.800 145.000,152.800 144.200 C 152.800 143.400,152.600 143.200,151.800 143.200 C 151.000 143.200,150.800 143.000,150.800 142.200 C 150.800 141.267,150.667 141.200,148.800 141.200 C 146.933 141.200,146.800 141.133,146.800 140.200 C 146.800 139.267,146.667 139.200,144.800 139.200 C 142.933 139.200,142.800 139.133,142.800 138.200 C 142.800 137.222,142.733 137.200,139.800 137.200 C 136.867 137.200,136.800 137.178,136.800 136.200 L 136.800 135.200 124.000 135.200 L 111.200 135.200 111.200 136.200 M107.426 249.559 C 107.306 249.752,107.461 250.118,107.769 250.374 C 108.498 250.979,108.882 250.353,108.179 249.704 C 107.884 249.431,107.545 249.366,107.426 249.559 M109.087 251.468 C 108.925 251.725,109.077 252.220,109.425 252.568 C 110.141 253.284,110.800 253.395,110.800 252.800 C 110.800 252.580,110.543 252.400,110.229 252.400 C 109.915 252.400,109.596 252.085,109.520 251.700 C 109.426 251.225,109.287 251.150,109.087 251.468 M97.400 277.200 C 97.264 277.420,97.433 277.600,97.776 277.600 C 98.119 277.600,98.400 277.870,98.400 278.200 C 98.400 278.530,98.580 278.800,98.800 278.800 C 99.394 278.800,99.285 278.142,98.571 277.429 C 97.833 276.690,97.728 276.670,97.400 277.200 M99.400 279.200 C 99.264 279.420,99.433 279.600,99.776 279.600 C 100.119 279.600,100.400 279.870,100.400 280.200 C 100.400 280.530,100.580 280.800,100.800 280.800 C 101.394 280.800,101.285 280.142,100.571 279.429 C 99.833 278.690,99.728 278.670,99.400 279.200 M101.478 281.481 C 101.371 281.656,101.526 282.023,101.821 282.296 C 102.116 282.569,102.455 282.634,102.574 282.441 C 102.694 282.248,102.539 281.882,102.231 281.626 C 101.923 281.370,101.584 281.305,101.478 281.481 M263.200 284.194 C 263.200 285.179,263.138 285.200,260.200 285.200 C 257.267 285.200,257.200 285.222,257.200 286.200 C 257.200 287.133,257.067 287.200,255.200 287.200 C 253.333 287.200,253.200 287.267,253.200 288.200 C 253.200 289.133,253.067 289.200,251.200 289.200 C 249.333 289.200,249.200 289.267,249.200 290.200 C 249.200 291.000,249.000 291.200,248.200 291.200 C 247.400 291.200,247.200 291.400,247.200 292.200 C 247.200 293.000,247.000 293.200,246.200 293.200 C 245.400 293.200,245.200 293.400,245.200 294.200 C 245.200 295.000,245.000 295.200,244.200 295.200 C 243.400 295.200,243.200 295.400,243.200 296.200 C 243.200 297.000,243.000 297.200,242.200 297.200 C 241.400 297.200,241.200 297.400,241.200 298.200 C 241.200 299.000,241.000 299.200,240.200 299.200 C 239.400 299.200,239.200 299.400,239.200 300.200 C 239.200 301.000,239.000 301.200,238.200 301.200 C 237.267 301.200,237.200 301.333,237.200 303.200 C 237.200 305.067,237.133 305.200,236.200 305.200 C 235.267 305.200,235.200 305.333,235.200 307.200 C 235.200 309.067,235.133 309.200,234.200 309.200 C 233.222 309.200,233.200 309.267,233.200 312.200 C 233.200 315.133,233.178 315.200,232.200 315.200 L 231.200 315.200 231.200 328.000 L 231.200 340.800 232.200 340.800 C 233.178 340.800,233.200 340.867,233.200 343.800 C 233.200 346.733,233.222 346.800,234.200 346.800 C 235.133 346.800,235.200 346.933,235.200 348.800 C 235.200 350.667,235.267 350.800,236.200 350.800 C 237.133 350.800,237.200 350.933,237.200 352.800 C 237.200 354.667,237.267 354.800,238.200 354.800 C 239.000 354.800,239.200 355.000,239.200 355.800 C 239.200 356.600,239.400 356.800,240.200 356.800 C 241.000 356.800,241.200 357.000,241.200 357.800 C 241.200 358.600,241.400 358.800,242.200 358.800 C 243.000 358.800,243.200 359.000,243.200 359.800 C 243.200 360.600,243.400 360.800,244.200 360.800 C 245.000 360.800,245.200 361.000,245.200 361.800 C 245.200 362.600,245.400 362.800,246.200 362.800 C 247.000 362.800,247.200 363.000,247.200 363.800 C 247.200 364.600,247.400 364.800,248.200 364.800 C 249.000 364.800,249.200 365.000,249.200 365.800 C 249.200 366.733,249.333 366.800,251.200 366.800 C 253.067 366.800,253.200 366.867,253.200 367.800 C 253.200 368.733,253.333 368.800,255.200 368.800 C 257.067 368.800,257.200 368.867,257.200 369.800 C 257.200 370.778,257.267 370.800,260.200 370.800 C 263.133 370.800,263.200 370.822,263.200 371.800 L 263.200 372.800 276.224 372.800 C 283.943 372.800,289.156 372.652,289.022 372.436 C 288.323 371.305,289.317 370.800,292.243 370.800 C 295.113 370.800,295.200 370.771,295.200 369.822 C 295.200 368.939,295.385 368.832,297.100 368.722 C 298.721 368.618,299.019 368.468,299.128 367.700 C 299.238 366.922,299.495 366.800,301.020 366.800 C 302.569 366.800,302.815 366.678,303.036 365.800 C 303.193 365.174,303.570 364.800,304.043 364.800 C 304.584 364.800,304.800 364.514,304.800 363.800 C 304.800 363.000,305.000 362.800,305.800 362.800 C 306.600 362.800,306.800 362.600,306.800 361.800 C 306.800 361.000,307.000 360.800,307.800 360.800 C 308.600 360.800,308.800 360.600,308.800 359.800 C 308.800 359.000,309.000 358.800,309.800 358.800 C 310.600 358.800,310.800 358.600,310.800 357.800 C 310.800 357.000,311.000 356.800,311.800 356.800 C 312.600 356.800,312.800 356.600,312.800 355.800 C 312.800 355.000,313.000 354.800,313.800 354.800 C 314.733 354.800,314.800 354.667,314.800 352.800 C 314.800 350.933,314.867 350.800,315.800 350.800 C 316.733 350.800,316.800 350.667,316.800 348.800 C 316.800 346.933,316.867 346.800,317.800 346.800 C 318.778 346.800,318.800 346.733,318.800 343.800 C 318.800 340.867,318.822 340.800,319.800 340.800 L 320.800 340.800 320.800 328.000 L 320.800 315.200 319.800 315.200 C 318.822 315.200,318.800 315.133,318.800 312.200 C 318.800 309.267,318.778 309.200,317.800 309.200 C 316.867 309.200,316.800 309.067,316.800 307.200 C 316.800 305.333,316.733 305.200,315.800 305.200 C 314.867 305.200,314.800 305.067,314.800 303.200 C 314.800 301.333,314.733 301.200,313.800 301.200 C 313.000 301.200,312.800 301.000,312.800 300.200 C 312.800 299.400,312.600 299.200,311.800 299.200 C 311.000 299.200,310.800 299.000,310.800 298.200 C 310.800 297.400,310.600 297.200,309.800 297.200 C 309.000 297.200,308.800 297.000,308.800 296.200 C 308.800 295.400,308.600 295.200,307.800 295.200 C 307.000 295.200,306.800 295.000,306.800 294.200 C 306.800 293.400,306.600 293.200,305.800 293.200 C 305.000 293.200,304.800 293.000,304.800 292.200 C 304.800 291.486,304.584 291.200,304.043 291.200 C 303.570 291.200,303.193 290.826,303.036 290.200 C 302.815 289.322,302.569 289.200,301.020 289.200 C 299.495 289.200,299.238 289.078,299.128 288.300 C 299.019 287.534,298.721 287.382,297.125 287.279 C 295.544 287.177,295.231 287.021,295.125 286.279 C 295.011 285.479,294.740 285.389,292.100 285.282 C 290.505 285.218,289.178 284.993,289.150 284.782 C 289.123 284.572,289.078 284.175,289.050 283.900 C 289.011 283.514,286.064 283.376,276.100 283.294 L 263.200 283.189 263.200 284.194 M105.342 285.700 C 105.470 286.536,106.157 286.877,106.926 286.486 C 107.293 286.300,106.061 284.800,105.541 284.800 C 105.356 284.800,105.267 285.205,105.342 285.700 M107.338 287.663 C 107.417 288.178,107.733 288.605,108.041 288.611 C 108.348 288.616,108.825 288.762,109.100 288.934 C 109.924 289.450,109.672 289.065,108.397 287.862 C 107.212 286.743,107.196 286.740,107.338 287.663 M111.149 291.300 C 111.327 291.795,111.851 292.373,112.314 292.585 C 112.777 292.797,113.252 293.352,113.369 293.819 C 113.486 294.285,113.839 294.667,114.154 294.667 C 114.587 294.667,114.522 294.392,113.887 293.533 C 113.427 292.910,112.809 292.400,112.515 292.400 C 112.221 292.400,111.809 291.950,111.600 291.400 C 111.127 290.157,110.705 290.063,111.149 291.300 M117.200 298.167 C 117.200 298.515,117.485 298.800,117.833 298.800 C 118.182 298.800,118.395 298.586,118.308 298.325 C 118.061 297.583,117.200 297.460,117.200 298.167 M119.108 299.552 C 119.212 300.261,119.530 300.800,119.845 300.800 C 120.583 300.800,120.533 300.396,119.653 299.252 L 118.925 298.305 119.108 299.552 M123.200 303.919 C 123.200 304.343,123.523 304.654,124.000 304.690 C 124.440 304.723,124.800 304.711,124.800 304.664 C 124.800 304.617,124.440 304.270,124.000 303.893 C 123.243 303.245,123.200 303.246,123.200 303.919 M163.671 311.220 C 163.192 311.666,162.800 312.192,162.800 312.389 C 162.800 312.585,162.457 312.855,162.037 312.988 C 161.324 313.215,160.593 314.326,160.933 314.667 C 161.269 315.003,162.384 314.280,162.605 313.584 C 162.735 313.175,163.183 312.733,163.600 312.600 C 164.304 312.377,165.084 310.950,164.707 310.574 C 164.616 310.483,164.150 310.774,163.671 311.220 M159.680 315.680 C 159.096 316.264,159.058 316.800,159.600 316.800 C 159.820 316.800,159.955 316.575,159.900 316.300 C 159.845 316.025,160.025 315.845,160.300 315.900 C 160.575 315.955,160.800 315.820,160.800 315.600 C 160.800 315.058,160.264 315.096,159.680 315.680 M155.362 319.669 C 154.943 320.147,154.810 320.415,155.066 320.266 C 155.322 320.117,155.615 320.221,155.716 320.498 C 155.860 320.890,156.020 320.880,156.450 320.450 C 156.880 320.020,156.890 319.860,156.498 319.716 C 156.221 319.615,156.097 319.367,156.221 319.166 C 156.636 318.495,156.134 318.789,155.362 319.669 M153.600 322.233 C 153.600 322.582,153.814 322.795,154.075 322.708 C 154.817 322.461,154.940 321.600,154.233 321.600 C 153.885 321.600,153.600 321.885,153.600 322.233 M149.827 325.970 C 149.165 326.701,149.165 326.734,149.832 326.636 C 150.218 326.580,150.578 326.233,150.631 325.867 C 150.756 325.020,150.678 325.030,149.827 325.970 M147.827 327.970 C 147.165 328.701,147.165 328.734,147.832 328.636 C 148.218 328.580,148.578 328.233,148.631 327.867 C 148.756 327.020,148.678 327.030,147.827 327.970 M141.927 334.006 C 141.295 334.701,141.297 334.743,141.948 334.694 C 142.329 334.665,142.666 334.317,142.696 333.921 C 142.760 333.069,142.782 333.066,141.927 334.006 M137.876 337.916 C 137.275 338.580,137.283 338.636,137.987 338.689 C 138.508 338.728,138.728 338.503,138.688 337.973 C 138.617 337.037,138.668 337.041,137.876 337.916 M136.000 340.200 C 135.592 340.691,135.620 340.800,136.151 340.800 C 136.508 340.800,136.800 340.530,136.800 340.200 C 136.800 339.870,136.732 339.600,136.649 339.600 C 136.566 339.600,136.274 339.870,136.000 340.200 M87.338 351.817 C 86.966 352.788,87.402 352.882,88.198 352.002 C 88.860 351.270,88.865 351.200,88.249 351.200 C 87.878 351.200,87.468 351.478,87.338 351.817 " stroke="none" fill="#5c3c6c" fill-rule="evenodd"></path></g></svg> \ No newline at end of file diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/database.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/database.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/database.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/database.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/default.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/default.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/documents.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/documents.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/elasticsearch.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/globe.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/globe.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/globe.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/globe.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg new file mode 100644 index 0000000000000..16dd58cd53184 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/graphql.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M7.61104 2.90528C7.73443 2.94089 7.8649 2.95997 7.99988 2.95997C8.13532 2.95997 8.26621 2.94076 8.38997 2.90492L12.6057 10.2065C12.5113 10.2965 12.4282 10.4013 12.36 10.5199C12.2921 10.6379 12.2434 10.7618 12.2128 10.8879H3.78721C3.75663 10.7619 3.70787 10.638 3.63997 10.5199C3.57146 10.402 3.48848 10.2977 3.3946 10.2082L7.61104 2.90528ZM6.99579 2.53459C7.00877 2.54802 7.02203 2.5612 7.03554 2.5741L2.81896 9.87725C2.80073 9.87196 2.7824 9.86703 2.76397 9.86248V6.13697C3.11928 6.04864 3.43942 5.82202 3.63595 5.48015C3.83222 5.13872 3.86861 4.74921 3.76807 4.39817L6.99579 2.53459ZM9.33817 1.96312C9.37572 1.83669 9.39588 1.70272 9.39588 1.56397C9.39588 0.791969 8.77188 0.167969 7.99988 0.167969C7.22788 0.167969 6.60388 0.791969 6.60388 1.56397C6.60388 1.70201 6.62383 1.83531 6.66101 1.96117L3.43929 3.82129C3.34892 3.72586 3.24348 3.64157 3.12395 3.57215C2.45594 3.18814 1.59994 3.41614 1.21594 4.08415C0.831944 4.75215 1.05994 5.60815 1.72794 5.99215C1.84721 6.06071 1.97248 6.10977 2.09998 6.14029V9.8607C1.97394 9.89128 1.85008 9.94004 1.73197 10.0079C1.06397 10.3959 0.835972 11.2479 1.21997 11.9159C1.60397 12.5839 2.45597 12.8119 3.12797 12.4279C3.24622 12.3593 3.35068 12.276 3.44039 12.1819L6.66038 14.041C6.62361 14.1662 6.60388 14.2988 6.60388 14.436C6.60388 15.208 7.22788 15.832 7.99988 15.832C8.77188 15.832 9.39588 15.204 9.39588 14.436C9.39588 14.2823 9.37113 14.1344 9.32538 13.9961L12.5268 12.1477C12.6235 12.2564 12.739 12.3515 12.872 12.4279C13.54 12.8119 14.396 12.5839 14.78 11.9159C15.168 11.2479 14.94 10.3959 14.268 10.0079C14.15 9.94009 14.0261 9.89135 13.9 9.86076V6.14029C14.0275 6.10977 14.1527 6.06072 14.272 5.99215C14.94 5.60415 15.168 4.75215 14.784 4.08415C14.396 3.41614 13.544 3.18814 12.876 3.57215C12.7558 3.64195 12.6499 3.72677 12.5592 3.82283L9.33817 1.96312ZM8.96516 2.57321C8.97786 2.56106 8.99033 2.54867 9.00256 2.53605L12.2313 4.40022C12.1315 4.75073 12.1681 5.13938 12.364 5.48015C12.5605 5.82201 12.8807 6.04863 13.236 6.13696V9.86213C13.2178 9.86658 13.1998 9.87139 13.1819 9.87656L8.96516 2.57321ZM12.24 11.6436L9.03231 13.4956C8.77714 13.2156 8.40933 13.04 7.99988 13.04C7.60455 13.04 7.24802 13.2037 6.99423 13.467L3.77093 11.606C3.77612 11.5881 3.78095 11.5701 3.78542 11.5519H12.2142C12.2217 11.5828 12.2304 11.6133 12.24 11.6436Z" fill="#E535AB"/> +</svg> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg new file mode 100644 index 0000000000000..768294776f382 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/grpc.svg @@ -0,0 +1,150 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path d="M2.01301 10.7211C2.12728 10.8731 2.27577 10.9961 2.44645 11.08C2.63104 11.1714 2.83473 11.2176 3.0407 11.2149C3.20724 11.2192 3.37302 11.1909 3.5287 11.1316C3.65321 11.0827 3.76437 11.005 3.85306 10.9048C3.9363 10.8073 3.99707 10.6926 4.03106 10.569C4.06812 10.4353 4.08647 10.2972 4.08558 10.1585V9.67623H4.07413C3.96618 9.84733 3.81048 9.98305 3.62625 10.0666C3.4506 10.1431 3.26096 10.1822 3.06938 10.1815C2.88165 10.1829 2.69544 10.1478 2.52111 10.0781C2.35605 10.0126 2.20502 9.91613 2.07615 9.79393C1.94827 9.67115 1.84669 9.52366 1.77759 9.3604C1.70405 9.18806 1.66692 9.00238 1.66851 8.81501C1.66692 8.62819 1.70205 8.44287 1.77188 8.26958C1.8379 8.10564 1.93431 7.95565 2.05605 7.82753C2.17776 7.70045 2.32434 7.5998 2.48665 7.53184C2.66033 7.45963 2.84688 7.4235 3.03496 7.42563C3.1206 7.42613 3.20604 7.4338 3.2904 7.44856C3.38317 7.46477 3.47374 7.49176 3.56025 7.52897C3.76564 7.61799 3.94319 7.76082 4.07413 7.94239H4.08558V7.49451H4.46454V10.1585C4.46325 10.315 4.44495 10.4709 4.40998 10.6235C4.3734 10.7885 4.30087 10.9433 4.19757 11.0771C4.08003 11.2246 3.92961 11.3426 3.75834 11.4216C3.57076 11.5134 3.32579 11.5593 3.02343 11.5593C2.77782 11.5635 2.53384 11.5186 2.30579 11.4273C2.08761 11.3338 1.89189 11.1949 1.73172 11.0197L2.01301 10.7211ZM2.08189 8.80357C2.08094 8.94039 2.1073 9.07604 2.15942 9.20255C2.20911 9.32396 2.28115 9.43496 2.37179 9.52979C2.46191 9.62377 2.56908 9.69977 2.68758 9.75372C2.81103 9.80983 2.94527 9.83824 3.08086 9.83696C3.3494 9.83793 3.60817 9.73628 3.80425 9.5528C3.90055 9.46193 3.97598 9.35123 4.02532 9.22835C4.07849 9.09328 4.10294 8.94861 4.0971 8.80357C4.09807 8.66751 4.07473 8.53236 4.02819 8.40451C3.98396 8.28299 3.9156 8.17167 3.82722 8.07727C3.73638 7.98157 3.62699 7.90539 3.50573 7.85337C3.37146 7.7963 3.22674 7.76794 3.08086 7.7701C2.94527 7.76885 2.81104 7.79727 2.68758 7.85337C2.56912 7.90738 2.46196 7.98335 2.37179 8.07727C2.28118 8.17213 2.20913 8.28312 2.15938 8.40451C2.10728 8.53105 2.08094 8.66672 2.08189 8.80357V8.80357ZM6.00886 10.1815H5.59552V6.11657H6.84711C7.26048 6.11657 7.58677 6.20652 7.82598 6.38642C8.06517 6.56637 8.18479 6.83813 8.18485 7.20169C8.19212 7.46559 8.09637 7.72195 7.91787 7.91646C7.73987 8.10979 7.48246 8.22174 7.14567 8.25231L8.3169 10.1814H7.81163L6.69787 8.29256H6.0089L6.00886 10.1815ZM6.00886 7.91363H6.74378C6.90169 7.91696 7.05926 7.89762 7.21168 7.8562C7.32315 7.8264 7.42757 7.77469 7.51885 7.70409C7.59248 7.64483 7.64978 7.56777 7.68532 7.48019C7.72025 7.39153 7.7378 7.29698 7.73701 7.20169C7.73743 7.10833 7.71988 7.01575 7.68532 6.92902C7.64937 6.84085 7.59218 6.76294 7.51885 6.70221C7.42845 6.63021 7.32373 6.57834 7.21168 6.55006C7.05891 6.51072 6.9015 6.49238 6.74378 6.49554H6.00886V7.91363ZM9.02872 6.11657H10.2115C10.6248 6.11657 10.9511 6.20654 11.1903 6.38646C11.4295 6.56641 11.5491 6.83817 11.5492 7.20173C11.5492 7.56541 11.4295 7.83811 11.1903 8.01984C10.9511 8.20168 10.6248 8.29258 10.2115 8.29256H9.44214V10.1815H9.02872V6.11657ZM9.44218 7.91367H10.1082C10.2661 7.91699 10.4237 7.89765 10.5761 7.85624C10.6875 7.82642 10.792 7.77472 10.8832 7.70413C10.9569 7.64487 11.0142 7.5678 11.0497 7.48023C11.0847 7.39158 11.1022 7.29702 11.1014 7.20173C11.1018 7.10837 11.0843 7.01579 11.0497 6.92906C11.0138 6.8409 10.9566 6.76298 10.8832 6.70225C10.7928 6.63026 10.6881 6.57839 10.5761 6.5501C10.4233 6.51077 10.2659 6.49243 10.1082 6.49558H9.44214L9.44218 7.91367ZM15.7 9.53844C15.6273 9.6462 15.5415 9.74456 15.4446 9.83126C15.3397 9.92549 15.2229 10.0056 15.0972 10.0695C14.963 10.138 14.8215 10.191 14.6753 10.2274C14.5184 10.2663 14.3574 10.2856 14.1958 10.2848C13.9076 10.2875 13.6217 10.2329 13.3548 10.1241C13.1028 10.0223 12.874 9.87062 12.6822 9.67824C12.4903 9.48585 12.3392 9.25669 12.238 9.00449C12.1287 8.7327 12.074 8.44198 12.0773 8.14902C12.074 7.85606 12.1287 7.56534 12.238 7.29354C12.3392 7.04136 12.4903 6.8122 12.6822 6.61983C12.874 6.42746 13.1028 6.27583 13.3548 6.17404C13.6217 6.06523 13.9076 6.01058 14.1958 6.01328C14.4556 6.01467 14.7128 6.06531 14.9537 6.16252C15.2106 6.26535 15.4348 6.43602 15.6024 6.6563L15.2408 6.92615C15.1922 6.85377 15.1342 6.78813 15.0685 6.73092C14.9925 6.66366 14.9087 6.60583 14.8188 6.55871C14.7228 6.50814 14.6217 6.46773 14.5173 6.43815C14.4128 6.40791 14.3046 6.39244 14.1958 6.3922C13.9546 6.38825 13.7155 6.43724 13.4954 6.53574C13.2956 6.62636 13.1168 6.75731 12.9701 6.92036C12.8249 7.08388 12.7128 7.274 12.6399 7.48019C12.4868 7.91294 12.4868 8.3851 12.6399 8.81785C12.7127 9.02403 12.8249 9.21414 12.9701 9.37763C13.1168 9.54074 13.2956 9.67171 13.4954 9.7623C13.7155 9.86086 13.9546 9.90988 14.1958 9.90588C14.3027 9.90592 14.4093 9.89631 14.5145 9.87716C14.6214 9.85749 14.7256 9.82472 14.8245 9.77957C14.9288 9.73173 15.0255 9.66884 15.1116 9.59292C15.2082 9.50657 15.2923 9.40706 15.3613 9.29731L15.7 9.53844Z" fill="#244B5A"/> +<path d="M0.328069 5.31445L0.309204 5.48148V5.31445H0.328069Z" fill="#75CACA"/> +<path d="M0.353035 5.31445L0.309204 5.70219V5.48148L0.328068 5.31445H0.353035V5.31445Z" fill="#74CBCA"/> +<path d="M0.377961 5.31445L0.309204 5.92286V5.70219L0.353035 5.31445H0.377961V5.31445Z" fill="#74CBCA"/> +<path d="M0.402888 5.31445L0.309204 6.14357V5.92286L0.377961 5.31445H0.402888Z" fill="#73CBCA"/> +<path d="M0.427814 5.31445L0.309204 6.36427V6.14357L0.402888 5.31445H0.427814Z" fill="#73CBCA"/> +<path d="M0.452781 5.31445L0.309204 6.58498V6.36428L0.427815 5.31445H0.452781ZM0.477707 5.31445L0.309204 6.80565V6.58498L0.452781 5.31445H0.477707V5.31445Z" fill="#72C9C9"/> +<path d="M0.502633 5.31445L0.309204 7.02636V6.80565L0.477707 5.31445H0.502633V5.31445Z" fill="#70CACA"/> +<path d="M0.52756 5.31445L0.309204 7.24707V7.02636L0.502633 5.31445H0.52756V5.31445Z" fill="#70CACA"/> +<path d="M0.552527 5.31445L0.311637 7.44633L0.309204 7.44605V7.24708L0.527561 5.31445H0.552527V5.31445Z" fill="#6FCAC8"/> +<path d="M0.577461 5.31445L0.336412 7.44764H0.323211L0.311646 7.44632L0.552535 5.31445H0.577461V5.31445Z" fill="#6EC8C8"/> +<path d="M0.602402 5.31445L0.361392 7.44764H0.336426L0.577476 5.31445H0.602402Z" fill="#6EC8C8"/> +<path d="M0.627303 5.31445L0.386255 7.44764H0.361328L0.602337 5.31445H0.627303Z" fill="#6CC9C9"/> +<path d="M0.652327 5.31445L0.411279 7.44764H0.386353L0.627401 5.31445H0.652327Z" fill="#6DC9C8"/> +<path d="M0.67723 5.31445L0.436221 7.44764H0.411255L0.652303 5.31445H0.67723V5.31445Z" fill="#69C7C8"/> +<path d="M0.702216 5.31445L0.461206 7.44764H0.436279L0.677289 5.31445H0.702216Z" fill="#69C7C8"/> +<path d="M0.727156 5.31445L0.486108 7.44764H0.461182L0.70219 5.31445H0.727156V5.31445Z" fill="#69C8C7"/> +<path d="M0.752058 5.31445L0.51101 7.44764H0.486084L0.727132 5.31445H0.752058V5.31445Z" fill="#68C8C7"/> +<path d="M0.776963 5.31445L0.535953 7.44764H0.510986L0.752036 5.31445H0.776963V5.31445Z" fill="#67C7C7"/> +<path d="M0.801824 5.31445L0.560815 7.44764H0.535889L0.776898 5.31445H0.801824V5.31445Z" fill="#65C7C7"/> +<path d="M0.826765 5.31445L0.585717 7.44764H0.560791L0.801799 5.31445H0.826765V5.31445Z" fill="#64C7C7"/> +<path d="M0.851668 5.31445L0.61062 7.44764H0.585693L0.826741 5.31445H0.851668V5.31445Z" fill="#63C6C6"/> +<path d="M0.876693 5.31445L0.635684 7.44764H0.610718L0.851767 5.31445H0.876693V5.31445Z" fill="#60C6C6"/> +<path d="M0.901555 5.31445L0.660546 7.44764H0.63562L0.876628 5.31445H0.901555V5.31445Z" fill="#60C6C5"/> +<path d="M0.926498 5.31445L0.685449 7.44764H0.660522L0.901531 5.31445H0.926498V5.31445Z" fill="#5FC6C5"/> +<path d="M0.951522 5.31445L0.710473 7.44764H0.685547L0.926596 5.31445H0.951522V5.31445Z" fill="#5CC5C4"/> +<path d="M0.976425 5.31445L0.735416 7.44764H0.710449L0.951498 5.31445H0.976425V5.31445Z" fill="#5AC5C4"/> +<path d="M1.00141 5.31445L0.7604 7.44764H0.735474L0.976484 5.31445H1.00141V5.31445Z" fill="#5AC5C4"/> +<path d="M1.02635 5.31445L0.785302 7.44764H0.760376L1.00138 5.31445H1.02635V5.31445Z" fill="#57C4C4"/> +<path d="M1.05125 5.31445L0.810205 7.44764H0.785278L1.02633 5.31445H1.05125V5.31445Z" fill="#58C4C3"/> +<path d="M1.07616 5.31445L0.835147 7.44764H0.810181L1.05123 5.31445H1.07616V5.31445Z" fill="#55C4C3"/> +<path d="M1.10114 5.31445L0.860132 7.44764H0.835205L1.07621 5.31445H1.10114V5.31445Z" fill="#53C3C2"/> +<path d="M1.12608 5.31445L0.885034 7.44764H0.860107L1.10111 5.31445H1.12608V5.31445Z" fill="#52C3C2"/> +<path d="M1.15099 5.31445L0.909936 7.44764H0.88501L1.12606 5.31445H1.15099V5.31445Z" fill="#50C3C2"/> +<path d="M1.17589 5.31445L0.934879 7.44764H0.909912L1.15096 5.31445H1.17589Z" fill="#50C1C0"/> +<path d="M1.20091 5.31445L0.959863 7.44764H0.934937L1.17595 5.31445H1.20091V5.31445Z" fill="#4BC2C0"/> +<path d="M1.22582 5.31445L0.984766 7.44764H0.959839L1.20089 5.31445H1.22582V5.31445Z" fill="#4CC1C0"/> +<path d="M1.25084 5.31445L1.00979 7.44764H0.984863L1.22591 5.31445H1.25084V5.31445Z" fill="#47C1BE"/> +<path d="M1.27574 5.31445L1.03473 7.44764H1.00977L1.25081 5.31445H1.27574V5.31445Z" fill="#46C0BE"/> +<path d="M1.30065 5.31445L1.05959 7.44764H1.03467L1.27568 5.31445H1.30065V5.31445Z" fill="#43C0BE"/> +<path d="M1.32567 5.31445L1.08462 7.44764H1.05969L1.30074 5.31445H1.32567V5.31445Z" fill="#40BFBD"/> +<path d="M1.35057 5.31445L1.10956 7.44764H1.08459L1.32564 5.31445H1.35057V5.31445Z" fill="#3BBFBD"/> +<path d="M1.37543 5.31445L1.13442 7.44764H1.1095L1.35051 5.31445H1.37543V5.31445Z" fill="#39BFBC"/> +<path d="M1.40037 5.31445L1.15933 7.44764H1.1344L1.37541 5.31445H1.40037V5.31445Z" fill="#34BEBD"/> +<path d="M1.42528 5.31445L1.18423 7.44764H1.1593L1.40035 5.31445H1.42528V5.31445Z" fill="#32BEBB"/> +<path d="M1.45018 5.31445L1.20917 7.44764H1.1842L1.42525 5.31445H1.45018V5.31445Z" fill="#2DBDBB"/> +<path d="M1.47516 5.31445L1.23415 7.44764H1.20923L1.45024 5.31445H1.47516V5.31445Z" fill="#24BCBB"/> +<path d="M1.50011 5.31445L1.25906 7.44764H1.23413L1.47514 5.31445H1.50011V5.31445Z" fill="#21BCB9"/> +<path d="M1.525 5.31445L1.28396 7.44764H1.25903L1.50008 5.31445H1.525V5.31445Z" fill="#18BBB9"/> +<path d="M1.54991 5.31445L1.3089 7.44764H1.28394L1.52499 5.31445H1.54991V5.31445Z" fill="#0FBAB9"/> +<path d="M1.57489 5.31445L1.33389 7.44764H1.30896L1.54997 5.31445H1.57489V5.31445Z" fill="#03BBB8"/> +<path d="M1.59984 5.31445L1.35879 7.44764H1.33386L1.57487 5.31445H1.59984V5.31445Z" fill="#06B8B6"/> +<path d="M1.62486 5.31445L1.38381 7.44764H1.35889L1.59993 5.31445H1.62486V5.31445Z" fill="#06B8B6"/> +<path d="M1.64977 5.31445L1.40876 7.44764H1.38379L1.62484 5.31445H1.64977Z" fill="#03B7B5"/> +<path d="M1.67463 5.31445L1.43362 7.44764H1.40869L1.6497 5.31445H1.67463V5.31445Z" fill="#04B7B4"/> +<path d="M1.69969 5.31445L1.45864 7.44764H1.43372L1.67472 5.31445H1.69969Z" fill="#00B6B5"/> +<path d="M1.7246 5.31445L1.48354 7.44764H1.45862L1.69967 5.31445H1.7246V5.31445Z" fill="#03B6B4"/> +<path d="M1.74949 5.31445L1.50849 7.44764H1.48352L1.72457 5.31445H1.74949V5.31445Z" fill="#05B6B2"/> +<path d="M1.77448 5.31445L1.53347 7.44764H1.50854L1.74955 5.31445H1.77448V5.31445Z" fill="#01B5B1"/> +<path d="M1.79942 5.31445L1.55837 7.44764H1.53345L1.77446 5.31445H1.79942V5.31445Z" fill="#01B5B1"/> +<path d="M1.82432 5.31445L1.58328 7.44764H1.55835L1.7994 5.31445H1.82432V5.31445Z" fill="#00B4B1"/> +<path d="M1.84923 5.31445L1.60822 7.44764H1.58325L1.8243 5.31445H1.84923V5.31445Z" fill="#00B3B0"/> +<path d="M1.87413 5.31445L1.63308 7.44764H1.60815L1.84916 5.31445H1.87413V5.31445Z" fill="#00B3B0"/> +<path d="M1.89903 5.31445L1.65798 7.44764H1.63306L1.87411 5.31445H1.89903V5.31445Z" fill="#00B2AF"/> +<path d="M1.92406 5.31445L1.68301 7.44764H1.65808L1.89913 5.31445H1.92406V5.31445Z" fill="#00B2AE"/> +<path d="M1.94896 5.31445L1.70795 7.44764H1.68298L1.92403 5.31445H1.94896V5.31445Z" fill="#00B1AE"/> +<path d="M1.97386 5.31445L1.73281 7.44764H1.70789L1.94889 5.31445H1.97386V5.31445Z" fill="#00B1AD"/> +<path d="M1.99889 5.31445L1.75784 7.44764H1.73291L1.97396 5.31445H1.99889V5.31445Z" fill="#00B0AC"/> +<path d="M2.02379 5.31445L1.78278 7.44764H1.75781L1.99886 5.31445H2.02379V5.31445Z" fill="#00AFAC"/> +<path d="M2.42849 5.31445L2.44245 5.31601V7.44764H1.78284L2.02384 5.31445H2.42849V5.31445Z" fill="#00AFAC"/> +<path d="M4.91608 6.10816L4.89905 5.74268H4.91608V6.10816Z" fill="#75CACA"/> +<path d="M4.91612 6.49218L4.88123 5.74268H4.89909L4.91612 6.10816V6.49218Z" fill="#74CBCA"/> +<path d="M4.9162 6.87621L4.8634 5.74268H4.88131L4.9162 6.49218V6.87621V6.87621Z" fill="#74CBCA"/> +<path d="M4.90384 6.99394L4.84558 5.74268H4.86345L4.91624 6.87621V6.99394H4.90384V6.99394Z" fill="#73CBCA"/> +<path d="M4.88583 6.99394L4.82756 5.74268H4.84547L4.90374 6.99394H4.88583ZM4.86796 6.99394L4.80969 5.74268H4.82756L4.88583 6.99394H4.86796Z" fill="#72C9C9"/> +<path d="M4.85001 6.99394L4.79175 5.74268H4.80965L4.86792 6.99394H4.85001Z" fill="#70CACA"/> +<path d="M4.8322 6.99394L4.77393 5.74268H4.79179L4.85007 6.99394H4.8322Z" fill="#6FCAC8"/> +<path d="M4.8143 6.99394L4.75598 5.74268H4.77389L4.83216 6.99394H4.8143Z" fill="#6EC8C8"/> +<path d="M4.79642 6.99394L4.73816 5.74268H4.75602L4.81433 6.99394H4.79642Z" fill="#6EC8C8"/> +<path d="M4.77852 6.99394L4.72021 5.74268H4.73812L4.79638 6.99394H4.77852Z" fill="#6CC9C9"/> +<path d="M4.76053 6.99394L4.70227 5.74268H4.72014L4.77844 6.99394H4.76053Z" fill="#6DC9C8"/> +<path d="M4.74276 6.99394L4.68445 5.74268H4.70236L4.76063 6.99394H4.74276Z" fill="#69C7C8"/> +<path d="M4.72489 6.99394L4.66663 5.74268H4.68449L4.74279 6.99394H4.72489Z" fill="#69C7C8"/> +<path d="M4.70699 6.99394L4.64868 5.74268H4.66659L4.72486 6.99394H4.70699Z" fill="#69C8C7"/> +<path d="M4.68913 6.99394L4.63086 5.74268H4.64873L4.70704 6.99394H4.68913Z" fill="#68C8C7"/> +<path d="M4.67123 6.99394L4.61292 5.74268H4.63083L4.68911 6.99394H4.67123Z" fill="#67C7C7"/> +<path d="M4.65336 6.99394L4.59509 5.74268H4.61296L4.67126 6.99394H4.65336Z" fill="#65C7C7"/> +<path d="M4.63542 6.99394L4.57715 5.74268H4.59502L4.65328 6.99394H4.63542Z" fill="#64C7C7"/> +<path d="M4.61747 6.99394L4.5592 5.74268H4.57711L4.63537 6.99394H4.61747Z" fill="#63C6C6"/> +<path d="M4.59965 6.99394L4.54138 5.74268H4.55925L4.61752 6.99394H4.59965Z" fill="#60C6C6"/> +<path d="M4.58183 6.99394L4.52356 5.74268H4.54147L4.59973 6.99394H4.58183Z" fill="#60C6C5"/> +<path d="M4.564 6.99394L4.50574 5.74268H4.5236L4.58187 6.99394H4.564Z" fill="#5FC6C5"/> +<path d="M4.54607 6.99394L4.48779 5.74268H4.5057L4.56398 6.99394H4.54607Z" fill="#5FC6C5"/> +<path d="M4.52812 6.99394L4.46985 5.74268H4.48772L4.54599 6.99394H4.52812Z" fill="#5CC5C4"/> +<path d="M4.51017 6.99394L4.4519 5.74268H4.46981L4.52808 6.99394H4.51017Z" fill="#5AC5C4"/> +<path d="M4.49235 6.99394L4.43408 5.74268H4.45195L4.51022 6.99394H4.49235Z" fill="#5AC5C4"/> +<path d="M4.47445 6.99394L4.41614 5.74268H4.43404L4.49231 6.99394H4.47445Z" fill="#57C4C4"/> +<path d="M4.45658 6.99394L4.39832 5.74268H4.41618L4.47449 6.99394H4.45658Z" fill="#58C4C3"/> +<path d="M4.43868 6.99394L4.38037 5.74268H4.39828L4.45655 6.99394H4.43868Z" fill="#55C4C3"/> +<path d="M4.4209 6.99394L4.36263 5.74268H4.3805L4.4388 6.99394H4.4209ZM4.40303 6.99394L4.34473 5.74268H4.36263L4.4209 6.99394H4.40303Z" fill="#53C3C2"/> +<path d="M4.38504 6.99394L4.32678 5.74268H4.34465L4.40295 6.99394H4.38504Z" fill="#52C3C2"/> +<path d="M4.36715 6.99394L4.30884 5.74268H4.32674L4.38501 6.99394H4.36715Z" fill="#50C3C2"/> +<path d="M4.34928 6.99394L4.29102 5.74268H4.30888L4.36719 6.99394H4.34928Z" fill="#50C1C0"/> +<path d="M4.33137 6.99394L4.27307 5.74268H4.29098L4.34924 6.99394H4.33137Z" fill="#4DC2C1"/> +<path d="M4.31352 6.99394L4.25525 5.74268H4.27312L4.33142 6.99394H4.31352Z" fill="#4CC2C0"/> +<path d="M4.29558 6.99394L4.2373 5.74268H4.25517L4.31345 6.99394H4.29558Z" fill="#4BC0BF"/> +<path d="M4.27764 6.99394L4.21936 5.74268H4.23727L4.29555 6.99394H4.27764Z" fill="#47C1BE"/> +<path d="M4.2598 6.99394L4.20154 5.74268H4.2194L4.27767 6.99394H4.2598Z" fill="#46C0BE"/> +<path d="M4.24198 6.99394L4.18372 5.74268H4.20162L4.25989 6.99394H4.24198Z" fill="#43C0BE"/> +<path d="M4.22416 6.99394L4.16589 5.74268H4.18376L4.24203 6.99394H4.22416Z" fill="#41C0BE"/> +<path d="M4.20622 6.99394L4.14795 5.74268H4.16586L4.22413 6.99394H4.20622Z" fill="#40BFBD"/> +<path d="M4.18827 6.99394L4.13 5.74268H4.14787L4.20613 6.99394H4.18827Z" fill="#3BBFBD"/> +<path d="M4.17032 6.99394L4.11206 5.74268H4.12997L4.18823 6.99394H4.17032Z" fill="#3CBFBC"/> +<path d="M4.1525 6.99394L4.09424 5.74268H4.1121L4.17037 6.99394H4.1525Z" fill="#39BFBC"/> +<path d="M4.13457 6.99394L4.07629 5.74268H4.0942L4.15248 6.99394H4.13457Z" fill="#34BEBD"/> +<path d="M4.11673 6.99394L4.05847 5.74268H4.07634L4.1346 6.99394H4.11673Z" fill="#32BEBB"/> +<path d="M4.09883 6.99394L4.04053 5.74268H4.05843L4.11669 6.99394H4.09883Z" fill="#2FBEBC"/> +<path d="M4.08098 6.99394L4.02271 5.74268H4.04057L4.09889 6.99394H4.08098Z" fill="#2DBDBB"/> +<path d="M4.0632 6.99394L4.00488 5.74268H4.02279L4.08107 6.99394H4.0632Z" fill="#2BBDBA"/> +<path d="M4.04521 6.99394L3.98694 5.74268H4.00481L4.06312 6.99394H4.04521Z" fill="#24BCBB"/> +<path d="M4.0273 6.99394L3.96899 5.74268H3.9869L4.04517 6.99394H4.0273Z" fill="#21BCB9"/> +<path d="M4.00944 6.99394L3.95117 5.74268H3.96904L4.02735 6.99394H4.00944V6.99394Z" fill="#18BBB9"/> +<path d="M3.99154 6.99394L3.93323 5.74268H3.95114L4.00941 6.99394H3.99154Z" fill="#16BAB9"/> +<path d="M3.97367 6.99394L3.91541 5.74268H3.93327L3.99158 6.99394H3.97367Z" fill="#11BAB7"/> +<path d="M3.95577 6.99394L3.89746 5.74268H3.91537L3.97364 6.99394H3.95577Z" fill="#03BBB8"/> +<path d="M3.93778 6.99394L3.87952 5.74268H3.89738L3.95569 6.99394H3.93778Z" fill="#06B8B6"/> +<path d="M3.91996 6.99394L3.86169 5.74268H3.87956L3.93783 6.99394H3.91996Z" fill="#06B8B6"/> +<path d="M3.90214 6.99394L3.84387 5.74268H3.86178L3.92005 6.99394H3.90214Z" fill="#06B8B6"/> +<path d="M3.88432 6.99394L3.82605 5.74268H3.84392L3.90219 6.99394H3.88432Z" fill="#00B7B6"/> +<path d="M3.86637 6.99394L3.80811 5.74268H3.82601L3.88428 6.99394H3.86637Z" fill="#03B7B5"/> +<path d="M3.84843 6.99394L3.79016 5.74268H3.80803L3.8663 6.99394H3.84843Z" fill="#04B7B4"/> +<path d="M3.83048 6.99394L3.77222 5.74268H3.79012L3.84839 6.99394H3.83048Z" fill="#00B6B5"/> +<path d="M3.81266 6.99394L3.75439 5.74268H3.77226L3.83053 6.99394H3.81266Z" fill="#00B6B5"/> +<path d="M3.79472 6.99394L3.73645 5.74268H3.75436L3.81263 6.99394H3.79472Z" fill="#00B6B5"/> +<path d="M3.77698 6.99394L3.71871 5.74268H3.73658L3.79485 6.99394H3.77698ZM3.75911 6.99394L3.70081 5.74268H3.71871L3.77698 6.99394H3.75911Z" fill="#03B6B4"/> +<path d="M3.74113 6.99394L3.68286 5.74268H3.70073L3.75904 6.99394H3.74113Z" fill="#05B6B2"/> +<path d="M3.7242 6.99394L3.66589 5.74268H3.6838L3.74207 6.99394H3.7242Z" fill="#01B5B1"/> +<path d="M3.70536 6.99394L3.66492 6.12519V5.74268H3.66496L3.72327 6.99394H3.70536V6.99394ZM3.68749 6.99394L3.66492 6.50921V6.12519L3.70536 6.99394H3.68749V6.99394Z" fill="#01B5B1"/> +<path d="M3.66958 6.99401L3.66492 6.8933V6.50928L3.68749 6.99401H3.66958Z" fill="#00B4B1"/> +<path d="M3.66492 6.89307L3.66958 6.99377H3.66492V6.89307Z" fill="#00B4B1"/> +<path d="M1.41877 5.73592L0.775269 6.38425L1.42356 7.02775L1.97202 7.02572L1.39687 6.4552L4.23686 6.44467L3.98795 6.69525L4.26218 6.69425L4.58395 6.37009L4.25979 6.04836L3.98556 6.04936L4.23634 6.29814L1.39636 6.30867L1.96719 5.73389L1.41877 5.73592V5.73592Z" fill="#244B5A"/> +</g> +<defs> +<clipPath id="clip0"> +<rect y="5" width="16" height="6.85697" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg new file mode 100644 index 0000000000000..b8e808baa1ac1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/handlebars.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M5.8885 6.00003C6.44472 5.9976 7.01513 6.15717 7.44744 6.51679C7.66896 6.69609 7.84791 6.92107 7.99986 7.16059C8.2733 6.71858 8.67273 6.34304 9.16526 6.16201C9.76544 5.93669 10.4438 5.95226 11.043 6.17309C11.5663 6.36415 12.0239 6.69712 12.4389 7.06332C12.7636 7.34992 13.064 7.66351 13.3984 7.93937C13.5746 8.08232 13.7639 8.21627 13.9806 8.29034C14.2685 8.38864 14.6091 8.29588 14.8151 8.07401C15.0058 7.86634 14.994 7.5074 14.7839 7.31773C14.6503 7.20247 14.4219 7.22566 14.3274 7.37934C14.2446 7.50117 14.291 7.65312 14.3558 7.77115C14.1837 7.64551 14.0114 7.47037 14.0121 7.24193C13.9764 6.97783 14.1723 6.74489 14.407 6.65247C14.8649 6.46937 15.4391 6.62721 15.7281 7.03044C16.0061 7.40668 16.0282 7.90129 15.9798 8.34953C15.9213 8.77526 15.6831 9.17261 15.3301 9.41906C14.8344 9.7728 14.2062 9.88252 13.6085 9.8749C13.0339 9.85933 12.4652 9.72953 11.9329 9.51493C11.0426 9.15704 10.2095 8.66243 9.29021 8.37653C8.98528 8.25815 8.66096 8.19412 8.33491 8.17439C8.07532 8.17266 7.82922 8.16054 7.58416 8.17716C7.28511 8.20589 6.98779 8.26715 6.70778 8.37687C5.77289 8.66762 4.92731 9.17469 4.02011 9.53328C3.25414 9.83129 2.40544 9.96974 1.59239 9.80429C1.13758 9.71222 0.684853 9.51112 0.37784 9.15288C0.115131 8.84829 -0.0108587 8.44056 0.00160182 8.04113C-0.0129354 7.6642 0.0697885 7.26373 0.318999 6.97022C0.550902 6.69193 0.924371 6.55071 1.28296 6.58186C1.51382 6.59259 1.7478 6.69332 1.89041 6.87919C2.00255 7.02629 2.00947 7.22773 1.96032 7.39976C1.90044 7.55448 1.77688 7.67527 1.64258 7.76804C1.71111 7.65243 1.75265 7.49979 1.67166 7.37899C1.57197 7.21804 1.32657 7.20178 1.19677 7.334C1.03686 7.4984 1.00329 7.767 1.11059 7.96844C1.22688 8.17958 1.46744 8.30003 1.70038 8.32461C2.05274 8.35507 2.35837 8.13666 2.61692 7.92483C3.15203 7.46414 3.63384 6.93699 4.22744 6.54725C4.71686 6.21912 5.29351 5.99726 5.8885 6.00003Z" fill="#423426"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg new file mode 100644 index 0000000000000..ed3f00b0dadf2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/kafka.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.767 8.85446C10.1445 8.85446 9.58638 9.13027 9.20419 9.5645L8.22484 8.87119C8.32881 8.58496 8.3885 8.27762 8.3885 7.95592C8.3885 7.63981 8.33088 7.33762 8.23034 7.05562L9.2075 6.36965C9.58965 6.80169 10.1463 7.076 10.767 7.076C11.9161 7.076 12.851 6.14119 12.851 4.992C12.851 3.84281 11.9161 2.908 10.767 2.908C9.618 2.908 8.68304 3.84281 8.68304 4.992C8.68304 5.19769 8.71411 5.39604 8.76988 5.58388L7.79208 6.27019C7.38361 5.76346 6.7955 5.40965 6.12542 5.30165V4.12319C7.06942 3.92492 7.78069 3.08619 7.78069 2.084C7.78069 0.934808 6.84573 0 5.69669 0C4.54765 0 3.61269 0.934808 3.61269 2.084C3.61269 3.07277 4.30554 3.90115 5.23065 4.11358V5.30735C3.96811 5.52896 3.00488 6.63081 3.00488 7.95592C3.00488 9.2875 3.97765 10.3934 5.24931 10.6075V11.868C4.31473 12.0734 3.61269 12.9069 3.61269 13.9024C3.61269 15.0516 4.54765 15.9864 5.69669 15.9864C6.84573 15.9864 7.78069 15.0516 7.78069 13.9024C7.78069 12.9069 7.07865 12.0734 6.14408 11.868V10.6074C6.78772 10.499 7.37003 10.1603 7.78254 9.6545L8.76842 10.3523C8.71369 10.5385 8.68304 10.7348 8.68304 10.9385C8.68304 12.0877 9.618 13.0225 10.767 13.0225C11.9161 13.0225 12.851 12.0877 12.851 10.9385C12.851 9.78927 11.9161 8.85446 10.767 8.85446V8.85446ZM10.767 3.98158C11.3242 3.98158 11.7774 4.43496 11.7774 4.992C11.7774 5.54904 11.3242 6.00238 10.767 6.00238C10.2098 6.00238 9.75665 5.54904 9.75665 4.992C9.75665 4.43496 10.2098 3.98158 10.767 3.98158V3.98158ZM4.68627 2.084C4.68627 1.52696 5.1395 1.07362 5.69669 1.07362C6.25388 1.07362 6.70708 1.52696 6.70708 2.084C6.70708 2.64104 6.25388 3.09438 5.69669 3.09438C5.1395 3.09438 4.68627 2.64104 4.68627 2.084ZM6.70708 13.9024C6.70708 14.4594 6.25388 14.9128 5.69669 14.9128C5.1395 14.9128 4.68627 14.4594 4.68627 13.9024C4.68627 13.3453 5.1395 12.892 5.69669 12.892C6.25388 12.892 6.70708 13.3453 6.70708 13.9024ZM5.69661 9.36508C4.9195 9.36508 4.28731 8.733 4.28731 7.95592C4.28731 7.17881 4.9195 6.54662 5.69661 6.54662C6.47369 6.54662 7.10588 7.17881 7.10588 7.95592C7.10588 8.733 6.47369 9.36508 5.69661 9.36508ZM10.767 11.9489C10.2098 11.9489 9.75665 11.4955 9.75665 10.9385C9.75665 10.3814 10.2098 9.92808 10.767 9.92808C11.3242 9.92808 11.7774 10.3814 11.7774 10.9385C11.7774 11.4955 11.3242 11.9489 10.767 11.9489Z" fill="black"/> +</svg> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg new file mode 100644 index 0000000000000..bb73f9a5ea90b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mongodb.svg @@ -0,0 +1,8 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.42201 15.9235L7.99874 15.7788C7.99874 15.7788 8.05043 13.6209 7.27594 13.4661C6.7598 12.8672 7.35874 -11.934 9.2172 13.3834C9.2172 13.3834 8.57698 13.7034 8.46341 14.2508C8.33944 14.7877 8.42201 15.9235 8.42201 15.9235Z" fill="white"/> +<path d="M8.42201 15.9235L7.99874 15.7788C7.99874 15.7788 8.05043 13.6209 7.27594 13.4661C6.7598 12.8672 7.35874 -11.934 9.2172 13.3834C9.2172 13.3834 8.57698 13.7034 8.46341 14.2508C8.33944 14.7877 8.42201 15.9235 8.42201 15.9235Z" fill="#A6A385"/> +<path d="M8.6493 13.838C8.6493 13.838 12.3561 11.4012 11.4887 6.3314C10.6524 2.64547 8.68029 1.43741 8.46345 0.97274C8.22602 0.642335 7.99878 0.064209 7.99878 0.064209L8.15374 10.317C8.15374 10.3274 7.83363 13.4559 8.64941 13.8381" fill="white"/> +<path d="M8.6493 13.838C8.6493 13.838 12.3561 11.4012 11.4887 6.3314C10.6524 2.64547 8.68029 1.43741 8.46345 0.97274C8.22602 0.642335 7.99878 0.064209 7.99878 0.064209L8.15374 10.317C8.15374 10.3274 7.83363 13.4559 8.64941 13.8381" fill="#499D4A"/> +<path d="M7.78202 13.972C7.78202 13.972 4.3023 11.5972 4.50884 7.4156C4.70498 3.23389 7.16238 1.17918 7.63735 0.807488C7.94717 0.477083 7.95746 0.353111 7.97816 0.0227051C8.195 0.487376 8.15371 6.97163 8.18459 7.7356C8.27746 10.6783 8.01945 13.4146 7.78202 13.972Z" fill="white"/> +<path d="M7.78202 13.972C7.78202 13.972 4.3023 11.5972 4.50884 7.4156C4.70498 3.23389 7.16238 1.17918 7.63735 0.807488C7.94717 0.477083 7.95746 0.353111 7.97816 0.0227051C8.195 0.487376 8.15371 6.97163 8.18459 7.7356C8.27746 10.6783 8.01945 13.4146 7.78202 13.972Z" fill="#58AA50"/> +</svg> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg new file mode 100644 index 0000000000000..77e4e9b3b5ff8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/mysql.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M14.7281 12.1382C13.8582 12.1165 13.184 12.2036 12.6185 12.4427C12.4554 12.508 12.1944 12.508 12.1727 12.7146C12.2597 12.8016 12.2706 12.943 12.3467 13.0626C12.4773 13.28 12.7055 13.5736 12.9121 13.7259C13.1405 13.8999 13.3689 14.0738 13.608 14.2261C14.0321 14.4871 14.5106 14.6393 14.9238 14.9003C15.1631 15.0524 15.4022 15.2482 15.6414 15.4113C15.7611 15.4982 15.8371 15.6397 15.9894 15.694V15.6614C15.9133 15.5635 15.8915 15.4222 15.8155 15.3134C15.7068 15.2047 15.598 15.1068 15.4892 14.9981C15.1739 14.574 14.7824 14.2043 14.3583 13.8999C14.0104 13.6606 13.2492 13.3344 13.1079 12.9321C13.1079 12.9321 13.0969 12.9212 13.0861 12.9104C13.3253 12.8886 13.608 12.8016 13.8364 12.7364C14.2061 12.6385 14.5432 12.6602 14.9238 12.5624C15.0977 12.5189 15.2718 12.4645 15.4457 12.4101V12.3123C15.2501 12.1165 15.1087 11.8556 14.902 11.6707C14.3475 11.1922 13.7385 10.7247 13.1079 10.3333C12.7707 10.1158 12.3358 9.97438 11.977 9.78959C11.8465 9.72423 11.629 9.6917 11.5529 9.58296C11.3571 9.34372 11.2484 9.02842 11.1071 8.74566C10.7918 8.14754 10.4872 7.48433 10.2154 6.85367C10.0198 6.42956 9.9001 6.00546 9.66086 5.61403C8.54088 3.76549 7.323 2.64551 5.4527 1.54722C5.05036 1.31889 4.57196 1.221 4.06087 1.10142C3.7891 1.0905 3.51719 1.06881 3.24534 1.0579C3.07139 0.981766 2.89736 0.775207 2.7451 0.677315C2.12542 0.285818 0.526896 -0.562254 0.0701855 0.557731C-0.22342 1.26453 0.505136 1.9604 0.755216 2.31929C0.940153 2.56938 1.17932 2.85207 1.30982 3.13483C1.38595 3.31956 1.40764 3.51541 1.48377 3.71112C1.65779 4.18946 1.82083 4.72237 2.04922 5.16824C2.16888 5.39656 2.29931 5.6358 2.45157 5.84235C2.53854 5.96194 2.69073 6.01638 2.72341 6.21209C2.57122 6.42956 2.5603 6.75578 2.47326 7.02762C2.08183 8.25634 2.23409 9.77868 2.78862 10.6812C2.96258 10.953 3.37583 11.5512 3.93037 11.3228C4.41969 11.127 4.31095 10.5072 4.4523 9.96354C4.48498 9.83297 4.46321 9.74606 4.52843 9.65909C4.52849 9.66993 4.52843 9.68085 4.52843 9.68085C4.68069 9.9853 4.83288 10.2789 4.97423 10.5834C5.31135 11.1161 5.8985 11.6707 6.38781 12.0405C6.64888 12.2361 6.85544 12.5732 7.18165 12.6929V12.6602H7.15989C7.09461 12.5623 6.99679 12.5189 6.90981 12.4427C6.71409 12.247 6.49655 12.0078 6.34436 11.7903C5.88772 11.1813 5.4853 10.5072 5.12649 9.81128C4.95253 9.47422 4.80027 9.10448 4.65893 8.76743C4.59364 8.63686 4.59364 8.44121 4.48491 8.376C4.3218 8.61517 4.08263 8.8218 3.96298 9.1154C3.75649 9.58296 3.73466 10.1593 3.65853 10.7573C3.61507 10.7682 3.63677 10.7573 3.615 10.7791C3.2671 10.692 3.14745 10.3333 3.01694 10.0288C2.69073 9.25675 2.63636 8.01711 2.91912 7.12551C2.99525 6.89712 3.32147 6.17948 3.19097 5.96201C3.12575 5.75538 2.90821 5.63579 2.78862 5.47269C2.64728 5.26606 2.49502 5.00513 2.3972 4.77674C2.1362 4.16784 2.0057 3.49365 1.72301 2.88468C1.59251 2.60192 1.36419 2.30838 1.17932 2.04745C0.972761 1.75385 0.74437 1.54722 0.581262 1.19924C0.526961 1.07966 0.450762 0.883943 0.537739 0.753444C0.5595 0.666468 0.602954 0.63386 0.689999 0.612098C0.831344 0.492446 1.23369 0.644706 1.37503 0.709921C1.77737 0.872958 2.11443 1.02529 2.45156 1.25361C2.60375 1.36235 2.76686 1.56898 2.96257 1.62335H3.19096C3.53894 1.69941 3.93036 1.64511 4.25658 1.74293C4.83294 1.92773 5.3548 2.19964 5.82243 2.49325C7.24693 3.39575 8.42122 4.67891 9.21506 6.21209C9.34556 6.4621 9.39986 6.69056 9.51951 6.95149C9.7479 7.48434 10.0306 8.02802 10.2589 8.54995C10.4872 9.06096 10.7047 9.58296 11.0309 10.0071C11.1941 10.2353 11.8465 10.355 12.1401 10.4746C12.3575 10.5724 12.6947 10.6595 12.8904 10.7791C13.26 11.0073 13.6298 11.2684 13.9778 11.5185C14.1518 11.6489 14.6954 11.9208 14.7281 12.1382L14.7281 12.1382Z" fill="#00546B"/> +<path d="M3.63671 2.68896C3.45185 2.68896 3.32142 2.7108 3.19092 2.7434C3.19092 2.74335 3.19092 2.75432 3.19092 2.76516H3.21268C3.29972 2.93912 3.45185 3.05877 3.56058 3.21096C3.64763 3.38491 3.72369 3.55894 3.81074 3.73296C3.82158 3.72204 3.83243 3.7112 3.83243 3.7112C3.98476 3.60239 4.06082 3.42844 4.06082 3.16751C3.99553 3.09138 3.98469 3.01525 3.93032 2.93912C3.8651 2.83031 3.72369 2.77601 3.63671 2.68896Z" fill="#00546B"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg new file mode 100644 index 0000000000000..fcae8d10013da --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/postgresql.svg @@ -0,0 +1,6 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M15.938 9.58085C15.842 9.2991 15.5907 9.10285 15.2657 9.05576C15.1125 9.03358 14.937 9.04303 14.7293 9.08455C14.3673 9.15697 14.0987 9.18455 13.9027 9.18988C14.6425 7.97867 15.2441 6.59746 15.5904 5.29728C16.1504 3.19491 15.8512 2.23716 15.5015 1.80394C14.576 0.657277 13.2259 0.0412775 11.597 0.022429C10.728 0.012126 9.96523 0.17849 9.56729 0.298126C9.19679 0.234732 8.79841 0.199338 8.38041 0.192793C7.59685 0.180671 6.90454 0.346308 6.31298 0.686732C5.98548 0.579338 5.45985 0.427944 4.85285 0.331338C3.42523 0.104065 2.2746 0.281156 1.43298 0.857702C0.413851 1.55576 -0.058586 2.76861 0.028789 4.46255C0.056539 5.00037 0.366789 6.63673 0.855289 8.18861C1.13604 9.08061 1.43541 9.82134 1.7451 10.3904C2.18429 11.1974 2.65423 11.6726 3.18179 11.8433C3.47748 11.9388 4.01473 12.0057 4.57979 11.5493C4.65141 11.6335 4.74698 11.717 4.87379 11.7947C5.03485 11.8932 5.23179 11.9736 5.42848 12.0213C6.13729 12.1932 6.80123 12.1502 7.36766 11.9093C7.37116 12.007 7.37385 12.1004 7.3761 12.181C7.37985 12.3117 7.3836 12.4399 7.38854 12.5598C7.4221 13.3703 7.47898 14.0006 7.64748 14.4415C7.65673 14.4658 7.66916 14.5027 7.68229 14.5419C7.76635 14.7915 7.90691 15.2093 8.26454 15.5366C8.63485 15.8755 9.08279 15.9795 9.49304 15.9795C9.69879 15.9795 9.8951 15.9533 10.0673 15.9175C10.681 15.7899 11.3781 15.5956 11.8824 14.8994C12.3591 14.2412 12.5909 13.2499 12.6328 11.6879C12.6382 11.6437 12.6432 11.6015 12.648 11.5613L12.658 11.4788L12.7704 11.4884L12.7993 11.4902C13.4244 11.5179 14.1888 11.3893 14.6582 11.1779C15.0292 11.011 16.2178 10.4024 15.938 9.58085Z" fill="black"/> +<path d="M14.8691 9.74058C13.0103 10.1124 12.8825 9.50216 12.8825 9.50216C14.845 6.67834 15.6654 3.09392 14.9575 2.21664C13.026 -0.176386 9.68255 0.955372 9.62674 0.984705L9.6088 0.987856C9.24155 0.913917 8.83062 0.869917 8.36868 0.862584C7.52768 0.84925 6.88968 1.0764 6.40555 1.4324C6.40555 1.4324 0.441178 -0.950265 0.718616 4.42901C0.777616 5.57337 2.41012 13.088 4.35724 10.8182C5.06893 9.98822 5.75662 9.28646 5.75662 9.28646C6.09812 9.50646 6.50699 9.61871 6.93562 9.5784L6.96893 9.55101C6.95855 9.65404 6.9633 9.75483 6.98224 9.87416C6.48062 10.4176 6.62805 10.513 5.6253 10.7132C4.61062 10.916 5.20668 11.2769 5.59587 11.3714C6.06768 11.4857 7.15918 11.6478 7.89668 10.6466L7.8673 10.7609C8.0638 10.9136 8.2018 11.7538 8.17868 12.5156C8.15555 13.2773 8.14012 13.8002 8.29493 14.2087C8.44987 14.6172 8.60418 15.5363 9.92243 15.2624C11.0239 15.0335 11.5947 14.4403 11.6741 13.4509C11.7304 12.7475 11.858 12.8515 11.866 12.2226L11.9683 11.9249C12.0862 10.9714 11.9871 10.6637 12.6656 10.8068L12.8306 10.8209C13.33 10.8429 13.9837 10.7429 14.3674 10.57C15.1936 10.1983 15.6836 9.57749 14.8689 9.74058H14.8691Z" fill="#336791"/> +<path d="M6.75467 4.94072C6.58717 4.91812 6.43548 4.93903 6.35873 4.99539C6.31561 5.02709 6.30223 5.06381 6.29861 5.08909C6.28898 5.15606 6.33736 5.23012 6.36711 5.2683C6.45123 5.37642 6.57411 5.45072 6.69573 5.46709C6.71336 5.46951 6.73092 5.4706 6.74836 5.4706C6.95117 5.4706 7.13561 5.31745 7.15186 5.20442C7.17217 5.06284 6.96023 4.96848 6.75467 4.94078V4.94072ZM12.3037 4.94521C12.2877 4.83424 12.084 4.8026 11.8908 4.82866C11.6978 4.85472 11.5107 4.93921 11.5263 5.05042C11.5388 5.1369 11.6999 5.28454 11.8905 5.28454C11.9066 5.28454 11.9229 5.28351 11.9392 5.28127C12.0665 5.26418 12.1599 5.18581 12.2042 5.14066C12.2717 5.07181 12.3109 4.99509 12.3037 4.94521Z" fill="white"/> +<path d="M15.4876 9.69828C15.4167 9.49046 15.1886 9.42361 14.8096 9.49949C13.6843 9.72471 13.2813 9.56871 13.149 9.47422C14.0237 8.1821 14.7432 6.62016 15.1314 5.16289C15.3153 4.47258 15.4169 3.83149 15.4252 3.30895C15.4344 2.73543 15.3336 2.31398 15.1259 2.05652C14.2882 1.01865 13.0589 0.461918 11.5707 0.446645C10.5477 0.435494 9.68331 0.689373 9.51575 0.760766C9.16287 0.675676 8.77818 0.623433 8.35937 0.616766C7.59137 0.604706 6.9275 0.783009 6.37787 1.14646C6.13912 1.06028 5.52212 0.854888 4.76756 0.737009C3.46306 0.533373 2.42643 0.687676 1.68668 1.19586C0.803996 1.80228 0.396496 2.88628 0.475434 4.41768C0.501996 4.93289 0.804746 6.51786 1.2825 8.03574C1.91137 10.0336 2.595 11.1645 3.31425 11.3972C3.39843 11.4244 3.4955 11.4434 3.60256 11.4434C3.86493 11.4434 4.18662 11.3288 4.52131 10.9386C4.92953 10.4637 5.35181 10.0004 5.78762 9.54913C6.07037 9.69628 6.381 9.77846 6.69868 9.78671C6.69931 9.79477 6.70012 9.80283 6.70087 9.81083C6.64619 9.87406 6.59264 9.93821 6.54025 10.0033C6.32012 10.2742 6.27431 10.3306 5.56575 10.4721C5.36418 10.5125 4.82887 10.6194 4.821 10.9833C4.8125 11.3809 5.45381 11.5479 5.52687 11.5656C5.7815 11.6274 6.02681 11.6579 6.26075 11.6579C6.82968 11.6579 7.33037 11.4765 7.7305 11.1257C7.71818 12.543 7.77912 13.9396 7.95462 14.3651C8.09837 14.7135 8.4495 15.5648 9.55862 15.5648C9.72137 15.5648 9.9005 15.5464 10.0976 15.5054C11.2551 15.2648 11.7578 14.7687 11.9522 13.6749C12.0563 13.0904 12.2349 11.6946 12.3189 10.9459C12.4961 10.9995 12.7243 11.024 12.971 11.024C13.4855 11.024 14.0792 10.918 14.4515 10.7504C14.8697 10.562 15.6245 10.0999 15.4876 9.69828ZM12.731 4.63889C12.7272 4.85992 12.6958 5.06058 12.6626 5.27004C12.6267 5.49531 12.5897 5.72822 12.5804 6.01095C12.5712 6.2861 12.6067 6.57216 12.6409 6.84883C12.7102 7.40762 12.7812 7.98289 12.5062 8.55052C12.4605 8.47202 12.4198 8.39088 12.3843 8.30755C12.3501 8.22719 12.2759 8.0981 12.1731 7.91943C11.7731 7.22392 10.8366 5.59519 11.316 4.93058C11.4588 4.73277 11.8212 4.52937 12.731 4.63889ZM11.6282 0.894221C12.9616 0.922766 14.0164 1.40646 14.7631 2.3318C15.3359 3.04155 14.7052 6.27107 12.8794 9.05719C12.861 9.03455 12.8426 9.01198 12.824 8.98949L12.8009 8.96149C13.2727 8.20592 13.1804 7.45834 13.0983 6.79555C13.0646 6.52355 13.0327 6.26665 13.0408 6.02531C13.0492 5.76955 13.0841 5.55016 13.1178 5.33804C13.1592 5.07665 13.2014 4.80616 13.1898 4.48731C13.1985 4.45386 13.202 4.41434 13.1974 4.36743C13.1677 4.06168 12.8077 3.14658 12.074 2.31834C11.6726 1.86531 11.0873 1.35834 10.2881 1.0164C10.6319 0.947312 11.1019 0.882888 11.6282 0.894221ZM4.16712 10.653C3.79837 11.0829 3.54375 11.0005 3.46 10.9735C2.91437 10.797 2.28125 9.67871 1.72306 7.90537C1.24006 6.37095 0.957809 4.82798 0.935434 4.39531C0.864934 3.02695 1.207 2.07331 1.95218 1.56083C3.16493 0.726888 5.15881 1.22604 5.96 1.47919C5.9485 1.49022 5.9365 1.50052 5.92512 1.51174C4.61037 2.79925 4.64156 4.99901 4.64481 5.13349C4.64468 5.18537 4.64918 5.25883 4.65531 5.35986C4.67793 5.72986 4.72006 6.41846 4.60756 7.19828C4.50306 7.92295 4.73343 8.63222 5.2395 9.14428C5.29143 9.19673 5.34602 9.24665 5.40306 9.29386C5.17781 9.5278 4.68825 10.0451 4.16712 10.653ZM5.57206 8.83525C5.16418 8.42252 4.97893 7.84846 5.06375 7.2601C5.1825 6.43634 5.13868 5.71889 5.11512 5.33343C5.11181 5.27949 5.10887 5.23222 5.10718 5.19495C5.29925 5.0298 6.18931 4.56737 6.82406 4.7084C7.11368 4.77271 7.29018 4.96398 7.36356 5.29295C7.74331 6.99616 7.41381 7.70604 7.14906 8.27652C7.0945 8.39404 7.04293 8.50513 6.99893 8.62004L6.96481 8.70889C6.87843 8.93349 6.79806 9.14234 6.74825 9.34065C6.31462 9.33943 5.89281 9.1598 5.57206 8.83519V8.83525ZM5.63862 11.1322C5.512 11.1016 5.39812 11.0483 5.33131 11.0041C5.38712 10.9786 5.48643 10.944 5.65868 10.9096C6.49225 10.7432 6.621 10.6257 6.90212 10.2796C6.96656 10.2002 7.03962 10.1103 7.14081 10.0007L7.14093 10.0006C7.29162 9.83695 7.36056 9.86471 7.48556 9.91501C7.58687 9.95561 7.68556 10.0788 7.72556 10.2143C7.7445 10.2783 7.76575 10.3997 7.69618 10.4943C7.10893 11.2916 6.25318 11.2814 5.63862 11.1322ZM10.0011 15.0687C8.98131 15.2806 8.62025 14.776 8.38231 14.1993C8.22875 13.8269 8.15325 12.1477 8.20681 10.2934C8.2075 10.2687 8.20387 10.2449 8.19687 10.2225C8.19061 10.1782 8.18109 10.1344 8.16837 10.0914C8.08875 9.82162 7.89468 9.59592 7.66187 9.50234C7.56937 9.46519 7.39962 9.39701 7.19562 9.44761C7.23912 9.2738 7.31456 9.07749 7.39637 8.86495L7.43068 8.77555C7.46931 8.67477 7.51781 8.57034 7.56906 8.4598C7.84612 7.86295 8.22556 7.04543 7.81375 5.19858C7.6595 4.50683 7.14437 4.16901 6.3635 4.24749C5.89537 4.29446 5.46706 4.47762 5.25343 4.58265C5.2075 4.60519 5.1655 4.62701 5.12625 4.64822C5.18587 3.95125 5.41112 2.64871 6.25375 1.82458C6.78431 1.3058 7.49087 1.04955 8.35175 1.06337C10.048 1.09028 11.1357 1.9344 11.7496 2.63786C12.2786 3.24398 12.5651 3.85458 12.6794 4.18392C11.8197 4.09913 11.235 4.26368 10.9386 4.67458C10.2938 5.5684 11.2914 7.30319 11.7708 8.13695C11.8587 8.28974 11.9346 8.4218 11.9585 8.47792C12.1146 8.84483 12.3167 9.0898 12.4643 9.26859C12.5096 9.32337 12.5534 9.37652 12.5868 9.42295C12.3264 9.49574 11.8587 9.66392 11.9014 10.5046C11.867 10.9264 11.6226 12.9013 11.4984 13.5991C11.3344 14.5209 10.9846 14.8642 10.0011 15.0688V15.0687ZM14.2576 10.3453C13.9913 10.4651 13.5457 10.555 13.1225 10.5743C12.655 10.5956 12.417 10.5236 12.361 10.4793C12.3347 9.95537 12.5358 9.90065 12.7486 9.84271C12.7821 9.83361 12.8147 9.82471 12.8462 9.81404C12.8657 9.82949 12.8872 9.84483 12.9107 9.85986C13.2864 10.1003 13.9566 10.1263 14.9028 9.93689L14.9132 9.93489C14.7856 10.0506 14.5672 10.2059 14.2576 10.3453Z" fill="white"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/redis.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/redis.svg new file mode 100644 index 0000000000000..907312ebe74e6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/redis.svg @@ -0,0 +1,12 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M15.297 11.8956C14.4525 12.3331 10.078 14.1211 9.14702 14.6036C8.21552 15.0866 7.69802 15.0816 6.96202 14.7321C6.22652 14.3826 1.57102 12.5136 0.732018 12.1151C0.313018 11.9156 0.0930176 11.7476 0.0930176 11.5891V9.99913C0.0930176 9.99913 6.15352 8.68763 7.13202 8.33913C8.11052 7.98963 8.45002 7.97713 9.28252 8.28013C10.1155 8.58363 15.094 9.47663 15.9175 9.77613L15.917 11.3436C15.917 11.5006 15.727 11.6731 15.297 11.8956V11.8956Z" fill="#A41E11"/> +<path d="M15.297 10.3052C14.4525 10.7427 10.078 12.5307 9.14704 13.0137C8.21554 13.4962 7.69804 13.4917 6.96204 13.1417C6.22654 12.7927 1.57104 10.9227 0.732042 10.5247C-0.105958 10.1262 -0.123958 9.85221 0.700042 9.53121C1.52354 9.21071 6.15354 7.40521 7.13204 7.05621C8.11054 6.70721 8.45004 6.69471 9.28254 6.99771C10.1155 7.30121 14.464 9.02171 15.287 9.32121C16.11 9.62121 16.142 9.86771 15.297 10.3052V10.3052Z" fill="#D82C20"/> +<path d="M15.297 9.30315C14.4525 9.74065 10.078 11.5282 9.14702 12.0112C8.21552 12.4937 7.69802 12.4892 6.96202 12.1397C6.22652 11.7897 1.57102 9.92066 0.732018 9.52216C0.313018 9.32316 0.0930176 9.15515 0.0930176 8.99665V7.40665C0.0930176 7.40665 6.15352 6.09515 7.13202 5.74615C8.11052 5.39715 8.45002 5.38466 9.28252 5.68766C10.1155 5.99066 15.094 6.88366 15.9175 7.18316L15.917 8.75065C15.917 8.90815 15.727 9.08065 15.297 9.30315V9.30315Z" fill="#A41E11"/> +<path d="M15.297 7.71264C14.4525 8.15014 10.078 9.93764 9.14704 10.4206C8.21554 10.9036 7.69804 10.8986 6.96204 10.5491C6.22654 10.1996 1.57104 8.33014 0.732042 7.93164C-0.105958 7.53364 -0.123958 7.25914 0.700042 6.93864C1.52354 6.61814 6.15354 4.81214 7.13204 4.46364C8.11054 4.11464 8.45004 4.10164 9.28254 4.40514C10.1155 4.70814 14.464 6.42864 15.287 6.72814C16.11 7.02814 16.142 7.27514 15.297 7.71264V7.71264Z" fill="#D82C20"/> +<path d="M15.297 6.61406C14.4525 7.05156 10.078 8.83956 9.14701 9.32256C8.21551 9.80506 7.69801 9.80006 6.96201 9.45056C6.22651 9.10106 1.57101 7.23156 0.732006 6.83356C0.313506 6.63406 0.0935059 6.46606 0.0935059 6.30756V4.71756C0.0935059 4.71756 6.15401 3.40606 7.13251 3.05756C8.11101 2.70806 8.45051 2.69556 9.28301 2.99906C10.116 3.30206 15.0945 4.19456 15.918 4.49406L15.9175 6.06206C15.9175 6.21906 15.7275 6.39156 15.2975 6.61406H15.297Z" fill="#A41E11"/> +<path d="M15.297 5.0236C14.4525 5.4611 10.078 7.2491 9.14703 7.7321C8.21553 8.2146 7.69803 8.2096 6.96203 7.8601C6.22653 7.5106 1.57103 5.6416 0.732034 5.2431C-0.105466 4.8446 -0.123466 4.5701 0.700034 4.2496C1.52353 3.9291 6.15353 2.1236 7.13203 1.7751C8.11053 1.4251 8.45003 1.4131 9.28253 1.7161C10.1155 2.0196 14.464 3.7401 15.287 4.0396C16.11 4.3396 16.142 4.5861 15.297 5.0236V5.0236Z" fill="#D82C20"/> +<path d="M10.0785 3.49248L8.70752 3.63398L8.40052 4.36798L7.90452 3.54898L6.32152 3.40748L7.50252 2.98398L7.14852 2.33398L8.25452 2.76398L9.29702 2.42398L9.01552 3.09648L10.0785 3.49248ZM8.31852 7.05398L5.75952 5.99898L9.42652 5.43948L8.31852 7.05398Z" fill="white"/> +<path d="M4.77055 5.41063C5.85303 5.41063 6.73055 5.07261 6.73055 4.65563C6.73055 4.23866 5.85303 3.90063 4.77055 3.90063C3.68807 3.90063 2.81055 4.23866 2.81055 4.65563C2.81055 5.07261 3.68807 5.41063 4.77055 5.41063Z" fill="white"/> +<path d="M11.6991 3.69312L13.8691 4.54562L11.7011 5.39712L11.6991 3.69312Z" fill="#7A0C00"/> +<path d="M9.2981 4.63712L11.6991 3.69312L11.7011 5.39712L11.4656 5.48862L9.2981 4.63712Z" fill="#AD2115"/> +</svg> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg rename to x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg new file mode 100644 index 0000000000000..2c83babd0bac1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/websocket.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.0275 11.0403H14.0138V6.27121L11.776 4.03341L10.3714 5.43793L12.0275 7.09401V11.0403ZM14.019 12.036H11.1261H7.09073L5.43465 10.38L6.13691 9.67769L7.50475 11.0455H10.319L7.54668 8.26793L8.25418 7.56043L11.0265 10.3328V7.51851L9.66394 6.15591L10.361 5.45889L6.91779 2H3.52178H0L1.981 3.981V3.98624H1.99148H6.08975L7.54143 5.43793L5.41893 7.56043L3.96725 6.10875V4.98198H1.981V6.93154L5.41893 10.3695L4.01965 11.7688L6.25745 14.0066H9.65346H16L14.019 12.036Z" fill="#231F20"/> +</svg> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx new file mode 100644 index 0000000000000..c7e25269511bf --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { render } from '@testing-library/react'; +import React, { FunctionComponent } from 'react'; +import { License } from '../../../../../licensing/common/license'; +import { LicenseContext } from '../../../context/LicenseContext'; +import { ServiceMap } from './'; +import { MockApmPluginContextWrapper } from '../../../context/ApmPluginContext/MockApmPluginContext'; + +const expiredLicense = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'platinum', + status: 'expired', + type: 'platinum', + uid: '1' + } +}); + +const Wrapper: FunctionComponent = ({ children }) => { + return ( + <LicenseContext.Provider value={expiredLicense}> + <MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); +}; + +describe('ServiceMap', () => { + describe('with an inactive license', () => { + it('renders the license banner', async () => { + expect( + ( + await render(<ServiceMap />, { + wrapper: Wrapper + }).findAllByText(/Platinum/) + ).length + ).toBeGreaterThan(0); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx new file mode 100644 index 0000000000000..b57f0b047c613 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; +import { + invalidLicenseMessage, + isValidPlatinumLicense +} from '../../../../common/service_map'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { useLicense } from '../../../hooks/useLicense'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { callApmApi } from '../../../services/rest/createCallApmApi'; +import { LicensePrompt } from '../../shared/LicensePrompt'; +import { Controls } from './Controls'; +import { Cytoscape } from './Cytoscape'; +import { cytoscapeDivStyle } from './cytoscapeOptions'; +import { EmptyBanner } from './EmptyBanner'; +import { Popover } from './Popover'; +import { useRefDimensions } from './useRefDimensions'; +import { BetaBadge } from './BetaBadge'; +import { useTrackPageview } from '../../../../../observability/public'; + +interface ServiceMapProps { + serviceName?: string; +} + +export function ServiceMap({ serviceName }: ServiceMapProps) { + const license = useLicense(); + const { urlParams } = useUrlParams(); + + const { data = { elements: [] } } = useFetcher(() => { + // When we don't have a license or a valid license, don't make the request. + if (!license || !isValidPlatinumLicense(license)) { + return; + } + + const { start, end, environment } = urlParams; + if (start && end) { + return callApmApi({ + isCachable: false, + pathname: '/api/apm/service-map', + params: { + query: { + start, + end, + environment, + serviceName + } + } + }); + } + }, [license, serviceName, urlParams]); + + const { ref, height, width } = useRefDimensions(); + + useTrackPageview({ app: 'apm', path: 'service_map' }); + useTrackPageview({ app: 'apm', path: 'service_map', delay: 15000 }); + + if (!license) { + return null; + } + + return isValidPlatinumLicense(license) ? ( + <div + style={{ height: height - parseInt(theme.gutterTypes.gutterLarge, 10) }} + ref={ref} + > + <Cytoscape + elements={data?.elements ?? []} + height={height} + serviceName={serviceName} + style={cytoscapeDivStyle} + width={width} + > + <Controls /> + <BetaBadge /> + {serviceName && <EmptyBanner />} + <Popover focusedServiceName={serviceName} /> + </Cytoscape> + </div> + ) : ( + <EuiFlexGroup + alignItems="center" + justifyContent="spaceAround" + // Set the height to give it some top margin + style={{ height: '60vh' }} + > + <EuiFlexItem + grow={false} + style={{ width: 600, textAlign: 'center' as const }} + > + <LicensePrompt text={invalidLicenseMessage} showBetaBadge /> + </EuiFlexItem> + </EuiFlexGroup> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts rename to x-pack/plugins/apm/public/components/app/ServiceMap/useRefDimensions.ts diff --git a/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx new file mode 100644 index 0000000000000..0fb8c00a2b162 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMetrics/index.tsx @@ -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 { + EuiFlexGrid, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiFlexGroup +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; +import { MetricsChart } from '../../shared/charts/MetricsChart'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; + +interface ServiceMetricsProps { + agentName: string; +} + +export function ServiceMetrics({ agentName }: ServiceMetricsProps) { + const { urlParams } = useUrlParams(); + const { serviceName, serviceNodeName } = urlParams; + const { data } = useServiceMetricCharts(urlParams, agentName); + const { start, end } = urlParams; + + const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( + () => ({ + filterNames: ['host', 'containerId', 'podName', 'serviceVersion'], + params: { + serviceName, + serviceNodeName + }, + projection: PROJECTION.METRICS, + showCount: false + }), + [serviceName, serviceNodeName] + ); + + return ( + <> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <LocalUIFilters {...localFiltersConfig} /> + </EuiFlexItem> + <EuiFlexItem grow={7}> + <ChartsSyncContextProvider> + <EuiFlexGrid columns={2} gutterSize="s"> + {data.charts.map(chart => ( + <EuiFlexItem key={chart.key}> + <EuiPanel> + <MetricsChart start={start} end={end} chart={chart} /> + </EuiPanel> + </EuiFlexItem> + ))} + </EuiFlexGrid> + <EuiSpacer size="xxl" /> + </ChartsSyncContextProvider> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx new file mode 100644 index 0000000000000..3929c153ae419 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeMetrics/index.tsx @@ -0,0 +1,186 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiFlexGrid, + EuiPanel, + EuiSpacer, + EuiStat, + EuiToolTip, + EuiCallOut +} from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { ApmHeader } from '../../shared/ApmHeader'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useAgentName } from '../../../hooks/useAgentName'; +import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { MetricsChart } from '../../shared/charts/MetricsChart'; +import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher'; +import { truncate, px, unit } from '../../../style/variables'; +import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; + +const INITIAL_DATA = { + host: '', + containerId: '' +}; + +const Truncate = styled.span` + display: block; + ${truncate(px(unit * 12))} +`; + +export function ServiceNodeMetrics() { + const { urlParams, uiFilters } = useUrlParams(); + const { serviceName, serviceNodeName } = urlParams; + + const { agentName } = useAgentName(); + const { data } = useServiceMetricCharts(urlParams, agentName); + const { start, end } = urlParams; + + const { data: { host, containerId } = INITIAL_DATA, status } = useFetcher( + callApmApi => { + if (serviceName && serviceNodeName && start && end) { + return callApmApi({ + pathname: + '/api/apm/services/{serviceName}/node/{serviceNodeName}/metadata', + params: { + path: { serviceName, serviceNodeName }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, + [serviceName, serviceNodeName, start, end, uiFilters] + ); + + const isLoading = status === FETCH_STATUS.LOADING; + + const isAggregatedData = serviceNodeName === SERVICE_NODE_NAME_MISSING; + + return ( + <div> + <ApmHeader> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="l"> + <h1>{serviceName}</h1> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </ApmHeader> + <EuiHorizontalRule margin="m" /> + {isAggregatedData ? ( + <EuiCallOut + title={i18n.translate( + 'xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle', + { + defaultMessage: 'Could not identify JVMs' + } + )} + iconType="help" + color="warning" + > + <FormattedMessage + id="xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningText" + defaultMessage="We could not identify which JVMs these metrics belong to. This is likely caused by running a version of APM Server that is older than 7.5. Upgrading to APM Server 7.5 or higher should resolve this issue. For more information on upgrading, see the {link}. As an alternative, you can use the Kibana Query bar to filter by hostname, container ID or other fields." + values={{ + link: ( + <ElasticDocsLink + target="_blank" + section="/apm/server" + path="/upgrading.html" + > + {i18n.translate( + 'xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningDocumentationLink', + { defaultMessage: 'documentation of APM Server' } + )} + </ElasticDocsLink> + ) + }} + /> + </EuiCallOut> + ) : ( + <EuiFlexGroup gutterSize="xl"> + <EuiFlexItem grow={false}> + <EuiStat + titleSize="s" + description={i18n.translate( + 'xpack.apm.serviceNodeMetrics.serviceName', + { + defaultMessage: 'Service name' + } + )} + title={ + <EuiToolTip content={serviceName}> + <Truncate>{serviceName}</Truncate> + </EuiToolTip> + } + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiStat + titleSize="s" + isLoading={isLoading} + description={i18n.translate('xpack.apm.serviceNodeMetrics.host', { + defaultMessage: 'Host' + })} + title={ + <EuiToolTip content={host}> + <Truncate>{host}</Truncate> + </EuiToolTip> + } + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiStat + titleSize="s" + isLoading={isLoading} + description={i18n.translate( + 'xpack.apm.serviceNodeMetrics.containerId', + { + defaultMessage: 'Container ID' + } + )} + title={ + <EuiToolTip content={containerId}> + <Truncate>{containerId}</Truncate> + </EuiToolTip> + } + /> + </EuiFlexItem> + </EuiFlexGroup> + )} + <EuiHorizontalRule margin="m" /> + {agentName && serviceNodeName && ( + <ChartsSyncContextProvider> + <EuiFlexGrid columns={2} gutterSize="s"> + {data.charts.map(chart => ( + <EuiFlexItem key={chart.key}> + <EuiPanel> + <MetricsChart start={start} end={end} chart={chart} /> + </EuiPanel> + </EuiFlexItem> + ))} + </EuiFlexGrid> + <EuiSpacer size="xxl" /> + </ChartsSyncContextProvider> + )} + </div> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx new file mode 100644 index 0000000000000..4e57cb47691be --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceNodeOverview/index.tsx @@ -0,0 +1,187 @@ +/* + * 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, { useMemo } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiToolTip, + EuiSpacer +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; +import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { ManagedTable, ITableColumn } from '../../shared/ManagedTable'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { + asDynamicBytes, + asInteger, + asPercent +} from '../../../utils/formatters'; +import { ServiceNodeMetricOverviewLink } from '../../shared/Links/apm/ServiceNodeMetricOverviewLink'; +import { truncate, px, unit } from '../../../style/variables'; + +const INITIAL_PAGE_SIZE = 25; +const INITIAL_SORT_FIELD = 'cpu'; +const INITIAL_SORT_DIRECTION = 'desc'; + +const ServiceNodeName = styled.div` + ${truncate(px(8 * unit))} +`; + +const ServiceNodeOverview = () => { + const { uiFilters, urlParams } = useUrlParams(); + const { serviceName, start, end } = urlParams; + + const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( + () => ({ + filterNames: ['host', 'containerId', 'podName'], + params: { + serviceName + }, + projection: PROJECTION.SERVICE_NODES + }), + [serviceName] + ); + + const { data: items = [] } = useFetcher( + callApmApi => { + if (!serviceName || !start || !end) { + return undefined; + } + return callApmApi({ + pathname: '/api/apm/services/{serviceName}/serviceNodes', + params: { + path: { + serviceName + }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + }, + [serviceName, start, end, uiFilters] + ); + + if (!serviceName) { + return null; + } + + const columns: Array<ITableColumn<typeof items[0]>> = [ + { + name: ( + <EuiToolTip + content={i18n.translate('xpack.apm.jvmsTable.nameExplanation', { + defaultMessage: `By default, the JVM name is the container ID (where applicable) or the hostname, but it can be manually configured through the agent's 'service_node_name' configuration.` + })} + > + <> + {i18n.translate('xpack.apm.jvmsTable.nameColumnLabel', { + defaultMessage: 'Name' + })} + </> + </EuiToolTip> + ), + field: 'name', + sortable: true, + render: (name: string) => { + const { displayedName, tooltip } = + name === SERVICE_NODE_NAME_MISSING + ? { + displayedName: UNIDENTIFIED_SERVICE_NODES_LABEL, + tooltip: i18n.translate( + 'xpack.apm.jvmsTable.explainServiceNodeNameMissing', + { + defaultMessage: + 'We could not identify which JVMs these metrics belong to. This is likely caused by running a version of APM Server that is older than 7.5. Upgrading to APM Server 7.5 or higher should resolve this issue.' + } + ) + } + : { displayedName: name, tooltip: name }; + + return ( + <EuiToolTip content={tooltip}> + <ServiceNodeMetricOverviewLink + serviceName={serviceName} + serviceNodeName={name} + > + <ServiceNodeName>{displayedName}</ServiceNodeName> + </ServiceNodeMetricOverviewLink> + </EuiToolTip> + ); + } + }, + { + name: i18n.translate('xpack.apm.jvmsTable.cpuColumnLabel', { + defaultMessage: 'CPU avg' + }), + field: 'cpu', + sortable: true, + render: (value: number | null) => asPercent(value || 0, 1) + }, + { + name: i18n.translate('xpack.apm.jvmsTable.heapMemoryColumnLabel', { + defaultMessage: 'Heap memory avg' + }), + field: 'heapMemory', + sortable: true, + render: asDynamicBytes + }, + { + name: i18n.translate('xpack.apm.jvmsTable.nonHeapMemoryColumnLabel', { + defaultMessage: 'Non-heap memory avg' + }), + field: 'nonHeapMemory', + sortable: true, + render: asDynamicBytes + }, + { + name: i18n.translate('xpack.apm.jvmsTable.threadCountColumnLabel', { + defaultMessage: 'Thread count max' + }), + field: 'threadCount', + sortable: true, + render: asInteger + } + ]; + + return ( + <> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <LocalUIFilters {...localFiltersConfig} /> + </EuiFlexItem> + <EuiFlexItem grow={7}> + <EuiPanel> + <ManagedTable + noItemsMessage={i18n.translate( + 'xpack.apm.jvmsTable.noJvmsLabel', + { + defaultMessage: 'No JVMs were found' + } + )} + items={items} + columns={columns} + initialPageSize={INITIAL_PAGE_SIZE} + initialSortField={INITIAL_SORT_FIELD} + initialSortDirection={INITIAL_SORT_DIRECTION} + /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +export { ServiceNodeOverview }; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/NoServicesMessage.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx new file mode 100644 index 0000000000000..7e2d03ad35899 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -0,0 +1,135 @@ +/* + * 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 { EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import styled from 'styled-components'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { fontSizes, truncate } from '../../../../style/variables'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; +import { ManagedTable } from '../../../shared/ManagedTable'; +import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; +import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; + +interface Props { + items: ServiceListAPIResponse['items']; + noItemsMessage?: React.ReactNode; +} + +function formatNumber(value: number) { + if (value === 0) { + return '0'; + } else if (value <= 0.1) { + return '< 0.1'; + } else { + return asDecimal(value); + } +} + +function formatString(value?: string | null) { + return value || NOT_AVAILABLE_LABEL; +} + +const AppLink = styled(TransactionOverviewLink)` + font-size: ${fontSizes.large}; + ${truncate('100%')}; +`; + +export const SERVICE_COLUMNS = [ + { + field: 'serviceName', + name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { + defaultMessage: 'Name' + }), + width: '40%', + sortable: true, + render: (serviceName: string) => ( + <EuiToolTip content={formatString(serviceName)} id="service-name-tooltip"> + <AppLink serviceName={serviceName}>{formatString(serviceName)}</AppLink> + </EuiToolTip> + ) + }, + { + field: 'environments', + name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { + defaultMessage: 'Environment' + }), + width: '20%', + sortable: true, + render: (environments: string[]) => ( + <EnvironmentBadge environments={environments} /> + ) + }, + { + field: 'agentName', + name: i18n.translate('xpack.apm.servicesTable.agentColumnLabel', { + defaultMessage: 'Agent' + }), + sortable: true, + render: (agentName: string) => formatString(agentName) + }, + { + field: 'avgResponseTime', + name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', { + defaultMessage: 'Avg. response time' + }), + sortable: true, + dataType: 'number', + render: (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted + }, + { + field: 'transactionsPerMinute', + name: i18n.translate( + 'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel', + { + defaultMessage: 'Trans. per minute' + } + ), + sortable: true, + dataType: 'number', + render: (value: number) => + `${formatNumber(value)} ${i18n.translate( + 'xpack.apm.servicesTable.transactionsPerMinuteUnitLabel', + { + defaultMessage: 'tpm' + } + )}` + }, + { + field: 'errorsPerMinute', + name: i18n.translate('xpack.apm.servicesTable.errorsPerMinuteColumnLabel', { + defaultMessage: 'Errors per minute' + }), + sortable: true, + dataType: 'number', + render: (value: number) => + `${formatNumber(value)} ${i18n.translate( + 'xpack.apm.servicesTable.errorsPerMinuteUnitLabel', + { + defaultMessage: 'err.' + } + )}` + } +]; + +export function ServiceList({ items, noItemsMessage }: Props) { + return ( + <ManagedTable + columns={SERVICE_COLUMNS} + items={items} + noItemsMessage={noItemsMessage} + initialSortField="serviceName" + initialPageSize={50} + /> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/NoServicesMessage.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/NoServicesMessage.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap rename to x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx new file mode 100644 index 0000000000000..99b169e3ec361 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useMemo } from 'react'; +import url from 'url'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { NoServicesMessage } from './NoServicesMessage'; +import { ServiceList } from './ServiceList'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useTrackPageview } from '../../../../../observability/public'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; + +const initalData = { + items: [], + hasHistoricalData: true, + hasLegacyData: false +}; + +let hasDisplayedToast = false; + +export function ServiceOverview() { + const { core } = useApmPluginContext(); + const { + urlParams: { start, end }, + uiFilters + } = useUrlParams(); + const { data = initalData, status } = useFetcher( + callApmApi => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/services', + params: { + query: { start, end, uiFilters: JSON.stringify(uiFilters) } + } + }); + } + }, + [start, end, uiFilters] + ); + + useEffect(() => { + if (data.hasLegacyData && !hasDisplayedToast) { + hasDisplayedToast = true; + + core.notifications.toasts.addWarning({ + title: i18n.translate('xpack.apm.serviceOverview.toastTitle', { + defaultMessage: + 'Legacy data was detected within the selected time range' + }), + text: toMountPoint( + <p> + {i18n.translate('xpack.apm.serviceOverview.toastText', { + defaultMessage: + "You're running Elastic Stack 7.0+ and we've detected incompatible data from a previous 6.x version. If you want to view this data in APM, you should migrate it. See more in " + })} + + <EuiLink + href={url.format({ + pathname: core.http.basePath.prepend('/app/kibana'), + hash: '/management/elasticsearch/upgrade_assistant' + })} + > + {i18n.translate( + 'xpack.apm.serviceOverview.upgradeAssistantLink', + { + defaultMessage: 'the upgrade assistant' + } + )} + </EuiLink> + </p> + ) + }); + } + }, [data.hasLegacyData, core.http.basePath, core.notifications.toasts]); + + useTrackPageview({ app: 'apm', path: 'services_overview' }); + useTrackPageview({ app: 'apm', path: 'services_overview', delay: 15000 }); + + const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( + () => ({ + filterNames: ['host', 'agentName'], + projection: PROJECTION.SERVICES + }), + [] + ); + + return ( + <> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <LocalUIFilters {...localFiltersConfig} /> + </EuiFlexItem> + <EuiFlexItem grow={7}> + <EuiPanel> + <ServiceList + items={data.items} + noItemsMessage={ + <NoServicesMessage + historicalDataFound={data.hasHistoricalData} + status={status} + /> + } + /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/FormRowSelect.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx index 43002c79aa2b4..f4b942c7f46eb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/ServicePage/ServicePage.tsx @@ -16,11 +16,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { isString } from 'lodash'; import { EuiButtonEmpty } from '@elastic/eui'; -import { AgentConfigurationIntake } from '../../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types'; import { omitAllOption, getOptionLabel -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +} from '../../../../../../../common/agent_configuration/all_option'; import { useFetcher, FETCH_STATUS } from '../../../../../../hooks/useFetcher'; import { FormRowSelect } from './FormRowSelect'; import { APMLink } from '../../../../../shared/Links/apm/APMLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx index baab600145b81..fcd75a05b01d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingFormRow.tsx @@ -17,12 +17,12 @@ import { EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SettingDefinition } from '../../../../../../../../../../plugins/apm/common/agent_configuration/setting_definitions/types'; -import { isValid } from '../../../../../../../../../../plugins/apm/common/agent_configuration/setting_definitions'; +import { SettingDefinition } from '../../../../../../../common/agent_configuration/setting_definitions/types'; +import { isValid } from '../../../../../../../common/agent_configuration/setting_definitions'; import { amountAndUnitToString, amountAndUnitToObject -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/amount_and_unit'; +} from '../../../../../../../common/agent_configuration/amount_and_unit'; import { SelectWithPlaceholder } from '../../../../../shared/SelectWithPlaceholder'; function FormRow({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx index 6d76b69600333..e41bdaf0c9c09 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/SettingsPage.tsx @@ -23,19 +23,19 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty } from '@elastic/eui'; import { EuiCallOut } from '@elastic/eui'; import { FETCH_STATUS } from '../../../../../../hooks/useFetcher'; -import { AgentName } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/fields/agent'; +import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent'; import { history } from '../../../../../../utils/history'; -import { AgentConfigurationIntake } from '../../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types'; import { filterByAgent, settingDefinitions, isValid -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/setting_definitions'; +} from '../../../../../../../common/agent_configuration/setting_definitions'; import { saveConfig } from './saveConfig'; import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; -import { useUiTracker } from '../../../../../../../../../../plugins/observability/public'; +import { useUiTracker } from '../../../../../../../../observability/public'; import { SettingFormRow } from './SettingFormRow'; -import { getOptionLabel } from '../../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +import { getOptionLabel } from '../../../../../../../common/agent_configuration/all_option'; function removeEmpty<T>(obj: T): T { return Object.fromEntries( diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts index 7e3bcd68699be..5f7354bf6f713 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/SettingsPage/saveConfig.ts @@ -6,11 +6,11 @@ import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; -import { AgentConfigurationIntake } from '../../../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../../../common/agent_configuration/configuration_types'; import { getOptionLabel, omitAllOption -} from '../../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +} from '../../../../../../../common/agent_configuration/all_option'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; export async function saveConfig({ diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx new file mode 100644 index 0000000000000..089bc58f50a88 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.stories.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * 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 { storiesOf } from '@storybook/react'; +import React from 'react'; +import { HttpSetup } from 'kibana/public'; +import { AgentConfiguration } from '../../../../../../common/agent_configuration/configuration_types'; +import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { createCallApmApi } from '../../../../../services/rest/createCallApmApi'; +import { AgentConfigurationCreateEdit } from './index'; +import { + ApmPluginContext, + ApmPluginContextValue +} from '../../../../../context/ApmPluginContext'; + +storiesOf( + 'app/Settings/AgentConfigurations/AgentConfigurationCreateEdit', + module +).add( + 'with config', + () => { + const httpMock = {}; + + // mock + createCallApmApi((httpMock as unknown) as HttpSetup); + + const contextMock = { + core: { + notifications: { toasts: { addWarning: () => {}, addDanger: () => {} } } + } + }; + return ( + <ApmPluginContext.Provider + value={(contextMock as unknown) as ApmPluginContextValue} + > + <AgentConfigurationCreateEdit + pageStep="choose-settings-step" + existingConfigResult={{ + status: FETCH_STATUS.SUCCESS, + data: { + service: { name: 'opbeans-node', environment: 'production' }, + settings: {} + } as AgentConfiguration + }} + /> + </ApmPluginContext.Provider> + ); + }, + { + info: { + source: false + } + } +); diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx new file mode 100644 index 0000000000000..3a6f94b975800 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationCreateEdit/index.tsx @@ -0,0 +1,157 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import React, { useState, useEffect, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FetcherResult } from '../../../../../hooks/useFetcher'; +import { history } from '../../../../../utils/history'; +import { + AgentConfigurationIntake, + AgentConfiguration +} from '../../../../../../common/agent_configuration/configuration_types'; +import { ServicePage } from './ServicePage/ServicePage'; +import { SettingsPage } from './SettingsPage/SettingsPage'; +import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; + +type PageStep = 'choose-service-step' | 'choose-settings-step' | 'review-step'; + +function getInitialNewConfig( + existingConfig: AgentConfigurationIntake | undefined +) { + return { + agent_name: existingConfig?.agent_name, + service: existingConfig?.service || {}, + settings: existingConfig?.settings || {} + }; +} + +function setPage(pageStep: PageStep) { + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + pageStep + }) + }); +} + +function getUnsavedChanges({ + newConfig, + existingConfig +}: { + newConfig: AgentConfigurationIntake; + existingConfig?: AgentConfigurationIntake; +}) { + return Object.fromEntries( + Object.entries(newConfig.settings).filter(([key, value]) => { + const existingValue = existingConfig?.settings?.[key]; + + // don't highlight changes that were added and removed + if (value === '' && existingValue == null) { + return false; + } + + return existingValue !== value; + }) + ); +} + +export function AgentConfigurationCreateEdit({ + pageStep, + existingConfigResult +}: { + pageStep: PageStep; + existingConfigResult?: FetcherResult<AgentConfiguration>; +}) { + const existingConfig = existingConfigResult?.data; + const isEditMode = Boolean(existingConfigResult); + const [newConfig, setNewConfig] = useState<AgentConfigurationIntake>( + getInitialNewConfig(existingConfig) + ); + + const resetSettings = useCallback(() => { + setNewConfig(_newConfig => ({ + ..._newConfig, + settings: existingConfig?.settings || {} + })); + }, [existingConfig]); + + // update newConfig when existingConfig has loaded + useEffect(() => { + setNewConfig(getInitialNewConfig(existingConfig)); + }, [existingConfig]); + + useEffect(() => { + // the user tried to edit the service of an existing config + if (pageStep === 'choose-service-step' && isEditMode) { + setPage('choose-settings-step'); + } + + // the user skipped the first step (select service) + if ( + pageStep === 'choose-settings-step' && + !isEditMode && + isEmpty(newConfig.service) + ) { + setPage('choose-service-step'); + } + }, [isEditMode, newConfig, pageStep]); + + const unsavedChanges = getUnsavedChanges({ newConfig, existingConfig }); + + return ( + <> + <EuiTitle> + <h2> + {isEditMode + ? i18n.translate('xpack.apm.agentConfig.editConfigTitle', { + defaultMessage: 'Edit configuration' + }) + : i18n.translate('xpack.apm.agentConfig.createConfigTitle', { + defaultMessage: 'Create configuration' + })} + </h2> + </EuiTitle> + + <EuiText size="s"> + {i18n.translate('xpack.apm.agentConfig.newConfig.description', { + defaultMessage: `This allows you to fine-tune your agent configuration directly in + Kibana. Best of all, changes are automatically propagated to your APM + agents so there’s no need to redeploy.` + })} + </EuiText> + + <EuiSpacer size="m" /> + + {pageStep === 'choose-service-step' && ( + <ServicePage + newConfig={newConfig} + setNewConfig={setNewConfig} + onClickNext={() => setPage('choose-settings-step')} + /> + )} + + {pageStep === 'choose-settings-step' && ( + <SettingsPage + status={existingConfigResult?.status} + unsavedChanges={unsavedChanges} + onClickEdit={() => setPage('choose-service-step')} + newConfig={newConfig} + setNewConfig={setNewConfig} + resetSettings={resetSettings} + isEditMode={isEditMode} + /> + )} + + {/* + TODO: Add review step + {pageStep === 'review-step' && <div>Review will be here </div>} + */} + </> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx rename to x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 267aaddc93f76..6a1a472562305 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -9,8 +9,8 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { NotificationsStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AgentConfigurationListAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/settings/agent_configuration/list_configurations'; -import { getOptionLabel } from '../../../../../../../../../plugins/apm/common/agent_configuration/all_option'; +import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; +import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; import { callApmApi } from '../../../../../services/rest/createCallApmApi'; import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx new file mode 100644 index 0000000000000..9eaa7786baca0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -0,0 +1,221 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiEmptyPrompt, + EuiButton, + EuiButtonEmpty, + EuiHealth, + EuiToolTip, + EuiButtonIcon +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { ITableColumn, ManagedTable } from '../../../../shared/ManagedTable'; +import { LoadingStatePrompt } from '../../../../shared/LoadingStatePrompt'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AgentConfigurationListAPIResponse } from '../../../../../../server/lib/settings/agent_configuration/list_configurations'; +import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; +import { px, units } from '../../../../../style/variables'; +import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; +import { + createAgentConfigurationHref, + editAgentConfigurationHref +} from '../../../../shared/Links/apm/agentConfigurationLinks'; +import { ConfirmDeleteModal } from './ConfirmDeleteModal'; + +type Config = AgentConfigurationListAPIResponse[0]; + +export function AgentConfigurationList({ + status, + data, + refetch +}: { + status: FETCH_STATUS; + data: Config[]; + refetch: () => void; +}) { + const [configToBeDeleted, setConfigToBeDeleted] = useState<Config | null>( + null + ); + + const emptyStatePrompt = ( + <EuiEmptyPrompt + iconType="controlsHorizontal" + title={ + <h2> + {i18n.translate( + 'xpack.apm.agentConfig.configTable.emptyPromptTitle', + { defaultMessage: 'No configurations found.' } + )} + </h2> + } + body={ + <> + <p> + {i18n.translate( + 'xpack.apm.agentConfig.configTable.emptyPromptText', + { + defaultMessage: + "Let's change that! You can fine-tune agent configuration directly from Kibana without having to redeploy. Get started by creating your first configuration." + } + )} + </p> + </> + } + actions={ + <EuiButton color="primary" fill href={createAgentConfigurationHref()}> + {i18n.translate( + 'xpack.apm.agentConfig.configTable.createConfigButtonLabel', + { defaultMessage: 'Create configuration' } + )} + </EuiButton> + } + /> + ); + + const failurePrompt = ( + <EuiEmptyPrompt + iconType="alert" + body={ + <> + <p> + {i18n.translate( + 'xpack.apm.agentConfig.configTable.configTable.failurePromptText', + { + defaultMessage: + 'The list of agent configurations could not be fetched. Your user may not have the sufficient permissions.' + } + )} + </p> + </> + } + /> + ); + + if (status === FETCH_STATUS.FAILURE) { + return failurePrompt; + } + + if (status === FETCH_STATUS.SUCCESS && isEmpty(data)) { + return emptyStatePrompt; + } + + const columns: Array<ITableColumn<Config>> = [ + { + field: 'applied_by_agent', + align: 'center', + width: px(units.double), + name: '', + sortable: true, + render: (isApplied: boolean) => ( + <EuiToolTip + content={ + isApplied + ? i18n.translate( + 'xpack.apm.agentConfig.configTable.appliedTooltipMessage', + { defaultMessage: 'Applied by at least one agent' } + ) + : i18n.translate( + 'xpack.apm.agentConfig.configTable.notAppliedTooltipMessage', + { defaultMessage: 'Not yet applied by any agents' } + ) + } + > + <EuiHealth color={isApplied ? 'success' : theme.euiColorLightShade} /> + </EuiToolTip> + ) + }, + { + field: 'service.name', + name: i18n.translate( + 'xpack.apm.agentConfig.configTable.serviceNameColumnLabel', + { defaultMessage: 'Service name' } + ), + sortable: true, + render: (_, config: Config) => ( + <EuiButtonEmpty + flush="left" + size="s" + color="primary" + href={editAgentConfigurationHref(config.service)} + > + {getOptionLabel(config.service.name)} + </EuiButtonEmpty> + ) + }, + { + field: 'service.environment', + name: i18n.translate( + 'xpack.apm.agentConfig.configTable.environmentColumnLabel', + { defaultMessage: 'Service environment' } + ), + sortable: true, + render: (environment: string) => getOptionLabel(environment) + }, + { + align: 'right', + field: '@timestamp', + name: i18n.translate( + 'xpack.apm.agentConfig.configTable.lastUpdatedColumnLabel', + { defaultMessage: 'Last updated' } + ), + sortable: true, + render: (value: number) => ( + <TimestampTooltip time={value} timeUnit="minutes" /> + ) + }, + { + width: px(units.double), + name: '', + render: (config: Config) => ( + <EuiButtonIcon + aria-label="Edit" + iconType="pencil" + href={editAgentConfigurationHref(config.service)} + /> + ) + }, + { + width: px(units.double), + name: '', + render: (config: Config) => ( + <EuiButtonIcon + aria-label="Delete" + iconType="trash" + onClick={() => setConfigToBeDeleted(config)} + /> + ) + } + ]; + + return ( + <> + {configToBeDeleted && ( + <ConfirmDeleteModal + config={configToBeDeleted} + onCancel={() => setConfigToBeDeleted(null)} + onConfirm={() => { + setConfigToBeDeleted(null); + refetch(); + }} + /> + )} + + <ManagedTable + noItemsMessage={<LoadingStatePrompt />} + columns={columns} + items={data} + initialSortField="service.name" + initialSortDirection="asc" + initialPageSize={20} + /> + </> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx new file mode 100644 index 0000000000000..4349e542449cc --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiButton +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { AgentConfigurationList } from './List'; +import { useTrackPageview } from '../../../../../../observability/public'; +import { createAgentConfigurationHref } from '../../../shared/Links/apm/agentConfigurationLinks'; + +export function AgentConfigurations() { + const { refetch, data = [], status } = useFetcher( + callApmApi => + callApmApi({ pathname: '/api/apm/settings/agent-configuration' }), + [], + { preservePreviousData: false } + ); + + useTrackPageview({ app: 'apm', path: 'agent_configuration' }); + useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); + + const hasConfigurations = !isEmpty(data); + + return ( + <> + <EuiPanel> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle> + <h2> + {i18n.translate( + 'xpack.apm.agentConfig.configurationsPanelTitle', + { defaultMessage: 'Agent remote configuration' } + )} + </h2> + </EuiTitle> + </EuiFlexItem> + + {hasConfigurations ? <CreateConfigurationButton /> : null} + </EuiFlexGroup> + + <EuiSpacer size="m" /> + + <AgentConfigurationList status={status} data={data} refetch={refetch} /> + </EuiPanel> + </> + ); +} + +function CreateConfigurationButton() { + const href = createAgentConfigurationHref(); + return ( + <EuiFlexItem> + <EuiFlexGroup alignItems="center" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButton color="primary" fill iconType="plusInCircle" href={href}> + {i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', { + defaultMessage: 'Create configuration' + })} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx new file mode 100644 index 0000000000000..b03960861e0ad --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { ApmIndices } from '.'; +import * as hooks from '../../../../hooks/useFetcher'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; + +describe('ApmIndices', () => { + it('should not get stuck in infinite loop', () => { + const spy = spyOn(hooks, 'useFetcher').and.returnValue({ + data: undefined, + status: 'loading' + }); + const { getByText } = render( + <MockApmPluginContextWrapper> + <ApmIndices /> + </MockApmPluginContextWrapper> + ); + + expect(getByText('Indices')).toMatchInlineSnapshot(` + <h2 + class="euiTitle euiTitle--medium" + > + Indices + </h2> + `); + + expect(spy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CreateCustomLinkButton.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/DeleteButton.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/Documentation.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx index fb8ffe6722c87..9c244e3cde411 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FiltersSection.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { Filter, FilterKey -} from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +} from '../../../../../../../common/custom_link/custom_link_types'; import { DEFAULT_OPTION, FILTER_SELECT_OPTIONS, diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/FlyoutFooter.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx index 8edfb176a1af8..8fed838a48261 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkPreview.tsx @@ -17,8 +17,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; -import { Filter } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; import { replaceTemplateVariables, convertFiltersToQuery } from './helper'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx index 630f7148ad408..210033888d90c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/LinkSection.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { CustomLink } from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink } from '../../../../../../../common/custom_link/custom_link_types'; import { Documentation } from './Documentation'; interface InputField { diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts new file mode 100644 index 0000000000000..49e381aab675d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.test.ts @@ -0,0 +1,147 @@ +/* + * 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 { + getSelectOptions, + replaceTemplateVariables +} from '../CustomLinkFlyout/helper'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; + +describe('Custom link helper', () => { + describe('getSelectOptions', () => { + it('returns all available options when no filters were selected', () => { + expect( + getSelectOptions( + [ + { key: '', value: '' }, + { key: '', value: '' }, + { key: '', value: '' }, + { key: '', value: '' } + ], + '' + ) + ).toEqual([ + { value: 'DEFAULT', text: 'Select field...' }, + { value: 'service.name', text: 'service.name' }, + { value: 'service.environment', text: 'service.environment' }, + { value: 'transaction.type', text: 'transaction.type' }, + { value: 'transaction.name', text: 'transaction.name' } + ]); + }); + it('removes item added in another filter', () => { + expect( + getSelectOptions( + [ + { key: 'service.name', value: 'foo' }, + { key: '', value: '' }, + { key: '', value: '' }, + { key: '', value: '' } + ], + '' + ) + ).toEqual([ + { value: 'DEFAULT', text: 'Select field...' }, + { value: 'service.environment', text: 'service.environment' }, + { value: 'transaction.type', text: 'transaction.type' }, + { value: 'transaction.name', text: 'transaction.name' } + ]); + }); + it('removes item added in another filter but keep the current selected', () => { + expect( + getSelectOptions( + [ + { key: 'service.name', value: 'foo' }, + { key: 'transaction.name', value: 'bar' }, + { key: '', value: '' }, + { key: '', value: '' } + ], + 'transaction.name' + ) + ).toEqual([ + { value: 'DEFAULT', text: 'Select field...' }, + { value: 'service.environment', text: 'service.environment' }, + { value: 'transaction.type', text: 'transaction.type' }, + { value: 'transaction.name', text: 'transaction.name' } + ]); + }); + it('returns empty when all option were selected', () => { + expect( + getSelectOptions( + [ + { key: 'service.name', value: 'foo' }, + { key: 'transaction.name', value: 'bar' }, + { key: 'service.environment', value: 'baz' }, + { key: 'transaction.type', value: 'qux' } + ], + '' + ) + ).toEqual([{ value: 'DEFAULT', text: 'Select field...' }]); + }); + }); + + describe('replaceTemplateVariables', () => { + const transaction = ({ + service: { name: 'foo' }, + trace: { id: '123' } + } as unknown) as Transaction; + + it('replaces template variables', () => { + expect( + replaceTemplateVariables( + 'https://elastic.co?service.name={{service.name}}&trace.id={{trace.id}}', + transaction + ) + ).toEqual({ + error: undefined, + formattedUrl: 'https://elastic.co?service.name=foo&trace.id=123' + }); + }); + + it('returns error when transaction is not defined', () => { + const expectedResult = { + error: + "We couldn't find a matching transaction document based on the defined filters.", + formattedUrl: 'https://elastic.co?service.name=&trace.id=' + }; + expect( + replaceTemplateVariables( + 'https://elastic.co?service.name={{service.name}}&trace.id={{trace.id}}' + ) + ).toEqual(expectedResult); + expect( + replaceTemplateVariables( + 'https://elastic.co?service.name={{service.name}}&trace.id={{trace.id}}', + ({} as unknown) as Transaction + ) + ).toEqual(expectedResult); + }); + + it('returns error when could not replace variables', () => { + expect( + replaceTemplateVariables( + 'https://elastic.co?service.name={{service.nam}}&trace.id={{trace.i}}', + transaction + ) + ).toEqual({ + error: + "We couldn't find a value match for {{service.nam}}, {{trace.i}} in the example transaction document.", + formattedUrl: 'https://elastic.co?service.name=&trace.id=' + }); + }); + + it('returns error when variable is invalid', () => { + expect( + replaceTemplateVariables( + 'https://elastic.co?service.name={{service.name}', + transaction + ) + ).toEqual({ + error: + "We couldn't find an example transaction document due to invalid variable(s) defined.", + formattedUrl: 'https://elastic.co?service.name={{service.name}' + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts new file mode 100644 index 0000000000000..8c35b8fe77506 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/helper.ts @@ -0,0 +1,120 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import Mustache from 'mustache'; +import { isEmpty, get } from 'lodash'; +import { FILTER_OPTIONS } from '../../../../../../../common/custom_link/custom_link_filter_options'; +import { + Filter, + FilterKey +} from '../../../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; + +interface FilterSelectOption { + value: 'DEFAULT' | FilterKey; + text: string; +} + +export const DEFAULT_OPTION: FilterSelectOption = { + value: 'DEFAULT', + text: i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyOut.filters.defaultOption', + { defaultMessage: 'Select field...' } + ) +}; + +export const FILTER_SELECT_OPTIONS: FilterSelectOption[] = [ + DEFAULT_OPTION, + ...FILTER_OPTIONS.map(filter => ({ + value: filter, + text: filter + })) +]; + +/** + * Returns the options available, removing filters already added, but keeping the selected filter. + * + * @param filters + * @param selectedKey + */ +export const getSelectOptions = ( + filters: Filter[], + selectedKey: Filter['key'] +) => { + return FILTER_SELECT_OPTIONS.filter( + ({ value }) => + !filters.some(({ key }) => key === value && key !== selectedKey) + ); +}; + +const getInvalidTemplateVariables = ( + template: string, + transaction: Transaction +) => { + return (Mustache.parse(template) as Array<[string, string]>) + .filter(([type]) => type === 'name') + .map(([, value]) => value) + .filter(templateVar => get(transaction, templateVar) == null); +}; + +const validateUrl = (url: string, transaction?: Transaction) => { + if (!transaction || isEmpty(transaction)) { + return i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.preview.transaction.notFound', + { + defaultMessage: + "We couldn't find a matching transaction document based on the defined filters." + } + ); + } + try { + const invalidVariables = getInvalidTemplateVariables(url, transaction); + if (!isEmpty(invalidVariables)) { + return i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.preview.contextVariable.noMatch', + { + defaultMessage: + "We couldn't find a value match for {variables} in the example transaction document.", + values: { + variables: invalidVariables + .map(variable => `{{${variable}}}`) + .join(', ') + } + } + ); + } + } catch (e) { + return i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.preview.contextVariable.invalid', + { + defaultMessage: + "We couldn't find an example transaction document due to invalid variable(s) defined." + } + ); + } +}; + +export const replaceTemplateVariables = ( + url: string, + transaction?: Transaction +) => { + const error = validateUrl(url, transaction); + try { + return { formattedUrl: Mustache.render(url, transaction), error }; + } catch (e) { + // errors will be caught on validateUrl function + return { formattedUrl: url, error }; + } +}; + +export const convertFiltersToQuery = (filters: Filter[]) => { + return filters.reduce((acc: Record<string, string>, { key, value }) => { + if (key && value) { + acc[key] = value; + } + return acc; + }, {}); +}; diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx new file mode 100644 index 0000000000000..150147d9af405 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/index.tsx @@ -0,0 +1,142 @@ +/* + * 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 { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiPortal, + EuiSpacer, + EuiText, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState } from 'react'; +import { Filter } from '../../../../../../../common/custom_link/custom_link_types'; +import { useApmPluginContext } from '../../../../../../hooks/useApmPluginContext'; +import { FiltersSection } from './FiltersSection'; +import { FlyoutFooter } from './FlyoutFooter'; +import { LinkSection } from './LinkSection'; +import { saveCustomLink } from './saveCustomLink'; +import { LinkPreview } from './LinkPreview'; +import { Documentation } from './Documentation'; + +interface Props { + onClose: () => void; + onSave: () => void; + onDelete: () => void; + defaults?: { + url?: string; + label?: string; + filters?: Filter[]; + }; + customLinkId?: string; +} + +const filtersEmptyState: Filter[] = [{ key: '', value: '' }]; + +export const CustomLinkFlyout = ({ + onClose, + onSave, + onDelete, + defaults, + customLinkId +}: Props) => { + const { toasts } = useApmPluginContext().core.notifications; + const [isSaving, setIsSaving] = useState(false); + + const [label, setLabel] = useState(defaults?.label || ''); + const [url, setUrl] = useState(defaults?.url || ''); + const [filters, setFilters] = useState( + defaults?.filters?.length ? defaults.filters : filtersEmptyState + ); + + const isFormValid = !!label && !!url; + + const onSubmit = async ( + event: + | React.FormEvent<HTMLFormElement> + | React.MouseEvent<HTMLButtonElement> + ) => { + event.preventDefault(); + setIsSaving(true); + await saveCustomLink({ + id: customLinkId, + label, + url, + filters, + toasts + }); + setIsSaving(false); + onSave(); + }; + + return ( + <EuiPortal> + <form onSubmit={onSubmit}> + <EuiFlyout ownFocus onClose={onClose} size="m"> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="s"> + <h2> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.title', + { + defaultMessage: 'Create link' + } + )} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiText> + <p> + {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.label', + { + defaultMessage: + 'Links will be available in the context of transaction details throughout the APM app. You can create an unlimited number of links. You can refer to dynamic variables by using any of the transaction metadata to fill in your URLs. More information, including examples, are available in the' + } + )}{' '} + <Documentation + label={i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.label.doc', + { + defaultMessage: 'documentation.' + } + )} + /> + </p> + </EuiText> + + <EuiSpacer size="l" /> + + <LinkSection + label={label} + onChangeLabel={setLabel} + url={url} + onChangeUrl={setUrl} + /> + + <EuiSpacer size="l" /> + + <FiltersSection filters={filters} onChangeFilters={setFilters} /> + + <EuiSpacer size="l" /> + + <LinkPreview label={label} url={url} filters={filters} /> + </EuiFlyoutBody> + + <FlyoutFooter + isSaveButtonEnabled={isFormValid} + onClose={onClose} + isSaving={isSaving} + onDelete={onDelete} + customLinkId={customLinkId} + /> + </EuiFlyout> + </form> + </EuiPortal> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts index 9cbaf16320a6b..685b3ab022950 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkFlyout/saveCustomLink.ts @@ -9,7 +9,7 @@ import { NotificationsStart } from 'kibana/public'; import { Filter, CustomLink -} from '../../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +} from '../../../../../../../common/custom_link/custom_link_types'; import { callApmApi } from '../../../../../../services/rest/createCallApmApi'; export async function saveCustomLink({ diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx index 68e6ee52af0b0..d68fb757e53d1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/CustomLinkTable.tsx @@ -13,7 +13,7 @@ import { EuiSpacer } from '@elastic/eui'; import { isEmpty } from 'lodash'; -import { CustomLink } from '../../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { units, px } from '../../../../../style/variables'; import { ManagedTable } from '../../../../shared/ManagedTable'; import { TimestampTooltip } from '../../../../shared/TimestampTooltip'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/EmptyPrompt.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/Title.tsx diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx new file mode 100644 index 0000000000000..32a08f5ffaf7c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -0,0 +1,357 @@ +/* + * 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 { fireEvent, render, wait, RenderResult } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import * as apmApi from '../../../../../services/rest/createCallApmApi'; +import { License } from '../../../../../../../licensing/common/license'; +import * as hooks from '../../../../../hooks/useFetcher'; +import { LicenseContext } from '../../../../../context/LicenseContext'; +import { CustomLinkOverview } from '.'; +import { + expectTextsInDocument, + expectTextsNotInDocument +} from '../../../../../utils/testHelpers'; +import * as saveCustomLink from './CustomLinkFlyout/saveCustomLink'; +import { MockApmPluginContextWrapper } from '../../../../../context/ApmPluginContext/MockApmPluginContext'; + +const data = [ + { + id: '1', + label: 'label 1', + url: 'url 1', + 'service.name': 'opbeans-java' + }, + { + id: '2', + label: 'label 2', + url: 'url 2', + 'transaction.type': 'request' + } +]; + +describe('CustomLink', () => { + let callApmApiSpy: jasmine.Spy; + beforeAll(() => { + callApmApiSpy = spyOn(apmApi, 'callApmApi').and.returnValue({}); + }); + afterAll(() => { + jest.resetAllMocks(); + }); + const goldLicense = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'gold', + status: 'active', + type: 'gold', + uid: '1' + } + }); + describe('empty prompt', () => { + beforeAll(() => { + spyOn(hooks, 'useFetcher').and.returnValue({ + data: [], + status: 'success' + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + it('shows when no link is available', () => { + const component = render( + <LicenseContext.Provider value={goldLicense}> + <CustomLinkOverview /> + </LicenseContext.Provider> + ); + expectTextsInDocument(component, ['No links found.']); + }); + }); + + describe('overview', () => { + beforeAll(() => { + spyOn(hooks, 'useFetcher').and.returnValue({ + data, + status: 'success' + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('shows a table with all custom link', () => { + const component = render( + <LicenseContext.Provider value={goldLicense}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expectTextsInDocument(component, [ + 'label 1', + 'url 1', + 'label 2', + 'url 2' + ]); + }); + + it('checks if create custom link button is available and working', async () => { + const { queryByText, getByText } = render( + <LicenseContext.Provider value={goldLicense}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expect(queryByText('Create link')).not.toBeInTheDocument(); + act(() => { + fireEvent.click(getByText('Create custom link')); + }); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); + expect(queryByText('Create link')).toBeInTheDocument(); + }); + }); + + describe('Flyout', () => { + const refetch = jest.fn(); + let saveCustomLinkSpy: Function; + beforeAll(() => { + saveCustomLinkSpy = spyOn(saveCustomLink, 'saveCustomLink'); + spyOn(hooks, 'useFetcher').and.returnValue({ + data, + status: 'success', + refetch + }); + }); + afterEach(() => { + jest.resetAllMocks(); + }); + + const openFlyout = async () => { + const component = render( + <LicenseContext.Provider value={goldLicense}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expect(component.queryByText('Create link')).not.toBeInTheDocument(); + act(() => { + fireEvent.click(component.getByText('Create custom link')); + }); + await wait(() => + expect(component.queryByText('Create link')).toBeInTheDocument() + ); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); + return component; + }; + + it('creates a custom link', async () => { + const component = await openFlyout(); + const labelInput = component.getByTestId('label'); + act(() => { + fireEvent.change(labelInput, { + target: { value: 'foo' } + }); + }); + const urlInput = component.getByTestId('url'); + act(() => { + fireEvent.change(urlInput, { + target: { value: 'bar' } + }); + }); + await act(async () => { + await wait(() => fireEvent.submit(component.getByText('Save'))); + }); + expect(saveCustomLinkSpy).toHaveBeenCalledTimes(1); + }); + + it('deletes a custom link', async () => { + const component = render( + <LicenseContext.Provider value={goldLicense}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expect(component.queryByText('Create link')).not.toBeInTheDocument(); + const editButtons = component.getAllByLabelText('Edit'); + expect(editButtons.length).toEqual(2); + act(() => { + fireEvent.click(editButtons[0]); + }); + expect(component.queryByText('Create link')).toBeInTheDocument(); + await act(async () => { + await wait(() => fireEvent.click(component.getByText('Delete'))); + }); + expect(callApmApiSpy).toHaveBeenCalled(); + expect(refetch).toHaveBeenCalled(); + }); + + describe('Filters', () => { + const addFilterField = (component: RenderResult, amount: number) => { + for (let i = 1; i <= amount; i++) { + fireEvent.click(component.getByText('Add another filter')); + } + }; + it('checks if add filter button is disabled after all elements have been added', async () => { + const component = await openFlyout(); + expect(component.getAllByText('service.name').length).toEqual(1); + addFilterField(component, 1); + expect(component.getAllByText('service.name').length).toEqual(2); + addFilterField(component, 2); + expect(component.getAllByText('service.name').length).toEqual(4); + // After 4 items, the button is disabled + addFilterField(component, 2); + expect(component.getAllByText('service.name').length).toEqual(4); + }); + it('removes items already selected', async () => { + const component = await openFlyout(); + + const addFieldAndCheck = ( + fieldName: string, + selectValue: string, + addNewFilter: boolean, + optionsExpected: string[] + ) => { + if (addNewFilter) { + addFilterField(component, 1); + } + const field = component.getByTestId(fieldName) as HTMLSelectElement; + const optionsAvailable = Object.values(field) + .map(option => (option as HTMLOptionElement).text) + .filter(option => option); + + act(() => { + fireEvent.change(field, { + target: { value: selectValue } + }); + }); + expect(field.value).toEqual(selectValue); + expect(optionsAvailable).toEqual(optionsExpected); + }; + + addFieldAndCheck('filter-0', 'transaction.name', false, [ + 'Select field...', + 'service.name', + 'service.environment', + 'transaction.type', + 'transaction.name' + ]); + + addFieldAndCheck('filter-1', 'service.name', true, [ + 'Select field...', + 'service.name', + 'service.environment', + 'transaction.type' + ]); + + addFieldAndCheck('filter-2', 'transaction.type', true, [ + 'Select field...', + 'service.environment', + 'transaction.type' + ]); + + addFieldAndCheck('filter-3', 'service.environment', true, [ + 'Select field...', + 'service.environment' + ]); + }); + }); + }); + + describe('invalid license', () => { + beforeAll(() => { + spyOn(hooks, 'useFetcher').and.returnValue({ + data: [], + status: 'success' + }); + }); + it('shows license prompt when user has a basic license', () => { + const license = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'basic', + status: 'active', + type: 'basic', + uid: '1' + } + }); + const component = render( + <LicenseContext.Provider value={license}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expectTextsInDocument(component, ['Start free 30-day trial']); + }); + it('shows license prompt when user has an invalid gold license', () => { + const license = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'gold', + status: 'invalid', + type: 'gold', + uid: '1' + } + }); + const component = render( + <LicenseContext.Provider value={license}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expectTextsInDocument(component, ['Start free 30-day trial']); + }); + it('shows license prompt when user has an invalid trial license', () => { + const license = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'trial', + status: 'invalid', + type: 'trial', + uid: '1' + } + }); + const component = render( + <LicenseContext.Provider value={license}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expectTextsInDocument(component, ['Start free 30-day trial']); + }); + it('doesnt show license prompt when user has a trial license', () => { + const license = new License({ + signature: 'test signature', + license: { + expiryDateInMillis: 0, + mode: 'trial', + status: 'active', + type: 'trial', + uid: '1' + } + }); + const component = render( + <LicenseContext.Provider value={license}> + <MockApmPluginContextWrapper> + <CustomLinkOverview /> + </MockApmPluginContextWrapper> + </LicenseContext.Provider> + ); + expectTextsNotInDocument(component, ['Start free 30-day trial']); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx new file mode 100644 index 0000000000000..b94ce513bc210 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -0,0 +1,110 @@ +/* + * 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 { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; +import { useLicense } from '../../../../../hooks/useLicense'; +import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; +import { CustomLinkFlyout } from './CustomLinkFlyout'; +import { CustomLinkTable } from './CustomLinkTable'; +import { EmptyPrompt } from './EmptyPrompt'; +import { Title } from './Title'; +import { CreateCustomLinkButton } from './CreateCustomLinkButton'; +import { LicensePrompt } from '../../../../shared/LicensePrompt'; + +export const CustomLinkOverview = () => { + const license = useLicense(); + const hasValidLicense = license?.isActive && license?.hasAtLeast('gold'); + + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const [customLinkSelected, setCustomLinkSelected] = useState< + CustomLink | undefined + >(); + + const { data: customLinks, status, refetch } = useFetcher( + callApmApi => callApmApi({ pathname: '/api/apm/settings/custom_links' }), + [] + ); + + useEffect(() => { + if (customLinkSelected) { + setIsFlyoutOpen(true); + } + }, [customLinkSelected]); + + const onCloseFlyout = () => { + setCustomLinkSelected(undefined); + setIsFlyoutOpen(false); + }; + + const onCreateCustomLinkClick = () => { + setIsFlyoutOpen(true); + }; + + const showEmptyPrompt = + status === FETCH_STATUS.SUCCESS && isEmpty(customLinks); + + return ( + <> + {isFlyoutOpen && ( + <CustomLinkFlyout + onClose={onCloseFlyout} + defaults={customLinkSelected} + customLinkId={customLinkSelected?.id} + onSave={() => { + onCloseFlyout(); + refetch(); + }} + onDelete={() => { + onCloseFlyout(); + refetch(); + }} + /> + )} + <EuiPanel> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <Title /> + </EuiFlexItem> + {hasValidLicense && !showEmptyPrompt && ( + <EuiFlexItem> + <EuiFlexGroup alignItems="center" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <CreateCustomLinkButton onClick={onCreateCustomLinkClick} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + )} + </EuiFlexGroup> + + <EuiSpacer size="m" /> + {hasValidLicense ? ( + showEmptyPrompt ? ( + <EmptyPrompt onCreateCustomLinkClick={onCreateCustomLinkClick} /> + ) : ( + <CustomLinkTable + items={customLinks} + onCustomLinkSelected={setCustomLinkSelected} + /> + ) + ) : ( + <LicensePrompt + text={i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.license.text', + { + defaultMessage: + "To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services." + } + )} + /> + )} + </EuiPanel> + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx rename to x-pack/plugins/apm/public/components/app/Settings/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx rename to x-pack/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx new file mode 100644 index 0000000000000..04d830f4649d4 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -0,0 +1,96 @@ +/* + * 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 { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import styled from 'styled-components'; +import url from 'url'; +import { TRACE_ID } from '../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../hooks/useUrlParams'; + +const CentralizedContainer = styled.div` + height: 100%; + display: flex; +`; + +const redirectToTransactionDetailPage = ({ + transaction, + rangeFrom, + rangeTo +}: { + transaction: Transaction; + rangeFrom?: string; + rangeTo?: string; +}) => + url.format({ + pathname: `/services/${transaction.service.name}/transactions/view`, + query: { + traceId: transaction.trace.id, + transactionId: transaction.transaction.id, + transactionName: transaction.transaction.name, + transactionType: transaction.transaction.type, + rangeFrom, + rangeTo + } + }); + +const redirectToTracePage = ({ + traceId, + rangeFrom, + rangeTo +}: { + traceId: string; + rangeFrom?: string; + rangeTo?: string; +}) => + url.format({ + pathname: `/traces`, + query: { + kuery: encodeURIComponent(`${TRACE_ID} : "${traceId}"`), + rangeFrom, + rangeTo + } + }); + +export const TraceLink = () => { + const { urlParams } = useUrlParams(); + const { traceIdLink: traceId, rangeFrom, rangeTo } = urlParams; + + const { data = { transaction: null }, status } = useFetcher( + callApmApi => { + if (traceId) { + return callApmApi({ + pathname: '/api/apm/transaction/{traceId}', + params: { + path: { + traceId + } + } + }); + } + }, + [traceId] + ); + if (traceId && status === FETCH_STATUS.SUCCESS) { + const to = data.transaction + ? redirectToTransactionDetailPage({ + transaction: data.transaction, + rangeFrom, + rangeTo + }) + : redirectToTracePage({ traceId, rangeFrom, rangeTo }); + return <Redirect to={to} />; + } + + return ( + <CentralizedContainer> + <EuiEmptyPrompt iconType="apmTrace" title={<h2>Fetching trace...</h2>} /> + </CentralizedContainer> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx rename to x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx index 91f3051acf077..92d5a38cc11ca 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceOverview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ITransactionGroup } from '../../../../../../../plugins/apm/server/lib/transaction_groups/transform'; +import { ITransactionGroup } from '../../../../server/lib/transaction_groups/transform'; import { fontSizes, truncate } from '../../../style/variables'; import { convertTo } from '../../../utils/formatters'; import { EmptyMessage } from '../../shared/EmptyMessage'; diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx new file mode 100644 index 0000000000000..a7fa927f9e9b1 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -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 { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { TraceList } from './TraceList'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useTrackPageview } from '../../../../../observability/public'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; + +export function TraceOverview() { + const { urlParams, uiFilters } = useUrlParams(); + const { start, end } = urlParams; + const { status, data = [] } = useFetcher( + callApmApi => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/traces', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters) + } + } + }); + } + }, + [start, end, uiFilters] + ); + + useTrackPageview({ app: 'apm', path: 'traces_overview' }); + useTrackPageview({ app: 'apm', path: 'traces_overview', delay: 15000 }); + + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps<typeof LocalUIFilters> = { + filterNames: ['transactionResult', 'host', 'containerId', 'podName'], + projection: PROJECTION.TRACES + }; + + return config; + }, []); + + return ( + <> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <LocalUIFilters {...localUIFiltersConfig} showCount={false} /> + </EuiFlexItem> + <EuiFlexItem grow={7}> + <EuiPanel> + <TraceList + items={data} + isLoading={status === FETCH_STATUS.LOADING} + /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts index 08682fb3be842..7ad0a77505b9d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/__test__/distribution.test.ts @@ -6,7 +6,7 @@ import { getFormattedBuckets } from '../index'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IBucket } from '../../../../../../../../../plugins/apm/server/lib/transactions/distribution/get_buckets/transform'; +import { IBucket } from '../../../../../../server/lib/transactions/distribution/get_buckets/transform'; describe('Distribution', () => { it('getFormattedBuckets', () => { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx new file mode 100644 index 0000000000000..b7dbfbdbd7d7e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -0,0 +1,212 @@ +/* + * 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 { EuiIconTip, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import d3 from 'd3'; +import React, { FunctionComponent, useCallback } from 'react'; +import { isEmpty } from 'lodash'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { getDurationFormatter } from '../../../../utils/formatters'; +// @ts-ignore +import Histogram from '../../../shared/charts/Histogram'; +import { EmptyMessage } from '../../../shared/EmptyMessage'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { history } from '../../../../utils/history'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; + +interface IChartPoint { + samples: IBucket['samples']; + x0: number; + x: number; + y: number; + style: { + cursor: string; + }; +} + +export function getFormattedBuckets(buckets: IBucket[], bucketSize: number) { + if (!buckets) { + return []; + } + + return buckets.map( + ({ samples, count, key }): IChartPoint => { + return { + samples, + x0: key, + x: key + bucketSize, + y: count, + style: { + cursor: isEmpty(samples) ? 'default' : 'pointer' + } + }; + } + ); +} + +const getFormatYShort = (transactionType: string | undefined) => ( + t: number +) => { + return i18n.translate( + 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.unitShortLabel', + { + defaultMessage: + '{transCount} {transType, select, request {req.} other {trans.}}', + values: { + transCount: t, + transType: transactionType + } + } + ); +}; + +const getFormatYLong = (transactionType: string | undefined) => (t: number) => { + return transactionType === 'request' + ? i18n.translate( + 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel', + { + defaultMessage: + '{transCount, plural, =0 {# request} one {# request} other {# requests}}', + values: { + transCount: t + } + } + ) + : i18n.translate( + 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.transactionTypeUnitLongLabel', + { + defaultMessage: + '{transCount, plural, =0 {# transaction} one {# transaction} other {# transactions}}', + values: { + transCount: t + } + } + ); +}; + +interface Props { + distribution?: TransactionDistributionAPIResponse; + urlParams: IUrlParams; + isLoading: boolean; + bucketIndex: number; +} + +export const TransactionDistribution: FunctionComponent<Props> = ( + props: Props +) => { + const { + distribution, + urlParams: { transactionType }, + isLoading, + bucketIndex + } = props; + + const formatYShort = useCallback(getFormatYShort(transactionType), [ + transactionType + ]); + + const formatYLong = useCallback(getFormatYLong(transactionType), [ + transactionType + ]); + + // no data in response + if (!distribution || distribution.noHits) { + // only show loading state if there is no data - else show stale data until new data has loaded + if (isLoading) { + return <LoadingStatePrompt />; + } + + return ( + <EmptyMessage + heading={i18n.translate('xpack.apm.transactionDetails.notFoundLabel', { + defaultMessage: 'No transactions were found.' + })} + /> + ); + } + + const buckets = getFormattedBuckets( + distribution.buckets, + distribution.bucketSize + ); + + const xMax = d3.max(buckets, d => d.x) || 0; + const timeFormatter = getDurationFormatter(xMax); + + return ( + <div> + <EuiTitle size="xs"> + <h5> + {i18n.translate( + 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTitle', + { + defaultMessage: 'Transactions duration distribution' + } + )}{' '} + <EuiIconTip + title={i18n.translate( + 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingLabel', + { + defaultMessage: 'Sampling' + } + )} + content={i18n.translate( + 'xpack.apm.transactionDetails.transactionsDurationDistributionChartTooltip.samplingDescription', + { + defaultMessage: + "Each bucket will show a sample transaction. If there's no sample available, it's most likely because of the sampling limit set in the agent configuration." + } + )} + position="top" + /> + </h5> + </EuiTitle> + + <Histogram + buckets={buckets} + bucketSize={distribution.bucketSize} + bucketIndex={bucketIndex} + onClick={(bucket: IChartPoint) => { + if (!isEmpty(bucket.samples)) { + const sample = bucket.samples[0]; + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + transactionId: sample.transactionId, + traceId: sample.traceId + }) + }); + } + }} + formatX={(time: number) => timeFormatter(time).formatted} + formatYShort={formatYShort} + formatYLong={formatYLong} + verticalLineHover={(bucket: IChartPoint) => isEmpty(bucket.samples)} + backgroundHover={(bucket: IChartPoint) => !isEmpty(bucket.samples)} + tooltipHeader={(bucket: IChartPoint) => { + const xFormatted = timeFormatter(bucket.x); + const x0Formatted = timeFormatter(bucket.x0); + return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`; + }} + tooltipFooter={(bucket: IChartPoint) => + isEmpty(bucket.samples) && + i18n.translate( + 'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip', + { + defaultMessage: 'No sample available for this bucket' + } + ) + } + /> + </div> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/ErrorCount.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx index 4e105957f5f9d..1db8e02e38692 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/MaybeViewTraceLink.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Transaction as ITransaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/transaction'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/PercentOfParent.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx index 9026dd90ddceb..27e0584c696c1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/TransactionTabs.tsx @@ -8,7 +8,7 @@ import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Location } from 'history'; import React from 'react'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; import { history } from '../../../../utils/history'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts index 030729522f35e..ae908b25cc615 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_agent_marks.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { getAgentMarks } from '../get_agent_marks'; describe('getAgentMarks', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts index 1dcb1598662c9..2bc64e30b4f7e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks.ts @@ -5,7 +5,7 @@ */ import { sortBy } from 'lodash'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { Mark } from '.'; // Extends Mark without adding new properties to it. diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts similarity index 89% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts index a9694efcbcae7..ad54cec5c26a7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { isEmpty } from 'lodash'; -import { ErrorRaw } from '../../../../../../../../../../plugins/apm/typings/es_schemas/raw/error_raw'; +import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/error_raw'; import { IWaterfallError, IServiceColors diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/index.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx index 6e58dbc5b6ea3..bbc457450e475 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx @@ -9,8 +9,8 @@ import React from 'react'; import { SERVICE_NAME, TRANSACTION_NAME -} from '../../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +} from '../../../../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction'; import { TransactionDetailLink } from '../../../../../shared/Links/apm/TransactionDetailLink'; import { StickyProperties } from '../../../../../shared/StickyProperties'; import { TransactionOverviewLink } from '../../../../../shared/Links/apm/TransactionOverviewLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index 6200d5f098ad5..7a08a84bf30ba 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -18,7 +18,7 @@ import SyntaxHighlighter, { // @ts-ignore import { xcode } from 'react-syntax-highlighter/dist/styles'; import styled from 'styled-components'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { borderRadius, fontFamilyCode, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx index 438e88df3351d..28564481074fa 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx @@ -17,7 +17,7 @@ import { unit, units } from '../../../../../../../style/variables'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; const ContextUrl = styled.div` padding: ${px(units.half)} ${px(unit)}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx similarity index 86% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx index 621497a0b22e0..d49959c5cbffb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx @@ -6,14 +6,14 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { SPAN_NAME, TRANSACTION_NAME, SERVICE_NAME -} from '../../../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../../../../../plugins/apm/common/i18n'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +} from '../../../../../../../../common/elasticsearch_fieldnames'; +import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { StickyProperties } from '../../../../../../shared/StickyProperties'; import { TransactionOverviewLink } from '../../../../../../shared/Links/apm/TransactionOverviewLink'; import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx new file mode 100644 index 0000000000000..1da22516629f2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -0,0 +1,239 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiHorizontalRule, + EuiPortal, + EuiSpacer, + EuiTabbedContent, + EuiTitle, + EuiBadge, + EuiToolTip +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import styled from 'styled-components'; +import { px, units } from '../../../../../../../style/variables'; +import { Summary } from '../../../../../../shared/Summary'; +import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; +import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; +import { DiscoverSpanLink } from '../../../../../../shared/Links/DiscoverLinks/DiscoverSpanLink'; +import { Stacktrace } from '../../../../../../shared/Stacktrace'; +import { ResponsiveFlyout } from '../ResponsiveFlyout'; +import { DatabaseContext } from './DatabaseContext'; +import { StickySpanProperties } from './StickySpanProperties'; +import { HttpInfoSummaryItem } from '../../../../../../shared/Summary/HttpInfoSummaryItem'; +import { SpanMetadata } from '../../../../../../shared/MetadataTable/SpanMetadata'; +import { SyncBadge } from '../SyncBadge'; + +function formatType(type: string) { + switch (type) { + case 'db': + return 'DB'; + case 'hard-navigation': + return i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel', + { + defaultMessage: 'Navigation timing' + } + ); + default: + return type; + } +} + +function formatSubtype(subtype: string | undefined) { + switch (subtype) { + case 'mysql': + return 'MySQL'; + default: + return subtype; + } +} + +function getSpanTypes(span: Span) { + const { type, subtype, action } = span.span; + + return { + spanType: formatType(type), + spanSubtype: formatSubtype(subtype), + spanAction: action + }; +} + +const SpanBadge = (styled(EuiBadge)` + display: inline-block; + margin-right: ${px(units.quarter)}; +` as unknown) as typeof EuiBadge; + +const HttpInfoContainer = styled('div')` + margin-right: ${px(units.quarter)}; +`; + +interface Props { + span?: Span; + parentTransaction?: Transaction; + totalDuration?: number; + onClose: () => void; +} + +export function SpanFlyout({ + span, + parentTransaction, + totalDuration, + onClose +}: Props) { + if (!span) { + return null; + } + + const stackframes = span.span.stacktrace; + const codeLanguage = parentTransaction?.service.language?.name; + const dbContext = span.span.db; + const httpContext = span.span.http; + const spanTypes = getSpanTypes(span); + const spanHttpStatusCode = httpContext?.response?.status_code; + const spanHttpUrl = httpContext?.url?.original; + const spanHttpMethod = httpContext?.method; + + return ( + <EuiPortal> + <ResponsiveFlyout onClose={onClose} size="m" ownFocus={true}> + <EuiFlyoutHeader hasBorder> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiTitle> + <h2> + {i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.spanDetailsTitle', + { + defaultMessage: 'Span details' + } + )} + </h2> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <DiscoverSpanLink span={span}> + <EuiButtonEmpty iconType="discoverApp"> + {i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel', + { + defaultMessage: 'View span in Discover' + } + )} + </EuiButtonEmpty> + </DiscoverSpanLink> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <StickySpanProperties span={span} transaction={parentTransaction} /> + <EuiSpacer size="m" /> + <Summary + items={[ + <TimestampTooltip time={span.timestamp.us / 1000} />, + <DurationSummaryItem + duration={span.span.duration.us} + totalDuration={totalDuration} + parentType="transaction" + />, + <> + {spanHttpUrl && ( + <HttpInfoContainer> + <HttpInfoSummaryItem + method={spanHttpMethod} + url={spanHttpUrl} + status={spanHttpStatusCode} + /> + </HttpInfoContainer> + )} + <EuiToolTip + content={i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.spanType', + { defaultMessage: 'Type' } + )} + > + <SpanBadge color="hollow">{spanTypes.spanType}</SpanBadge> + </EuiToolTip> + {spanTypes.spanSubtype && ( + <EuiToolTip + content={i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.spanSubtype', + { defaultMessage: 'Subtype' } + )} + > + <SpanBadge color="hollow"> + {spanTypes.spanSubtype} + </SpanBadge> + </EuiToolTip> + )} + {spanTypes.spanAction && ( + <EuiToolTip + content={i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.spanAction', + { defaultMessage: 'Action' } + )} + > + <SpanBadge color="hollow">{spanTypes.spanAction}</SpanBadge> + </EuiToolTip> + )} + <SyncBadge sync={span.span.sync} /> + </> + ]} + /> + <EuiHorizontalRule /> + <DatabaseContext dbContext={dbContext} /> + <EuiTabbedContent + tabs={[ + { + id: 'stack-trace', + name: i18n.translate( + 'xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel', + { + defaultMessage: 'Stack Trace' + } + ), + content: ( + <Fragment> + <EuiSpacer size="l" /> + <Stacktrace + stackframes={stackframes} + codeLanguage={codeLanguage} + /> + </Fragment> + ) + }, + { + id: 'metadata', + name: i18n.translate( + 'xpack.apm.propertiesTable.tabs.metadataLabel', + { + defaultMessage: 'Metadata' + } + ), + content: ( + <Fragment> + <EuiSpacer size="m" /> + <SpanMetadata span={span} /> + </Fragment> + ) + } + ]} + /> + </EuiFlyoutBody> + </ResponsiveFlyout> + </EuiPortal> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx index 764f15f943ad2..f01b2aa335a3a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx @@ -10,10 +10,10 @@ import React from 'react'; import styled from 'styled-components'; import { px, units } from '../../../../../../style/variables'; -const SpanBadge = styled(EuiBadge)` +const SpanBadge = (styled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -`; +` as unknown) as typeof EuiBadge; interface SyncBadgeProps { /** diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx index 85cf0b642530f..87ecb96f74735 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/DroppedSpansWarning.tsx @@ -7,7 +7,7 @@ import { EuiCallOut, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { ElasticDocsLink } from '../../../../../../shared/Links/ElasticDocsLink'; export function DroppedSpansWarning({ diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx new file mode 100644 index 0000000000000..5fb679818f0a7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/TransactionFlyout/index.tsx @@ -0,0 +1,97 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiPortal, + EuiSpacer, + EuiTitle, + EuiHorizontalRule +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; +import { TransactionActionMenu } from '../../../../../../shared/TransactionActionMenu/TransactionActionMenu'; +import { TransactionSummary } from '../../../../../../shared/Summary/TransactionSummary'; +import { FlyoutTopLevelProperties } from '../FlyoutTopLevelProperties'; +import { ResponsiveFlyout } from '../ResponsiveFlyout'; +import { TransactionMetadata } from '../../../../../../shared/MetadataTable/TransactionMetadata'; +import { DroppedSpansWarning } from './DroppedSpansWarning'; + +interface Props { + onClose: () => void; + transaction?: Transaction; + errorCount?: number; + rootTransactionDuration?: number; +} + +function TransactionPropertiesTable({ + transaction +}: { + transaction: Transaction; +}) { + return ( + <div> + <EuiTitle size="s"> + <h4>Metadata</h4> + </EuiTitle> + <TransactionMetadata transaction={transaction} /> + </div> + ); +} + +export function TransactionFlyout({ + transaction: transactionDoc, + onClose, + errorCount = 0, + rootTransactionDuration +}: Props) { + if (!transactionDoc) { + return null; + } + + return ( + <EuiPortal> + <ResponsiveFlyout onClose={onClose} ownFocus={true} maxWidth={false}> + <EuiFlyoutHeader hasBorder> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiTitle> + <h4> + {i18n.translate( + 'xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle', + { + defaultMessage: 'Transaction details' + } + )} + </h4> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <TransactionActionMenu transaction={transactionDoc} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <FlyoutTopLevelProperties transaction={transactionDoc} /> + <EuiSpacer size="m" /> + <TransactionSummary + transaction={transactionDoc} + totalDuration={rootTransactionDuration} + errorCount={errorCount} + /> + <EuiHorizontalRule margin="m" /> + <DroppedSpansWarning transactionDoc={transactionDoc} /> + <TransactionPropertiesTable transaction={transactionDoc} /> + </EuiFlyoutBody> + </ResponsiveFlyout> + </EuiPortal> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallFlyout.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index 5c6e0cc5ce435..d8edcce46c2d7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -10,13 +10,13 @@ import styled from 'styled-components'; import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { isRumAgentName } from '../../../../../../../../../../plugins/apm/common/agent_name'; +import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; import { asDuration } from '../../../../../../utils/formatters'; import { ErrorCount } from '../../ErrorCount'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; -import { TRACE_ID } from '../../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; import { SyncBadge } from './SyncBadge'; import { Margins } from '../../../../../shared/charts/Timeline'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/spans.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/mock_responses/transaction.json diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index d844ac8b5988d..75304932ed2ba 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -5,8 +5,8 @@ */ import { groupBy } from 'lodash'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; import { getClockSkew, getOrderedWaterfallItems, @@ -15,7 +15,7 @@ import { IWaterfallTransaction, IWaterfallError } from './waterfall_helpers'; -import { APMError } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error'; describe('waterfall_helpers', () => { describe('getWaterfall', () => { @@ -191,7 +191,7 @@ describe('waterfall_helpers', () => { name: 'SELECT FROM products', id: 'mySpanIdB' }, - child_ids: ['mySpanIdA', 'mySpanIdC'] + child: { id: ['mySpanIdA', 'mySpanIdC'] } } as Span, { parent: { id: 'mySpanIdD' }, @@ -294,7 +294,7 @@ describe('waterfall_helpers', () => { name: 'SELECT FROM products', id: 'mySpanIdB' }, - child_ids: ['incorrectId', 'mySpanIdC'] + child: { id: ['incorrectId', 'mySpanIdC'] } } as Span, { parent: { id: 'mySpanIdD' }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 8a873b2ddf1c9..8ddce66f0b853 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -16,10 +16,10 @@ import { zipObject } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TraceAPIResponse } from '../../../../../../../../../../../plugins/apm/server/lib/traces/get_trace'; -import { APMError } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace'; +import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction'; interface IWaterfallGroup { [key: string]: IWaterfallItem[]; @@ -237,7 +237,7 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => }); /** - * Changes the parent_id of items based on the child_ids property. + * Changes the parent_id of items based on the child.id property. * Solves the problem of Inferred spans that are created as child of trace spans * when it actually should be its parent. * @param waterfallItems @@ -245,10 +245,10 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => const reparentSpans = (waterfallItems: IWaterfallItem[]) => { return waterfallItems.map(waterfallItem => { if (waterfallItem.docType === 'span') { - const { child_ids: childIds } = waterfallItem.doc; - if (childIds) { - childIds.forEach(childId => { - const item = waterfallItems.find(_item => _item.id === childId); + const childId = waterfallItem.doc.child?.id; + if (childId) { + childId.forEach(id => { + const item = waterfallItems.find(_item => _item.id === id); if (item) { item.parentId = waterfallItem.id; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx index f681f4dfc675a..87710fb9b8d96 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TraceAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/traces/get_trace'; +import { TraceAPIResponse } from '../../../../../../server/lib/traces/get_trace'; import { WaterfallContainer } from './index'; import { location, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts similarity index 99% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index 306c8e4f3fedb..2f28e37f73f62 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -2027,7 +2027,7 @@ export const inferredSpans = { id: '41226ae63af4f235', type: 'unknown' }, - child_ids: ['8d80de06aa11a6fc'] + child: { ids: ['8d80de06aa11a6fc'] } }, { container: { diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx rename to x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/__tests__/ErrorCount.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx new file mode 100644 index 0000000000000..056e9cdb75148 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/index.tsx @@ -0,0 +1,138 @@ +/* + * 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 { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiPagination, + EuiPanel, + EuiSpacer, + EuiTitle +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Location } from 'history'; +import React, { useEffect, useState } from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { history } from '../../../../utils/history'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { TransactionSummary } from '../../../shared/Summary/TransactionSummary'; +import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu'; +import { MaybeViewTraceLink } from './MaybeViewTraceLink'; +import { TransactionTabs } from './TransactionTabs'; +import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers'; + +interface Props { + urlParams: IUrlParams; + location: Location; + waterfall: IWaterfall; + exceedsMax: boolean; + isLoading: boolean; + traceSamples: IBucket['samples']; +} + +export const WaterfallWithSummmary: React.FC<Props> = ({ + urlParams, + location, + waterfall, + exceedsMax, + isLoading, + traceSamples +}) => { + const [sampleActivePage, setSampleActivePage] = useState(0); + + useEffect(() => { + setSampleActivePage(0); + }, [traceSamples]); + + const goToSample = (index: number) => { + setSampleActivePage(index); + const sample = traceSamples[index]; + history.push({ + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + transactionId: sample.transactionId, + traceId: sample.traceId + }) + }); + }; + + const { entryTransaction } = waterfall; + if (!entryTransaction) { + const content = isLoading ? ( + <LoadingStatePrompt /> + ) : ( + <EuiEmptyPrompt + title={ + <div> + {i18n.translate('xpack.apm.transactionDetails.traceNotFound', { + defaultMessage: 'The selected trace cannot be found' + })} + </div> + } + titleSize="s" + /> + ); + + return <EuiPanel paddingSize="m">{content}</EuiPanel>; + } + + return ( + <EuiPanel paddingSize="m"> + <EuiFlexGroup> + <EuiFlexItem style={{ flexDirection: 'row', alignItems: 'center' }}> + <EuiTitle size="xs"> + <h5> + {i18n.translate('xpack.apm.transactionDetails.traceSampleTitle', { + defaultMessage: 'Trace sample' + })} + </h5> + </EuiTitle> + {traceSamples && ( + <EuiPagination + pageCount={traceSamples.length} + activePage={sampleActivePage} + onPageClick={goToSample} + compressed + /> + )} + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <TransactionActionMenu transaction={entryTransaction} /> + </EuiFlexItem> + <MaybeViewTraceLink + transaction={entryTransaction} + waterfall={waterfall} + /> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <TransactionSummary + errorCount={waterfall.errorsCount} + totalDuration={waterfall.rootTransaction?.transaction.duration.us} + transaction={entryTransaction} + /> + <EuiSpacer size="s" /> + + <TransactionTabs + transaction={entryTransaction} + location={location} + urlParams={urlParams} + waterfall={waterfall} + exceedsMax={exceedsMax} + /> + </EuiPanel> + ); +}; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx new file mode 100644 index 0000000000000..2544dc2a1a77c --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/index.tsx @@ -0,0 +1,127 @@ +/* + * 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 { + EuiPanel, + EuiSpacer, + EuiTitle, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem +} from '@elastic/eui'; +import _ from 'lodash'; +import React, { useMemo } from 'react'; +import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; +import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution'; +import { useWaterfall } from '../../../hooks/useWaterfall'; +import { TransactionCharts } from '../../shared/charts/TransactionCharts'; +import { ApmHeader } from '../../shared/ApmHeader'; +import { TransactionDistribution } from './Distribution'; +import { WaterfallWithSummmary } from './WaterfallWithSummmary'; +import { useLocation } from '../../../hooks/useLocation'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { useTrackPageview } from '../../../../../observability/public'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { HeightRetainer } from '../../shared/HeightRetainer'; + +export function TransactionDetails() { + const location = useLocation(); + const { urlParams } = useUrlParams(); + const { + data: distributionData, + status: distributionStatus + } = useTransactionDistribution(urlParams); + + const { data: transactionChartsData } = useTransactionCharts(); + const { waterfall, exceedsMax, status: waterfallStatus } = useWaterfall( + urlParams + ); + const { transactionName, transactionType, serviceName } = urlParams; + + useTrackPageview({ app: 'apm', path: 'transaction_details' }); + useTrackPageview({ app: 'apm', path: 'transaction_details', delay: 15000 }); + + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps<typeof LocalUIFilters> = { + filterNames: ['transactionResult', 'serviceVersion'], + projection: PROJECTION.TRANSACTIONS, + params: { + transactionName, + transactionType, + serviceName + } + }; + return config; + }, [transactionName, transactionType, serviceName]); + + const bucketIndex = distributionData.buckets.findIndex(bucket => + bucket.samples.some( + sample => + sample.transactionId === urlParams.transactionId && + sample.traceId === urlParams.traceId + ) + ); + + const traceSamples = distributionData.buckets[bucketIndex]?.samples; + + return ( + <div> + <ApmHeader> + <EuiTitle size="l"> + <h1>{transactionName}</h1> + </EuiTitle> + </ApmHeader> + + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <LocalUIFilters {...localUIFiltersConfig} /> + </EuiFlexItem> + <EuiFlexItem grow={7}> + <ChartsSyncContextProvider> + <TransactionBreakdown /> + + <EuiSpacer size="s" /> + + <TransactionCharts + hasMLJob={false} + charts={transactionChartsData} + urlParams={urlParams} + location={location} + /> + </ChartsSyncContextProvider> + + <EuiHorizontalRule size="full" margin="l" /> + + <EuiPanel> + <TransactionDistribution + distribution={distributionData} + isLoading={distributionStatus === FETCH_STATUS.LOADING} + urlParams={urlParams} + bucketIndex={bucketIndex} + /> + </EuiPanel> + + <EuiSpacer size="s" /> + + <HeightRetainer> + <WaterfallWithSummmary + location={location} + urlParams={urlParams} + waterfall={waterfall} + isLoading={waterfallStatus === FETCH_STATUS.LOADING} + exceedsMax={exceedsMax} + traceSamples={traceSamples} + /> + </HeightRetainer> + </EuiFlexItem> + </EuiFlexGroup> + </div> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx new file mode 100644 index 0000000000000..e3b33f11d0805 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx @@ -0,0 +1,160 @@ +/* + * 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, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform'; +import { fontFamilyCode, truncate } from '../../../../style/variables'; +import { asDecimal, convertTo } from '../../../../utils/formatters'; +import { ImpactBar } from '../../../shared/ImpactBar'; +import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { EmptyMessage } from '../../../shared/EmptyMessage'; +import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; + +const TransactionNameLink = styled(TransactionDetailLink)` + ${truncate('100%')}; + font-family: ${fontFamilyCode}; +`; + +interface Props { + items: ITransactionGroup[]; + isLoading: boolean; +} + +const toMilliseconds = (time: number) => + convertTo({ + unit: 'milliseconds', + microseconds: time + }).formatted; + +export function TransactionList({ items, isLoading }: Props) { + const columns: Array<ITableColumn<ITransactionGroup>> = useMemo( + () => [ + { + field: 'name', + name: i18n.translate('xpack.apm.transactionsTable.nameColumnLabel', { + defaultMessage: 'Name' + }), + width: '50%', + sortable: true, + render: (transactionName: string, { sample }: ITransactionGroup) => { + return ( + <EuiToolTip + id="transaction-name-link-tooltip" + content={transactionName || NOT_AVAILABLE_LABEL} + > + <TransactionNameLink + serviceName={sample.service.name} + transactionId={sample.transaction.id} + traceId={sample.trace.id} + transactionName={sample.transaction.name} + transactionType={sample.transaction.type} + > + {transactionName || NOT_AVAILABLE_LABEL} + </TransactionNameLink> + </EuiToolTip> + ); + } + }, + { + field: 'averageResponseTime', + name: i18n.translate( + 'xpack.apm.transactionsTable.avgDurationColumnLabel', + { + defaultMessage: 'Avg. duration' + } + ), + sortable: true, + dataType: 'number', + render: (time: number) => toMilliseconds(time) + }, + { + field: 'p95', + name: i18n.translate( + 'xpack.apm.transactionsTable.95thPercentileColumnLabel', + { + defaultMessage: '95th percentile' + } + ), + sortable: true, + dataType: 'number', + render: (time: number) => toMilliseconds(time) + }, + { + field: 'transactionsPerMinute', + name: i18n.translate( + 'xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel', + { + defaultMessage: 'Trans. per minute' + } + ), + sortable: true, + dataType: 'number', + render: (value: number) => + `${asDecimal(value)} ${i18n.translate( + 'xpack.apm.transactionsTable.transactionsPerMinuteUnitLabel', + { + defaultMessage: 'tpm' + } + )}` + }, + { + field: 'impact', + name: ( + <EuiToolTip + content={i18n.translate( + 'xpack.apm.transactionsTable.impactColumnDescription', + { + defaultMessage: + "The most used and slowest endpoints in your service. It's calculated by taking the relative average duration times the number of transactions per minute." + } + )} + > + <> + {i18n.translate('xpack.apm.transactionsTable.impactColumnLabel', { + defaultMessage: 'Impact' + })}{' '} + <EuiIcon + size="s" + color="subdued" + type="questionInCircle" + className="eui-alignTop" + /> + </> + </EuiToolTip> + ), + sortable: true, + dataType: 'number', + render: (value: number) => <ImpactBar value={value} /> + } + ], + [] + ); + + const noItemsMessage = ( + <EmptyMessage + heading={i18n.translate('xpack.apm.transactionsTable.notFoundLabel', { + defaultMessage: 'No transactions were found.' + })} + /> + ); + + return ( + <ManagedTable + noItemsMessage={isLoading ? <LoadingStatePrompt /> : noItemsMessage} + columns={columns} + items={items} + initialSortField="impact" + initialSortDirection="desc" + initialPageSize={25} + /> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx rename to x-pack/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx new file mode 100644 index 0000000000000..60aac3fcdfeef --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -0,0 +1,162 @@ +/* + * 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 { + EuiPanel, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule +} from '@elastic/eui'; +import { Location } from 'history'; +import { first } from 'lodash'; +import React, { useMemo } from 'react'; +import { useTransactionList } from '../../../hooks/useTransactionList'; +import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; +import { IUrlParams } from '../../../context/UrlParamsContext/types'; +import { TransactionCharts } from '../../shared/charts/TransactionCharts'; +import { TransactionBreakdown } from '../../shared/TransactionBreakdown'; +import { TransactionList } from './List'; +import { useRedirect } from './useRedirect'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { getHasMLJob } from '../../../services/rest/ml'; +import { history } from '../../../utils/history'; +import { useLocation } from '../../../hooks/useLocation'; +import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext'; +import { useTrackPageview } from '../../../../../observability/public'; +import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; +import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; + +function getRedirectLocation({ + urlParams, + location, + serviceTransactionTypes +}: { + location: Location; + urlParams: IUrlParams; + serviceTransactionTypes: string[]; +}): Location | undefined { + const { transactionType } = urlParams; + const firstTransactionType = first(serviceTransactionTypes); + + if (!transactionType && firstTransactionType) { + return { + ...location, + search: fromQuery({ + ...toQuery(location.search), + transactionType: firstTransactionType + }) + }; + } +} + +export function TransactionOverview() { + const location = useLocation(); + const { urlParams } = useUrlParams(); + const { serviceName, transactionType } = urlParams; + + // TODO: fetching of transaction types should perhaps be lifted since it is needed in several places. Context? + const serviceTransactionTypes = useServiceTransactionTypes(urlParams); + + // redirect to first transaction type + useRedirect( + history, + getRedirectLocation({ + urlParams, + location, + serviceTransactionTypes + }) + ); + + const { data: transactionCharts } = useTransactionCharts(); + + useTrackPageview({ app: 'apm', path: 'transaction_overview' }); + useTrackPageview({ app: 'apm', path: 'transaction_overview', delay: 15000 }); + const { + data: transactionListData, + status: transactionListStatus + } = useTransactionList(urlParams); + + const { http } = useApmPluginContext().core; + + const { data: hasMLJob = false } = useFetcher(() => { + if (serviceName && transactionType) { + return getHasMLJob({ serviceName, transactionType, http }); + } + }, [http, serviceName, transactionType]); + + const localFiltersConfig: React.ComponentProps<typeof LocalUIFilters> = useMemo( + () => ({ + filterNames: [ + 'transactionResult', + 'host', + 'containerId', + 'podName', + 'serviceVersion' + ], + params: { + serviceName, + transactionType + }, + projection: PROJECTION.TRANSACTION_GROUPS + }), + [serviceName, transactionType] + ); + + // TODO: improve urlParams typings. + // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed + if (!serviceName || !transactionType) { + return null; + } + + return ( + <> + <EuiSpacer /> + <EuiFlexGroup> + <EuiFlexItem grow={1}> + <LocalUIFilters {...localFiltersConfig}> + <TransactionTypeFilter transactionTypes={serviceTransactionTypes} /> + <EuiSpacer size="xl" /> + <EuiHorizontalRule margin="none" /> + </LocalUIFilters> + </EuiFlexItem> + <EuiFlexItem grow={7}> + <ChartsSyncContextProvider> + <TransactionBreakdown initialIsOpen={true} /> + + <EuiSpacer size="s" /> + + <TransactionCharts + hasMLJob={hasMLJob} + charts={transactionCharts} + location={location} + urlParams={urlParams} + /> + </ChartsSyncContextProvider> + + <EuiSpacer size="s" /> + + <EuiPanel> + <EuiTitle size="xs"> + <h3>Transactions</h3> + </EuiTitle> + <EuiSpacer size="s" /> + <TransactionList + isLoading={transactionListStatus === 'loading'} + items={transactionListData} + /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts b/x-pack/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts rename to x-pack/plugins/apm/public/components/app/TransactionOverview/useRedirect.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ApmHeader/index.tsx rename to x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx rename to x-pack/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/index.tsx b/x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/DatePicker/index.tsx rename to x-pack/plugins/apm/public/components/shared/DatePicker/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/EmptyMessage.tsx rename to x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx rename to x-pack/plugins/apm/public/components/shared/EnvironmentBadge/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx new file mode 100644 index 0000000000000..d17b3b7689b19 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useFetcher } from '../../../hooks/useFetcher'; +import { useLocation } from '../../../hooks/useLocation'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { history } from '../../../utils/history'; +import { fromQuery, toQuery } from '../Links/url_helpers'; +import { + ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED +} from '../../../../common/environment_filter_values'; + +function updateEnvironmentUrl( + location: ReturnType<typeof useLocation>, + environment?: string +) { + const nextEnvironmentQueryParam = + environment !== ENVIRONMENT_ALL ? environment : undefined; + history.push({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + environment: nextEnvironmentQueryParam + }) + }); +} + +const ALL_OPTION = { + value: ENVIRONMENT_ALL, + text: i18n.translate('xpack.apm.filter.environment.allLabel', { + defaultMessage: 'All' + }) +}; + +const NOT_DEFINED_OPTION = { + value: ENVIRONMENT_NOT_DEFINED, + text: i18n.translate('xpack.apm.filter.environment.notDefinedLabel', { + defaultMessage: 'Not defined' + }) +}; + +const SEPARATOR_OPTION = { + text: `- ${i18n.translate( + 'xpack.apm.filter.environment.selectEnvironmentLabel', + { defaultMessage: 'Select environment' } + )} -`, + disabled: true +}; + +function getOptions(environments: string[]) { + const environmentOptions = environments + .filter(env => env !== ENVIRONMENT_NOT_DEFINED) + .map(environment => ({ + value: environment, + text: environment + })); + + return [ + ALL_OPTION, + ...(environments.includes(ENVIRONMENT_NOT_DEFINED) + ? [NOT_DEFINED_OPTION] + : []), + ...(environmentOptions.length > 0 ? [SEPARATOR_OPTION] : []), + ...environmentOptions + ]; +} + +export const EnvironmentFilter: React.FC = () => { + const location = useLocation(); + const { urlParams, uiFilters } = useUrlParams(); + const { start, end, serviceName } = urlParams; + + const { environment } = uiFilters; + const { data: environments = [], status = 'loading' } = useFetcher( + callApmApi => { + if (start && end) { + return callApmApi({ + pathname: '/api/apm/ui_filters/environments', + params: { + query: { + start, + end, + serviceName + } + } + }); + } + }, + [start, end, serviceName] + ); + + return ( + <EuiSelect + prepend={i18n.translate('xpack.apm.filter.environment.label', { + defaultMessage: 'environment' + })} + options={getOptions(environments)} + value={environment || ENVIRONMENT_ALL} + onChange={event => { + updateEnvironmentUrl(location, event.target.value); + }} + isLoading={status === 'loading'} + /> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx new file mode 100644 index 0000000000000..658def7ddbb57 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx @@ -0,0 +1,87 @@ +/* + * 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 { EuiFieldNumber } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isFinite } from 'lodash'; +import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; +import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; +import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; + +export interface ErrorRateAlertTriggerParams { + windowSize: number; + windowUnit: string; + threshold: number; +} + +interface Props { + alertParams: ErrorRateAlertTriggerParams; + setAlertParams: (key: string, value: any) => void; + setAlertProperty: (key: string, value: any) => void; +} + +export function ErrorRateAlertTrigger(props: Props) { + const { setAlertParams, setAlertProperty, alertParams } = props; + + const defaults = { + threshold: 25, + windowSize: 1, + windowUnit: 'm' + }; + + const params = { + ...defaults, + ...alertParams + }; + + const threshold = isFinite(params.threshold) ? params.threshold : ''; + + const fields = [ + <PopoverExpression + title={i18n.translate('xpack.apm.errorRateAlertTrigger.isAbove', { + defaultMessage: 'is above' + })} + value={threshold.toString()} + > + <EuiFieldNumber + value={threshold} + step={0} + onChange={e => + setAlertParams('threshold', parseInt(e.target.value, 10)) + } + compressed + append={i18n.translate('xpack.apm.errorRateAlertTrigger.errors', { + defaultMessage: 'errors' + })} + /> + </PopoverExpression>, + <ForLastExpression + onChangeWindowSize={windowSize => + setAlertParams('windowSize', windowSize || '') + } + onChangeWindowUnit={windowUnit => + setAlertParams('windowUnit', windowUnit) + } + timeWindowSize={params.windowSize} + timeWindowUnit={params.windowUnit} + errors={{ + timeWindowSize: [], + timeWindowUnit: [] + }} + /> + ]; + + return ( + <ServiceAlertTrigger + alertTypeName={ALERT_TYPES_CONFIG['apm.error_rate'].name} + defaults={defaults} + fields={fields} + setAlertParams={setAlertParams} + setAlertProperty={setAlertProperty} + /> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ErrorStatePrompt.tsx b/x-pack/plugins/apm/public/components/shared/ErrorStatePrompt.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ErrorStatePrompt.tsx rename to x-pack/plugins/apm/public/components/shared/ErrorStatePrompt.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/EuiTabLink.tsx b/x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/EuiTabLink.tsx rename to x-pack/plugins/apm/public/components/shared/EuiTabLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx b/x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/HeightRetainer/index.tsx rename to x-pack/plugins/apm/public/components/shared/HeightRetainer/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js rename to x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ImpactBar/index.tsx rename to x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx index 4de07f75ff84f..d33960fe5196b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx @@ -8,7 +8,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { isBoolean, isNumber, isObject } from 'lodash'; import React from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../../plugins/apm/common/i18n'; +import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; const EmptyValue = styled.span` color: ${theme.euiColorMediumShade}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/__test__/KeyValueTable.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/index.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KeyValueTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js index 5ad256efe0945..93a95c844a975 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/ClickOutside.js @@ -27,6 +27,7 @@ export default class ClickOutside extends Component { }; render() { + // eslint-disable-next-line no-unused-vars const { onClickOutside, ...restProps } = this.props; return ( <div ref={this.setNodeRef} {...restProps}> diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js rename to x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts rename to x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts index 19a9ae9538ad6..f4628524cced5 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/get_bool_filter.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from '../../../../../../../plugins/apm/typings/elasticsearch'; +import { ESFilter } from '../../../../typings/elasticsearch'; import { TRANSACTION_TYPE, ERROR_GROUP_ID, PROCESSOR_EVENT, TRANSACTION_NAME, SERVICE_NAME -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../common/elasticsearch_fieldnames'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; export function getBoolFilter(urlParams: IUrlParams) { diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx new file mode 100644 index 0000000000000..2622d08d4779d --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -0,0 +1,156 @@ +/* + * 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 } from 'react'; +import { uniqueId, startsWith } from 'lodash'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { fromQuery, toQuery } from '../Links/url_helpers'; +// @ts-ignore +import { Typeahead } from './Typeahead'; +import { getBoolFilter } from './get_bool_filter'; +import { useLocation } from '../../../hooks/useLocation'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { history } from '../../../utils/history'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; +import { + QuerySuggestion, + esKuery, + IIndexPattern +} from '../../../../../../../src/plugins/data/public'; + +const Container = styled.div` + margin-bottom: 10px; +`; + +interface State { + suggestions: QuerySuggestion[]; + isLoadingSuggestions: boolean; +} + +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); +} + +export function KueryBar() { + const [state, setState] = useState<State>({ + suggestions: [], + isLoadingSuggestions: false + }); + const { urlParams } = useUrlParams(); + const location = useLocation(); + const { data } = useApmPluginContext().plugins; + + let currentRequestCheck; + + const { processorEvent } = urlParams; + + const examples = { + transaction: 'transaction.duration.us > 300000', + error: 'http.response.status_code >= 400', + metric: 'process.pid = "1234"', + defaults: + 'transaction.duration.us > 300000 AND http.response.status_code >= 400' + }; + + const example = examples[processorEvent || 'defaults']; + + const { indexPattern } = useDynamicIndexPattern(processorEvent); + + const placeholder = i18n.translate('xpack.apm.kueryBar.placeholder', { + defaultMessage: `Search {event, select, + transaction {transactions} + metric {metrics} + error {errors} + other {transactions, errors and metrics} + } (E.g. {queryExample})`, + values: { + queryExample: example, + event: processorEvent + } + }); + + // The bar should be disabled when viewing the service map + const disabled = /\/service-map$/.test(location.pathname); + const disabledPlaceholder = i18n.translate( + 'xpack.apm.kueryBar.disabledPlaceholder', + { defaultMessage: 'Search is not available for service map' } + ); + + async function onChange(inputValue: string, selectionStart: number) { + if (indexPattern == null) { + return; + } + + setState({ ...state, suggestions: [], isLoadingSuggestions: true }); + + const currentRequest = uniqueId(); + currentRequestCheck = currentRequest; + + try { + const suggestions = ( + (await data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: getBoolFilter(urlParams), + query: inputValue, + selectionStart, + selectionEnd: selectionStart + })) || [] + ) + .filter(suggestion => !startsWith(suggestion.text, 'span.')) + .slice(0, 15); + + if (currentRequest !== currentRequestCheck) { + return; + } + + setState({ ...state, suggestions, isLoadingSuggestions: false }); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error while fetching suggestions', e); + } + } + + function onSubmit(inputValue: string) { + if (indexPattern == null) { + return; + } + + try { + const res = convertKueryToEsQuery(inputValue, indexPattern); + if (!res) { + return; + } + + history.push({ + ...location, + search: fromQuery({ + ...toQuery(location.search), + kuery: encodeURIComponent(inputValue.trim()) + }) + }); + } catch (e) { + console.log('Invalid kuery syntax'); // eslint-disable-line no-console + } + } + + return ( + <Container> + <Typeahead + disabled={disabled} + isLoading={state.isLoadingSuggestions} + initialValue={urlParams.kuery} + onChange={onChange} + onSubmit={onSubmit} + suggestions={state.suggestions} + placeholder={disabled ? disabledPlaceholder : placeholder} + /> + </Container> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx rename to x-pack/plugins/apm/public/components/shared/LicensePrompt/LicensePrompt.stories.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/index.tsx b/x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LicensePrompt/index.tsx rename to x-pack/plugins/apm/public/components/shared/LicensePrompt/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx index 806d9f73369c3..05e4080d5d0b7 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverErrorLink.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { ERROR_GROUP_ID, SERVICE_NAME -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { APMError } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +} from '../../../../../common/elasticsearch_fieldnames'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import { DiscoverLink } from './DiscoverLink'; function getDiscoverQuery(error: APMError, kuery?: string) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 6dc93292956fa..b58a450d26644 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -11,7 +11,7 @@ import url from 'url'; import rison, { RisonValue } from 'rison-node'; import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData } from '../rison_helpers'; -import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../../../../plugins/apm/common/index_pattern_constants'; +import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx similarity index 79% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx index 8fe5be28def22..ac9e33b3acd69 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverSpanLink.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { SPAN_ID } from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Span } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { SPAN_ID } from '../../../../../common/elasticsearch_fieldnames'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; import { DiscoverLink } from './DiscoverLink'; function getDiscoverQuery(span: Span) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx index b0af33fd7d7f7..a5f4df7dbac1b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverTransactionLink.tsx @@ -9,8 +9,8 @@ import { PROCESSOR_EVENT, TRACE_ID, TRANSACTION_ID -} from '../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +} from '../../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { DiscoverLink } from './DiscoverLink'; export function getDiscoverQuery(transaction: Transaction) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx index eeb9fd20a4bcb..acf8d89432b23 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorButton.test.tsx @@ -6,7 +6,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx index eeb9fd20a4bcb..acf8d89432b23 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverErrorLink.test.tsx @@ -6,7 +6,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; describe('DiscoverErrorLink without kuery', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 759caa785c1af..ea79fe12ff0bd 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -6,9 +6,9 @@ import { Location } from 'history'; import React from 'react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../../../typings/es_schemas/ui/span'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; import { getRenderedHref } from '../../../../../utils/testHelpers'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; import { DiscoverSpanLink } from '../DiscoverSpanLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx index d72925c1956a4..5769ca34a9a87 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionButton.test.tsx @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; import { DiscoverTransactionLink, getDiscoverQuery diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx similarity index 88% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx index 15a92474fcc6d..2f65cf7734631 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverTransactionLink.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; // @ts-ignore import configureStore from '../../../../../store/config/configureStore'; import { getDiscoverQuery } from '../DiscoverTransactionLink'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorButton.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverErrorLink.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionButton.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/__snapshots__/DiscoverTransactionLink.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json b/x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json rename to x-pack/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/mockTransaction.json diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/InfraLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx index 7efe5cb96cfbd..0ae9f64dc24ef 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -10,7 +10,7 @@ import url from 'url'; import { fromQuery } from './url_helpers'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { AppMountContextBasePath } from '../../../context/ApmPluginContext'; -import { InfraAppId } from '../../../../../../../plugins/infra/public'; +import { InfraAppId } from '../../../../../infra/public'; interface InfraQueryParams { time?: number; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx similarity index 88% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx index ecf788ddd2e69..81c5d17d491c0 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { getMlJobId } from '../../../../../../../../plugins/apm/common/ml_job_constants'; +import { getMlJobId } from '../../../../../common/ml_job_constants'; import { MLLink } from './MLLink'; interface Props { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/SetupInstructionsLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/APMLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/APMLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ErrorDetailLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx index fcc0dc7d26695..ebcf220994cda 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ErrorOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; interface Props extends APMLinkExtendProps { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts b/x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts rename to x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts b/x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts rename to x-pack/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/HomeLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index 7d21e1efa44f2..bd3e3b36a8601 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceMapLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx index 527c3da9e7e1c..1473221cca2be 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeMetricOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx index db1b6ec117bf4..b479ab77e1127 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceNodeOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx index 101f1602506aa..577209a26e46b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/ServiceOverviewLink.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; const ServiceOverviewLink = (props: APMLinkExtendProps) => { const { urlParams } = useUrlParams(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/SettingsLink.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx index 371544c142a2d..dc4519365cbc2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TraceOverviewLink.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; const TraceOverviewLink = (props: APMLinkExtendProps) => { const { urlParams } = useUrlParams(); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx index 784f9b36ff621..6278336751851 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionDetailLink.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx index af60a0a748445..ccef83ee73fb8 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { APMLink, APMLinkExtendProps } from './APMLink'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { pickKeys } from '../../../../utils/pickKeys'; +import { pickKeys } from '../../../../../common/utils/pick_keys'; interface Props extends APMLinkExtendProps { serviceName: string; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx rename to x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx index 0c747e0773a69..6885e44f1ad1f 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/agentConfigurationLinks.tsx @@ -5,7 +5,7 @@ */ import { getAPMHref } from './APMLink'; -import { AgentConfigurationIntake } from '../../../../../../../../plugins/apm/common/agent_configuration/configuration_types'; +import { AgentConfigurationIntake } from '../../../../../common/agent_configuration/configuration_types'; import { history } from '../../../../utils/history'; export function editAgentConfigurationHref( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/rison_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/rison_helpers.ts rename to x-pack/plugins/apm/public/components/shared/Links/rison_helpers.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.test.tsx rename to x-pack/plugins/apm/public/components/shared/Links/url_helpers.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts similarity index 87% rename from x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts rename to x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts index c7d71d0b6dac5..b296302c47edf 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/url_helpers.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/url_helpers.ts @@ -6,8 +6,8 @@ import { parse, stringify } from 'query-string'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFilterName } from '../../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { url } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; +import { url } from '../../../../../../../src/plugins/kibana_utils/public'; export function toQuery(search?: string): APMQueryParamsRaw { return search ? parse(search.slice(1), { sort: false }) : {}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx b/x-pack/plugins/apm/public/components/shared/LoadingStatePrompt.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LoadingStatePrompt.tsx rename to x-pack/plugins/apm/public/components/shared/LoadingStatePrompt.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterBadgeList.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/FilterTitleButton.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/Filter/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx rename to x-pack/plugins/apm/public/components/shared/LocalUIFilters/TransactionTypeFilter/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx new file mode 100644 index 0000000000000..2c755009ed13a --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -0,0 +1,95 @@ +/* + * 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 { + EuiTitle, + EuiSpacer, + EuiHorizontalRule, + EuiButtonEmpty +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { LocalUIFilterName } from '../../../../server/lib/ui_filters/local_ui_filters/config'; +import { Filter } from './Filter'; +import { useLocalUIFilters } from '../../../hooks/useLocalUIFilters'; +import { PROJECTION } from '../../../../common/projections/typings'; + +interface Props { + projection: PROJECTION; + filterNames: LocalUIFilterName[]; + params?: Record<string, string | number | boolean | undefined>; + showCount?: boolean; + children?: React.ReactNode; +} + +const ButtonWrapper = styled.div` + display: inline-block; +`; + +const LocalUIFilters = ({ + projection, + params, + filterNames, + children, + showCount = true +}: Props) => { + const { filters, setFilterValue, clearValues } = useLocalUIFilters({ + filterNames, + projection, + params + }); + + const hasValues = filters.some(filter => filter.value.length > 0); + + return ( + <> + <EuiTitle size="s"> + <h3> + {i18n.translate('xpack.apm.localFiltersTitle', { + defaultMessage: 'Filters' + })} + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + {children} + {filters.map(filter => { + return ( + <React.Fragment key={filter.name}> + <Filter + {...filter} + onChange={value => { + setFilterValue(filter.name, value); + }} + showCount={showCount} + /> + <EuiHorizontalRule margin="none" /> + </React.Fragment> + ); + })} + {hasValues ? ( + <> + <EuiSpacer size="s" /> + <ButtonWrapper> + <EuiButtonEmpty + size="xs" + iconType="cross" + flush="left" + onClick={clearValues} + > + {i18n.translate('xpack.apm.clearFilters', { + defaultMessage: 'Clear filters' + })} + </EuiButtonEmpty> + </ButtonWrapper> + </> + ) : null} + </> + ); +}; + +export { LocalUIFilters }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js rename to x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap rename to x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ManagedTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx index 258788252379a..1913cf79c7935 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { ErrorMetadata } from '..'; import { render } from '@testing-library/react'; -import { APMError } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/apm_error'; +import { APMError } from '../../../../../../typings/es_schemas/ui/apm_error'; import { expectTextsInDocument, expectTextsNotInDocument diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx new file mode 100644 index 0000000000000..7cae42a94322b --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx @@ -0,0 +1,23 @@ +/* + * 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, { useMemo } from 'react'; +import { ERROR_METADATA_SECTIONS } from './sections'; +import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; +import { getSectionsWithRows } from '../helper'; +import { MetadataTable } from '..'; + +interface Props { + error: APMError; +} + +export function ErrorMetadata({ error }: Props) { + const sectionsWithRows = useMemo( + () => getSectionsWithRows(ERROR_METADATA_SECTIONS, error), + [error] + ); + return <MetadataTable sections={sectionsWithRows} />; +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/Section.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 0059b7b8fb4b3..a46539fe72fcb 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { SpanMetadata } from '..'; -import { Span } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/span'; +import { Span } from '../../../../../../typings/es_schemas/ui/span'; import { expectTextsInDocument, expectTextsNotInDocument diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx new file mode 100644 index 0000000000000..abef083e39b9e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx @@ -0,0 +1,23 @@ +/* + * 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, { useMemo } from 'react'; +import { SPAN_METADATA_SECTIONS } from './sections'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; +import { getSectionsWithRows } from '../helper'; +import { MetadataTable } from '..'; + +interface Props { + span: Span; +} + +export function SpanMetadata({ span }: Props) { + const sectionsWithRows = useMemo( + () => getSectionsWithRows(SPAN_METADATA_SECTIONS, span), + [span] + ); + return <MetadataTable sections={sectionsWithRows} />; +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 3d78f36db9786..8ae46d359efc3 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { TransactionMetadata } from '..'; import { render } from '@testing-library/react'; -import { Transaction } from '../../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../../typings/es_schemas/ui/transaction'; import { expectTextsInDocument, expectTextsNotInDocument diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx new file mode 100644 index 0000000000000..86ecbba6a0aaa --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx @@ -0,0 +1,23 @@ +/* + * 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, { useMemo } from 'react'; +import { TRANSACTION_METADATA_SECTIONS } from './sections'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { getSectionsWithRows } from '../helper'; +import { MetadataTable } from '..'; + +interface Props { + transaction: Transaction; +} + +export function TransactionMetadata({ transaction }: Props) { + const sectionsWithRows = useMemo( + () => getSectionsWithRows(TRANSACTION_METADATA_SECTIONS, transaction), + [transaction] + ); + return <MetadataTable sections={sectionsWithRows} />; +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/Section.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts new file mode 100644 index 0000000000000..e754f7163ca11 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/__test__/helper.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSectionsWithRows, filterSectionsByTerm } from '../helper'; +import { LABELS, HTTP, SERVICE } from '../sections'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; + +describe('MetadataTable Helper', () => { + const sections = [ + { ...LABELS, required: true }, + HTTP, + { ...SERVICE, properties: ['environment'] } + ]; + const apmDoc = ({ + http: { + headers: { + Connection: 'close', + Host: 'opbeans:3000', + request: { method: 'get' } + } + }, + service: { + framework: { name: 'express' }, + environment: 'production' + } + } as unknown) as Transaction; + const metadataItems = getSectionsWithRows(sections, apmDoc); + + it('returns flattened data and required section', () => { + expect(metadataItems).toEqual([ + { key: 'labels', label: 'Labels', required: true, rows: [] }, + { + key: 'http', + label: 'HTTP', + rows: [ + { key: 'http.headers.Connection', value: 'close' }, + { key: 'http.headers.Host', value: 'opbeans:3000' }, + { key: 'http.headers.request.method', value: 'get' } + ] + }, + { + key: 'service', + label: 'Service', + properties: ['environment'], + rows: [{ key: 'service.environment', value: 'production' }] + } + ]); + }); + describe('filter', () => { + it('items by key', () => { + const filteredItems = filterSectionsByTerm(metadataItems, 'http'); + expect(filteredItems).toEqual([ + { + key: 'http', + label: 'HTTP', + rows: [ + { key: 'http.headers.Connection', value: 'close' }, + { key: 'http.headers.Host', value: 'opbeans:3000' }, + { key: 'http.headers.request.method', value: 'get' } + ] + } + ]); + }); + + it('items by value', () => { + const filteredItems = filterSectionsByTerm(metadataItems, 'product'); + expect(filteredItems).toEqual([ + { + key: 'service', + label: 'Service', + properties: ['environment'], + rows: [{ key: 'service.environment', value: 'production' }] + } + ]); + }); + + it('returns empty when no item matches', () => { + const filteredItems = filterSectionsByTerm(metadataItems, 'post'); + expect(filteredItems).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts new file mode 100644 index 0000000000000..a8678ee596e43 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, pick, isEmpty } from 'lodash'; +import { Section } from './sections'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; +import { Span } from '../../../../typings/es_schemas/ui/span'; +import { flattenObject, KeyValuePair } from '../../../utils/flattenObject'; + +export type SectionsWithRows = ReturnType<typeof getSectionsWithRows>; + +export const getSectionsWithRows = ( + sections: Section[], + apmDoc: Transaction | APMError | Span +) => { + return sections + .map(section => { + const sectionData: Record<string, unknown> = get(apmDoc, section.key); + const filteredData: + | Record<string, unknown> + | undefined = section.properties + ? pick(sectionData, section.properties) + : sectionData; + + const rows: KeyValuePair[] = flattenObject(filteredData, section.key); + return { ...section, rows }; + }) + .filter(({ required, rows }) => required || !isEmpty(rows)); +}; + +export const filterSectionsByTerm = ( + sections: SectionsWithRows, + searchTerm: string +) => { + if (!searchTerm) { + return sections; + } + return sections + .map(section => { + const { rows = [] } = section; + const filteredRows = rows.filter(({ key, value }) => { + const valueAsString = String(value).toLowerCase(); + return ( + key.toLowerCase().includes(searchTerm) || + valueAsString.includes(searchTerm) + ); + }); + return { ...section, rows: filteredRows }; + }) + .filter(({ rows }) => !isEmpty(rows)); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/index.tsx rename to x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/sections.ts rename to x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx b/x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx rename to x-pack/plugins/apm/public/components/shared/SelectWithPlaceholder/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx rename to x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/PopoverExpression/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/shared/ServiceAlertTrigger/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx index eab414ad47c2c..6dfc8778fe1fc 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiTitle } from '@elastic/eui'; import { px, unit } from '../../../style/variables'; import { Stacktrace } from '.'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; // @ts-ignore Styled Components has trouble inferring the types of the default props here. const Accordion = styled(EuiAccordion)` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Context.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index d289539ca44b1..d48f4b4f51a6a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -22,7 +22,7 @@ import { registerLanguage } from 'react-syntax-highlighter/dist/light'; // @ts-ignore import { xcode } from 'react-syntax-highlighter/dist/styles'; import styled from 'styled-components'; -import { IStackframeWithLineContext } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, px, unit, units } from '../../../style/variables'; registerLanguage('javascript', javascript); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index daa722255bdf3..4467fe7ad615e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -7,7 +7,7 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import React, { Fragment } from 'react'; import styled from 'styled-components'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { fontFamilyCode, fontSize, px, units } from '../../../style/variables'; const FileDetails = styled.div` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx index be6595153aa77..009e97358428c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx @@ -8,7 +8,7 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { Stackframe } from './Stackframe'; import { px, unit } from '../../../style/variables'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx index 404d474a7960a..4c55add56bc40 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx @@ -11,7 +11,7 @@ import { EuiAccordion } from '@elastic/eui'; import { IStackframe, IStackframeWithLineContext -} from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +} from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, fontFamilyCode, diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index 0786116a659c7..ec5fb39f83f8c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -10,7 +10,7 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { borderRadius, px, unit, units } from '../../../style/variables'; -import { IStackframe } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { KeyValueTable } from '../KeyValueTable'; import { flattenObject } from '../../../utils/flattenObject'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx index 1b2268326e6be..478f9cfe921d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/Stackframe.test.tsx @@ -6,7 +6,7 @@ import { mount, ReactWrapper, shallow } from 'enzyme'; import React from 'react'; -import { IStackframe } from '../../../../../../../../plugins/apm/typings/es_schemas/raw/fields/stackframe'; +import { IStackframe } from '../../../../../typings/es_schemas/raw/fields/stackframe'; import { Stackframe } from '../Stackframe'; import stacktracesMock from './stacktraces.json'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap similarity index 99% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap index 0aeb2443679fa..4177b54f20385 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/Stackframe.test.tsx.snap @@ -192,7 +192,9 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] /> </EuiIcon> </span> - <span> + <span + className="euiIEFlexWrapFix" + > <FrameHeading isLibraryFrame={false} stackframe={ @@ -258,17 +260,8 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] className="euiAccordion__childWrapper" id="test" > - <EuiMutationObserver - observerOptions={ - Object { - "attributeFilter": Array [ - "style", - ], - "childList": true, - "subtree": true, - } - } - onMutation={[Function]} + <EuiResizeObserver + onResize={[Function]} > <div> <div @@ -1560,7 +1553,7 @@ exports[`Stackframe when stackframe has source lines should render correctly 1`] <Variables /> </div> </div> - </EuiMutationObserver> + </EuiResizeObserver> </div> </div> </EuiAccordion> diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/__snapshots__/index.test.ts.snap diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts new file mode 100644 index 0000000000000..22357b9590887 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/index.test.ts @@ -0,0 +1,173 @@ +/* + * 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 { IStackframe } from '../../../../../typings/es_schemas/raw/fields/stackframe'; +import { getGroupedStackframes } from '../index'; +import stacktracesMock from './stacktraces.json'; + +describe('Stacktrace/index', () => { + describe('getGroupedStackframes', () => { + it('should collapse the library frames into a set of grouped stackframes', () => { + const result = getGroupedStackframes(stacktracesMock as IStackframe[]); + expect(result).toMatchSnapshot(); + }); + + it('should group stackframes when `library_frame` is identical and `exclude_from_grouping` is false', () => { + const stackframes = [ + { + library_frame: false, + exclude_from_grouping: false, + filename: 'file-a.txt' + }, + { + library_frame: false, + exclude_from_grouping: false, + filename: 'file-b.txt' + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'file-c.txt' + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'file-d.txt' + } + ] as IStackframe[]; + + const result = getGroupedStackframes(stackframes); + + expect(result).toEqual([ + { + excludeFromGrouping: false, + isLibraryFrame: false, + stackframes: [ + { + exclude_from_grouping: false, + filename: 'file-a.txt', + library_frame: false + }, + { + exclude_from_grouping: false, + filename: 'file-b.txt', + library_frame: false + } + ] + }, + { + excludeFromGrouping: false, + isLibraryFrame: true, + stackframes: [ + { + exclude_from_grouping: false, + filename: 'file-c.txt', + library_frame: true + }, + { + exclude_from_grouping: false, + filename: 'file-d.txt', + library_frame: true + } + ] + } + ]); + }); + + it('should not group stackframes when `library_frame` is the different', () => { + const stackframes = [ + { + library_frame: false, + exclude_from_grouping: false, + filename: 'file-a.txt' + }, + { + library_frame: true, + exclude_from_grouping: false, + filename: 'file-b.txt' + } + ] as IStackframe[]; + const result = getGroupedStackframes(stackframes); + expect(result).toEqual([ + { + excludeFromGrouping: false, + isLibraryFrame: false, + stackframes: [ + { + exclude_from_grouping: false, + filename: 'file-a.txt', + library_frame: false + } + ] + }, + { + excludeFromGrouping: false, + isLibraryFrame: true, + stackframes: [ + { + exclude_from_grouping: false, + filename: 'file-b.txt', + library_frame: true + } + ] + } + ]); + }); + + it('should not group stackframes when `exclude_from_grouping` is true', () => { + const stackframes = [ + { + library_frame: false, + exclude_from_grouping: false, + filename: 'file-a.txt' + }, + { + library_frame: false, + exclude_from_grouping: true, + filename: 'file-b.txt' + } + ] as IStackframe[]; + const result = getGroupedStackframes(stackframes); + expect(result).toEqual([ + { + excludeFromGrouping: false, + isLibraryFrame: false, + stackframes: [ + { + exclude_from_grouping: false, + filename: 'file-a.txt', + library_frame: false + } + ] + }, + { + excludeFromGrouping: true, + isLibraryFrame: false, + stackframes: [ + { + exclude_from_grouping: true, + filename: 'file-b.txt', + library_frame: false + } + ] + } + ]); + }); + + it('should handle empty stackframes', () => { + const result = getGroupedStackframes([] as IStackframe[]); + expect(result).toHaveLength(0); + }); + + it('should handle one stackframe', () => { + const result = getGroupedStackframes([ + stacktracesMock[0] + ] as IStackframe[]); + expect(result).toHaveLength(1); + expect(result[0].stackframes).toHaveLength(1); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json b/x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json rename to x-pack/plugins/apm/public/components/shared/Stacktrace/__test__/stacktraces.json diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx new file mode 100644 index 0000000000000..b6435f7c42183 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.tsx @@ -0,0 +1,103 @@ +/* + * 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 { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty, last } from 'lodash'; +import React, { Fragment } from 'react'; +import { IStackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; +import { EmptyMessage } from '../../shared/EmptyMessage'; +import { LibraryStacktrace } from './LibraryStacktrace'; +import { Stackframe } from './Stackframe'; + +interface Props { + stackframes?: IStackframe[]; + codeLanguage?: string; +} + +export function Stacktrace({ stackframes = [], codeLanguage }: Props) { + if (isEmpty(stackframes)) { + return ( + <EmptyMessage + heading={i18n.translate( + 'xpack.apm.stacktraceTab.noStacktraceAvailableLabel', + { + defaultMessage: 'No stack trace available.' + } + )} + hideSubheading + /> + ); + } + + const groups = getGroupedStackframes(stackframes); + + return ( + <Fragment> + {groups.map((group, i) => { + // library frame + if (group.isLibraryFrame && groups.length > 1) { + return ( + <Fragment key={i}> + <EuiSpacer size="m" /> + <LibraryStacktrace + id={i.toString()} + stackframes={group.stackframes} + codeLanguage={codeLanguage} + /> + <EuiSpacer size="m" /> + </Fragment> + ); + } + + // non-library frame + return group.stackframes.map((stackframe, idx) => ( + <Fragment key={`${i}-${idx}`}> + {idx > 0 && <EuiSpacer size="m" />} + <Stackframe + codeLanguage={codeLanguage} + id={`${i}-${idx}`} + initialIsOpen={i === 0 && groups.length > 1} + stackframe={stackframe} + /> + </Fragment> + )); + })} + <EuiSpacer size="m" /> + </Fragment> + ); +} + +interface StackframesGroup { + isLibraryFrame: boolean; + excludeFromGrouping: boolean; + stackframes: IStackframe[]; +} + +export function getGroupedStackframes(stackframes: IStackframe[]) { + return stackframes.reduce((acc, stackframe) => { + const prevGroup = last(acc); + const shouldAppend = + prevGroup && + prevGroup.isLibraryFrame === stackframe.library_frame && + !prevGroup.excludeFromGrouping && + !stackframe.exclude_from_grouping; + + // append to group + if (shouldAppend) { + prevGroup.stackframes.push(stackframe); + return acc; + } + + // create new group + acc.push({ + isLibraryFrame: Boolean(stackframe.library_frame), + excludeFromGrouping: Boolean(stackframe.exclude_from_grouping), + stackframes: [stackframe] + }); + return acc; + }, [] as StackframesGroup[]); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js b/x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js rename to x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js index 08283dee3825d..b6acb6904f865 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js @@ -7,10 +7,7 @@ import React from 'react'; import { StickyProperties } from './index'; import { shallow } from 'enzyme'; -import { - USER_ID, - URL_FULL -} from '../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { USER_ID, URL_FULL } from '../../../../common/elasticsearch_fieldnames'; import { mockMoment } from '../../../utils/testHelpers'; describe('StickyProperties', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap b/x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap rename to x-pack/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx rename to x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/DurationSummaryItem.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 7558f002c0afc..2be3c82a8385b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -15,9 +15,9 @@ interface Props { count: number; } -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` margin-top: ${px(units.eighth)}; -`; +` as unknown) as typeof EuiBadge; export const ErrorCountSummaryItemBadge = ({ count }: Props) => ( <Badge color={euiThemeLight.euiColorDanger}> diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/__test__/HttpInfoSummaryItem.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx new file mode 100644 index 0000000000000..d499dddeeb8b3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiToolTip, EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { units, px, truncate, unit } from '../../../../style/variables'; +import { HttpStatusBadge } from '../HttpStatusBadge'; + +const HttpInfoBadge = (styled(EuiBadge)` + margin-right: ${px(units.quarter)}; +` as unknown) as typeof EuiBadge; + +const Url = styled('span')` + display: inline-block; + vertical-align: bottom; + ${truncate(px(unit * 24))}; +`; +interface HttpInfoProps { + method?: string; + status?: number; + url: string; +} + +const Span = styled('span')` + white-space: nowrap; +`; + +export function HttpInfoSummaryItem({ status, method, url }: HttpInfoProps) { + if (!url) { + return null; + } + + const methodLabel = i18n.translate( + 'xpack.apm.transactionDetails.requestMethodLabel', + { + defaultMessage: 'Request method' + } + ); + + return ( + <Span> + <HttpInfoBadge title={undefined}> + {method && ( + <EuiToolTip content={methodLabel}> + <>{method.toUpperCase()}</> + </EuiToolTip> + )}{' '} + <EuiToolTip content={url}> + <Url>{url}</Url> + </EuiToolTip> + </HttpInfoBadge> + {status && <HttpStatusBadge status={status} />} + </Span> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/__test__/HttpStatusBadge.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts b/x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts rename to x-pack/plugins/apm/public/components/shared/Summary/HttpStatusBadge/statusCodes.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/TransactionResultSummaryItem.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx similarity index 91% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index f0fe57e46f2fe..f24a806426510 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { Summary } from './'; import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; import { ErrorCountSummaryItemBadge } from './ErrorCountSummaryItemBadge'; -import { isRumAgentName } from '../../../../../../../plugins/apm/common/agent_name'; +import { isRumAgentName } from '../../../../common/agent_name'; import { HttpInfoSummaryItem } from './HttpInfoSummaryItem'; import { TransactionResultSummaryItem } from './TransactionResultSummaryItem'; import { UserAgentSummaryItem } from './UserAgentSummaryItem'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx index 10a6bcc1ef7bd..8173170b72f23 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx @@ -9,7 +9,7 @@ import styled from 'styled-components'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { UserAgent } from '../../../../../../../plugins/apm/typings/es_schemas/raw/fields/user_agent'; +import { UserAgent } from '../../../../typings/es_schemas/raw/fields/user_agent'; type UserAgentSummaryItemProps = UserAgent; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts b/x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts rename to x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts index 05fb73a9e2749..e1615934cd92e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts +++ b/x-pack/plugins/apm/public/components/shared/Summary/__fixtures__/transactions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; export const httpOk: Transaction = { '@timestamp': '0', diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx b/x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx rename to x-pack/plugins/apm/public/components/shared/Summary/__test__/ErrorCountSummaryItemBadge.test.tsx diff --git a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx new file mode 100644 index 0000000000000..ce6935d1858aa --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx @@ -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 React from 'react'; +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { px, units } from '../../../../public/style/variables'; +import { Maybe } from '../../../../typings/common'; + +interface Props { + items: Array<Maybe<React.ReactElement>>; +} + +// TODO: Light/Dark theme (@see https://github.com/elastic/kibana/issues/44840) +const theme = euiLightVars; + +const Item = styled(EuiFlexItem)` + flex-wrap: nowrap; + border-right: 1px solid ${theme.euiColorLightShade}; + padding-right: ${px(units.half)}; + flex-flow: row nowrap; + line-height: 1.5; + align-items: center !important; + &:last-child { + border-right: none; + padding-right: 0; + } +`; + +const Summary = ({ items }: Props) => { + const filteredItems = items.filter(Boolean) as React.ReactElement[]; + + return ( + <EuiFlexGrid gutterSize="s"> + {filteredItems.map((item, index) => ( + <Item key={index} grow={false}> + {item} + </Item> + ))} + </EuiFlexGrid> + ); +}; + +export { Summary }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/TimestampTooltip/__test__/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TimestampTooltip/index.tsx rename to x-pack/plugins/apm/public/components/shared/TimestampTooltip/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx similarity index 91% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx index 8df6d952cfacd..49f8fddb303bd 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.test.tsx @@ -5,10 +5,10 @@ */ import React from 'react'; import { render, act, fireEvent } from '@testing-library/react'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLinkPopover } from './CustomLinkPopover'; import { expectTextsInDocument } from '../../../../utils/testHelpers'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; describe('CustomLinkPopover', () => { const customLinks = [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx similarity index 90% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx index 3aed1b7ac2953..a63c226a5c46e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkPopover.tsx @@ -12,8 +12,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { CustomLinkSection } from './CustomLinkSection'; import { ManageCustomLink } from './ManageCustomLink'; import { px } from '../../../../style/variables'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx similarity index 85% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx index d429fa56894eb..6cf8b9ee5e98a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.test.tsx @@ -10,8 +10,8 @@ import { expectTextsInDocument, expectTextsNotInDocument } from '../../../../utils/testHelpers'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; describe('CustomLinkSection', () => { const customLinks = [ diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx similarity index 86% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx index bd00bcf600ffe..e22f4b4a37745 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/CustomLinkSection.tsx @@ -7,8 +7,8 @@ import { EuiLink, EuiText } from '@elastic/eui'; import Mustache from 'mustache'; import React from 'react'; import styled from 'styled-components'; -import { CustomLink } from '../../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { CustomLink } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { px, truncate, units } from '../../../../style/variables'; const LinkContainer = styled.li` diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/ManageCustomLink.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx new file mode 100644 index 0000000000000..c7a2d77d85fa6 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.test.tsx @@ -0,0 +1,128 @@ +/* + * 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 { render, act, fireEvent } from '@testing-library/react'; +import { CustomLink } from '.'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { + expectTextsInDocument, + expectTextsNotInDocument +} from '../../../../utils/testHelpers'; +import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; + +describe('Custom links', () => { + it('shows empty message when no custom link is available', () => { + const component = render( + <CustomLink + customLinks={[]} + transaction={({} as unknown) as Transaction} + onCreateCustomLinkClick={jest.fn()} + onSeeMoreClick={jest.fn()} + status={FETCH_STATUS.SUCCESS} + /> + ); + + expectTextsInDocument(component, [ + 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.' + ]); + expectTextsNotInDocument(component, ['Create']); + }); + + it('shows loading while custom links are fetched', () => { + const { getByTestId } = render( + <CustomLink + customLinks={[]} + transaction={({} as unknown) as Transaction} + onCreateCustomLinkClick={jest.fn()} + onSeeMoreClick={jest.fn()} + status={FETCH_STATUS.LOADING} + /> + ); + expect(getByTestId('loading-spinner')).toBeInTheDocument(); + }); + + it('shows first 3 custom links available', () => { + const customLinks = [ + { id: '1', label: 'foo', url: 'foo' }, + { id: '2', label: 'bar', url: 'bar' }, + { id: '3', label: 'baz', url: 'baz' }, + { id: '4', label: 'qux', url: 'qux' } + ] as CustomLinkType[]; + const component = render( + <CustomLink + customLinks={customLinks} + transaction={({} as unknown) as Transaction} + onCreateCustomLinkClick={jest.fn()} + onSeeMoreClick={jest.fn()} + status={FETCH_STATUS.SUCCESS} + /> + ); + expectTextsInDocument(component, ['foo', 'bar', 'baz']); + expectTextsNotInDocument(component, ['qux']); + }); + + it('clicks on See more button', () => { + const customLinks = [ + { id: '1', label: 'foo', url: 'foo' }, + { id: '2', label: 'bar', url: 'bar' }, + { id: '3', label: 'baz', url: 'baz' }, + { id: '4', label: 'qux', url: 'qux' } + ] as CustomLinkType[]; + const onSeeMoreClickMock = jest.fn(); + const component = render( + <CustomLink + customLinks={customLinks} + transaction={({} as unknown) as Transaction} + onCreateCustomLinkClick={jest.fn()} + onSeeMoreClick={onSeeMoreClickMock} + status={FETCH_STATUS.SUCCESS} + /> + ); + expect(onSeeMoreClickMock).not.toHaveBeenCalled(); + act(() => { + fireEvent.click(component.getByText('See more')); + }); + expect(onSeeMoreClickMock).toHaveBeenCalled(); + }); + + describe('create custom link buttons', () => { + it('shows create button below empty message', () => { + const component = render( + <CustomLink + customLinks={[]} + transaction={({} as unknown) as Transaction} + onCreateCustomLinkClick={jest.fn()} + onSeeMoreClick={jest.fn()} + status={FETCH_STATUS.SUCCESS} + /> + ); + + expectTextsInDocument(component, ['Create custom link']); + expectTextsNotInDocument(component, ['Create']); + }); + it('shows create button besides the title', () => { + const customLinks = [ + { id: '1', label: 'foo', url: 'foo' }, + { id: '2', label: 'bar', url: 'bar' }, + { id: '3', label: 'baz', url: 'baz' }, + { id: '4', label: 'qux', url: 'qux' } + ] as CustomLinkType[]; + const component = render( + <CustomLink + customLinks={customLinks} + transaction={({} as unknown) as Transaction} + onCreateCustomLinkClick={jest.fn()} + onSeeMoreClick={jest.fn()} + status={FETCH_STATUS.SUCCESS} + /> + ); + expectTextsInDocument(component, ['Create']); + expectTextsNotInDocument(component, ['Create custom link']); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx new file mode 100644 index 0000000000000..710b2175e3377 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/CustomLink/index.tsx @@ -0,0 +1,128 @@ +/* + * 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 { + EuiText, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiButtonEmpty +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash'; +import { CustomLink as CustomLinkType } from '../../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; +import { + ActionMenuDivider, + SectionSubtitle +} from '../../../../../../observability/public'; +import { CustomLinkSection } from './CustomLinkSection'; +import { ManageCustomLink } from './ManageCustomLink'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { LoadingStatePrompt } from '../../LoadingStatePrompt'; +import { px } from '../../../../style/variables'; + +const SeeMoreButton = styled.button<{ show: boolean }>` + display: ${props => (props.show ? 'flex' : 'none')}; + align-items: center; + width: 100%; + justify-content: space-between; + &:hover { + text-decoration: underline; + } +`; + +export const CustomLink = ({ + customLinks, + status, + onCreateCustomLinkClick, + onSeeMoreClick, + transaction +}: { + customLinks: CustomLinkType[]; + status: FETCH_STATUS; + onCreateCustomLinkClick: () => void; + onSeeMoreClick: () => void; + transaction: Transaction; +}) => { + const renderEmptyPrompt = ( + <> + <EuiText size="xs" grow={false} style={{ width: px(300) }}> + {i18n.translate('xpack.apm.customLink.empty', { + defaultMessage: + 'No custom links found. Set up your own custom links, e.g., a link to a specific Dashboard or external link.' + })} + </EuiText> + <EuiSpacer size="s" /> + <EuiButtonEmpty + iconType="plusInCircle" + size="xs" + onClick={onCreateCustomLinkClick} + > + {i18n.translate('xpack.apm.customLink.buttom.create', { + defaultMessage: 'Create custom link' + })} + </EuiButtonEmpty> + </> + ); + + const renderCustomLinkBottomSection = isEmpty(customLinks) ? ( + renderEmptyPrompt + ) : ( + <SeeMoreButton onClick={onSeeMoreClick} show={customLinks.length > 3}> + <EuiText size="s"> + {i18n.translate('xpack.apm.transactionActionMenu.customLink.seeMore', { + defaultMessage: 'See more' + })} + </EuiText> + <EuiIcon type="arrowRight" /> + </SeeMoreButton> + ); + + return ( + <> + <ActionMenuDivider /> + <EuiFlexGroup> + <EuiFlexItem style={{ justifyContent: 'center' }}> + <EuiText size={'s'} grow={false}> + <h5> + {i18n.translate( + 'xpack.apm.transactionActionMenu.customLink.section', + { + defaultMessage: 'Custom Links' + } + )} + </h5> + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <ManageCustomLink + onCreateCustomLinkClick={onCreateCustomLinkClick} + showCreateCustomLinkButton={!!customLinks.length} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="s" /> + <SectionSubtitle> + {i18n.translate('xpack.apm.transactionActionMenu.customLink.subtitle', { + defaultMessage: 'Links will open in a new window.' + })} + </SectionSubtitle> + <CustomLinkSection + customLinks={customLinks.slice(0, 3)} + transaction={transaction} + /> + <EuiSpacer size="s" /> + {status === FETCH_STATUS.LOADING ? ( + <LoadingStatePrompt /> + ) : ( + renderCustomLinkBottomSection + )} + </> + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 7ebfe26b83630..c9376cdc01b5b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -7,8 +7,8 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FunctionComponent, useMemo, useState } from 'react'; -import { Filter } from '../../../../../../../plugins/apm/common/custom_link/custom_link_types'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Filter } from '../../../../common/custom_link/custom_link_types'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { ActionMenu, ActionMenuDivider, @@ -17,7 +17,7 @@ import { SectionLinks, SectionSubtitle, SectionTitle -} from '../../../../../../../plugins/observability/public'; +} from '../../../../../observability/public'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLocation } from '../../../hooks/useLocation'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index ce42bd3e39ad1..cda602204469c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import { render, fireEvent, act } from '@testing-library/react'; +import { render, fireEvent, act, wait } from '@testing-library/react'; import { TransactionActionMenu } from '../TransactionActionMenu'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import * as Transactions from './mockData'; import { expectTextsNotInDocument, @@ -15,7 +15,7 @@ import { } from '../../../../utils/testHelpers'; import * as hooks from '../../../../hooks/useFetcher'; import { LicenseContext } from '../../../../context/LicenseContext'; -import { License } from '../../../../../../../../plugins/licensing/common/license'; +import { License } from '../../../../../../licensing/common/license'; import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; import * as apmApi from '../../../../services/rest/createCallApmApi'; @@ -143,8 +143,9 @@ describe('TransactionActionMenu component', () => { }); describe('Custom links', () => { + let callApmApiSpy: jasmine.Spy; beforeAll(() => { - spyOn(apmApi, 'callApmApi').and.returnValue({}); + callApmApiSpy = spyOn(apmApi, 'callApmApi').and.returnValue({}); }); afterAll(() => { jest.resetAllMocks(); @@ -257,7 +258,7 @@ describe('TransactionActionMenu component', () => { }); expectTextsInDocument(component, ['Custom Links']); }); - it('opens flyout with filters prefilled', () => { + it('opens flyout with filters prefilled', async () => { const license = new License({ signature: 'test signature', license: { @@ -287,6 +288,7 @@ describe('TransactionActionMenu component', () => { fireEvent.click(component.getByText('Create custom link')); }); expectTextsInDocument(component, ['Create link']); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); const getFilterKeyValue = (key: string) => { return { [(component.getAllByText(key)[0] as HTMLOptionElement) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/mockData.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts index 3032dd1704f4e..b2f6f39e0b596 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/__test__/sections.test.ts @@ -5,7 +5,7 @@ */ import { Location } from 'history'; import { getSections } from '../sections'; -import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { AppMountContextBasePath } from '../../../../context/ApmPluginContext'; describe('Transaction action menu', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts similarity index 98% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts rename to x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts index ffdf0b485da64..2c2f4bfcadd7d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/TransactionActionMenu/sections.ts @@ -8,7 +8,7 @@ import { Location } from 'history'; import { pick, isEmpty } from 'lodash'; import moment from 'moment'; import url from 'url'; -import { Transaction } from '../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; +import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; import { getDiscoverHref } from '../Links/DiscoverLinks/DiscoverLink'; import { getDiscoverQuery } from '../Links/DiscoverLinks/DiscoverTransactionLink'; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx new file mode 100644 index 0000000000000..966cc64fde505 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownGraph/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import numeral from '@elastic/numeral'; +import { throttle } from 'lodash'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { Maybe } from '../../../../../typings/common'; +import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart'; +import { asPercent } from '../../../../utils/formatters'; +import { unit } from '../../../../style/variables'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { useUiTracker } from '../../../../../../observability/public'; + +interface Props { + timeseries: TimeSeries[]; +} + +const tickFormatY = (y: Maybe<number>) => { + return numeral(y || 0).format('0 %'); +}; + +const formatTooltipValue = (coordinate: Coordinate) => { + return isValidCoordinateValue(coordinate.y) + ? asPercent(coordinate.y, 1) + : NOT_AVAILABLE_LABEL; +}; + +const TransactionBreakdownGraph: React.FC<Props> = props => { + const { timeseries } = props; + const trackApmEvent = useUiTracker({ app: 'apm' }); + const handleHover = useMemo( + () => + throttle(() => trackApmEvent({ metric: 'hover_breakdown_chart' }), 60000), + [trackApmEvent] + ); + + return ( + <TransactionLineChart + series={timeseries} + tickFormatY={tickFormatY} + formatTooltipValue={formatTooltipValue} + yMax={1} + height={unit * 12} + stacked={true} + onHover={handleHover} + /> + ); +}; + +export { TransactionBreakdownGraph }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownHeader.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx similarity index 95% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx index 91f5f4e0a7176..c4a8e07fb3004 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/TransactionBreakdownKpiList.tsx @@ -13,10 +13,7 @@ import { EuiIcon } from '@elastic/eui'; import styled from 'styled-components'; -import { - FORMATTERS, - InfraFormatterType -} from '../../../../../../../plugins/infra/public'; +import { FORMATTERS, InfraFormatterType } from '../../../../../infra/public'; interface TransactionBreakdownKpi { name: string; diff --git a/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx new file mode 100644 index 0000000000000..be5860190c11e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionBreakdown/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown'; +import { TransactionBreakdownHeader } from './TransactionBreakdownHeader'; +import { TransactionBreakdownKpiList } from './TransactionBreakdownKpiList'; +import { TransactionBreakdownGraph } from './TransactionBreakdownGraph'; +import { FETCH_STATUS } from '../../../hooks/useFetcher'; +import { useUiTracker } from '../../../../../observability/public'; + +const emptyMessage = i18n.translate('xpack.apm.transactionBreakdown.noData', { + defaultMessage: 'No data within this time range.' +}); + +const TransactionBreakdown: React.FC<{ + initialIsOpen?: boolean; +}> = ({ initialIsOpen }) => { + const [showChart, setShowChart] = useState(!!initialIsOpen); + const { data, status } = useTransactionBreakdown(); + const trackApmEvent = useUiTracker({ app: 'apm' }); + const { kpis, timeseries } = data; + const noHits = data.kpis.length === 0 && status === FETCH_STATUS.SUCCESS; + const showEmptyMessage = noHits && !showChart; + + return ( + <EuiPanel> + <EuiFlexGroup direction="column" gutterSize="s"> + <EuiFlexItem grow={false}> + <TransactionBreakdownHeader + showChart={showChart} + onToggleClick={() => { + setShowChart(!showChart); + if (showChart) { + trackApmEvent({ metric: 'hide_breakdown_chart' }); + } else { + trackApmEvent({ metric: 'show_breakdown_chart' }); + } + }} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {showEmptyMessage ? ( + <EuiText>{emptyMessage}</EuiText> + ) : ( + <TransactionBreakdownKpiList kpis={kpis} /> + )} + </EuiFlexItem> + {showChart ? ( + <EuiFlexItem grow={false}> + <TransactionBreakdownGraph timeseries={timeseries} /> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + </EuiPanel> + ); +}; + +export { TransactionBreakdown }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.stories.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx new file mode 100644 index 0000000000000..1e9fbd2c1c135 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAlertTrigger/index.tsx @@ -0,0 +1,149 @@ +/* + * 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 { map } from 'lodash'; +import { EuiFieldNumber, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; +import { + TRANSACTION_ALERT_AGGREGATION_TYPES, + ALERT_TYPES_CONFIG +} from '../../../../common/alert_types'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; +import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; + +interface Params { + windowSize: number; + windowUnit: string; + threshold: number; + aggregationType: 'avg' | '95th' | '99th'; + serviceName: string; + transactionType: string; +} + +interface Props { + alertParams: Params; + setAlertParams: (key: string, value: any) => void; + setAlertProperty: (key: string, value: any) => void; +} + +export function TransactionDurationAlertTrigger(props: Props) { + const { setAlertParams, alertParams, setAlertProperty } = props; + + const { urlParams } = useUrlParams(); + + const transactionTypes = useServiceTransactionTypes(urlParams); + + if (!transactionTypes.length) { + return null; + } + + const defaults = { + threshold: 1500, + aggregationType: 'avg', + windowSize: 5, + windowUnit: 'm', + transactionType: transactionTypes[0] + }; + + const params = { + ...defaults, + ...alertParams + }; + + const fields = [ + <PopoverExpression + value={params.transactionType} + title={i18n.translate('xpack.apm.transactionDurationAlertTrigger.type', { + defaultMessage: 'Type' + })} + > + <EuiSelect + value={params.transactionType} + options={transactionTypes.map(key => { + return { + text: key, + value: key + }; + })} + onChange={e => + setAlertParams( + 'transactionType', + e.target.value as Params['transactionType'] + ) + } + compressed + /> + </PopoverExpression>, + <PopoverExpression + value={params.aggregationType} + title={i18n.translate('xpack.apm.transactionDurationAlertTrigger.when', { + defaultMessage: 'When' + })} + > + <EuiSelect + value={params.aggregationType} + options={map(TRANSACTION_ALERT_AGGREGATION_TYPES, (label, key) => { + return { + text: label, + value: key + }; + })} + onChange={e => + setAlertParams( + 'aggregationType', + e.target.value as Params['aggregationType'] + ) + } + compressed + /> + </PopoverExpression>, + <PopoverExpression + value={params.threshold ? `${params.threshold}ms` : ''} + title={i18n.translate( + 'xpack.apm.transactionDurationAlertTrigger.isAbove', + { + defaultMessage: 'is above' + } + )} + > + <EuiFieldNumber + value={params.threshold ?? ''} + onChange={e => setAlertParams('threshold', e.target.value)} + append={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', { + defaultMessage: 'ms' + })} + compressed + /> + </PopoverExpression>, + <ForLastExpression + onChangeWindowSize={timeWindowSize => + setAlertParams('windowSize', timeWindowSize || '') + } + onChangeWindowUnit={timeWindowUnit => + setAlertParams('windowUnit', timeWindowUnit) + } + timeWindowSize={params.windowSize} + timeWindowUnit={params.windowUnit} + errors={{ + timeWindowSize: [], + timeWindowUnit: [] + }} + /> + ]; + + return ( + <ServiceAlertTrigger + alertTypeName={ALERT_TYPES_CONFIG['apm.transaction_duration'].name} + fields={fields} + defaults={defaults} + setAlertParams={setAlertParams} + setAlertProperty={setAlertProperty} + /> + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx similarity index 92% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx index ec6168df5b134..6eff4759b2e7c 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/AnnotationsPlot.tsx @@ -14,8 +14,8 @@ import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Maybe } from '../../../../../../../../plugins/apm/typings/common'; -import { Annotation } from '../../../../../../../../plugins/apm/common/annotations'; +import { Maybe } from '../../../../../typings/common'; +import { Annotation } from '../../../../../common/annotations'; import { PlotValues, SharedPlot } from './plotUtils'; import { asAbsoluteDateTime } from '../../../../utils/formatters'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/Legends.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/SelectionMarker.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StaticPlot.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/StatusText.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/VoronoiPlot.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getEmptySeries.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/getTimezoneOffsetInMs.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/index.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts index bfc5c7c243f31..b130deed7f098 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.test.ts @@ -6,10 +6,7 @@ // @ts-ignore import * as plotUtils from './plotUtils'; -import { - TimeSeries, - Coordinate -} from '../../../../../../../../plugins/apm/typings/timeseries'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; describe('plotUtils', () => { describe('getPlotValues', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx index c489c270d19ac..64350a5741647 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/plotUtils.tsx @@ -11,10 +11,7 @@ import d3 from 'd3'; import PropTypes from 'prop-types'; import React from 'react'; -import { - TimeSeries, - Coordinate -} from '../../../../../../../../plugins/apm/typings/timeseries'; +import { TimeSeries, Coordinate } from '../../../../../typings/timeseries'; import { unit } from '../../../../style/variables'; import { getDomainTZ, getTimeTicksTZ } from '../helper/timezone'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/CustomPlot.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json rename to x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/responseWithData.json diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/SingleRect.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/response.json diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Histogram/index.js rename to x-pack/plugins/apm/public/components/shared/charts/Histogram/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Legend/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx new file mode 100644 index 0000000000000..862f2a8987067 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/MetricsChart/index.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { EuiTitle } from '@elastic/eui'; +import React from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart'; +// @ts-ignore +import CustomPlot from '../CustomPlot'; +import { + asDecimal, + asPercent, + asInteger, + asDynamicBytes, + getFixedByteFormatter, + asDuration +} from '../../../../utils/formatters'; +import { Coordinate } from '../../../../../typings/timeseries'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { useChartsSync } from '../../../../hooks/useChartsSync'; +import { Maybe } from '../../../../../typings/common'; + +interface Props { + start: Maybe<number | string>; + end: Maybe<number | string>; + chart: GenericMetricsChart; +} + +export function MetricsChart({ chart }: Props) { + const formatYValue = getYTickFormatter(chart); + const formatTooltip = getTooltipFormatter(chart); + + const transformedSeries = chart.series.map(series => ({ + ...series, + legendValue: formatYValue(series.overallValue) + })); + + const syncedChartProps = useChartsSync(); + + return ( + <React.Fragment> + <EuiTitle size="xs"> + <span>{chart.title}</span> + </EuiTitle> + <CustomPlot + {...syncedChartProps} + series={transformedSeries} + tickFormatY={formatYValue} + formatTooltipValue={formatTooltip} + yMax={chart.yUnit === 'percent' ? 1 : 'max'} + /> + </React.Fragment> + ); +} + +function getYTickFormatter(chart: GenericMetricsChart) { + switch (chart.yUnit) { + case 'bytes': { + const max = Math.max( + ...chart.series.map(({ data }) => + Math.max(...data.map(({ y }) => y || 0)) + ) + ); + return getFixedByteFormatter(max); + } + case 'percent': { + return (y: Maybe<number>) => asPercent(y || 0, 1); + } + case 'time': { + return (y: Maybe<number>) => asDuration(y); + } + case 'integer': { + return (y: Maybe<number>) => + isValidCoordinateValue(y) ? asInteger(y) : y; + } + default: { + return (y: Maybe<number>) => + isValidCoordinateValue(y) ? asDecimal(y) : y; + } + } +} + +function getTooltipFormatter({ yUnit }: GenericMetricsChart) { + switch (yUnit) { + case 'bytes': { + return (c: Coordinate) => asDynamicBytes(c.y); + } + case 'percent': { + return (c: Coordinate) => asPercent(c.y || 0, 1); + } + case 'time': { + return (c: Coordinate) => asDuration(c.y); + } + case 'integer': { + return (c: Coordinate) => + isValidCoordinateValue(c.y) ? asInteger(c.y) : c.y; + } + default: { + return (c: Coordinate) => + isValidCoordinateValue(c.y) ? asDecimal(c.y) : c.y; + } + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/LastTickValue.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx similarity index 97% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index 48265ce7c80a8..51368a4fb946d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { TRACE_ID, TRANSACTION_ID -} from '../../../../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../../../../common/elasticsearch_fieldnames'; import { useUrlParams } from '../../../../../hooks/useUrlParams'; import { px, unit, units } from '../../../../../style/variables'; import { asDuration } from '../../../../../utils/formatters'; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/AgentMarker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/ErrorMarker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/Marker.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/AgentMarker.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/ErrorMarker.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/__test__/__snapshots__/Marker.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/Timeline.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/TimelineAxis.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/VerticalLines.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/__snapshots__/Timeline.test.tsx.snap diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts b/x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts rename to x-pack/plugins/apm/public/components/shared/charts/Timeline/plotUtils.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js b/x-pack/plugins/apm/public/components/shared/charts/Tooltip/index.js similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/Tooltip/index.js rename to x-pack/plugins/apm/public/components/shared/charts/Tooltip/index.js diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx rename to x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx new file mode 100644 index 0000000000000..27c829f63cf0a --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/TransactionLineChart/index.tsx @@ -0,0 +1,73 @@ +/* + * 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, { useCallback } from 'react'; +import { + Coordinate, + RectCoordinate +} from '../../../../../../typings/timeseries'; +import { useChartsSync } from '../../../../../hooks/useChartsSync'; +// @ts-ignore +import CustomPlot from '../../CustomPlot'; + +interface Props { + series: Array<{ + color: string; + title: React.ReactNode; + titleShort?: React.ReactNode; + data: Array<Coordinate | RectCoordinate>; + type: string; + }>; + truncateLegends?: boolean; + tickFormatY: (y: number) => React.ReactNode; + formatTooltipValue: (c: Coordinate) => React.ReactNode; + yMax?: string | number; + height?: number; + stacked?: boolean; + onHover?: () => void; +} + +const TransactionLineChart: React.FC<Props> = (props: Props) => { + const { + series, + tickFormatY, + formatTooltipValue, + yMax = 'max', + height, + truncateLegends, + stacked = false, + onHover + } = props; + + const syncedChartsProps = useChartsSync(); + + // combine callback for syncedChartsProps.onHover and props.onHover + const combinedOnHover = useCallback( + (hoverX: number) => { + if (onHover) { + onHover(); + } + return syncedChartsProps.onHover(hoverX); + }, + [syncedChartsProps, onHover] + ); + + return ( + <CustomPlot + series={series} + {...syncedChartsProps} + onHover={combinedOnHover} + tickFormatY={tickFormatY} + formatTooltipValue={formatTooltipValue} + yMax={yMax} + height={height} + truncateLegends={truncateLegends} + {...(stacked ? { stackBy: 'y' } : {})} + /> + ); +}; + +export { TransactionLineChart }; diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx new file mode 100644 index 0000000000000..b0555da705a30 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -0,0 +1,266 @@ +/* + * 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 { + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiPanel, + EuiText, + EuiTitle, + EuiSpacer +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Location } from 'history'; +import React, { Component } from 'react'; +import { isEmpty, flatten } from 'lodash'; +import styled from 'styled-components'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; +import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { ITransactionChartData } from '../../../../selectors/chartSelectors'; +import { IUrlParams } from '../../../../context/UrlParamsContext/types'; +import { + asInteger, + tpmUnit, + TimeFormatter, + getDurationFormatter +} from '../../../../utils/formatters'; +import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink'; +import { LicenseContext } from '../../../../context/LicenseContext'; +import { TransactionLineChart } from './TransactionLineChart'; +import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; +import { BrowserLineChart } from './BrowserLineChart'; +import { DurationByCountryMap } from './DurationByCountryMap'; +import { + TRANSACTION_PAGE_LOAD, + TRANSACTION_ROUTE_CHANGE, + TRANSACTION_REQUEST +} from '../../../../../common/transaction_types'; + +interface TransactionChartProps { + hasMLJob: boolean; + charts: ITransactionChartData; + location: Location; + urlParams: IUrlParams; +} + +const ShiftedIconWrapper = styled.span` + padding-right: 5px; + position: relative; + top: -1px; + display: inline-block; +`; + +const ShiftedEuiText = styled(EuiText)` + position: relative; + top: 5px; +`; + +export function getResponseTimeTickFormatter(formatter: TimeFormatter) { + return (t: number) => formatter(t).formatted; +} + +export function getResponseTimeTooltipFormatter(formatter: TimeFormatter) { + return (p: Coordinate) => { + return isValidCoordinateValue(p.y) + ? formatter(p.y).formatted + : NOT_AVAILABLE_LABEL; + }; +} + +export function getMaxY(responseTimeSeries: TimeSeries[]) { + const coordinates = flatten( + responseTimeSeries.map((serie: TimeSeries) => serie.data as Coordinate[]) + ); + + const numbers: number[] = coordinates.map((c: Coordinate) => (c.y ? c.y : 0)); + + return Math.max(...numbers, 0); +} + +export class TransactionCharts extends Component<TransactionChartProps> { + public getTPMFormatter = (t: number) => { + const { urlParams } = this.props; + const unit = tpmUnit(urlParams.transactionType); + return `${asInteger(t)} ${unit}`; + }; + + public getTPMTooltipFormatter = (p: Coordinate) => { + return isValidCoordinateValue(p.y) + ? this.getTPMFormatter(p.y) + : NOT_AVAILABLE_LABEL; + }; + + public renderMLHeader(hasValidMlLicense: boolean | undefined) { + const { hasMLJob } = this.props; + if (!hasValidMlLicense || !hasMLJob) { + return null; + } + + const { serviceName, transactionType, kuery } = this.props.urlParams; + if (!serviceName) { + return null; + } + + const hasKuery = !isEmpty(kuery); + const icon = hasKuery ? ( + <EuiIconTip + aria-label="Warning" + type="alert" + color="warning" + content="The Machine learning results are hidden when the search bar is used for filtering" + /> + ) : ( + <EuiIconTip + content={i18n.translate( + 'xpack.apm.metrics.transactionChart.machineLearningTooltip', + { + defaultMessage: + 'The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores >= 75.' + } + )} + /> + ); + + return ( + <EuiFlexItem grow={false}> + <ShiftedEuiText size="xs"> + <ShiftedIconWrapper>{icon}</ShiftedIconWrapper> + <span> + {i18n.translate( + 'xpack.apm.metrics.transactionChart.machineLearningLabel', + { + defaultMessage: 'Machine learning:' + } + )}{' '} + </span> + <MLJobLink + serviceName={serviceName} + transactionType={transactionType} + > + View Job + </MLJobLink> + </ShiftedEuiText> + </EuiFlexItem> + ); + } + + public render() { + const { charts, urlParams } = this.props; + const { responseTimeSeries, tpmSeries } = charts; + const { transactionType } = urlParams; + const maxY = getMaxY(responseTimeSeries); + const formatter = getDurationFormatter(maxY); + + return ( + <> + <EuiFlexGrid columns={2} gutterSize="s"> + <EuiFlexItem data-cy={`transaction-duration-charts`}> + <EuiPanel> + <React.Fragment> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiTitle size="xs"> + <span>{responseTimeLabel(transactionType)}</span> + </EuiTitle> + </EuiFlexItem> + <LicenseContext.Consumer> + {license => + this.renderMLHeader(license?.getFeature('ml').isAvailable) + } + </LicenseContext.Consumer> + </EuiFlexGroup> + <TransactionLineChart + series={responseTimeSeries} + tickFormatY={getResponseTimeTickFormatter(formatter)} + formatTooltipValue={getResponseTimeTooltipFormatter( + formatter + )} + /> + </React.Fragment> + </EuiPanel> + </EuiFlexItem> + + <EuiFlexItem style={{ flexShrink: 1 }}> + <EuiPanel> + <React.Fragment> + <EuiTitle size="xs"> + <span>{tpmLabel(transactionType)}</span> + </EuiTitle> + <TransactionLineChart + series={tpmSeries} + tickFormatY={this.getTPMFormatter} + formatTooltipValue={this.getTPMTooltipFormatter} + truncateLegends + /> + </React.Fragment> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGrid> + {transactionType === TRANSACTION_PAGE_LOAD && ( + <> + <EuiSpacer size="s" /> + <EuiFlexGrid columns={2} gutterSize="s"> + <EuiFlexItem> + <EuiPanel> + <DurationByCountryMap /> + </EuiPanel> + </EuiFlexItem> + <EuiFlexItem> + <EuiPanel> + <BrowserLineChart /> + </EuiPanel> + </EuiFlexItem> + </EuiFlexGrid> + </> + )} + </> + ); + } +} + +function tpmLabel(type?: string) { + return type === TRANSACTION_REQUEST + ? i18n.translate( + 'xpack.apm.metrics.transactionChart.requestsPerMinuteLabel', + { + defaultMessage: 'Requests per minute' + } + ) + : i18n.translate( + 'xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel', + { + defaultMessage: 'Transactions per minute' + } + ); +} + +function responseTimeLabel(type?: string) { + switch (type) { + case TRANSACTION_PAGE_LOAD: + return i18n.translate( + 'xpack.apm.metrics.transactionChart.pageLoadTimesLabel', + { + defaultMessage: 'Page load times' + } + ); + case TRANSACTION_ROUTE_CHANGE: + return i18n.translate( + 'xpack.apm.metrics.transactionChart.routeChangeTimesLabel', + { + defaultMessage: 'Route change times' + } + ); + default: + return i18n.translate( + 'xpack.apm.metrics.transactionChart.transactionDurationLabel', + { + defaultMessage: 'Transaction duration' + } + ); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts rename to x-pack/plugins/apm/public/components/shared/charts/helper/__test__/timezone.test.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/helper/timezone.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/charts/helper/timezone.ts rename to x-pack/plugins/apm/public/components/shared/charts/helper/timezone.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts b/x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts rename to x-pack/plugins/apm/public/components/shared/useDelayedVisibility/index.ts diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx similarity index 93% rename from x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx rename to x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index cc2e382611628..865e3dbe6dafc 100644 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { ApmPluginContext, ApmPluginContextValue } from '.'; import { createCallApmApi } from '../../services/rest/createCallApmApi'; -import { ConfigSchema } from '../../new-platform/plugin'; +import { ConfigSchema } from '../..'; const mockCore = { chrome: { @@ -30,7 +30,6 @@ const mockCore = { }; const mockConfig: ConfigSchema = { - indexPatternTitle: 'apm-*', serviceMapEnabled: true, ui: { enabled: false diff --git a/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx new file mode 100644 index 0000000000000..37304d292540d --- /dev/null +++ b/x-pack/plugins/apm/public/context/ApmPluginContext/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createContext } from 'react'; +import { AppMountContext } from 'kibana/public'; +import { ConfigSchema } from '../..'; +import { ApmPluginSetupDeps } from '../../plugin'; + +export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; + +export interface ApmPluginContextValue { + config: ConfigSchema; + core: AppMountContext['core']; + plugins: ApmPluginSetupDeps; +} + +export const ApmPluginContext = createContext({} as ApmPluginContextValue); diff --git a/x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx b/x-pack/plugins/apm/public/context/ChartsSyncContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/ChartsSyncContext.tsx rename to x-pack/plugins/apm/public/context/ChartsSyncContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx rename to x-pack/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx diff --git a/x-pack/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/plugins/apm/public/context/LicenseContext/index.tsx new file mode 100644 index 0000000000000..e6615a2fc98bf --- /dev/null +++ b/x-pack/plugins/apm/public/context/LicenseContext/index.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { ILicense } from '../../../../licensing/public'; +import { useApmPluginContext } from '../../hooks/useApmPluginContext'; +import { InvalidLicenseNotification } from './InvalidLicenseNotification'; + +export const LicenseContext = React.createContext<ILicense | undefined>( + undefined +); + +export function LicenseProvider({ children }: { children: React.ReactChild }) { + const { license$ } = useApmPluginContext().plugins.licensing; + const license = useObservable(license$); + // if license is not loaded yet, consider it valid + const hasInvalidLicense = license?.isActive === false; + + // if license is invalid show an error message + if (hasInvalidLicense) { + return <InvalidLicenseNotification />; + } + + // render rest of application and pass down license via context + return <LicenseContext.Provider value={license} children={children} />; +} diff --git a/x-pack/legacy/plugins/apm/public/context/LoadingIndicatorContext.tsx b/x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/LoadingIndicatorContext.tsx rename to x-pack/plugins/apm/public/context/LoadingIndicatorContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/LocationContext.tsx b/x-pack/plugins/apm/public/context/LocationContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/LocationContext.tsx rename to x-pack/plugins/apm/public/context/LocationContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/MatchedRouteContext.tsx b/x-pack/plugins/apm/public/context/MatchedRouteContext.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/MatchedRouteContext.tsx rename to x-pack/plugins/apm/public/context/MatchedRouteContext.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx rename to x-pack/plugins/apm/public/context/UrlParamsContext/MockUrlParamsContextProvider.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx rename to x-pack/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/constants.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/constants.ts rename to x-pack/plugins/apm/public/context/UrlParamsContext/constants.ts diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts new file mode 100644 index 0000000000000..f1e45fe45255d --- /dev/null +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -0,0 +1,131 @@ +/* + * 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 { compact, pick } from 'lodash'; +import datemath from '@elastic/datemath'; +import { IUrlParams } from './types'; +import { ProcessorEvent } from '../../../common/processor_event'; + +interface PathParams { + processorEvent?: ProcessorEvent; + serviceName?: string; + errorGroupId?: string; + serviceNodeName?: string; + traceId?: string; +} + +export function getParsedDate(rawDate?: string, opts = {}) { + if (rawDate) { + const parsed = datemath.parse(rawDate, opts); + if (parsed) { + return parsed.toISOString(); + } + } +} + +export function getStart(prevState: IUrlParams, rangeFrom?: string) { + if (prevState.rangeFrom !== rangeFrom) { + return getParsedDate(rangeFrom); + } + return prevState.start; +} + +export function getEnd(prevState: IUrlParams, rangeTo?: string) { + if (prevState.rangeTo !== rangeTo) { + return getParsedDate(rangeTo, { roundUp: true }); + } + return prevState.end; +} + +export function toNumber(value?: string) { + if (value !== undefined) { + return parseInt(value, 10); + } +} + +export function toString(value?: string) { + if (value === '' || value === 'null' || value === 'undefined') { + return; + } + return value; +} + +export function toBoolean(value?: string) { + return value === 'true'; +} + +export function getPathAsArray(pathname: string = '') { + return compact(pathname.split('/')); +} + +export function removeUndefinedProps<T>(obj: T): Partial<T> { + return pick(obj, value => value !== undefined); +} + +export function getPathParams(pathname: string = ''): PathParams { + const paths = getPathAsArray(pathname); + const pageName = paths[0]; + // TODO: use react router's real match params instead of guessing the path order + + switch (pageName) { + case 'services': + let servicePageName = paths[2]; + const serviceName = paths[1]; + const serviceNodeName = paths[3]; + + if (servicePageName === 'nodes' && paths.length > 3) { + servicePageName = 'metrics'; + } + + switch (servicePageName) { + case 'transactions': + return { + processorEvent: ProcessorEvent.transaction, + serviceName + }; + case 'errors': + return { + processorEvent: ProcessorEvent.error, + serviceName, + errorGroupId: paths[3] + }; + case 'metrics': + return { + processorEvent: ProcessorEvent.metric, + serviceName, + serviceNodeName + }; + case 'nodes': + return { + processorEvent: ProcessorEvent.metric, + serviceName + }; + case 'service-map': + return { + serviceName + }; + default: + return {}; + } + + case 'traces': + return { + processorEvent: ProcessorEvent.transaction + }; + case 'link-to': + const link = paths[1]; + switch (link) { + case 'trace': + return { + traceId: paths[2] + }; + default: + return {}; + } + default: + return {}; + } +} diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx b/x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx new file mode 100644 index 0000000000000..7a929380bce37 --- /dev/null +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/index.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, { + createContext, + useMemo, + useCallback, + useRef, + useState +} from 'react'; +import { withRouter } from 'react-router-dom'; +import { uniqueId, mapValues } from 'lodash'; +import { IUrlParams } from './types'; +import { getParsedDate } from './helpers'; +import { resolveUrlParams } from './resolveUrlParams'; +import { UIFilters } from '../../../typings/ui_filters'; +import { + localUIFilterNames, + LocalUIFilterName + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../../common/utils/pick_keys'; +import { useDeepObjectIdentity } from '../../hooks/useDeepObjectIdentity'; + +interface TimeRange { + rangeFrom: string; + rangeTo: string; +} + +function useUiFilters(params: IUrlParams): UIFilters { + const { kuery, environment, ...urlParams } = params; + const localUiFilters = mapValues( + pickKeys(urlParams, ...localUIFilterNames), + val => (val ? val.split(',') : []) + ) as Partial<Record<LocalUIFilterName, string[]>>; + + return useDeepObjectIdentity({ kuery, environment, ...localUiFilters }); +} + +const defaultRefresh = (time: TimeRange) => {}; + +const UrlParamsContext = createContext({ + urlParams: {} as IUrlParams, + refreshTimeRange: defaultRefresh, + uiFilters: {} as UIFilters +}); + +const UrlParamsProvider: React.ComponentClass<{}> = withRouter( + ({ location, children }) => { + const refUrlParams = useRef(resolveUrlParams(location, {})); + + const { start, end, rangeFrom, rangeTo } = refUrlParams.current; + + const [, forceUpdate] = useState(''); + + const urlParams = useMemo( + () => + resolveUrlParams(location, { + start, + end, + rangeFrom, + rangeTo + }), + [location, start, end, rangeFrom, rangeTo] + ); + + refUrlParams.current = urlParams; + + const refreshTimeRange = useCallback( + (timeRange: TimeRange) => { + refUrlParams.current = { + ...refUrlParams.current, + start: getParsedDate(timeRange.rangeFrom), + end: getParsedDate(timeRange.rangeTo, { roundUp: true }) + }; + + forceUpdate(uniqueId()); + }, + [forceUpdate] + ); + + const uiFilters = useUiFilters(urlParams); + + const contextValue = useMemo(() => { + return { + urlParams, + refreshTimeRange, + uiFilters + }; + }, [urlParams, refreshTimeRange, uiFilters]); + + return ( + <UrlParamsContext.Provider children={children} value={contextValue} /> + ); + } +); + +export { UrlParamsContext, UrlParamsProvider, useUiFilters }; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts similarity index 93% rename from x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts rename to x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index f022d2084583b..34af18431a2df 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -18,8 +18,8 @@ import { import { toQuery } from '../../components/shared/Links/url_helpers'; import { TIMEPICKER_DEFAULTS } from './constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { localUIFilterNames } from '../../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; -import { pickKeys } from '../../utils/pickKeys'; +import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { pickKeys } from '../../../common/utils/pick_keys'; type TimeUrlParams = Pick< IUrlParams, diff --git a/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts new file mode 100644 index 0000000000000..78fe662b88d75 --- /dev/null +++ b/x-pack/plugins/apm/public/context/UrlParamsContext/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { LocalUIFilterName } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { ProcessorEvent } from '../../../common/processor_event'; + +export type IUrlParams = { + detailTab?: string; + end?: string; + errorGroupId?: string; + flyoutDetailTab?: string; + kuery?: string; + environment?: string; + rangeFrom?: string; + rangeTo?: string; + refreshInterval?: number; + refreshPaused?: boolean; + serviceName?: string; + sortDirection?: string; + sortField?: string; + start?: string; + traceId?: string; + transactionId?: string; + transactionName?: string; + transactionType?: string; + waterfallItemId?: string; + page?: number; + pageSize?: number; + serviceNodeName?: string; + searchTerm?: string; + processorEvent?: ProcessorEvent; + traceIdLink?: string; +} & Partial<Record<LocalUIFilterName, string>>; diff --git a/x-pack/legacy/plugins/apm/public/new-platform/featureCatalogueEntry.ts b/x-pack/plugins/apm/public/featureCatalogueEntry.ts similarity index 88% rename from x-pack/legacy/plugins/apm/public/new-platform/featureCatalogueEntry.ts rename to x-pack/plugins/apm/public/featureCatalogueEntry.ts index 7a150de6d5d02..f76c6f5169dc5 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/featureCatalogueEntry.ts +++ b/x-pack/plugins/apm/public/featureCatalogueEntry.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { FeatureCatalogueCategory } from '../../../../../../src/plugins/home/public'; +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; export const featureCatalogueEntry = { id: 'apm', diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAgentName.ts b/x-pack/plugins/apm/public/hooks/useAgentName.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useAgentName.ts rename to x-pack/plugins/apm/public/hooks/useAgentName.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useApmPluginContext.ts b/x-pack/plugins/apm/public/hooks/useApmPluginContext.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useApmPluginContext.ts rename to x-pack/plugins/apm/public/hooks/useApmPluginContext.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts similarity index 83% rename from x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts index 256c2fa68bfbc..5d0c9d1435798 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByBrowser.ts +++ b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts @@ -8,9 +8,9 @@ import theme from '@elastic/eui/dist/eui_theme_light.json'; import { useFetcher } from './useFetcher'; import { useUrlParams } from './useUrlParams'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AvgDurationByBrowserAPIResponse } from '../../../../../plugins/apm/server/lib/transactions/avg_duration_by_browser'; -import { TimeSeries } from '../../../../../plugins/apm/typings/timeseries'; -import { getVizColorForIndex } from '../../../../../plugins/apm/common/viz_colors'; +import { AvgDurationByBrowserAPIResponse } from '../../server/lib/transactions/avg_duration_by_browser'; +import { TimeSeries } from '../../typings/timeseries'; +import { getVizColorForIndex } from '../../common/viz_colors'; function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { if (!data) { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useAvgDurationByCountry.ts rename to x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts b/x-pack/plugins/apm/public/hooks/useCallApi.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts rename to x-pack/plugins/apm/public/hooks/useCallApi.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useChartsSync.tsx b/x-pack/plugins/apm/public/hooks/useChartsSync.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useChartsSync.tsx rename to x-pack/plugins/apm/public/hooks/useChartsSync.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useComponentId.tsx b/x-pack/plugins/apm/public/hooks/useComponentId.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useComponentId.tsx rename to x-pack/plugins/apm/public/hooks/useComponentId.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useDeepObjectIdentity.ts b/x-pack/plugins/apm/public/hooks/useDeepObjectIdentity.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useDeepObjectIdentity.ts rename to x-pack/plugins/apm/public/hooks/useDeepObjectIdentity.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useDynamicIndexPattern.ts b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts similarity index 89% rename from x-pack/legacy/plugins/apm/public/hooks/useDynamicIndexPattern.ts rename to x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts index ee3d2e81f259f..9a95bd925d6e1 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useDynamicIndexPattern.ts +++ b/x-pack/plugins/apm/public/hooks/useDynamicIndexPattern.ts @@ -5,7 +5,7 @@ */ import { useFetcher } from './useFetcher'; -import { ProcessorEvent } from '../../../../../plugins/apm/common/processor_event'; +import { ProcessorEvent } from '../../common/processor_event'; export function useDynamicIndexPattern( processorEvent: ProcessorEvent | undefined diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.integration.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx rename to x-pack/plugins/apm/public/hooks/useFetcher.integration.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx rename to x-pack/plugins/apm/public/hooks/useFetcher.test.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.tsx similarity index 98% rename from x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx rename to x-pack/plugins/apm/public/hooks/useFetcher.tsx index 95cebd6b2a465..5d5128d969aad 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/useFetcher.tsx @@ -9,7 +9,7 @@ import React, { useContext, useEffect, useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { IHttpFetchError } from 'src/core/public'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; import { APMClient, callApmApi } from '../services/rest/createCallApmApi'; import { useApmPluginContext } from './useApmPluginContext'; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useKibanaUrl.ts b/x-pack/plugins/apm/public/hooks/useKibanaUrl.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useKibanaUrl.ts rename to x-pack/plugins/apm/public/hooks/useKibanaUrl.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLicense.ts b/x-pack/plugins/apm/public/hooks/useLicense.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useLicense.ts rename to x-pack/plugins/apm/public/hooks/useLicense.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLoadingIndicator.ts b/x-pack/plugins/apm/public/hooks/useLoadingIndicator.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useLoadingIndicator.ts rename to x-pack/plugins/apm/public/hooks/useLoadingIndicator.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts similarity index 88% rename from x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts rename to x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 9f14b2b25fc94..1dfd3ec7c3ee3 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -7,18 +7,18 @@ import { omit } from 'lodash'; import { useFetcher } from './useFetcher'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFiltersAPIResponse } from '../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters'; +import { LocalUIFiltersAPIResponse } from '../../server/lib/ui_filters/local_ui_filters'; import { useUrlParams } from './useUrlParams'; import { LocalUIFilterName, localUIFilters // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/apm/server/lib/ui_filters/local_ui_filters/config'; +} from '../../server/lib/ui_filters/local_ui_filters/config'; import { history } from '../utils/history'; import { toQuery, fromQuery } from '../components/shared/Links/url_helpers'; import { removeUndefinedProps } from '../context/UrlParamsContext/helpers'; -import { PROJECTION } from '../../../../../plugins/apm/common/projections/typings'; -import { pickKeys } from '../utils/pickKeys'; +import { PROJECTION } from '../../common/projections/typings'; +import { pickKeys } from '../../common/utils/pick_keys'; import { useCallApi } from './useCallApi'; const getInitialData = ( diff --git a/x-pack/legacy/plugins/apm/public/hooks/useLocation.tsx b/x-pack/plugins/apm/public/hooks/useLocation.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useLocation.tsx rename to x-pack/plugins/apm/public/hooks/useLocation.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useMatchedRoutes.tsx b/x-pack/plugins/apm/public/hooks/useMatchedRoutes.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useMatchedRoutes.tsx rename to x-pack/plugins/apm/public/hooks/useMatchedRoutes.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts similarity index 91% rename from x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts rename to x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts index 72618a6254f4c..ebcd6ab063708 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useServiceMetricCharts.ts +++ b/x-pack/plugins/apm/public/hooks/useServiceMetricCharts.ts @@ -5,7 +5,7 @@ */ // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsChartsByAgentAPIResponse } from '../../../../../plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent'; +import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; diff --git a/x-pack/legacy/plugins/apm/public/hooks/useServiceTransactionTypes.tsx b/x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useServiceTransactionTypes.tsx rename to x-pack/plugins/apm/public/hooks/useServiceTransactionTypes.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts b/x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionBreakdown.ts rename to x-pack/plugins/apm/public/hooks/useTransactionBreakdown.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts b/x-pack/plugins/apm/public/hooks/useTransactionCharts.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionCharts.ts rename to x-pack/plugins/apm/public/hooks/useTransactionCharts.ts diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts similarity index 93% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts rename to x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts index 9a93a2334924a..152980b5655d6 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionDistribution.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionDistribution.ts @@ -8,7 +8,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types'; import { useFetcher } from './useFetcher'; import { useUiFilters } from '../context/UrlParamsContext'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionDistributionAPIResponse } from '../../../../../plugins/apm/server/lib/transactions/distribution'; +import { TransactionDistributionAPIResponse } from '../../server/lib/transactions/distribution'; const INITIAL_DATA = { buckets: [] as TransactionDistributionAPIResponse['buckets'], diff --git a/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts b/x-pack/plugins/apm/public/hooks/useTransactionList.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts rename to x-pack/plugins/apm/public/hooks/useTransactionList.ts index 6ede77023790b..e048e8fe0e3cb 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useTransactionList.ts +++ b/x-pack/plugins/apm/public/hooks/useTransactionList.ts @@ -9,7 +9,7 @@ import { IUrlParams } from '../context/UrlParamsContext/types'; import { useUiFilters } from '../context/UrlParamsContext'; import { useFetcher } from './useFetcher'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TransactionGroupListAPIResponse } from '../../../../../plugins/apm/server/lib/transaction_groups'; +import { TransactionGroupListAPIResponse } from '../../server/lib/transaction_groups'; const getRelativeImpact = ( impact: number, diff --git a/x-pack/legacy/plugins/apm/public/hooks/useUrlParams.tsx b/x-pack/plugins/apm/public/hooks/useUrlParams.tsx similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useUrlParams.tsx rename to x-pack/plugins/apm/public/hooks/useUrlParams.tsx diff --git a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/plugins/apm/public/hooks/useWaterfall.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts rename to x-pack/plugins/apm/public/hooks/useWaterfall.ts diff --git a/x-pack/legacy/plugins/apm/public/icon.svg b/x-pack/plugins/apm/public/icon.svg similarity index 100% rename from x-pack/legacy/plugins/apm/public/icon.svg rename to x-pack/plugins/apm/public/icon.svg diff --git a/x-pack/legacy/plugins/apm/public/images/apm-ml-anomaly-detection-example.png b/x-pack/plugins/apm/public/images/apm-ml-anomaly-detection-example.png similarity index 100% rename from x-pack/legacy/plugins/apm/public/images/apm-ml-anomaly-detection-example.png rename to x-pack/plugins/apm/public/images/apm-ml-anomaly-detection-example.png diff --git a/x-pack/plugins/apm/public/index.ts b/x-pack/plugins/apm/public/index.ts new file mode 100644 index 0000000000000..4ac06e1eb8a1c --- /dev/null +++ b/x-pack/plugins/apm/public/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PluginInitializer, + PluginInitializerContext +} from '../../../../src/core/public'; +import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; + +export interface ConfigSchema { + serviceMapEnabled: boolean; + ui: { + enabled: boolean; + }; +} + +export const plugin: PluginInitializer<ApmPluginSetup, ApmPluginStart> = ( + pluginInitializerContext: PluginInitializerContext<ConfigSchema> +) => new ApmPlugin(pluginInitializerContext); + +export { ApmPluginSetup, ApmPluginStart }; +export { getTraceUrl } from './components/shared/Links/apm/ExternalLinks'; diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts new file mode 100644 index 0000000000000..f13c8853d0582 --- /dev/null +++ b/x-pack/plugins/apm/public/plugin.ts @@ -0,0 +1,133 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext +} from '../../../../src/core/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; + +import { + PluginSetupContract as AlertingPluginPublicSetup, + PluginStartContract as AlertingPluginPublicStart +} from '../../alerting/public'; +import { FeaturesPluginSetup } from '../../features/public'; +import { + DataPublicPluginSetup, + DataPublicPluginStart +} from '../../../../src/plugins/data/public'; +import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart +} from '../../triggers_actions_ui/public'; +import { ConfigSchema } from '.'; +import { createCallApmApi } from './services/rest/createCallApmApi'; +import { featureCatalogueEntry } from './featureCatalogueEntry'; +import { AlertType } from '../common/alert_types'; +import { ErrorRateAlertTrigger } from './components/shared/ErrorRateAlertTrigger'; +import { TransactionDurationAlertTrigger } from './components/shared/TransactionDurationAlertTrigger'; +import { setHelpExtension } from './setHelpExtension'; +import { toggleAppLinkInNav } from './toggleAppLinkInNav'; +import { setReadonlyBadge } from './updateBadge'; +import { createStaticIndexPattern } from './services/rest/index_pattern'; + +export type ApmPluginSetup = void; +export type ApmPluginStart = void; + +export interface ApmPluginSetupDeps { + alerting?: AlertingPluginPublicSetup; + data: DataPublicPluginSetup; + features: FeaturesPluginSetup; + home: HomePublicPluginSetup; + licensing: LicensingPluginSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} + +export interface ApmPluginStartDeps { + alerting?: AlertingPluginPublicStart; + data: DataPublicPluginStart; + home: void; + licensing: void; + triggers_actions_ui: TriggersAndActionsUIPublicPluginStart; +} + +export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> { + private readonly initializerContext: PluginInitializerContext<ConfigSchema>; + constructor(initializerContext: PluginInitializerContext<ConfigSchema>) { + this.initializerContext = initializerContext; + } + public setup(core: CoreSetup, plugins: ApmPluginSetupDeps) { + const config = this.initializerContext.config.get(); + const pluginSetupDeps = plugins; + + pluginSetupDeps.home.environment.update({ apmUi: true }); + pluginSetupDeps.home.featureCatalogue.register(featureCatalogueEntry); + + core.application.register({ + id: 'apm', + title: 'APM', + order: 8100, + euiIconType: 'apmApp', + appRoute: '/app/apm', + icon: 'plugins/apm/public/icon.svg', + category: DEFAULT_APP_CATEGORIES.observability, + + async mount(params: AppMountParameters<unknown>) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services + const [coreStart] = await core.getStartServices(); + + // render APM feedback link in global help menu + setHelpExtension(coreStart); + setReadonlyBadge(coreStart); + + // Automatically creates static index pattern and stores as saved object + createStaticIndexPattern().catch(e => { + // eslint-disable-next-line no-console + console.log('Error creating static index pattern', e); + }); + + return renderApp(coreStart, pluginSetupDeps, params, config); + } + }); + } + public start(core: CoreStart, plugins: ApmPluginStartDeps) { + createCallApmApi(core.http); + + toggleAppLinkInNav(core, this.initializerContext.config.get()); + + plugins.triggers_actions_ui.alertTypeRegistry.register({ + id: AlertType.ErrorRate, + name: i18n.translate('xpack.apm.alertTypes.errorRate', { + defaultMessage: 'Error rate' + }), + iconClass: 'bell', + alertParamsExpression: ErrorRateAlertTrigger, + validate: () => ({ + errors: [] + }) + }); + + plugins.triggers_actions_ui.alertTypeRegistry.register({ + id: AlertType.TransactionDuration, + name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { + defaultMessage: 'Transaction duration' + }), + iconClass: 'bell', + alertParamsExpression: TransactionDurationAlertTrigger, + validate: () => ({ + errors: [] + }) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts rename to x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts b/x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts rename to x-pack/plugins/apm/public/selectors/__tests__/mockData/anomalyData.ts diff --git a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts rename to x-pack/plugins/apm/public/selectors/chartSelectors.ts index d60b63e243d71..e6ef9361ee52a 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/chartSelectors.ts +++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts @@ -10,14 +10,14 @@ import { difference, zipObject } from 'lodash'; import mean from 'lodash.mean'; import { rgba } from 'polished'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TimeSeriesAPIResponse } from '../../../../../plugins/apm/server/lib/transactions/charts'; +import { TimeSeriesAPIResponse } from '../../server/lib/transactions/charts'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ApmTimeSeriesResponse } from '../../../../../plugins/apm/server/lib/transactions/charts/get_timeseries_data/transform'; +import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform'; import { Coordinate, RectCoordinate, TimeSeries -} from '../../../../../plugins/apm/typings/timeseries'; +} from '../../typings/timeseries'; import { asDecimal, tpmUnit, convertTo } from '../utils/formatters'; import { IUrlParams } from '../context/UrlParamsContext/types'; import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries'; diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/SessionStorageMock.ts b/x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/__test__/SessionStorageMock.ts rename to x-pack/plugins/apm/public/services/__test__/SessionStorageMock.ts diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/plugins/apm/public/services/__test__/callApi.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts rename to x-pack/plugins/apm/public/services/__test__/callApi.test.ts diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts b/x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/__test__/callApmApi.test.ts rename to x-pack/plugins/apm/public/services/__test__/callApmApi.test.ts diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/plugins/apm/public/services/rest/callApi.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/rest/callApi.ts rename to x-pack/plugins/apm/public/services/rest/callApi.ts diff --git a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts similarity index 89% rename from x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts rename to x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 2fffb40d353fc..1027e8b885d71 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -6,9 +6,9 @@ import { HttpSetup } from 'kibana/public'; import { callApi, FetchOptions } from './callApi'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { APMAPI } from '../../../../../../plugins/apm/server/routes/create_apm_api'; +import { APMAPI } from '../../../server/routes/create_apm_api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Client } from '../../../../../../plugins/apm/server/routes/typings'; +import { Client } from '../../../server/routes/typings'; export type APMClient = Client<APMAPI['_S']>; export type APMClientOptions = Omit<FetchOptions, 'query' | 'body'> & { diff --git a/x-pack/plugins/apm/public/services/rest/index_pattern.ts b/x-pack/plugins/apm/public/services/rest/index_pattern.ts new file mode 100644 index 0000000000000..ac7a0d3cf734b --- /dev/null +++ b/x-pack/plugins/apm/public/services/rest/index_pattern.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { callApmApi } from './createCallApmApi'; + +export const createStaticIndexPattern = async () => { + return await callApmApi({ + method: 'POST', + pathname: '/api/apm/index_pattern/static' + }); +}; + +export const getApmIndexPatternTitle = async () => { + return await callApmApi({ + pathname: '/api/apm/index_pattern/title' + }); +}; diff --git a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts b/x-pack/plugins/apm/public/services/rest/ml.ts similarity index 91% rename from x-pack/legacy/plugins/apm/public/services/rest/ml.ts rename to x-pack/plugins/apm/public/services/rest/ml.ts index 0cd1bdf907531..b333a08d2eb05 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/ml.ts +++ b/x-pack/plugins/apm/public/services/rest/ml.ts @@ -9,14 +9,14 @@ import { PROCESSOR_EVENT, SERVICE_NAME, TRANSACTION_TYPE -} from '../../../../../../plugins/apm/common/elasticsearch_fieldnames'; +} from '../../../common/elasticsearch_fieldnames'; import { getMlJobId, getMlPrefix, encodeForMlApi -} from '../../../../../../plugins/apm/common/ml_job_constants'; +} from '../../../common/ml_job_constants'; import { callApi } from './callApi'; -import { ESFilter } from '../../../../../../plugins/apm/typings/elasticsearch'; +import { ESFilter } from '../../../typings/elasticsearch'; import { callApmApi } from './createCallApmApi'; interface MlResponseItem { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/watcher.ts b/x-pack/plugins/apm/public/services/rest/watcher.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/services/rest/watcher.ts rename to x-pack/plugins/apm/public/services/rest/watcher.ts diff --git a/x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts b/x-pack/plugins/apm/public/setHelpExtension.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/new-platform/setHelpExtension.ts rename to x-pack/plugins/apm/public/setHelpExtension.ts diff --git a/x-pack/legacy/plugins/apm/public/style/variables.ts b/x-pack/plugins/apm/public/style/variables.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/style/variables.ts rename to x-pack/plugins/apm/public/style/variables.ts diff --git a/x-pack/legacy/plugins/apm/public/new-platform/toggleAppLinkInNav.ts b/x-pack/plugins/apm/public/toggleAppLinkInNav.ts similarity index 91% rename from x-pack/legacy/plugins/apm/public/new-platform/toggleAppLinkInNav.ts rename to x-pack/plugins/apm/public/toggleAppLinkInNav.ts index c807cebf97525..8204e1a022d7e 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/toggleAppLinkInNav.ts +++ b/x-pack/plugins/apm/public/toggleAppLinkInNav.ts @@ -5,7 +5,7 @@ */ import { CoreStart } from 'kibana/public'; -import { ConfigSchema } from './plugin'; +import { ConfigSchema } from '.'; export function toggleAppLinkInNav(core: CoreStart, { ui }: ConfigSchema) { if (ui.enabled === false) { diff --git a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts b/x-pack/plugins/apm/public/updateBadge.ts similarity index 99% rename from x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts rename to x-pack/plugins/apm/public/updateBadge.ts index b3e29bb891c23..10849754313c4 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/updateBadge.ts +++ b/x-pack/plugins/apm/public/updateBadge.ts @@ -10,7 +10,6 @@ import { CoreStart } from 'kibana/public'; export function setReadonlyBadge({ application, chrome }: CoreStart) { const canSave = application.capabilities.apm.save; const { setBadge } = chrome; - setBadge( !canSave ? { diff --git a/x-pack/legacy/plugins/apm/public/utils/__test__/flattenObject.test.ts b/x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/__test__/flattenObject.test.ts rename to x-pack/plugins/apm/public/utils/__test__/flattenObject.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts b/x-pack/plugins/apm/public/utils/flattenObject.ts similarity index 94% rename from x-pack/legacy/plugins/apm/public/utils/flattenObject.ts rename to x-pack/plugins/apm/public/utils/flattenObject.ts index 020bfec2cbd6a..295ea1f9f900f 100644 --- a/x-pack/legacy/plugins/apm/public/utils/flattenObject.ts +++ b/x-pack/plugins/apm/public/utils/flattenObject.ts @@ -5,7 +5,7 @@ */ import { compact, isObject } from 'lodash'; -import { Maybe } from '../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../typings/common'; export interface KeyValuePair { key: string; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/datetime.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/datetime.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/duration.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/formatters.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/formatters.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/size.test.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/__test__/size.test.ts rename to x-pack/plugins/apm/public/utils/formatters/__test__/size.test.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts b/x-pack/plugins/apm/public/utils/formatters/datetime.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts rename to x-pack/plugins/apm/public/utils/formatters/datetime.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts b/x-pack/plugins/apm/public/utils/formatters/duration.ts similarity index 96% rename from x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts rename to x-pack/plugins/apm/public/utils/formatters/duration.ts index 681d876ca3beb..39341e1ff4443 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/public/utils/formatters/duration.ts @@ -7,10 +7,10 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { memoize } from 'lodash'; -import { NOT_AVAILABLE_LABEL } from '../../../../../../plugins/apm/common/i18n'; +import { NOT_AVAILABLE_LABEL } from '../../../common/i18n'; import { asDecimal, asInteger } from './formatters'; import { TimeUnit } from './datetime'; -import { Maybe } from '../../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../../typings/common'; interface FormatterOptions { defaultValue?: string; diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts b/x-pack/plugins/apm/public/utils/formatters/formatters.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/formatters.ts rename to x-pack/plugins/apm/public/utils/formatters/formatters.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/index.ts b/x-pack/plugins/apm/public/utils/formatters/index.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/formatters/index.ts rename to x-pack/plugins/apm/public/utils/formatters/index.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts b/x-pack/plugins/apm/public/utils/formatters/size.ts similarity index 95% rename from x-pack/legacy/plugins/apm/public/utils/formatters/size.ts rename to x-pack/plugins/apm/public/utils/formatters/size.ts index 8fe6ebf3e573d..2cdf8af1d46de 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters/size.ts +++ b/x-pack/plugins/apm/public/utils/formatters/size.ts @@ -5,7 +5,7 @@ */ import { memoize } from 'lodash'; import { asDecimal } from './formatters'; -import { Maybe } from '../../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../../typings/common'; function asKilobytes(value: number) { return `${asDecimal(value / 1000)} KB`; diff --git a/x-pack/legacy/plugins/apm/public/utils/getRangeFromTimeSeries.ts b/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts similarity index 88% rename from x-pack/legacy/plugins/apm/public/utils/getRangeFromTimeSeries.ts rename to x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts index 4301ead2fc79f..1865d5ae574a7 100644 --- a/x-pack/legacy/plugins/apm/public/utils/getRangeFromTimeSeries.ts +++ b/x-pack/plugins/apm/public/utils/getRangeFromTimeSeries.ts @@ -5,7 +5,7 @@ */ import { flatten } from 'lodash'; -import { TimeSeries } from '../../../../../plugins/apm/typings/timeseries'; +import { TimeSeries } from '../../typings/timeseries'; export const getRangeFromTimeSeries = (timeseries: TimeSeries[]) => { const dataPoints = flatten(timeseries.map(series => series.data)); diff --git a/x-pack/legacy/plugins/apm/public/utils/history.ts b/x-pack/plugins/apm/public/utils/history.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/history.ts rename to x-pack/plugins/apm/public/utils/history.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/httpStatusCodeToColor.ts b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts similarity index 100% rename from x-pack/legacy/plugins/apm/public/utils/httpStatusCodeToColor.ts rename to x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts diff --git a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts b/x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts similarity index 84% rename from x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts rename to x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts index f7c13603c3535..c36efc232b782 100644 --- a/x-pack/legacy/plugins/apm/public/utils/isValidCoordinateValue.ts +++ b/x-pack/plugins/apm/public/utils/isValidCoordinateValue.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Maybe } from '../../../../../plugins/apm/typings/common'; +import { Maybe } from '../../typings/common'; export const isValidCoordinateValue = (value: Maybe<number>): value is number => value !== null && value !== undefined; diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx similarity index 96% rename from x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx rename to x-pack/plugins/apm/public/utils/testHelpers.tsx index 36c0e18777bfd..def41a1cabd61 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -16,14 +16,14 @@ import { render, waitForElement } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { APMConfig } from '../../../../../plugins/apm/server'; +import { APMConfig } from '../../server'; import { LocationProvider } from '../context/LocationContext'; -import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { PromiseReturnType } from '../../typings/common'; import { ESFilter, ESSearchResponse, ESSearchRequest -} from '../../../../../plugins/apm/typings/elasticsearch'; +} from '../../typings/elasticsearch'; import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; export function toJson(wrapper: ReactWrapper) { diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/plugins/apm/readme.md similarity index 92% rename from x-pack/legacy/plugins/apm/readme.md rename to x-pack/plugins/apm/readme.md index e8e2514c83fcb..62465e920d793 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/plugins/apm/readme.md @@ -32,7 +32,7 @@ _Docker Compose is required_ ### E2E (Cypress) tests ```sh -x-pack/legacy/plugins/apm/e2e/run-e2e.sh +x-pack/plugins/apm/e2e/run-e2e.sh ``` _Starts Kibana (:5701), APM Server (:8201) and Elasticsearch (:9201). Ingests sample data into Elasticsearch via APM Server and runs the Cypress tests_ @@ -94,13 +94,13 @@ _Note: Run the following commands from `kibana/`._ #### Prettier ``` -yarn prettier "./x-pack/legacy/plugins/apm/**/*.{tsx,ts,js}" --write +yarn prettier "./x-pack/plugins/apm/**/*.{tsx,ts,js}" --write ``` #### ESLint ``` -yarn eslint ./x-pack/legacy/plugins/apm --fix +yarn eslint ./x-pack/plugins/apm --fix ``` ### Setup default APM users @@ -117,7 +117,7 @@ For testing purposes APM uses 3 custom users: To create the users with the correct roles run the following script: ```sh -node x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js --role-suffix <github-username-or-something-unique> +node x-pack/plugins/apm/scripts/setup-kibana-security.js --role-suffix <github-username-or-something-unique> ``` The users will be created with the password specified in kibana.dev.yml for `elasticsearch.password` diff --git a/x-pack/legacy/plugins/apm/scripts/.gitignore b/x-pack/plugins/apm/scripts/.gitignore similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/.gitignore rename to x-pack/plugins/apm/scripts/.gitignore diff --git a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts rename to x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js b/x-pack/plugins/apm/scripts/optimize-tsconfig.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig.js diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/optimize.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js similarity index 90% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/paths.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index cab55a2526202..aeccd403c5ce6 100644 --- a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -5,7 +5,7 @@ */ const path = require('path'); -const xpackRoot = path.resolve(__dirname, '../../../../..'); +const xpackRoot = path.resolve(__dirname, '../../../..'); const kibanaRoot = path.resolve(xpackRoot, '..'); const tsconfigTpl = path.resolve(__dirname, './tsconfig.json'); diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json new file mode 100644 index 0000000000000..5e05d3962eccb --- /dev/null +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json @@ -0,0 +1,10 @@ +{ + "include": [ + "./plugins/apm/**/*", + "./typings/**/*" + ], + "exclude": [ + "**/__fixtures__/**/*", + "./plugins/apm/e2e/cypress/**/*" + ] +} diff --git a/x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/unoptimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/unoptimize.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/optimize-tsconfig/unoptimize.js rename to x-pack/plugins/apm/scripts/optimize-tsconfig/unoptimize.js diff --git a/x-pack/legacy/plugins/apm/scripts/package.json b/x-pack/plugins/apm/scripts/package.json similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/package.json rename to x-pack/plugins/apm/scripts/package.json diff --git a/x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js b/x-pack/plugins/apm/scripts/setup-kibana-security.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/setup-kibana-security.js rename to x-pack/plugins/apm/scripts/setup-kibana-security.js diff --git a/x-pack/legacy/plugins/apm/scripts/storybook.js b/x-pack/plugins/apm/scripts/storybook.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/storybook.js rename to x-pack/plugins/apm/scripts/storybook.js diff --git a/x-pack/legacy/plugins/apm/scripts/unoptimize-tsconfig.js b/x-pack/plugins/apm/scripts/unoptimize-tsconfig.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/unoptimize-tsconfig.js rename to x-pack/plugins/apm/scripts/unoptimize-tsconfig.js diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data.js b/x-pack/plugins/apm/scripts/upload-telemetry-data.js similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/upload-telemetry-data.js rename to x-pack/plugins/apm/scripts/upload-telemetry-data.js diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts similarity index 100% rename from x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts rename to x-pack/plugins/apm/scripts/upload-telemetry-data/download-telemetry-template.ts diff --git a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts similarity index 97% rename from x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts rename to x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts index 8d76063a7fdf6..390609996874b 100644 --- a/x-pack/legacy/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/generate-sample-documents.ts @@ -18,7 +18,7 @@ import { CollectTelemetryParams, collectDataTelemetry // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/apm/server/lib/apm_telemetry/collect_data_telemetry'; +} from '../../server/lib/apm_telemetry/collect_data_telemetry'; interface GenerateOptions { days: number; diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts new file mode 100644 index 0000000000000..4f69a3a3bd213 --- /dev/null +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -0,0 +1,208 @@ +/* + * 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. + */ + +// This script downloads the telemetry mapping, runs the APM telemetry tasks, +// generates a bunch of randomized data based on the downloaded sample, +// and uploads it to a cluster of your choosing in the same format as it is +// stored in the telemetry cluster. Its purpose is twofold: +// - Easier testing of the telemetry tasks +// - Validate whether we can run the queries we want to on the telemetry data + +import fs from 'fs'; +import path from 'path'; +// @ts-ignore +import { Octokit } from '@octokit/rest'; +import { merge, chunk, flatten, pick, identity } from 'lodash'; +import axios from 'axios'; +import yaml from 'js-yaml'; +import { Client } from 'elasticsearch'; +import { argv } from 'yargs'; +import { promisify } from 'util'; +import { Logger } from 'kibana/server'; +// @ts-ignore +import consoleStamp from 'console-stamp'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CollectTelemetryParams } from '../../server/lib/apm_telemetry/collect_data_telemetry'; +import { downloadTelemetryTemplate } from './download-telemetry-template'; +import mapping from '../../mappings.json'; +import { generateSampleDocuments } from './generate-sample-documents'; + +consoleStamp(console, '[HH:MM:ss.l]'); + +const githubToken = process.env.GITHUB_TOKEN; + +if (!githubToken) { + throw new Error('GITHUB_TOKEN was not provided.'); +} + +const kibanaConfigDir = path.join(__filename, '../../../../../../../config'); +const kibanaDevConfig = path.join(kibanaConfigDir, 'kibana.dev.yml'); +const kibanaConfig = path.join(kibanaConfigDir, 'kibana.yml'); + +const xpackTelemetryIndexName = 'xpack-phone-home'; + +const loadedKibanaConfig = (yaml.safeLoad( + fs.readFileSync( + fs.existsSync(kibanaDevConfig) ? kibanaDevConfig : kibanaConfig, + 'utf8' + ) +) || {}) as {}; + +const cliEsCredentials = pick( + { + 'elasticsearch.username': process.env.ELASTICSEARCH_USERNAME, + 'elasticsearch.password': process.env.ELASTICSEARCH_PASSWORD, + 'elasticsearch.hosts': process.env.ELASTICSEARCH_HOST + }, + identity +) as { + 'elasticsearch.username': string; + 'elasticsearch.password': string; + 'elasticsearch.hosts': string; +}; + +const config = { + 'apm_oss.transactionIndices': 'apm-*', + 'apm_oss.metricsIndices': 'apm-*', + 'apm_oss.errorIndices': 'apm-*', + 'apm_oss.spanIndices': 'apm-*', + 'apm_oss.onboardingIndices': 'apm-*', + 'apm_oss.sourcemapIndices': 'apm-*', + 'elasticsearch.hosts': 'http://localhost:9200', + ...loadedKibanaConfig, + ...cliEsCredentials +}; + +async function uploadData() { + const octokit = new Octokit({ + auth: githubToken + }); + + const telemetryTemplate = await downloadTelemetryTemplate(octokit); + + const kibanaMapping = mapping['apm-telemetry']; + + const httpAuth = + config['elasticsearch.username'] && config['elasticsearch.password'] + ? { + username: config['elasticsearch.username'], + password: config['elasticsearch.password'] + } + : null; + + const client = new Client({ + host: config['elasticsearch.hosts'], + ...(httpAuth + ? { + httpAuth: `${httpAuth.username}:${httpAuth.password}` + } + : {}) + }); + + if (argv.clear) { + try { + await promisify(client.indices.delete.bind(client))({ + index: xpackTelemetryIndexName + }); + } catch (err) { + // 404 = index not found, totally okay + if (err.status !== 404) { + throw err; + } + } + } + + const axiosInstance = axios.create({ + baseURL: config['elasticsearch.hosts'], + ...(httpAuth ? { auth: httpAuth } : {}) + }); + + const newTemplate = merge(telemetryTemplate, { + settings: { + index: { mapping: { total_fields: { limit: 10000 } } } + } + }); + + // override apm mapping instead of merging + newTemplate.mappings.properties.stack_stats.properties.kibana.properties.plugins.properties.apm = kibanaMapping; + + await axiosInstance.put(`/_template/xpack-phone-home`, newTemplate); + + const sampleDocuments = await generateSampleDocuments({ + collectTelemetryParams: { + logger: (console as unknown) as Logger, + indices: { + ...config, + apmCustomLinkIndex: '.apm-custom-links', + apmAgentConfigurationIndex: '.apm-agent-configuration' + }, + search: body => { + return promisify(client.search.bind(client))({ + ...body, + requestTimeout: 120000 + }) as any; + }, + indicesStats: body => { + return promisify(client.indices.stats.bind(client))({ + ...body, + requestTimeout: 120000 + }) as any; + }, + transportRequest: (params => { + return axiosInstance[params.method](params.path); + }) as CollectTelemetryParams['transportRequest'] + } + }); + + const chunks = chunk(sampleDocuments, 250); + + await chunks.reduce<Promise<any>>((prev, documents) => { + return prev.then(async () => { + const body = flatten( + documents.map(doc => [{ index: { _index: 'xpack-phone-home' } }, doc]) + ); + + return promisify(client.bulk.bind(client))({ + body, + refresh: true + }).then((response: any) => { + if (response.errors) { + const firstError = response.items.filter( + (item: any) => item.index.status >= 400 + )[0].index.error; + throw new Error(`Failed to upload documents: ${firstError.reason} `); + } + }); + }); + }, Promise.resolve()); +} + +uploadData() + .catch(e => { + if ('response' in e) { + if (typeof e.response === 'string') { + // eslint-disable-next-line no-console + console.log(e.response); + } else { + // eslint-disable-next-line no-console + console.log( + JSON.stringify( + e.response, + ['status', 'statusText', 'headers', 'data'], + 2 + ) + ); + } + } else { + // eslint-disable-next-line no-console + console.log(e); + } + process.exit(1); + }) + .then(() => { + // eslint-disable-next-line no-console + console.log('Finished uploading generated telemetry data'); + }); diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts new file mode 100644 index 0000000000000..ae0f5510cd80e --- /dev/null +++ b/x-pack/plugins/apm/server/feature.ts @@ -0,0 +1,72 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const APM_FEATURE = { + id: 'apm', + name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { + defaultMessage: 'APM' + }), + order: 900, + icon: 'apmApp', + navLinkId: 'apm', + app: ['apm', 'kibana'], + catalogue: ['apm'], + // see x-pack/plugins/features/common/feature_kibana_privileges.ts + privileges: { + all: { + app: ['apm', 'kibana'], + api: [ + 'apm', + 'apm_write', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], + catalogue: ['apm'], + savedObject: { + all: ['alert', 'action', 'action_task_params'], + read: [] + }, + ui: [ + 'show', + 'save', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete' + ] + }, + read: { + app: ['apm', 'kibana'], + api: [ + 'apm', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], + catalogue: ['apm'], + savedObject: { + all: ['alert', 'action', 'action_task_params'], + read: [] + }, + ui: [ + 'show', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete' + ] + } + } +}; diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 77655568a7e9c..9009008790631 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -73,4 +73,4 @@ export type APMConfig = ReturnType<typeof mergeConfigs>; export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); -export { APMPlugin, APMPluginContract } from './plugin'; +export { APMPlugin, APMPluginSetup } from './plugin'; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts index 0d539a09f091b..fcc456c653303 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/queries.test.ts @@ -8,7 +8,7 @@ import { getErrorDistribution } from './get_distribution'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; describe('error distribution queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/plugins/apm/server/lib/errors/queries.test.ts index 5b063c2fb2b61..f1e5d31efd4bd 100644 --- a/x-pack/plugins/apm/server/lib/errors/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/queries.test.ts @@ -9,7 +9,7 @@ import { getErrorGroups } from './get_error_groups'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('error queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index c22084dbb7168..de9ab87b69fc8 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -20,7 +20,7 @@ import { ESSearchResponse } from '../../../typings/elasticsearch'; import { OBSERVER_VERSION_MAJOR } from '../../../common/elasticsearch_fieldnames'; -import { pickKeys } from '../../../../../legacy/plugins/apm/public/utils/pickKeys'; +import { pickKeys } from '../../../common/utils/pick_keys'; import { APMRequestHandlerContext } from '../../routes/typings'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 8e8cf698a84cf..40a2a0e7216a0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -39,19 +39,6 @@ function getMockRequest() { _debug: false } }, - __LEGACY: { - server: { - plugins: { - elasticsearch: { - getCluster: jest.fn().mockReturnValue({ callWithInternalUser: {} }) - } - }, - savedObjects: { - SavedObjectsClient: jest.fn(), - getSavedObjectsRepository: jest.fn() - } - } - }, core: { elasticsearch: { dataClient: { diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts new file mode 100644 index 0000000000000..b3b40bac7bd54 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_apm_index_pattern_title.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { APMRequestHandlerContext } from '../../routes/typings'; + +export function getApmIndexPatternTitle(context: APMRequestHandlerContext) { + return context.config['apm_oss.indexPattern']; +} diff --git a/x-pack/plugins/apm/server/lib/metrics/queries.test.ts b/x-pack/plugins/apm/server/lib/metrics/queries.test.ts index ac4e13ae442cf..f276fa69e20e3 100644 --- a/x-pack/plugins/apm/server/lib/metrics/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/metrics/queries.test.ts @@ -12,7 +12,7 @@ import { getThreadCountChart } from './by_agent/java/thread_count'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; describe('metrics queries', () => { diff --git a/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts b/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts index 672d568b3a5e3..80cd94b1549d7 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/queries.test.ts @@ -13,7 +13,7 @@ import { getServiceNodes } from './'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; import { getServiceNodeMetadata } from '../services/get_service_node_metadata'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; diff --git a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts index 0e52982c6de28..614014ee37afc 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/index.test.ts @@ -7,7 +7,7 @@ import { getServiceAnnotations } from '.'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import noVersions from './__fixtures__/no_versions.json'; import oneVersion from './__fixtures__/one_version.json'; import multipleVersions from './__fixtures__/multiple_versions.json'; diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index e75d9ad05648c..16490ace77744 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -12,7 +12,7 @@ import { hasHistoricalAgentData } from './get_services/has_historical_agent_data import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('services queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index b951b7f350eed..a1a915e6a84a5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -12,7 +12,7 @@ import { searchConfigurations } from './search_configurations'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { findExactConfiguration } from './find_exact_configuration'; describe('agent configuration queries', () => { diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts index 9bd6c78797605..5d0bf329368f7 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/create_or_update_custom_link.test.ts @@ -5,7 +5,7 @@ */ import { Setup } from '../../helpers/setup_request'; -import { mockNow } from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +import { mockNow } from '../../../../public/utils/testHelpers'; import { CustomLink } from '../../../../common/custom_link/custom_link_types'; import { createOrUpdateCustomLink } from './create_or_update_custom_link'; diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts index 514e473b8e78c..0254660e3523f 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/get_transaction.test.ts @@ -6,7 +6,7 @@ import { inspectSearchParams, SearchParamsMock -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { getTransaction } from './get_transaction'; import { Setup } from '../../helpers/setup_request'; import { diff --git a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts index 45610e36786b7..6a67c30bee197 100644 --- a/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/custom_link/list_custom_links.test.ts @@ -8,7 +8,7 @@ import { listCustomLinks } from './list_custom_links'; import { inspectSearchParams, SearchParamsMock -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { Setup } from '../../helpers/setup_request'; import { SERVICE_NAME, diff --git a/x-pack/plugins/apm/server/lib/traces/queries.test.ts b/x-pack/plugins/apm/server/lib/traces/queries.test.ts index 0c2ac4d0f9201..871d0fd1c7fb6 100644 --- a/x-pack/plugins/apm/server/lib/traces/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/traces/queries.test.ts @@ -8,7 +8,7 @@ import { getTraceItems } from './get_trace_items'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('trace queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap index 580cafff95e0c..64f06ad0a81cd 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/fetcher.test.ts.snap @@ -16,6 +16,9 @@ Array [ "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], @@ -126,6 +129,9 @@ Array [ "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 1096c1638f3f2..b93f842b878cb 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -14,6 +14,9 @@ Object { "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], @@ -120,6 +123,9 @@ Object { "p95": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, ], diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 39f2be551ab6e..fb1aafc2d6c95 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -83,7 +83,11 @@ export function transactionGroupsFetcher( sample: { top_hits: { size: 1, sort } }, avg: { avg: { field: TRANSACTION_DURATION } }, p95: { - percentiles: { field: TRANSACTION_DURATION, percents: [95] } + percentiles: { + field: TRANSACTION_DURATION, + percents: [95], + hdr: { number_of_significant_value_digits: 2 } + } }, sum: { sum: { field: TRANSACTION_DURATION } } } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts index 8d6495c2e0b5f..73122d8580134 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/queries.test.ts @@ -8,7 +8,7 @@ import { transactionGroupsFetcher } from './fetcher'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('transaction group queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap index 49e0e0669c241..cc5900919f829 100644 --- a/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/__snapshots__/queries.test.ts.snap @@ -333,6 +333,9 @@ Object { "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, @@ -425,6 +428,9 @@ Object { "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, @@ -522,6 +528,9 @@ Object { "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap index 6c8430a3e71cf..25ebb15fd73e8 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/__snapshots__/fetcher.test.ts.snap @@ -21,6 +21,9 @@ Array [ "pct": Object { "percentiles": Object { "field": "transaction.duration.us", + "hdr": Object { + "number_of_significant_value_digits": 2, + }, "percents": Array [ 95, 99, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 8a2e01c9a7891..e33b98592da2d 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -69,7 +69,11 @@ export function timeseriesFetcher({ aggs: { avg: { avg: { field: TRANSACTION_DURATION } }, pct: { - percentiles: { field: TRANSACTION_DURATION, percents: [95, 99] } + percentiles: { + field: TRANSACTION_DURATION, + percents: [95, 99], + hdr: { number_of_significant_value_digits: 2 } + } } } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts index 116738da5ef9b..a9e4204fde1ad 100644 --- a/x-pack/plugins/apm/server/lib/transactions/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/transactions/queries.test.ts @@ -11,7 +11,7 @@ import { getTransaction } from './get_transaction'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('transaction queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts index 21cc35da72cb9..b72186f528f28 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts @@ -8,7 +8,7 @@ import { getLocalUIFilters } from './'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../../public/utils/testHelpers'; import { getServicesProjection } from '../../../../common/projections/services'; describe('local ui filter queries', () => { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts index 63c8c3e494bb0..079ab64f32db3 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts @@ -8,7 +8,7 @@ import { getEnvironments } from './get_environments'; import { SearchParamsMock, inspectSearchParams -} from '../../../../../legacy/plugins/apm/public/utils/testHelpers'; +} from '../../../public/utils/testHelpers'; describe('ui filter queries', () => { let mock: SearchParamsMock; diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index b434d41982f4c..8cf29de5b8b73 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -10,10 +10,8 @@ import { CoreStart, Logger } from 'src/core/server'; -import { Observable, combineLatest, AsyncSubject } from 'rxjs'; +import { Observable, combineLatest } from 'rxjs'; import { map, take } from 'rxjs/operators'; -import { Server } from 'hapi'; -import { once } from 'lodash'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; import { AlertingPlugin } from '../../alerting/server'; @@ -31,24 +29,20 @@ import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_ import { LicensingPluginSetup } from '../../licensing/public'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../plugins/features/server'; +import { APM_FEATURE } from './feature'; +import { apmIndices, apmTelemetry } from './saved_objects'; -export interface LegacySetup { - server: Server; -} - -export interface APMPluginContract { +export interface APMPluginSetup { config$: Observable<APMConfig>; - registerLegacyAPI: (__LEGACY: LegacySetup) => void; getApmIndices: () => ReturnType<typeof getApmIndices>; } -export class APMPlugin implements Plugin<APMPluginContract> { +export class APMPlugin implements Plugin<APMPluginSetup> { private currentConfig?: APMConfig; private logger?: Logger; - legacySetup$: AsyncSubject<LegacySetup>; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; - this.legacySetup$ = new AsyncSubject(); } public async setup( @@ -62,6 +56,7 @@ export class APMPlugin implements Plugin<APMPluginContract> { taskManager?: TaskManagerSetupContract; alerting?: AlertingPlugin['setup']; actions?: ActionsPlugin['setup']; + features: FeaturesPluginSetup; } ) { this.logger = this.initContext.logger.get(); @@ -70,6 +65,9 @@ export class APMPlugin implements Plugin<APMPluginContract> { map(([apmOssConfig, apmConfig]) => mergeConfigs(apmOssConfig, apmConfig)) ); + core.savedObjects.registerType(apmIndices); + core.savedObjects.registerType(apmTelemetry); + if (plugins.actions && plugins.alerting) { registerApmAlerts({ alerting: plugins.alerting, @@ -78,14 +76,6 @@ export class APMPlugin implements Plugin<APMPluginContract> { }); } - this.legacySetup$.subscribe(__LEGACY => { - createApmApi().init(core, { - config$: mergedConfig$, - logger: this.logger!, - __LEGACY - }); - }); - this.currentConfig = await mergedConfig$.pipe(take(1)).toPromise(); if ( @@ -116,13 +106,15 @@ export class APMPlugin implements Plugin<APMPluginContract> { } }) ); + plugins.features.registerFeature(APM_FEATURE); + + createApmApi().init(core, { + config$: mergedConfig$, + logger: this.logger! + }); return { config$: mergedConfig$, - registerLegacyAPI: once((__LEGACY: LegacySetup) => { - this.legacySetup$.next(__LEGACY); - this.legacySetup$.complete(); - }), getApmIndices: async () => getApmIndices({ savedObjectsClient: await getInternalSavedObjectsClient(core), diff --git a/x-pack/plugins/apm/server/routes/create_api/index.test.ts b/x-pack/plugins/apm/server/routes/create_api/index.test.ts index 312dae1d1f9d2..20c586868a979 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.test.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.test.ts @@ -9,7 +9,6 @@ import { CoreSetup, Logger } from 'src/core/server'; import { Params } from '../typings'; import { BehaviorSubject } from 'rxjs'; import { APMConfig } from '../..'; -import { LegacySetup } from '../../plugin'; const getCoreMock = () => { const get = jest.fn(); @@ -40,8 +39,7 @@ const getCoreMock = () => { config$: new BehaviorSubject({} as APMConfig), logger: ({ error: jest.fn() - } as unknown) as Logger, - __LEGACY: {} as LegacySetup + } as unknown) as Logger } }; }; diff --git a/x-pack/plugins/apm/server/routes/create_api/index.ts b/x-pack/plugins/apm/server/routes/create_api/index.ts index e216574f8a02e..a97e2f30fc2b6 100644 --- a/x-pack/plugins/apm/server/routes/create_api/index.ts +++ b/x-pack/plugins/apm/server/routes/create_api/index.ts @@ -30,7 +30,7 @@ export function createApi() { factoryFns.push(fn); return this as any; }, - init(core, { config$, logger, __LEGACY }) { + init(core, { config$, logger }) { const router = core.http.createRouter(); let config = {} as APMConfig; @@ -136,7 +136,6 @@ export function createApi() { request, context: { ...context, - __LEGACY, // Only return values for parameters that have runtime types, // but always include query as _debug is always set even if // it's not defined in the route. diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 57b3f282852c4..7964d8b0268e8 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -6,7 +6,8 @@ import { staticIndexPatternRoute, - dynamicIndexPatternRoute + dynamicIndexPatternRoute, + apmIndexPatternTitleRoute } from './index_pattern'; import { errorDistributionRoute, @@ -73,6 +74,7 @@ const createApmApi = () => { // index pattern .add(staticIndexPatternRoute) .add(dynamicIndexPatternRoute) + .add(apmIndexPatternTitleRoute) // Errors .add(errorDistributionRoute) diff --git a/x-pack/plugins/apm/server/routes/index_pattern.ts b/x-pack/plugins/apm/server/routes/index_pattern.ts index b7964dc8e91ed..e296057203ff1 100644 --- a/x-pack/plugins/apm/server/routes/index_pattern.ts +++ b/x-pack/plugins/apm/server/routes/index_pattern.ts @@ -8,6 +8,7 @@ import { createStaticIndexPattern } from '../lib/index_pattern/create_static_ind import { createRoute } from './create_route'; import { setupRequest } from '../lib/helpers/setup_request'; import { getInternalSavedObjectsClient } from '../lib/helpers/get_internal_saved_objects_client'; +import { getApmIndexPatternTitle } from '../lib/index_pattern/get_apm_index_pattern_title'; export const staticIndexPatternRoute = createRoute(core => ({ method: 'POST', @@ -38,3 +39,10 @@ export const dynamicIndexPatternRoute = createRoute(() => ({ return { dynamicIndexPattern }; } })); + +export const apmIndexPatternTitleRoute = createRoute(() => ({ + path: '/api/apm/index_pattern/title', + handler: async ({ context }) => { + return getApmIndexPatternTitle(context); + } +})); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 8cfd736a336c2..6268f5899d7ff 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -143,11 +143,15 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ params: { body: t.intersection([ t.type({ service: serviceRt }), - t.partial({ etag: t.string }) + t.partial({ etag: t.string, mark_as_applied_by_agent: t.boolean }) ]) }, handler: async ({ context, request }) => { - const { service, etag } = context.params.body; + const { + service, + etag, + mark_as_applied_by_agent: markAsAppliedByAgent + } = context.params.body; const setup = await setupRequest(context, request); const config = await searchConfigurations({ @@ -166,9 +170,14 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ `Config was found for ${service.name}/${service.environment}` ); - // update `applied_by_agent` field if etags match + // update `applied_by_agent` field + // when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags) + // or if etags match. // this happens in the background and doesn't block the response - if (etag === config._source.etag && !config._source.applied_by_agent) { + if ( + (markAsAppliedByAgent || etag === config._source.etag) && + !config._source.applied_by_agent + ) { markAppliedByAgent({ id: config._id, body: config._source, setup }); } diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 3dc485630c180..6543f2015599b 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -14,7 +14,8 @@ import { import { PickByValue, Optional } from 'utility-types'; import { Observable } from 'rxjs'; import { Server } from 'hapi'; -import { FetchOptions } from '../../../../legacy/plugins/apm/public/services/rest/callApi'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FetchOptions } from '../../public/services/rest/callApi'; import { APMConfig } from '..'; export interface Params { @@ -61,9 +62,6 @@ export type APMRequestHandlerContext< params: { query: { _debug: boolean } } & TDecodedParams; config: APMConfig; logger: Logger; - __LEGACY: { - server: APMLegacyServer; - }; }; export type RouteFactoryFn< @@ -107,7 +105,6 @@ export interface ServerAPI<TRouteState extends RouteState> { context: { config$: Observable<APMConfig>; logger: Logger; - __LEGACY: { server: Server }; } ) => void; } diff --git a/x-pack/plugins/apm/server/saved_objects/apm_indices.ts b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts new file mode 100644 index 0000000000000..c641f4546aae7 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/apm_indices.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsType } from 'src/core/server'; + +export const apmIndices: SavedObjectsType = { + name: 'apm-indices', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + 'apm_oss.sourcemapIndices': { + type: 'keyword' + }, + 'apm_oss.errorIndices': { + type: 'keyword' + }, + 'apm_oss.onboardingIndices': { + type: 'keyword' + }, + 'apm_oss.spanIndices': { + type: 'keyword' + }, + 'apm_oss.transactionIndices': { + type: 'keyword' + }, + 'apm_oss.metricsIndices': { + type: 'keyword' + } + } + } +}; diff --git a/x-pack/plugins/apm/server/saved_objects/apm_telemetry.ts b/x-pack/plugins/apm/server/saved_objects/apm_telemetry.ts new file mode 100644 index 0000000000000..f711e85076e14 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/apm_telemetry.ts @@ -0,0 +1,921 @@ +/* + * 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 { SavedObjectsType } from 'src/core/server'; + +export const apmTelemetry: SavedObjectsType = { + name: 'apm-telemetry', + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + agents: { + properties: { + dotnet: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + go: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + java: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + 'js-base': { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + nodejs: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + python: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + ruby: { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + }, + 'rum-js': { + properties: { + agent: { + properties: { + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + service: { + properties: { + framework: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + language: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + runtime: { + properties: { + composite: { + type: 'keyword', + ignore_above: 1024 + }, + name: { + type: 'keyword', + ignore_above: 1024 + }, + version: { + type: 'keyword', + ignore_above: 1024 + } + } + } + } + } + } + } + } + }, + counts: { + properties: { + agent_configuration: { + properties: { + all: { + type: 'long' + } + } + }, + error: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + max_error_groups_per_service: { + properties: { + '1d': { + type: 'long' + } + } + }, + max_transaction_groups_per_service: { + properties: { + '1d': { + type: 'long' + } + } + }, + metric: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + onboarding: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + services: { + properties: { + '1d': { + type: 'long' + } + } + }, + sourcemap: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + span: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + }, + traces: { + properties: { + '1d': { + type: 'long' + } + } + }, + transaction: { + properties: { + '1d': { + type: 'long' + }, + all: { + type: 'long' + } + } + } + } + }, + cardinality: { + properties: { + user_agent: { + properties: { + original: { + properties: { + all_agents: { + properties: { + '1d': { + type: 'long' + } + } + }, + rum: { + properties: { + '1d': { + type: 'long' + } + } + } + } + } + } + }, + transaction: { + properties: { + name: { + properties: { + all_agents: { + properties: { + '1d': { + type: 'long' + } + } + }, + rum: { + properties: { + '1d': { + type: 'long' + } + } + } + } + } + } + } + } + }, + has_any_services: { + type: 'boolean' + }, + indices: { + properties: { + all: { + properties: { + total: { + properties: { + docs: { + properties: { + count: { + type: 'long' + } + } + }, + store: { + properties: { + size_in_bytes: { + type: 'long' + } + } + } + } + } + } + }, + shards: { + properties: { + total: { + type: 'long' + } + } + } + } + }, + integrations: { + properties: { + ml: { + properties: { + all_jobs_count: { + type: 'long' + } + } + } + } + }, + retainment: { + properties: { + error: { + properties: { + ms: { + type: 'long' + } + } + }, + metric: { + properties: { + ms: { + type: 'long' + } + } + }, + onboarding: { + properties: { + ms: { + type: 'long' + } + } + }, + span: { + properties: { + ms: { + type: 'long' + } + } + }, + transaction: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + services_per_agent: { + properties: { + dotnet: { + type: 'long', + null_value: 0 + }, + go: { + type: 'long', + null_value: 0 + }, + java: { + type: 'long', + null_value: 0 + }, + 'js-base': { + type: 'long', + null_value: 0 + }, + nodejs: { + type: 'long', + null_value: 0 + }, + python: { + type: 'long', + null_value: 0 + }, + ruby: { + type: 'long', + null_value: 0 + }, + 'rum-js': { + type: 'long', + null_value: 0 + } + } + }, + tasks: { + properties: { + agent_configuration: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + agents: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + cardinality: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + groupings: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + indices_stats: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + integrations: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + processor_events: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + services: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + }, + versions: { + properties: { + took: { + properties: { + ms: { + type: 'long' + } + } + } + } + } + } + }, + version: { + properties: { + apm_server: { + properties: { + major: { + type: 'long' + }, + minor: { + type: 'long' + }, + patch: { + type: 'long' + } + } + } + } + } + } as SavedObjectsType['mappings']['properties'] + } +}; diff --git a/x-pack/plugins/apm/server/saved_objects/index.ts b/x-pack/plugins/apm/server/saved_objects/index.ts new file mode 100644 index 0000000000000..30c557c526356 --- /dev/null +++ b/x-pack/plugins/apm/server/saved_objects/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { apmIndices } from './apm_indices'; +export { apmTelemetry } from './apm_telemetry'; diff --git a/x-pack/plugins/apm/server/tutorial/index_pattern.json b/x-pack/plugins/apm/server/tutorial/index_pattern.json index ede9ba54681c0..e0d9595585b88 100644 --- a/x-pack/plugins/apm/server/tutorial/index_pattern.json +++ b/x-pack/plugins/apm/server/tutorial/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, diff --git a/x-pack/plugins/apm/typings/common.d.ts b/x-pack/plugins/apm/typings/common.d.ts index bdd2c75f161e9..eeeb85cd1e7c3 100644 --- a/x-pack/plugins/apm/typings/common.d.ts +++ b/x-pack/plugins/apm/typings/common.d.ts @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../legacy/plugins/infra/types/rison_node'; -import '../../../legacy/plugins/infra/types/eui'; +import '../../../typings/rison_node'; +import '../../infra/types/eui'; // EUIBasicTable -import '../../../legacy/plugins/reporting/public/components/report_listing'; -// .svg -import '../../../legacy/plugins/canvas/types/webpack'; +import '../../reporting/public/components/report_listing'; // Allow unknown properties in an object export type AllowUnknownProperties<T> = T extends Array<infer X> diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 8a8d256cf4273..0739e8e6120bf 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -86,6 +86,7 @@ export interface AggregationOptionsByType { percentiles: { field: string; percents?: number[]; + hdr?: { number_of_significant_value_digits: number }; }; extended_stats: { field: string; diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index e98b2f52089b3..f6c4fce76f134 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -52,5 +52,5 @@ export interface SpanRaw extends APMBaseDoc { id: string; }; observer?: Observer; - child_ids?: string[]; + child?: { id: string[] }; } diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts index f21a9c25b6e64..285a998030cf6 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts @@ -45,7 +45,6 @@ const customElement: CustomElementPayload = { name: 'MyCustomElement', displayName: 'My Wonderful Custom Element', content: 'This is content', - tags: ['filter', 'graphic'], '@created': '2019-02-08T18:35:23.029Z', '@timestamp': '2019-02-08T18:35:23.029Z', }; diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 1f08a41024905..d1bcae549805e 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -127,35 +127,34 @@ export const ServiceConnectorCommentParamsRt = rt.type({ updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), }); -export const ServiceConnectorCaseParamsRt = rt.intersection([ - rt.type({ - caseId: rt.string, - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - incidentId: rt.union([rt.string, rt.null]), - title: rt.string, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), - }), - rt.partial({ - description: rt.string, - comments: rt.array(ServiceConnectorCommentParamsRt), - }), -]); +export const ServiceConnectorCaseParamsRt = rt.type({ + caseId: rt.string, + createdAt: rt.string, + createdBy: ServiceConnectorUserParams, + externalId: rt.union([rt.string, rt.null]), + title: rt.string, + updatedAt: rt.union([rt.string, rt.null]), + updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), + description: rt.union([rt.string, rt.null]), + comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), +}); export const ServiceConnectorCaseResponseRt = rt.intersection([ rt.type({ - number: rt.string, - incidentId: rt.string, + title: rt.string, + id: rt.string, pushedDate: rt.string, url: rt.string, }), rt.partial({ comments: rt.array( - rt.type({ - commentId: rt.string, - pushedDate: rt.string, - }) + rt.intersection([ + rt.type({ + commentId: rt.string, + pushedDate: rt.string, + }), + rt.partial({ externalCommentId: rt.string }), + ]) ), }), ]); diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts new file mode 100644 index 0000000000000..0efdcd3819659 --- /dev/null +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -0,0 +1,28 @@ +/* + * 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 { + CASE_DETAILS_URL, + CASE_COMMENTS_URL, + CASE_USER_ACTIONS_URL, + CASE_COMMENT_DETAILS_URL, +} from '../constants'; + +export const getCaseDetailsUrl = (id: string): string => { + return CASE_DETAILS_URL.replace('{case_id}', id); +}; + +export const getCaseCommentsUrl = (id: string): string => { + return CASE_COMMENTS_URL.replace('{case_id}', id); +}; + +export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): string => { + return CASE_COMMENT_DETAILS_URL.replace('{case_id}', caseId).replace('{comment_id}', commentId); +}; + +export const getCaseUserActionUrl = (id: string): string => { + return CASE_USER_ACTIONS_URL.replace('{case_id}', id); +}; diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts new file mode 100644 index 0000000000000..855a5c3d63507 --- /dev/null +++ b/x-pack/plugins/case/common/constants.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const APP_ID = 'case'; + +/** + * Case routes + */ + +export const CASES_URL = '/api/cases'; +export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`; +export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`; +export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; +export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`; +export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`; +export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`; +export const CASE_STATUS_URL = `${CASES_URL}/status`; +export const CASE_TAGS_URL = `${CASES_URL}/tags`; +export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; + +/** + * Action routes + */ + +export const ACTION_URL = '/api/action'; +export const ACTION_TYPES_URL = '/api/action/types'; + +export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira']; diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts index 1dfab165eccd7..e9bcb9690ebd8 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_all_comments.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initDeleteAllCommentsApi({ caseService, router, userActionService }: RouteDeps) { router.delete( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts index b2022e6dec26d..67cb998409570 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts @@ -15,6 +15,7 @@ import { mockCaseComments, } from '../../__fixtures__'; import { initDeleteCommentApi } from './delete_comment'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; describe('DELETE comment', () => { let routeHandler: RequestHandler<any, any, any>; @@ -23,7 +24,7 @@ describe('DELETE comment', () => { }); it(`deletes the comment. responds with 204`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'delete', params: { case_id: 'mock-id-1', @@ -43,7 +44,7 @@ describe('DELETE comment', () => { }); it(`returns an error when thrown from deleteComment service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'delete', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts index ff0729afed96a..72ef400415d0f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.ts @@ -11,11 +11,12 @@ import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; export function initDeleteCommentApi({ caseService, router, userActionService }: RouteDeps) { router.delete( { - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts index 92da64cebee74..3df9fdb80ba8a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/find_comments.ts @@ -18,11 +18,12 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { escapeHatch, transformComments, wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initFindCaseCommentsApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/comments/_find', + path: `${CASE_COMMENTS_URL}/_find`, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts index 1500039eb2cc2..8d7820d4e8fec 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_all_comment.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { AllCommentsResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObjects, wrapError } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initGetAllCommentsApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts index 9c8d0e5254df0..b5a7d6367ea4b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts @@ -15,6 +15,7 @@ import { } from '../../__fixtures__'; import { flattenCommentSavedObject } from '../../utils'; import { initGetCommentApi } from './get_comment'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; describe('GET comment', () => { let routeHandler: RequestHandler<any, any, any>; @@ -23,7 +24,7 @@ describe('GET comment', () => { }); it(`returns the comment`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', @@ -48,7 +49,7 @@ describe('GET comment', () => { }); it(`returns an error when getComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts index 24f44a5f5129b..5fa668f6ae5de 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { CommentResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { flattenCommentSavedObject, wrapError } from '../../utils'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../../common/constants'; export function initGetCommentApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/comments/{comment_id}', + path: CASE_COMMENT_DETAILS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 8d9906c2abe7f..04473e302e468 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -14,6 +14,7 @@ import { mockCases, } from '../../__fixtures__'; import { initPatchCommentApi } from './patch_comment'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; describe('PATCH comment', () => { let routeHandler: RequestHandler<any, any, any>; @@ -22,7 +23,7 @@ describe('PATCH comment', () => { }); it(`Patch a comment`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'patch', params: { case_id: 'mock-id-1', @@ -50,7 +51,7 @@ describe('PATCH comment', () => { it(`Fails with 409 if version does not match`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'patch', params: { case_id: 'mock-id-1', @@ -74,7 +75,7 @@ describe('PATCH comment', () => { }); it(`Returns an error if updateComment throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'patch', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index 3b38afc02ed81..dd9b124ff1b79 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -15,11 +15,12 @@ import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError, flattenCaseSavedObject } from '../../utils'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initPatchCommentApi({ caseService, router, userActionService }: RouteDeps) { router.patch( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 23039da681ec6..9006470f36f36 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -15,6 +15,7 @@ import { mockCaseComments, } from '../../__fixtures__'; import { initPostCommentApi } from './post_comment'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; describe('POST comment', () => { let routeHandler: RequestHandler<any, any, any>; @@ -27,7 +28,7 @@ describe('POST comment', () => { }); it(`Posts a new comment`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'mock-id-1', @@ -52,7 +53,7 @@ describe('POST comment', () => { }); it(`Returns an error if the case does not exist`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'this-is-not-real', @@ -75,7 +76,7 @@ describe('POST comment', () => { }); it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'mock-id-1', @@ -100,7 +101,7 @@ describe('POST comment', () => { routeHandler = await createRoute(initPostCommentApi, 'post', true); const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, method: 'post', params: { case_id: 'mock-id-1', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts index 70405af26f576..a296d9815f251 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.ts @@ -15,11 +15,12 @@ import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; import { buildCommentUserActionItem } from '../../../../services/user_actions/helpers'; import { escapeHatch, transformNewComment, wrapError, flattenCaseSavedObject } from '../../utils'; import { RouteDeps } from '../../types'; +import { CASE_COMMENTS_URL } from '../../../../../common/constants'; export function initPostCommentApi({ caseService, router, userActionService }: RouteDeps) { router.post( { - path: '/api/cases/{case_id}/comments', + path: CASE_COMMENTS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index 66d39c3f11d28..5b3b6e77b9403 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -15,6 +15,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initGetCaseConfigure } from './get_configure'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; describe('GET configuration', () => { let routeHandler: RequestHandler<any, any, any>; @@ -24,7 +25,7 @@ describe('GET configuration', () => { it('returns the configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); @@ -44,7 +45,7 @@ describe('GET configuration', () => { it('handles undefined version correctly', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); @@ -78,7 +79,7 @@ describe('GET configuration', () => { it('returns an empty object when there is no configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); @@ -95,7 +96,7 @@ describe('GET configuration', () => { it('returns an error if find throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'get', }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 2832edaa892d5..03bec1fe72d39 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -7,11 +7,12 @@ import { CaseConfigureResponseRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; export function initGetCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, validate: false, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index 62edaa0a4792a..09692ff73b94b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -16,6 +16,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initCaseConfigureGetActionConnector } from './get_connectors'; import { getActions } from '../../__mocks__/request_responses'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../common/constants'; describe('GET connectors', () => { let routeHandler: RequestHandler<any, any, any>; @@ -25,7 +26,7 @@ describe('GET connectors', () => { it('returns the connectors', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure/connectors/_find', + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, method: 'get', }); @@ -44,7 +45,7 @@ describe('GET connectors', () => { it('it throws an error when actions client is null', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure/connectors/_find', + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, method: 'get', }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 3e9a1c96d55ed..43167d56de015 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -8,16 +8,19 @@ import Boom from 'boom'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { + CASE_CONFIGURE_CONNECTORS_URL, + SUPPORTED_CONNECTORS, +} from '../../../../../common/constants'; + /* * Be aware that this api will only return 20 connectors */ -const CASE_SERVICE_NOW_ACTION = '.servicenow'; - export function initCaseConfigureGetActionConnector({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/configure/connectors/_find', + path: `${CASE_CONFIGURE_CONNECTORS_URL}/_find`, validate: false, }, async (context, request, response) => { @@ -28,8 +31,8 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou throw Boom.notFound('Action client have not been found'); } - const results = (await actionsClient.getAll()).filter( - action => action.actionTypeId === CASE_SERVICE_NOW_ACTION + const results = (await actionsClient.getAll()).filter(action => + SUPPORTED_CONNECTORS.includes(action.actionTypeId) ); return response.ok({ body: results }); } catch (error) { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index 5b3d68a258664..9b71f777b95ab 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -15,6 +15,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPatchCaseConfigure } from './patch_configure'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; describe('PATCH configuration', () => { let routeHandler: RequestHandler<any, any, any>; @@ -29,7 +30,7 @@ describe('PATCH configuration', () => { it('patch configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -61,7 +62,7 @@ describe('PATCH configuration', () => { routeHandler = await createRoute(initPatchCaseConfigure, 'patch', true); const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -91,7 +92,7 @@ describe('PATCH configuration', () => { it('throw error when configuration have not being created', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -113,7 +114,7 @@ describe('PATCH configuration', () => { it('throw error when the versions are different', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { closure_type: 'close-by-pushing', @@ -135,7 +136,7 @@ describe('PATCH configuration', () => { it('handles undefined version correctly', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'patch', body: { connector_id: 'no-version', version: mockCaseConfigure[0].version }, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 3a1b9d5059cbc..29df97c5f8476 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -16,11 +16,12 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; export function initPatchCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { router.patch( { - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, validate: { body: escapeHatch, }, @@ -38,7 +39,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout if (myCaseConfigure.saved_objects.length === 0) { throw Boom.conflict( - 'You can not patch this configuration since you did not created first with a post' + 'You can not patch this configuration since you did not created first with a post.' ); } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 7e40cad5b1298..fb95cc53a1710 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -16,6 +16,7 @@ import { import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPostCaseConfigure } from './post_configure'; import { newConfiguration } from '../../__mocks__/request_responses'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; describe('POST configuration', () => { let routeHandler: RequestHandler<any, any, any>; @@ -30,7 +31,7 @@ describe('POST configuration', () => { it('create configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -61,7 +62,7 @@ describe('POST configuration', () => { routeHandler = await createRoute(initPostCaseConfigure, 'post', true); const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -90,7 +91,7 @@ describe('POST configuration', () => { it('throws when missing connector_id', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_name: 'My connector 2', @@ -111,7 +112,7 @@ describe('POST configuration', () => { it('throws when missing connector_name', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_id: '456', @@ -132,7 +133,7 @@ describe('POST configuration', () => { it('throws when missing closure_type', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_id: '456', @@ -153,7 +154,7 @@ describe('POST configuration', () => { it('it deletes the previous configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -172,7 +173,7 @@ describe('POST configuration', () => { it('it does NOT delete when not found', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -191,7 +192,7 @@ describe('POST configuration', () => { it('it deletes all configuration', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -214,7 +215,7 @@ describe('POST configuration', () => { it('returns an error if find throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -232,7 +233,7 @@ describe('POST configuration', () => { it('returns an error if delete throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: newConfiguration, }); @@ -250,7 +251,7 @@ describe('POST configuration', () => { it('returns an error if post throws an error', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { connector_id: 'throw-error-create', @@ -272,7 +273,7 @@ describe('POST configuration', () => { it('handles undefined version correctly', async () => { const req = httpServerMock.createKibanaRequest({ - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, method: 'post', body: { ...newConfiguration, connector_id: 'no-version' }, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index 2a23abf0cbf21..5c1693e728c37 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -16,11 +16,12 @@ import { } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; +import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; export function initPostCaseConfigure({ caseConfigureService, caseService, router }: RouteDeps) { router.post( { - path: '/api/cases/configure', + path: CASE_CONFIGURE_URL, validate: { body: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts index c5be6f78a1570..e655339e05eb1 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts @@ -16,6 +16,7 @@ import { mockCaseComments, } from '../__fixtures__'; import { initDeleteCasesApi } from './delete_cases'; +import { CASES_URL } from '../../../../common/constants'; describe('DELETE case', () => { let routeHandler: RequestHandler<any, any, any>; @@ -24,7 +25,7 @@ describe('DELETE case', () => { }); it(`deletes the case. responds with 204`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['mock-id-1'], @@ -43,7 +44,7 @@ describe('DELETE case', () => { }); it(`returns an error when thrown from deleteCase service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['not-real'], @@ -62,7 +63,7 @@ describe('DELETE case', () => { }); it(`returns an error when thrown from getAllCaseComments service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['bad-guy'], @@ -81,7 +82,7 @@ describe('DELETE case', () => { }); it(`returns an error when thrown from deleteComment service`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'delete', query: { ids: ['valid-id'], diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts index 0214017ae5c29..20591637a6c23 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; +import { CASES_URL } from '../../../../common/constants'; export function initDeleteCasesApi({ caseService, router, userActionService }: RouteDeps) { router.delete( { - path: '/api/cases', + path: CASES_URL, validate: { query: schema.object({ ids: schema.arrayOf(schema.string()), diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts index 8fafb1af0eb82..7af1cee494457 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts @@ -14,6 +14,7 @@ import { mockCases, } from '../__fixtures__'; import { initFindCasesApi } from './find_cases'; +import { CASES_URL } from '../../../../common/constants'; describe('GET all cases', () => { let routeHandler: RequestHandler<any, any, any>; @@ -22,7 +23,7 @@ describe('GET all cases', () => { }); it(`gets all the cases`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: `${CASES_URL}/_find`, method: 'get', }); diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts index b2716749e9749..40fc0301b058a 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.ts @@ -15,6 +15,7 @@ import { CasesFindResponseRt, CasesFindRequestRt, throwErrors } from '../../../. import { transformCases, sortToSnake, wrapError, escapeHatch } from '../utils'; import { RouteDeps, TotalCommentByCase } from '../types'; import { CASE_SAVED_OBJECT } from '../../../saved_object_types'; +import { CASES_URL } from '../../../../common/constants'; const combineFilters = (filters: string[], operator: 'OR' | 'AND'): string => filters?.filter(i => i !== '').join(` ${operator} `); @@ -41,7 +42,7 @@ const buildFilter = ( export function initFindCasesApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/_find', + path: `${CASES_URL}/_find`, validate: { query: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 5912df2c40aa3..a8c12d4734b53 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -18,6 +18,7 @@ import { } from '../__fixtures__'; import { flattenCaseSavedObject } from '../utils'; import { initGetCaseApi } from './get_case'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; describe('GET case', () => { let routeHandler: RequestHandler<any, any, any>; @@ -26,7 +27,7 @@ describe('GET case', () => { }); it(`returns the case with empty case comments when includeComments is false`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', @@ -55,7 +56,7 @@ describe('GET case', () => { }); it(`returns an error when thrown from getCase`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'abcdefg', @@ -78,7 +79,7 @@ describe('GET case', () => { }); it(`returns the case with case comments when includeComments is true`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'mock-id-1', @@ -102,7 +103,7 @@ describe('GET case', () => { }); it(`returns an error when thrown from getAllCaseComments`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, method: 'get', params: { case_id: 'bad-guy', diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index ac32b20541a9c..1e836d38c285c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -9,11 +9,12 @@ import { schema } from '@kbn/config-schema'; import { CaseResponseRt } from '../../../../common/api'; import { RouteDeps } from '../types'; import { flattenCaseSavedObject, wrapError } from '../utils'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetCaseApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}', + path: CASE_DETAILS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index c36ea8964dc80..57f9fc20dbf34 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -20,11 +20,12 @@ import { escapeHatch, wrapError, flattenCaseSavedObject } from '../utils'; import { RouteDeps } from '../types'; import { getCaseToUpdate } from './helpers'; import { buildCaseUserActions } from '../../../services/user_actions/helpers'; +import { CASES_URL } from '../../../../common/constants'; export function initPatchCasesApi({ caseService, router, userActionService }: RouteDeps) { router.patch( { - path: '/api/cases', + path: CASES_URL, validate: { body: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 5899102224774..0bbceb5214046 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -14,6 +14,7 @@ import { mockCases, } from '../__fixtures__'; import { initPostCaseApi } from './post_case'; +import { CASES_URL } from '../../../../common/constants'; describe('POST cases', () => { let routeHandler: RequestHandler<any, any, any>; @@ -26,7 +27,7 @@ describe('POST cases', () => { }); it(`Posts a new case`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'This is a brand new case of a bad meanie defacing data', @@ -49,7 +50,7 @@ describe('POST cases', () => { it(`Error if you passing status for a new case`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'This is a brand new case of a bad meanie defacing data', @@ -70,7 +71,7 @@ describe('POST cases', () => { }); it(`Returns an error if postNewCase throws`, async () => { const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'Throw an error', @@ -93,7 +94,7 @@ describe('POST cases', () => { routeHandler = await createRoute(initPostCaseApi, 'post', true); const request = httpServerMock.createKibanaRequest({ - path: '/api/cases', + path: CASES_URL, method: 'post', body: { description: 'This is a brand new case of a bad meanie defacing data', diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.ts index 239b8bfdf9b29..059a8b1affd54 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.ts @@ -14,11 +14,12 @@ import { flattenCaseSavedObject, transformNewCase, wrapError, escapeHatch } from import { CasePostRequestRt, throwErrors, excess, CaseResponseRt } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; +import { CASES_URL } from '../../../../common/constants'; export function initPostCaseApi({ caseService, router, userActionService }: RouteDeps) { router.post( { - path: '/api/cases', + path: CASES_URL, validate: { body: escapeHatch, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index aff057adea37f..94ebe24c3d2ae 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -15,6 +15,7 @@ import { flattenCaseSavedObject, wrapError, escapeHatch } from '../utils'; import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initPushCaseUserActionApi({ caseConfigureService, @@ -24,7 +25,7 @@ export function initPushCaseUserActionApi({ }: RouteDeps) { router.post( { - path: '/api/cases/{case_id}/_push', + path: `${CASE_DETAILS_URL}/_push`, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts index 56862a96e0563..3fc96f506d175 100644 --- a/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/case/server/routes/api/cases/reporters/get_reporters.ts @@ -7,11 +7,12 @@ import { UsersRt } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_REPORTERS_URL } from '../../../../../common/constants'; export function initGetReportersApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/reporters', + path: CASE_REPORTERS_URL, validate: {}, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts index f7431729d398c..8f86dbc91f315 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.ts @@ -9,11 +9,12 @@ import { wrapError } from '../../utils'; import { CasesStatusResponseRt } from '../../../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../../saved_object_types'; +import { CASE_STATUS_URL } from '../../../../../common/constants'; export function initGetCasesStatusApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/status', + path: CASE_STATUS_URL, validate: {}, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts index 55e8fe2af128c..1a3da659c58c4 100644 --- a/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/case/server/routes/api/cases/tags/get_tags.ts @@ -6,11 +6,12 @@ import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_TAGS_URL } from '../../../../../common/constants'; export function initGetTagsApi({ caseService, router }: RouteDeps) { router.get( { - path: '/api/cases/tags', + path: CASE_TAGS_URL, validate: {}, }, async (context, request, response) => { diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 2d4f16e46d561..c90979f60d23f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -10,11 +10,12 @@ import { CaseUserActionsResponseRt } from '../../../../../common/api'; import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../../../saved_object_types'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; +import { CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; export function initGetAllUserActionsApi({ userActionService, router }: RouteDeps) { router.get( { - path: '/api/cases/{case_id}/user_actions', + path: CASE_USER_ACTIONS_URL, validate: { params: schema.object({ case_id: schema.string(), diff --git a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts new file mode 100644 index 0000000000000..797141b0996af --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { LicenseType } from '../../../licensing/common/types'; + +const platinumLicense: LicenseType = 'platinum'; + +export const PLUGIN = { + ID: 'crossClusterReplication', + TITLE: i18n.translate('xpack.crossClusterReplication.appTitle', { + defaultMessage: 'Cross-Cluster Replication', + }), + minimumLicenseType: platinumLicense, +}; + +export const APPS = { + CCR_APP: 'ccr', + REMOTE_CLUSTER_APP: 'remote_cluster', +}; + +export const MANAGEMENT_ID = 'cross_cluster_replication'; +export const BASE_PATH = `/management/elasticsearch/${MANAGEMENT_ID}`; +export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters'; +export const API_BASE_PATH = '/api/cross_cluster_replication'; +export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters'; +export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management'; + +export const FOLLOWER_INDEX_ADVANCED_SETTINGS = { + maxReadRequestOperationCount: 5120, + maxOutstandingReadRequests: 12, + maxReadRequestSize: '32mb', + maxWriteRequestOperationCount: 5120, + maxWriteRequestSize: '9223372036854775807b', + maxOutstandingWriteRequests: 9, + maxWriteBufferCount: 2147483647, + maxWriteBufferSize: '512mb', + maxRetryDelay: '500ms', + readPollTimeout: '1m', +}; diff --git a/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap new file mode 100644 index 0000000000000..c20556fe1434d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = ` +Object { + "leaderIndex": "leader 1", + "maxOutstandingReadRequests": 1, + "maxOutstandingWriteRequests": 1, + "maxReadRequestOperationCount": 1, + "maxReadRequestSize": "1b", + "maxRetryDelay": "1s", + "maxWriteBufferCount": 1, + "maxWriteBufferSize": "1b", + "maxWriteRequestOperationCount": 1, + "maxWriteRequestSize": "1b", + "name": "follower index 1", + "readPollTimeout": "1s", + "remoteCluster": "cluster 1", + "shards": Array [ + Object { + "bytesReadCount": 1, + "failedReadRequestsCount": 1, + "failedWriteRequestsCount": 1, + "followerGlobalCheckpoint": 1, + "followerMappingVersion": 1, + "followerMaxSequenceNum": 1, + "followerSettingsVersion": 1, + "id": 1, + "lastRequestedSequenceNum": 1, + "leaderGlobalCheckpoint": 1, + "leaderIndex": "leader 1", + "leaderMaxSequenceNum": 1, + "operationsReadCount": 1, + "operationsWrittenCount": 1, + "outstandingReadRequestsCount": 1, + "outstandingWriteRequestsCount": 1, + "readExceptions": Array [], + "remoteCluster": "cluster 1", + "successfulReadRequestCount": 1, + "successfulWriteRequestsCount": 1, + "timeSinceLastReadMs": 1, + "totalReadRemoteExecTimeMs": 1, + "totalReadTimeMs": 1, + "totalWriteTimeMs": 1, + "writeBufferOperationsCount": 1, + "writeBufferSizeBytes": 1, + }, + Object { + "bytesReadCount": undefined, + "failedReadRequestsCount": undefined, + "failedWriteRequestsCount": undefined, + "followerGlobalCheckpoint": undefined, + "followerMappingVersion": undefined, + "followerMaxSequenceNum": undefined, + "followerSettingsVersion": undefined, + "id": "shard 2", + "lastRequestedSequenceNum": undefined, + "leaderGlobalCheckpoint": undefined, + "leaderIndex": "leader_index 2", + "leaderMaxSequenceNum": undefined, + "operationsReadCount": undefined, + "operationsWrittenCount": undefined, + "outstandingReadRequestsCount": undefined, + "outstandingWriteRequestsCount": undefined, + "readExceptions": undefined, + "remoteCluster": "remote_cluster 2", + "successfulReadRequestCount": undefined, + "successfulWriteRequestsCount": undefined, + "timeSinceLastReadMs": undefined, + "totalReadRemoteExecTimeMs": undefined, + "totalReadTimeMs": undefined, + "totalWriteTimeMs": undefined, + "writeBufferOperationsCount": undefined, + "writeBufferSizeBytes": undefined, + }, + ], + "status": "active", +} +`; + +exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = ` +Object { + "bytesReadCount": 1, + "failedReadRequestsCount": 1, + "failedWriteRequestsCount": 1, + "followerGlobalCheckpoint": 1, + "followerMappingVersion": 1, + "followerMaxSequenceNum": 1, + "followerSettingsVersion": 1, + "id": 1, + "lastRequestedSequenceNum": 1, + "leaderGlobalCheckpoint": 1, + "leaderIndex": "leader index", + "leaderMaxSequenceNum": 1, + "operationsReadCount": 1, + "operationsWrittenCount": 1, + "outstandingReadRequestsCount": 1, + "outstandingWriteRequestsCount": 1, + "readExceptions": Array [ + "read exception", + ], + "remoteCluster": "remote cluster", + "successfulReadRequestCount": 1, + "successfulWriteRequestsCount": 1, + "timeSinceLastReadMs": 1, + "totalReadRemoteExecTimeMs": 1, + "totalReadTimeMs": 1, + "totalWriteTimeMs": 1, + "writeBufferOperationsCount": 1, + "writeBufferSizeBytes": 1, +} +`; + +exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = ` +Object { + "leader_index": "leader index", + "max_outstanding_read_requests": 1, + "max_outstanding_write_requests": 1, + "max_read_request_operation_count": 1, + "max_read_request_size": "1b", + "max_retry_delay": "1s", + "max_write_buffer_count": 1, + "max_write_buffer_size": "1b", + "max_write_request_operation_count": 1, + "max_write_request_size": "1b", + "read_poll_timeout": "1s", + "remote_cluster": "remote cluster", +} +`; diff --git a/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts new file mode 100644 index 0000000000000..fe3e59f21ee23 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { AutoFollowPattern, AutoFollowPatternFromEs } from '../types'; + +import { + deserializeAutoFollowPattern, + deserializeListAutoFollowPatterns, + serializeAutoFollowPattern, +} from './auto_follow_pattern_serialization'; + +describe('[CCR] auto-follow_serialization', () => { + describe('deserializeAutoFollowPattern()', () => { + it('should deserialize Elasticsearch object', () => { + const expected = { + name: 'some-name', + active: true, + remoteCluster: 'foo', + leaderIndexPatterns: ['foo-*'], + followIndexPattern: 'bar', + }; + + const esObject = { + name: 'some-name', + pattern: { + active: true, + remote_cluster: expected.remoteCluster, + leader_index_patterns: expected.leaderIndexPatterns, + follow_index_pattern: expected.followIndexPattern, + }, + }; + + expect(deserializeAutoFollowPattern(esObject as AutoFollowPatternFromEs)).toEqual(expected); + }); + }); + + describe('deserializeListAutoFollowPatterns()', () => { + it('should deserialize list of Elasticsearch objects', () => { + const name1 = 'foo1'; + const name2 = 'foo2'; + + const expected = [ + { + name: name1, + remoteCluster: 'foo1', + leaderIndexPatterns: ['foo1-*'], + followIndexPattern: 'bar2', + }, + { + name: name2, + remoteCluster: 'foo2', + leaderIndexPatterns: ['foo2-*'], + followIndexPattern: 'bar2', + }, + ]; + + const esObjects = { + patterns: [ + { + name: name1, + pattern: { + remote_cluster: expected[0].remoteCluster, + leader_index_patterns: expected[0].leaderIndexPatterns, + follow_index_pattern: expected[0].followIndexPattern, + }, + }, + { + name: name2, + pattern: { + remote_cluster: expected[1].remoteCluster, + leader_index_patterns: expected[1].leaderIndexPatterns, + follow_index_pattern: expected[1].followIndexPattern, + }, + }, + ], + }; + + expect( + deserializeListAutoFollowPatterns(esObjects.patterns as AutoFollowPatternFromEs[]) + ).toEqual(expected); + }); + }); + + describe('serializeAutoFollowPattern()', () => { + it('should serialize object to Elasticsearch object', () => { + const expected = { + remote_cluster: 'foo', + leader_index_patterns: ['bar-*'], + follow_index_pattern: 'faz', + }; + + const object = { + remoteCluster: expected.remote_cluster, + leaderIndexPatterns: expected.leader_index_patterns, + followIndexPattern: expected.follow_index_pattern, + }; + + expect(serializeAutoFollowPattern(object as AutoFollowPattern)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts new file mode 100644 index 0000000000000..265af0ede1462 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts @@ -0,0 +1,38 @@ +/* + * 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 { AutoFollowPattern, AutoFollowPatternFromEs, AutoFollowPatternToEs } from '../types'; + +export const deserializeAutoFollowPattern = ( + autoFollowPattern: AutoFollowPatternFromEs +): AutoFollowPattern => { + const { + name, + pattern: { active, remote_cluster, leader_index_patterns, follow_index_pattern }, + } = autoFollowPattern; + + return { + name, + active, + remoteCluster: remote_cluster, + leaderIndexPatterns: leader_index_patterns, + followIndexPattern: follow_index_pattern, + }; +}; + +export const deserializeListAutoFollowPatterns = ( + autoFollowPatterns: AutoFollowPatternFromEs[] +): AutoFollowPattern[] => autoFollowPatterns.map(deserializeAutoFollowPattern); + +export const serializeAutoFollowPattern = ({ + remoteCluster, + leaderIndexPatterns, + followIndexPattern, +}: AutoFollowPattern): AutoFollowPatternToEs => ({ + remote_cluster: remoteCluster, + leader_index_patterns: leaderIndexPatterns, + follow_index_pattern: followIndexPattern, +}); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts new file mode 100644 index 0000000000000..bfe3e1b3443e6 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts @@ -0,0 +1,224 @@ +/* + * 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 { ShardFromEs, FollowerIndexFromEs, FollowerIndex } from '../types'; + +import { + deserializeShard, + deserializeFollowerIndex, + deserializeListFollowerIndices, + serializeFollowerIndex, +} from './follower_index_serialization'; + +describe('[CCR] follower index serialization', () => { + describe('deserializeShard()', () => { + it('deserializes shard', () => { + const serializedShard = { + remote_cluster: 'remote cluster', + leader_index: 'leader index', + shard_id: 1, + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: ['read exception'], + time_since_last_read_millis: 1, + }; + + expect(deserializeShard(serializedShard as ShardFromEs)).toMatchSnapshot(); + }); + }); + + describe('deserializeFollowerIndex()', () => { + it('deserializes Elasticsearch follower index object', () => { + const serializedFollowerIndex = { + follower_index: 'follower index 1', + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + shards: [ + { + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + shard_id: 1, + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + // This is an array of exception objects + read_exceptions: [], + time_since_last_read_millis: 1, + }, + { + remote_cluster: 'remote_cluster 2', + leader_index: 'leader_index 2', + shard_id: 'shard 2', + }, + ], + }; + + expect( + deserializeFollowerIndex(serializedFollowerIndex as FollowerIndexFromEs) + ).toMatchSnapshot(); + }); + }); + + describe('deserializeListFollowerIndices()', () => { + it('deserializes list of Elasticsearch follower index objects', () => { + const serializedFollowerIndexList = [ + { + follower_index: 'follower index 1', + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + shards: [], + }, + { + follower_index: 'follower index 2', + remote_cluster: 'cluster 2', + leader_index: 'leader 2', + status: 'paused', + parameters: { + max_read_request_operation_count: 2, + max_outstanding_read_requests: 2, + max_read_request_size: '2b', + max_write_request_operation_count: 2, + max_write_request_size: '2b', + max_outstanding_write_requests: 2, + max_write_buffer_count: 2, + max_write_buffer_size: '2b', + max_retry_delay: '2s', + read_poll_timeout: '2s', + }, + shards: [], + }, + ]; + + const deserializedFollowerIndexList = [ + { + name: 'follower index 1', + remoteCluster: 'cluster 1', + leaderIndex: 'leader 1', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [], + }, + { + name: 'follower index 2', + remoteCluster: 'cluster 2', + leaderIndex: 'leader 2', + status: 'paused', + maxReadRequestOperationCount: 2, + maxOutstandingReadRequests: 2, + maxReadRequestSize: '2b', + maxWriteRequestOperationCount: 2, + maxWriteRequestSize: '2b', + maxOutstandingWriteRequests: 2, + maxWriteBufferCount: 2, + maxWriteBufferSize: '2b', + maxRetryDelay: '2s', + readPollTimeout: '2s', + shards: [], + }, + ]; + + expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual( + deserializedFollowerIndexList + ); + }); + }); + + describe('serializeFollowerIndex()', () => { + it('serializes object to Elasticsearch follower index object', () => { + const deserializedFollowerIndex = { + name: 'test', + status: 'active', + shards: [], + remoteCluster: 'remote cluster', + leaderIndex: 'leader index', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + }; + + expect(serializeFollowerIndex(deserializedFollowerIndex as FollowerIndex)).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts new file mode 100644 index 0000000000000..df476a0b2db89 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts @@ -0,0 +1,142 @@ +/* + * 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 { + ShardFromEs, + Shard, + FollowerIndexFromEs, + FollowerIndex, + FollowerIndexToEs, + FollowerIndexAdvancedSettings, + FollowerIndexAdvancedSettingsToEs, +} from '../types'; + +export const deserializeShard = ({ + remote_cluster, + leader_index, + shard_id, + leader_global_checkpoint, + leader_max_seq_no, + follower_global_checkpoint, + follower_max_seq_no, + last_requested_seq_no, + outstanding_read_requests, + outstanding_write_requests, + write_buffer_operation_count, + write_buffer_size_in_bytes, + follower_mapping_version, + follower_settings_version, + total_read_time_millis, + total_read_remote_exec_time_millis, + successful_read_requests, + failed_read_requests, + operations_read, + bytes_read, + total_write_time_millis, + successful_write_requests, + failed_write_requests, + operations_written, + read_exceptions, + time_since_last_read_millis, +}: ShardFromEs): Shard => ({ + id: shard_id, + remoteCluster: remote_cluster, + leaderIndex: leader_index, + leaderGlobalCheckpoint: leader_global_checkpoint, + leaderMaxSequenceNum: leader_max_seq_no, + followerGlobalCheckpoint: follower_global_checkpoint, + followerMaxSequenceNum: follower_max_seq_no, + lastRequestedSequenceNum: last_requested_seq_no, + outstandingReadRequestsCount: outstanding_read_requests, + outstandingWriteRequestsCount: outstanding_write_requests, + writeBufferOperationsCount: write_buffer_operation_count, + writeBufferSizeBytes: write_buffer_size_in_bytes, + followerMappingVersion: follower_mapping_version, + followerSettingsVersion: follower_settings_version, + totalReadTimeMs: total_read_time_millis, + totalReadRemoteExecTimeMs: total_read_remote_exec_time_millis, + successfulReadRequestCount: successful_read_requests, + failedReadRequestsCount: failed_read_requests, + operationsReadCount: operations_read, + bytesReadCount: bytes_read, + totalWriteTimeMs: total_write_time_millis, + successfulWriteRequestsCount: successful_write_requests, + failedWriteRequestsCount: failed_write_requests, + operationsWrittenCount: operations_written, + // This is an array of exception objects + readExceptions: read_exceptions, + timeSinceLastReadMs: time_since_last_read_millis, +}); + +export const deserializeFollowerIndex = ({ + follower_index, + remote_cluster, + leader_index, + status, + parameters: { + max_read_request_operation_count, + max_outstanding_read_requests, + max_read_request_size, + max_write_request_operation_count, + max_write_request_size, + max_outstanding_write_requests, + max_write_buffer_count, + max_write_buffer_size, + max_retry_delay, + read_poll_timeout, + } = {}, + shards, +}: FollowerIndexFromEs): FollowerIndex => ({ + name: follower_index, + remoteCluster: remote_cluster, + leaderIndex: leader_index, + status, + maxReadRequestOperationCount: max_read_request_operation_count, + maxOutstandingReadRequests: max_outstanding_read_requests, + maxReadRequestSize: max_read_request_size, + maxWriteRequestOperationCount: max_write_request_operation_count, + maxWriteRequestSize: max_write_request_size, + maxOutstandingWriteRequests: max_outstanding_write_requests, + maxWriteBufferCount: max_write_buffer_count, + maxWriteBufferSize: max_write_buffer_size, + maxRetryDelay: max_retry_delay, + readPollTimeout: read_poll_timeout, + shards: shards && shards.map(deserializeShard), +}); + +export const deserializeListFollowerIndices = ( + followerIndices: FollowerIndexFromEs[] +): FollowerIndex[] => followerIndices.map(deserializeFollowerIndex); + +export const serializeAdvancedSettings = ({ + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, +}: FollowerIndexAdvancedSettings): FollowerIndexAdvancedSettingsToEs => ({ + max_read_request_operation_count: maxReadRequestOperationCount, + max_outstanding_read_requests: maxOutstandingReadRequests, + max_read_request_size: maxReadRequestSize, + max_write_request_operation_count: maxWriteRequestOperationCount, + max_write_request_size: maxWriteRequestSize, + max_outstanding_write_requests: maxOutstandingWriteRequests, + max_write_buffer_count: maxWriteBufferCount, + max_write_buffer_size: maxWriteBufferSize, + max_retry_delay: maxRetryDelay, + read_poll_timeout: readPollTimeout, +}); + +export const serializeFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexToEs => ({ + remote_cluster: followerIndex.remoteCluster, + leader_index: followerIndex.leaderIndex, + ...serializeAdvancedSettings(followerIndex), +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js rename to x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts diff --git a/x-pack/plugins/cross_cluster_replication/common/services/utils.ts b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts new file mode 100644 index 0000000000000..dda6732254cc3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const arrify = (val: any): any[] => (Array.isArray(val) ? val : [val]); + +/** + * Utilty to add some latency in a Promise chain + * + * @param {number} time Time in millisecond to wait + */ +export const wait = (time = 1000) => (data: any): Promise<any> => { + return new Promise(resolve => { + setTimeout(() => resolve(data), time); + }); +}; + +/** + * Utility to remove empty fields ("") from a request body + */ +export const removeEmptyFields = (body: Record<string, any>): Record<string, any> => + Object.entries(body).reduce((acc: Record<string, any>, [key, value]: [string, any]): Record< + string, + any + > => { + if (value !== '') { + acc[key] = value; + } + return acc; + }, {}); diff --git a/x-pack/plugins/cross_cluster_replication/common/types.ts b/x-pack/plugins/cross_cluster_replication/common/types.ts new file mode 100644 index 0000000000000..4932d6c570297 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/types.ts @@ -0,0 +1,186 @@ +/* + * 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 interface AutoFollowPattern { + name: string; + active: boolean; + remoteCluster: string; + leaderIndexPatterns: string[]; + followIndexPattern: string; +} + +export interface AutoFollowPatternFromEs { + name: string; + pattern: { + active: boolean; + remote_cluster: string; + leader_index_patterns: string[]; + follow_index_pattern: string; + }; +} + +export interface AutoFollowPatternToEs { + remote_cluster: string; + leader_index_patterns: string[]; + follow_index_pattern: string; +} + +export interface ShardFromEs { + remote_cluster: string; + leader_index: string; + shard_id: number; + leader_global_checkpoint: number; + leader_max_seq_no: number; + follower_global_checkpoint: number; + follower_max_seq_no: number; + last_requested_seq_no: number; + outstanding_read_requests: number; + outstanding_write_requests: number; + write_buffer_operation_count: number; + write_buffer_size_in_bytes: number; + follower_mapping_version: number; + follower_settings_version: number; + total_read_time_millis: number; + total_read_remote_exec_time_millis: number; + successful_read_requests: number; + failed_read_requests: number; + operations_read: number; + bytes_read: number; + total_write_time_millis: number; + successful_write_requests: number; + failed_write_requests: number; + operations_written: number; + // This is an array of exception objects + read_exceptions: any[]; + time_since_last_read_millis: number; +} + +export interface Shard { + remoteCluster: string; + leaderIndex: string; + id: number; + leaderGlobalCheckpoint: number; + leaderMaxSequenceNum: number; + followerGlobalCheckpoint: number; + followerMaxSequenceNum: number; + lastRequestedSequenceNum: number; + outstandingReadRequestsCount: number; + outstandingWriteRequestsCount: number; + writeBufferOperationsCount: number; + writeBufferSizeBytes: number; + followerMappingVersion: number; + followerSettingsVersion: number; + totalReadTimeMs: number; + totalReadRemoteExecTimeMs: number; + successfulReadRequestCount: number; + failedReadRequestsCount: number; + operationsReadCount: number; + bytesReadCount: number; + totalWriteTimeMs: number; + successfulWriteRequestsCount: number; + failedWriteRequestsCount: number; + operationsWrittenCount: number; + // This is an array of exception objects + readExceptions: any[]; + timeSinceLastReadMs: number; +} + +export interface FollowerIndexFromEs { + follower_index: string; + remote_cluster: string; + leader_index: string; + status: string; + // Once https://github.com/elastic/elasticsearch/issues/54996 is resolved so that paused follower + // indices contain this information, we can removed this optional typing as well as the optional + // typing in FollowerIndexAdvancedSettings and FollowerIndexAdvancedSettingsToEs. + parameters?: FollowerIndexAdvancedSettingsToEs; + shards: ShardFromEs[]; +} + +export interface FollowerIndex extends FollowerIndexAdvancedSettings { + name: string; + remoteCluster: string; + leaderIndex: string; + status: string; + shards: Shard[]; +} + +export interface FollowerIndexToEs extends FollowerIndexAdvancedSettingsToEs { + remote_cluster: string; + leader_index: string; +} + +export interface FollowerIndexAdvancedSettings { + maxReadRequestOperationCount?: number; + maxOutstandingReadRequests?: number; + maxReadRequestSize?: string; // byte value + maxWriteRequestOperationCount?: number; + maxWriteRequestSize?: string; // byte value + maxOutstandingWriteRequests?: number; + maxWriteBufferCount?: number; + maxWriteBufferSize?: string; // byte value + maxRetryDelay?: string; // time value + readPollTimeout?: string; // time value +} + +export interface FollowerIndexAdvancedSettingsToEs { + max_read_request_operation_count?: number; + max_outstanding_read_requests?: number; + max_read_request_size?: string; // byte value + max_write_request_operation_count?: number; + max_write_request_size?: string; // byte value + max_outstanding_write_requests?: number; + max_write_buffer_count?: number; + max_write_buffer_size?: string; // byte value + max_retry_delay?: string; // time value + read_poll_timeout?: string; // time value +} + +export interface RecentAutoFollowError { + timestamp: number; + leaderIndex: string; + autoFollowException: { + type: string; + reason: string; + }; +} + +export interface RecentAutoFollowErrorFromEs { + timestamp: number; + leader_index: string; + auto_follow_exception: { + type: string; + reason: string; + }; +} + +export interface AutoFollowedCluster { + clusterName: string; + timeSinceLastCheckMillis: number; + lastSeenMetadataVersion: number; +} + +export interface AutoFollowedClusterFromEs { + cluster_name: string; + time_since_last_check_millis: number; + last_seen_metadata_version: number; +} + +export interface AutoFollowStats { + numberOfFailedFollowIndices: number; + numberOfFailedRemoteClusterStateRequests: number; + numberOfSuccessfulFollowIndices: number; + recentAutoFollowErrors: RecentAutoFollowError[]; + autoFollowedClusters: AutoFollowedCluster[]; +} + +export interface AutoFollowStatsFromEs { + number_of_failed_follow_indices: number; + number_of_failed_remote_cluster_state_requests: number; + number_of_successful_follow_indices: number; + recent_auto_follow_errors: RecentAutoFollowErrorFromEs[]; + auto_followed_clusters: AutoFollowedClusterFromEs[]; +} diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json new file mode 100644 index 0000000000000..ccf98f41def47 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "crossClusterReplication", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "home", + "licensing", + "management", + "remoteClusters", + "indexManagement" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": ["xpack", "ccr"] +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js index 2be00e70f6f84..db1430d157183 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -3,11 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { indexPatterns } from '../../../../../../src/plugins/data/public'; -jest.mock('ui/new_platform'); +import { indexPatterns } from '../../../../../../src/plugins/data/public'; +import './mocks'; +import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; const { setup } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js index abc3e5dc9def2..170bce7b82085 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { AutoFollowPatternForm } from '../../public/np_ready/app/components/auto_follow_pattern_form'; +import { AutoFollowPatternForm } from '../../app/components/auto_follow_pattern_form'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { AUTO_FOLLOW_PATTERN_EDIT } from './helpers/constants'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.autoFollowPatternEdit; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js index 20e982856dc19..190400e988634 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; +import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { getAutoFollowPatternClientMock } from '../../fixtures/auto_follow_pattern'; - -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.autoFollowPatternList; describe('<AutoFollowPatternList />', () => { @@ -79,11 +76,11 @@ describe('<AutoFollowPatternList />', () => { const testPrefix = 'prefix_'; const testSuffix = '_suffix'; - const autoFollowPattern1 = getAutoFollowPatternClientMock({ + const autoFollowPattern1 = getAutoFollowPatternMock({ name: `a${getRandomString()}`, followIndexPattern: `${testPrefix}{{leader_index}}${testSuffix}`, }); - const autoFollowPattern2 = getAutoFollowPatternClientMock({ + const autoFollowPattern2 = getAutoFollowPatternMock({ name: `b${getRandomString()}`, followIndexPattern: '{{leader_index}}', // no prefix nor suffix }); @@ -305,10 +302,12 @@ describe('<AutoFollowPatternList />', () => { const message = 'bar'; const recentAutoFollowErrors = [ { + timestamp: 1587081600021, leaderIndex: `${autoFollowPattern1.name}:my-leader-test`, autoFollowException: { type: 'exception', reason: message }, }, { + timestamp: 1587081600021, leaderIndex: `${autoFollowPattern2.name}:my-leader-test`, autoFollowException: { type: 'exception', reason: message }, }, @@ -327,7 +326,7 @@ describe('<AutoFollowPatternList />', () => { expect(exists('autoFollowPatternDetail.errors')).toBe(true); expect(exists('autoFollowPatternDetail.titleErrors')).toBe(true); expect(find('autoFollowPatternDetail.recentError').map(error => error.text())).toEqual([ - message, + 'April 16th, 2020 8:00:00 PM: bar', ]); }); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts new file mode 100644 index 0000000000000..e6444c37e8590 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts @@ -0,0 +1,28 @@ +/* + * 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 { getRandomString } from '../../../../../../test_utils'; +import { AutoFollowPattern } from '../../../../common/types'; + +export const getAutoFollowPatternMock = ({ + name = getRandomString(), + active = false, + remoteCluster = getRandomString(), + leaderIndexPatterns = [`${getRandomString()}-*`], + followIndexPattern = getRandomString(), +}: { + name: string; + active: boolean; + remoteCluster: string; + leaderIndexPatterns: string[]; + followIndexPattern: string; +}): AutoFollowPattern => ({ + name, + active, + remoteCluster, + leaderIndexPatterns, + followIndexPattern, +}); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts new file mode 100644 index 0000000000000..ff051d470531b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getRandomString } from '../../../../../../test_utils'; +import { FollowerIndex } from '../../../../common/types'; + +const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires +const chance = new Chance(); + +interface FollowerIndexMock { + name: string; + remoteCluster: string; + leaderIndex: string; + status: string; +} + +export const getFollowerIndexMock = ({ + name = getRandomString(), + remoteCluster = getRandomString(), + leaderIndex = getRandomString(), + status = 'Active', +}: FollowerIndexMock): FollowerIndex => ({ + name, + remoteCluster, + leaderIndex, + status, + maxReadRequestOperationCount: chance.integer(), + maxOutstandingReadRequests: chance.integer(), + maxReadRequestSize: getRandomString({ length: 5 }), + maxWriteRequestOperationCount: chance.integer(), + maxWriteRequestSize: '9223372036854775807b', + maxOutstandingWriteRequests: chance.integer(), + maxWriteBufferCount: chance.integer(), + maxWriteBufferSize: getRandomString({ length: 5 }), + maxRetryDelay: getRandomString({ length: 5 }), + readPollTimeout: getRandomString({ length: 5 }), + shards: [ + { + id: 0, + remoteCluster, + leaderIndex, + leaderGlobalCheckpoint: chance.integer(), + leaderMaxSequenceNum: chance.integer(), + followerGlobalCheckpoint: chance.integer(), + followerMaxSequenceNum: chance.integer(), + lastRequestedSequenceNum: chance.integer(), + outstandingReadRequestsCount: chance.integer(), + outstandingWriteRequestsCount: chance.integer(), + writeBufferOperationsCount: chance.integer(), + writeBufferSizeBytes: chance.integer(), + followerMappingVersion: chance.integer(), + followerSettingsVersion: chance.integer(), + totalReadTimeMs: chance.integer(), + totalReadRemoteExecTimeMs: chance.integer(), + successfulReadRequestCount: chance.integer(), + failedReadRequestsCount: chance.integer(), + operationsReadCount: chance.integer(), + bytesReadCount: chance.integer(), + totalWriteTimeMs: chance.integer(), + successfulWriteRequestsCount: chance.integer(), + failedWriteRequestsCount: chance.integer(), + operationsWrittenCount: chance.integer(), + readExceptions: [], + timeSinceLastReadMs: chance.integer(), + }, + ], +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js index 7680be9d858a4..4c99339e16952 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { RemoteClustersFormField } from '../../public/np_ready/app/components'; - import { indexPatterns } from '../../../../../../src/plugins/data/public'; - -jest.mock('ui/new_platform'); +import './mocks'; +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { RemoteClustersFormField } from '../../app/components'; const { setup } = pageHelpers.followerIndexAdd; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js new file mode 100644 index 0000000000000..0e4586f707f42 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js @@ -0,0 +1,174 @@ +/* + * 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 { API_BASE_PATH } from '../../../common/constants'; +import { FollowerIndexForm } from '../../app/components/follower_index_form/follower_index_form'; +import './mocks'; +import { FOLLOWER_INDEX_EDIT } from './helpers/constants'; +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; + +const { setup } = pageHelpers.followerIndexEdit; +const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd; + +describe('Edit follower index', () => { + let server; + let httpRequestsMockHelpers; + + beforeAll(() => { + ({ server, httpRequestsMockHelpers } = setupEnvironment()); + }); + + afterAll(() => { + server.restore(); + }); + + describe('on component mount', () => { + let find; + let component; + + const remoteClusters = [{ name: 'new-york', seeds: ['localhost:123'], isConnected: true }]; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); + httpRequestsMockHelpers.setGetFollowerIndexResponse(FOLLOWER_INDEX_EDIT); + ({ component, find } = setup()); + + await nextTick(); + component.update(); + }); + + /** + * As the "edit" follower index component uses the same form underneath that + * the "create" follower index, we won't test it again but simply make sure that + * the form component is indeed shared between the 2 app sections. + */ + test('should use the same Form component as the "<FollowerIndexAdd />" component', async () => { + const { component: addFollowerIndexComponent } = setupFollowerIndexAdd(); + + await nextTick(); + addFollowerIndexComponent.update(); + + const formEdit = component.find(FollowerIndexForm); + const formAdd = addFollowerIndexComponent.find(FollowerIndexForm); + + expect(formEdit.length).toBe(1); + expect(formAdd.length).toBe(1); + }); + + test('should populate the form fields with the values from the follower index loaded', () => { + const inputToPropMap = { + remoteClusterInput: 'remoteCluster', + leaderIndexInput: 'leaderIndex', + followerIndexInput: 'name', + maxReadRequestOperationCountInput: 'maxReadRequestOperationCount', + maxOutstandingReadRequestsInput: 'maxOutstandingReadRequests', + maxReadRequestSizeInput: 'maxReadRequestSize', + maxWriteRequestOperationCountInput: 'maxWriteRequestOperationCount', + maxWriteRequestSizeInput: 'maxWriteRequestSize', + maxOutstandingWriteRequestsInput: 'maxOutstandingWriteRequests', + maxWriteBufferCountInput: 'maxWriteBufferCount', + maxWriteBufferSizeInput: 'maxWriteBufferSize', + maxRetryDelayInput: 'maxRetryDelay', + readPollTimeoutInput: 'readPollTimeout', + }; + + Object.entries(inputToPropMap).forEach(([input, prop]) => { + const expected = FOLLOWER_INDEX_EDIT[prop]; + const { value } = find(input).props(); + try { + expect(value).toBe(expected); + } catch { + throw new Error( + `Input "${input}" does not equal "${expected}". (Value received: "${value}")` + ); + } + }); + }); + }); + + describe('API', () => { + const remoteClusters = [{ name: 'new-york', seeds: ['localhost:123'], isConnected: true }]; + let testBed; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); + httpRequestsMockHelpers.setGetFollowerIndexResponse(FOLLOWER_INDEX_EDIT); + + testBed = await setup(); + await testBed.waitFor('followerIndexForm'); + }); + + test('is consumed correctly', async () => { + const { actions, form, component, find, waitFor } = testBed; + + form.setInputValue('maxRetryDelayInput', '10s'); + + actions.clickSaveForm(); + component.update(); // The modal to confirm the update opens + await waitFor('confirmModalTitleText'); + find('confirmModalConfirmButton').simulate('click'); + + await nextTick(); // Make sure the Request went through + + const latestRequest = server.requests[server.requests.length - 1]; + const requestBody = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + // Verify the API endpoint called: method, path and payload + expect(latestRequest.method).toBe('PUT'); + expect(latestRequest.url).toBe( + `${API_BASE_PATH}/follower_indices/${FOLLOWER_INDEX_EDIT.name}` + ); + expect(requestBody).toEqual({ + maxReadRequestOperationCount: 7845, + maxOutstandingReadRequests: 16, + maxReadRequestSize: '64mb', + maxWriteRequestOperationCount: 2456, + maxWriteRequestSize: '1048b', + maxOutstandingWriteRequests: 69, + maxWriteBufferCount: 123456, + maxWriteBufferSize: '256mb', + maxRetryDelay: '10s', + readPollTimeout: '2m', + }); + }); + }); + + describe('when the remote cluster is disconnected', () => { + let find; + let exists; + let component; + let actions; + let form; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadRemoteClustersResponse([ + { name: 'new-york', seeds: ['localhost:123'], isConnected: false }, + ]); + httpRequestsMockHelpers.setGetFollowerIndexResponse(FOLLOWER_INDEX_EDIT); + ({ component, find, exists, actions, form } = setup()); + + await nextTick(); + component.update(); + }); + + test('should display an error and have a button to edit the remote cluster', () => { + const error = find('remoteClusterFormField.notConnectedError'); + + expect(error.length).toBe(1); + expect(error.find('.euiCallOutHeader__title').text()).toBe( + `Can't edit follower index because remote cluster '${FOLLOWER_INDEX_EDIT.remoteCluster}' is not connected` + ); + expect(exists('remoteClusterFormField.notConnectedError.editButton')).toBe(true); + }); + + test('should prevent saving the form and display an error message for the required remote cluster', () => { + actions.clickSaveForm(); + + expect(form.getErrorsMessages()).toEqual(['A connected remote cluster is required.']); + expect(find('submitButton').props().disabled).toBe(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index dde31d1d166f9..f98a1dafbbcbf 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getFollowerIndexMock } from './fixtures/follower_index'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { getFollowerIndexMock } from '../../fixtures/follower_index'; - -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.followerIndexList; describe('<FollowerIndicesList />', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js similarity index 76% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js index 1f64e589bc4c1..1cb4e7c7725df 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternAdd } from '../../../public/np_ready/app/sections/auto_follow_pattern_add'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternAdd } from '../../../app/sections/auto_follow_pattern_add'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js similarity index 82% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js index 2b110c6552072..9cad61893c409 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternEdit } from '../../../public/np_ready/app/sections/auto_follow_pattern_edit'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternEdit } from '../../../app/sections/auto_follow_pattern_edit'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js index 1d3e8ad6dff83..450feed49f9f2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { AutoFollowPatternList } from '../../../public/np_ready/app/sections/home/auto_follow_pattern_list'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternList } from '../../../app/sections/home/auto_follow_pattern_list'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js similarity index 79% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js index f74baa1b2ad0a..856b09f3f3cba 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexAdd } from '../../../public/np_ready/app/sections/follower_index_add'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndexAdd } from '../../../app/sections/follower_index_add'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js similarity index 84% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js index 47f8539bb593b..893d01f151bc2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexEdit } from '../../../public/np_ready/app/sections/follower_index_edit'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndexEdit } from '../../../app/sections/follower_index_edit'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; import { FOLLOWER_INDEX_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js similarity index 90% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js index 2154e11e17b1f..52f4267594cc1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { FollowerIndicesList } from '../../../public/np_ready/app/sections/home/follower_indices_list'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndicesList } from '../../../app/sections/home/follower_indices_list'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js new file mode 100644 index 0000000000000..56dfa765bfa4f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerTestBed } from '../../../../../../test_utils'; +import { BASE_PATH } from '../../../../common/constants'; +import { CrossClusterReplicationHome } from '../../../app/sections/home/home'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; + +const testBedConfig = { + store: ccrStore, + memoryRouter: { + initialEntries: [`${BASE_PATH}/follower_indices`], + componentRoutePath: `${BASE_PATH}/:section`, + onRouter: router => (routing.reactRouter = router), + }, +}; + +export const setup = registerTestBed(CrossClusterReplicationHome, testBedConfig); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js similarity index 91% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js index 3562ad0df5b51..6dedbbfa79b19 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js @@ -7,7 +7,7 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { setHttpClient } from '../../../public/np_ready/app/services/api'; +import { setHttpClient } from '../../../app/services/api'; import { init as initHttpRequests } from './http_requests'; export const setupEnvironment = () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js similarity index 93% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js index 2c536d069ef53..18d8b4eb9dbe0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.home; describe('<CrossClusterReplicationHome />', () => { @@ -36,7 +34,7 @@ describe('<CrossClusterReplicationHome />', () => { ({ exists, find, component } = setup()); }); - test('should set the correct an app title', () => { + test('should set the correct app title', () => { expect(exists('appTitle')).toBe(true); expect(find('appTitle').text()).toEqual('Cross-Cluster Replication'); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts new file mode 100644 index 0000000000000..60a196254d408 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../app/services/breadcrumbs', () => ({ + ...jest.requireActual('../../../app/services/breadcrumbs'), + setBreadcrumbs: jest.fn(), +})); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts new file mode 100644 index 0000000000000..cff9c003f3e80 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './breadcrumbs.mock'; +import './track_ui_metric.mock'; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts new file mode 100644 index 0000000000000..016e259343285 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../app/services/track_ui_metric', () => ({ + ...jest.requireActual('../../../app/services/track_ui_metric'), + trackUiMetric: jest.fn(), + trackUserRequest: (request: Promise<any>) => { + return request.then(response => response); + }, +})); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/app.tsx b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx new file mode 100644 index 0000000000000..ec349ccd6f2c7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Component, Fragment } from 'react'; +import { Route, Switch, Redirect, withRouter, RouteComponentProps } from 'react-router-dom'; +import { History } from 'history'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPageContent, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { BASE_PATH } from '../../common/constants'; +import { getFatalErrors } from './services/notifications'; +import { SectionError } from './components'; +import { routing } from './services/routing'; +// @ts-ignore +import { loadPermissions } from './services/api'; + +// @ts-ignore +import { + CrossClusterReplicationHome, + AutoFollowPatternAdd, + AutoFollowPatternEdit, + FollowerIndexAdd, + FollowerIndexEdit, +} from './sections'; + +interface AppProps { + history: History; + location: any; +} + +interface AppState { + isFetchingPermissions: boolean; + fetchPermissionError: any; + hasPermission: boolean; + missingClusterPrivileges: any[]; +} + +class AppComponent extends Component<RouteComponentProps & AppProps, AppState> { + constructor(props: any) { + super(props); + this.registerRouter(); + + this.state = { + isFetchingPermissions: false, + fetchPermissionError: undefined, + hasPermission: false, + missingClusterPrivileges: [], + }; + } + + componentDidMount() { + this.checkPermissions(); + } + + async checkPermissions() { + this.setState({ + isFetchingPermissions: true, + }); + + try { + const { hasPermission, missingClusterPrivileges } = await loadPermissions(); + + this.setState({ + isFetchingPermissions: false, + hasPermission, + missingClusterPrivileges, + }); + } catch (error) { + // Expect an error in the shape provided by Angular's $http service. + if (error && error.body) { + return this.setState({ + isFetchingPermissions: false, + fetchPermissionError: error, + }); + } + + // This error isn't an HTTP error, so let the fatal error screen tell the user something + // unexpected happened. + getFatalErrors().add( + error, + i18n.translate('xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle', { + defaultMessage: 'Cross-Cluster Replication app', + }) + ); + } + } + + registerRouter() { + const { history, location } = this.props; + routing.reactRouter = { + history, + route: { + location, + }, + }; + } + + render() { + const { + isFetchingPermissions, + fetchPermissionError, + hasPermission, + missingClusterPrivileges, + } = this.state; + + if (isFetchingPermissions) { + return ( + <EuiPageContent horizontalPosition="center"> + <EuiFlexGroup alignItems="center" gutterSize="m"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="l" /> + </EuiFlexItem> + + <EuiFlexItem> + <EuiTitle size="s"> + <h2> + <FormattedMessage + id="xpack.crossClusterReplication.app.permissionCheckTitle" + defaultMessage="Checking permissions…" + /> + </h2> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPageContent> + ); + } + + if (fetchPermissionError) { + return ( + <Fragment> + <SectionError + title={ + <FormattedMessage + id="xpack.crossClusterReplication.app.permissionCheckErrorTitle" + defaultMessage="Error checking permissions" + /> + } + error={fetchPermissionError} + /> + + <EuiSpacer size="m" /> + </Fragment> + ); + } + + if (!hasPermission) { + return ( + <EuiPageContent horizontalPosition="center"> + <EuiEmptyPrompt + iconType="securityApp" + title={ + <h2> + <FormattedMessage + id="xpack.crossClusterReplication.app.deniedPermissionTitle" + defaultMessage="You're missing cluster privileges" + /> + </h2> + } + body={ + <p> + <FormattedMessage + id="xpack.crossClusterReplication.app.deniedPermissionDescription" + defaultMessage="To use Cross-Cluster Replication, you must have {clusterPrivilegesCount, + plural, one {this cluster privilege} other {these cluster privileges}}: {clusterPrivileges}." + values={{ + clusterPrivileges: missingClusterPrivileges.join(', '), + clusterPrivilegesCount: missingClusterPrivileges.length, + }} + /> + </p> + } + /> + </EuiPageContent> + ); + } + + return ( + <div> + <Switch> + <Redirect exact from={`${BASE_PATH}`} to={`${BASE_PATH}/follower_indices`} /> + <Route + exact + path={`${BASE_PATH}/auto_follow_patterns/add`} + component={AutoFollowPatternAdd} + /> + <Route + exact + path={`${BASE_PATH}/auto_follow_patterns/edit/:id`} + component={AutoFollowPatternEdit} + /> + <Route exact path={`${BASE_PATH}/follower_indices/add`} component={FollowerIndexAdd} /> + <Route + exact + path={`${BASE_PATH}/follower_indices/edit/:id`} + component={FollowerIndexEdit} + /> + <Route exact path={`${BASE_PATH}/:section`} component={CrossClusterReplicationHome} /> + </Switch> + </div> + ); + } +} + +export const App = withRouter(AppComponent); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap b/x-pack/plugins/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap rename to x-pack/plugins/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx index 12654e56bde97..5474708f313c8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { AutoFollowPatternDeleteProvider } from '../auto_follow_pattern_delete_provider'; // @ts-ignore -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; const actionsAriaLabel = i18n.translate( 'xpack.crossClusterReplication.autoFollowActionMenu.autoFollowPatternActionMenuButtonAriaLabel', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/index.ts b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/index.ts rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.d.ts rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js index 7803b329e6258..f9c03165dcf97 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { deleteAutoFollowPattern } from '../store/actions'; -import { arrify } from '../../../../common/services/utils'; +import { arrify } from '../../../common/services/utils'; class AutoFollowPatternDeleteProviderUi extends PureComponent { state = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js index 5bc5d8ba6e402..c817637ae1854 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js @@ -29,10 +29,10 @@ import { EuiTitle, } from '@elastic/eui'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../src/plugins/data/public'; -import routing from '../services/routing'; +import { routing } from '../services/routing'; import { extractQueryParams } from '../services/query_params'; import { getRemoteClusterName } from '../services/get_remote_cluster_name'; import { API_STATUS } from '../constants'; @@ -512,7 +512,6 @@ export class AutoFollowPatternForm extends PureComponent { <EuiFlexGroup gutterSize="s"> <EuiFlexItem> <EuiFormRow - className="ccrFollowerIndicesFormRow" label={ <FormattedMessage id="xpack.crossClusterReplication.autoFollowPatternForm.autoFollowPattern.fieldPrefixLabel" @@ -535,7 +534,6 @@ export class AutoFollowPatternForm extends PureComponent { <EuiFlexItem> <EuiFormRow - className="ccrFollowerIndicesFormRow" label={ <FormattedMessage id="xpack.crossClusterReplication.autoFollowPatternForm.autoFollowPattern.fieldSuffixLabel" @@ -557,9 +555,7 @@ export class AutoFollowPatternForm extends PureComponent { </EuiFlexItem> </EuiFlexGroup> - <EuiFormHelpText - className={isPrefixInvalid || isSuffixInvalid ? null : 'ccrFollowerIndicesHelpText'} - > + <EuiFormHelpText> <FormattedMessage id="xpack.crossClusterReplication.autoFollowPatternForm.fieldFollowerIndicesHelpLabel" defaultMessage="Spaces and the characters {characterList} are not allowed." diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_indices_preview.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_indices_preview.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_page_title.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_page_title.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_request_flyout.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js similarity index 96% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_request_flyout.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js index 1377ccc9debd6..7e0bd510a1a86 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_request_flyout.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_request_flyout.js @@ -26,7 +26,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { serializeAutoFollowPattern } from '../../../common/services/auto_follow_pattern_serialization'; export class AutoFollowPatternRequestFlyout extends PureComponent { static propTypes = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/advanced_settings_fields.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/advanced_settings_fields.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/advanced_settings_fields.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_form.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_form.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js index 50b22289e30f8..d824efd56cef1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_form.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.js @@ -28,9 +28,9 @@ import { EuiTitle, } from '@elastic/eui'; -import { indices } from '../../../../../../../../../src/plugins/es_ui_shared/public'; +import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; import { indexNameValidator, leaderIndexValidator } from '../../services/input_validation'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { getFatalErrors } from '../../services/notifications'; import { loadIndices } from '../../services/api'; import { API_STATUS } from '../../constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_form.test.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_form.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_request_flyout.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js similarity index 96% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_request_flyout.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js index cb02a929b16f8..cba1c104e45d9 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_request_flyout.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js @@ -26,7 +26,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { serializeFollowerIndex } from '../../../../../common/services/follower_index_serialization'; +import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; export class FollowerIndexRequestFlyout extends PureComponent { static propTypes = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/index.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_form/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_page_title.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_page_title.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_pause_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_pause_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js index 56372425b708f..5965bfd8cc603 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_pause_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_pause_provider.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { pauseFollowerIndex } from '../store/actions'; -import { arrify } from '../../../../common/services/utils'; +import { arrify } from '../../../common/services/utils'; import { areAllSettingsDefault } from '../services/follower_index_default_settings'; class FollowerIndexPauseProviderUi extends PureComponent { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_resume_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_resume_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js index 56e1d93106807..ed9bc015c87e2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_resume_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_resume_provider.js @@ -11,9 +11,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiLink, EuiOverlayMask } from '@elastic/eui'; -import routing from '../services/routing'; +import { routing } from '../services/routing'; import { resumeFollowerIndex } from '../store/actions'; -import { arrify } from '../../../../common/services/utils'; +import { arrify } from '../../../common/services/utils'; class FollowerIndexResumeProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_unfollow_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_unfollow_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js index 8ebc38e9296c1..f3b267a69b18c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_unfollow_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_unfollow_provider.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { unfollowLeaderIndex } from '../store/actions'; -import { arrify } from '../../../../common/services/utils'; +import { arrify } from '../../../common/services/utils'; class FollowerIndexUnfollowProviderUi extends PureComponent { static propTypes = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/form_entry_row.js b/x-pack/plugins/cross_cluster_replication/public/app/components/form_entry_row.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/form_entry_row.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/form_entry_row.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/index.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/components/index.d.ts new file mode 100644 index 0000000000000..b27d04b33ada7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/index.d.ts @@ -0,0 +1,7 @@ +/* + * 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 declare const SectionError: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_form_field.js b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_form_field.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_form_field.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_form_field.js index 6a4eb7e775f3c..00d29ffdca3a6 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_form_field.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_form_field.js @@ -18,8 +18,8 @@ import { EuiFieldText, } from '@elastic/eui'; -import routing from '../services/routing'; -import { BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants'; +import { routing } from '../services/routing'; +import { BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; const errorMessages = { noClusterFound: () => ( diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts diff --git a/x-pack/plugins/cross_cluster_replication/public/app/index.tsx b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx new file mode 100644 index 0000000000000..79569b587f97f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Provider } from 'react-redux'; +import { HashRouter } from 'react-router-dom'; +import { I18nStart } from 'kibana/public'; +import { UnmountCallback } from 'src/core/public'; + +import { init as initBreadcrumbs, SetBreadcrumbs } from './services/breadcrumbs'; +import { init as initDocumentation } from './services/documentation_links'; +import { App } from './app'; +import { ccrStore } from './store'; + +const renderApp = (element: Element, I18nContext: I18nStart['Context']): UnmountCallback => { + render( + <I18nContext> + <Provider store={ccrStore}> + <HashRouter> + <App /> + </HashRouter> + </Provider> + </I18nContext>, + element + ); + + return () => unmountComponentAtNode(element); +}; + +export async function mountApp({ + element, + setBreadcrumbs, + I18nContext, + ELASTIC_WEBSITE_URL, + DOC_LINK_VERSION, +}: { + element: Element; + setBreadcrumbs: SetBreadcrumbs; + I18nContext: I18nStart['Context']; + ELASTIC_WEBSITE_URL: string; + DOC_LINK_VERSION: string; +}): Promise<UnmountCallback> { + // Import and initialize additional services here instead of in plugin.ts to reduce the size of the + // initial bundle as much as possible. + initBreadcrumbs(setBreadcrumbs); + initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`); + + return renderApp(element, I18nContext); +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js similarity index 80% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js index 2c90456076f85..be470edc07537 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js @@ -39,8 +39,23 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ getAutoFollowPattern: id => dispatch(getAutoFollowPattern(id)), selectAutoFollowPattern: id => dispatch(selectEditAutoFollowPattern(id)), - saveAutoFollowPattern: (id, autoFollowPattern) => - dispatch(saveAutoFollowPattern(id, autoFollowPattern, true)), + saveAutoFollowPattern: (id, autoFollowPattern) => { + // Strip out errors. + const { active, remoteCluster, leaderIndexPatterns, followIndexPattern } = autoFollowPattern; + + dispatch( + saveAutoFollowPattern( + id, + { + active, + remoteCluster, + leaderIndexPatterns, + followIndexPattern, + }, + true + ) + ); + }, clearApiError: () => { dispatch(clearApiError(`${scope}-get`)); dispatch(clearApiError(`${scope}-save`)); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js index 4cd3617abd989..387d7817a0357 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui'; import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { AutoFollowPatternForm, AutoFollowPatternPageTitle, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js index 21493602c12a7..22f9a7338384b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { FollowerIndexForm, FollowerIndexPageTitle, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js index e9e14f57e814f..c8cf94842aa68 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import routing from '../../../services/routing'; +import { routing } from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 956a9f10d810b..eb90e59e99fee 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -20,8 +20,8 @@ import { AutoFollowPatternDeleteProvider, AutoFollowPatternActionMenu, } from '../../../../../components'; -import routing from '../../../../../services/routing'; -import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; +import { routing } from '../../../../../services/routing'; +import { trackUiMetric } from '../../../../../services/track_ui_metric'; export class AutoFollowPatternTable extends PureComponent { static propTypes = { @@ -86,7 +86,7 @@ export class AutoFollowPatternTable extends PureComponent { return ( <EuiLink onClick={() => { - trackUiMetric(METRIC_TYPE.CLICK, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); selectAutoFollowPattern(name); }} data-test-subj="autoFollowPatternLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js new file mode 100644 index 0000000000000..3f2ed82420ff1 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js @@ -0,0 +1,373 @@ +/* + * 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, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; + +import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public'; + +import { + EuiButtonEmpty, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiHealth, + EuiIcon, + EuiLink, + EuiSpacer, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; + +import { + AutoFollowPatternIndicesPreview, + AutoFollowPatternActionMenu, +} from '../../../../../components'; + +export class DetailPanel extends Component { + static propTypes = { + apiStatus: PropTypes.string, + autoFollowPatternId: PropTypes.string, + autoFollowPattern: PropTypes.object, + closeDetailPanel: PropTypes.func.isRequired, + }; + + renderAutoFollowPattern({ + followIndexPatternPrefix, + followIndexPatternSuffix, + remoteCluster, + leaderIndexPatterns, + active, + }) { + return ( + <> + <section> + <EuiDescriptionList> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.statusLabel" + defaultMessage="Status" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="status"> + {!active ? ( + <EuiHealth color="subdued"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.pausedStatus" + defaultMessage="Paused" + /> + </EuiHealth> + ) : ( + <EuiHealth color="success"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.activeStatus" + defaultMessage="Active" + /> + </EuiHealth> + )} + </EuiDescriptionListDescription> + </EuiDescriptionList> + </section> + + <EuiSpacer size="s" /> + <section + aria-labelledby="ccrAutoFollowPatternDetailSettingsTitle" + data-test-subj="settingsSection" + > + <EuiTitle size="s"> + <h3 id="ccrAutoFollowPatternDetailSettingsTitle"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.statusTitle" + defaultMessage="Settings" + /> + </h3> + </EuiTitle> + + <EuiSpacer size="s" /> + + <EuiDescriptionList data-test-subj="settingsValues"> + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.remoteClusterLabel" + defaultMessage="Remote cluster" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="remoteCluster"> + {remoteCluster} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.leaderPatternsLabel" + defaultMessage="Leader patterns" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="leaderIndexPatterns"> + {leaderIndexPatterns.join(', ')} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.prefixLabel" + defaultMessage="Prefix" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="patternPrefix"> + {followIndexPatternPrefix || ( + <em> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.prefixEmptyValue" + defaultMessage="No prefix" + /> + </em> + )} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.suffixLabel" + defaultMessage="Suffix" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="patternSuffix"> + {followIndexPatternSuffix || ( + <em> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.suffixEmptyValue" + defaultMessage="No suffix" + /> + </em> + )} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + </EuiDescriptionList> + </section> + </> + ); + } + + renderIndicesPreview(prefix, suffix, leaderIndexPatterns) { + return ( + <section data-test-subj="indicesPreviewSection"> + <AutoFollowPatternIndicesPreview + prefix={prefix} + suffix={suffix} + leaderIndexPatterns={leaderIndexPatterns} + /> + </section> + ); + } + + renderAutoFollowPatternNotFound() { + return ( + <EuiFlyoutBody> + <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiIcon size="m" type="alert" color="danger" /> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiText> + <EuiTextColor color="subdued"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.notFoundLabel" + defaultMessage="Auto-follow pattern not found" + /> + </EuiTextColor> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + ); + } + + renderAutoFollowPatternErrors(autoFollowPattern) { + if (!autoFollowPattern.errors.length) { + return null; + } + + return ( + <section data-test-subj="errors"> + <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiIcon type="alert" color="danger" /> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiTitle size="s" data-test-subj="titleErrors"> + <h3> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.recentErrorsTitle" + defaultMessage="Recent errors" + /> + </h3> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="s" /> + <EuiText> + <ul> + {autoFollowPattern.errors.map((error, i) => ( + <li key={i} data-test-subj="recentError"> + <strong>{moment(error.timestamp).format('MMMM Do, YYYY h:mm:ss A')}</strong>:{' '} + {error.autoFollowException.reason} + </li> + ))} + </ul> + </EuiText> + </section> + ); + } + + renderFlyoutBody() { + const { autoFollowPattern } = this.props; + + if (!autoFollowPattern) { + return this.renderAutoFollowPatternNotFound(); + } + + const { + followIndexPatternPrefix, + followIndexPatternSuffix, + leaderIndexPatterns, + } = autoFollowPattern; + + let indexManagementFilter; + + if (followIndexPatternPrefix) { + indexManagementFilter = `name:${followIndexPatternPrefix}`; + } else if (followIndexPatternSuffix) { + indexManagementFilter = `name:${followIndexPatternSuffix}`; + } + + const indexManagementUri = getIndexListUri(indexManagementFilter); + + return ( + <EuiFlyoutBody> + {this.renderAutoFollowPattern(autoFollowPattern)} + + <EuiSpacer size="m" /> + + {this.renderIndicesPreview( + followIndexPatternPrefix, + followIndexPatternSuffix, + leaderIndexPatterns + )} + + <EuiSpacer size="l" /> + + <EuiLink href={indexManagementUri} data-test-subj="viewIndexManagementLink"> + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.viewIndicesLink" + defaultMessage="View your follower indices in Index Management" + /> + </EuiLink> + + <EuiSpacer size="l" /> + + {this.renderAutoFollowPatternErrors(autoFollowPattern)} + </EuiFlyoutBody> + ); + } + + renderFlyoutFooter() { + const { autoFollowPattern, closeDetailPanel } = this.props; + + return ( + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="cross" + flush="left" + onClick={closeDetailPanel} + data-test-subj="closeFlyoutButton" + > + <FormattedMessage + id="xpack.crossClusterReplication.autoFollowPatternDetailPanel.closeButtonLabel" + defaultMessage="Close" + /> + </EuiButtonEmpty> + </EuiFlexItem> + + {autoFollowPattern && ( + <EuiFlexItem grow={false}> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={false}> + <AutoFollowPatternActionMenu + edit + arrowDirection="up" + patterns={[autoFollowPattern]} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlyoutFooter> + ); + } + + render() { + const { autoFollowPatternId, closeDetailPanel } = this.props; + + return ( + <EuiFlyout + data-test-subj="autoFollowPatternDetail" + onClose={closeDetailPanel} + aria-labelledby="autoFollowPatternDetailsFlyoutTitle" + size="m" + maxWidth={400} + > + <EuiFlyoutHeader> + <EuiTitle size="m" id="autoFollowPatternDetailsFlyoutTitle" data-test-subj="title"> + <h2>{autoFollowPatternId}</h2> + </EuiTitle> + </EuiFlyoutHeader> + + {this.renderFlyoutBody()} + {this.renderFlyoutFooter()} + </EuiFlyout> + ); + } +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js index 0f6ef75522ff7..4a66f7b717bac 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js @@ -15,7 +15,7 @@ import { EuiPopoverTitle, } from '@elastic/eui'; -import routing from '../../../../../services/routing'; +import { routing } from '../../../../../services/routing'; import { FollowerIndexPauseProvider, FollowerIndexResumeProvider, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js new file mode 100644 index 0000000000000..4436d76643e6c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js @@ -0,0 +1,509 @@ +/* + * 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, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCodeEditor, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiHealth, + EuiIcon, + EuiLoadingSpinner, + EuiSpacer, + EuiText, + EuiTextColor, + EuiTitle, +} from '@elastic/eui'; +import 'brace/theme/textmate'; + +import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public'; +import { API_STATUS } from '../../../../../constants'; +import { ContextMenu } from '../context_menu'; + +export class DetailPanel extends Component { + static propTypes = { + apiStatus: PropTypes.string, + followerIndexId: PropTypes.string, + followerIndex: PropTypes.object, + closeDetailPanel: PropTypes.func.isRequired, + }; + + renderFollowerIndex() { + const { + followerIndex: { + remoteCluster, + leaderIndex, + isPaused, + shards, + maxReadRequestOperationCount, + maxOutstandingReadRequests, + maxReadRequestSize, + maxWriteRequestOperationCount, + maxWriteRequestSize, + maxOutstandingWriteRequests, + maxWriteBufferCount, + maxWriteBufferSize, + maxRetryDelay, + readPollTimeout, + }, + } = this.props; + + return ( + <Fragment> + <EuiFlyoutBody> + <section> + <EuiDescriptionList> + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.statusLabel" + defaultMessage="Status" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="status"> + {isPaused ? ( + <EuiHealth color="subdued"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.pausedStatus" + defaultMessage="Paused" + /> + </EuiHealth> + ) : ( + <EuiHealth color="success"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.activeStatus" + defaultMessage="Active" + /> + </EuiHealth> + )} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.remoteClusterLabel" + defaultMessage="Remote cluster" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="remoteCluster"> + {remoteCluster} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.leaderIndexLabel" + defaultMessage="Leader index" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="leaderIndex"> + {leaderIndex} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + </EuiDescriptionList> + </section> + + <EuiSpacer size="l" /> + + <section + aria-labelledby="ccrFollowerIndexDetailSettingsTitle" + data-test-subj="settingsSection" + > + <EuiTitle size="s"> + <h3 id="ccrFollowerIndexDetailSettingsTitle"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.settingsTitle" + defaultMessage="Settings" + /> + </h3> + </EuiTitle> + + <EuiSpacer size="s" /> + + {isPaused ? ( + <EuiCallOut + size="s" + title={ + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.pausedFollowerCalloutTitle" + defaultMessage="A paused follower index does not have settings or shard statistics." + /> + } + /> + ) : ( + <EuiDescriptionList data-test-subj="settingsValues"> + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestOperationCountTitle" + defaultMessage="Max read request operation count" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxReadReqOpCount"> + {maxReadRequestOperationCount} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingReadRequestsTitle" + defaultMessage="Max outstanding read requests" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxOutstandingReadReq"> + {maxOutstandingReadRequests} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxReadRequestSizeTitle" + defaultMessage="Max read request size" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxReadReqSize"> + {maxReadRequestSize} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestOperationCountTitle" + defaultMessage="Max write request operation count" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxWriteReqOpCount"> + {maxWriteRequestOperationCount} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteRequestSizeTitle" + defaultMessage="Max write request size" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxWriteReqSize"> + {maxWriteRequestSize} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxOutstandingWriteRequestsTitle" + defaultMessage="Max outstanding write requests" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxOutstandingWriteReq"> + {maxOutstandingWriteRequests} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferCountTitle" + defaultMessage="Max write buffer count" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxWriteBufferCount"> + {maxWriteBufferCount} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxWriteBufferSizeTitle" + defaultMessage="Max write buffer size" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxWriteBufferSize"> + {maxWriteBufferSize} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + <EuiFlexGroup> + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.maxRetryDelayTitle" + defaultMessage="Max retry delay" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="maxRetryDelay"> + {maxRetryDelay} + </EuiDescriptionListDescription> + </EuiFlexItem> + + <EuiFlexItem> + <EuiDescriptionListTitle> + <EuiTitle size="xs"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexForm.advancedSettings.readPollTimeoutTitle" + defaultMessage="Read poll timeout" + /> + </EuiTitle> + </EuiDescriptionListTitle> + + <EuiDescriptionListDescription data-test-subj="readPollTimeout"> + {readPollTimeout} + </EuiDescriptionListDescription> + </EuiFlexItem> + </EuiFlexGroup> + </EuiDescriptionList> + )} + </section> + + <EuiSpacer size="l" /> + + <section data-test-subj="shardsStatsSection"> + {shards && + shards.map((shard, i) => ( + <Fragment key={i}> + <EuiSpacer size="m" /> + <EuiTitle size="xs"> + <h3> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.shardStatsTitle" + defaultMessage="Shard {id} stats" + values={{ + id: shard.id, + }} + /> + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiCodeEditor + mode="json" + theme="textmate" + width="100%" + isReadOnly + setOptions={{ maxLines: Infinity }} + value={JSON.stringify(shard, null, 2)} + editorProps={{ + $blockScrolling: Infinity, + }} + data-test-subj={`shardsStats${i}`} + /> + </Fragment> + ))} + </section> + </EuiFlyoutBody> + </Fragment> + ); + } + + renderContent() { + const { apiStatus, followerIndex } = this.props; + + if (apiStatus === API_STATUS.LOADING) { + return ( + <EuiFlyoutBody> + <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="m" /> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiText> + <EuiTextColor color="subdued"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.loadingLabel" + defaultMessage="Loading follower index…" + /> + </EuiTextColor> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + ); + } + + if (!followerIndex) { + return ( + <EuiFlyoutBody> + <EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiIcon size="m" type="alert" color="danger" /> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiText> + <EuiTextColor color="subdued"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.notFoundLabel" + defaultMessage="Follower index not found" + /> + </EuiTextColor> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutBody> + ); + } + + return this.renderFollowerIndex(); + } + + renderFooter() { + const { followerIndexId, followerIndex, closeDetailPanel } = this.props; + + // Use ID instead of followerIndex, because followerIndex may not be loaded yet. + const indexManagementUri = getIndexListUri(`name:${followerIndexId}`); + + return ( + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + iconType="cross" + flush="left" + onClick={closeDetailPanel} + data-test-subj="closeFlyoutButton" + > + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.closeButtonLabel" + defaultMessage="Close" + /> + </EuiButtonEmpty> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton href={indexManagementUri} data-test-subj="viewIndexManagementButton"> + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.viewIndexLink" + defaultMessage="View in Index Management" + /> + </EuiButton> + </EuiFlexItem> + + {followerIndex && ( + <EuiFlexItem grow={false}> + <ContextMenu + iconSide="left" + iconType="arrowUp" + anchorPosition="upRight" + label={ + <FormattedMessage + id="xpack.crossClusterReplication.followerIndexDetailPanel.manageButtonLabel" + defaultMessage="Manage" + /> + } + followerIndices={[followerIndex]} + testSubj="manageButton" + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + ); + } + + render() { + const { followerIndexId, closeDetailPanel } = this.props; + + return ( + <EuiFlyout + data-test-subj="followerIndexDetail" + onClose={closeDetailPanel} + aria-labelledby="followerIndexDetailsFlyoutTitle" + size="m" + maxWidth={600} + > + <EuiFlyoutHeader> + <EuiTitle size="m" id="followerIndexDetailsFlyoutTitle" data-test-subj="title"> + <h2>{followerIndexId}</h2> + </EuiTitle> + </EuiFlyoutHeader> + + {this.renderContent()} + {this.renderFooter()} + </EuiFlyout> + ); + } +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 60feac5ec8c6e..67f42fb622bf8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -22,8 +22,8 @@ import { FollowerIndexResumeProvider, FollowerIndexUnfollowProvider, } from '../../../../../components'; -import routing from '../../../../../services/routing'; -import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; +import { routing } from '../../../../../services/routing'; +import { trackUiMetric } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; export class FollowerIndicesTable extends PureComponent { @@ -171,7 +171,7 @@ export class FollowerIndicesTable extends PureComponent { return ( <EuiLink onClick={() => { - trackUiMetric(METRIC_TYPE.CLICK, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); selectFollowerIndex(name); }} data-test-subj="followerIndexLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index b7e04721f4748..7b843d08cefd3 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import routing from '../../../services/routing'; +import { routing } from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js similarity index 96% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js index 88db909612245..bcd9dad114862 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js @@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; -import { BASE_PATH } from '../../../../../common/constants'; +import { BASE_PATH } from '../../../../common/constants'; import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { AutoFollowPatternList } from './auto_follow_pattern_list'; import { FollowerIndicesList } from './follower_indices_list'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts new file mode 100644 index 0000000000000..b7c1f495604be --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts @@ -0,0 +1,11 @@ +/* + * 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 declare const CrossClusterReplicationHome: any; +export declare const AutoFollowPatternAdd: any; +export declare const AutoFollowPatternEdit: any; +export declare const FollowerIndexAdd: any; +export declare const FollowerIndexEdit: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap b/x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap rename to x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/api.js index 24bc7e17356e2..adff40ef29be6 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js @@ -7,8 +7,8 @@ import { API_BASE_PATH, API_REMOTE_CLUSTERS_BASE_PATH, API_INDEX_MANAGEMENT_BASE_PATH, -} from '../../../../common/constants'; -import { arrify } from '../../../../common/services/utils'; +} from '../../../common/constants'; +import { arrify } from '../../../common/services/utils'; import { UIM_FOLLOWER_INDEX_CREATE, UIM_FOLLOWER_INDEX_UPDATE, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js index 95aa3f0ebc3e4..70311d5ba1e4d 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js @@ -9,11 +9,12 @@ export const parseAutoFollowError = error => { return null; } - const { leaderIndex, autoFollowException } = error; + const { leaderIndex, autoFollowException, timestamp } = error; const id = leaderIndex.substring(0, leaderIndex.lastIndexOf(':')); return { id, + timestamp, leaderIndex, autoFollowException, }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js index 1b5a39658ee46..cf394d4b3c7d8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js @@ -8,8 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../src/plugins/data/public'; const { indexNameBeginsWithPeriod, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts new file mode 100644 index 0000000000000..84ac9356462ad --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts @@ -0,0 +1,41 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ChromeBreadcrumb } from 'src/core/public'; + +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; + +import { BASE_PATH } from '../../../common/constants'; + +export type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + +let setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + +export const init = (_setBreadcrumbs: SetBreadcrumbs): void => { + setBreadcrumbs = _setBreadcrumbs; +}; + +export const listBreadcrumb = { + text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', { + defaultMessage: 'Cross-Cluster Replication', + }), + href: `#${BASE_PATH}`, +}; + +export const addBreadcrumb = { + text: i18n.translate('xpack.crossClusterReplication.addBreadcrumbTitle', { + defaultMessage: 'Add', + }), +}; + +export const editBreadcrumb = { + text: i18n.translate('xpack.crossClusterReplication.editBreadcrumbTitle', { + defaultMessage: 'Edit', + }), +}; + +export { setBreadcrumbs }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts new file mode 100644 index 0000000000000..c8b00f6e246b5 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +let _esBase: string; + +export const init = (esBase: string) => { + _esBase = esBase; +}; + +export const getAutoFollowPatternUrl = (): string => `${_esBase}/ccr-put-auto-follow-pattern.html`; +export const getFollowerIndexUrl = (): string => `${_esBase}/ccr-put-follow.html`; +export const getByteUnitsUrl = (): string => `${_esBase}/common-options.html#byte-units`; +export const getTimeUnitsUrl = (): string => `${_esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js similarity index 89% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js index d20fa76ef5451..118a54887d404 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../common/constants'; +import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../common/constants'; export const getSettingDefault = name => { if (!FOLLOWER_INDEX_ADVANCED_SETTINGS[name]) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js b/x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js index 64c3e8412437e..7e2b45b625c1f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; const isEmpty = value => { return !value || !value.trim().length; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts new file mode 100644 index 0000000000000..66fc9de00995c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IToasts, FatalErrorsSetup } from 'src/core/public'; + +let _toasts: IToasts; +let _fatalErrors: FatalErrorsSetup; + +export const init = (toasts: IToasts, fatalErrors: FatalErrorsSetup) => { + _toasts = toasts; + _fatalErrors = fatalErrors; +}; + +export const getToasts = () => _toasts; +export const getFatalErrors = () => _fatalErrors; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js b/x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts new file mode 100644 index 0000000000000..9e96ea12856f6 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts @@ -0,0 +1,7 @@ +/* + * 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 declare const routing: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js similarity index 93% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/routing.js index 965aeaaad22ad..124c61e1ba19e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js @@ -10,7 +10,7 @@ import { createLocation } from 'history'; import { stringify } from 'query-string'; -import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants'; +import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; const isModifiedEvent = event => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); @@ -32,7 +32,6 @@ const appToBasePathMap = { }; class Routing { - _userHasLeftApp = false; _reactRouter = null; /** @@ -97,14 +96,6 @@ class Routing { set reactRouter(router) { this._reactRouter = router; } - - get userHasLeftApp() { - return this._userHasLeftApp; - } - - set userHasLeftApp(hasLeft) { - this._userHasLeftApp = hasLeft; - } } -export default new Routing(); +export const routing = new Routing(); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts new file mode 100644 index 0000000000000..aecc4eb83893f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics'; + +import { UIM_APP_NAME } from '../constants'; + +export { METRIC_TYPE }; + +// usageCollection is an optional dependency, so we default to a no-op. +export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; + +export function init(usageCollection: UsageCollectionSetup): void { + trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME); +} + +/** + * Transparently return provided request Promise, while allowing us to track + * a successful completion of the request. + */ +export function trackUserRequest(request: Promise<any>, actionType: string) { + // Only track successful actions. + return request.then(response => { + // It looks like we're using the wrong type here, added via + // https://github.com/elastic/kibana/pull/41113/files#diff-e65a0a6696a9d723969afd871cbd60cdR19 + // but we'll keep it for now to avoid discontinuity in our telemetry data. + trackUiMetric(METRIC_TYPE.LOADED, actionType); + + // We return the response immediately without waiting for the tracking request to resolve, + // to avoid adding additional latency. + return response; + }); +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js b/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js new file mode 100644 index 0000000000000..52a22cb17d0a9 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js @@ -0,0 +1,257 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { getToasts } from '../../services/notifications'; +import { SECTIONS, API_STATUS } from '../../constants'; +import { + loadAutoFollowPatterns as loadAutoFollowPatternsRequest, + getAutoFollowPattern as getAutoFollowPatternRequest, + createAutoFollowPattern as createAutoFollowPatternRequest, + updateAutoFollowPattern as updateAutoFollowPatternRequest, + deleteAutoFollowPattern as deleteAutoFollowPatternRequest, + pauseAutoFollowPattern as pauseAutoFollowPatternRequest, + resumeAutoFollowPattern as resumeAutoFollowPatternRequest, +} from '../../services/api'; +import { routing } from '../../services/routing'; +import * as t from '../action_types'; +import { sendApiRequest } from './api'; +import { getSelectedAutoFollowPatternId } from '../selectors'; + +const { AUTO_FOLLOW_PATTERN: scope } = SECTIONS; + +export const selectDetailAutoFollowPattern = id => ({ + type: t.AUTO_FOLLOW_PATTERN_SELECT_DETAIL, + payload: id, +}); + +export const selectEditAutoFollowPattern = id => ({ + type: t.AUTO_FOLLOW_PATTERN_SELECT_EDIT, + payload: id, +}); + +export const loadAutoFollowPatterns = (isUpdating = false) => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_LOAD, + scope, + status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, + handler: async () => await loadAutoFollowPatternsRequest(), + }); + +export const getAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_GET, + scope: `${scope}-get`, + handler: async () => await getAutoFollowPatternRequest(id), + }); + +export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) => + sendApiRequest({ + label: isUpdating ? t.AUTO_FOLLOW_PATTERN_UPDATE : t.AUTO_FOLLOW_PATTERN_CREATE, + status: API_STATUS.SAVING, + scope: `${scope}-save`, + handler: async () => { + if (isUpdating) { + return await updateAutoFollowPatternRequest(id, autoFollowPattern); + } + return await createAutoFollowPatternRequest({ id, ...autoFollowPattern }); + }, + onSuccess() { + const successMessage = isUpdating + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.updateAction.successNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' updated successfully`, + values: { name: id }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.addAction.successNotificationTitle', + { + defaultMessage: `Added auto-follow pattern '{name}'`, + values: { name: id }, + } + ); + + getToasts().addSuccess(successMessage); + routing.navigate(`/auto_follow_patterns`, undefined, { + pattern: encodeURIComponent(id), + }); + }, + }); + +export const deleteAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_DELETE, + scope: `${scope}-delete`, + status: API_STATUS.DELETING, + handler: async () => deleteAutoFollowPatternRequest(id), + onSuccess(response, dispatch, getState) { + /** + * We can have 1 or more auto-follow pattern delete operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error removing {count} auto-follow patterns`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.errorSingleNotificationTitle', + { + defaultMessage: `Error removing the '{name}' auto-follow pattern`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsDeleted.length) { + const hasMultipleDelete = response.itemsDeleted.length > 1; + + const successMessage = hasMultipleDelete + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} auto-follow patterns were removed`, + values: { count: response.itemsDeleted.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.removeAction.successSingleNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' was removed`, + values: { name: response.itemsDeleted[0] }, + } + ); + + getToasts().addSuccess(successMessage); + + // If we've just deleted a pattern we were looking at, we need to close the panel. + const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState()); + if (response.itemsDeleted.includes(autoFollowPatternId)) { + dispatch(selectDetailAutoFollowPattern(null)); + } + } + }, + }); + +export const pauseAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_PAUSE, + scope: `${scope}-pause`, + status: API_STATUS.UPDATING, + handler: () => pauseAutoFollowPatternRequest(id), + onSuccess: response => { + /** + * We can have 1 or more auto-follow pattern pause operations + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error pausing {count} auto-follow patterns`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.errorSingleNotificationTitle', + { + defaultMessage: `Error pausing the '{name}' auto-follow pattern`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsPaused.length) { + const hasMultiple = response.itemsPaused.length > 1; + + const successMessage = hasMultiple + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} auto-follow patterns were paused`, + values: { count: response.itemsPaused.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.pauseAction.successSingleNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' was paused`, + values: { name: response.itemsPaused[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + }, + }); + +export const resumeAutoFollowPattern = id => + sendApiRequest({ + label: t.AUTO_FOLLOW_PATTERN_RESUME, + scope: `${scope}-resume`, + status: API_STATUS.UPDATING, + handler: () => resumeAutoFollowPatternRequest(id), + onSuccess: response => { + /** + * We can have 1 or more auto-follow pattern resume operations + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error resuming {count} auto-follow patterns`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.errorSingleNotificationTitle', + { + defaultMessage: `Error resuming the '{name}' auto-follow pattern`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsResumed.length) { + const hasMultiple = response.itemsResumed.length > 1; + + const successMessage = hasMultiple + ? i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} auto-follow patterns were resumed`, + values: { count: response.itemsResumed.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.autoFollowPattern.resumeAction.successSingleNotificationTitle', + { + defaultMessage: `Auto-follow pattern '{name}' was resumed`, + values: { name: response.itemsResumed[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + }, + }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js new file mode 100644 index 0000000000000..d081e0444eb58 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js @@ -0,0 +1,287 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { routing } from '../../services/routing'; +import { getToasts } from '../../services/notifications'; +import { SECTIONS, API_STATUS } from '../../constants'; +import { + loadFollowerIndices as loadFollowerIndicesRequest, + getFollowerIndex as getFollowerIndexRequest, + createFollowerIndex as createFollowerIndexRequest, + pauseFollowerIndex as pauseFollowerIndexRequest, + resumeFollowerIndex as resumeFollowerIndexRequest, + unfollowLeaderIndex as unfollowLeaderIndexRequest, + updateFollowerIndex as updateFollowerIndexRequest, +} from '../../services/api'; +import * as t from '../action_types'; +import { sendApiRequest } from './api'; +import { getSelectedFollowerIndexId } from '../selectors'; + +const { FOLLOWER_INDEX: scope } = SECTIONS; + +export const selectDetailFollowerIndex = id => ({ + type: t.FOLLOWER_INDEX_SELECT_DETAIL, + payload: id, +}); + +export const selectEditFollowerIndex = id => ({ + type: t.FOLLOWER_INDEX_SELECT_EDIT, + payload: id, +}); + +export const loadFollowerIndices = (isUpdating = false) => + sendApiRequest({ + label: t.FOLLOWER_INDEX_LOAD, + scope, + status: isUpdating ? API_STATUS.UPDATING : API_STATUS.LOADING, + handler: async () => await loadFollowerIndicesRequest(), + }); + +export const getFollowerIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_GET, + scope: `${scope}-get`, + handler: async () => await getFollowerIndexRequest(id), + }); + +export const saveFollowerIndex = (name, followerIndex, isUpdating = false) => + sendApiRequest({ + label: t.FOLLOWER_INDEX_CREATE, + status: API_STATUS.SAVING, + scope: `${scope}-save`, + handler: async () => { + if (isUpdating) { + return await updateFollowerIndexRequest(name, followerIndex); + } + return await createFollowerIndexRequest({ name, ...followerIndex }); + }, + onSuccess() { + const successMessage = isUpdating + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.updateAction.successNotificationTitle', + { + defaultMessage: `Follower index '{name}' updated successfully`, + values: { name }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.addAction.successNotificationTitle', + { + defaultMessage: `Added follower index '{name}'`, + values: { name }, + } + ); + + getToasts().addSuccess(successMessage); + routing.navigate(`/follower_indices`, undefined, { + name: encodeURIComponent(name), + }); + }, + }); + +export const pauseFollowerIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_PAUSE, + status: API_STATUS.SAVING, + scope, + handler: async () => pauseFollowerIndexRequest(id), + onSuccess(response, dispatch) { + /** + * We can have 1 or more follower index pause operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error pausing {count} follower indices`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.errorSingleNotificationTitle', + { + defaultMessage: `Error pausing follower index '{name}'`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsPaused.length) { + const hasMultiplePaused = response.itemsPaused.length > 1; + + const successMessage = hasMultiplePaused + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} follower indices were paused`, + values: { count: response.itemsPaused.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.pauseAction.successSingleNotificationTitle', + { + defaultMessage: `Follower index '{name}' was paused`, + values: { name: response.itemsPaused[0] }, + } + ); + + getToasts().addSuccess(successMessage); + + // Refresh list + dispatch(loadFollowerIndices(true)); + } + }, + }); + +export const resumeFollowerIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_RESUME, + status: API_STATUS.SAVING, + scope, + handler: async () => resumeFollowerIndexRequest(id), + onSuccess(response, dispatch) { + console.log('response', response); + /** + * We can have 1 or more follower index resume operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error resuming {count} follower indices`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.errorSingleNotificationTitle', + { + defaultMessage: `Error resuming follower index '{name}'`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsResumed.length) { + const hasMultipleResumed = response.itemsResumed.length > 1; + + const successMessage = hasMultipleResumed + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.successMultipleNotificationTitle', + { + defaultMessage: `{count} follower indices were resumed`, + values: { count: response.itemsResumed.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.resumeAction.successSingleNotificationTitle', + { + defaultMessage: `Follower index '{name}' was resumed`, + values: { name: response.itemsResumed[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + + // Refresh list + dispatch(loadFollowerIndices(true)); + }, + }); + +export const unfollowLeaderIndex = id => + sendApiRequest({ + label: t.FOLLOWER_INDEX_UNFOLLOW, + status: API_STATUS.DELETING, + scope: `${scope}-delete`, + handler: async () => unfollowLeaderIndexRequest(id), + onSuccess(response, dispatch, getState) { + /** + * We can have 1 or more follower index unfollow operation + * that can fail or succeed. We will show 1 toast notification for each. + */ + if (response.errors.length) { + const hasMultipleErrors = response.errors.length > 1; + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorMultipleNotificationTitle', + { + defaultMessage: `Error unfollowing leader index of {count} follower indices`, + values: { count: response.errors.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.errorSingleNotificationTitle', + { + defaultMessage: `Error unfollowing leader index of follower index '{name}'`, + values: { name: response.errors[0].id }, + } + ); + + getToasts().addDanger(errorMessage); + } + + if (response.itemsUnfollowed.length) { + const hasMultipleUnfollow = response.itemsUnfollowed.length > 1; + + const successMessage = hasMultipleUnfollow + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.successMultipleNotificationTitle', + { + defaultMessage: `Leader indices of {count} follower indices were unfollowed`, + values: { count: response.itemsUnfollowed.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.successSingleNotificationTitle', + { + defaultMessage: `Leader index of follower index '{name}' was unfollowed`, + values: { name: response.itemsUnfollowed[0] }, + } + ); + + getToasts().addSuccess(successMessage); + } + + if (response.itemsNotOpen.length) { + const hasMultipleNotOpen = response.itemsNotOpen.length > 1; + + const warningMessage = hasMultipleNotOpen + ? i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningMultipleNotificationTitle', + { + defaultMessage: `{count} indices could not be re-opened`, + values: { count: response.itemsNotOpen.length }, + } + ) + : i18n.translate( + 'xpack.crossClusterReplication.followerIndex.unfollowAction.notOpenWarningSingleNotificationTitle', + { + defaultMessage: `Index '{name}' could not be re-opened`, + values: { name: response.itemsNotOpen[0] }, + } + ); + + getToasts().addWarning(warningMessage); + } + + // If we've just unfollowed a follower index we were looking at, we need to close the panel. + const followerIndexId = getSelectedFollowerIndexId('detail')(getState()); + if (response.itemsUnfollowed.includes(followerIndexId)) { + dispatch(selectDetailFollowerIndex(null)); + } + }, + }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js diff --git a/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts new file mode 100644 index 0000000000000..6d35dfeddfd46 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts @@ -0,0 +1,7 @@ +/* + * 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 declare const ccrStore: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js b/x-pack/plugins/cross_cluster_replication/public/app/store/store.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/store.js diff --git a/x-pack/plugins/cross_cluster_replication/public/index.ts b/x-pack/plugins/cross_cluster_replication/public/index.ts new file mode 100644 index 0000000000000..e3e2d860e526d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'src/core/public'; + +import { CrossClusterReplicationPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new CrossClusterReplicationPlugin(initializerContext); diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts new file mode 100644 index 0000000000000..bdaa04e9d53ee --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -0,0 +1,102 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; +import { first } from 'rxjs/operators'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; + +import { PLUGIN, MANAGEMENT_ID } from '../common/constants'; +import { init as initUiMetric } from './app/services/track_ui_metric'; +import { init as initNotification } from './app/services/notifications'; +import { PluginDependencies, ClientConfigType } from './types'; + +// @ts-ignore; +import { setHttpClient } from './app/services/api'; + +export class CrossClusterReplicationPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(coreSetup: CoreSetup, plugins: PluginDependencies) { + const { licensing, remoteClusters, usageCollection, management, indexManagement } = plugins; + const esSection = management.sections.getSection('elasticsearch'); + + const { + http, + notifications: { toasts }, + fatalErrors, + getStartServices, + } = coreSetup; + + // Initialize services even if the app isn't mounted, because they're used by index management extensions. + setHttpClient(http); + initUiMetric(usageCollection); + initNotification(toasts, fatalErrors); + + const ccrApp = esSection!.registerApp({ + id: MANAGEMENT_ID, + title: PLUGIN.TITLE, + order: 4, + mount: async ({ element, setBreadcrumbs }) => { + const { mountApp } = await import('./app'); + + const [coreStart] = await getStartServices(); + const { + i18n: { Context: I18nContext }, + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + } = coreStart; + + return mountApp({ + element, + setBreadcrumbs, + I18nContext, + ELASTIC_WEBSITE_URL, + DOC_LINK_VERSION, + }); + }, + }); + + ccrApp.disable(); + + licensing.license$ + .pipe(first()) + .toPromise() + .then(license => { + const licenseStatus = license.check(PLUGIN.ID, PLUGIN.minimumLicenseType); + const isLicenseOk = licenseStatus.state === 'valid'; + const config = this.initializerContext.config.get<ClientConfigType>(); + + // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting. + // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if + // the Remote Clusters UI is disabled we can't show the CCR UI. + const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; + + if (isLicenseOk && isCcrUiEnabled) { + ccrApp.enable(); + + if (indexManagement) { + const propertyPath = 'isFollowerIndex'; + + const followerBadgeExtension = { + matchIndex: (index: any) => { + return get(index, propertyPath); + }, + label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', { + defaultMessage: 'Follower', + }), + color: 'default', + filterExpression: 'isFollowerIndex:true', + }; + + indexManagement.extensionsService.addBadge(followerBadgeExtension); + } + } + }); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/cross_cluster_replication/public/types.ts b/x-pack/plugins/cross_cluster_replication/public/types.ts new file mode 100644 index 0000000000000..aac174b7524d3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/types.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { IndexManagementPluginSetup } from '../../index_management/public'; +import { RemoteClustersPluginSetup } from '../../remote_clusters/public'; +import { LicensingPluginSetup } from '../../licensing/public'; + +export interface PluginDependencies { + usageCollection: UsageCollectionSetup; + management: ManagementSetup; + indexManagement: IndexManagementPluginSetup; + remoteClusters: RemoteClustersPluginSetup; + licensing: LicensingPluginSetup; +} + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts new file mode 100644 index 0000000000000..d4de54391286b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts @@ -0,0 +1,198 @@ +/* + * 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 const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { + const ca = components.clientAction.factory; + + Client.prototype.ccr = components.clientAction.namespaceFactory(); + const ccr = Client.prototype.ccr.prototype; + + ccr.permissions = ca({ + urls: [ + { + fmt: '/_security/user/_has_privileges', + }, + ], + needBody: true, + method: 'POST', + }); + + ccr.autoFollowPatterns = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow', + }, + ], + method: 'GET', + }); + + ccr.autoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ccr.saveAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>', + req: { + id: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'PUT', + }); + + ccr.deleteAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>', + req: { + id: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'DELETE', + }); + + ccr.pauseAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>/pause', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); + + ccr.resumeAutoFollowPattern = ca({ + urls: [ + { + fmt: '/_ccr/auto_follow/<%=id%>/resume', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); + + ccr.info = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/info', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ccr.stats = ca({ + urls: [ + { + fmt: '/_ccr/stats', + }, + ], + method: 'GET', + }); + + ccr.followerIndexStats = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/stats', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'GET', + }); + + ccr.saveFollowerIndex = ca({ + urls: [ + { + fmt: '/<%=name%>/_ccr/follow', + req: { + name: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'PUT', + }); + + ccr.pauseFollowerIndex = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/pause_follow', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); + + ccr.resumeFollowerIndex = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/resume_follow', + req: { + id: { + type: 'string', + }, + }, + }, + ], + needBody: true, + method: 'POST', + }); + + ccr.unfollowLeaderIndex = ca({ + urls: [ + { + fmt: '/<%=id%>/_ccr/unfollow', + req: { + id: { + type: 'string', + }, + }, + }, + ], + method: 'POST', + }); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/config.ts b/x-pack/plugins/cross_cluster_replication/server/config.ts new file mode 100644 index 0000000000000..17999d37c76b7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/config.ts @@ -0,0 +1,16 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), +}); + +export type CrossClusterReplicationConfig = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/cross_cluster_replication/server/index.ts b/x-pack/plugins/cross_cluster_replication/server/index.ts new file mode 100644 index 0000000000000..597c039ad202e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { CrossClusterReplicationServerPlugin } from './plugin'; +import { configSchema, CrossClusterReplicationConfig } from './config'; + +export const plugin = (pluginInitializerContext: PluginInitializerContext) => + new CrossClusterReplicationServerPlugin(pluginInitializerContext); + +export const config: PluginConfigDescriptor<CrossClusterReplicationConfig> = { + schema: configSchema, + exposeToBrowser: { + ui: true, + }, +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap new file mode 100644 index 0000000000000..3eced37112a35 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[CCR] auto-follow stats serialization should deserialize auto-follow stats 1`] = ` +Object { + "autoFollowedClusters": Array [ + Object { + "clusterName": "new-york", + "lastSeenMetadataVersion": 15, + "timeSinceLastCheckMillis": 2426, + }, + ], + "numberOfFailedFollowIndices": 0, + "numberOfFailedRemoteClusterStateRequests": 0, + "numberOfSuccessfulFollowIndices": 0, + "recentAutoFollowErrors": Array [ + Object { + "autoFollowException": Object { + "reason": "index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]", + "type": "exception", + }, + "leaderIndex": "pattern-1:kibana_sample_1", + "timestamp": 1587081600021, + }, + Object { + "autoFollowException": Object { + "reason": "index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]", + "type": "exception", + }, + "leaderIndex": "pattern-2:kibana_sample_1", + "timestamp": 1587081600021, + }, + ], +} +`; diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts new file mode 100644 index 0000000000000..5141aa56c1d7e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.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 { deserializeAutoFollowStats } from './ccr_stats_serialization'; + +describe('[CCR] auto-follow stats serialization', () => { + it('should deserialize auto-follow stats', () => { + const esObject = { + number_of_failed_follow_indices: 0, + number_of_failed_remote_cluster_state_requests: 0, + number_of_successful_follow_indices: 0, + recent_auto_follow_errors: [ + { + leader_index: 'pattern-1:kibana_sample_1', + timestamp: 1587081600021, + auto_follow_exception: { + type: 'exception', + reason: + 'index to follow [kibana_sample_1] for pattern [pattern-1] matches with other patterns [pattern-2]', + }, + }, + { + leader_index: 'pattern-2:kibana_sample_1', + timestamp: 1587081600021, + auto_follow_exception: { + type: 'exception', + reason: + 'index to follow [kibana_sample_1] for pattern [pattern-2] matches with other patterns [pattern-1]', + }, + }, + ], + auto_followed_clusters: [ + { + cluster_name: 'new-york', + time_since_last_check_millis: 2426, + last_seen_metadata_version: 15, + }, + ], + }; + + expect(deserializeAutoFollowStats(esObject)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts new file mode 100644 index 0000000000000..7e2b088919842 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + RecentAutoFollowError, + RecentAutoFollowErrorFromEs, + AutoFollowedCluster, + AutoFollowedClusterFromEs, + AutoFollowStats, + AutoFollowStatsFromEs, +} from '../../common/types'; + +export const deserializeRecentAutoFollowErrors = ({ + timestamp, + leader_index, + auto_follow_exception: { type, reason }, +}: RecentAutoFollowErrorFromEs): RecentAutoFollowError => ({ + timestamp, + leaderIndex: leader_index, + autoFollowException: { + type, + reason, + }, +}); + +export const deserializeAutoFollowedClusters = ({ + cluster_name, + time_since_last_check_millis, + last_seen_metadata_version, +}: AutoFollowedClusterFromEs): AutoFollowedCluster => ({ + clusterName: cluster_name, + timeSinceLastCheckMillis: time_since_last_check_millis, + lastSeenMetadataVersion: last_seen_metadata_version, +}); + +export const deserializeAutoFollowStats = ({ + number_of_failed_follow_indices, + number_of_failed_remote_cluster_state_requests, + number_of_successful_follow_indices, + recent_auto_follow_errors, + auto_followed_clusters, +}: AutoFollowStatsFromEs): AutoFollowStats => ({ + numberOfFailedFollowIndices: number_of_failed_follow_indices, + numberOfFailedRemoteClusterStateRequests: number_of_failed_remote_cluster_state_requests, + numberOfSuccessfulFollowIndices: number_of_successful_follow_indices, + recentAutoFollowErrors: recent_auto_follow_errors.map(deserializeRecentAutoFollowErrors), + autoFollowedClusters: auto_followed_clusters.map(deserializeAutoFollowedClusters), +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts new file mode 100644 index 0000000000000..9dde027cd6949 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +function extractCausedByChain( + causedBy: Record<string, any> = {}, + accumulator: string[] = [] +): string[] { + const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase + + if (reason) { + accumulator.push(reason); + } + + // eslint-disable-next-line @typescript-eslint/camelcase + if (caused_by) { + return extractCausedByChain(caused_by, accumulator); + } + + return accumulator; +} + +/** + * Wraps an error thrown by the ES JS client into a Boom error response and returns it + * + * @param err Object Error thrown by ES JS client + * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages + */ +export function wrapEsError( + err: any, + statusCodeToMessageMap: Record<string, string> = {} +): { message: string; body?: { cause?: string[] }; statusCode: number } { + const { statusCode, response } = err; + + const { + error: { + root_cause = [], // eslint-disable-line @typescript-eslint/camelcase + caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase + } = {}, + } = JSON.parse(response); + + // If no custom message if specified for the error's status code, just + // wrap the error as a Boom error response and return it + if (!statusCodeToMessageMap[statusCode]) { + // The caused_by chain has the most information so use that if it's available. If not then + // settle for the root_cause. + const causedByChain = extractCausedByChain(caused_by); + const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; + + return { + message: err.message, + statusCode, + body: { + cause: causedByChain.length ? causedByChain : defaultCause, + }, + }; + } + + // Otherwise, use the custom message to create a Boom error response and + // return it + const message = statusCodeToMessageMap[statusCode]; + return { message, statusCode }; +} + +export function formatEsError(err: any): any { + const { statusCode, message, body } = wrapEsError(err); + return { + statusCode, + body: { + message, + attributes: { + cause: body?.cause, + }, + }, + }; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts rename to x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts new file mode 100644 index 0000000000000..25c99803480f3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/plugin.ts @@ -0,0 +1,139 @@ +/* + * 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. + */ + +declare module 'src/core/server' { + interface RequestHandlerContext { + crossClusterReplication?: CrossClusterReplicationContext; + } +} + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import { + CoreSetup, + Plugin, + Logger, + PluginInitializerContext, + APICaller, + IScopedClusterClient, +} from 'src/core/server'; + +import { Index } from '../../index_management/server'; +import { PLUGIN } from '../common/constants'; +import { Dependencies } from './types'; +import { registerApiRoutes } from './routes'; +import { License } from './services'; +import { elasticsearchJsPlugin } from './client/elasticsearch_ccr'; +import { CrossClusterReplicationConfig } from './config'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; + +interface CrossClusterReplicationContext { + client: IScopedClusterClient; +} + +const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => { + if (!indicesList?.length) { + return indicesList; + } + const params = { + path: '/_all/_ccr/info', + method: 'GET', + }; + try { + const { follower_indices: followerIndices } = await callWithRequest( + 'transport.request', + params + ); + return indicesList.map(index => { + const isFollowerIndex = !!followerIndices.find( + (followerIndex: { follower_index: string }) => { + return followerIndex.follower_index === index.name; + } + ); + return { + ...index, + isFollowerIndex, + }; + }); + } catch (e) { + return indicesList; + } +}; + +export class CrossClusterReplicationServerPlugin implements Plugin<void, void, any, any> { + private readonly config$: Observable<CrossClusterReplicationConfig>; + private readonly license: License; + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.config$ = initializerContext.config.create(); + this.license = new License(); + } + + setup( + { http, elasticsearch }: CoreSetup, + { licensing, indexManagement, remoteClusters }: Dependencies + ) { + this.config$ + .pipe(first()) + .toPromise() + .then(config => { + // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting. + // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if + // the Remote Clusters UI is disabled we can't show the CCR UI. + const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; + + // If the UI isn't enabled, then we don't want to expose any CCR concepts in the UI, including + // "follower" badges for follower indices. + if (isCcrUiEnabled) { + if (indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(ccrDataEnricher); + } + } + }); + + this.license.setup( + { + pluginId: PLUGIN.ID, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate( + 'xpack.crossClusterReplication.licenseCheckErrorMessage', + { + defaultMessage: 'License check failed', + } + ), + }, + { + licensing, + logger: this.logger, + } + ); + + // Extend the elasticsearchJs client with additional endpoints. + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + const ccrEsClient = elasticsearch.createClient('crossClusterReplication', esClientConfig); + http.registerRouteHandlerContext('crossClusterReplication', (ctx, request) => { + return { + client: ccrEsClient.asScoped(request), + }; + }); + + registerApiRoutes({ + router: http.createRouter(), + license: this.license, + lib: { + isEsError, + formatEsError, + }, + }); + } + + start() {} + stop() {} +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts new file mode 100644 index 0000000000000..4cbdc7703a694 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerDeleteRoute } from './register_delete_route'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerGetRoute } from './register_get_route'; +import { registerPauseRoute } from './register_pause_route'; +import { registerResumeRoute } from './register_resume_route'; +import { registerUpdateRoute } from './register_update_route'; + +export function registerAutoFollowPatternRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerDeleteRoute(dependencies); + registerFetchRoute(dependencies); + registerGetRoute(dependencies); + registerPauseRoute(dependencies); + registerResumeRoute(dependencies); + registerUpdateRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts new file mode 100644 index 0000000000000..b41b52e1764c8 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerCreateRoute } from './register_create_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Create auto-follow pattern', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerCreateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('should throw a 409 conflict error if id already exists', async () => { + const routeContextMock = mockRouteContext({ + // Fail the uniqueness check. + callAsCurrentUser: jest.fn().mockResolvedValueOnce(true), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + id: 'some-id', + foo: 'bar', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); + + it('should return 200 status when the id does not exist', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // Pass the uniqueness check. + .mockRejectedValueOnce({ statusCode: 404 }) + .mockResolvedValueOnce(true), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + id: 'some-id', + foo: 'bar', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts new file mode 100644 index 0000000000000..12503e3532a47 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { AutoFollowPattern } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Create an auto-follow pattern + */ +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const bodySchema = schema.object({ + id: schema.string(), + remoteCluster: schema.string(), + leaderIndexPatterns: schema.arrayOf(schema.string()), + followIndexPattern: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id, ...rest } = request.body; + const body = serializeAutoFollowPattern(rest as AutoFollowPattern); + + /** + * First let's make sure that an auto-follow pattern with + * the same id does not exist. + */ + try { + await context.crossClusterReplication!.client.callAsCurrentUser('ccr.autoFollowPattern', { + id, + }); + // If we get here it means that an auto-follow pattern with the same id exists + return response.conflict({ + body: `An auto-follow pattern with the name "${id}" already exists.`, + }); + } catch (err) { + if (err.statusCode !== 404) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + } + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveAutoFollowPattern', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts new file mode 100644 index 0000000000000..e610d09b44275 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerDeleteRoute } from './register_delete_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Delete auto-follow pattern(s)', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerDeleteRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it('deletes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsDeleted).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('deletes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsDeleted).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload.itemsDeleted).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts new file mode 100644 index 0000000000000..ed2633a4a469e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.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 { schema } from '@kbn/config-schema'; + +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Delete an auto-follow pattern + */ +export const registerDeleteRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.delete( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsDeleted: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(_id => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.deleteAutoFollowPattern', { + id: _id, + }) + .then(() => itemsDeleted.push(_id)) + .catch((err: any) => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsDeleted, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts new file mode 100644 index 0000000000000..dd102c45665cb --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerFetchRoute } from './register_fetch_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Fetch all auto-follow patterns', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerFetchRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('deserializes the response from Elasticsearch', async () => { + const ccrAutoFollowPatternResponseMock = { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.patterns).toEqual([ + { + active: true, + followIndexPattern: 'follow', + leaderIndexPatterns: ['leader*'], + name: 'autoFollowPattern', + remoteCluster: 'remoteCluster', + }, + ]); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts new file mode 100644 index 0000000000000..70d8ae4d51e3b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { deserializeListAutoFollowPatterns } from '../../../../common/services/auto_follow_pattern_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Get a list of all auto-follow patterns + */ +export const registerFetchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/auto_follow_patterns'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const result = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.autoFollowPatterns' + ); + return response.ok({ + body: { + patterns: deserializeListAutoFollowPatterns(result.patterns), + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts new file mode 100644 index 0000000000000..d5889074651f5 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerGetRoute } from './register_get_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Get one auto-follow pattern', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerGetRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return a single resource even though ES returns an array with 1 item', async () => { + const ccrAutoFollowPatternResponseMock = { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload).toEqual({ + active: true, + followIndexPattern: 'follow', + leaderIndexPatterns: ['leader*'], + name: 'autoFollowPattern', + remoteCluster: 'remoteCluster', + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts new file mode 100644 index 0000000000000..1edbf7e8806c7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts @@ -0,0 +1,54 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { deserializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Get a single auto-follow pattern + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.get( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + try { + const result = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.autoFollowPattern', + { id } + ); + const autoFollowPattern = result.patterns[0]; + + return response.ok({ + body: deserializeAutoFollowPattern(autoFollowPattern), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts new file mode 100644 index 0000000000000..1eaac02918b88 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerPauseRoute } from './register_pause_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Pause auto-follow pattern(s)', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerPauseRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('pauses a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('pauses multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts new file mode 100644 index 0000000000000..325939709e751 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Pause auto-follow pattern(s) + */ +export const registerPauseRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns/{id}/pause'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(_id => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseAutoFollowPattern', { + id: _id, + }) + .then(() => itemsPaused.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts new file mode 100644 index 0000000000000..9839761e701fc --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerResumeRoute } from './register_resume_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Resume auto-follow pattern(s)', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerResumeRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('resumes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('resumes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts new file mode 100644 index 0000000000000..f5e917773704c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Resume auto-follow pattern(s) + */ +export const registerResumeRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns/{id}/resume'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeAutoFollowPattern', { + id: _id, + }) + .then(() => itemsResumed.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts new file mode 100644 index 0000000000000..85f2270ec3aee --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerUpdateRoute } from './register_update_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Update auto-follow pattern', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerUpdateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('should serialize the payload before sending it to Elasticsearch', async () => { + const routeContextMock = mockRouteContext({ + // Just echo back what we send so we can inspect it. + callAsCurrentUser: jest.fn().mockImplementation((endpoint, payload) => payload), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'foo' }, + body: { + remoteCluster: 'bar1', + leaderIndexPatterns: ['bar2'], + followIndexPattern: 'bar3', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + id: 'foo', + body: { + remote_cluster: 'bar1', + leader_index_patterns: ['bar2'], + follow_index_pattern: 'bar3', + }, + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts new file mode 100644 index 0000000000000..836e5f55c5a48 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts @@ -0,0 +1,60 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { AutoFollowPattern } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Update an auto-follow pattern + */ +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + const bodySchema = schema.object({ + active: schema.boolean(), + remoteCluster: schema.string(), + leaderIndexPatterns: schema.arrayOf(schema.string()), + followIndexPattern: schema.string(), + }); + + router.put( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const body = serializeAutoFollowPattern(request.body as AutoFollowPattern); + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveAutoFollowPattern', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts new file mode 100644 index 0000000000000..45c5729535e58 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { RouteDependencies } from '../../../types'; +import { registerPermissionsRoute } from './register_permissions_route'; +import { registerStatsRoute } from './register_stats_route'; + +export function registerCrossClusterReplicationRoutes(dependencies: RouteDependencies) { + registerPermissionsRoute(dependencies); + registerStatsRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts new file mode 100644 index 0000000000000..b8eb5ae14750e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns whether the user has CCR permissions + */ +export const registerPermissionsRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/permissions'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + if (!license.isEsSecurityEnabled) { + // If security has been disabled in elasticsearch.yml. we'll just let the user use CCR + // because permissions are irrelevant. + return response.ok({ + body: { + hasPermission: true, + missingClusterPrivileges: [], + }, + }); + } + + try { + const { + has_all_requested: hasPermission, + cluster, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.permissions', { + body: { + cluster: ['manage', 'manage_ccr'], + }, + }); + + const missingClusterPrivileges = Object.keys(cluster).reduce( + (permissions: any, permissionName: any) => { + if (!cluster[permissionName]) { + permissions.push(permissionName); + return permissions; + } + }, + [] as any[] + ); + + return response.ok({ + body: { + hasPermission, + missingClusterPrivileges, + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts new file mode 100644 index 0000000000000..d4288cf7303e2 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addBasePath } from '../../../services'; +import { deserializeAutoFollowStats } from '../../../lib/ccr_stats_serialization'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns Auto-follow stats + */ +export const registerStatsRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/stats/auto_follow'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { + auto_follow_stats: autoFollowStats, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + + return response.ok({ + body: deserializeAutoFollowStats(autoFollowStats), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts new file mode 100644 index 0000000000000..f5d8c7a4f5bda --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerGetRoute } from './register_get_route'; +import { registerPauseRoute } from './register_pause_route'; +import { registerResumeRoute } from './register_resume_route'; +import { registerUnfollowRoute } from './register_unfollow_route'; +import { registerUpdateRoute } from './register_update_route'; + +export function registerFollowerIndexRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerFetchRoute(dependencies); + registerGetRoute(dependencies); + registerPauseRoute(dependencies); + registerResumeRoute(dependencies); + registerUnfollowRoute(dependencies); + registerUpdateRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts new file mode 100644 index 0000000000000..bba82b04ce9a0 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerCreateRoute } from './register_create_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Create follower index', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerCreateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('should return 200 status when follower index is created', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + name: 'follower_index', + remoteCluster: 'remote_cluster', + leaderIndex: 'leader_index', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts new file mode 100644 index 0000000000000..acaeedacfdb2a --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; +import { FollowerIndex } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { removeEmptyFields } from '../../../../common/services/utils'; +import { RouteDependencies } from '../../../types'; + +/** + * Create a follower index + */ +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const bodySchema = schema.object({ + name: schema.string(), + remoteCluster: schema.string(), + leaderIndex: schema.string(), + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }); + + router.post( + { + path: addBasePath('/follower_indices'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { name, ...rest } = request.body; + const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex)); + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveFollowerIndex', + { name, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts new file mode 100644 index 0000000000000..151ab84fabf4c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerFetchRoute } from './register_fetch_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Fetch all follower indices', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerFetchRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('deserializes the response from Elasticsearch', async () => { + const ccrInfoMockResponse = { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + }, + ], + }; + + // These stats correlate to the above follower indices. + const ccrStatsMockResponse = { + follow_stats: { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }, + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce(ccrInfoMockResponse) + .mockResolvedValueOnce(ccrStatsMockResponse), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload.indices).toEqual([ + { + name: 'followerIndexName', + remoteCluster: 'remoteCluster', + leaderIndex: 'leaderIndex', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [ + { + id: 1, + leaderIndex: 'leaderIndex', + leaderGlobalCheckpoint: 1, + leaderMaxSequenceNum: 1, + followerGlobalCheckpoint: 1, + followerMaxSequenceNum: 1, + lastRequestedSequenceNum: 1, + outstandingReadRequestsCount: 1, + outstandingWriteRequestsCount: 1, + writeBufferOperationsCount: 1, + writeBufferSizeBytes: 1, + followerMappingVersion: 1, + followerSettingsVersion: 1, + totalReadTimeMs: 1, + totalReadRemoteExecTimeMs: 1, + successfulReadRequestCount: 1, + failedReadRequestsCount: 1, + operationsReadCount: 1, + bytesReadCount: 1, + totalWriteTimeMs: 1, + successfulWriteRequestsCount: 1, + failedWriteRequestsCount: 1, + operationsWrittenCount: 1, + readExceptions: 1, + timeSinceLastReadMs: 1, + }, + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts new file mode 100644 index 0000000000000..a78901ce174e4 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts @@ -0,0 +1,62 @@ +/* + * 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 { deserializeListFollowerIndices } from '../../../../common/services/follower_index_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a list of all follower indices + */ +export const registerFetchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/follower_indices'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { + id: '_all', + }); + + const { + follow_stats: { indices: followerIndicesStats }, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + + const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { + map[stats.index] = stats; + return map; + }, {}); + + const collatedFollowerIndices = followerIndices.map((followerIndex: any) => { + return { + ...followerIndex, + ...followerIndicesStatsMap[followerIndex.follower_index], + }; + }); + + return response.ok({ + body: { + indices: deserializeListFollowerIndices(collatedFollowerIndices), + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts new file mode 100644 index 0000000000000..42d04ca65b1cb --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerGetRoute } from './register_get_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Get one follower index', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerGetRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return a single resource even though ES returns an array with 1 item', async () => { + const ccrInfoMockResponse = { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + }, + ], + }; + + // These stats correlate to the above follower indices. + const ccrFollowerIndexStatsMockResponse = { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce(ccrInfoMockResponse) + .mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'doesnt_matter' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + name: 'followerIndexName', + remoteCluster: 'remoteCluster', + leaderIndex: 'leaderIndex', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [ + { + id: 1, + leaderIndex: 'leaderIndex', + leaderGlobalCheckpoint: 1, + leaderMaxSequenceNum: 1, + followerGlobalCheckpoint: 1, + followerMaxSequenceNum: 1, + lastRequestedSequenceNum: 1, + outstandingReadRequestsCount: 1, + outstandingWriteRequestsCount: 1, + writeBufferOperationsCount: 1, + writeBufferSizeBytes: 1, + followerMappingVersion: 1, + followerSettingsVersion: 1, + totalReadTimeMs: 1, + totalReadRemoteExecTimeMs: 1, + successfulReadRequestCount: 1, + failedReadRequestsCount: 1, + operationsReadCount: 1, + bytesReadCount: 1, + totalWriteTimeMs: 1, + successfulWriteRequestsCount: 1, + failedWriteRequestsCount: 1, + operationsWrittenCount: 1, + readExceptions: 1, + timeSinceLastReadMs: 1, + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts new file mode 100644 index 0000000000000..98a182fc15681 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { deserializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a single follower index pattern + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.get( + { + path: addBasePath('/follower_indices/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; + + if (!followerIndexInfo) { + return response.notFound({ + body: `The follower index "${id}" does not exist.`, + }); + } + + // If this follower is paused, skip call to ES stats api since it will return 404 + if (followerIndexInfo.status === 'paused') { + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + }), + }); + } else { + const { + indices: followerIndicesStats, + } = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.followerIndexStats', + { id } + ); + + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + ...(followerIndicesStats ? followerIndicesStats[0] : {}), + }), + }); + } + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts new file mode 100644 index 0000000000000..82cb88cbacea7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerPauseRoute } from './register_pause_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Pause follower index/indices', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerPauseRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('pauses a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('pauses multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts new file mode 100644 index 0000000000000..7432ea7ca5c82 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Pauses a follower index + */ +export const registerPauseRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/pause'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseFollowerIndex', { + id: _id, + }) + .then(() => itemsPaused.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts new file mode 100644 index 0000000000000..04167c5db3162 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerResumeRoute } from './register_resume_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Resume follower index/indices', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerResumeRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('resumes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('resumes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts new file mode 100644 index 0000000000000..ca8f3a9f5fe9d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Resumes a follower index + */ +export const registerResumeRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/resume'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeFollowerIndex', { + id: _id, + }) + .then(() => itemsResumed.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts new file mode 100644 index 0000000000000..6302d5868b0db --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerUnfollowRoute } from './register_unfollow_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Unfollow follower index/indices', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked<IRouter>; + + registerUnfollowRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('unfollows a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('unfollows multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // a + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // b + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // c + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // a + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // b + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts new file mode 100644 index 0000000000000..282fead02bbe0 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts @@ -0,0 +1,95 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Unfollow follower index's leader index + */ +export const registerUnfollowRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/unfollow'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsUnfollowed: string[] = []; + const itemsNotOpen: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(async (_id: string) => { + try { + // Try to pause follower, let it fail silently since it may already be paused + try { + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.pauseFollowerIndex', + { id: _id } + ); + } catch (e) { + // Swallow errors + } + + // Close index + await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', { + index: _id, + }); + + // Unfollow leader + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.unfollowLeaderIndex', + { id: _id } + ); + + // Try to re-open the index, store failures in a separate array to surface warnings in the UI + // This will allow users to query their index normally after unfollowing + try { + await context.crossClusterReplication!.client.callAsCurrentUser('indices.open', { + index: _id, + }); + } catch (e) { + itemsNotOpen.push(_id); + } + + // Push success + itemsUnfollowed.push(_id); + } catch (err) { + errors.push({ id: _id, error: formatError(err) }); + } + }) + ); + + return response.ok({ + body: { + itemsUnfollowed, + itemsNotOpen, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts new file mode 100644 index 0000000000000..521de77180974 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { serializeAdvancedSettings } from '../../../../common/services/follower_index_serialization'; +import { FollowerIndexAdvancedSettings } from '../../../../common/types'; +import { removeEmptyFields } from '../../../../common/services/utils'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Update a follower index + */ +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + const bodySchema = schema.object({ + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }); + + router.put( + { + path: addBasePath('/follower_indices/{id}'), + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + // We need to first pause the follower and then resume it by passing the advanced settings + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; + + if (!followerIndexInfo) { + return response.notFound({ body: `The follower index "${id}" does not exist.` }); + } + + // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. + const isPaused = followerIndexInfo.status === 'paused'; + + // Pause follower if not already paused + if (!isPaused) { + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.pauseFollowerIndex', + { + id, + } + ); + } + + // Resume follower + const body = removeEmptyFields( + serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings) + ); + + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.resumeFollowerIndex', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts new file mode 100644 index 0000000000000..9b4fb134ed230 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts @@ -0,0 +1,23 @@ +/* + * 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 { RequestHandlerContext } from 'src/core/server'; + +export function mockRouteContext({ + callAsCurrentUser, +}: { + callAsCurrentUser: any; +}): RequestHandlerContext { + const routeContextMock = ({ + crossClusterReplication: { + client: { + callAsCurrentUser, + }, + }, + } as unknown) as RequestHandlerContext; + + return routeContextMock; +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts new file mode 100644 index 0000000000000..84abfb369e002 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/index.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. + */ + +import { RouteDependencies } from '../types'; + +import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern'; +import { registerFollowerIndexRoutes } from './api/follower_index'; +import { registerCrossClusterReplicationRoutes } from './api/cross_cluster_replication'; + +export function registerApiRoutes(dependencies: RouteDependencies) { + registerAutoFollowPatternRoutes(dependencies); + registerFollowerIndexRoutes(dependencies); + registerCrossClusterReplicationRoutes(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts new file mode 100644 index 0000000000000..3f3dd131df7c7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts @@ -0,0 +1,9 @@ +/* + * 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 { API_BASE_PATH } from '../../common/constants'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/plugins/cross_cluster_replication/server/services/index.ts b/x-pack/plugins/cross_cluster_replication/server/services/index.ts new file mode 100644 index 0000000000000..d7b544b290c39 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/services/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { License } from './license'; +export { addBasePath } from './add_base_path'; diff --git a/x-pack/plugins/cross_cluster_replication/server/services/license.ts b/x-pack/plugins/cross_cluster_replication/server/services/license.ts new file mode 100644 index 0000000000000..bfd357867c3e2 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/services/license.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + private _isEsSecurityEnabled: boolean = false; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + // Retrieving security checks the results of GET /_xpack as well as license state, + // so we're also checking whether the security is disabled in elasticsearch.yml. + this._isEsSecurityEnabled = license.getFeature('security').isEnabled; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute<P, Q, B>(handler: RequestHandler<P, Q, B>) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest<P, Q, B>, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + get isEsSecurityEnabled() { + return this._isEsSecurityEnabled; + } +} diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts new file mode 100644 index 0000000000000..049d440e3d85d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/types.ts @@ -0,0 +1,28 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { IndexManagementPluginSetup } from '../../index_management/server'; +import { RemoteClustersPluginSetup } from '../../remote_clusters/server'; +import { License } from './services'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; + +export interface Dependencies { + licensing: LicensingPluginSetup; + indexManagement: IndexManagementPluginSetup; + remoteClusters: RemoteClustersPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + formatEsError: typeof formatEsError; + }; +} diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts index 95f2c9e477064..6c635cc5b4489 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts @@ -45,9 +45,64 @@ describe('Async search strategy', () => { it('stops polling when the response is complete', async () => { mockSearch - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })) + .mockReturnValueOnce( + of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }) + ); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch.search(mockRequest, mockOptions).toPromise(); + + expect(mockSearch).toBeCalledTimes(2); + }); + + it('stops polling when the response is an error', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch + .search(mockRequest, mockOptions) + .toPromise() + .catch(() => { + expect(mockSearch).toBeCalledTimes(2); + }); + }); + + // For bug fixed in https://github.com/elastic/kibana/pull/64155 + it('Continues polling if no records are returned on first async request', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 0, loaded: 0, is_running: true, is_partial: true })) + .mockReturnValueOnce( + of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }) + ); const asyncSearch = asyncSearchStrategyProvider({ core: mockCoreStart, @@ -65,12 +120,16 @@ describe('Async search strategy', () => { await asyncSearch.search(mockRequest, mockOptions).toPromise(); expect(mockSearch).toBeCalledTimes(2); + expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest); + expect(mockSearch.mock.calls[1][0]).toEqual({ id: 1, serverStrategy: 'foo' }); }); it('only sends the ID and server strategy after the first request', async () => { mockSearch - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce( + of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }) + ); const asyncSearch = asyncSearchStrategyProvider({ core: mockCoreStart, diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts index 6271d7fcbeaac..18b5b976b3c1b 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts @@ -14,7 +14,7 @@ import { SYNC_SEARCH_STRATEGY, TSearchStrategyProvider, } from '../../../../../src/plugins/data/public'; -import { IAsyncSearchRequest, IAsyncSearchOptions } from './types'; +import { IAsyncSearchRequest, IAsyncSearchOptions, IAsyncSearchResponse } from './types'; export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY'; @@ -52,9 +52,14 @@ export const asyncSearchStrategyProvider: TSearchStrategyProvider<typeof ASYNC_S : NEVER; return search(request, options).pipe( - expand(response => { + expand((response: IAsyncSearchResponse) => { + // If the response indicates of an error, stop polling and complete the observable + if (!response || (response.is_partial && !response.is_running)) { + return throwError(new AbortError()); + } + // If the response indicates it is complete, stop polling and complete the observable - if ((response.loaded ?? 0) >= (response.total ?? 0)) return EMPTY; + if (!response.is_running) return EMPTY; id = response.id; diff --git a/x-pack/plugins/data_enhanced/public/search/types.ts b/x-pack/plugins/data_enhanced/public/search/types.ts index edaaf1b22654d..8ffc8eddda052 100644 --- a/x-pack/plugins/data_enhanced/public/search/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/types.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ISearchOptions, ISyncSearchRequest } from '../../../../../src/plugins/data/public'; +import { + IKibanaSearchResponse, + ISearchOptions, + ISyncSearchRequest, +} from '../../../../../src/plugins/data/public'; export interface IAsyncSearchRequest extends ISyncSearchRequest { /** @@ -19,3 +23,14 @@ export interface IAsyncSearchOptions extends ISearchOptions { */ pollInterval?: number; } + +export interface IAsyncSearchResponse extends IKibanaSearchResponse { + /** + * Indicates whether async search is still in flight + */ + is_running?: boolean; + /** + * Indicates whether the results returned are complete or partial + */ + is_partial?: boolean; +} diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 6b329bccab4a7..bf502889ffa4f 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -23,6 +23,8 @@ import { shimHitsTotal } from './shim_hits_total'; export interface AsyncSearchResponse<T> { id: string; + is_partial: boolean; + is_running: boolean; response: SearchResponse<T>; } @@ -71,13 +73,19 @@ async function asyncSearch( // Wait up to 1s for the response to return const query = toSnakeCase({ waitForCompletionTimeout: '1s', ...queryParams }); - const { response, id } = (await caller( + const { id, response, is_partial, is_running } = (await caller( 'transport.request', { method, path, body, query }, options )) as AsyncSearchResponse<any>; - return { id, rawResponse: shimHitsTotal(response), ...getTotalLoaded(response._shards) }; + return { + id, + is_partial, + is_running, + rawResponse: shimHitsTotal(response), + ...getTotalLoaded(response._shards), + }; } async function rollupSearch( diff --git a/x-pack/plugins/endpoint/common/alert_constants.ts b/x-pack/plugins/endpoint/common/alert_constants.ts new file mode 100644 index 0000000000000..85e1643d684f2 --- /dev/null +++ b/x-pack/plugins/endpoint/common/alert_constants.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class AlertConstants { + /** + * The prefix for all Alert APIs + */ + static BASE_API_URL = '/api/endpoint'; + /** + * The path for the Alert's Index Pattern API. + */ + static INDEX_PATTERN_ROUTE = `${AlertConstants.BASE_API_URL}/index_pattern`; + /** + * Alert's Index pattern + */ + static ALERT_INDEX_NAME = 'events-endpoint-1'; + /** + * A paramter passed to Alert's Index Pattern. + */ + static EVENT_DATASET = 'events'; + /** + * Alert's Search API default page size + */ + static DEFAULT_TOTAL_HITS = 10000; + /** + * Alerts + **/ + static ALERT_LIST_DEFAULT_PAGE_SIZE = 10; + static ALERT_LIST_DEFAULT_SORT = '@timestamp'; + static MAX_LONG_INT = '9223372036854775807'; // 2^63-1 +} diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 1978d780f54f5..e40fc3e386bc8 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -6,7 +6,16 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; -import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields, PolicyData } from './types'; +import { + AlertEvent, + EndpointEvent, + Host, + HostMetadata, + HostOS, + PolicyData, + HostPolicyResponse, + HostPolicyResponseActionStatus, +} from './types'; import { factory as policyFactory } from './models/policy_config'; export type Event = AlertEvent | EndpointEvent; @@ -20,7 +29,7 @@ interface EventOptions { processName?: string; } -const Windows: OSFields[] = [ +const Windows: HostOS[] = [ { name: 'windows 10.0', full: 'Windows 10', @@ -47,11 +56,11 @@ const Windows: OSFields[] = [ }, ]; -const Linux: OSFields[] = []; +const Linux: HostOS[] = []; -const Mac: OSFields[] = []; +const Mac: HostOS[] = []; -const OS: OSFields[] = [...Windows, ...Mac, ...Linux]; +const OS: HostOS[] = [...Windows, ...Mac, ...Linux]; const POLICIES: Array<{ name: string; id: string }> = [ { @@ -93,7 +102,7 @@ interface HostInfo { version: string; id: string; }; - host: HostFields; + host: Host; endpoint: { policy: { id: string; @@ -298,7 +307,7 @@ export class EndpointDocGenerator { process: { entity_id: options.entityID ? options.entityID : this.randomString(10), parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined, - name: options.processName ? options.processName : 'powershell.exe', + name: options.processName ? options.processName : randomProcessName(), }, }; } @@ -486,6 +495,112 @@ export class EndpointDocGenerator { }; } + /** + * Generates a Host Policy response message + */ + generatePolicyResponse(): HostPolicyResponse { + return { + '@timestamp': new Date().toISOString(), + elastic: { + agent: { + id: 'c2a9093e-e289-4c0a-aa44-8c32a414fa7a', + }, + }, + ecs: { + version: '1.0.0', + }, + event: { + created: '2015-01-01T12:10:30Z', + kind: 'policy_response', + }, + agent: { + version: '6.0.0-rc2', + id: '8a4f500d', + }, + endpoint: { + artifacts: { + 'global-manifest': { + version: '1.2.3', + sha256: 'abcdef', + }, + 'endpointpe-v4-windows': { + version: '1.2.3', + sha256: 'abcdef', + }, + 'user-whitelist-windows': { + version: '1.2.3', + sha256: 'abcdef', + }, + 'global-whitelist-windows': { + version: '1.2.3', + sha256: 'abcdef', + }, + }, + policy: { + applied: { + version: '1.0.0', + id: '17d4b81d-9940-4b64-9de5-3e03ef1fb5cf', + status: HostPolicyResponseActionStatus.success, + response: { + configurations: { + malware: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: ['download_model', 'workflow', 'a_custom_future_action'], + }, + events: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: ['ingest_events_config', 'workflow'], + }, + logging: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: ['configure_elasticsearch_connection'], + }, + streaming: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: [ + 'detect_file_open_events', + 'download_global_artifacts', + 'a_custom_future_action', + ], + }, + }, + actions: { + download_model: { + status: HostPolicyResponseActionStatus.success, + message: 'model downloaded', + }, + ingest_events_config: { + status: HostPolicyResponseActionStatus.success, + message: 'no action taken', + }, + workflow: { + status: HostPolicyResponseActionStatus.success, + message: 'the flow worked well', + }, + a_custom_future_action: { + status: HostPolicyResponseActionStatus.success, + message: 'future message', + }, + configure_elasticsearch_connection: { + status: HostPolicyResponseActionStatus.success, + message: 'some message', + }, + detect_file_open_events: { + status: HostPolicyResponseActionStatus.success, + message: 'some message', + }, + download_global_artifacts: { + status: HostPolicyResponseActionStatus.success, + message: 'some message', + }, + }, + }, + }, + }, + }, + }; + } + private randomN(n: number): number { return Math.floor(this.random() * n); } @@ -530,3 +645,16 @@ export class EndpointDocGenerator { return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); } } + +const fakeProcessNames = [ + 'lsass.exe', + 'notepad.exe', + 'mimikatz.exe', + 'powershell.exe', + 'iexlorer.exe', + 'explorer.exe', +]; +/** Return a random fake process name */ +function randomProcessName(): string { + return fakeProcessNames[Math.floor(Math.random() * fakeProcessNames.length)]; +} diff --git a/x-pack/plugins/endpoint/common/models/event.ts b/x-pack/plugins/endpoint/common/models/event.ts index 650486f3c3858..47f39d2d11797 100644 --- a/x-pack/plugins/endpoint/common/models/event.ts +++ b/x-pack/plugins/endpoint/common/models/event.ts @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointEvent, LegacyEndpointEvent } from '../types'; +import { LegacyEndpointEvent, ResolverEvent } from '../types'; -export function isLegacyEvent( - event: EndpointEvent | LegacyEndpointEvent -): event is LegacyEndpointEvent { +export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEvent { return (event as LegacyEndpointEvent).endgame !== undefined; } -export function eventTimestamp( - event: EndpointEvent | LegacyEndpointEvent -): string | undefined | number { +export function eventTimestamp(event: ResolverEvent): string | undefined | number { if (isLegacyEvent(event)) { return event.endgame.timestamp_utc; } else { @@ -22,10 +18,31 @@ export function eventTimestamp( } } -export function eventName(event: EndpointEvent | LegacyEndpointEvent): string { +export function eventName(event: ResolverEvent): string { if (isLegacyEvent(event)) { return event.endgame.process_name ? event.endgame.process_name : ''; } else { return event.process.name; } } + +export function eventId(event: ResolverEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.serial_event_id ? String(event.endgame.serial_event_id) : ''; + } + return event.event.id; +} + +export function entityId(event: ResolverEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.unique_pid ? String(event.endgame.unique_pid) : ''; + } + return event.process.entity_id; +} + +export function parentEntityId(event: ResolverEvent): string | undefined { + if (isLegacyEvent(event)) { + return event.endgame.unique_ppid ? String(event.endgame.unique_ppid) : undefined; + } + return event.process.parent?.entity_id; +} diff --git a/x-pack/plugins/endpoint/common/schema/alert_index.ts b/x-pack/plugins/endpoint/common/schema/alert_index.ts index 7b48780f2d86b..cffc00661515f 100644 --- a/x-pack/plugins/endpoint/common/schema/alert_index.ts +++ b/x-pack/plugins/endpoint/common/schema/alert_index.ts @@ -7,7 +7,7 @@ import { schema, Type } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { decode } from 'rison-node'; -import { EndpointAppConstants } from '../types'; +import { AlertConstants } from '../alert_constants'; /** * Used to validate GET requests against the index of the alerting APIs. @@ -18,7 +18,7 @@ export const alertingIndexGetQuerySchema = schema.object( schema.number({ min: 1, max: 100, - defaultValue: EndpointAppConstants.ALERT_LIST_DEFAULT_PAGE_SIZE, + defaultValue: AlertConstants.ALERT_LIST_DEFAULT_PAGE_SIZE, }) ), page_index: schema.maybe( diff --git a/x-pack/plugins/endpoint/common/schema/resolver.ts b/x-pack/plugins/endpoint/common/schema/resolver.ts new file mode 100644 index 0000000000000..f21307e407fd0 --- /dev/null +++ b/x-pack/plugins/endpoint/common/schema/resolver.ts @@ -0,0 +1,59 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +/** + * Used to validate GET requests for a complete resolver tree. + */ +export const validateTree = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + children: schema.number({ defaultValue: 10, min: 0, max: 100 }), + generations: schema.number({ defaultValue: 3, min: 0, max: 3 }), + ancestors: schema.number({ defaultValue: 3, min: 0, max: 5 }), + events: schema.number({ defaultValue: 100, min: 0, max: 1000 }), + afterEvent: schema.maybe(schema.string()), + afterChild: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for non process events for a specific event. + */ +export const validateEvents = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + events: schema.number({ defaultValue: 100, min: 1, max: 1000 }), + afterEvent: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for the ancestors of a process event. + */ +export const validateAncestry = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + ancestors: schema.number({ defaultValue: 0, min: 0, max: 10 }), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; + +/** + * Used to validate GET requests for children of a specified process event. + */ +export const validateChildren = { + params: schema.object({ id: schema.string() }), + query: schema.object({ + children: schema.number({ defaultValue: 10, min: 10, max: 100 }), + generations: schema.number({ defaultValue: 3, min: 0, max: 3 }), + afterChild: schema.maybe(schema.string()), + legacyEndpointID: schema.maybe(schema.string()), + }), +}; diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 49f8ebbd580d8..8fce15d1c794c 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -25,32 +25,44 @@ export type Immutable<T> = T extends undefined | null | boolean | string | numbe ? ImmutableSet<M> : ImmutableObject<T>; -export type ImmutableArray<T> = ReadonlyArray<Immutable<T>>; -export type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>; -export type ImmutableSet<T> = ReadonlySet<Immutable<T>>; -export type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }; - -export type Direction = 'asc' | 'desc'; - -export class EndpointAppConstants { - static BASE_API_URL = '/api/endpoint'; - static INDEX_PATTERN_ROUTE = `${EndpointAppConstants.BASE_API_URL}/index_pattern`; - static ALERT_INDEX_NAME = 'events-endpoint-1'; - static EVENT_DATASET = 'events'; - static DEFAULT_TOTAL_HITS = 10000; - /** - * Legacy events are stored in indices with endgame-* prefix - */ - static LEGACY_EVENT_INDEX_NAME = 'endgame-*'; +type ImmutableArray<T> = ReadonlyArray<Immutable<T>>; +type ImmutableMap<K, V> = ReadonlyMap<Immutable<K>, Immutable<V>>; +type ImmutableSet<T> = ReadonlySet<Immutable<T>>; +type ImmutableObject<T> = { readonly [K in keyof T]: Immutable<T[K]> }; - /** - * Alerts - **/ - static ALERT_LIST_DEFAULT_PAGE_SIZE = 10; - static ALERT_LIST_DEFAULT_SORT = '@timestamp'; - static MAX_LONG_INT = '9223372036854775807'; // 2^63-1 +/** + * Values for the Alert APIs 'order' and 'direction' parameters. + */ +export type AlertAPIOrdering = 'asc' | 'desc'; + +export interface ResolverNodeStats { + totalEvents: number; + totalAlerts: number; +} + +export interface ResolverNodePagination { + nextChild?: string | null; + nextEvent?: string | null; + nextAncestor?: string | null; + nextAlert?: string | null; +} + +/** + * A node that contains pointers to other nodes, arrrays of resolver events, and any metadata associated with resolver specific data + */ +export interface ResolverNode { + id: string; + children: ResolverNode[]; + events: ResolverEvent[]; + lifecycle: ResolverEvent[]; + ancestors?: ResolverNode[]; + pagination: ResolverNodePagination; + stats?: ResolverNodeStats; } +/** + * Returned by 'api/endpoint/alerts' + */ export interface AlertResultList { /** * The alerts restricted by page size. @@ -88,6 +100,9 @@ export interface AlertResultList { prev: string | null; } +/** + * Returned by the server via /api/endpoint/metadata + */ export interface HostResultList { /* the hosts restricted by the page size */ hosts: HostInfo[]; @@ -99,43 +114,61 @@ export interface HostResultList { request_page_index: number; } -export interface OSFields { +/** + * Operating System metadata for a host. + */ +export interface HostOS { full: string; name: string; version: string; variant: string; } -export interface HostFields { + +/** + * Host metadata. Describes an endpoint host. + */ +export interface Host { id: string; hostname: string; ip: string[]; mac: string[]; - os: OSFields; + os: HostOS; } -export interface HashFields { + +/** + * A record of hashes for something. Provides hashes in multiple formats. A favorite structure of the Elastic Endpoint. + */ +interface Hashes { + /** + * A hash in MD5 format. + */ md5: string; + /** + * A hash in SHA-1 format. + */ sha1: string; + /** + * A hash in SHA-256 format. + */ sha256: string; } -export interface MalwareClassificationFields { + +interface MalwareClassification { identifier: string; score: number; threshold: number; version: string; } -export interface PrivilegesFields { - description: string; - name: string; - enabled: boolean; -} -export interface ThreadFields { + +interface ThreadFields { id: number; service_name: string; start: number; start_address: number; start_address_module: string; } -export interface DllFields { + +interface DllFields { pe: { architecture: string; imphash: string; @@ -145,8 +178,8 @@ export interface DllFields { trusted: boolean; }; compile_time: number; - hash: HashFields; - malware_classification: MalwareClassificationFields; + hash: Hashes; + malware_classification: MalwareClassification; mapped_address: number; mapped_size: number; path: string; @@ -154,7 +187,6 @@ export interface DllFields { /** * Describes an Alert Event. - * Should be in line with ECS schema. */ export type AlertEvent = Immutable<{ '@timestamp': number; @@ -191,14 +223,14 @@ export type AlertEvent = Immutable<{ entity_id: string; }; name: string; - hash: HashFields; + hash: Hashes; pe?: { imphash: string; }; executable: string; sid?: string; start: number; - malware_classification?: MalwareClassificationFields; + malware_classification?: MalwareClassification; token: { domain: string; type: string; @@ -206,7 +238,11 @@ export type AlertEvent = Immutable<{ sid: string; integrity_level: number; integrity_level_name: string; - privileges?: PrivilegesFields[]; + privileges?: Array<{ + description: string; + name: string; + enabled: boolean; + }>; }; thread?: ThreadFields[]; uptime: number; @@ -220,7 +256,7 @@ export type AlertEvent = Immutable<{ mtime: number; created: number; size: number; - hash: HashFields; + hash: Hashes; pe?: { imphash: string; }; @@ -228,10 +264,10 @@ export type AlertEvent = Immutable<{ trusted: boolean; subject_name: string; }; - malware_classification: MalwareClassificationFields; + malware_classification: MalwareClassification; temp_file_path: string; }; - host: HostFields; + host: Host; dll?: DllFields[]; }>; @@ -249,9 +285,6 @@ interface AlertState { }; } -/** - * Union of alert data and metadata. - */ export type AlertData = AlertEvent & AlertMetadata; export type AlertDetails = AlertData & AlertState; @@ -301,7 +334,7 @@ export type HostMetadata = Immutable<{ id: string; version: string; }; - host: HostFields; + host: Host; }>; /** @@ -365,7 +398,7 @@ export interface EndpointEvent { hostname: string; ip: string[]; mac: string[]; - os: OSFields; + os: HostOS; }; process: { entity_id: string; @@ -500,28 +533,22 @@ export interface PolicyConfig { }; } -/** - * Windows-specific policy configuration that is supported via the UI - */ -type WindowsPolicyConfig = Pick<PolicyConfig['windows'], 'events' | 'malware'>; - -/** - * Mac-specific policy configuration that is supported via the UI - */ -type MacPolicyConfig = Pick<PolicyConfig['mac'], 'malware' | 'events'>; - -/** - * Linux-specific policy configuration that is supported via the UI - */ -type LinuxPolicyConfig = Pick<PolicyConfig['linux'], 'events'>; - /** * The set of Policy configuration settings that are show/edited via the UI */ export interface UIPolicyConfig { - windows: WindowsPolicyConfig; - mac: MacPolicyConfig; - linux: LinuxPolicyConfig; + /** + * Windows-specific policy configuration that is supported via the UI + */ + windows: Pick<PolicyConfig['windows'], 'events' | 'malware'>; + /** + * Mac-specific policy configuration that is supported via the UI + */ + mac: Pick<PolicyConfig['mac'], 'malware' | 'events'>; + /** + * Linux-specific policy configuration that is supported via the UI + */ + linux: Pick<PolicyConfig['linux'], 'events'>; } interface PolicyConfigAdvancedOptions { @@ -573,3 +600,103 @@ export type NewPolicyData = NewDatasource & { } ]; }; + +/** + * the possible status for actions, configurations and overall Policy Response + */ +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +/** + * The details of a given action + */ +interface HostPolicyResponseActionDetails { + status: HostPolicyResponseActionStatus; + message: string; +} + +/** + * A known list of possible Endpoint actions + */ +interface HostPolicyResponseActions { + download_model: HostPolicyResponseActionDetails; + ingest_events_config: HostPolicyResponseActionDetails; + workflow: HostPolicyResponseActionDetails; + configure_elasticsearch_connection: HostPolicyResponseActionDetails; + configure_kernel: HostPolicyResponseActionDetails; + configure_logging: HostPolicyResponseActionDetails; + configure_malware: HostPolicyResponseActionDetails; + connect_kernel: HostPolicyResponseActionDetails; + detect_file_open_events: HostPolicyResponseActionDetails; + detect_file_write_events: HostPolicyResponseActionDetails; + detect_image_load_events: HostPolicyResponseActionDetails; + detect_process_events: HostPolicyResponseActionDetails; + download_global_artifacts: HostPolicyResponseActionDetails; + load_config: HostPolicyResponseActionDetails; + load_malware_model: HostPolicyResponseActionDetails; + read_elasticsearch_config: HostPolicyResponseActionDetails; + read_events_config: HostPolicyResponseActionDetails; + read_kernel_config: HostPolicyResponseActionDetails; + read_logging_config: HostPolicyResponseActionDetails; + read_malware_config: HostPolicyResponseActionDetails; + // The list of possible Actions will change rapidly, so the below entry will allow + // them without us defining them here statically + [key: string]: HostPolicyResponseActionDetails; +} + +interface HostPolicyResponseConfigurationStatus { + status: HostPolicyResponseActionStatus; + concerned_actions: Array<keyof HostPolicyResponseActions>; +} + +/** + * Information about the applying of a policy to a given host + */ +export interface HostPolicyResponse { + '@timestamp': string; + elastic: { + agent: { + id: string; + }; + }; + ecs: { + version: string; + }; + event: { + created: string; + kind: string; + }; + agent: { + version: string; + id: string; + }; + endpoint: { + artifacts: {}; + policy: { + applied: { + version: string; + id: string; + status: HostPolicyResponseActionStatus; + response: { + configurations: { + malware: HostPolicyResponseConfigurationStatus; + events: HostPolicyResponseConfigurationStatus; + logging: HostPolicyResponseConfigurationStatus; + streaming: HostPolicyResponseConfigurationStatus; + }; + actions: Partial<HostPolicyResponseActions>; + }; + }; + }; + }; +} + +/** + * REST API response for retrieving a host's Policy Response status + */ +export interface GetHostPolicyResponse { + policy_response: HostPolicyResponse; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts index 90d6b8b82198a..6bc728db99819 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/middleware.ts @@ -9,7 +9,7 @@ import { AlertResultList, AlertDetails } from '../../../../../common/types'; import { ImmutableMiddlewareFactory, AlertListState } from '../../types'; import { isOnAlertPage, apiQueryParams, hasSelectedAlert, uiQueryParams } from './selectors'; import { cloneHttpFetchQuery } from '../../../../common/clone_http_fetch_query'; -import { EndpointAppConstants } from '../../../../../common/types'; +import { AlertConstants } from '../../../../../common/alert_constants'; export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState> = ( coreStart, @@ -18,7 +18,7 @@ export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState> async function fetchIndexPatterns(): Promise<IIndexPattern[]> { const { indexPatterns } = depsStart.data; const eventsPattern: { indexPattern: string } = await coreStart.http.get( - `${EndpointAppConstants.INDEX_PATTERN_ROUTE}/${EndpointAppConstants.EVENT_DATASET}` + `${AlertConstants.INDEX_PATTERN_ROUTE}/${AlertConstants.EVENT_DATASET}` ); const fields = await indexPatterns.getFieldsForWildcard({ pattern: eventsPattern.indexPattern, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts index 21871ec8ca849..16a1f96c926b8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostListPagination, ServerApiError } from '../../types'; -import { HostResultList, HostInfo } from '../../../../../common/types'; +import { ServerApiError } from '../../types'; +import { HostResultList, HostInfo, GetHostPolicyResponse } from '../../../../../common/types'; interface ServerReturnedHostList { type: 'serverReturnedHostList'; payload: HostResultList; } +interface ServerFailedToReturnHostList { + type: 'serverFailedToReturnHostList'; + payload: ServerApiError; +} + interface ServerReturnedHostDetails { type: 'serverReturnedHostDetails'; payload: HostInfo; @@ -22,13 +27,14 @@ interface ServerFailedToReturnHostDetails { payload: ServerApiError; } -interface UserPaginatedHostList { - type: 'userPaginatedHostList'; - payload: HostListPagination; +interface ServerReturnedHostPolicyResponse { + type: 'serverReturnedHostPolicyResponse'; + payload: GetHostPolicyResponse; } export type HostAction = | ServerReturnedHostList + | ServerFailedToReturnHostList | ServerReturnedHostDetails | ServerFailedToReturnHostDetails - | UserPaginatedHostList; + | ServerReturnedHostPolicyResponse; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts new file mode 100644 index 0000000000000..d2e1985d055c6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart, HttpSetup } from 'kibana/public'; +import { DepsStartMock, depsStartMock } from '../../mocks'; +import { AppAction, HostState, HostIndexUIQueryParams } from '../../types'; +import { Immutable, HostResultList } from '../../../../../common/types'; +import { History, createBrowserHistory } from 'history'; +import { hostMiddlewareFactory } from './middleware'; +import { applyMiddleware, Store, createStore } from 'redux'; +import { hostListReducer } from './reducer'; +import { coreMock } from 'src/core/public/mocks'; +import { urlFromQueryParams } from '../../view/hosts/url_from_query_params'; +import { uiQueryParams } from './selectors'; +import { mockHostResultList } from './mock_host_result_list'; +import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../test_utils'; + +describe('host list pagination: ', () => { + let fakeCoreStart: jest.Mocked<CoreStart>; + let depsStart: DepsStartMock; + let fakeHttpServices: jest.Mocked<HttpSetup>; + let history: History<never>; + let store: Store<Immutable<HostState>, Immutable<AppAction>>; + let queryParams: () => HostIndexUIQueryParams; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let actionSpyMiddleware; + const getEndpointListApiResponse = (): HostResultList => { + return mockHostResultList({ request_page_size: 1, request_page_index: 1, total: 10 }); + }; + + let historyPush: (params: HostIndexUIQueryParams) => void; + beforeEach(() => { + fakeCoreStart = coreMock.createStart(); + depsStart = depsStartMock(); + fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>; + history = createBrowserHistory(); + const middleware = hostMiddlewareFactory(fakeCoreStart, depsStart); + ({ actionSpyMiddleware, waitForAction } = createSpyMiddleware<HostState>()); + store = createStore(hostListReducer, applyMiddleware(middleware, actionSpyMiddleware)); + + history.listen(location => { + store.dispatch({ type: 'userChangedUrl', payload: location }); + }); + + queryParams = () => uiQueryParams(store.getState()); + + historyPush = (nextQueryParams: HostIndexUIQueryParams): void => { + return history.push(urlFromQueryParams(nextQueryParams)); + }; + }); + + describe('when the user enteres the host list for the first time', () => { + it('the api is called with page_index and page_size defaulting to 0 and 10 respectively', async () => { + const apiResponse = getEndpointListApiResponse(); + fakeHttpServices.post.mockResolvedValue(apiResponse); + expect(fakeHttpServices.post).not.toHaveBeenCalled(); + + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + }, + }); + await waitForAction('serverReturnedHostList'); + expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: '0' }, { page_size: '10' }], + }), + }); + }); + }); + describe('when a new page size is passed', () => { + it('should modify the url correctly', () => { + historyPush({ ...queryParams(), page_size: '20' }); + expect(queryParams()).toMatchInlineSnapshot(` + Object { + "page_index": "0", + "page_size": "20", + } + `); + }); + }); + describe('when an invalid page size is passed', () => { + it('should modify the page size in the url to the default page size', () => { + historyPush({ ...queryParams(), page_size: '1' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when a negative page size is passed', () => { + it('should modify the page size in the url to the default page size', () => { + historyPush({ ...queryParams(), page_size: '-1' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when a new page index is passed', () => { + it('should modify the page index in the url correctly', () => { + historyPush({ ...queryParams(), page_index: '2' }); + expect(queryParams()).toEqual({ page_index: '2', page_size: '10' }); + }); + }); + + describe('when a negative page index is passed', () => { + it('should modify the page index in the url to the default page index', () => { + historyPush({ ...queryParams(), page_index: '-2' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when invalid params are passed in the url', () => { + it('ignores non-numeric values for page_index and page_size', () => { + historyPush({ ...queryParams, page_index: 'one', page_size: 'fifty' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + + it('ignores unknown url search params', () => { + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + search: '?foo=bar', + }, + }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + + it('ignores multiple values of the same query params except the last value', () => { + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + search: '?page_index=2&page_index=3&page_size=20&page_size=50', + }, + }); + expect(queryParams()).toEqual({ page_index: '3', page_size: '50' }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts index 6148934343635..863ffc50d0155 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts @@ -6,12 +6,12 @@ import { createStore, Dispatch, Store } from 'redux'; import { HostAction, hostListReducer } from './index'; -import { HostListState } from '../../types'; +import { HostState } from '../../types'; import { listData } from './selectors'; import { mockHostResultList } from './mock_host_result_list'; describe('HostList store concerns', () => { - let store: Store<HostListState>; + let store: Store<HostState>; let dispatch: Dispatch<HostAction>; const createTestStore = () => { store = createStore(hostListReducer); @@ -37,6 +37,11 @@ describe('HostList store concerns', () => { pageIndex: 0, total: 0, loading: false, + error: undefined, + details: undefined, + detailsLoading: false, + detailsError: undefined, + location: undefined, }); }); @@ -52,7 +57,7 @@ describe('HostList store concerns', () => { }); const currentState = store.getState(); - expect(currentState.hosts).toEqual(payload.hosts.map(hostInfo => hostInfo.metadata)); + expect(currentState.hosts).toEqual(payload.hosts); expect(currentState.pageSize).toEqual(payload.request_page_size); expect(currentState.pageIndex).toEqual(payload.request_page_index); expect(currentState.total).toEqual(payload.total); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index 8f39baddda00e..2064c76f7dfb5 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -9,21 +9,23 @@ import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; import { hostListReducer, hostMiddlewareFactory } from './index'; import { HostResultList, Immutable } from '../../../../../common/types'; -import { HostListState } from '../../types'; +import { HostState } from '../../types'; import { AppAction } from '../action'; import { listData } from './selectors'; import { DepsStartMock, depsStartMock } from '../../mocks'; import { mockHostResultList } from './mock_host_result_list'; +import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils'; describe('host list middleware', () => { - const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); let fakeCoreStart: jest.Mocked<CoreStart>; let depsStart: DepsStartMock; let fakeHttpServices: jest.Mocked<HttpSetup>; - type HostListStore = Store<Immutable<HostListState>, Immutable<AppAction>>; + type HostListStore = Store<Immutable<HostState>, Immutable<AppAction>>; let store: HostListStore; let getState: HostListStore['getState']; let dispatch: HostListStore['dispatch']; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let actionSpyMiddleware; let history: History<never>; const getEndpointListApiResponse = (): HostResultList => { @@ -33,15 +35,16 @@ describe('host list middleware', () => { fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); depsStart = depsStartMock(); fakeHttpServices = fakeCoreStart.http as jest.Mocked<HttpSetup>; + ({ actionSpyMiddleware, waitForAction } = createSpyMiddleware<HostState>()); store = createStore( hostListReducer, - applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart)) + applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart), actionSpyMiddleware) ); getState = store.getState; dispatch = store.dispatch; history = createBrowserHistory(); }); - test('handles `userChangedUrl`', async () => { + it('handles `userChangedUrl`', async () => { const apiResponse = getEndpointListApiResponse(); fakeHttpServices.post.mockResolvedValue(apiResponse); expect(fakeHttpServices.post).not.toHaveBeenCalled(); @@ -53,12 +56,12 @@ describe('host list middleware', () => { pathname: '/hosts', }, }); - await sleep(); + await waitForAction('serverReturnedHostList'); expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { body: JSON.stringify({ - paging_properties: [{ page_index: 0 }, { page_size: 10 }], + paging_properties: [{ page_index: '0' }, { page_size: '10' }], }), }); - expect(listData(getState())).toEqual(apiResponse.hosts.map(hostInfo => hostInfo.metadata)); + expect(listData(getState())).toEqual(apiResponse.hosts); }); }); 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 83e11f5408bcd..bcfd6b96c9eb8 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 @@ -4,34 +4,65 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HostResultList } from '../../../../../common/types'; +import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors'; +import { HostState } from '../../types'; import { ImmutableMiddlewareFactory } from '../../types'; -import { pageIndex, pageSize, isOnHostPage, hasSelectedHost, uiQueryParams } from './selectors'; -import { HostListState } from '../../types'; +import { HostPolicyResponse } from '../../../../../common/types'; -export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostListState> = coreStart => { +export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = coreStart => { return ({ getState, dispatch }) => next => async action => { next(action); const state = getState(); if ( - (action.type === 'userChangedUrl' && - isOnHostPage(state) && - hasSelectedHost(state) !== true) || - action.type === 'userPaginatedHostList' + action.type === 'userChangedUrl' && + isOnHostPage(state) && + hasSelectedHost(state) !== true ) { - const hostPageIndex = pageIndex(state); - const hostPageSize = pageSize(state); - const response = await coreStart.http.post('/api/endpoint/metadata', { - body: JSON.stringify({ - paging_properties: [{ page_index: hostPageIndex }, { page_size: hostPageSize }], - }), - }); - response.request_page_index = hostPageIndex; - dispatch({ - type: 'serverReturnedHostList', - payload: response, - }); + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state); + try { + const response = await coreStart.http.post<HostResultList>('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedHostList', + payload: response, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnHostList', + payload: error, + }); + } } if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) { + // If user navigated directly to a host details page, load the host list + if (listData(state).length === 0) { + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state); + try { + const response = await coreStart.http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedHostList', + payload: response, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnHostList', + payload: error, + }); + return; + } + } + + // call the host details api const { selected_host: selectedHost } = uiQueryParams(state); try { const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`); @@ -39,6 +70,20 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostListState> = type: 'serverReturnedHostDetails', payload: response, }); + dispatch({ + type: 'serverReturnedHostPolicyResponse', + payload: { + policy_response: ({ + endpoint: { + policy: { + applied: { + status: 'success', + }, + }, + }, + } as unknown) as HostPolicyResponse, // Temporary until we get API + }, + }); } catch (error) { dispatch({ type: 'serverFailedToReturnHostDetails', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index 298e819645dbe..93e995194353b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -4,23 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostListState, ImmutableReducer } from '../../types'; +import { Immutable } from '../../../../../common/types'; +import { HostState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; +import { isOnHostPage, hasSelectedHost } from './selectors'; -const initialState = (): HostListState => { +const initialState = (): HostState => { return { hosts: [], pageSize: 10, pageIndex: 0, total: 0, loading: false, - detailsError: undefined, + error: undefined, details: undefined, + detailsLoading: false, + detailsError: undefined, + policyResponse: undefined, location: undefined, }; }; -export const hostListReducer: ImmutableReducer<HostListState, AppAction> = ( +export const hostListReducer: ImmutableReducer<HostState, AppAction> = ( state = initialState(), action ) => { @@ -33,35 +38,87 @@ export const hostListReducer: ImmutableReducer<HostListState, AppAction> = ( } = action.payload; return { ...state, - hosts: hosts.map(hostInfo => hostInfo.metadata), + hosts, total, pageSize, pageIndex, loading: false, + error: undefined, + }; + } else if (action.type === 'serverFailedToReturnHostList') { + return { + ...state, + error: action.payload, + loading: false, }; } else if (action.type === 'serverReturnedHostDetails') { return { ...state, details: action.payload.metadata, + detailsLoading: false, + detailsError: undefined, }; } else if (action.type === 'serverFailedToReturnHostDetails') { return { ...state, detailsError: action.payload, + detailsLoading: false, }; - } else if (action.type === 'userPaginatedHostList') { + } else if (action.type === 'serverReturnedHostPolicyResponse') { return { ...state, - ...action.payload, - loading: true, + policyResponse: action.payload.policy_response, }; } else if (action.type === 'userChangedUrl') { + const newState: Immutable<HostState> = { + ...state, + location: action.payload, + }; + const isCurrentlyOnListPage = isOnHostPage(newState) && !hasSelectedHost(newState); + const wasPreviouslyOnListPage = isOnHostPage(state) && !hasSelectedHost(state); + const isCurrentlyOnDetailsPage = isOnHostPage(newState) && hasSelectedHost(newState); + const wasPreviouslyOnDetailsPage = isOnHostPage(state) && hasSelectedHost(state); + + // if on the host list page for the first time, return new location and load list + if (isCurrentlyOnListPage) { + if (!wasPreviouslyOnListPage) { + return { + ...state, + location: action.payload, + loading: true, + error: undefined, + detailsError: undefined, + }; + } + } else if (isCurrentlyOnDetailsPage) { + // if previous page was the list or another host details page, load host details only + if (wasPreviouslyOnDetailsPage || wasPreviouslyOnListPage) { + return { + ...state, + location: action.payload, + detailsLoading: true, + error: undefined, + detailsError: undefined, + }; + } else { + // if previous page was not host list or host details, load both list and details + return { + ...state, + location: action.payload, + loading: true, + detailsLoading: true, + error: undefined, + detailsError: undefined, + }; + } + } + // otherwise we are not on a host list or details page return { ...state, location: action.payload, + error: undefined, detailsError: undefined, }; } - return state; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts index 03cdba8505800..b0711baf9cdff 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -6,38 +6,47 @@ import querystring from 'querystring'; import { createSelector } from 'reselect'; import { Immutable } from '../../../../../common/types'; -import { HostListState, HostIndexUIQueryParams } from '../../types'; +import { HostState, HostIndexUIQueryParams } from '../../types'; -export const listData = (state: Immutable<HostListState>) => state.hosts; +const PAGE_SIZES = Object.freeze([10, 20, 50]); -export const pageIndex = (state: Immutable<HostListState>) => state.pageIndex; +export const listData = (state: Immutable<HostState>) => state.hosts; -export const pageSize = (state: Immutable<HostListState>) => state.pageSize; +export const pageIndex = (state: Immutable<HostState>): number => state.pageIndex; -export const totalHits = (state: Immutable<HostListState>) => state.total; +export const pageSize = (state: Immutable<HostState>): number => state.pageSize; -export const isLoading = (state: Immutable<HostListState>) => state.loading; +export const totalHits = (state: Immutable<HostState>): number => state.total; -export const detailsError = (state: Immutable<HostListState>) => state.detailsError; +export const listLoading = (state: Immutable<HostState>): boolean => state.loading; -export const detailsData = (state: Immutable<HostListState>) => { - return state.details; -}; +export const listError = (state: Immutable<HostState>) => state.error; -export const isOnHostPage = (state: Immutable<HostListState>) => +export const detailsData = (state: Immutable<HostState>) => state.details; + +export const detailsLoading = (state: Immutable<HostState>): boolean => state.detailsLoading; + +export const detailsError = (state: Immutable<HostState>) => state.detailsError; + +export const isOnHostPage = (state: Immutable<HostState>) => state.location ? state.location.pathname === '/hosts' : false; export const uiQueryParams: ( - state: Immutable<HostListState> + state: Immutable<HostState> ) => Immutable<HostIndexUIQueryParams> = createSelector( - (state: Immutable<HostListState>) => state.location, - (location: Immutable<HostListState>['location']) => { - const data: HostIndexUIQueryParams = {}; + (state: Immutable<HostState>) => state.location, + (location: Immutable<HostState>['location']) => { + const data: HostIndexUIQueryParams = { page_index: '0', page_size: '10' }; if (location) { // Removes the `?` from the beginning of query string if it exists const query = querystring.parse(location.search.slice(1)); - const keys: Array<keyof HostIndexUIQueryParams> = ['selected_host', 'show']; + const keys: Array<keyof HostIndexUIQueryParams> = [ + 'selected_host', + 'page_size', + 'page_index', + 'show', + ]; for (const key of keys) { const value = query[key]; @@ -47,12 +56,23 @@ export const uiQueryParams: ( data[key] = value[value.length - 1]; } } + + // Check if page size is an expected size, otherwise default to 10 + if (!PAGE_SIZES.includes(Number(data.page_size))) { + data.page_size = '10'; + } + + // Check if page index is a valid positive integer, otherwise default to 0 + const pageIndexAsNumber = Number(data.page_index); + if (!Number.isFinite(pageIndexAsNumber) || pageIndexAsNumber < 0) { + data.page_index = '0'; + } } return data; } ); -export const hasSelectedHost: (state: Immutable<HostListState>) => boolean = createSelector( +export const hasSelectedHost: (state: Immutable<HostState>) => boolean = createSelector( uiQueryParams, ({ selected_host: selectedHost }) => { return selectedHost !== undefined; @@ -60,9 +80,19 @@ export const hasSelectedHost: (state: Immutable<HostListState>) => boolean = cre ); /** What policy details panel view to show */ -export const showView: (state: HostListState) => 'policy_response' | 'details' = createSelector( +export const showView: (state: HostState) => 'policy_response' | 'details' = createSelector( uiQueryParams, searchParams => { return searchParams.show === 'policy_response' ? 'policy_response' : 'details'; } ); + +/** + * Returns the Policy Response overall status + */ +export const policyResponseStatus: (state: Immutable<HostState>) => string = createSelector( + state => state.policyResponse, + policyResponse => { + return (policyResponse && policyResponse?.endpoint?.policy?.applied?.status) || ''; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts index 69b11fb3c1f0e..9912c9a81e6e1 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/index.test.ts @@ -16,6 +16,7 @@ import { setPolicyListApiMockImplementation } from './test_mock_utils'; import { INGEST_API_DATASOURCES } from './services/ingest'; import { Immutable } from '../../../../../common/types'; import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; describe('policy list store concerns', () => { let fakeCoreStart: ReturnType<typeof coreMock.createStart>; @@ -121,7 +122,11 @@ describe('policy list store concerns', () => { }); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 10, + }, }); }); @@ -140,7 +145,11 @@ describe('policy list store concerns', () => { dispatchUserChangedUrl('?page_size=50&page_index=0'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 50 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 50, + }, }); }); it('uses defaults for params not in url', async () => { @@ -159,21 +168,33 @@ describe('policy list store concerns', () => { dispatchUserChangedUrl('?page_size=-50&page_index=-99'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 10, + }, }); }); it('it ignores non-numeric values for page_index and page_size', async () => { dispatchUserChangedUrl('?page_size=fifty&page_index=ten'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 1, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 1, + perPage: 10, + }, }); }); it('accepts only known values for `page_size`', async () => { dispatchUserChangedUrl('?page_size=300&page_index=10'); await waitForAction('serverReturnedPolicyListData'); expect(fakeCoreStart.http.get).toHaveBeenCalledWith(INGEST_API_DATASOURCES, { - query: { kuery: 'datasources.package.name: endpoint', page: 11, perPage: 10 }, + query: { + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, + page: 11, + perPage: 10, + }, }); }); it(`ignores unknown url search params`, async () => { @@ -186,8 +207,8 @@ describe('policy list store concerns', () => { it(`uses last param value if param is defined multiple times`, async () => { dispatchUserChangedUrl('?page_size=20&page_size=50&page_index=20&page_index=40'); expect(urlSearchParams(getState())).toEqual({ - page_index: 20, - page_size: 20, + page_index: 40, + page_size: 50, }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts index 6d2e952fa07bb..4986a342cca19 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/selectors.ts @@ -46,12 +46,16 @@ export const urlSearchParams: ( const query = parse(location.search); // Search params can appear multiple times in the URL, in which case the value for them, - // once parsed, would be an array. In these case, we take the first value defined + // once parsed, would be an array. In these case, we take the last value defined searchParams.page_index = Number( - (Array.isArray(query.page_index) ? query.page_index[0] : query.page_index) ?? 0 + (Array.isArray(query.page_index) + ? query.page_index[query.page_index.length - 1] + : query.page_index) ?? 0 ); searchParams.page_size = Number( - (Array.isArray(query.page_size) ? query.page_size[0] : query.page_size) ?? 10 + (Array.isArray(query.page_size) + ? query.page_size[query.page_size.length - 1] + : query.page_size) ?? 10 ); // If pageIndex is not a valid positive integer, set it to 0 diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts index c2865d36c95f2..46f4c09e05a74 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.test.ts @@ -6,6 +6,7 @@ import { sendGetDatasource, sendGetEndpointSpecificDatasources } from './ingest'; import { httpServiceMock } from '../../../../../../../../../src/core/public/mocks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../../../../ingest_manager/common'; describe('ingest service', () => { let http: ReturnType<typeof httpServiceMock.createStartContract>; @@ -19,7 +20,7 @@ describe('ingest service', () => { await sendGetEndpointSpecificDatasources(http); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources', { query: { - kuery: 'datasources.package.name: endpoint', + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, }, }); }); @@ -29,7 +30,7 @@ describe('ingest service', () => { }); expect(http.get).toHaveBeenCalledWith('/api/ingest_manager/datasources', { query: { - kuery: 'someValueHere and datasources.package.name: endpoint', + kuery: `someValueHere and ${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, perPage: 10, page: 1, }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts index 4356517e43c2c..5c27680d6a35c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/policy_list/services/ingest.ts @@ -8,6 +8,7 @@ import { HttpFetchOptions, HttpStart } from 'kibana/public'; import { GetDatasourcesRequest, GetAgentStatusResponse, + DATASOURCE_SAVED_OBJECT_TYPE, } from '../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; import { NewPolicyData } from '../../../../../../common/types'; @@ -33,7 +34,7 @@ export const sendGetEndpointSpecificDatasources = ( ...options.query, kuery: `${ options?.query?.kuery ? options.query.kuery + ' and ' : '' - }datasources.package.name: endpoint`, + }${DATASOURCE_SAVED_OBJECT_TYPE}.package.name: endpoint`, }, }); }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index f407d32cb3b42..58e706f20ec8e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -17,11 +17,12 @@ import { AlertData, AlertResultList, Immutable, - ImmutableArray, AlertDetails, MalwareFields, UIPolicyConfig, PolicyData, + HostPolicyResponse, + HostInfo, } from '../../../common/types'; import { EndpointPluginStartDependencies } from '../../plugin'; import { AppAction } from './store/action'; @@ -88,23 +89,42 @@ export type SubstateMiddlewareFactory = <Substate>( middleware: ImmutableMiddleware<Substate, AppAction> ) => Middleware<{}, GlobalState, Dispatch<AppAction | Immutable<AppAction>>>; -export interface HostListState { - hosts: HostMetadata[]; +export interface HostState { + /** list of host **/ + hosts: HostInfo[]; + /** number of items per page */ pageSize: number; + /** which page to show */ pageIndex: number; + /** total number of hosts returned */ total: number; + /** list page is retrieving data */ loading: boolean; - detailsError?: ServerApiError; + /** api error from retrieving host list */ + error?: ServerApiError; + /** details data for a specific host */ details?: Immutable<HostMetadata>; + /** details page is retrieving data */ + detailsLoading: boolean; + /** api error from retrieving host details */ + detailsError?: ServerApiError; + /** Holds the Policy Response for the Host currently being displayed in the details */ + policyResponse?: HostPolicyResponse; + /** current location info */ location?: Immutable<EndpointAppLocation>; } -export interface HostListPagination { - pageIndex: number; - pageSize: number; -} +/** + * Query params on the host page parsed from the URL + */ export interface HostIndexUIQueryParams { + /** Selected host id shows host details flyout */ selected_host?: string; + /** How many items to show in list */ + page_size?: string; + /** Which page to show */ + page_index?: string; + /** show the policy response or host details */ show?: string; } @@ -257,7 +277,7 @@ export type KeysByValueCriteria<O, Criteria> = { export type MalwareProtectionOSes = KeysByValueCriteria<UIPolicyConfig, { malware: MalwareFields }>; export interface GlobalState { - readonly hostList: HostListState; + readonly hostList: HostState; readonly alertList: AlertListState; readonly policyList: PolicyListState; readonly policyDetails: PolicyDetailsState; @@ -291,7 +311,7 @@ export type AlertListData = AlertResultList; export interface AlertListState { /** Array of alert items. */ - readonly alerts: ImmutableArray<AlertData>; + readonly alerts: Immutable<AlertData[]>; /** The total number of alerts on the page. */ readonly total: number; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx index d18bc59a35f52..d32ad4dd9defc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -33,5 +33,6 @@ export const AlertDetailResolver = styled( width: 100%; display: flex; flex-grow: 1; - min-height: 500px; + /* gross demo hack */ + min-height: calc(100vh - 505px); `; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx index 6c294d9c86548..7475229853698 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEvent, useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTabs, EuiTab } from '@elastic/eui'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Immutable } from '../../../../../common/types'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; interface NavTabs { name: string; @@ -48,33 +49,30 @@ const navTabs: Immutable<NavTabs[]> = [ }, ]; -export const HeaderNavigation: React.FunctionComponent = React.memo(() => { - const history = useHistory(); - const location = useLocation(); +const NavTab = memo<{ tab: NavTabs }>(({ tab }) => { + const { pathname } = useLocation(); const { services } = useKibana(); + const onClickHandler = useNavigateByRouterEventHandler(tab.href); const BASE_PATH = services.application.getUrlForApp('endpoint'); + return ( + <EuiTab + data-test-subj={`${tab.id}EndpointTab`} + href={`${BASE_PATH}${tab.href}`} + onClick={onClickHandler} + isSelected={tab.href === pathname || (tab.href !== '/' && pathname.startsWith(tab.href))} + > + {tab.name} + </EuiTab> + ); +}); + +export const HeaderNavigation: React.FunctionComponent = React.memo(() => { const tabList = useMemo(() => { return navTabs.map((tab, index) => { - return ( - <EuiTab - data-test-subj={`${tab.id}EndpointTab`} - key={index} - href={`${BASE_PATH}${tab.href}`} - onClick={(event: MouseEvent) => { - event.preventDefault(); - history.push(tab.href); - }} - isSelected={ - tab.href === location.pathname || - (tab.href !== '/' && location.pathname.startsWith(tab.href)) - } - > - {tab.name} - </EuiTab> - ); + return <NavTab tab={tab} key={index} />; }); - }, [BASE_PATH, history, location.pathname]); + }, []); return <EuiTabs>{tabList}</EuiTabs>; }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx index d0a8f9690dafb..2d4d1ca8a1b5b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx @@ -110,7 +110,7 @@ describe('LinkToApp component', () => { const clickEventArg = spyOnClickHandler.mock.calls[0][0]; expect(clickEventArg.isDefaultPrevented()).toBe(true); }); - it('should not navigate if onClick callback prevents defalut', () => { + it('should not navigate if onClick callback prevents default', () => { const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(ev => { ev.preventDefault(); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx new file mode 100644 index 0000000000000..b1f09617f0174 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; +import { useNavigateByRouterEventHandler } from './use_navigate_by_router_event_handler'; +import { act, fireEvent, cleanup } from '@testing-library/react'; + +type ClickHandlerMock<Return = void> = jest.Mock< + Return, + [React.MouseEvent<HTMLAnchorElement, MouseEvent>] +>; + +describe('useNavigateByRouterEventHandler hook', () => { + let render: AppContextTestRender['render']; + let history: AppContextTestRender['history']; + let renderResult: ReturnType<AppContextTestRender['render']>; + let linkEle: HTMLAnchorElement; + let clickHandlerSpy: ClickHandlerMock; + const Link = React.memo<{ + routeTo: Parameters<typeof useNavigateByRouterEventHandler>[0]; + onClick?: Parameters<typeof useNavigateByRouterEventHandler>[1]; + }>(({ routeTo, onClick }) => { + const onClickHandler = useNavigateByRouterEventHandler(routeTo, onClick); + return ( + <a href="/mock/path" onClick={onClickHandler}> + mock link + </a> + ); + }); + + beforeEach(async () => { + ({ render, history } = createAppRootMockRenderer()); + clickHandlerSpy = jest.fn(); + renderResult = render(<Link routeTo="/mock/path" onClick={clickHandlerSpy} />); + linkEle = (await renderResult.findByText('mock link')) as HTMLAnchorElement; + }); + afterEach(cleanup); + + it('should navigate to path via Router', () => { + const containerClickSpy = jest.fn(); + renderResult.container.addEventListener('click', containerClickSpy); + expect(history.location.pathname).not.toEqual('/mock/path'); + act(() => { + fireEvent.click(linkEle); + }); + expect(containerClickSpy.mock.calls[0][0].defaultPrevented).toBe(true); + expect(history.location.pathname).toEqual('/mock/path'); + renderResult.container.removeEventListener('click', containerClickSpy); + }); + it('should support onClick prop', () => { + act(() => { + fireEvent.click(linkEle); + }); + expect(clickHandlerSpy).toHaveBeenCalled(); + expect(history.location.pathname).toEqual('/mock/path'); + }); + it('should not navigate if preventDefault is true', () => { + clickHandlerSpy.mockImplementation(event => { + event.preventDefault(); + }); + act(() => { + fireEvent.click(linkEle); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not navigate via router if click was not the primary mouse button', async () => { + act(() => { + fireEvent.click(linkEle, { button: 2 }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not navigate via router if anchor has target', () => { + linkEle.setAttribute('target', '_top'); + act(() => { + fireEvent.click(linkEle, { button: 2 }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not to navigate if meta|alt|ctrl|shift keys are pressed', () => { + ['meta', 'alt', 'ctrl', 'shift'].forEach(key => { + act(() => { + fireEvent.click(linkEle, { [`${key}Key`]: true }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts new file mode 100644 index 0000000000000..dc33f0befaf35 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MouseEventHandler, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { LocationDescriptorObject } from 'history'; + +type EventHandlerCallback = MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>; + +/** + * Provides an event handler that can be used with (for example) `onClick` props to prevent the + * event's default behaviour and instead navigate to to a route via the Router + * + * @param routeTo + * @param onClick + */ +export const useNavigateByRouterEventHandler = ( + routeTo: string | [string, unknown] | LocationDescriptorObject<unknown>, // Cover the calling signature of `history.push()` + + /** Additional onClick callback */ + onClick?: EventHandlerCallback +): EventHandlerCallback => { + const history = useHistory(); + return useCallback( + ev => { + try { + if (onClick) { + onClick(ev); + } + } catch (error) { + ev.preventDefault(); + throw error; + } + + if (ev.defaultPrevented) { + return; + } + + if (ev.button !== 0) { + return; + } + + if ( + ev.currentTarget instanceof HTMLAnchorElement && + ev.currentTarget.target !== '' && + ev.currentTarget.target !== '_self' + ) { + return; + } + + if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { + return; + } + + ev.preventDefault(); + + if (Array.isArray(routeTo)) { + history.push(...routeTo); + } else if (typeof routeTo === 'string') { + history.push(routeTo); + } else { + history.push(routeTo); + } + }, + [history, onClick, routeTo] + ); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx index 26f2203790a9e..02f91307c988e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, MouseEventHandler } from 'react'; import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui'; import styled from 'styled-components'; @@ -12,7 +12,7 @@ export type FlyoutSubHeaderProps = CommonProps & { children: React.ReactNode; backButton?: { title: string; - onClick: (event: React.MouseEvent) => void; + onClick: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>; href?: string; }; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx index 32c69426b03f3..7d948f54bd0bc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx @@ -16,13 +16,13 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; -import { HostMetadata } from '../../../../../../common/types'; +import { HostMetadata, HostPolicyResponseActionStatus } from '../../../../../../common/types'; import { FormattedDateAndTime } from '../../formatted_date_time'; import { LinkToApp } from '../../components/link_to_app'; -import { useHostListSelector, useHostLogsUrl } from '../hooks'; +import { useHostSelector, useHostLogsUrl } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; -import { uiQueryParams } from '../../../store/hosts/selectors'; +import { policyResponseStatus, uiQueryParams } from '../../../store/hosts/selectors'; +import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -31,10 +31,20 @@ const HostIds = styled(EuiListGroupItem)` } `; +const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< + { [key in keyof typeof HostPolicyResponseActionStatus]: string } +>({ + success: 'success', + warning: 'warning', + failure: 'danger', +}); + export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { appId, appPath, url } = useHostLogsUrl(details.host.id); - const queryParams = useHostListSelector(uiQueryParams); - const history = useHistory(); + const queryParams = useHostSelector(uiQueryParams); + const policyStatus = useHostSelector( + policyResponseStatus + ) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR; const detailsResultsUpper = useMemo(() => { return [ { @@ -65,6 +75,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { show: 'policy_response', }); }, [details.host.id, queryParams]); + const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseUri); const detailsResultsLower = useMemo(() => { return [ @@ -79,19 +90,20 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { defaultMessage: 'Policy Status', }), description: ( - <EuiHealth color="success"> + <EuiHealth + color={POLICY_STATUS_TO_HEALTH_COLOR[policyStatus] || 'subdued'} + data-test-subj="policyStatusHealth" + > {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} <EuiLink data-test-subj="policyStatusValue" href={'?' + policyResponseUri.search} - onClick={(ev: React.MouseEvent) => { - ev.preventDefault(); - history.push(policyResponseUri); - }} + onClick={policyStatusClickHandler} > <FormattedMessage - id="xpack.endpoint.host.details.policyStatus.success" - defaultMessage="Successful" + id="xpack.endpoint.host.details.policyStatusValue" + defaultMessage="{policyStatus, select, success {Success} warning {Warning} failure {Failed} other {Unknown}}" + values={{ policyStatus }} /> </EuiLink> </EuiHealth> @@ -127,8 +139,9 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { details.endpoint.policy.id, details.host.hostname, details.host.ip, - history, - policyResponseUri, + policyResponseUri.search, + policyStatusClickHandler, + policyStatus, ]); return ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx index a41d4a968f177..e44a45f300daa 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx @@ -17,22 +17,30 @@ import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { useHostListSelector } from '../hooks'; +import { useHostSelector } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; -import { uiQueryParams, detailsData, detailsError, showView } from '../../../store/hosts/selectors'; +import { + uiQueryParams, + detailsData, + detailsError, + showView, + detailsLoading, +} from '../../../store/hosts/selectors'; import { HostDetails } from './host_details'; import { PolicyResponse } from './policy_response'; import { HostMetadata } from '../../../../../../common/types'; import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; +import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; export const HostDetailsFlyout = memo(() => { const history = useHistory(); const { notifications } = useKibana(); - const queryParams = useHostListSelector(uiQueryParams); + const queryParams = useHostSelector(uiQueryParams); const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; - const details = useHostListSelector(detailsData); - const error = useHostListSelector(detailsError); - const show = useHostListSelector(showView); + const details = useHostSelector(detailsData); + const loading = useHostSelector(detailsLoading); + const error = useHostSelector(detailsError); + const show = useHostSelector(showView); const handleFlyoutClose = useCallback(() => { history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); @@ -63,7 +71,7 @@ export const HostDetailsFlyout = memo(() => { <EuiFlyoutHeader hasBorder> <EuiTitle size="s"> <h2 data-test-subj="hostDetailsFlyoutTitle"> - {details === undefined ? <EuiLoadingContent lines={1} /> : details.host.hostname} + {loading ? <EuiLoadingContent lines={1} /> : details?.host?.hostname} </h2> </EuiTitle> </EuiFlyoutHeader> @@ -92,24 +100,25 @@ export const HostDetailsFlyout = memo(() => { const PolicyResponseFlyoutPanel = memo<{ hostMeta: HostMetadata; }>(({ hostMeta }) => { - const history = useHistory(); - const { show, ...queryParams } = useHostListSelector(uiQueryParams); + const { show, ...queryParams } = useHostSelector(uiQueryParams); + const detailsUri = useMemo( + () => + urlFromQueryParams({ + ...queryParams, + selected_host: hostMeta.host.id, + }), + [hostMeta.host.id, queryParams] + ); + const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsUri); const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { - const detailsUri = urlFromQueryParams({ - ...queryParams, - selected_host: hostMeta.host.id, - }); return { title: i18n.translate('xpack.endpoint.host.policyResponse.backLinkTitle', { defaultMessage: 'Endpoint Details', }), href: '?' + detailsUri.search, - onClick: ev => { - ev.preventDefault(); - history.push(detailsUri); - }, + onClick: backToDetailsClickHandler, }; - }, [history, hostMeta.host.id, queryParams]); + }, [backToDetailsClickHandler, detailsUri.search]); return ( <> diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts index 7eb51f3a7b294..eb242f5c535f4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts @@ -6,10 +6,10 @@ import { useSelector } from 'react-redux'; import { useMemo } from 'react'; -import { GlobalState, HostListState } from '../../types'; +import { GlobalState, HostState } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -export function useHostListSelector<TSelected>(selector: (state: HostListState) => TSelected) { +export function useHostSelector<TSelected>(selector: (state: HostState) => TSelected) { return useSelector(function(state: GlobalState) { return selector(state.hostList); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index 88416b577ed0c..5a8765110c909 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -14,9 +14,11 @@ import { mockHostResultList, } from '../../store/hosts/mock_host_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; -import { HostInfo } from '../../../../../common/types'; +import { HostInfo, HostStatus, HostPolicyResponseActionStatus } from '../../../../../common/types'; +import { EndpointDocGenerator } from '../../../../../common/generate_data'; describe('when on the hosts page', () => { + const docGenerator = new EndpointDocGenerator(); let render: () => ReturnType<AppContextTestRender['render']>; let history: AppContextTestRender['history']; let store: AppContextTestRender['store']; @@ -46,21 +48,50 @@ describe('when on the hosts page', () => { describe('when list data loads', () => { beforeEach(() => { reactTestingLibrary.act(() => { + const hostListData = mockHostResultList({ total: 3 }); + [HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE].forEach((status, index) => { + hostListData.hosts[index] = { + metadata: hostListData.hosts[index].metadata, + host_status: status, + }; + }); const action: AppAction = { type: 'serverReturnedHostList', - payload: mockHostResultList(), + payload: hostListData, }; store.dispatch(action); }); }); - it('should render the host summary row in the table', async () => { + it('should display rows in the table', async () => { const renderResult = render(); const rows = await renderResult.findAllByRole('row'); - expect(rows).toHaveLength(2); + expect(rows).toHaveLength(4); + }); + it('should show total', async () => { + const renderResult = render(); + const total = await renderResult.findByTestId('hostListTableTotal'); + expect(total.textContent).toEqual('3 Hosts'); + }); + it('should display correct status', async () => { + const renderResult = render(); + const hostStatuses = await renderResult.findAllByTestId('rowHostStatus'); + + expect(hostStatuses[0].textContent).toEqual('Error'); + expect(hostStatuses[0].querySelector('[data-euiicon-type][color="danger"]')).not.toBeNull(); + + expect(hostStatuses[1].textContent).toEqual('Online'); + expect( + hostStatuses[1].querySelector('[data-euiicon-type][color="success"]') + ).not.toBeNull(); + + expect(hostStatuses[2].textContent).toEqual('Offline'); + expect( + hostStatuses[2].querySelector('[data-euiicon-type][color="subdued"]') + ).not.toBeNull(); }); - describe('when the user clicks the hostname in the table', () => { + describe('when the user clicks the first hostname in the table', () => { let renderResult: reactTestingLibrary.RenderResult; beforeEach(async () => { const hostDetailsApiResponse = mockHostDetailsApiResult(); @@ -74,9 +105,9 @@ describe('when on the hosts page', () => { }); renderResult = render(); - const detailsLink = await renderResult.findByTestId('hostnameCellLink'); - if (detailsLink) { - reactTestingLibrary.fireEvent.click(detailsLink); + const hostNameLinks = await renderResult.findAllByTestId('hostnameCellLink'); + if (hostNameLinks.length) { + reactTestingLibrary.fireEvent.click(hostNameLinks[0]); } }); @@ -91,6 +122,19 @@ describe('when on the hosts page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; + const dispatchServerReturnedHostPolicyResponse = ( + overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success + ) => { + const policyResponse = docGenerator.generatePolicyResponse(); + policyResponse.endpoint.policy.applied.status = overallStatus; + store.dispatch({ + type: 'serverReturnedHostPolicyResponse', + payload: { + policy_response: policyResponse, + }, + }); + }; + beforeEach(() => { const { host_status, @@ -137,9 +181,8 @@ describe('when on the hosts page', () => { const renderResult = render(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); - expect(policyStatusLink.textContent).toEqual('Successful'); expect(policyStatusLink.getAttribute('href')).toEqual( - '?selected_host=1&show=policy_response' + '?page_index=0&page_size=10&selected_host=1&show=policy_response' ); }); it('should update the URL when policy status link is clicked', async () => { @@ -150,7 +193,61 @@ describe('when on the hosts page', () => { fireEvent.click(policyStatusLink); }); const changedUrlAction = await userChangedUrlChecker; - expect(changedUrlAction.payload.search).toEqual('?selected_host=1&show=policy_response'); + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_host=1&show=policy_response' + ); + }); + it('should display Success overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.success); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Success'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="success"]') + ).not.toBeNull(); + }); + it('should display Warning overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Warning'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="warning"]') + ).not.toBeNull(); + }); + it('should display Failed overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Failed'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="danger"]') + ).not.toBeNull(); + }); + it('should display Unknown overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse('' as HostPolicyResponseActionStatus); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Unknown'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="subdued"]') + ).not.toBeNull(); }); it('should include the link to logs', async () => { const renderResult = render(); @@ -170,11 +267,11 @@ describe('when on the hosts page', () => { }); }); - it('should navigate to logs without full page refresh', async () => { + it('should navigate to logs without full page refresh', () => { expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); }); }); - describe('when showing host Policy Response', () => { + describe('when showing host Policy Response panel', () => { let renderResult: ReturnType<typeof render>; beforeEach(async () => { renderResult = render(); @@ -205,7 +302,9 @@ describe('when on the hosts page', () => { it('should include the back to details link', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); - expect(subHeaderBackLink.getAttribute('href')).toBe('?selected_host=1'); + expect(subHeaderBackLink.getAttribute('href')).toBe( + '?page_index=0&page_size=10&selected_host=1' + ); }); it('should update URL when back to details link is clicked', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); @@ -214,7 +313,9 @@ describe('when on the hosts page', () => { fireEvent.click(subHeaderBackLink); }); const changedUrlAction = await userChangedUrlChecker; - expect(changedUrlAction.payload.search).toEqual('?selected_host=1'); + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_host=1' + ); }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx index 1d81d6e8a16db..026ba2ff15126 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -4,47 +4,60 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useMemo, useCallback, memo } from 'react'; +import { EuiHorizontalRule, EuiBasicTable, EuiText, EuiLink, EuiHealth } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageContent, - EuiHorizontalRule, - EuiTitle, - EuiBasicTable, - EuiText, - EuiLink, - EuiHealth, -} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; import { EuiBasicTableColumn } from '@elastic/eui'; import { HostDetailsFlyout } from './details'; import * as selectors from '../../store/hosts/selectors'; -import { HostAction } from '../../store/hosts/action'; -import { useHostListSelector } from './hooks'; +import { useHostSelector } from './hooks'; import { CreateStructuredSelector } from '../../types'; import { urlFromQueryParams } from './url_from_query_params'; -import { HostMetadata, Immutable } from '../../../../../common/types'; +import { HostInfo, HostStatus, Immutable } from '../../../../../common/types'; +import { PageView } from '../components/page_view'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; + +const HOST_STATUS_TO_HEALTH_COLOR = Object.freeze< + { + [key in HostStatus]: string; + } +>({ + [HostStatus.ERROR]: 'danger', + [HostStatus.ONLINE]: 'success', + [HostStatus.OFFLINE]: 'subdued', +}); + +const HostLink = memo<{ + name: string; + href: string; + route: ReturnType<typeof urlFromQueryParams>; +}>(({ name, href, route }) => { + const clickHandler = useNavigateByRouterEventHandler(route); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + <EuiLink data-test-subj="hostnameCellLink" href={href} onClick={clickHandler}> + {name} + </EuiLink> + ); +}); const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const HostList = () => { - const dispatch = useDispatch<(a: HostAction) => void>(); const history = useHistory(); const { listData, pageIndex, pageSize, totalHits: totalItemCount, - isLoading, + listLoading: loading, + listError, uiQueryParams: queryParams, hasSelectedHost, - } = useHostListSelector(selector); + } = useHostSelector(selector); const paginationSetup = useMemo(() => { return { @@ -59,34 +72,48 @@ export const HostList = () => { const onTableChange = useCallback( ({ page }: { page: { index: number; size: number } }) => { const { index, size } = page; - dispatch({ - type: 'userPaginatedHostList', - payload: { pageIndex: index, pageSize: size }, - }); + history.push( + urlFromQueryParams({ + ...queryParams, + page_index: JSON.stringify(index), + page_size: JSON.stringify(size), + }) + ); }, - [dispatch] + [history, queryParams] ); - const columns: Array<EuiBasicTableColumn<Immutable<HostMetadata>>> = useMemo(() => { + const columns: Array<EuiBasicTableColumn<Immutable<HostInfo>>> = useMemo(() => { return [ { - field: '', + field: 'metadata.host', name: i18n.translate('xpack.endpoint.host.list.hostname', { defaultMessage: 'Hostname', }), - render: ({ host: { hostname, id } }: { host: { hostname: string; id: string } }) => { + render: ({ hostname, id }: HostInfo['metadata']['host']) => { + const newQueryParams = urlFromQueryParams({ ...queryParams, selected_host: id }); + return ( + <HostLink name={hostname} href={'?' + newQueryParams.search} route={newQueryParams} /> + ); + }, + }, + { + field: 'host_status', + name: i18n.translate('xpack.endpoint.host.list.hostStatus', { + defaultMessage: 'Host Status', + }), + render: (hostStatus: HostInfo['host_status']) => { return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - <EuiLink - data-test-subj="hostnameCellLink" - href={'?' + urlFromQueryParams({ ...queryParams, selected_host: id }).search} - onClick={(ev: React.MouseEvent) => { - ev.preventDefault(); - history.push(urlFromQueryParams({ ...queryParams, selected_host: id })); - }} + <EuiHealth + color={HOST_STATUS_TO_HEALTH_COLOR[hostStatus]} + data-test-subj="rowHostStatus" > - {hostname} - </EuiLink> + <FormattedMessage + id="xpack.endpoint.host.list.hostStatusValue" + defaultMessage="{hostStatus, select, online {Online} error {Error} other {Offline}}" + values={{ hostStatus }} + /> + </EuiHealth> ); }, }, @@ -95,6 +122,7 @@ export const HostList = () => { name: i18n.translate('xpack.endpoint.host.list.policy', { defaultMessage: 'Policy', }), + truncateText: true, render: () => { return 'Policy Name'; }, @@ -119,25 +147,23 @@ export const HostList = () => { }, }, { - field: 'host.os.name', + field: 'metadata.host.os.name', name: i18n.translate('xpack.endpoint.host.list.os', { defaultMessage: 'Operating System', }), }, { - field: 'host.ip', + field: 'metadata.host.ip', name: i18n.translate('xpack.endpoint.host.list.ip', { defaultMessage: 'IP Address', }), + truncateText: true, }, { - field: '', - name: i18n.translate('xpack.endpoint.host.list.sensorVersion', { - defaultMessage: 'Sensor Version', + field: 'metadata.agent.version', + name: i18n.translate('xpack.endpoint.host.list.endpointVersion', { + defaultMessage: 'Version', }), - render: () => { - return 'version'; - }, }, { field: '', @@ -150,62 +176,32 @@ export const HostList = () => { }, }, ]; - }, [queryParams, history]); + }, [queryParams]); return ( - <HostPage> + <PageView + viewType="list" + data-test-subj="hostPage" + headerLeft={i18n.translate('xpack.endpoint.host.hosts', { defaultMessage: 'Hosts' })} + > {hasSelectedHost && <HostDetailsFlyout />} - <EuiPage className="hostPage"> - <EuiPageBody> - <EuiPageHeader className="hostHeader"> - <EuiTitle size="l"> - <h1 data-test-subj="hostListTitle"> - <FormattedMessage id="xpack.endpoint.host.hosts" defaultMessage="Hosts" /> - </h1> - </EuiTitle> - </EuiPageHeader> - - <EuiPageContent className="hostPageContent"> - <EuiText color="subdued" size="xs"> - <FormattedMessage - id="xpack.endpoint.host.list.totalCount" - defaultMessage="Showing: {totalItemCount, plural, one {# Host} other {# Hosts}}" - values={{ totalItemCount }} - /> - </EuiText> - <EuiHorizontalRule margin="xs" /> - <EuiBasicTable - data-test-subj="hostListTable" - items={useMemo(() => [...listData], [listData])} - columns={columns} - loading={isLoading} - pagination={paginationSetup} - onChange={onTableChange} - /> - </EuiPageContent> - </EuiPageBody> - </EuiPage> - </HostPage> + <EuiText color="subdued" size="xs" data-test-subj="hostListTableTotal"> + <FormattedMessage + id="xpack.endpoint.host.list.totalCount" + defaultMessage="{totalItemCount, plural, one {# Host} other {# Hosts}}" + values={{ totalItemCount }} + /> + </EuiText> + <EuiHorizontalRule margin="xs" /> + <EuiBasicTable + data-test-subj="hostListTable" + items={useMemo(() => [...listData], [listData])} + columns={columns} + loading={loading} + error={listError?.message} + pagination={paginationSetup} + onChange={onTableChange} + /> + </PageView> ); }; - -const HostPage = styled.div` - .hostPage { - padding: 0; - } - .hostHeader { - background-color: ${props => props.theme.eui.euiColorLightestShade}; - border-bottom: ${props => props.theme.eui.euiBorderThin}; - padding: ${props => - props.theme.eui.euiSizeXL + - ' ' + - 0 + - props.theme.eui.euiSizeXL + - ' ' + - props.theme.eui.euiSizeL}; - margin-bottom: 0; - } - .hostPageContent { - border: none; - } -`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx index 2ecc2b117bf01..d780b7bde8af3 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx @@ -101,7 +101,7 @@ describe('Policy Details', () => { 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty' ); expect(history.location.pathname).toEqual('/policy/1'); - backToListButton.simulate('click'); + backToListButton.simulate('click', { button: 0 }); expect(history.location.pathname).toEqual('/policy'); }); it('should display agent stats', async () => { @@ -130,7 +130,7 @@ describe('Policy Details', () => { 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' ); expect(history.location.pathname).toEqual('/policy/1'); - cancelbutton.simulate('click'); + cancelbutton.simulate('click', { button: 0 }); expect(history.location.pathname).toEqual('/policy'); }); it('should display save button', async () => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index 076de7b57b44b..d9bb7eabcf7b0 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -20,7 +20,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { usePolicyDetailsSelector } from './policy_hooks'; import { policyDetails, @@ -36,11 +35,11 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); const { notifications, services } = useKibana(); - const history = useHistory(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -53,7 +52,7 @@ export const PolicyDetails = React.memo(() => { const [showConfirm, setShowConfirm] = useState<boolean>(false); const policyName = policyItem?.name ?? ''; - // Handle showing udpate statuses + // Handle showing update statuses useEffect(() => { if (policyUpdateStatus) { if (policyUpdateStatus.success) { @@ -80,15 +79,9 @@ export const PolicyDetails = React.memo(() => { }); } } - }, [notifications.toasts, policyItem, policyName, policyUpdateStatus]); + }, [notifications.toasts, policyName, policyUpdateStatus]); - const handleBackToListOnClick: React.MouseEventHandler = useCallback( - ev => { - ev.preventDefault(); - history.push(`/policy`); - }, - [history] - ); + const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy'); const handleSaveOnClick = useCallback(() => { setShowConfirm(true); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx index 7f946de9614ca..9d73f12869058 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/events/windows.tsx @@ -17,18 +17,18 @@ import { } from '../../../../store/policy_details/selectors'; import { ConfigForm } from '../config_form'; import { setIn, getIn } from '../../../../models/policy_details_config'; -import { UIPolicyConfig, ImmutableArray } from '../../../../../../../common/types'; +import { UIPolicyConfig, Immutable } from '../../../../../../../common/types'; export const WindowsEvents = React.memo(() => { const selected = usePolicyDetailsSelector(selectedWindowsEvents); const total = usePolicyDetailsSelector(totalWindowsEvents); const checkboxes = useMemo(() => { - const items: ImmutableArray<{ + const items: Immutable<Array<{ name: string; os: 'windows'; protectionField: keyof UIPolicyConfig['windows']['events']; - }> = [ + }>> = [ { name: i18n.translate('xpack.endpoint.policyDetailsConfig.windows.events.dllDriverLoad', { defaultMessage: 'DLL and Driver Load', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx index 14871c71ec038..4b2a6ece9f58f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_forms/protections/malware.tsx @@ -11,7 +11,7 @@ import { EuiRadio, EuiSwitch, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { htmlIdGenerator } from '@elastic/eui'; -import { Immutable, ProtectionModes, ImmutableArray } from '../../../../../../../common/types'; +import { Immutable, ProtectionModes } from '../../../../../../../common/types'; import { OS, MalwareProtectionOSes } from '../../../../types'; import { ConfigForm } from '../config_form'; import { policyConfig } from '../../../../store/policy_details/selectors'; @@ -73,11 +73,11 @@ export const MalwareProtections = React.memo(() => { // currently just taking windows.malware, but both windows.malware and mac.malware should be the same value const selected = policyDetailsConfig && policyDetailsConfig.windows.malware.mode; - const radios: ImmutableArray<{ + const radios: Immutable<Array<{ id: ProtectionModes; label: string; protection: 'malware'; - }> = useMemo(() => { + }>> = useMemo(() => { return [ { id: ProtectionModes.detect, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index 062c7afb6706d..f7eafff137f51 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -24,30 +24,26 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { PageView } from '../components/page_view'; import { LinkToApp } from '../components/link_to_app'; import { Immutable, PolicyData } from '../../../../../common/types'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; interface TableChangeCallbackArguments { page: { index: number; size: number }; } -const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) => { - const history = useHistory(); - +const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ + name, + route, + href, +}) => { + const clickHandler = useNavigateByRouterEventHandler(route); return ( - <EuiLink - onClick={(event: React.MouseEvent) => { - event.preventDefault(); - history.push(route); - }} - > + // eslint-disable-next-line @elastic/eui/href-or-on-click + <EuiLink href={href} onClick={clickHandler}> {name} </EuiLink> ); }; -const renderPolicyNameLink = (value: string, item: Immutable<PolicyData>) => { - return <PolicyLink name={value} route={`/policy/${item.id}`} />; -}; - export const PolicyList = React.memo(() => { const { services, notifications } = useKibana(); const history = useHistory(); @@ -95,7 +91,16 @@ export const PolicyList = React.memo(() => { name: i18n.translate('xpack.endpoint.policyList.nameField', { defaultMessage: 'Policy Name', }), - render: renderPolicyNameLink, + render: (value: string, item: Immutable<PolicyData>) => { + const routeUri = `/policy/${item.id}`; + return ( + <PolicyLink + name={value} + route={routeUri} + href={services.application.getUrlForApp('endpoint') + routeUri} + /> + ); + }, truncateText: true, }, { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index 373afa89921dc..3ec15f2f1985d 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -8,13 +8,11 @@ import { ResolverEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; - readonly payload: { - readonly data: { - readonly result: { - readonly search_results: readonly ResolverEvent[]; - }; - }; - }; + readonly payload: ResolverEvent[]; } -export type DataAction = ServerReturnedResolverData; +interface ServerFailedToReturnResolverData { + readonly type: 'serverFailedToReturnResolverData'; +} + +export type DataAction = ServerReturnedResolverData | ServerFailedToReturnResolverData; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts index f01136fe20ebf..f95ecc63d2a66 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/graphing.test.ts @@ -8,7 +8,7 @@ import { Store, createStore } from 'redux'; import { DataAction } from './action'; import { dataReducer } from './reducer'; import { DataState } from '../../types'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { LegacyEndpointEvent, ResolverEvent } from '../../../../../common/types'; import { graphableProcesses, processNodePositionsAndEdgeLineSegments } from './selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; @@ -113,13 +113,7 @@ describe('resolver graph layout', () => { }); describe('when rendering no nodes', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [], - }, - }, - }; + const payload: ResolverEvent[] = []; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -133,13 +127,7 @@ describe('resolver graph layout', () => { }); describe('when rendering one node', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [processA], - }, - }, - }; + const payload = [processA]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -153,13 +141,7 @@ describe('resolver graph layout', () => { }); describe('when rendering two nodes, one being the parent of the other', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [processA, processB], - }, - }, - }; + const payload = [processA, processB]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); @@ -173,23 +155,17 @@ describe('resolver graph layout', () => { }); describe('when rendering two forks, and one fork has an extra long tine', () => { beforeEach(() => { - const payload = { - data: { - result: { - search_results: [ - processA, - processB, - processC, - processD, - processE, - processF, - processG, - processH, - processI, - ], - }, - }, - }; + const payload = [ + processA, + processB, + processC, + processD, + processE, + processF, + processG, + processH, + processI, + ]; const action: DataAction = { type: 'serverReturnedResolverData', payload }; store.dispatch(action); }); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts index a3184389a794e..fc307002819a9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/reducer.ts @@ -11,25 +11,28 @@ function initialState(): DataState { return { results: [], isLoading: false, + hasError: false, }; } export const dataReducer: Reducer<DataState, ResolverAction> = (state = initialState(), action) => { if (action.type === 'serverReturnedResolverData') { - const { - data: { - result: { search_results }, - }, - } = action.payload; return { ...state, - results: search_results, + results: action.payload, isLoading: false, + hasError: false, }; } else if (action.type === 'appRequestedResolverData') { return { ...state, isLoading: true, + hasError: false, + }; + } else if (action.type === 'serverFailedToReturnResolverData') { + return { + ...state, + hasError: true, }; } else { return state; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 5dda54d4ed029..59ee4b3b87505 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -34,6 +34,10 @@ export function isLoading(state: DataState) { return state.isLoading; } +export function hasError(state: DataState) { + return state.hasError; +} + /** * An isometric projection is a method for representing three dimensional objects in 2 dimensions. * More information about isometric projections can be found here https://en.wikipedia.org/wiki/Isometric_projection. @@ -293,7 +297,7 @@ function* levelOrderWithWidths( metadata.firstChildWidth = width; } else { const firstChildWidth = widths.get(siblings[0]); - const lastChildWidth = widths.get(siblings[0]); + const lastChildWidth = widths.get(siblings[siblings.length - 1]); if (firstChildWidth === undefined || lastChildWidth === undefined) { /** * All widths have been precalcluated, so this will not happen. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts index 4e57212e5c0c2..c7177c6387e7a 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -8,7 +8,7 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { EndpointPluginServices } from '../../../plugin'; import { ResolverState, ResolverAction } from '../types'; -import { ResolverEvent } from '../../../../common/types'; +import { ResolverEvent, ResolverNode } from '../../../../common/types'; import * as event from '../../../../common/models/event'; type MiddlewareFactory<S = ResolverState> = ( @@ -16,18 +16,18 @@ type MiddlewareFactory<S = ResolverState> = ( ) => ( api: MiddlewareAPI<Dispatch<ResolverAction>, S> ) => (next: Dispatch<ResolverAction>) => (action: ResolverAction) => unknown; -interface Lifecycle { - lifecycle: ResolverEvent[]; -} -type ChildResponse = [Lifecycle]; -function flattenEvents(events: ChildResponse): ResolverEvent[] { - return events - .map((child: Lifecycle) => child.lifecycle) - .reduce( - (accumulator: ResolverEvent[], value: ResolverEvent[]) => accumulator.concat(value), - [] - ); +function flattenEvents(children: ResolverNode[], events: ResolverEvent[] = []): ResolverEvent[] { + return children.reduce((flattenedEvents, currentNode) => { + if (currentNode.lifecycle && currentNode.lifecycle.length > 0) { + flattenedEvents.push(...currentNode.lifecycle); + } + if (currentNode.children && currentNode.children.length > 0) { + return flattenEvents(currentNode.children, events); + } else { + return flattenedEvents; + } + }, events); } export const resolverMiddlewareFactory: MiddlewareFactory = context => { @@ -39,53 +39,43 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => { */ if (context?.services.http && action.payload.selectedEvent) { api.dispatch({ type: 'appRequestedResolverData' }); - let response = []; - let lifecycle: ResolverEvent[]; - let childEvents: ResolverEvent[]; - let relatedEvents: ResolverEvent[]; - let children = []; - const ancestors: ResolverEvent[] = []; - const maxAncestors = 5; - if (event.isLegacyEvent(action.payload.selectedEvent)) { - const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; - const legacyEndpointID = action.payload.selectedEvent?.agent?.id; - [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { - query: { legacyEndpointID }, - }), - ]); - childEvents = children.length > 0 ? flattenEvents(children) : []; - } else { - const uniquePid = action.payload.selectedEvent.process.entity_id; - const ppid = action.payload.selectedEvent.process.parent?.entity_id; - async function getAncestors(pid: string | undefined) { - if (ancestors.length < maxAncestors && pid !== undefined) { - const parent = await context?.services.http.get(`/api/endpoint/resolver/${pid}`); - ancestors.push(parent.lifecycle[0]); - if (parent.lifecycle[0].process?.parent?.entity_id) { - await getAncestors(parent.lifecycle[0].process.parent.entity_id); - } - } + try { + let lifecycle: ResolverEvent[]; + let children: ResolverNode[]; + let ancestors: ResolverNode[]; + if (event.isLegacyEvent(action.payload.selectedEvent)) { + const entityId = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + [{ lifecycle, children, ancestors }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${entityId}`, { + query: { legacyEndpointID, children: 5, ancestors: 5 }, + }), + ]); + } else { + const entityId = action.payload.selectedEvent.process.entity_id; + [{ lifecycle, children, ancestors }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${entityId}`, { + query: { + children: 5, + ancestors: 5, + }, + }), + ]); } - [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`), - getAncestors(ppid), - ]); + const response: ResolverEvent[] = [ + ...lifecycle, + ...flattenEvents(children), + ...flattenEvents(ancestors), + ]; + api.dispatch({ + type: 'serverReturnedResolverData', + payload: response, + }); + } catch (error) { + api.dispatch({ + type: 'serverFailedToReturnResolverData', + }); } - childEvents = children.length > 0 ? flattenEvents(children) : []; - response = [...lifecycle, ...childEvents, ...relatedEvents, ...ancestors]; - api.dispatch({ - type: 'serverReturnedResolverData', - payload: { data: { result: { search_results: response } } }, - }); } } }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts index e8ae3d08e5cb6..7d09d90881da9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/selectors.ts @@ -102,6 +102,11 @@ function uiStateSelector(state: ResolverState) { */ export const isLoading = composeSelectors(dataStateSelector, dataSelectors.isLoading); +/** + * Whether or not the resolver encountered an error while fetching data + */ +export const hasError = composeSelectors(dataStateSelector, dataSelectors.hasError); + /** * Calls the `secondSelector` with the result of the `selector`. Use this when re-exporting a * concern-specific selector. `selector` should return the concern-specific state. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index d370bda0d1842..17aa598720c59 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -136,6 +136,7 @@ export type CameraState = { export interface DataState { readonly results: readonly ResolverEvent[]; isLoading: boolean; + hasError: boolean; } export type Vector2 = readonly [number, number]; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx index de9c3c7e8f8f3..064645019ca34 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/defs.tsx @@ -54,7 +54,11 @@ type ResolverColorNames = | 'activeNoWarning' | 'activeWarning' | 'fullLabelBackground' - | 'inertDescription'; + | 'inertDescription' + | 'labelBackgroundTerminatedProcess' + | 'labelBackgroundTerminatedTrigger' + | 'labelBackgroundRunningProcess' + | 'labelBackgroundRunningTrigger'; export const NamedColors: Record<ResolverColorNames, string> = { ok: saturate(0.5, resolverPalette.temperatures[0]), @@ -70,6 +74,10 @@ export const NamedColors: Record<ResolverColorNames, string> = { activeNoWarning: '#0078FF', activeWarning: '#C61F38', fullLabelBackground: '#3B3C41', + labelBackgroundTerminatedProcess: '#8A96A8', + labelBackgroundTerminatedTrigger: '#8A96A8', + labelBackgroundRunningProcess: '#8A96A8', + labelBackgroundRunningTrigger: '#8A96A8', inertDescription: '#747474', }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 36155ece57a9c..2e7ca65c92dc1 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -8,6 +8,7 @@ import React, { useLayoutEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import * as selectors from '../store/selectors'; import { EdgeLine } from './edge_line'; import { Panel } from './panel'; @@ -59,6 +60,7 @@ export const Resolver = styled( const { projectionMatrix, ref, onMouseDown } = useCamera(); const isLoading = useSelector(selectors.isLoading); + const hasError = useSelector(selectors.hasError); const activeDescendantId = useSelector(selectors.uiActiveDescendantId); useLayoutEffect(() => { @@ -74,6 +76,16 @@ export const Resolver = styled( <div className="loading-container"> <EuiLoadingSpinner size="xl" /> </div> + ) : hasError ? ( + <div className="loading-container"> + <div> + {' '} + <FormattedMessage + id="xpack.endpoint.resolver.loadingError" + defaultMessage="Error loading data." + /> + </div> + </div> ) : ( <StyledResolverContainer className="resolver-graph kbn-resetFocusState" diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 10e331ffff02d..3201e83164dba 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -7,7 +7,13 @@ import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { htmlIdGenerator, EuiKeyboardAccessible } from '@elastic/eui'; +import { + htmlIdGenerator, + EuiKeyboardAccessible, + EuiButton, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { useSelector } from 'react-redux'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3, AdjacentProcessMap, ResolverProcessType } from '../types'; @@ -21,7 +27,7 @@ import * as selectors from '../store/selectors'; const nodeAssets = { runningProcessCube: { cubeSymbol: `#${SymbolIds.runningProcessCube}`, - labelBackground: NamedColors.fullLabelBackground, + labelBackground: NamedColors.labelBackgroundRunningProcess, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.runningProcess', { defaultMessage: 'Running Process', @@ -29,7 +35,7 @@ const nodeAssets = { }, runningTriggerCube: { cubeSymbol: `#${SymbolIds.runningTriggerCube}`, - labelBackground: NamedColors.fullLabelBackground, + labelBackground: NamedColors.labelBackgroundRunningTrigger, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.runningTrigger', { defaultMessage: 'Running Trigger', @@ -37,7 +43,7 @@ const nodeAssets = { }, terminatedProcessCube: { cubeSymbol: `#${SymbolIds.terminatedProcessCube}`, - labelBackground: NamedColors.fullLabelBackground, + labelBackground: NamedColors.labelBackgroundTerminatedProcess, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedProcess', { defaultMessage: 'Terminated Process', @@ -45,7 +51,7 @@ const nodeAssets = { }, terminatedTriggerCube: { cubeSymbol: `#${SymbolIds.terminatedTriggerCube}`, - labelBackground: NamedColors.fullLabelBackground, + labelBackground: NamedColors.labelBackgroundTerminatedTrigger, descriptionFill: NamedColors.empty, descriptionText: i18n.translate('xpack.endpoint.resolver.terminatedTrigger', { defaultMessage: 'Terminated Trigger', @@ -53,8 +59,46 @@ const nodeAssets = { }, }; +const ChildEventsButton = React.memo(() => { + return ( + <EuiButton + onClick={useCallback((clickEvent: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { + clickEvent.preventDefault(); + clickEvent.stopPropagation(); + }, [])} + color="ghost" + size="s" + iconType="arrowDown" + iconSide="right" + tabIndex={-1} + > + {i18n.translate('xpack.endpoint.resolver.relatedEvents', { + defaultMessage: 'Events', + })} + </EuiButton> + ); +}); + +const RelatedAlertsButton = React.memo(() => { + return ( + <EuiButton + onClick={useCallback((clickEvent: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { + clickEvent.preventDefault(); + clickEvent.stopPropagation(); + }, [])} + color="ghost" + size="s" + tabIndex={-1} + > + {i18n.translate('xpack.endpoint.resolver.relatedAlerts', { + defaultMessage: 'Related Alerts', + })} + </EuiButton> + ); +}); + /** - * A placeholder view for a process node. + * An artefact that represents a process node. */ export const ProcessEventDot = styled( React.memo( @@ -184,6 +228,7 @@ export const ProcessEventDot = styled( }, [animationTarget, dispatch, nodeId] ); + /* eslint-disable jsx-a11y/click-events-have-key-events */ /** * Key event handling (e.g. 'Enter'/'Space') is provisioned by the `EuiKeyboardAccessible` component @@ -256,13 +301,17 @@ export const ProcessEventDot = styled( </svg> <div style={{ + display: 'flex', + flexFlow: 'column', left: '25%', top: '30%', position: 'absolute', width: '50%', - color: 'white', + color: NamedColors.full, fontSize: `${scaledTypeSize}px`, lineHeight: '140%', + backgroundColor: NamedColors.resolverBackground, + padding: '.25rem', }} > <div @@ -271,33 +320,50 @@ export const ProcessEventDot = styled( textTransform: 'uppercase', letterSpacing: '-0.01px', backgroundColor: NamedColors.resolverBackground, - lineHeight: '1.2', + lineHeight: '1', fontWeight: 'bold', - fontSize: '.5em', + fontSize: '0.8rem', width: '100%', - margin: '0 0 .05em 0', + margin: '0', textAlign: 'left', padding: '0', + color: NamedColors.empty, }} > {descriptionText} </div> <div + className={magFactorX >= 2 ? 'euiButton' : 'euiButton euiButton--small'} data-test-subject="nodeLabel" id={labelId} style={{ backgroundColor: labelBackground, - padding: '.15em 0', + padding: '.15rem 0', textAlign: 'center', - maxWidth: '100%', + maxWidth: '20rem', + minWidth: '12rem', + width: '60%', overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', contain: 'content', + margin: '.25rem 0 .35rem 0', }} > - {eventModel.eventName(event)} + <span className="euiButton__content"> + <span className="euiButton__text">{eventModel.eventName(event)}</span> + </span> </div> + {magFactorX >= 2 && ( + <EuiFlexGroup justifyContent="flexStart" gutterSize="xs"> + <EuiFlexItem grow={false}> + <RelatedAlertsButton /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <ChildEventsButton /> + </EuiFlexItem> + </EuiFlexGroup> + )} </div> </div> </EuiKeyboardAccessible> @@ -317,6 +383,8 @@ export const ProcessEventDot = styled( white-space: nowrap; will-change: left, top, width, height; contain: strict; + min-width: 280px; + min-height: 90px; //dasharray & dashoffset should be equal to "pull" the stroke back //when it is transitioned. diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index 6e83fc19a922e..1545e9c2ae6e8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -153,13 +153,7 @@ describe('useCamera on an unpainted element', () => { } const serverResponseAction: ResolverAction = { type: 'serverReturnedResolverData', - payload: { - data: { - result: { - search_results: events, - }, - }, - }, + payload: events, }; act(() => { store.dispatch(serverResponseAction); diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts index dd9e591f4b034..2129bef0624b8 100644 --- a/x-pack/plugins/endpoint/scripts/resolver_generator.ts +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -29,7 +29,7 @@ async function main() { alertIndex: { alias: 'ai', describe: 'index to store alerts in', - default: '.alerts-endpoint-000001', + default: 'events-endpoint-1', type: 'string', }, eventIndex: { diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts new file mode 100644 index 0000000000000..943a9c22c6aae --- /dev/null +++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts @@ -0,0 +1,14 @@ +/* + * 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 { EndpointAppContextService } from './endpoint_app_context_services'; + +describe('test endpoint app context services', () => { + it('should throw error if start is not called', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(() => endpointAppContextService.getIndexPatternRetriever()).toThrow(Error); + expect(() => endpointAppContextService.getAgentService()).toThrow(Error); + }); +}); diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts new file mode 100644 index 0000000000000..b087405c7bc5b --- /dev/null +++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IndexPatternRetriever } from './index_pattern'; +import { AgentService } from '../../ingest_manager/server'; + +/** + * A singleton that holds shared services that are initialized during the start up phase + * of the plugin lifecycle. And stop during the stop phase, if needed. + */ +export class EndpointAppContextService { + private indexPatternRetriever: IndexPatternRetriever | undefined; + private agentService: AgentService | undefined; + + public start(dependencies: { + indexPatternRetriever: IndexPatternRetriever; + agentService: AgentService; + }) { + this.indexPatternRetriever = dependencies.indexPatternRetriever; + this.agentService = dependencies.agentService; + } + + public stop() {} + + public getAgentService(): AgentService { + if (!this.agentService) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.agentService; + } + + public getIndexPatternRetriever(): IndexPatternRetriever { + if (!this.indexPatternRetriever) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.indexPatternRetriever; + } +} diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts index ea612bfd75441..903d48746bfb3 100644 --- a/x-pack/plugins/endpoint/server/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/index_pattern.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server'; +import { AlertConstants } from '../common/alert_constants'; import { ESIndexPatternService } from '../../ingest_manager/server'; -import { EndpointAppConstants } from '../common/types'; export interface IndexPatternRetriever { getIndexPattern(ctx: RequestHandlerContext, datasetPath: string): Promise<string>; @@ -33,7 +33,7 @@ export class IngestIndexPatternRetriever implements IndexPatternRetriever { * @returns a string representing the index pattern (e.g. `events-endpoint-*`) */ async getEventIndexPattern(ctx: RequestHandlerContext) { - return await this.getIndexPattern(ctx, EndpointAppConstants.EVENT_DATASET); + return await this.getIndexPattern(ctx, AlertConstants.EVENT_DATASET); } /** diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts index 903aa19cd8843..519ca15cf8427 100644 --- a/x-pack/plugins/endpoint/server/mocks.ts +++ b/x-pack/plugins/endpoint/server/mocks.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AgentService, IngestManagerStartContract } from '../../ingest_manager/server'; + /** * Creates a mock IndexPatternRetriever for use in tests. * @@ -28,6 +30,15 @@ export const createMockMetadataIndexPatternRetriever = () => { return createMockIndexPatternRetriever(MetadataIndexPattern); }; +/** + * Creates a mock AgentService + */ +export const createMockAgentService = (): jest.Mocked<AgentService> => { + return { + getAgentStatusById: jest.fn(), + }; +}; + /** * Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's * ESIndexPatternService. @@ -35,10 +46,13 @@ export const createMockMetadataIndexPatternRetriever = () => { * @param indexPattern a string index pattern to return when called by a test * @returns the same value as `indexPattern` parameter */ -export const createMockIndexPatternService = (indexPattern: string) => { +export const createMockIngestManagerStartContract = ( + indexPattern: string +): IngestManagerStartContract => { return { esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), }, + agentService: createMockAgentService(), }; }; diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 8d55e64f16dcf..45e9591a14975 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -4,15 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; +import { + EndpointPlugin, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies, +} from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; -import { createMockIndexPatternService } from './mocks'; +import { createMockIngestManagerStartContract } from './mocks'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; let mockCoreSetup: ReturnType<typeof coreMock.createSetup>; + let mockCoreStart: ReturnType<typeof coreMock.createStart>; let mockedEndpointPluginSetupDependencies: jest.Mocked<EndpointPluginSetupDependencies>; + let mockedEndpointPluginStartDependencies: jest.Mocked<EndpointPluginStartDependencies>; let mockedPluginSetupContract: jest.Mocked<PluginSetupContract>; beforeEach(() => { plugin = new EndpointPlugin( @@ -23,21 +29,32 @@ describe('test endpoint plugin', () => { ); mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); mockedPluginSetupContract = { registerFeature: jest.fn(), getFeatures: jest.fn(), getFeaturesUICapabilities: jest.fn(), registerLegacyAPI: jest.fn(), }; - mockedEndpointPluginSetupDependencies = { - features: mockedPluginSetupContract, - ingestManager: createMockIndexPatternService(''), - }; }); it('test properly setup plugin', async () => { + mockedEndpointPluginSetupDependencies = { + features: mockedPluginSetupContract, + }; await plugin.setup(mockCoreSetup, mockedEndpointPluginSetupDependencies); expect(mockedPluginSetupContract.registerFeature).toBeCalledTimes(1); expect(mockCoreSetup.http.createRouter).toBeCalledTimes(1); + expect(() => plugin.getEndpointAppContextService().getIndexPatternRetriever()).toThrow(Error); + expect(() => plugin.getEndpointAppContextService().getAgentService()).toThrow(Error); + }); + + it('test properly start plugin', async () => { + mockedEndpointPluginStartDependencies = { + ingestManager: createMockIngestManagerStartContract(''), + }; + await plugin.start(mockCoreStart, mockedEndpointPluginStartDependencies); + expect(plugin.getEndpointAppContextService().getAgentService()).toBeTruthy(); + expect(plugin.getEndpointAppContextService().getIndexPatternRetriever()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index 6a42014e91130..f3cc569ad16a7 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -3,10 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, PluginInitializerContext, Logger } from 'kibana/server'; +import { Plugin, CoreSetup, PluginInitializerContext, Logger, CoreStart } from 'kibana/server'; import { first } from 'rxjs/operators'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; -import { IngestManagerSetupContract } from '../../ingest_manager/server'; import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; @@ -15,14 +14,17 @@ import { registerResolverRoutes } from './routes/resolver'; import { registerIndexPatternRoute } from './routes/index_pattern'; import { registerEndpointRoutes } from './routes/metadata'; import { IngestIndexPatternRetriever } from './index_pattern'; +import { IngestManagerStartContract } from '../../ingest_manager/server'; +import { EndpointAppContextService } from './endpoint_app_context_services'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; -export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface EndpointPluginStartDependencies { + ingestManager: IngestManagerStartContract; +} export interface EndpointPluginSetupDependencies { features: FeaturesPluginSetupContract; - ingestManager: IngestManagerSetupContract; } export class EndpointPlugin @@ -34,9 +36,15 @@ export class EndpointPlugin EndpointPluginStartDependencies > { private readonly logger: Logger; + private readonly endpointAppContextService: EndpointAppContextService = new EndpointAppContextService(); constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get('endpoint'); } + + public getEndpointAppContextService(): EndpointAppContextService { + return this.endpointAppContextService; + } + public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { plugins.features.registerFeature({ id: 'endpoint', @@ -66,11 +74,8 @@ export class EndpointPlugin }, }); const endpointContext = { - indexPatternRetriever: new IngestIndexPatternRetriever( - plugins.ingestManager.esIndexPatternService, - this.initializerContext.logger - ), logFactory: this.initializerContext.logger, + service: this.endpointAppContextService, config: (): Promise<EndpointConfigType> => { return createConfig$(this.initializerContext) .pipe(first()) @@ -84,10 +89,18 @@ export class EndpointPlugin registerIndexPatternRoute(router, endpointContext); } - public start() { + public start(core: CoreStart, plugins: EndpointPluginStartDependencies) { this.logger.debug('Starting plugin'); + this.endpointAppContextService.start({ + indexPatternRetriever: new IngestIndexPatternRetriever( + plugins.ingestManager.esIndexPatternService, + this.initializerContext.logger + ), + agentService: plugins.ingestManager.agentService, + }); } public stop() { this.logger.debug('Stopping plugin'); + this.endpointAppContextService.stop(); } } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts index 6be7b26898206..1124c977ff924 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts @@ -12,25 +12,36 @@ import { import { registerAlertRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; -import { createMockIndexPatternRetriever } from '../../mocks'; +import { createMockAgentService, createMockIndexPatternRetriever } from '../../mocks'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('test alerts route', () => { let routerMock: jest.Mocked<IRouter>; let mockClusterClient: jest.Mocked<IClusterClient>; let mockScopedClient: jest.Mocked<IScopedClusterClient>; + let endpointAppContextService: EndpointAppContextService; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient(); mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); - registerAlertRoutes(routerMock, { + + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start({ indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'), + agentService: createMockAgentService(), + }); + + registerAlertRoutes(routerMock, { logFactory: loggingServiceMock.create(), + service: endpointAppContextService, config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + afterEach(() => endpointAppContextService.stop()); + it('should fail to validate when `page_size` is not a number', async () => { const validate = () => { alertingIndexGetQuerySchema.validate({ diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index 86e9f55da5697..92f8aacbf26a2 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -5,7 +5,8 @@ */ import { GetResponse } from 'elasticsearch'; import { KibanaRequest, RequestHandler } from 'kibana/server'; -import { AlertEvent, EndpointAppConstants } from '../../../../common/types'; +import { AlertEvent } from '../../../../common/types'; +import { AlertConstants } from '../../../../common/alert_constants'; import { EndpointAppContext } from '../../../types'; import { AlertDetailsRequestParams } from '../types'; import { AlertDetailsPagination } from './lib'; @@ -22,11 +23,13 @@ export const alertDetailsHandlerWrapper = function( try { const alertId = req.params.id; const response = (await ctx.core.elasticsearch.dataClient.callAsCurrentUser('get', { - index: EndpointAppConstants.ALERT_INDEX_NAME, + index: AlertConstants.ALERT_INDEX_NAME, id: alertId, })) as GetResponse<AlertEvent>; - const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const indexPattern = await endpointAppContext.service + .getIndexPatternRetriever() + .getEventIndexPattern(ctx); const config = await endpointAppContext.config(); const pagination: AlertDetailsPagination = new AlertDetailsPagination( @@ -37,7 +40,13 @@ export const alertDetailsHandlerWrapper = function( indexPattern ); - const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern); + const currentHostInfo = await getHostData( + { + endpointAppContext, + requestHandlerContext: ctx, + }, + response._source.host.id + ); return res.ok({ body: { diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts index d482da03872c6..0f69e1bb60c44 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/lib/pagination.ts @@ -5,12 +5,8 @@ */ import { GetResponse, SearchResponse } from 'elasticsearch'; -import { - AlertEvent, - AlertHits, - Direction, - EndpointAppConstants, -} from '../../../../../common/types'; +import { AlertEvent, AlertHits, AlertAPIOrdering } from '../../../../../common/types'; +import { AlertConstants } from '../../../../../common/alert_constants'; import { EndpointConfigType } from '../../../../config'; import { searchESForAlerts, Pagination } from '../../lib'; import { AlertSearchQuery, SearchCursor, AlertDetailsRequestParams } from '../../types'; @@ -36,12 +32,12 @@ export class AlertDetailsPagination extends Pagination< } protected async doSearch( - direction: Direction, + direction: AlertAPIOrdering, cursor: SearchCursor ): Promise<SearchResponse<AlertEvent>> { const reqData: AlertSearchQuery = { pageSize: 1, - sort: EndpointAppConstants.ALERT_LIST_DEFAULT_SORT, + sort: AlertConstants.ALERT_LIST_DEFAULT_SORT, order: 'desc', query: { query: '', language: 'kuery' }, filters: [] as Filter[], diff --git a/x-pack/plugins/endpoint/server/routes/alerts/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/index.ts index 09de11897813b..b61f90b5b17f5 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/index.ts @@ -5,12 +5,12 @@ */ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../../types'; -import { EndpointAppConstants } from '../../../common/types'; +import { AlertConstants } from '../../../common/alert_constants'; import { alertListHandlerWrapper } from './list'; import { alertDetailsHandlerWrapper, alertDetailsReqSchema } from './details'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; -export const BASE_ALERTS_ROUTE = `${EndpointAppConstants.BASE_API_URL}/alerts`; +export const BASE_ALERTS_ROUTE = `${AlertConstants.BASE_API_URL}/alerts`; export function registerAlertRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.get( diff --git a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts index 74db24c85eab5..7bc1c0c306ae2 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/lib/index.ts @@ -7,7 +7,8 @@ import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; import { esQuery } from '../../../../../../../src/plugins/data/server'; -import { AlertEvent, Direction, EndpointAppConstants } from '../../../../common/types'; +import { AlertEvent, AlertAPIOrdering } from '../../../../common/types'; +import { AlertConstants } from '../../../../common/alert_constants'; import { AlertSearchQuery, AlertSearchRequest, @@ -18,7 +19,7 @@ import { export { Pagination } from './pagination'; -function reverseSortDirection(order: Direction): Direction { +function reverseSortDirection(order: AlertAPIOrdering): AlertAPIOrdering { if (order === 'asc') { return 'desc'; } @@ -100,13 +101,13 @@ const buildAlertSearchQuery = async ( query: AlertSearchQuery, indexPattern: string ): Promise<AlertSearchRequestWrapper> => { - let totalHitsMin: number = EndpointAppConstants.DEFAULT_TOTAL_HITS; + let totalHitsMin: number = AlertConstants.DEFAULT_TOTAL_HITS; // Calculate minimum total hits set to indicate there's a next page if (query.fromIndex) { totalHitsMin = Math.max( query.fromIndex + query.pageSize * 2, - EndpointAppConstants.DEFAULT_TOTAL_HITS + AlertConstants.DEFAULT_TOTAL_HITS ); } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts index f23dffd13db4f..44a0cf8744a9e 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts @@ -18,7 +18,9 @@ export const alertListHandlerWrapper = function( res ) => { try { - const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const indexPattern = await endpointAppContext.service + .getIndexPatternRetriever() + .getEventIndexPattern(ctx); const reqData = await getRequestData(req, endpointAppContext); const response = await searchESForAlerts( ctx.core.elasticsearch.dataClient, 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 95c8e4662cfce..114251820ce4b 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 @@ -13,10 +13,10 @@ import { AlertData, AlertResultList, AlertHits, - EndpointAppConstants, ESTotal, AlertingIndexGetQueryResult, } from '../../../../../common/types'; +import { AlertConstants } from '../../../../../common/alert_constants'; import { EndpointAppContext } from '../../../../types'; import { AlertSearchQuery } from '../../types'; import { AlertListPagination } from './pagination'; @@ -28,8 +28,8 @@ export const getRequestData = async ( const config = await endpointAppContext.config(); const reqData: AlertSearchQuery = { // Defaults not enforced by schema - pageSize: request.query.page_size || EndpointAppConstants.ALERT_LIST_DEFAULT_PAGE_SIZE, - sort: request.query.sort || EndpointAppConstants.ALERT_LIST_DEFAULT_SORT, + pageSize: request.query.page_size || AlertConstants.ALERT_LIST_DEFAULT_PAGE_SIZE, + sort: request.query.sort || AlertConstants.ALERT_LIST_DEFAULT_SORT, order: request.query.order || 'desc', dateRange: ((request.query.date_range !== undefined ? decode(request.query.date_range) @@ -60,14 +60,12 @@ 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] === '' && reqData.emptyStringIsUndefined ) { - reqData.searchBefore[0] = EndpointAppConstants.MAX_LONG_INT; + reqData.searchBefore[0] = AlertConstants.MAX_LONG_INT; } if ( @@ -75,7 +73,7 @@ export const getRequestData = async ( reqData.searchAfter[0] === '' && reqData.emptyStringIsUndefined ) { - reqData.searchAfter[0] = EndpointAppConstants.MAX_LONG_INT; + reqData.searchAfter[0] = AlertConstants.MAX_LONG_INT; } return reqData; diff --git a/x-pack/plugins/endpoint/server/routes/alerts/types.ts b/x-pack/plugins/endpoint/server/routes/alerts/types.ts index cf4eac5d25f80..5aefc35b5758f 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/types.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/types.ts @@ -5,14 +5,14 @@ */ import { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/public'; -import { Direction } from '../../../common/types'; +import { AlertAPIOrdering } from '../../../common/types'; /** * Sort parameters for alerts in ES. */ export interface AlertSortParam { [key: string]: { - order: Direction; + order: AlertAPIOrdering; missing?: UndefinedResultPosition; }; } @@ -38,7 +38,7 @@ export interface AlertSearchQuery { filters: Filter[]; dateRange?: TimeRange; sort: string; - order: Direction; + order: AlertAPIOrdering; searchAfter?: SearchCursor; searchBefore?: SearchCursor; emptyStringIsUndefined?: boolean; @@ -83,7 +83,7 @@ export interface AlertListRequestQuery { filters?: string; date_range: string; sort: string; - order: Direction; + order: AlertAPIOrdering; after?: SearchCursor; before?: SearchCursor; empty_string_is_undefined?: boolean; diff --git a/x-pack/plugins/endpoint/server/routes/index_pattern.ts b/x-pack/plugins/endpoint/server/routes/index_pattern.ts index 3b71f6a6957ba..7e78caaf178e4 100644 --- a/x-pack/plugins/endpoint/server/routes/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/routes/index_pattern.ts @@ -6,16 +6,17 @@ import { IRouter, Logger, RequestHandler } from 'kibana/server'; import { EndpointAppContext } from '../types'; -import { IndexPatternGetParamsResult, EndpointAppConstants } from '../../common/types'; +import { IndexPatternGetParamsResult } from '../../common/types'; +import { AlertConstants } from '../../common/alert_constants'; import { indexPatternGetParamsSchema } from '../../common/schema/index_pattern'; -import { IndexPatternRetriever } from '../index_pattern'; function handleIndexPattern( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler<IndexPatternGetParamsResult> { return async (context, req, res) => { try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); return res.ok({ body: { indexPattern: await indexRetriever.getIndexPattern(context, req.params.datasetPath), @@ -33,10 +34,10 @@ export function registerIndexPatternRoute(router: IRouter, endpointAppContext: E router.get( { - path: `${EndpointAppConstants.INDEX_PATTERN_ROUTE}/{datasetPath}`, + path: `${AlertConstants.INDEX_PATTERN_ROUTE}/{datasetPath}`, validate: { params: indexPatternGetParamsSchema }, options: { authRequired: true }, }, - handleIndexPattern(log, endpointAppContext.indexPatternRetriever) + handleIndexPattern(log, endpointAppContext) ); } diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 883bb88204fd4..08950930441df 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -4,18 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, RequestHandlerContext } from 'kibana/server'; +import { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; -import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; +import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { EndpointAppContext } from '../../types'; +import { AgentStatus } from '../../../../ingest_manager/common/types/models'; interface HitSource { _source: HostMetadata; } +interface MetadataRequestContext { + requestHandlerContext: RequestHandlerContext; + endpointAppContext: EndpointAppContext; +} + +const HOST_STATUS_MAPPING = new Map<AgentStatus, HostStatus>([ + ['online', HostStatus.ONLINE], + ['offline', HostStatus.OFFLINE], +]); + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.post( { @@ -50,9 +61,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( - context - ); + const index = await endpointAppContext.service + .getIndexPatternRetriever() + .getMetadataIndexPattern(context); const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, @@ -62,7 +73,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp 'search', queryParams )) as SearchResponse<HostMetadata>; - return res.ok({ body: mapToHostResultList(queryParams, response) }); + return res.ok({ + body: await mapToHostResultList(queryParams, response, { + endpointAppContext, + requestHandlerContext: context, + }), + }); } catch (err) { return res.internalError({ body: err }); } @@ -79,11 +95,13 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( - context + const doc = await getHostData( + { + endpointAppContext, + requestHandlerContext: context, + }, + req.params.id ); - - const doc = await getHostData(context, req.params.id, index); if (doc) { return res.ok({ body: doc }); } @@ -96,12 +114,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp } export async function getHostData( - context: RequestHandlerContext, - id: string, - index: string + metadataRequestContext: MetadataRequestContext, + id: string ): Promise<HostInfo | undefined> { + const index = await metadataRequestContext.endpointAppContext.service + .getIndexPatternRetriever() + .getMetadataIndexPattern(metadataRequestContext.requestHandlerContext); const query = getESQueryHostMetadataByID(id, index); - const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( + const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.dataClient.callAsCurrentUser( 'search', query )) as SearchResponse<HostMetadata>; @@ -110,22 +130,25 @@ export async function getHostData( return undefined; } - return enrichHostMetadata(response.hits.hits[0]._source); + return await enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext); } -function mapToHostResultList( +async function mapToHostResultList( queryParams: Record<string, any>, - searchResponse: SearchResponse<HostMetadata> -): HostResultList { + searchResponse: SearchResponse<HostMetadata>, + metadataRequestContext: MetadataRequestContext +): Promise<HostResultList> { const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0; if (searchResponse.hits.hits.length > 0) { return { request_page_size: queryParams.size, request_page_index: queryParams.from, - hosts: searchResponse.hits.hits - .map(response => response.inner_hits.most_recent.hits.hits) - .flatMap(data => data as HitSource) - .map(entry => enrichHostMetadata(entry._source)), + hosts: await Promise.all( + searchResponse.hits.hits + .map(response => response.inner_hits.most_recent.hits.hits) + .flatMap(data => data as HitSource) + .map(async entry => enrichHostMetadata(entry._source, metadataRequestContext)) + ), total: totalNumberOfHosts, }; } else { @@ -138,9 +161,44 @@ function mapToHostResultList( } } -function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo { +async function enrichHostMetadata( + hostMetadata: HostMetadata, + metadataRequestContext: MetadataRequestContext +): Promise<HostInfo> { + let hostStatus = HostStatus.ERROR; + let elasticAgentId = hostMetadata?.elastic?.agent?.id; + const log = logger(metadataRequestContext.endpointAppContext); + try { + /** + * Get agent status by elastic agent id if available or use the host id. + */ + + if (!elasticAgentId) { + elasticAgentId = hostMetadata.host.id; + log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`); + } + + const status = await metadataRequestContext.endpointAppContext.service + .getAgentService() + .getAgentStatusById( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + elasticAgentId + ); + hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR; + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + log.warn(`agent with id ${elasticAgentId} not found`); + } else { + log.error(e); + throw e; + } + } return { metadata: hostMetadata, - host_status: HostStatus.ERROR, + host_status: hostStatus, }; } + +const logger = (endpointAppContext: EndpointAppContext): Logger => { + return endpointAppContext.logFactory.get('metadata'); +}; diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index 9a7d3fb3188a6..8f0c0b4c2efaf 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -25,7 +25,10 @@ import { SearchResponse } from 'elasticsearch'; import { registerEndpointRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; -import { createMockMetadataIndexPatternRetriever } from '../../mocks'; +import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks'; +import { AgentService } from '../../../../ingest_manager/server'; +import Boom from 'boom'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('test endpoint route', () => { let routerMock: jest.Mocked<IRouter>; @@ -35,6 +38,8 @@ describe('test endpoint route', () => { let mockSavedObjectClient: jest.Mocked<SavedObjectsClientContract>; let routeHandler: RequestHandler<any, any, any>; let routeConfig: RouteConfig<any, any, any, any>; + let mockAgentService: jest.Mocked<AgentService>; + let endpointAppContextService: EndpointAppContextService; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -45,13 +50,22 @@ describe('test endpoint route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); - registerEndpointRoutes(routerMock, { + mockAgentService = createMockAgentService(); + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start({ indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + agentService: mockAgentService, + }); + + registerEndpointRoutes(routerMock, { logFactory: loggingServiceMock.create(), + service: endpointAppContextService, config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + afterEach(() => endpointAppContextService.stop()); + function createRouteHandlerContext( dataClient: jest.Mocked<IScopedClusterClient>, savedObjectsClient: jest.Mocked<SavedObjectsClientContract> @@ -83,7 +97,7 @@ describe('test endpoint route', () => { [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; - + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -113,6 +127,8 @@ describe('test endpoint route', () => { ], }, }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve((data as unknown) as SearchResponse<HostMetadata>) ); @@ -154,6 +170,8 @@ describe('test endpoint route', () => { filter: 'not host.ip:10.140.73.246', }, }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve((data as unknown) as SearchResponse<HostMetadata>) ); @@ -216,10 +234,10 @@ describe('test endpoint route', () => { }, }) ); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; - await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -233,13 +251,14 @@ describe('test endpoint route', () => { expect(message).toEqual('Endpoint Not Found'); }); - it('should return a single endpoint with status error', async () => { + it('should return a single endpoint with status online', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: (data as any).hits.hits[0]._id }, }); const response: SearchResponse<HostMetadata> = (data as unknown) as SearchResponse< HostMetadata >; + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -256,6 +275,64 @@ describe('test endpoint route', () => { expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result).toHaveProperty('metadata.endpoint'); + expect(result.host_status).toEqual(HostStatus.ONLINE); + }); + + it('should return a single endpoint with status error when AgentService throw 404', async () => { + const response: SearchResponse<HostMetadata> = (data as unknown) as SearchResponse< + HostMetadata + >; + + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: response.hits.hits[0]._id }, + }); + + mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { + throw Boom.notFound('Agent not found'); + }); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; + expect(result.host_status).toEqual(HostStatus.ERROR); + }); + + it('should return a single endpoint with status error when status is not offline or online', async () => { + const response: SearchResponse<HostMetadata> = (data as unknown) as SearchResponse< + HostMetadata + >; + + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: response.hits.hits[0]._id }, + }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); }); }); diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts index c8143fbdda1ea..28bac2fa10e0c 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts @@ -6,7 +6,8 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { createMockMetadataIndexPatternRetriever, MetadataIndexPattern } from '../../mocks'; +import { MetadataIndexPattern } from '../../mocks'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -17,8 +18,8 @@ describe('query builder', () => { const query = await kibanaRequestToMetadataListESQuery( mockRequest, { - indexPatternRetriever: createMockMetadataIndexPatternRetriever(), logFactory: loggingServiceMock.create(), + service: new EndpointAppContextService(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, MetadataIndexPattern @@ -68,8 +69,8 @@ describe('query builder', () => { const query = await kibanaRequestToMetadataListESQuery( mockRequest, { - indexPatternRetriever: createMockMetadataIndexPatternRetriever(), logFactory: loggingServiceMock.create(), + service: new EndpointAppContextService(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, MetadataIndexPattern diff --git a/x-pack/plugins/endpoint/server/routes/resolver.ts b/x-pack/plugins/endpoint/server/routes/resolver.ts index 77fcbc87baeb1..3599acacb4f59 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver.ts @@ -6,21 +6,27 @@ import { IRouter } from 'kibana/server'; import { EndpointAppContext } from '../types'; -import { handleRelatedEvents, validateRelatedEvents } from './resolver/related_events'; -import { handleChildren, validateChildren } from './resolver/children'; -import { handleLifecycle, validateLifecycle } from './resolver/lifecycle'; +import { + validateTree, + validateEvents, + validateChildren, + validateAncestry, +} from '../../common/schema/resolver'; +import { handleEvents } from './resolver/events'; +import { handleChildren } from './resolver/children'; +import { handleAncestry } from './resolver/ancestry'; +import { handleTree } from './resolver/tree'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); - const indexPatternService = endpointAppContext.indexPatternRetriever; router.get( { - path: '/api/endpoint/resolver/{id}/related', - validate: validateRelatedEvents, + path: '/api/endpoint/resolver/{id}/events', + validate: validateEvents, options: { authRequired: true }, }, - handleRelatedEvents(log, indexPatternService) + handleEvents(log, endpointAppContext) ); router.get( @@ -29,15 +35,24 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateChildren, options: { authRequired: true }, }, - handleChildren(log, indexPatternService) + handleChildren(log, endpointAppContext) + ); + + router.get( + { + path: '/api/endpoint/resolver/{id}/ancestry', + validate: validateAncestry, + options: { authRequired: true }, + }, + handleAncestry(log, endpointAppContext) ); router.get( { path: '/api/endpoint/resolver/{id}', - validate: validateLifecycle, + validate: validateTree, options: { authRequired: true }, }, - handleLifecycle(log, indexPatternService) + handleTree(log, endpointAppContext) ); } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/ancestry.ts b/x-pack/plugins/endpoint/server/routes/resolver/ancestry.ts new file mode 100644 index 0000000000000..6648dc5b9e493 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/ancestry.ts @@ -0,0 +1,39 @@ +/* + * 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 { RequestHandler, Logger } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { validateAncestry } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleAncestry( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler<TypeOf<typeof validateAncestry.params>, TypeOf<typeof validateAncestry.query>> { + return async (context, req, res) => { + const { + params: { id }, + query: { ancestors, legacyEndpointID: endpointID }, + } = req; + try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + + const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.ancestors(ancestors + 1); + + return res.ok({ + body: tree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/children.ts index 05b8f0b5f8608..bb18b29a4b947 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/children.ts @@ -4,86 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; -import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; -import { extractEntityID } from './utils/normalize'; -import { getPaginationParams } from './utils/pagination'; -import { LifecycleQuery } from './queries/lifecycle'; -import { ChildrenQuery } from './queries/children'; -import { IndexPatternRetriever } from '../../index_pattern'; - -interface ChildrenQueryParams { - after?: string; - limit: number; - /** - * legacyEndpointID is optional because there are two different types of identifiers: - * - * Legacy - * A legacy Entity ID is made up of the agent.id and unique_pid fields. The client will need to identify if - * it's looking at a legacy event and use those fields when making requests to the backend. The - * request would be /resolver/{id}?legacyEndpointID=<some uuid>and the {id} would be the unique_pid. - * - * Elastic Endpoint - * When interacting with the new form of data the client doesn't need the legacyEndpointID because it's already a - * part of the entityID in the new type of event. So for the same request the client would just hit resolver/{id} - * and the {id} would be entityID stored in the event's process.entity_id field. - */ - legacyEndpointID?: string; -} - -interface ChildrenPathParams { - id: string; -} - -export const validateChildren = { - params: schema.object({ id: schema.string() }), - query: schema.object({ - after: schema.maybe(schema.string()), - limit: schema.number({ defaultValue: 10, min: 1, max: 100 }), - legacyEndpointID: schema.maybe(schema.string()), - }), -}; +import { TypeOf } from '@kbn/config-schema'; +import { validateChildren } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; export function handleChildren( log: Logger, - indexRetriever: IndexPatternRetriever -): RequestHandler<ChildrenPathParams, ChildrenQueryParams> { + endpointAppContext: EndpointAppContext +): RequestHandler<TypeOf<typeof validateChildren.params>, TypeOf<typeof validateChildren.query>> { return async (context, req, res) => { const { params: { id }, - query: { limit, after, legacyEndpointID }, + query: { children, generations, afterChild, legacyEndpointID: endpointID }, } = req; try { - const pagination = getPaginationParams(limit, after); + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); const indexPattern = await indexRetriever.getEventIndexPattern(context); const client = context.core.elasticsearch.dataClient; - const childrenQuery = new ChildrenQuery(indexPattern, legacyEndpointID, pagination); - const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); - - // Retrieve the related child process events for a given process - const { total, results: events, nextCursor } = await childrenQuery.search(client, id); - const childIDs = events.map(extractEntityID); - - // Retrieve the lifecycle events for the child processes (e.g. started, terminated etc) - // this needs to fire after the above since we don't yet have the entity ids until we - // run the first query - const { results: lifecycleEvents } = await lifecycleQuery.search(client, ...childIDs); - // group all of the lifecycle events by the child process id - const lifecycleGroups = Object.values(_.groupBy(lifecycleEvents, extractEntityID)); - const children = lifecycleGroups.map(group => ({ lifecycle: group })); + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.children(children, generations, afterChild); return res.ok({ - body: { - children, - pagination: { - total, - next: nextCursor, - limit, - }, - }, + body: tree.render(), }); } catch (err) { log.warn(err); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/events.ts b/x-pack/plugins/endpoint/server/routes/resolver/events.ts new file mode 100644 index 0000000000000..a70a6e8d097d0 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/events.ts @@ -0,0 +1,38 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; +import { RequestHandler, Logger } from 'kibana/server'; +import { validateEvents } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { EndpointAppContext } from '../../types'; + +export function handleEvents( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler<TypeOf<typeof validateEvents.params>, TypeOf<typeof validateEvents.query>> { + return async (context, req, res) => { + const { + params: { id }, + query: { events, afterEvent, legacyEndpointID: endpointID }, + } = req; + try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + const client = context.core.elasticsearch.dataClient; + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await fetcher.events(events, afterEvent); + + return res.ok({ + body: tree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: err }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts deleted file mode 100644 index 6d155b79651a7..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import { schema } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; -import { extractParentEntityID } from './utils/normalize'; -import { LifecycleQuery } from './queries/lifecycle'; -import { ResolverEvent } from '../../../common/types'; -import { IndexPatternRetriever } from '../../index_pattern'; - -interface LifecycleQueryParams { - ancestors: number; - /** - * legacyEndpointID is optional because there are two different types of identifiers: - * - * Legacy - * A legacy Entity ID is made up of the agent.id and unique_pid fields. The client will need to identify if - * it's looking at a legacy event and use those fields when making requests to the backend. The - * request would be /resolver/{id}?legacyEndpointID=<some uuid>and the {id} would be the unique_pid. - * - * Elastic Endpoint - * When interacting with the new form of data the client doesn't need the legacyEndpointID because it's already a - * part of the entityID in the new type of event. So for the same request the client would just hit resolver/{id} - * and the {id} would be entityID stored in the event's process.entity_id field. - */ - legacyEndpointID?: string; -} - -interface LifecyclePathParams { - id: string; -} - -export const validateLifecycle = { - params: schema.object({ id: schema.string() }), - query: schema.object({ - ancestors: schema.number({ defaultValue: 0, min: 0, max: 10 }), - legacyEndpointID: schema.maybe(schema.string()), - }), -}; - -function getParentEntityID(results: ResolverEvent[]) { - return results.length === 0 ? undefined : extractParentEntityID(results[0]); -} - -export function handleLifecycle( - log: Logger, - indexRetriever: IndexPatternRetriever -): RequestHandler<LifecyclePathParams, LifecycleQueryParams> { - return async (context, req, res) => { - const { - params: { id }, - query: { ancestors, legacyEndpointID }, - } = req; - try { - const ancestorLifecycles = []; - const client = context.core.elasticsearch.dataClient; - const indexPattern = await indexRetriever.getEventIndexPattern(context); - const lifecycleQuery = new LifecycleQuery(indexPattern, legacyEndpointID); - const { results: processLifecycle } = await lifecycleQuery.search(client, id); - let nextParentID = getParentEntityID(processLifecycle); - - if (nextParentID) { - for (let i = 0; i < ancestors; i++) { - const { results: lifecycle } = await lifecycleQuery.search(client, nextParentID); - nextParentID = getParentEntityID(lifecycle); - - if (!nextParentID) { - break; - } - - ancestorLifecycles.push({ - lifecycle, - }); - } - } - - return res.ok({ - body: { - lifecycle: processLifecycle, - ancestors: ancestorLifecycles, - pagination: { - next: nextParentID || null, - ancestors, - }, - }, - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: err }); - } - }; -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts index b049439207e50..eba4e5581c136 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/base.ts @@ -4,10 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; import { IScopedClusterClient } from 'kibana/server'; -import { EndpointAppConstants } from '../../../../common/types'; -import { paginate, paginatedResults, PaginationParams } from '../utils/pagination'; +import { ResolverEvent } from '../../../../common/types'; +import { + paginate, + paginatedResults, + PaginationParams, + PaginatedResults, +} from '../utils/pagination'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; export abstract class ResolverQuery { constructor( @@ -16,22 +23,28 @@ export abstract class ResolverQuery { private readonly pagination?: PaginationParams ) {} - protected paginateBy(field: string, query: JsonObject) { - if (!this.pagination) { - return query; - } - return paginate(this.pagination, field, query); + protected paginateBy(tiebreaker: string, aggregator: string) { + return (query: JsonObject) => { + if (!this.pagination) { + return query; + } + return paginate(this.pagination, tiebreaker, aggregator, query); + }; } build(...ids: string[]) { if (this.endpointID) { - return this.legacyQuery(this.endpointID, ids, EndpointAppConstants.LEGACY_EVENT_INDEX_NAME); + return this.legacyQuery(this.endpointID, ids, legacyEventIndexPattern); } return this.query(ids, this.indexPattern); } async search(client: IScopedClusterClient, ...ids: string[]) { - return paginatedResults(await client.callAsCurrentUser('search', this.build(...ids))); + return this.postSearch(await client.callAsCurrentUser('search', this.build(...ids))); + } + + protected postSearch(response: SearchResponse<ResolverEvent>): PaginatedResults { + return paginatedResults(response); } protected abstract legacyQuery( diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts index e73053d53dee0..2a097e87c38b2 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { ChildrenQuery } from './children'; -import { EndpointAppConstants } from '../../../../common/types'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; export const fakeEventIndexPattern = 'events-endpoint-*'; @@ -12,7 +12,7 @@ describe('children events query', () => { it('generates the correct legacy queries', () => { const timestamp = new Date().getTime(); expect( - new ChildrenQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { + new ChildrenQuery(legacyEventIndexPattern, 'awesome-id', { size: 1, timestamp, eventID: 'foo', @@ -32,15 +32,28 @@ describe('children events query', () => { term: { 'event.category': 'process' }, }, { - term: { 'event.type': 'process_start' }, + term: { 'event.kind': 'event' }, + }, + { + bool: { + should: [ + { + term: { 'event.type': 'process_start' }, + }, + { + term: { 'event.action': 'fork_event' }, + }, + ], + }, }, ], }, }, aggs: { - total: { - value_count: { - field: 'endgame.serial_event_id', + totals: { + terms: { + field: 'endgame.unique_ppid', + size: 1, }, }, }, @@ -48,7 +61,7 @@ describe('children events query', () => { size: 1, sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], }, - index: EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, + index: legacyEventIndexPattern, }); }); @@ -67,20 +80,14 @@ describe('children events query', () => { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.parent.entity_id': ['baz'] }, - }, - { - terms: { 'process.parent.entity_id': ['baz'] }, - }, - ], - }, + terms: { 'process.parent.entity_id': ['baz'] }, }, { term: { 'event.category': 'process' }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.type': 'start' }, }, @@ -88,9 +95,10 @@ describe('children events query', () => { }, }, aggs: { - total: { - value_count: { - field: 'event.id', + totals: { + terms: { + field: 'process.parent.entity_id', + size: 1, }, }, }, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts index 6d084a0cf20e5..690c926d7e6d6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/children.ts @@ -7,8 +7,9 @@ import { ResolverQuery } from './base'; export class ChildrenQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string) { + const paginator = this.paginateBy('endgame.serial_event_id', 'endgame.unique_ppid'); return { - body: this.paginateBy('endgame.serial_event_id', { + body: paginator({ query: { bool: { filter: [ @@ -22,11 +23,19 @@ export class ChildrenQuery extends ResolverQuery { term: { 'event.category': 'process' }, }, { - // Corner case, we could only have a process_running or process_terminated - // so to solve this we'll probably want to either search for all of them and only return one if that's - // possible in elastic search or in memory pull out a single event to return - // https://github.com/elastic/endpoint-app-team/issues/168 - term: { 'event.type': 'process_start' }, + term: { 'event.kind': 'event' }, + }, + { + bool: { + should: [ + { + term: { 'event.type': 'process_start' }, + }, + { + term: { 'event.action': 'fork_event' }, + }, + ], + }, }, ], }, @@ -37,31 +46,22 @@ export class ChildrenQuery extends ResolverQuery { } protected query(entityIDs: string[], index: string) { + const paginator = this.paginateBy('event.id', 'process.parent.entity_id'); return { - body: this.paginateBy('event.id', { + body: paginator({ query: { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.parent.entity_id': entityIDs }, - }, - { - terms: { 'process.parent.entity_id': entityIDs }, - }, - ], - }, + terms: { 'process.parent.entity_id': entityIDs }, }, { term: { 'event.category': 'process' }, }, { - // Corner case, we could only have a process_running or process_terminated - // so to solve this we'll probably want to either search for all of them and only return one if that's - // possible in elastic search or in memory pull out a single event to return - // https://github.com/elastic/endpoint-app-team/issues/168 + term: { 'event.kind': 'event' }, + }, + { term: { 'event.type': 'start' }, }, ], diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts new file mode 100644 index 0000000000000..78e5ee9226581 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.test.ts @@ -0,0 +1,104 @@ +/* + * 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 { EventsQuery } from './events'; +import { fakeEventIndexPattern } from './children.test'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; + +describe('related events query', () => { + it('generates the correct legacy queries', () => { + const timestamp = new Date().getTime(); + expect( + new EventsQuery(legacyEventIndexPattern, 'awesome-id', { + size: 1, + timestamp, + eventID: 'foo', + }).build('5') + ).toStrictEqual({ + body: { + query: { + bool: { + filter: [ + { + terms: { 'endgame.unique_pid': ['5'] }, + }, + { + term: { 'agent.id': 'awesome-id' }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + aggs: { + totals: { + terms: { + field: 'endgame.unique_pid', + size: 1, + }, + }, + }, + search_after: [timestamp, 'foo'], + size: 1, + sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], + }, + index: legacyEventIndexPattern, + }); + }); + + it('generates the correct non-legacy queries', () => { + const timestamp = new Date().getTime(); + + expect( + new EventsQuery(fakeEventIndexPattern, undefined, { + size: 1, + timestamp, + eventID: 'bar', + }).build('baz') + ).toStrictEqual({ + body: { + query: { + bool: { + filter: [ + { + terms: { 'process.entity_id': ['baz'] }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + aggs: { + totals: { + terms: { + field: 'process.entity_id', + size: 1, + }, + }, + }, + search_after: [timestamp, 'bar'], + size: 1, + sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], + }, + index: fakeEventIndexPattern, + }); + }); +}); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts new file mode 100644 index 0000000000000..b622cb8a21111 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/events.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ResolverQuery } from './base'; +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; + +export class EventsQuery extends ResolverQuery { + protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { + const paginator = this.paginateBy('endgame.serial_event_id', 'endgame.unique_pid'); + return { + body: paginator({ + query: { + bool: { + filter: [ + { + terms: { 'endgame.unique_pid': uniquePIDs }, + }, + { + term: { 'agent.id': endpointID }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + }), + index, + }; + } + + protected query(entityIDs: string[], index: string): JsonObject { + const paginator = this.paginateBy('event.id', 'process.entity_id'); + return { + body: paginator({ + query: { + bool: { + filter: [ + { + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, + }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + }), + index, + }; + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/legacy_event_index_pattern.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/legacy_event_index_pattern.ts new file mode 100644 index 0000000000000..01e818c89b3ef --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/legacy_event_index_pattern.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Legacy events are stored in indices with endgame-* prefix + */ +export const legacyEventIndexPattern = 'endgame-*'; diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts index 8a3955706b278..296135af83b72 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.test.ts @@ -3,15 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EndpointAppConstants } from '../../../../common/types'; + import { LifecycleQuery } from './lifecycle'; import { fakeEventIndexPattern } from './children.test'; +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; describe('lifecycle query', () => { it('generates the correct legacy queries', () => { - expect( - new LifecycleQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id').build('5') - ).toStrictEqual({ + expect(new LifecycleQuery(legacyEventIndexPattern, 'awesome-id').build('5')).toStrictEqual({ body: { query: { bool: { @@ -22,15 +21,19 @@ describe('lifecycle query', () => { { term: { 'agent.id': 'awesome-id' }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.category': 'process' }, }, ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, - index: EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, + index: legacyEventIndexPattern, }); }); @@ -41,16 +44,10 @@ describe('lifecycle query', () => { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': ['baz'] }, - }, - { - terms: { 'process.entity_id': ['baz'] }, - }, - ], - }, + terms: { 'process.entity_id': ['baz'] }, + }, + { + term: { 'event.kind': 'event' }, }, { term: { 'event.category': 'process' }, @@ -58,6 +55,7 @@ describe('lifecycle query', () => { ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index: fakeEventIndexPattern, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts index 290c601e0e9d8..e775b0cf9b6d2 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/lifecycle.ts @@ -6,7 +6,6 @@ import { ResolverQuery } from './base'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; -// consider limiting the response size to a reasonable value in case we have a bunch of lifecycle events export class LifecycleQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { return { @@ -20,12 +19,16 @@ export class LifecycleQuery extends ResolverQuery { { term: { 'agent.id': endpointID }, }, + { + term: { 'event.kind': 'event' }, + }, { term: { 'event.category': 'process' }, }, ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index, @@ -39,16 +42,10 @@ export class LifecycleQuery extends ResolverQuery { bool: { filter: [ { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': entityIDs }, - }, - { - terms: { 'process.entity_id': entityIDs }, - }, - ], - }, + terms: { 'process.entity_id': entityIDs }, + }, + { + term: { 'event.kind': 'event' }, }, { term: { 'event.category': 'process' }, @@ -56,6 +53,7 @@ export class LifecycleQuery extends ResolverQuery { ], }, }, + size: 10000, sort: [{ '@timestamp': 'asc' }], }, index, diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts deleted file mode 100644 index 5caef935ce621..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.test.ts +++ /dev/null @@ -1,105 +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 { RelatedEventsQuery } from './related_events'; -import { EndpointAppConstants } from '../../../../common/types'; -import { fakeEventIndexPattern } from './children.test'; - -describe('related events query', () => { - it('generates the correct legacy queries', () => { - const timestamp = new Date().getTime(); - expect( - new RelatedEventsQuery(EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, 'awesome-id', { - size: 1, - timestamp, - eventID: 'foo', - }).build('5') - ).toStrictEqual({ - body: { - query: { - bool: { - filter: [ - { - terms: { 'endgame.unique_pid': ['5'] }, - }, - { - term: { 'agent.id': 'awesome-id' }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - aggs: { - total: { - value_count: { - field: 'endgame.serial_event_id', - }, - }, - }, - search_after: [timestamp, 'foo'], - size: 1, - sort: [{ '@timestamp': 'asc' }, { 'endgame.serial_event_id': 'asc' }], - }, - index: EndpointAppConstants.LEGACY_EVENT_INDEX_NAME, - }); - }); - - it('generates the correct non-legacy queries', () => { - const timestamp = new Date().getTime(); - - expect( - new RelatedEventsQuery(fakeEventIndexPattern, undefined, { - size: 1, - timestamp, - eventID: 'bar', - }).build('baz') - ).toStrictEqual({ - body: { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': ['baz'] }, - }, - { - terms: { 'process.entity_id': ['baz'] }, - }, - ], - }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - aggs: { - total: { - value_count: { - field: 'event.id', - }, - }, - }, - search_after: [timestamp, 'bar'], - size: 1, - sort: [{ '@timestamp': 'asc' }, { 'event.id': 'asc' }], - }, - index: fakeEventIndexPattern, - }); - }); -}); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts deleted file mode 100644 index cc5afe8face8d..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/queries/related_events.ts +++ /dev/null @@ -1,69 +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 { ResolverQuery } from './base'; -import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; - -export class RelatedEventsQuery extends ResolverQuery { - protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { - return { - body: this.paginateBy('endgame.serial_event_id', { - query: { - bool: { - filter: [ - { - terms: { 'endgame.unique_pid': uniquePIDs }, - }, - { - term: { 'agent.id': endpointID }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - }), - index, - }; - } - - protected query(entityIDs: string[], index: string): JsonObject { - return { - body: this.paginateBy('event.id', { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - terms: { 'endpoint.process.entity_id': entityIDs }, - }, - { - terms: { 'process.entity_id': entityIDs }, - }, - ], - }, - }, - { - bool: { - must_not: { - term: { 'event.category': 'process' }, - }, - }, - }, - ], - }, - }, - }), - index, - }; - } -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts new file mode 100644 index 0000000000000..17a158aec7cf5 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.test.ts @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { legacyEventIndexPattern } from './legacy_event_index_pattern'; +import { StatsQuery } from './stats'; +import { fakeEventIndexPattern } from './children.test'; + +describe('stats query', () => { + it('generates the correct legacy queries', () => { + expect(new StatsQuery(legacyEventIndexPattern, 'awesome-id').build('5')).toStrictEqual({ + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + 'agent.id': 'awesome-id', + }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'event.kind': 'event', + }, + }, + { + terms: { + 'endgame.unique_pid': ['5'], + }, + }, + { + bool: { + must_not: { + term: { + 'event.category': 'process', + }, + }, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { + term: { + 'event.kind': 'alert', + }, + }, + { + terms: { + 'endgame.data.alert_details.acting_process.unique_pid': ['5'], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { + term: { + 'event.kind': 'alert', + }, + }, + aggs: { + ids: { + terms: { + field: 'endgame.data.alert_details.acting_process.unique_pid', + }, + }, + }, + }, + events: { + filter: { + term: { + 'event.kind': 'event', + }, + }, + aggs: { + ids: { + terms: { + field: 'endgame.unique_pid', + }, + }, + }, + }, + }, + }, + index: legacyEventIndexPattern, + }); + }); + + it('generates the correct non-legacy queries', () => { + expect(new StatsQuery(fakeEventIndexPattern).build('baz')).toStrictEqual({ + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + 'process.entity_id': ['baz'], + }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { + term: { + 'event.kind': 'event', + }, + }, + { + bool: { + must_not: { + term: { + 'event.category': 'process', + }, + }, + }, + }, + ], + }, + }, + { + term: { + 'event.kind': 'alert', + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { + term: { + 'event.kind': 'alert', + }, + }, + aggs: { + ids: { + terms: { + field: 'process.entity_id', + }, + }, + }, + }, + events: { + filter: { + term: { + 'event.kind': 'event', + }, + }, + aggs: { + ids: { + terms: { + field: 'process.entity_id', + }, + }, + }, + }, + }, + }, + index: fakeEventIndexPattern, + }); + }); +}); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts new file mode 100644 index 0000000000000..7db3ab2b0cb1f --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/queries/stats.ts @@ -0,0 +1,147 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { ResolverQuery } from './base'; +import { ResolverEvent } from '../../../../common/types'; +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; +import { PaginatedResults } from '../utils/pagination'; + +export class StatsQuery extends ResolverQuery { + protected postSearch(response: SearchResponse<ResolverEvent>): PaginatedResults { + const alerts = response.aggregations.alerts.ids.buckets.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); + const events = response.aggregations.events.ids.buckets.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); + return { + totals: {}, + results: [], + extras: { + alerts, + events, + }, + }; + } + + protected legacyQuery(endpointID: string, uniquePIDs: string[], index: string): JsonObject { + return { + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { 'agent.id': endpointID }, + }, + { + bool: { + should: [ + { + bool: { + filter: [ + { term: { 'event.kind': 'event' } }, + { terms: { 'endgame.unique_pid': uniquePIDs } }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + { + bool: { + filter: [ + { term: { 'event.kind': 'alert' } }, + { + terms: { + 'endgame.data.alert_details.acting_process.unique_pid': uniquePIDs, + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { term: { 'event.kind': 'alert' } }, + aggs: { + ids: { terms: { field: 'endgame.data.alert_details.acting_process.unique_pid' } }, + }, + }, + events: { + filter: { term: { 'event.kind': 'event' } }, + aggs: { + ids: { terms: { field: 'endgame.unique_pid' } }, + }, + }, + }, + }, + index, + }; + } + + protected query(entityIDs: string[], index: string): JsonObject { + return { + body: { + size: 0, + query: { + bool: { + filter: [ + { terms: { 'process.entity_id': entityIDs } }, + { + bool: { + should: [ + { + bool: { + filter: [ + { term: { 'event.kind': 'event' } }, + { + bool: { + must_not: { + term: { 'event.category': 'process' }, + }, + }, + }, + ], + }, + }, + { term: { 'event.kind': 'alert' } }, + ], + }, + }, + ], + }, + }, + aggs: { + alerts: { + filter: { term: { 'event.kind': 'alert' } }, + aggs: { + ids: { terms: { field: 'process.entity_id' } }, + }, + }, + events: { + filter: { term: { 'event.kind': 'event' } }, + aggs: { + ids: { terms: { field: 'process.entity_id' } }, + }, + }, + }, + }, + index, + }; + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts deleted file mode 100644 index 46e205464f53c..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts +++ /dev/null @@ -1,76 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler, Logger } from 'kibana/server'; -import { getPaginationParams } from './utils/pagination'; -import { RelatedEventsQuery } from './queries/related_events'; -import { IndexPatternRetriever } from '../../index_pattern'; - -interface RelatedEventsQueryParams { - after?: string; - limit: number; - /** - * legacyEndpointID is optional because there are two different types of identifiers: - * - * Legacy - * A legacy Entity ID is made up of the agent.id and unique_pid fields. The client will need to identify if - * it's looking at a legacy event and use those fields when making requests to the backend. The - * request would be /resolver/{id}?legacyEndpointID=<some uuid>and the {id} would be the unique_pid. - * - * Elastic Endpoint - * When interacting with the new form of data the client doesn't need the legacyEndpointID because it's already a - * part of the entityID in the new type of event. So for the same request the client would just hit resolver/{id} - * and the {id} would be entityID stored in the event's process.entity_id field. - */ - legacyEndpointID?: string; -} - -interface RelatedEventsPathParams { - id: string; -} - -export const validateRelatedEvents = { - params: schema.object({ id: schema.string() }), - query: schema.object({ - after: schema.maybe(schema.string()), - limit: schema.number({ defaultValue: 100, min: 1, max: 1000 }), - legacyEndpointID: schema.maybe(schema.string()), - }), -}; - -export function handleRelatedEvents( - log: Logger, - indexRetriever: IndexPatternRetriever -): RequestHandler<RelatedEventsPathParams, RelatedEventsQueryParams> { - return async (context, req, res) => { - const { - params: { id }, - query: { limit, after, legacyEndpointID }, - } = req; - try { - const pagination = getPaginationParams(limit, after); - - const client = context.core.elasticsearch.dataClient; - const indexPattern = await indexRetriever.getEventIndexPattern(context); - // Retrieve the related non-process events for a given process - const relatedEventsQuery = new RelatedEventsQuery(indexPattern, legacyEndpointID, pagination); - const relatedEvents = await relatedEventsQuery.search(client, id); - - const { total, results: events, nextCursor } = relatedEvents; - - return res.ok({ - body: { - events, - pagination: { total, next: nextCursor, limit }, - }, - }); - } catch (err) { - log.warn(err); - return res.internalError({ body: err }); - } - }; -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/tree.ts b/x-pack/plugins/endpoint/server/routes/resolver/tree.ts new file mode 100644 index 0000000000000..25f15586341d5 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/tree.ts @@ -0,0 +1,53 @@ +/* + * 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 { RequestHandler, Logger } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { validateTree } from '../../../common/schema/resolver'; +import { Fetcher } from './utils/fetch'; +import { Tree } from './utils/tree'; +import { EndpointAppContext } from '../../types'; + +export function handleTree( + log: Logger, + endpointAppContext: EndpointAppContext +): RequestHandler<TypeOf<typeof validateTree.params>, TypeOf<typeof validateTree.query>> { + return async (context, req, res) => { + const { + params: { id }, + query: { + children, + generations, + ancestors, + events, + afterEvent, + afterChild, + legacyEndpointID: endpointID, + }, + } = req; + try { + const client = context.core.elasticsearch.dataClient; + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); + const indexPattern = await indexRetriever.getEventIndexPattern(context); + + const fetcher = new Fetcher(client, id, indexPattern, endpointID); + const tree = await Tree.merge( + fetcher.children(children, generations, afterChild), + fetcher.ancestors(ancestors + 1), + fetcher.events(events, afterEvent) + ); + + const enrichedTree = await fetcher.stats(tree); + + return res.ok({ + body: enrichedTree.render(), + }); + } catch (err) { + log.warn(err); + return res.internalError({ body: 'Error retrieving tree.' }); + } + }; +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts new file mode 100644 index 0000000000000..7315b4ee6c618 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/fetch.ts @@ -0,0 +1,123 @@ +/* + * 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 { IScopedClusterClient } from 'kibana/server'; +import { entityId, parentEntityId } from '../../../../common/models/event'; +import { getPaginationParams } from './pagination'; +import { Tree } from './tree'; +import { LifecycleQuery } from '../queries/lifecycle'; +import { ChildrenQuery } from '../queries/children'; +import { EventsQuery } from '../queries/events'; +import { StatsQuery } from '../queries/stats'; + +export class Fetcher { + constructor( + private readonly client: IScopedClusterClient, + private readonly id: string, + private readonly indexPattern: string, + private readonly endpointID?: string + ) {} + + public async ancestors(limit: number): Promise<Tree> { + const tree = new Tree(this.id); + await this.doAncestors(tree, this.id, this.id, limit); + return tree; + } + + public async children(limit: number, generations: number, after?: string): Promise<Tree> { + const tree = new Tree(this.id); + await this.doChildren(tree, [this.id], limit, generations, after); + return tree; + } + + public async events(limit: number, after?: string): Promise<Tree> { + const tree = new Tree(this.id); + await this.doEvents(tree, limit, after); + return tree; + } + + public async stats(tree: Tree): Promise<Tree> { + await this.doStats(tree); + return tree; + } + + private async doAncestors(tree: Tree, curNode: string, previousNode: string, levels: number) { + if (levels === 0) { + tree.setNextAncestor(curNode); + return; + } + + const query = new LifecycleQuery(this.indexPattern, this.endpointID); + const { results } = await query.search(this.client, curNode); + + if (results.length === 0) { + tree.setNextAncestor(null); + return; + } + tree.addAncestor(previousNode, ...results); + + const next = parentEntityId(results[0]); + if (next !== undefined) { + await this.doAncestors(tree, next, curNode, levels - 1); + } + } + + private async doEvents(tree: Tree, limit: number, after?: string) { + const query = new EventsQuery( + this.indexPattern, + this.endpointID, + getPaginationParams(limit, after) + ); + + const { totals, results } = await query.search(this.client, this.id); + tree.addEvent(...results); + tree.paginateEvents(totals, results); + if (results.length === 0) tree.setNextEvent(null); + } + + private async doChildren( + tree: Tree, + ids: string[], + limit: number, + levels: number, + after?: string + ) { + if (levels === 0 || ids.length === 0) return; + + const childrenQuery = new ChildrenQuery( + this.indexPattern, + this.endpointID, + getPaginationParams(limit, after) + ); + const lifecycleQuery = new LifecycleQuery(this.indexPattern, this.endpointID); + + const { totals, results } = await childrenQuery.search(this.client, ...ids); + if (results.length === 0) { + tree.markLeafNode(...ids); + return; + } + + const childIDs = results.map(entityId); + const children = (await lifecycleQuery.search(this.client, ...childIDs)).results; + + tree.addChild(...children); + tree.paginateChildren(totals, results); + tree.markLeafNode(...childIDs); + + await this.doChildren(tree, childIDs, limit * limit, levels - 1); + } + + private async doStats(tree: Tree) { + const statsQuery = new StatsQuery(this.indexPattern, this.endpointID); + const ids = tree.ids(); + const { extras } = await statsQuery.search(this.client, ...ids); + const alerts = extras?.alerts || {}; + const events = extras?.events || {}; + ids.forEach(id => { + tree.addStats(id, { totalAlerts: alerts[id] || 0, totalEvents: events[id] || 0 }); + }); + } +} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts deleted file mode 100644 index 6d5ac8efdc1da..0000000000000 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/normalize.ts +++ /dev/null @@ -1,30 +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 { ResolverEvent } from '../../../../common/types'; -import { isLegacyEvent } from '../../../../common/models/event'; - -export function extractEventID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - return String(event.endgame.serial_event_id); - } - return event.event.id; -} - -export function extractEntityID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - return String(event.endgame.unique_pid); - } - return event.process.entity_id; -} - -export function extractParentEntityID(event: ResolverEvent) { - if (isLegacyEvent(event)) { - const ppid = event.endgame.unique_ppid; - return ppid && String(ppid); // if unique_ppid is undefined return undefined - } - return event.process.parent?.entity_id; -} diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts index 5a64f3ff9ddb6..20249b81660bb 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/pagination.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverEvent } from '../../../../common/types'; -import { extractEventID } from './normalize'; +import { entityId } from '../../../../common/models/event'; import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; export interface PaginationParams { @@ -15,12 +15,19 @@ export interface PaginationParams { eventID?: string; } +export interface PaginatedResults { + totals: Record<string, number>; + results: ResolverEvent[]; + // content holder for any other extra aggregation counts + extras?: Record<string, Record<string, number>>; +} + interface PaginationCursor { timestamp: number; eventID: string; } -function urlEncodeCursor(data: PaginationCursor) { +function urlEncodeCursor(data: PaginationCursor): string { const value = JSON.stringify(data); return Buffer.from(value, 'utf8') .toString('base64') @@ -56,10 +63,16 @@ export function getPaginationParams(limit: number, after?: string): PaginationPa return { size: limit }; } -export function paginate(pagination: PaginationParams, field: string, query: JsonObject) { +export function paginate( + pagination: PaginationParams, + tiebreaker: string, + aggregator: string, + query: JsonObject +): JsonObject { const { size, timestamp, eventID } = pagination; - query.sort = [{ '@timestamp': 'asc' }, { [field]: 'asc' }]; - query.aggs = { total: { value_count: { field } } }; + query.sort = [{ '@timestamp': 'asc' }, { [tiebreaker]: 'asc' }]; + query.aggs = query.aggs || {}; + query.aggs = Object.assign({}, query.aggs, { totals: { terms: { field: aggregator, size } } }); query.size = size; if (timestamp && eventID) { query.search_after = [timestamp, eventID] as Array<number | string>; @@ -67,25 +80,28 @@ export function paginate(pagination: PaginationParams, field: string, query: Jso return query; } -export function paginatedResults( - response: SearchResponse<ResolverEvent> -): { total: number; results: ResolverEvent[]; nextCursor: string | null } { - const total = response.aggregations?.total?.value || 0; - if (response.hits.hits.length === 0) { - return { total, results: [], nextCursor: null }; +export function buildPaginationCursor(total: number, results: ResolverEvent[]): string | null { + if (total > results.length && results.length > 0) { + const lastResult = results[results.length - 1]; + const cursor = { + timestamp: lastResult['@timestamp'], + eventID: entityId(lastResult), + }; + return urlEncodeCursor(cursor); } + return null; +} - const results: ResolverEvent[] = []; - for (const hit of response.hits.hits) { - results.push(hit._source); +export function paginatedResults(response: SearchResponse<ResolverEvent>): PaginatedResults { + if (response.hits.hits.length === 0) { + return { totals: {}, results: [] }; } - // results will be at least 1 because of length check at the top of the function - const next = results[results.length - 1]; - const cursor = { - timestamp: next['@timestamp'], - eventID: extractEventID(next), - }; + const totals = response.aggregations?.totals?.buckets?.reduce( + (cummulative: any, bucket: any) => ({ ...cummulative, [bucket.key]: bucket.doc_count }), + {} + ); - return { total, results, nextCursor: urlEncodeCursor(cursor) }; + const results = response.hits.hits.map(hit => hit._source); + return { totals, results }; } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts b/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts new file mode 100644 index 0000000000000..5a55c23b90873 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/resolver/utils/tree.ts @@ -0,0 +1,230 @@ +/* + * 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 _ from 'lodash'; +import { + ResolverEvent, + ResolverNode, + ResolverNodeStats, + ResolverNodePagination, +} from '../../../../common/types'; +import { entityId, parentEntityId } from '../../../../common/models/event'; +import { buildPaginationCursor } from './pagination'; + +type ExtractFunction = (event: ResolverEvent) => string | undefined; + +function createNode(id: string): ResolverNode { + return { id, children: [], pagination: {}, events: [], lifecycle: [] }; +} +/** + * This class aids in constructing a tree of process events. It works in the following way: + * + * 1. We construct a tree structure starting with the root node for the event we're requesting. + * 2. We leverage the ability to pass hashes and arrays by reference to construct a fast cache of + * process identifiers that updates the tree structure as we push values into the cache. + * + * When we query a single level of results for child process events we have a flattened, sorted result + * list that we need to add into a constructed tree. We also need to signal in an API response whether + * or not there are more child processes events that we have not yet retrieved, and, if so, for what parent + * process. So, at the end of our tree construction we have a relational layout of the events with no + * pagination information for the given parent nodes. In order to actually construct both the tree and + * insert the pagination information we basically do the following: + * + * 1. Using a terms aggregation query, we return an approximate roll-up of the number of child process + * "creation" events, this gives us an estimation of the number of associated children per parent + * 2. We feed these child process creation event "unique identifiers" (basically a process.entity_id) + * into a second query to get the current state of the process via its "lifecycle" events. + * 3. We construct the tree above with the "lifecycle" events. + * 4. Using the terms query results, we mark each non-leaf node with the number of expected children, if our + * tree has less children than expected, we create a pagination cursor to indicate "we have a truncated set + * of values". + * 5. We mark each leaf node (the last level of the tree we're constructing) with a "null" for the expected + * number of children to indicate "we have not yet attempted to get any children". + * + * Following this scheme, we use exactly 2 queries per level of children that we return--one for the pagination + * and one for the lifecycle events of the processes. The downside to this is that we need to dynamically expand + * the number of documents we can retrieve per level due to the exponential fanout of child processes, + * what this means is that noisy neighbors for a given level may hide other child process events that occur later + * temporally in the same level--so, while a heavily forking process might get shown, maybe the actually malicious + * event doesn't show up in the tree at the beginning. + */ + +export class Tree { + protected cache: Map<string, ResolverNode>; + protected root: ResolverNode; + protected id: string; + + constructor(id: string) { + const root = createNode(id); + this.id = id; + this.cache = new Map(); + this.root = root; + this.cache.set(id, root); + } + + public render(): ResolverNode { + return this.root; + } + + public ids(): string[] { + return [...this.cache.keys()]; + } + + public static async merge( + childrenPromise: Promise<Tree>, + ancestorsPromise: Promise<Tree>, + eventsPromise: Promise<Tree> + ): Promise<Tree> { + const [children, ancestors, events] = await Promise.all([ + childrenPromise, + ancestorsPromise, + eventsPromise, + ]); + + /* + * we only allow for merging when we have partial trees that + * represent the same root node + */ + const rootID = children.id; + if (rootID !== ancestors.id || rootID !== events.id) { + throw new Error('cannot merge trees with different roots'); + } + + Object.entries(ancestors.cache).forEach(([id, node]) => { + if (rootID !== id) { + children.cache.set(id, node); + } + }); + + children.root.lifecycle = ancestors.root.lifecycle; + children.root.ancestors = ancestors.root.ancestors; + children.root.events = events.root.events; + + Object.assign(children.root.pagination, ancestors.root.pagination, events.root.pagination); + + return children; + } + + public addEvent(...events: ResolverEvent[]): void { + events.forEach(event => { + const id = entityId(event); + + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.events.push(event); + } + }); + } + + public addAncestor(id: string, ...events: ResolverEvent[]): void { + events.forEach(event => { + const ancestorID = entityId(event); + if (this.cache.get(ancestorID) === undefined) { + const newParent = createNode(ancestorID); + this.cache.set(ancestorID, newParent); + if (!this.root.ancestors) { + this.root.ancestors = []; + } + this.root.ancestors.push(newParent); + } + const currentAncestor = this.cache.get(ancestorID); + if (currentAncestor !== undefined) { + currentAncestor.lifecycle.push(event); + } + }); + } + + public addStats(id: string, stats: ResolverNodeStats): void { + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.stats = stats; + } + } + + public setNextAncestor(next: string | null): void { + this.root.pagination.nextAncestor = next; + } + + public setNextEvent(next: string | null): void { + this.root.pagination.nextEvent = next; + } + + public setNextAlert(next: string | null): void { + this.root.pagination.nextAlert = next; + } + + public addChild(...events: ResolverEvent[]): void { + events.forEach(event => { + const id = entityId(event); + const parentID = parentEntityId(event); + + this.ensureCache(parentID); + let currentNode = this.cache.get(id); + + if (currentNode === undefined) { + currentNode = createNode(id); + this.cache.set(id, currentNode); + if (parentID !== undefined) { + const parentNode = this.cache.get(parentID); + if (parentNode !== undefined) { + parentNode.children.push(currentNode); + } + } + } + currentNode.lifecycle.push(event); + }); + } + + public markLeafNode(...ids: string[]): void { + ids.forEach(id => { + this.ensureCache(id); + const currentNode = this.cache.get(id); + if (currentNode !== undefined && !currentNode.pagination.nextChild) { + currentNode.pagination.nextChild = null; + } + }); + } + + public paginateEvents(totals: Record<string, number>, events: ResolverEvent[]): void { + return this.paginate(entityId, 'nextEvent', totals, events); + } + + public paginateChildren(totals: Record<string, number>, children: ResolverEvent[]): void { + return this.paginate(parentEntityId, 'nextChild', totals, children); + } + + private paginate( + grouper: ExtractFunction, + attribute: keyof ResolverNodePagination, + totals: Record<string, number>, + records: ResolverEvent[] + ): void { + const grouped = _.groupBy(records, grouper); + Object.entries(totals).forEach(([id, total]) => { + if (this.cache.get(id) !== undefined) { + if (grouped[id]) { + /* + * if we have any results, attempt to build a pagination cursor, the function + * below hands back a null value if no cursor is necessary because we have + * all of the records. + */ + const currentNode = this.cache.get(id); + if (currentNode !== undefined) { + currentNode.pagination[attribute] = buildPaginationCursor(total, grouped[id]); + } + } + } + }); + } + + private ensureCache(id: string | undefined): void { + if (id === undefined || this.cache.get(id) === undefined) { + throw new Error('dangling node'); + } + } +} diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts index 46a23060339f4..dfa5950adba5c 100644 --- a/x-pack/plugins/endpoint/server/types.ts +++ b/x-pack/plugins/endpoint/server/types.ts @@ -5,13 +5,17 @@ */ import { LoggerFactory } from 'kibana/server'; import { EndpointConfigType } from './config'; -import { IndexPatternRetriever } from './index_pattern'; +import { EndpointAppContextService } from './endpoint_app_context_services'; /** * The context for Endpoint apps. */ export interface EndpointAppContext { - indexPatternRetriever: IndexPatternRetriever; logFactory: LoggerFactory; config(): Promise<EndpointConfigType>; + + /** + * Object readiness is tied to plugin start method + */ + service: EndpointAppContextService; } diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index 38364033cb70b..941dedc3d1093 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -274,10 +274,16 @@ PUT _ilm/policy/event_log_policy "hot": { "actions": { "rollover": { - "max_size": "5GB", + "max_size": "50GB", "max_age": "30d" } } + }, + "delete": { + "min_age": "90d", + "actions": { + "delete": {} + } } } } @@ -285,10 +291,11 @@ PUT _ilm/policy/event_log_policy ``` This means that ILM would "rollover" the current index, say -`.kibana-event-log-000001` by creating a new index `.kibana-event-log-000002`, +`.kibana-event-log-8.0.0-000001` by creating a new index `.kibana-event-log-8.0.0-000002`, which would "inherit" everything from the index template, and then ILM will set the write index of the the alias to the new index. This would happen -when the original index grew past 5 GB, or was created more than 30 days ago. +when the original index grew past 50 GB, or was created more than 30 days ago. +After rollover, the indices will be removed after 90 days to avoid disks to fill up. For more relevant information on ILM, see: [getting started with ILM doc][] and [write index alias behavior][]: diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index 9c1dff60f9727..0a858969c4f6a 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -41,6 +41,10 @@ }, "end": { "type": "date" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" } } }, @@ -82,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 5e93f320c009f..57fe90a8e876e 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -41,6 +41,7 @@ export const EventSchema = schema.maybe( start: ecsDate(), duration: ecsNumber(), end: ecsDate(), + outcome: ecsString(), }) ), error: schema.maybe( @@ -64,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 de3c9d631fbca..fd149d132031e 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', @@ -53,10 +58,12 @@ exports.EcsEventLogProperties = [ 'event.start', 'event.duration', 'event.end', + 'event.outcome', // optional, but one of failure, success, unknown 'error.message', '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 986486902c3fa..66c16d0ddf383 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 @@ -42,6 +42,8 @@ describe('indexDocument', () => { }); describe('doesIlmPolicyExist', () => { + // ElasticsearchError can be a bit random in shape, we need an any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any const notFoundError = new Error('Not found') as any; notFoundError.statusCode = 404; @@ -187,6 +189,8 @@ describe('createIndex', () => { }); test(`shouldn't throw when an error of type resource_already_exists_exception is thrown`, async () => { + // ElasticsearchError can be a bit random in shape, we need an any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any const err = new Error('Already exists') as any; err.body = { error: { @@ -222,7 +226,7 @@ describe('queryEventsBySavedObject', () => { body: { from: 0, size: 10, - sort: { 'event.start': { order: 'asc' } }, + sort: { '@timestamp': { order: 'asc' } }, query: { bool: { must: [ @@ -232,6 +236,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -315,6 +326,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -336,7 +354,7 @@ describe('queryEventsBySavedObject', () => { }, { range: { - 'event.start': { + '@timestamp': { gte: start, }, }, @@ -384,6 +402,13 @@ describe('queryEventsBySavedObject', () => { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: 'primary', + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -405,14 +430,14 @@ describe('queryEventsBySavedObject', () => { }, { range: { - 'event.start': { + '@timestamp': { gte: start, }, }, }, { range: { - 'event.end': { + '@timestamp': { lte: end, }, }, 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 409bb2d00e161..c0ff87234c09d 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 @@ -5,8 +5,9 @@ */ 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<ClusterClient, 'callAsInternalUser' | 'asScoped'>; @@ -33,8 +34,8 @@ export class ClusterClientAdapter { this.clusterClientPromise = opts.clusterClientPromise; } - public async indexDocument(doc: any): Promise<void> { - await this.callEs('index', doc); + public async indexDocument(doc: unknown): Promise<void> { + await this.callEs<ReturnType<Client['index']>>('index', doc); } public async doesIlmPolicyExist(policyName: string): Promise<boolean> { @@ -51,7 +52,7 @@ export class ClusterClientAdapter { return true; } - public async createIlmPolicy(policyName: string, policy: any): Promise<void> { + public async createIlmPolicy(policyName: string, policy: unknown): Promise<void> { const request = { method: 'PUT', path: `_ilm/policy/${policyName}`, @@ -67,21 +68,27 @@ export class ClusterClientAdapter { public async doesIndexTemplateExist(name: string): Promise<boolean> { let result; try { - result = await this.callEs('indices.existsTemplate', { name }); + result = await this.callEs<ReturnType<Client['indices']['existsTemplate']>>( + 'indices.existsTemplate', + { name } + ); } catch (err) { throw new Error(`error checking existance of index template: ${err.message}`); } return result as boolean; } - public async createIndexTemplate(name: string, template: any): Promise<void> { + public async createIndexTemplate(name: string, template: unknown): Promise<void> { const addTemplateParams = { name, create: true, body: template, }; try { - await this.callEs('indices.putTemplate', addTemplateParams); + await this.callEs<ReturnType<Client['indices']['putTemplate']>>( + 'indices.putTemplate', + addTemplateParams + ); } catch (err) { // The error message doesn't have a type attribute we can look to guarantee it's due // to the template already existing (only long message) so we'll check ourselves to see @@ -97,16 +104,19 @@ export class ClusterClientAdapter { public async doesAliasExist(name: string): Promise<boolean> { let result; try { - result = await this.callEs('indices.existsAlias', { name }); + result = await this.callEs<ReturnType<Client['indices']['existsAlias']>>( + 'indices.existsAlias', + { name } + ); } catch (err) { throw new Error(`error checking existance of initial index: ${err.message}`); } return result as boolean; } - public async createIndex(name: string, body: any = {}): Promise<void> { + public async createIndex(name: string, body: unknown = {}): Promise<void> { try { - await this.callEs('indices.create', { + await this.callEs<ReturnType<Client['indices']['create']>>('indices.create', { index: name, body, }); @@ -125,12 +135,12 @@ export class ClusterClientAdapter { ): Promise<QueryEventsBySavedObjectResult> { try { const { - hits: { - hits, - total: { value: total }, - }, - } = await this.callEs('search', { + hits: { hits, total }, + }: SearchResponse<unknown> = await this.callEs('search', { index, + // The SearchResponse type only supports total as an int, + // so we're forced to explicitly request that it return as an int + rest_total_hits_as_int: true, body: { size: perPage, from: (page - 1) * perPage, @@ -145,6 +155,13 @@ export class ClusterClientAdapter { query: { bool: { must: [ + { + term: { + 'kibana.saved_objects.rel': { + value: SAVED_OBJECT_REL_PRIMARY, + }, + }, + }, { term: { 'kibana.saved_objects.type': { @@ -166,14 +183,14 @@ export class ClusterClientAdapter { }, start && { range: { - 'event.start': { + '@timestamp': { gte: start, }, }, }, end && { range: { - 'event.end': { + '@timestamp': { lte: end, }, }, @@ -189,7 +206,7 @@ export class ClusterClientAdapter { page, per_page: perPage, total, - data: hits.map((hit: any) => hit._source) as IEvent[], + data: hits.map(hit => hit._source) as IEvent[], }; } catch (err) { throw new Error( @@ -198,13 +215,15 @@ export class ClusterClientAdapter { } } - private async callEs(operation: string, body?: any): Promise<any> { + // We have a common problem typing ES-DSL Queries + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async callEs<ESQueryResult = unknown>(operation: string, body?: any) { try { this.debug(`callEs(${operation}) calls:`, body); const clusterClient = await this.clusterClientPromise; const result = await clusterClient.callAsInternalUser(operation, body); this.debug(`callEs(${operation}) result:`, result); - return result; + return result as ESQueryResult; } catch (err) { this.debug(`callEs(${operation}) error:`, { message: err.message, @@ -214,7 +233,7 @@ export class ClusterClientAdapter { } } - private debug(message: string, object?: any) { + private debug(message: string, object?: unknown) { const objectString = object == null ? '' : JSON.stringify(object); this.logger.debug(`esContext: ${message} ${objectString}`); } diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index 982454e671008..91b3db554964f 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -9,7 +9,7 @@ import mappings from '../../generated/mappings.json'; // returns the body of an index template used in an ES indices.putTemplate call export function getIndexTemplate(esNames: EsNames) { - const indexTemplateBody: any = { + const indexTemplateBody = { index_patterns: [esNames.indexPatternWithVersion], settings: { number_of_shards: 1, @@ -31,12 +31,18 @@ export function getIlmPolicy() { hot: { actions: { rollover: { - max_size: '5GB', + max_size: '50GB', max_age: '30d', // max_docs: 1, // you know, for testing }, }, }, + delete: { + min_age: '90d', + actions: { + delete: {}, + }, + }, }, }, }; diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts index 268421235b4b2..7b3d01f3baa89 100644 --- a/x-pack/plugins/event_log/server/es/names.mock.ts +++ b/x-pack/plugins/event_log/server/es/names.mock.ts @@ -10,7 +10,7 @@ const createNamesMock = () => { const mock: jest.Mocked<EsNames> = { base: '.kibana', alias: '.kibana-event-log-8.0.0', - ilmPolicy: '.kibana-event-log-policy', + ilmPolicy: 'kibana-event-log-policy', indexPattern: '.kibana-event-log-*', indexPatternWithVersion: '.kibana-event-log-8.0.0-*', initialIndex: '.kibana-event-log-8.0.0-000001', diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts index baefd756bb1ed..bc6a4c9a52fac 100644 --- a/x-pack/plugins/event_log/server/es/names.test.ts +++ b/x-pack/plugins/event_log/server/es/names.test.ts @@ -23,4 +23,10 @@ describe('getEsNames()', () => { expect(esNames.initialIndex).toEqual(`${base}-event-log-${version}-000001`); expect(esNames.indexTemplate).toEqual(`${base}-event-log-${version}-template`); }); + + test('ilm policy name does not contain dot prefix', () => { + const base = '.XYZ'; + const esNames = getEsNames(base); + expect(esNames.ilmPolicy).toEqual('XYZ-event-log-policy'); + }); }); diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts index d55d02a16fc9a..8cd56a89d3fbe 100644 --- a/x-pack/plugins/event_log/server/es/names.ts +++ b/x-pack/plugins/event_log/server/es/names.ts @@ -22,10 +22,13 @@ export interface EsNames { export function getEsNames(baseName: string): EsNames { const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`; const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`; + const eventLogPolicyName = `${ + baseName.startsWith('.') ? baseName.substring(1) : baseName + }${EVENT_LOG_NAME_SUFFIX}-policy`; return { base: baseName, alias: eventLogNameWithVersion, - ilmPolicy: `${eventLogName}-policy`, + ilmPolicy: `${eventLogPolicyName}`, indexPattern: `${eventLogName}-*`, indexPatternWithVersion: `${eventLogNameWithVersion}-*`, initialIndex: `${eventLogNameWithVersion}-000001`, diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index 6d4c9b67abc1b..17c073c4b27f9 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -112,7 +112,7 @@ describe('EventLogStart', () => { { page: 1, per_page: 10, - sort_field: 'event.start', + sort_field: '@timestamp', sort_order: 'asc', } ); @@ -193,7 +193,7 @@ describe('EventLogStart', () => { { page: 1, per_page: 10, - sort_field: 'event.start', + sort_field: '@timestamp', sort_order: 'asc', start, end, diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 765f0895f8e0d..8ef245e60aadc 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -36,6 +36,7 @@ export const findOptionsSchema = schema.object({ end: optionalDateFieldSchema, sort_field: schema.oneOf( [ + schema.literal('@timestamp'), schema.literal('event.start'), schema.literal('event.end'), schema.literal('event.provider'), @@ -44,7 +45,7 @@ export const findOptionsSchema = schema.object({ schema.literal('message'), ], { - defaultValue: 'event.start', + defaultValue: '@timestamp', } ), sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], { diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts index 3b250b7462009..43883ea4e384c 100644 --- a/x-pack/plugins/event_log/server/event_log_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_service.test.ts @@ -7,7 +7,7 @@ import { IEventLogConfig } from './types'; import { EventLogService } from './event_log_service'; import { contextMock } from './es/context.mock'; -import { loggingServiceMock } from '../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const systemLogger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index a8d75bc6c2e5a..58dd3ae6eb514 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -7,8 +7,7 @@ import { EventLogClientService } from './event_log_start_service'; import { contextMock } from './es/context.mock'; import { KibanaRequest } from 'kibana/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('./event_log_client'); @@ -41,7 +40,7 @@ describe('EventLogClientService', () => { function fakeRequest(): KibanaRequest { const savedObjectsClient = savedObjectsClientMock.create(); - return { + return ({ headers: {}, getBasePath: () => '', path: '/', @@ -55,5 +54,5 @@ function fakeRequest(): KibanaRequest { }, }, getSavedObjectsClient: () => savedObjectsClient, - } as any; + } as unknown) as KibanaRequest; } 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 673bac4f396e1..2bda194a65d13 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -9,20 +9,20 @@ import { ECS_VERSION } from './types'; import { EventLogService } from './event_log_service'; import { EsContext } from './es/context'; import { contextMock } from './es/context.mock'; -import { loggerMock, MockedLogger } from '../../../../src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { delay } from './lib/delay'; import { EVENT_LOGGED_PREFIX } from './event_logger'; const KIBANA_SERVER_UUID = '424-24-2424'; describe('EventLogger', () => { - let systemLogger: MockedLogger; + let systemLogger: ReturnType<typeof loggingServiceMock.createLogger>; let esContext: EsContext; let service: IEventLogService; let eventLogger: IEventLogger; beforeEach(() => { - systemLogger = loggerMock.create(); + systemLogger = loggingServiceMock.createLogger(); esContext = contextMock.create(); service = new EventLogService({ esContext, @@ -150,10 +150,42 @@ 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 -async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1): Promise<IEvent> { +async function waitForLogEvent( + mockLogger: ReturnType<typeof loggingServiceMock.createLogger>, + waitSeconds: number = 1 +): Promise<IEvent> { const result = await waitForLog(mockLogger, waitSeconds); if (typeof result === 'string') throw new Error('expecting an event'); return result; @@ -161,7 +193,7 @@ async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1 // return the next logged message; throw if it is an event async function waitForLogMessage( - mockLogger: MockedLogger, + mockLogger: ReturnType<typeof loggingServiceMock.createLogger>, waitSeconds: number = 1 ): Promise<string> { const result = await waitForLog(mockLogger, waitSeconds); @@ -171,7 +203,7 @@ async function waitForLogMessage( // return the next logged message, if it's an event log entry, parse it async function waitForLog( - mockLogger: MockedLogger, + mockLogger: ReturnType<typeof loggingServiceMock.createLogger>, waitSeconds: number = 1 ): Promise<string | IEvent> { const intervals = 4; diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index f5149da069953..1a710a6fa4865 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: `; @@ -168,7 +181,7 @@ function indexEventDoc(esContext: EsContext, doc: Doc): void { } // whew, the thing that actually writes the event log document! -async function indexLogEventDoc(esContext: EsContext, doc: any) { +async function indexLogEventDoc(esContext: EsContext, doc: unknown) { esContext.logger.debug(`writing to event log: ${JSON.stringify(doc)}`); await esContext.waitTillReady(); await esContext.esAdapter.indexDocument(doc); @@ -176,6 +189,6 @@ async function indexLogEventDoc(esContext: EsContext, doc: any) { } // TODO: write log entry to a bounded queue buffer -function writeLogEventDocOnError(esContext: EsContext, doc: any) { +function writeLogEventDocOnError(esContext: EsContext, doc: unknown) { esContext.logger.warn(`unable to write event doc: ${JSON.stringify(doc)}`); } diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index b7fa25cb6eb9c..0612b5319c15b 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/lib/bounded_queue.test.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts index c1bb6d70879f3..dd6d15a6e4843 100644 --- a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts +++ b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts @@ -5,7 +5,7 @@ */ import { createBoundedQueue } from './bounded_queue'; -import { loggingServiceMock } from '../../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const logger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/lib/ready_signal.test.ts b/x-pack/plugins/event_log/server/lib/ready_signal.test.ts index d4dbb9064a1ba..6f1d92034c06f 100644 --- a/x-pack/plugins/event_log/server/lib/ready_signal.test.ts +++ b/x-pack/plugins/event_log/server/lib/ready_signal.test.ts @@ -16,11 +16,11 @@ describe('ReadySignal', () => { test('works as expected', async done => { let value = 41; - timeoutSet(100, () => { + timeoutSet(100, async () => { expect(value).toBe(41); }); - timeoutSet(250, () => readySignal.signal(42)); + timeoutSet(250, async () => readySignal.signal(42)); timeoutSet(400, async () => { expect(value).toBe(42); @@ -35,6 +35,6 @@ describe('ReadySignal', () => { }); }); -function timeoutSet(ms: number, fn: any) { +function timeoutSet(ms: number, fn: () => Promise<unknown>): void { setTimeout(fn, ms); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index e5034f599f118..dd83b2cfb03b8 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -120,7 +120,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi } private createRouteHandlerContext = (): IContextProvider< - RequestHandler<any, any, any>, + RequestHandler<unknown, unknown, unknown>, 'eventLog' > => { return async (context, request) => { diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts index 19933649277aa..2d5e37e870b28 100644 --- a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts @@ -11,9 +11,9 @@ import { IEventLogClient } from '../types'; export function mockHandlerArguments( eventLogClient: IEventLogClient, - req: any, + req: unknown, res?: Array<MethodKeysOf<KibanaResponseFactory>> -): [RequestHandlerContext, KibanaRequest<any, any, any, any>, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest<unknown, unknown, unknown>, KibanaResponseFactory] { return [ ({ eventLog: { @@ -22,7 +22,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest<any, any, any, any>, + req as KibanaRequest<unknown, unknown, unknown>, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts index 844a84dc117a9..f47df499d742f 100644 --- a/x-pack/plugins/event_log/server/routes/find.test.ts +++ b/x-pack/plugins/event_log/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; import { eventLogClientMock } from '../event_log_client.mock'; @@ -17,7 +17,7 @@ beforeEach(() => { describe('find', () => { it('finds events with proper parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); @@ -56,7 +56,7 @@ describe('find', () => { }); it('supports optional pagination parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts index cb170e50fb447..f8e1c842ae436 100644 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -31,9 +31,9 @@ export const findRoute = (router: IRouter) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest<TypeOf<typeof paramSchema>, FindOptionsType, any, any>, + req: KibanaRequest<TypeOf<typeof paramSchema>, FindOptionsType, unknown>, res: KibanaResponseFactory - ): Promise<IKibanaResponse<any>> { + ): Promise<IKibanaResponse> { if (!context.eventLog) { return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' }); } diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index baf53ef447914..58be6707b0373 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/plugins/graph/public/_main.scss b/x-pack/plugins/graph/public/_main.scss index 2559b7d1aba5c..0d17015385292 100644 --- a/x-pack/plugins/graph/public/_main.scss +++ b/x-pack/plugins/graph/public/_main.scss @@ -25,3 +25,9 @@ -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } + +.gphAppWrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/x-pack/plugins/graph/public/angular/templates/_graph.scss b/x-pack/plugins/graph/public/angular/templates/_graph.scss index e6bd4693d1e9b..4ba65e7ec6b96 100644 --- a/x-pack/plugins/graph/public/angular/templates/_graph.scss +++ b/x-pack/plugins/graph/public/angular/templates/_graph.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/chrome/variables'; - @mixin gphSvgText() { font-family: $euiFontFamily; font-size: $euiSizeS; diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index f804265f1f5ab..fa479b1b06d5e 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -10,8 +10,10 @@ import angular from 'angular'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import '../../../../webpackShims/ace'; -// required for i18nIdDirective +// required for i18nIdDirective and `ngSanitize` angular module import 'angular-sanitize'; +// required for ngRoute +import 'angular-route'; // type imports import { AppMountContext, @@ -94,7 +96,7 @@ export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) }; }; -const mainTemplate = (basePath: string) => `<div ng-view class="kbnLocalApplicationWrapper"> +const mainTemplate = (basePath: string) => `<div ng-view class="gphAppWrapper"> <base href="${basePath}" /> </div> `; @@ -105,14 +107,14 @@ const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.boo function mountGraphApp(appBasePath: string, element: HTMLElement) { const mountpoint = document.createElement('div'); - mountpoint.setAttribute('class', 'kbnLocalApplicationWrapper'); + mountpoint.setAttribute('class', 'gphAppWrapper'); // eslint-disable-next-line mountpoint.innerHTML = mainTemplate(appBasePath); // bootstrap angular into detached element and attach it later to // make angular-within-angular possible const $injector = angular.bootstrap(mountpoint, [moduleName]); element.appendChild(mountpoint); - element.setAttribute('class', 'kbnLocalApplicationWrapper'); + element.setAttribute('class', 'gphAppWrapper'); return $injector; } diff --git a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx index 78e4180aa2b2a..211458e67d05b 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_editor.tsx @@ -129,7 +129,7 @@ export function FieldEditor({ })} onClickAriaLabel={badgeDescription} title="" - onClick={e => { + onClick={(e: React.MouseEvent<HTMLButtonElement>) => { if (e.shiftKey) { toggleDisabledState(); } else { diff --git a/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx b/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx index 948f89705a5d5..ac656ebdd9512 100644 --- a/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx +++ b/x-pack/plugins/graph/public/components/field_manager/field_manager.test.tsx @@ -252,7 +252,11 @@ describe('field_manager', () => { act(() => { getDisplayForm() .find(EuiColorPicker) - .prop('onChange')!('#aaa'); + .prop('onChange')!('#aaa', { + rgba: [170, 170, 170, 1], + hex: '#aaa', + isValid: true, + }); }); fieldEditor.update(); getDisplayForm() diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index a169953d5a10b..141d5d0ea8db4 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -13,6 +13,7 @@ import { registerExploreRoute } from './routes/explore'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { registerSampleData } from './sample_data'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; @@ -32,6 +33,7 @@ export class GraphPlugin implements Plugin { const licenseState = new LicenseState(); licenseState.start(licensing.license$); this.licenseState = licenseState; + core.savedObjects.registerType(graphWorkspace); if (home) { registerSampleData(home.sampleData, licenseState); diff --git a/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts new file mode 100644 index 0000000000000..8e8cb64aac1b9 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsType } from 'kibana/server'; +import { graphMigrations } from './migrations'; + +export const graphWorkspace: SavedObjectsType = { + name: 'graph-workspace', + namespaceType: 'single', + hidden: false, + migrations: graphMigrations, + mappings: { + properties: { + description: { + type: 'text', + }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { + type: 'text', + }, + }, + }, + numLinks: { + type: 'integer', + }, + numVertices: { + type: 'integer', + }, + title: { + type: 'text', + }, + version: { + type: 'integer', + }, + wsState: { + type: 'text', + }, + }, + }, +}; diff --git a/x-pack/plugins/graph/server/saved_objects/index.ts b/x-pack/plugins/graph/server/saved_objects/index.ts new file mode 100644 index 0000000000000..67d1501950175 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * 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 { graphWorkspace } from './graph_workspace'; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.test.ts b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..ecf1f3ca3b69e --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { graphMigrations } from './migrations'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +describe('graph-workspace', () => { + describe('7.0.0', () => { + const migration = graphMigrations['7.0.0']; + + test('returns doc on empty object', () => { + expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` + Object { + "references": Array [], + } + `); + }); + + test('returns doc when wsState is not a string', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: true, + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": true, + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when wsState is not valid JSON', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: '123abc', + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "123abc", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when "indexPattern" is missing from wsState', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true })), + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('extract "indexPattern" attribute from doc', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), + bar: true, + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "bar": true, + "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "indexPattern_0", + "type": "index-pattern", + }, + ], + "type": "graph-workspace", + } + `); + }); + }); +}); diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..beb31d548c670 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +export const graphMigrations = { + '7.0.0': (doc: SavedObjectUnsanitizedDoc<any>) => { + // Set new "references" attribute + doc.references = doc.references || []; + // Migrate index pattern + const wsState = get(doc, 'attributes.wsState'); + if (typeof wsState !== 'string') { + return doc; + } + let state; + try { + state = JSON.parse(JSON.parse(wsState)); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + const { indexPattern } = state; + if (!indexPattern) { + return doc; + } + state.indexPatternRefName = 'indexPattern_0'; + delete state.indexPattern; + doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); + doc.references.push({ + name: 'indexPattern_0', + type: 'index-pattern', + id: indexPattern, + }); + return doc; + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts index 7d24bc31006b4..aa3ac9ea75c22 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export let toasts: any; -export let fatalErrors: any; +import { IToasts, FatalErrorsSetup } from 'src/core/public'; -export function init(_toasts: any, _fatalErrors: any): void { +export let toasts: IToasts; +export let fatalErrors: FatalErrorsSetup; + +export function init(_toasts: IToasts, _fatalErrors: FatalErrorsSetup): void { toasts = _toasts; fatalErrors = _fatalErrors; } diff --git a/x-pack/plugins/infra/common/alerting/logs/types.ts b/x-pack/plugins/infra/common/alerting/logs/types.ts new file mode 100644 index 0000000000000..cbfffbfd8f940 --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/logs/types.ts @@ -0,0 +1,95 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const LOG_DOCUMENT_COUNT_ALERT_TYPE_ID = 'logs.alert.document.count'; + +export enum Comparator { + GT = 'more than', + GT_OR_EQ = 'more than or equals', + LT = 'less than', + LT_OR_EQ = 'less than or equals', + EQ = 'equals', + NOT_EQ = 'does not equal', + MATCH = 'matches', + NOT_MATCH = 'does not match', + MATCH_PHRASE = 'matches phrase', + NOT_MATCH_PHRASE = 'does not match phrase', +} + +// Maps our comparators to i18n strings, some comparators have more specific wording +// depending on the field type the comparator is being used with. +export const ComparatorToi18nMap = { + [Comparator.GT]: i18n.translate('xpack.infra.logs.alerting.comparator.gt', { + defaultMessage: 'more than', + }), + [Comparator.GT_OR_EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.gtOrEq', { + defaultMessage: 'more than or equals', + }), + [Comparator.LT]: i18n.translate('xpack.infra.logs.alerting.comparator.lt', { + defaultMessage: 'less than', + }), + [Comparator.LT_OR_EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.ltOrEq', { + defaultMessage: 'less than or equals', + }), + [Comparator.EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.eq', { + defaultMessage: 'is', + }), + [Comparator.NOT_EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.notEq', { + defaultMessage: 'is not', + }), + [`${Comparator.EQ}:number`]: i18n.translate('xpack.infra.logs.alerting.comparator.eqNumber', { + defaultMessage: 'equals', + }), + [`${Comparator.NOT_EQ}:number`]: i18n.translate( + 'xpack.infra.logs.alerting.comparator.notEqNumber', + { + defaultMessage: 'does not equal', + } + ), + [Comparator.MATCH]: i18n.translate('xpack.infra.logs.alerting.comparator.match', { + defaultMessage: 'matches', + }), + [Comparator.NOT_MATCH]: i18n.translate('xpack.infra.logs.alerting.comparator.notMatch', { + defaultMessage: 'does not match', + }), + [Comparator.MATCH_PHRASE]: i18n.translate('xpack.infra.logs.alerting.comparator.matchPhrase', { + defaultMessage: 'matches phrase', + }), + [Comparator.NOT_MATCH_PHRASE]: i18n.translate( + 'xpack.infra.logs.alerting.comparator.notMatchPhrase', + { + defaultMessage: 'does not match phrase', + } + ), +}; + +export enum AlertStates { + OK, + ALERT, + NO_DATA, + ERROR, +} + +export interface DocumentCount { + comparator: Comparator; + value: number; +} + +export interface Criterion { + field: string; + comparator: Comparator; + value: string | number; +} + +export interface LogDocumentCountAlertParams { + count: DocumentCount; + criteria: Criterion[]; + timeUnit: 's' | 'm' | 'h' | 'd'; + timeSize: number; +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; diff --git a/x-pack/plugins/infra/public/utils/formatters/bytes.test.ts b/x-pack/plugins/infra/common/formatters/bytes.test.ts similarity index 93% rename from x-pack/plugins/infra/public/utils/formatters/bytes.test.ts rename to x-pack/plugins/infra/common/formatters/bytes.test.ts index 4c872bcee057d..ccdeed120acca 100644 --- a/x-pack/plugins/infra/public/utils/formatters/bytes.test.ts +++ b/x-pack/plugins/infra/common/formatters/bytes.test.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { InfraWaffleMapDataFormat } from '../../lib/lib'; +import { InfraWaffleMapDataFormat } from './types'; import { createBytesFormatter } from './bytes'; + describe('createDataFormatter', () => { it('should format bytes as bytesDecimal', () => { const formatter = createBytesFormatter(InfraWaffleMapDataFormat.bytesDecimal); diff --git a/x-pack/plugins/infra/common/formatters/bytes.ts b/x-pack/plugins/infra/common/formatters/bytes.ts new file mode 100644 index 0000000000000..3a45caa8b5e15 --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/bytes.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { formatNumber } from './number'; +import { InfraWaffleMapDataFormat } from './types'; + +/** + * The labels are derived from these two Wikipedia articles. + * https://en.wikipedia.org/wiki/Kilobit + * https://en.wikipedia.org/wiki/Kilobyte + */ +const LABELS = { + [InfraWaffleMapDataFormat.bytesDecimal]: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], + [InfraWaffleMapDataFormat.bitsDecimal]: [ + 'bit', + 'kbit', + 'Mbit', + 'Gbit', + 'Tbit', + 'Pbit', + 'Ebit', + 'Zbit', + 'Ybit', + ], + [InfraWaffleMapDataFormat.abbreviatedNumber]: ['', 'K', 'M', 'B', 'T'], +}; + +const BASES = { + [InfraWaffleMapDataFormat.bytesDecimal]: 1000, + [InfraWaffleMapDataFormat.bitsDecimal]: 1000, + [InfraWaffleMapDataFormat.abbreviatedNumber]: 1000, +}; + +/* + * This formatter always assumes you're input is bytes and the output is a string + * in whatever format you've defined. Bytes in Format Out. + */ +export const createBytesFormatter = (format: InfraWaffleMapDataFormat) => (bytes: number) => { + const labels = LABELS[format]; + const base = BASES[format]; + const value = format === InfraWaffleMapDataFormat.bitsDecimal ? bytes * 8 : bytes; + // Use an exponetial equation to get the power to determine which label to use. If the power + // is greater then the max label then use the max label. + const power = Math.min(Math.floor(Math.log(Math.abs(value)) / Math.log(base)), labels.length - 1); + if (power < 0) { + return `${formatNumber(value)}${labels[0]}`; + } + return `${formatNumber(value / Math.pow(base, power))}${labels[power]}`; +}; diff --git a/x-pack/plugins/infra/public/utils/formatters/datetime.ts b/x-pack/plugins/infra/common/formatters/datetime.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/datetime.ts rename to x-pack/plugins/infra/common/formatters/datetime.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/high_precision.ts b/x-pack/plugins/infra/common/formatters/high_precision.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/high_precision.ts rename to x-pack/plugins/infra/common/formatters/high_precision.ts diff --git a/x-pack/plugins/infra/common/formatters/index.ts b/x-pack/plugins/infra/common/formatters/index.ts new file mode 100644 index 0000000000000..096085696bd6b --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/index.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Mustache from 'mustache'; +import { createBytesFormatter } from './bytes'; +import { formatNumber } from './number'; +import { formatPercent } from './percent'; +import { InventoryFormatterType } from '../inventory_models/types'; +import { formatHighPercision } from './high_precision'; +import { InfraWaffleMapDataFormat } from './types'; + +export const FORMATTERS = { + number: formatNumber, + // Because the implimentation for formatting large numbers is the same as formatting + // bytes we are re-using the same code, we just format the number using the abbreviated number format. + abbreviatedNumber: createBytesFormatter(InfraWaffleMapDataFormat.abbreviatedNumber), + // bytes in bytes formatted string out + bytes: createBytesFormatter(InfraWaffleMapDataFormat.bytesDecimal), + // bytes in bits formatted string out + bits: createBytesFormatter(InfraWaffleMapDataFormat.bitsDecimal), + percent: formatPercent, + highPercision: formatHighPercision, +}; + +export const createFormatter = (format: InventoryFormatterType, template: string = '{{value}}') => ( + val: string | number +) => { + if (val == null) { + return ''; + } + const fmtFn = FORMATTERS[format]; + const value = fmtFn(Number(val)); + return Mustache.render(template, { value }); +}; diff --git a/x-pack/plugins/infra/public/utils/formatters/number.ts b/x-pack/plugins/infra/common/formatters/number.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/number.ts rename to x-pack/plugins/infra/common/formatters/number.ts diff --git a/x-pack/plugins/infra/public/utils/formatters/percent.ts b/x-pack/plugins/infra/common/formatters/percent.ts similarity index 100% rename from x-pack/plugins/infra/public/utils/formatters/percent.ts rename to x-pack/plugins/infra/common/formatters/percent.ts diff --git a/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts new file mode 100644 index 0000000000000..8b4ae27cb3061 --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +enum InfraFormatterType { + number = 'number', + abbreviatedNumber = 'abbreviatedNumber', + bytes = 'bytes', + bits = 'bits', + percent = 'percent', +} + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +export const METRIC_FORMATTERS: MetricFormatters = { + ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, + ['cpu']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['memory']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['logRate']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, + ['diskIOReadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['diskIOWriteBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['s3BucketSize']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3TotalRequests']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3NumberOfObjects']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3UploadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3DownloadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['sqsOldestMessage']: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, +}; diff --git a/x-pack/plugins/infra/common/formatters/types.ts b/x-pack/plugins/infra/common/formatters/types.ts new file mode 100644 index 0000000000000..c438ec2d4205d --- /dev/null +++ b/x-pack/plugins/infra/common/formatters/types.ts @@ -0,0 +1,11 @@ +/* + * 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 enum InfraWaffleMapDataFormat { + bytesDecimal = 'bytesDecimal', + bitsDecimal = 'bitsDecimal', + abbreviatedNumber = 'abbreviatedNumber', +} diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index d532c079e3e9c..1c5a2f0fe1ad9 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -78,11 +78,11 @@ export const logEntryRT = rt.type({ id: rt.string, cursor: logEntriesCursorRT, columns: rt.array(logColumnRT), - context: rt.partial({ - 'log.file.path': rt.string, - 'host.name': rt.string, - 'container.id': rt.string, - }), + context: rt.union([ + rt.type({}), + rt.type({ 'container.id': rt.string }), + rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }), + ]), }); export type LogMessageConstantPart = rt.TypeOf<typeof logMessageConstantPartRT>; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts new file mode 100644 index 0000000000000..bb79b5ebc5290 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_configuration.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { badRequestErrorRT, forbiddenErrorRT, routeTimingMetadataRT } from '../shared'; +import { logSourceConfigurationRT } from './log_source_configuration'; + +/** + * request + */ + +export const getLogSourceConfigurationRequestParamsRT = rt.type({ + // the id of the source configuration + sourceId: rt.string, +}); + +export type GetLogSourceConfigurationRequestParams = rt.TypeOf< + typeof getLogSourceConfigurationRequestParamsRT +>; + +/** + * response + */ + +export const getLogSourceConfigurationSuccessResponsePayloadRT = rt.intersection([ + rt.type({ + data: logSourceConfigurationRT, + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetLogSourceConfigurationSuccessResponsePayload = rt.TypeOf< + typeof getLogSourceConfigurationSuccessResponsePayloadRT +>; + +export const getLogSourceConfigurationErrorResponsePayloadRT = rt.union([ + badRequestErrorRT, + forbiddenErrorRT, +]); + +export type GetLogSourceConfigurationErrorReponsePayload = rt.TypeOf< + typeof getLogSourceConfigurationErrorResponsePayloadRT +>; + +export const getLogSourceConfigurationResponsePayloadRT = rt.union([ + getLogSourceConfigurationSuccessResponsePayloadRT, + getLogSourceConfigurationErrorResponsePayloadRT, +]); + +export type GetLogSourceConfigurationReponsePayload = rt.TypeOf< + typeof getLogSourceConfigurationResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts new file mode 100644 index 0000000000000..ae872cee9aa56 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/get_log_source_status.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { routeTimingMetadataRT } from '../shared'; +import { + getLogSourceConfigurationPath, + LOG_SOURCE_CONFIGURATION_PATH, +} from './log_source_configuration'; + +export const LOG_SOURCE_STATUS_PATH_SUFFIX = 'status'; +export const LOG_SOURCE_STATUS_PATH = `${LOG_SOURCE_CONFIGURATION_PATH}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`; +export const getLogSourceStatusPath = (sourceId: string) => + `${getLogSourceConfigurationPath(sourceId)}/${LOG_SOURCE_STATUS_PATH_SUFFIX}`; + +/** + * request + */ + +export const getLogSourceStatusRequestParamsRT = rt.type({ + // the id of the source configuration + sourceId: rt.string, +}); + +export type GetLogSourceStatusRequestParams = rt.TypeOf<typeof getLogSourceStatusRequestParamsRT>; + +/** + * response + */ + +const logIndexFieldRT = rt.strict({ + name: rt.string, + type: rt.string, + searchable: rt.boolean, + aggregatable: rt.boolean, +}); + +export type LogIndexField = rt.TypeOf<typeof logIndexFieldRT>; + +const logSourceStatusRT = rt.strict({ + logIndexFields: rt.array(logIndexFieldRT), + logIndexNames: rt.array(rt.string), +}); + +export type LogSourceStatus = rt.TypeOf<typeof logSourceStatusRT>; + +export const getLogSourceStatusSuccessResponsePayloadRT = rt.intersection([ + rt.type({ + data: logSourceStatusRT, + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetLogSourceStatusSuccessResponsePayload = rt.TypeOf< + typeof getLogSourceStatusSuccessResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/index.ts b/x-pack/plugins/infra/common/http_api/log_sources/index.ts new file mode 100644 index 0000000000000..5fd1b5efec177 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './get_log_source_configuration'; +export * from './get_log_source_status'; +export * from './log_source_configuration'; +export * from './patch_log_source_configuration'; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts new file mode 100644 index 0000000000000..e8bf63843c623 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/log_source_configuration.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const LOG_SOURCE_CONFIGURATION_PATH_PREFIX = '/api/infra/log_source_configurations'; +export const LOG_SOURCE_CONFIGURATION_PATH = `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/{sourceId}`; +export const getLogSourceConfigurationPath = (sourceId: string) => + `${LOG_SOURCE_CONFIGURATION_PATH_PREFIX}/${sourceId}`; + +export const logSourceConfigurationOriginRT = rt.keyof({ + fallback: null, + internal: null, + stored: null, +}); + +export type LogSourceConfigurationOrigin = rt.TypeOf<typeof logSourceConfigurationOriginRT>; + +const logSourceFieldsConfigurationRT = rt.strict({ + timestamp: rt.string, + tiebreaker: rt.string, +}); + +const logSourceCommonColumnConfigurationRT = rt.strict({ + id: rt.string, +}); + +const logSourceTimestampColumnConfigurationRT = rt.strict({ + timestampColumn: logSourceCommonColumnConfigurationRT, +}); + +const logSourceMessageColumnConfigurationRT = rt.strict({ + messageColumn: logSourceCommonColumnConfigurationRT, +}); + +const logSourceFieldColumnConfigurationRT = rt.strict({ + fieldColumn: rt.intersection([ + logSourceCommonColumnConfigurationRT, + rt.strict({ + field: rt.string, + }), + ]), +}); + +const logSourceColumnConfigurationRT = rt.union([ + logSourceTimestampColumnConfigurationRT, + logSourceMessageColumnConfigurationRT, + logSourceFieldColumnConfigurationRT, +]); + +export const logSourceConfigurationPropertiesRT = rt.strict({ + name: rt.string, + description: rt.string, + logAlias: rt.string, + fields: logSourceFieldsConfigurationRT, + logColumns: rt.array(logSourceColumnConfigurationRT), +}); + +export type LogSourceConfigurationProperties = rt.TypeOf<typeof logSourceConfigurationPropertiesRT>; + +export const logSourceConfigurationRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + origin: logSourceConfigurationOriginRT, + configuration: logSourceConfigurationPropertiesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export type LogSourceConfiguration = rt.TypeOf<typeof logSourceConfigurationRT>; diff --git a/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts b/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts new file mode 100644 index 0000000000000..fd312d7429b60 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/log_sources/patch_log_source_configuration.ts @@ -0,0 +1,60 @@ +/* + * 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 * as rt from 'io-ts'; +import { badRequestErrorRT, forbiddenErrorRT } from '../shared'; +import { getLogSourceConfigurationSuccessResponsePayloadRT } from './get_log_source_configuration'; +import { logSourceConfigurationPropertiesRT } from './log_source_configuration'; + +/** + * request + */ + +export const patchLogSourceConfigurationRequestParamsRT = rt.type({ + // the id of the source configuration + sourceId: rt.string, +}); + +export type PatchLogSourceConfigurationRequestParams = rt.TypeOf< + typeof patchLogSourceConfigurationRequestParamsRT +>; + +const logSourceConfigurationProperiesPatchRT = rt.partial({ + ...logSourceConfigurationPropertiesRT.type.props, + fields: rt.partial(logSourceConfigurationPropertiesRT.type.props.fields.type.props), +}); + +export type LogSourceConfigurationPropertiesPatch = rt.TypeOf< + typeof logSourceConfigurationProperiesPatchRT +>; + +export const patchLogSourceConfigurationRequestBodyRT = rt.type({ + data: logSourceConfigurationProperiesPatchRT, +}); + +export type PatchLogSourceConfigurationRequestBody = rt.TypeOf< + typeof patchLogSourceConfigurationRequestBodyRT +>; + +/** + * response + */ + +export const patchLogSourceConfigurationSuccessResponsePayloadRT = getLogSourceConfigurationSuccessResponsePayloadRT; + +export type PatchLogSourceConfigurationSuccessResponsePayload = rt.TypeOf< + typeof patchLogSourceConfigurationSuccessResponsePayloadRT +>; + +export const patchLogSourceConfigurationResponsePayloadRT = rt.union([ + patchLogSourceConfigurationSuccessResponsePayloadRT, + badRequestErrorRT, + forbiddenErrorRT, +]); + +export type PatchLogSourceConfigurationReponsePayload = rt.TypeOf< + typeof patchLogSourceConfigurationResponsePayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts new file mode 100644 index 0000000000000..eb5b0bdbcfbc5 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -0,0 +1,116 @@ +/* + * 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 * as rt from 'io-ts'; + +export const METRIC_EXPLORER_AGGREGATIONS = [ + 'avg', + 'max', + 'min', + 'cardinality', + 'rate', + 'count', + 'sum', +] as const; + +type MetricExplorerAggregations = typeof METRIC_EXPLORER_AGGREGATIONS[number]; + +const metricsExplorerAggregationKeys = METRIC_EXPLORER_AGGREGATIONS.reduce< + Record<MetricExplorerAggregations, null> +>((acc, agg) => ({ ...acc, [agg]: null }), {} as Record<MetricExplorerAggregations, null>); + +export const metricsExplorerAggregationRT = rt.keyof(metricsExplorerAggregationKeys); + +export const metricsExplorerMetricRequiredFieldsRT = rt.type({ + aggregation: metricsExplorerAggregationRT, +}); + +export const metricsExplorerMetricOptionalFieldsRT = rt.partial({ + field: rt.union([rt.string, rt.undefined]), +}); + +export const metricsExplorerMetricRT = rt.intersection([ + metricsExplorerMetricRequiredFieldsRT, + metricsExplorerMetricOptionalFieldsRT, +]); + +export const timeRangeRT = rt.type({ + field: rt.string, + from: rt.number, + to: rt.number, + interval: rt.string, +}); + +export const metricsExplorerRequestBodyRequiredFieldsRT = rt.type({ + timerange: timeRangeRT, + indexPattern: rt.string, + metrics: rt.array(metricsExplorerMetricRT), +}); + +export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({ + groupBy: rt.union([rt.string, rt.null, rt.undefined]), + afterKey: rt.union([rt.string, rt.null, rt.undefined]), + limit: rt.union([rt.number, rt.null, rt.undefined]), + filterQuery: rt.union([rt.string, rt.null, rt.undefined]), + forceInterval: rt.boolean, +}); + +export const metricsExplorerRequestBodyRT = rt.intersection([ + metricsExplorerRequestBodyRequiredFieldsRT, + metricsExplorerRequestBodyOptionalFieldsRT, +]); + +export const metricsExplorerPageInfoRT = rt.type({ + total: rt.number, + afterKey: rt.union([rt.string, rt.null]), +}); + +export const metricsExplorerColumnTypeRT = rt.keyof({ + date: null, + number: null, + string: null, +}); + +export const metricsExplorerColumnRT = rt.type({ + name: rt.string, + type: metricsExplorerColumnTypeRT, +}); + +export const metricsExplorerRowRT = rt.intersection([ + rt.type({ + timestamp: rt.number, + }), + rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])), +]); + +export const metricsExplorerSeriesRT = rt.type({ + id: rt.string, + columns: rt.array(metricsExplorerColumnRT), + rows: rt.array(metricsExplorerRowRT), +}); + +export const metricsExplorerResponseRT = rt.type({ + series: rt.array(metricsExplorerSeriesRT), + pageInfo: metricsExplorerPageInfoRT, +}); + +export type MetricsExplorerAggregation = rt.TypeOf<typeof metricsExplorerAggregationRT>; + +export type MetricsExplorerColumnType = rt.TypeOf<typeof metricsExplorerColumnTypeRT>; + +export type MetricsExplorerMetric = rt.TypeOf<typeof metricsExplorerMetricRT>; + +export type MetricsExplorerPageInfo = rt.TypeOf<typeof metricsExplorerPageInfoRT>; + +export type MetricsExplorerColumn = rt.TypeOf<typeof metricsExplorerColumnRT>; + +export type MetricsExplorerRow = rt.TypeOf<typeof metricsExplorerRowRT>; + +export type MetricsExplorerSeries = rt.TypeOf<typeof metricsExplorerSeriesRT>; + +export type MetricsExplorerRequestBody = rt.TypeOf<typeof metricsExplorerRequestBodyRT>; + +export type MetricsExplorerResponse = rt.TypeOf<typeof metricsExplorerResponseRT>; diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts deleted file mode 100644 index 93655f931f45d..0000000000000 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer/index.ts +++ /dev/null @@ -1,114 +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 * as rt from 'io-ts'; - -export const METRIC_EXPLORER_AGGREGATIONS = [ - 'avg', - 'max', - 'min', - 'cardinality', - 'rate', - 'count', -] as const; - -type MetricExplorerAggregations = typeof METRIC_EXPLORER_AGGREGATIONS[number]; - -const metricsExplorerAggregationKeys = METRIC_EXPLORER_AGGREGATIONS.reduce< - Record<MetricExplorerAggregations, null> ->((acc, agg) => ({ ...acc, [agg]: null }), {} as Record<MetricExplorerAggregations, null>); - -export const metricsExplorerAggregationRT = rt.keyof(metricsExplorerAggregationKeys); - -export const metricsExplorerMetricRequiredFieldsRT = rt.type({ - aggregation: metricsExplorerAggregationRT, -}); - -export const metricsExplorerMetricOptionalFieldsRT = rt.partial({ - field: rt.union([rt.string, rt.undefined]), -}); - -export const metricsExplorerMetricRT = rt.intersection([ - metricsExplorerMetricRequiredFieldsRT, - metricsExplorerMetricOptionalFieldsRT, -]); - -export const timeRangeRT = rt.type({ - field: rt.string, - from: rt.number, - to: rt.number, - interval: rt.string, -}); - -export const metricsExplorerRequestBodyRequiredFieldsRT = rt.type({ - timerange: timeRangeRT, - indexPattern: rt.string, - metrics: rt.array(metricsExplorerMetricRT), -}); - -export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({ - groupBy: rt.union([rt.string, rt.null, rt.undefined]), - afterKey: rt.union([rt.string, rt.null, rt.undefined]), - limit: rt.union([rt.number, rt.null, rt.undefined]), - filterQuery: rt.union([rt.string, rt.null, rt.undefined]), -}); - -export const metricsExplorerRequestBodyRT = rt.intersection([ - metricsExplorerRequestBodyRequiredFieldsRT, - metricsExplorerRequestBodyOptionalFieldsRT, -]); - -export const metricsExplorerPageInfoRT = rt.type({ - total: rt.number, - afterKey: rt.union([rt.string, rt.null]), -}); - -export const metricsExplorerColumnTypeRT = rt.keyof({ - date: null, - number: null, - string: null, -}); - -export const metricsExplorerColumnRT = rt.type({ - name: rt.string, - type: metricsExplorerColumnTypeRT, -}); - -export const metricsExplorerRowRT = rt.intersection([ - rt.type({ - timestamp: rt.number, - }), - rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])), -]); - -export const metricsExplorerSeriesRT = rt.type({ - id: rt.string, - columns: rt.array(metricsExplorerColumnRT), - rows: rt.array(metricsExplorerRowRT), -}); - -export const metricsExplorerResponseRT = rt.type({ - series: rt.array(metricsExplorerSeriesRT), - pageInfo: metricsExplorerPageInfoRT, -}); - -export type MetricsExplorerAggregation = rt.TypeOf<typeof metricsExplorerAggregationRT>; - -export type MetricsExplorerColumnType = rt.TypeOf<typeof metricsExplorerColumnTypeRT>; - -export type MetricsExplorerMetric = rt.TypeOf<typeof metricsExplorerMetricRT>; - -export type MetricsExplorerPageInfo = rt.TypeOf<typeof metricsExplorerPageInfoRT>; - -export type MetricsExplorerColumn = rt.TypeOf<typeof metricsExplorerColumnRT>; - -export type MetricsExplorerRow = rt.TypeOf<typeof metricsExplorerRowRT>; - -export type MetricsExplorerSeries = rt.TypeOf<typeof metricsExplorerSeriesRT>; - -export type MetricsExplorerRequestBody = rt.TypeOf<typeof metricsExplorerRequestBodyRT>; - -export type MetricsExplorerResponse = rt.TypeOf<typeof metricsExplorerResponseRT>; diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx index 68bfe41fd538e..9494e4aa396a5 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/layout.tsx @@ -6,19 +6,19 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; export const Layout = withTheme(({ metrics, theme, onChangeRangeTime }: LayoutPropsWithTheme) => ( <React.Fragment> diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx index 683851fec4d77..764db2164b711 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx @@ -6,32 +6,34 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const ec2MetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'rx', + 'tx', + 'diskIOReadBytes', + 'diskIOWriteBytes', +]; + +export const ec2groupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'aws.ec2.instance.image.id', + 'aws.ec2.instance.state.name', +]; + export const AwsEC2ToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'cpu', - 'rx', - 'tx', - 'diskIOReadBytes', - 'diskIOWriteBytes', - ]; - const groupByFields = [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'aws.ec2.instance.image.id', - 'aws.ec2.instance.state.name', - ]; return ( <> <CloudToolbarItems {...props} /> <MetricsAndGroupByToolbarItems {...props} - metricTypes={metricTypes} - groupByFields={groupByFields} + metricTypes={ec2MetricTypes} + groupByFields={ec2groupByFields} /> </> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx index 220c6c67f4aea..08b865f01b06c 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/layout.tsx @@ -6,17 +6,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> diff --git a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx index 24f05fd91589c..3eebdee22b2c3 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx @@ -6,31 +6,33 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const rdsMetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'rdsConnections', + 'rdsQueriesExecuted', + 'rdsActiveTransactions', + 'rdsLatency', +]; + +export const rdsGroupByFields = [ + 'cloud.availability_zone', + 'aws.rds.db_instance.class', + 'aws.rds.db_instance.status', +]; + export const AwsRDSToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'cpu', - 'rdsConnections', - 'rdsQueriesExecuted', - 'rdsActiveTransactions', - 'rdsLatency', - ]; - const groupByFields = [ - 'cloud.availability_zone', - 'aws.rds.db_instance.class', - 'aws.rds.db_instance.status', - ]; return ( <> <CloudToolbarItems {...props} /> <MetricsAndGroupByToolbarItems {...props} - metricTypes={metricTypes} - groupByFields={groupByFields} + metricTypes={rdsMetricTypes} + groupByFields={rdsGroupByFields} /> </> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx index 805236cf47082..e16f8ef6addde 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/layout.tsx @@ -6,17 +6,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> diff --git a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx index 54c3196ae2833..ede618b1bf19d 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx @@ -6,27 +6,29 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const s3MetricTypes: SnapshotMetricType[] = [ + 's3BucketSize', + 's3NumberOfObjects', + 's3TotalRequests', + 's3DownloadBytes', + 's3UploadBytes', +]; + +export const s3GroupByFields = ['cloud.region']; + export const AwsS3ToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 's3BucketSize', - 's3NumberOfObjects', - 's3TotalRequests', - 's3DownloadBytes', - 's3UploadBytes', - ]; - const groupByFields = ['cloud.region']; return ( <> <CloudToolbarItems {...props} /> <MetricsAndGroupByToolbarItems {...props} - metricTypes={metricTypes} - groupByFields={groupByFields} + metricTypes={s3MetricTypes} + groupByFields={s3GroupByFields} /> </> ); diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx index d581ac751682d..ff13f2db104de 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/layout.tsx @@ -6,17 +6,17 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> diff --git a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx index 22cb2740a4a7f..e77f3af578197 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx @@ -6,27 +6,28 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; -import { CloudToolbarItems } from '../shared/compontents/cloud_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; +import { CloudToolbarItems } from '../shared/components/cloud_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const sqsMetricTypes: SnapshotMetricType[] = [ + 'sqsMessagesVisible', + 'sqsMessagesDelayed', + 'sqsMessagesSent', + 'sqsMessagesEmpty', + 'sqsOldestMessage', +]; +export const sqsGroupByFields = ['cloud.region']; + export const AwsSQSToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = [ - 'sqsMessagesVisible', - 'sqsMessagesDelayed', - 'sqsMessagesSent', - 'sqsMessagesEmpty', - 'sqsOldestMessage', - ]; - const groupByFields = ['cloud.region']; return ( <> <CloudToolbarItems {...props} /> <MetricsAndGroupByToolbarItems {...props} - metricTypes={metricTypes} - groupByFields={groupByFields} + metricTypes={sqsMetricTypes} + groupByFields={sqsGroupByFields} /> </> ); diff --git a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx index 9956b2c9a2ce4..b9366a43e40c6 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/layout.tsx @@ -6,21 +6,21 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> diff --git a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx index dbb77cb3b677e..f193adbf6aadc 100644 --- a/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/container/toolbar_items.tsx @@ -6,25 +6,26 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const containerMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; +export const containerGroupByFields = [ + 'host.name', + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', +]; + export const ContainerToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; - const groupByFields = [ - 'host.name', - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ]; return ( <MetricsAndGroupByToolbarItems {...props} - metricTypes={metricTypes} - groupByFields={groupByFields} + metricTypes={containerMetricTypes} + groupByFields={containerGroupByFields} /> ); }; diff --git a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx index 6d7d361254220..e23118c747a9b 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/layout.tsx @@ -8,21 +8,21 @@ import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; import * as Aws from '../shared/layouts/aws'; import * as Ngnix from '../shared/layouts/nginx'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> diff --git a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx index fc7696ee53c9d..8ed684b3885de 100644 --- a/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/host/toolbar_items.tsx @@ -6,24 +6,31 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const hostMetricTypes: SnapshotMetricType[] = [ + 'cpu', + 'memory', + 'load', + 'rx', + 'tx', + 'logRate', +]; +export const hostGroupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', +]; export const HostToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'load', 'rx', 'tx', 'logRate']; - const groupByFields = [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ]; return ( <MetricsAndGroupByToolbarItems {...props} - metricTypes={metricTypes} - groupByFields={groupByFields} + metricTypes={hostMetricTypes} + groupByFields={hostGroupByFields} /> ); }; diff --git a/x-pack/plugins/infra/common/inventory_models/layouts.ts b/x-pack/plugins/infra/common/inventory_models/layouts.ts index b59ce010361ec..df82f154b47b3 100644 --- a/x-pack/plugins/infra/common/inventory_models/layouts.ts +++ b/x-pack/plugins/infra/common/inventory_models/layouts.ts @@ -23,7 +23,7 @@ import { Layout as AwsRDSLayout } from './aws_rds/layout'; import { Layout as AwsSQSLayout } from './aws_sqs/layout'; import { InventoryItemType } from './types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutProps } from '../../public/pages/metrics/types'; +import { LayoutProps } from '../../public/pages/metrics/metric_detail/types'; interface Layouts { [type: string]: ReactNode; diff --git a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx index 8bc2f3ee8b4b3..271e32556ae28 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/layout.tsx @@ -6,22 +6,22 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../public/pages/metrics/components/section'; +import { Section } from '../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../observability/public'; import * as Nginx from '../shared/layouts/nginx'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetadataDetails } from '../../../public/pages/metrics/components/metadata_details'; +import { MetadataDetails } from '../../../public/pages/metrics/metric_detail/components/metadata_details'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutContent } from '../../../public/pages/metrics/components/layout_content'; +import { LayoutContent } from '../../../public/pages/metrics/metric_detail/components/layout_content'; export const Layout = withTheme(({ metrics, onChangeRangeTime, theme }: LayoutPropsWithTheme) => ( <React.Fragment> diff --git a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx index d48c27efd88fd..54a32e3e0180a 100644 --- a/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/pod/toolbar_items.tsx @@ -6,18 +6,19 @@ import React from 'react'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { ToolbarProps } from '../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/components/metrics_and_groupby_toolbar_items'; import { SnapshotMetricType } from '../types'; +export const podMetricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; +export const podGroupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; + export const PodToolbarItems = (props: ToolbarProps) => { - const metricTypes: SnapshotMetricType[] = ['cpu', 'memory', 'rx', 'tx']; - const groupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; return ( <MetricsAndGroupByToolbarItems {...props} - metricTypes={metricTypes} - groupByFields={groupByFields} + metricTypes={podMetricTypes} + groupByFields={podGroupByFields} /> ); }; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/compontents/cloud_toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/shared/components/cloud_toolbar_items.tsx similarity index 75% rename from x-pack/plugins/infra/common/inventory_models/shared/compontents/cloud_toolbar_items.tsx rename to x-pack/plugins/infra/common/inventory_models/shared/components/cloud_toolbar_items.tsx index 766a8ae8142f5..da5017b0f3a36 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/compontents/cloud_toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/components/cloud_toolbar_items.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { EuiFlexItem } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../../public/components/inventory/toolbars/toolbar'; +import { ToolbarProps } from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleAccountsControls } from '../../../../public/components/waffle/waffle_accounts_controls'; +import { WaffleAccountsControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleRegionControls } from '../../../../public/components/waffle/waffle_region_controls'; +import { WaffleRegionControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_region_controls'; type Props = ToolbarProps; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx similarity index 81% rename from x-pack/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx rename to x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx index 738fce45ee99f..bf37828ed0856 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/components/metrics_and_groupby_toolbar_items.tsx @@ -7,16 +7,16 @@ import React, { useMemo } from 'react'; import { EuiFlexItem } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../../../public/components/inventory/toolbars/toolbar'; +import { ToolbarProps } from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleMetricControls } from '../../../../public/components/waffle/metric_control'; +import { WaffleMetricControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/metric_control'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleGroupByControls } from '../../../../public/components/waffle/waffle_group_by_controls'; +import { WaffleGroupByControls } from '../../../../public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls'; import { toGroupByOpt, toMetricOpt, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../public/components/inventory/toolbars/toolbar_wrapper'; +} from '../../../../public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper'; import { SnapshotMetricType } from '../../types'; interface Props extends ToolbarProps { diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx index 7a0b898d406ce..6f2791534c17e 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/aws.tsx @@ -6,15 +6,15 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../../public/pages/metrics/components/section'; +import { Section } from '../../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { GaugesSectionVis } from '../../../../public/pages/metrics/components/gauges_section_vis'; +import { GaugesSectionVis } from '../../../../public/pages/metrics/metric_detail/components/gauges_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../../observability/public'; diff --git a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx index 79cea5150d498..cf3a06994cc96 100644 --- a/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx +++ b/x-pack/plugins/infra/common/inventory_models/shared/layouts/nginx.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/types'; +import { LayoutPropsWithTheme } from '../../../../public/pages/metrics/metric_detail/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Section } from '../../../../public/pages/metrics/components/section'; +import { Section } from '../../../../public/pages/metrics/metric_detail/components/section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SubSection } from '../../../../public/pages/metrics/components/sub_section'; +import { SubSection } from '../../../../public/pages/metrics/metric_detail/components/sub_section'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ChartSectionVis } from '../../../../public/pages/metrics/components/chart_section_vis'; +import { ChartSectionVis } from '../../../../public/pages/metrics/metric_detail/components/chart_section_vis'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { withTheme } from '../../../../../observability/public'; diff --git a/x-pack/plugins/infra/common/inventory_models/toolbars.ts b/x-pack/plugins/infra/common/inventory_models/toolbars.ts index 39e9f5a260f7a..e00cb094d0455 100644 --- a/x-pack/plugins/infra/common/inventory_models/toolbars.ts +++ b/x-pack/plugins/infra/common/inventory_models/toolbars.ts @@ -11,7 +11,7 @@ import { HostToolbarItems } from './host/toolbar_items'; import { ContainerToolbarItems } from './container/toolbar_items'; import { PodToolbarItems } from './pod/toolbar_items'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ToolbarProps } from '../../public/components/inventory/toolbars/toolbar'; +import { ToolbarProps } from '../../public/pages/metrics/inventory_view/components/toolbars/toolbar'; import { AwsEC2ToolbarItems } from './aws_ec2/toolbar_items'; import { AwsS3ToolbarItems } from './aws_s3/toolbar_items'; import { AwsRDSToolbarItems } from './aws_rds/toolbar_items'; diff --git a/x-pack/plugins/infra/common/runtime_types.ts b/x-pack/plugins/infra/common/runtime_types.ts index d5b858df38def..a8d5cd8693a3d 100644 --- a/x-pack/plugins/infra/common/runtime_types.ts +++ b/x-pack/plugins/infra/common/runtime_types.ts @@ -9,6 +9,7 @@ import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { Errors, Type } from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; +import { RouteValidationFunction } from 'kibana/server'; type ErrorFactory = (message: string) => Error; @@ -18,8 +19,21 @@ export const throwErrors = (createError: ErrorFactory) => (errors: Errors) => { throw createError(failure(errors).join('\n')); }; -export const decodeOrThrow = <A, O, I>( - runtimeType: Type<A, O, I>, +export const decodeOrThrow = <DecodedValue, EncodedValue, InputValue>( + runtimeType: Type<DecodedValue, EncodedValue, InputValue>, createError: ErrorFactory = createPlainError -) => (inputValue: I) => +) => (inputValue: InputValue) => pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); + +type ValdidationResult<Value> = ReturnType<RouteValidationFunction<Value>>; + +export const createValidationFunction = <DecodedValue, EncodedValue, InputValue>( + runtimeType: Type<DecodedValue, EncodedValue, InputValue> +): RouteValidationFunction<DecodedValue> => (inputValue, { badRequest, ok }) => + pipe( + runtimeType.decode(inputValue), + fold<Errors, DecodedValue, ValdidationResult<DecodedValue>>( + (errors: Errors) => badRequest(failure(errors).join('\n')), + (result: DecodedValue) => ok(result) + ) + ); diff --git a/x-pack/plugins/infra/common/saved_objects/inventory_view.ts b/x-pack/plugins/infra/common/saved_objects/inventory_view.ts index 8ae765f379add..8933de57b0448 100644 --- a/x-pack/plugins/infra/common/saved_objects/inventory_view.ts +++ b/x-pack/plugins/infra/common/saved_objects/inventory_view.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ElasticsearchMappingOf } from '../../server/utils/typed_elasticsearch_mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { WaffleViewState } from '../../public/pages/inventory_view/hooks/use_waffle_view_state'; +import { WaffleViewState } from '../../public/pages/metrics/inventory_view/hooks/use_waffle_view_state'; export const inventoryViewSavedObjectType = 'inventory-view'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts index a9f1194844640..add6ab0f132b5 100644 --- a/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts +++ b/x-pack/plugins/infra/common/saved_objects/metrics_explorer_view.ts @@ -11,7 +11,7 @@ import { MetricsExplorerChartOptions, MetricsExplorerTimeOptions, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../public/containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedViewSavedObject } from '../../public/hooks/use_saved_view'; @@ -35,6 +35,9 @@ export const metricsExplorerViewSavedObjectMappings: { }, options: { properties: { + forceInterval: { + type: 'boolean', + }, metrics: { type: 'nested', properties: { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx new file mode 100644 index 0000000000000..8bcf0e9ed5be5 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_dropdown.tsx @@ -0,0 +1,62 @@ +/* + * 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, useCallback, useMemo } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertFlyout } from './alert_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +export const MetricsAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const kibana = useKibana(); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = useMemo(() => { + return [ + <EuiContextMenuItem icon="bell" key="createLink" onClick={() => setFlyoutVisible(true)}> + <FormattedMessage + id="xpack.infra.alerting.createAlertButton" + defaultMessage="Create alert" + /> + </EuiContextMenuItem>, + <EuiContextMenuItem + icon="tableOfContents" + key="manageLink" + href={kibana.services?.application?.getUrlForApp( + 'kibana#/management/kibana/triggersActions/alerts' + )} + > + <FormattedMessage id="xpack.infra.alerting.manageAlerts" defaultMessage="Manage alerts" /> + </EuiContextMenuItem>, + ]; + }, [kibana.services]); + + return ( + <> + <EuiPopover + button={ + <EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}> + <FormattedMessage id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts" /> + </EuiButtonEmpty> + } + isOpen={popoverOpen} + closePopover={closePopover} + > + <EuiContextMenuPanel items={menuItems} /> + </EuiPopover> + <AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} /> + </> + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx new file mode 100644 index 0000000000000..38709c117c817 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/alert_flyout.tsx @@ -0,0 +1,54 @@ +/* + * 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, { useContext } from 'react'; +import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; + +interface Props { + visible?: boolean; + options?: Partial<MetricsExplorerOptions>; + series?: MetricsExplorerSeries; + setVisible: React.Dispatch<React.SetStateAction<boolean>>; +} + +export const AlertFlyout = (props: Props) => { + const { triggersActionsUI } = useContext(TriggerActionsContext); + const { services } = useKibana(); + + return ( + <> + {triggersActionsUI && ( + <AlertsContextProvider + value={{ + metadata: { + currentOptions: props.options, + series: props.series, + }, + toastNotifications: services.notifications?.toasts, + http: services.http, + docLinks: services.docLinks, + actionTypeRegistry: triggersActionsUI.actionTypeRegistry, + alertTypeRegistry: triggersActionsUI.alertTypeRegistry, + }} + > + <AlertAdd + addFlyoutVisible={props.visible!} + setAddFlyoutVisibility={props.setVisible} + alertTypeId={METRIC_THRESHOLD_ALERT_TYPE_ID} + canChangeTrigger={false} + consumer={'metrics'} + /> + </AlertsContextProvider> + )} + </> + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx new file mode 100644 index 0000000000000..5e14babddcb07 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -0,0 +1,359 @@ +/* + * 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, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react'; +import { + EuiSpacer, + EuiText, + EuiFormRow, + EuiButtonEmpty, + EuiCheckbox, + EuiToolTip, + EuiIcon, + EuiFieldSearch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + Comparator, + Aggregators, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +import { + ForLastExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricsExplorerGroupBy } from '../../../pages/metrics/metrics_explorer/components/group_by'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; + +import { ExpressionRow } from './expression_row'; +import { AlertContextMeta, TimeUnit, MetricExpression } from '../types'; +import { ExpressionChart } from './expression_chart'; + +interface Props { + errors: IErrorObject[]; + alertParams: { + criteria: MetricExpression[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; + filterQueryText?: string; + alertOnNoData?: boolean; + }; + alertsContext: AlertsContextValue<AlertContextMeta>; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; +} + +const defaultExpression = { + aggType: Aggregators.AVERAGE, + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', +} as MetricExpression; + +export const Expressions: React.FC<Props> = props => { + const { setAlertParams, alertParams, errors, alertsContext } = props; + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: alertsContext.http.fetch, + toastWarning: alertsContext.toastNotifications.addWarning, + }); + const [timeSize, setTimeSize] = useState<number | undefined>(1); + const [timeUnit, setTimeUnit] = useState<TimeUnit>('m'); + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const options = useMemo<MetricsExplorerOptions>(() => { + if (alertsContext.metadata?.currentOptions?.metrics) { + return alertsContext.metadata.currentOptions as MetricsExplorerOptions; + } else { + return { + metrics: [], + aggregation: 'avg', + }; + } + }, [alertsContext.metadata]); + + const updateParams = useCallback( + (id, e: MetricExpression) => { + const exp = alertParams.criteria ? alertParams.criteria.slice() : []; + exp[id] = { ...exp[id], ...e }; + setAlertParams('criteria', exp); + }, + [setAlertParams, alertParams.criteria] + ); + + const addExpression = useCallback(() => { + const exp = alertParams.criteria?.slice() || []; + exp.push(defaultExpression); + setAlertParams('criteria', exp); + }, [setAlertParams, alertParams.criteria]); + + const removeExpression = useCallback( + (id: number) => { + const exp = alertParams.criteria?.slice() || []; + if (exp.length > 1) { + exp.splice(id, 1); + setAlertParams('criteria', exp); + } + }, + [setAlertParams, alertParams.criteria] + ); + + const onFilterChange = useCallback( + (filter: any) => { + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [setAlertParams, derivedIndexPattern] + ); + + const onGroupByChange = useCallback( + (group: string | null) => { + setAlertParams('groupBy', group || ''); + }, + [setAlertParams] + ); + + const emptyError = useMemo(() => { + return { + aggField: [], + timeSizeUnit: [], + timeWindowSize: [], + }; + }, []); + + const updateTimeSize = useCallback( + (ts: number | undefined) => { + const criteria = + alertParams.criteria?.map(c => ({ + ...c, + timeSize: ts, + })) || []; + setTimeSize(ts || undefined); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + const updateTimeUnit = useCallback( + (tu: string) => { + const criteria = + alertParams.criteria?.map(c => ({ + ...c, + timeUnit: tu, + })) || []; + setTimeUnit(tu as TimeUnit); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + useEffect(() => { + const md = alertsContext.metadata; + if (md) { + if (md.currentOptions?.metrics) { + setAlertParams( + 'criteria', + md.currentOptions.metrics.map(metric => ({ + metric: metric.field, + comparator: Comparator.GT, + threshold: [], + timeSize, + timeUnit, + aggType: metric.aggregation, + })) + ); + } else { + setAlertParams('criteria', [defaultExpression]); + } + + if (md.currentOptions) { + if (md.currentOptions.filterQuery) { + setAlertParams('filterQueryText', md.currentOptions.filterQuery); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) || + '' + ); + } else if (md.currentOptions.groupBy && md.series) { + const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`; + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + } + + setAlertParams('groupBy', md.currentOptions.groupBy); + } + setAlertParams('sourceId', source?.id); + } else { + if (!alertParams.criteria) { + setAlertParams('criteria', [defaultExpression]); + } + if (!alertParams.sourceId) { + setAlertParams('sourceId', source?.id || 'default'); + } + } + }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + const handleFieldSearchChange = useCallback( + (e: ChangeEvent<HTMLInputElement>) => onFilterChange(e.target.value), + [onFilterChange] + ); + + return ( + <> + <EuiSpacer size={'m'} /> + <EuiText size="xs"> + <h4> + <FormattedMessage + id="xpack.infra.metrics.alertFlyout.conditions" + defaultMessage="Conditions" + /> + </h4> + </EuiText> + <EuiSpacer size={'xs'} /> + {alertParams.criteria && + alertParams.criteria.map((e, idx) => { + return ( + <ExpressionRow + canDelete={(alertParams.criteria && alertParams.criteria.length > 1) || false} + fields={derivedIndexPattern.fields} + remove={removeExpression} + addExpression={addExpression} + key={idx} // idx's don't usually make good key's but here the index has semantic meaning + expressionId={idx} + setAlertParams={updateParams} + errors={errors[idx] || emptyError} + expression={e || {}} + > + <ExpressionChart + expression={e} + context={alertsContext} + derivedIndexPattern={derivedIndexPattern} + source={source} + filterQuery={alertParams.filterQuery} + groupBy={alertParams.groupBy} + /> + </ExpressionRow> + ); + })} + + <div style={{ marginLeft: 28 }}> + <ForLastExpression + timeWindowSize={timeSize} + timeWindowUnit={timeUnit} + errors={emptyError} + onChangeWindowSize={updateTimeSize} + onChangeWindowUnit={updateTimeUnit} + /> + </div> + + <div> + <EuiButtonEmpty + color={'primary'} + iconSide={'left'} + flush={'left'} + iconType={'plusInCircleFilled'} + onClick={addExpression} + > + <FormattedMessage + id="xpack.infra.metrics.alertFlyout.addCondition" + defaultMessage="Add condition" + /> + </EuiButtonEmpty> + </div> + + <EuiSpacer size={'m'} /> + <EuiCheckbox + id="metrics-alert-no-data-toggle" + label={ + <> + {i18n.translate('xpack.infra.metrics.alertFlyout.alertOnNoData', { + defaultMessage: "Alert me if there's no data", + })}{' '} + <EuiToolTip + content={i18n.translate('xpack.infra.metrics.alertFlyout.noDataHelpText', { + defaultMessage: + 'Enable this to trigger the action if the metric(s) do not report any data over the expected time period, or if the alert fails to query Elasticsearch', + })} + > + <EuiIcon type="questionInCircle" color="subdued" /> + </EuiToolTip> + </> + } + checked={alertParams.alertOnNoData} + onChange={e => setAlertParams('alertOnNoData', e.target.checked)} + /> + + <EuiSpacer size={'m'} /> + + <EuiFormRow + label={i18n.translate('xpack.infra.metrics.alertFlyout.filterLabel', { + defaultMessage: 'Filter (optional)', + })} + helpText={i18n.translate('xpack.infra.metrics.alertFlyout.filterHelpText', { + defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.', + })} + fullWidth + compressed + > + {(alertsContext.metadata && ( + <MetricsExplorerKueryBar + derivedIndexPattern={derivedIndexPattern} + onChange={onFilterChange} + onSubmit={onFilterChange} + value={alertParams.filterQueryText} + /> + )) || ( + <EuiFieldSearch + onChange={handleFieldSearchChange} + value={alertParams.filterQueryText} + fullWidth + /> + )} + </EuiFormRow> + + <EuiSpacer size={'m'} /> + <EuiFormRow + label={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerText', { + defaultMessage: 'Create alert per (optional)', + })} + helpText={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerHelpText', { + defaultMessage: + 'Create an alert for every unique value. For example: "host.id" or "cloud.region".', + })} + fullWidth + compressed + > + <MetricsExplorerGroupBy + onChange={onGroupByChange} + fields={derivedIndexPattern.fields} + options={{ + ...options, + groupBy: alertParams.groupBy || undefined, + }} + /> + </EuiFormRow> + </> + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx new file mode 100644 index 0000000000000..a600d59865ccc --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -0,0 +1,302 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { + Axis, + Chart, + niceTimeFormatter, + Position, + Settings, + TooltipValue, + RectAnnotation, +} from '@elastic/charts'; +import { first, last } from 'lodash'; +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IIndexPattern } from 'src/plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { InfraSource } from '../../../../common/http_api/source_api'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerColor, colorTransformer } from '../../../../common/color_palette'; +import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; +import { MetricExplorerSeriesChart } from '../../../pages/metrics/metrics_explorer/components/series_chart'; +import { MetricExpression, AlertContextMeta } from '../types'; +import { MetricsExplorerChartType } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { getChartTheme } from '../../../pages/metrics/metrics_explorer/components/helpers/get_chart_theme'; +import { createFormatterForMetric } from '../../../pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric'; +import { calculateDomain } from '../../../pages/metrics/metrics_explorer/components/helpers/calculate_domain'; +import { useMetricsExplorerChartData } from '../hooks/use_metrics_explorer_chart_data'; + +interface Props { + context: AlertsContextValue<AlertContextMeta>; + expression: MetricExpression; + derivedIndexPattern: IIndexPattern; + source: InfraSource | null; + filterQuery?: string; + groupBy?: string; +} + +const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), +}; + +const TIME_LABELS = { + s: i18n.translate('xpack.infra.metrics.alerts.timeLabels.seconds', { defaultMessage: 'seconds' }), + m: i18n.translate('xpack.infra.metrics.alerts.timeLabels.minutes', { defaultMessage: 'minutes' }), + h: i18n.translate('xpack.infra.metrics.alerts.timeLabels.hours', { defaultMessage: 'hours' }), + d: i18n.translate('xpack.infra.metrics.alerts.timeLabels.days', { defaultMessage: 'days' }), +}; + +export const ExpressionChart: React.FC<Props> = ({ + expression, + context, + derivedIndexPattern, + source, + filterQuery, + groupBy, +}) => { + const { loading, data } = useMetricsExplorerChartData( + expression, + context, + derivedIndexPattern, + source, + filterQuery, + groupBy + ); + + const metric = { + field: expression.metric, + aggregation: expression.aggType as MetricsExplorerAggregation, + color: MetricsExplorerColor.color0, + }; + const isDarkMode = context.uiSettings?.get('theme:darkMode') || false; + const dateFormatter = useMemo(() => { + const firstSeries = data ? first(data.series) : null; + return firstSeries && firstSeries.rows.length > 0 + ? niceTimeFormatter([first(firstSeries.rows).timestamp, last(firstSeries.rows).timestamp]) + : (value: number) => `${value}`; + }, [data]); + + const yAxisFormater = useCallback(createFormatterForMetric(metric), [expression]); + + if (loading || !data) { + return ( + <EmptyContainer> + <EuiText color="subdued"> + <FormattedMessage + id="xpack.infra.metrics.alerts.loadingMessage" + defaultMessage="Loading" + /> + </EuiText> + </EmptyContainer> + ); + } + + const thresholds = expression.threshold.slice().sort(); + + // Creating a custom series where the ID is changed to 0 + // so that we can get a proper domian + const firstSeries = first(data.series); + if (!firstSeries) { + return ( + <EmptyContainer> + <EuiText color="subdued">Oops, no chart data available</EuiText> + </EmptyContainer> + ); + } + + const series = { + ...firstSeries, + rows: firstSeries.rows.map(row => { + const newRow: MetricsExplorerRow = { + timestamp: row.timestamp, + metric_0: row.metric_0 || null, + }; + thresholds.forEach((thresholdValue, index) => { + newRow[`metric_threshold_${index}`] = thresholdValue; + }); + return newRow; + }), + }; + + const firstTimestamp = first(firstSeries.rows).timestamp; + const lastTimestamp = last(firstSeries.rows).timestamp; + const dataDomain = calculateDomain(series, [metric], false); + const domain = { + max: Math.max(dataDomain.max, last(thresholds) || dataDomain.max) * 1.1, // add 10% headroom. + min: Math.min(dataDomain.min, first(thresholds) || dataDomain.min), + }; + + if (domain.min === first(expression.threshold)) { + domain.min = domain.min * 0.9; + } + + const isAbove = [Comparator.GT, Comparator.GT_OR_EQ].includes(expression.comparator); + const opacity = 0.3; + const timeLabel = TIME_LABELS[expression.timeUnit]; + + return ( + <> + <ChartContainer> + <Chart> + <MetricExplorerSeriesChart + type={MetricsExplorerChartType.area} + metric={metric} + id="0" + series={series} + stack={false} + /> + {thresholds.length ? ( + <MetricExplorerSeriesChart + type={isAbove ? MetricsExplorerChartType.line : MetricsExplorerChartType.area} + metric={{ + ...metric, + color: MetricsExplorerColor.color1, + label: i18n.translate('xpack.infra.metrics.alerts.thresholdLabel', { + defaultMessage: 'Threshold', + }), + }} + id={thresholds.map((t, i) => `threshold_${i}`)} + series={series} + stack={false} + opacity={opacity} + /> + ) : null} + {thresholds.length && expression.comparator === Comparator.OUTSIDE_RANGE ? ( + <> + <MetricExplorerSeriesChart + type={MetricsExplorerChartType.line} + metric={{ + ...metric, + color: MetricsExplorerColor.color1, + label: i18n.translate('xpack.infra.metrics.alerts.thresholdLabel', { + defaultMessage: 'Threshold', + }), + }} + id={thresholds.map((t, i) => `threshold_${i}`)} + series={series} + stack={false} + opacity={opacity} + /> + <RectAnnotation + id="lower-threshold" + style={{ + fill: colorTransformer(MetricsExplorerColor.color1), + opacity, + }} + dataValues={[ + { + coordinates: { + x0: firstTimestamp, + x1: lastTimestamp, + y0: domain.min, + y1: first(expression.threshold), + }, + }, + ]} + /> + <RectAnnotation + id="upper-threshold" + style={{ + fill: colorTransformer(MetricsExplorerColor.color1), + opacity, + }} + dataValues={[ + { + coordinates: { + x0: firstTimestamp, + x1: lastTimestamp, + y0: last(expression.threshold), + y1: domain.max, + }, + }, + ]} + /> + </> + ) : null} + {isAbove ? ( + <RectAnnotation + id="upper-threshold" + style={{ + fill: colorTransformer(MetricsExplorerColor.color1), + opacity, + }} + dataValues={[ + { + coordinates: { + x0: firstTimestamp, + x1: lastTimestamp, + y0: first(expression.threshold), + y1: domain.max, + }, + }, + ]} + /> + ) : null} + <Axis + id={'timestamp'} + position={Position.Bottom} + showOverlappingTicks={true} + tickFormat={dateFormatter} + /> + <Axis id={'values'} position={Position.Left} tickFormat={yAxisFormater} domain={domain} /> + <Settings tooltip={tooltipProps} theme={getChartTheme(isDarkMode)} /> + </Chart> + </ChartContainer> + <div style={{ textAlign: 'center' }}> + {series.id !== 'ALL' ? ( + <EuiText size="xs" color="subdued"> + <FormattedMessage + id="xpack.infra.metrics.alerts.dataTimeRangeLabelWithGrouping" + defaultMessage="Last 20 {timeLabel} of data for {id}" + values={{ id: series.id, timeLabel }} + /> + </EuiText> + ) : ( + <EuiText size="xs" color="subdued"> + <FormattedMessage + id="xpack.infra.metrics.alerts.dataTimeRangeLabel" + defaultMessage="Last 20 {timeLabel}" + values={{ timeLabel }} + /> + </EuiText> + )} + </div> + </> + ); +}; + +const EmptyContainer: React.FC = ({ children }) => ( + <div + style={{ + width: '100%', + height: 150, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }} + > + {children} + </div> +); + +const ChartContainer: React.FC = ({ children }) => ( + <div + style={{ + width: '100%', + height: 150, + }} + > + {children} + </div> +); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx new file mode 100644 index 0000000000000..8801df380b48d --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx @@ -0,0 +1,237 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiSpacer } from '@elastic/eui'; +import { IFieldType } from 'src/plugins/data/public'; +import { + WhenExpression, + OfExpression, + ThresholdExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +import { euiStyled } from '../../../../../observability/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { MetricExpression, AGGREGATION_TYPES } from '../types'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; + +const customComparators = { + ...builtInComparators, + [Comparator.OUTSIDE_RANGE]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { + defaultMessage: 'Is not between', + }), + value: Comparator.OUTSIDE_RANGE, + requiredValues: 2, + }, +}; + +interface ExpressionRowProps { + fields: IFieldType[]; + expressionId: number; + expression: MetricExpression; + errors: IErrorObject; + canDelete: boolean; + addExpression(): void; + remove(id: number): void; + setAlertParams(id: number, params: MetricExpression): void; +} + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +export const ExpressionRow: React.FC<ExpressionRowProps> = props => { + const [isExpanded, setRowState] = useState(true); + const toggleRowState = useCallback(() => setRowState(!isExpanded), [isExpanded]); + const { + children, + setAlertParams, + expression, + errors, + expressionId, + remove, + fields, + canDelete, + } = props; + const { + aggType = AGGREGATION_TYPES.MAX, + metric, + comparator = Comparator.GT, + threshold = [], + } = expression; + + const updateAggType = useCallback( + (at: string) => { + setAlertParams(expressionId, { + ...expression, + aggType: at as MetricExpression['aggType'], + metric: at === 'count' ? undefined : expression.metric, + }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateMetric = useCallback( + (m?: MetricExpression['metric']) => { + setAlertParams(expressionId, { ...expression, metric: m }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, comparator: c as Comparator }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateThreshold = useCallback( + t => { + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } + }, + [expressionId, expression, setAlertParams] + ); + + return ( + <> + <EuiFlexGroup gutterSize="xs"> + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType={isExpanded ? 'arrowDown' : 'arrowRight'} + onClick={toggleRowState} + aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.expandRowLabel', { + defaultMessage: 'Expand row.', + })} + /> + </EuiFlexItem> + <EuiFlexItem grow> + <StyledExpressionRow> + <StyledExpression> + <WhenExpression + customAggTypesOptions={aggregationType} + aggType={aggType} + onChangeSelectedAggType={updateAggType} + /> + </StyledExpression> + {aggType !== 'count' && ( + <StyledExpression> + <OfExpression + customAggTypesOptions={aggregationType} + aggField={metric} + fields={fields.map(f => ({ + normalizedType: f.type, + name: f.name, + }))} + aggType={aggType} + errors={errors} + onChangeSelectedAggField={updateMetric} + /> + </StyledExpression> + )} + <StyledExpression> + <ThresholdExpression + thresholdComparator={comparator || Comparator.GT} + threshold={threshold} + customComparators={customComparators} + onChangeSelectedThresholdComparator={updateComparator} + onChangeSelectedThreshold={updateThreshold} + errors={errors} + /> + </StyledExpression> + </StyledExpressionRow> + </EuiFlexItem> + {canDelete && ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.removeCondition', { + defaultMessage: 'Remove condition', + })} + color={'danger'} + iconType={'trash'} + onClick={() => remove(expressionId)} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + {isExpanded ? <div style={{ padding: '0 0 0 28px' }}>{children}</div> : null} + <EuiSpacer size={'s'} /> + </> + ); +}; + +export const aggregationType: { [key: string]: any } = { + avg: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { + defaultMessage: 'Average', + }), + fieldRequired: true, + validNormalizedTypes: ['number'], + value: AGGREGATION_TYPES.AVERAGE, + }, + max: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', { + defaultMessage: 'Max', + }), + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MAX, + }, + min: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', { + defaultMessage: 'Min', + }), + fieldRequired: true, + validNormalizedTypes: ['number', 'date'], + value: AGGREGATION_TYPES.MIN, + }, + cardinality: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', { + defaultMessage: 'Cardinality', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.CARDINALITY, + validNormalizedTypes: ['number'], + }, + rate: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', { + defaultMessage: 'Rate', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.RATE, + validNormalizedTypes: ['number'], + }, + count: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', { + defaultMessage: 'Document count', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.COUNT, + validNormalizedTypes: ['number'], + }, + sum: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.sum', { + defaultMessage: 'Sum', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.SUM, + validNormalizedTypes: ['number'], + }, +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx new file mode 100644 index 0000000000000..42a764cb27229 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/validation.tsx @@ -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 { i18n } from '@kbn/i18n'; +import { + MetricExpressionParams, + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; + +export function validateMetricThreshold({ + criteria, +}: { + criteria: MetricExpressionParams[]; +}): ValidationResult { + const validationResult = { errors: {} }; + const errors: { + [id: string]: { + aggField: string[]; + timeSizeUnit: string[]; + timeWindowSize: string[]; + threshold0: string[]; + threshold1: string[]; + metric: string[]; + }; + } = {}; + validationResult.errors = errors; + + if (!criteria || !criteria.length) { + return validationResult; + } + + criteria.forEach((c, idx) => { + // Create an id for each criteria, so we can map errors to specific criteria. + const id = idx.toString(); + + errors[id] = errors[id] || { + aggField: [], + timeSizeUnit: [], + timeWindowSize: [], + threshold0: [], + threshold1: [], + metric: [], + }; + if (!c.aggType) { + errors[id].aggField.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', { + defaultMessage: 'Aggreation is required.', + }) + ); + } + + if (!c.threshold || !c.threshold.length) { + errors[id].threshold0.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (c.comparator === Comparator.BETWEEN && (!c.threshold || c.threshold.length < 2)) { + errors[id].threshold1.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (!c.timeSize) { + errors[id].timeWindowSize.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', { + defaultMessage: 'Time size is Required.', + }) + ); + } + + if (!c.metric && c.aggType !== 'count') { + errors[id].metric.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { + defaultMessage: 'Metric is required.', + }) + ); + } + }); + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts new file mode 100644 index 0000000000000..67f66bf742f43 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -0,0 +1,59 @@ +/* + * 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 { IIndexPattern } from 'src/plugins/data/public'; +import { useMemo } from 'react'; +import { InfraSource } from '../../../../common/http_api/source_api'; +import { AlertContextMeta, MetricExpression } from '../types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +import { MetricsExplorerOptions } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data'; + +export const useMetricsExplorerChartData = ( + expression: MetricExpression, + context: AlertsContextValue<AlertContextMeta>, + derivedIndexPattern: IIndexPattern, + source: InfraSource | null, + filterQuery?: string, + groupBy?: string +) => { + const { timeSize, timeUnit } = expression || { timeSize: 1, timeUnit: 'm' }; + const options: MetricsExplorerOptions = useMemo( + () => ({ + limit: 1, + forceInterval: true, + groupBy, + filterQuery, + metrics: [ + { + field: expression.metric, + aggregation: expression.aggType, + }, + ], + aggregation: expression.aggType || 'avg', + }), + [expression.aggType, expression.metric, filterQuery, groupBy] + ); + const timerange = useMemo( + () => ({ + interval: `>=${timeSize || 1}${timeUnit}`, + from: `now-${(timeSize || 1) * 20}${timeUnit}`, + to: 'now', + }), + [timeSize, timeUnit] + ); + + return useMetricsExplorerData( + options, + source?.configuration, + derivedIndexPattern, + timerange, + null, + null, + context.http.fetch + ); +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts new file mode 100644 index 0000000000000..91b9bafad5011 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; +import { Expressions } from './components/expression'; +import { validateMetricThreshold } from './components/validation'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types'; + +export function createMetricThresholdAlertType(): AlertTypeModel { + return { + id: METRIC_THRESHOLD_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.alertFlyout.alertName', { + defaultMessage: 'Metric threshold', + }), + iconClass: 'bell', + alertParamsExpression: Expressions, + validate: validateMetricThreshold, + defaultActionMessage: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\} is in a state of \\{\\{context.alertState\\}\\} + +Reason: +\\{\\{context.reason\\}\\} +`, + } + ), + }; +} diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts new file mode 100644 index 0000000000000..0e631b1e333d7 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/lib/transform_metrics_explorer_data.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first } from 'lodash'; +import { MetricsExplorerResponse } from '../../../../common/http_api/metrics_explorer'; +import { MetricThresholdAlertParams, ExpressionChartSeries } from '../types'; + +export const transformMetricsExplorerData = ( + params: MetricThresholdAlertParams, + data: MetricsExplorerResponse | null +) => { + const { criteria } = params; + if (criteria && data) { + const firstSeries = first(data.series); + const series = firstSeries.rows.reduce((acc, row) => { + const { timestamp } = row; + criteria.forEach((item, index) => { + if (!acc[index]) { + acc[index] = []; + } + const value = (row[`metric_${index}`] as number) || 0; + acc[index].push({ timestamp, value }); + }); + return acc; + }, [] as ExpressionChartSeries); + return { id: firstSeries.id, series }; + } +}; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts new file mode 100644 index 0000000000000..af3baf191bed2 --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + MetricExpressionParams, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../server/lib/alerting/metric_threshold/types'; +import { MetricsExplorerOptions } from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; +import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; + +export interface AlertContextMeta { + currentOptions?: Partial<MetricsExplorerOptions>; + series?: MetricsExplorerSeries; +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; +export type MetricExpression = Omit<MetricExpressionParams, 'metric'> & { + metric?: string; +}; + +export enum AGGREGATION_TYPES { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', +} + +export interface MetricThresholdAlertParams { + criteria?: MetricExpression[]; + groupBy?: string; + filterQuery?: string; + sourceId?: string; +} + +export interface ExpressionChartRow { + timestamp: number; + value: number; +} + +export type ExpressionChartSeries = ExpressionChartRow[][]; + +export interface ExpressionChartData { + id: string; + series: ExpressionChartSeries; +} diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index ebf9562c38d7a..4c213700b62e6 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createBrowserHistory } from 'history'; import React from 'react'; import ReactDOM from 'react-dom'; import { ApolloProvider } from 'react-apollo'; @@ -25,6 +24,7 @@ import { AppRouter } from '../routers'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; import '../index.scss'; +import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt'; export const CONTAINER_CLASSNAME = 'infra-container-element'; @@ -36,8 +36,8 @@ export async function startApp( Router: AppRouter, triggersActionsUI: TriggersAndActionsUIPublicPluginSetup ) { - const { element, appBasePath } = params; - const history = createBrowserHistory({ basename: appBasePath }); + const { element, history } = params; + const InfraPluginRoot: React.FunctionComponent = () => { const [darkMode] = useUiSetting$<boolean>('theme:darkMode'); @@ -49,7 +49,9 @@ export async function startApp( <ApolloClientContext.Provider value={libs.apolloClient}> <EuiThemeProvider darkMode={darkMode}> <HistoryContext.Provider value={history}> - <Router history={history} /> + <NavigationWarningPromptProvider> + <Router history={history} /> + </NavigationWarningPromptProvider> </HistoryContext.Provider> </EuiThemeProvider> </ApolloClientContext.Provider> diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx new file mode 100644 index 0000000000000..d2904206875c7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_dropdown.tsx @@ -0,0 +1,62 @@ +/* + * 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, useCallback, useMemo } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertFlyout } from './alert_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +export const InventoryAlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const kibana = useKibana(); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = useMemo(() => { + return [ + <EuiContextMenuItem icon="bell" key="createLink" onClick={() => setFlyoutVisible(true)}> + <FormattedMessage + id="xpack.infra.alerting.createAlertButton" + defaultMessage="Create alert" + /> + </EuiContextMenuItem>, + <EuiContextMenuItem + icon="tableOfContents" + key="manageLink" + href={kibana.services?.application?.getUrlForApp( + 'kibana#/management/kibana/triggersActions/alerts' + )} + > + <FormattedMessage id="xpack.infra.alerting.manageAlerts" defaultMessage="Manage alerts" /> + </EuiContextMenuItem>, + ]; + }, [kibana.services]); + + return ( + <> + <EuiPopover + button={ + <EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}> + <FormattedMessage id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts" /> + </EuiButtonEmpty> + } + isOpen={popoverOpen} + closePopover={closePopover} + > + <EuiContextMenuPanel items={menuItems} /> + </EuiPopover> + <AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} /> + </> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx new file mode 100644 index 0000000000000..83298afd4fc5a --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/alert_flyout.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +interface Props { + visible?: boolean; + options?: Partial<InfraWaffleMapOptions>; + nodeType?: InventoryItemType; + filter?: string; + setVisible: React.Dispatch<React.SetStateAction<boolean>>; +} + +export const AlertFlyout = (props: Props) => { + const { triggersActionsUI } = useContext(TriggerActionsContext); + const { services } = useKibana(); + + return ( + <> + {triggersActionsUI && ( + <AlertsContextProvider + value={{ + metadata: { options: props.options, nodeType: props.nodeType, filter: props.filter }, + toastNotifications: services.notifications?.toasts, + http: services.http, + docLinks: services.docLinks, + actionTypeRegistry: triggersActionsUI.actionTypeRegistry, + alertTypeRegistry: triggersActionsUI.alertTypeRegistry, + }} + > + <AlertAdd + addFlyoutVisible={props.visible!} + setAddFlyoutVisibility={props.setVisible} + alertTypeId={METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID} + canChangeTrigger={false} + consumer={'metrics'} + /> + </AlertsContextProvider> + )} + </> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx new file mode 100644 index 0000000000000..15cad770836bd --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -0,0 +1,498 @@ +/* + * 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, { useCallback, useMemo, useEffect, useState, ChangeEvent } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiSpacer, + EuiText, + EuiFormRow, + EuiButtonEmpty, + EuiFieldSearch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + Comparator, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../server/lib/alerting/metric_threshold/types'; +import { euiStyled } from '../../../../../observability/public'; +import { + ThresholdExpression, + ForLastExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MetricsExplorerKueryBar } from '../../../pages/metrics/metrics_explorer/components/kuery_bar'; +import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; +import { toMetricOpt } from '../../../pages/metrics/inventory_view/components/toolbars/toolbar_wrapper'; +import { sqsMetricTypes } from '../../../../common/inventory_models/aws_sqs/toolbar_items'; +import { ec2MetricTypes } from '../../../../common/inventory_models/aws_ec2/toolbar_items'; +import { s3MetricTypes } from '../../../../common/inventory_models/aws_s3/toolbar_items'; +import { rdsMetricTypes } from '../../../../common/inventory_models/aws_rds/toolbar_items'; +import { hostMetricTypes } from '../../../../common/inventory_models/host/toolbar_items'; +import { containerMetricTypes } from '../../../../common/inventory_models/container/toolbar_items'; +import { podMetricTypes } from '../../../../common/inventory_models/pod/toolbar_items'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { InventoryMetricConditions } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; +import { MetricExpression } from './metric'; +import { NodeTypeExpression } from './node_type'; +import { InfraWaffleMapOptions } from '../../../lib/lib'; +import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; + +interface AlertContextMeta { + options?: Partial<InfraWaffleMapOptions>; + nodeType?: InventoryItemType; + filter?: string; +} + +interface Props { + errors: IErrorObject[]; + alertParams: { + criteria: InventoryMetricConditions[]; + nodeType: InventoryItemType; + groupBy?: string; + filterQuery?: string; + filterQueryText?: string; + sourceId?: string; + }; + alertsContext: AlertsContextValue<AlertContextMeta>; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; +} + +type TimeUnit = 's' | 'm' | 'h' | 'd'; + +const defaultExpression = { + metric: 'cpu' as SnapshotMetricType, + comparator: Comparator.GT, + threshold: [], + timeSize: 1, + timeUnit: 'm', +} as InventoryMetricConditions; + +export const Expressions: React.FC<Props> = props => { + const { setAlertParams, alertParams, errors, alertsContext } = props; + const { source, createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + fetch: alertsContext.http.fetch, + toastWarning: alertsContext.toastNotifications.addWarning, + }); + const [timeSize, setTimeSize] = useState<number | undefined>(1); + const [timeUnit, setTimeUnit] = useState<TimeUnit>('m'); + + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const updateParams = useCallback( + (id, e: InventoryMetricConditions) => { + const exp = alertParams.criteria ? alertParams.criteria.slice() : []; + exp[id] = { ...exp[id], ...e }; + setAlertParams('criteria', exp); + }, + [setAlertParams, alertParams.criteria] + ); + + const addExpression = useCallback(() => { + const exp = alertParams.criteria.slice(); + exp.push(defaultExpression); + setAlertParams('criteria', exp); + }, [setAlertParams, alertParams.criteria]); + + const removeExpression = useCallback( + (id: number) => { + const exp = alertParams.criteria.slice(); + if (exp.length > 1) { + exp.splice(id, 1); + setAlertParams('criteria', exp); + } + }, + [setAlertParams, alertParams.criteria] + ); + + const onFilterChange = useCallback( + (filter: any) => { + setAlertParams('filterQueryText', filter || ''); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + }, + [derivedIndexPattern, setAlertParams] + ); + + const emptyError = useMemo(() => { + return { + aggField: [], + timeSizeUnit: [], + timeWindowSize: [], + }; + }, []); + + const updateTimeSize = useCallback( + (ts: number | undefined) => { + const criteria = alertParams.criteria.map(c => ({ + ...c, + timeSize: ts, + })); + setTimeSize(ts || undefined); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + const updateTimeUnit = useCallback( + (tu: string) => { + const criteria = alertParams.criteria.map(c => ({ + ...c, + timeUnit: tu, + })); + setTimeUnit(tu as TimeUnit); + setAlertParams('criteria', criteria); + }, + [alertParams.criteria, setAlertParams] + ); + + const updateNodeType = useCallback( + (nt: any) => { + setAlertParams('nodeType', nt); + }, + [setAlertParams] + ); + + const handleFieldSearchChange = useCallback( + (e: ChangeEvent<HTMLInputElement>) => onFilterChange(e.target.value), + [onFilterChange] + ); + + useEffect(() => { + const md = alertsContext.metadata; + if (!alertParams.nodeType) { + if (md && md.nodeType) { + setAlertParams('nodeType', md.nodeType); + } else { + setAlertParams('nodeType', 'host'); + } + } + + if (!alertParams.criteria) { + if (md && md.options) { + setAlertParams('criteria', [ + { + ...defaultExpression, + metric: md.options.metric!.type, + } as InventoryMetricConditions, + ]); + } else { + setAlertParams('criteria', [defaultExpression]); + } + } + + if (!alertParams.filterQuery) { + if (md && md.filter) { + setAlertParams('filterQueryText', md.filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' + ); + } + } + + if (!alertParams.sourceId) { + setAlertParams('sourceId', source?.id); + } + }, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + <EuiSpacer size={'m'} /> + <EuiText size="xs"> + <h4> + <FormattedMessage + id="xpack.infra.metrics.alertFlyout.conditions" + defaultMessage="Conditions" + /> + </h4> + </EuiText> + <StyledExpression> + <NodeTypeExpression + options={nodeTypes} + value={alertParams.nodeType || 'host'} + onChange={updateNodeType} + /> + </StyledExpression> + <EuiSpacer size={'xs'} /> + {alertParams.criteria && + alertParams.criteria.map((e, idx) => { + return ( + <ExpressionRow + nodeType={alertParams.nodeType} + canDelete={alertParams.criteria.length > 1} + remove={removeExpression} + addExpression={addExpression} + key={idx} // idx's don't usually make good key's but here the index has semantic meaning + expressionId={idx} + setAlertParams={updateParams} + errors={errors[idx] || emptyError} + expression={e || {}} + /> + ); + })} + + <ForLastExpression + timeWindowSize={timeSize} + timeWindowUnit={timeUnit} + errors={emptyError} + onChangeWindowSize={updateTimeSize} + onChangeWindowUnit={updateTimeUnit} + /> + + <div> + <EuiButtonEmpty + color={'primary'} + iconSide={'left'} + flush={'left'} + iconType={'plusInCircleFilled'} + onClick={addExpression} + > + <FormattedMessage + id="xpack.infra.metrics.alertFlyout.addCondition" + defaultMessage="Add condition" + /> + </EuiButtonEmpty> + </div> + + <EuiSpacer size={'m'} /> + + <EuiFormRow + label={i18n.translate('xpack.infra.metrics.alertFlyout.filterLabel', { + defaultMessage: 'Filter (optional)', + })} + helpText={i18n.translate('xpack.infra.metrics.alertFlyout.filterHelpText', { + defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.', + })} + fullWidth + compressed + > + {(alertsContext.metadata && ( + <MetricsExplorerKueryBar + derivedIndexPattern={derivedIndexPattern} + onSubmit={onFilterChange} + onChange={onFilterChange} + value={alertParams.filterQueryText} + /> + )) || ( + <EuiFieldSearch + onChange={handleFieldSearchChange} + value={alertParams.filterQueryText} + fullWidth + /> + )} + </EuiFormRow> + + <EuiSpacer size={'m'} /> + </> + ); +}; + +interface ExpressionRowProps { + nodeType: InventoryItemType; + expressionId: number; + expression: Omit<InventoryMetricConditions, 'metric'> & { + metric?: SnapshotMetricType; + }; + errors: IErrorObject; + canDelete: boolean; + addExpression(): void; + remove(id: number): void; + setAlertParams(id: number, params: Partial<InventoryMetricConditions>): void; +} + +const StyledExpressionRow = euiStyled(EuiFlexGroup)` + display: flex; + flex-wrap: wrap; + margin: 0 -4px; +`; + +const StyledExpression = euiStyled.div` + padding: 0 4px; +`; + +export const ExpressionRow: React.FC<ExpressionRowProps> = props => { + const { setAlertParams, expression, errors, expressionId, remove, canDelete } = props; + const { metric, comparator = Comparator.GT, threshold = [] } = expression; + + const updateMetric = useCallback( + (m?: SnapshotMetricType) => { + setAlertParams(expressionId, { ...expression, metric: m }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateComparator = useCallback( + (c?: string) => { + setAlertParams(expressionId, { ...expression, comparator: c as Comparator | undefined }); + }, + [expressionId, expression, setAlertParams] + ); + + const updateThreshold = useCallback( + t => { + if (t.join() !== expression.threshold.join()) { + setAlertParams(expressionId, { ...expression, threshold: t }); + } + }, + [expressionId, expression, setAlertParams] + ); + + const ofFields = useMemo(() => { + let myMetrics = hostMetricTypes; + + switch (props.nodeType) { + case 'awsEC2': + myMetrics = ec2MetricTypes; + break; + case 'awsRDS': + myMetrics = rdsMetricTypes; + break; + case 'awsS3': + myMetrics = s3MetricTypes; + break; + case 'awsSQS': + myMetrics = sqsMetricTypes; + break; + case 'host': + myMetrics = hostMetricTypes; + break; + case 'pod': + myMetrics = podMetricTypes; + break; + case 'container': + myMetrics = containerMetricTypes; + break; + } + return myMetrics.map(toMetricOpt); + }, [props.nodeType]); + + return ( + <> + <EuiFlexGroup gutterSize="xs"> + <EuiFlexItem grow> + <StyledExpressionRow> + <StyledExpression> + <MetricExpression + metric={{ + value: metric!, + text: ofFields.find(v => v?.value === metric)?.text || '', + }} + metrics={ + ofFields.filter(m => m !== undefined && m.value !== undefined) as Array<{ + value: SnapshotMetricType; + text: string; + }> + } + onChange={updateMetric} + errors={errors} + /> + </StyledExpression> + <StyledExpression> + <ThresholdExpression + thresholdComparator={comparator || Comparator.GT} + threshold={threshold} + onChangeSelectedThresholdComparator={updateComparator} + onChangeSelectedThreshold={updateThreshold} + errors={errors} + /> + </StyledExpression> + {metric && ( + <StyledExpression> + <div style={{ display: 'flex', alignItems: 'center', height: '100%' }}> + <div>{metricUnit[metric]?.label || ''}</div> + </div> + </StyledExpression> + )} + </StyledExpressionRow> + </EuiFlexItem> + {canDelete && ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.removeCondition', { + defaultMessage: 'Remove condition', + })} + color={'danger'} + iconType={'trash'} + onClick={() => remove(expressionId)} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + <EuiSpacer size={'s'} /> + </> + ); +}; + +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + +export const nodeTypes: { [key: string]: any } = { + host: { + text: getDisplayNameForType('host'), + value: 'host', + }, + pod: { + text: getDisplayNameForType('pod'), + value: 'pod', + }, + container: { + text: getDisplayNameForType('container'), + value: 'container', + }, + awsEC2: { + text: getDisplayNameForType('awsEC2'), + value: 'awsEC2', + }, + awsS3: { + text: getDisplayNameForType('awsS3'), + value: 'awsS3', + }, + awsRDS: { + text: getDisplayNameForType('awsRDS'), + value: 'awsRDS', + }, + awsSQS: { + text: getDisplayNameForType('awsSQS'), + value: 'awsSQS', + }, +}; + +const metricUnit: Record<string, { label: string }> = { + count: { label: '' }, + cpu: { label: '%' }, + memory: { label: '%' }, + rx: { label: 'bits/s' }, + tx: { label: 'bits/s' }, + logRate: { label: '/s' }, + diskIOReadBytes: { label: 'bytes/s' }, + diskIOWriteBytes: { label: 'bytes/s' }, + s3BucketSize: { label: 'bytes' }, + s3TotalRequests: { label: '' }, + s3NumberOfObjects: { label: '' }, + s3UploadBytes: { label: 'bytes' }, + s3DownloadBytes: { label: 'bytes' }, + sqsOldestMessage: { label: 'seconds' }, +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx new file mode 100644 index 0000000000000..faafdf1b81eed --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx @@ -0,0 +1,150 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiExpression, + EuiPopover, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiComboBox, +} from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; + +interface Props { + metric?: { value: SnapshotMetricType; text: string }; + metrics: Array<{ value: string; text: string }>; + errors: IErrorObject; + onChange: (metric: SnapshotMetricType) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosition }: Props) => { + const [aggFieldPopoverOpen, setAggFieldPopoverOpen] = useState(false); + const firstFieldOption = { + text: i18n.translate('xpack.infra.metrics.alertFlyout.expression.metric.selectFieldLabel', { + defaultMessage: 'Select a metric', + }), + value: '', + }; + + const availablefieldsOptions = metrics.map(m => { + return { label: m.text, value: m.value }; + }, []); + + return ( + <EuiPopover + id="aggFieldPopover" + button={ + <EuiExpression + description={i18n.translate( + 'xpack.infra.metrics.alertFlyout.expression.metric.whenLabel', + { + defaultMessage: 'When', + } + )} + value={metric?.text || firstFieldOption.text} + isActive={aggFieldPopoverOpen || !metric} + onClick={() => { + setAggFieldPopoverOpen(true); + }} + color={metric ? 'secondary' : 'danger'} + /> + } + isOpen={aggFieldPopoverOpen} + closePopover={() => { + setAggFieldPopoverOpen(false); + }} + withTitle + anchorPosition={popupPosition ?? 'downRight'} + zIndex={8000} + > + <div> + <ClosablePopoverTitle onClose={() => setAggFieldPopoverOpen(false)}> + <FormattedMessage + id="xpack.infra.metrics.alertFlyout.expression.metric.popoverTitle" + defaultMessage="Metric" + /> + </ClosablePopoverTitle> + <EuiFlexGroup> + <EuiFlexItem grow={false} className="actOf__aggFieldContainer"> + <EuiFormRow + fullWidth + isInvalid={errors.metric.length > 0 && metric !== undefined} + error={errors.metric} + > + <EuiComboBox + fullWidth + singleSelection={{ asPlainText: true }} + data-test-subj="availablefieldsOptionsComboBox" + isInvalid={errors.metric.length > 0 && metric !== undefined} + placeholder={firstFieldOption.text} + options={availablefieldsOptions} + noSuggestions={!availablefieldsOptions.length} + selectedOptions={ + metric ? availablefieldsOptions.filter(a => a.value === metric.value) : [] + } + renderOption={(o: any) => o.label} + onChange={selectedOptions => { + if (selectedOptions.length > 0) { + onChange(selectedOptions[0].value as SnapshotMetricType); + setAggFieldPopoverOpen(false); + } + }} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + </div> + </EuiPopover> + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + <EuiPopoverTitle> + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem>{children}</EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType="cross" + color="danger" + aria-label={i18n.translate( + 'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel', + { + defaultMessage: 'Close', + } + )} + onClick={() => onClose()} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPopoverTitle> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts new file mode 100644 index 0000000000000..b7abaf5b36373 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric_inventory_threshold_alert_type.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; +import { Expressions } from './expression'; +import { validateMetricThreshold } from './validation'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/inventory_metric_threshold/types'; + +export function getInventoryMetricAlertType(): AlertTypeModel { + return { + id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertName', { + defaultMessage: 'Inventory', + }), + iconClass: 'bell', + alertParamsExpression: Expressions, + validate: validateMetricThreshold, + defaultActionMessage: i18n.translate( + 'xpack.infra.metrics.alerting.inventory.threshold.defaultActionMessage', + { + defaultMessage: `\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\} + +\\{\\{context.metricOf.condition0\\}\\} has crossed a threshold of \\{\\{context.thresholdOf.condition0\\}\\} +Current value is \\{\\{context.valueOf.condition0\\}\\} +`, + } + ), + }; +} diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx new file mode 100644 index 0000000000000..1623fc4e24dcb --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/node_type.tsx @@ -0,0 +1,115 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiExpression, EuiPopover, EuiFlexGroup, EuiFlexItem, EuiSelect } from '@elastic/eui'; +import { EuiPopoverTitle, EuiButtonIcon } from '@elastic/eui'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; + +interface WhenExpressionProps { + value: InventoryItemType; + options: { [key: string]: { text: string; value: InventoryItemType } }; + onChange: (value: InventoryItemType) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; +} + +export const NodeTypeExpression = ({ + value, + options, + onChange, + popupPosition, +}: WhenExpressionProps) => { + const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); + + return ( + <EuiPopover + button={ + <EuiExpression + data-test-subj="nodeTypeExpression" + description={i18n.translate( + 'xpack.infra.metrics.alertFlyout.expression.for.descriptionLabel', + { + defaultMessage: 'For', + } + )} + value={options[value].text} + isActive={aggTypePopoverOpen} + onClick={() => { + setAggTypePopoverOpen(true); + }} + /> + } + isOpen={aggTypePopoverOpen} + closePopover={() => { + setAggTypePopoverOpen(false); + }} + ownFocus + withTitle + anchorPosition={popupPosition ?? 'downLeft'} + > + <div> + <ClosablePopoverTitle onClose={() => setAggTypePopoverOpen(false)}> + <FormattedMessage + id="xpack.infra.metrics.alertFlyout.expression.for.popoverTitle" + defaultMessage="Inventory Type" + /> + </ClosablePopoverTitle> + <EuiSelect + data-test-subj="forExpressionSelect" + value={value} + fullWidth + onChange={e => { + onChange(e.target.value as InventoryItemType); + setAggTypePopoverOpen(false); + }} + options={Object.values(options).map(o => o)} + /> + </div> + </EuiPopover> + ); +}; + +interface ClosablePopoverTitleProps { + children: JSX.Element; + onClose: () => void; +} + +export const ClosablePopoverTitle = ({ children, onClose }: ClosablePopoverTitleProps) => { + return ( + <EuiPopoverTitle> + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem>{children}</EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonIcon + iconType="cross" + color="danger" + aria-label={i18n.translate( + 'xpack.infra.metrics.expressionItems.components.closablePopoverTitle.closeLabel', + { + defaultMessage: 'Close', + } + )} + onClick={() => onClose()} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPopoverTitle> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx new file mode 100644 index 0000000000000..803893dd5a323 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/inventory/validation.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; + +export function validateMetricThreshold({ + criteria, +}: { + criteria: MetricExpressionParams[]; +}): ValidationResult { + const validationResult = { errors: {} }; + const errors: { + [id: string]: { + timeSizeUnit: string[]; + timeWindowSize: string[]; + threshold0: string[]; + threshold1: string[]; + metric: string[]; + }; + } = {}; + validationResult.errors = errors; + + if (!criteria || !criteria.length) { + return validationResult; + } + + criteria.forEach((c, idx) => { + // Create an id for each criteria, so we can map errors to specific criteria. + const id = idx.toString(); + + errors[id] = errors[id] || { + timeSizeUnit: [], + timeWindowSize: [], + threshold0: [], + threshold1: [], + metric: [], + }; + + if (!c.threshold || !c.threshold.length) { + errors[id].threshold0.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) { + errors[id].threshold1.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { + defaultMessage: 'Threshold is required.', + }) + ); + } + + if (!c.timeSize) { + errors[id].timeWindowSize.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', { + defaultMessage: 'Time size is Required.', + }) + ); + } + + if (!c.metric && c.aggType !== 'count') { + errors[id].metric.push( + i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { + defaultMessage: 'Metric is required.', + }) + ); + } + }); + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx new file mode 100644 index 0000000000000..dd888639b6d07 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx @@ -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 React, { useState, useCallback, useMemo } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertFlyout } from './alert_flyout'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const AlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const manageAlertsLinkProps = useLinkProps( + { + app: 'kibana', + hash: 'management/kibana/triggersActions/alerts', + }, + { + hrefOnly: true, + } + ); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = useMemo(() => { + return [ + <EuiContextMenuItem icon="bell" key="createLink" onClick={() => setFlyoutVisible(true)}> + <FormattedMessage + id="xpack.infra.alerting.logs.createAlertButton" + defaultMessage="Create alert" + /> + </EuiContextMenuItem>, + <EuiContextMenuItem icon="tableOfContents" key="manageLink" {...manageAlertsLinkProps}> + <FormattedMessage + id="xpack.infra.alerting.logs.manageAlerts" + defaultMessage="Manage alerts" + /> + </EuiContextMenuItem>, + ]; + }, [manageAlertsLinkProps]); + + return ( + <> + <EuiPopover + button={ + <EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}> + <FormattedMessage id="xpack.infra.alerting.logs.alertsButton" defaultMessage="Alerts" /> + </EuiButtonEmpty> + } + isOpen={popoverOpen} + closePopover={closePopover} + > + <EuiContextMenuPanel items={menuItems} /> + </EuiPopover> + <AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} /> + </> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx new file mode 100644 index 0000000000000..b18c2e5b8d69c --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types'; + +interface Props { + visible?: boolean; + setVisible: React.Dispatch<React.SetStateAction<boolean>>; +} + +export const AlertFlyout = (props: Props) => { + const { triggersActionsUI } = useContext(TriggerActionsContext); + const { services } = useKibana(); + + return ( + <> + {triggersActionsUI && ( + <AlertsContextProvider + value={{ + metadata: {}, + toastNotifications: services.notifications?.toasts, + http: services.http, + docLinks: services.docLinks, + actionTypeRegistry: triggersActionsUI.actionTypeRegistry, + alertTypeRegistry: triggersActionsUI.alertTypeRegistry, + }} + > + <AlertAdd + addFlyoutVisible={props.visible!} + setAddFlyoutVisibility={props.setVisible} + alertTypeId={LOG_DOCUMENT_COUNT_ALERT_TYPE_ID} + canChangeTrigger={false} + consumer={'logs'} + /> + </AlertsContextProvider> + )} + </> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx new file mode 100644 index 0000000000000..a9b45a117c281 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx @@ -0,0 +1,54 @@ +/* + * 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 { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { IFieldType } from 'src/plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { Criterion } from './criterion'; +import { + LogDocumentCountAlertParams, + Criterion as CriterionType, +} from '../../../../../common/alerting/logs/types'; + +interface Props { + fields: IFieldType[]; + criteria?: LogDocumentCountAlertParams['criteria']; + updateCriterion: (idx: number, params: Partial<CriterionType>) => void; + removeCriterion: (idx: number) => void; + errors: IErrorObject; +} + +export const Criteria: React.FC<Props> = ({ + fields, + criteria, + updateCriterion, + removeCriterion, + errors, +}) => { + if (!criteria) return null; + return ( + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow> + {criteria.map((criterion, idx) => { + return ( + <Criterion + key={idx} + idx={idx} + fields={fields} + criterion={criterion} + updateCriterion={updateCriterion} + removeCriterion={removeCriterion} + canDelete={criteria.length > 1} + errors={errors[idx.toString()] as IErrorObject} + /> + ); + })} + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx new file mode 100644 index 0000000000000..e8cafecd94db1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx @@ -0,0 +1,269 @@ +/* + * 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, useMemo, useCallback } from 'react'; +import { + EuiPopoverTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiSelect, + EuiFieldNumber, + EuiExpression, + EuiFieldText, + EuiButtonIcon, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IFieldType } from 'src/plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { + Comparator, + Criterion as CriterionType, + ComparatorToi18nMap, +} from '../../../../../common/alerting/logs/types'; + +const firstCriterionFieldPrefix = i18n.translate( + 'xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix', + { + defaultMessage: 'with', + } +); + +const successiveCriterionFieldPrefix = i18n.translate( + 'xpack.infra.logs.alertFlyout.successiveCriterionFieldPrefix', + { + defaultMessage: 'and', + } +); + +const criterionFieldTitle = i18n.translate('xpack.infra.logs.alertFlyout.criterionFieldTitle', { + defaultMessage: 'Field', +}); + +const criterionComparatorValueTitle = i18n.translate( + 'xpack.infra.logs.alertFlyout.criterionComparatorValueTitle', + { + defaultMessage: 'Comparison : Value', + } +); + +const getCompatibleComparatorsForField = (fieldInfo: IFieldType | undefined) => { + if (fieldInfo?.type === 'number') { + return [ + { value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] }, + { value: Comparator.GT_OR_EQ, text: ComparatorToi18nMap[Comparator.GT_OR_EQ] }, + { value: Comparator.LT, text: ComparatorToi18nMap[Comparator.LT] }, + { value: Comparator.LT_OR_EQ, text: ComparatorToi18nMap[Comparator.LT_OR_EQ] }, + { value: Comparator.EQ, text: ComparatorToi18nMap[`${Comparator.EQ}:number`] }, + { value: Comparator.NOT_EQ, text: ComparatorToi18nMap[`${Comparator.NOT_EQ}:number`] }, + ]; + } else if (fieldInfo?.aggregatable) { + return [ + { value: Comparator.EQ, text: ComparatorToi18nMap[Comparator.EQ] }, + { value: Comparator.NOT_EQ, text: ComparatorToi18nMap[Comparator.NOT_EQ] }, + ]; + } else { + return [ + { value: Comparator.MATCH, text: ComparatorToi18nMap[Comparator.MATCH] }, + { value: Comparator.NOT_MATCH, text: ComparatorToi18nMap[Comparator.NOT_MATCH] }, + { value: Comparator.MATCH_PHRASE, text: ComparatorToi18nMap[Comparator.MATCH_PHRASE] }, + { + value: Comparator.NOT_MATCH_PHRASE, + text: ComparatorToi18nMap[Comparator.NOT_MATCH_PHRASE], + }, + ]; + } +}; + +const getFieldInfo = (fields: IFieldType[], fieldName: string): IFieldType | undefined => { + return fields.find(field => { + return field.name === fieldName; + }); +}; + +interface Props { + idx: number; + fields: IFieldType[]; + criterion: CriterionType; + updateCriterion: (idx: number, params: Partial<CriterionType>) => void; + removeCriterion: (idx: number) => void; + canDelete: boolean; + errors: IErrorObject; +} + +export const Criterion: React.FC<Props> = ({ + idx, + fields, + criterion, + updateCriterion, + removeCriterion, + canDelete, + errors, +}) => { + const [isFieldPopoverOpen, setIsFieldPopoverOpen] = useState(false); + const [isComparatorPopoverOpen, setIsComparatorPopoverOpen] = useState(false); + + const fieldOptions = useMemo(() => { + return fields.map(field => { + return { value: field.name, text: field.name }; + }); + }, [fields]); + + const fieldInfo: IFieldType | undefined = useMemo(() => { + return getFieldInfo(fields, criterion.field); + }, [fields, criterion]); + + const compatibleComparatorOptions = useMemo(() => { + return getCompatibleComparatorsForField(fieldInfo); + }, [fieldInfo]); + + const handleFieldChange = useCallback( + e => { + const fieldName = e.target.value; + const nextFieldInfo = getFieldInfo(fields, fieldName); + // If the field information we're dealing with has changed, reset the comparator and value. + if ( + fieldInfo && + nextFieldInfo && + (fieldInfo.type !== nextFieldInfo.type || + fieldInfo.aggregatable !== nextFieldInfo.aggregatable) + ) { + const compatibleComparators = getCompatibleComparatorsForField(nextFieldInfo); + updateCriterion(idx, { + field: fieldName, + comparator: compatibleComparators[0].value, + value: undefined, + }); + } else { + updateCriterion(idx, { field: fieldName }); + } + }, + [fieldInfo, fields, idx, updateCriterion] + ); + + return ( + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiPopover + id="criterion-field" + button={ + <EuiExpression + description={ + idx === 0 ? firstCriterionFieldPrefix : successiveCriterionFieldPrefix + } + uppercase={true} + value={criterion.field} + isActive={isFieldPopoverOpen} + color={errors.field.length === 0 ? 'secondary' : 'danger'} + onClick={() => setIsFieldPopoverOpen(true)} + /> + } + isOpen={isFieldPopoverOpen} + closePopover={() => setIsFieldPopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > + <div> + <EuiPopoverTitle>{criterionFieldTitle}</EuiPopoverTitle> + <EuiFormRow isInvalid={errors.field.length > 0} error={errors.field}> + <EuiSelect + compressed + value={criterion.field} + onChange={handleFieldChange} + options={fieldOptions} + /> + </EuiFormRow> + </div> + </EuiPopover> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiPopover + id="criterion-comparator-value" + button={ + <EuiExpression + description={ + ComparatorToi18nMap[`${criterion.comparator}:${fieldInfo?.type}`] + ? ComparatorToi18nMap[`${criterion.comparator}:${fieldInfo?.type}`] + : ComparatorToi18nMap[criterion.comparator] + } + uppercase={true} + value={criterion.value} + isActive={isComparatorPopoverOpen} + color={ + errors.comparator.length === 0 && errors.value.length === 0 + ? 'secondary' + : 'danger' + } + onClick={() => setIsComparatorPopoverOpen(true)} + /> + } + isOpen={isComparatorPopoverOpen} + closePopover={() => setIsComparatorPopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > + <div> + <EuiPopoverTitle>{criterionComparatorValueTitle}</EuiPopoverTitle> + <EuiFlexGroup gutterSize="l"> + <EuiFlexItem grow={false}> + <EuiFormRow isInvalid={errors.comparator.length > 0} error={errors.comparator}> + <EuiSelect + compressed + value={criterion.comparator} + onChange={e => + updateCriterion(idx, { comparator: e.target.value as Comparator }) + } + options={compatibleComparatorOptions} + /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFormRow isInvalid={errors.value.length > 0} error={errors.value}> + {fieldInfo?.type === 'number' ? ( + <EuiFieldNumber + compressed + value={criterion.value as number | undefined} + onChange={e => { + const number = parseInt(e.target.value, 10); + updateCriterion(idx, { value: number ? number : undefined }); + }} + /> + ) : ( + <EuiFieldText + compressed + value={criterion.value} + onChange={e => updateCriterion(idx, { value: e.target.value })} + /> + )} + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + </div> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + {canDelete && ( + <EuiFlexItem grow={false}> + <EuiButtonIcon + aria-label={i18n.translate('xpack.infra.logs.alertFlyout.removeCondition', { + defaultMessage: 'Remove condition', + })} + color={'danger'} + iconType={'trash'} + onClick={() => removeCriterion(idx)} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx new file mode 100644 index 0000000000000..308165ce08a9b --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx @@ -0,0 +1,127 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPopoverTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiSelect, + EuiFieldNumber, + EuiExpression, + EuiFormRow, +} from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { + Comparator, + ComparatorToi18nMap, + LogDocumentCountAlertParams, +} from '../../../../../common/alerting/logs/types'; + +const documentCountPrefix = i18n.translate('xpack.infra.logs.alertFlyout.documentCountPrefix', { + defaultMessage: 'when', +}); + +const getComparatorOptions = (): Array<{ + value: Comparator; + text: string; +}> => { + return [ + { value: Comparator.LT, text: ComparatorToi18nMap[Comparator.LT] }, + { value: Comparator.LT_OR_EQ, text: ComparatorToi18nMap[Comparator.LT_OR_EQ] }, + { value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] }, + { value: Comparator.GT_OR_EQ, text: ComparatorToi18nMap[Comparator.GT_OR_EQ] }, + ]; +}; + +interface Props { + comparator?: Comparator; + value?: number; + updateCount: (params: Partial<LogDocumentCountAlertParams['count']>) => void; + errors: IErrorObject; +} + +export const DocumentCount: React.FC<Props> = ({ comparator, value, updateCount, errors }) => { + const [isComparatorPopoverOpen, setComparatorPopoverOpenState] = useState(false); + const [isValuePopoverOpen, setIsValuePopoverOpen] = useState(false); + + const documentCountValue = i18n.translate('xpack.infra.logs.alertFlyout.documentCountValue', { + defaultMessage: '{value, plural, one {log entry} other {log entries}}', + values: { value }, + }); + + return ( + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiPopover + id="comparator" + button={ + <EuiExpression + description={documentCountPrefix} + uppercase={true} + value={comparator ? ComparatorToi18nMap[comparator] : ''} + isActive={isComparatorPopoverOpen} + onClick={() => setComparatorPopoverOpenState(true)} + /> + } + isOpen={isComparatorPopoverOpen} + closePopover={() => setComparatorPopoverOpenState(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > + <div> + <EuiPopoverTitle>{documentCountPrefix}</EuiPopoverTitle> + <EuiSelect + compressed + value={comparator} + onChange={e => updateCount({ comparator: e.target.value as Comparator })} + options={getComparatorOptions()} + /> + </div> + </EuiPopover> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiPopover + id="comparator" + button={ + <EuiExpression + description={value} + uppercase={true} + value={documentCountValue} + isActive={isValuePopoverOpen} + onClick={() => setIsValuePopoverOpen(true)} + color={errors.value.length === 0 ? 'secondary' : 'danger'} + /> + } + isOpen={isValuePopoverOpen} + closePopover={() => setIsValuePopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > + <div> + <EuiPopoverTitle>{documentCountValue}</EuiPopoverTitle> + <EuiFormRow isInvalid={errors.value.length > 0} error={errors.value}> + <EuiFieldNumber + compressed + value={value} + onChange={e => { + const number = parseInt(e.target.value, 10); + updateCount({ value: number ? number : undefined }); + }} + /> + </EuiFormRow> + </div> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx new file mode 100644 index 0000000000000..3aed0db53bf2c --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx @@ -0,0 +1,175 @@ +/* + * 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, { useCallback, useMemo, useEffect, useState } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + ForLastExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { useSource } from '../../../../containers/source'; +import { + LogDocumentCountAlertParams, + Comparator, + TimeUnit, +} from '../../../../../common/alerting/logs/types'; +import { DocumentCount } from './document_count'; +import { Criteria } from './criteria'; + +export interface ExpressionCriteria { + field?: string; + comparator?: Comparator; + value?: string | number; +} + +interface Props { + errors: IErrorObject; + alertParams: Partial<LogDocumentCountAlertParams>; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; +} + +const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; + +const DEFAULT_EXPRESSION = { + count: { + value: 75, + comparator: Comparator.GT, + }, + criteria: [DEFAULT_CRITERIA], + timeSize: 5, + timeUnit: 'm', +}; + +export const ExpressionEditor: React.FC<Props> = props => { + const { setAlertParams, alertParams, errors } = props; + const { createDerivedIndexPattern } = useSource({ sourceId: 'default' }); + const [timeSize, setTimeSize] = useState<number | undefined>(1); + const [timeUnit, setTimeUnit] = useState<TimeUnit>('m'); + const [hasSetDefaults, setHasSetDefaults] = useState<boolean>(false); + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('logs'), [ + createDerivedIndexPattern, + ]); + + const supportedFields = useMemo(() => { + if (derivedIndexPattern?.fields) { + return derivedIndexPattern.fields.filter(field => { + return (field.type === 'string' || field.type === 'number') && field.searchable; + }); + } else { + return []; + } + }, [derivedIndexPattern]); + + // Set the default expression (disables exhaustive-deps as we only want to run this once on mount) + useEffect(() => { + for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) { + setAlertParams(key, value); + setHasSetDefaults(true); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const updateCount = useCallback( + countParams => { + const nextCountParams = { ...alertParams.count, ...countParams }; + setAlertParams('count', nextCountParams); + }, + [alertParams.count, setAlertParams] + ); + + const updateCriterion = useCallback( + (idx, criterionParams) => { + const nextCriteria = alertParams.criteria?.map((criterion, index) => { + return idx === index ? { ...criterion, ...criterionParams } : criterion; + }); + setAlertParams('criteria', nextCriteria ? nextCriteria : []); + }, + [alertParams, setAlertParams] + ); + + const updateTimeSize = useCallback( + (ts: number | undefined) => { + setTimeSize(ts || undefined); + setAlertParams('timeSize', ts); + }, + [setTimeSize, setAlertParams] + ); + + const updateTimeUnit = useCallback( + (tu: string) => { + setTimeUnit(tu as TimeUnit); + setAlertParams('timeUnit', tu); + }, + [setAlertParams] + ); + + const addCriterion = useCallback(() => { + const nextCriteria = alertParams?.criteria + ? [...alertParams.criteria, DEFAULT_CRITERIA] + : [DEFAULT_CRITERIA]; + setAlertParams('criteria', nextCriteria); + }, [alertParams, setAlertParams]); + + const removeCriterion = useCallback( + idx => { + const nextCriteria = alertParams?.criteria?.filter((criterion, index) => { + return index !== idx; + }); + setAlertParams('criteria', nextCriteria); + }, + [alertParams, setAlertParams] + ); + + // Wait until field info has loaded + if (supportedFields.length === 0) return null; + // Wait until the alert param defaults have been set + if (!hasSetDefaults) return null; + + return ( + <> + <DocumentCount + comparator={alertParams.count?.comparator} + value={alertParams.count?.value} + updateCount={updateCount} + errors={errors.count as IErrorObject} + /> + + <Criteria + fields={supportedFields} + criteria={alertParams.criteria} + updateCriterion={updateCriterion} + removeCriterion={removeCriterion} + errors={errors.criteria as IErrorObject} + /> + + <ForLastExpression + timeWindowSize={timeSize} + timeWindowUnit={timeUnit} + onChangeWindowSize={updateTimeSize} + onChangeWindowUnit={updateTimeUnit} + errors={errors as { [key: string]: string[] }} + /> + + <div> + <EuiButtonEmpty + color={'primary'} + iconSide={'left'} + flush={'left'} + iconType={'plusInCircleFilled'} + onClick={addCriterion} + > + <FormattedMessage + id="xpack.infra.logs.alertFlyout.addCondition" + defaultMessage="Add condition" + /> + </EuiButtonEmpty> + </div> + </> + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx new file mode 100644 index 0000000000000..8b0fd5eb721b3 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx @@ -0,0 +1,7 @@ +/* + * 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 * from './editor'; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts new file mode 100644 index 0000000000000..18126ec583696 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts @@ -0,0 +1,29 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types'; +import { ExpressionEditor } from './expression_editor'; +import { validateExpression } from './validation'; + +export function getAlertType(): AlertTypeModel { + return { + id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.logs.alertFlyout.alertName', { + defaultMessage: 'Log threshold', + }), + iconClass: 'bell', + alertParamsExpression: ExpressionEditor, + validate: validateExpression, + defaultActionMessage: i18n.translate( + 'xpack.infra.logs.alerting.threshold.defaultActionMessage', + { + defaultMessage: `\\{\\{context.matchingDocuments\\}\\} log entries have matched the following conditions: \\{\\{context.conditions\\}\\}`, + } + ), + }; +} diff --git a/x-pack/plugins/infra/public/components/alerting/logs/validation.ts b/x-pack/plugins/infra/public/components/alerting/logs/validation.ts new file mode 100644 index 0000000000000..c8c513f57a9d7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/validation.ts @@ -0,0 +1,102 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; +import { LogDocumentCountAlertParams } from '../../../../common/alerting/logs/types'; + +export function validateExpression({ + count, + criteria, + timeSize, + timeUnit, +}: Partial<LogDocumentCountAlertParams>): ValidationResult { + const validationResult = { errors: {} }; + + // NOTE: In the case of components provided by the Alerting framework the error property names + // must match what they expect. + const errors: { + count: { + value: string[]; + }; + criteria: { + [id: string]: { + field: string[]; + comparator: string[]; + value: string[]; + }; + }; + timeWindowSize: string[]; + timeSizeUnit: string[]; + } = { + count: { + value: [], + }, + criteria: {}, + timeSizeUnit: [], + timeWindowSize: [], + }; + + validationResult.errors = errors; + + // Document count validation + if (typeof count?.value !== 'number') { + errors.count.value.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.documentCountRequired', { + defaultMessage: 'Document count is Required.', + }) + ); + } + + // Time validation + if (!timeSize) { + errors.timeWindowSize.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.timeSizeRequired', { + defaultMessage: 'Time size is Required.', + }) + ); + } + + if (criteria && criteria.length > 0) { + // Criteria validation + criteria.forEach((criterion, idx: number) => { + const id = idx.toString(); + + errors.criteria[id] = { + field: [], + comparator: [], + value: [], + }; + + if (!criterion.field) { + errors.criteria[id].field.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionFieldRequired', { + defaultMessage: 'Field is required.', + }) + ); + } + + if (!criterion.comparator) { + errors.criteria[id].comparator.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionComparatorRequired', { + defaultMessage: 'Comparator is required.', + }) + ); + } + + if (!criterion.value) { + errors.criteria[id].value.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionValueRequired', { + defaultMessage: 'Value is required.', + }) + ); + } + }); + } + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx deleted file mode 100644 index bb664f4067662..0000000000000 --- a/x-pack/plugins/infra/public/components/alerting/metrics/alert_dropdown.tsx +++ /dev/null @@ -1,62 +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 React, { useState, useCallback, useMemo } from 'react'; -import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AlertFlyout } from './alert_flyout'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; - -export const AlertDropdown = () => { - const [popoverOpen, setPopoverOpen] = useState(false); - const [flyoutVisible, setFlyoutVisible] = useState(false); - const kibana = useKibana(); - - const closePopover = useCallback(() => { - setPopoverOpen(false); - }, [setPopoverOpen]); - - const openPopover = useCallback(() => { - setPopoverOpen(true); - }, [setPopoverOpen]); - - const menuItems = useMemo(() => { - return [ - <EuiContextMenuItem icon="bell" key="createLink" onClick={() => setFlyoutVisible(true)}> - <FormattedMessage - id="xpack.infra.alerting.createAlertButton" - defaultMessage="Create alert" - /> - </EuiContextMenuItem>, - <EuiContextMenuItem - icon="tableOfContents" - key="manageLink" - href={kibana.services?.application?.getUrlForApp( - 'kibana#/management/kibana/triggersActions/alerts' - )} - > - <FormattedMessage id="xpack.infra.alerting.manageAlerts" defaultMessage="Manage alerts" /> - </EuiContextMenuItem>, - ]; - }, [kibana.services]); - - return ( - <> - <EuiPopover - button={ - <EuiButtonEmpty iconSide={'right'} iconType={'arrowDown'} onClick={openPopover}> - <FormattedMessage id="xpack.infra.alerting.alertsButton" defaultMessage="Alerts" /> - </EuiButtonEmpty> - } - isOpen={popoverOpen} - closePopover={closePopover} - > - <EuiContextMenuPanel items={menuItems} /> - </EuiPopover> - <AlertFlyout setVisible={setFlyoutVisible} visible={flyoutVisible} /> - </> - ); -}; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx deleted file mode 100644 index 914054e1fd9b7..0000000000000 --- a/x-pack/plugins/infra/public/components/alerting/metrics/alert_flyout.tsx +++ /dev/null @@ -1,54 +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 React, { useContext } from 'react'; -import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; -import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; -import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; - -interface Props { - visible?: boolean; - options?: Partial<MetricsExplorerOptions>; - series?: MetricsExplorerSeries; - setVisible: React.Dispatch<React.SetStateAction<boolean>>; -} - -export const AlertFlyout = (props: Props) => { - const { triggersActionsUI } = useContext(TriggerActionsContext); - const { services } = useKibana(); - - return ( - <> - {triggersActionsUI && ( - <AlertsContextProvider - value={{ - metadata: { - currentOptions: props.options, - series: props.series, - }, - toastNotifications: services.notifications?.toasts, - http: services.http, - docLinks: services.docLinks, - actionTypeRegistry: triggersActionsUI.actionTypeRegistry, - alertTypeRegistry: triggersActionsUI.alertTypeRegistry, - }} - > - <AlertAdd - addFlyoutVisible={props.visible!} - setAddFlyoutVisibility={props.setVisible} - alertTypeId={METRIC_THRESHOLD_ALERT_TYPE_ID} - canChangeTrigger={false} - consumer={'metrics'} - /> - </AlertsContextProvider> - )} - </> - ); -}; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx deleted file mode 100644 index 0e9da32aaa509..0000000000000 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ /dev/null @@ -1,487 +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 React, { useCallback, useMemo, useEffect, useState } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiSpacer, - EuiText, - EuiFormRow, - EuiButtonEmpty, -} from '@elastic/eui'; -import { IFieldType } from 'src/plugins/data/public'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - MetricExpressionParams, - Comparator, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../server/lib/alerting/metric_threshold/types'; -import { euiStyled } from '../../../../../observability/public'; -import { - WhenExpression, - OfExpression, - ThresholdExpression, - ForLastExpression, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../triggers_actions_ui/public/common'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { MetricsExplorerKueryBar } from '../../metrics_explorer/kuery_bar'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { MetricsExplorerGroupBy } from '../../metrics_explorer/group_by'; -import { useSourceViaHttp } from '../../../containers/source/use_source_via_http'; - -interface AlertContextMeta { - currentOptions?: Partial<MetricsExplorerOptions>; - series?: MetricsExplorerSeries; -} - -interface Props { - errors: IErrorObject[]; - alertParams: { - criteria: MetricExpression[]; - groupBy?: string; - filterQuery?: string; - sourceId?: string; - }; - alertsContext: AlertsContextValue<AlertContextMeta>; - setAlertParams(key: string, value: any): void; - setAlertProperty(key: string, value: any): void; -} - -type TimeUnit = 's' | 'm' | 'h' | 'd'; -type MetricExpression = Omit<MetricExpressionParams, 'metric'> & { - metric?: string; -}; - -enum AGGREGATION_TYPES { - COUNT = 'count', - AVERAGE = 'avg', - SUM = 'sum', - MIN = 'min', - MAX = 'max', - RATE = 'rate', - CARDINALITY = 'cardinality', -} - -const defaultExpression = { - aggType: AGGREGATION_TYPES.AVERAGE, - comparator: Comparator.GT, - threshold: [], - timeSize: 1, - timeUnit: 'm', -} as MetricExpression; - -export const Expressions: React.FC<Props> = props => { - const { setAlertParams, alertParams, errors, alertsContext } = props; - const { source, createDerivedIndexPattern } = useSourceViaHttp({ - sourceId: 'default', - type: 'metrics', - fetch: alertsContext.http.fetch, - toastWarning: alertsContext.toastNotifications.addWarning, - }); - const [timeSize, setTimeSize] = useState<number | undefined>(1); - const [timeUnit, setTimeUnit] = useState<TimeUnit>('m'); - - const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ - createDerivedIndexPattern, - ]); - - const options = useMemo<MetricsExplorerOptions>(() => { - if (alertsContext.metadata?.currentOptions?.metrics) { - return alertsContext.metadata.currentOptions as MetricsExplorerOptions; - } else { - return { - metrics: [], - aggregation: 'avg', - }; - } - }, [alertsContext.metadata]); - - const updateParams = useCallback( - (id, e: MetricExpression) => { - const exp = alertParams.criteria ? alertParams.criteria.slice() : []; - exp[id] = { ...exp[id], ...e }; - setAlertParams('criteria', exp); - }, - [setAlertParams, alertParams.criteria] - ); - - const addExpression = useCallback(() => { - const exp = alertParams.criteria.slice(); - exp.push(defaultExpression); - setAlertParams('criteria', exp); - }, [setAlertParams, alertParams.criteria]); - - const removeExpression = useCallback( - (id: number) => { - const exp = alertParams.criteria.slice(); - if (exp.length > 1) { - exp.splice(id, 1); - setAlertParams('criteria', exp); - } - }, - [setAlertParams, alertParams.criteria] - ); - - const onFilterQuerySubmit = useCallback( - (filter: any) => { - setAlertParams('filterQuery', filter); - }, - [setAlertParams] - ); - - const onGroupByChange = useCallback( - (group: string | null) => { - setAlertParams('groupBy', group || undefined); - }, - [setAlertParams] - ); - - const emptyError = useMemo(() => { - return { - aggField: [], - timeSizeUnit: [], - timeWindowSize: [], - }; - }, []); - - const updateTimeSize = useCallback( - (ts: number | undefined) => { - const criteria = alertParams.criteria.map(c => ({ - ...c, - timeSize: ts, - })); - setTimeSize(ts || undefined); - setAlertParams('criteria', criteria); - }, - [alertParams.criteria, setAlertParams] - ); - - const updateTimeUnit = useCallback( - (tu: string) => { - const criteria = alertParams.criteria.map(c => ({ - ...c, - timeUnit: tu, - })); - setTimeUnit(tu as TimeUnit); - setAlertParams('criteria', criteria); - }, - [alertParams.criteria, setAlertParams] - ); - - useEffect(() => { - const md = alertsContext.metadata; - if (md) { - if (md.currentOptions?.metrics) { - setAlertParams( - 'criteria', - md.currentOptions.metrics.map(metric => ({ - metric: metric.field, - comparator: Comparator.GT, - threshold: [], - timeSize, - timeUnit, - aggType: metric.aggregation, - })) - ); - } else { - setAlertParams('criteria', [defaultExpression]); - } - - if (md.currentOptions) { - if (md.currentOptions.filterQuery) { - setAlertParams('filterQuery', md.currentOptions.filterQuery); - } else if (md.currentOptions.groupBy && md.series) { - const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`; - setAlertParams('filterQuery', filter); - } - - setAlertParams('groupBy', md.currentOptions.groupBy); - } - setAlertParams('sourceId', source?.id); - } else { - setAlertParams('criteria', [defaultExpression]); - } - }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps - - return ( - <> - <EuiSpacer size={'m'} /> - <EuiText size="xs"> - <h4> - <FormattedMessage - id="xpack.infra.metrics.alertFlyout.conditions" - defaultMessage="Conditions" - /> - </h4> - </EuiText> - <EuiSpacer size={'xs'} /> - {alertParams.criteria && - alertParams.criteria.map((e, idx) => { - return ( - <ExpressionRow - canDelete={alertParams.criteria.length > 1} - fields={derivedIndexPattern.fields} - remove={removeExpression} - addExpression={addExpression} - key={idx} // idx's don't usually make good key's but here the index has semantic meaning - expressionId={idx} - setAlertParams={updateParams} - errors={errors[idx] || emptyError} - expression={e || {}} - /> - ); - })} - - <ForLastExpression - timeWindowSize={timeSize} - timeWindowUnit={timeUnit} - errors={emptyError} - onChangeWindowSize={updateTimeSize} - onChangeWindowUnit={updateTimeUnit} - /> - - <div> - <EuiButtonEmpty - color={'primary'} - iconSide={'left'} - flush={'left'} - iconType={'plusInCircleFilled'} - onClick={addExpression} - > - <FormattedMessage - id="xpack.infra.metrics.alertFlyout.addCondition" - defaultMessage="Add condition" - /> - </EuiButtonEmpty> - </div> - - <EuiSpacer size={'m'} /> - - {alertsContext.metadata && ( - <> - <EuiFormRow - label={i18n.translate('xpack.infra.metrics.alertFlyout.filterLabel', { - defaultMessage: 'Filter (optional)', - })} - helpText={i18n.translate('xpack.infra.metrics.alertFlyout.filterHelpText', { - defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.', - })} - fullWidth - compressed - > - <MetricsExplorerKueryBar - derivedIndexPattern={derivedIndexPattern} - onSubmit={onFilterQuerySubmit} - value={alertParams.filterQuery} - /> - </EuiFormRow> - - <EuiSpacer size={'m'} /> - <EuiFormRow - label={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerText', { - defaultMessage: 'Create alert per (optional)', - })} - helpText={i18n.translate('xpack.infra.metrics.alertFlyout.createAlertPerHelpText', { - defaultMessage: - 'Create an alert for every unique value. For example: "host.id" or "cloud.region".', - })} - fullWidth - compressed - > - <MetricsExplorerGroupBy - onChange={onGroupByChange} - fields={derivedIndexPattern.fields} - options={{ - ...options, - groupBy: alertParams.groupBy || undefined, - }} - /> - </EuiFormRow> - </> - )} - </> - ); -}; - -interface ExpressionRowProps { - fields: IFieldType[]; - expressionId: number; - expression: MetricExpression; - errors: IErrorObject; - canDelete: boolean; - addExpression(): void; - remove(id: number): void; - setAlertParams(id: number, params: MetricExpression): void; -} - -const StyledExpressionRow = euiStyled(EuiFlexGroup)` - display: flex; - flex-wrap: wrap; - margin: 0 -4px; -`; - -const StyledExpression = euiStyled.div` - padding: 0 4px; -`; - -export const ExpressionRow: React.FC<ExpressionRowProps> = props => { - const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props; - const { - aggType = AGGREGATION_TYPES.MAX, - metric, - comparator = Comparator.GT, - threshold = [], - } = expression; - - const updateAggType = useCallback( - (at: string) => { - setAlertParams(expressionId, { - ...expression, - aggType: at as MetricExpression['aggType'], - metric: at === 'count' ? undefined : expression.metric, - }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateMetric = useCallback( - (m?: MetricExpression['metric']) => { - setAlertParams(expressionId, { ...expression, metric: m }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateComparator = useCallback( - (c?: string) => { - setAlertParams(expressionId, { ...expression, comparator: c as Comparator }); - }, - [expressionId, expression, setAlertParams] - ); - - const updateThreshold = useCallback( - t => { - if (t.join() !== expression.threshold.join()) { - setAlertParams(expressionId, { ...expression, threshold: t }); - } - }, - [expressionId, expression, setAlertParams] - ); - - return ( - <> - <EuiFlexGroup gutterSize="xs"> - <EuiFlexItem grow> - <StyledExpressionRow> - <StyledExpression> - <WhenExpression - customAggTypesOptions={aggregationType} - aggType={aggType} - onChangeSelectedAggType={updateAggType} - /> - </StyledExpression> - {aggType !== 'count' && ( - <StyledExpression> - <OfExpression - customAggTypesOptions={aggregationType} - aggField={metric} - fields={fields.map(f => ({ - normalizedType: f.type, - name: f.name, - }))} - aggType={aggType} - errors={errors} - onChangeSelectedAggField={updateMetric} - /> - </StyledExpression> - )} - <StyledExpression> - <ThresholdExpression - thresholdComparator={comparator || Comparator.GT} - threshold={threshold} - onChangeSelectedThresholdComparator={updateComparator} - onChangeSelectedThreshold={updateThreshold} - errors={errors} - /> - </StyledExpression> - </StyledExpressionRow> - </EuiFlexItem> - {canDelete && ( - <EuiFlexItem grow={false}> - <EuiButtonIcon - aria-label={i18n.translate('xpack.infra.metrics.alertFlyout.removeCondition', { - defaultMessage: 'Remove condition', - })} - color={'danger'} - iconType={'trash'} - onClick={() => remove(expressionId)} - /> - </EuiFlexItem> - )} - </EuiFlexGroup> - <EuiSpacer size={'s'} /> - </> - ); -}; - -export const aggregationType: { [key: string]: any } = { - avg: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.avg', { - defaultMessage: 'Average', - }), - fieldRequired: true, - validNormalizedTypes: ['number'], - value: AGGREGATION_TYPES.AVERAGE, - }, - max: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.max', { - defaultMessage: 'Max', - }), - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: AGGREGATION_TYPES.MAX, - }, - min: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.min', { - defaultMessage: 'Min', - }), - fieldRequired: true, - validNormalizedTypes: ['number', 'date'], - value: AGGREGATION_TYPES.MIN, - }, - cardinality: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.cardinality', { - defaultMessage: 'Cardinality', - }), - fieldRequired: false, - value: AGGREGATION_TYPES.CARDINALITY, - validNormalizedTypes: ['number'], - }, - rate: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.rate', { - defaultMessage: 'Rate', - }), - fieldRequired: false, - value: AGGREGATION_TYPES.RATE, - validNormalizedTypes: ['number'], - }, - count: { - text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.count', { - defaultMessage: 'Document count', - }), - fieldRequired: false, - value: AGGREGATION_TYPES.COUNT, - validNormalizedTypes: ['number'], - }, -}; diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts deleted file mode 100644 index 04179e34222c5..0000000000000 --- a/x-pack/plugins/infra/public/components/alerting/metrics/metric_threshold_alert_type.ts +++ /dev/null @@ -1,34 +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 { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; -import { Expressions } from './expression'; -import { validateMetricThreshold } from './validation'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../../server/lib/alerting/metric_threshold/types'; - -export function getAlertType(): AlertTypeModel { - return { - id: METRIC_THRESHOLD_ALERT_TYPE_ID, - name: i18n.translate('xpack.infra.metrics.alertFlyout.alertName', { - defaultMessage: 'Metric threshold', - }), - iconClass: 'bell', - alertParamsExpression: Expressions, - validate: validateMetricThreshold, - defaultActionMessage: i18n.translate( - 'xpack.infra.metrics.alerting.threshold.defaultActionMessage', - { - defaultMessage: `\\{\\{alertName\\}\\} - \\{\\{context.group\\}\\} - -\\{\\{context.metricOf.condition0\\}\\} has crossed a threshold of \\{\\{context.thresholdOf.condition0\\}\\} -Current value is \\{\\{context.valueOf.condition0\\}\\} -`, - } - ), - }; -} diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx deleted file mode 100644 index d84e46d08a287..0000000000000 --- a/x-pack/plugins/infra/public/components/alerting/metrics/validation.tsx +++ /dev/null @@ -1,89 +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 { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricExpressionParams } from '../../../../server/lib/alerting/metric_threshold/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; - -export function validateMetricThreshold({ - criteria, -}: { - criteria: MetricExpressionParams[]; -}): ValidationResult { - const validationResult = { errors: {} }; - const errors: { - [id: string]: { - aggField: string[]; - timeSizeUnit: string[]; - timeWindowSize: string[]; - threshold0: string[]; - threshold1: string[]; - metric: string[]; - }; - } = {}; - validationResult.errors = errors; - - if (!criteria || !criteria.length) { - return validationResult; - } - - criteria.forEach((c, idx) => { - // Create an id for each criteria, so we can map errors to specific criteria. - const id = idx.toString(); - - errors[id] = errors[id] || { - aggField: [], - timeSizeUnit: [], - timeWindowSize: [], - threshold0: [], - threshold1: [], - metric: [], - }; - if (!c.aggType) { - errors[id].aggField.push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.aggregationRequired', { - defaultMessage: 'Aggreation is required.', - }) - ); - } - - if (!c.threshold || !c.threshold.length) { - errors[id].threshold0.push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { - defaultMessage: 'Threshold is required.', - }) - ); - } - - if (c.comparator === 'between' && (!c.threshold || c.threshold.length < 2)) { - errors[id].threshold1.push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.thresholdRequired', { - defaultMessage: 'Threshold is required.', - }) - ); - } - - if (!c.timeSize) { - errors[id].timeWindowSize.push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.timeRequred', { - defaultMessage: 'Time size is Required.', - }) - ); - } - - if (!c.metric && c.aggType !== 'count') { - errors[id].metric.push( - i18n.translate('xpack.infra.metrics.alertFlyout.error.metricRequired', { - defaultMessage: 'Metric is required.', - }) - ); - } - }); - - return validationResult; -} diff --git a/x-pack/plugins/infra/public/components/inventory/layout.tsx b/x-pack/plugins/infra/public/components/inventory/layout.tsx deleted file mode 100644 index 3c91f9fa5946f..0000000000000 --- a/x-pack/plugins/infra/public/components/inventory/layout.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { useInterval } from 'react-use'; - -import { euiPaletteColorBlind } from '@elastic/eui'; -import { NodesOverview } from '../nodes_overview'; -import { Toolbar } from './toolbars/toolbar'; -import { PageContent } from '../page'; -import { useSnapshot } from '../../containers/waffle/use_snaphot'; -import { useInventoryMeta } from '../../containers/inventory_metadata/use_inventory_meta'; -import { useWaffleTimeContext } from '../../pages/inventory_view/hooks/use_waffle_time'; -import { useWaffleFiltersContext } from '../../pages/inventory_view/hooks/use_waffle_filters'; -import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; -import { useSourceContext } from '../../containers/source'; -import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../lib/lib'; - -const euiVisColorPalette = euiPaletteColorBlind(); - -export const Layout = () => { - const { sourceId, source } = useSourceContext(); - const { - metric, - groupBy, - nodeType, - accountId, - region, - changeView, - view, - autoBounds, - boundsOverride, - } = useWaffleOptionsContext(); - const { accounts, regions } = useInventoryMeta(sourceId, nodeType); - const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); - const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); - const { loading, nodes, reload, interval } = useSnapshot( - filterQueryAsJson, - metric, - groupBy, - nodeType, - sourceId, - currentTime, - accountId, - region - ); - - const options = { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - legend: { - type: 'gradient', - rules: [ - { value: 0, color: '#D3DAE6' }, - { value: 1, color: euiVisColorPalette[1] }, - ], - } as InfraWaffleMapGradientLegend, - metric, - fields: source?.configuration?.fields, - groupBy, - }; - - useInterval( - () => { - if (!loading) { - jumpToTime(Date.now()); - } - }, - isAutoReloading ? 5000 : null - ); - - return ( - <> - <Toolbar accounts={accounts} regions={regions} nodeType={nodeType} /> - <PageContent> - <NodesOverview - nodes={nodes} - options={options} - nodeType={nodeType} - loading={loading} - reload={reload} - onDrilldown={applyFilterQuery} - currentTime={currentTime} - onViewChange={changeView} - view={view} - autoBounds={autoBounds} - boundsOverride={boundsOverride} - interval={interval} - /> - </PageContent> - </> - ); -}; diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx b/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx deleted file mode 100644 index cb315d3e17b03..0000000000000 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/save_views.tsx +++ /dev/null @@ -1,21 +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 React from 'react'; -import { SavedViewsToolbarControls } from '../../saved_views/toolbar_control'; -import { inventoryViewSavedObjectType } from '../../../../common/saved_objects/inventory_view'; -import { useWaffleViewState } from '../../../pages/inventory_view/hooks/use_waffle_view_state'; - -export const SavedViews = () => { - const { viewState, defaultViewState, onViewChange } = useWaffleViewState(); - return ( - <SavedViewsToolbarControls - defaultViewState={defaultViewState} - viewState={viewState} - onViewChange={onViewChange} - viewType={inventoryViewSavedObjectType} - /> - ); -}; diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx deleted file mode 100644 index 63ab6d2f4465a..0000000000000 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar.tsx +++ /dev/null @@ -1,65 +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 React, { FunctionComponent } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; -import { - SnapshotMetricInput, - SnapshotGroupBy, - SnapshotCustomMetricInput, -} from '../../../../common/http_api/snapshot_api'; -import { InventoryCloudAccount } from '../../../../common/http_api/inventory_meta_api'; -import { findToolbar } from '../../../../common/inventory_models/toolbars'; -import { ToolbarWrapper } from './toolbar_wrapper'; - -import { InfraGroupByOptions } from '../../../lib/lib'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { WaffleOptionsState } from '../../../pages/inventory_view/hooks/use_waffle_options'; -import { SavedViews } from './save_views'; - -export interface ToolbarProps - extends Omit<WaffleOptionsState, 'view' | 'boundsOverride' | 'autoBounds'> { - createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; - changeMetric: (payload: SnapshotMetricInput) => void; - changeGroupBy: (payload: SnapshotGroupBy) => void; - changeCustomOptions: (payload: InfraGroupByOptions[]) => void; - changeAccount: (id: string) => void; - changeRegion: (name: string) => void; - accounts: InventoryCloudAccount[]; - regions: string[]; - changeCustomMetrics: (payload: SnapshotCustomMetricInput[]) => void; -} - -const wrapToolbarItems = ( - ToolbarItems: FunctionComponent<ToolbarProps>, - accounts: InventoryCloudAccount[], - regions: string[] -) => { - return ( - <ToolbarWrapper> - {props => ( - <> - <ToolbarItems {...props} accounts={accounts} regions={regions} /> - <EuiFlexItem grow={true} /> - <EuiFlexItem grow={false}> - <SavedViews /> - </EuiFlexItem> - </> - )} - </ToolbarWrapper> - ); -}; - -interface Props { - nodeType: InventoryItemType; - regions: string[]; - accounts: InventoryCloudAccount[]; -} -export const Toolbar = ({ nodeType, accounts, regions }: Props) => { - const ToolbarItems = findToolbar(nodeType); - return wrapToolbarItems(ToolbarItems, accounts, regions); -}; diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index 206e9821190fb..a8597b7073c95 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -9,7 +9,7 @@ import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } f import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; import { useVisibilityState } from '../../../utils/use_visibility_state'; -import { getTraceUrl } from '../../../../../../legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks'; +import { getTraceUrl } from '../../../../../apm/public'; import { LogEntriesItem } from '../../../../common/http_api'; import { useLinkProps, LinkDescriptor } from '../../../hooks/use_link_props'; import { decodeOrThrow } from '../../../../common/runtime_types'; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx index 72d6aea5ecfc6..c713839a1bba8 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/column_headers.tsx @@ -22,7 +22,7 @@ import { } from './log_entry_column'; import { ASSUMED_SCROLLBAR_WIDTH } from './vertical_scroll_panel'; import { LogPositionState } from '../../../containers/logs/log_position'; -import { localizedDate } from '../../../utils/formatters/datetime'; +import { localizedDate } from '../../../../common/formatters/datetime'; export const LogColumnHeaders: React.FunctionComponent<{ columnConfigurations: LogColumnConfiguration[]; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx index fbc450950b828..144caed744bab 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_date_row.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; -import { localizedDate } from '../../../utils/formatters/datetime'; +import { localizedDate } from '../../../../common/formatters/datetime'; interface LogDateRowProps { timestamp: number; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx index e02346c4e758a..976e4165eb6d5 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx @@ -24,29 +24,49 @@ interface LogEntryActionsColumnProps { isMenuOpen: boolean; onOpenMenu: () => void; onCloseMenu: () => void; - onViewDetails: () => void; + onViewDetails?: () => void; + onViewLogInContext?: () => void; } const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', { - defaultMessage: 'View Details', + defaultMessage: 'View actions for line', }); const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', { - defaultMessage: 'View actions for line', + defaultMessage: 'View details', }); +const LOG_VIEW_IN_CONTEXT_LABEL = i18n.translate( + 'xpack.infra.lobs.logEntryActionsViewInContextButton', + { + defaultMessage: 'View in context', + } +); + export const LogEntryActionsColumn: React.FC<LogEntryActionsColumnProps> = ({ isHovered, isMenuOpen, onOpenMenu, onCloseMenu, onViewDetails, + onViewLogInContext, }) => { const handleClickViewDetails = useCallback(() => { onCloseMenu(); - onViewDetails(); + + // Function might be `undefined` and the linter doesn't like that. + // eslint-disable-next-line no-unused-expressions + onViewDetails?.(); }, [onCloseMenu, onViewDetails]); + const handleClickViewInContext = useCallback(() => { + onCloseMenu(); + + // Function might be `undefined` and the linter doesn't like that. + // eslint-disable-next-line no-unused-expressions + onViewLogInContext?.(); + }, [onCloseMenu, onViewLogInContext]); + const button = ( <ButtonWrapper> <EuiButtonIcon @@ -72,6 +92,12 @@ export const LogEntryActionsColumn: React.FC<LogEntryActionsColumnProps> = ({ </SectionTitle> <SectionLinks> <SectionLink label={LOG_DETAILS_LABEL} onClick={handleClickViewDetails} /> + {onViewLogInContext !== undefined ? ( + <SectionLink + label={LOG_VIEW_IN_CONTEXT_LABEL} + onClick={handleClickViewInContext} + /> + ) : null} </SectionLinks> </Section> </ActionMenu> diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 7d7df796d13ad..5c20df000ae51 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -5,6 +5,7 @@ */ import React, { memo, useState, useCallback, useMemo } from 'react'; +import { isEmpty } from 'lodash'; import { euiStyled } from '../../../../../observability/public'; import { isTimestampColumn } from '../../../utils/log_entry'; @@ -32,6 +33,7 @@ interface LogEntryRowProps { isHighlighted: boolean; logEntry: LogEntry; openFlyoutWithItem?: (id: string) => void; + openViewLogInContext?: (entry: LogEntry) => void; scale: TextScale; wrap: boolean; } @@ -46,6 +48,7 @@ export const LogEntryRow = memo( isHighlighted, logEntry, openFlyoutWithItem, + openViewLogInContext, scale, wrap, }: LogEntryRowProps) => { @@ -63,6 +66,16 @@ export const LogEntryRow = memo( logEntry.id, ]); + const handleOpenViewLogInContext = useCallback(() => openViewLogInContext?.(logEntry), [ + openViewLogInContext, + logEntry, + ]); + + const hasContext = useMemo(() => !isEmpty(logEntry.context), [logEntry]); + const hasActionFlyoutWithItem = openFlyoutWithItem !== undefined; + const hasActionViewLogInContext = hasContext && openViewLogInContext !== undefined; + const hasActionsMenu = hasActionFlyoutWithItem || hasActionViewLogInContext; + const logEntryColumnsById = useMemo( () => logEntry.columns.reduce<{ @@ -165,18 +178,23 @@ export const LogEntryRow = memo( ); } })} - <LogEntryColumn - key="logColumn iconLogColumn iconLogColumn:details" - {...columnWidths[iconColumnId]} - > - <LogEntryActionsColumn - isHovered={isHovered} - isMenuOpen={isMenuOpen} - onOpenMenu={openMenu} - onCloseMenu={closeMenu} - onViewDetails={openFlyout} - /> - </LogEntryColumn> + {hasActionsMenu ? ( + <LogEntryColumn + key="logColumn iconLogColumn iconLogColumn:details" + {...columnWidths[iconColumnId]} + > + <LogEntryActionsColumn + isHovered={isHovered} + isMenuOpen={isMenuOpen} + onOpenMenu={openMenu} + onCloseMenu={closeMenu} + onViewDetails={hasActionFlyoutWithItem ? openFlyout : undefined} + onViewLogInContext={ + hasActionViewLogInContext ? handleOpenViewLogInContext : undefined + } + /> + </LogEntryColumn> + ) : null} </LogEntryRowWrapper> ); } diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index 2c389b47fa6cf..f89aaf12db1bc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -26,6 +26,7 @@ import { MeasurableItemView } from './measurable_item_view'; import { VerticalScrollPanel } from './vertical_scroll_panel'; import { useColumnWidths, LogEntryColumnWidths } from './log_entry_column'; import { LogDateRow } from './log_date_row'; +import { LogEntry } from '../../../../common/http_api'; interface ScrollableLogTextStreamViewProps { columnConfigurations: LogColumnConfiguration[]; @@ -50,8 +51,9 @@ interface ScrollableLogTextStreamViewProps { }) => any; loadNewerItems: () => void; reloadItems: () => void; - setFlyoutItem: (id: string) => void; - setFlyoutVisibility: (visible: boolean) => void; + setFlyoutItem?: (id: string) => void; + setFlyoutVisibility?: (visible: boolean) => void; + setContextEntry?: (entry: LogEntry) => void; highlightedItem: string | null; currentHighlightKey: UniqueTimeKey | null; startDateExpression: string; @@ -140,9 +142,16 @@ export class ScrollableLogTextStreamView extends React.PureComponent< lastLoadedTime, updateDateRange, startLiveStreaming, + setFlyoutItem, + setFlyoutVisibility, + setContextEntry, } = this.props; + const { targetId, items, isScrollLocked } = this.state; const hasItems = items.length > 0; + const hasFlyoutAction = !!(setFlyoutItem && setFlyoutVisibility); + const hasContextAction = !!setContextEntry; + return ( <ScrollableLogTextStreamViewWrapper> {isReloading && (!isStreaming || !hasItems) ? ( @@ -227,7 +236,14 @@ export class ScrollableLogTextStreamView extends React.PureComponent< <LogEntryRow columnConfigurations={columnConfigurations} columnWidths={columnWidths} - openFlyoutWithItem={this.handleOpenFlyout} + openFlyoutWithItem={ + hasFlyoutAction ? this.handleOpenFlyout : undefined + } + openViewLogInContext={ + hasContextAction + ? this.handleOpenViewLogInContext + : undefined + } boundingBoxRef={itemMeasureRef} logEntry={item.logEntry} highlights={item.highlights} @@ -287,8 +303,19 @@ export class ScrollableLogTextStreamView extends React.PureComponent< } private handleOpenFlyout = (id: string) => { - this.props.setFlyoutItem(id); - this.props.setFlyoutVisibility(true); + const { setFlyoutItem, setFlyoutVisibility } = this.props; + + if (setFlyoutItem && setFlyoutVisibility) { + setFlyoutItem(id); + setFlyoutVisibility(true); + } + }; + + private handleOpenViewLogInContext = (entry: LogEntry) => { + const { setContextEntry } = this.props; + if (setContextEntry) { + setContextEntry(entry); + } }; private handleReload = () => { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx index 69a6abbca4b34..0eb6140c0de84 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx @@ -33,7 +33,7 @@ export const hoveredContentStyle = css` `; export const highlightedContentStyle = css` - background-color: ${props => props.theme.eui.euiFocusBackgroundColor}; + background-color: ${props => props.theme.eui.euiColorHighlight}; `; export const longWrappedContentStyle = css` diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_options.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/chart_options.tsx deleted file mode 100644 index 657c4cea30f3a..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_options.tsx +++ /dev/null @@ -1,166 +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 React, { useState, useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { - EuiRadioGroup, - EuiButtonEmpty, - EuiPopover, - EuiForm, - EuiFormRow, - EuiSwitch, -} from '@elastic/eui'; -import { - MetricsExplorerChartOptions as ChartOptions, - MetricsExplorerYAxisMode, - MetricsExplorerChartType, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; - -interface Props { - chartOptions: ChartOptions; - onChange: (options: ChartOptions) => void; -} - -export const MetricsExplorerChartOptions = ({ chartOptions, onChange }: Props) => { - const [isPopoverOpen, setPopoverState] = useState<boolean>(false); - - const handleClosePopover = useCallback(() => { - setPopoverState(false); - }, []); - - const handleOpenPopover = useCallback(() => { - setPopoverState(true); - }, []); - - const button = ( - <EuiButtonEmpty iconSide="left" iconType="eye" onClick={handleOpenPopover}> - <FormattedMessage - id="xpack.infra.metricsExplorer.customizeChartOptions" - defaultMessage="Customize" - /> - </EuiButtonEmpty> - ); - - const yAxisRadios = [ - { - id: MetricsExplorerYAxisMode.auto, - label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.autoLabel', { - defaultMessage: 'Automatic (min to max)', - }), - }, - { - id: MetricsExplorerYAxisMode.fromZero, - label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.fromZeroLabel', { - defaultMessage: 'From zero (0 to max)', - }), - }, - ]; - - const typeRadios = [ - { - id: MetricsExplorerChartType.line, - label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.lineLabel', { - defaultMessage: 'Line', - }), - }, - { - id: MetricsExplorerChartType.area, - label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.areaLabel', { - defaultMessage: 'Area', - }), - }, - { - id: MetricsExplorerChartType.bar, - label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.barLabel', { - defaultMessage: 'Bar', - }), - }, - ]; - - const handleYAxisChange = useCallback( - (id: string) => { - onChange({ - ...chartOptions, - yAxisMode: id as MetricsExplorerYAxisMode, - }); - }, - [chartOptions, onChange] - ); - - const handleTypeChange = useCallback( - (id: string) => { - onChange({ - ...chartOptions, - type: id as MetricsExplorerChartType, - }); - }, - [chartOptions, onChange] - ); - - const handleStackChange = useCallback( - e => { - onChange({ - ...chartOptions, - stack: e.target.checked, - }); - }, - [chartOptions, onChange] - ); - - return ( - <EuiPopover - id="MetricExplorerChartOptionsPopover" - button={button} - isOpen={isPopoverOpen} - closePopover={handleClosePopover} - > - <EuiForm> - <EuiFormRow - compressed - label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.typeLabel', { - defaultMessage: 'Chart style', - })} - > - <EuiRadioGroup - compressed - options={typeRadios} - idSelected={chartOptions.type} - onChange={handleTypeChange} - /> - </EuiFormRow> - <EuiFormRow - compressed - label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.stackLabel', { - defaultMessage: 'Stack series', - })} - > - <EuiSwitch - label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.stackSwitchLabel', { - defaultMessage: 'Stack', - })} - checked={chartOptions.stack} - onChange={handleStackChange} - /> - </EuiFormRow> - <EuiFormRow - compressed - label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.yAxisDomainLabel', { - defaultMessage: 'Y Axis Domain', - })} - > - <EuiRadioGroup - compressed - options={yAxisRadios} - idSelected={chartOptions.yAxisMode} - onChange={handleYAxisChange} - /> - </EuiFormRow> - </EuiForm> - </EuiPopover> - ); -}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx deleted file mode 100644 index 750894fd0188b..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/group_by.tsx +++ /dev/null @@ -1,60 +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 { EuiComboBox } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import React, { useCallback } from 'react'; -import { IFieldType } from 'src/plugins/data/public'; -import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { isDisplayable } from '../../utils/is_displayable'; - -interface Props { - options: MetricsExplorerOptions; - onChange: (groupBy: string | null) => void; - fields: IFieldType[]; -} - -export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => { - const handleChange = useCallback( - selectedOptions => { - const groupBy = (selectedOptions.length === 1 && selectedOptions[0].label) || null; - onChange(groupBy); - }, - [onChange] - ); - - const metricPrefixes = options.metrics - .map( - metric => - (metric.field && - metric.field - .split(/\./) - .slice(0, 2) - .join('.')) || - null - ) - .filter(metric => metric) as string[]; - - return ( - <EuiComboBox - placeholder={i18n.translate('xpack.infra.metricsExplorer.groupByLabel', { - defaultMessage: 'Everything', - })} - aria-label={i18n.translate('xpack.infra.metricsExplorer.groupByAriaLabel', { - defaultMessage: 'Graph per', - })} - fullWidth - singleSelection={true} - selectedOptions={(options.groupBy && [{ label: options.groupBy }]) || []} - options={fields - .filter(f => isDisplayable(f, metricPrefixes) && f.aggregatable && f.type === 'string') - .map(f => ({ label: f.name }))} - onChange={handleChange} - isClearable={true} - /> - ); -}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.ts b/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.ts deleted file mode 100644 index b6453a81317b1..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.ts +++ /dev/null @@ -1,11 +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 { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; - -export const createMetricLabel = (metric: MetricsExplorerMetric) => { - return `${metric.aggregation}(${metric.field || ''})`; -}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx deleted file mode 100644 index dcc160d05b6ad..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/kuery_bar.tsx +++ /dev/null @@ -1,81 +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 { i18n } from '@kbn/i18n'; - -import React, { useEffect, useState } from 'react'; -import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; -import { AutocompleteField } from '../autocomplete_field'; -import { isDisplayable } from '../../utils/is_displayable'; -import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; - -interface Props { - derivedIndexPattern: IIndexPattern; - onSubmit: (query: string) => void; - value?: string | null; - placeholder?: string; -} - -function validateQuery(query: string) { - try { - esKuery.fromKueryExpression(query); - } catch (err) { - return false; - } - return true; -} - -export const MetricsExplorerKueryBar = ({ - derivedIndexPattern, - onSubmit, - value, - placeholder, -}: Props) => { - const [draftQuery, setDraftQuery] = useState<string>(value || ''); - const [isValid, setValidation] = useState<boolean>(true); - - // This ensures that if value changes out side this component it will update. - useEffect(() => { - if (value) { - setDraftQuery(value); - } - }, [value]); - - const handleChange = (query: string) => { - setValidation(validateQuery(query)); - setDraftQuery(query); - }; - - const filteredDerivedIndexPattern = { - ...derivedIndexPattern, - fields: derivedIndexPattern.fields.filter(field => isDisplayable(field)), - }; - - const defaultPlaceholder = i18n.translate( - 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', - { - defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)', - } - ); - - return ( - <WithKueryAutocompletion indexPattern={filteredDerivedIndexPattern}> - {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( - <AutocompleteField - aria-label={placeholder} - isLoadingSuggestions={isLoadingSuggestions} - isValid={isValid} - loadSuggestions={loadSuggestions} - onChange={handleChange} - onSubmit={onSubmit} - placeholder={placeholder || defaultPlaceholder} - suggestions={suggestions} - value={draftQuery} - /> - )} - </WithKueryAutocompletion> - ); -}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/series_chart.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/series_chart.tsx deleted file mode 100644 index ed7a994dd2bbe..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/series_chart.tsx +++ /dev/null @@ -1,107 +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 React from 'react'; -import { - ScaleType, - AreaSeries, - BarSeries, - RecursivePartial, - AreaSeriesStyle, - BarSeriesStyle, -} from '@elastic/charts'; -import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; -import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; -import { createMetricLabel } from './helpers/create_metric_label'; -import { - MetricsExplorerOptionsMetric, - MetricsExplorerChartType, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; - -interface Props { - metric: MetricsExplorerOptionsMetric; - id: string | number; - series: MetricsExplorerSeries; - type: MetricsExplorerChartType; - stack: boolean; -} - -export const MetricExplorerSeriesChart = (props: Props) => { - if (MetricsExplorerChartType.bar === props.type) { - return <MetricsExplorerBarChart {...props} />; - } - return <MetricsExplorerAreaChart {...props} />; -}; - -export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack }: Props) => { - const color = - (metric.color && colorTransformer(metric.color)) || - colorTransformer(MetricsExplorerColor.color0); - - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; - - const seriesAreaStyle: RecursivePartial<AreaSeriesStyle> = { - line: { - strokeWidth: 2, - visible: true, - }, - area: { - opacity: 0.5, - visible: type === MetricsExplorerChartType.area, - }, - }; - return ( - <AreaSeries - id={yAccessor} - key={chartId} - name={createMetricLabel(metric)} - xScaleType={ScaleType.Time} - yScaleType={ScaleType.Linear} - xAccessor="timestamp" - yAccessors={[yAccessor]} - data={series.rows} - stackAccessors={stack ? ['timestamp'] : void 0} - areaSeriesStyle={seriesAreaStyle} - color={color} - /> - ); -}; - -export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => { - const color = - (metric.color && colorTransformer(metric.color)) || - colorTransformer(MetricsExplorerColor.color0); - - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; - - const seriesBarStyle: RecursivePartial<BarSeriesStyle> = { - rectBorder: { - stroke: color, - strokeWidth: 1, - visible: true, - }, - rect: { - opacity: 1, - }, - }; - return ( - <BarSeries - id={yAccessor} - key={chartId} - name={createMetricLabel(metric)} - xScaleType={ScaleType.Time} - yScaleType={ScaleType.Linear} - xAccessor="timestamp" - yAccessors={[yAccessor]} - data={series.rows} - stackAccessors={stack ? ['timestamp'] : void 0} - barSeriesStyle={seriesBarStyle} - color={color} - /> - ); -}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx b/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx deleted file mode 100644 index 0fbb0b6acad17..0000000000000 --- a/x-pack/plugins/infra/public/components/metrics_explorer/toolbar.tsx +++ /dev/null @@ -1,148 +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 { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; -import { - MetricsExplorerMetric, - MetricsExplorerAggregation, -} from '../../../common/http_api/metrics_explorer'; -import { - MetricsExplorerOptions, - MetricsExplorerTimeOptions, - MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { Toolbar } from '../eui/toolbar'; -import { MetricsExplorerKueryBar } from './kuery_bar'; -import { MetricsExplorerMetrics } from './metrics'; -import { MetricsExplorerGroupBy } from './group_by'; -import { MetricsExplorerAggregationPicker } from './aggregation'; -import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options'; -import { SavedViewsToolbarControls } from '../saved_views/toolbar_control'; -import { MetricExplorerViewState } from '../../pages/infrastructure/metrics_explorer/use_metric_explorer_state'; -import { metricsExplorerViewSavedObjectType } from '../../../common/saved_objects/metrics_explorer_view'; -import { useKibanaUiSetting } from '../../utils/use_kibana_ui_setting'; -import { mapKibanaQuickRangesToDatePickerRanges } from '../../utils/map_timepicker_quickranges_to_datepicker_ranges'; - -interface Props { - derivedIndexPattern: IIndexPattern; - timeRange: MetricsExplorerTimeOptions; - options: MetricsExplorerOptions; - chartOptions: MetricsExplorerChartOptions; - defaultViewState: MetricExplorerViewState; - onRefresh: () => void; - onTimeChange: (start: string, end: string) => void; - onGroupByChange: (groupBy: string | null) => void; - onFilterQuerySubmit: (query: string) => void; - onMetricsChange: (metrics: MetricsExplorerMetric[]) => void; - onAggregationChange: (aggregation: MetricsExplorerAggregation) => void; - onChartOptionsChange: (chartOptions: MetricsExplorerChartOptions) => void; - onViewStateChange: (vs: MetricExplorerViewState) => void; -} - -export const MetricsExplorerToolbar = ({ - timeRange, - derivedIndexPattern, - options, - onTimeChange, - onRefresh, - onGroupByChange, - onFilterQuerySubmit, - onMetricsChange, - onAggregationChange, - chartOptions, - onChartOptionsChange, - defaultViewState, - onViewStateChange, -}: Props) => { - const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0; - const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); - const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); - - return ( - <Toolbar> - <EuiFlexGroup alignItems="center"> - <EuiFlexItem grow={options.aggregation === 'count' ? 2 : false}> - <MetricsExplorerAggregationPicker - fullWidth - options={options} - onChange={onAggregationChange} - /> - </EuiFlexItem> - {options.aggregation !== 'count' && ( - <EuiText size="s" color="subdued"> - <FormattedMessage - id="xpack.infra.metricsExplorer.aggregationLabel" - defaultMessage="of" - /> - </EuiText> - )} - {options.aggregation !== 'count' && ( - <EuiFlexItem grow={2}> - <MetricsExplorerMetrics - autoFocus={isDefaultOptions} - fields={derivedIndexPattern.fields} - options={options} - onChange={onMetricsChange} - /> - </EuiFlexItem> - )} - <EuiText size="s" color="subdued"> - <FormattedMessage - id="xpack.infra.metricsExplorer.groupByToolbarLabel" - defaultMessage="graph per" - /> - </EuiText> - <EuiFlexItem grow={1}> - <MetricsExplorerGroupBy - onChange={onGroupByChange} - fields={derivedIndexPattern.fields} - options={options} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFlexGroup alignItems="center"> - <EuiFlexItem> - <MetricsExplorerKueryBar - derivedIndexPattern={derivedIndexPattern} - onSubmit={onFilterQuerySubmit} - value={options.filterQuery} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <MetricsExplorerChartOptionsComponent - onChange={onChartOptionsChange} - chartOptions={chartOptions} - /> - </EuiFlexItem> - - <EuiFlexItem grow={false}> - <SavedViewsToolbarControls - defaultViewState={defaultViewState} - viewState={{ - options, - chartOptions, - currentTimerange: timeRange, - }} - viewType={metricsExplorerViewSavedObjectType} - onViewChange={onViewStateChange} - /> - </EuiFlexItem> - <EuiFlexItem grow={false} style={{ marginRight: 5 }}> - <EuiSuperDatePicker - start={timeRange.from} - end={timeRange.to} - onTimeChange={({ start, end }) => onTimeChange(start, end)} - onRefresh={onRefresh} - commonlyUsedRanges={commonlyUsedRanges} - /> - </EuiFlexItem> - </EuiFlexGroup> - </Toolbar> - ); -}; diff --git a/x-pack/plugins/infra/public/components/nodes_overview/index.tsx b/x-pack/plugins/infra/public/components/nodes_overview/index.tsx deleted file mode 100644 index ef22e0486f892..0000000000000 --- a/x-pack/plugins/infra/public/components/nodes_overview/index.tsx +++ /dev/null @@ -1,260 +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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { get, max, min } from 'lodash'; -import React from 'react'; - -import { euiStyled } from '../../../../observability/public'; -import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; -import { createFormatter } from '../../utils/formatters'; -import { NoData } from '../empty_states'; -import { InfraLoadingPanel } from '../loading'; -import { Map } from '../waffle/map'; -import { ViewSwitcher } from '../waffle/view_switcher'; -import { TableView } from './table'; -import { SnapshotNode, SnapshotCustomMetricInputRT } from '../../../common/http_api/snapshot_api'; -import { convertIntervalToString } from '../../utils/convert_interval_to_string'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { createFormatterForMetric } from '../metrics_explorer/helpers/create_formatter_for_metric'; - -export interface KueryFilterQuery { - kind: 'kuery'; - expression: string; -} - -interface Props { - options: InfraWaffleMapOptions; - nodeType: InventoryItemType; - nodes: SnapshotNode[]; - loading: boolean; - reload: () => void; - onDrilldown: (filter: KueryFilterQuery) => void; - currentTime: number; - onViewChange: (view: string) => void; - view: string; - boundsOverride: InfraWaffleMapBounds; - autoBounds: boolean; - interval: string; -} - -interface MetricFormatter { - formatter: InfraFormatterType; - template: string; - bounds?: { min: number; max: number }; -} - -interface MetricFormatters { - [key: string]: MetricFormatter; -} - -const METRIC_FORMATTERS: MetricFormatters = { - ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, - ['cpu']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['memory']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['logRate']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}/s', - }, - ['diskIOReadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['diskIOWriteBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['s3BucketSize']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3TotalRequests']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3NumberOfObjects']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3UploadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3DownloadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['sqsOldestMessage']: { - formatter: InfraFormatterType.number, - template: '{{value}} seconds', - }, -}; - -const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { - const maxValues = nodes.map(node => node.metric.max); - const minValues = nodes.map(node => node.metric.value); - // if there is only one value then we need to set the bottom range to zero for min - // otherwise the legend will look silly since both values are the same for top and - // bottom. - if (minValues.length === 1) { - minValues.unshift(0); - } - return { min: min(minValues) || 0, max: max(maxValues) || 0 }; -}; - -export const NodesOverview = class extends React.Component<Props, {}> { - public static displayName = 'Waffle'; - public render() { - const { - autoBounds, - boundsOverride, - loading, - nodes, - nodeType, - reload, - view, - currentTime, - options, - interval, - } = this.props; - if (loading) { - return ( - <InfraLoadingPanel - height="100%" - width="100%" - text={i18n.translate('xpack.infra.waffle.loadingDataText', { - defaultMessage: 'Loading data', - })} - /> - ); - } else if (!loading && nodes && nodes.length === 0) { - return ( - <NoData - titleText={i18n.translate('xpack.infra.waffle.noDataTitle', { - defaultMessage: 'There is no data to display.', - })} - bodyText={i18n.translate('xpack.infra.waffle.noDataDescription', { - defaultMessage: 'Try adjusting your time or filter.', - })} - refetchText={i18n.translate('xpack.infra.waffle.checkNewDataButtonLabel', { - defaultMessage: 'Check for new data', - })} - onRefetch={() => { - reload(); - }} - testString="noMetricsDataPrompt" - /> - ); - } - const dataBounds = calculateBoundsFromNodes(nodes); - const bounds = autoBounds ? dataBounds : boundsOverride; - const intervalAsString = convertIntervalToString(interval); - return ( - <MainContainer> - <ViewSwitcherContainer> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <ViewSwitcher view={view} onChange={this.handleViewChange} /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiText color="subdued"> - <p> - <FormattedMessage - id="xpack.infra.homePage.toolbar.showingLastOneMinuteDataText" - defaultMessage="Showing the last {duration} of data at the selected time" - values={{ duration: intervalAsString }} - /> - </p> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - </ViewSwitcherContainer> - {view === 'table' ? ( - <TableContainer> - <TableView - nodeType={nodeType} - nodes={nodes} - options={options} - formatter={this.formatter} - currentTime={currentTime} - onFilter={this.handleDrilldown} - /> - </TableContainer> - ) : ( - <MapContainer> - <Map - nodeType={nodeType} - nodes={nodes} - options={options} - formatter={this.formatter} - currentTime={currentTime} - onFilter={this.handleDrilldown} - bounds={bounds} - dataBounds={dataBounds} - /> - </MapContainer> - )} - </MainContainer> - ); - } - - private handleViewChange = (view: string) => this.props.onViewChange(view); - - // TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example. - private formatter = (val: string | number) => { - const { metric } = this.props.options; - if (SnapshotCustomMetricInputRT.is(metric)) { - const formatter = createFormatterForMetric(metric); - return formatter(val); - } - const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); - if (val == null) { - return ''; - } - const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); - return formatter(val); - }; - - private handleDrilldown = (filter: string) => { - this.props.onDrilldown({ - kind: 'kuery', - expression: filter, - }); - return; - }; -}; - -const MainContainer = euiStyled.div` - position: relative; - flex: 1 1 auto; -`; - -const TableContainer = euiStyled.div` - padding: ${props => props.theme.eui.paddingSizes.l}; -`; - -const ViewSwitcherContainer = euiStyled.div` - padding: ${props => props.theme.eui.paddingSizes.l}; -`; - -const MapContainer = euiStyled.div` - position: absolute; - display: flex; - top: 70px; - right: 0; - bottom: 0; - left: 0; -`; diff --git a/x-pack/plugins/infra/public/components/nodes_overview/table.tsx b/x-pack/plugins/infra/public/components/nodes_overview/table.tsx deleted file mode 100644 index 82991076255ee..0000000000000 --- a/x-pack/plugins/infra/public/components/nodes_overview/table.tsx +++ /dev/null @@ -1,170 +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 { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -import { last } from 'lodash'; -import React, { useState, useCallback, useEffect } from 'react'; -import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap'; -import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; -import { fieldToName } from '../waffle/lib/field_to_display_name'; -import { NodeContextMenu } from '../waffle/node_context_menu'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { SnapshotNode, SnapshotNodePath } from '../../../common/http_api/snapshot_api'; -import { CONTAINER_CLASSNAME } from '../../apps/start_app'; - -interface Props { - nodes: SnapshotNode[]; - nodeType: InventoryItemType; - options: InfraWaffleMapOptions; - currentTime: number; - formatter: (subject: string | number) => string; - onFilter: (filter: string) => void; -} - -const getGroupPaths = (path: SnapshotNodePath[]) => { - switch (path.length) { - case 3: - return path.slice(0, 2); - case 2: - return path.slice(0, 1); - default: - return []; - } -}; - -export const TableView = (props: Props) => { - const { nodes, options, formatter, currentTime, nodeType } = props; - const [openPopovers, setOpenPopovers] = useState<string[]>([]); - const openPopoverFor = useCallback( - (id: string) => () => { - setOpenPopovers([...openPopovers, id]); - }, - [openPopovers] - ); - - const closePopoverFor = useCallback( - (id: string) => () => { - if (openPopovers.includes(id)) { - setOpenPopovers(openPopovers.filter(subject => subject !== id)); - } - }, - [openPopovers] - ); - - useEffect(() => { - const el = document.getElementsByClassName(CONTAINER_CLASSNAME)[0]; - if (el instanceof HTMLElement) { - if (openPopovers.length > 0) { - el.style.overflowY = 'hidden'; - } else { - el.style.overflowY = 'auto'; - } - } - }, [openPopovers]); - - const columns: Array<EuiBasicTableColumn<typeof items[number]>> = [ - { - field: 'name', - name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }), - sortable: true, - truncateText: true, - textOnly: true, - render: (value: string, item: { node: InfraWaffleMapNode }) => { - const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`; - // For the table we need to create a UniqueID that takes into to account the groupings - // as well as the node name. There is the possibility that a node can be present in two - // different groups and be on the screen at the same time. - const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':'); - return ( - <NodeContextMenu - node={item.node} - nodeType={nodeType} - closePopover={closePopoverFor(uniqueID)} - currentTime={currentTime} - isPopoverOpen={openPopovers.includes(uniqueID)} - options={options} - popoverPosition="rightCenter" - > - <EuiToolTip content={tooltipText}> - <EuiButtonEmpty onClick={openPopoverFor(uniqueID)}>{value}</EuiButtonEmpty> - </EuiToolTip> - </NodeContextMenu> - ); - }, - }, - ...options.groupBy.map((grouping, index) => ({ - field: `group_${index}`, - name: fieldToName((grouping && grouping.field) || ''), - sortable: true, - truncateText: true, - textOnly: true, - render: (value: string) => { - const handleClick = () => props.onFilter(`${grouping.field}:"${value}"`); - return ( - <EuiToolTip content="Set Filter"> - <EuiButtonEmpty onClick={handleClick}>{value}</EuiButtonEmpty> - </EuiToolTip> - ); - }, - })), - { - field: 'value', - name: i18n.translate('xpack.infra.tableView.columnName.last1m', { - defaultMessage: 'Last 1m', - }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => <span>{formatter(value)}</span>, - }, - { - field: 'avg', - name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => <span>{formatter(value)}</span>, - }, - { - field: 'max', - name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }), - sortable: true, - truncateText: true, - dataType: 'number', - render: (value: number) => <span>{formatter(value)}</span>, - }, - ]; - - const items = nodes.map(node => { - const name = last(node.path); - return { - name: (name && name.label) || 'unknown', - ...getGroupPaths(node.path).reduce( - (acc, path, index) => ({ - ...acc, - [`group_${index}`]: path.label, - }), - {} - ), - value: node.metric.value, - avg: node.metric.avg, - max: node.metric.max, - node: createWaffleMapNode(node), - }; - }); - const initialSorting = { - sort: { - field: 'value', - direction: 'desc', - }, - } as const; - - return ( - <EuiInMemoryTable pagination={true} sorting={initialSorting} items={items} columns={columns} /> - ); -}; diff --git a/x-pack/plugins/infra/public/components/source_configuration/index.ts b/x-pack/plugins/infra/public/components/source_configuration/index.ts index 98825567cc204..66f64aee24f6e 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/index.ts +++ b/x-pack/plugins/infra/public/components/source_configuration/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './input_fields'; export { SourceConfigurationSettings } from './source_configuration_settings'; export { ViewSourceConfigurationButton } from './view_source_configuration_button'; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 36645fa3f1f35..7f248cd103003 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -17,7 +17,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useContext, useMemo } from 'react'; -import { Prompt } from 'react-router-dom'; import { Source } from '../../containers/source'; import { FieldsConfigurationPanel } from './fields_configuration_panel'; @@ -26,6 +25,7 @@ import { NameConfigurationPanel } from './name_configuration_panel'; import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; import { SourceLoadingPage } from '../source_loading_page'; +import { Prompt } from '../../utils/navigation_warning_prompt'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; @@ -100,10 +100,13 @@ export const SourceConfigurationSettings = ({ data-test-subj="sourceConfigurationContent" > <Prompt - when={isFormDirty} - message={i18n.translate('xpack.infra.sourceConfiguration.unsavedFormPrompt', { - defaultMessage: 'Are you sure you want to leave? Changes will be lost', - })} + prompt={ + isFormDirty + ? i18n.translate('xpack.infra.sourceConfiguration.unsavedFormPrompt', { + defaultMessage: 'Are you sure you want to leave? Changes will be lost', + }) + : undefined + } /> <EuiPanel paddingSize="l"> <NameConfigurationPanel diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts new file mode 100644 index 0000000000000..65cde03ec98e7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel } from '@elastic/eui'; +import { euiStyled } from '../../../observability/public'; + +export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ + grow: false, + paddingSize: 'none', +}))` + border-top: none; + border-right: none; + border-left: none; + border-radius: 0; + padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`}; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/legend.tsx b/x-pack/plugins/infra/public/components/waffle/legend.tsx deleted file mode 100644 index 13e533b225d4d..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/legend.tsx +++ /dev/null @@ -1,59 +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 React from 'react'; - -import { euiStyled } from '../../../../observability/public'; -import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../lib/lib'; -import { GradientLegend } from './gradient_legend'; -import { LegendControls } from './legend_controls'; -import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './lib/type_guards'; -import { StepLegend } from './steps_legend'; -import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; -interface Props { - legend: InfraWaffleMapLegend; - bounds: InfraWaffleMapBounds; - dataBounds: InfraWaffleMapBounds; - formatter: InfraFormatter; -} - -interface LegendControlOptions { - auto: boolean; - bounds: InfraWaffleMapBounds; -} - -export const Legend: React.FC<Props> = ({ dataBounds, legend, bounds, formatter }) => { - const { - changeBoundsOverride, - changeAutoBounds, - autoBounds, - boundsOverride, - } = useWaffleOptionsContext(); - return ( - <LegendContainer> - <LegendControls - dataBounds={dataBounds} - bounds={bounds} - autoBounds={autoBounds} - boundsOverride={boundsOverride} - onChange={(options: LegendControlOptions) => { - changeBoundsOverride(options.bounds); - changeAutoBounds(options.auto); - }} - /> - {isInfraWaffleMapGradientLegend(legend) && ( - <GradientLegend formatter={formatter} legend={legend} bounds={bounds} /> - )} - {isInfraWaffleMapStepLegend(legend) && <StepLegend formatter={formatter} legend={legend} />} - </LegendContainer> - ); -}; - -const LegendContainer = euiStyled.div` - position: absolute; - bottom: 10px; - left: 10px; - right: 10px; -`; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts b/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts deleted file mode 100644 index f793afee1b948..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/lib/type_guards.ts +++ /dev/null @@ -1,17 +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 { InfraWaffleMapGradientLegend, InfraWaffleMapStepLegend } from '../../../lib/lib'; - -export function isInfraWaffleMapStepLegend(subject: any): subject is InfraWaffleMapStepLegend { - return subject.type && subject.type === 'step'; -} - -export function isInfraWaffleMapGradientLegend( - subject: any -): subject is InfraWaffleMapGradientLegend { - return subject.type && subject.type === 'gradient'; -} diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/components/waffle/metric_control/index.tsx deleted file mode 100644 index 0f2034fe9cb25..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/index.tsx +++ /dev/null @@ -1,199 +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 { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState, useCallback } from 'react'; -import { IFieldType } from 'src/plugins/data/public'; -import { - SnapshotMetricInput, - SnapshotCustomMetricInput, - SnapshotCustomMetricInputRT, -} from '../../../../common/http_api/snapshot_api'; -import { CustomMetricForm } from './custom_metric_form'; -import { getCustomMetricLabel } from './get_custom_metric_label'; -import { MetricsContextMenu } from './metrics_context_menu'; -import { ModeSwitcher } from './mode_switcher'; -import { MetricsEditMode } from './metrics_edit_mode'; -import { CustomMetricMode } from './types'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; - -interface Props { - options: Array<{ text: string; value: string }>; - metric: SnapshotMetricInput; - fields: IFieldType[]; - onChange: (metric: SnapshotMetricInput) => void; - onChangeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void; - customMetrics: SnapshotCustomMetricInput[]; -} - -export const WaffleMetricControls = ({ - fields, - onChange, - onChangeCustomMetrics, - metric, - options, - customMetrics, -}: Props) => { - const [isPopoverOpen, setPopoverState] = useState<boolean>(false); - const [mode, setMode] = useState<CustomMetricMode>('pick'); - const [editModeCustomMetrics, setEditModeCustomMetrics] = useState<SnapshotCustomMetricInput[]>( - [] - ); - const [editCustomMetric, setEditCustomMetric] = useState<SnapshotCustomMetricInput | undefined>(); - const handleClose = useCallback(() => { - setPopoverState(false); - }, [setPopoverState]); - - const handleToggle = useCallback(() => { - setPopoverState(!isPopoverOpen); - }, [isPopoverOpen]); - - const handleCustomMetric = useCallback( - (newMetric: SnapshotCustomMetricInput) => { - onChangeCustomMetrics([...customMetrics, newMetric]); - onChange(newMetric); - setMode('pick'); - }, - [customMetrics, onChange, onChangeCustomMetrics, setMode] - ); - - const setModeToEdit = useCallback(() => { - setMode('edit'); - setEditModeCustomMetrics(customMetrics); - }, [customMetrics]); - - const setModeToAdd = useCallback(() => { - setMode('addMetric'); - }, [setMode]); - - const setModeToPick = useCallback(() => { - setMode('pick'); - setEditModeCustomMetrics([]); - }, [setMode]); - - const handleDeleteCustomMetric = useCallback( - (m: SnapshotCustomMetricInput) => { - // If the metric we are deleting is the currently selected metric - // we need to change to the default. - if (SnapshotCustomMetricInputRT.is(metric) && m.id === metric.id) { - onChange({ type: options[0].value as SnapshotMetricType }); - } - // Filter out the deleted metric from the editbale. - const newMetrics = editModeCustomMetrics.filter(v => v.id !== m.id); - setEditModeCustomMetrics(newMetrics); - }, - [editModeCustomMetrics, metric, onChange, options] - ); - - const handleEditCustomMetric = useCallback( - (currentMetric: SnapshotCustomMetricInput) => { - const newMetrics = customMetrics.map(m => (m.id === currentMetric.id && currentMetric) || m); - onChangeCustomMetrics(newMetrics); - setModeToPick(); - setEditCustomMetric(void 0); - setEditModeCustomMetrics([]); - }, - [customMetrics, onChangeCustomMetrics, setModeToPick] - ); - - const handleSelectMetricToEdit = useCallback( - (currentMetric: SnapshotCustomMetricInput) => { - setEditCustomMetric(currentMetric); - setMode('editMetric'); - }, - [setMode, setEditCustomMetric] - ); - - const handleSaveEdit = useCallback(() => { - onChangeCustomMetrics(editModeCustomMetrics); - setMode('pick'); - }, [editModeCustomMetrics, onChangeCustomMetrics]); - - if (!options.length || !metric.type) { - throw Error( - i18n.translate('xpack.infra.waffle.unableToSelectMetricErrorTitle', { - defaultMessage: 'Unable to select options or value for metric.', - }) - ); - } - - const id = SnapshotCustomMetricInputRT.is(metric) && metric.id ? metric.id : metric.type; - const currentLabel = SnapshotCustomMetricInputRT.is(metric) - ? getCustomMetricLabel(metric) - : options.find(o => o.value === id)?.text; - - if (!currentLabel) { - return null; - } - - const button = ( - <EuiFilterButton iconType="arrowDown" onClick={handleToggle}> - <FormattedMessage - id="xpack.infra.waffle.metricButtonLabel" - defaultMessage="Metric: {selectedMetric}" - values={{ selectedMetric: currentLabel }} - /> - </EuiFilterButton> - ); - - return ( - <EuiFilterGroup> - <EuiPopover - isOpen={isPopoverOpen} - id="metricsPanel" - button={button} - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={handleClose} - > - {mode === 'pick' ? ( - <MetricsContextMenu - onChange={onChange} - onClose={handleClose} - metric={metric} - customMetrics={customMetrics} - options={options} - /> - ) : null} - {mode === 'addMetric' ? ( - <CustomMetricForm - fields={fields} - customMetrics={customMetrics} - onChange={handleCustomMetric} - onCancel={setModeToPick} - /> - ) : null} - {mode === 'editMetric' ? ( - <CustomMetricForm - metric={editCustomMetric} - fields={fields} - customMetrics={customMetrics} - onChange={handleEditCustomMetric} - onCancel={setModeToEdit} - /> - ) : null} - {mode === 'edit' ? ( - <MetricsEditMode - customMetrics={editModeCustomMetrics} - options={options} - onEdit={handleSelectMetricToEdit} - onDelete={handleDeleteCustomMetric} - /> - ) : null} - <ModeSwitcher - onEditCancel={setModeToPick} - onEdit={setModeToEdit} - onAdd={setModeToAdd} - mode={mode} - onSave={handleSaveEdit} - customMetrics={customMetrics} - /> - </EuiPopover> - </EuiFilterGroup> - ); -}; diff --git a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx deleted file mode 100644 index 5f05cebd8f616..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx +++ /dev/null @@ -1,193 +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 { EuiPopoverProps, EuiCode } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import React, { useMemo, useState } from 'react'; -import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; -import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to'; -import { createUptimeLink } from './lib/create_uptime_link'; -import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { - Section, - SectionLinkProps, - ActionMenu, - SectionTitle, - SectionSubtitle, - SectionLinks, - SectionLink, -} from '../../../../observability/public'; -import { useLinkProps } from '../../hooks/use_link_props'; -import { AlertFlyout } from '../alerting/metrics/alert_flyout'; - -interface Props { - options: InfraWaffleMapOptions; - currentTime: number; - node: InfraWaffleMapNode; - nodeType: InventoryItemType; - isPopoverOpen: boolean; - closePopover: () => void; - popoverPosition: EuiPopoverProps['anchorPosition']; -} - -export const NodeContextMenu: React.FC<Props> = ({ - options, - currentTime, - children, - node, - isPopoverOpen, - closePopover, - nodeType, - popoverPosition, -}) => { - const [flyoutVisible, setFlyoutVisible] = useState(false); - const inventoryModel = findInventoryModel(nodeType); - const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; - const uiCapabilities = useKibana().services.application?.capabilities; - // Due to the changing nature of the fields between APM and this UI, - // We need to have some exceptions until 7.0 & ECS is finalized. Reference - // #26620 for the details for these fields. - // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; - - const showDetail = inventoryModel.crosslinkSupport.details; - const showLogsLink = - inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show; - const showAPMTraceLink = - inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show; - const showUptimeLink = - inventoryModel.crosslinkSupport.uptime && (['pod', 'container'].includes(nodeType) || node.ip); - - const inventoryId = useMemo(() => { - if (nodeType === 'host') { - if (node.ip) { - return { label: <EuiCode>host.ip</EuiCode>, value: node.ip }; - } - } else { - if (options.fields) { - const { id } = findInventoryFields(nodeType, options.fields); - return { - label: <EuiCode>{id}</EuiCode>, - value: node.id, - }; - } - } - return { label: '', value: '' }; - }, [nodeType, node.ip, node.id, options.fields]); - - const nodeLogsMenuItemLinkProps = useLinkProps({ - app: 'logs', - ...getNodeLogsUrl({ - nodeType, - nodeId: node.id, - time: currentTime, - }), - }); - const nodeDetailMenuItemLinkProps = useLinkProps({ - ...getNodeDetailUrl({ - nodeType, - nodeId: node.id, - from: nodeDetailFrom, - to: currentTime, - }), - }); - const apmTracesMenuItemLinkProps = useLinkProps({ - app: 'apm', - hash: 'traces', - search: { - kuery: `${apmField}:"${node.id}"`, - }, - }); - const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); - - const nodeLogsMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { - defaultMessage: '{inventoryName} logs', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...nodeLogsMenuItemLinkProps, - 'data-test-subj': 'viewLogsContextMenuItem', - isDisabled: !showLogsLink, - }; - - const nodeDetailMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { - defaultMessage: '{inventoryName} metrics', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...nodeDetailMenuItemLinkProps, - isDisabled: !showDetail, - }; - - const apmTracesMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { - defaultMessage: '{inventoryName} APM traces', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...apmTracesMenuItemLinkProps, - 'data-test-subj': 'viewApmTracesContextMenuItem', - isDisabled: !showAPMTraceLink, - }; - - const uptimeMenuItem: SectionLinkProps = { - label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { - defaultMessage: '{inventoryName} in Uptime', - values: { inventoryName: inventoryModel.singularDisplayName }, - }), - ...uptimeMenuItemLinkProps, - isDisabled: !showUptimeLink, - }; - - return ( - <> - <ActionMenu - closePopover={closePopover} - id={`${node.pathId}-popover`} - isOpen={isPopoverOpen} - button={children!} - anchorPosition={popoverPosition} - > - <div style={{ maxWidth: 300 }} data-test-subj="nodeContextMenu"> - <Section> - <SectionTitle> - <FormattedMessage - id="xpack.infra.nodeContextMenu.title" - defaultMessage="{inventoryName} details" - values={{ inventoryName: inventoryModel.singularDisplayName }} - /> - </SectionTitle> - {inventoryId.label && ( - <SectionSubtitle> - <div style={{ wordBreak: 'break-all' }}> - <FormattedMessage - id="xpack.infra.nodeContextMenu.description" - defaultMessage="View details for {label} {value}" - values={{ label: inventoryId.label, value: inventoryId.value }} - /> - </div> - </SectionSubtitle> - )} - <SectionLinks> - <SectionLink data-test-subj="viewLogsContextMenuItem" {...nodeLogsMenuItem} /> - <SectionLink {...nodeDetailMenuItem} /> - <SectionLink data-test-subj="viewApmTracesContextMenuItem" {...apmTracesMenuItem} /> - <SectionLink {...uptimeMenuItem} /> - </SectionLinks> - </Section> - </div> - </ActionMenu> - <AlertFlyout - options={{ filterQuery: `${nodeType}: ${node.id}` }} - setVisible={setFlyoutVisible} - visible={flyoutVisible} - /> - </> - ); -}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_accounts_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_accounts_controls.tsx deleted file mode 100644 index 56c3a205b05d4..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/waffle_accounts_controls.tsx +++ /dev/null @@ -1,94 +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 { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { InventoryCloudAccount } from '../../../common/http_api/inventory_meta_api'; - -interface Props { - accountId: string; - options: InventoryCloudAccount[]; - changeAccount: (id: string) => void; -} - -export const WaffleAccountsControls = (props: Props) => { - const { accountId, options } = props; - const [isOpen, setIsOpen] = useState(); - - const showPopover = useCallback(() => { - setIsOpen(true); - }, [setIsOpen]); - - const closePopover = useCallback(() => { - setIsOpen(false); - }, [setIsOpen]); - - const currentLabel = options.find(o => o.value === accountId); - - const changeAccount = useCallback( - (val: string) => { - if (accountId === val) { - props.changeAccount(''); - } else { - props.changeAccount(val); - } - closePopover(); - }, - [accountId, closePopover, props] - ); - - const panels = useMemo<EuiContextMenuPanelDescriptor[]>( - () => [ - { - id: 0, - title: '', - items: options.map(o => { - const icon = o.value === accountId ? 'check' : 'empty'; - const panel = { name: o.name, onClick: () => changeAccount(o.value), icon }; - return panel; - }), - }, - ], - [options, accountId, changeAccount] - ); - - return ( - <EuiFilterGroup> - <EuiPopover - isOpen={isOpen} - id="accontPopOver" - button={ - <EuiFilterButton iconType="arrowDown" onClick={showPopover}> - <FormattedMessage - id="xpack.infra.waffle.accountLabel" - defaultMessage="Account: {selectedAccount}" - values={{ - selectedAccount: currentLabel - ? currentLabel.name - : i18n.translate('xpack.infra.waffle.accountAllTitle', { - defaultMessage: 'All', - }), - }} - /> - </EuiFilterButton> - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - <EuiContextMenu initialPanelId={0} panels={panels} /> - </EuiPopover> - </EuiFilterGroup> - ); -}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_region_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_region_controls.tsx deleted file mode 100644 index 52377bf4b8296..0000000000000 --- a/x-pack/plugins/infra/public/components/waffle/waffle_region_controls.tsx +++ /dev/null @@ -1,93 +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 { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -interface Props { - region?: string; - options: string[]; - changeRegion: (name: string) => void; -} - -export const WaffleRegionControls = (props: Props) => { - const { region, options } = props; - const [isOpen, setIsOpen] = useState(); - - const showPopover = useCallback(() => { - setIsOpen(true); - }, [setIsOpen]); - - const closePopover = useCallback(() => { - setIsOpen(false); - }, [setIsOpen]); - - const currentLabel = options.find(o => region === o); - - const changeRegion = useCallback( - (val: string) => { - if (region === val) { - props.changeRegion(''); - } else { - props.changeRegion(val); - } - closePopover(); - }, - [region, closePopover, props] - ); - - const panels = useMemo<EuiContextMenuPanelDescriptor[]>( - () => [ - { - id: 0, - title: '', - items: options.map(o => { - const icon = o === region ? 'check' : 'empty'; - const panel = { name: o, onClick: () => changeRegion(o), icon }; - return panel; - }), - }, - ], - [changeRegion, options, region] - ); - - return ( - <EuiFilterGroup> - <EuiPopover - isOpen={isOpen} - id="regionPanel" - button={ - <EuiFilterButton iconType="arrowDown" onClick={showPopover}> - <FormattedMessage - id="xpack.infra.waffle.regionLabel" - defaultMessage="Region: {selectedRegion}" - values={{ - selectedRegion: - currentLabel || - i18n.translate('xpack.infra.waffle.region', { - defaultMessage: 'All', - }), - }} - /> - </EuiFilterButton> - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - <EuiContextMenu initialPanelId={0} panels={panels} /> - </EuiPopover> - </EuiFilterGroup> - ); -}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx index 267abe631c142..b0981f9b3c41f 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx @@ -8,11 +8,11 @@ import createContainer from 'constate'; import { isString } from 'lodash'; import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { LogEntriesItem } from '../../../common/http_api'; import { UrlStateContainer } from '../../utils/url_state'; import { useTrackedPromise } from '../../utils/use_tracked_promise'; -import { Source } from '../source'; import { fetchLogEntriesItem } from './log_entries/api/fetch_log_entries_item'; -import { LogEntriesItem } from '../../../common/http_api'; +import { useLogSourceContext } from './log_source'; export enum FlyoutVisibility { hidden = 'hidden', @@ -26,7 +26,7 @@ export interface FlyoutOptionsUrlState { } export const useLogFlyout = () => { - const { sourceId } = useContext(Source.Context); + const { sourceId } = useLogSourceContext(); const [flyoutVisible, setFlyoutVisibility] = useState<boolean>(false); const [flyoutId, setFlyoutId] = useState<string | null>(null); const [flyoutItem, setFlyoutItem] = useState<LogEntriesItem | null>(null); diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts new file mode 100644 index 0000000000000..786cb485b38dd --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_configuration.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogSourceConfigurationPath, + getLogSourceConfigurationSuccessResponsePayloadRT, +} from '../../../../../common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { npStart } from '../../../../legacy_singletons'; + +export const callFetchLogSourceConfigurationAPI = async (sourceId: string) => { + const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), { + method: 'GET', + }); + + return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts new file mode 100644 index 0000000000000..2f1d15ffaf4d3 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/fetch_log_source_status.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogSourceStatusPath, + getLogSourceStatusSuccessResponsePayloadRT, +} from '../../../../../common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { npStart } from '../../../../legacy_singletons'; + +export const callFetchLogSourceStatusAPI = async (sourceId: string) => { + const response = await npStart.http.fetch(getLogSourceStatusPath(sourceId), { + method: 'GET', + }); + + return decodeOrThrow(getLogSourceStatusSuccessResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts new file mode 100644 index 0000000000000..848801ab3c7ce --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/api/patch_log_source_configuration.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getLogSourceConfigurationPath, + patchLogSourceConfigurationSuccessResponsePayloadRT, + patchLogSourceConfigurationRequestBodyRT, + LogSourceConfigurationPropertiesPatch, +} from '../../../../../common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { npStart } from '../../../../legacy_singletons'; + +export const callPatchLogSourceConfigurationAPI = async ( + sourceId: string, + patchedProperties: LogSourceConfigurationPropertiesPatch +) => { + const response = await npStart.http.fetch(getLogSourceConfigurationPath(sourceId), { + method: 'PATCH', + body: JSON.stringify( + patchLogSourceConfigurationRequestBodyRT.encode({ + data: patchedProperties, + }) + ), + }); + + return decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/index.ts b/x-pack/plugins/infra/public/containers/logs/log_source/index.ts new file mode 100644 index 0000000000000..5d9c6373050a1 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './log_source'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts new file mode 100644 index 0000000000000..8332018fddf90 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -0,0 +1,157 @@ +/* + * 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 createContainer from 'constate'; +import { useState, useMemo, useCallback } from 'react'; +import { + LogSourceConfiguration, + LogSourceStatus, + LogSourceConfigurationPropertiesPatch, + LogSourceConfigurationProperties, +} from '../../../../common/http_api/log_sources'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callFetchLogSourceConfigurationAPI } from './api/fetch_log_source_configuration'; +import { callFetchLogSourceStatusAPI } from './api/fetch_log_source_status'; +import { callPatchLogSourceConfigurationAPI } from './api/patch_log_source_configuration'; + +export { + LogSourceConfiguration, + LogSourceConfigurationProperties, + LogSourceConfigurationPropertiesPatch, + LogSourceStatus, +}; + +export const useLogSource = ({ sourceId }: { sourceId: string }) => { + const [sourceConfiguration, setSourceConfiguration] = useState< + LogSourceConfiguration | undefined + >(undefined); + + const [sourceStatus, setSourceStatus] = useState<LogSourceStatus | undefined>(undefined); + + const [loadSourceConfigurationRequest, loadSourceConfiguration] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callFetchLogSourceConfigurationAPI(sourceId); + }, + onResolve: ({ data }) => { + setSourceConfiguration(data); + }, + }, + [sourceId] + ); + + const [updateSourceConfigurationRequest, updateSourceConfiguration] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async (patchedProperties: LogSourceConfigurationPropertiesPatch) => { + return await callPatchLogSourceConfigurationAPI(sourceId, patchedProperties); + }, + onResolve: ({ data }) => { + setSourceConfiguration(data); + loadSourceStatus(); + }, + }, + [sourceId] + ); + + const [loadSourceStatusRequest, loadSourceStatus] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callFetchLogSourceStatusAPI(sourceId); + }, + onResolve: ({ data }) => { + setSourceStatus(data); + }, + }, + [sourceId] + ); + + const logIndicesExist = useMemo(() => (sourceStatus?.logIndexNames?.length ?? 0) > 0, [ + sourceStatus, + ]); + + const derivedIndexPattern = useMemo( + () => ({ + fields: sourceStatus?.logIndexFields ?? [], + title: sourceConfiguration?.configuration.name ?? 'unknown', + }), + [sourceConfiguration, sourceStatus] + ); + + const isLoadingSourceConfiguration = useMemo( + () => loadSourceConfigurationRequest.state === 'pending', + [loadSourceConfigurationRequest.state] + ); + + const isUpdatingSourceConfiguration = useMemo( + () => updateSourceConfigurationRequest.state === 'pending', + [updateSourceConfigurationRequest.state] + ); + + const isLoadingSourceStatus = useMemo(() => loadSourceStatusRequest.state === 'pending', [ + loadSourceStatusRequest.state, + ]); + + const isLoading = useMemo( + () => isLoadingSourceConfiguration || isLoadingSourceStatus || isUpdatingSourceConfiguration, + [isLoadingSourceConfiguration, isLoadingSourceStatus, isUpdatingSourceConfiguration] + ); + + const isUninitialized = useMemo( + () => + loadSourceConfigurationRequest.state === 'uninitialized' || + loadSourceStatusRequest.state === 'uninitialized', + [loadSourceConfigurationRequest.state, loadSourceStatusRequest.state] + ); + + const hasFailedLoadingSource = useMemo( + () => loadSourceConfigurationRequest.state === 'rejected', + [loadSourceConfigurationRequest.state] + ); + + const loadSourceFailureMessage = useMemo( + () => + loadSourceConfigurationRequest.state === 'rejected' + ? `${loadSourceConfigurationRequest.value}` + : undefined, + [loadSourceConfigurationRequest] + ); + + const loadSource = useCallback(() => { + return Promise.all([loadSourceConfiguration(), loadSourceStatus()]); + }, [loadSourceConfiguration, loadSourceStatus]); + + const initialize = useCallback(async () => { + if (!isUninitialized) { + return; + } + + return await loadSource(); + }, [isUninitialized, loadSource]); + + return { + derivedIndexPattern, + hasFailedLoadingSource, + initialize, + isLoading, + isLoadingSourceConfiguration, + isLoadingSourceStatus, + isUninitialized, + loadSource, + loadSourceFailureMessage, + loadSourceConfiguration, + loadSourceStatus, + logIndicesExist, + sourceConfiguration, + sourceId, + sourceStatus, + updateSourceConfiguration, + }; +}; + +export const [LogSourceProvider, useLogSourceContext] = createContainer(useLogSource); diff --git a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts index 14da2b47bcfa2..625a1ead4d930 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_summary/with_summary.ts @@ -8,10 +8,10 @@ import { useContext } from 'react'; import { useThrottle } from 'react-use'; import { RendererFunction } from '../../../utils/typed_react'; -import { Source } from '../../source'; import { LogSummaryBuckets, useLogSummary } from './log_summary'; import { LogFilterState } from '../log_filter'; import { LogPositionState } from '../log_position'; +import { useLogSourceContext } from '../log_source'; const FETCH_THROTTLE_INTERVAL = 3000; @@ -24,7 +24,7 @@ export const WithSummary = ({ end: number | null; }>; }) => { - const { sourceId } = useContext(Source.Context); + const { sourceId } = useLogSourceContext(); const { filterQuery } = useContext(LogFilterState.Context); const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context); diff --git a/x-pack/plugins/infra/public/containers/logs/view_log_in_context/index.ts b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/index.ts new file mode 100644 index 0000000000000..0110c55c7c556 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './view_log_in_context'; diff --git a/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts new file mode 100644 index 0000000000000..bc719cbd694e4 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts @@ -0,0 +1,83 @@ +/* + * 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 { useState, useEffect, useCallback } from 'react'; +import createContainer from 'constate'; +import { LogEntry } from '../../../../common/http_api'; +import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; +import { esKuery } from '../../../../../../../src/plugins/data/public'; + +function getQueryFromLogEntry(entry: LogEntry) { + const expression = Object.entries(entry.context).reduce((kuery, [key, value]) => { + const currentExpression = `${key} : "${value}"`; + if (kuery.length > 0) { + return `${kuery} AND ${currentExpression}`; + } else { + return currentExpression; + } + }, ''); + + return JSON.stringify(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(expression))); +} + +interface ViewLogInContextProps { + sourceId: string; + startTimestamp: number; + endTimestamp: number; +} + +export interface ViewLogInContextState { + entries: LogEntry[]; + isLoading: boolean; + contextEntry?: LogEntry; +} + +interface ViewLogInContextCallbacks { + setContextEntry: (entry?: LogEntry) => void; +} + +export const useViewLogInContext = ( + props: ViewLogInContextProps +): [ViewLogInContextState, ViewLogInContextCallbacks] => { + const [contextEntry, setContextEntry] = useState<LogEntry | undefined>(); + const [entries, setEntries] = useState<LogEntry[]>([]); + const [isLoading, setIsLoading] = useState<boolean>(false); + const { startTimestamp, endTimestamp, sourceId } = props; + + const maybeFetchLogs = useCallback(async () => { + if (contextEntry) { + setIsLoading(true); + const { data } = await fetchLogEntries({ + sourceId, + startTimestamp, + endTimestamp, + center: contextEntry.cursor, + query: getQueryFromLogEntry(contextEntry), + }); + setEntries(data.entries); + setIsLoading(false); + } else { + setEntries([]); + setIsLoading(false); + } + }, [contextEntry, startTimestamp, endTimestamp, sourceId]); + + useEffect(() => { + maybeFetchLogs(); + }, [maybeFetchLogs]); + + return [ + { + contextEntry, + entries, + isLoading, + }, + { + setContextEntry, + }, + ]; +}; + +export const ViewLogInContext = createContainer(useViewLogInContext); diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx index d2fa49e8d5d9f..04f518aa9080f 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx +++ b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx @@ -17,7 +17,7 @@ import { MetricsExplorerYAxisMode, MetricsExplorerChartType, MetricsExplorerChartOptions, -} from './use_metrics_explorer_options'; +} from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; interface MetricsExplorerUrlState { timerange?: MetricsExplorerTimeOptions; diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts index bc6374a6538e3..aad54bd2222b7 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useCallback } from 'react'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -69,12 +69,15 @@ export const useSourceViaHttp = ({ })(); }, [makeRequest]); - const createDerivedIndexPattern = (indexType: 'logs' | 'metrics' | 'both' = type) => { - return { - fields: response?.source ? response.status.indexFields : [], - title: pickIndexPattern(response?.source, indexType), - }; - }; + const createDerivedIndexPattern = useCallback( + (indexType: 'logs' | 'metrics' | 'both' = type) => { + return { + fields: response?.source ? response.status.indexFields : [], + title: pickIndexPattern(response?.source, indexType), + }; + }, + [response, type] + ); const source = useMemo(() => { return response ? { ...response.source, status: response.status } : null; diff --git a/x-pack/plugins/infra/public/containers/waffle/type_guards.ts b/x-pack/plugins/infra/public/containers/waffle/type_guards.ts deleted file mode 100644 index 3e21e3a56a6c6..0000000000000 --- a/x-pack/plugins/infra/public/containers/waffle/type_guards.ts +++ /dev/null @@ -1,15 +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 { InfraWaffleMapGroupOfGroups, InfraWaffleMapGroupOfNodes } from '../../lib/lib'; - -export function isWaffleMapGroupWithNodes(subject: any): subject is InfraWaffleMapGroupOfNodes { - return subject && subject.nodes != null && Array.isArray(subject.nodes); -} - -export function isWaffleMapGroupWithGroups(subject: any): subject is InfraWaffleMapGroupOfGroups { - return subject && subject.groups != null && Array.isArray(subject.groups); -} diff --git a/x-pack/plugins/infra/public/containers/with_options.tsx b/x-pack/plugins/infra/public/containers/with_options.tsx deleted file mode 100644 index e18fc85a68d60..0000000000000 --- a/x-pack/plugins/infra/public/containers/with_options.tsx +++ /dev/null @@ -1,64 +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 moment from 'moment'; -import React from 'react'; - -import { euiPaletteColorBlind } from '@elastic/eui'; -import { InfraFormatterType, InfraOptions } from '../lib/lib'; -import { RendererFunction } from '../utils/typed_react'; - -const euiVisColorPalette = euiPaletteColorBlind(); - -const initialState = { - options: { - timerange: { - interval: '1m', - to: moment.utc().valueOf(), - from: moment - .utc() - .subtract(1, 'h') - .valueOf(), - }, - wafflemap: { - formatter: InfraFormatterType.percent, - formatTemplate: '{{value}}', - metric: { type: 'cpu' }, - groupBy: [], - legend: { - type: 'gradient', - rules: [ - { - value: 0, - color: '#D3DAE6', - }, - { - value: 1, - color: euiVisColorPalette[1], - }, - ], - }, - }, - } as InfraOptions, -}; - -interface WithOptionsProps { - children: RendererFunction<InfraOptions>; -} - -type State = Readonly<typeof initialState>; - -export const withOptions = (WrappedComponent: React.ComponentType<InfraOptions>) => ( - <WithOptions>{args => <WrappedComponent {...args} />}</WithOptions> -); - -export class WithOptions extends React.Component<WithOptionsProps, State> { - public readonly state: State = initialState; - - public render() { - return this.props.children(this.state.options); - } -} diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx index 13e054de2dcf7..f9cfaf71036f6 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.test.tsx @@ -5,13 +5,14 @@ */ import { encode } from 'rison-node'; -import { createMemoryHistory, LocationDescriptorObject } from 'history'; +import { createMemoryHistory } from 'history'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { HistoryContext } from '../utils/history_context'; import { coreMock } from 'src/core/public/mocks'; import { useLinkProps, LinkDescriptor } from './use_link_props'; +import { ScopedHistory } from '../../../../../src/core/public'; const PREFIX = '/test-basepath/s/test-space/app/'; @@ -23,18 +24,13 @@ coreStartMock.application.getUrlForApp.mockImplementation((app, options) => { const INTERNAL_APP = 'metrics'; -// Note: Memory history doesn't support basename, -// we'll work around this by re-assigning 'createHref' so that -// it includes a basename, this then acts as our browserHistory instance would. const history = createMemoryHistory(); -const originalCreateHref = history.createHref; -history.createHref = (location: LocationDescriptorObject): string => { - return `${PREFIX}${INTERNAL_APP}${originalCreateHref.call(history, location)}`; -}; +history.push(`${PREFIX}${INTERNAL_APP}`); +const scopedHistory = new ScopedHistory(history, `${PREFIX}${INTERNAL_APP}`); const ProviderWrapper: React.FC = ({ children }) => { return ( - <HistoryContext.Provider value={history}> + <HistoryContext.Provider value={scopedHistory}> <KibanaContextProvider services={{ ...coreStartMock }}>{children}</KibanaContextProvider>; </HistoryContext.Provider> ); @@ -111,7 +107,7 @@ describe('useLinkProps hook', () => { pathname: '/', }); expect(result.current.href).toBe('/test-basepath/s/test-space/app/ml/'); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with pathname options', () => { @@ -127,7 +123,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml/explorer?type=host&id=some-id&count=12345' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with hash options', () => { @@ -143,7 +139,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml#/explorer?type=host&id=some-id&count=12345' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with more complex encoding', () => { @@ -161,7 +157,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml#/explorer?type=host%20%2B%20host&name=this%20name%20has%20spaces%20and%20**%20and%20%25&id=some-id&count=12345&animals=dog,cat,bear' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with a consumer using Rison encoding for search', () => { @@ -180,7 +176,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/rison-app#rison-route?type=host%20%2B%20host&state=(refreshInterval:(pause:!t,value:0),time:(from:12345,to:54321))' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); }); }); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index e60ab32046832..dec8eaae56f41 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -9,7 +9,8 @@ import { stringify } from 'query-string'; import url from 'url'; import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public'; import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath'; -import { useHistory } from '../utils/history_context'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useNavigationWarningPrompt } from '../utils/navigation_warning_prompt'; type Search = Record<string, string | string[]>; @@ -25,34 +26,37 @@ interface LinkProps { onClick?: (e: React.MouseEvent | React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void; } -export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): LinkProps => { +interface Options { + hrefOnly?: boolean; +} + +export const useLinkProps = ( + { app, pathname, hash, search }: LinkDescriptor, + options: Options = {} +): LinkProps => { validateParams({ app, pathname, hash, search }); - const history = useHistory(); + const { prompt } = useNavigationWarningPrompt(); const prefixer = usePrefixPathWithBasepath(); + const navigateToApp = useKibana().services.application?.navigateToApp; + const { hrefOnly } = options; const encodedSearch = useMemo(() => { return search ? encodeSearch(search) : undefined; }, [search]); - const internalLinkResult = useMemo(() => { - // When the logs / metrics apps are first mounted a history instance is setup with a 'basename' equal to the - // 'appBasePath' received from Core's 'AppMountParams', e.g. /BASE_PATH/s/SPACE_ID/app/APP_ID. With internal - // linking we are using 'createHref' and 'push' on top of this history instance. So a pathname of /inventory used within - // the metrics app will ultimatey end up as /BASE_PATH/s/SPACE_ID/app/metrics/inventory. React-router responds to this - // as it is instantiated with the same history instance. - return history?.createHref({ - pathname: pathname ? formatPathname(pathname) : undefined, - search: encodedSearch, - }); - }, [history, pathname, encodedSearch]); - - const externalLinkResult = useMemo(() => { + const mergedHash = useMemo(() => { // The URI spec defines that the query should appear before the fragment // https://tools.ietf.org/html/rfc3986#section-3 (e.g. url.format()). However, in Kibana, apps that use // hash based routing expect the query to be part of the hash. This will handle that. - const mergedHash = hash && encodedSearch ? `${hash}?${encodedSearch}` : hash; + return hash && encodedSearch ? `${hash}?${encodedSearch}` : hash; + }, [hash, encodedSearch]); + + const mergedPathname = useMemo(() => { + return pathname && encodedSearch ? `${pathname}?${encodedSearch}` : pathname; + }, [pathname, encodedSearch]); + const href = useMemo(() => { const link = url.format({ pathname, hash: mergedHash, @@ -60,29 +64,40 @@ export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): L }); return prefixer(app, link); - }, [hash, encodedSearch, pathname, prefixer, app]); + }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]); const onClick = useMemo(() => { - // If these results are equal we know we're trying to navigate within the same application - // that the current history instance is representing - if (internalLinkResult && linksAreEquivalent(externalLinkResult, internalLinkResult)) { - return (e: React.MouseEvent | React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => { - e.preventDefault(); - if (history) { - history.push({ - pathname: pathname ? formatPathname(pathname) : undefined, - search: encodedSearch, - }); + return (e: React.MouseEvent | React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => { + e.preventDefault(); + + const navigate = () => { + if (navigateToApp) { + const navigationPath = mergedHash ? `#${mergedHash}` : mergedPathname; + navigateToApp(app, { path: navigationPath ? navigationPath : undefined }); } }; - } else { - return undefined; - } - }, [internalLinkResult, externalLinkResult, history, pathname, encodedSearch]); + + // A <Prompt /> component somewhere within the app hierarchy is requesting that we + // prompt the user before navigating. + if (prompt) { + const wantsToNavigate = window.confirm(prompt); + if (wantsToNavigate) { + navigate(); + } else { + return; + } + } else { + navigate(); + } + }; + }, [navigateToApp, mergedHash, mergedPathname, app, prompt]); return { - href: externalLinkResult, - onClick, + href, + // Sometimes it may not be desirable to have onClick call "navigateToApp". + // E.g. the management section of Kibana cannot be successfully deeplinked to via + // "navigateToApp". In those cases we can choose to defer to legacy behaviour. + onClick: hrefOnly ? undefined : onClick, }; }; @@ -90,10 +105,6 @@ const encodeSearch = (search: Search) => { return stringify(urlUtils.encodeQuery(search), { sort: false, encode: false }); }; -const formatPathname = (pathname: string) => { - return pathname[0] === '/' ? pathname : `/${pathname}`; -}; - const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { if (!app && hash) { throw new Error( @@ -101,9 +112,3 @@ const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { ); } }; - -const linksAreEquivalent = (externalLink: string, internalLink: string): boolean => { - // Compares with trailing slashes removed. This handles the case where the pathname is '/' - // and 'createHref' will include the '/' but Kibana's 'getUrlForApp' will remove it. - return externalLink.replace(/\/$/, '') === internalLink.replace(/\/$/, ''); -}; diff --git a/x-pack/plugins/infra/public/index.ts b/x-pack/plugins/infra/public/index.ts index 4465bde377c12..1dfdf827f203b 100644 --- a/x-pack/plugins/infra/public/index.ts +++ b/x-pack/plugins/infra/public/index.ts @@ -16,7 +16,7 @@ export const plugin: PluginInitializer< return new Plugin(context); }; -export { FORMATTERS } from './utils/formatters'; +export { FORMATTERS } from '../common/formatters'; export { InfraFormatterType } from './lib/lib'; export type InfraAppId = 'logs' | 'metrics'; diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index e4de0caf9bb8b..9043b4d9f6979 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -186,12 +186,6 @@ export enum InfraFormatterType { percent = 'percent', } -export enum InfraWaffleMapDataFormat { - bytesDecimal = 'bytesDecimal', - bitsDecimal = 'bitsDecimal', - abbreviatedNumber = 'abbreviatedNumber', -} - export interface InfraGroupByOptions { text: string; field: string; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/index.tsx deleted file mode 100644 index d592ae3480fc9..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/index.tsx +++ /dev/null @@ -1,135 +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 { i18n } from '@kbn/i18n'; - -import React from 'react'; -import { Route, RouteComponentProps, Switch } from 'react-router-dom'; - -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { DocumentTitle } from '../../components/document_title'; -import { HelpCenterContent } from '../../components/help_center_content'; -import { RoutedTabs } from '../../components/navigation/routed_tabs'; -import { ColumnarPage } from '../../components/page'; -import { Header } from '../../components/header'; -import { MetricsExplorerOptionsContainer } from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; -import { WithSource } from '../../containers/with_source'; -import { Source } from '../../containers/source'; -import { MetricsExplorerPage } from './metrics_explorer'; -import { SnapshotPage } from './snapshot'; -import { MetricsSettingsPage } from './settings'; -import { AppNavigation } from '../../components/navigation/app_navigation'; -import { SourceLoadingPage } from '../../components/source_loading_page'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { WaffleOptionsProvider } from '../inventory_view/hooks/use_waffle_options'; -import { WaffleTimeProvider } from '../inventory_view/hooks/use_waffle_time'; -import { WaffleFiltersProvider } from '../inventory_view/hooks/use_waffle_filters'; -import { AlertDropdown } from '../../components/alerting/metrics/alert_dropdown'; - -export const InfrastructurePage = ({ match }: RouteComponentProps) => { - const uiCapabilities = useKibana().services.application?.capabilities; - - return ( - <Source.Provider sourceId="default"> - <WaffleOptionsProvider> - <WaffleTimeProvider> - <WaffleFiltersProvider> - <ColumnarPage> - <DocumentTitle - title={i18n.translate('xpack.infra.homePage.documentTitle', { - defaultMessage: 'Metrics', - })} - /> - - <HelpCenterContent - feedbackLink="https://discuss.elastic.co/c/metrics" - appName={i18n.translate('xpack.infra.header.infrastructureHelpAppName', { - defaultMessage: 'Metrics', - })} - /> - - <Header - breadcrumbs={[ - { - text: i18n.translate('xpack.infra.header.infrastructureTitle', { - defaultMessage: 'Metrics', - }), - }, - ]} - readOnlyBadge={!uiCapabilities?.infrastructure?.save} - /> - <AppNavigation - aria-label={i18n.translate('xpack.infra.header.infrastructureNavigationTitle', { - defaultMessage: 'Metrics', - })} - > - <EuiFlexGroup gutterSize={'none'} alignItems={'center'}> - <EuiFlexItem> - <RoutedTabs - tabs={[ - { - app: 'metrics', - title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', { - defaultMessage: 'Inventory', - }), - pathname: '/inventory', - }, - { - app: 'metrics', - title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', { - defaultMessage: 'Metrics Explorer', - }), - pathname: '/explorer', - }, - { - app: 'metrics', - title: i18n.translate('xpack.infra.homePage.settingsTabTitle', { - defaultMessage: 'Settings', - }), - pathname: '/settings', - }, - ]} - /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <Route path={'/explorer'} component={AlertDropdown} /> - </EuiFlexItem> - </EuiFlexGroup> - </AppNavigation> - - <Switch> - <Route path={'/inventory'} component={SnapshotPage} /> - <Route - path={'/explorer'} - render={props => ( - <WithSource> - {({ configuration, createDerivedIndexPattern }) => ( - <MetricsExplorerOptionsContainer.Provider> - <WithMetricsExplorerOptionsUrlState /> - {configuration ? ( - <MetricsExplorerPage - derivedIndexPattern={createDerivedIndexPattern('metrics')} - source={configuration} - {...props} - /> - ) : ( - <SourceLoadingPage /> - )} - </MetricsExplorerOptionsContainer.Provider> - )} - </WithSource> - )} - /> - <Route path={'/settings'} component={MetricsSettingsPage} /> - </Switch> - </ColumnarPage> - </WaffleFiltersProvider> - </WaffleTimeProvider> - </WaffleOptionsProvider> - </Source.Provider> - ); -}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx deleted file mode 100644 index 0999cea59731c..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx +++ /dev/null @@ -1,100 +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 { i18n } from '@kbn/i18n'; - -import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; -import { DocumentTitle } from '../../../components/document_title'; -import { MetricsExplorerCharts } from '../../../components/metrics_explorer/charts'; -import { MetricsExplorerToolbar } from '../../../components/metrics_explorer/toolbar'; -import { SourceQuery } from '../../../../common/graphql/types'; -import { NoData } from '../../../components/empty_states'; -import { useMetricsExplorerState } from './use_metric_explorer_state'; -import { useTrackPageview } from '../../../../../observability/public'; - -interface MetricsExplorerPageProps { - source: SourceQuery.Query['source']['configuration']; - derivedIndexPattern: IIndexPattern; -} - -export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { - const { - loading, - error, - data, - currentTimerange, - options, - chartOptions, - setChartOptions, - handleAggregationChange, - handleMetricsChange, - handleFilterQuerySubmit, - handleGroupByChange, - handleTimeChange, - handleRefresh, - handleLoadMore, - defaultViewState, - onViewStateChange, - } = useMetricsExplorerState(source, derivedIndexPattern); - - useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' }); - useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); - - return ( - <React.Fragment> - <DocumentTitle - title={(previousTitle: string) => - i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { - defaultMessage: '{previousTitle} | Metrics Explorer', - values: { - previousTitle, - }, - }) - } - /> - <MetricsExplorerToolbar - derivedIndexPattern={derivedIndexPattern} - timeRange={currentTimerange} - options={options} - chartOptions={chartOptions} - onRefresh={handleRefresh} - onTimeChange={handleTimeChange} - onGroupByChange={handleGroupByChange} - onFilterQuerySubmit={handleFilterQuerySubmit} - onMetricsChange={handleMetricsChange} - onAggregationChange={handleAggregationChange} - onChartOptionsChange={setChartOptions} - defaultViewState={defaultViewState} - onViewStateChange={onViewStateChange} - /> - {error ? ( - <NoData - titleText="Whoops!" - bodyText={i18n.translate('xpack.infra.metricsExplorer.errorMessage', { - defaultMessage: 'It looks like the request failed with "{message}"', - values: { message: error.message }, - })} - onRefetch={handleRefresh} - refetchText="Try Again" - /> - ) : ( - <MetricsExplorerCharts - timeRange={currentTimerange} - loading={loading} - data={data} - source={source} - options={options} - chartOptions={chartOptions} - onLoadMore={handleLoadMore} - onFilter={handleFilterQuerySubmit} - onRefetch={handleRefresh} - onTimeChange={handleTimeChange} - /> - )} - </React.Fragment> - ); -}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx deleted file mode 100644 index 48cc56388c0f2..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/index.tsx +++ /dev/null @@ -1,105 +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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useContext } from 'react'; - -import { SnapshotToolbar } from './toolbar'; - -import { DocumentTitle } from '../../../components/document_title'; -import { NoIndices } from '../../../components/empty_states/no_indices'; -import { ColumnarPage } from '../../../components/page'; - -import { SourceErrorPage } from '../../../components/source_error_page'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; -import { Source } from '../../../containers/source'; -import { useTrackPageview } from '../../../../../observability/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { Layout } from '../../../components/inventory/layout'; -import { useLinkProps } from '../../../hooks/use_link_props'; - -export const SnapshotPage = () => { - const uiCapabilities = useKibana().services.application?.capabilities; - const { - hasFailedLoadingSource, - isLoading, - loadSourceFailureMessage, - loadSource, - metricIndicesExist, - } = useContext(Source.Context); - useTrackPageview({ app: 'infra_metrics', path: 'inventory' }); - useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 }); - - const tutorialLinkProps = useLinkProps({ - app: 'kibana', - hash: '/home/tutorial_directory/metrics', - }); - - return ( - <ColumnarPage> - <DocumentTitle - title={(previousTitle: string) => - i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { - defaultMessage: '{previousTitle} | Inventory', - values: { - previousTitle, - }, - }) - } - /> - {isLoading ? ( - <SourceLoadingPage /> - ) : metricIndicesExist ? ( - <> - <SnapshotToolbar /> - <Layout /> - </> - ) : hasFailedLoadingSource ? ( - <SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} /> - ) : ( - <NoIndices - title={i18n.translate('xpack.infra.homePage.noMetricsIndicesTitle', { - defaultMessage: "Looks like you don't have any metrics indices.", - })} - message={i18n.translate('xpack.infra.homePage.noMetricsIndicesDescription', { - defaultMessage: "Let's add some!", - })} - actions={ - <EuiFlexGroup> - <EuiFlexItem> - <EuiButton - {...tutorialLinkProps} - color="primary" - fill - data-test-subj="infrastructureViewSetupInstructionsButton" - > - {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', { - defaultMessage: 'View setup instructions', - })} - </EuiButton> - </EuiFlexItem> - {uiCapabilities?.infrastructure?.configureSource ? ( - <EuiFlexItem> - <ViewSourceConfigurationButton - app="metrics" - data-test-subj="configureSourceButton" - > - {i18n.translate('xpack.infra.configureSourceActionLabel', { - defaultMessage: 'Change source configuration', - })} - </ViewSourceConfigurationButton> - </EuiFlexItem> - ) : null} - </EuiFlexGroup> - } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - </ColumnarPage> - ); -}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx b/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx deleted file mode 100644 index ccdaa5e8dc785..0000000000000 --- a/x-pack/plugins/infra/public/pages/infrastructure/snapshot/toolbar.tsx +++ /dev/null @@ -1,29 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; - -import { Toolbar } from '../../../components/eui/toolbar'; -import { WaffleTimeControls } from '../../../components/waffle/waffle_time_controls'; -import { WaffleInventorySwitcher } from '../../../components/waffle/waffle_inventory_switcher'; -import { SearchBar } from '../../inventory_view/compontents/search_bar'; - -export const SnapshotToolbar = () => ( - <Toolbar> - <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m"> - <EuiFlexItem grow={false}> - <WaffleInventorySwitcher /> - </EuiFlexItem> - <EuiFlexItem> - <SearchBar /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <WaffleTimeControls /> - </EuiFlexItem> - </EuiFlexGroup> - </Toolbar> -); diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index b1dab3bd3f673..3ad242c77412d 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time'; +import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time'; import { useHostIpToName } from './use_host_ip_to_name'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { LoadingPage } from '../../components/loading_page'; diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx index 72a41f5264244..3d25e4c6c258d 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import { replaceMetricTimeInQueryString } from '../metrics/hooks/use_metrics_time'; +import { replaceMetricTimeInQueryString } from '../metrics/metric_detail/hooks/use_metrics_time'; import { getFromFromLocation, getToFromLocation } from './query_params'; import { InventoryItemType } from '../../../common/inventory_models/types'; import { LinkDescriptor } from '../../hooks/use_link_props'; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx index ed1aa9e72ebae..04b472ceb59c8 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_content.tsx @@ -17,7 +17,7 @@ import { import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryCategoriesResultsContent } from './page_results_content'; import { LogEntryCategoriesSetupContent } from './page_setup_content'; import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_module'; @@ -25,11 +25,11 @@ import { useLogEntryCategoriesModuleContext } from './use_log_entry_categories_m export const LogEntryCategoriesPageContent = () => { const { hasFailedLoadingSource, - isLoadingSource, + isLoading, isUninitialized, loadSource, loadSourceFailureMessage, - } = useSourceContext(); + } = useLogSourceContext(); const { hasLogAnalysisCapabilites, @@ -45,7 +45,7 @@ export const LogEntryCategoriesPageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); - if (isLoadingSource || isUninitialized) { + if (isLoading || isUninitialized) { return <SourceLoadingPage />; } else if (hasFailedLoadingSource) { return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx index 619cea6eda8b7..cecea733b49e4 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/page_providers.tsx @@ -6,20 +6,20 @@ import React from 'react'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; import { LogEntryCategoriesModuleProvider } from './use_log_entry_categories_module'; export const LogEntryCategoriesPageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, source } = useSourceContext(); + const { sourceId, sourceConfiguration } = useLogSourceContext(); const spaceId = useKibanaSpaceId(); return ( <LogEntryCategoriesModuleProvider - indexPattern={source ? source.configuration.logAlias : ''} + indexPattern={sourceConfiguration?.configuration.logAlias ?? ''} sourceId={sourceId} spaceId={spaceId} - timestampField={source ? source.configuration.fields.timestamp : ''} + timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''} > {children} </LogEntryCategoriesModuleProvider> diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index 2f34e62d8e611..fc07289f02fe7 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -17,7 +17,7 @@ import { import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { LogEntryRateResultsContent } from './page_results_content'; import { LogEntryRateSetupContent } from './page_setup_content'; import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; @@ -25,11 +25,11 @@ import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; export const LogEntryRatePageContent = () => { const { hasFailedLoadingSource, - isLoadingSource, + isLoading, isUninitialized, loadSource, loadSourceFailureMessage, - } = useSourceContext(); + } = useLogSourceContext(); const { hasLogAnalysisCapabilites, @@ -45,7 +45,7 @@ export const LogEntryRatePageContent = () => { } }, [fetchJobStatus, hasLogAnalysisReadCapabilities]); - if (isLoadingSource || isUninitialized) { + if (isLoading || isUninitialized) { return <SourceLoadingPage />; } else if (hasFailedLoadingSource) { return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index 67c8ea7660a26..e91ef87bdf34a 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -6,20 +6,20 @@ import React from 'react'; -import { useSourceContext } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; import { LogEntryRateModuleProvider } from './use_log_entry_rate_module'; export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, source } = useSourceContext(); + const { sourceId, sourceConfiguration } = useLogSourceContext(); const spaceId = useKibanaSpaceId(); return ( <LogEntryRateModuleProvider - indexPattern={source ? source.configuration.logAlias : ''} + indexPattern={sourceConfiguration?.configuration.logAlias ?? ''} sourceId={sourceId} spaceId={spaceId} - timestampField={source ? source.configuration.fields.timestamp : ''} + timestampField={sourceConfiguration?.configuration.fields.timestamp ?? ''} > {children} </LogEntryRateModuleProvider> diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index ed6f06deeef64..2974939a83215 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, Switch } from 'react-router-dom'; +import { useMount } from 'react-use'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { DocumentTitle } from '../../components/document_title'; @@ -16,16 +18,24 @@ import { AppNavigation } from '../../components/navigation/app_navigation'; import { RoutedTabs } from '../../components/navigation/routed_tabs'; import { ColumnarPage } from '../../components/page'; import { useLogAnalysisCapabilitiesContext } from '../../containers/logs/log_analysis'; +import { useLogSourceContext } from '../../containers/logs/log_source'; import { RedirectWithQueryParams } from '../../utils/redirect_with_query_params'; import { LogEntryCategoriesPage } from './log_entry_categories'; import { LogEntryRatePage } from './log_entry_rate'; import { LogsSettingsPage } from './settings'; import { StreamPage } from './stream'; +import { AlertDropdown } from '../../components/alerting/logs/alert_dropdown'; export const LogsPageContent: React.FunctionComponent = () => { const uiCapabilities = useKibana().services.application?.capabilities; const logAnalysisCapabilities = useLogAnalysisCapabilitiesContext(); + const { initialize } = useLogSourceContext(); + + useMount(() => { + initialize(); + }); + const streamTab = { app: 'logs', title: streamTabTitle, @@ -65,13 +75,20 @@ export const LogsPageContent: React.FunctionComponent = () => { readOnlyBadge={!uiCapabilities?.logs?.save} /> <AppNavigation aria-label={pageTitle}> - <RoutedTabs - tabs={ - logAnalysisCapabilities.hasLogAnalysisCapabilites - ? [streamTab, logRateTab, logCategoriesTab, settingsTab] - : [streamTab, settingsTab] - } - /> + <EuiFlexGroup gutterSize={'none'} alignItems={'center'}> + <EuiFlexItem> + <RoutedTabs + tabs={ + logAnalysisCapabilities.hasLogAnalysisCapabilites + ? [streamTab, logRateTab, logCategoriesTab, settingsTab] + : [streamTab, settingsTab] + } + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <AlertDropdown /> + </EuiFlexItem> + </EuiFlexGroup> </AppNavigation> <Switch> <Route path={streamTab.pathname} component={StreamPage} /> diff --git a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx index 24c1598787a20..d2db5002f4aa2 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_providers.tsx @@ -6,15 +6,16 @@ import React from 'react'; import { LogAnalysisCapabilitiesProvider } from '../../containers/logs/log_analysis'; -import { SourceProvider } from '../../containers/source'; +import { LogSourceProvider } from '../../containers/logs/log_source'; +// import { SourceProvider } from '../../containers/source'; import { useSourceId } from '../../containers/source_id'; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { const [sourceId] = useSourceId(); return ( - <SourceProvider sourceId={sourceId}> + <LogSourceProvider sourceId={sourceId}> <LogAnalysisCapabilitiesProvider>{children}</LogAnalysisCapabilitiesProvider> - </SourceProvider> + </LogSourceProvider> ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings.tsx deleted file mode 100644 index faee7a643085a..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/settings.tsx +++ /dev/null @@ -1,19 +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 React from 'react'; -import { SourceConfigurationSettings } from '../../components/source_configuration/source_configuration_settings'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; - -export const LogsSettingsPage = () => { - const uiCapabilities = useKibana().services.application?.capabilities; - return ( - <SourceConfigurationSettings - shouldAllowEdit={uiCapabilities?.logs?.configureSource as boolean} - displaySettings="logs" - /> - ); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx b/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx new file mode 100644 index 0000000000000..6e68debceac70 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/add_log_column_popover.tsx @@ -0,0 +1,166 @@ +/* + * 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 { + EuiBadge, + EuiButton, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableOption, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { euiStyled } from '../../../../../observability/public'; +import { LogColumnConfiguration } from '../../../utils/source_configuration'; +import { useVisibilityState } from '../../../utils/use_visibility_state'; + +interface SelectableColumnOption { + optionProps: EuiSelectableOption; + columnConfiguration: LogColumnConfiguration; +} + +export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ + addLogColumn: (logColumnConfiguration: LogColumnConfiguration) => void; + availableFields: string[]; + isDisabled?: boolean; +}> = ({ addLogColumn, availableFields, isDisabled }) => { + const { isVisible: isOpen, show: openPopover, hide: closePopover } = useVisibilityState(false); + + const availableColumnOptions = useMemo<SelectableColumnOption[]>( + () => [ + { + optionProps: { + append: <SystemColumnBadge />, + 'data-test-subj': 'addTimestampLogColumn', + // this key works around EuiSelectable using a lowercased label as + // key, which leads to conflicts with field names + key: 'timestamp', + label: 'Timestamp', + }, + columnConfiguration: { + timestampColumn: { + id: uuidv4(), + }, + }, + }, + { + optionProps: { + 'data-test-subj': 'addMessageLogColumn', + append: <SystemColumnBadge />, + // this key works around EuiSelectable using a lowercased label as + // key, which leads to conflicts with field names + key: 'message', + label: 'Message', + }, + columnConfiguration: { + messageColumn: { + id: uuidv4(), + }, + }, + }, + ...availableFields.map<SelectableColumnOption>(field => ({ + optionProps: { + 'data-test-subj': `addFieldLogColumn addFieldLogColumn:${field}`, + // this key works around EuiSelectable using a lowercased label as + // key, which leads to conflicts with fields that only differ in the + // case (e.g. the metricbeat mongodb module) + key: `field-${field}`, + label: field, + }, + columnConfiguration: { + fieldColumn: { + id: uuidv4(), + field, + }, + }, + })), + ], + [availableFields] + ); + + const availableOptions = useMemo<EuiSelectableOption[]>( + () => availableColumnOptions.map(availableColumnOption => availableColumnOption.optionProps), + [availableColumnOptions] + ); + + const handleColumnSelection = useCallback( + (selectedOptions: EuiSelectableOption[]) => { + closePopover(); + + const selectedOptionIndex = selectedOptions.findIndex( + selectedOption => selectedOption.checked === 'on' + ); + const selectedOption = availableColumnOptions[selectedOptionIndex]; + + addLogColumn(selectedOption.columnConfiguration); + }, + [addLogColumn, availableColumnOptions, closePopover] + ); + + return ( + <EuiPopover + anchorPosition="downRight" + button={ + <EuiButton + data-test-subj="addLogColumnButton" + isDisabled={isDisabled} + iconType="plusInCircle" + onClick={openPopover} + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.addLogColumnButtonLabel" + defaultMessage="Add column" + /> + </EuiButton> + } + closePopover={closePopover} + id="addLogColumn" + isOpen={isOpen} + ownFocus + panelPaddingSize="none" + > + <EuiSelectable + height={600} + listProps={selectableListProps} + onChange={handleColumnSelection} + options={availableOptions} + searchable + searchProps={searchProps} + singleSelection + > + {(list, search) => ( + <SelectableContent data-test-subj="addLogColumnPopover"> + <EuiPopoverTitle>{search}</EuiPopoverTitle> + {list} + </SelectableContent> + )} + </EuiSelectable> + </EuiPopover> + ); +}; + +const searchProps = { + 'data-test-subj': 'fieldSearchInput', +}; + +const selectableListProps = { + showIcons: false, +}; + +const SystemColumnBadge: React.FunctionComponent = () => ( + <EuiBadge> + <FormattedMessage + id="xpack.infra.sourceConfiguration.systemColumnBadgeLabel" + defaultMessage="System" + /> + </EuiBadge> +); + +const SelectableContent = euiStyled.div` + width: 400px; +`; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx new file mode 100644 index 0000000000000..ac3b75ad97bb2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/fields_configuration_panel.tsx @@ -0,0 +1,178 @@ +/* + * 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 { + EuiCallOut, + EuiCode, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { InputFieldProps } from '../../../components/source_configuration'; + +interface FieldsConfigurationPanelProps { + isLoading: boolean; + readOnly: boolean; + tiebreakerFieldProps: InputFieldProps; + timestampFieldProps: InputFieldProps; +} + +export const FieldsConfigurationPanel = ({ + isLoading, + readOnly, + tiebreakerFieldProps, + timestampFieldProps, +}: FieldsConfigurationPanelProps) => { + const isTimestampValueDefault = timestampFieldProps.value === '@timestamp'; + const isTiebreakerValueDefault = tiebreakerFieldProps.value === '_doc'; + + return ( + <EuiForm> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.infra.sourceConfiguration.fieldsSectionTitle" + defaultMessage="Fields" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiCallOut + title={i18n.translate('xpack.infra.sourceConfiguration.deprecationNotice', { + defaultMessage: 'Deprecation Notice', + })} + color="warning" + iconType="help" + > + <p> + <FormattedMessage + id="xpack.infra.sourceConfiguration.deprecationMessage" + defaultMessage="Configuring these fields have been deprecated and will be removed in 8.0.0. This application is designed to work with {ecsLink}, you should adjust your indexing to use the {documentationLink}." + values={{ + documentationLink: ( + <EuiLink + href="https://www.elastic.co/guide/en/infrastructure/guide/7.4/infrastructure-metrics.html" + target="BLANK" + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.documentedFields" + defaultMessage="documented fields" + /> + </EuiLink> + ), + ecsLink: ( + <EuiLink + href="https://www.elastic.co/guide/en/ecs/current/index.html" + target="BLANK" + > + ECS + </EuiLink> + ), + }} + /> + </p> + </EuiCallOut> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldLabel" + defaultMessage="Timestamp" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldDescription" + defaultMessage="Timestamp used to sort log entries" + /> + } + > + <EuiFormRow + error={timestampFieldProps.error} + fullWidth + helpText={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldRecommendedValue" + defaultMessage="The recommended value is {defaultValue}" + values={{ + defaultValue: <EuiCode>@timestamp</EuiCode>, + }} + /> + } + isInvalid={timestampFieldProps.isInvalid} + label={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.timestampFieldLabel" + defaultMessage="Timestamp" + /> + } + > + <EuiFieldText + fullWidth + disabled={isLoading || isTimestampValueDefault} + readOnly={readOnly} + isLoading={isLoading} + {...timestampFieldProps} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldLabel" + defaultMessage="Tiebreaker" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldDescription" + defaultMessage="Field used to break ties between two entries with the same timestamp" + /> + } + > + <EuiFormRow + error={tiebreakerFieldProps.error} + fullWidth + helpText={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldRecommendedValue" + defaultMessage="The recommended value is {defaultValue}" + values={{ + defaultValue: <EuiCode>_doc</EuiCode>, + }} + /> + } + isInvalid={tiebreakerFieldProps.isInvalid} + label={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.tiebreakerFieldLabel" + defaultMessage="Tiebreaker" + /> + } + > + <EuiFieldText + fullWidth + disabled={isLoading || isTiebreakerValueDefault} + readOnly={readOnly} + isLoading={isLoading} + {...tiebreakerFieldProps} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + </EuiForm> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/index.ts b/x-pack/plugins/infra/public/pages/logs/settings/index.ts new file mode 100644 index 0000000000000..ebdda7ebbd587 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './source_configuration_settings'; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts new file mode 100644 index 0000000000000..a97e38884a5bd --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_form_state.ts @@ -0,0 +1,123 @@ +/* + * 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 { ReactNode, useCallback, useMemo, useState } from 'react'; +import { + createInputFieldProps, + validateInputFieldNotEmpty, +} from '../../../components/source_configuration/input_fields'; + +interface FormState { + name: string; + description: string; + logAlias: string; + tiebreakerField: string; + timestampField: string; +} + +type FormStateChanges = Partial<FormState>; + +export const useLogIndicesConfigurationFormState = ({ + initialFormState = defaultFormState, +}: { + initialFormState?: FormState; +}) => { + const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({}); + + const resetForm = useCallback(() => setFormStateChanges({}), []); + + const formState = useMemo( + () => ({ + ...initialFormState, + ...formStateChanges, + }), + [initialFormState, formStateChanges] + ); + + const nameFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.name), + name: 'name', + onChange: name => setFormStateChanges(changes => ({ ...changes, name })), + value: formState.name, + }), + [formState.name] + ); + const logAliasFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.logAlias), + name: 'logAlias', + onChange: logAlias => setFormStateChanges(changes => ({ ...changes, logAlias })), + value: formState.logAlias, + }), + [formState.logAlias] + ); + const tiebreakerFieldFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.tiebreakerField), + name: `tiebreakerField`, + onChange: tiebreakerField => + setFormStateChanges(changes => ({ ...changes, tiebreakerField })), + value: formState.tiebreakerField, + }), + [formState.tiebreakerField] + ); + const timestampFieldFieldProps = useMemo( + () => + createInputFieldProps({ + errors: validateInputFieldNotEmpty(formState.timestampField), + name: `timestampField`, + onChange: timestampField => + setFormStateChanges(changes => ({ ...changes, timestampField })), + value: formState.timestampField, + }), + [formState.timestampField] + ); + + const fieldProps = useMemo( + () => ({ + name: nameFieldProps, + logAlias: logAliasFieldProps, + tiebreakerField: tiebreakerFieldFieldProps, + timestampField: timestampFieldFieldProps, + }), + [nameFieldProps, logAliasFieldProps, tiebreakerFieldFieldProps, timestampFieldFieldProps] + ); + + const errors = useMemo( + () => + Object.values(fieldProps).reduce<ReactNode[]>( + (accumulatedErrors, { error }) => [...accumulatedErrors, ...error], + [] + ), + [fieldProps] + ); + + const isFormValid = useMemo(() => errors.length <= 0, [errors]); + + const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]); + + return { + errors, + fieldProps, + formState, + formStateChanges, + isFormDirty, + isFormValid, + resetForm, + }; +}; + +const defaultFormState: FormState = { + name: '', + description: '', + logAlias: '', + tiebreakerField: '', + timestampField: '', +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx new file mode 100644 index 0000000000000..83effaa3d51a5 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiCode, + EuiDescribedFormGroup, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { InputFieldProps } from '../../../components/source_configuration'; + +interface IndicesConfigurationPanelProps { + isLoading: boolean; + readOnly: boolean; + logAliasFieldProps: InputFieldProps; +} + +export const IndicesConfigurationPanel = ({ + isLoading, + readOnly, + logAliasFieldProps, +}: IndicesConfigurationPanelProps) => ( + <EuiForm> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.infra.sourceConfiguration.indicesSectionTitle" + defaultMessage="Indices" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesTitle" + defaultMessage="Log indices" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesDescription" + defaultMessage="Index pattern for matching indices that contain log data" + /> + } + > + <EuiFormRow + error={logAliasFieldProps.error} + fullWidth + helpText={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesRecommendedValue" + defaultMessage="The recommended value is {defaultValue}" + values={{ + defaultValue: <EuiCode>filebeat-*</EuiCode>, + }} + /> + } + isInvalid={logAliasFieldProps.isInvalid} + label={ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logIndicesLabel" + defaultMessage="Log indices" + /> + } + > + <EuiFieldText + data-test-subj="logIndicesInput" + fullWidth + disabled={isLoading} + isLoading={isLoading} + readOnly={readOnly} + {...logAliasFieldProps} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + </EuiForm> +); diff --git a/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx new file mode 100644 index 0000000000000..0beccffe5f4e8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_form_state.tsx @@ -0,0 +1,153 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { + FieldLogColumnConfiguration, + isMessageLogColumnConfiguration, + isTimestampLogColumnConfiguration, + LogColumnConfiguration, + MessageLogColumnConfiguration, + TimestampLogColumnConfiguration, +} from '../../../utils/source_configuration'; + +export interface TimestampLogColumnConfigurationProps { + logColumnConfiguration: TimestampLogColumnConfiguration['timestampColumn']; + remove: () => void; + type: 'timestamp'; +} + +export interface MessageLogColumnConfigurationProps { + logColumnConfiguration: MessageLogColumnConfiguration['messageColumn']; + remove: () => void; + type: 'message'; +} + +export interface FieldLogColumnConfigurationProps { + logColumnConfiguration: FieldLogColumnConfiguration['fieldColumn']; + remove: () => void; + type: 'field'; +} + +export type LogColumnConfigurationProps = + | TimestampLogColumnConfigurationProps + | MessageLogColumnConfigurationProps + | FieldLogColumnConfigurationProps; + +interface FormState { + logColumns: LogColumnConfiguration[]; +} + +type FormStateChanges = Partial<FormState>; + +export const useLogColumnsConfigurationFormState = ({ + initialFormState = defaultFormState, +}: { + initialFormState?: FormState; +}) => { + const [formStateChanges, setFormStateChanges] = useState<FormStateChanges>({}); + + const resetForm = useCallback(() => setFormStateChanges({}), []); + + const formState = useMemo( + () => ({ + ...initialFormState, + ...formStateChanges, + }), + [initialFormState, formStateChanges] + ); + + const logColumnConfigurationProps = useMemo<LogColumnConfigurationProps[]>( + () => + formState.logColumns.map( + (logColumn): LogColumnConfigurationProps => { + const remove = () => + setFormStateChanges(changes => ({ + ...changes, + logColumns: formState.logColumns.filter(item => item !== logColumn), + })); + + if (isTimestampLogColumnConfiguration(logColumn)) { + return { + logColumnConfiguration: logColumn.timestampColumn, + remove, + type: 'timestamp', + }; + } else if (isMessageLogColumnConfiguration(logColumn)) { + return { + logColumnConfiguration: logColumn.messageColumn, + remove, + type: 'message', + }; + } else { + return { + logColumnConfiguration: logColumn.fieldColumn, + remove, + type: 'field', + }; + } + } + ), + [formState.logColumns] + ); + + const addLogColumn = useCallback( + (logColumnConfiguration: LogColumnConfiguration) => + setFormStateChanges(changes => ({ + ...changes, + logColumns: [...formState.logColumns, logColumnConfiguration], + })), + [formState.logColumns] + ); + + const moveLogColumn = useCallback( + (sourceIndex, destinationIndex) => { + if (destinationIndex >= 0 && sourceIndex <= formState.logColumns.length - 1) { + const newLogColumns = [...formState.logColumns]; + newLogColumns.splice(destinationIndex, 0, newLogColumns.splice(sourceIndex, 1)[0]); + setFormStateChanges(changes => ({ + ...changes, + logColumns: newLogColumns, + })); + } + }, + [formState.logColumns] + ); + + const errors = useMemo( + () => + logColumnConfigurationProps.length <= 0 + ? [ + <FormattedMessage + id="xpack.infra.sourceConfiguration.logColumnListEmptyErrorMessage" + defaultMessage="The log column list must not be empty." + />, + ] + : [], + [logColumnConfigurationProps] + ); + + const isFormValid = useMemo(() => (errors.length <= 0 ? true : false), [errors]); + + const isFormDirty = useMemo(() => Object.keys(formStateChanges).length > 0, [formStateChanges]); + + return { + addLogColumn, + moveLogColumn, + errors, + logColumnConfigurationProps, + formState, + formStateChanges, + isFormDirty, + isFormValid, + resetForm, + }; +}; + +const defaultFormState: FormState = { + logColumns: [], +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx new file mode 100644 index 0000000000000..777f611ef33f7 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/log_columns_configuration_panel.tsx @@ -0,0 +1,276 @@ +/* + * 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 { + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiIcon, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback } from 'react'; +import { DragHandleProps, DropResult } from '../../../../../observability/public'; +import { LogColumnConfiguration } from '../../../utils/source_configuration'; +import { AddLogColumnButtonAndPopover } from './add_log_column_popover'; +import { + FieldLogColumnConfigurationProps, + LogColumnConfigurationProps, +} from './log_columns_configuration_form_state'; + +interface LogColumnsConfigurationPanelProps { + availableFields: string[]; + isLoading: boolean; + logColumnConfiguration: LogColumnConfigurationProps[]; + addLogColumn: (logColumn: LogColumnConfiguration) => void; + moveLogColumn: (sourceIndex: number, destinationIndex: number) => void; +} + +export const LogColumnsConfigurationPanel: React.FunctionComponent<LogColumnsConfigurationPanelProps> = ({ + addLogColumn, + moveLogColumn, + availableFields, + isLoading, + logColumnConfiguration, +}) => { + const onDragEnd = useCallback( + ({ source, destination }: DropResult) => + destination && moveLogColumn(source.index, destination.index), + [moveLogColumn] + ); + + return ( + <EuiForm> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle size="s" data-test-subj="sourceConfigurationLogColumnsSectionTitle"> + <h3> + <FormattedMessage + id="xpack.infra.sourceConfiguration.logColumnsSectionTitle" + defaultMessage="Log Columns" + /> + </h3> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <AddLogColumnButtonAndPopover + addLogColumn={addLogColumn} + availableFields={availableFields} + isDisabled={isLoading} + /> + </EuiFlexItem> + </EuiFlexGroup> + {logColumnConfiguration.length > 0 ? ( + <EuiDragDropContext onDragEnd={onDragEnd}> + <EuiDroppable droppableId="COLUMN_CONFIG_DROPPABLE_AREA"> + <> + {/* Fragment here necessary for typechecking */} + {logColumnConfiguration.map((column, index) => ( + <EuiDraggable + key={`logColumnConfigurationPanel-${column.logColumnConfiguration.id}`} + index={index} + draggableId={column.logColumnConfiguration.id} + customDragHandle + > + {provided => ( + <LogColumnConfigurationPanel + dragHandleProps={provided.dragHandleProps} + logColumnConfigurationProps={column} + /> + )} + </EuiDraggable> + ))} + </> + </EuiDroppable> + </EuiDragDropContext> + ) : ( + <LogColumnConfigurationEmptyPrompt /> + )} + </EuiForm> + ); +}; + +interface LogColumnConfigurationPanelProps { + logColumnConfigurationProps: LogColumnConfigurationProps; + dragHandleProps: DragHandleProps; +} + +const LogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = props => ( + <> + <EuiSpacer size="m" /> + {props.logColumnConfigurationProps.type === 'timestamp' ? ( + <TimestampLogColumnConfigurationPanel {...props} /> + ) : props.logColumnConfigurationProps.type === 'message' ? ( + <MessageLogColumnConfigurationPanel {...props} /> + ) : ( + <FieldLogColumnConfigurationPanel + logColumnConfigurationProps={props.logColumnConfigurationProps} + dragHandleProps={props.dragHandleProps} + /> + )} + </> +); + +const TimestampLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({ + logColumnConfigurationProps, + dragHandleProps, +}) => ( + <ExplainedLogColumnConfigurationPanel + fieldName="Timestamp" + helpText={ + <FormattedMessage + tagName="span" + id="xpack.infra.sourceConfiguration.timestampLogColumnDescription" + defaultMessage="This system field shows the log entry's time as determined by the {timestampSetting} field setting." + values={{ + timestampSetting: <code>timestamp</code>, + }} + /> + } + removeColumn={logColumnConfigurationProps.remove} + dragHandleProps={dragHandleProps} + /> +); + +const MessageLogColumnConfigurationPanel: React.FunctionComponent<LogColumnConfigurationPanelProps> = ({ + logColumnConfigurationProps, + dragHandleProps, +}) => ( + <ExplainedLogColumnConfigurationPanel + fieldName="Message" + helpText={ + <FormattedMessage + tagName="span" + id="xpack.infra.sourceConfiguration.messageLogColumnDescription" + defaultMessage="This system field shows the log entry message as derived from the document fields." + /> + } + removeColumn={logColumnConfigurationProps.remove} + dragHandleProps={dragHandleProps} + /> +); + +const FieldLogColumnConfigurationPanel: React.FunctionComponent<{ + logColumnConfigurationProps: FieldLogColumnConfigurationProps; + dragHandleProps: DragHandleProps; +}> = ({ + logColumnConfigurationProps: { + logColumnConfiguration: { field }, + remove, + }, + dragHandleProps, +}) => { + const fieldLogColumnTitle = i18n.translate( + 'xpack.infra.sourceConfiguration.fieldLogColumnTitle', + { + defaultMessage: 'Field', + } + ); + return ( + <EuiPanel data-test-subj={`logColumnPanel fieldLogColumnPanel fieldLogColumnPanel:${field}`}> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <div data-test-subj="moveLogColumnHandle" {...dragHandleProps}> + <EuiIcon type="grab" /> + </div> + </EuiFlexItem> + <EuiFlexItem grow={1}>{fieldLogColumnTitle}</EuiFlexItem> + <EuiFlexItem grow={3}> + <code>{field}</code> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <RemoveLogColumnButton + onClick={remove} + columnDescription={`${fieldLogColumnTitle} - ${field}`} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + ); +}; + +const ExplainedLogColumnConfigurationPanel: React.FunctionComponent<{ + fieldName: React.ReactNode; + helpText: React.ReactNode; + removeColumn: () => void; + dragHandleProps: DragHandleProps; +}> = ({ fieldName, helpText, removeColumn, dragHandleProps }) => ( + <EuiPanel + data-test-subj={`logColumnPanel systemLogColumnPanel systemLogColumnPanel:${fieldName}`} + > + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <div data-test-subj="moveLogColumnHandle" {...dragHandleProps}> + <EuiIcon type="grab" /> + </div> + </EuiFlexItem> + <EuiFlexItem grow={1}>{fieldName}</EuiFlexItem> + <EuiFlexItem grow={3}> + <EuiText size="s" color="subdued"> + {helpText} + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <RemoveLogColumnButton onClick={removeColumn} columnDescription={String(fieldName)} /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> +); + +const RemoveLogColumnButton: React.FunctionComponent<{ + onClick?: () => void; + columnDescription: string; +}> = ({ onClick, columnDescription }) => { + const removeColumnLabel = i18n.translate( + 'xpack.infra.sourceConfiguration.removeLogColumnButtonLabel', + { + defaultMessage: 'Remove {columnDescription} column', + values: { columnDescription }, + } + ); + + return ( + <EuiButtonIcon + color="danger" + data-test-subj="removeLogColumnButton" + iconType="trash" + onClick={onClick} + title={removeColumnLabel} + aria-label={removeColumnLabel} + /> + ); +}; + +const LogColumnConfigurationEmptyPrompt: React.FunctionComponent = () => ( + <EuiEmptyPrompt + iconType="list" + title={ + <h2> + <FormattedMessage + id="xpack.infra.sourceConfiguration.noLogColumnsTitle" + defaultMessage="No columns" + /> + </h2> + } + body={ + <p> + <FormattedMessage + id="xpack.infra.sourceConfiguration.noLogColumnsDescription" + defaultMessage="Add a column to this list using the button above." + /> + </p> + } + /> +); diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx new file mode 100644 index 0000000000000..92d70955c678f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_form_state.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useMemo } from 'react'; +import { LogSourceConfigurationProperties } from '../../../containers/logs/log_source'; +import { useLogIndicesConfigurationFormState } from './indices_configuration_form_state'; +import { useLogColumnsConfigurationFormState } from './log_columns_configuration_form_state'; + +export const useLogSourceConfigurationFormState = ( + configuration?: LogSourceConfigurationProperties +) => { + const indicesConfigurationFormState = useLogIndicesConfigurationFormState({ + initialFormState: useMemo( + () => + configuration + ? { + name: configuration.name, + description: configuration.description, + logAlias: configuration.logAlias, + tiebreakerField: configuration.fields.tiebreaker, + timestampField: configuration.fields.timestamp, + } + : undefined, + [configuration] + ), + }); + + const logColumnsConfigurationFormState = useLogColumnsConfigurationFormState({ + initialFormState: useMemo( + () => + configuration + ? { + logColumns: configuration.logColumns, + } + : undefined, + [configuration] + ), + }); + + const errors = useMemo( + () => [...indicesConfigurationFormState.errors, ...logColumnsConfigurationFormState.errors], + [indicesConfigurationFormState.errors, logColumnsConfigurationFormState.errors] + ); + + const resetForm = useCallback(() => { + indicesConfigurationFormState.resetForm(); + logColumnsConfigurationFormState.resetForm(); + }, [indicesConfigurationFormState, logColumnsConfigurationFormState]); + + const isFormDirty = useMemo( + () => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty, + [indicesConfigurationFormState.isFormDirty, logColumnsConfigurationFormState.isFormDirty] + ); + + const isFormValid = useMemo( + () => indicesConfigurationFormState.isFormValid && logColumnsConfigurationFormState.isFormValid, + [indicesConfigurationFormState.isFormValid, logColumnsConfigurationFormState.isFormValid] + ); + + const formState = useMemo( + () => ({ + name: indicesConfigurationFormState.formState.name, + description: indicesConfigurationFormState.formState.description, + logAlias: indicesConfigurationFormState.formState.logAlias, + fields: { + tiebreaker: indicesConfigurationFormState.formState.tiebreakerField, + timestamp: indicesConfigurationFormState.formState.timestampField, + }, + logColumns: logColumnsConfigurationFormState.formState.logColumns, + }), + [indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState] + ); + + const formStateChanges = useMemo( + () => ({ + name: indicesConfigurationFormState.formStateChanges.name, + description: indicesConfigurationFormState.formStateChanges.description, + logAlias: indicesConfigurationFormState.formStateChanges.logAlias, + fields: { + tiebreaker: indicesConfigurationFormState.formStateChanges.tiebreakerField, + timestamp: indicesConfigurationFormState.formStateChanges.timestampField, + }, + logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns, + }), + [ + indicesConfigurationFormState.formStateChanges, + logColumnsConfigurationFormState.formStateChanges, + ] + ); + + return { + addLogColumn: logColumnsConfigurationFormState.addLogColumn, + moveLogColumn: logColumnsConfigurationFormState.moveLogColumn, + errors, + formState, + formStateChanges, + isFormDirty, + isFormValid, + indicesConfigurationProps: indicesConfigurationFormState.fieldProps, + logColumnConfigurationProps: logColumnsConfigurationFormState.logColumnConfigurationProps, + resetForm, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx new file mode 100644 index 0000000000000..88b1441f0ba7c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/settings/source_configuration_settings.tsx @@ -0,0 +1,193 @@ +/* + * 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 { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiPage, + EuiPageBody, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { FieldsConfigurationPanel } from './fields_configuration_panel'; +import { IndicesConfigurationPanel } from './indices_configuration_panel'; +import { NameConfigurationPanel } from '../../../components/source_configuration/name_configuration_panel'; +import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; +import { useLogSourceConfigurationFormState } from './source_configuration_form_state'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { Prompt } from '../../../utils/navigation_warning_prompt'; + +export const LogsSettingsPage = () => { + const uiCapabilities = useKibana().services.application?.capabilities; + const shouldAllowEdit = uiCapabilities?.logs?.configureSource === true; + + const { + sourceConfiguration: source, + sourceStatus, + isLoading, + isUninitialized, + updateSourceConfiguration, + } = useLogSourceContext(); + + const availableFields = useMemo( + () => sourceStatus?.logIndexFields.map(field => field.name) ?? [], + [sourceStatus] + ); + + const { + addLogColumn, + moveLogColumn, + indicesConfigurationProps, + logColumnConfigurationProps, + errors, + resetForm, + isFormDirty, + isFormValid, + formStateChanges, + } = useLogSourceConfigurationFormState(source?.configuration); + + const persistUpdates = useCallback(async () => { + await updateSourceConfiguration(formStateChanges); + resetForm(); + }, [updateSourceConfiguration, resetForm, formStateChanges]); + + const isWriteable = useMemo(() => shouldAllowEdit && source && source.origin !== 'internal', [ + shouldAllowEdit, + source, + ]); + + if ((isLoading || isUninitialized) && !source) { + return <SourceLoadingPage />; + } + if (!source?.configuration) { + return null; + } + + return ( + <> + <EuiPage> + <EuiPageBody + className="eui-displayBlock" + restrictWidth + data-test-subj="sourceConfigurationContent" + > + <Prompt prompt={isFormDirty ? unsavedFormPromptMessage : undefined} /> + <EuiPanel paddingSize="l"> + <NameConfigurationPanel + isLoading={isLoading} + nameFieldProps={indicesConfigurationProps.name} + readOnly={!isWriteable} + /> + </EuiPanel> + <EuiSpacer /> + <EuiPanel paddingSize="l"> + <IndicesConfigurationPanel + isLoading={isLoading} + logAliasFieldProps={indicesConfigurationProps.logAlias} + readOnly={!isWriteable} + /> + </EuiPanel> + <EuiSpacer /> + <EuiPanel paddingSize="l"> + <FieldsConfigurationPanel + isLoading={isLoading} + readOnly={!isWriteable} + tiebreakerFieldProps={indicesConfigurationProps.tiebreakerField} + timestampFieldProps={indicesConfigurationProps.timestampField} + /> + </EuiPanel> + <EuiSpacer /> + <EuiPanel paddingSize="l"> + <LogColumnsConfigurationPanel + addLogColumn={addLogColumn} + moveLogColumn={moveLogColumn} + availableFields={availableFields} + isLoading={isLoading} + logColumnConfiguration={logColumnConfigurationProps} + /> + </EuiPanel> + {errors.length > 0 ? ( + <> + <EuiCallOut color="danger"> + <ul> + {errors.map((error, errorIndex) => ( + <li key={errorIndex}>{error}</li> + ))} + </ul> + </EuiCallOut> + <EuiSpacer size="m" /> + </> + ) : null} + <EuiSpacer size="m" /> + <EuiFlexGroup> + {isWriteable && ( + <EuiFlexItem> + {isLoading ? ( + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButton color="primary" isLoading fill> + Loading + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + ) : ( + <> + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="discardSettingsButton" + color="danger" + iconType="cross" + isDisabled={isLoading || !isFormDirty} + onClick={() => { + resetForm(); + }} + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.discardSettingsButtonLabel" + defaultMessage="Discard" + /> + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="applySettingsButton" + color="primary" + isDisabled={!isFormDirty || !isFormValid} + fill + onClick={persistUpdates} + > + <FormattedMessage + id="xpack.infra.sourceConfiguration.applySettingsButtonLabel" + defaultMessage="Apply" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiPageBody> + </EuiPage> + </> + ); +}; + +const unsavedFormPromptMessage = i18n.translate( + 'xpack.infra.logSourceConfiguration.unsavedFormPromptMessage', + { + defaultMessage: 'Are you sure you want to leave? Changes will be lost', + } +); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index 3e07dcebf112d..712d625052140 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -5,12 +5,11 @@ */ import React from 'react'; - +import { useTrackPageview } from '../../../../../observability/public'; import { ColumnarPage } from '../../../components/page'; import { StreamPageContent } from './page_content'; import { StreamPageHeader } from './page_header'; import { LogsPageProviders } from './page_providers'; -import { useTrackPageview } from '../../../../../observability/public'; export const StreamPage = () => { useTrackPageview({ app: 'infra_logs', path: 'stream' }); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx index 010a17dae4ebd..40ac5c74a6836 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_content.tsx @@ -7,21 +7,21 @@ import React from 'react'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; -import { useSourceContext } from '../../../containers/source'; import { LogsPageLogsContent } from './page_logs_content'; import { LogsPageNoIndicesContent } from './page_no_indices_content'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; export const StreamPageContent: React.FunctionComponent = () => { const { hasFailedLoadingSource, - isLoadingSource, + isLoading, isUninitialized, loadSource, loadSourceFailureMessage, logIndicesExist, - } = useSourceContext(); + } = useLogSourceContext(); - if (isLoadingSource || isUninitialized) { + if (isLoading || isUninitialized) { return <SourceLoadingPage />; } else if (hasFailedLoadingSource) { return <SourceErrorPage errorMessage={loadSourceFailureMessage ?? ''} retry={loadSource} />; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index b6061203a1c72..85781c48f9512 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -5,31 +5,30 @@ */ import React, { useContext } from 'react'; - import { euiStyled } from '../../../../../observability/public'; import { AutoSizer } from '../../../components/auto_sizer'; import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout'; import { LogMinimap } from '../../../components/logging/log_minimap'; import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream'; import { PageContent } from '../../../components/page'; - -import { WithSummary } from '../../../containers/logs/log_summary'; -import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; import { LogFilterState } from '../../../containers/logs/log_filter'; import { LogFlyout as LogFlyoutState, WithFlyoutOptionsUrlState, } from '../../../containers/logs/log_flyout'; +import { LogHighlightsState } from '../../../containers/logs/log_highlights'; import { LogPositionState } from '../../../containers/logs/log_position'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; +import { WithSummary } from '../../../containers/logs/log_summary'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; import { WithLogTextviewUrlState } from '../../../containers/logs/with_log_textview'; import { WithStreamItems } from '../../../containers/logs/with_stream_items'; -import { Source } from '../../../containers/source'; - import { LogsToolbar } from './page_toolbar'; -import { LogHighlightsState } from '../../../containers/logs/log_highlights'; +import { PageViewLogInContext } from './page_view_log_in_context'; export const LogsPageLogsContent: React.FunctionComponent = () => { - const { source, sourceId, version } = useContext(Source.Context); + const { sourceConfiguration, sourceId } = useLogSourceContext(); const { textScale, textWrap } = useContext(LogViewConfiguration.Context); const { setFlyoutVisibility, @@ -55,11 +54,15 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { endDateExpression, updateDateRange, } = useContext(LogPositionState.Context); + + const [, { setContextEntry }] = useContext(ViewLogInContext.Context); + return ( <> <WithLogTextviewUrlState /> <WithFlyoutOptionsUrlState /> <LogsToolbar /> + <PageViewLogInContext /> {flyoutVisible ? ( <LogEntryFlyout setFilter={applyLogFilterQuery} @@ -73,7 +76,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { loading={isLoading} /> ) : null} - <PageContent key={`${sourceId}-${version}`}> + <PageContent key={`${sourceId}-${sourceConfiguration?.version}`}> <WithStreamItems> {({ currentHighlightKey, @@ -87,7 +90,9 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { checkForNewEntries, }) => ( <ScrollableLogTextStreamView - columnConfigurations={(source && source.configuration.logColumns) || []} + columnConfigurations={ + (sourceConfiguration && sourceConfiguration.configuration.logColumns) || [] + } hasMoreAfterEnd={hasMoreAfterEnd} hasMoreBeforeStart={hasMoreBeforeStart} isLoadingMore={isLoadingMore} @@ -104,6 +109,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { wrap={textWrap} setFlyoutItem={setFlyoutId} setFlyoutVisibility={setFlyoutVisibility} + setContextEntry={setContextEntry} highlightedItem={surroundingLogsId ? surroundingLogsId : null} currentHighlightKey={currentHighlightKey} startDateExpression={startDateExpression} diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx index e4ccdaf7c5748..428a7d3fdfe4b 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -12,12 +12,11 @@ import { LogHighlightsState } from '../../../containers/logs/log_highlights/log_ import { LogPositionState, WithLogPositionUrlState } from '../../../containers/logs/log_position'; import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/log_filter'; import { LogEntriesState } from '../../../containers/logs/log_entries'; - -import { Source } from '../../../containers/source'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; +import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; const LogFilterStateProvider: React.FC = ({ children }) => { - const { createDerivedIndexPattern } = useContext(Source.Context); - const derivedIndexPattern = createDerivedIndexPattern('logs'); + const { derivedIndexPattern } = useLogSourceContext(); return ( <LogFilterState.Provider indexPattern={derivedIndexPattern}> <WithLogFilterUrlState /> @@ -26,8 +25,27 @@ const LogFilterStateProvider: React.FC = ({ children }) => { ); }; +const ViewLogInContextProvider: React.FC = ({ children }) => { + const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context); + const { sourceId } = useLogSourceContext(); + + if (!startTimestamp || !endTimestamp) { + return null; + } + + return ( + <ViewLogInContext.Provider + startTimestamp={startTimestamp} + endTimestamp={endTimestamp} + sourceId={sourceId} + > + {children} + </ViewLogInContext.Provider> + ); +}; + const LogEntriesStateProvider: React.FC = ({ children }) => { - const { sourceId } = useContext(Source.Context); + const { sourceId } = useLogSourceContext(); const { startTimestamp, endTimestamp, @@ -69,13 +87,13 @@ const LogEntriesStateProvider: React.FC = ({ children }) => { }; const LogHighlightsStateProvider: React.FC = ({ children }) => { - const { sourceId, version } = useContext(Source.Context); + const { sourceId, sourceConfiguration } = useLogSourceContext(); const [{ topCursor, bottomCursor, centerCursor, entries }] = useContext(LogEntriesState.Context); const { filterQuery } = useContext(LogFilterState.Context); const highlightsProps = { sourceId, - sourceVersion: version, + sourceVersion: sourceConfiguration?.version, entriesStart: topCursor, entriesEnd: bottomCursor, centerCursor, @@ -86,16 +104,25 @@ const LogHighlightsStateProvider: React.FC = ({ children }) => { }; export const LogsPageProviders: React.FunctionComponent = ({ children }) => { + const { logIndicesExist } = useLogSourceContext(); + + // The providers assume the source is loaded, so short-circuit them otherwise + if (!logIndicesExist) { + return <>{children}</>; + } + return ( <LogViewConfiguration.Provider> <LogFlyout.Provider> <LogPositionState.Provider> <WithLogPositionUrlState /> - <LogFilterStateProvider> - <LogEntriesStateProvider> - <LogHighlightsStateProvider>{children}</LogHighlightsStateProvider> - </LogEntriesStateProvider> - </LogFilterStateProvider> + <ViewLogInContextProvider> + <LogFilterStateProvider> + <LogEntriesStateProvider> + <LogHighlightsStateProvider>{children}</LogHighlightsStateProvider> + </LogEntriesStateProvider> + </LogFilterStateProvider> + </ViewLogInContextProvider> </LogPositionState.Provider> </LogFlyout.Provider> </LogViewConfiguration.Provider> diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx index 2f9a76fd47490..9667272eb2417 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_toolbar.tsx @@ -19,13 +19,12 @@ import { LogFlyout } from '../../../containers/logs/log_flyout'; import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; import { LogFilterState } from '../../../containers/logs/log_filter'; import { LogPositionState } from '../../../containers/logs/log_position'; -import { Source } from '../../../containers/source'; import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion'; import { LogDatepicker } from '../../../components/logging/log_datepicker'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; export const LogsToolbar = () => { - const { createDerivedIndexPattern } = useContext(Source.Context); - const derivedIndexPattern = createDerivedIndexPattern('logs'); + const { derivedIndexPattern } = useLogSourceContext(); const { availableTextScales, setTextScale, setTextWrap, textScale, textWrap } = useContext( LogViewConfiguration.Context ); diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx new file mode 100644 index 0000000000000..9e0f7d5035aaf --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -0,0 +1,124 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiOverlayMask, + EuiText, + EuiTextColor, + EuiToolTip, +} from '@elastic/eui'; +import { noop } from 'lodash'; +import React, { useCallback, useContext, useMemo } from 'react'; +import { LogEntry } from '../../../../common/http_api'; +import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream'; +import { useLogSourceContext } from '../../../containers/logs/log_source'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; +import { useViewportDimensions } from '../../../utils/use_viewport_dimensions'; + +const MODAL_MARGIN = 25; + +export const PageViewLogInContext: React.FC = () => { + const { sourceConfiguration } = useLogSourceContext(); + const { textScale, textWrap } = useContext(LogViewConfiguration.Context); + const columnConfigurations = useMemo(() => sourceConfiguration?.configuration.logColumns ?? [], [ + sourceConfiguration, + ]); + const [{ contextEntry, entries, isLoading }, { setContextEntry }] = useContext( + ViewLogInContext.Context + ); + const closeModal = useCallback(() => setContextEntry(undefined), [setContextEntry]); + const { width: vw, height: vh } = useViewportDimensions(); + + const streamItems = useMemo( + () => + entries.map(entry => ({ + kind: 'logEntry' as const, + logEntry: entry, + highlights: [], + })), + [entries] + ); + + if (!contextEntry) { + return null; + } + + return ( + <EuiOverlayMask> + <EuiModal onClose={closeModal} maxWidth={false}> + <EuiModalBody style={{ width: vw - MODAL_MARGIN * 2, height: vh - MODAL_MARGIN * 2 }}> + <EuiFlexGroup + direction="column" + responsive={false} + wrap={false} + style={{ height: '100%' }} + > + <EuiFlexItem grow={1}> + <LogEntryContext context={contextEntry.context} /> + <ScrollableLogTextStreamView + target={contextEntry.cursor} + columnConfigurations={columnConfigurations} + items={streamItems} + scale={textScale} + wrap={textWrap} + isReloading={isLoading} + isLoadingMore={false} + hasMoreBeforeStart={false} + hasMoreAfterEnd={false} + isStreaming={false} + lastLoadedTime={null} + jumpToTarget={noop} + reportVisibleInterval={noop} + loadNewerItems={noop} + reloadItems={noop} + highlightedItem={contextEntry.id} + currentHighlightKey={null} + startDateExpression={''} + endDateExpression={''} + updateDateRange={noop} + startLiveStreaming={noop} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiModalBody> + </EuiModal> + </EuiOverlayMask> + ); +}; + +const LogEntryContext: React.FC<{ context: LogEntry['context'] }> = ({ context }) => { + if ('container.id' in context) { + return <p>Displayed logs are from container {context['container.id']}</p>; + } + + if ('host.name' in context) { + const shortenedFilePath = + context['log.file.path'].length > 45 + ? context['log.file.path'].slice(0, 20) + '...' + context['log.file.path'].slice(-25) + : context['log.file.path']; + + return ( + <EuiText size="s"> + <p> + <EuiTextColor color="subdued"> + Displayed logs are from file{' '} + <EuiToolTip content={context['log.file.path']}> + <span>{shortenedFilePath}</span> + </EuiToolTip>{' '} + and host {context['host.name']} + </EuiTextColor> + </p> + </EuiText> + ); + } + + return null; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/helpers.ts b/x-pack/plugins/infra/public/pages/metrics/components/helpers.ts deleted file mode 100644 index 4449196f2fb53..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/components/helpers.ts +++ /dev/null @@ -1,99 +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 { ReactText } from 'react'; -import Color from 'color'; -import { get, first, last, min, max } from 'lodash'; -import { createFormatter } from '../../../utils/formatters'; -import { InfraDataSeries } from '../../../graphql/types'; -import { - InventoryVisTypeRT, - InventoryFormatterType, - InventoryVisType, -} from '../../../../common/inventory_models/types'; -import { SeriesOverrides } from '../types'; -import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; - -/** - * Returns a formatter - */ -export const getFormatter = ( - formatter: InventoryFormatterType = 'number', - template: string = '{{value}}' -) => (val: ReactText) => (val != null ? createFormatter(formatter, template)(val) : ''); - -/** - * Does a series have more then two points? - */ -export const seriesHasLessThen2DataPoints = (series: InfraDataSeries): boolean => { - return series.data.length < 2; -}; - -/** - * Returns the minimum and maximum timestamp for a metric - */ -export const getMaxMinTimestamp = (metric: NodeDetailsMetricData): [number, number] => { - if (metric.series.some(seriesHasLessThen2DataPoints)) { - return [0, 0]; - } - const values = metric.series.reduce((acc, item) => { - const firstRow = first(item.data); - const lastRow = last(item.data); - return acc.concat([(firstRow && firstRow.timestamp) || 0, (lastRow && lastRow.timestamp) || 0]); - }, [] as number[]); - return [min(values), max(values)]; -}; - -/** - * Returns the chart name from the visConfig based on the series id, otherwise it - * just returns the seriesId - */ -export const getChartName = ( - seriesOverrides: SeriesOverrides | undefined, - seriesId: string, - label: string -) => { - if (!seriesOverrides) { - return label; - } - return get(seriesOverrides, [seriesId, 'name'], label); -}; - -/** - * Returns the chart color from the visConfig based on the series id, otherwise it - * just returns null if the color doesn't exists in the overrides. - */ -export const getChartColor = (seriesOverrides: SeriesOverrides | undefined, seriesId: string) => { - const rawColor: string | null = seriesOverrides - ? get(seriesOverrides, [seriesId, 'color']) - : null; - if (!rawColor) { - return null; - } - const color = new Color(rawColor); - return color.hex().toString(); -}; - -/** - * Gets the chart type based on the section and seriesId - */ -export const getChartType = ( - seriesOverrides: SeriesOverrides | undefined, - type: InventoryVisType | undefined, - seriesId: string -) => { - if (!seriesOverrides || !type) { - return 'line'; - } - const overrideValue = get(seriesOverrides, [seriesId, 'type']); - if (InventoryVisTypeRT.is(overrideValue)) { - return overrideValue; - } - if (InventoryVisTypeRT.is(type)) { - return type; - } - return 'line'; -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/components/series_chart.tsx deleted file mode 100644 index 849a5b8922165..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/components/series_chart.tsx +++ /dev/null @@ -1,87 +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 React from 'react'; -import { - AreaSeries, - BarSeries, - ScaleType, - RecursivePartial, - BarSeriesStyle, - AreaSeriesStyle, -} from '@elastic/charts'; -import { InfraDataSeries } from '../../../graphql/types'; -import { InventoryVisType } from '../../../../common/inventory_models/types'; - -interface Props { - id: string; - name: string; - color: string | null; - series: InfraDataSeries; - type: InventoryVisType; - stack: boolean | undefined; -} - -export const SeriesChart = (props: Props) => { - if (props.type === 'bar') { - return <BarChart {...props} />; - } - return <AreaChart {...props} />; -}; - -export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { - const style: RecursivePartial<AreaSeriesStyle> = { - area: { - opacity: 1, - visible: 'area' === type, - }, - line: { - strokeWidth: 'area' === type ? 1 : 2, - visible: true, - }, - }; - return ( - <AreaSeries - id={id} - name={name} - xScaleType={ScaleType.Time} - yScaleType={ScaleType.Linear} - xAccessor="timestamp" - yAccessors={['value']} - data={series.data} - areaSeriesStyle={style} - color={color ? color : void 0} - stackAccessors={stack ? ['timestamp'] : void 0} - /> - ); -}; - -export const BarChart = ({ id, color, series, name, type, stack }: Props) => { - const style: RecursivePartial<BarSeriesStyle> = { - rectBorder: { - stroke: color || void 0, - strokeWidth: 1, - visible: true, - }, - rect: { - opacity: 1, - }, - }; - return ( - <BarSeries - id={id} - name={name} - xScaleType={ScaleType.Time} - yScaleType={ScaleType.Linear} - xAccessor="timestamp" - yAccessors={['value']} - data={series.data} - barSeriesStyle={style} - color={color ? color : void 0} - stackAccessors={stack ? ['timestamp'] : void 0} - /> - ); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 531be40d2dc43..dbf71665ea869 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -3,138 +3,136 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { i18n } from '@kbn/i18n'; -import React, { useContext, useState } from 'react'; -import { euiStyled, EuiTheme, withTheme } from '../../../../observability/public'; + +import React from 'react'; +import { Route, RouteComponentProps, Switch } from 'react-router-dom'; + +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { DocumentTitle } from '../../components/document_title'; +import { HelpCenterContent } from '../../components/help_center_content'; +import { RoutedTabs } from '../../components/navigation/routed_tabs'; +import { ColumnarPage } from '../../components/page'; import { Header } from '../../components/header'; -import { ColumnarPage, PageContent } from '../../components/page'; -import { withMetricPageProviders } from './page_providers'; -import { useMetadata } from '../../containers/metadata/use_metadata'; +import { MetricsExplorerOptionsContainer } from './metrics_explorer/hooks/use_metrics_explorer_options'; +import { WithMetricsExplorerOptionsUrlState } from '../../containers/metrics_explorer/with_metrics_explorer_options_url_state'; +import { WithSource } from '../../containers/with_source'; import { Source } from '../../containers/source'; -import { InfraLoadingPanel } from '../../components/loading'; -import { findInventoryModel } from '../../../common/inventory_models'; -import { NavItem } from './lib/side_nav_context'; -import { NodeDetailsPage } from './components/node_details_page'; +import { MetricsExplorerPage } from './metrics_explorer'; +import { SnapshotPage } from './inventory_view'; +import { MetricsSettingsPage } from './settings'; +import { AppNavigation } from '../../components/navigation/app_navigation'; +import { SourceLoadingPage } from '../../components/source_loading_page'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { useMetricsTimeContext } from './hooks/use_metrics_time'; -import { useLinkProps } from '../../hooks/use_link_props'; - -const DetailPageContent = euiStyled(PageContent)` - overflow: auto; - background-color: ${props => props.theme.eui.euiColorLightestShade}; -`; - -interface Props { - theme: EuiTheme; - match: { - params: { - type: string; - node: string; - }; - }; -} - -export const MetricDetail = withMetricPageProviders( - withTheme(({ match }: Props) => { - const uiCapabilities = useKibana().services.application?.capabilities; - const nodeId = match.params.node; - const nodeType = match.params.type as InventoryItemType; - const inventoryModel = findInventoryModel(nodeType); - const { sourceId } = useContext(Source.Context); - const { - timeRange, - parsedTimeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - triggerRefresh, - } = useMetricsTimeContext(); - const { - name, - filteredRequiredMetrics, - loading: metadataLoading, - cloudId, - metadata, - } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); +import { WaffleOptionsProvider } from './inventory_view/hooks/use_waffle_options'; +import { WaffleTimeProvider } from './inventory_view/hooks/use_waffle_time'; +import { WaffleFiltersProvider } from './inventory_view/hooks/use_waffle_filters'; - const [sideNav, setSideNav] = useState<NavItem[]>([]); +import { InventoryAlertDropdown } from '../../components/alerting/inventory/alert_dropdown'; +import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components/alert_dropdown'; - const addNavItem = React.useCallback( - (item: NavItem) => { - if (!sideNav.some(n => n.id === item.id)) { - setSideNav([item, ...sideNav]); - } - }, - [sideNav] - ); +export const InfrastructurePage = ({ match }: RouteComponentProps) => { + const uiCapabilities = useKibana().services.application?.capabilities; - const metricsLinkProps = useLinkProps({ - app: 'metrics', - pathname: '/', - }); + return ( + <Source.Provider sourceId="default"> + <WaffleOptionsProvider> + <WaffleTimeProvider> + <WaffleFiltersProvider> + <ColumnarPage> + <DocumentTitle + title={i18n.translate('xpack.infra.homePage.documentTitle', { + defaultMessage: 'Metrics', + })} + /> - const breadcrumbs = [ - { - ...metricsLinkProps, - text: i18n.translate('xpack.infra.header.infrastructureTitle', { - defaultMessage: 'Metrics', - }), - }, - { text: name }, - ]; + <HelpCenterContent + feedbackLink="https://discuss.elastic.co/c/metrics" + appName={i18n.translate('xpack.infra.header.infrastructureHelpAppName', { + defaultMessage: 'Metrics', + })} + /> - if (metadataLoading && !filteredRequiredMetrics.length) { - return ( - <InfraLoadingPanel - height="100vh" - width="100%" - text={i18n.translate('xpack.infra.metrics.loadingNodeDataText', { - defaultMessage: 'Loading data', - })} - /> - ); - } + <Header + breadcrumbs={[ + { + text: i18n.translate('xpack.infra.header.infrastructureTitle', { + defaultMessage: 'Metrics', + }), + }, + ]} + readOnlyBadge={!uiCapabilities?.infrastructure?.save} + /> + <AppNavigation + aria-label={i18n.translate('xpack.infra.header.infrastructureNavigationTitle', { + defaultMessage: 'Metrics', + })} + > + <EuiFlexGroup gutterSize={'none'} alignItems={'center'}> + <EuiFlexItem> + <RoutedTabs + tabs={[ + { + app: 'metrics', + title: i18n.translate('xpack.infra.homePage.inventoryTabTitle', { + defaultMessage: 'Inventory', + }), + pathname: '/inventory', + }, + { + app: 'metrics', + title: i18n.translate('xpack.infra.homePage.metricsExplorerTabTitle', { + defaultMessage: 'Metrics Explorer', + }), + pathname: '/explorer', + }, + { + app: 'metrics', + title: i18n.translate('xpack.infra.homePage.settingsTabTitle', { + defaultMessage: 'Settings', + }), + pathname: '/settings', + }, + ]} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <Route path={'/explorer'} component={MetricsAlertDropdown} /> + <Route path={'/inventory'} component={InventoryAlertDropdown} /> + </EuiFlexItem> + </EuiFlexGroup> + </AppNavigation> - return ( - <ColumnarPage> - <Header breadcrumbs={breadcrumbs} readOnlyBadge={!uiCapabilities?.infrastructure?.save} /> - <DocumentTitle - title={i18n.translate('xpack.infra.metricDetailPage.documentTitle', { - defaultMessage: 'Infrastructure | Metrics | {name}', - values: { - name, - }, - })} - /> - <DetailPageContent data-test-subj="infraMetricsPage"> - {metadata ? ( - <NodeDetailsPage - name={name} - requiredMetrics={filteredRequiredMetrics} - sourceId={sourceId} - timeRange={timeRange} - parsedTimeRange={parsedTimeRange} - nodeType={nodeType} - nodeId={nodeId} - cloudId={cloudId} - metadataLoading={metadataLoading} - isAutoReloading={isAutoReloading} - refreshInterval={refreshInterval} - sideNav={sideNav} - metadata={metadata} - addNavItem={addNavItem} - setRefreshInterval={setRefreshInterval} - setAutoReload={setAutoReload} - triggerRefresh={triggerRefresh} - setTimeRange={setTimeRange} - /> - ) : null} - </DetailPageContent> - </ColumnarPage> - ); - }) -); + <Switch> + <Route path={'/inventory'} component={SnapshotPage} /> + <Route + path={'/explorer'} + render={props => ( + <WithSource> + {({ configuration, createDerivedIndexPattern }) => ( + <MetricsExplorerOptionsContainer.Provider> + <WithMetricsExplorerOptionsUrlState /> + {configuration ? ( + <MetricsExplorerPage + derivedIndexPattern={createDerivedIndexPattern('metrics')} + source={configuration} + {...props} + /> + ) : ( + <SourceLoadingPage /> + )} + </MetricsExplorerOptionsContainer.Provider> + )} + </WithSource> + )} + /> + <Route path={'/settings'} component={MetricsSettingsPage} /> + </Switch> + </ColumnarPage> + </WaffleFiltersProvider> + </WaffleTimeProvider> + </WaffleOptionsProvider> + </Source.Provider> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx new file mode 100644 index 0000000000000..f0bc404dc3797 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx @@ -0,0 +1,53 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import React, { ReactNode } from 'react'; +import { withTheme, EuiTheme } from '../../../../../../observability/public'; + +interface Props { + label: string; + onClick: () => void; + theme: EuiTheme; + children: ReactNode; +} + +export const DropdownButton = withTheme(({ onClick, label, theme, children }: Props) => { + return ( + <EuiFlexGroup + alignItems="center" + gutterSize="none" + style={{ + border: theme.eui.euiFormInputGroupBorder, + boxShadow: `0px 3px 2px ${theme.eui.euiTableActionsBorderColor}, 0px 1px 1px ${theme.eui.euiTableActionsBorderColor}`, + }} + > + <EuiFlexItem + grow={false} + style={{ + padding: 12, + background: theme.eui.euiFormInputGroupLabelBackground, + fontSize: '0.75em', + fontWeight: 600, + color: theme.eui.euiTitleColor, + }} + > + {label} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + color="text" + iconType="arrowDown" + onClick={onClick} + iconSide="right" + size="xs" + > + {children} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + ); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx new file mode 100644 index 0000000000000..708d5f7d75907 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import { WaffleTimeControls } from './waffle/waffle_time_controls'; +import { SearchBar } from './search_bar'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; + +export const FilterBar = () => ( + <ToolbarPanel> + <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" gutterSize="m"> + <EuiFlexItem> + <SearchBar /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <WaffleTimeControls /> + </EuiFlexItem> + </EuiFlexGroup> + </ToolbarPanel> +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx new file mode 100644 index 0000000000000..a71e43874b480 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -0,0 +1,151 @@ +/* + * 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, { useCallback } from 'react'; +import { useInterval } from 'react-use'; + +import { euiPaletteColorBlind, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; +import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview'; +import { PageContent } from '../../../../components/page'; +import { useSnapshot } from '../hooks/use_snaphot'; +import { useWaffleTimeContext } from '../hooks/use_waffle_time'; +import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; +import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; +import { useSourceContext } from '../../../../containers/source'; +import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../../../lib/lib'; +import { euiStyled } from '../../../../../../observability/public'; +import { Toolbar } from './toolbars/toolbar'; +import { ViewSwitcher } from './waffle/view_switcher'; +import { SavedViews } from './saved_views'; +import { IntervalLabel } from './waffle/interval_label'; +import { Legend } from './waffle/legend'; +import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; + +const euiVisColorPalette = euiPaletteColorBlind(); + +export const Layout = () => { + const { sourceId, source } = useSourceContext(); + const { + metric, + groupBy, + nodeType, + accountId, + region, + changeView, + view, + autoBounds, + boundsOverride, + } = useWaffleOptionsContext(); + const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); + const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); + const { loading, nodes, reload, interval } = useSnapshot( + filterQueryAsJson, + metric, + groupBy, + nodeType, + sourceId, + currentTime, + accountId, + region + ); + + const options = { + formatter: InfraFormatterType.percent, + formatTemplate: '{{value}}', + legend: { + type: 'gradient', + rules: [ + { value: 0, color: '#D3DAE6' }, + { value: 1, color: euiVisColorPalette[1] }, + ], + } as InfraWaffleMapGradientLegend, + metric, + fields: source?.configuration?.fields, + groupBy, + }; + + useInterval( + () => { + if (!loading) { + jumpToTime(Date.now()); + } + }, + isAutoReloading ? 5000 : null + ); + + const intervalAsString = convertIntervalToString(interval); + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; + const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); + + return ( + <> + <PageContent> + <MainContainer> + <TopActionContainer> + <EuiFlexGroup justifyContent="spaceBetween" gutterSize="m"> + <Toolbar nodeType={nodeType} /> + <EuiFlexItem grow={false}> + <ViewSwitcher view={view} onChange={changeView} /> + </EuiFlexItem> + </EuiFlexGroup> + </TopActionContainer> + <NodesOverview + nodes={nodes} + options={options} + nodeType={nodeType} + loading={loading} + reload={reload} + onDrilldown={applyFilterQuery} + currentTime={currentTime} + view={view} + autoBounds={autoBounds} + boundsOverride={boundsOverride} + formatter={formatter} + /> + <BottomActionContainer> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <SavedViews /> + </EuiFlexItem> + <EuiFlexItem grow={false} style={{ position: 'relative', minWidth: 400 }}> + <Legend + formatter={formatter} + bounds={bounds} + dataBounds={dataBounds} + legend={options.legend} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <IntervalLabel intervalAsString={intervalAsString} /> + </EuiFlexItem> + </EuiFlexGroup> + </BottomActionContainer> + </MainContainer> + </PageContent> + </> + ); +}; + +const MainContainer = euiStyled.div` + position: relative; + flex: 1 1 auto; +`; + +const TopActionContainer = euiStyled.div` + padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`}; +`; + +const BottomActionContainer = euiStyled.div` + background-color: ${props => props.theme.eui.euiPageBackgroundColor}; + padding: ${props => props.theme.eui.paddingSizes.m} ${props => + props.theme.eui.paddingSizes.m} ${props => props.theme.eui.paddingSizes.s}; + position: absolute; + left: 0; + bottom: 4px; + right: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx new file mode 100644 index 0000000000000..966a327f40bc1 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -0,0 +1,149 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { max, min } from 'lodash'; +import React, { useCallback } from 'react'; + +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { euiStyled } from '../../../../../../observability/public'; +import { InfraWaffleMapBounds, InfraWaffleMapOptions, InfraFormatter } from '../../../../lib/lib'; +import { NoData } from '../../../../components/empty_states'; +import { InfraLoadingPanel } from '../../../../components/loading'; +import { Map } from './waffle/map'; +import { TableView } from './table_view'; +import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; + +export interface KueryFilterQuery { + kind: 'kuery'; + expression: string; +} + +interface Props { + options: InfraWaffleMapOptions; + nodeType: InventoryItemType; + nodes: SnapshotNode[]; + loading: boolean; + reload: () => void; + onDrilldown: (filter: KueryFilterQuery) => void; + currentTime: number; + view: string; + boundsOverride: InfraWaffleMapBounds; + autoBounds: boolean; + formatter: InfraFormatter; +} + +export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { + const maxValues = nodes.map(node => node.metric.max); + const minValues = nodes.map(node => node.metric.value); + // if there is only one value then we need to set the bottom range to zero for min + // otherwise the legend will look silly since both values are the same for top and + // bottom. + if (minValues.length === 1) { + minValues.unshift(0); + } + return { min: min(minValues) || 0, max: max(maxValues) || 0 }; +}; + +export const NodesOverview = ({ + autoBounds, + boundsOverride, + loading, + nodes, + nodeType, + reload, + view, + currentTime, + options, + formatter, + onDrilldown, +}: Props) => { + const handleDrilldown = useCallback( + (filter: string) => { + onDrilldown({ + kind: 'kuery', + expression: filter, + }); + return; + }, + [onDrilldown] + ); + + const noData = !loading && nodes && nodes.length === 0; + if (loading) { + return ( + <InfraLoadingPanel + height="100%" + width="100%" + text={i18n.translate('xpack.infra.waffle.loadingDataText', { + defaultMessage: 'Loading data', + })} + /> + ); + } else if (noData) { + return ( + <NoData + titleText={i18n.translate('xpack.infra.waffle.noDataTitle', { + defaultMessage: 'There is no data to display.', + })} + bodyText={i18n.translate('xpack.infra.waffle.noDataDescription', { + defaultMessage: 'Try adjusting your time or filter.', + })} + refetchText={i18n.translate('xpack.infra.waffle.checkNewDataButtonLabel', { + defaultMessage: 'Check for new data', + })} + onRefetch={() => { + reload(); + }} + testString="noMetricsDataPrompt" + /> + ); + } + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; + + if (view === 'table') { + return ( + <TableContainer> + <TableView + nodeType={nodeType} + nodes={nodes} + options={options} + formatter={formatter} + currentTime={currentTime} + onFilter={handleDrilldown} + /> + </TableContainer> + ); + } + return ( + <MapContainer> + <Map + nodeType={nodeType} + nodes={nodes} + options={options} + formatter={formatter} + currentTime={currentTime} + onFilter={handleDrilldown} + bounds={bounds} + dataBounds={dataBounds} + /> + </MapContainer> + ); +}; + +const TableContainer = euiStyled.div` + padding: ${props => props.theme.eui.paddingSizes.l}; +`; + +const MapContainer = euiStyled.div` + position: absolute; + display: flex; + top: 70px; + right: 0; + bottom: 0; + left: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx new file mode 100644 index 0000000000000..356f0598e00d2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx @@ -0,0 +1,21 @@ +/* + * 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 { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { inventoryViewSavedObjectType } from '../../../../../common/saved_objects/inventory_view'; +import { useWaffleViewState } from '../hooks/use_waffle_view_state'; + +export const SavedViews = () => { + const { viewState, defaultViewState, onViewChange } = useWaffleViewState(); + return ( + <SavedViewsToolbarControls + defaultViewState={defaultViewState} + viewState={viewState} + onViewChange={onViewChange} + viewType={inventoryViewSavedObjectType} + /> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx similarity index 86% rename from x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx index f4fde46d434f8..1f84ef3685f34 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/compontents/search_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/search_bar.tsx @@ -5,9 +5,9 @@ */ import React, { useContext } from 'react'; import { i18n } from '@kbn/i18n'; -import { Source } from '../../../containers/source'; -import { AutocompleteField } from '../../../components/autocomplete_field'; -import { WithKueryAutocompletion } from '../../../containers/with_kuery_autocompletion'; +import { Source } from '../../../../containers/source'; +import { AutocompleteField } from '../../../../components/autocomplete_field'; +import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; export const SearchBar = () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx new file mode 100644 index 0000000000000..0557343e735f9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/table_view.tsx @@ -0,0 +1,170 @@ +/* + * 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 { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip, EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { last } from 'lodash'; +import React, { useState, useCallback, useEffect } from 'react'; +import { createWaffleMapNode } from '../lib/nodes_to_wafflemap'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; +import { fieldToName } from '../lib/field_to_display_name'; +import { NodeContextMenu } from './waffle/node_context_menu'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { SnapshotNode, SnapshotNodePath } from '../../../../../common/http_api/snapshot_api'; +import { CONTAINER_CLASSNAME } from '../../../../apps/start_app'; + +interface Props { + nodes: SnapshotNode[]; + nodeType: InventoryItemType; + options: InfraWaffleMapOptions; + currentTime: number; + formatter: (subject: string | number) => string; + onFilter: (filter: string) => void; +} + +const getGroupPaths = (path: SnapshotNodePath[]) => { + switch (path.length) { + case 3: + return path.slice(0, 2); + case 2: + return path.slice(0, 1); + default: + return []; + } +}; + +export const TableView = (props: Props) => { + const { nodes, options, formatter, currentTime, nodeType } = props; + const [openPopovers, setOpenPopovers] = useState<string[]>([]); + const openPopoverFor = useCallback( + (id: string) => () => { + setOpenPopovers([...openPopovers, id]); + }, + [openPopovers] + ); + + const closePopoverFor = useCallback( + (id: string) => () => { + if (openPopovers.includes(id)) { + setOpenPopovers(openPopovers.filter(subject => subject !== id)); + } + }, + [openPopovers] + ); + + useEffect(() => { + const el = document.getElementsByClassName(CONTAINER_CLASSNAME)[0]; + if (el instanceof HTMLElement) { + if (openPopovers.length > 0) { + el.style.overflowY = 'hidden'; + } else { + el.style.overflowY = 'auto'; + } + } + }, [openPopovers]); + + const columns: Array<EuiBasicTableColumn<typeof items[number]>> = [ + { + field: 'name', + name: i18n.translate('xpack.infra.tableView.columnName.name', { defaultMessage: 'Name' }), + sortable: true, + truncateText: true, + textOnly: true, + render: (value: string, item: { node: InfraWaffleMapNode }) => { + const tooltipText = item.node.id === value ? `${value}` : `${value} (${item.node.id})`; + // For the table we need to create a UniqueID that takes into to account the groupings + // as well as the node name. There is the possibility that a node can be present in two + // different groups and be on the screen at the same time. + const uniqueID = [...item.node.path.map(p => p.value), item.node.name].join(':'); + return ( + <NodeContextMenu + node={item.node} + nodeType={nodeType} + closePopover={closePopoverFor(uniqueID)} + currentTime={currentTime} + isPopoverOpen={openPopovers.includes(uniqueID)} + options={options} + popoverPosition="rightCenter" + > + <EuiToolTip content={tooltipText}> + <EuiButtonEmpty onClick={openPopoverFor(uniqueID)}>{value}</EuiButtonEmpty> + </EuiToolTip> + </NodeContextMenu> + ); + }, + }, + ...options.groupBy.map((grouping, index) => ({ + field: `group_${index}`, + name: fieldToName((grouping && grouping.field) || ''), + sortable: true, + truncateText: true, + textOnly: true, + render: (value: string) => { + const handleClick = () => props.onFilter(`${grouping.field}:"${value}"`); + return ( + <EuiToolTip content="Set Filter"> + <EuiButtonEmpty onClick={handleClick}>{value}</EuiButtonEmpty> + </EuiToolTip> + ); + }, + })), + { + field: 'value', + name: i18n.translate('xpack.infra.tableView.columnName.last1m', { + defaultMessage: 'Last 1m', + }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => <span>{formatter(value)}</span>, + }, + { + field: 'avg', + name: i18n.translate('xpack.infra.tableView.columnName.avg', { defaultMessage: 'Avg' }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => <span>{formatter(value)}</span>, + }, + { + field: 'max', + name: i18n.translate('xpack.infra.tableView.columnName.max', { defaultMessage: 'Max' }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => <span>{formatter(value)}</span>, + }, + ]; + + const items = nodes.map(node => { + const name = last(node.path); + return { + name: (name && name.label) || 'unknown', + ...getGroupPaths(node.path).reduce( + (acc, path, index) => ({ + ...acc, + [`group_${index}`]: path.label, + }), + {} + ), + value: node.metric.value, + avg: node.metric.avg, + max: node.metric.max, + node: createWaffleMapNode(node), + }; + }); + const initialSorting = { + sort: { + field: 'value', + direction: 'desc', + }, + } as const; + + return ( + <EuiInMemoryTable pagination={true} sorting={initialSorting} items={items} columns={columns} /> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx new file mode 100644 index 0000000000000..e8485fb812586 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { useSourceContext } from '../../../../../containers/source'; +import { + SnapshotMetricInput, + SnapshotGroupBy, + SnapshotCustomMetricInput, +} from '../../../../../../common/http_api/snapshot_api'; +import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api'; +import { findToolbar } from '../../../../../../common/inventory_models/toolbars'; +import { ToolbarWrapper } from './toolbar_wrapper'; + +import { InfraGroupByOptions } from '../../../../../lib/lib'; +import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { WaffleOptionsState } from '../../hooks/use_waffle_options'; +import { useInventoryMeta } from '../../hooks/use_inventory_meta'; + +export interface ToolbarProps + extends Omit<WaffleOptionsState, 'view' | 'boundsOverride' | 'autoBounds'> { + createDerivedIndexPattern: (type: 'logs' | 'metrics' | 'both') => IIndexPattern; + changeMetric: (payload: SnapshotMetricInput) => void; + changeGroupBy: (payload: SnapshotGroupBy) => void; + changeCustomOptions: (payload: InfraGroupByOptions[]) => void; + changeAccount: (id: string) => void; + changeRegion: (name: string) => void; + accounts: InventoryCloudAccount[]; + regions: string[]; + changeCustomMetrics: (payload: SnapshotCustomMetricInput[]) => void; +} + +const wrapToolbarItems = ( + ToolbarItems: FunctionComponent<ToolbarProps>, + accounts: InventoryCloudAccount[], + regions: string[] +) => { + return ( + <ToolbarWrapper> + {props => ( + <> + <ToolbarItems {...props} accounts={accounts} regions={regions} /> + <EuiFlexItem grow={true} /> + </> + )} + </ToolbarWrapper> + ); +}; + +interface Props { + nodeType: InventoryItemType; +} + +export const Toolbar = ({ nodeType }: Props) => { + const { sourceId } = useSourceContext(); + const { accounts, regions } = useInventoryMeta(sourceId, nodeType); + const ToolbarItems = findToolbar(nodeType); + return wrapToolbarItems(ToolbarItems, accounts, regions); +}; diff --git a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx similarity index 88% rename from x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index fefda94372cfb..ea53122984161 100644 --- a/x-pack/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -5,14 +5,14 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { Toolbar } from '../../eui/toolbar'; +import { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; +import { fieldToName } from '../../lib/field_to_display_name'; +import { useSourceContext } from '../../../../../containers/source'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher'; import { ToolbarProps } from './toolbar'; -import { fieldToName } from '../../waffle/lib/field_to_display_name'; -import { useSourceContext } from '../../../containers/source'; -import { useWaffleOptionsContext } from '../../../pages/inventory_view/hooks/use_waffle_options'; interface Props { children: (props: Omit<ToolbarProps, 'accounts' | 'regions'>) => React.ReactElement; @@ -36,26 +36,27 @@ export const ToolbarWrapper = (props: Props) => { } = useWaffleOptionsContext(); const { createDerivedIndexPattern } = useSourceContext(); return ( - <Toolbar> - <EuiFlexGroup alignItems="center" gutterSize="m"> - {props.children({ - createDerivedIndexPattern, - changeMetric, - changeGroupBy, - changeAccount, - changeRegion, - changeCustomOptions, - customOptions, - groupBy, - metric, - nodeType, - region, - accountId, - customMetrics, - changeCustomMetrics, - })} - </EuiFlexGroup> - </Toolbar> + <> + <EuiFlexItem grow={false}> + <WaffleInventorySwitcher /> + </EuiFlexItem> + {props.children({ + createDerivedIndexPattern, + changeMetric, + changeGroupBy, + changeAccount, + changeRegion, + changeCustomOptions, + customOptions, + groupBy, + metric, + nodeType, + region, + accountId, + customMetrics, + changeCustomMetrics, + })} + </> ); }; diff --git a/x-pack/plugins/infra/public/components/waffle/conditional_tooltip.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/conditional_tooltip.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.tsx diff --git a/x-pack/plugins/infra/public/components/waffle/custom_field_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx similarity index 97% rename from x-pack/plugins/infra/public/components/waffle/custom_field_panel.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx index d2dc535f6d6b3..090d53f1ff737 100644 --- a/x-pack/plugins/infra/public/components/waffle/custom_field_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/custom_field_panel.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { InfraGroupByOptions } from '../../lib/lib'; +import { InfraGroupByOptions } from '../../../../../lib/lib'; interface Props { onSubmit: (field: string) => void; diff --git a/x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx index 6b0c4bb41dc98..87f7e4cbff11e 100644 --- a/x-pack/plugins/infra/public/components/waffle/gradient_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/gradient_legend.tsx @@ -6,13 +6,13 @@ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapGradientLegend, InfraWaffleMapGradientRule, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; interface Props { legend: InfraWaffleMapGradientLegend; diff --git a/x-pack/plugins/infra/public/components/waffle/group_name.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/group_name.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx index 01bd3600a1624..308460203b132 100644 --- a/x-pack/plugins/infra/public/components/waffle/group_name.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_name.tsx @@ -6,8 +6,8 @@ import { EuiLink, EuiToolTip } from '@elastic/eui'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; -import { InfraWaffleMapGroup, InfraWaffleMapOptions } from '../../lib/lib'; +import { euiStyled } from '../../../../../../../observability/public'; +import { InfraWaffleMapGroup, InfraWaffleMapOptions } from '../../../../../lib/lib'; interface Props { onDrilldown: (filter: string) => void; diff --git a/x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx similarity index 90% rename from x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx index 9634293587d49..6b3f22007f580 100644 --- a/x-pack/plugins/infra/public/components/waffle/group_of_groups.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_groups.tsx @@ -6,15 +6,15 @@ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfGroups, InfraWaffleMapOptions, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; import { GroupName } from './group_name'; import { GroupOfNodes } from './group_of_nodes'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; interface Props { onDrilldown: (filter: string) => void; diff --git a/x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx similarity index 90% rename from x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx index 6b82671617df7..fc438ed4ca0a2 100644 --- a/x-pack/plugins/infra/public/components/waffle/group_of_nodes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/group_of_nodes.tsx @@ -6,15 +6,15 @@ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfNodes, InfraWaffleMapOptions, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; import { GroupName } from './group_name'; import { Node } from './node'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; interface Props { onDrilldown: (filter: string) => void; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx new file mode 100644 index 0000000000000..dbbfb0f49c0e9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + intervalAsString?: string; +} + +export const IntervalLabel = ({ intervalAsString }: Props) => { + if (!intervalAsString) { + return null; + } + + return ( + <EuiText color="subdued" size="s"> + <p> + <FormattedMessage + id="xpack.infra.homePage.toolbar.showingLastOneMinuteDataText" + defaultMessage="Last {duration} of data for the selected time" + values={{ duration: intervalAsString }} + /> + </p> + </EuiText> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx new file mode 100644 index 0000000000000..ac699f96a75a6 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -0,0 +1,59 @@ +/* + * 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 { euiStyled } from '../../../../../../../observability/public'; +import { InfraFormatter, InfraWaffleMapBounds, InfraWaffleMapLegend } from '../../../../../lib/lib'; +import { GradientLegend } from './gradient_legend'; +import { LegendControls } from './legend_controls'; +import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from '../../lib/type_guards'; +import { StepLegend } from './steps_legend'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +interface Props { + legend: InfraWaffleMapLegend; + bounds: InfraWaffleMapBounds; + dataBounds: InfraWaffleMapBounds; + formatter: InfraFormatter; +} + +interface LegendControlOptions { + auto: boolean; + bounds: InfraWaffleMapBounds; +} + +export const Legend: React.FC<Props> = ({ dataBounds, legend, bounds, formatter }) => { + const { + changeBoundsOverride, + changeAutoBounds, + autoBounds, + boundsOverride, + } = useWaffleOptionsContext(); + return ( + <LegendContainer> + <LegendControls + dataBounds={dataBounds} + bounds={bounds} + autoBounds={autoBounds} + boundsOverride={boundsOverride} + onChange={(options: LegendControlOptions) => { + changeBoundsOverride(options.bounds); + changeAutoBounds(options.auto); + }} + /> + {isInfraWaffleMapGradientLegend(legend) && ( + <GradientLegend formatter={formatter} legend={legend} bounds={bounds} /> + )} + {isInfraWaffleMapStepLegend(legend) && <StepLegend formatter={formatter} legend={legend} />} + </LegendContainer> + ); +}; + +const LegendContainer = euiStyled.div` + position: absolute; + bottom: 0px; + left: 10px; + right: 10px; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/legend_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 26b5b1c0af727..30447e5244241 100644 --- a/x-pack/plugins/infra/public/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -23,8 +23,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { SyntheticEvent, useState } from 'react'; -import { euiStyled } from '../../../../observability/public'; -import { InfraWaffleMapBounds } from '../../lib/lib'; +import { euiStyled } from '../../../../../../../observability/public'; +import { InfraWaffleMapBounds } from '../../../../../lib/lib'; interface Props { onChange: (options: { auto: boolean; bounds: InfraWaffleMapBounds }) => void; @@ -40,7 +40,7 @@ export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBound const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop const buttonComponent = ( <EuiButtonIcon - iconType="gear" + iconType="controlsHorizontal" color="text" aria-label={i18n.translate('xpack.infra.legendControls.buttonLabel', { defaultMessage: 'configure legend', diff --git a/x-pack/plugins/infra/public/components/waffle/map.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx similarity index 79% rename from x-pack/plugins/infra/public/components/waffle/map.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx index 7cab2307dacfa..b1f816fb46d2e 100644 --- a/x-pack/plugins/infra/public/components/waffle/map.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/map.tsx @@ -5,20 +5,16 @@ */ import React from 'react'; -import { euiStyled } from '../../../../observability/public'; -import { nodesToWaffleMap } from '../../containers/waffle/nodes_to_wafflemap'; -import { - isWaffleMapGroupWithGroups, - isWaffleMapGroupWithNodes, -} from '../../containers/waffle/type_guards'; -import { InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; -import { AutoSizer } from '../auto_sizer'; +import { euiStyled } from '../../../../../../../observability/public'; +import { nodesToWaffleMap } from '../../lib/nodes_to_wafflemap'; +import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from '../../lib/type_guards'; +import { InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../../../../lib/lib'; +import { AutoSizer } from '../../../../../components/auto_sizer'; import { GroupOfGroups } from './group_of_groups'; import { GroupOfNodes } from './group_of_nodes'; -import { Legend } from './legend'; -import { applyWaffleMapLayout } from './lib/apply_wafflemap_layout'; -import { SnapshotNode } from '../../../common/http_api/snapshot_api'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { applyWaffleMapLayout } from '../../lib/apply_wafflemap_layout'; +import { SnapshotNode } from '../../../../../../common/http_api/snapshot_api'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; interface Props { nodes: SnapshotNode[]; @@ -81,12 +77,6 @@ export const Map: React.FC<Props> = ({ } })} </WaffleMapInnerContainer> - <Legend - formatter={formatter} - bounds={bounds} - dataBounds={dataBounds} - legend={options.legend} - /> </WaffleMapOuterContainer> ); }} diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/custom_metric_form.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx similarity index 97% rename from x-pack/plugins/infra/public/components/waffle/metric_control/custom_metric_form.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx index 26e42061ed10b..aec624c0aff1f 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/custom_metric_form.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx @@ -26,8 +26,11 @@ import { SnapshotCustomMetricInput, SNAPSHOT_CUSTOM_AGGREGATIONS, SnapshotCustomAggregationRT, -} from '../../../../common/http_api/snapshot_api'; -import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components'; +} from '../../../../../../../common/http_api/snapshot_api'; +import { + EuiTheme, + withTheme, +} from '../../../../../../../../../legacy/common/eui_styled_components'; interface SelectedOption { label: string; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/get_custom_metric_label.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts similarity index 91% rename from x-pack/plugins/infra/public/components/waffle/metric_control/get_custom_metric_label.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts index 4f88c1b29c1f2..495cc8197d2e7 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/get_custom_metric_label.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/get_custom_metric_label.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; +import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; export const getCustomMetricLabel = (metric: SnapshotCustomMetricInput) => { const METRIC_LABELS = { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx new file mode 100644 index 0000000000000..f91e9a4034bc2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx @@ -0,0 +1,195 @@ +/* + * 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 { EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useState, useCallback } from 'react'; +import { IFieldType } from 'src/plugins/data/public'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInput, + SnapshotCustomMetricInputRT, +} from '../../../../../../../common/http_api/snapshot_api'; +import { CustomMetricForm } from './custom_metric_form'; +import { getCustomMetricLabel } from './get_custom_metric_label'; +import { MetricsContextMenu } from './metrics_context_menu'; +import { ModeSwitcher } from './mode_switcher'; +import { MetricsEditMode } from './metrics_edit_mode'; +import { CustomMetricMode } from './types'; +import { SnapshotMetricType } from '../../../../../../../common/inventory_models/types'; +import { DropdownButton } from '../../dropdown_button'; + +interface Props { + options: Array<{ text: string; value: string }>; + metric: SnapshotMetricInput; + fields: IFieldType[]; + onChange: (metric: SnapshotMetricInput) => void; + onChangeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void; + customMetrics: SnapshotCustomMetricInput[]; +} + +export const WaffleMetricControls = ({ + fields, + onChange, + onChangeCustomMetrics, + metric, + options, + customMetrics, +}: Props) => { + const [isPopoverOpen, setPopoverState] = useState<boolean>(false); + const [mode, setMode] = useState<CustomMetricMode>('pick'); + const [editModeCustomMetrics, setEditModeCustomMetrics] = useState<SnapshotCustomMetricInput[]>( + [] + ); + const [editCustomMetric, setEditCustomMetric] = useState<SnapshotCustomMetricInput | undefined>(); + const handleClose = useCallback(() => { + setPopoverState(false); + }, [setPopoverState]); + + const handleToggle = useCallback(() => { + setPopoverState(!isPopoverOpen); + }, [isPopoverOpen]); + + const handleCustomMetric = useCallback( + (newMetric: SnapshotCustomMetricInput) => { + onChangeCustomMetrics([...customMetrics, newMetric]); + onChange(newMetric); + setMode('pick'); + }, + [customMetrics, onChange, onChangeCustomMetrics, setMode] + ); + + const setModeToEdit = useCallback(() => { + setMode('edit'); + setEditModeCustomMetrics(customMetrics); + }, [customMetrics]); + + const setModeToAdd = useCallback(() => { + setMode('addMetric'); + }, [setMode]); + + const setModeToPick = useCallback(() => { + setMode('pick'); + setEditModeCustomMetrics([]); + }, [setMode]); + + const handleDeleteCustomMetric = useCallback( + (m: SnapshotCustomMetricInput) => { + // If the metric we are deleting is the currently selected metric + // we need to change to the default. + if (SnapshotCustomMetricInputRT.is(metric) && m.id === metric.id) { + onChange({ type: options[0].value as SnapshotMetricType }); + } + // Filter out the deleted metric from the editbale. + const newMetrics = editModeCustomMetrics.filter(v => v.id !== m.id); + setEditModeCustomMetrics(newMetrics); + }, + [editModeCustomMetrics, metric, onChange, options] + ); + + const handleEditCustomMetric = useCallback( + (currentMetric: SnapshotCustomMetricInput) => { + const newMetrics = customMetrics.map(m => (m.id === currentMetric.id && currentMetric) || m); + onChangeCustomMetrics(newMetrics); + setModeToPick(); + setEditCustomMetric(void 0); + setEditModeCustomMetrics([]); + }, + [customMetrics, onChangeCustomMetrics, setModeToPick] + ); + + const handleSelectMetricToEdit = useCallback( + (currentMetric: SnapshotCustomMetricInput) => { + setEditCustomMetric(currentMetric); + setMode('editMetric'); + }, + [setMode, setEditCustomMetric] + ); + + const handleSaveEdit = useCallback(() => { + onChangeCustomMetrics(editModeCustomMetrics); + setMode('pick'); + }, [editModeCustomMetrics, onChangeCustomMetrics]); + + if (!options.length || !metric.type) { + throw Error( + i18n.translate('xpack.infra.waffle.unableToSelectMetricErrorTitle', { + defaultMessage: 'Unable to select options or value for metric.', + }) + ); + } + + const id = SnapshotCustomMetricInputRT.is(metric) && metric.id ? metric.id : metric.type; + const currentLabel = SnapshotCustomMetricInputRT.is(metric) + ? getCustomMetricLabel(metric) + : options.find(o => o.value === id)?.text; + + if (!currentLabel) { + return null; + } + + const button = ( + <DropdownButton onClick={handleToggle} label="Metric"> + {currentLabel} + </DropdownButton> + ); + + return ( + <> + <EuiPopover + isOpen={isPopoverOpen} + id="metricsPanel" + button={button} + anchorPosition="downLeft" + panelPaddingSize="none" + closePopover={handleClose} + > + {mode === 'pick' ? ( + <MetricsContextMenu + onChange={onChange} + onClose={handleClose} + metric={metric} + customMetrics={customMetrics} + options={options} + /> + ) : null} + {mode === 'addMetric' ? ( + <CustomMetricForm + fields={fields} + customMetrics={customMetrics} + onChange={handleCustomMetric} + onCancel={setModeToPick} + /> + ) : null} + {mode === 'editMetric' ? ( + <CustomMetricForm + metric={editCustomMetric} + fields={fields} + customMetrics={customMetrics} + onChange={handleEditCustomMetric} + onCancel={setModeToEdit} + /> + ) : null} + {mode === 'edit' ? ( + <MetricsEditMode + customMetrics={editModeCustomMetrics} + options={options} + onEdit={handleSelectMetricToEdit} + onDelete={handleDeleteCustomMetric} + /> + ) : null} + <ModeSwitcher + onEditCancel={setModeToPick} + onEdit={setModeToEdit} + onAdd={setModeToAdd} + mode={mode} + onSave={handleSaveEdit} + customMetrics={customMetrics} + /> + </EuiPopover> + </> + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx similarity index 94% rename from x-pack/plugins/infra/public/components/waffle/metric_control/metrics_context_menu.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx index 1aacf54244c37..1cdef493aee4f 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_context_menu.tsx @@ -9,11 +9,11 @@ import { SnapshotMetricInput, SnapshotCustomMetricInput, SnapshotCustomMetricInputRT, -} from '../../../../common/http_api/snapshot_api'; +} from '../../../../../../../common/http_api/snapshot_api'; import { SnapshotMetricTypeRT, SnapshotMetricType, -} from '../../../../common/inventory_models/types'; +} from '../../../../../../../common/inventory_models/types'; import { getCustomMetricLabel } from './get_custom_metric_label'; interface Props { diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_edit_mode.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/waffle/metric_control/metrics_edit_mode.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx index ba1f46815db20..4d1bc906de0b9 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/metrics_edit_mode.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx @@ -6,9 +6,12 @@ import React from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; +import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; import { getCustomMetricLabel } from './get_custom_metric_label'; -import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components'; +import { + EuiTheme, + withTheme, +} from '../../../../../../../../../legacy/common/eui_styled_components'; interface Props { theme: EuiTheme; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/mode_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx similarity index 95% rename from x-pack/plugins/infra/public/components/waffle/metric_control/mode_switcher.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx index 43bb904594c68..acb740f1750c8 100644 --- a/x-pack/plugins/infra/public/components/waffle/metric_control/mode_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx @@ -8,8 +8,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CustomMetricMode } from './types'; -import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; -import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components'; +import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; +import { + EuiTheme, + withTheme, +} from '../../../../../../../../../legacy/common/eui_styled_components'; interface Props { theme: EuiTheme; diff --git a/x-pack/plugins/infra/public/components/waffle/metric_control/types.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/types.ts similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/metric_control/types.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/types.ts diff --git a/x-pack/plugins/infra/public/components/waffle/node.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx similarity index 93% rename from x-pack/plugins/infra/public/components/waffle/node.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx index 4eb5ccb8c1b4d..fd5b5b01f329f 100644 --- a/x-pack/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node.tsx @@ -10,11 +10,15 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ConditionalToolTip } from './conditional_tooltip'; -import { euiStyled } from '../../../../observability/public'; -import { InfraWaffleMapBounds, InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; -import { colorFromValue } from './lib/color_from_value'; +import { euiStyled } from '../../../../../../../observability/public'; +import { + InfraWaffleMapBounds, + InfraWaffleMapNode, + InfraWaffleMapOptions, +} from '../../../../../lib/lib'; +import { colorFromValue } from '../../lib/color_from_value'; import { NodeContextMenu } from './node_context_menu'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; const initialState = { isPopoverOpen: false, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx new file mode 100644 index 0000000000000..d576f08108649 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/node_context_menu.tsx @@ -0,0 +1,216 @@ +/* + * 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 { EuiPopoverProps, EuiCode } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import React, { useMemo, useState } from 'react'; +import { AlertFlyout } from '../../../../../components/alerting/inventory/alert_flyout'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib'; +import { getNodeDetailUrl, getNodeLogsUrl } from '../../../../link_to'; +import { createUptimeLink } from '../../lib/create_uptime_link'; +import { findInventoryModel, findInventoryFields } from '../../../../../../common/inventory_models'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { + Section, + SectionLinkProps, + ActionMenu, + SectionTitle, + SectionSubtitle, + SectionLinks, + SectionLink, + withTheme, + EuiTheme, +} from '../../../../../../../observability/public'; +import { useLinkProps } from '../../../../../hooks/use_link_props'; + +interface Props { + options: InfraWaffleMapOptions; + currentTime: number; + node: InfraWaffleMapNode; + nodeType: InventoryItemType; + isPopoverOpen: boolean; + closePopover: () => void; + popoverPosition: EuiPopoverProps['anchorPosition']; +} + +export const NodeContextMenu: React.FC<Props & { theme?: EuiTheme }> = withTheme( + ({ + options, + currentTime, + children, + node, + isPopoverOpen, + closePopover, + nodeType, + popoverPosition, + theme, + }) => { + const [flyoutVisible, setFlyoutVisible] = useState(false); + const inventoryModel = findInventoryModel(nodeType); + const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; + const uiCapabilities = useKibana().services.application?.capabilities; + // Due to the changing nature of the fields between APM and this UI, + // We need to have some exceptions until 7.0 & ECS is finalized. Reference + // #26620 for the details for these fields. + // TODO: This is tech debt, remove it after 7.0 & ECS migration. + const apmField = nodeType === 'host' ? 'host.hostname' : inventoryModel.fields.id; + + const showDetail = inventoryModel.crosslinkSupport.details; + const showLogsLink = + inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities?.logs?.show; + const showAPMTraceLink = + inventoryModel.crosslinkSupport.apm && uiCapabilities?.apm && uiCapabilities?.apm.show; + const showUptimeLink = + inventoryModel.crosslinkSupport.uptime && + (['pod', 'container'].includes(nodeType) || node.ip); + + const inventoryId = useMemo(() => { + if (nodeType === 'host') { + if (node.ip) { + return { label: <EuiCode>host.ip</EuiCode>, value: node.ip }; + } + } else { + if (options.fields) { + const { id } = findInventoryFields(nodeType, options.fields); + return { + label: <EuiCode>{id}</EuiCode>, + value: node.id, + }; + } + } + return { label: '', value: '' }; + }, [nodeType, node.ip, node.id, options.fields]); + + const nodeLogsMenuItemLinkProps = useLinkProps({ + app: 'logs', + ...getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: currentTime, + }), + }); + const nodeDetailMenuItemLinkProps = useLinkProps({ + ...getNodeDetailUrl({ + nodeType, + nodeId: node.id, + from: nodeDetailFrom, + to: currentTime, + }), + }); + const apmTracesMenuItemLinkProps = useLinkProps({ + app: 'apm', + hash: 'traces', + search: { + kuery: `${apmField}:"${node.id}"`, + }, + }); + const uptimeMenuItemLinkProps = useLinkProps(createUptimeLink(options, nodeType, node)); + + const nodeLogsMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { + defaultMessage: '{inventoryName} logs', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...nodeLogsMenuItemLinkProps, + 'data-test-subj': 'viewLogsContextMenuItem', + isDisabled: !showLogsLink, + }; + + const nodeDetailMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { + defaultMessage: '{inventoryName} metrics', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...nodeDetailMenuItemLinkProps, + isDisabled: !showDetail, + }; + + const apmTracesMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { + defaultMessage: '{inventoryName} APM traces', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...apmTracesMenuItemLinkProps, + 'data-test-subj': 'viewApmTracesContextMenuItem', + isDisabled: !showAPMTraceLink, + }; + + const uptimeMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { + defaultMessage: '{inventoryName} in Uptime', + values: { inventoryName: inventoryModel.singularDisplayName }, + }), + ...uptimeMenuItemLinkProps, + isDisabled: !showUptimeLink, + }; + + const createAlertMenuItem: SectionLinkProps = { + label: i18n.translate('xpack.infra.nodeContextMenu.createAlertLink', { + defaultMessage: 'Create alert', + }), + style: { color: theme?.eui.euiLinkColor || '#006BB4', fontWeight: 500, padding: 0 }, + onClick: () => { + setFlyoutVisible(true); + }, + }; + + return ( + <> + <ActionMenu + closePopover={closePopover} + id={`${node.pathId}-popover`} + isOpen={isPopoverOpen} + button={children!} + anchorPosition={popoverPosition} + > + <div style={{ maxWidth: 300 }} data-test-subj="nodeContextMenu"> + <Section> + <SectionTitle> + <FormattedMessage + id="xpack.infra.nodeContextMenu.title" + defaultMessage="{inventoryName} details" + values={{ inventoryName: inventoryModel.singularDisplayName }} + /> + </SectionTitle> + {inventoryId.label && ( + <SectionSubtitle> + <div style={{ wordBreak: 'break-all' }}> + <FormattedMessage + id="xpack.infra.nodeContextMenu.description" + defaultMessage="View details for {label} {value}" + values={{ label: inventoryId.label, value: inventoryId.value }} + /> + </div> + </SectionSubtitle> + )} + <SectionLinks> + <SectionLink data-test-subj="viewLogsContextMenuItem" {...nodeLogsMenuItem} /> + <SectionLink {...nodeDetailMenuItem} /> + <SectionLink data-test-subj="viewApmTracesContextMenuItem" {...apmTracesMenuItem} /> + <SectionLink {...uptimeMenuItem} /> + <SectionLink {...createAlertMenuItem} /> + </SectionLinks> + </Section> + </div> + </ActionMenu> + <AlertFlyout + filter={ + options.fields + ? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"` + : '' + } + options={options} + nodeType={nodeType} + setVisible={setFlyoutVisible} + visible={flyoutVisible} + /> + </> + ); + } +); diff --git a/x-pack/plugins/infra/public/components/waffle/steps_legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx similarity index 95% rename from x-pack/plugins/infra/public/components/waffle/steps_legend.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx index d2079d53dad71..1ef0f2d0c4288 100644 --- a/x-pack/plugins/infra/public/components/waffle/steps_legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/steps_legend.tsx @@ -7,13 +7,13 @@ import { darken } from 'polished'; import React from 'react'; -import { euiStyled } from '../../../../observability/public'; +import { euiStyled } from '../../../../../../../observability/public'; import { InfraFormatter, InfraWaffleMapRuleOperator, InfraWaffleMapStepLegend, InfraWaffleMapStepRule, -} from '../../lib/lib'; +} from '../../../../../lib/lib'; const OPERATORS = { [InfraWaffleMapRuleOperator.gte]: '>=', diff --git a/x-pack/plugins/infra/public/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/waffle/view_switcher.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx index 78a2cad9ca7ee..76756637eb69e 100644 --- a/x-pack/plugins/infra/public/components/waffle/view_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx @@ -28,7 +28,7 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { label: i18n.translate('xpack.infra.viewSwitcher.tableViewLabel', { defaultMessage: 'Table view', }), - iconType: 'editorUnorderedList', + iconType: 'visTable', }, ]; return ( @@ -37,9 +37,11 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { defaultMessage: 'Switch between table and map view', })} options={buttons} - color="primary" + color="text" + buttonSize="m" idSelected={view} onChange={onChange} + isIconOnly /> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx new file mode 100644 index 0000000000000..3e4ff1de8291d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx @@ -0,0 +1,82 @@ +/* + * 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 { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api'; +import { DropdownButton } from '../dropdown_button'; + +interface Props { + accountId: string; + options: InventoryCloudAccount[]; + changeAccount: (id: string) => void; +} + +export const WaffleAccountsControls = (props: Props) => { + const { accountId, options } = props; + const [isOpen, setIsOpen] = useState<boolean>(false); + + const showPopover = useCallback(() => { + setIsOpen(true); + }, [setIsOpen]); + + const closePopover = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const currentLabel = options.find(o => o.value === accountId); + + const changeAccount = useCallback( + (val: string) => { + if (accountId === val) { + props.changeAccount(''); + } else { + props.changeAccount(val); + } + closePopover(); + }, + [accountId, closePopover, props] + ); + + const panels = useMemo<EuiContextMenuPanelDescriptor[]>( + () => [ + { + id: 0, + title: '', + items: options.map(o => { + const icon = o.value === accountId ? 'check' : 'empty'; + const panel = { name: o.name, onClick: () => changeAccount(o.value), icon }; + return panel; + }), + }, + ], + [options, accountId, changeAccount] + ); + + const button = ( + <DropdownButton label="Account" onClick={showPopover}> + {currentLabel + ? currentLabel.name + : i18n.translate('xpack.infra.waffle.accountAllTitle', { + defaultMessage: 'All', + })} + </DropdownButton> + ); + + return ( + <EuiPopover + isOpen={isOpen} + id="accontPopOver" + button={button} + anchorPosition="downLeft" + panelPaddingSize="none" + closePopover={closePopover} + > + <EuiContextMenu initialPanelId={0} panels={panels} /> + </EuiPopover> + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx similarity index 85% rename from x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index 1a3cef591bc07..c1f406f31e85e 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -9,19 +9,18 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor, - EuiFilterButton, - EuiFilterGroup, EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { InfraGroupByOptions } from '../../lib/lib'; +import { InfraGroupByOptions } from '../../../../../lib/lib'; import { CustomFieldPanel } from './custom_field_panel'; -import { euiStyled } from '../../../../observability/public'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { SnapshotGroupBy } from '../../../common/http_api/snapshot_api'; +import { euiStyled } from '../../../../../../../observability/public'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api'; +import { DropdownButton } from '../dropdown_button'; interface Props { options: Array<{ text: string; field: string; toolTipContent?: string }>; @@ -121,29 +120,31 @@ export const WaffleGroupByControls = class extends React.PureComponent<Props, St .filter(o => o != null) // In this map the `o && o.field` is totally unnecessary but Typescript is // too stupid to realize that the filter above prevents the next map from being null - .map(o => <EuiBadge key={o && o.field}>{o && o.text}</EuiBadge>) + .map(o => ( + <EuiBadge color="hollow" key={o && o.field}> + {o && o.text} + </EuiBadge> + )) ) : ( <FormattedMessage id="xpack.infra.waffle.groupByAllTitle" defaultMessage="All" /> ); + const button = ( - <EuiFilterButton iconType="arrowDown" onClick={this.handleToggle}> - <FormattedMessage id="xpack.infra.waffle.groupByButtonLabel" defaultMessage="Group By: " /> + <DropdownButton label="Group By" onClick={this.handleToggle}> {buttonBody} - </EuiFilterButton> + </DropdownButton> ); return ( - <EuiFilterGroup> - <EuiPopover - isOpen={this.state.isPopoverOpen} - id="groupByPanel" - button={button} - panelPaddingSize="none" - closePopover={this.handleClose} - > - <StyledContextMenu initialPanelId="firstPanel" panels={panels} /> - </EuiPopover> - </EuiFilterGroup> + <EuiPopover + isOpen={this.state.isPopoverOpen} + id="groupByPanel" + button={button} + panelPaddingSize="none" + closePopover={this.handleClose} + > + <StyledContextMenu initialPanelId="firstPanel" panels={panels} /> + </EuiPopover> ); } diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx similarity index 75% rename from x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx index 21da10a0a7650..e534c97eda090 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx @@ -4,19 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiPopover, - EuiContextMenu, - EuiFilterButton, - EuiFilterGroup, - EuiContextMenuPanelDescriptor, -} from '@elastic/eui'; +import { EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { findInventoryModel } from '../../../common/inventory_models'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { useWaffleOptionsContext } from '../../pages/inventory_view/hooks/use_waffle_options'; +import { findInventoryModel } from '../../../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../../../common/inventory_models/types'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { DropdownButton } from '../dropdown_button'; const getDisplayNameForType = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); @@ -120,27 +114,23 @@ export const WaffleInventorySwitcher: React.FC = () => { return getDisplayNameForType(nodeType); }, [nodeType]); + const button = ( + <DropdownButton onClick={openPopover} label="Show"> + {selectedText} + </DropdownButton> + ); + return ( - <EuiFilterGroup> - <EuiPopover - id="contextMenu" - button={ - <EuiFilterButton iconType="arrowDown" onClick={openPopover}> - <FormattedMessage - id="xpack.infra.waffle.inventoryButtonLabel" - defaultMessage="View: {selectedText}" - values={{ selectedText }} - /> - </EuiFilterButton> - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" - withTitle - anchorPosition="downLeft" - > - <EuiContextMenu initialPanelId="firstPanel" panels={panels} /> - </EuiPopover> - </EuiFilterGroup> + <EuiPopover + id="contextMenu" + button={button} + isOpen={isOpen} + closePopover={closePopover} + panelPaddingSize="none" + withTitle + anchorPosition="downLeft" + > + <EuiContextMenu initialPanelId="firstPanel" panels={panels} /> + </EuiPopover> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx new file mode 100644 index 0000000000000..9d759424cdc93 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import React, { useCallback, useState, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { DropdownButton } from '../dropdown_button'; + +interface Props { + region?: string; + options: string[]; + changeRegion: (name: string) => void; +} + +export const WaffleRegionControls = (props: Props) => { + const { region, options } = props; + const [isOpen, setIsOpen] = useState<boolean>(false); + + const showPopover = useCallback(() => { + setIsOpen(true); + }, [setIsOpen]); + + const closePopover = useCallback(() => { + setIsOpen(false); + }, [setIsOpen]); + + const currentLabel = options.find(o => region === o); + + const changeRegion = useCallback( + (val: string) => { + if (region === val) { + props.changeRegion(''); + } else { + props.changeRegion(val); + } + closePopover(); + }, + [region, closePopover, props] + ); + + const panels = useMemo<EuiContextMenuPanelDescriptor[]>( + () => [ + { + id: 0, + title: '', + items: options.map(o => { + const icon = o === region ? 'check' : 'empty'; + const panel = { name: o, onClick: () => changeRegion(o), icon }; + return panel; + }), + }, + ], + [changeRegion, options, region] + ); + + const button = ( + <DropdownButton onClick={showPopover} label="Region"> + {currentLabel || + i18n.translate('xpack.infra.waffle.region', { + defaultMessage: 'All', + })} + </DropdownButton> + ); + + return ( + <EuiPopover + isOpen={isOpen} + id="regionPanel" + button={button} + anchorPosition="downLeft" + panelPaddingSize="none" + closePopover={closePopover} + > + <EuiContextMenu initialPanelId={0} panels={panels} /> + </EuiPopover> + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx similarity index 95% rename from x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx index 458bb674afade..7f190f21484d9 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiDatePicker, EuiFormControlLayout } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; import React, { useCallback } from 'react'; -import { useWaffleTimeContext } from '../../pages/inventory_view/hooks/use_waffle_time'; +import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; export const WaffleTimeControls = () => { const { diff --git a/x-pack/plugins/infra/public/containers/inventory_metadata/use_inventory_meta.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts similarity index 79% rename from x-pack/plugins/infra/public/containers/inventory_metadata/use_inventory_meta.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts index 0ed1f3e35449b..b038491690a13 100644 --- a/x-pack/plugins/infra/public/containers/inventory_metadata/use_inventory_meta.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_inventory_meta.ts @@ -7,13 +7,13 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { useEffect } from 'react'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; import { InventoryMetaResponseRT, InventoryMetaResponse, -} from '../../../common/http_api/inventory_meta_api'; -import { InventoryItemType } from '../../../common/inventory_models/types'; +} from '../../../../../common/http_api/inventory_meta_api'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; export function useInventoryMeta(sourceId: string, nodeType: InventoryItemType) { const decodeResponse = (response: any) => { diff --git a/x-pack/plugins/infra/public/containers/waffle/use_snaphot.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts similarity index 83% rename from x-pack/plugins/infra/public/containers/waffle/use_snaphot.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts index 31f02f46caeda..3ec63d7b2de28 100644 --- a/x-pack/plugins/infra/public/containers/waffle/use_snaphot.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts @@ -8,14 +8,17 @@ import { useEffect } from 'react'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; import { SnapshotNodeResponseRT, SnapshotNodeResponse, SnapshotGroupBy, -} from '../../../common/http_api/snapshot_api'; -import { InventoryItemType, SnapshotMetricType } from '../../../common/inventory_models/types'; +} from '../../../../../common/http_api/snapshot_api'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; export function useSnapshot( filterQuery: string | null | undefined, diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts similarity index 90% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts index 02c079dcaddc4..f6cbb59779039 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_filters.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_filters.ts @@ -10,10 +10,10 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainter from 'constate'; -import { useUrlState } from '../../../utils/use_url_state'; -import { useSourceContext } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../utils/kuery'; -import { esKuery } from '../../../../../../../src/plugins/data/public'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { useSourceContext } from '../../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; const validateKuery = (expression: string) => { try { diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts similarity index 95% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts index 2853917d5f683..32bfe6e085b4e 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_options.ts @@ -17,9 +17,9 @@ import { SnapshotMetricInputRT, SnapshotGroupByRT, SnapshotCustomMetricInputRT, -} from '../../../../common/http_api/snapshot_api'; -import { useUrlState } from '../../../utils/use_url_state'; -import { InventoryItemType, ItemTypeRT } from '../../../../common/inventory_models/types'; +} from '../../../../../common/http_api/snapshot_api'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { InventoryItemType, ItemTypeRT } from '../../../../../common/inventory_models/types'; export const DEFAULT_WAFFLE_OPTIONS_STATE: WaffleOptionsState = { metric: { type: 'cpu' }, diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_time.ts similarity index 97% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_time.ts index 051b5e598cb75..db3abd37b58dd 100644 --- a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_time.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_time.ts @@ -9,7 +9,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import createContainer from 'constate'; -import { useUrlState } from '../../../utils/use_url_state'; +import { useUrlState } from '../../../../utils/use_url_state'; export const DEFAULT_WAFFLE_TIME_STATE: WaffleTimeState = { currentTime: Date.now(), diff --git a/x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_view_state.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts similarity index 100% rename from x-pack/plugins/infra/public/pages/inventory_view/hooks/use_waffle_view_state.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_waffle_view_state.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx new file mode 100644 index 0000000000000..3a2c33d1c824c --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useContext } from 'react'; + +import { FilterBar } from './components/filter_bar'; + +import { DocumentTitle } from '../../../components/document_title'; +import { NoIndices } from '../../../components/empty_states/no_indices'; +import { ColumnarPage } from '../../../components/page'; + +import { SourceErrorPage } from '../../../components/source_error_page'; +import { SourceLoadingPage } from '../../../components/source_loading_page'; +import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; +import { Source } from '../../../containers/source'; +import { useTrackPageview } from '../../../../../observability/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { Layout } from './components/layout'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const SnapshotPage = () => { + const uiCapabilities = useKibana().services.application?.capabilities; + const { + hasFailedLoadingSource, + isLoading, + loadSourceFailureMessage, + loadSource, + metricIndicesExist, + } = useContext(Source.Context); + useTrackPageview({ app: 'infra_metrics', path: 'inventory' }); + useTrackPageview({ app: 'infra_metrics', path: 'inventory', delay: 15000 }); + + const tutorialLinkProps = useLinkProps({ + app: 'kibana', + hash: '/home/tutorial_directory/metrics', + }); + + return ( + <ColumnarPage> + <DocumentTitle + title={(previousTitle: string) => + i18n.translate('xpack.infra.infrastructureSnapshotPage.documentTitle', { + defaultMessage: '{previousTitle} | Inventory', + values: { + previousTitle, + }, + }) + } + /> + {isLoading ? ( + <SourceLoadingPage /> + ) : metricIndicesExist ? ( + <> + <FilterBar /> + <Layout /> + </> + ) : hasFailedLoadingSource ? ( + <SourceErrorPage errorMessage={loadSourceFailureMessage || ''} retry={loadSource} /> + ) : ( + <NoIndices + title={i18n.translate('xpack.infra.homePage.noMetricsIndicesTitle', { + defaultMessage: "Looks like you don't have any metrics indices.", + })} + message={i18n.translate('xpack.infra.homePage.noMetricsIndicesDescription', { + defaultMessage: "Let's add some!", + })} + actions={ + <EuiFlexGroup> + <EuiFlexItem> + <EuiButton + {...tutorialLinkProps} + color="primary" + fill + data-test-subj="infrastructureViewSetupInstructionsButton" + > + {i18n.translate('xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', { + defaultMessage: 'View setup instructions', + })} + </EuiButton> + </EuiFlexItem> + {uiCapabilities?.infrastructure?.configureSource ? ( + <EuiFlexItem> + <ViewSourceConfigurationButton + app="metrics" + data-test-subj="configureSourceButton" + > + {i18n.translate('xpack.infra.configureSourceActionLabel', { + defaultMessage: 'Change source configuration', + })} + </ViewSourceConfigurationButton> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + } + data-test-subj="noMetricsIndicesPrompt" + /> + )} + </ColumnarPage> + ); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/apply_wafflemap_layout.ts similarity index 94% rename from x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/apply_wafflemap_layout.ts index 5f3c06fcfbba7..68600ac5d2ce4 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/apply_wafflemap_layout.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/apply_wafflemap_layout.ts @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { first, sortBy } from 'lodash'; -import { - isWaffleMapGroupWithGroups, - isWaffleMapGroupWithNodes, -} from '../../../containers/waffle/type_guards'; -import { InfraWaffleMapGroup } from '../../../lib/lib'; +import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from './type_guards'; +import { InfraWaffleMapGroup } from '../../../../lib/lib'; import { sizeOfSquares } from './size_of_squares'; export function getColumns(n: number, w = 1, h = 1) { diff --git a/x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/color_from_value.ts similarity index 98% rename from x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/color_from_value.ts index c082686bb9d63..334865306ee88 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/color_from_value.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/color_from_value.ts @@ -12,7 +12,7 @@ import { InfraWaffleMapLegend, InfraWaffleMapRuleOperator, InfraWaffleMapStepLegend, -} from '../../../lib/lib'; +} from '../../../../lib/lib'; import { isInfraWaffleMapGradientLegend, isInfraWaffleMapStepLegend } from './type_guards'; const OPERATOR_TO_FN = { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts new file mode 100644 index 0000000000000..f8c7a10f12831 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -0,0 +1,89 @@ +/* + * 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 { get } from 'lodash'; +import { InfraFormatterType } from '../../../../lib/lib'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInputRT, +} from '../../../../../common/http_api/snapshot_api'; +import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric'; +import { createFormatter } from '../../../../../common/formatters'; + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +const METRIC_FORMATTERS: MetricFormatters = { + ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, + ['cpu']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['memory']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['logRate']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, + ['diskIOReadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['diskIOWriteBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['s3BucketSize']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3TotalRequests']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3NumberOfObjects']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3UploadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3DownloadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['sqsOldestMessage']: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, +}; + +export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => ( + val: string | number +) => { + if (SnapshotCustomMetricInputRT.is(metric)) { + const formatter = createFormatterForMetric(metric); + return formatter(val); + } + const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); + if (val == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(val); +}; diff --git a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts similarity index 96% rename from x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts index 902969c83ba39..5f760cf2f591e 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts @@ -5,8 +5,8 @@ */ import { createUptimeLink } from './create_uptime_link'; -import { InfraWaffleMapOptions, InfraFormatterType } from '../../../lib/lib'; -import { SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { InfraWaffleMapOptions, InfraFormatterType } from '../../../../lib/lib'; +import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; const options: InfraWaffleMapOptions = { fields: { diff --git a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts similarity index 83% rename from x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts index 72b46f4fb5c7b..6c089ee8d22f4 100644 --- a/x-pack/plugins/infra/public/components/waffle/lib/create_uptime_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts @@ -5,9 +5,9 @@ */ import { get } from 'lodash'; -import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../lib/lib'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; -import { LinkDescriptor } from '../../../hooks/use_link_props'; +import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { LinkDescriptor } from '../../../../hooks/use_link_props'; export const createUptimeLink = ( options: InfraWaffleMapOptions, diff --git a/x-pack/plugins/infra/public/components/waffle/lib/field_to_display_name.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/field_to_display_name.ts similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/lib/field_to_display_name.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/field_to_display_name.ts diff --git a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts similarity index 97% rename from x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts index d2511ba7e669e..469b54b2d9d68 100644 --- a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/nodes_to_wafflemap.ts @@ -11,9 +11,9 @@ import { InfraWaffleMapGroupOfGroups, InfraWaffleMapGroupOfNodes, InfraWaffleMapNode, -} from '../../lib/lib'; +} from '../../../../lib/lib'; import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from './type_guards'; -import { SnapshotNodePath, SnapshotNode } from '../../../common/http_api/snapshot_api'; +import { SnapshotNodePath, SnapshotNode } from '../../../../../common/http_api/snapshot_api'; export function createId(path: SnapshotNodePath[]) { return path.map(p => p.value).join('/'); diff --git a/x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/size_of_squares.ts similarity index 100% rename from x-pack/plugins/infra/public/components/waffle/lib/size_of_squares.ts rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/size_of_squares.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/type_guards.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/type_guards.ts new file mode 100644 index 0000000000000..62d02022d4c93 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/type_guards.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + InfraWaffleMapGroupOfGroups, + InfraWaffleMapGroupOfNodes, + InfraWaffleMapGradientLegend, + InfraWaffleMapStepLegend, +} from '../../../../lib/lib'; + +export function isInfraWaffleMapStepLegend(subject: any): subject is InfraWaffleMapStepLegend { + return subject.type && subject.type === 'step'; +} + +export function isInfraWaffleMapGradientLegend( + subject: any +): subject is InfraWaffleMapGradientLegend { + return subject.type && subject.type === 'gradient'; +} + +export function isWaffleMapGroupWithNodes(subject: any): subject is InfraWaffleMapGroupOfNodes { + return subject && subject.nodes != null && Array.isArray(subject.nodes); +} + +export function isWaffleMapGroupWithGroups(subject: any): subject is InfraWaffleMapGroupOfGroups { + return subject && subject.groups != null && Array.isArray(subject.groups); +} diff --git a/x-pack/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx similarity index 94% rename from x-pack/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx index 290f0eda452ce..588a0d84918c6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/chart_section_vis.tsx @@ -8,7 +8,7 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { Axis, Chart, niceTimeFormatter, Position, Settings, TooltipValue } from '@elastic/charts'; import { EuiPageContentBody } from '@elastic/eui'; -import { getChartTheme } from '../../../components/metrics_explorer/helpers/get_chart_theme'; +import { getChartTheme } from '../../metrics_explorer/components/helpers/get_chart_theme'; import { SeriesChart } from './series_chart'; import { getFormatter, @@ -19,8 +19,8 @@ import { seriesHasLessThen2DataPoints, } from './helpers'; import { ErrorMessage } from './error_message'; -import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; -import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; +import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; import { VisSectionProps } from '../types'; export const ChartSectionVis = ({ diff --git a/x-pack/plugins/infra/public/pages/metrics/components/error_message.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/error_message.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/components/error_message.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/error_message.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/components/gauges_section_vis.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx similarity index 92% rename from x-pack/plugins/infra/public/pages/metrics/components/gauges_section_vis.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx index e069b52be8be7..0f53ced80888b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/gauges_section_vis.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/gauges_section_vis.tsx @@ -16,9 +16,9 @@ import { import { get, last, max } from 'lodash'; import React, { ReactText } from 'react'; -import { euiStyled } from '../../../../../observability/public'; -import { createFormatter } from '../../../utils/formatters'; -import { InventoryFormatterType } from '../../../../common/inventory_models/types'; +import { euiStyled } from '../../../../../../observability/public'; +import { createFormatter } from '../../../../../common/formatters'; +import { InventoryFormatterType } from '../../../../../common/inventory_models/types'; import { SeriesOverrides, VisSectionProps } from '../types'; import { getChartName } from './helpers'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts new file mode 100644 index 0000000000000..0b8773db2dddf --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/helpers.ts @@ -0,0 +1,99 @@ +/* + * 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 { ReactText } from 'react'; +import Color from 'color'; +import { get, first, last, min, max } from 'lodash'; +import { createFormatter } from '../../../../../common/formatters'; +import { InfraDataSeries } from '../../../../graphql/types'; +import { + InventoryVisTypeRT, + InventoryFormatterType, + InventoryVisType, +} from '../../../../../common/inventory_models/types'; +import { SeriesOverrides } from '../types'; +import { NodeDetailsMetricData } from '../../../../../common/http_api/node_details_api'; + +/** + * Returns a formatter + */ +export const getFormatter = ( + formatter: InventoryFormatterType = 'number', + template: string = '{{value}}' +) => (val: ReactText) => (val != null ? createFormatter(formatter, template)(val) : ''); + +/** + * Does a series have more then two points? + */ +export const seriesHasLessThen2DataPoints = (series: InfraDataSeries): boolean => { + return series.data.length < 2; +}; + +/** + * Returns the minimum and maximum timestamp for a metric + */ +export const getMaxMinTimestamp = (metric: NodeDetailsMetricData): [number, number] => { + if (metric.series.some(seriesHasLessThen2DataPoints)) { + return [0, 0]; + } + const values = metric.series.reduce((acc, item) => { + const firstRow = first(item.data); + const lastRow = last(item.data); + return acc.concat([(firstRow && firstRow.timestamp) || 0, (lastRow && lastRow.timestamp) || 0]); + }, [] as number[]); + return [min(values), max(values)]; +}; + +/** + * Returns the chart name from the visConfig based on the series id, otherwise it + * just returns the seriesId + */ +export const getChartName = ( + seriesOverrides: SeriesOverrides | undefined, + seriesId: string, + label: string +) => { + if (!seriesOverrides) { + return label; + } + return get(seriesOverrides, [seriesId, 'name'], label); +}; + +/** + * Returns the chart color from the visConfig based on the series id, otherwise it + * just returns null if the color doesn't exists in the overrides. + */ +export const getChartColor = (seriesOverrides: SeriesOverrides | undefined, seriesId: string) => { + const rawColor: string | null = seriesOverrides + ? get(seriesOverrides, [seriesId, 'color']) + : null; + if (!rawColor) { + return null; + } + const color = new Color(rawColor); + return color.hex().toString(); +}; + +/** + * Gets the chart type based on the section and seriesId + */ +export const getChartType = ( + seriesOverrides: SeriesOverrides | undefined, + type: InventoryVisType | undefined, + seriesId: string +) => { + if (!seriesOverrides || !type) { + return 'line'; + } + const overrideValue = get(seriesOverrides, [seriesId, 'type']); + if (InventoryVisTypeRT.is(overrideValue)) { + return overrideValue; + } + if (InventoryVisTypeRT.is(type)) { + return type; + } + return 'line'; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx similarity index 90% rename from x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index b089e2237c2e5..6d83017a3f689 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -7,9 +7,9 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { euiStyled } from '../../../../../observability/public'; -import { ViewSourceConfigurationButton } from '../../../components/source_configuration'; -import { useLinkProps } from '../../../hooks/use_link_props'; +import { euiStyled } from '../../../../../../observability/public'; +import { ViewSourceConfigurationButton } from '../../../../components/source_configuration'; +import { useLinkProps } from '../../../../hooks/use_link_props'; interface InvalidNodeErrorProps { nodeName: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/layout_content.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx similarity index 84% rename from x-pack/plugins/infra/public/pages/metrics/components/layout_content.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx index 4e10f245acdcc..4620102517549 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/layout_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/layout_content.tsx @@ -5,7 +5,7 @@ */ import { EuiPageContent } from '@elastic/eui'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; export const LayoutContent = euiStyled(EuiPageContent)` position: relative; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/metadata_details.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx similarity index 97% rename from x-pack/plugins/infra/public/pages/metrics/components/metadata_details.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx index 3bf492d398f2c..7ca69dd56251d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/metadata_details.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/metadata_details.tsx @@ -8,8 +8,8 @@ import React, { useContext, useState, useCallback, useMemo } from 'react'; import { EuiButtonIcon, EuiFlexGrid, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { InfraMetadata } from '../../../../common/http_api'; -import { euiStyled } from '../../../../../observability/public'; +import { InfraMetadata } from '../../../../../common/http_api'; +import { euiStyled } from '../../../../../../observability/public'; import { MetadataContext } from '../containers/metadata_context'; interface FieldDef { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx similarity index 90% rename from x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx index dd2a5f2bdb39e..0d0bc8c82397e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/node_details_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/node_details_page.tsx @@ -13,19 +13,19 @@ import { EuiHideFor, EuiTitle, } from '@elastic/eui'; -import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; -import { InventoryMetric, InventoryItemType } from '../../../../common/inventory_models/types'; -import { useNodeDetails } from '../../../containers/node_details/use_node_details'; +import { InfraTimerangeInput } from '../../../../../common/http_api/snapshot_api'; +import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types'; +import { useNodeDetails } from '../hooks/use_node_details'; import { MetricsSideNav } from './side_nav'; -import { AutoSizer } from '../../../components/auto_sizer'; +import { AutoSizer } from '../../../../components/auto_sizer'; import { MetricsTimeControls } from './time_controls'; import { SideNavContext, NavItem } from '../lib/side_nav_context'; import { PageBody } from './page_body'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; -import { InfraMetadata } from '../../../../common/http_api/metadata_api'; +import { InfraMetadata } from '../../../../../common/http_api/metadata_api'; import { PageError } from './page_error'; -import { MetadataContext } from '../../../pages/metrics/containers/metadata_context'; +import { MetadataContext } from '../containers/metadata_context'; interface Props { name: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_body.tsx similarity index 82% rename from x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_body.tsx index e651d6b92d981..68166a6e53bfd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/page_body.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_body.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { findLayout } from '../../../../common/inventory_models/layouts'; -import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { findLayout } from '../../../../../common/inventory_models/layouts'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; -import { InfraLoadingPanel } from '../../../components/loading'; -import { NoData } from '../../../components/empty_states'; -import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; +import { InfraLoadingPanel } from '../../../../components/loading'; +import { NoData } from '../../../../components/empty_states'; +import { NodeDetailsMetricData } from '../../../../../common/http_api/node_details_api'; interface Props { loading: boolean; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/page_error.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx similarity index 90% rename from x-pack/plugins/infra/public/pages/metrics/components/page_error.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx index e54cdcd151f6f..bda2a5941e023 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/page_error.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/page_error.tsx @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { IHttpFetchError } from 'src/core/public'; import { InvalidNodeError } from './invalid_node'; // import { InfraMetricsErrorCodes } from '../../../../common/errors'; -import { DocumentTitle } from '../../../components/document_title'; -import { ErrorPageBody } from '../../error'; +import { DocumentTitle } from '../../../../components/document_title'; +import { ErrorPageBody } from '../../../error'; interface Props { name: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/section.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/section.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/components/section.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/section.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx new file mode 100644 index 0000000000000..0d7716ad3cc66 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/series_chart.tsx @@ -0,0 +1,87 @@ +/* + * 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 { + AreaSeries, + BarSeries, + ScaleType, + RecursivePartial, + BarSeriesStyle, + AreaSeriesStyle, +} from '@elastic/charts'; +import { InfraDataSeries } from '../../../../graphql/types'; +import { InventoryVisType } from '../../../../../common/inventory_models/types'; + +interface Props { + id: string; + name: string; + color: string | null; + series: InfraDataSeries; + type: InventoryVisType; + stack: boolean | undefined; +} + +export const SeriesChart = (props: Props) => { + if (props.type === 'bar') { + return <BarChart {...props} />; + } + return <AreaChart {...props} />; +}; + +export const AreaChart = ({ id, color, series, name, type, stack }: Props) => { + const style: RecursivePartial<AreaSeriesStyle> = { + area: { + opacity: 1, + visible: 'area' === type, + }, + line: { + strokeWidth: 'area' === type ? 1 : 2, + visible: true, + }, + }; + return ( + <AreaSeries + id={id} + name={name} + xScaleType={ScaleType.Time} + yScaleType={ScaleType.Linear} + xAccessor="timestamp" + yAccessors={['value']} + data={series.data} + areaSeriesStyle={style} + color={color ? color : void 0} + stackAccessors={stack ? ['timestamp'] : void 0} + /> + ); +}; + +export const BarChart = ({ id, color, series, name, type, stack }: Props) => { + const style: RecursivePartial<BarSeriesStyle> = { + rectBorder: { + stroke: color || void 0, + strokeWidth: 1, + visible: true, + }, + rect: { + opacity: 1, + }, + }; + return ( + <BarSeries + id={id} + name={name} + xScaleType={ScaleType.Time} + yScaleType={ScaleType.Linear} + xAccessor="timestamp" + yAccessors={['value']} + data={series.data} + barSeriesStyle={style} + color={color ? color : void 0} + stackAccessors={stack ? ['timestamp'] : void 0} + /> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/components/side_nav.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx similarity index 95% rename from x-pack/plugins/infra/public/pages/metrics/components/side_nav.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx index 94f97c7f45e61..1cba3366acbbb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/side_nav.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/side_nav.tsx @@ -6,7 +6,7 @@ import { EuiHideFor, EuiPageSideBar, EuiShowFor, EuiSideNav } from '@elastic/eui'; import React, { useState, useCallback } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; import { NavItem } from '../lib/side_nav_context'; interface Props { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/sub_section.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx similarity index 94% rename from x-pack/plugins/infra/public/pages/metrics/components/sub_section.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx index 325d510293135..7b269adc96638 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/sub_section.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/sub_section.tsx @@ -6,7 +6,7 @@ import React, { isValidElement, cloneElement, FunctionComponent, Children, useMemo } from 'react'; import { EuiTitle } from '@elastic/eui'; -import { InventoryMetric } from '../../../../common/inventory_models/types'; +import { InventoryMetric } from '../../../../../common/inventory_models/types'; import { LayoutProps } from '../types'; type SubSectionProps = LayoutProps & { diff --git a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.test.tsx index 02ba506e8abe1..83f5187f8a46c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.test.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../utils/use_kibana_ui_setting', () => ({ +jest.mock('../../../../utils/use_kibana_ui_setting', () => ({ _esModule: true, useKibanaUiSetting: jest.fn(() => [ [ diff --git a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx similarity index 91% rename from x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx index cdbdc9bb7ecdb..ef6486eac0fdb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/components/time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/time_controls.tsx @@ -6,10 +6,10 @@ import { EuiSuperDatePicker, OnRefreshChangeProps, OnTimeChangeProps } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../observability/public'; import { MetricsTimeInput } from '../hooks/use_metrics_time'; -import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; -import { mapKibanaQuickRangesToDatePickerRanges } from '../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; +import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; interface MetricsTimeControlsProps { currentTimeRange: MetricsTimeInput; diff --git a/x-pack/plugins/infra/public/pages/metrics/containers/metadata_context.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/containers/metadata_context.ts similarity index 84% rename from x-pack/plugins/infra/public/pages/metrics/containers/metadata_context.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/containers/metadata_context.ts index 4ecf7fa15548c..b8995d27d4dbe 100644 --- a/x-pack/plugins/infra/public/pages/metrics/containers/metadata_context.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/containers/metadata_context.ts @@ -5,5 +5,5 @@ */ import React from 'react'; -import { InfraMetadata } from '../../../../common/http_api'; +import { InfraMetadata } from '../../../../../common/http_api'; export const MetadataContext = React.createContext<InfraMetadata | null>(null); diff --git a/x-pack/plugins/infra/public/pages/metrics/hooks/metrics_time.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/metrics_time.test.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/hooks/metrics_time.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/metrics_time.test.tsx diff --git a/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metadata.ts similarity index 76% rename from x-pack/plugins/infra/public/containers/metadata/use_metadata.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metadata.ts index 1ba016195bef4..d2a20d449d89a 100644 --- a/x-pack/plugins/infra/public/containers/metadata/use_metadata.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metadata.ts @@ -8,12 +8,12 @@ import { useEffect } from 'react'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { InfraMetadata, InfraMetadataRT } from '../../../common/http_api/metadata_api'; -import { useHTTPRequest } from '../../hooks/use_http_request'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { InventoryMetric, InventoryItemType } from '../../../common/inventory_models/types'; -import { getFilteredMetrics } from './lib/get_filtered_metrics'; -import { MetricsTimeInput } from '../../pages/metrics/hooks/use_metrics_time'; +import { InfraMetadata, InfraMetadataRT } from '../../../../../common/http_api/metadata_api'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types'; +import { getFilteredMetrics } from '../lib/get_filtered_metrics'; +import { MetricsTimeInput } from './use_metrics_time'; export function useMetadata( nodeId: string, diff --git a/x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metrics_time.ts similarity index 96% rename from x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metrics_time.ts index 2ed86863535ff..98803ef2e69c6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hooks/use_metrics_time.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_metrics_time.ts @@ -12,8 +12,8 @@ import * as rt from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; -import { useUrlState } from '../../../utils/use_url_state'; -import { replaceStateKeyInQueryString } from '../../../utils/url_state'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { replaceStateKeyInQueryString } from '../../../../utils/url_state'; const parseRange = (range: MetricsTimeInput) => { const parsedFrom = dateMath.parse(range.from.toString()); diff --git a/x-pack/plugins/infra/public/containers/node_details/use_node_details.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_node_details.ts similarity index 75% rename from x-pack/plugins/infra/public/containers/node_details/use_node_details.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_node_details.ts index 189c48ba1a70c..3bb6c98c8eac0 100644 --- a/x-pack/plugins/infra/public/containers/node_details/use_node_details.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_node_details.ts @@ -6,14 +6,14 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { useHTTPRequest } from '../../hooks/use_http_request'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; import { NodeDetailsMetricDataResponseRT, NodeDetailsMetricDataResponse, -} from '../../../common/http_api/node_details_api'; -import { InventoryMetric, InventoryItemType } from '../../../common/inventory_models/types'; -import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; +} from '../../../../../common/http_api/node_details_api'; +import { InventoryMetric, InventoryItemType } from '../../../../../common/inventory_models/types'; +import { InfraTimerangeInput } from '../../../../../common/http_api/snapshot_api'; export function useNodeDetails( metrics: InventoryMetric[], diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx new file mode 100644 index 0000000000000..197a735f7fd1f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -0,0 +1,140 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useContext, useState } from 'react'; +import { euiStyled, EuiTheme, withTheme } from '../../../../../observability/public'; +import { DocumentTitle } from '../../../components/document_title'; +import { Header } from '../../../components/header'; +import { ColumnarPage, PageContent } from '../../../components/page'; +import { withMetricPageProviders } from './page_providers'; +import { useMetadata } from './hooks/use_metadata'; +import { Source } from '../../../containers/source'; +import { InfraLoadingPanel } from '../../../components/loading'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { NavItem } from './lib/side_nav_context'; +import { NodeDetailsPage } from './components/node_details_page'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { useMetricsTimeContext } from './hooks/use_metrics_time'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +const DetailPageContent = euiStyled(PageContent)` + overflow: auto; + background-color: ${props => props.theme.eui.euiColorLightestShade}; +`; + +interface Props { + theme: EuiTheme; + match: { + params: { + type: string; + node: string; + }; + }; +} + +export const MetricDetail = withMetricPageProviders( + withTheme(({ match }: Props) => { + const uiCapabilities = useKibana().services.application?.capabilities; + const nodeId = match.params.node; + const nodeType = match.params.type as InventoryItemType; + const inventoryModel = findInventoryModel(nodeType); + const { sourceId } = useContext(Source.Context); + const { + timeRange, + parsedTimeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + triggerRefresh, + } = useMetricsTimeContext(); + const { + name, + filteredRequiredMetrics, + loading: metadataLoading, + cloudId, + metadata, + } = useMetadata(nodeId, nodeType, inventoryModel.requiredMetrics, sourceId, parsedTimeRange); + + const [sideNav, setSideNav] = useState<NavItem[]>([]); + + const addNavItem = React.useCallback( + (item: NavItem) => { + if (!sideNav.some(n => n.id === item.id)) { + setSideNav([item, ...sideNav]); + } + }, + [sideNav] + ); + + const metricsLinkProps = useLinkProps({ + app: 'metrics', + pathname: '/', + }); + + const breadcrumbs = [ + { + ...metricsLinkProps, + text: i18n.translate('xpack.infra.header.infrastructureTitle', { + defaultMessage: 'Metrics', + }), + }, + { text: name }, + ]; + + if (metadataLoading && !filteredRequiredMetrics.length) { + return ( + <InfraLoadingPanel + height="100vh" + width="100%" + text={i18n.translate('xpack.infra.metrics.loadingNodeDataText', { + defaultMessage: 'Loading data', + })} + /> + ); + } + + return ( + <ColumnarPage> + <Header breadcrumbs={breadcrumbs} readOnlyBadge={!uiCapabilities?.infrastructure?.save} /> + <DocumentTitle + title={i18n.translate('xpack.infra.metricDetailPage.documentTitle', { + defaultMessage: 'Infrastructure | Metrics | {name}', + values: { + name, + }, + })} + /> + <DetailPageContent data-test-subj="infraMetricsPage"> + {metadata ? ( + <NodeDetailsPage + name={name} + requiredMetrics={filteredRequiredMetrics} + sourceId={sourceId} + timeRange={timeRange} + parsedTimeRange={parsedTimeRange} + nodeType={nodeType} + nodeId={nodeId} + cloudId={cloudId} + metadataLoading={metadataLoading} + isAutoReloading={isAutoReloading} + refreshInterval={refreshInterval} + sideNav={sideNav} + metadata={metadata} + addNavItem={addNavItem} + setRefreshInterval={setRefreshInterval} + setAutoReload={setAutoReload} + triggerRefresh={triggerRefresh} + setTimeRange={setTimeRange} + /> + ) : null} + </DetailPageContent> + </ColumnarPage> + ); + }) +); diff --git a/x-pack/plugins/infra/public/containers/metadata/lib/get_filtered_metrics.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts similarity index 78% rename from x-pack/plugins/infra/public/containers/metadata/lib/get_filtered_metrics.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts index b485c90700145..57ff182f01963 100644 --- a/x-pack/plugins/infra/public/containers/metadata/lib/get_filtered_metrics.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraMetadataFeature } from '../../../../common/http_api/metadata_api'; -import { InventoryMetric } from '../../../../common/inventory_models/types'; -import { metrics } from '../../../../common/inventory_models/metrics'; +import { InfraMetadataFeature } from '../../../../../common/http_api/metadata_api'; +import { InventoryMetric } from '../../../../../common/inventory_models/types'; +import { metrics } from '../../../../../common/inventory_models/metrics'; export const getFilteredMetrics = ( requiredMetrics: InventoryMetric[], diff --git a/x-pack/plugins/infra/public/pages/metrics/lib/side_nav_context.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/side_nav_context.ts similarity index 100% rename from x-pack/plugins/infra/public/pages/metrics/lib/side_nav_context.ts rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/side_nav_context.ts diff --git a/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx similarity index 91% rename from x-pack/plugins/infra/public/pages/metrics/page_providers.tsx rename to x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx index d3f10adec06ed..597977d9d2735 100644 --- a/x-pack/plugins/infra/public/pages/metrics/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/page_providers.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { Source } from '../../containers/source'; +import { Source } from '../../../containers/source'; import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const withMetricPageProviders = <T extends object>(Component: React.ComponentType<T>) => ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts new file mode 100644 index 0000000000000..3ec57e23a425d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts @@ -0,0 +1,62 @@ +/* + * 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 rt from 'io-ts'; +import { EuiTheme } from '../../../../../observability/public'; +import { InventoryFormatterTypeRT } from '../../../../common/inventory_models/types'; +import { MetricsTimeInput } from './hooks/use_metrics_time'; +import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; + +export interface LayoutProps { + metrics?: NodeDetailsMetricData[]; + onChangeRangeTime?: (time: MetricsTimeInput) => void; + isLiveStreaming?: boolean; + stopLiveStreaming?: () => void; +} + +export type LayoutPropsWithTheme = LayoutProps & { theme: EuiTheme }; + +const ChartTypesRT = rt.keyof({ + area: null, + bar: null, + line: null, +}); + +export const SeriesOverridesObjectRT = rt.intersection([ + rt.type({ + color: rt.string, + }), + rt.partial({ + name: rt.string, + formatter: InventoryFormatterTypeRT, + formatterTemplate: rt.string, + gaugeMax: rt.number, + type: ChartTypesRT, + }), +]); + +export const SeriesOverridesRT = rt.record( + rt.string, + rt.union([rt.undefined, SeriesOverridesObjectRT]) +); + +export type SeriesOverrides = rt.TypeOf<typeof SeriesOverridesRT>; + +export const VisSectionPropsRT = rt.partial({ + type: ChartTypesRT, + stacked: rt.boolean, + formatter: InventoryFormatterTypeRT, + formatterTemplate: rt.string, + seriesOverrides: SeriesOverridesRT, +}); + +export type VisSectionProps = rt.TypeOf<typeof VisSectionPropsRT> & { + id?: string; + metric?: NodeDetailsMetricData; + onChangeRangeTime?: (time: MetricsTimeInput) => void; + isLiveStreaming?: boolean; + stopLiveStreaming?: () => void; +}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/aggregation.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx similarity index 86% rename from x-pack/plugins/infra/public/components/metrics_explorer/aggregation.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx index 76fa519ab3756..5a84d204b3b25 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/aggregation.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/aggregation.tsx @@ -8,12 +8,12 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback } from 'react'; -import { MetricsExplorerAggregation } from '../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerAggregation } from '../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; import { metricsExplorerAggregationRT, METRIC_EXPLORER_AGGREGATIONS, -} from '../../../common/http_api/metrics_explorer'; +} from '../../../../../common/http_api/metrics_explorer'; interface Props { options: MetricsExplorerOptions; @@ -26,6 +26,9 @@ export const MetricsExplorerAggregationPicker = ({ options, onChange }: Props) = ['avg']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.avg', { defaultMessage: 'Average', }), + ['sum']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.sum', { + defaultMessage: 'Sum', + }), ['max']: i18n.translate('xpack.infra.metricsExplorer.aggregationLables.max', { defaultMessage: 'Max', }), diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/metrics_explorer/chart.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx index 43b08f45eed34..089e1abfc4c91 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx @@ -10,24 +10,24 @@ import { EuiTitle, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Axis, Chart, niceTimeFormatter, Position, Settings, TooltipValue } from '@elastic/charts'; import { first, last } from 'lodash'; import moment from 'moment'; -import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; +import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions, MetricsExplorerYAxisMode, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { euiStyled } from '../../../../observability/public'; +} from '../hooks/use_metrics_explorer_options'; +import { euiStyled } from '../../../../../../observability/public'; import { createFormatterForMetric } from './helpers/create_formatter_for_metric'; import { MetricExplorerSeriesChart } from './series_chart'; import { MetricsExplorerChartContextMenu } from './chart_context_menu'; -import { SourceQuery } from '../../graphql/types'; +import { SourceQuery } from '../../../../graphql/types'; import { MetricsExplorerEmptyChart } from './empty_chart'; import { MetricsExplorerNoMetrics } from './no_metrics'; import { getChartTheme } from './helpers/get_chart_theme'; -import { useKibanaUiSetting } from '../../utils/use_kibana_ui_setting'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; import { calculateDomain } from './helpers/calculate_domain'; -import { useKibana, useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana, useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; interface Props { title?: string | null; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx index 8ffef269a42ea..5c0abb8fd845f 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.test.tsx @@ -7,9 +7,14 @@ import React from 'react'; import { MetricsExplorerChartContextMenu, createNodeDetailLink, Props } from './chart_context_menu'; import { ReactWrapper, mount } from 'enzyme'; -import { options, source, timeRange, chartOptions } from '../../utils/fixtures/metrics_explorer'; +import { + options, + source, + timeRange, + chartOptions, +} from '../../../../utils/fixtures/metrics_explorer'; import { Capabilities } from 'src/core/public'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { coreMock } from 'src/core/public/mocks'; const coreStartMock = coreMock.createStart(); diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx similarity index 91% rename from x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index 75a04cbe9799e..e13c9dcc06984 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -14,18 +14,18 @@ import { } from '@elastic/eui'; import DateMath from '@elastic/datemath'; import { Capabilities } from 'src/core/public'; -import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; +import { AlertFlyout } from '../../../../alerting/metric_threshold/components/alert_flyout'; +import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../hooks/use_metrics_explorer_options'; import { createTSVBLink } from './helpers/create_tsvb_link'; -import { getNodeDetailUrl } from '../../pages/link_to/redirect_to_node_detail'; -import { SourceConfiguration } from '../../utils/source_configuration'; -import { InventoryItemType } from '../../../common/inventory_models/types'; -import { AlertFlyout } from '../alerting/metrics/alert_flyout'; -import { useLinkProps } from '../../hooks/use_link_props'; +import { getNodeDetailUrl } from '../../../link_to/redirect_to_node_detail'; +import { SourceConfiguration } from '../../../../utils/source_configuration'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { useLinkProps } from '../../../../hooks/use_link_props'; export interface Props { options: MetricsExplorerOptions; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx new file mode 100644 index 0000000000000..ba28075ededb6 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_options.tsx @@ -0,0 +1,166 @@ +/* + * 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, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + EuiRadioGroup, + EuiButtonEmpty, + EuiPopover, + EuiForm, + EuiFormRow, + EuiSwitch, +} from '@elastic/eui'; +import { + MetricsExplorerChartOptions as ChartOptions, + MetricsExplorerYAxisMode, + MetricsExplorerChartType, +} from '../hooks/use_metrics_explorer_options'; + +interface Props { + chartOptions: ChartOptions; + onChange: (options: ChartOptions) => void; +} + +export const MetricsExplorerChartOptions = ({ chartOptions, onChange }: Props) => { + const [isPopoverOpen, setPopoverState] = useState<boolean>(false); + + const handleClosePopover = useCallback(() => { + setPopoverState(false); + }, []); + + const handleOpenPopover = useCallback(() => { + setPopoverState(true); + }, []); + + const button = ( + <EuiButtonEmpty iconSide="left" iconType="eye" onClick={handleOpenPopover}> + <FormattedMessage + id="xpack.infra.metricsExplorer.customizeChartOptions" + defaultMessage="Customize" + /> + </EuiButtonEmpty> + ); + + const yAxisRadios = [ + { + id: MetricsExplorerYAxisMode.auto, + label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.autoLabel', { + defaultMessage: 'Automatic (min to max)', + }), + }, + { + id: MetricsExplorerYAxisMode.fromZero, + label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.fromZeroLabel', { + defaultMessage: 'From zero (0 to max)', + }), + }, + ]; + + const typeRadios = [ + { + id: MetricsExplorerChartType.line, + label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.lineLabel', { + defaultMessage: 'Line', + }), + }, + { + id: MetricsExplorerChartType.area, + label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.areaLabel', { + defaultMessage: 'Area', + }), + }, + { + id: MetricsExplorerChartType.bar, + label: i18n.translate('xpack.infra.metricsExplorer.chartOptions.barLabel', { + defaultMessage: 'Bar', + }), + }, + ]; + + const handleYAxisChange = useCallback( + (id: string) => { + onChange({ + ...chartOptions, + yAxisMode: id as MetricsExplorerYAxisMode, + }); + }, + [chartOptions, onChange] + ); + + const handleTypeChange = useCallback( + (id: string) => { + onChange({ + ...chartOptions, + type: id as MetricsExplorerChartType, + }); + }, + [chartOptions, onChange] + ); + + const handleStackChange = useCallback( + e => { + onChange({ + ...chartOptions, + stack: e.target.checked, + }); + }, + [chartOptions, onChange] + ); + + return ( + <EuiPopover + id="MetricExplorerChartOptionsPopover" + button={button} + isOpen={isPopoverOpen} + closePopover={handleClosePopover} + > + <EuiForm> + <EuiFormRow + compressed + label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.typeLabel', { + defaultMessage: 'Chart style', + })} + > + <EuiRadioGroup + compressed + options={typeRadios} + idSelected={chartOptions.type} + onChange={handleTypeChange} + /> + </EuiFormRow> + <EuiFormRow + compressed + label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.stackLabel', { + defaultMessage: 'Stack series', + })} + > + <EuiSwitch + label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.stackSwitchLabel', { + defaultMessage: 'Stack', + })} + checked={chartOptions.stack} + onChange={handleStackChange} + /> + </EuiFormRow> + <EuiFormRow + compressed + label={i18n.translate('xpack.infra.metricsExplorer.chartOptions.yAxisDomainLabel', { + defaultMessage: 'Y Axis Domain', + })} + > + <EuiRadioGroup + compressed + options={yAxisRadios} + idSelected={chartOptions.yAxisMode} + onChange={handleYAxisChange} + /> + </EuiFormRow> + </EuiForm> + </EuiPopover> + ); +}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/charts.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx similarity index 92% rename from x-pack/plugins/infra/public/components/metrics_explorer/charts.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx index 64e7b27f5722f..ecec116310875 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/charts.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx @@ -8,16 +8,16 @@ import { EuiButton, EuiFlexGrid, EuiFlexItem, EuiText, EuiHorizontalRule } from import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { MetricsExplorerResponse } from '../../../common/http_api/metrics_explorer'; +import { MetricsExplorerResponse } from '../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { InfraLoadingPanel } from '../loading'; -import { NoData } from '../empty_states/no_data'; +} from '../hooks/use_metrics_explorer_options'; +import { InfraLoadingPanel } from '../../../../components/loading'; +import { NoData } from '../../../../components/empty_states/no_data'; import { MetricsExplorerChart } from './chart'; -import { SourceQuery } from '../../graphql/types'; +import { SourceQuery } from '../../../../graphql/types'; interface Props { loading: boolean; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/empty_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/empty_chart.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/metrics_explorer/empty_chart.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/empty_chart.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/group_by.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/group_by.tsx new file mode 100644 index 0000000000000..bfe8ddb2e0829 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/group_by.tsx @@ -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 { EuiComboBox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import React, { useCallback } from 'react'; +import { IFieldType } from 'src/plugins/data/public'; +import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; + +interface Props { + options: MetricsExplorerOptions; + onChange: (groupBy: string | null) => void; + fields: IFieldType[]; +} + +export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => { + const handleChange = useCallback( + selectedOptions => { + const groupBy = (selectedOptions.length === 1 && selectedOptions[0].label) || null; + onChange(groupBy); + }, + [onChange] + ); + + return ( + <EuiComboBox + placeholder={i18n.translate('xpack.infra.metricsExplorer.groupByLabel', { + defaultMessage: 'Everything', + })} + aria-label={i18n.translate('xpack.infra.metricsExplorer.groupByAriaLabel', { + defaultMessage: 'Graph per', + })} + fullWidth + singleSelection={true} + selectedOptions={(options.groupBy && [{ label: options.groupBy }]) || []} + options={fields + .filter(f => f.aggregatable && f.type === 'string') + .map(f => ({ label: f.name }))} + onChange={handleChange} + isClearable={true} + /> + ); +}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domain.ts similarity index 87% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domain.ts index 90569854b833b..811486d355f2e 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domain.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domain.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { min, max, sum, isNumber } from 'lodash'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptionsMetric } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; const getMin = (values: Array<number | null>) => { const minValue = min(values); diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts similarity index 85% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts index 4b45534d41db8..f94c6b6156ae4 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/calculate_domian.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/calculate_domian.test.ts @@ -5,9 +5,9 @@ */ import { calculateDomain } from './calculate_domain'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptionsMetric } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { MetricsExplorerColor } from '../../../../common/color_palette'; +import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; +import { MetricsExplorerColor } from '../../../../../../common/color_palette'; describe('calculateDomain()', () => { const series: MetricsExplorerSeries = { id: 'test-01', diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metric.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts similarity index 75% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metric.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts index 33ec2ce2715a3..46bd7b006446a 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metric.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metric.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -import { createFormatter } from '../../../utils/formatters'; -import { InfraFormatterType } from '../../../lib/lib'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { createFormatter } from '../../../../../../common/formatters'; +import { InfraFormatterType } from '../../../../../lib/lib'; import { metricToFormat } from './metric_to_format'; export const createFormatterForMetric = (metric?: MetricsExplorerMetric) => { if (metric && metric.field) { diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metrics.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metrics.test.ts similarity index 94% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metrics.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metrics.test.ts index ec41e90e441a4..e039d5d4b3eeb 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_formatter_for_metrics.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_formatter_for_metrics.test.ts @@ -5,7 +5,7 @@ */ import { createFormatterForMetric } from './create_formatter_for_metric'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; describe('createFormatterForMetric()', () => { it('should just work for count', () => { diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.test.ts similarity index 88% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.test.ts index cbf6904d246c7..367c472f414e4 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_metric_label.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.test.ts @@ -5,7 +5,7 @@ */ import { createMetricLabel } from './create_metric_label'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; describe('createMetricLabel()', () => { it('should work with metrics with fields', () => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts new file mode 100644 index 0000000000000..6343f2848054b --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_metric_label.ts @@ -0,0 +1,14 @@ +/* + * 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 { MetricsExplorerOptionsMetric } from '../../hooks/use_metrics_explorer_options'; + +export const createMetricLabel = (metric: MetricsExplorerOptionsMetric) => { + if (metric.label) { + return metric.label; + } + return `${metric.aggregation}(${metric.field || ''})`; +}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts similarity index 97% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index 05637642b8dd9..47bb4c8c57716 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -5,14 +5,19 @@ */ import { createTSVBLink, createFilterFromOptions } from './create_tsvb_link'; -import { source, options, timeRange, chartOptions } from '../../../utils/fixtures/metrics_explorer'; +import { + source, + options, + timeRange, + chartOptions, +} from '../../../../../utils/fixtures/metrics_explorer'; import uuid from 'uuid'; import { OutputBuffer } from 'uuid/interfaces'; import { MetricsExplorerYAxisMode, MetricsExplorerChartType, -} from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { MetricsExplorerOptions } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../hooks/use_metrics_explorer_options'; +import { MetricsExplorerOptions } from '../../hooks/use_metrics_explorer_options'; jest.mock('uuid'); const mockedUuid = uuid as jest.Mocked<typeof uuid>; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts similarity index 91% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 20706f563ec63..559422584f579 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -7,8 +7,8 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; import { set } from 'lodash'; -import { colorTransformer, MetricsExplorerColor } from '../../../../common/color_palette'; -import { MetricsExplorerSeries } from '../../../../common/http_api/metrics_explorer'; +import { colorTransformer, MetricsExplorerColor } from '../../../../../../common/color_palette'; +import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerOptionsMetric, @@ -16,12 +16,12 @@ import { MetricsExplorerChartOptions, MetricsExplorerYAxisMode, MetricsExplorerChartType, -} from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../hooks/use_metrics_explorer_options'; import { metricToFormat } from './metric_to_format'; -import { InfraFormatterType } from '../../../lib/lib'; -import { SourceQuery } from '../../../graphql/types'; +import { InfraFormatterType } from '../../../../../lib/lib'; +import { SourceQuery } from '../../../../../graphql/types'; import { createMetricLabel } from './create_metric_label'; -import { LinkDescriptor } from '../../../hooks/use_link_props'; +import { LinkDescriptor } from '../../../../../hooks/use_link_props'; export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptionsMetric) => { if (metric.aggregation === 'rate') { diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts similarity index 100% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/get_chart_theme.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.test.ts similarity index 90% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.test.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.test.ts index 4cb27b4fb65c3..6b27627a9fb5b 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.test.ts @@ -5,8 +5,8 @@ */ import { metricToFormat } from './metric_to_format'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -import { InfraFormatterType } from '../../../lib/lib'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { InfraFormatterType } from '../../../../../lib/lib'; describe('metricToFormat()', () => { it('should just work for numeric metrics', () => { const metric: MetricsExplorerMetric = { aggregation: 'avg', field: 'system.load.1' }; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.ts similarity index 82% rename from x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.ts index 63272c86a5dc7..1dbbf97a32217 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/helpers/metric_to_format.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/metric_to_format.ts @@ -5,8 +5,8 @@ */ import { last } from 'lodash'; -import { MetricsExplorerMetric } from '../../../../common/http_api/metrics_explorer'; -import { InfraFormatterType } from '../../../lib/lib'; +import { MetricsExplorerMetric } from '../../../../../../common/http_api/metrics_explorer'; +import { InfraFormatterType } from '../../../../../lib/lib'; export const metricToFormat = (metric?: MetricsExplorerMetric) => { if (metric && metric.field) { const suffix = last(metric.field.split(/\./)); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx new file mode 100644 index 0000000000000..04661bbc37702 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import React, { useEffect, useState } from 'react'; +import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; +import { AutocompleteField } from '../../../../components/autocomplete_field'; +import { esKuery, IIndexPattern } from '../../../../../../../../src/plugins/data/public'; + +interface Props { + derivedIndexPattern: IIndexPattern; + onSubmit: (query: string) => void; + onChange?: (query: string) => void; + value?: string | null; + placeholder?: string; +} + +function validateQuery(query: string) { + try { + esKuery.fromKueryExpression(query); + } catch (err) { + return false; + } + return true; +} + +export const MetricsExplorerKueryBar = ({ + derivedIndexPattern, + onSubmit, + onChange, + value, + placeholder, +}: Props) => { + const [draftQuery, setDraftQuery] = useState<string>(value || ''); + const [isValid, setValidation] = useState<boolean>(true); + + // This ensures that if value changes out side this component it will update. + useEffect(() => { + if (value) { + setDraftQuery(value); + } + }, [value]); + + const handleChange = (query: string) => { + setValidation(validateQuery(query)); + setDraftQuery(query); + if (onChange) { + onChange(query); + } + }; + + const filteredDerivedIndexPattern = { + ...derivedIndexPattern, + fields: derivedIndexPattern.fields, + }; + + const defaultPlaceholder = i18n.translate( + 'xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder', + { + defaultMessage: 'Search for infrastructure data… (e.g. host.name:host-1)', + } + ); + + return ( + <WithKueryAutocompletion indexPattern={filteredDerivedIndexPattern}> + {({ isLoadingSuggestions, loadSuggestions, suggestions }) => ( + <AutocompleteField + aria-label={placeholder} + isLoadingSuggestions={isLoadingSuggestions} + isValid={isValid} + loadSuggestions={loadSuggestions} + onChange={handleChange} + onSubmit={onSubmit} + placeholder={placeholder || defaultPlaceholder} + suggestions={suggestions} + value={draftQuery} + /> + )} + </WithKueryAutocompletion> + ); +}; diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx similarity index 85% rename from x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx index 79d4122733c55..612735e2ba772 100644 --- a/x-pack/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/metrics.tsx @@ -9,10 +9,9 @@ import { i18n } from '@kbn/i18n'; import React, { useCallback, useState } from 'react'; import { IFieldType } from 'src/plugins/data/public'; -import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; -import { MetricsExplorerMetric } from '../../../common/http_api/metrics_explorer'; -import { MetricsExplorerOptions } from '../../containers/metrics_explorer/use_metrics_explorer_options'; -import { isDisplayable } from '../../utils/is_displayable'; +import { colorTransformer, MetricsExplorerColor } from '../../../../../common/color_palette'; +import { MetricsExplorerMetric } from '../../../../../common/http_api/metrics_explorer'; +import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options'; interface Props { autoFocus?: boolean; @@ -54,9 +53,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = [onChange, options.aggregation, colors] ); - const comboOptions = fields - .filter(field => isDisplayable(field)) - .map(field => ({ label: field.name, value: field.name })); + const comboOptions = fields.map(field => ({ label: field.name, value: field.name })); const selectedOptions = options.metrics .filter(m => m.aggregation !== 'count') .map(metric => ({ diff --git a/x-pack/plugins/infra/public/components/metrics_explorer/no_metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/no_metrics.tsx similarity index 100% rename from x-pack/plugins/infra/public/components/metrics_explorer/no_metrics.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/no_metrics.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx new file mode 100644 index 0000000000000..3b84fcbc34836 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -0,0 +1,116 @@ +/* + * 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 { + ScaleType, + AreaSeries, + BarSeries, + RecursivePartial, + AreaSeriesStyle, + BarSeriesStyle, +} from '@elastic/charts'; +import { MetricsExplorerSeries } from '../../../../../common/http_api/metrics_explorer'; +import { colorTransformer, MetricsExplorerColor } from '../../../../../common/color_palette'; +import { createMetricLabel } from './helpers/create_metric_label'; +import { + MetricsExplorerOptionsMetric, + MetricsExplorerChartType, +} from '../hooks/use_metrics_explorer_options'; + +type NumberOrString = string | number; + +interface Props { + metric: MetricsExplorerOptionsMetric; + id: NumberOrString | NumberOrString[]; + series: MetricsExplorerSeries; + type: MetricsExplorerChartType; + stack: boolean; + opacity?: number; +} + +export const MetricExplorerSeriesChart = (props: Props) => { + if (MetricsExplorerChartType.bar === props.type) { + return <MetricsExplorerBarChart {...props} />; + } + return <MetricsExplorerAreaChart {...props} />; +}; + +export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opacity }: Props) => { + const color = + (metric.color && colorTransformer(metric.color)) || + colorTransformer(MetricsExplorerColor.color0); + + const yAccessors = Array.isArray(id) + ? id.map(i => `metric_${i}`).slice(id.length - 1, id.length) + : [`metric_${id}`]; + const y0Accessors = + Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined; + const chartId = `series-${series.id}-${yAccessors.join('-')}`; + + const seriesAreaStyle: RecursivePartial<AreaSeriesStyle> = { + line: { + strokeWidth: 2, + visible: true, + }, + area: { + opacity: opacity || 0.5, + visible: type === MetricsExplorerChartType.area, + }, + }; + + return ( + <AreaSeries + id={chartId} + key={chartId} + name={createMetricLabel(metric)} + xScaleType={ScaleType.Time} + yScaleType={ScaleType.Linear} + xAccessor="timestamp" + yAccessors={yAccessors} + y0Accessors={y0Accessors} + data={series.rows} + stackAccessors={stack ? ['timestamp'] : void 0} + areaSeriesStyle={seriesAreaStyle} + color={color} + /> + ); +}; + +export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => { + const color = + (metric.color && colorTransformer(metric.color)) || + colorTransformer(MetricsExplorerColor.color0); + + const yAccessor = `metric_${id}`; + const chartId = `series-${series.id}-${yAccessor}`; + + const seriesBarStyle: RecursivePartial<BarSeriesStyle> = { + rectBorder: { + stroke: color, + strokeWidth: 1, + visible: true, + }, + rect: { + opacity: 1, + }, + }; + return ( + <BarSeries + id={yAccessor} + key={chartId} + name={createMetricLabel(metric)} + xScaleType={ScaleType.Time} + yScaleType={ScaleType.Linear} + xAccessor="timestamp" + yAccessors={[yAccessor]} + data={series.rows} + stackAccessors={stack ? ['timestamp'] : void 0} + barSeriesStyle={seriesBarStyle} + color={color} + /> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx new file mode 100644 index 0000000000000..6913f67bad08a --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { IIndexPattern } from 'src/plugins/data/public'; +import { + MetricsExplorerMetric, + MetricsExplorerAggregation, +} from '../../../../../common/http_api/metrics_explorer'; +import { + MetricsExplorerOptions, + MetricsExplorerTimeOptions, + MetricsExplorerChartOptions, +} from '../hooks/use_metrics_explorer_options'; +import { MetricsExplorerKueryBar } from './kuery_bar'; +import { MetricsExplorerMetrics } from './metrics'; +import { MetricsExplorerGroupBy } from './group_by'; +import { MetricsExplorerAggregationPicker } from './aggregation'; +import { MetricsExplorerChartOptions as MetricsExplorerChartOptionsComponent } from './chart_options'; +import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state'; +import { metricsExplorerViewSavedObjectType } from '../../../../../common/saved_objects/metrics_explorer_view'; +import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; +import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; + +interface Props { + derivedIndexPattern: IIndexPattern; + timeRange: MetricsExplorerTimeOptions; + options: MetricsExplorerOptions; + chartOptions: MetricsExplorerChartOptions; + defaultViewState: MetricExplorerViewState; + onRefresh: () => void; + onTimeChange: (start: string, end: string) => void; + onGroupByChange: (groupBy: string | null) => void; + onFilterQuerySubmit: (query: string) => void; + onMetricsChange: (metrics: MetricsExplorerMetric[]) => void; + onAggregationChange: (aggregation: MetricsExplorerAggregation) => void; + onChartOptionsChange: (chartOptions: MetricsExplorerChartOptions) => void; + onViewStateChange: (vs: MetricExplorerViewState) => void; +} + +export const MetricsExplorerToolbar = ({ + timeRange, + derivedIndexPattern, + options, + onTimeChange, + onRefresh, + onGroupByChange, + onFilterQuerySubmit, + onMetricsChange, + onAggregationChange, + chartOptions, + onChartOptionsChange, + defaultViewState, + onViewStateChange, +}: Props) => { + const isDefaultOptions = options.aggregation === 'avg' && options.metrics.length === 0; + const [timepickerQuickRanges] = useKibanaUiSetting('timepicker:quickRanges'); + const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); + + return ( + <ToolbarPanel> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem grow={options.aggregation === 'count' ? 2 : false}> + <MetricsExplorerAggregationPicker + fullWidth + options={options} + onChange={onAggregationChange} + /> + </EuiFlexItem> + {options.aggregation !== 'count' && ( + <EuiText size="s" color="subdued"> + <FormattedMessage + id="xpack.infra.metricsExplorer.aggregationLabel" + defaultMessage="of" + /> + </EuiText> + )} + {options.aggregation !== 'count' && ( + <EuiFlexItem grow={2}> + <MetricsExplorerMetrics + autoFocus={isDefaultOptions} + fields={derivedIndexPattern.fields} + options={options} + onChange={onMetricsChange} + /> + </EuiFlexItem> + )} + <EuiText size="s" color="subdued"> + <FormattedMessage + id="xpack.infra.metricsExplorer.groupByToolbarLabel" + defaultMessage="graph per" + /> + </EuiText> + <EuiFlexItem grow={1}> + <MetricsExplorerGroupBy + onChange={onGroupByChange} + fields={derivedIndexPattern.fields} + options={options} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem> + <MetricsExplorerKueryBar + derivedIndexPattern={derivedIndexPattern} + onSubmit={onFilterQuerySubmit} + value={options.filterQuery} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <MetricsExplorerChartOptionsComponent + onChange={onChartOptionsChange} + chartOptions={chartOptions} + /> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <SavedViewsToolbarControls + defaultViewState={defaultViewState} + viewState={{ + options, + chartOptions, + currentTimerange: timeRange, + }} + viewType={metricsExplorerViewSavedObjectType} + onViewChange={onViewStateChange} + /> + </EuiFlexItem> + <EuiFlexItem grow={false} style={{ marginRight: 5 }}> + <EuiSuperDatePicker + start={timeRange.from} + end={timeRange.to} + onTimeChange={({ start, end }) => onTimeChange(start, end)} + onRefresh={onRefresh} + commonlyUsedRanges={commonlyUsedRanges} + /> + </EuiFlexItem> + </EuiFlexGroup> + </ToolbarPanel> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx index 874ac0987023b..f0734f76cfacd 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.test.tsx @@ -6,14 +6,14 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useMetricsExplorerState } from './use_metric_explorer_state'; -import { MetricsExplorerOptionsContainer } from '../../../containers/metrics_explorer/use_metrics_explorer_options'; +import { MetricsExplorerOptionsContainer } from './use_metrics_explorer_options'; import React from 'react'; import { source, derivedIndexPattern, resp, createSeries, -} from '../../../utils/fixtures/metrics_explorer'; +} from '../../../../utils/fixtures/metrics_explorer'; const renderUseMetricsExplorerStateHook = () => renderHook(props => useMetricsExplorerState(props.source, props.derivedIndexPattern), { @@ -27,7 +27,7 @@ const renderUseMetricsExplorerStateHook = () => const mockedUseMetricsExplorerData = jest.fn(); -jest.mock('../../../containers/metrics_explorer/use_metrics_explorer_data', () => { +jest.mock('./use_metrics_explorer_data', () => { return { useMetricsExplorerData: () => { return mockedUseMetricsExplorerData(); diff --git a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts similarity index 92% rename from x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index 88e6d9d800661..8a9ed901de0b0 100644 --- a/x-pack/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -9,15 +9,15 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { MetricsExplorerMetric, MetricsExplorerAggregation, -} from '../../../../common/http_api/metrics_explorer'; -import { useMetricsExplorerData } from '../../../containers/metrics_explorer/use_metrics_explorer_data'; +} from '../../../../../common/http_api/metrics_explorer'; +import { useMetricsExplorerData } from './use_metrics_explorer_data'; import { MetricsExplorerOptionsContainer, MetricsExplorerChartOptions, MetricsExplorerTimeOptions, MetricsExplorerOptions, -} from '../../../containers/metrics_explorer/use_metrics_explorer_options'; -import { SourceQuery } from '../../../graphql/types'; +} from './use_metrics_explorer_options'; +import { SourceQuery } from '../../../../graphql/types'; export interface MetricExplorerViewState { chartOptions: MetricsExplorerChartOptions; diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx similarity index 96% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx index bbc8778545b4a..94edab54fb71e 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { useMetricsExplorerData } from './use_metrics_explorer_data'; import { renderHook } from '@testing-library/react-hooks'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { options, @@ -17,7 +17,7 @@ import { timeRange, resp, createSeries, -} from '../../utils/fixtures/metrics_explorer'; +} from '../../../../utils/fixtures/metrics_explorer'; const mockedFetch = jest.fn(); @@ -54,7 +54,7 @@ const renderUseMetricsExplorerDataHook = () => { ); }; -jest.mock('../../utils/kuery', () => { +jest.mock('../../../../utils/kuery', () => { return { convertKueryToElasticSearchQuery: (query: string) => query, }; diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts similarity index 81% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index b32496fbf30a1..414e204f3df50 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -7,16 +7,17 @@ import DateMath from '@elastic/datemath'; import { isEqual } from 'lodash'; import { useEffect, useState } from 'react'; +import { HttpHandler } from 'target/types/core/public/http'; import { IIndexPattern } from 'src/plugins/data/public'; -import { SourceQuery } from '../../../common/graphql/types'; +import { SourceQuery } from '../../../../../common/graphql/types'; import { MetricsExplorerResponse, metricsExplorerResponseRT, -} from '../../../common/http_api/metrics_explorer'; -import { convertKueryToElasticSearchQuery } from '../../utils/kuery'; +} from '../../../../../common/http_api/metrics_explorer'; +import { convertKueryToElasticSearchQuery } from '../../../../utils/kuery'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions } from './use_metrics_explorer_options'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; -import { decodeOrThrow } from '../../../common/runtime_types'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; function isSameOptions(current: MetricsExplorerOptions, next: MetricsExplorerOptions) { return isEqual(current, next); @@ -24,13 +25,15 @@ function isSameOptions(current: MetricsExplorerOptions, next: MetricsExplorerOpt export function useMetricsExplorerData( options: MetricsExplorerOptions, - source: SourceQuery.Query['source']['configuration'], + source: SourceQuery.Query['source']['configuration'] | undefined, derivedIndexPattern: IIndexPattern, timerange: MetricsExplorerTimeOptions, afterKey: string | null, - signal: any + signal: any, + fetch?: HttpHandler ) { - const fetch = useKibana().services.http?.fetch; + const kibana = useKibana(); + const fetchFn = fetch ? fetch : kibana.services.http?.fetch; const [error, setError] = useState<Error | null>(null); const [loading, setLoading] = useState<boolean>(true); const [data, setData] = useState<MetricsExplorerResponse | null>(null); @@ -46,13 +49,17 @@ export function useMetricsExplorerData( if (!from || !to) { throw new Error('Unalble to parse timerange'); } - if (!fetch) { + if (!fetchFn) { throw new Error('HTTP service is unavailable'); } + if (!source) { + throw new Error('Source is unavailable'); + } const response = decodeOrThrow(metricsExplorerResponseRT)( - await fetch('/api/infra/metrics_explorer', { + await fetchFn('/api/infra/metrics_explorer', { method: 'POST', body: JSON.stringify({ + forceInterval: options.forceInterval, metrics: options.aggregation === 'count' ? [{ aggregation: 'count' }] diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx similarity index 100% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.test.tsx rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.test.tsx diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts similarity index 95% rename from x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts rename to x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts index 2b802af8e8c15..1b3e809fde61f 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options.ts @@ -6,11 +6,11 @@ import createContainer from 'constate'; import { useState, useEffect, Dispatch, SetStateAction } from 'react'; -import { MetricsExplorerColor } from '../../../common/color_palette'; +import { MetricsExplorerColor } from '../../../../../common/color_palette'; import { MetricsExplorerAggregation, MetricsExplorerMetric, -} from '../../../common/http_api/metrics_explorer'; +} from '../../../../../common/http_api/metrics_explorer'; export type MetricsExplorerOptionsMetric = MetricsExplorerMetric & { color?: MetricsExplorerColor; @@ -40,6 +40,7 @@ export interface MetricsExplorerOptions { groupBy?: string; filterQuery?: string; aggregation: MetricsExplorerAggregation; + forceInterval?: boolean; } export interface MetricsExplorerTimeOptions { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx new file mode 100644 index 0000000000000..a213671e9436e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -0,0 +1,100 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import React from 'react'; +import { IIndexPattern } from 'src/plugins/data/public'; +import { DocumentTitle } from '../../../components/document_title'; +import { MetricsExplorerCharts } from './components/charts'; +import { MetricsExplorerToolbar } from './components/toolbar'; +import { SourceQuery } from '../../../../common/graphql/types'; +import { NoData } from '../../../components/empty_states'; +import { useMetricsExplorerState } from './hooks/use_metric_explorer_state'; +import { useTrackPageview } from '../../../../../observability/public'; + +interface MetricsExplorerPageProps { + source: SourceQuery.Query['source']['configuration']; + derivedIndexPattern: IIndexPattern; +} + +export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { + const { + loading, + error, + data, + currentTimerange, + options, + chartOptions, + setChartOptions, + handleAggregationChange, + handleMetricsChange, + handleFilterQuerySubmit, + handleGroupByChange, + handleTimeChange, + handleRefresh, + handleLoadMore, + defaultViewState, + onViewStateChange, + } = useMetricsExplorerState(source, derivedIndexPattern); + + useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' }); + useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); + + return ( + <React.Fragment> + <DocumentTitle + title={(previousTitle: string) => + i18n.translate('xpack.infra.infrastructureMetricsExplorerPage.documentTitle', { + defaultMessage: '{previousTitle} | Metrics Explorer', + values: { + previousTitle, + }, + }) + } + /> + <MetricsExplorerToolbar + derivedIndexPattern={derivedIndexPattern} + timeRange={currentTimerange} + options={options} + chartOptions={chartOptions} + onRefresh={handleRefresh} + onTimeChange={handleTimeChange} + onGroupByChange={handleGroupByChange} + onFilterQuerySubmit={handleFilterQuerySubmit} + onMetricsChange={handleMetricsChange} + onAggregationChange={handleAggregationChange} + onChartOptionsChange={setChartOptions} + defaultViewState={defaultViewState} + onViewStateChange={onViewStateChange} + /> + {error ? ( + <NoData + titleText="Whoops!" + bodyText={i18n.translate('xpack.infra.metricsExplorer.errorMessage', { + defaultMessage: 'It looks like the request failed with "{message}"', + values: { message: error.message }, + })} + onRefetch={handleRefresh} + refetchText="Try Again" + /> + ) : ( + <MetricsExplorerCharts + timeRange={currentTimerange} + loading={loading} + data={data} + source={source} + options={options} + chartOptions={chartOptions} + onLoadMore={handleLoadMore} + onFilter={handleFilterQuerySubmit} + onRefetch={handleRefresh} + onTimeChange={handleTimeChange} + /> + )} + </React.Fragment> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/infrastructure/settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings.tsx similarity index 100% rename from x-pack/plugins/infra/public/pages/infrastructure/settings.tsx rename to x-pack/plugins/infra/public/pages/metrics/settings.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/types.ts b/x-pack/plugins/infra/public/pages/metrics/types.ts deleted file mode 100644 index 2cc261df28977..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/types.ts +++ /dev/null @@ -1,62 +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 rt from 'io-ts'; -import { EuiTheme } from '../../../../observability/public'; -import { InventoryFormatterTypeRT } from '../../../common/inventory_models/types'; -import { MetricsTimeInput } from './hooks/use_metrics_time'; -import { NodeDetailsMetricData } from '../../../common/http_api/node_details_api'; - -export interface LayoutProps { - metrics?: NodeDetailsMetricData[]; - onChangeRangeTime?: (time: MetricsTimeInput) => void; - isLiveStreaming?: boolean; - stopLiveStreaming?: () => void; -} - -export type LayoutPropsWithTheme = LayoutProps & { theme: EuiTheme }; - -const ChartTypesRT = rt.keyof({ - area: null, - bar: null, - line: null, -}); - -export const SeriesOverridesObjectRT = rt.intersection([ - rt.type({ - color: rt.string, - }), - rt.partial({ - name: rt.string, - formatter: InventoryFormatterTypeRT, - formatterTemplate: rt.string, - gaugeMax: rt.number, - type: ChartTypesRT, - }), -]); - -export const SeriesOverridesRT = rt.record( - rt.string, - rt.union([rt.undefined, SeriesOverridesObjectRT]) -); - -export type SeriesOverrides = rt.TypeOf<typeof SeriesOverridesRT>; - -export const VisSectionPropsRT = rt.partial({ - type: ChartTypesRT, - stacked: rt.boolean, - formatter: InventoryFormatterTypeRT, - formatterTemplate: rt.string, - seriesOverrides: SeriesOverridesRT, -}); - -export type VisSectionProps = rt.TypeOf<typeof VisSectionPropsRT> & { - id?: string; - metric?: NodeDetailsMetricData; - onChangeRangeTime?: (time: MetricsTimeInput) => void; - isLiveStreaming?: boolean; - stopLiveStreaming?: () => void; -}; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 3b6647b9bfbbe..d61ef7fc4a631 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -21,7 +21,9 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/p import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; -import { getAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; +import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; +import { getInventoryMetricAlertType } from './components/alerting/inventory/metric_inventory_threshold_alert_type'; +import { createMetricThresholdAlertType } from './alerting/metric_threshold'; export type ClientSetup = void; export type ClientStart = void; @@ -52,7 +54,9 @@ export class Plugin setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) { registerFeatures(pluginsSetup.home); - pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getInventoryMetricAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(createMetricThresholdAlertType()); core.application.register({ id: 'logs', diff --git a/x-pack/plugins/infra/public/routers/metrics_router.tsx b/x-pack/plugins/infra/public/routers/metrics_router.tsx index 7cb9de65e7291..0e427150a46cc 100644 --- a/x-pack/plugins/infra/public/routers/metrics_router.tsx +++ b/x-pack/plugins/infra/public/routers/metrics_router.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { Route, Router, Switch } from 'react-router-dom'; import { NotFoundPage } from '../pages/404'; -import { InfrastructurePage } from '../pages/infrastructure'; -import { LinkToMetricsPage } from '../pages/link_to'; -import { MetricDetail } from '../pages/metrics'; +import { InfrastructurePage } from '../pages/metrics'; +import { MetricDetail } from '../pages/metrics/metric_detail'; import { RedirectWithQueryParams } from '../utils/redirect_with_query_params'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { AppRouter } from './index'; +import { LinkToMetricsPage } from '../pages/link_to'; export const MetricsRouter: AppRouter = ({ history }) => { const uiCapabilities = useKibana().services.application?.capabilities; diff --git a/x-pack/plugins/infra/public/utils/datemath.test.ts b/x-pack/plugins/infra/public/utils/datemath.test.ts index 0f272733c5f97..c8fbe5583db2e 100644 --- a/x-pack/plugins/infra/public/utils/datemath.test.ts +++ b/x-pack/plugins/infra/public/utils/datemath.test.ts @@ -198,7 +198,7 @@ describe('extendDatemath()', () => { }); }); - describe('with a positive Operator', () => { + describe('with a positive operator', () => { it('Halves miliseconds', () => { expect(extendDatemath('now+250ms')).toEqual({ value: 'now+125ms', @@ -307,6 +307,274 @@ describe('extendDatemath()', () => { }); }); }); + + describe('moving after', () => { + describe('with a negative operator', () => { + it('Halves miliseconds', () => { + expect(extendDatemath('now-250ms', 'after')).toEqual({ + value: 'now-125ms', + diffAmount: 125, + diffUnit: 'ms', + }); + }); + + it('Halves seconds', () => { + expect(extendDatemath('now-10s', 'after')).toEqual({ + value: 'now-5s', + diffAmount: 5, + diffUnit: 's', + }); + }); + + it('Halves minutes when the amount is low', () => { + expect(extendDatemath('now-2m', 'after')).toEqual({ + value: 'now-1m', + diffAmount: 1, + diffUnit: 'm', + }); + expect(extendDatemath('now-4m', 'after')).toEqual({ + value: 'now-2m', + diffAmount: 2, + diffUnit: 'm', + }); + expect(extendDatemath('now-6m', 'after')).toEqual({ + value: 'now-3m', + diffAmount: 3, + diffUnit: 'm', + }); + }); + + it('advances minutes in half ammounts when the amount is high', () => { + expect(extendDatemath('now-30m', 'after')).toEqual({ + value: 'now-20m', + diffAmount: 10, + diffUnit: 'm', + }); + }); + + it('advances half an hour when the amount is one hour', () => { + expect(extendDatemath('now-1h', 'after')).toEqual({ + value: 'now-30m', + diffAmount: 30, + diffUnit: 'm', + }); + }); + + it('advances one hour when the amount is one day', () => { + expect(extendDatemath('now-1d', 'after')).toEqual({ + value: 'now-23h', + diffAmount: 1, + diffUnit: 'h', + }); + }); + + it('advances one day when the amount is more than one day', () => { + expect(extendDatemath('now-2d', 'after')).toEqual({ + value: 'now-1d', + diffAmount: 1, + diffUnit: 'd', + }); + expect(extendDatemath('now-3d', 'after')).toEqual({ + value: 'now-2d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('advances one day when the amount is one week', () => { + expect(extendDatemath('now-1w', 'after')).toEqual({ + value: 'now-6d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('advances one week when the amount is more than one week', () => { + expect(extendDatemath('now-2w', 'after')).toEqual({ + value: 'now-1w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('advances one week when the amount is one month', () => { + expect(extendDatemath('now-1M', 'after')).toEqual({ + value: 'now-3w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('advances one month when the amount is more than one month', () => { + expect(extendDatemath('now-2M', 'after')).toEqual({ + value: 'now-1M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('advances one month when the amount is one year', () => { + expect(extendDatemath('now-1y', 'after')).toEqual({ + value: 'now-11M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('advances one year when the amount is in years', () => { + expect(extendDatemath('now-2y', 'after')).toEqual({ + value: 'now-1y', + diffAmount: 1, + diffUnit: 'y', + }); + }); + }); + + describe('with a positive operator', () => { + it('doubles miliseconds', () => { + expect(extendDatemath('now+250ms', 'after')).toEqual({ + value: 'now+500ms', + diffAmount: 250, + diffUnit: 'ms', + }); + }); + + it('normalizes miliseconds', () => { + expect(extendDatemath('now+500ms', 'after')).toEqual({ + value: 'now+1s', + diffAmount: 500, + diffUnit: 'ms', + }); + }); + + it('doubles seconds', () => { + expect(extendDatemath('now+10s', 'after')).toEqual({ + value: 'now+20s', + diffAmount: 10, + diffUnit: 's', + }); + }); + + it('normalizes seconds', () => { + expect(extendDatemath('now+30s', 'after')).toEqual({ + value: 'now+1m', + diffAmount: 30, + diffUnit: 's', + }); + }); + + it('doubles minutes when amount is low', () => { + expect(extendDatemath('now+1m', 'after')).toEqual({ + value: 'now+2m', + diffAmount: 1, + diffUnit: 'm', + }); + expect(extendDatemath('now+2m', 'after')).toEqual({ + value: 'now+4m', + diffAmount: 2, + diffUnit: 'm', + }); + expect(extendDatemath('now+3m', 'after')).toEqual({ + value: 'now+6m', + diffAmount: 3, + diffUnit: 'm', + }); + }); + + it('adds half the minutes when the amount is high', () => { + expect(extendDatemath('now+20m', 'after')).toEqual({ + value: 'now+30m', + diffAmount: 10, + diffUnit: 'm', + }); + }); + + it('Adds half an hour when the amount is one hour', () => { + expect(extendDatemath('now+1h', 'after')).toEqual({ + value: 'now+90m', + diffAmount: 30, + diffUnit: 'm', + }); + }); + + it('Adds one hour when the amount more than one hour', () => { + expect(extendDatemath('now+2h', 'after')).toEqual({ + value: 'now+3h', + diffAmount: 1, + diffUnit: 'h', + }); + }); + + it('Adds one hour when the amount is one day', () => { + expect(extendDatemath('now+1d', 'after')).toEqual({ + value: 'now+25h', + diffAmount: 1, + diffUnit: 'h', + }); + }); + + it('Adds one day when the amount is more than one day', () => { + expect(extendDatemath('now+2d', 'after')).toEqual({ + value: 'now+3d', + diffAmount: 1, + diffUnit: 'd', + }); + expect(extendDatemath('now+3d', 'after')).toEqual({ + value: 'now+4d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('Adds one day when the amount is one week', () => { + expect(extendDatemath('now+1w', 'after')).toEqual({ + value: 'now+8d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('Adds one week when the amount is more than one week', () => { + expect(extendDatemath('now+2w', 'after')).toEqual({ + value: 'now+3w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('Adds one week when the amount is one month', () => { + expect(extendDatemath('now+1M', 'after')).toEqual({ + value: 'now+5w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('Adds one month when the amount is more than one month', () => { + expect(extendDatemath('now+2M', 'after')).toEqual({ + value: 'now+3M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('Adds one month when the amount is one year', () => { + expect(extendDatemath('now+1y', 'after')).toEqual({ + value: 'now+13M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('Adds one year when the amount is in years', () => { + expect(extendDatemath('now+2y', 'after')).toEqual({ + value: 'now+3y', + diffAmount: 1, + diffUnit: 'y', + }); + }); + }); + }); }); describe('convertDate()', () => { diff --git a/x-pack/plugins/infra/public/utils/datemath.ts b/x-pack/plugins/infra/public/utils/datemath.ts index 50a9b6e4f6945..7331a2450956f 100644 --- a/x-pack/plugins/infra/public/utils/datemath.ts +++ b/x-pack/plugins/infra/public/utils/datemath.ts @@ -68,7 +68,8 @@ function extendRelativeDatemath( return undefined; } - const mustIncreaseAmount = operator === '-' && direction === 'before'; + const mustIncreaseAmount = + (operator === '-' && direction === 'before') || (operator === '+' && direction === 'after'); const parsedAmount = parseInt(amount, 10); let newUnit: Unit = unit as Unit; let newAmount: number; diff --git a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts index e39d6f785d53e..15159ad45c7f3 100644 --- a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts +++ b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts @@ -14,7 +14,7 @@ import { MetricsExplorerChartType, MetricsExplorerYAxisMode, MetricsExplorerChartOptions, -} from '../../containers/metrics_explorer/use_metrics_explorer_options'; +} from '../../pages/metrics/metrics_explorer/hooks/use_metrics_explorer_options'; export const options: MetricsExplorerOptions = { limit: 3, diff --git a/x-pack/plugins/infra/public/utils/formatters/bytes.ts b/x-pack/plugins/infra/public/utils/formatters/bytes.ts deleted file mode 100644 index 80a5603ed6994..0000000000000 --- a/x-pack/plugins/infra/public/utils/formatters/bytes.ts +++ /dev/null @@ -1,52 +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 { InfraWaffleMapDataFormat } from '../../lib/lib'; -import { formatNumber } from './number'; - -/** - * The labels are derived from these two Wikipedia articles. - * https://en.wikipedia.org/wiki/Kilobit - * https://en.wikipedia.org/wiki/Kilobyte - */ -const LABELS = { - [InfraWaffleMapDataFormat.bytesDecimal]: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'], - [InfraWaffleMapDataFormat.bitsDecimal]: [ - 'bit', - 'kbit', - 'Mbit', - 'Gbit', - 'Tbit', - 'Pbit', - 'Ebit', - 'Zbit', - 'Ybit', - ], - [InfraWaffleMapDataFormat.abbreviatedNumber]: ['', 'K', 'M', 'B', 'T'], -}; - -const BASES = { - [InfraWaffleMapDataFormat.bytesDecimal]: 1000, - [InfraWaffleMapDataFormat.bitsDecimal]: 1000, - [InfraWaffleMapDataFormat.abbreviatedNumber]: 1000, -}; - -/* - * This formatter always assumes you're input is bytes and the output is a string - * in whatever format you've defined. Bytes in Format Out. - */ -export const createBytesFormatter = (format: InfraWaffleMapDataFormat) => (bytes: number) => { - const labels = LABELS[format]; - const base = BASES[format]; - const value = format === InfraWaffleMapDataFormat.bitsDecimal ? bytes * 8 : bytes; - // Use an exponetial equation to get the power to determine which label to use. If the power - // is greater then the max label then use the max label. - const power = Math.min(Math.floor(Math.log(Math.abs(value)) / Math.log(base)), labels.length - 1); - if (power < 0) { - return `${formatNumber(value)}${labels[0]}`; - } - return `${formatNumber(value / Math.pow(base, power))}${labels[power]}`; -}; diff --git a/x-pack/plugins/infra/public/utils/formatters/index.ts b/x-pack/plugins/infra/public/utils/formatters/index.ts deleted file mode 100644 index 3c60dba747825..0000000000000 --- a/x-pack/plugins/infra/public/utils/formatters/index.ts +++ /dev/null @@ -1,37 +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 Mustache from 'mustache'; -import { InfraWaffleMapDataFormat } from '../../lib/lib'; -import { createBytesFormatter } from './bytes'; -import { formatNumber } from './number'; -import { formatPercent } from './percent'; -import { InventoryFormatterType } from '../../../common/inventory_models/types'; -import { formatHighPercision } from './high_precision'; - -export const FORMATTERS = { - number: formatNumber, - // Because the implimentation for formatting large numbers is the same as formatting - // bytes we are re-using the same code, we just format the number using the abbreviated number format. - abbreviatedNumber: createBytesFormatter(InfraWaffleMapDataFormat.abbreviatedNumber), - // bytes in bytes formatted string out - bytes: createBytesFormatter(InfraWaffleMapDataFormat.bytesDecimal), - // bytes in bits formatted string out - bits: createBytesFormatter(InfraWaffleMapDataFormat.bitsDecimal), - percent: formatPercent, - highPercision: formatHighPercision, -}; - -export const createFormatter = (format: InventoryFormatterType, template: string = '{{value}}') => ( - val: string | number -) => { - if (val == null) { - return ''; - } - const fmtFn = FORMATTERS[format]; - const value = fmtFn(Number(val)); - return Mustache.render(template, { value }); -}; diff --git a/x-pack/plugins/infra/public/utils/history_context.ts b/x-pack/plugins/infra/public/utils/history_context.ts index fe036e3179ec1..844d5b5e8e76f 100644 --- a/x-pack/plugins/infra/public/utils/history_context.ts +++ b/x-pack/plugins/infra/public/utils/history_context.ts @@ -5,9 +5,9 @@ */ import { createContext, useContext } from 'react'; -import { History } from 'history'; +import { ScopedHistory } from 'src/core/public'; -export const HistoryContext = createContext<History | undefined>(undefined); +export const HistoryContext = createContext<ScopedHistory | undefined>(undefined); export const useHistory = () => { return useContext(HistoryContext); diff --git a/x-pack/plugins/infra/public/utils/is_displayable.test.ts b/x-pack/plugins/infra/public/utils/is_displayable.test.ts deleted file mode 100644 index ebd5c07327e9b..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.test.ts +++ /dev/null @@ -1,65 +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 { isDisplayable } from './is_displayable'; - -describe('isDisplayable()', () => { - test('field that is not displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: false, - }; - expect(isDisplayable(field)).toBe(false); - }); - test('field that is displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that an ecs field', () => { - const field = { - name: '@timestamp', - type: 'date', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that matches same prefix', () => { - const field = { - name: 'system.network.name', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(true); - }); - test('field that does not matches same prefix', () => { - const field = { - name: 'system.load.1', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(false); - }); - test('field that is an K8s allowed field but does not match prefix', () => { - const field = { - name: 'kubernetes.namespace', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['kubernetes.pod'])).toBe(true); - }); - test('field that is a Prometheus allowed field but does not match prefix', () => { - const field = { - name: 'prometheus.labels.foo.bar', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['prometheus.metrics'])).toBe(true); - }); -}); diff --git a/x-pack/plugins/infra/public/utils/is_displayable.ts b/x-pack/plugins/infra/public/utils/is_displayable.ts deleted file mode 100644 index 534282e807036..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.ts +++ /dev/null @@ -1,30 +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 { IFieldType } from 'src/plugins/data/public'; -import { startsWith, uniq } from 'lodash'; -import { getAllowedListForPrefix } from '../../common/ecs_allowed_list'; - -interface DisplayableFieldType extends IFieldType { - displayable?: boolean; -} - -const fieldStartsWith = (field: DisplayableFieldType) => (name: string) => - startsWith(field.name, name); - -export const isDisplayable = (field: DisplayableFieldType, additionalPrefixes: string[] = []) => { - // We need to start with at least one prefix, even if it's empty - const prefixes = additionalPrefixes && additionalPrefixes.length ? additionalPrefixes : ['']; - // Create a set of allowed list based on the prefixes - const allowedList = prefixes.reduce((acc, prefix) => { - return uniq([...acc, ...getAllowedListForPrefix(prefix)]); - }, [] as string[]); - // If the field is displayable and part of the allowed list or covered by the prefix - return ( - (field.displayable && prefixes.some(fieldStartsWith(field))) || - allowedList.some(fieldStartsWith(field)) - ); -}; diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx new file mode 100644 index 0000000000000..10f8fb9e71f43 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { createContext, useContext } from 'react'; + +interface ContextValues { + prompt?: string; + setPrompt: (prompt: string | undefined) => void; +} + +export const NavigationWarningPromptContext = createContext<ContextValues>({ + setPrompt: (prompt: string | undefined) => {}, +}); + +export const useNavigationWarningPrompt = () => { + return useContext(NavigationWarningPromptContext); +}; + +export const NavigationWarningPromptProvider: React.FC = ({ children }) => { + const [prompt, setPrompt] = useState<string | undefined>(undefined); + + return ( + <NavigationWarningPromptContext.Provider value={{ prompt, setPrompt }}> + {children} + </NavigationWarningPromptContext.Provider> + ); +}; diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts new file mode 100644 index 0000000000000..dcdbf8e912a83 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './context'; +export * from './prompt'; diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx new file mode 100644 index 0000000000000..65ec4729c036d --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useNavigationWarningPrompt } from './context'; + +interface Props { + prompt?: string; +} + +export const Prompt: React.FC<Props> = ({ prompt }) => { + const { setPrompt } = useNavigationWarningPrompt(); + + useEffect(() => { + setPrompt(prompt); + return () => { + setPrompt(undefined); + }; + }, [prompt, setPrompt]); + + return null; +}; diff --git a/x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts b/x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts new file mode 100644 index 0000000000000..ddaf8fcd31a39 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts @@ -0,0 +1,39 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { throttle } from 'lodash'; + +interface ViewportDimensions { + width: number; + height: number; +} + +const getViewportWidth = () => + window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; +const getViewportHeight = () => + window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; + +export function useViewportDimensions(): ViewportDimensions { + const [dimensions, setDimensions] = useState<ViewportDimensions>({ + width: getViewportWidth(), + height: getViewportHeight(), + }); + + useEffect(() => { + const updateDimensions = throttle(() => { + setDimensions({ + width: getViewportWidth(), + height: getViewportHeight(), + }); + }, 250); + + window.addEventListener('resize', updateDimensions); + return () => window.removeEventListener('resize', updateDimensions); + }, []); + + return dimensions; +} diff --git a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts index 1fe1431392a38..cffab4ba4f6f0 100644 --- a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts @@ -93,12 +93,17 @@ export const createSourcesResolvers = ( } => ({ Query: { async source(root, args, { req }) { - const requestedSourceConfiguration = await libs.sources.getSourceConfiguration(req, args.id); + const requestedSourceConfiguration = await libs.sources.getSourceConfiguration( + req.core.savedObjects.client, + args.id + ); return requestedSourceConfiguration; }, async allSources(root, args, { req }) { - const sourceConfigurations = await libs.sources.getAllSourceConfigurations(req); + const sourceConfigurations = await libs.sources.getAllSourceConfigurations( + req.core.savedObjects.client + ); return sourceConfigurations; }, @@ -128,7 +133,7 @@ export const createSourcesResolvers = ( Mutation: { async createSource(root, args, { req }) { const sourceConfiguration = await libs.sources.createSourceConfiguration( - req, + req.core.savedObjects.client, args.id, compactObject({ ...args.sourceProperties, @@ -144,7 +149,7 @@ export const createSourcesResolvers = ( }; }, async deleteSource(root, args, { req }) { - await libs.sources.deleteSourceConfiguration(req, args.id); + await libs.sources.deleteSourceConfiguration(req.core.savedObjects.client, args.id); return { id: args.id, @@ -152,7 +157,7 @@ export const createSourcesResolvers = ( }, async updateSource(root, args, { req }) { const updatedSourceConfiguration = await libs.sources.updateSourceConfiguration( - req, + req.core.savedObjects.client, args.id, compactObject({ ...args.sourceProperties, diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 88b78dfd3e41c..4ed30380dc164 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -29,6 +29,7 @@ import { initLogEntriesItemRoute, } from './routes/log_entries'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; +import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources'; import { initSourceRoute } from './routes/source'; export const initInfraServer = (libs: InfraBackendLibs) => { @@ -59,4 +60,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initMetricExplorerRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); + initLogSourceConfigurationRoutes(libs); + initLogSourceStatusRoutes(libs); }; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 038fd457fb6c7..4bbbf8dcdee03 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -11,7 +11,7 @@ import { RouteMethod, RouteConfig } from '../../../../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { VisTypeTimeseriesSetup } from '../../../../../../../src/plugins/vis_type_timeseries/server'; -import { APMPluginContract } from '../../../../../../plugins/apm/server'; +import { APMPluginSetup } from '../../../../../../plugins/apm/server'; import { HomeServerPluginSetup } from '../../../../../../../src/plugins/home/server'; import { PluginSetupContract as AlertingPluginContract } from '../../../../../../plugins/alerting/server'; @@ -22,7 +22,7 @@ export interface InfraServerPluginDeps { usageCollection: UsageCollectionSetup; visTypeTimeseries: VisTypeTimeseriesSetup; features: FeaturesPluginSetup; - apm: APMPluginContract; + apm: APMPluginSetup; alerting: AlertingPluginContract; } diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index eda1fbfa5f4ce..eed7d39b8e74a 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -70,6 +70,9 @@ export class KibanaFramework { case 'put': this.router.put(routeConfig, handler); break; + case 'patch': + this.router.patch(routeConfig, handler); + break; } } diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 5a5f9d0f8f529..62f324e01f8d9 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -18,6 +18,7 @@ import { InventoryMetricRT, } from '../../../../common/inventory_models/types'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; +import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { private framework: KibanaFramework; @@ -120,9 +121,14 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { indexPattern, options.timerange.interval ); + + const client = <Hit = {}, Aggregation = undefined>( + opts: CallWithRequestParams + ): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> => + this.framework.callWithRequest(requestContext, 'search', opts); + const calculatedInterval = await calculateMetricInterval( - this.framework, - requestContext, + client, { indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts new file mode 100644 index 0000000000000..cc8a35f6e47a1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -0,0 +1,214 @@ +/* + * 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 { mapValues, last, get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { + InfraDatabaseSearchResponse, + CallWithRequestParams, +} from '../../adapters/framework/adapter_types'; +import { Comparator, AlertStates, InventoryMetricConditions } from './types'; +import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; +import { InfraSnapshot } from '../../snapshot'; +import { parseFilterQuery } from '../../../utils/serialized_query'; +import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; +import { InfraSourceConfiguration } from '../../sources'; +import { InfraBackendLibs } from '../../infra_types'; +import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats'; +import { createFormatter } from '../../../../common/formatters'; + +interface InventoryMetricThresholdParams { + criteria: InventoryMetricConditions[]; + groupBy: string | undefined; + filterQuery: string | undefined; + nodeType: InventoryItemType; + sourceId?: string; +} + +export const createInventoryMetricThresholdExecutor = ( + libs: InfraBackendLibs, + alertId: string +) => async ({ services, params }: AlertExecutorOptions) => { + const { criteria, filterQuery, sourceId, nodeType } = params as InventoryMetricThresholdParams; + + const source = await libs.sources.getSourceConfiguration( + services.savedObjectsClient, + sourceId || 'default' + ); + + const results = await Promise.all( + criteria.map(c => evaluateCondtion(c, nodeType, source.configuration, services, filterQuery)) + ); + + const invenotryItems = Object.keys(results[0]); + for (const item of invenotryItems) { + const alertInstance = services.alertInstanceFactory(`${alertId}-${item}`); + // AND logic; all criteria must be across the threshold + const shouldAlertFire = results.every(result => result[item].shouldFire); + + // AND logic; because we need to evaluate all criteria, if one of them reports no data then the + // whole alert is in a No Data/Error state + const isNoData = results.some(result => result[item].isNoData); + const isError = results.some(result => result[item].isError); + + if (shouldAlertFire) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + group: item, + item, + valueOf: mapToConditionsLookup(results, result => + formatMetric(result[item].metric, result[item].currentValue) + ), + thresholdOf: mapToConditionsLookup(criteria, c => c.threshold), + metricOf: mapToConditionsLookup(criteria, c => c.metric), + }); + } + + alertInstance.replaceState({ + alertState: isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK, + }); + } +}; + +interface ConditionResult { + shouldFire: boolean; + currentValue?: number | null; + isNoData: boolean; + isError: boolean; +} + +const evaluateCondtion = async ( + condition: InventoryMetricConditions, + nodeType: InventoryItemType, + sourceConfiguration: InfraSourceConfiguration, + services: AlertServices, + filterQuery?: string +): Promise<Record<string, ConditionResult>> => { + const { comparator, metric } = condition; + let { threshold } = condition; + + const currentValues = await getData( + services, + nodeType, + metric, + { + to: Date.now(), + from: moment() + .subtract(condition.timeSize, condition.timeUnit) + .toDate() + .getTime(), + interval: condition.timeUnit, + }, + sourceConfiguration, + filterQuery + ); + + threshold = threshold.map(n => convertMetricValue(metric, n)); + + const comparisonFunction = comparatorMap[comparator]; + + return mapValues(currentValues, value => ({ + shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold), + metric, + currentValue: value, + isNoData: value === null, + isError: value === undefined, + })); +}; + +const getData = async ( + services: AlertServices, + nodeType: InventoryItemType, + metric: SnapshotMetricType, + timerange: InfraTimerangeInput, + sourceConfiguration: InfraSourceConfiguration, + filterQuery?: string +) => { + const snapshot = new InfraSnapshot(); + const esClient = <Hit = {}, Aggregation = undefined>( + options: CallWithRequestParams + ): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> => + services.callCluster('search', options); + + const options = { + filterQuery: parseFilterQuery(filterQuery), + nodeType, + groupBy: [], + sourceConfiguration, + metric: { type: metric }, + timerange, + }; + + const { nodes } = await snapshot.getNodes(esClient, options); + + return nodes.reduce((acc, n) => { + const nodePathItem = last(n.path); + acc[nodePathItem.label] = n.metric && n.metric.value; + return acc; + }, {} as Record<string, number | undefined | null>); +}; + +const comparatorMap = { + [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => + value >= Math.min(a, b) && value <= Math.max(a, b), + // `threshold` is always an array of numbers in case the BETWEEN comparator is + // used; all other compartors will just destructure the first value in the array + [Comparator.GT]: (a: number, [b]: number[]) => a > b, + [Comparator.LT]: (a: number, [b]: number[]) => a < b, + [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b, + [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b, + [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, +}; + +const mapToConditionsLookup = ( + list: any[], + mapFn: (value: any, index: number, array: any[]) => unknown +) => + list + .map(mapFn) + .reduce( + (result: Record<string, any>, value, i) => ({ ...result, [`condition${i}`]: value }), + {} + ); + +export const FIRED_ACTIONS = { + id: 'metrics.invenotry_threshold.fired', + name: i18n.translate('xpack.infra.metrics.alerting.inventory.threshold.fired', { + defaultMessage: 'Fired', + }), +}; + +// Some metrics in the UI are in a different unit that what we store in ES. +const convertMetricValue = (metric: SnapshotMetricType, value: number) => { + if (converters[metric]) { + return converters[metric](value); + } else { + return value; + } +}; +const converters: Record<string, (n: number) => number> = { + cpu: n => Number(n) / 100, + memory: n => Number(n) / 100, +}; + +const formatMetric = (metric: SnapshotMetricType, value: number) => { + // if (SnapshotCustomMetricInputRT.is(metric)) { + // const formatter = createFormatterForMetric(metric); + // return formatter(val); + // } + const metricFormatter = get(METRIC_FORMATTERS, metric, METRIC_FORMATTERS.count); + if (value == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(value); +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts new file mode 100644 index 0000000000000..3b6a1b5557bc6 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.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 { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { curry } from 'lodash'; +import uuid from 'uuid'; +import { + createInventoryMetricThresholdExecutor, + FIRED_ACTIONS, +} from './inventory_metric_threshold_executor'; +import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from './types'; +import { InfraBackendLibs } from '../../infra_types'; + +const condition = schema.object({ + threshold: schema.arrayOf(schema.number()), + comparator: schema.oneOf([ + schema.literal('>'), + schema.literal('<'), + schema.literal('>='), + schema.literal('<='), + schema.literal('between'), + schema.literal('outside'), + ]), + timeUnit: schema.string(), + timeSize: schema.number(), + metric: schema.string(), +}); + +export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs) => ({ + id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, + name: 'Inventory', + validate: { + params: schema.object( + { + criteria: schema.arrayOf(condition), + nodeType: schema.string(), + filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), + }, + { unknowns: 'allow' } + ), + }, + defaultActionGroupId: FIRED_ACTIONS.id, + actionGroups: [FIRED_ACTIONS], + executor: curry(createInventoryMetricThresholdExecutor)(libs, uuid.v4()), + actionVariables: { + context: [ + { + name: 'group', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.groupActionVariableDescription', + { + defaultMessage: 'Name of the group reporting data', + } + ), + }, + { + name: 'valueOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription', + { + defaultMessage: + 'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.', + } + ), + }, + { + name: 'thresholdOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription', + { + defaultMessage: + 'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.', + } + ), + }, + { + name: 'metricOf', + description: i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription', + { + defaultMessage: + 'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.', + } + ), + }, + ], + }, +}); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts new file mode 100644 index 0000000000000..73ee1ab6b7615 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/types.ts @@ -0,0 +1,35 @@ +/* + * 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 { SnapshotMetricType } from '../../../../common/inventory_models/types'; + +export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; + +export enum Comparator { + GT = '>', + LT = '<', + GT_OR_EQ = '>=', + LT_OR_EQ = '<=', + BETWEEN = 'between', + OUTSIDE_RANGE = 'outside', +} + +export enum AlertStates { + OK, + ALERT, + NO_DATA, + ERROR, +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; + +export interface InventoryMetricConditions { + metric: SnapshotMetricType; + timeSize: number; + timeUnit: TimeUnit; + sourceId?: string; + threshold: number[]; + comparator: Comparator; +} diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts new file mode 100644 index 0000000000000..cdec04ab81a8e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -0,0 +1,250 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { AlertExecutorOptions, AlertServices } from '../../../../../alerting/server'; +import { + AlertStates, + Comparator, + LogDocumentCountAlertParams, + Criterion, +} from '../../../../common/alerting/logs/types'; +import { InfraBackendLibs } from '../../infra_types'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { InfraSource } from '../../../../common/http_api/source_api'; + +const checkValueAgainstComparatorMap: { + [key: string]: (a: number, b: number) => boolean; +} = { + [Comparator.GT]: (a: number, b: number) => a > b, + [Comparator.GT_OR_EQ]: (a: number, b: number) => a >= b, + [Comparator.LT]: (a: number, b: number) => a < b, + [Comparator.LT_OR_EQ]: (a: number, b: number) => a <= b, +}; + +export const createLogThresholdExecutor = (alertUUID: string, libs: InfraBackendLibs) => + async function({ services, params }: AlertExecutorOptions) { + const { count, criteria } = params as LogDocumentCountAlertParams; + const { alertInstanceFactory, savedObjectsClient, callCluster } = services; + const { sources } = libs; + + const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); + const indexPattern = sourceConfiguration.configuration.logAlias; + + const alertInstance = alertInstanceFactory(alertUUID); + + try { + const query = getESQuery( + params as LogDocumentCountAlertParams, + sourceConfiguration.configuration + ); + const result = await getResults(query, indexPattern, callCluster); + + if (checkValueAgainstComparatorMap[count.comparator](result.count, count.value)) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + matchingDocuments: result.count, + conditions: createConditionsMessage(criteria), + }); + + alertInstance.replaceState({ + alertState: AlertStates.ALERT, + }); + } else { + alertInstance.replaceState({ + alertState: AlertStates.OK, + }); + } + } catch (e) { + alertInstance.replaceState({ + alertState: AlertStates.ERROR, + }); + + throw new Error(e); + } + }; + +const getESQuery = ( + params: LogDocumentCountAlertParams, + sourceConfiguration: InfraSource['configuration'] +): object => { + const { timeSize, timeUnit, criteria } = params; + const interval = `${timeSize}${timeUnit}`; + const intervalAsSeconds = getIntervalInSeconds(interval); + const to = Date.now(); + const from = to - intervalAsSeconds * 1000; + + const rangeFilters = [ + { + range: { + [sourceConfiguration.fields.timestamp]: { + gte: from, + lte: to, + format: 'epoch_millis', + }, + }, + }, + ]; + + const positiveComparators = getPositiveComparators(); + const negativeComparators = getNegativeComparators(); + const positiveCriteria = criteria.filter(criterion => + positiveComparators.includes(criterion.comparator) + ); + const negativeCriteria = criteria.filter(criterion => + negativeComparators.includes(criterion.comparator) + ); + // Positive assertions (things that "must" match) + const mustFilters = buildFiltersForCriteria(positiveCriteria); + // Negative assertions (things that "must not" match) + const mustNotFilters = buildFiltersForCriteria(negativeCriteria); + + const query = { + query: { + bool: { + filter: [...rangeFilters], + ...(mustFilters.length > 0 && { must: mustFilters }), + ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), + }, + }, + }; + + return query; +}; + +type SupportedESQueryTypes = 'term' | 'match' | 'match_phrase' | 'range'; +type Filter = { + [key in SupportedESQueryTypes]?: object; +}; + +const buildFiltersForCriteria = (criteria: LogDocumentCountAlertParams['criteria']) => { + let filters: Filter[] = []; + + criteria.forEach(criterion => { + const criterionQuery = buildCriterionQuery(criterion); + if (criterionQuery) { + filters = [...filters, criterionQuery]; + } + }); + return filters; +}; + +const buildCriterionQuery = (criterion: Criterion): Filter | undefined => { + const { field, value, comparator } = criterion; + + const queryType = getQueryMappingForComparator(comparator); + + switch (queryType) { + case 'term': + return { + term: { + [field]: { + value, + }, + }, + }; + break; + case 'match': { + return { + match: { + [field]: value, + }, + }; + } + case 'match_phrase': { + return { + match_phrase: { + [field]: value, + }, + }; + } + case 'range': { + const comparatorToRangePropertyMapping: { + [key: string]: string; + } = { + [Comparator.LT]: 'lt', + [Comparator.LT_OR_EQ]: 'lte', + [Comparator.GT]: 'gt', + [Comparator.GT_OR_EQ]: 'gte', + }; + + const rangeProperty = comparatorToRangePropertyMapping[comparator]; + + return { + range: { + [field]: { + [rangeProperty]: value, + }, + }, + }; + } + default: { + return undefined; + } + } +}; + +const getPositiveComparators = () => { + return [ + Comparator.GT, + Comparator.GT_OR_EQ, + Comparator.LT, + Comparator.LT_OR_EQ, + Comparator.EQ, + Comparator.MATCH, + Comparator.MATCH_PHRASE, + ]; +}; + +const getNegativeComparators = () => { + return [Comparator.NOT_EQ, Comparator.NOT_MATCH, Comparator.NOT_MATCH_PHRASE]; +}; + +const queryMappings: { + [key: string]: string; +} = { + [Comparator.GT]: 'range', + [Comparator.GT_OR_EQ]: 'range', + [Comparator.LT]: 'range', + [Comparator.LT_OR_EQ]: 'range', + [Comparator.EQ]: 'term', + [Comparator.MATCH]: 'match', + [Comparator.MATCH_PHRASE]: 'match_phrase', + [Comparator.NOT_EQ]: 'term', + [Comparator.NOT_MATCH]: 'match', + [Comparator.NOT_MATCH_PHRASE]: 'match_phrase', +}; + +const getQueryMappingForComparator = (comparator: Comparator) => { + return queryMappings[comparator]; +}; + +const getResults = async ( + query: object, + index: string, + callCluster: AlertServices['callCluster'] +) => { + return await callCluster('count', { + body: query, + index, + }); +}; + +const createConditionsMessage = (criteria: LogDocumentCountAlertParams['criteria']) => { + const parts = criteria.map((criterion, index) => { + const { field, comparator, value } = criterion; + return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`; + }); + return parts.join(' '); +}; + +// When the Alerting plugin implements support for multiple action groups, add additional +// action groups here to send different messages, e.g. a recovery notification +export const FIRED_ACTIONS = { + id: 'logs.threshold.fired', + name: i18n.translate('xpack.infra.logs.alerting.threshold.fired', { + defaultMessage: 'Fired', + }), +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts new file mode 100644 index 0000000000000..04207a4233dfd --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import uuid from 'uuid'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { PluginSetupContract } from '../../../../../alerting/server'; +import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; +import { + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + Comparator, +} from '../../../../common/alerting/logs/types'; +import { InfraBackendLibs } from '../../infra_types'; + +const documentCountActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription', + { + defaultMessage: 'The number of log entries that matched the conditions provided', + } +); + +const conditionsActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.conditionsActionVariableDescription', + { + defaultMessage: 'The conditions that log entries needed to fulfill', + } +); + +const countSchema = schema.object({ + value: schema.number(), + comparator: schema.oneOf([ + schema.literal(Comparator.GT), + schema.literal(Comparator.LT), + schema.literal(Comparator.GT_OR_EQ), + schema.literal(Comparator.LT_OR_EQ), + schema.literal(Comparator.EQ), + ]), +}); + +const criteriaSchema = schema.object({ + field: schema.string(), + comparator: schema.oneOf([ + schema.literal(Comparator.GT), + schema.literal(Comparator.LT), + schema.literal(Comparator.GT_OR_EQ), + schema.literal(Comparator.LT_OR_EQ), + schema.literal(Comparator.EQ), + schema.literal(Comparator.NOT_EQ), + schema.literal(Comparator.MATCH), + schema.literal(Comparator.NOT_MATCH), + ]), + value: schema.oneOf([schema.number(), schema.string()]), +}); + +export async function registerLogThresholdAlertType( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs +) { + if (!alertingPlugin) { + throw new Error( + 'Cannot register log threshold alert type. Both the actions and alerting plugins need to be enabled.' + ); + } + + const alertUUID = uuid.v4(); + + alertingPlugin.registerType({ + id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + name: 'Log threshold', + validate: { + params: schema.object({ + count: countSchema, + criteria: schema.arrayOf(criteriaSchema), + timeUnit: schema.string(), + timeSize: schema.number(), + }), + }, + defaultActionGroupId: FIRED_ACTIONS.id, + actionGroups: [FIRED_ACTIONS], + executor: createLogThresholdExecutor(alertUUID, libs), + actionVariables: { + context: [ + { name: 'matchingDocuments', description: documentCountActionVariableDescription }, + { name: 'conditions', description: conditionsActionVariableDescription }, + ], + }, + }); +} diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts new file mode 100644 index 0000000000000..4878574e39d16 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts @@ -0,0 +1,109 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Comparator, AlertStates } from './types'; + +export const DOCUMENT_COUNT_I18N = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.documentCount', + { + defaultMessage: 'Document count', + } +); + +export const stateToAlertMessage = { + [AlertStates.ALERT]: i18n.translate('xpack.infra.metrics.alerting.threshold.alertState', { + defaultMessage: 'ALERT', + }), + [AlertStates.NO_DATA]: i18n.translate('xpack.infra.metrics.alerting.threshold.noDataState', { + defaultMessage: 'NO DATA', + }), + [AlertStates.ERROR]: i18n.translate('xpack.infra.metrics.alerting.threshold.errorState', { + defaultMessage: 'ERROR', + }), + // TODO: Implement recovered message state + [AlertStates.OK]: i18n.translate('xpack.infra.metrics.alerting.threshold.okState', { + defaultMessage: 'OK [Recovered]', + }), +}; + +const comparatorToI18n = (comparator: Comparator, threshold: number[], currentValue: number) => { + const gtText = i18n.translate('xpack.infra.metrics.alerting.threshold.gtComparator', { + defaultMessage: 'greater than', + }); + const ltText = i18n.translate('xpack.infra.metrics.alerting.threshold.ltComparator', { + defaultMessage: 'less than', + }); + const eqText = i18n.translate('xpack.infra.metrics.alerting.threshold.eqComparator', { + defaultMessage: 'equal to', + }); + + switch (comparator) { + case Comparator.BETWEEN: + return i18n.translate('xpack.infra.metrics.alerting.threshold.betweenComparator', { + defaultMessage: 'between', + }); + case Comparator.OUTSIDE_RANGE: + return i18n.translate('xpack.infra.metrics.alerting.threshold.outsideRangeComparator', { + defaultMessage: 'not between', + }); + case Comparator.GT: + return gtText; + case Comparator.LT: + return ltText; + case Comparator.GT_OR_EQ: + case Comparator.LT_OR_EQ: + if (threshold[0] === currentValue) return eqText; + else if (threshold[0] < currentValue) return ltText; + return gtText; + } +}; + +const thresholdToI18n = ([a, b]: number[]) => { + if (typeof b === 'undefined') return a; + return i18n.translate('xpack.infra.metrics.alerting.threshold.thresholdRange', { + defaultMessage: '{a} and {b}', + values: { a, b }, + }); +}; + +export const buildFiredAlertReason: (alertResult: { + metric: string; + comparator: Comparator; + threshold: number[]; + currentValue: number; +}) => string = ({ metric, comparator, threshold, currentValue }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', { + defaultMessage: + '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue})', + values: { + metric, + comparator: comparatorToI18n(comparator, threshold, currentValue), + threshold: thresholdToI18n(threshold), + currentValue, + }, + }); + +export const buildNoDataAlertReason: (alertResult: { + metric: string; + timeSize: number; + timeUnit: string; +}) => string = ({ metric, timeSize, timeUnit }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.noDataAlertReason', { + defaultMessage: '{metric} has reported no data over the past {interval}', + values: { + metric, + interval: `${timeSize}${timeUnit}`, + }, + }); + +export const buildErrorAlertReason = (metric: string) => + i18n.translate('xpack.infra.metrics.alerting.threshold.errorAlertReason', { + defaultMessage: 'Elasticsearch failed when attempting to query data for {metric}', + values: { + metric, + }, + }); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 000d0823311b3..2531e939792af 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -3,7 +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 { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; import { Comparator, AlertStates } from './types'; import * as mocks from './test_mocks'; @@ -13,79 +12,14 @@ import { AlertServicesMock, AlertInstanceMock, } from '../../../../../alerting/server/mocks'; - -const executor = createMetricThresholdExecutor('test') as (opts: { - params: AlertExecutorOptions['params']; - services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; -}) => Promise<void>; - -const services: AlertServicesMock = alertsMock.createAlertServices(); -services.callCluster.mockImplementation((_: string, { body, index }: any) => { - if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; - const metric = body.query.bool.filter[1]?.exists.field; - if (body.aggs.groupings) { - if (body.aggs.groupings.composite.after) { - return mocks.compositeEndResponse; - } - if (metric === 'test.metric.2') { - return mocks.alternateCompositeResponse; - } - return mocks.basicCompositeResponse; - } - if (metric === 'test.metric.2') { - return mocks.alternateMetricResponse; - } - return mocks.basicMetricResponse; -}); -services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { - if (sourceId === 'alternate') - return { - id: 'alternate', - attributes: { metricAlias: 'alternatebeat-*' }, - type, - references: [], - }; - return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; -}); +import { InfraSources } from '../../sources'; interface AlertTestInstance { instance: AlertInstanceMock; actionQueue: any[]; state: any; } -const alertInstances = new Map<string, AlertTestInstance>(); -services.alertInstanceFactory.mockImplementation((instanceID: string) => { - const alertInstance: AlertTestInstance = { - instance: alertsMock.createAlertInstanceFactory(), - actionQueue: [], - state: {}, - }; - alertInstances.set(instanceID, alertInstance); - alertInstance.instance.replaceState.mockImplementation((newState: any) => { - alertInstance.state = newState; - return alertInstance.instance; - }); - alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { - alertInstance.actionQueue.push({ id, action }); - return alertInstance.instance; - }); - return alertInstance.instance; -}); - -function mostRecentAction(id: string) { - return alertInstances.get(id)!.actionQueue.pop(); -} - -function getState(id: string) { - return alertInstances.get(id)!.state; -} -const baseCriterion = { - aggType: 'avg', - metric: 'test.metric.1', - timeSize: 1, - timeUnit: 'm', -}; describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = 'test-*'; @@ -149,22 +83,22 @@ describe('The metric threshold alert type', () => { expect(mostRecentAction(instanceID)).toBe(undefined); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); - test('reports expected values to the action context', async () => { - await execute(Comparator.GT, [0.75]); - const { action } = mostRecentAction(instanceID); - expect(action.group).toBe('*'); - expect(action.valueOf.condition0).toBe(1); - expect(action.thresholdOf.condition0).toStrictEqual([0.75]); - expect(action.metricOf.condition0).toBe('test.metric.1'); - }); - test('fetches the index pattern dynamically', async () => { - await execute(Comparator.LT, [17], 'alternate'); + test('alerts as expected with the outside range comparator', async () => { + await execute(Comparator.OUTSIDE_RANGE, [0, 0.75]); expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); - await execute(Comparator.LT, [1.5], 'alternate'); + await execute(Comparator.OUTSIDE_RANGE, [0, 1.5]); expect(mostRecentAction(instanceID)).toBe(undefined); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); + test('reports expected values to the action context', async () => { + await execute(Comparator.GT, [0.75]); + const { action } = mostRecentAction(instanceID); + expect(action.group).toBe('*'); + expect(action.reason).toContain('current value is 1'); + expect(action.reason).toContain('threshold of 0.75'); + expect(action.reason).toContain('test.metric.1'); + }); }); describe('querying with a groupBy parameter', () => { @@ -263,12 +197,14 @@ describe('The metric threshold alert type', () => { const instanceID = 'test-*'; await execute(Comparator.GT_OR_EQ, [1.0], [3.0]); const { action } = mostRecentAction(instanceID); - expect(action.valueOf.condition0).toBe(1); - expect(action.valueOf.condition1).toBe(3.5); - expect(action.thresholdOf.condition0).toStrictEqual([1.0]); - expect(action.thresholdOf.condition1).toStrictEqual([3.0]); - expect(action.metricOf.condition0).toBe('test.metric.1'); - expect(action.metricOf.condition1).toBe('test.metric.2'); + const reasons = action.reason.split('\n'); + expect(reasons.length).toBe(2); + expect(reasons[0]).toContain('test.metric.1'); + expect(reasons[1]).toContain('test.metric.2'); + expect(reasons[0]).toContain('current value is 1'); + expect(reasons[1]).toContain('current value is 3.5'); + expect(reasons[0]).toContain('threshold of 1'); + expect(reasons[1]).toContain('threshold of 3'); }); }); describe('querying with the count aggregator', () => { @@ -297,4 +233,146 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); + describe("querying a metric that hasn't reported data", () => { + const instanceID = 'test-*'; + const execute = (alertOnNoData: boolean) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator: Comparator.GT, + threshold: 1, + metric: 'test.metric.3', + }, + ], + alertOnNoData, + }, + }); + test('sends a No Data alert when configured to do so', async () => { + await execute(true); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); + }); + test('does not send a No Data alert when not configured to do so', async () => { + await execute(false); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); + }); + }); +}); + +const createMockStaticConfiguration = (sources: any) => ({ + enabled: true, + query: { + partitionSize: 1, + partitionFactor: 1, + }, + sources, +}); + +const mockLibs: any = { + sources: new InfraSources({ + config: createMockStaticConfiguration({}), + }), + configuration: createMockStaticConfiguration({}), +}; + +const executor = createMetricThresholdExecutor(mockLibs, 'test') as (opts: { + params: AlertExecutorOptions['params']; + services: { callCluster: AlertExecutorOptions['params']['callCluster'] }; +}) => Promise<void>; + +const services: AlertServicesMock = alertsMock.createAlertServices(); +services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { + if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; + const metric = body.query.bool.filter[1]?.exists.field; + if (body.aggs.groupings) { + if (body.aggs.groupings.composite.after) { + return mocks.compositeEndResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateCompositeResponse; + } + return mocks.basicCompositeResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateMetricResponse; + } + return mocks.basicMetricResponse; +}); +services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { + if (sourceId === 'alternate') + return { + id: 'alternate', + attributes: { metricAlias: 'alternatebeat-*' }, + type, + references: [], + }; + return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; +}); + +services.callCluster.mockImplementation(async (_: string, { body, index }: any) => { + if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse; + const metric = body.query.bool.filter[1]?.exists.field; + if (body.aggs.groupings) { + if (body.aggs.groupings.composite.after) { + return mocks.compositeEndResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateCompositeResponse; + } + return mocks.basicCompositeResponse; + } + if (metric === 'test.metric.2') { + return mocks.alternateMetricResponse; + } else if (metric === 'test.metric.3') { + return mocks.emptyMetricResponse; + } + return mocks.basicMetricResponse; +}); +services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => { + if (sourceId === 'alternate') + return { + id: 'alternate', + attributes: { metricAlias: 'alternatebeat-*' }, + type, + references: [], + }; + return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] }; +}); + +const alertInstances = new Map<string, AlertTestInstance>(); +services.alertInstanceFactory.mockImplementation((instanceID: string) => { + const alertInstance: AlertTestInstance = { + instance: alertsMock.createAlertInstanceFactory(), + actionQueue: [], + state: {}, + }; + alertInstances.set(instanceID, alertInstance); + alertInstance.instance.replaceState.mockImplementation((newState: any) => { + alertInstance.state = newState; + return alertInstance.instance; + }); + alertInstance.instance.scheduleActions.mockImplementation((id: string, action: any) => { + alertInstance.actionQueue.push({ id, action }); + return alertInstance.instance; + }); + return alertInstance.instance; }); + +function mostRecentAction(id: string) { + return alertInstances.get(id)!.actionQueue.pop(); +} + +function getState(id: string) { + return alertInstances.get(id)!.state; +} + +const baseCriterion = { + aggType: 'avg', + metric: 'test.metric.1', + timeSize: 1, + timeUnit: 'm', +}; 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 c5ea65f7a4d1a..5c34a058577a1 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 @@ -5,19 +5,24 @@ */ import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { convertSavedObjectToSavedSourceConfiguration } from '../../sources/sources'; -import { infraSourceConfigurationSavedObjectType } from '../../sources/saved_object_mappings'; import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; -import { MetricExpressionParams, Comparator, AlertStates } from './types'; +import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './types'; +import { + buildErrorAlertReason, + buildFiredAlertReason, + buildNoDataAlertReason, + DOCUMENT_COUNT_I18N, + stateToAlertMessage, +} from './messages'; import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; +import { InfraBackendLibs } from '../../infra_types'; const TOTAL_BUCKETS = 5; -const DEFAULT_INDEX_PATTERN = 'metricbeat-*'; interface Aggregation { aggregatedIntervals: { @@ -39,7 +44,7 @@ const getCurrentValueFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state const mostRecentBucket = buckets[buckets.length - 1]; - if (aggType === 'count') { + if (aggType === Aggregators.COUNT) { return mostRecentBucket.doc_count; } const { value } = mostRecentBucket.aggregatedValue; @@ -51,29 +56,32 @@ const getCurrentValueFromAggregations = ( const getParsedFilterQuery: ( filterQuery: string | undefined -) => Record<string, any> = filterQuery => { +) => Record<string, any> | Array<Record<string, any>> = 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, + }, }, - }; + ]; } }; export const getElasticsearchMetricQuery = ( { metric, aggType, timeUnit, timeSize }: MetricExpressionParams, + timefield: string, groupBy?: string, filterQuery?: string ) => { - if (aggType === 'count' && metric) { + if (aggType === Aggregators.COUNT && metric) { throw new Error('Cannot aggregate document count with a metric'); } - if (aggType !== 'count' && !metric) { + if (aggType !== Aggregators.COUNT && !metric) { throw new Error('Can only aggregate without a metric if using the document count aggregator'); } const interval = `${timeSize}${timeUnit}`; @@ -85,9 +93,9 @@ export const getElasticsearchMetricQuery = ( const offset = getDateHistogramOffset(from, interval); const aggregations = - aggType === 'count' + aggType === Aggregators.COUNT ? {} - : aggType === 'rate' + : aggType === Aggregators.RATE ? networkTraffic('aggregatedValue', metric) : { aggregatedValue: { @@ -100,7 +108,7 @@ export const getElasticsearchMetricQuery = ( const baseAggs = { aggregatedIntervals: { date_histogram: { - field: '@timestamp', + field: timefield, fixed_interval: interval, offset, extended_bounds: { @@ -159,8 +167,12 @@ export const getElasticsearchMetricQuery = ( return { query: { bool: { - filter: [...rangeFilters, ...metricFieldFilters], - ...parsedFilterQuery, + filter: [ + ...rangeFilters, + ...metricFieldFilters, + ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []), + ], + ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}), }, }, size: 0, @@ -168,43 +180,23 @@ export const getElasticsearchMetricQuery = ( }; }; -const getIndexPattern: ( - services: AlertServices, - sourceId?: string -) => Promise<string> = async function({ savedObjectsClient }, sourceId = 'default') { - try { - const sourceConfiguration = await savedObjectsClient.get( - infraSourceConfigurationSavedObjectType, - sourceId - ); - const { metricAlias } = convertSavedObjectToSavedSourceConfiguration( - sourceConfiguration - ).configuration; - return metricAlias || DEFAULT_INDEX_PATTERN; - } catch (e) { - if (e.output.statusCode === 404) { - return DEFAULT_INDEX_PATTERN; - } else { - throw e; - } - } -}; - const getMetric: ( services: AlertServices, params: MetricExpressionParams, index: string, + timefield: string, groupBy: string | undefined, filterQuery: string | undefined ) => Promise<Record<string, number>> = async function( - { savedObjectsClient, callCluster }, + { callCluster }, params, index, + timefield, groupBy, filterQuery ) { const { aggType } = params; - const searchBody = getElasticsearchMetricQuery(params, groupBy, filterQuery); + const searchBody = getElasticsearchMetricQuery(params, timefield, groupBy, filterQuery); try { if (groupBy) { @@ -233,6 +225,7 @@ const getMetric: ( body: searchBody, index, }); + return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) }; } catch (e) { return { '*': undefined }; // Trigger an Error state @@ -242,7 +235,8 @@ const getMetric: ( const comparatorMap = { [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => value >= Math.min(a, b) && value <= Math.max(a, b), - // `threshold` is always an array of numbers in case the BETWEEN comparator is + [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b, + // `threshold` is always an array of numbers in case the BETWEEN/OUTSIDE_RANGE comparator is // used; all other compartors will just destructure the first value in the array [Comparator.GT]: (a: number, [b]: number[]) => a > b, [Comparator.LT]: (a: number, [b]: number[]) => a < b, @@ -250,47 +244,51 @@ const comparatorMap = { [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b, }; -const mapToConditionsLookup = ( - list: any[], - mapFn: (value: any, index: number, array: any[]) => unknown -) => - list - .map(mapFn) - .reduce( - (result: Record<string, any>, value, i) => ({ ...result, [`condition${i}`]: value }), - {} - ); - -export const createMetricThresholdExecutor = (alertUUID: string) => +export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: string) => async function({ services, params }: AlertExecutorOptions) { - const { criteria, groupBy, filterQuery, sourceId } = params as { + const { criteria, groupBy, filterQuery, sourceId, alertOnNoData } = params as { criteria: MetricExpressionParams[]; groupBy: string | undefined; filterQuery: string | undefined; sourceId?: string; + alertOnNoData: boolean; }; + const source = await libs.sources.getSourceConfiguration( + services.savedObjectsClient, + sourceId || 'default' + ); + const config = source.configuration; const alertResults = await Promise.all( - criteria.map(criterion => - (async () => { - const index = await getIndexPattern(services, sourceId); - const currentValues = await getMetric(services, criterion, index, groupBy, filterQuery); + criteria.map(criterion => { + return (async () => { + const currentValues = await getMetric( + services, + criterion, + config.fields.timestamp, + config.metricAlias, + groupBy, + filterQuery + ); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ + ...criterion, + metric: criterion.metric ?? DOCUMENT_COUNT_I18N, + currentValue: value, shouldFire: value !== undefined && value !== null && comparisonFunction(value, threshold), - currentValue: value, isNoData: value === null, isError: value === undefined, })); - })() - ) + })(); + }) ); + // Because each alert result has the same group definitions, just grap the groups from the first one. const groups = Object.keys(alertResults[0]); for (const group of groups) { - const alertInstance = services.alertInstanceFactory(`${alertUUID}-${group}`); + const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`); // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every(result => result[group].shouldFire); @@ -298,23 +296,43 @@ export const createMetricThresholdExecutor = (alertUUID: string) => // whole alert is in a No Data/Error state const isNoData = alertResults.some(result => result[group].isNoData); const isError = alertResults.some(result => result[group].isError); - if (shouldAlertFire) { + + const nextState = isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK; + + let reason; + if (nextState === AlertStates.ALERT) { + reason = alertResults.map(result => buildFiredAlertReason(result[group])).join('\n'); + } + if (alertOnNoData) { + if (nextState === AlertStates.NO_DATA) { + reason = alertResults + .filter(result => result[group].isNoData) + .map(result => buildNoDataAlertReason(result[group])) + .join('\n'); + } else if (nextState === AlertStates.ERROR) { + reason = alertResults + .filter(result => result[group].isError) + .map(result => buildErrorAlertReason(result[group].metric)) + .join('\n'); + } + } + if (reason) { alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, - valueOf: mapToConditionsLookup(alertResults, result => result[group].currentValue), - thresholdOf: mapToConditionsLookup(criteria, criterion => criterion.threshold), - metricOf: mapToConditionsLookup(criteria, criterion => criterion.metric), + alertState: stateToAlertMessage[nextState], + reason, }); } + // Future use: ability to fetch display current alert state alertInstance.replaceState({ - alertState: isError - ? AlertStates.ERROR - : isNoData - ? AlertStates.NO_DATA - : shouldAlertFire - ? AlertStates.ALERT - : AlertStates.OK, + alertState: nextState, }); } }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 8808219cabaa7..23611559a184f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -6,27 +6,22 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { PluginSetupContract } from '../../../../../alerting/server'; +import { curry } from 'lodash'; +import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; +import { InfraBackendLibs } from '../../infra_types'; -export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { - if (!alertingPlugin) { - throw new Error( - 'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.' - ); - } - const alertUUID = uuid.v4(); +const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) => + schema.string({ + validate: value => + arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, + }); +export function registerMetricThresholdAlertType(libs: InfraBackendLibs) { const baseCriterion = { threshold: schema.arrayOf(schema.number()), - comparator: schema.oneOf([ - schema.literal('>'), - schema.literal('<'), - schema.literal('>='), - schema.literal('<='), - schema.literal('between'), - ]), + comparator: oneOfLiterals(Object.values(Comparator)), timeUnit: schema.string(), timeSize: schema.number(), }; @@ -34,13 +29,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet const nonCountCriterion = schema.object({ ...baseCriterion, metric: schema.string(), - aggType: schema.oneOf([ - schema.literal('avg'), - schema.literal('min'), - schema.literal('max'), - schema.literal('rate'), - schema.literal('cardinality'), - ]), + aggType: oneOfLiterals(METRIC_EXPLORER_AGGREGATIONS), }); const countCriterion = schema.object({ @@ -56,51 +45,45 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet } ); - const valueOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.valueOfActionVariableDescription', + const alertStateActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.alertStateActionVariableDescription', { - defaultMessage: - 'Record of the current value of the watched metric; grouped by condition, i.e valueOf.condition0, valueOf.condition1, etc.', + defaultMessage: 'Current state of the alert', } ); - const thresholdOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.thresholdOfActionVariableDescription', + const reasonActionVariableDescription = i18n.translate( + 'xpack.infra.metrics.alerting.threshold.alerting.reasonActionVariableDescription', { defaultMessage: - 'Record of the alerting threshold; grouped by condition, i.e thresholdOf.condition0, thresholdOf.condition1, etc.', + 'A description of why the alert is in this state, including which metrics have crossed which thresholds', } ); - const metricOfActionVariableDescription = i18n.translate( - 'xpack.infra.metrics.alerting.threshold.alerting.metricOfActionVariableDescription', - { - defaultMessage: - 'Record of the watched metric; grouped by condition, i.e metricOf.condition0, metricOf.condition1, etc.', - } - ); - - alertingPlugin.registerType({ + return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, name: 'Metric threshold', validate: { - params: schema.object({ - criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), - groupBy: schema.maybe(schema.string()), - filterQuery: schema.maybe(schema.string()), - sourceId: schema.string(), - }), + params: schema.object( + { + criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion])), + groupBy: schema.maybe(schema.string()), + filterQuery: schema.maybe(schema.string()), + sourceId: schema.string(), + alertOnNoData: schema.maybe(schema.boolean()), + }, + { unknowns: 'allow' } + ), }, defaultActionGroupId: FIRED_ACTIONS.id, actionGroups: [FIRED_ACTIONS], - executor: createMetricThresholdExecutor(alertUUID), + executor: curry(createMetricThresholdExecutor)(libs, uuid.v4()), actionVariables: { context: [ { name: 'group', description: groupActionVariableDescription }, - { name: 'valueOf', description: valueOfActionVariableDescription }, - { name: 'thresholdOf', description: thresholdOfActionVariableDescription }, - { name: 'metricOf', description: metricOfActionVariableDescription }, + { name: 'alertState', description: alertStateActionVariableDescription }, + { name: 'reason', description: reasonActionVariableDescription }, ], }, - }); + }; } diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index 66e0a363c8983..fa55f80e472de 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -53,6 +53,14 @@ export const alternateMetricResponse = { }, }; +export const emptyMetricResponse = { + aggregations: { + aggregatedIntervals: { + buckets: [], + }, + }, +}; + export const basicCompositeResponse = { aggregations: { groupings: { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index abed691f109c0..18f5503fe2c9e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerAggregation } from '../../../../common/http_api/metrics_explorer'; - export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export enum Comparator { @@ -14,6 +12,17 @@ export enum Comparator { GT_OR_EQ = '>=', LT_OR_EQ = '<=', BETWEEN = 'between', + OUTSIDE_RANGE = 'outside', +} + +export enum Aggregators { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', } export enum AlertStates { @@ -34,7 +43,7 @@ interface BaseMetricExpressionParams { } interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { - aggType: Exclude<MetricsExplorerAggregation, 'count'>; + aggType: Exclude<Aggregators, Aggregators.COUNT>; metric: string; } diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index 6ec6f31256b78..44d30d7281f20 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -6,13 +6,18 @@ import { PluginSetupContract } from '../../../../alerting/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; +import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; +import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; +import { InfraBackendLibs } from '../infra_types'; -const registerAlertTypes = (alertingPlugin: PluginSetupContract) => { +const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { if (alertingPlugin) { - const registerFns = [registerMetricThresholdAlertType]; + alertingPlugin.registerType(registerMetricThresholdAlertType(libs)); + alertingPlugin.registerType(registerMetricInventoryThresholdAlertType(libs)); + const registerFns = [registerLogThresholdAlertType]; registerFns.forEach(fn => { - fn(alertingPlugin); + fn(alertingPlugin, libs); }); } }; diff --git a/x-pack/plugins/infra/server/lib/compose/kibana.ts b/x-pack/plugins/infra/server/lib/compose/kibana.ts index f100726b5b92e..d22ca2961cfa5 100644 --- a/x-pack/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/plugins/infra/server/lib/compose/kibana.ts @@ -28,7 +28,7 @@ export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServ const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { sources, }); - const snapshot = new InfraSnapshot({ sources, framework }); + const snapshot = new InfraSnapshot(); const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); diff --git a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts index d2e151ca2c3f5..ecbc71f4895c7 100644 --- a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts @@ -21,7 +21,7 @@ export class InfraFieldsDomain { indexType: InfraIndexType ): Promise<InfraIndexField[]> { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType); @@ -29,9 +29,10 @@ export class InfraFieldsDomain { const fields = await this.adapter.getIndexFields( requestContext, - `${includeMetricIndices ? configuration.metricAlias : ''},${ - includeLogIndices ? configuration.logAlias : '' - }` + [ + ...(includeMetricIndices ? [configuration.metricAlias] : []), + ...(includeLogIndices ? [configuration.logAlias] : []), + ].join(',') ); return fields; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 528b9a69327fa..07bc965dda77a 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -113,7 +113,7 @@ export class InfraLogEntriesDomain { params: LogEntriesParams ): Promise<LogEntry[]> { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); @@ -156,14 +156,7 @@ export class InfraLogEntriesDomain { } } ), - context: FIELDS_FROM_CONTEXT.reduce<LogEntry['context']>((ctx, field) => { - // Users might have different types here in their mappings. - const value = doc.fields[field]; - if (typeof value === 'string') { - ctx[field] = value; - } - return ctx; - }, {}), + context: getContextFromDoc(doc), }; }); @@ -179,7 +172,7 @@ export class InfraLogEntriesDomain { filterQuery?: LogEntryQuery ): Promise<LogEntriesSummaryBucket[]> { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( @@ -203,7 +196,7 @@ export class InfraLogEntriesDomain { filterQuery?: LogEntryQuery ): Promise<LogEntriesSummaryHighlightsBucket[][]> { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const messageFormattingRules = compileFormattingRules( @@ -352,3 +345,20 @@ const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({ type: 'phrase', }, }); + +const getContextFromDoc = (doc: LogEntryDocument): LogEntry['context'] => { + // Get all context fields, then test for the presence and type of the ones that go together + const containerId = doc.fields['container.id']; + const hostName = doc.fields['host.name']; + const logFilePath = doc.fields['log.file.path']; + + if (typeof containerId === 'string') { + return { 'container.id': containerId }; + } + + if (typeof hostName === 'string' && typeof logFilePath === 'string') { + return { 'host.name': hostName, 'log.file.path': logFilePath }; + } + + return {}; +}; diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts index cf2b1e59b2a22..c75ee6d644044 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -5,26 +5,23 @@ */ import { uniq } from 'lodash'; -import { RequestHandlerContext } from 'kibana/server'; import { InfraSnapshotRequestOptions } from './types'; import { getMetricsAggregations } from './query_helpers'; import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field'; import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; +import { ESSearchClient } from '.'; export const createTimeRangeWithInterval = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, options: InfraSnapshotRequestOptions ): Promise<InfraTimerangeInput> => { const aggregations = getMetricsAggregations(options); - const modules = await aggregationsToModules(framework, requestContext, aggregations, options); + const modules = await aggregationsToModules(client, aggregations, options); const interval = Math.max( (await calculateMetricInterval( - framework, - requestContext, + client, { indexPattern: options.sourceConfiguration.metricAlias, timestampField: options.sourceConfiguration.fields.timestamp, @@ -43,8 +40,7 @@ export const createTimeRangeWithInterval = async ( }; const aggregationsToModules = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, aggregations: SnapshotModel, options: InfraSnapshotRequestOptions ): Promise<string[]> => { @@ -59,12 +55,7 @@ const aggregationsToModules = async ( const fields = await Promise.all( uniqueFields.map( async field => - await getDatasetForField( - framework, - requestContext, - field as string, - options.sourceConfiguration.metricAlias - ) + await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias) ) ); return fields.filter(f => f) as string[]; diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts index 07abfa5fd474a..4057ed246ccaf 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts @@ -3,11 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { RequestHandlerContext } from 'src/core/server'; -import { InfraDatabaseSearchResponse } from '../adapters/framework'; -import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; -import { InfraSources } from '../sources'; +import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework'; import { JsonObject } from '../../../common/typed_json'; import { SNAPSHOT_COMPOSITE_REQUEST_SIZE } from './constants'; @@ -31,36 +27,26 @@ import { InfraSnapshotRequestOptions } from './types'; import { createTimeRangeWithInterval } from './create_timerange_with_interval'; import { SnapshotNode } from '../../../common/http_api/snapshot_api'; +export type ESSearchClient = <Hit = {}, Aggregation = undefined>( + options: CallWithRequestParams +) => Promise<InfraDatabaseSearchResponse<Hit, Aggregation>>; export class InfraSnapshot { - constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} - public async getNodes( - requestContext: RequestHandlerContext, + client: ESSearchClient, options: InfraSnapshotRequestOptions ): Promise<{ nodes: SnapshotNode[]; interval: string }> { // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const timeRangeWithIntervalApplied = await createTimeRangeWithInterval( - this.libs.framework, - requestContext, - options - ); + const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options); const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied }; - const groupedNodesPromise = requestGroupedNodes( - requestContext, - optionsWithTimerange, - this.libs.framework - ); - const nodeMetricsPromise = requestNodeMetrics( - requestContext, - optionsWithTimerange, - this.libs.framework - ); + const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange); + const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; + return { nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options), interval: timeRangeWithIntervalApplied.interval, @@ -77,15 +63,12 @@ const handleAfterKey = createAfterKeyHandler( input => input?.aggregations?.nodes?.after_key ); -const callClusterFactory = (framework: KibanaFramework, requestContext: RequestHandlerContext) => ( - opts: any -) => - framework.callWithRequest<{}, InfraSnapshotAggregationResponse>(requestContext, 'search', opts); +const callClusterFactory = (search: ESSearchClient) => (opts: any) => + search<{}, InfraSnapshotAggregationResponse>(opts); const requestGroupedNodes = async ( - requestContext: RequestHandlerContext, - options: InfraSnapshotRequestOptions, - framework: KibanaFramework + client: ESSearchClient, + options: InfraSnapshotRequestOptions ): Promise<InfraSnapshotNodeGroupByBucket[]> => { const inventoryModel = findInventoryModel(options.nodeType); const query = { @@ -124,13 +107,12 @@ const requestGroupedNodes = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeGroupByBucket - >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); + >(callClusterFactory(client), query, bucketSelector, handleAfterKey); }; const requestNodeMetrics = async ( - requestContext: RequestHandlerContext, - options: InfraSnapshotRequestOptions, - framework: KibanaFramework + client: ESSearchClient, + options: InfraSnapshotRequestOptions ): Promise<InfraSnapshotNodeMetricsBucket[]> => { const index = options.metric.type === 'logRate' @@ -175,7 +157,7 @@ const requestNodeMetrics = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeMetricsBucket - >(callClusterFactory(framework, requestContext), query, bucketSelector, handleAfterKey); + >(callClusterFactory(client), query, bucketSelector, handleAfterKey); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] diff --git a/x-pack/plugins/infra/server/lib/source_status.ts b/x-pack/plugins/infra/server/lib/source_status.ts index 1f0845b6b223f..9bb953845e5a1 100644 --- a/x-pack/plugins/infra/server/lib/source_status.ts +++ b/x-pack/plugins/infra/server/lib/source_status.ts @@ -18,7 +18,7 @@ export class InfraSourceStatus { sourceId: string ): Promise<string[]> { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const indexNames = await this.adapter.getIndexNames( @@ -32,7 +32,7 @@ export class InfraSourceStatus { sourceId: string ): Promise<string[]> { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const indexNames = await this.adapter.getIndexNames( @@ -46,7 +46,7 @@ export class InfraSourceStatus { sourceId: string ): Promise<boolean> { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasAlias = await this.adapter.hasAlias( @@ -60,7 +60,7 @@ export class InfraSourceStatus { sourceId: string ): Promise<boolean> { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasAlias = await this.adapter.hasAlias( @@ -74,7 +74,7 @@ export class InfraSourceStatus { sourceId: string ): Promise<boolean> { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasIndices = await this.adapter.hasIndices( @@ -88,7 +88,7 @@ export class InfraSourceStatus { sourceId: string ): Promise<boolean> { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasIndices = await this.adapter.hasIndices( diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index 4a83ca730ff83..57efb0f676b2f 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -29,7 +29,9 @@ describe('the InfraSources lib', () => { }, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, @@ -74,7 +76,9 @@ describe('the InfraSources lib', () => { }, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, @@ -104,7 +108,9 @@ describe('the InfraSources lib', () => { attributes: {}, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 99e062aa49ccf..71682c9e798a6 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -9,7 +9,7 @@ import { failure } from 'io-ts/lib/PathReporter'; import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { RequestHandlerContext } from 'src/core/server'; +import { SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -37,11 +37,10 @@ export class InfraSources { } public async getSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string ): Promise<InfraSource> { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) .then(internalSourceConfiguration => ({ id: sourceId, @@ -55,7 +54,7 @@ export class InfraSources { })) .catch(err => err instanceof NotFoundError - ? this.getSavedSourceConfiguration(requestContext, sourceId).then(result => ({ + ? this.getSavedSourceConfiguration(savedObjectsClient, sourceId).then(result => ({ ...result, configuration: mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -65,7 +64,7 @@ export class InfraSources { : Promise.reject(err) ) .catch(err => - requestContext.core.savedObjects.client.errors.isNotFoundError(err) + savedObjectsClient.errors.isNotFoundError(err) ? Promise.resolve({ id: sourceId, version: undefined, @@ -79,10 +78,12 @@ export class InfraSources { return savedSourceConfiguration; } - public async getAllSourceConfigurations(requestContext: RequestHandlerContext) { + public async getAllSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext); + const savedSourceConfigurations = await this.getAllSavedSourceConfigurations( + savedObjectsClient + ); return savedSourceConfigurations.map(savedSourceConfiguration => ({ ...savedSourceConfiguration, @@ -94,7 +95,7 @@ export class InfraSources { } public async createSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string, source: InfraSavedSourceConfiguration ) { @@ -106,7 +107,7 @@ export class InfraSources { ); const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await requestContext.core.savedObjects.client.create( + await savedObjectsClient.create( infraSourceConfigurationSavedObjectType, pickSavedSourceConfiguration(newSourceConfiguration) as any, { id: sourceId } @@ -122,21 +123,24 @@ export class InfraSources { }; } - public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { - await requestContext.core.savedObjects.client.delete( - infraSourceConfigurationSavedObjectType, - sourceId - ); + public async deleteSourceConfiguration( + savedObjectsClient: SavedObjectsClientContract, + sourceId: string + ) { + await savedObjectsClient.delete(infraSourceConfigurationSavedObjectType, sourceId); } public async updateSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string, sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId); + const { configuration, version } = await this.getSourceConfiguration( + savedObjectsClient, + sourceId + ); const updatedSourceConfigurationAttributes = mergeSourceConfiguration( configuration, @@ -144,7 +148,7 @@ export class InfraSources { ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await requestContext.core.savedObjects.client.update( + await savedObjectsClient.update( infraSourceConfigurationSavedObjectType, sourceId, pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, @@ -199,10 +203,10 @@ export class InfraSources { } private async getSavedSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string ) { - const savedObject = await requestContext.core.savedObjects.client.get( + const savedObject = await savedObjectsClient.get( infraSourceConfigurationSavedObjectType, sourceId ); @@ -210,8 +214,8 @@ export class InfraSources { return convertSavedObjectToSavedSourceConfiguration(savedObject); } - private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) { - const savedObjects = await requestContext.core.savedObjects.client.find({ + private async getAllSavedSourceConfigurations(savedObjectsClient: SavedObjectsClientContract) { + const savedObjects = await savedObjectsClient.find({ type: infraSourceConfigurationSavedObjectType, }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e3804078604cc..db34033c1d4f8 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -109,7 +109,7 @@ export class InfraServerPlugin { sources, } ); - const snapshot = new InfraSnapshot({ sources, framework }); + const snapshot = new InfraSnapshot(); const logEntryCategoriesAnalysis = new LogEntryCategoriesAnalysis({ framework }); const logEntryRateAnalysis = new LogEntryRateAnalysis({ framework }); @@ -147,7 +147,7 @@ export class InfraServerPlugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerting); + registerAlertTypes(plugins.alerting, this.libs); // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts index 7e9b7ada28c8e..687e368736a41 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -39,7 +39,7 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { ); const { configuration } = await libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const awsMetadata = await getCloudMetadata( diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts index 3a6bdaf3804e3..85dba8f598a89 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/item.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/item.ts @@ -37,8 +37,9 @@ export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: Infr ); const { id, sourceId } = payload; - const sourceConfiguration = (await sources.getSourceConfiguration(requestContext, sourceId)) - .configuration; + const sourceConfiguration = ( + await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId) + ).configuration; const logEntry = await logEntries.getLogItem(requestContext, id, sourceConfiguration); diff --git a/x-pack/plugins/infra/server/routes/log_sources/configuration.ts b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts new file mode 100644 index 0000000000000..46929954431f5 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_sources/configuration.ts @@ -0,0 +1,114 @@ +/* + * 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 Boom from 'boom'; +import { + getLogSourceConfigurationRequestParamsRT, + getLogSourceConfigurationSuccessResponsePayloadRT, + LOG_SOURCE_CONFIGURATION_PATH, + patchLogSourceConfigurationRequestBodyRT, + patchLogSourceConfigurationRequestParamsRT, + patchLogSourceConfigurationSuccessResponsePayloadRT, +} from '../../../common/http_api/log_sources'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { InfraBackendLibs } from '../../lib/infra_types'; + +export const initLogSourceConfigurationRoutes = ({ framework, sources }: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'get', + path: LOG_SOURCE_CONFIGURATION_PATH, + validate: { + params: createValidationFunction(getLogSourceConfigurationRequestParamsRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { sourceId } = request.params; + + try { + const sourceConfiguration = await sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + + return response.ok({ + body: getLogSourceConfigurationSuccessResponsePayloadRT.encode({ + data: sourceConfiguration, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); + + framework.registerRoute( + { + method: 'patch', + path: LOG_SOURCE_CONFIGURATION_PATH, + validate: { + params: createValidationFunction(patchLogSourceConfigurationRequestParamsRT), + body: createValidationFunction(patchLogSourceConfigurationRequestBodyRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { sourceId } = request.params; + const { data: patchedSourceConfigurationProperties } = request.body; + + try { + const sourceConfiguration = await sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + + if (sourceConfiguration.origin === 'internal') { + response.conflict({ + body: 'A conflicting read-only source configuration already exists.', + }); + } + + const sourceConfigurationExists = sourceConfiguration.origin === 'stored'; + const patchedSourceConfiguration = await (sourceConfigurationExists + ? sources.updateSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId, + patchedSourceConfigurationProperties + ) + : sources.createSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId, + patchedSourceConfigurationProperties + )); + + return response.ok({ + body: patchLogSourceConfigurationSuccessResponsePayloadRT.encode({ + data: patchedSourceConfiguration, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; diff --git a/x-pack/plugins/infra/server/routes/log_sources/index.ts b/x-pack/plugins/infra/server/routes/log_sources/index.ts new file mode 100644 index 0000000000000..5b68152b329ef --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_sources/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './configuration'; +export * from './status'; diff --git a/x-pack/plugins/infra/server/routes/log_sources/status.ts b/x-pack/plugins/infra/server/routes/log_sources/status.ts new file mode 100644 index 0000000000000..cdd053d2bb10a --- /dev/null +++ b/x-pack/plugins/infra/server/routes/log_sources/status.ts @@ -0,0 +1,62 @@ +/* + * 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 Boom from 'boom'; +import { + getLogSourceStatusRequestParamsRT, + getLogSourceStatusSuccessResponsePayloadRT, + LOG_SOURCE_STATUS_PATH, +} from '../../../common/http_api/log_sources'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { InfraIndexType } from '../../graphql/types'; +import { InfraBackendLibs } from '../../lib/infra_types'; + +export const initLogSourceStatusRoutes = ({ + framework, + sourceStatus, + fields, +}: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'get', + path: LOG_SOURCE_STATUS_PATH, + validate: { + params: createValidationFunction(getLogSourceStatusRequestParamsRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { sourceId } = request.params; + + try { + const logIndexNames = await sourceStatus.getLogIndexNames(requestContext, sourceId); + const logIndexFields = + logIndexNames.length > 0 + ? await fields.getFields(requestContext, sourceId, InfraIndexType.LOGS) + : []; + + return response.ok({ + body: getLogSourceStatusSuccessResponsePayloadRT.encode({ + data: { + logIndexFields, + logIndexNames, + }, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index c45f191b1130d..fe142aa93dcda 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -44,7 +44,7 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { ); const { configuration } = await libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const metricsMetadata = await getMetricMetadata( diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts index 66f0ca8fc706a..94e91d32b14bb 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'kibana/server'; -import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; +import { ESSearchClient } from '../../../lib/snapshot'; interface EventDatasetHit { _source: { @@ -16,8 +15,7 @@ interface EventDatasetHit { } export const getDatasetForField = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, field: string, indexPattern: string ) => { @@ -33,11 +31,8 @@ export const getDatasetForField = async ( }, }; - const response = await framework.callWithRequest<EventDatasetHit>( - requestContext, - 'search', - params - ); + const response = await client<EventDatasetHit>(params); + if (response.hits.total.value === 0) { return null; } diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 3517800ea0dd1..a709cbdeeb680 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -17,6 +17,10 @@ import { createMetricModel } from './create_metrics_model'; import { JsonObject } from '../../../../common/typed_json'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; import { getDatasetForField } from './get_dataset_for_field'; +import { + CallWithRequestParams, + InfraDatabaseSearchResponse, +} from '../../../lib/adapters/framework'; export const populateSeriesWithTSVBData = ( request: KibanaRequest, @@ -52,17 +56,21 @@ export const populateSeriesWithTSVBData = ( } const timerange = { min: options.timerange.from, max: options.timerange.to }; + const client = <Hit = {}, Aggregation = undefined>( + opts: CallWithRequestParams + ): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> => + framework.callWithRequest(requestContext, 'search', opts); + // Create the TSVB model based on the request options const model = createMetricModel(options); const modules = await Promise.all( uniq(options.metrics.filter(m => m.field)).map( - async m => - await getDatasetForField(framework, requestContext, m.field as string, options.indexPattern) + async m => await getDatasetForField(client, m.field as string, options.indexPattern) ) ); + const calculatedInterval = await calculateMetricInterval( - framework, - requestContext, + client, { indexPattern: options.indexPattern, timestampField: options.timerange.field, @@ -72,7 +80,9 @@ export const populateSeriesWithTSVBData = ( ); if (calculatedInterval) { - model.interval = `>=${calculatedInterval}s`; + model.interval = options.forceInterval + ? options.timerange.interval + : `>=${calculatedInterval}s`; } // Get TSVB results using the model, timerange and filters diff --git a/x-pack/plugins/infra/server/routes/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index 36906f6f4125b..a457ccac2416c 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -37,7 +37,10 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { NodeDetailsRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); UsageCollector.countNode(nodeType); diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index e45b9884967d0..2d951d426b03a 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -13,6 +13,7 @@ import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../../lib/adapters/framework'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); @@ -42,7 +43,10 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { SnapshotRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); UsageCollector.countNode(nodeType); const options = { filterQuery: parseFilterQuery(filterQuery), @@ -54,7 +58,13 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { metric, timerange, }; - const nodesWithInterval = await libs.snapshot.getNodes(requestContext, options); + + const searchES = <Hit = {}, Aggregation = undefined>( + opts: CallWithRequestParams + ): Promise<InfraDatabaseSearchResponse<Hit, Aggregation>> => + framework.callWithRequest(requestContext, 'search', opts); + + const nodesWithInterval = await libs.snapshot.getNodes(searchES, options); return response.ok({ body: SnapshotNodeResponseRT.encode(nodesWithInterval), }); diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 2f29320d7bb81..62b7fd7ba902f 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -37,7 +37,10 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { try { const { type, sourceId } = request.params; - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); if (!source) { return response.notFound(); } diff --git a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts index 7cbbdc0f2145b..43e109b009f48 100644 --- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext } from 'src/core/server'; +// import { RequestHandlerContext } from 'src/core/server'; import { findInventoryModel } from '../../common/inventory_models'; -import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; +// import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; import { InventoryItemType } from '../../common/inventory_models/types'; +import { ESSearchClient } from '../lib/snapshot'; interface Options { indexPattern: string; @@ -23,8 +24,7 @@ interface Options { * This is useful for visualizing metric modules like s3 that only send metrics once per day. */ export const calculateMetricInterval = async ( - framework: KibanaFramework, - requestContext: RequestHandlerContext, + client: ESSearchClient, options: Options, modules?: string[], nodeType?: InventoryItemType // TODO: check that this type still makes sense @@ -73,11 +73,7 @@ export const calculateMetricInterval = async ( }, }; - const resp = await framework.callWithRequest<{}, PeriodAggregationData>( - requestContext, - 'search', - query - ); + const resp = await client<{}, PeriodAggregationData>(query); // if ES doesn't return an aggregations key, something went seriously wrong. if (!resp.aggregations) { diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index 07acdf8affd49..0e7abcc3d74a9 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -1,60 +1,70 @@ # Ingest Manager + ## Plugin - - No features enabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/feature-ingest/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L19) - - Setting `xpack.ingestManager.enabled=true` is required to enable the plugin. It adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) - - Adding `--xpack.ingestManager.epm.enabled=true` will add the EPM API & UI - - Adding `--xpack.ingestManager.fleet.enabled=true` will add the Fleet API & UI - - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) - - [Integration tests](server/integration_tests/router.test.ts) - - Both EPM and Fleet require `ingestManager` be enabled. They are not standalone features. + +- The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) +- Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) +- Adding `--xpack.ingestManager.epm.enabled=false` will disable the EPM API & UI +- Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI + - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) + - [Integration tests](server/integration_tests/router.test.ts) +- Both EPM and Fleet require `ingestManager` be enabled. They are not standalone features. ## Development ### Getting started -See the Kibana docs for [how to set up your dev environment](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#setting-up-your-development-environment), [run Elasticsearch](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-elasticsearch), and [start Kibana](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-kibana) +See the Kibana docs for [how to set up your dev environment](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#setting-up-your-development-environment), [run Elasticsearch](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-elasticsearch), and [start Kibana](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-kibana) One common development workflow is: - - Bootstrap Kibana - ``` - yarn kbn bootstrap - ``` - - Start Elasticsearch in one shell - ``` - yarn es snapshot -E xpack.security.authc.api_key.enabled=true - ``` - - Start Kibana in another shell - ``` - yarn start --xpack.ingestManager.enabled=true --xpack.ingestManager.epm.enabled=true --xpack.ingestManager.fleet.enabled=true --no-base-path --xpack.endpoint.enabled=true - ``` + +- Bootstrap Kibana + ``` + yarn kbn bootstrap + ``` +- Start Elasticsearch in one shell + ``` + yarn es snapshot -E xpack.security.authc.api_key.enabled=true + ``` +- Start Kibana in another shell + ``` + yarn start --xpack.ingestManager.enabled=true --no-base-path --xpack.endpoint.enabled=true + ``` This plugin follows the `common`, `server`, `public` structure from the [Architecture Style Guide ](https://github.com/elastic/kibana/blob/master/style_guides/architecture_style_guide.md#file-and-folder-structure). We also follow the pattern of developing feature branches under your personal fork of Kibana. ### API Tests + #### Ingest & Fleet - 1. In one terminal, change to the `x-pack` directory and start the test server with - ``` - node scripts/functional_tests_server.js --config test/api_integration/config.ts - ``` - - 1. in a second terminal, run the tests from the Kibana root directory with - ``` - node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.ts - ``` + +1. In one terminal, change to the `x-pack` directory and start the test server with + + ``` + node scripts/functional_tests_server.js --config test/api_integration/config.js + ``` + +1. in a second terminal, run the tests from the Kibana root directory with + ``` + node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.js + ``` + #### EPM - 1. In one terminal, change to the `x-pack` directory and start the test server with - ``` - node scripts/functional_tests_server.js --config test/epm_api_integration/config.ts - ``` - 1. in a second terminal, run the tests from the Kibana root directory with - ``` - node scripts/functional_test_runner.js --config x-pack/test/epm_api_integration/config.ts - ``` +1. In one terminal, change to the `x-pack` directory and start the test server with + + ``` + node scripts/functional_tests_server.js --config test/epm_api_integration/config.ts + ``` - ### Staying up-to-date with `master` - While we're developing in the `feature-ingest` feature branch, here's is more information on keeping up to date with upstream kibana. +1. in a second terminal, run the tests from the Kibana root directory with + ``` + node scripts/functional_test_runner.js --config x-pack/test/epm_api_integration/config.ts + ``` + +### Staying up-to-date with `master` + +While we're developing in the `feature-ingest` feature branch, here's is more information on keeping up to date with upstream kibana. <details> <summary>merge upstream <code>master</code> into <code>feature-ingest</code></summary> @@ -80,6 +90,7 @@ git push origin ## push your changes to upstream feature branch from the terminal; not GitHub UI git push upstream ``` + </details> See https://github.com/elastic/kibana/pull/37950 for an example. diff --git a/x-pack/plugins/ingest_manager/common/constants/agent.ts b/x-pack/plugins/ingest_manager/common/constants/agent.ts index 0b462fb4c0319..f3990ba78c539 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export const AGENT_SAVED_OBJECT_TYPE = 'agents'; -export const AGENT_EVENT_SAVED_OBJECT_TYPE = 'agent_events'; -export const AGENT_ACTION_SAVED_OBJECT_TYPE = 'agent_actions'; +export const AGENT_SAVED_OBJECT_TYPE = 'fleet-agents'; +export const AGENT_EVENT_SAVED_OBJECT_TYPE = 'fleet-agent-events'; +export const AGENT_ACTION_SAVED_OBJECT_TYPE = 'fleet-agent-actions'; export const AGENT_TYPE_PERMANENT = 'PERMANENT'; export const AGENT_TYPE_EPHEMERAL = 'EPHEMERAL'; 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 337022e552278..9bc1293799d3c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -5,7 +5,7 @@ */ import { AgentConfigStatus, DefaultPackages } from '../types'; -export const AGENT_CONFIG_SAVED_OBJECT_TYPE = 'agent_configs'; +export const AGENT_CONFIG_SAVED_OBJECT_TYPE = 'ingest-agent-configs'; export const DEFAULT_AGENT_CONFIG = { name: 'Default config', @@ -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/constants/datasource.ts b/x-pack/plugins/ingest_manager/common/constants/datasource.ts index 0ff472b2afeb0..08113cff53bda 100644 --- a/x-pack/plugins/ingest_manager/common/constants/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/constants/datasource.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const DATASOURCE_SAVED_OBJECT_TYPE = 'datasources'; +export const DATASOURCE_SAVED_OBJECT_TYPE = 'ingest-datasources'; diff --git a/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts index f4a4bcde2f393..fd28b6632b15c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'enrollment_api_keys'; +export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'fleet-enrollment-api-keys'; diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index eb72c28e7bf39..4fb259609493d 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-package'; +export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; +export const DEFAULT_REGISTRY_URL = 'https://epr.elastic.co'; diff --git a/x-pack/plugins/ingest_manager/common/constants/index.ts b/x-pack/plugins/ingest_manager/common/constants/index.ts index 45d315e6d5664..6a2e559bbbe4f 100644 --- a/x-pack/plugins/ingest_manager/common/constants/index.ts +++ b/x-pack/plugins/ingest_manager/common/constants/index.ts @@ -12,3 +12,4 @@ export * from './datasource'; export * from './epm'; export * from './output'; export * from './enrollment_api_key'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/common/constants/output.ts b/x-pack/plugins/ingest_manager/common/constants/output.ts index 6060a2b63fc8e..ac2d6117be921 100644 --- a/x-pack/plugins/ingest_manager/common/constants/output.ts +++ b/x-pack/plugins/ingest_manager/common/constants/output.ts @@ -5,12 +5,11 @@ */ import { OutputType } from '../types'; -export const OUTPUT_SAVED_OBJECT_TYPE = 'outputs'; +export const OUTPUT_SAVED_OBJECT_TYPE = 'ingest-outputs'; export const DEFAULT_OUTPUT = { name: 'default', is_default: true, type: OutputType.Elasticsearch, hosts: [''], - api_key: '', }; diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index a31d38a723c2c..35e3be98e3982 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -5,9 +5,10 @@ */ // Base API paths export const API_ROOT = `/api/ingest_manager`; +export const EPM_API_ROOT = `${API_ROOT}/epm`; +export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`; export const DATASOURCE_API_ROOT = `${API_ROOT}/datasources`; export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; -export const EPM_API_ROOT = `${API_ROOT}/epm`; export const FLEET_API_ROOT = `${API_ROOT}/fleet`; // EPM API routes @@ -23,6 +24,11 @@ export const EPM_API_ROUTES = { CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, }; +// Data stream API routes +export const DATA_STREAM_API_ROUTES = { + LIST_PATTERN: `${DATA_STREAM_API_ROOT}`, +}; + // Datasource API routes export const DATASOURCE_API_ROUTES = { LIST_PATTERN: `${DATASOURCE_API_ROOT}`, @@ -42,6 +48,19 @@ export const AGENT_CONFIG_API_ROUTES = { FULL_INFO_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}/full`, }; +// Output API routes +export const OUTPUT_API_ROUTES = { + LIST_PATTERN: `${API_ROOT}/outputs`, + INFO_PATTERN: `${API_ROOT}/outputs/{outputId}`, + UPDATE_PATTERN: `${API_ROOT}/outputs/{outputId}`, +}; + +// Settings API routes +export const SETTINGS_API_ROUTES = { + INFO_PATTERN: `${API_ROOT}/settings`, + UPDATE_PATTERN: `${API_ROOT}/settings`, +}; + // Agent API routes export const AGENT_API_ROUTES = { LIST_PATTERN: `${FLEET_API_ROOT}/agents`, @@ -53,7 +72,8 @@ export const AGENT_API_ROUTES = { ACKS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/acks`, ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`, ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`, - UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/unenroll`, + UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/unenroll`, + REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`, STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`, }; diff --git a/x-pack/plugins/ingest_manager/common/constants/settings.ts b/x-pack/plugins/ingest_manager/common/constants/settings.ts new file mode 100644 index 0000000000000..a9e7f1df4119c --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/constants/settings.ts @@ -0,0 +1,7 @@ +/* + * 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 const GLOBAL_SETTINGS_SAVED_OBJECT_TYPE = 'ingest_manager_settings'; diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index 7bbac55f11937..36e6e84b35bfe 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -8,20 +8,22 @@ import { AGENT_TYPE_TEMPORARY, AGENT_POLLING_THRESHOLD_MS, AGENT_TYPE_PERMANENT, + AGENT_SAVED_OBJECT_TYPE, } from '../constants'; export function buildKueryForOnlineAgents() { - return `agents.last_checkin >= now-${(3 * AGENT_POLLING_THRESHOLD_MS) / 1000}s`; + return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${(3 * AGENT_POLLING_THRESHOLD_MS) / + 1000}s`; } export function buildKueryForOfflineAgents() { - return `agents.type:${AGENT_TYPE_TEMPORARY} AND agents.last_checkin < now-${(3 * + return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${(3 * AGENT_POLLING_THRESHOLD_MS) / 1000}s`; } export function buildKueryForErrorAgents() { - return `agents.type:${AGENT_TYPE_PERMANENT} AND agents.last_checkin < now-${(4 * + return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${(4 * AGENT_POLLING_THRESHOLD_MS) / 1000}s`; } 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 3496ea782ee99..bff799798ff6e 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 { NewDatasource, DatasourceInput } from '../types'; +import { Datasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { - const mockDatasource: NewDatasource = { + const mockDatasource: Datasource = { + id: 'some-uuid', name: 'mock-datasource', description: '', config_id: '', @@ -15,12 +16,13 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { output_id: '', namespace: 'default', inputs: [], + revision: 1, }; const mockInput: DatasourceInput = { type: 'test-logs', enabled: true, - config: { + vars: { inputVar: { value: 'input-value' }, inputVar2: { value: undefined }, inputVar3: { @@ -34,11 +36,11 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { id: 'test-logs-foo', enabled: true, dataset: 'foo', - config: { + vars: { fooVar: { value: 'foo-value' }, fooVar2: { value: [1, 2] }, }, - pkg_stream: { + agent_stream: { fooKey: 'fooValue1', fooKey2: ['fooValue2'], }, @@ -47,7 +49,7 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { id: 'test-logs-bar', enabled: true, dataset: 'bar', - config: { + vars: { barVar: { value: 'bar-value' }, barVar2: { value: [1, 2] }, barVar3: { @@ -70,7 +72,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { it('returns agent datasource config for datasource with no inputs', () => { expect(storedDatasourceToAgentDatasource(mockDatasource)).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -87,7 +90,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }, }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -101,7 +105,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -140,7 +145,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { ], }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -169,7 +175,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { inputs: [{ ...mockInput, enabled: false }], }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', 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 b509878b7f945..620b663451ea3 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,17 @@ * 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: name, + id: id || name, + name, namespace, enabled, use_output: DEFAULT_OUTPUT.name, // TODO: hardcoded to default output for now @@ -21,18 +22,28 @@ export const storedDatasourceToAgentDatasource = ( .map(input => { const fullInput = { ...input, + ...Object.entries(input.config || {}).reduce((acc, [key, { value }]) => { + acc[key] = value; + return acc; + }, {} as { [k: string]: any }), streams: input.streams .filter(stream => stream.enabled) .map(stream => { const fullStream = { ...stream, - ...stream.pkg_stream, + ...stream.agent_stream, + ...Object.entries(stream.config || {}).reduce((acc, [key, { value }]) => { + acc[key] = value; + return acc; + }, {} as { [k: string]: any }), }; - delete fullStream.pkg_stream; + delete fullStream.agent_stream; + delete fullStream.vars; delete fullStream.config; return fullStream; }), }; + delete fullInput.vars; delete fullInput.config; return fullInput; }), 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 5fa7af2dda79a..a977a1a66e059 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: [], @@ -132,7 +133,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'foo-foo', enabled: true, dataset: 'foo', - config: { 'var-name': { value: 'foo-var-value' } }, + vars: { 'var-name': { value: 'foo-var-value' } }, }, ], }, @@ -144,13 +145,13 @@ describe('Ingest Manager - packageToConfig', () => { id: 'bar-bar', enabled: true, dataset: 'bar', - config: { 'var-name': { type: 'text', value: 'bar-var-value' } }, + vars: { 'var-name': { type: 'text', value: 'bar-var-value' } }, }, { id: 'bar-bar2', enabled: true, dataset: 'bar2', - config: { 'var-name': { type: 'yaml', value: 'bar2-var-value' } }, + vars: { 'var-name': { type: 'yaml', value: 'bar2-var-value' } }, }, ], }, @@ -205,7 +206,7 @@ describe('Ingest Manager - packageToConfig', () => { { type: 'foo', enabled: true, - config: { + vars: { 'foo-input-var-name': { value: 'foo-input-var-value' }, 'foo-input2-var-name': { value: 'foo-input2-var-value' }, 'foo-input3-var-name': { value: undefined }, @@ -215,7 +216,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'foo-foo', enabled: true, dataset: 'foo', - config: { + vars: { 'var-name': { value: 'foo-var-value' }, }, }, @@ -224,7 +225,7 @@ describe('Ingest Manager - packageToConfig', () => { { type: 'bar', enabled: true, - config: { + vars: { 'bar-input-var-name': { value: ['value1', 'value2'] }, 'bar-input2-var-name': { value: 123456 }, }, @@ -233,7 +234,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'bar-bar', enabled: true, dataset: 'bar', - config: { + vars: { 'var-name': { value: 'bar-var-value' }, }, }, @@ -241,7 +242,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'bar-bar2', enabled: true, dataset: 'bar2', - config: { + vars: { 'var-name': { value: 'bar2-var-value' }, }, }, @@ -255,7 +256,7 @@ describe('Ingest Manager - packageToConfig', () => { id: 'with-disabled-streams-disabled', enabled: false, dataset: 'disabled', - config: { + vars: { 'var-name': { value: [] }, }, }, diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts index fa3479a69e39d..e7a912ddf1741 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_config.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_config.ts @@ -53,7 +53,7 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas dataset: packageStream.dataset, }; if (packageStream.vars && packageStream.vars.length) { - stream.config = packageStream.vars.reduce(varsReducer, {}); + stream.vars = packageStream.vars.reduce(varsReducer, {}); } return stream; }) @@ -66,7 +66,7 @@ export const packageToConfigDatasourceInputs = (packageInfo: PackageInfo): Datas }; if (packageInput.vars && packageInput.vars.length) { - input.config = packageInput.vars.reduce(varsReducer, {}); + input.vars = packageInput.vars.reduce(varsReducer, {}); } inputs.push(input); diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index 7cc6fc3c66afb..1a1bd7c65aa25 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -8,10 +8,13 @@ import { EPM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_CONFIG_API_ROUTES, + DATA_STREAM_API_ROUTES, FLEET_SETUP_API_ROUTES, AGENT_API_ROUTES, ENROLLMENT_API_KEY_ROUTES, SETUP_API_ROUTE, + OUTPUT_API_ROUTES, + SETTINGS_API_ROUTES, } from '../constants'; export const epmRouteService = { @@ -88,6 +91,12 @@ export const agentConfigRouteService = { }, }; +export const dataStreamRouteService = { + getListPath: () => { + return DATA_STREAM_API_ROUTES.LIST_PATTERN; + }, +}; + export const fleetSetupRouteService = { getFleetSetupPath: () => FLEET_SETUP_API_ROUTES.INFO_PATTERN, postFleetSetupPath: () => FLEET_SETUP_API_ROUTES.CREATE_PATTERN, @@ -97,11 +106,26 @@ export const agentRouteService = { getInfoPath: (agentId: string) => AGENT_API_ROUTES.INFO_PATTERN.replace('{agentId}', agentId), getUpdatePath: (agentId: string) => AGENT_API_ROUTES.UPDATE_PATTERN.replace('{agentId}', agentId), getEventsPath: (agentId: string) => AGENT_API_ROUTES.EVENTS_PATTERN.replace('{agentId}', agentId), - getUnenrollPath: () => AGENT_API_ROUTES.UNENROLL_PATTERN, + getUnenrollPath: (agentId: string) => + AGENT_API_ROUTES.UNENROLL_PATTERN.replace('{agentId}', agentId), + getReassignPath: (agentId: string) => + AGENT_API_ROUTES.REASSIGN_PATTERN.replace('{agentId}', agentId), getListPath: () => AGENT_API_ROUTES.LIST_PATTERN, getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN, }; +export const outputRoutesService = { + getInfoPath: (outputId: string) => OUTPUT_API_ROUTES.INFO_PATTERN.replace('{outputId}', outputId), + getUpdatePath: (outputId: string) => + OUTPUT_API_ROUTES.UPDATE_PATTERN.replace('{outputId}', outputId), + getListPath: () => OUTPUT_API_ROUTES.LIST_PATTERN, +}; + +export const settingsRoutesService = { + getInfoPath: () => SETTINGS_API_ROUTES.INFO_PATTERN, + getUpdatePath: () => SETTINGS_API_ROUTES.UPDATE_PATTERN, +}; + export const enrollmentAPIKeyRouteService = { getListPath: () => ENROLLMENT_API_KEY_ROUTES.LIST_PATTERN, getCreatePath: () => ENROLLMENT_API_KEY_ROUTES.CREATE_PATTERN, diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 42f7a9333118e..748bb14d2d35d 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -10,7 +10,7 @@ export interface IngestManagerConfigType { enabled: boolean; epm: { enabled: boolean; - registryUrl: string; + registryUrl?: string; }; fleet: { enabled: boolean; 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 4d03a30f9a590..e3ca7635fdb40 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -20,15 +20,18 @@ export interface NewAgentAction { sent_at?: string; } -export type AgentAction = NewAgentAction & { +export interface AgentAction extends NewAgentAction { id: string; agent_id: string; created_at: string; -} & SavedObjectAttributes; +} -export interface AgentActionSOAttributes extends NewAgentAction, SavedObjectAttributes { +export interface AgentActionSOAttributes extends SavedObjectAttributes { + type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE'; + sent_at?: string; created_at: string; agent_id: string; + data?: string; } export interface AgentEvent { @@ -57,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; @@ -64,23 +72,22 @@ interface AgentBase { shared_id?: string; access_api_key_id?: string; default_api_key?: string; + default_api_key_id?: string; config_id?: string; - config_revision?: number; + 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<string, string>; - local_metadata: Record<string, string>; 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/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 002c3784446a8..96121251b133e 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, @@ -23,9 +21,10 @@ export interface NewAgentConfig { namespace?: string; description?: string; is_default?: boolean; + monitoring_enabled?: Array<'logs' | 'metrics'>; } -export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { +export interface AgentConfig extends NewAgentConfig { id: string; status: AgentConfigStatus; datasources: string[] | Datasource[]; @@ -34,8 +33,10 @@ export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { revision: number; } -export type FullAgentConfigDatasource = Pick<Datasource, 'namespace' | 'enabled'> & { - id: string; +export type FullAgentConfigDatasource = Pick< + Datasource, + 'id' | 'name' | 'namespace' | 'enabled' +> & { package?: Pick<DatasourcePackage, 'name' | 'version'>; use_output: string; inputs: Array< @@ -58,4 +59,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/common/types/models/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts new file mode 100644 index 0000000000000..7da9bbad1b170 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts @@ -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. + */ + +export interface DataStream { + index: string; + dataset: string; + namespace: string; + type: string; + package: string; + last_activity: string; + size_in_bytes: number; +} 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 48243a12120f9..ca61a93d9be93 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/datasource.ts @@ -17,20 +17,29 @@ export interface DatasourceConfigRecordEntry { export type DatasourceConfigRecord = Record<string, DatasourceConfigRecordEntry>; -export interface DatasourceInputStream { +export interface NewDatasourceInputStream { id: string; enabled: boolean; dataset: string; processors?: string[]; config?: DatasourceConfigRecord; - pkg_stream?: any; + vars?: DatasourceConfigRecord; } -export interface DatasourceInput { +export interface DatasourceInputStream extends NewDatasourceInputStream { + agent_stream?: any; +} + +export interface NewDatasourceInput { type: string; enabled: boolean; processors?: string[]; config?: DatasourceConfigRecord; + vars?: DatasourceConfigRecord; + streams: NewDatasourceInputStream[]; +} + +export interface DatasourceInput extends Omit<NewDatasourceInput, 'streams'> { streams: DatasourceInputStream[]; } @@ -42,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<NewDatasource, 'inputs'> { id: string; + inputs: DatasourceInput[]; revision: number; -}; +} 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 53ad0310ea613..82de90e4735f2 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -32,6 +32,7 @@ export enum KibanaAssetType { } export enum ElasticsearchAssetType { + componentTemplate = 'component-template', ingestPipeline = 'ingest-pipeline', indexTemplate = 'index-template', ilmPolicy = 'ilm-policy', @@ -57,6 +58,7 @@ export interface RegistryPackage { icons?: RegistryImage[]; assets?: string[]; internal?: boolean; + removable?: boolean; format_version: string; datasets?: Dataset[]; datasources?: RegistryDatasource[]; @@ -95,6 +97,7 @@ export interface RegistryStream { description?: string; enabled?: boolean; vars?: RegistryVarsEntry[]; + template?: string; } export type RequirementVersion = string; @@ -201,6 +204,7 @@ export interface RegistryVarsEntry { // internal until we need them interface PackageAdditions { title: string; + latestVersion: string; assets: AssetsGroupedByServiceByType; } @@ -245,6 +249,7 @@ export enum IngestAssetType { DataFrameTransform = 'data-frame-transform', IlmPolicy = 'ilm-policy', IndexTemplate = 'index-template', + ComponentTemplate = 'component-template', IngestPipeline = 'ingest-pipeline', MlJob = 'ml-job', RollupJob = 'rollup-job', @@ -256,10 +261,24 @@ export enum DefaultPackages { endpoint = 'endpoint', } +export interface IndexTemplateMappings { + properties: any; +} + +// This is an index template v2, see https://github.com/elastic/elasticsearch/issues/53101 +// until "proper" documentation of the new format is available. +// Ingest Manager does not use nor support the legacy index template v1 format at all export interface IndexTemplate { - order: number; + priority: number; index_patterns: string[]; - settings: any; - mappings: object; - aliases: object; + template: { + settings: any; + mappings: object; + aliases: object; + }; +} + +export interface TemplateRef { + templateName: string; + indexTemplate: IndexTemplate; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/index.ts b/x-pack/plugins/ingest_manager/common/types/models/index.ts index 579b510e52daa..2310fdd54a719 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/index.ts @@ -7,6 +7,8 @@ export * from './agent'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './output'; export * from './epm'; export * from './enrollment_api_key'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/common/types/models/settings.ts b/x-pack/plugins/ingest_manager/common/types/models/settings.ts new file mode 100644 index 0000000000000..2921808230b47 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/settings.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectAttributes } from 'src/core/public'; + +interface BaseSettings { + agent_auto_upgrade?: boolean; + package_auto_upgrade?: boolean; + kibana_url?: string; + kibana_ca_sha256?: string; +} + +export interface Settings extends BaseSettings { + id: string; +} + +export interface SettingsSOAttributes extends BaseSettings, SavedObjectAttributes {} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 21ab41740ce3e..64ed95db74f4c 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -96,16 +96,23 @@ export interface PostNewAgentActionResponse { } export interface PostAgentUnenrollRequest { - body: { kuery: string } | { ids: string[] }; + params: { + agentId: string; + }; } export interface PostAgentUnenrollResponse { - results: Array<{ - success: boolean; - error?: any; - id: string; - action: string; - }>; + success: boolean; +} + +export interface PutAgentReassignRequest { + params: { + agentId: string; + }; + body: { config_id: string }; +} + +export interface PutAgentReassignResponse { success: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts index 89d548d11dadb..82d7fa51b2082 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent_config.ts @@ -49,16 +49,16 @@ export interface UpdateAgentConfigResponse { success: boolean; } -export interface DeleteAgentConfigsRequest { +export interface DeleteAgentConfigRequest { body: { - agentConfigIds: string[]; + agentConfigId: string; }; } -export type DeleteAgentConfigsResponse = Array<{ +export interface DeleteAgentConfigResponse { id: string; success: boolean; -}>; +} export interface GetFullAgentConfigRequest { params: { diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts new file mode 100644 index 0000000000000..24f8110562bfc --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DataStream } from '../models'; + +export interface GetDataStreamsResponse { + data_streams: DataStream[]; +} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts index dc1d748a8743a..c4ba8ee595acf 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts @@ -4,16 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface GetFleetSetupRequest {} - -export interface CreateFleetSetupRequest { - body: { - fleet_enroll_username: string; - fleet_enroll_password: string; - }; -} - export interface CreateFleetSetupResponse { isInitialized: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts index abe1bc8e3eddb..763fb7d820b2a 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts @@ -5,9 +5,12 @@ */ export * from './common'; export * from './datasource'; +export * from './data_stream'; export * from './agent'; export * from './agent_config'; export * from './fleet_setup'; export * from './epm'; export * from './enrollment_api_key'; export * from './install_script'; +export * from './output'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts new file mode 100644 index 0000000000000..4162060363381 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Output } from '../models'; + +export interface GetOneOutputResponse { + item: Output; + success: boolean; +} + +export interface GetOneOutputRequest { + params: { + outputId: string; + }; +} + +export interface PutOutputRequest { + params: { + outputId: string; + }; + body: { + hosts?: string[]; + ca_sha256?: string; + }; +} + +export interface PutOutputResponse { + item: Output; + success: boolean; +} + +export interface GetOutputsResponse { + items: Output[]; + total: number; + page: number; + perPage: number; + success: boolean; +} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts new file mode 100644 index 0000000000000..c02a5e5878ee9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Settings } from '../models'; + +export interface GetSettingsResponse { + item: Settings; + success: boolean; +} + +export interface PutSettingsRequest { + body: Partial<Omit<Settings, 'id'>>; +} + +export interface PutSettingsResponse { + item: Settings; + success: boolean; +} diff --git a/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md b/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md index 38f80a8bdc022..fdb54411f8610 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md +++ b/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md @@ -18,5 +18,5 @@ ## Example ```js -GET /api/ingest_manager/fleet/agents?kuery=agents.last_checkin:2019-10-01T13:42:54.323Z +GET /api/ingest_manager/fleet/agents?kuery=fleet-agents.last_checkin:2019-10-01T13:42:54.323Z ``` 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 0000000000000..1e7a14e350229 --- /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<Props> = ({ onClose }) => { + return ( + <EuiFlyout onClose={onClose} size="m" maxWidth={640}> + <EuiFlyoutHeader hasBorder aria-labelledby="AlphaMessagingFlyoutTitle"> + <EuiTitle size="m"> + <h2 id="AlphaMessagingFlyoutTitle"> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.flyoutTitle" + defaultMessage="About this release" + /> + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiText size="m"> + <p> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.introText" + defaultMessage="This release is experimental and is not subject to the support SLA. It is designed for users to test and offer feedback about Ingest + Manager and the new Elastic Agent. It is not intended for use in production environments since certain features may change or go away in a future release." + /> + </p> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.feedbackText" + defaultMessage="We encourage you to read our {docsLink} or to ask questions and send feedback in our {forumLink}." + values={{ + docsLink: ( + <EuiLink href="https://ela.st/ingest-manager-docs" external target="_blank"> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.docsLink" + defaultMessage="documentation" + /> + </EuiLink> + ), + forumLink: ( + <EuiLink href="https://ela.st/ingest-manager-forum" external target="_blank"> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.forumLink" + defaultMessage="Discuss forum" + /> + </EuiLink> + ), + }} + /> + <p /> + + <p> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.warningText" + defaultMessage="{note}: you should not store important data with Ingest Manager + since you will have limited visibility to it in a future release. This version uses an + indexing strategy that will be deprecated in a future release and there is no migration + path. Also, licensing for certain features is under consideration and may change in the future. As a result, you may lose access to certain features based on your license + tier." + values={{ + note: ( + <strong> + <FormattedMessage + id="xpack.ingestManager.alphaMessaging.warningNote" + defaultMessage="Note" + /> + </strong> + ), + }} + /> + </p> + </EuiText> + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> + <FormattedMessage + id="xpack.ingestManager.alphaMessging.closeFlyoutLabel" + defaultMessage="Close" + /> + </EuiButtonEmpty> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; 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 new file mode 100644 index 0000000000000..5a06a9a879441 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/alpha_messaging.tsx @@ -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 React, { useState } from 'react'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +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<{}> = () => { + const [isAlphaFlyoutOpen, setIsAlphaFlyoutOpen] = useState<boolean>(false); + + return ( + <> + <Message> + <p> + <strong> + <FormattedMessage + id="xpack.ingestManager.alphaMessageTitle" + defaultMessage="Experimental" + /> + </strong> + {' – '} + <FormattedMessage + id="xpack.ingestManager.alphaMessageDescription" + defaultMessage="Ingest Manager is under active development and is not + intended for production purposes." + />{' '} + <EuiLink color="subdued" onClick={() => setIsAlphaFlyoutOpen(true)}> + View more details. + </EuiLink> + </p> + </Message> + {isAlphaFlyoutOpen && <AlphaFlyout onClose={() => setIsAlphaFlyoutOpen(false)} />} + </> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx index 1aab6d901a992..ceb87fb048ae3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx @@ -31,7 +31,7 @@ const Tabs = styled(EuiTabs)` `; export interface HeaderProps { - restrictHeaderWidth?: number; + maxWidth?: number; leftColumn?: JSX.Element; rightColumn?: JSX.Element; rightColumnGrow?: EuiFlexItemProps['grow']; @@ -52,10 +52,10 @@ export const Header: React.FC<HeaderProps> = ({ rightColumn, rightColumnGrow, tabs, - restrictHeaderWidth, + maxWidth, }) => ( <Container> - <Wrapper maxWidth={restrictHeaderWidth}> + <Wrapper maxWidth={maxWidth}> <HeaderColumns leftColumn={leftColumn} rightColumn={rightColumn} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts index 5551bff2c8bde..b0b4e79cece79 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts @@ -6,3 +6,5 @@ export { Loading } from './loading'; export { Error } from './error'; export { Header, HeaderProps } from './header'; +export { AlphaMessaging } from './alpha_messaging'; +export * from './settings_flyout'; 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 41c24dadba068..1c9bd9107515d 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 @@ -8,12 +8,11 @@ import React, { useState, useEffect } from 'react'; import { IFieldType } from 'src/plugins/data/public'; // @ts-ignore import { EuiSuggest, EuiSuggestItemProps } from '@elastic/eui'; -import { useDebounce } from '../hooks'; -import { useStartDeps } from '../hooks/use_deps'; -import { INDEX_NAME } from '../constants'; +import { useDebounce, useStartDeps } from '../hooks'; +import { INDEX_NAME, AGENT_SAVED_OBJECT_TYPE } from '../constants'; const DEBOUNCE_SEARCH_MS = 150; -const HIDDEN_FIELDS = ['agents.actions']; +const HIDDEN_FIELDS = [`${AGENT_SAVED_OBJECT_TYPE}.actions`]; interface Suggestion { label: string; @@ -93,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/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx new file mode 100644 index 0000000000000..9863463e68a01 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -0,0 +1,240 @@ +/* + * 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, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiButton, + EuiFlyoutFooter, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiRadioGroup, + EuiComboBox, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText } from '@elastic/eui'; +import { useInput, useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks'; +import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs'; + +interface Props { + onClose: () => void; +} + +function useSettingsForm(outputId: string | undefined) { + const { notifications } = useCore(); + const kibanaUrlInput = useInput(); + const elasticsearchUrlInput = useComboInput([]); + + return { + onSubmit: async () => { + try { + if (!outputId) { + throw new Error('Unable to load outputs'); + } + await sendPutOutput(outputId, { + hosts: elasticsearchUrlInput.value, + }); + await sendPutSettings({ + kibana_url: kibanaUrlInput.value, + }); + } catch (error) { + notifications.toasts.addError(error, { + title: 'Error', + }); + } + notifications.toasts.addSuccess( + i18n.translate('xpack.ingestManager.settings.success.message', { + defaultMessage: 'Settings saved', + }) + ); + }, + inputs: { + kibanaUrl: kibanaUrlInput, + elasticsearchUrl: elasticsearchUrlInput, + }, + }; +} + +export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => { + const core = useCore(); + const settingsRequest = useGetSettings(); + const settings = settingsRequest?.data?.item; + const outputsRequest = useGetOutputs(); + const output = outputsRequest.data?.items?.[0]; + const { inputs, onSubmit } = useSettingsForm(output?.id); + + useEffect(() => { + if (output) { + inputs.elasticsearchUrl.setValue(output.hosts || []); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [output]); + + useEffect(() => { + if (settings) { + inputs.kibanaUrl.setValue( + settings.kibana_url || `${window.location.origin}${core.http.basePath.get()}` + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settings]); + + const body = ( + <EuiForm> + <EuiRadioGroup + options={[ + { + id: 'enabled', + label: i18n.translate('xpack.ingestManager.settings.autoUpgradeEnabledLabel', { + defaultMessage: + 'Automatically update agent binaries to use the latest minor version.', + }), + }, + { + id: 'disabled', + disabled: true, + label: i18n.translate('xpack.ingestManager.settings.autoUpgradeDisabledLabel', { + defaultMessage: 'Manually manage agent binary versions. Requires Gold license.', + }), + }, + ]} + idSelected={'enabled'} + onChange={id => {}} + legend={{ + children: ( + <EuiTitle size="xs"> + <h3> + <FormattedMessage + id="xpack.ingestManager.settings.autoUpgradeFieldLabel" + defaultMessage="Elastic Agent binary version" + /> + </h3> + </EuiTitle> + ), + }} + /> + <EuiSpacer size="l" /> + <EuiRadioGroup + options={[ + { + id: 'enabled', + label: i18n.translate( + 'xpack.ingestManager.settings.integrationUpgradeEnabledFieldLabel', + { + defaultMessage: + 'Automatically update Integrations to the latest version to receive the latest assets. Agent configurations may need to be updated in order to use new features.', + } + ), + }, + { + id: 'disabled', + disabled: true, + label: i18n.translate( + 'xpack.ingestManager.settings.integrationUpgradeDisabledFieldLabel', + { + defaultMessage: 'Manually manage integration versions yourself.', + } + ), + }, + ]} + idSelected={'enabled'} + onChange={id => {}} + legend={{ + children: ( + <EuiTitle size="xs"> + <h3> + <FormattedMessage + id="xpack.ingestManager.settings.integrationUpgradeFieldLabel" + defaultMessage="Elastic integration version" + /> + </h3> + </EuiTitle> + ), + }} + /> + <EuiSpacer size="l" /> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.ingestManager.settings.globalOutputTitle" + defaultMessage="Global output" + /> + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiText color="subdued" size="s"> + <FormattedMessage + id="xpack.ingestManager.settings.globalOutputDescription" + defaultMessage="The global output is applied to all agent configurations and specifies where data is sent." + /> + </EuiText> + <EuiSpacer size="m" /> + <EuiFormRow> + <EuiFormRow + label={i18n.translate('xpack.ingestManager.settings.kibanaUrlLabel', { + defaultMessage: 'Kibana URL', + })} + > + <EuiFieldText required={true} {...inputs.kibanaUrl.props} name="kibanaUrl" /> + </EuiFormRow> + </EuiFormRow> + <EuiSpacer size="m" /> + <EuiFormRow> + <EuiFormRow + label={i18n.translate('xpack.ingestManager.settings.elasticsearchUrlLabel', { + defaultMessage: 'Elasticsearch URL', + })} + > + <EuiComboBox noSuggestions {...inputs.elasticsearchUrl.props} /> + </EuiFormRow> + </EuiFormRow> + </EuiForm> + ); + + return ( + <EuiFlyout onClose={onClose} size="l" maxWidth={640}> + <EuiFlyoutHeader hasBorder aria-labelledby="IngestManagerSettingsFlyoutTitle"> + <EuiTitle size="m"> + <h2 id="IngestManagerSettingsFlyoutTitle"> + <FormattedMessage + id="xpack.ingestManager.settings.flyoutTitle" + defaultMessage="Ingest Manager settings" + /> + </h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody>{body}</EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> + <FormattedMessage + id="xpack.ingestManager.settings.cancelButtonLabel" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton onClick={onSubmit} iconType="save"> + <FormattedMessage + id="xpack.ingestManager.settings.saveButtonLabel" + defaultMessage="Save settings" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index 282ea8dbee3a2..e9b736e379b58 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -3,7 +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. */ -export { PLUGIN_ID, EPM_API_ROUTES, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../../../../common'; +export { + PLUGIN_ID, + EPM_API_ROUTES, + AGENT_API_ROUTES, + AGENT_CONFIG_SAVED_OBJECT_TYPE, + AGENT_EVENT_SAVED_OBJECT_TYPE, + AGENT_SAVED_OBJECT_TYPE, + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + DATASOURCE_SAVED_OBJECT_TYPE, +} from '../../../../common'; export const BASE_PATH = '/app/ingestManager'; export const EPM_PATH = '/epm'; @@ -12,6 +21,7 @@ export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`; export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`; export const AGENT_CONFIG_PATH = '/configs'; export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; +export const DATA_STREAM_PATH = '/data-streams'; export const FLEET_PATH = '/fleet'; export const FLEET_AGENTS_PATH = `${FLEET_PATH}/agents`; export const FLEET_AGENT_DETAIL_PATH = `${FLEET_AGENTS_PATH}/`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts index 4aa0ad7155d2f..c535dc899638d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts @@ -20,5 +20,27 @@ export function useInput(defaultValue = '') { clear: () => { setValue(''); }, + setValue, + }; +} + +export function useComboInput(defaultValue = []) { + const [value, setValue] = React.useState<string[]>(defaultValue); + + return { + props: { + selectedOptions: value.map((val: string) => ({ label: val })), + onCreateOption: (newVal: any) => { + setValue([...value, newVal]); + }, + onChange: (newVals: any[]) => { + setValue(newVals.map(val => val.label)); + }, + }, + value, + clear: () => { + setValue([]); + }, + setValue, }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts index d16d835f8c701..f80c468677f48 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agent_config.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { HttpFetchQuery } from 'src/core/public'; -import { useRequest, sendRequest } from './use_request'; +import { + useRequest, + sendRequest, + useConditionalRequest, + SendConditionalRequestConfig, +} from './use_request'; import { agentConfigRouteService } from '../../services'; import { GetAgentConfigsResponse, @@ -13,8 +18,8 @@ import { CreateAgentConfigResponse, UpdateAgentConfigRequest, UpdateAgentConfigResponse, - DeleteAgentConfigsRequest, - DeleteAgentConfigsResponse, + DeleteAgentConfigRequest, + DeleteAgentConfigResponse, } from '../../types'; export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => { @@ -25,11 +30,12 @@ export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => { }); }; -export const useGetOneAgentConfig = (agentConfigId: string) => { - return useRequest<GetOneAgentConfigResponse>({ - path: agentConfigRouteService.getInfoPath(agentConfigId), +export const useGetOneAgentConfig = (agentConfigId: string | undefined) => { + return useConditionalRequest<GetOneAgentConfigResponse>({ + path: agentConfigId ? agentConfigRouteService.getInfoPath(agentConfigId) : undefined, method: 'get', - }); + shouldSendRequest: !!agentConfigId, + } as SendConditionalRequestConfig); }; export const useGetOneAgentConfigFull = (agentConfigId: string) => { @@ -69,8 +75,8 @@ export const sendUpdateAgentConfig = ( }); }; -export const sendDeleteAgentConfigs = (body: DeleteAgentConfigsRequest['body']) => { - return sendRequest<DeleteAgentConfigsResponse>({ +export const sendDeleteAgentConfig = (body: DeleteAgentConfigRequest['body']) => { + return sendRequest<DeleteAgentConfigResponse>({ path: agentConfigRouteService.getDeletePath(), method: 'post', body: JSON.stringify(body), diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts index f08b950e71ea8..453bcf2bd81e7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts @@ -10,6 +10,8 @@ import { GetOneAgentResponse, GetOneAgentEventsResponse, GetOneAgentEventsRequest, + PutAgentReassignRequest, + PutAgentReassignResponse, GetAgentsRequest, GetAgentsResponse, GetAgentStatusRequest, @@ -59,3 +61,16 @@ export function sendGetAgentStatus( ...options, }); } + +export function sendPutAgentReassign( + agentId: string, + body: PutAgentReassignRequest['body'], + options?: RequestOptions +) { + return sendRequest<PutAgentReassignResponse>({ + method: 'put', + path: agentRouteService.getReassignPath(agentId), + body, + ...options, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts new file mode 100644 index 0000000000000..9acf4b1e17449 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts @@ -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 { useRequest } from './use_request'; +import { dataStreamRouteService } from '../../services'; +import { GetDataStreamsResponse } from '../../types'; + +export const useGetDataStreams = () => { + return useRequest<GetDataStreamsResponse>({ + path: dataStreamRouteService.getListPath(), + method: 'get', + }); +}; 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 0d19ecd0cb735..e2fc190e158f9 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<UpdateDatasourceResponse>({ + path: datasourceRouteService.getUpdatePath(datasourceId), + method: 'put', + body: JSON.stringify(body), + }); +}; + export const sendDeleteDatasource = (body: DeleteDatasourcesRequest['body']) => { return sendRequest<DeleteDatasourcesResponse>({ path: datasourceRouteService.getDeletePath(), @@ -36,3 +53,10 @@ export function useGetDatasources(query: GetDatasourcesRequest['query']) { query, }); } + +export const sendGetOneDatasource = (datasourceId: string) => { + return sendRequest<GetOneDatasourceResponse>({ + path: datasourceRouteService.getInfoPath(datasourceId), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts index e4abb4ccd22cb..10d9e03e986e1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/enrollment_api_keys.ts @@ -4,7 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useRequest, UseRequestConfig, sendRequest } from './use_request'; +import { + useRequest, + UseRequestConfig, + sendRequest, + useConditionalRequest, + SendConditionalRequestConfig, +} from './use_request'; import { enrollmentAPIKeyRouteService } from '../../services'; import { GetOneEnrollmentAPIKeyResponse, @@ -14,12 +20,12 @@ import { type RequestOptions = Pick<Partial<UseRequestConfig>, 'pollIntervalMs'>; -export function useGetOneEnrollmentAPIKey(keyId: string, options?: RequestOptions) { - return useRequest<GetOneEnrollmentAPIKeyResponse>({ +export function useGetOneEnrollmentAPIKey(keyId: string | undefined) { + return useConditionalRequest<GetOneEnrollmentAPIKeyResponse>({ method: 'get', - path: enrollmentAPIKeyRouteService.getInfoPath(keyId), - ...options, - }); + path: keyId ? enrollmentAPIKeyRouteService.getInfoPath(keyId) : undefined, + shouldSendRequest: !!keyId, + } as SendConditionalRequestConfig); } export function sendGetOneEnrollmentAPIKey(keyId: string, options?: RequestOptions) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts index 5014049407e65..c39d2a5860bf0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -6,6 +6,9 @@ export { setHttpClient, sendRequest, useRequest } from './use_request'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './agents'; export * from './enrollment_api_keys'; export * from './epm'; +export * from './outputs'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/outputs.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/outputs.ts new file mode 100644 index 0000000000000..e57256d33ab2f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/outputs.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sendRequest, useRequest } from './use_request'; +import { outputRoutesService } from '../../services'; +import { PutOutputRequest, GetOutputsResponse } from '../../types'; + +export function useGetOutputs() { + return useRequest<GetOutputsResponse>({ + method: 'get', + path: outputRoutesService.getListPath(), + }); +} + +export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) { + return sendRequest({ + method: 'put', + path: outputRoutesService.getUpdatePath(outputId), + body, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/settings.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/settings.ts new file mode 100644 index 0000000000000..45e4eccf6625e --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/settings.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sendRequest, useRequest } from './use_request'; +import { settingsRoutesService } from '../../services'; +import { PutSettingsResponse, PutSettingsRequest, GetSettingsResponse } from '../../types'; + +export function useGetSettings() { + return useRequest<GetSettingsResponse>({ + method: 'get', + path: settingsRoutesService.getInfoPath(), + }); +} + +export function sendPutSettings(body: PutSettingsRequest['body']) { + return sendRequest<PutSettingsResponse>({ + method: 'put', + path: settingsRoutesService.getUpdatePath(), + body, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts index c63383637e792..fbbc482fb96af 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/use_request.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useState, useEffect } from 'react'; import { HttpSetup } from 'src/core/public'; import { SendRequestConfig, @@ -35,3 +36,68 @@ export const useRequest = <D = any>(config: UseRequestConfig) => { } return _useRequest<D>(httpClient, config); }; + +export type SendConditionalRequestConfig = + | (SendRequestConfig & { shouldSendRequest: true }) + | (Partial<SendRequestConfig> & { shouldSendRequest: false }); + +export const useConditionalRequest = <D = any>(config: SendConditionalRequestConfig) => { + const [state, setState] = useState<{ + error: Error | null; + data: D | null; + isLoading: boolean; + }>({ + error: null, + data: null, + isLoading: false, + }); + + const { path, method, shouldSendRequest, query, body } = config; + + async function sendGetOneEnrollmentAPIKeyRequest() { + if (!config.shouldSendRequest) { + setState({ + data: null, + isLoading: false, + error: null, + }); + return; + } + + try { + setState({ + data: null, + isLoading: true, + error: null, + }); + const res = await sendRequest<D>({ + method: config.method, + path: config.path, + query: config.query, + body: config.body, + }); + if (res.error) { + throw res.error; + } + setState({ + data: res.data, + isLoading: false, + error: null, + }); + return res; + } catch (error) { + setState({ + data: null, + isLoading: false, + error, + }); + } + } + + useEffect(() => { + sendGetOneEnrollmentAPIKeyRequest(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [path, method, shouldSendRequest, JSON.stringify(query), JSON.stringify(body)]); + + return { ...state, sendRequest: sendGetOneEnrollmentAPIKeyRequest }; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss new file mode 100644 index 0000000000000..fb95b1fa8bbfc --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss @@ -0,0 +1,14 @@ +@import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/nav_drawer/variables'; + +/** + * 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout. + */ +.ingestManager__bottomBar { + z-index: 0; /* 1 */ + left: $euiNavDrawerWidthCollapsed; +} + +.ingestManager__bottomBar-isNavDrawerLocked { + left: $euiNavDrawerWidthExpanded; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index f7c2805c6ea7c..295a35693726f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -16,13 +16,14 @@ import { IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from './constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; -import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp } from './sections'; +import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { sendSetup } from './hooks/use_request/setup'; +import './index.scss'; export interface ProtectedRouteProps extends RouteProps { isAllowed?: boolean; @@ -98,6 +99,11 @@ const IngestManagerRoutes = ({ ...rest }) => { <AgentConfigApp /> </DefaultLayout> </Route> + <Route path={DATA_STREAM_PATH}> + <DefaultLayout section="data_stream"> + <DataStreamApp /> + </DefaultLayout> + </Route> <ProtectedRoute path={FLEET_PATH} isAllowed={fleet.enabled}> <DefaultLayout section="fleet"> <FleetApp /> 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 26f2c85a291a3..4a9cfe02b74ac 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 @@ -5,11 +5,12 @@ */ import React from 'react'; import styled from 'styled-components'; -import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; +import { AlphaMessaging, SettingFlyout } from '../components'; import { useLink, useConfig } from '../hooks'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; interface Props { section?: Section; @@ -34,52 +35,95 @@ const Nav = styled.nav` export const DefaultLayout: React.FunctionComponent<Props> = ({ section, children }) => { const { epm, fleet } = useConfig(); + + const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false); + return ( - <Container> - <Nav> - <EuiFlexGroup gutterSize="l" alignItems="center"> - <EuiFlexItem grow={false}> - <EuiIcon type="savedObjectsApp" size="l" /> - </EuiFlexItem> - <EuiFlexItem> - <EuiTabs display="condensed"> - <EuiTab isSelected={section === 'overview'} href={useLink()}> - <FormattedMessage - id="xpack.ingestManager.appNavigation.overviewLinkText" - defaultMessage="Overview" - /> - </EuiTab> - <EuiTab - isSelected={section === 'epm'} - href={useLink(EPM_PATH)} - disabled={!epm?.enabled} - > - <FormattedMessage - id="xpack.ingestManager.appNavigation.epmLinkText" - defaultMessage="Integrations" - /> - </EuiTab> - <EuiTab isSelected={section === 'agent_config'} href={useLink(AGENT_CONFIG_PATH)}> - <FormattedMessage - id="xpack.ingestManager.appNavigation.configurationsLinkText" - defaultMessage="Configurations" - /> - </EuiTab> - <EuiTab - isSelected={section === 'fleet'} - href={useLink(FLEET_PATH)} - disabled={!fleet?.enabled} - > - <FormattedMessage - id="xpack.ingestManager.appNavigation.fleetLinkText" - defaultMessage="Fleet" - /> - </EuiTab> - </EuiTabs> - </EuiFlexItem> - </EuiFlexGroup> - </Nav> - {children} - </Container> + <> + {isSettingsFlyoutOpen && ( + <SettingFlyout + onClose={() => { + setIsSettingsFlyoutOpen(false); + }} + /> + )} + <Container> + <Nav> + <EuiFlexGroup gutterSize="l" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiIcon type="savedObjectsApp" size="l" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTabs display="condensed"> + <EuiTab isSelected={section === 'overview'} href={useLink()}> + <FormattedMessage + id="xpack.ingestManager.appNavigation.overviewLinkText" + defaultMessage="Overview" + /> + </EuiTab> + <EuiTab + isSelected={section === 'epm'} + href={useLink(EPM_PATH)} + disabled={!epm?.enabled} + > + <FormattedMessage + id="xpack.ingestManager.appNavigation.epmLinkText" + defaultMessage="Integrations" + /> + </EuiTab> + <EuiTab isSelected={section === 'agent_config'} href={useLink(AGENT_CONFIG_PATH)}> + <FormattedMessage + id="xpack.ingestManager.appNavigation.configurationsLinkText" + defaultMessage="Configurations" + /> + </EuiTab> + <EuiTab + isSelected={section === 'fleet'} + href={useLink(FLEET_PATH)} + disabled={!fleet?.enabled} + > + <FormattedMessage + id="xpack.ingestManager.appNavigation.fleetLinkText" + defaultMessage="Fleet" + /> + </EuiTab> + <EuiTab isSelected={section === 'data_stream'} href={useLink(DATA_STREAM_PATH)}> + <FormattedMessage + id="xpack.ingestManager.appNavigation.dataStreamsLinkText" + defaultMessage="Data streams" + /> + </EuiTab> + </EuiTabs> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="s" direction="row"> + <EuiFlexItem> + <EuiButtonEmpty + iconType="popout" + href="https://ela.st/ingest-manager-feedback" + target="_blank" + > + <FormattedMessage + id="xpack.ingestManager.appNavigation.sendFeedbackButton" + defaultMessage="Send Feedback" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem> + <EuiButtonEmpty iconType="gear" onClick={() => setIsSettingsFlyoutOpen(true)}> + <FormattedMessage + id="xpack.ingestManager.appNavigation.settingsButton" + defaultMessage="Settings" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </Nav> + {children} + <AlphaMessaging /> + </Container> + </> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx index c77a50d95dca3..d5ce5e17ad84e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/with_header.tsx @@ -14,12 +14,18 @@ const Page = styled(EuiPage)` interface Props extends HeaderProps { restrictWidth?: number; + restrictHeaderWidth?: number; children?: React.ReactNode; } -export const WithHeaderLayout: React.FC<Props> = ({ restrictWidth, children, ...rest }) => ( +export const WithHeaderLayout: React.FC<Props> = ({ + restrictWidth, + restrictHeaderWidth, + children, + ...rest +}) => ( <Fragment> - <Header {...rest} /> + <Header maxWidth={restrictHeaderWidth} {...rest} /> <Page restrictWidth={restrictWidth || 1200}> <EuiPageBody> <EuiSpacer size="m" /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx index b18349e078766..d517dde45d5e3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx @@ -5,116 +5,92 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiOverlayMask, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sendDeleteAgentConfigs, useCore, sendRequest } from '../../../hooks'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { sendDeleteAgentConfig, useCore, useConfig, sendRequest } from '../../../hooks'; interface Props { - children: (deleteAgentConfigs: deleteAgentConfigs) => React.ReactElement; + children: (deleteAgentConfig: DeleteAgentConfig) => React.ReactElement; } -export type deleteAgentConfigs = (agentConfigs: string[], onSuccess?: OnSuccessCallback) => void; +export type DeleteAgentConfig = (agentConfig: string, onSuccess?: OnSuccessCallback) => void; -type OnSuccessCallback = (agentConfigsUnenrolled: string[]) => void; +type OnSuccessCallback = (agentConfigDeleted: string) => void; export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ children }) => { const { notifications } = useCore(); - const [agentConfigs, setAgentConfigs] = useState<string[]>([]); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const [agentConfig, setAgentConfig] = useState<string>(); const [isModalOpen, setIsModalOpen] = useState<boolean>(false); const [isLoadingAgentsCount, setIsLoadingAgentsCount] = useState<boolean>(false); const [agentsCount, setAgentsCount] = useState<number>(0); const [isLoading, setIsLoading] = useState<boolean>(false); const onSuccessCallback = useRef<OnSuccessCallback | null>(null); - const deleteAgentConfigsPrompt: deleteAgentConfigs = ( - agentConfigsToDelete, + const deleteAgentConfigPrompt: DeleteAgentConfig = ( + agentConfigToDelete, onSuccess = () => undefined ) => { - if ( - agentConfigsToDelete === undefined || - (Array.isArray(agentConfigsToDelete) && agentConfigsToDelete.length === 0) - ) { - throw new Error('No agent configs specified for deletion'); + if (!agentConfigToDelete) { + throw new Error('No agent config specified for deletion'); } setIsModalOpen(true); - setAgentConfigs(agentConfigsToDelete); - fetchAgentsCount(agentConfigsToDelete); + setAgentConfig(agentConfigToDelete); + fetchAgentsCount(agentConfigToDelete); onSuccessCallback.current = onSuccess; }; const closeModal = () => { - setAgentConfigs([]); + setAgentConfig(undefined); setIsLoading(false); setIsLoadingAgentsCount(false); setIsModalOpen(false); }; - const deleteAgentConfigs = async () => { + const deleteAgentConfig = async () => { setIsLoading(true); try { - const { data } = await sendDeleteAgentConfigs({ - agentConfigIds: agentConfigs, + const { data } = await sendDeleteAgentConfig({ + agentConfigId: agentConfig!, }); - const successfulResults = data?.filter(result => result.success) || []; - const failedResults = data?.filter(result => !result.success) || []; - if (successfulResults.length) { - const hasMultipleSuccesses = successfulResults.length > 1; - const successMessage = hasMultipleSuccesses - ? i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle', - { - defaultMessage: 'Deleted {count} agent configs', - values: { count: successfulResults.length }, - } - ) - : i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle', - { - defaultMessage: "Deleted agent config '{id}'", - values: { id: successfulResults[0].id }, - } - ); - notifications.toasts.addSuccess(successMessage); + if (data?.success) { + notifications.toasts.addSuccess( + i18n.translate('xpack.ingestManager.deleteAgentConfig.successSingleNotificationTitle', { + defaultMessage: "Deleted agent config '{id}'", + values: { id: agentConfig }, + }) + ); + if (onSuccessCallback.current) { + onSuccessCallback.current(agentConfig!); + } } - if (failedResults.length) { - const hasMultipleFailures = failedResults.length > 1; - const failureMessage = hasMultipleFailures - ? i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle', - { - defaultMessage: 'Error deleting {count} agent configs', - values: { count: failedResults.length }, - } - ) - : i18n.translate( - 'xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle', - { - defaultMessage: "Error deleting agent config '{id}'", - values: { id: failedResults[0].id }, - } - ); - notifications.toasts.addDanger(failureMessage); - } - - if (onSuccessCallback.current) { - onSuccessCallback.current(successfulResults.map(result => result.id)); + if (!data?.success) { + notifications.toasts.addDanger( + i18n.translate('xpack.ingestManager.deleteAgentConfig.failureSingleNotificationTitle', { + defaultMessage: "Error deleting agent config '{id}'", + values: { id: agentConfig }, + }) + ); } } catch (e) { notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle', { - defaultMessage: 'Error deleting agent configs', + i18n.translate('xpack.ingestManager.deleteAgentConfig.fatalErrorNotificationTitle', { + defaultMessage: 'Error deleting agent config', }) ); } closeModal(); }; - const fetchAgentsCount = async (agentConfigsToCheck: string[]) => { - if (isLoadingAgentsCount) { + const fetchAgentsCount = async (agentConfigToCheck: string) => { + if (!isFleetEnabled || isLoadingAgentsCount) { return; } setIsLoadingAgentsCount(true); @@ -122,7 +98,7 @@ export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ chil path: `/api/ingest_manager/fleet/agents`, method: 'get', query: { - kuery: `agents.config_id : (${agentConfigsToCheck.join(' or ')})`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : ${agentConfigToCheck}`, }, }); setAgentsCount(data?.total || 0); @@ -139,68 +115,61 @@ export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ chil <EuiConfirmModal title={ <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle" - defaultMessage="Delete {count, plural, one {this agent config} other {# agent configs}}?" - values={{ count: agentConfigs.length }} + id="xpack.ingestManager.deleteAgentConfig.confirmModal.deleteConfigTitle" + defaultMessage="Delete this agent configuration?" /> } onCancel={closeModal} - onConfirm={deleteAgentConfigs} + onConfirm={deleteAgentConfig} cancelButtonText={ <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel" + id="xpack.ingestManager.deleteAgentConfig.confirmModal.cancelButtonLabel" defaultMessage="Cancel" /> } confirmButtonText={ isLoading || isLoadingAgentsCount ? ( <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel" + id="xpack.ingestManager.deleteAgentConfig.confirmModal.loadingButtonLabel" defaultMessage="Loading…" /> - ) : agentsCount ? ( - <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel" - defaultMessage="Delete {agentConfigsCount, plural, one {agent config} other {agent configs}} and unenroll {agentsCount, plural, one {agent} other {agents}}" - values={{ - agentsCount, - agentConfigsCount: agentConfigs.length, - }} - /> ) : ( <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel" - defaultMessage="Delete {agentConfigsCount, plural, one {agent config} other {agent configs}}" - values={{ - agentConfigsCount: agentConfigs.length, - }} + id="xpack.ingestManager.deleteAgentConfig.confirmModal.confirmButtonLabel" + defaultMessage="Delete configuration" /> ) } buttonColor="danger" - confirmButtonDisabled={isLoading || isLoadingAgentsCount} + confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount} > {isLoadingAgentsCount ? ( <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage" + id="xpack.ingestManager.deleteAgentConfig.confirmModal.loadingAgentsCountMessage" defaultMessage="Checking amount of affected agents…" /> ) : agentsCount ? ( - <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage" - defaultMessage="{agentsCount, plural, one {# agent is} other {# agents are}} assigned {agentConfigsCount, plural, one {to this agent config} other {across these agentConfigs}}. {agentsCount, plural, one {This agent} other {These agents}} will be unenrolled." - values={{ - agentsCount, - agentConfigsCount: agentConfigs.length, - }} - /> + <EuiCallOut + color="danger" + title={i18n.translate( + 'xpack.ingestManager.deleteAgentConfig.confirmModal.affectedAgentsTitle', + { + defaultMessage: 'Configuration in use', + } + )} + > + <FormattedMessage + id="xpack.ingestManager.deleteAgentConfig.confirmModal.affectedAgentsMessage" + defaultMessage="{agentsCount, plural, one {# agent is} other {# agents are}} assigned to this agent configuration. Unassign these agents before deleting this configuration." + values={{ + agentsCount, + }} + /> + </EuiCallOut> ) : ( <FormattedMessage - id="xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage" - defaultMessage="There are no agents assigned to {agentConfigsCount, plural, one {this agent config} other {these agentConfigs}}." - values={{ - agentConfigsCount: agentConfigs.length, - }} + id="xpack.ingestManager.deleteAgentConfig.confirmModal.irreversibleMessage" + defaultMessage="This action cannot be undone." /> )} </EuiConfirmModal> @@ -210,7 +179,7 @@ export const AgentConfigDeleteProvider: React.FunctionComponent<Props> = ({ chil return ( <Fragment> - {children(deleteAgentConfigsPrompt)} + {children(deleteAgentConfigPrompt)} {renderModal()} </Fragment> ); 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 0d53ca34a1fef..c55d6009074b0 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 @@ -8,8 +8,7 @@ import React, { useMemo, useState } from 'react'; import { EuiAccordion, EuiFieldText, - EuiFlexGroup, - EuiFlexItem, + EuiDescribedFormGroup, EuiForm, EuiFormRow, EuiHorizontalRule, @@ -18,11 +17,14 @@ import { EuiText, EuiComboBox, EuiIconTip, + EuiCheckboxGroup, + EuiButton, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; -import { NewAgentConfig } from '../../../types'; +import { NewAgentConfig, AgentConfig } from '../../../types'; +import { AgentConfigDeleteProvider } from './config_delete_provider'; interface ValidationResults { [key: string]: JSX.Element[]; @@ -30,12 +32,12 @@ interface ValidationResults { const StyledEuiAccordion = styled(EuiAccordion)` .ingest-active-button { - color: ${props => props.theme.eui.euiColorPrimary}}; + color: ${props => props.theme.eui.euiColorPrimary}; } `; export const agentConfigFormValidation = ( - agentConfig: Partial<NewAgentConfig> + agentConfig: Partial<NewAgentConfig | AgentConfig> ): ValidationResults => { const errors: ValidationResults = {}; @@ -52,11 +54,13 @@ export const agentConfigFormValidation = ( }; interface Props { - agentConfig: Partial<NewAgentConfig>; - updateAgentConfig: (u: Partial<NewAgentConfig>) => void; + agentConfig: Partial<NewAgentConfig | AgentConfig>; + updateAgentConfig: (u: Partial<NewAgentConfig | AgentConfig>) => void; withSysMonitoring: boolean; updateSysMonitoring: (newValue: boolean) => void; validation: ValidationResults; + isEditing?: boolean; + onDelete?: () => void; } export const AgentConfigForm: React.FunctionComponent<Props> = ({ @@ -65,9 +69,11 @@ export const AgentConfigForm: React.FunctionComponent<Props> = ({ withSysMonitoring, updateSysMonitoring, validation, + isEditing = false, + onDelete = () => {}, }) => { const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); - const [showNamespace, setShowNamespace] = useState<boolean>(false); + const [showNamespace, setShowNamespace] = useState<boolean>(!!agentConfig.namespace); const fields: Array<{ name: 'name' | 'description' | 'namespace'; label: JSX.Element; @@ -104,147 +110,281 @@ export const AgentConfigForm: React.FunctionComponent<Props> = ({ ]; }, []); - return ( - <EuiForm> - {fields.map(({ name, label, placeholder }) => { - return ( - <EuiFormRow - fullWidth - key={name} - label={label} - error={touchedFields[name] && validation[name] ? validation[name] : null} - isInvalid={Boolean(touchedFields[name] && validation[name])} - > - <EuiFieldText - fullWidth - value={agentConfig[name]} - onChange={e => updateAgentConfig({ [name]: e.target.value })} - isInvalid={Boolean(touchedFields[name] && validation[name])} - onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })} - placeholder={placeholder} - /> - </EuiFormRow> - ); - })} + const generalSettingsWrapper = (children: JSX.Element[]) => ( + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.ingestManager.configForm.generalSettingsGroupTitle" + defaultMessage="General settings" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.ingestManager.configForm.generalSettingsGroupDescription" + defaultMessage="Choose a name and description for your agent configuration." + /> + } + > + {children} + </EuiDescribedFormGroup> + ); + + const generalFields = fields.map(({ name, label, placeholder }) => { + return ( <EuiFormRow - label={ - <EuiText size="xs" color="subdued"> + fullWidth + key={name} + label={label} + error={touchedFields[name] && validation[name] ? validation[name] : null} + isInvalid={Boolean(touchedFields[name] && validation[name])} + > + <EuiFieldText + fullWidth + value={agentConfig[name]} + onChange={e => updateAgentConfig({ [name]: e.target.value })} + isInvalid={Boolean(touchedFields[name] && validation[name])} + onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })} + placeholder={placeholder} + /> + </EuiFormRow> + ); + }); + + const advancedOptionsContent = ( + <> + <EuiDescribedFormGroup + title={ + <h4> <FormattedMessage - id="xpack.ingestManager.agentConfigForm.systemMonitoringFieldLabel" - defaultMessage="Optional" + id="xpack.ingestManager.agentConfigForm.namespaceFieldLabel" + defaultMessage="Default namespace" /> - </EuiText> + </h4> + } + description={ + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.namespaceFieldDescription" + defaultMessage="Apply a default namespace to data sources that use this configuration. Data sources can specify their own namespaces." + /> } > <EuiSwitch showLabel={true} label={ - <> - <FormattedMessage - id="xpack.ingestManager.agentConfigForm.systemMonitoringText" - defaultMessage="Collect system metrics" - />{' '} - <EuiIconTip - content={i18n.translate( - 'xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText', - { - defaultMessage: - 'Enable this option to bootstrap your configuration with a data source that collects system metrics and information.', - } - )} - position="right" - type="iInCircle" - /> - </> + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.namespaceUseDefaultsFieldLabel" + defaultMessage="Use default namespace" + /> } - checked={withSysMonitoring} + checked={showNamespace} onChange={() => { - updateSysMonitoring(!withSysMonitoring); + setShowNamespace(!showNamespace); + if (showNamespace) { + updateAgentConfig({ namespace: '' }); + } }} /> - </EuiFormRow> - <EuiHorizontalRule /> - <EuiSpacer size="xs" /> - <StyledEuiAccordion - id="advancedOptions" - buttonContent={ + {showNamespace && ( + <> + <EuiSpacer size="m" /> + <EuiFormRow + fullWidth + error={touchedFields.namespace && validation.namespace ? validation.namespace : null} + isInvalid={Boolean(touchedFields.namespace && validation.namespace)} + > + <EuiComboBox + fullWidth + singleSelection + noSuggestions + selectedOptions={agentConfig.namespace ? [{ label: agentConfig.namespace }] : []} + onCreateOption={(value: string) => { + updateAgentConfig({ namespace: value }); + }} + onChange={selectedOptions => { + updateAgentConfig({ + namespace: (selectedOptions.length ? selectedOptions[0] : '') as string, + }); + }} + isInvalid={Boolean(touchedFields.namespace && validation.namespace)} + onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })} + /> + </EuiFormRow> + </> + )} + </EuiDescribedFormGroup> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.monitoringLabel" + defaultMessage="Agent monitoring" + /> + </h4> + } + description={ <FormattedMessage - id="xpack.ingestManager.agentConfigForm.advancedOptionsToggleLabel" - defaultMessage="Advanced options" + id="xpack.ingestManager.agentConfigForm.monitoringDescription" + defaultMessage="Collect data about your agents for debugging and tracking performance." /> } - buttonClassName="ingest-active-button" > - <EuiSpacer size="l" /> - <EuiFlexGroup> - <EuiFlexItem> - <EuiText> - <h4> - <FormattedMessage - id="xpack.ingestManager.agentConfigForm.namespaceFieldLabel" - defaultMessage="Default namespace" - /> - </h4> - </EuiText> - <EuiSpacer size="m" /> - <EuiText size="s"> + <EuiCheckboxGroup + options={[ + { + id: 'logs', + label: i18n.translate( + 'xpack.ingestManager.agentConfigForm.monitoringLogsFieldLabel', + { defaultMessage: 'Collect agent logs' } + ), + }, + { + id: 'metrics', + label: i18n.translate( + 'xpack.ingestManager.agentConfigForm.monitoringMetricsFieldLabel', + { defaultMessage: 'Collect agent metrics' } + ), + }, + ]} + idToSelectedMap={(agentConfig.monitoring_enabled || []).reduce( + (acc: { logs: boolean; metrics: boolean }, key) => { + 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], + }); + }} + /> + </EuiDescribedFormGroup> + {isEditing && 'id' in agentConfig ? ( + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.ingestManager.configForm.deleteConfigGroupTitle" + defaultMessage="Delete configuration" + /> + </h4> + } + description={ + <> <FormattedMessage - id="xpack.ingestManager.agentConfigForm.namespaceFieldDescription" - defaultMessage="Apply a default namespace to data sources that use this configuration. Data sources can specify their own namespaces." + id="xpack.ingestManager.configForm.deleteConfigGroupDescription" + defaultMessage="Existing data will not be deleted." + /> + <EuiSpacer size="s" /> + <AgentConfigDeleteProvider> + {deleteAgentConfigPrompt => { + return ( + <EuiButton + color="danger" + disabled={Boolean(agentConfig.is_default)} + onClick={() => deleteAgentConfigPrompt(agentConfig.id!, onDelete)} + > + <FormattedMessage + id="xpack.ingestManager.configForm.deleteConfigActionText" + defaultMessage="Delete configuration" + /> + </EuiButton> + ); + }} + </AgentConfigDeleteProvider> + {agentConfig.is_default ? ( + <> + <EuiSpacer size="xs" /> + <EuiText color="subdued" size="xs"> + <FormattedMessage + id="xpack.ingestManager.configForm.unableToDeleteDefaultConfigText" + defaultMessage="Default configuration cannot be deleted" + /> + </EuiText> + </> + ) : null} + </> + } + /> + ) : null} + </> + ); + + return ( + <EuiForm> + {!isEditing ? generalFields : generalSettingsWrapper(generalFields)} + {!isEditing ? ( + <EuiFormRow + label={ + <EuiText size="xs" color="subdued"> + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.systemMonitoringFieldLabel" + defaultMessage="Optional" /> </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <EuiSwitch - showLabel={true} - label={ - <FormattedMessage - id="xpack.ingestManager.agentConfigForm.namespaceUseDefaultsFieldLabel" - defaultMessage="Use default namespace" - /> - } - checked={showNamespace} - onChange={() => { - setShowNamespace(!showNamespace); - if (showNamespace) { - updateAgentConfig({ namespace: '' }); - } - }} - /> - {showNamespace && ( + } + > + <EuiSwitch + showLabel={true} + label={ <> - <EuiSpacer size="m" /> - <EuiFormRow - fullWidth - error={ - touchedFields.namespace && validation.namespace ? validation.namespace : null - } - isInvalid={Boolean(touchedFields.namespace && validation.namespace)} - > - <EuiComboBox - fullWidth - singleSelection - noSuggestions - selectedOptions={ - agentConfig.namespace ? [{ label: agentConfig.namespace }] : [] + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.systemMonitoringText" + defaultMessage="Collect system metrics" + />{' '} + <EuiIconTip + content={i18n.translate( + 'xpack.ingestManager.agentConfigForm.systemMonitoringTooltipText', + { + defaultMessage: + 'Enable this option to bootstrap your configuration with a data source that collects system metrics and information.', } - onCreateOption={(value: string) => { - updateAgentConfig({ namespace: value }); - }} - onChange={selectedOptions => { - updateAgentConfig({ - namespace: (selectedOptions.length ? selectedOptions[0] : '') as string, - }); - }} - isInvalid={Boolean(touchedFields.namespace && validation.namespace)} - onBlur={() => setTouchedFields({ ...touchedFields, namespace: true })} - /> - </EuiFormRow> + )} + position="right" + type="iInCircle" + /> </> - )} - </EuiFlexItem> - </EuiFlexGroup> - </StyledEuiAccordion> + } + checked={withSysMonitoring} + onChange={() => { + updateSysMonitoring(!withSysMonitoring); + }} + /> + </EuiFormRow> + ) : null} + {!isEditing ? ( + <> + <EuiHorizontalRule /> + <EuiSpacer size="xs" /> + <StyledEuiAccordion + id="advancedOptions" + buttonContent={ + <FormattedMessage + id="xpack.ingestManager.agentConfigForm.advancedOptionsToggleLabel" + defaultMessage="Advanced options" + /> + } + buttonClassName="ingest-active-button" + > + <EuiSpacer size="l" /> + {advancedOptionsContent} + </StyledEuiAccordion> + </> + ) : ( + advancedOptionsContent + )} </EuiForm> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/confirm_deploy_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/confirm_deploy_modal.tsx new file mode 100644 index 0000000000000..a503beeffa8b4 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/confirm_deploy_modal.tsx @@ -0,0 +1,72 @@ +/* + * 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 { EuiCallOut, EuiOverlayMask, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { AgentConfig } from '../../../types'; + +export const ConfirmDeployConfigModal: React.FunctionComponent<{ + onConfirm: () => void; + onCancel: () => void; + agentCount: number; + agentConfig: AgentConfig; +}> = ({ onConfirm, onCancel, agentCount, agentConfig }) => { + return ( + <EuiOverlayMask> + <EuiConfirmModal + title={ + <FormattedMessage + id="xpack.ingestManager.agentConfig.confirmModalTitle" + defaultMessage="Save and deploy changes" + /> + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + <FormattedMessage + id="xpack.ingestManager.agentConfig.confirmModalCancelButtonLabel" + defaultMessage="Cancel" + /> + } + confirmButtonText={ + <FormattedMessage + id="xpack.ingestManager.agentConfig.confirmModalConfirmButtonLabel" + defaultMessage="Save and deploy changes" + /> + } + buttonColor="primary" + > + <EuiCallOut + iconType="iInCircle" + title={i18n.translate('xpack.ingestManager.agentConfig.confirmModalCalloutTitle', { + defaultMessage: + 'This action will update {agentCount, plural, one {# agent} other {# agents}}', + values: { + agentCount, + }, + })} + > + <FormattedMessage + id="xpack.ingestManager.agentConfig.confirmModalCalloutDescription" + defaultMessage="Fleet has detected that the selected agent configuration, {configName}, is already in use by + some of your agents. As a result of this action, Fleet will deploy updates to all agents + that use this configuration." + values={{ + configName: <b>{agentConfig.name}</b>, + }} + /> + </EuiCallOut> + <EuiSpacer size="l" /> + <FormattedMessage + id="xpack.ingestManager.agentConfig.confirmModalDescription" + defaultMessage="This action can not be undone. Are you sure you wish to continue?" + /> + </EuiConfirmModal> + </EuiOverlayMask> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx index 089b0631c2090..df679d33e0324 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx @@ -9,8 +9,8 @@ import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useCore, sendRequest, sendDeleteDatasource, useConfig } from '../../../hooks'; -import { AGENT_API_ROUTES } from '../../../../../../common/constants'; -import { AgentConfig } from '../../../../../../common/types/models'; +import { AGENT_API_ROUTES, AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AgentConfig } from '../../../types'; interface Props { agentConfig: AgentConfig; @@ -51,7 +51,7 @@ export const DatasourceDeleteProvider: React.FunctionComponent<Props> = ({ query: { page: 1, perPage: 1, - kuery: `agents.config_id : ${agentConfig.id}`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : ${agentConfig.id}`, }, }); setAgentsCount(data?.total || 0); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts index a0fdc656dd7ed..c1811b99588a8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/index.ts @@ -7,3 +7,4 @@ export { AgentConfigForm, agentConfigFormValidation } from './config_form'; export { AgentConfigDeleteProvider } from './config_delete_provider'; export { LinkedAgentCount } from './linked_agent_count'; +export { ConfirmDeployConfigModal } from './confirm_deploy_modal'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx index ec66108c60f68..3860439f26d44 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/linked_agent_count.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLink } from '@elastic/eui'; import { useLink } from '../../../hooks'; -import { FLEET_AGENTS_PATH } from '../../../constants'; +import { FLEET_AGENTS_PATH, AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; export const LinkedAgentCount = memo<{ count: number; agentConfigId: string }>( ({ count, agentConfigId }) => { @@ -21,7 +21,7 @@ export const LinkedAgentCount = memo<{ count: number; agentConfigId: string }>( /> ); return count > 0 ? ( - <EuiLink href={`${FLEET_URI}?kuery=agents.config_id : ${agentConfigId}`}> + <EuiLink href={`${FLEET_URI}?kuery=${AGENT_SAVED_OBJECT_TYPE}.config_id : ${agentConfigId}`}> {displayValue} </EuiLink> ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx deleted file mode 100644 index aa7eab8f5be8d..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/confirm_modal.tsx +++ /dev/null @@ -1,72 +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 React from 'react'; -import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { AgentConfig } from '../../../../types'; - -export const ConfirmCreateDatasourceModal: React.FunctionComponent<{ - onConfirm: () => void; - onCancel: () => void; - agentCount: number; - agentConfig: AgentConfig; -}> = ({ onConfirm, onCancel, agentCount, agentConfig }) => { - return ( - <EuiOverlayMask> - <EuiConfirmModal - title={ - <FormattedMessage - id="xpack.ingestManager.createDatasource.confirmModalTitle" - defaultMessage="Save and deploy changes" - /> - } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - <FormattedMessage - id="xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel" - defaultMessage="Cancel" - /> - } - confirmButtonText={ - <FormattedMessage - id="xpack.ingestManager.createDatasource.confirmModalConfirmButtonLabel" - defaultMessage="Save and deploy changes" - /> - } - buttonColor="primary" - > - <EuiCallOut - iconType="iInCircle" - title={i18n.translate('xpack.ingestManager.createDatasource.confirmModalCalloutTitle', { - defaultMessage: - 'This action will update {agentCount, plural, one {# agent} other {# agents}}', - values: { - agentCount, - }, - })} - > - <FormattedMessage - id="xpack.ingestManager.createDatasource.confirmModalCalloutDescription" - defaultMessage="Fleet has detected that the selected agent configuration, {configName}, is already in use by - some of your agents. As a result of this action, Fleet will deploy updates to all agents - that use this configuration." - values={{ - configName: <b>{agentConfig.name}</b>, - }} - /> - </EuiCallOut> - <EuiSpacer size="l" /> - <FormattedMessage - id="xpack.ingestManager.createDatasource.confirmModalDescription" - defaultMessage="This action can not be undone. Are you sure you wish to continue?" - /> - </EuiConfirmModal> - </EuiOverlayMask> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx index 0e8763cb2d4c0..36e987d007679 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_config.tsx @@ -97,7 +97,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ <EuiFlexGroup direction="column" gutterSize="m"> {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInput.config![varName].value; + const value = datasourceInput.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -105,8 +105,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInput({ - config: { - ...datasourceInput.config, + vars: { + ...datasourceInput.vars, [varName]: { type: varType, value: newValue, @@ -114,7 +114,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} - errors={inputVarsValidationResults.config![varName]} + errors={inputVarsValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> @@ -141,7 +141,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ {isShowingAdvanced ? advancedVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInput.config![varName].value; + const value = datasourceInput.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -149,8 +149,8 @@ export const DatasourceInputConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInput({ - config: { - ...datasourceInput.config, + vars: { + ...datasourceInput.vars, [varName]: { type: varType, value: newValue, @@ -158,7 +158,7 @@ export const DatasourceInputConfig: React.FunctionComponent<{ }, }); }} - errors={inputVarsValidationResults.config![varName]} + errors={inputVarsValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx index 6b0c68ccb7d3f..586fc6b1d4138 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_panel.tsx @@ -158,7 +158,7 @@ export const DatasourceInputPanel: React.FunctionComponent<{ packageInputVars={packageInput.vars} datasourceInput={datasourceInput} updateDatasourceInput={updateDatasourceInput} - inputVarsValidationResults={{ config: inputValidationResults.config }} + inputVarsValidationResults={{ vars: inputValidationResults.vars }} forceShowErrors={forceShowErrors} /> <EuiHorizontalRule margin="m" /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx index 43e8f5a2c060d..7e32936a6fffa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/datasource_input_stream_config.tsx @@ -101,7 +101,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ <EuiFlexGroup direction="column" gutterSize="m"> {requiredVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInputStream.config![varName].value; + const value = datasourceInputStream.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -109,8 +109,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInputStream({ - config: { - ...datasourceInputStream.config, + vars: { + ...datasourceInputStream.vars, [varName]: { type: varType, value: newValue, @@ -118,7 +118,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} - errors={inputStreamValidationResults.config![varName]} + errors={inputStreamValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> @@ -145,7 +145,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ {isShowingAdvanced ? advancedVars.map(varDef => { const { name: varName, type: varType } = varDef; - const value = datasourceInputStream.config![varName].value; + const value = datasourceInputStream.vars![varName].value; return ( <EuiFlexItem key={varName}> <DatasourceInputVarField @@ -153,8 +153,8 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ value={value} onChange={(newValue: any) => { updateDatasourceInputStream({ - config: { - ...datasourceInputStream.config, + vars: { + ...datasourceInputStream.vars, [varName]: { type: varType, value: newValue, @@ -162,7 +162,7 @@ export const DatasourceInputStreamConfig: React.FunctionComponent<{ }, }); }} - errors={inputStreamValidationResults.config![varName]} + errors={inputStreamValidationResults.vars![varName]} forceShowErrors={forceShowErrors} /> </EuiFlexItem> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts index aa564690a6092..3bfca75668911 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/index.ts @@ -5,5 +5,4 @@ */ export { CreateDatasourcePageLayout } from './layout'; export { DatasourceInputPanel } from './datasource_input_panel'; -export { ConfirmCreateDatasourceModal } from './confirm_modal'; export { DatasourceInputVarField } from './datasource_input_var_field'; 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 73a7ba8ec119d..f1e3fea6a0742 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 @@ -18,16 +18,14 @@ import { import { WithHeaderLayout } from '../../../../layouts'; import { AgentConfig, PackageInfo } from '../../../../types'; import { PackageIcon } from '../../../../components/package_icon'; -import { CreateDatasourceFrom, CreateDatasourceStep } from '../types'; +import { CreateDatasourceFrom } from '../types'; export const CreateDatasourcePageLayout: React.FunctionComponent<{ from: CreateDatasourceFrom; - basePath: string; cancelUrl: string; - maxStep: CreateDatasourceStep | ''; agentConfig?: AgentConfig; packageInfo?: PackageInfo; -}> = ({ from, basePath, cancelUrl, maxStep, agentConfig, packageInfo, children }) => { +}> = ({ from, cancelUrl, agentConfig, packageInfo, children }) => { const leftColumn = ( <EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart"> <EuiFlexItem> @@ -41,17 +39,29 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ <EuiFlexItem> <EuiText> <h1> - <FormattedMessage - id="xpack.ingestManager.createDatasource.pageTitle" - defaultMessage="Add data source" - /> + {from === 'edit' ? ( + <FormattedMessage + id="xpack.ingestManager.editDatasource.pageTitle" + defaultMessage="Edit data source" + /> + ) : ( + <FormattedMessage + id="xpack.ingestManager.createDatasource.pageTitle" + defaultMessage="Add data source" + /> + )} </h1> </EuiText> </EuiFlexItem> <EuiFlexItem> <EuiSpacer size="s" /> <EuiText color="subdued" size="s"> - {from === 'config' ? ( + {from === 'edit' ? ( + <FormattedMessage + id="xpack.ingestManager.editDatasource.pageDescription" + defaultMessage="Follow the instructions below to edit this data source." + /> + ) : from === 'config' ? ( <FormattedMessage id="xpack.ingestManager.createDatasource.pageDescriptionfromConfig" defaultMessage="Follow the instructions below to add an integration to this agent configuration." @@ -70,7 +80,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ <EuiFlexGroup justifyContent="flexEnd" direction={'row'} gutterSize="xl"> <EuiFlexItem grow={false}> <EuiSpacer size="s" /> - {agentConfig && from === 'config' ? ( + {agentConfig && (from === 'config' || from === 'edit') ? ( <EuiDescriptionList style={{ textAlign: 'right' }} textStyle="reverse"> <EuiDescriptionListTitle> <FormattedMessage diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 1ad579d591b21..5b7553dd8cf92 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -27,27 +27,37 @@ import { sendGetAgentStatus, } from '../../../hooks'; import { useLinks as useEPMLinks } from '../../epm/hooks'; -import { CreateDatasourcePageLayout, ConfirmCreateDatasourceModal } from './components'; -import { CreateDatasourceFrom, CreateDatasourceStep } from './types'; +import { ConfirmDeployConfigModal } from '../components'; +import { CreateDatasourcePageLayout } from './components'; +import { CreateDatasourceFrom, DatasourceFormState } from './types'; import { DatasourceValidationResults, validateDatasource, validationHasErrors } from './services'; import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; import { StepConfigureDatasource } from './step_configure_datasource'; - import { StepDefineDatasource } from './step_define_datasource'; export const CreateDatasourcePage: React.FunctionComponent = () => { - const { notifications } = useCore(); + const { + notifications, + chrome: { getIsNavDrawerLocked$ }, + } = useCore(); const { fleet: { enabled: isFleetEnabled }, } = useConfig(); const { params: { configId, pkgkey }, - url: basePath, } = useRouteMatch(); const history = useHistory(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; - const [maxStep, setMaxStep] = useState<CreateDatasourceStep | ''>(''); + const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); + + useEffect(() => { + const subscription = getIsNavDrawerLocked$().subscribe((newIsNavDrawerLocked: boolean) => { + setIsNavDrawerLocked(newIsNavDrawerLocked); + }); + + return () => subscription.unsubscribe(); + }); // Agent config and package info states const [agentConfig, setAgentConfig] = useState<AgentConfig>(); @@ -88,10 +98,10 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { if (updatedPackageInfo) { setPackageInfo(updatedPackageInfo); + setFormState('VALID'); } else { setFormState('INVALID'); setPackageInfo(undefined); - setMaxStep(''); } // eslint-disable-next-line no-console @@ -105,7 +115,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } else { setFormState('INVALID'); setAgentConfig(undefined); - setMaxStep(''); } // eslint-disable-next-line no-console @@ -157,9 +166,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<DatasourceFormState>('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -179,6 +186,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', @@ -189,9 +213,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const layoutProps = { from, - basePath, cancelUrl, - maxStep, agentConfig, packageInfo, }; @@ -236,6 +258,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} + validationResults={validationResults!} /> ) : null, }, @@ -247,7 +270,6 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { children: agentConfig && packageInfo ? ( <StepConfigureDatasource - agentConfig={agentConfig} packageInfo={packageInfo} datasource={datasource} updateDatasource={updateDatasource} @@ -260,7 +282,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { return ( <CreateDatasourcePageLayout {...layoutProps}> {formState === 'CONFIRM' && agentConfig && ( - <ConfirmCreateDatasourceModal + <ConfirmDeployConfigModal agentCount={agentCount} agentConfig={agentConfig} onConfirm={onSubmit} @@ -269,7 +291,14 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { )} <EuiSteps steps={steps} /> <EuiSpacer size="l" /> - <EuiBottomBar css={{ zIndex: 5 }} paddingSize="s"> + <EuiBottomBar + css={{ zIndex: 5 }} + className={ + isNavDrawerLocked + ? 'ingestManager__bottomBar-isNavDrawerLocked' + : 'ingestManager__bottomBar' + } + > <EuiFlexGroup gutterSize="s" justifyContent="flexEnd"> <EuiFlexItem grow={false}> <EuiButtonEmpty color="ghost" href={cancelUrl}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts index a45fabeb5ed6a..b970a7d222001 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.test.ts @@ -120,7 +120,7 @@ describe('Ingest Manager - validateDatasource()', () => { { type: 'foo', enabled: true, - config: { + vars: { 'foo-input-var-name': { value: 'foo-input-var-value', type: 'text' }, 'foo-input2-var-name': { value: 'foo-input2-var-value', type: 'text' }, 'foo-input3-var-name': { value: ['test'], type: 'text' }, @@ -130,14 +130,14 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'foo-foo', dataset: 'foo', enabled: true, - config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, }, ], }, { type: 'bar', enabled: true, - config: { + vars: { 'bar-input-var-name': { value: ['value1', 'value2'], type: 'text' }, 'bar-input2-var-name': { value: 'test', type: 'text' }, }, @@ -146,13 +146,13 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'bar-bar', dataset: 'bar', enabled: true, - config: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, + vars: { 'var-name': { value: 'test_yaml: value', type: 'yaml' } }, }, { id: 'bar-bar2', dataset: 'bar2', enabled: true, - config: { 'var-name': { value: undefined, type: 'text' } }, + vars: { 'var-name': { value: undefined, type: 'text' } }, }, ], }, @@ -169,7 +169,7 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'with-disabled-streams-disabled', dataset: 'disabled', enabled: false, - config: { 'var-name': { value: undefined, type: 'text' } }, + vars: { 'var-name': { value: undefined, type: 'text' } }, }, { id: 'with-disabled-streams-disabled2', @@ -188,7 +188,7 @@ describe('Ingest Manager - validateDatasource()', () => { { type: 'foo', enabled: true, - config: { + vars: { 'foo-input-var-name': { value: undefined, type: 'text' }, 'foo-input2-var-name': { value: '', type: 'text' }, 'foo-input3-var-name': { value: [], type: 'text' }, @@ -198,14 +198,14 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'foo-foo', dataset: 'foo', enabled: true, - config: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, + vars: { 'var-name': { value: 'invalidyaml: test\n foo bar:', type: 'yaml' } }, }, ], }, { type: 'bar', enabled: true, - config: { + vars: { 'bar-input-var-name': { value: 'invalid value for multi', type: 'text' }, 'bar-input2-var-name': { value: undefined, type: 'text' }, }, @@ -214,13 +214,13 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'bar-bar', dataset: 'bar', enabled: true, - config: { 'var-name': { value: ' \n\n', type: 'yaml' } }, + vars: { 'var-name': { value: ' \n\n', type: 'yaml' } }, }, { id: 'bar-bar2', dataset: 'bar2', enabled: true, - config: { 'var-name': { value: undefined, type: 'text' } }, + vars: { 'var-name': { value: undefined, type: 'text' } }, }, ], }, @@ -237,7 +237,7 @@ describe('Ingest Manager - validateDatasource()', () => { id: 'with-disabled-streams-disabled', dataset: 'disabled', enabled: false, - config: { + vars: { 'var-name': { value: 'invalid value but not checked due to not enabled', type: 'text', @@ -259,22 +259,22 @@ describe('Ingest Manager - validateDatasource()', () => { description: null, inputs: { foo: { - config: { + vars: { 'foo-input-var-name': null, 'foo-input2-var-name': null, 'foo-input3-var-name': null, }, - streams: { 'foo-foo': { config: { 'var-name': null } } }, + streams: { 'foo-foo': { vars: { 'var-name': null } } }, }, bar: { - config: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, + vars: { 'bar-input-var-name': null, 'bar-input2-var-name': null }, streams: { - 'bar-bar': { config: { 'var-name': null } }, - 'bar-bar2': { config: { 'var-name': null } }, + 'bar-bar': { vars: { 'var-name': null } }, + 'bar-bar2': { vars: { 'var-name': null } }, }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, }, }, }; @@ -289,25 +289,25 @@ describe('Ingest Manager - validateDatasource()', () => { description: null, inputs: { foo: { - config: { + vars: { 'foo-input-var-name': null, 'foo-input2-var-name': ['foo-input2-var-name is required'], 'foo-input3-var-name': ['foo-input3-var-name is required'], }, - streams: { 'foo-foo': { config: { 'var-name': ['Invalid YAML format'] } } }, + streams: { 'foo-foo': { vars: { 'var-name': ['Invalid YAML format'] } } }, }, bar: { - config: { + vars: { 'bar-input-var-name': ['Invalid format'], 'bar-input2-var-name': ['bar-input2-var-name is required'], }, streams: { - 'bar-bar': { config: { 'var-name': ['var-name is required'] } }, - 'bar-bar2': { config: { 'var-name': null } }, + 'bar-bar': { vars: { 'var-name': ['var-name is required'] } }, + 'bar-bar2': { vars: { 'var-name': null } }, }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, }, }, }); @@ -336,25 +336,25 @@ describe('Ingest Manager - validateDatasource()', () => { description: null, inputs: { foo: { - config: { + vars: { 'foo-input-var-name': null, 'foo-input2-var-name': ['foo-input2-var-name is required'], 'foo-input3-var-name': ['foo-input3-var-name is required'], }, - streams: { 'foo-foo': { config: { 'var-name': null } } }, + streams: { 'foo-foo': { vars: { 'var-name': null } } }, }, bar: { - config: { + vars: { 'bar-input-var-name': ['Invalid format'], 'bar-input2-var-name': ['bar-input2-var-name is required'], }, streams: { - 'bar-bar': { config: { 'var-name': null } }, - 'bar-bar2': { config: { 'var-name': null } }, + 'bar-bar': { vars: { 'var-name': null } }, + 'bar-bar2': { vars: { 'var-name': null } }, }, }, 'with-disabled-streams': { - streams: { 'with-disabled-streams-disabled': { config: { 'var-name': null } } }, + streams: { 'with-disabled-streams-disabled': { vars: { 'var-name': null } } }, }, }, }); @@ -411,7 +411,7 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns true for stream validation results with errors', () => { expect( validationHasErrors({ - config: { foo: ['foo error'], bar: null }, + vars: { foo: ['foo error'], bar: null }, }) ).toBe(true); }); @@ -419,7 +419,7 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns false for stream validation results with no errors', () => { expect( validationHasErrors({ - config: { foo: null, bar: null }, + vars: { foo: null, bar: null }, }) ).toBe(false); }); @@ -427,14 +427,14 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns true for input validation results with errors', () => { expect( validationHasErrors({ - config: { foo: ['foo error'], bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: ['foo error'], bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }) ).toBe(true); expect( validationHasErrors({ - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, }) ).toBe(true); }); @@ -442,8 +442,8 @@ describe('Ingest Manager - validationHasErrors()', () => { it('returns false for input validation results with no errors', () => { expect( validationHasErrors({ - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }) ).toBe(false); }); @@ -455,8 +455,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }, }, }) @@ -467,8 +467,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: ['foo error'], bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: ['foo error'], bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }, }, }) @@ -479,8 +479,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: ['foo error'], bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: ['foo error'], bar: null } } }, }, }, }) @@ -494,8 +494,8 @@ describe('Ingest Manager - validationHasErrors()', () => { description: null, inputs: { input1: { - config: { foo: null, bar: null }, - streams: { stream1: { config: { foo: null, bar: null } } }, + vars: { foo: null, bar: null }, + streams: { stream1: { vars: { foo: null, bar: null } } }, }, }, }) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts index 518e2bfc1af07..3a712b072dac1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/services/validate_datasource.ts @@ -21,7 +21,7 @@ type Errors = string[] | null; type ValidationEntry = Record<string, Errors>; export interface DatasourceConfigValidationResults { - config?: ValidationEntry; + vars?: ValidationEntry; } export type DatasourceInputValidationResults = DatasourceConfigValidationResults & { @@ -77,12 +77,12 @@ export const validateDatasource = ( // Validate each datasource input with either its own config fields or streams datasource.inputs.forEach(input => { - if (!input.config && !input.streams) { + if (!input.vars && !input.streams) { return; } const inputValidationResults: DatasourceInputValidationResults = { - config: undefined, + vars: undefined, streams: {}, }; @@ -95,27 +95,27 @@ export const validateDatasource = ( ); // Validate input-level config fields - const inputConfigs = Object.entries(input.config || {}); + const inputConfigs = Object.entries(input.vars || {}); if (inputConfigs.length) { - inputValidationResults.config = inputConfigs.reduce((results, [name, configEntry]) => { + inputValidationResults.vars = inputConfigs.reduce((results, [name, configEntry]) => { results[name] = input.enabled ? validateDatasourceConfig(configEntry, inputVarsByName[name]) : null; return results; }, {} as ValidationEntry); } else { - delete inputValidationResults.config; + delete inputValidationResults.vars; } // Validate each input stream with config fields if (input.streams.length) { input.streams.forEach(stream => { - if (!stream.config) { + if (!stream.vars) { return; } const streamValidationResults: DatasourceConfigValidationResults = { - config: undefined, + vars: undefined, }; const streamVarsByName = ( @@ -130,7 +130,7 @@ export const validateDatasource = ( }, {} as Record<string, RegistryVarsEntry>); // Validate stream-level config fields - streamValidationResults.config = Object.entries(stream.config).reduce( + streamValidationResults.vars = Object.entries(stream.vars).reduce( (results, [name, configEntry]) => { results[name] = input.enabled && stream.enabled @@ -147,7 +147,7 @@ export const validateDatasource = ( delete inputValidationResults.streams; } - if (inputValidationResults.config || inputValidationResults.streams) { + if (inputValidationResults.vars || inputValidationResults.streams) { validationResults.inputs![input.type] = inputValidationResults; } }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx index 843891b63ca01..118c7e30f13f4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_configure_datasource.tsx @@ -3,7 +3,7 @@ * 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, { useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPanel, @@ -15,75 +15,21 @@ import { EuiCallOut, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { - AgentConfig, - PackageInfo, - Datasource, - NewDatasource, - DatasourceInput, -} from '../../../types'; +import { PackageInfo, NewDatasource, DatasourceInput } from '../../../types'; import { Loading } from '../../../components'; -import { packageToConfigDatasourceInputs } from '../../../services'; import { DatasourceValidationResults, validationHasErrors } from './services'; import { DatasourceInputPanel } from './components'; export const StepConfigureDatasource: React.FunctionComponent<{ - agentConfig: AgentConfig; packageInfo: PackageInfo; datasource: NewDatasource; updateDatasource: (fields: Partial<NewDatasource>) => 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 792389381eaf0..c4d602c2c2081 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<NewDatasource>) => void; -}> = ({ agentConfig, packageInfo, datasource, updateDatasource }) => { + validationResults: DatasourceValidationResults; +}> = ({ agentConfig, packageInfo, datasource, updateDatasource, validationResults }) => { // Form show/hide states const [isShowingAdvancedDefine, setIsShowingAdvancedDefine] = useState<boolean>(false); @@ -64,11 +67,13 @@ export const StepDefineDatasource: React.FunctionComponent<{ } }, [datasource.package, datasource.config_id, agentConfig, packageInfo, updateDatasource]); - return ( + return validationResults ? ( <> <EuiFlexGrid columns={2}> <EuiFlexItem> <EuiFormRow + isInvalid={!!validationResults.name} + error={validationResults.name} label={ <FormattedMessage id="xpack.ingestManager.createDatasource.stepConfigure.datasourceNameInputLabel" @@ -102,6 +107,8 @@ export const StepDefineDatasource: React.FunctionComponent<{ /> </EuiText> } + isInvalid={!!validationResults.description} + error={validationResults.description} > <EuiFieldText value={datasource.description} @@ -161,5 +168,7 @@ export const StepDefineDatasource: React.FunctionComponent<{ </Fragment> ) : null} </> + ) : ( + <Loading /> ); }; 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 bd05be2d8a558..10b30a5696d83 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,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export type CreateDatasourceFrom = 'package' | 'config'; -export type CreateDatasourceStep = 'selectConfig' | 'selectPackage' | 'configure' | 'review'; +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/config_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/config_form.tsx deleted file mode 100644 index c4f8d944ceb14..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/config_form.tsx +++ /dev/null @@ -1,94 +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 React, { useState } from 'react'; -import { EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { AgentConfig } from '../../../../types'; - -interface ValidationResults { - [key: string]: JSX.Element[]; -} - -export const configFormValidation = (config: Partial<AgentConfig>): ValidationResults => { - const errors: ValidationResults = {}; - - if (!config.name?.trim()) { - errors.name = [ - <FormattedMessage - id="xpack.ingestManager.configForm.nameRequiredErrorMessage" - defaultMessage="Config name is required" - />, - ]; - } - - return errors; -}; - -interface Props { - config: Partial<AgentConfig>; - updateConfig: (u: Partial<AgentConfig>) => void; - validation: ValidationResults; -} - -export const ConfigForm: React.FunctionComponent<Props> = ({ - config, - updateConfig, - validation, -}) => { - const [touchedFields, setTouchedFields] = useState<{ [key: string]: boolean }>({}); - const fields: Array<{ name: 'name' | 'description' | 'namespace'; label: JSX.Element }> = [ - { - name: 'name', - label: ( - <FormattedMessage - id="xpack.ingestManager.configForm.nameFieldLabel" - defaultMessage="Name" - /> - ), - }, - { - name: 'description', - label: ( - <FormattedMessage - id="xpack.ingestManager.configForm.descriptionFieldLabel" - defaultMessage="Description" - /> - ), - }, - { - name: 'namespace', - label: ( - <FormattedMessage - id="xpack.ingestManager.configForm.namespaceFieldLabel" - defaultMessage="Namespace" - /> - ), - }, - ]; - - return ( - <EuiForm> - {fields.map(({ name, label }) => { - return ( - <EuiFormRow - key={name} - label={label} - error={touchedFields[name] && validation[name] ? validation[name] : null} - isInvalid={Boolean(touchedFields[name] && validation[name])} - > - <EuiFieldText - value={config[name]} - onChange={e => updateConfig({ [name]: e.target.value })} - isInvalid={Boolean(touchedFields[name] && validation[name])} - onBlur={() => setTouchedFields({ ...touchedFields, [name]: true })} - /> - </EuiFormRow> - ); - })} - </EuiForm> - ); -}; 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 1eee9f6b0c346..a0418c5f256c4 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<Props> = ({ }) => { 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<Props> = ({ <TableRowActions items={[ // FIXME: implement View datasource action + // <EuiContextMenuItem + // disabled + // icon="inspect" + // onClick={() => {}} + // key="datasourceView" + // > + // <FormattedMessage + // id="xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle" + // defaultMessage="View data source" + // /> + // </EuiContextMenuItem>, <EuiContextMenuItem - disabled - icon="inspect" - onClick={() => {}} - key="datasourceView" - > - <FormattedMessage - id="xpack.ingestManager.configDetails.datasourcesTable.viewActionTitle" - defaultMessage="View data source" - /> - </EuiContextMenuItem>, - // FIXME: implement Edit datasource action - <EuiContextMenuItem - disabled + disabled={!hasWriteCapabilities} icon="pencil" - onClick={() => {}} + href={`${editDatasourceLink}/${datasource.id}`} key="datasourceEdit" > <FormattedMessage @@ -225,12 +225,12 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({ /> </EuiContextMenuItem>, // FIXME: implement Copy datasource action - <EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="datasourceCopy"> - <FormattedMessage - id="xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle" - defaultMessage="Copy data source" - /> - </EuiContextMenuItem>, + // <EuiContextMenuItem disabled icon="copy" onClick={() => {}} key="datasourceCopy"> + // <FormattedMessage + // id="xpack.ingestManager.configDetails.datasourcesTable.copyActionTitle" + // defaultMessage="Copy data source" + // /> + // </EuiContextMenuItem>, <DatasourceDeleteProvider agentConfig={config} key="datasourceDelete"> {deleteDatasourcePrompt => { return ( @@ -256,7 +256,7 @@ export const DatasourcesTable: React.FunctionComponent<Props> = ({ ], }, ], - [config, hasWriteCapabilities, refreshConfig] + [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] ); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/donut_chart.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/donut_chart.tsx deleted file mode 100644 index 408ccc6e951f6..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/donut_chart.tsx +++ /dev/null @@ -1,65 +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 React, { useEffect, useRef } from 'react'; -import d3 from 'd3'; -import { EuiFlexItem } from '@elastic/eui'; - -interface DonutChartProps { - data: { - [key: string]: number; - }; - height: number; - width: number; -} - -export const DonutChart = ({ height, width, data }: DonutChartProps) => { - const chartElement = useRef<SVGSVGElement | null>(null); - - useEffect(() => { - if (chartElement.current !== null) { - // we must remove any existing paths before painting - d3.selectAll('g').remove(); - const svgElement = d3 - .select(chartElement.current) - .append('g') - .attr('transform', `translate(${width / 2}, ${height / 2})`); - const color = d3.scale - .ordinal() - // @ts-ignore - .domain(data) - .range(['#017D73', '#98A2B3', '#BD271E']); - const pieGenerator = d3.layout - .pie() - .value(({ value }: any) => value) - // these start/end angles will reverse the direction of the pie, - // which matches our design - .startAngle(2 * Math.PI) - .endAngle(0); - - svgElement - .selectAll('g') - // @ts-ignore - .data(pieGenerator(d3.entries(data))) - .enter() - .append('path') - .attr( - 'd', - // @ts-ignore attr does not expect a param of type Arc<Arc> but it behaves as desired - d3.svg - .arc() - .innerRadius(width * 0.28) - .outerRadius(Math.min(width, height) / 2 - 10) - ) - .attr('fill', (d: any) => color(d.data.key)); - } - }, [data, height, width]); - return ( - <EuiFlexItem grow={false}> - <svg ref={chartElement} width={width} height={height} /> - </EuiFlexItem> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/edit_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/edit_config.tsx deleted file mode 100644 index 65eb86d7d871f..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/edit_config.tsx +++ /dev/null @@ -1,135 +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 React, { useState } from 'react'; -import { - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiButton, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useCore, sendRequest } from '../../../../hooks'; -import { agentConfigRouteService } from '../../../../services'; -import { AgentConfig } from '../../../../types'; -import { ConfigForm, configFormValidation } from './config_form'; - -interface Props { - agentConfig: AgentConfig; - onClose: () => void; -} - -export const EditConfigFlyout: React.FunctionComponent<Props> = ({ - agentConfig: originalAgentConfig, - onClose, -}) => { - const { notifications } = useCore(); - const [config, setConfig] = useState<Partial<AgentConfig>>({ - name: originalAgentConfig.name, - description: originalAgentConfig.description, - }); - const [isLoading, setIsLoading] = useState<boolean>(false); - const updateConfig = (updatedFields: Partial<AgentConfig>) => { - setConfig({ - ...config, - ...updatedFields, - }); - }; - const validation = configFormValidation(config); - - const header = ( - <EuiFlyoutHeader hasBorder aria-labelledby="FleetEditConfigFlyoutTitle"> - <EuiTitle size="m"> - <h2 id="FleetEditConfigFlyoutTitle"> - <FormattedMessage - id="xpack.ingestManager.editConfig.flyoutTitle" - defaultMessage="Edit config" - /> - </h2> - </EuiTitle> - </EuiFlyoutHeader> - ); - - const body = ( - <EuiFlyoutBody> - <ConfigForm config={config} updateConfig={updateConfig} validation={validation} /> - </EuiFlyoutBody> - ); - - const footer = ( - <EuiFlyoutFooter> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> - <FormattedMessage - id="xpack.ingestManager.editConfig.cancelButtonLabel" - defaultMessage="Cancel" - /> - </EuiButtonEmpty> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiButton - fill - isLoading={isLoading} - disabled={isLoading || Object.keys(validation).length > 0} - onClick={async () => { - setIsLoading(true); - try { - const { error } = await sendRequest({ - path: agentConfigRouteService.getUpdatePath(originalAgentConfig.id), - method: 'put', - body: JSON.stringify(config), - }); - if (!error) { - notifications.toasts.addSuccess( - i18n.translate('xpack.ingestManager.editConfig.successNotificationTitle', { - defaultMessage: "Agent config '{name}' updated", - values: { name: config.name }, - }) - ); - } else { - notifications.toasts.addDanger( - error - ? error.message - : i18n.translate('xpack.ingestManager.editConfig.errorNotificationTitle', { - defaultMessage: 'Unable to update agent config', - }) - ); - } - } catch (e) { - notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.editConfig.errorNotificationTitle', { - defaultMessage: 'Unable to update agent config', - }) - ); - } - setIsLoading(false); - onClose(); - }} - > - <FormattedMessage - id="xpack.ingestManager.editConfig.submitButtonLabel" - defaultMessage="Update" - /> - </EuiButton> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlyoutFooter> - ); - - return ( - <EuiFlyout onClose={onClose} size="m" maxWidth={400}> - {header} - {body} - {footer} - </EuiFlyout> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts index 918b361a60d79..0123bd46c16e7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/index.ts @@ -4,5 +4,3 @@ * you may not use this file except in compliance with the Elastic License. */ export { DatasourcesTable } from './datasources/datasources_table'; -export { DonutChart } from './donut_chart'; -export { EditConfigFlyout } from './edit_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/settings/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/settings/index.tsx new file mode 100644 index 0000000000000..2d9d29bfc1ac7 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/settings/index.tsx @@ -0,0 +1,216 @@ +/* + * 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, { memo, useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import styled from 'styled-components'; +import { EuiBottomBar, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AGENT_CONFIG_PATH } from '../../../../../constants'; +import { AgentConfig } from '../../../../../types'; +import { + useCore, + useCapabilities, + sendUpdateAgentConfig, + useConfig, + sendGetAgentStatus, +} from '../../../../../hooks'; +import { + AgentConfigForm, + agentConfigFormValidation, + ConfirmDeployConfigModal, +} from '../../../components'; +import { useConfigRefresh } from '../../hooks'; + +const FormWrapper = styled.div` + max-width: 800px; + margin-right: auto; + margin-left: auto; +`; + +export const ConfigSettingsView = memo<{ config: AgentConfig }>( + ({ config: originalAgentConfig }) => { + const { + notifications, + chrome: { getIsNavDrawerLocked$ }, + } = useCore(); + const { + fleet: { enabled: isFleetEnabled }, + } = useConfig(); + const history = useHistory(); + const hasWriteCapabilites = useCapabilities().write; + const refreshConfig = useConfigRefresh(); + const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); + const [agentConfig, setAgentConfig] = useState<AgentConfig>({ + ...originalAgentConfig, + }); + const [isLoading, setIsLoading] = useState<boolean>(false); + const [hasChanges, setHasChanges] = useState<boolean>(false); + const [agentCount, setAgentCount] = useState<number>(0); + const [withSysMonitoring, setWithSysMonitoring] = useState<boolean>(true); + const validation = agentConfigFormValidation(agentConfig); + + useEffect(() => { + const subscription = getIsNavDrawerLocked$().subscribe((newIsNavDrawerLocked: boolean) => { + setIsNavDrawerLocked(newIsNavDrawerLocked); + }); + + return () => subscription.unsubscribe(); + }); + + const updateAgentConfig = (updatedFields: Partial<AgentConfig>) => { + setAgentConfig({ + ...agentConfig, + ...updatedFields, + }); + setHasChanges(true); + }; + + const submitUpdateAgentConfig = async () => { + setIsLoading(true); + try { + const { name, description, namespace, monitoring_enabled } = agentConfig; + const { data, error } = await sendUpdateAgentConfig(agentConfig.id, { + name, + description, + namespace, + monitoring_enabled, + }); + if (data?.success) { + notifications.toasts.addSuccess( + i18n.translate('xpack.ingestManager.editAgentConfig.successNotificationTitle', { + defaultMessage: "Successfully updated '{name}' settings", + values: { name: agentConfig.name }, + }) + ); + refreshConfig(); + setHasChanges(false); + } else { + notifications.toasts.addDanger( + error + ? error.message + : i18n.translate('xpack.ingestManager.editAgentConfig.errorNotificationTitle', { + defaultMessage: 'Unable to update agent config', + }) + ); + } + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.ingestManager.editAgentConfig.errorNotificationTitle', { + defaultMessage: 'Unable to update agent config', + }) + ); + } + setIsLoading(false); + }; + + const onSubmit = async () => { + // Retrieve agent count if fleet is enabled + if (isFleetEnabled) { + setIsLoading(true); + const { data } = await sendGetAgentStatus({ configId: agentConfig.id }); + if (data?.results.total) { + setAgentCount(data.results.total); + } else { + await submitUpdateAgentConfig(); + } + } else { + await submitUpdateAgentConfig(); + } + }; + + return ( + <FormWrapper> + {agentCount ? ( + <ConfirmDeployConfigModal + agentCount={agentCount} + agentConfig={agentConfig} + onConfirm={() => { + setAgentCount(0); + submitUpdateAgentConfig(); + }} + onCancel={() => { + setAgentCount(0); + setIsLoading(false); + }} + /> + ) : null} + <AgentConfigForm + agentConfig={agentConfig} + updateAgentConfig={updateAgentConfig} + withSysMonitoring={withSysMonitoring} + updateSysMonitoring={newValue => setWithSysMonitoring(newValue)} + validation={validation} + isEditing={true} + onDelete={() => { + history.push(AGENT_CONFIG_PATH); + }} + /> + {hasChanges ? ( + <EuiBottomBar + css={{ zIndex: 5 }} + className={ + isNavDrawerLocked + ? 'ingestManager__bottomBar-isNavDrawerLocked' + : 'ingestManager__bottomBar' + } + > + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <EuiFlexItem> + <FormattedMessage + id="xpack.ingestManager.editAgentConfig.unsavedChangesText" + defaultMessage="You have unsaved changes" + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup gutterSize="s" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + color="ghost" + onClick={() => { + setAgentConfig({ ...originalAgentConfig }); + setHasChanges(false); + }} + > + <FormattedMessage + id="xpack.ingestManager.editAgentConfig.cancelButtonText" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + onClick={onSubmit} + isLoading={isLoading} + isDisabled={ + !hasWriteCapabilites || isLoading || Object.keys(validation).length > 0 + } + iconType="save" + color="primary" + fill + > + {isLoading ? ( + <FormattedMessage + id="xpack.ingestManager.editAgentConfig.savingButtonText" + defaultMessage="Saving…" + /> + ) : ( + <FormattedMessage + id="xpack.ingestManager.editAgentConfig.saveButtonText" + defaultMessage="Save changes" + /> + )} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiBottomBar> + ) : null} + </FormWrapper> + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index 56b109a9bc062..9f2088521ed38 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx @@ -15,7 +15,7 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import { AgentConfig } from '../../../../../../../../common/types/models'; +import { AgentConfig } from '../../../../../types'; import { useGetOneAgentConfigFull, useGetEnrollmentAPIKeys, @@ -27,7 +27,9 @@ import { Loading } from '../../../../../components'; const CONFIG_KEYS_ORDER = [ 'id', + 'name', 'revision', + 'type', 'outputs', 'datasources', 'enabled', @@ -43,7 +45,7 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { page: 1, perPage: 1000, }); - const apiKeyRequest = useGetOneEnrollmentAPIKey(apiKeysRequest.data?.list?.[0]?.id as string); + const apiKeyRequest = useGetOneEnrollmentAPIKey(apiKeysRequest.data?.list?.[0]?.id); if (fullConfigRequest.isLoading && !fullConfigRequest.data) { return <Loading />; @@ -52,7 +54,7 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { return ( <EuiFlexGroup> <EuiFlexItem grow={7}> - <EuiCodeBlock language="yaml" isCopyable> + <EuiCodeBlock language="yaml" isCopyable overflowHeight={500}> {dump(fullConfigRequest.data.item, { sortKeys: (keyA: string, keyB: string) => { const indexA = CONFIG_KEYS_ORDER.indexOf(keyA); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts index 19be93676a734..76c6d64eb9e07 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ export { useGetAgentStatus, AgentStatusRefreshContext } from './use_agent_status'; -export { ConfigRefreshContext } from './use_config'; +export { ConfigRefreshContext, useConfigRefresh } from './use_config'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 450f86df5c03a..20a39724ce23c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -3,7 +3,7 @@ * 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, { Fragment, memo, useCallback, useMemo, useState } from 'react'; +import React, { Fragment, memo, useMemo, useState } from 'react'; import { Redirect, useRouteMatch, Switch, Route } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; @@ -26,12 +26,12 @@ import { useGetOneAgentConfig } from '../../../hooks'; import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; -import { EditConfigFlyout } from './components'; import { LinkedAgentCount } from '../components'; import { useAgentConfigLink } from './hooks/use_details_uri'; import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from './constants'; import { ConfigDatasourcesView } from './components/datasources'; import { ConfigYamlView } from './components/yaml'; +import { ConfigSettingsView } from './components/settings'; const Divider = styled.div` width: 0; @@ -70,14 +70,6 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { const configDetailsYamlLink = useAgentConfigLink('details-yaml', { configId }); const configDetailsSettingsLink = useAgentConfigLink('details-settings', { configId }); - // Flyout states - const [isEditConfigFlyoutOpen, setIsEditConfigFlyoutOpen] = useState<boolean>(false); - - const refreshData = useCallback(() => { - refreshAgentConfig(); - refreshAgentStatus(); - }, [refreshAgentConfig, refreshAgentStatus]); - const headerLeftContent = useMemo( () => ( <React.Fragment> @@ -196,7 +188,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { return [ { id: 'datasources', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasouces', { + name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasourcesTabText', { defaultMessage: 'Data sources', }), href: configDetailsLink, @@ -204,15 +196,15 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { }, { id: 'yaml', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlFile', { - defaultMessage: 'YAML File', + name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlTabText', { + defaultMessage: 'YAML', }), href: configDetailsYamlLink, isSelected: tabId === 'yaml', }, { id: 'settings', - name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settings', { + name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), href: configDetailsSettingsLink, @@ -269,16 +261,6 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - {isEditConfigFlyoutOpen ? ( - <EditConfigFlyout - onClose={() => { - setIsEditConfigFlyoutOpen(false); - refreshData(); - }} - agentConfig={agentConfig} - /> - ) : null} - <Switch> <Route path={`${DETAILS_ROUTER_PATH}/yaml`} @@ -289,8 +271,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { <Route path={`${DETAILS_ROUTER_PATH}/settings`} render={() => { - // TODO: Settings implementation tracked via: https://github.com/elastic/kibana/issues/57959 - return <div>Settings placeholder</div>; + return <ConfigSettingsView config={agentConfig} />; }} /> <Route 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 0000000000000..5f3d59ad60f1f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -0,0 +1,321 @@ +/* + * 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 { ConfirmDeployConfigModal } from '../components'; +import { CreateDatasourcePageLayout } 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<boolean>(true); + const [loadingError, setLoadingError] = useState<Error>(); + const [agentConfig, setAgentConfig] = useState<AgentConfig>(); + const [packageInfo, setPackageInfo] = useState<PackageInfo>(); + const [datasource, setDatasource] = useState<NewDatasource>({ + 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<number>(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<DatasourceValidationResults>(); + const hasErrors = validationResults ? validationHasErrors(validationResults) : false; + + // Update datasource method + const updateDatasource = (updatedFields: Partial<NewDatasource>) => { + 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<DatasourceFormState>('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 ( + <CreateDatasourcePageLayout {...layoutProps}> + {isLoadingData ? ( + <Loading /> + ) : loadingError || !agentConfig || !packageInfo ? ( + <Error + title={ + <FormattedMessage + id="xpack.ingestManager.editDatasource.errorLoadingDataTitle" + defaultMessage="Error loading data" + /> + } + error={ + loadingError || + i18n.translate('xpack.ingestManager.editDatasource.errorLoadingDataMessage', { + defaultMessage: 'There was an error loading this data source information', + }) + } + /> + ) : ( + <> + {formState === 'CONFIRM' && ( + <ConfirmDeployConfigModal + agentCount={agentCount} + agentConfig={agentConfig} + onConfirm={onSubmit} + onCancel={() => setFormState('VALID')} + /> + )} + <EuiSteps + steps={[ + { + title: i18n.translate( + 'xpack.ingestManager.editDatasource.stepDefineDatasourceTitle', + { + defaultMessage: 'Define your data source', + } + ), + children: ( + <StepDefineDatasource + agentConfig={agentConfig} + packageInfo={packageInfo} + datasource={datasource} + updateDatasource={updateDatasource} + validationResults={validationResults!} + /> + ), + }, + { + title: i18n.translate( + 'xpack.ingestManager.editDatasource.stepConfgiureDatasourceTitle', + { + defaultMessage: 'Select the data you want to collect', + } + ), + children: ( + <StepConfigureDatasource + packageInfo={packageInfo} + datasource={datasource} + updateDatasource={updateDatasource} + validationResults={validationResults!} + submitAttempted={formState === 'INVALID'} + /> + ), + }, + ]} + /> + <EuiSpacer size="l" /> + <EuiBottomBar css={{ zIndex: 5 }} paddingSize="s"> + <EuiFlexGroup gutterSize="s" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty color="ghost" href={cancelUrl}> + <FormattedMessage + id="xpack.ingestManager.editDatasource.cancelButton" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + onClick={onSubmit} + isLoading={formState === 'LOADING'} + disabled={formState !== 'VALID'} + iconType="save" + color="primary" + fill + > + <FormattedMessage + id="xpack.ingestManager.editDatasource.saveButton" + defaultMessage="Save data source" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiBottomBar> + </> + )} + </CreateDatasourcePageLayout> + ); +}; 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 71ada155373bf..ef88aa5d17f1e 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 = () => ( <Router> <Switch> + <Route path="/configs/:configId/edit-datasource/:datasourceId"> + <EditDatasourcePage /> + </Route> <Route path="/configs/:configId/add-datasource"> <CreateDatasourcePage /> </Route> 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 1fe116ef36090..9f582e7e2fbe6 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<Props> = ({ onClos description: '', namespace: '', is_default: undefined, + monitoring_enabled: ['logs', 'metrics'], }); const [isLoading, setIsLoading] = useState<boolean>(false); const [withSysMonitoring, setWithSysMonitoring] = useState<boolean>(true); 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 0498e814440c7..3dcc19bc4a5ae 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" /> </EuiContextMenuItem>, - - <EuiContextMenuItem disabled={true} icon="copy" key="copyConfig"> - <FormattedMessage - id="xpack.ingestManager.agentConfigList.copyConfigActionText" - defaultMessage="Copy configuration" - /> - </EuiContextMenuItem>, - - <AgentConfigDeleteProvider key="deleteConfig"> - {deleteAgentConfigsPrompt => { - return ( - <DangerEuiContextMenuItem - icon="trash" - disabled={Boolean(config.is_default)} - onClick={() => deleteAgentConfigsPrompt([config.id], onDelete)} - > - <FormattedMessage - id="xpack.ingestManager.agentConfigList.deleteConfigActionText" - defaultMessage="Delete Configuration" - /> - </DangerEuiContextMenuItem> - ); - }} - </AgentConfigDeleteProvider>, + // <EuiContextMenuItem disabled={true} icon="copy" key="copyConfig"> + // <FormattedMessage + // id="xpack.ingestManager.agentConfigList.copyConfigActionText" + // defaultMessage="Copy configuration" + // /> + // </EuiContextMenuItem>, ]} /> ); @@ -156,7 +136,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { : urlParams.kuery ?? '' ); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgentConfigs, setSelectedAgentConfigs] = useState<AgentConfig[]>([]); const history = useHistory(); const isCreateAgentConfigFlyoutOpen = 'create' in urlParams; const setIsCreateAgentConfigFlyoutOpen = useCallback( @@ -191,7 +170,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Name', }), width: '20%', - // FIXME: use version once available - see: https://github.com/elastic/kibana/issues/56750 render: (name: string, agentConfig: AgentConfig) => ( <EuiFlexGroup gutterSize="s" alignItems="baseline" style={{ minWidth: 0 }}> <EuiFlexItem grow={false} style={NO_WRAP_TRUNCATE_STYLE}> @@ -322,34 +300,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { /> ) : null} <EuiFlexGroup alignItems={'center'} gutterSize="m"> - {selectedAgentConfigs.length ? ( - <EuiFlexItem> - <AgentConfigDeleteProvider> - {deleteAgentConfigsPrompt => ( - <EuiButton - color="danger" - onClick={() => { - deleteAgentConfigsPrompt( - selectedAgentConfigs.map(agentConfig => agentConfig.id), - () => { - sendRequest(); - setSelectedAgentConfigs([]); - } - ); - }} - > - <FormattedMessage - id="xpack.ingestManager.agentConfigList.deleteButton" - defaultMessage="Delete {count, plural, one {# agent config} other {# agent configs}}" - values={{ - count: selectedAgentConfigs.length, - }} - /> - </EuiButton> - )} - </AgentConfigDeleteProvider> - </EuiFlexItem> - ) : null} <EuiFlexItem grow={4}> <SearchBar value={search} @@ -406,13 +356,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { 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/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx new file mode 100644 index 0000000000000..7b0641e66fd43 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { DataStreamListPage } from './list_page'; + +export const DataStreamApp: React.FunctionComponent = () => { + return ( + <Router> + <Switch> + <Route path="/data-streams"> + <DataStreamListPage /> + </Route> + </Switch> + </Router> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx new file mode 100644 index 0000000000000..d7a3e933f3bb5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -0,0 +1,283 @@ +/* + * 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, { useMemo } from 'react'; +import { + EuiBadge, + EuiButton, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiInMemoryTable, + EuiTableActionsColumnType, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; +import { DataStream } from '../../../types'; +import { WithHeaderLayout } from '../../../layouts'; +import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; + +const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( + <WithHeaderLayout + leftColumn={ + <EuiFlexGroup direction="column" gutterSize="m"> + <EuiFlexItem> + <EuiText> + <h1> + <FormattedMessage + id="xpack.ingestManager.dataStreamList.pageTitle" + defaultMessage="Data streams" + /> + </h1> + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <EuiText color="subdued"> + <p> + <FormattedMessage + id="xpack.ingestManager.dataStreamList.pageSubtitle" + defaultMessage="Manage the data created by your agents." + /> + </p> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + } + > + {children} + </WithHeaderLayout> +); + +export const DataStreamListPage: React.FunctionComponent<{}> = () => { + const { + data: { fieldFormats }, + } = useStartDeps(); + + const { pagination, pageSizeOptions } = usePagination(); + + // Fetch agent configs + const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams(); + + // Some configs retrieved, set up table props + const columns = useMemo(() => { + const cols: Array< + EuiTableFieldDataColumnType<DataStream> | EuiTableActionsColumnType<DataStream> + > = [ + { + field: 'dataset', + sortable: true, + width: '25%', + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + }, + { + field: 'type', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + }, + { + field: 'namespace', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + render: (namespace: string) => { + return namespace ? <EuiBadge color="hollow">{namespace}</EuiBadge> : ''; + }, + }, + { + field: 'package', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + }, + { + field: 'last_activity', + sortable: true, + width: '25%', + dataType: 'date', + name: i18n.translate('xpack.ingestManager.dataStreamList.lastActivityColumnTitle', { + defaultMessage: 'Last activity', + }), + render: (date: DataStream['last_activity']) => { + try { + const formatter = fieldFormats.getInstance('date'); + return formatter.convert(date); + } catch (e) { + return <FormattedDate value={date} year="numeric" month="short" day="2-digit" />; + } + }, + }, + { + field: 'size_in_bytes', + sortable: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.sizeColumnTitle', { + defaultMessage: 'Size', + }), + render: (size: DataStream['size_in_bytes']) => { + try { + const formatter = fieldFormats.getInstance('bytes'); + return formatter.convert(size); + } catch (e) { + return `${size}b`; + } + }, + }, + ]; + return cols; + }, [fieldFormats]); + + const emptyPrompt = useMemo( + () => ( + <EuiEmptyPrompt + title={ + <h2> + <FormattedMessage + id="xpack.ingestManager.dataStreamList.noDataStreamsPrompt" + defaultMessage="No data streams" + /> + </h2> + } + /> + ), + [] + ); + + const filterOptions: { [key: string]: string[] } = { + dataset: [], + type: [], + namespace: [], + package: [], + }; + + if (dataStreamsData && dataStreamsData.data_streams.length) { + dataStreamsData.data_streams.forEach(stream => { + const { dataset, type, namespace, package: pkg } = stream; + if (!filterOptions.dataset.includes(dataset)) { + filterOptions.dataset.push(dataset); + } + if (!filterOptions.type.includes(type)) { + filterOptions.type.push(type); + } + if (!filterOptions.namespace.includes(namespace)) { + filterOptions.namespace.push(namespace); + } + if (!filterOptions.package.includes(pkg)) { + filterOptions.package.push(pkg); + } + }); + } + + return ( + <DataStreamListPageLayout> + <EuiInMemoryTable + loading={isLoading} + hasActions={true} + message={ + isLoading ? ( + <FormattedMessage + id="xpack.ingestManager.dataStreamList.loadingDataStreamsMessage" + defaultMessage="Loading data streams…" + /> + ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( + emptyPrompt + ) : ( + <FormattedMessage + id="xpack.ingestManager.dataStreamList.noFilteredDataStreamsMessage" + defaultMessage="No matching data streams found" + /> + ) + } + items={dataStreamsData ? dataStreamsData.data_streams : []} + itemId="index" + columns={columns} + pagination={{ + initialPageSize: pagination.pageSize, + pageSizeOptions, + }} + sorting={true} + search={{ + toolsRight: [ + <EuiButton color="primary" iconType="refresh" onClick={() => sendRequest()}> + <FormattedMessage + id="xpack.ingestManager.dataStreamList.reloadDataStreamsButtonText" + defaultMessage="Reload" + /> + </EuiButton>, + ], + box: { + placeholder: i18n.translate( + 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', + { + defaultMessage: 'Filter data streams', + } + ), + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'dataset', + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + multiSelect: 'or', + options: filterOptions.dataset.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: filterOptions.type.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'namespace', + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + multiSelect: 'or', + options: filterOptions.namespace.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'package', + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + multiSelect: 'or', + options: filterOptions.package.map(option => ({ + value: option, + name: option, + })), + }, + ], + }} + /> + </DataStreamListPageLayout> + ); +}; 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 0000000000000..64223efefaab8 --- /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 = () => <StyledAlert type="alert" size="l" />; 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 8ad081cbbabe4..ab7e87b3ad06c 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 ( <Card diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx index 685199245df18..54cb5171f5a3e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/constants.tsx @@ -21,6 +21,7 @@ export const AssetTitleMap: Record<AssetType, string> = { 'ingest-pipeline': 'Ingest Pipeline', 'index-pattern': 'Index Pattern', 'index-template': 'Index Template', + 'component-template': 'Component Template', search: 'Saved Search', visualization: 'Visualization', input: 'Agent input', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx index 0c5f45cdc47a7..244a9a2c7426e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx @@ -11,6 +11,7 @@ import { NotificationsStart } from 'src/core/public'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { PackageInfo } from '../../../types'; import { sendInstallPackage, sendRemovePackage } from '../../../hooks'; +import { useLinks } from '.'; import { InstallStatus } from '../../../types'; interface PackagesInstall { @@ -19,31 +20,55 @@ interface PackagesInstall { interface PackageInstallItem { status: InstallStatus; + version: string | null; } -type InstallPackageProps = Pick<PackageInfo, 'name' | 'version' | 'title'>; +type InstallPackageProps = Pick<PackageInfo, 'name' | 'version' | 'title'> & { + fromUpdate?: boolean; +}; +type SetPackageInstallStatusProps = Pick<PackageInfo, 'name'> & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { + const { toDetailView } = useLinks(); const [packages, setPackage] = useState<PackagesInstall>({}); 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( <FormattedMessage @@ -61,8 +86,15 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar iconType: 'alert', }); } else { - setPackageInstallStatus({ name, status: InstallStatus.installed }); - + setPackageInstallStatus({ name, status: InstallStatus.installed, version }); + if (fromUpdate) { + const settingsUrl = toDetailView({ + name, + version, + panel: 'settings', + }); + window.location.href = settingsUrl; + } notifications.toasts.addSuccess({ title: toMountPoint( <FormattedMessage @@ -81,24 +113,17 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar }); } }, - [notifications.toasts, setPackageInstallStatus] - ); - - const getPackageInstallStatus = useCallback( - (pkg: string): InstallStatus => { - return packages[pkg].status; - }, - [packages] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] ); const uninstallPackage = useCallback( async ({ name, version, title }: Pick<PackageInfo, 'name' | 'version' | 'title'>) => { - 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( <FormattedMessage @@ -116,7 +141,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar iconType: 'alert', }); } else { - setPackageInstallStatus({ name, status: InstallStatus.notInstalled }); + setPackageInstallStatus({ name, status: InstallStatus.notInstalled, version: null }); notifications.toasts.addSuccess({ title: toMountPoint( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index 0d4b395895322..96aebb08e0c63 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -50,10 +50,19 @@ export function Content(props: ContentProps) { type ContentPanelProps = PackageInfo & Pick<DetailParams, 'panel'>; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title } = props; + const { panel, name, version, assets, title, removable, latestVersion } = props; switch (panel) { case 'settings': - return <SettingsPanel name={name} version={version} assets={assets} title={title} />; + return ( + <SettingsPanel + name={name} + version={version} + assets={assets} + title={title} + removable={removable} + latestVersion={latestVersion} + /> + ); case 'data-sources': return <DataSourcesPanel name={name} version={version} />; case 'overview': 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 fa3245aec02c5..c82b7ed2297a7 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 ( <Redirect to={toDetailView({ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index d83910f29f1a7..cf51296d468a9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -13,9 +13,9 @@ import { EPM_PATH } from '../../../../constants'; import { useCapabilities, useLink } from '../../../../hooks'; import { IconPanel } from '../../components/icon_panel'; import { NavButtonBack } from '../../components/nav_button_back'; -import { Version } from '../../components/version'; import { useLinks } from '../../hooks'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; +import { UpdateIcon } from '../../components/icons'; const FullWidthNavRow = styled(EuiPage)` /* no left padding so link is against column left edge */ @@ -26,19 +26,19 @@ const Text = styled.span` margin-right: ${props => 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, latestVersion } = props; + + let installedVersion; + if ('savedObject' in props) { + installedVersion = props.savedObject.attributes.version; + } 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 ( <Fragment> <FullWidthNavRow> @@ -59,7 +59,11 @@ export function Header(props: HeaderProps) { <EuiTitle size="l"> <h1> <Text>{title}</Text> - <StyledVersion version={version} /> + <EuiTitle size="xs"> + <span> + {version} {updateAvailable && <UpdateIcon />} + </span> + </EuiTitle> </h1> </EuiTitle> </CenterColumn> 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 3239d7b90e3c3..848d278819d1d 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,15 @@ export function Detail() { const packageInfo = response.data?.response; const title = packageInfo?.title; const name = packageInfo?.name; + let installedVersion; + if (packageInfo && 'savedObject' in packageInfo) { + installedVersion = packageInfo.savedObject.attributes.version; + } 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 +68,6 @@ type LayoutProps = PackageInfo & Pick<DetailParams, 'panel'> & Pick<EuiPageProps export function DetailLayout(props: LayoutProps) { const { name: packageName, version, icons, restrictWidth } = props; const iconType = usePackageIconType({ packageName, version, icons }); - return ( <Fragment> <FullWidthHeader> 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 cbbf1ce53c4ea..cdad67fd87548 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<PackageInfo, 'assets' | 'name' | 'title' | 'version'> & { - 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<boolean>(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) { </EuiButton> ); + const updateButton = ( + <EuiButton iconType={'refresh'} isLoading={isInstalling} onClick={handleClickUpdate}> + <FormattedMessage + id="xpack.ingestManager.integrations.updatePackage.updatePackageButtonLabel" + defaultMessage="Update to latest version" + /> + </EuiButton> + ); + const uninstallButton = ( <EuiButton iconType={'trash'} @@ -129,7 +144,7 @@ export function InstallationButton(props: InstallationButtonProps) { return hasWriteCapabilites ? ( <Fragment> - {isInstalled || isRemoving ? uninstallButton : installButton} + {isUpdate ? updateButton : showUninstallButton ? uninstallButton : installButton} {isModalVisible && (isInstalled ? uninstallModal : installModal)} </Fragment> ) : 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 ff7ecf97714b6..4d4dba2a64e5a 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,24 +8,60 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { useGetPackageInstallStatus } from '../../hooks'; +import styled from 'styled-components'; import { InstallStatus, PackageInfo } from '../../../../types'; -import { InstallationButton } from './installation_button'; 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 = () => ( + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel" + defaultMessage="Note:" + /> +); +const UpdatesAvailableMsg = () => ( + <UpdatesAvailableMsgContainer> + <UpdateIcon /> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.versionInfo.updatesAvailable" + defaultMessage="Updates are available" + /> + </UpdatesAvailableMsgContainer> +); export const SettingsPanel = ( - props: Pick<PackageInfo, 'assets' | 'name' | 'title' | 'version'> + props: Pick<PackageInfo, 'assets' | 'name' | 'title' | 'version' | 'removable' | 'latestVersion'> ) => { + const { name, title, removable, latestVersion, version } = props; const getPackageInstallStatus = useGetPackageInstallStatus(); const { data: datasourcesData } = useGetDatasources({ perPage: 0, page: 1, - kuery: `datasources.package.name:${props.name}`, + kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.package.name:${props.name}`, }); - const { name, title } = 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 ( <EuiText> <EuiTitle> @@ -37,14 +73,13 @@ export const SettingsPanel = ( </h3> </EuiTitle> <EuiSpacer size="s" /> - {packageInstallStatus === InstallStatus.notInstalled || - packageInstallStatus === InstallStatus.installing ? ( + {installedVersion !== null && ( <div> <EuiTitle> <h4> <FormattedMessage - id="xpack.ingestManager.integrations.settings.packageInstallTitle" - defaultMessage="Install {title}" + id="xpack.ingestManager.integrations.settings.packageVersionTitle" + defaultMessage="{title} version" values={{ title, }} @@ -52,67 +87,143 @@ export const SettingsPanel = ( </h4> </EuiTitle> <EuiSpacer size="s" /> - <p> - <FormattedMessage - id="xpack.ingestManager.integrations.settings.packageInstallDescription" - defaultMessage="Install this integration to setup Kibana and Elasticsearch assets designed for {title} data." - values={{ - title, - }} - /> - </p> + <table> + <tbody> + <tr> + <SettingsTitleCell> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.versionInfo.installedVersion" + defaultMessage="Installed version" + /> + </SettingsTitleCell> + <td> + <EuiTitle size="xs"> + <span>{installedVersion}</span> + </EuiTitle> + {updateAvailable && <UpdatesAvailableMsg />} + </td> + </tr> + <tr> + <SettingsTitleCell> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.versionInfo.latestVersion" + defaultMessage="Latest version" + /> + </SettingsTitleCell> + <td> + <EuiTitle size="xs"> + <span>{latestVersion}</span> + </EuiTitle> + </td> + </tr> + </tbody> + </table> + {updateAvailable && ( + <p> + <InstallationButton + {...props} + version={latestVersion} + disabled={false} + isUpdate={true} + /> + </p> + )} </div> - ) : ( + )} + {!hideInstallOptions && !isUpdating && ( <div> - <EuiTitle> - <h4> + <EuiSpacer size="s" /> + {installationStatus === InstallStatus.notInstalled || + installationStatus === InstallStatus.installing ? ( + <div> + <EuiTitle> + <h4> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageInstallTitle" + defaultMessage="Install {title}" + values={{ + title, + }} + /> + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageInstallDescription" + defaultMessage="Install this integration to setup Kibana and Elasticsearch assets designed for {title} data." + values={{ + title, + }} + /> + </p> + </div> + ) : ( + <div> + <EuiTitle> + <h4> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallTitle" + defaultMessage="Uninstall {title}" + values={{ + title, + }} + /> + </h4> + </EuiTitle> + <EuiSpacer size="s" /> + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallDescription" + defaultMessage="Remove Kibana and Elasticsearch assets that were installed by this Integration." + /> + </p> + </div> + )} + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <p> + <InstallationButton + {...props} + disabled={!datasourcesData || removable === false ? true : packageHasDatasources} + /> + </p> + </EuiFlexItem> + </EuiFlexGroup> + {packageHasDatasources && removable === true && ( + <p> <FormattedMessage - id="xpack.ingestManager.integrations.settings.packageUninstallTitle" - defaultMessage="Uninstall {title}" + id="xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail" + defaultMessage="{strongNote} {title} cannot be uninstalled because there are active agents that use this integration. To uninstall, remove all {title} data sources from your agent configurations." values={{ title, + strongNote: ( + <strong> + <NoteLabel /> + </strong> + ), }} /> - </h4> - </EuiTitle> - <EuiSpacer size="s" /> - <p> - <FormattedMessage - id="xpack.ingestManager.integrations.settings.packageUninstallDescription" - defaultMessage="Remove Kibana and Elasticsearch assets that were installed by this Integration." - /> - </p> + </p> + )} + {removable === false && ( + <p> + <FormattedMessage + id="xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail" + defaultMessage="{strongNote} The {title} integration is installed by default and cannot be removed." + values={{ + title, + strongNote: ( + <strong> + <NoteLabel /> + </strong> + ), + }} + /> + </p> + )} </div> )} - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <p> - <InstallationButton - {...props} - disabled={!datasourcesData ? true : packageHasDatasources} - /> - </p> - </EuiFlexItem> - </EuiFlexGroup> - {packageHasDatasources && ( - <p> - <FormattedMessage - id="xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail" - defaultMessage="{strongNote} {title} cannot be uninstalled because there are active agents that use this integration. To uninstall, remove all {title} data sources from your agent configurations." - values={{ - title, - strongNote: ( - <strong> - <FormattedMessage - id="xpack.ingestManager.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel" - defaultMessage="Note:" - /> - </strong> - ), - }} - /> - </p> - )} </EuiText> ); }; 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 05729ccfc1af4..ab168ef1530bd 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/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index bf785147502b5..983a322de1088 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -67,29 +67,34 @@ export function EPMHomePage() { function InstalledPackages() { const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages(); const [selectedCategory, setSelectedCategory] = useState(''); - const packages = - allPackages && allPackages.response && selectedCategory === '' - ? allPackages.response.filter(pkg => pkg.status === 'installed') - : []; const title = i18n.translate('xpack.ingestManager.epmList.installedTitle', { defaultMessage: 'Installed integrations', }); + const allInstalledPackages = + allPackages && allPackages.response + ? allPackages.response.filter(pkg => pkg.status === 'installed') + : []; + + const updatablePackages = allInstalledPackages.filter( + item => 'savedObject' in item && item.version > item.savedObject.attributes.version + ); + const categories = [ { id: '', title: i18n.translate('xpack.ingestManager.epmList.allFilterLinkText', { defaultMessage: 'All', }), - count: packages.length, + count: allInstalledPackages.length, }, { id: 'updates_available', title: i18n.translate('xpack.ingestManager.epmList.updatesAvailableFilterLinkText', { defaultMessage: 'Updates available', }), - count: 0, // TODO: Update with real count when available + count: updatablePackages.length, }, ]; @@ -106,7 +111,7 @@ function InstalledPackages() { isLoading={isLoadingPackages} controls={controls} title={title} - list={packages} + list={selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages} /> ); } @@ -134,7 +139,6 @@ function AvailablePackages() { }, ...(categoriesRes ? categoriesRes.response : []), ]; - const controls = categories ? ( <CategoryFacets isLoading={isLoadingCategories} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx index c0f55a5a275fd..9168669d132a0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_events_table.tsx @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedTime } from '@kbn/i18n/react'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../../constants'; import { Agent, AgentEvent } from '../../../../types'; import { usePagination, useGetOneAgentEvents } from '../../../../hooks'; import { SearchBar } from '../../../../components/search_bar'; @@ -130,7 +131,11 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag <EuiSpacer size="l" /> <EuiFlexGroup> <EuiFlexItem> - <SearchBar value={search} onChange={setSearch} fieldPrefix={'agent_events'} /> + <SearchBar + value={search} + onChange={setSearch} + fieldPrefix={AGENT_EVENT_SAVED_OBJECT_TYPE} + /> </EuiFlexItem> <EuiFlexItem grow={null}> <EuiButton color="secondary" iconType="refresh" onClick={onClickRefresh}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx index 0844368dc214b..b69dd6bcf8431 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -13,10 +13,13 @@ import { EuiFlexItem, EuiDescriptionList, EuiButton, + EuiPopover, EuiDescriptionListTitle, EuiDescriptionListDescription, EuiButtonEmpty, EuiIconTip, + EuiContextMenuPanel, + EuiContextMenuItem, EuiTextColor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -26,7 +29,7 @@ import { Agent } from '../../../../types'; import { AgentHealth } from '../../components/agent_health'; import { useCapabilities, useGetOneAgentConfig } from '../../../../hooks'; import { Loading } from '../../../../components'; -import { ConnectedLink } from '../../components'; +import { ConnectedLink, AgentReassignConfigFlyout } from '../../components'; import { AgentUnenrollProvider } from '../../components/agent_unenroll_provider'; const Item: React.FunctionComponent<{ label: string }> = ({ label, children }) => { @@ -56,10 +59,19 @@ export const AgentDetailSection: React.FunctionComponent<Props> = ({ agent }) => const hasWriteCapabilites = useCapabilities().write; const metadataFlyout = useFlyout(); const refreshAgent = useAgentRefresh(); + // Actions menu + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsActionsPopoverOpen(false), [ + setIsActionsPopoverOpen, + ]); + const handleToggleMenu = useCallback(() => setIsActionsPopoverOpen(!isActionsPopoverOpen), [ + isActionsPopoverOpen, + ]); + const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); // Fetch AgentConfig information const { isLoading: isAgentConfigLoading, data: agentConfigData } = useGetOneAgentConfig( - agent.config_id as string + agent.config_id ); const items = [ @@ -111,6 +123,9 @@ export const AgentDetailSection: React.FunctionComponent<Props> = ({ agent }) => return ( <> + {isReassignFlyoutOpen && ( + <AgentReassignConfigFlyout agent={agent} onClose={() => setIsReassignFlyoutOpen(false)} /> + )} <EuiFlexGroup alignItems="center" justifyContent="spaceBetween"> <EuiFlexItem grow={false}> <EuiTitle size="l"> @@ -123,21 +138,55 @@ export const AgentDetailSection: React.FunctionComponent<Props> = ({ agent }) => </EuiTitle> </EuiFlexItem> <EuiFlexItem grow={false}> - <AgentUnenrollProvider> - {unenrollAgentsPrompt => ( - <EuiButton - disabled={!hasWriteCapabilites || !agent.active} - onClick={() => { - unenrollAgentsPrompt([agent.id], 1, refreshAgent); - }} - > + <EuiPopover + anchorPosition="downRight" + panelPaddingSize="none" + button={ + <EuiButton onClick={handleToggleMenu}> <FormattedMessage - id="xpack.ingestManager.agentDetails.unenrollButtonText" - defaultMessage="Unenroll" + id="xpack.ingestManager.agentDetails.actionsButton" + defaultMessage="Actions" /> </EuiButton> - )} - </AgentUnenrollProvider> + } + isOpen={isActionsPopoverOpen} + closePopover={handleCloseMenu} + > + <EuiContextMenuPanel + items={[ + <EuiContextMenuItem + icon="pencil" + onClick={() => { + handleCloseMenu(); + setIsReassignFlyoutOpen(true); + }} + key="reassignConfig" + > + <FormattedMessage + id="xpack.ingestManager.agentList.reassignActionText" + defaultMessage="Assign new agent config" + /> + </EuiContextMenuItem>, + + <AgentUnenrollProvider> + {unenrollAgentsPrompt => ( + <EuiContextMenuItem + icon="cross" + disabled={!hasWriteCapabilites || !agent.active} + onClick={() => { + unenrollAgentsPrompt([agent.id], 1, refreshAgent); + }} + > + <FormattedMessage + id="xpack.ingestManager.agentList.unenrollOneButton" + defaultMessage="Unenroll" + /> + </EuiContextMenuItem> + )} + </AgentUnenrollProvider>, + ]} + /> + </EuiPopover> </EuiFlexItem> </EuiFlexGroup> <EuiSpacer size={'xl'} /> 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 0000000000000..508190fef0fc2 --- /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 ee43385e601c2..aa6da36f8fb6c 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<Props> = ({ agent, flyout }) => { const mapMetadata = (obj: { [key: string]: string } | undefined) => { return Object.keys(obj || {}).map(key => ({ @@ -30,8 +32,8 @@ export const AgentMetadataFlyout: React.FunctionComponent<Props> = ({ 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 ( <EuiFlyout onClose={() => 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 ce28bbdc590b0..af7e8c674db4c 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/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx index 9c14a2e9dfed1..dd34e7260b27b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/index.tsx @@ -30,7 +30,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({ onClose, agentConfigs = [], }) => { - const [selectedAPIKeyId, setSelectedAPIKeyId] = useState<string | null>(null); + const [selectedAPIKeyId, setSelectedAPIKeyId] = useState<string | undefined>(); return ( <EuiFlyout onClose={onClose} size="l" maxWidth={640}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx index a0244c601cd96..1d2f3bd155622 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/instructions.tsx @@ -7,16 +7,15 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButtonGroup, EuiSteps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useEnrollmentApiKey } from '../enrollment_api_keys'; import { ShellEnrollmentInstructions, ManualInstructions, } from '../../../../../components/enrollment_instructions'; -import { useCore, useGetAgents } from '../../../../../hooks'; +import { useCore, useGetAgents, useGetOneEnrollmentAPIKey } from '../../../../../hooks'; import { Loading } from '../../../components'; interface Props { - selectedAPIKeyId: string | null; + selectedAPIKeyId: string | undefined; } function useNewEnrolledAgents() { // New enrolled agents @@ -44,7 +43,7 @@ export const EnrollmentInstructions: React.FunctionComponent<Props> = ({ selecte const core = useCore(); const [installType, setInstallType] = useState<'quickInstall' | 'manual'>('quickInstall'); - const apiKey = useEnrollmentApiKey(selectedAPIKeyId); + const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId); const newAgents = useNewEnrolledAgents(); if (!apiKey.data) { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx index 89801bc6bee1e..67930e51418b0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/agent_enrollment_flyout/key_selection.tsx @@ -16,17 +16,16 @@ import { EuiFieldText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useEnrollmentApiKeys } from '../enrollment_api_keys'; import { AgentConfig } from '../../../../../types'; -import { useInput, useCore, sendRequest } from '../../../../../hooks'; +import { useInput, useCore, sendRequest, useGetEnrollmentAPIKeys } from '../../../../../hooks'; import { enrollmentAPIKeyRouteService } from '../../../../../services'; interface Props { - onKeyChange: (keyId: string | null) => void; + onKeyChange: (keyId: string | undefined) => void; agentConfigs: AgentConfig[]; } -function useCreateApiKeyForm(configId: string | null, onSuccess: (keyId: string) => void) { +function useCreateApiKeyForm(configId: string | undefined, onSuccess: (keyId: string) => void) { const { notifications } = useCore(); const [isLoading, setIsLoading] = useState(false); const apiKeyNameInput = useInput(''); @@ -62,17 +61,16 @@ function useCreateApiKeyForm(configId: string | null, onSuccess: (keyId: string) } export const APIKeySelection: React.FunctionComponent<Props> = ({ onKeyChange, agentConfigs }) => { - const enrollmentAPIKeysRequest = useEnrollmentApiKeys({ - currentPage: 1, - pageSize: 1000, + const enrollmentAPIKeysRequest = useGetEnrollmentAPIKeys({ + page: 1, + perPage: 1000, }); const [selectedState, setSelectedState] = useState<{ - agentConfigId: string | null; - enrollmentAPIKeyId: string | null; + agentConfigId?: string; + enrollmentAPIKeyId?: string; }>({ - agentConfigId: agentConfigs.length ? agentConfigs[0].id : null, - enrollmentAPIKeyId: null, + agentConfigId: agentConfigs.length ? agentConfigs[0].id : undefined, }); const filteredEnrollmentAPIKeys = React.useMemo(() => { if (!selectedState.agentConfigId || !enrollmentAPIKeysRequest.data) { @@ -99,10 +97,10 @@ export const APIKeySelection: React.FunctionComponent<Props> = ({ onKeyChange, a const [showAPIKeyForm, setShowAPIKeyForm] = useState(false); const apiKeyForm = useCreateApiKeyForm(selectedState.agentConfigId, async (keyId: string) => { - const res = await enrollmentAPIKeysRequest.refresh(); + const res = await enrollmentAPIKeysRequest.sendRequest(); setSelectedState({ ...selectedState, - enrollmentAPIKeyId: res.data?.list.find(key => key.id === keyId)?.id ?? null, + enrollmentAPIKeyId: res.data?.list.find(key => key.id === keyId)?.id, }); setShowAPIKeyForm(false); }); @@ -135,7 +133,7 @@ export const APIKeySelection: React.FunctionComponent<Props> = ({ onKeyChange, a onChange={e => setSelectedState({ agentConfigId: e.target.value, - enrollmentAPIKeyId: null, + enrollmentAPIKeyId: undefined, }) } /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/confirm_delete_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/confirm_delete_modal.tsx deleted file mode 100644 index 8ce20a85e14b8..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/confirm_delete_modal.tsx +++ /dev/null @@ -1,46 +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 React from 'react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ConfirmDeleteModal: React.FunctionComponent<{ - onConfirm: () => void; - onCancel: () => void; - apiKeyId: string; -}> = ({ onConfirm, onCancel, apiKeyId }) => { - return ( - <EuiOverlayMask> - <EuiConfirmModal - title={ - <FormattedMessage - id="xpack.ingestManager.deleteApiKeys.confirmModal.title" - defaultMessage="Delete api key: {apiKeyId}" - values={{ - apiKeyId, - }} - /> - } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - <FormattedMessage - id="xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel" - defaultMessage="Cancel" - /> - } - confirmButtonText={ - <FormattedMessage - id="xpack.ingestManager.deleteApiKeys.confirmModal.confirmButtonLabel" - defaultMessage="Delete" - /> - } - buttonColor="danger" - /> - </EuiOverlayMask> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/create_api_key_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/create_api_key_form.tsx deleted file mode 100644 index 009080a4da186..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/create_api_key_form.tsx +++ /dev/null @@ -1,94 +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 React from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiButton, - EuiSelect, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useInput, sendRequest } from '../../../../../hooks'; -import { useConfigs } from './hooks'; -import { enrollmentAPIKeyRouteService } from '../../../../../services'; - -export const CreateApiKeyForm: React.FunctionComponent<{ onChange: () => void }> = ({ - onChange, -}) => { - const { data: configs } = useConfigs(); - const { inputs, onSubmit, submitted } = useCreateApiKey(() => onChange()); - - return ( - <EuiFlexGroup style={{ maxWidth: 600 }}> - <EuiFlexItem> - <EuiFormRow - label={i18n.translate('xpack.ingestManager.apiKeysForm.nameLabel', { - defaultMessage: 'Key Name', - })} - > - <EuiFieldText autoComplete={'false'} {...inputs.nameInput.props} /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem> - <EuiFormRow - label={i18n.translate('xpack.ingestManager.apiKeysForm.configLabel', { - defaultMessage: 'Config', - })} - > - <EuiSelect - {...inputs.configIdInput.props} - options={configs.map(config => ({ - value: config.id, - text: config.name, - }))} - /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiFormRow hasEmptyLabelSpace> - <EuiButton disabled={submitted} onClick={() => onSubmit()}> - <FormattedMessage - id="xpack.ingestManager.apiKeysForm.saveButton" - defaultMessage="Save" - /> - </EuiButton> - </EuiFormRow> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -function useCreateApiKey(onSuccess: () => void) { - const [submitted, setSubmitted] = React.useState(false); - const inputs = { - nameInput: useInput(), - configIdInput: useInput('default'), - }; - - const onSubmit = async () => { - setSubmitted(true); - await sendRequest({ - method: 'post', - path: enrollmentAPIKeyRouteService.getCreatePath(), - body: JSON.stringify({ - name: inputs.nameInput.value, - config_id: inputs.configIdInput.value, - }), - }); - setSubmitted(false); - onSuccess(); - }; - - return { - inputs, - onSubmit, - submitted, - }; -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/hooks.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/hooks.tsx deleted file mode 100644 index 41c6b5912cd31..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/hooks.tsx +++ /dev/null @@ -1,43 +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 { - Pagination, - useGetAgentConfigs, - useGetEnrollmentAPIKeys, - useGetOneEnrollmentAPIKey, -} from '../../../../../hooks'; - -export function useEnrollmentApiKeys(pagination: Pagination) { - const request = useGetEnrollmentAPIKeys({ - page: pagination.currentPage, - perPage: pagination.pageSize, - }); - - return { - data: request.data, - isLoading: request.isLoading, - refresh: () => request.sendRequest(), - }; -} - -export function useConfigs() { - const request = useGetAgentConfigs(); - - return { - data: request.data ? request.data.items : [], - isLoading: request.isLoading, - }; -} - -export function useEnrollmentApiKey(apiKeyId: string | null) { - const request = useGetOneEnrollmentAPIKey(apiKeyId as string); - - return { - data: request.data, - isLoading: request.isLoading, - }; -} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/index.tsx deleted file mode 100644 index 19957e7827680..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/enrollment_api_keys/index.tsx +++ /dev/null @@ -1,152 +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 React, { useState } from 'react'; -import { EuiBasicTable, EuiButtonEmpty, EuiSpacer, EuiPopover, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { usePagination, sendRequest } from '../../../../../hooks'; -import { useEnrollmentApiKeys, useEnrollmentApiKey } from './hooks'; -import { ConfirmDeleteModal } from './confirm_delete_modal'; -import { CreateApiKeyForm } from './create_api_key_form'; -import { EnrollmentAPIKey } from '../../../../../types'; -import { useCapabilities } from '../../../../../hooks'; -import { enrollmentAPIKeyRouteService } from '../../../../../services'; -export { useEnrollmentApiKeys, useEnrollmentApiKey } from './hooks'; - -export const EnrollmentApiKeysTable: React.FunctionComponent<{ - onChange: () => void; -}> = ({ onChange }) => { - const [confirmDeleteApiKeyId, setConfirmDeleteApiKeyId] = useState<string | null>(null); - const { pagination } = usePagination(); - const { data, isLoading, refresh } = useEnrollmentApiKeys(pagination); - - const columns: any[] = [ - { - field: 'name', - name: i18n.translate('xpack.ingestManager.apiKeysList.nameColumnTitle', { - defaultMessage: 'Name', - }), - width: '300px', - }, - { - field: 'config_id', - name: i18n.translate('xpack.ingestManager.apiKeysList.configColumnTitle', { - defaultMessage: 'Config', - }), - width: '100px', - }, - { - field: null, - name: i18n.translate('xpack.ingestManager.apiKeysList.apiKeyColumnTitle', { - defaultMessage: 'API Key', - }), - render: (key: EnrollmentAPIKey) => <ApiKeyField apiKeyId={key.id} />, - }, - { - field: null, - width: '50px', - render: (key: EnrollmentAPIKey) => { - return ( - <EuiButtonEmpty onClick={() => setConfirmDeleteApiKeyId(key.id)} iconType={'trash'} /> - ); - }, - }, - ]; - - return ( - <> - {confirmDeleteApiKeyId && ( - <ConfirmDeleteModal - apiKeyId={confirmDeleteApiKeyId} - onCancel={() => setConfirmDeleteApiKeyId(null)} - onConfirm={async () => { - await sendRequest({ - method: 'delete', - path: enrollmentAPIKeyRouteService.getDeletePath(confirmDeleteApiKeyId), - }); - setConfirmDeleteApiKeyId(null); - refresh(); - }} - /> - )} - <EuiBasicTable - compressed={true} - loading={isLoading} - noItemsMessage={ - <FormattedMessage - id="xpack.ingestManager.apiKeysList.emptyEnrollmentKeysMessage" - defaultMessage="No api keys" - /> - } - items={data ? data.list : []} - itemId="id" - columns={columns} - /> - <EuiSpacer size={'s'} /> - <CreateApiKeyButton - onChange={() => { - refresh(); - onChange(); - }} - /> - </> - ); -}; - -export const CreateApiKeyButton: React.FunctionComponent<{ onChange: () => void }> = ({ - onChange, -}) => { - const hasWriteCapabilites = useCapabilities().write; - const [isOpen, setIsOpen] = React.useState(false); - - return ( - <EuiPopover - ownFocus - button={ - <EuiLink disabled={!hasWriteCapabilites} onClick={() => setIsOpen(true)} color="primary"> - <FormattedMessage - id="xpack.ingestManager.enrollmentApiKeyList.createNewButton" - defaultMessage="Create a new key" - /> - </EuiLink> - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - <CreateApiKeyForm - onChange={() => { - setIsOpen(false); - onChange(); - }} - /> - </EuiPopover> - ); - return <></>; -}; - -const ApiKeyField: React.FunctionComponent<{ apiKeyId: string }> = ({ apiKeyId }) => { - const [visible, setVisible] = useState(false); - const { data } = useEnrollmentApiKey(apiKeyId); - - return ( - <> - {visible && data ? data.item.api_key : '••••••••••••••••••••••••••••'} - <EuiButtonEmpty size="xs" color={'text'} onClick={() => setVisible(!visible)}> - {visible ? ( - <FormattedMessage - id="xpack.ingestManager.enrollmentApiKeyList.hideTableButton" - defaultMessage="Hide" - /> - ) : ( - <FormattedMessage - id="xpack.ingestManager.enrollmentApiKeyList.viewTableButton" - defaultMessage="View" - /> - )} - </EuiButtonEmpty>{' '} - </> - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.scss b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.scss deleted file mode 100644 index 10e809c5f5566..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.scss +++ /dev/null @@ -1,6 +0,0 @@ -.fleet__agentList__table .euiTableFooterCell { - .euiTableCellContent, - .euiTableCellContent__text { - overflow: visible; - } -} \ No newline at end of file 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 d363c472f2305..05264c157434e 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 @@ -35,12 +35,16 @@ import { useUrlParams, useLink, } from '../../../hooks'; -import { ConnectedLink } from '../components'; +import { ConnectedLink, AgentReassignConfigFlyout } from '../components'; import { SearchBar } from '../../../components/search_bar'; import { AgentHealth } from '../components/agent_health'; import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; import { AgentStatusKueryHelper } from '../../../services'; -import { FLEET_AGENT_DETAIL_PATH, AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +import { + FLEET_AGENT_DETAIL_PATH, + AGENT_CONFIG_DETAILS_PATH, + AGENT_SAVED_OBJECT_TYPE, +} from '../../../constants'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -71,61 +75,76 @@ const statusFilters = [ }, ] as Array<{ label: string; status: string }>; -const RowActions = React.memo<{ agent: Agent; refresh: () => void }>(({ agent, refresh }) => { - const hasWriteCapabilites = useCapabilities().write; - const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); - const [isOpen, setIsOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); - const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); +const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( + ({ agent, refresh, onReassignClick }) => { + const hasWriteCapabilites = useCapabilities().write; + const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); - return ( - <EuiPopover - anchorPosition="downRight" - panelPaddingSize="none" - button={ - <EuiButtonIcon - iconType="boxesHorizontal" - onClick={handleToggleMenu} - aria-label={i18n.translate('xpack.ingestManager.agentList.actionsMenuText', { - defaultMessage: 'Open', - })} - /> - } - isOpen={isOpen} - closePopover={handleCloseMenu} - > - <EuiContextMenuPanel - items={[ - <EuiContextMenuItem icon="inspect" href={`${DETAILS_URI}${agent.id}`} key="viewConfig"> - <FormattedMessage - id="xpack.ingestManager.agentList.viewActionText" - defaultMessage="View Agent" - /> - </EuiContextMenuItem>, + return ( + <EuiPopover + anchorPosition="downRight" + panelPaddingSize="none" + button={ + <EuiButtonIcon + iconType="boxesHorizontal" + onClick={handleToggleMenu} + aria-label={i18n.translate('xpack.ingestManager.agentList.actionsMenuText', { + defaultMessage: 'Open', + })} + /> + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + <EuiContextMenuPanel + items={[ + <EuiContextMenuItem icon="inspect" href={`${DETAILS_URI}${agent.id}`} key="viewConfig"> + <FormattedMessage + id="xpack.ingestManager.agentList.viewActionText" + defaultMessage="View agent" + /> + </EuiContextMenuItem>, + <EuiContextMenuItem + icon="pencil" + onClick={() => { + handleCloseMenu(); + onReassignClick(); + }} + key="reassignConfig" + > + <FormattedMessage + id="xpack.ingestManager.agentList.reassignActionText" + defaultMessage="Assign new agent config" + /> + </EuiContextMenuItem>, - <AgentUnenrollProvider> - {unenrollAgentsPrompt => ( - <EuiContextMenuItem - disabled={!hasWriteCapabilites} - icon="cross" - onClick={() => { - unenrollAgentsPrompt([agent.id], 1, () => { - refresh(); - }); - }} - > - <FormattedMessage - id="xpack.ingestManager.agentList.unenrollOneButton" - defaultMessage="Unenroll" - /> - </EuiContextMenuItem> - )} - </AgentUnenrollProvider>, - ]} - /> - </EuiPopover> - ); -}); + <AgentUnenrollProvider> + {unenrollAgentsPrompt => ( + <EuiContextMenuItem + disabled={!hasWriteCapabilites} + icon="cross" + onClick={() => { + unenrollAgentsPrompt([agent.id], 1, () => { + refresh(); + }); + }} + > + <FormattedMessage + id="xpack.ingestManager.agentList.unenrollOneButton" + defaultMessage="Unenroll" + /> + </EuiContextMenuItem> + )} + </AgentUnenrollProvider>, + ]} + /> + </EuiPopover> + ); + } +); export const AgentListPage: React.FunctionComponent<{}> = () => { const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; @@ -136,8 +155,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Table and search states const [search, setSearch] = useState(defaultKuery); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgents, setSelectedAgents] = useState<Agent[]>([]); - const [areAllAgentsSelected, setAreAllAgentsSelected] = useState<boolean>(false); // Configs state (for filtering) const [isConfigsFilterOpen, setIsConfigsFilterOpen] = useState<boolean>(false); @@ -159,12 +176,15 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState<boolean>(false); + // Agent reassignment flyout state + const [agentToReassignId, setAgentToReassignId] = useState<string | undefined>(undefined); + let kuery = search.trim(); if (selectedConfigs.length) { if (kuery) { kuery = `(${kuery}) and`; } - kuery = `${kuery} agents.config_id : (${selectedConfigs + kuery = `${kuery} ${AGENT_SAVED_OBJECT_TYPE}.config_id : (${selectedConfigs .map(config => `"${config}"`) .join(' or ')})`; } @@ -218,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', }), @@ -227,47 +247,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {host} </ConnectedLink> ), - footer: () => { - if (selectedAgents.length === agents.length && totalAgents > selectedAgents.length) { - return areAllAgentsSelected ? ( - <FormattedMessage - id="xpack.ingestManager.agentList.allAgentsSelectedMessage" - defaultMessage="All {count} agents are selected. {clearSelectionLink}" - values={{ - count: totalAgents, - clearSelectionLink: ( - <EuiLink onClick={() => setAreAllAgentsSelected(false)}> - <FormattedMessage - id="xpack.ingestManager.agentList.selectPageAgentsLinkText" - defaultMessage="Select just this page" - /> - </EuiLink> - ), - }} - /> - ) : ( - <FormattedMessage - id="xpack.ingestManager.agentList.agentsOnPageSelectedMessage" - defaultMessage="{count, plural, one {# agent} other {# agents}} on this page are selected. {selectAllLink}" - values={{ - count: selectedAgents.length, - selectAllLink: ( - <EuiLink onClick={() => setAreAllAgentsSelected(true)}> - <FormattedMessage - id="xpack.ingestManager.agentList.selectAllAgentsLinkText" - defaultMessage="Select all {count} agents" - values={{ - count: totalAgents, - }} - /> - </EuiLink> - ), - }} - /> - ); - } - return null; - }, }, { field: 'active', @@ -350,7 +329,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { actions: [ { render: (agent: Agent) => { - return <RowActions agent={agent} refresh={() => agentsRequest.sendRequest()} />; + return ( + <RowActions + agent={agent} + refresh={() => agentsRequest.sendRequest()} + onReassignClick={() => setAgentToReassignId(agent.id)} + /> + ); }, }, ], @@ -381,6 +366,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { /> ); + const agentToReassign = agentToReassignId && agents.find(a => a.id === agentToReassignId); + return ( <> {isEnrollmentFlyoutOpen ? ( @@ -389,48 +376,16 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { onClose={() => setIsEnrollmentFlyoutOpen(false)} /> ) : null} + {agentToReassign && ( + <AgentReassignConfigFlyout + agent={agentToReassign} + onClose={() => { + setAgentToReassignId(undefined); + agentsRequest.sendRequest(); + }} + /> + )} <EuiFlexGroup alignItems={'center'}> - {selectedAgents.length ? ( - <EuiFlexItem> - <AgentUnenrollProvider> - {unenrollAgentsPrompt => ( - <EuiButton - color="danger" - onClick={() => { - unenrollAgentsPrompt( - areAllAgentsSelected ? search : selectedAgents.map(agent => agent.id), - areAllAgentsSelected ? totalAgents : selectedAgents.length, - () => { - // Reload agents if on first page and no search query, otherwise - // reset to first page and reset search, which will trigger a reload - if (pagination.currentPage === 1 && !search) { - agentsRequest.sendRequest(); - } else { - setPagination({ - ...pagination, - currentPage: 1, - }); - setSearch(''); - } - - setAreAllAgentsSelected(false); - setSelectedAgents([]); - } - ); - }} - > - <FormattedMessage - id="xpack.ingestManager.agentList.unenrollButton" - defaultMessage="Unenroll {count, plural, one {# agent} other {# agents}}" - values={{ - count: areAllAgentsSelected ? totalAgents : selectedAgents.length, - }} - /> - </EuiButton> - )} - </AgentUnenrollProvider> - </EuiFlexItem> - ) : null} <EuiFlexItem grow={4}> <EuiFlexGroup gutterSize="s"> <EuiFlexItem grow={6}> @@ -443,7 +398,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }); setSearch(newSearch); }} - fieldPrefix="agents" + fieldPrefix={AGENT_SAVED_OBJECT_TYPE} /> </EuiFlexItem> <EuiFlexItem grow={2}> @@ -575,14 +530,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { items={totalAgents ? agents : []} itemId="id" columns={columns} - isSelectable={true} - selection={{ - selectable: (agent: Agent) => agent.active, - onSelectionChange: (newSelectedAgents: Agent[]) => { - setSelectedAgents(newSelectedAgents); - setAreAllAgentsSelected(false); - }, - }} pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx new file mode 100644 index 0000000000000..692c60cdce38c --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx @@ -0,0 +1,186 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiFlyoutFooter, + EuiSelect, + EuiFormRow, + EuiText, + EuiBadge, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Datasource, Agent } from '../../../../types'; +import { + useGetOneAgentConfig, + sendPutAgentReassign, + useCore, + useGetAgentConfigs, +} from '../../../../hooks'; +import { PackageIcon } from '../../../../components/package_icon'; + +interface Props { + onClose: () => void; + agent: Agent; +} + +export const AgentReassignConfigFlyout: React.FunctionComponent<Props> = ({ onClose, agent }) => { + const { notifications } = useCore(); + const [selectedAgentConfigId, setSelectedAgentConfigId] = useState<string | undefined>( + agent.config_id + ); + + const agentConfigsRequest = useGetAgentConfigs(); + const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; + + const agentConfigRequest = useGetOneAgentConfig(selectedAgentConfigId); + const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; + + const [isSubmitting, setIsSubmitting] = useState(false); + + async function onSubmit() { + try { + setIsSubmitting(true); + if (!selectedAgentConfigId) { + throw new Error('No selected config id'); + } + const res = await sendPutAgentReassign(agent.id, { + config_id: selectedAgentConfigId, + }); + if (res.error) { + throw res.error; + } + setIsSubmitting(false); + const successMessage = i18n.translate( + 'xpack.ingestManager.agentReassignConfig.successSingleNotificationTitle', + { + defaultMessage: 'Agent configuration reassigned', + } + ); + notifications.toasts.addSuccess(successMessage); + onClose(); + } catch (error) { + setIsSubmitting(false); + notifications.toasts.addError(error, { + title: 'Unable to reassign agent configuration', + }); + } + } + + return ( + <EuiFlyout onClose={onClose} size="l" maxWidth={640}> + <EuiFlyoutHeader hasBorder aria-labelledby="FleetAgentReassigmentFlyoutTitle"> + <EuiTitle size="m"> + <h2 id="FleetAgentReassigmentFlyoutTitle"> + <FormattedMessage + id="xpack.ingestManager.agentReassignConfig.flyoutTitle" + defaultMessage="Assign new agent configuration" + /> + </h2> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiText size="s"> + <FormattedMessage + id="xpack.ingestManager.agentReassignConfig.flyoutDescription" + defaultMessage="Choose a new agent configuration to assign the selected agent to." + /> + </EuiText> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + fullWidth + label={i18n.translate('xpack.ingestManager.agentReassignConfig.selectConfigLabel', { + defaultMessage: 'Agent configuration', + })} + > + <EuiSelect + fullWidth + options={agentConfigs.map(config => ({ + value: config.id, + text: config.name, + }))} + value={selectedAgentConfigId} + onChange={e => setSelectedAgentConfigId(e.target.value)} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="l" /> + + {agentConfig && ( + <EuiText> + <FormattedMessage + id="xpack.ingestManager.agentReassignConfig.configDescription" + defaultMessage="The selected agent configuration will collect data for {count, plural, one {{countValue} data source} other {{countValue} data sources}}:" + values={{ + count: agentConfig.datasources.length, + countValue: <b>{agentConfig.datasources.length}</b>, + }} + /> + </EuiText> + )} + <EuiSpacer size="s" /> + {agentConfig && + (agentConfig.datasources as Datasource[]).map((datasource, idx) => { + if (!datasource.package) { + return null; + } + return ( + <EuiBadge key={idx} color="hollow"> + <EuiFlexGroup direction="row" gutterSize="xs" alignItems="center"> + <EuiFlexItem grow={false}> + <PackageIcon + packageName={datasource.package.name} + version={datasource.package.version} + size="s" + tryApi={true} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}>{datasource.package.title}</EuiFlexItem> + </EuiFlexGroup> + </EuiBadge> + ); + })} + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> + <FormattedMessage + id="xpack.ingestManager.agentReassignConfig.cancelButtonLabel" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + disabled={!agentConfig || agentConfig.id === agent.config_id} + fill + onClick={onSubmit} + isLoading={isSubmitting} + > + <FormattedMessage + id="xpack.ingestManager.agentReassignConfig.continueButtonLabel" + defaultMessage="Assign configuration" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx index 25499495a7897..fec2253c0dd56 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx @@ -39,7 +39,8 @@ export const AgentUnenrollProvider: React.FunctionComponent<Props> = ({ children ) => { if ( agentsToUnenroll === undefined || - (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length === 0) + // !Only supports unenrolling one agent + (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length !== 1) ) { throw new Error('No agents specified for unenrollment'); } @@ -60,55 +61,27 @@ export const AgentUnenrollProvider: React.FunctionComponent<Props> = ({ children setIsLoading(true); try { - const unenrollByKuery = typeof agents === 'string'; - const { data, error } = await sendRequest<PostAgentUnenrollResponse>({ - path: agentRouteService.getUnenrollPath(), + const agentId = agents[0]; + const { error } = await sendRequest<PostAgentUnenrollResponse>({ + path: agentRouteService.getUnenrollPath(agentId), method: 'post', - body: JSON.stringify({ - kuery: unenrollByKuery ? agents : undefined, - ids: !unenrollByKuery ? agents : undefined, - }), }); if (error) { throw new Error(error.message); } - const results = data ? data.results : []; - - const successfulResults = results.filter(result => result.success); - const failedResults = results.filter(result => !result.success); - - if (successfulResults.length) { - const hasMultipleSuccesses = successfulResults.length > 1; - const successMessage = hasMultipleSuccesses - ? i18n.translate('xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle', { - defaultMessage: 'Unenrolled {count} agents', - values: { count: successfulResults.length }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { - defaultMessage: "Unenrolled agent '{id}'", - values: { id: successfulResults[0].id }, - }); - core.notifications.toasts.addSuccess(successMessage); - } - - if (failedResults.length) { - const hasMultipleFailures = failedResults.length > 1; - const failureMessage = hasMultipleFailures - ? i18n.translate('xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle', { - defaultMessage: 'Error unenrolling {count} agents', - values: { count: failedResults.length }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle', { - defaultMessage: "Error unenrolling agent '{id}'", - values: { id: failedResults[0].id }, - }); - core.notifications.toasts.addDanger(failureMessage); - } + const successMessage = i18n.translate( + 'xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', + { + defaultMessage: "Unenrolled agent '{id}'", + values: { id: agentId }, + } + ); + core.notifications.toasts.addSuccess(successMessage); if (onSuccessCallback.current) { - onSuccessCallback.current(successfulResults.map(result => result.id)); + onSuccessCallback.current([agentId]); } } catch (e) { core.notifications.toasts.addDanger( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/donut_chart.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx similarity index 100% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/donut_chart.tsx rename to x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/donut_chart.tsx diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx index 19378fe2fb952..a0092f4073e5a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx @@ -5,5 +5,6 @@ */ export * from './loading'; +export * from './agent_reassign_config_flyout'; export * from './navigation/child_routes'; export * from './navigation/connected_link'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx index 61306e823f2a8..a1786d596b791 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx @@ -19,12 +19,12 @@ import { } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { useRouteMatch } from 'react-router-dom'; -import { DonutChart } from '../agent_list_page/components/donut_chart'; -import { useGetAgentStatus } from '../../agent_config/details_page/hooks'; -import { useCapabilities, useLink, useGetAgentConfigs } from '../../../hooks'; import { WithHeaderLayout } from '../../../layouts'; import { FLEET_ENROLLMENT_TOKENS_PATH, FLEET_AGENTS_PATH } from '../../../constants'; +import { useCapabilities, useLink, useGetAgentConfigs } from '../../../hooks'; +import { useGetAgentStatus } from '../../agent_config/details_page/hooks'; import { AgentEnrollmentFlyout } from '../agent_list_page/components'; +import { DonutChart } from './donut_chart'; const REFRESH_INTERVAL_MS = 5000; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index 7520f88215efe..c11e3a49c7693 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -18,6 +18,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; +import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../../constants'; import { usePagination, useGetEnrollmentAPIKeys, @@ -29,7 +30,6 @@ import { import { EnrollmentAPIKey } from '../../../types'; import { SearchBar } from '../../../components/search_bar'; import { NewEnrollmentTokenFlyout } from './components/new_enrollment_key_flyout'; -import {} from '@elastic/eui'; import { ConfirmEnrollmentTokenDelete } from './components/confirm_delete_modal'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ @@ -251,7 +251,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }); setSearch(newSearch); }} - fieldPrefix="enrollment_api_keys" + fieldPrefix={ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE} /> </EuiFlexItem> <EuiFlexItem grow={false}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx index c691bb609d435..1f46c4cc820cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx @@ -6,6 +6,7 @@ export { IngestManagerOverview } from './overview'; export { EPMApp } from './epm'; export { AgentConfigApp } from './agent_config'; +export { DataStreamApp } from './data_stream'; export { FleetApp } from './fleet'; -export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet'; +export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet' | 'data_stream'; 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 ea6b045f504ec..70d8e7d6882f8 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 @@ -3,12 +3,78 @@ * 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 { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + EuiButton, + EuiButtonEmpty, + EuiBetaBadge, + EuiPanel, + EuiText, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGrid, + EuiFlexGroup, + 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, DATA_STREAM_PATH } from '../../constants'; + +const OverviewPanel = styled(EuiPanel).attrs(props => ({ + paddingSize: 'm', +}))` + header { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid ${props => props.theme.eui.euiColorLightShade}; + margin: -${props => props.theme.eui.paddingSizes.m} -${props => props.theme.eui.paddingSizes.m} + ${props => props.theme.eui.paddingSizes.m}; + padding: ${props => props.theme.eui.paddingSizes.s} ${props => props.theme.eui.paddingSizes.m}; + } + + h2 { + padding: ${props => props.theme.eui.paddingSizes.xs} 0; + } +`; + +const OverviewStats = styled(EuiDescriptionList).attrs(props => ({ + compressed: true, + textStyle: 'reverse', + type: 'column', +}))` + & > * { + margin-top: ${props => props.theme.eui.paddingSizes.s} !important; + + &:first-child, + &:nth-child(2) { + margin-top: 0 !important; + } + } +`; + +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<boolean>(false); + + // Agent configs required for enrollment flyout + const agentConfigsRequest = useGetAgentConfigs({ + page: 1, + perPage: 1000, + }); + const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; + return ( <WithHeaderLayout leftColumn={ @@ -20,6 +86,19 @@ export const IngestManagerOverview: React.FunctionComponent = () => { id="xpack.ingestManager.overviewPageTitle" defaultMessage="Ingest Manager" /> + <AlphaBadge + iconType="beaker" + label={i18n.translate('xpack.ingestManager.alphaBadge.labelText', { + defaultMessage: 'Experimental', + })} + title={i18n.translate('xpack.ingestManager.alphaBadge.titleText', { + defaultMessage: 'Experimental', + })} + tooltipContent={i18n.translate('xpack.ingestManager.alphaBadge.tooltipText', { + defaultMessage: + 'This plugin might change or be removed in a future release and is not subject to the support SLA.', + })} + /> </h1> </EuiText> </EuiFlexItem> @@ -28,13 +107,150 @@ export const IngestManagerOverview: React.FunctionComponent = () => { <p> <FormattedMessage id="xpack.ingestManager.overviewPageSubtitle" - defaultMessage="Lorem ipsum some description about ingest manager." + defaultMessage="Centralized management for Elastic Agents and configurations." /> </p> </EuiText> </EuiFlexItem> </EuiFlexGroup> } - /> + rightColumn={ + <EuiFlexGroup justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButton fill iconType="plusInCircle" onClick={() => setIsEnrollmentFlyoutOpen(true)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageEnrollAgentButton" + defaultMessage="Enroll new agent" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + } + > + {isEnrollmentFlyoutOpen && ( + <AgentEnrollmentFlyout + agentConfigs={agentConfigs} + onClose={() => setIsEnrollmentFlyoutOpen(false)} + /> + )} + + <EuiFlexGrid gutterSize="l" columns={2}> + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageIntegrationsPanelTitle" + defaultMessage="Integrations" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right" href={useLink(EPM_PATH)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageIntegrationsPanelAction" + defaultMessage="View integrations" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Total available</EuiDescriptionListTitle> + <EuiDescriptionListDescription>999</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Installed</EuiDescriptionListTitle> + <EuiDescriptionListDescription>1</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Updated available</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageConfigurationsPanelTitle" + defaultMessage="Configurations" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right" href={useLink(AGENT_CONFIG_PATH)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageConfigurationsPanelAction" + defaultMessage="View configs" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Total configs</EuiDescriptionListTitle> + <EuiDescriptionListDescription>1</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Data sources</EuiDescriptionListTitle> + <EuiDescriptionListDescription>1</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageFleetPanelTitle" + defaultMessage="Fleet" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right" href={useLink(FLEET_PATH)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageFleetPanelAction" + defaultMessage="View agents" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Total agents</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Active</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Offline</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Error</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + + <EuiFlexItem component="section"> + <OverviewPanel> + <header> + <EuiTitle size="xs"> + <h2> + <FormattedMessage + id="xpack.ingestManager.overviewPageDataStreamsPanelTitle" + defaultMessage="Data streams" + /> + </h2> + </EuiTitle> + <EuiButtonEmpty size="xs" flush="right" href={useLink(DATA_STREAM_PATH)}> + <FormattedMessage + id="xpack.ingestManager.overviewPageDataStreamsPanelAction" + defaultMessage="View data streams" + /> + </EuiButtonEmpty> + </header> + <OverviewStats> + <EuiDescriptionListTitle>Data streams</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Name spaces</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0</EuiDescriptionListDescription> + <EuiDescriptionListTitle>Total size</EuiDescriptionListTitle> + <EuiDescriptionListDescription>0 MB</EuiDescriptionListDescription> + </OverviewStats> + </OverviewPanel> + </EuiFlexItem> + </EuiFlexGrid> + </WithHeaderLayout> ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 5ebd1300baf65..e4791cc816d04 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -9,11 +9,14 @@ export { getFlattenedObject } from '../../../../../../../src/core/utils'; export { agentConfigRouteService, datasourceRouteService, + dataStreamRouteService, fleetSetupRouteService, agentRouteService, enrollmentAPIKeyRouteService, epmRouteService, setupRouteService, + outputRoutesService, + settingsRoutesService, packageToConfigDatasourceInputs, storedDatasourceToAgentDatasource, AgentStatusKueryHelper, 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 32615278b67d7..602015d23cefb 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, @@ -17,6 +18,8 @@ export { DatasourceInput, DatasourceInputStream, DatasourceConfigRecordEntry, + Output, + DataStream, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, @@ -25,11 +28,15 @@ export { CreateAgentConfigResponse, UpdateAgentConfigRequest, UpdateAgentConfigResponse, - DeleteAgentConfigsRequest, - DeleteAgentConfigsResponse, + DeleteAgentConfigRequest, + DeleteAgentConfigResponse, // API schemas - Datasource CreateDatasourceRequest, CreateDatasourceResponse, + UpdateDatasourceRequest, + UpdateDatasourceResponse, + // API schemas - Data Streams + GetDataStreamsResponse, // API schemas - Agents GetAgentsResponse, GetAgentsRequest, @@ -39,10 +46,20 @@ export { GetOneAgentEventsResponse, GetAgentStatusRequest, GetAgentStatusResponse, + PutAgentReassignRequest, + PutAgentReassignResponse, // API schemas - Enrollment API Keys GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, GetOneEnrollmentAPIKeyResponse, + // API schemas - Outputs + GetOutputsResponse, + PutOutputRequest, + PutOutputResponse, + // API schemas - Settings + GetSettingsResponse, + PutSettingsRequest, + PutSettingsResponse, // EPM types AssetReference, AssetsGroupedByServiceByType, @@ -73,4 +90,5 @@ export { DetailViewPanelName, InstallStatus, InstallationStatus, + Installable, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index 6ac92ca5d2a91..75c14ffc8fa84 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -12,13 +12,16 @@ export { // Routes PLUGIN_ID, EPM_API_ROUTES, + DATA_STREAM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_API_ROUTES, AGENT_CONFIG_API_ROUTES, FLEET_SETUP_API_ROUTES, ENROLLMENT_API_KEY_ROUTES, INSTALL_SCRIPT_API_ROUTES, + OUTPUT_API_ROUTES, SETUP_API_ROUTE, + SETTINGS_API_ROUTES, // Saved object types AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, @@ -29,7 +32,9 @@ export { PACKAGES_SAVED_OBJECT_TYPE, INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE as GLOBAL_SETTINGS_SAVED_OBJET_TYPE, // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, + DEFAULT_REGISTRY_URL, } from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 7859c44ccfd89..951ff2337d8c7 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -6,9 +6,12 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; - -export { ESIndexPatternService } from './services'; -export { IngestManagerSetupContract } from './plugin'; +export { AgentService, ESIndexPatternService } from './services'; +export { + IngestManagerSetupContract, + IngestManagerSetupDeps, + IngestManagerStartContract, +} from './plugin'; export const config = { exposeToBrowser: { @@ -18,11 +21,11 @@ export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: false }), epm: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - registryUrl: schema.uri({ defaultValue: 'https://epr-staging.elastic.co' }), + enabled: schema.boolean({ defaultValue: true }), + registryUrl: schema.maybe(schema.uri()), }), fleet: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), kibana: schema.object({ host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), @@ -40,13 +43,3 @@ export type IngestManagerConfigType = TypeOf<typeof config.schema>; export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; - -// Saved object information bootstrapped by legacy `ingest_manager` plugin -// TODO: Remove once saved object mappings can be done from NP -export { savedObjectMappings } from './saved_objects'; -export { - OUTPUT_SAVED_OBJECT_TYPE, - AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, - PACKAGES_SAVED_OBJECT_TYPE, -} from './constants'; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 4dd070a7414f0..3448685d1f279 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,11 +11,12 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, - RecursiveReadonly, } from 'kibana/server'; -import { deepFreeze } from '../../../../src/core/utils'; -import { LicensingPluginSetup } from '../../licensing/server'; -import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server'; +import { LicensingPluginSetup, ILicense } from '../../licensing/server'; +import { + EncryptedSavedObjectsPluginStart, + EncryptedSavedObjectsPluginSetup, +} from '../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { @@ -28,37 +29,39 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, } from './constants'; - +import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { registerEPMRoutes, registerDatasourceRoutes, + registerDataStreamRoutes, registerAgentConfigRoutes, registerSetupRoutes, registerAgentRoutes, registerEnrollmentApiKeyRoutes, registerInstallScriptRoutes, + registerOutputRoutes, + registerSettingsRoutes, } from './routes'; import { IngestManagerConfigType } from '../common'; import { appContextService, - ESIndexPatternService, + licenseService, ESIndexPatternSavedObjectService, + ESIndexPatternService, + AgentService, } from './services'; - -/** - * Describes public IngestManager plugin contract returned at the `setup` stage. - */ -export interface IngestManagerSetupContract { - esIndexPatternService: ESIndexPatternService; -} +import { getAgentStatusById } from './services/agents'; export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; features?: FeaturesPluginSetup; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; } +export type IngestManagerStartDeps = object; + export interface IngestManagerAppContext { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; security?: SecurityPluginSetup; @@ -66,6 +69,8 @@ export interface IngestManagerAppContext { savedObjects: SavedObjectsServiceStart; } +export type IngestManagerSetupContract = void; + const allSavedObjectTypes = [ OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, @@ -76,7 +81,23 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; -export class IngestManagerPlugin implements Plugin<IngestManagerSetupContract> { +/** + * Describes public IngestManager plugin contract returned at the `startup` stage. + */ +export interface IngestManagerStartContract { + esIndexPatternService: ESIndexPatternService; + agentService: AgentService; +} + +export class IngestManagerPlugin + implements + Plugin< + IngestManagerSetupContract, + IngestManagerStartContract, + IngestManagerSetupDeps, + IngestManagerStartDeps + > { + private licensing$!: Observable<ILicense>; private config$: Observable<IngestManagerConfigType>; private security: SecurityPluginSetup | undefined; @@ -84,14 +105,15 @@ export class IngestManagerPlugin implements Plugin<IngestManagerSetupContract> { this.config$ = this.initializerContext.config.create<IngestManagerConfigType>(); } - public async setup( - core: CoreSetup, - deps: IngestManagerSetupDeps - ): Promise<RecursiveReadonly<IngestManagerSetupContract>> { + public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { + this.licensing$ = deps.licensing.license$; if (deps.security) { this.security = deps.security; } + registerSavedObjects(core.savedObjects); + registerEncryptedSavedObjects(deps.encryptedSavedObjects); + // Register feature // TODO: Flesh out privileges if (deps.features) { @@ -128,8 +150,12 @@ export class IngestManagerPlugin implements Plugin<IngestManagerSetupContract> { const config = await this.config$.pipe(first()).toPromise(); // Register routes + registerSetupRoutes(router, config); registerAgentConfigRoutes(router); registerDatasourceRoutes(router); + registerOutputRoutes(router); + registerSettingsRoutes(router); + registerDataStreamRoutes(router); // Conditional routes if (config.epm.enabled) { @@ -137,7 +163,6 @@ export class IngestManagerPlugin implements Plugin<IngestManagerSetupContract> { } if (config.fleet.enabled) { - registerSetupRoutes(router); registerAgentRoutes(router); registerEnrollmentApiKeyRoutes(router); registerInstallScriptRoutes({ @@ -146,9 +171,6 @@ export class IngestManagerPlugin implements Plugin<IngestManagerSetupContract> { basePath: core.http.basePath, }); } - return deepFreeze({ - esIndexPatternService: new ESIndexPatternSavedObjectService(), - }); } public async start( @@ -163,9 +185,17 @@ export class IngestManagerPlugin implements Plugin<IngestManagerSetupContract> { config$: this.config$, savedObjects: core.savedObjects, }); + licenseService.start(this.licensing$); + return { + esIndexPatternService: new ESIndexPatternSavedObjectService(), + agentService: { + getAgentStatusById, + }, + }; } public async stop() { appContextService.stop(); + licenseService.stop(); } } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts index 76247c338a24f..bcb9a7797f26a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts @@ -10,8 +10,7 @@ import { RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; -import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { ActionsService } from '../../services/agents'; import { AgentAction } from '../../../common/types/models'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 89c827abe30ec..5820303e2a1a7 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -14,6 +14,7 @@ import { PostAgentEnrollResponse, PostAgentUnenrollResponse, GetAgentStatusResponse, + PutAgentReassignResponse, } from '../../../common/types'; import { GetAgentsRequestSchema, @@ -25,6 +26,7 @@ import { PostAgentEnrollRequestSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, + PutAgentReassignRequestSchema, } from '../../types'; import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; @@ -293,60 +295,36 @@ export const getAgentsHandler: RequestHandler< } }; -export const postAgentsUnenrollHandler: RequestHandler< - undefined, +export const postAgentsUnenrollHandler: RequestHandler<TypeOf< + typeof PostAgentUnenrollRequestSchema.params +>> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + await AgentService.unenrollAgent(soClient, request.params.agentId); + + const body: PostAgentUnenrollResponse = { + success: true, + }; + return response.ok({ body }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const putAgentsReassignHandler: RequestHandler< + TypeOf<typeof PutAgentReassignRequestSchema.params>, undefined, - TypeOf<typeof PostAgentUnenrollRequestSchema.body> + TypeOf<typeof PutAgentReassignRequestSchema.body> > = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { - const kuery = (request.body as { kuery: string }).kuery; - let toUnenrollIds: string[] = (request.body as { ids: string[] }).ids || []; - - if (kuery) { - let hasMore = true; - let page = 1; - while (hasMore) { - const { agents } = await AgentService.listAgents(soClient, { - page: page++, - perPage: 100, - kuery, - showInactive: true, - }); - if (agents.length === 0) { - hasMore = false; - } - const agentIds = agents.filter(a => a.active).map(a => a.id); - toUnenrollIds = toUnenrollIds.concat(agentIds); - } - } - const results = (await AgentService.unenrollAgents(soClient, toUnenrollIds)).map( - ({ - success, - id, - error, - }): { - success: boolean; - id: string; - action: 'unenrolled'; - error?: { - message: string; - }; - } => { - return { - success, - id, - action: 'unenrolled', - error: error && { - message: error.message, - }, - }; - } - ); + await AgentService.reassignAgent(soClient, request.params.agentId, request.body.config_id); - const body: PostAgentUnenrollResponse = { - results, - success: results.every(result => result.success), + const body: PutAgentReassignResponse = { + success: true, }; return response.ok({ body }); } catch (e) { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index ac27e47db155e..78bb178dce402 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -23,6 +23,7 @@ import { PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PostNewAgentActionRequestSchema, + PutAgentReassignRequestSchema, } from '../../types'; import { getAgentsHandler, @@ -35,6 +36,7 @@ import { postAgentsUnenrollHandler, getAgentStatusForConfigHandler, getInternalUserSOClient, + putAgentsReassignHandler, } from './handlers'; import { postAgentAcksHandlerBuilder } from './acks_handlers'; import * as AgentService from '../../services/agents'; @@ -135,6 +137,15 @@ export const registerRoutes = (router: IRouter) => { postAgentsUnenrollHandler ); + router.put( + { + path: AGENT_API_ROUTES.REASSIGN_PATTERN, + validate: PutAgentReassignRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + putAgentsReassignHandler + ); + // Get agent events router.get( { 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 67f758c2c1263..023d465c9cda9 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 @@ -8,12 +8,13 @@ import { RequestHandler } from 'src/core/server'; import bluebird from 'bluebird'; import { appContextService, agentConfigService, datasourceService } from '../../services'; import { listAgents } from '../../services/agents'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { GetAgentConfigsRequestSchema, GetOneAgentConfigRequestSchema, CreateAgentConfigRequestSchema, UpdateAgentConfigRequestSchema, - DeleteAgentConfigsRequestSchema, + DeleteAgentConfigRequestSchema, GetFullAgentConfigRequestSchema, AgentConfig, DefaultPackages, @@ -21,10 +22,11 @@ import { } from '../../types'; import { GetAgentConfigsResponse, + GetAgentConfigsResponseItem, GetOneAgentConfigResponse, CreateAgentConfigResponse, UpdateAgentConfigResponse, - DeleteAgentConfigsResponse, + DeleteAgentConfigResponse, GetFullAgentConfigResponse, } from '../../../common'; @@ -45,12 +47,12 @@ export const getAgentConfigsHandler: RequestHandler< await bluebird.map( items, - agentConfig => + (agentConfig: GetAgentConfigsResponseItem) => listAgents(soClient, { - showInactive: true, + showInactive: false, perPage: 0, page: 1, - kuery: `agents.config_id:${agentConfig.id}`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:${agentConfig.id}`, }).then(({ total: agentTotal }) => (agentConfig.agents = agentTotal)), { concurrency: 10 } ); @@ -177,13 +179,13 @@ export const updateAgentConfigHandler: RequestHandler< export const deleteAgentConfigsHandler: RequestHandler< unknown, unknown, - TypeOf<typeof DeleteAgentConfigsRequestSchema.body> + TypeOf<typeof DeleteAgentConfigRequestSchema.body> > = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { - const body: DeleteAgentConfigsResponse = await agentConfigService.delete( + const body: DeleteAgentConfigResponse = await agentConfigService.delete( soClient, - request.body.agentConfigIds + request.body.agentConfigId ); return response.ok({ body, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts index b8e827974ff81..e630f3c959590 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/index.ts @@ -10,7 +10,7 @@ import { GetOneAgentConfigRequestSchema, CreateAgentConfigRequestSchema, UpdateAgentConfigRequestSchema, - DeleteAgentConfigsRequestSchema, + DeleteAgentConfigRequestSchema, GetFullAgentConfigRequestSchema, } from '../../types'; import { @@ -67,7 +67,7 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: AGENT_CONFIG_API_ROUTES.DELETE_PATTERN, - validate: DeleteAgentConfigsRequestSchema, + validate: DeleteAgentConfigRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, deleteAgentConfigsHandler diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts new file mode 100644 index 0000000000000..a24518d644c4c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -0,0 +1,125 @@ +/* + * 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 { RequestHandler } from 'src/core/server'; +import { DataStream } from '../../types'; +import { GetDataStreamsResponse } from '../../../common'; + +const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*'; + +export const getListHandler: RequestHandler = async (context, request, response) => { + const callCluster = context.core.elasticsearch.dataClient.callAsCurrentUser; + + try { + // Get stats (size on disk) of all potentially matching indices + const { indices: indexStats } = await callCluster('indices.stats', { + index: DATA_STREAM_INDEX_PATTERN, + metric: ['store'], + }); + + // Get all matching indices and info about each + // This returns the top 100,000 indices (as buckets) by last activity + const { + aggregations: { + index: { buckets: indexResults }, + }, + } = await callCluster('search', { + index: DATA_STREAM_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + must: [ + { + exists: { + field: 'stream.namespace', + }, + }, + { + exists: { + field: 'stream.dataset', + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 100000, + order: { + last_activity: 'desc', + }, + }, + aggs: { + dataset: { + terms: { + field: 'stream.dataset', + size: 1, + }, + }, + namespace: { + terms: { + field: 'stream.namespace', + size: 1, + }, + }, + type: { + terms: { + field: 'stream.type', + size: 1, + }, + }, + package: { + terms: { + field: 'event.module', + size: 1, + }, + }, + last_activity: { + max: { + field: '@timestamp', + }, + }, + }, + }, + }, + }, + }); + + const dataStreams: DataStream[] = (indexResults as any[]).map(result => { + const { + key: indexName, + dataset: { buckets: datasetBuckets }, + namespace: { buckets: namespaceBuckets }, + type: { buckets: typeBuckets }, + package: { buckets: packageBuckets }, + last_activity: { value_as_string: lastActivity }, + } = result; + return { + index: indexName, + dataset: datasetBuckets.length ? datasetBuckets[0].key : '', + namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', + type: typeBuckets.length ? typeBuckets[0].key : '', + package: packageBuckets.length ? packageBuckets[0].key : '', + last_activity: lastActivity, + size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, + }; + }); + + const body: GetDataStreamsResponse = { + data_streams: dataStreams, + }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts new file mode 100644 index 0000000000000..39502eba89a6a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'src/core/server'; +import { PLUGIN_ID, DATA_STREAM_API_ROUTES } from '../../constants'; +import { getListHandler } from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // List of data streams + router.get( + { + path: DATA_STREAM_API_ROUTES.LIST_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getListHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts index 56d6053a1451b..8f07e3ed1de02 100644 --- a/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts @@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema'; import Boom from 'boom'; import { RequestHandler } from 'src/core/server'; import { appContextService, datasourceService } from '../../services'; -import { ensureInstalledPackage } from '../../services/epm/packages'; +import { ensureInstalledPackage, getPackageInfo } from '../../services/epm/packages'; import { GetDatasourcesRequestSchema, GetOneDatasourceRequestSchema, @@ -85,12 +85,13 @@ export const createDatasourceHandler: RequestHandler< pkgName: request.body.package.name, callCluster, }); - + const pkgInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: request.body.package.name, + pkgVersion: request.body.package.version, + }); newData.inputs = (await datasourceService.assignPackageStream( - { - pkgName: request.body.package.name, - pkgVersion: request.body.package.version, - }, + pkgInfo, request.body.inputs )) as TypeOf<typeof CreateDatasourceRequestSchema.body>['inputs']; } @@ -127,13 +128,14 @@ export const updateDatasourceHandler: RequestHandler< const pkg = newData.package || datasource.package; const inputs = newData.inputs || datasource.inputs; if (pkg && (newData.inputs || newData.package)) { - newData.inputs = (await datasourceService.assignPackageStream( - { - pkgName: pkg.name, - pkgVersion: pkg.version, - }, - inputs - )) as TypeOf<typeof CreateDatasourceRequestSchema.body>['inputs']; + const pkgInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: pkg.name, + pkgVersion: pkg.version, + }); + newData.inputs = (await datasourceService.assignPackageStream(pkgInfo, inputs)) as TypeOf< + typeof CreateDatasourceRequestSchema.body + >['inputs']; } const updatedDatasource = await datasourceService.update( diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index ad16e1dde456b..fd3a9c520b90a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -136,6 +136,12 @@ export const installPackageHandler: RequestHandler<TypeOf< }; return response.ok({ body }); } catch (e) { + if (e.isBoom) { + return response.customError({ + statusCode: e.output.statusCode, + body: { message: e.output.payload.message }, + }); + } return response.customError({ statusCode: 500, body: { message: e.message }, @@ -157,6 +163,12 @@ export const deletePackageHandler: RequestHandler<TypeOf< }; return response.ok({ body }); } catch (e) { + if (e.isBoom) { + return response.customError({ + statusCode: e.output.statusCode, + body: { message: e.output.payload.message }, + }); + } return response.customError({ statusCode: 500, body: { message: e.message }, diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index 33d75f3ab82cd..3ce34d15de46c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -5,8 +5,11 @@ */ export { registerRoutes as registerAgentConfigRoutes } from './agent_config'; export { registerRoutes as registerDatasourceRoutes } from './datasource'; +export { registerRoutes as registerDataStreamRoutes } from './data_streams'; export { registerRoutes as registerEPMRoutes } from './epm'; export { registerRoutes as registerSetupRoutes } from './setup'; export { registerRoutes as registerAgentRoutes } from './agent'; export { registerRoutes as registerEnrollmentApiKeyRoutes } from './enrollment_api_key'; export { registerRoutes as registerInstallScriptRoutes } from './install_script'; +export { registerRoutes as registerOutputRoutes } from './output'; +export { registerRoutes as registerSettingsRoutes } from './settings'; diff --git a/x-pack/plugins/ingest_manager/server/routes/output/handler.ts b/x-pack/plugins/ingest_manager/server/routes/output/handler.ts new file mode 100644 index 0000000000000..cd35b2a43426c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/output/handler.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandler } from 'src/core/server'; +import { TypeOf } from '@kbn/config-schema'; +import { GetOneOutputRequestSchema, PutOutputRequestSchema } from '../../types'; +import { GetOneOutputResponse, GetOutputsResponse } from '../../../common'; +import { outputService } from '../../services/output'; + +export const getOutputsHandler: RequestHandler = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const outputs = await outputService.list(soClient); + + const body: GetOutputsResponse = { + items: outputs.items, + page: outputs.page, + perPage: outputs.perPage, + total: outputs.total, + success: true, + }; + + return response.ok({ body }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const getOneOuputHandler: RequestHandler<TypeOf< + typeof GetOneOutputRequestSchema.params +>> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const output = await outputService.get(soClient, request.params.outputId); + + const body: GetOneOutputResponse = { + item: output, + success: true, + }; + + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Output ${request.params.outputId} not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const putOuputHandler: RequestHandler< + TypeOf<typeof PutOutputRequestSchema.params>, + undefined, + TypeOf<typeof PutOutputRequestSchema.body> +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + await outputService.update(soClient, request.params.outputId, request.body); + const output = await outputService.get(soClient, request.params.outputId); + + const body: GetOneOutputResponse = { + item: output, + success: true, + }; + + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Output ${request.params.outputId} not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/output/index.ts b/x-pack/plugins/ingest_manager/server/routes/output/index.ts new file mode 100644 index 0000000000000..139d11dba951a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/output/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { PLUGIN_ID, OUTPUT_API_ROUTES } from '../../constants'; +import { getOneOuputHandler, getOutputsHandler, putOuputHandler } from './handler'; +import { + GetOneOutputRequestSchema, + GetOutputsRequestSchema, + PutOutputRequestSchema, +} from '../../types'; + +export const registerRoutes = (router: IRouter) => { + router.get( + { + path: OUTPUT_API_ROUTES.LIST_PATTERN, + validate: GetOutputsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getOutputsHandler + ); + router.get( + { + path: OUTPUT_API_ROUTES.INFO_PATTERN, + validate: GetOneOutputRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getOneOuputHandler + ); + router.put( + { + path: OUTPUT_API_ROUTES.UPDATE_PATTERN, + validate: PutOutputRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + putOuputHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/settings/index.ts b/x-pack/plugins/ingest_manager/server/routes/settings/index.ts new file mode 100644 index 0000000000000..56e666056e8d0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/settings/index.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter, RequestHandler } from 'src/core/server'; +import { TypeOf } from '@kbn/config-schema'; +import { PLUGIN_ID, SETTINGS_API_ROUTES } from '../../constants'; +import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types'; + +import { settingsService } from '../../services'; + +export const getSettingsHandler: RequestHandler = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + try { + const settings = await settingsService.getSettings(soClient); + const body = { + success: true, + item: settings, + }; + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Setings not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const putSettingsHandler: RequestHandler< + undefined, + undefined, + TypeOf<typeof PutSettingsRequestSchema.body> +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const settings = await settingsService.saveSettings(soClient, request.body); + const body = { + success: true, + item: settings, + }; + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Setings not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const registerRoutes = (router: IRouter) => { + router.get( + { + path: SETTINGS_API_ROUTES.INFO_PATTERN, + validate: GetSettingsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getSettingsHandler + ); + router.put( + { + path: SETTINGS_API_ROUTES.UPDATE_PATTERN, + validate: PutSettingsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + putSettingsHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 5c66f9008e2a3..837e73b966feb 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -5,7 +5,7 @@ */ import { RequestHandler } from 'src/core/server'; import { outputService } from '../../services'; -import { CreateFleetSetupResponse } from '../../types'; +import { CreateFleetSetupResponse } from '../../../common'; import { setupIngestManager, setupFleet } from '../../services/setup'; export const getFleetSetupHandler: RequestHandler = async (context, request, response) => { diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index a2c641503e825..5ee7ee7733220 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -5,14 +5,14 @@ */ import { IRouter } from 'src/core/server'; import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; -import { GetFleetSetupRequestSchema, CreateFleetSetupRequestSchema } from '../../types'; +import { IngestManagerConfigType } from '../../../common'; import { getFleetSetupHandler, createFleetSetupHandler, ingestManagerSetupHandler, } from './handlers'; -export const registerRoutes = (router: IRouter) => { +export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) => { // Ingest manager setup router.post( { @@ -24,11 +24,16 @@ export const registerRoutes = (router: IRouter) => { }, ingestManagerSetupHandler ); + + if (!config.fleet.enabled) { + return; + } + // Get Fleet setup router.get( { path: FLEET_SETUP_API_ROUTES.INFO_PATTERN, - validate: GetFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getFleetSetupHandler @@ -38,7 +43,7 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: FLEET_SETUP_API_ROUTES.CREATE_PATTERN, - validate: CreateFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, createFleetSetupHandler diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index dc0b4695603e4..89d8b9e173ffe 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { SavedObjectsServiceSetup, SavedObjectsType } from 'kibana/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; import { OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, @@ -12,155 +15,301 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_SAVED_OBJET_TYPE, } from './constants'; /* - * Saved object mappings + * Saved object types and mappings * * Please update typings in `/common/types` if mappings are updated. */ -export const savedObjectMappings = { + +const savedObjectTypes: { [key: string]: SavedObjectsType } = { + [GLOBAL_SETTINGS_SAVED_OBJET_TYPE]: { + name: GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + agent_auto_upgrade: { type: 'keyword' }, + package_auto_upgrade: { type: 'keyword' }, + kibana_url: { type: 'keyword' }, + kibana_ca_sha256: { type: 'keyword' }, + }, + }, + }, [AGENT_SAVED_OBJECT_TYPE]: { - properties: { - shared_id: { type: 'keyword' }, - type: { type: 'keyword' }, - active: { type: 'boolean' }, - enrolled_at: { type: 'date' }, - access_api_key_id: { type: 'keyword' }, - version: { type: 'keyword' }, - user_provided_metadata: { type: 'text' }, - local_metadata: { type: 'text' }, - config_id: { type: 'keyword' }, - last_updated: { type: 'date' }, - last_checkin: { type: 'date' }, - config_revision: { type: 'integer' }, - config_newest_revision: { type: 'integer' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - default_api_key: { type: 'keyword' }, - updated_at: { type: 'date' }, - current_error_events: { type: 'text' }, + name: AGENT_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + shared_id: { type: 'keyword' }, + type: { type: 'keyword' }, + active: { type: 'boolean' }, + enrolled_at: { type: 'date' }, + access_api_key_id: { type: 'keyword' }, + version: { type: 'keyword' }, + user_provided_metadata: { type: 'flattened' }, + local_metadata: { type: 'flattened' }, + config_id: { type: 'keyword' }, + last_updated: { type: 'date' }, + last_checkin: { type: 'date' }, + config_revision: { type: 'integer' }, + config_newest_revision: { type: 'integer' }, + default_api_key_id: { type: 'keyword' }, + default_api_key: { type: 'keyword' }, + updated_at: { type: 'date' }, + current_error_events: { type: 'text' }, + }, }, }, [AGENT_ACTION_SAVED_OBJECT_TYPE]: { - properties: { - agent_id: { type: 'keyword' }, - type: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - data: { type: 'flattened' }, - sent_at: { type: 'date' }, - created_at: { type: 'date' }, + name: AGENT_ACTION_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + agent_id: { type: 'keyword' }, + type: { type: 'keyword' }, + data: { type: 'binary' }, + sent_at: { type: 'date' }, + created_at: { type: 'date' }, + }, }, }, [AGENT_EVENT_SAVED_OBJECT_TYPE]: { - properties: { - type: { type: 'keyword' }, - subtype: { type: 'keyword' }, - agent_id: { type: 'keyword' }, - action_id: { type: 'keyword' }, - config_id: { type: 'keyword' }, - stream_id: { type: 'keyword' }, - timestamp: { type: 'date' }, - message: { type: 'text' }, - payload: { type: 'text' }, - data: { type: 'text' }, + name: AGENT_EVENT_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + type: { type: 'keyword' }, + subtype: { type: 'keyword' }, + agent_id: { type: 'keyword' }, + action_id: { type: 'keyword' }, + config_id: { type: 'keyword' }, + stream_id: { type: 'keyword' }, + timestamp: { type: 'date' }, + message: { type: 'text' }, + payload: { type: 'text' }, + data: { type: 'text' }, + }, }, }, [AGENT_CONFIG_SAVED_OBJECT_TYPE]: { - properties: { - id: { type: 'keyword' }, - name: { type: 'text' }, - is_default: { type: 'boolean' }, - namespace: { type: 'keyword' }, - description: { type: 'text' }, - status: { type: 'keyword' }, - datasources: { type: 'keyword' }, - updated_on: { type: 'keyword' }, - updated_by: { type: 'keyword' }, - revision: { type: 'integer' }, + name: AGENT_CONFIG_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + id: { type: 'keyword' }, + name: { type: 'text' }, + is_default: { type: 'boolean' }, + namespace: { type: 'keyword' }, + description: { type: 'text' }, + status: { type: 'keyword' }, + datasources: { type: 'keyword' }, + updated_on: { type: 'keyword' }, + updated_by: { type: 'keyword' }, + revision: { type: 'integer' }, + monitoring_enabled: { type: 'keyword' }, + }, }, }, [ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - api_key: { type: 'binary' }, - api_key_id: { type: 'keyword' }, - config_id: { type: 'keyword' }, - created_at: { type: 'date' }, - updated_at: { type: 'date' }, - expire_at: { type: 'date' }, - active: { type: 'boolean' }, + name: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + api_key: { type: 'binary' }, + api_key_id: { type: 'keyword' }, + config_id: { type: 'keyword' }, + created_at: { type: 'date' }, + updated_at: { type: 'date' }, + expire_at: { type: 'date' }, + active: { type: 'boolean' }, + }, }, }, [OUTPUT_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - is_default: { type: 'boolean' }, - hosts: { type: 'keyword' }, - ca_sha256: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - api_key: { type: 'keyword' }, - fleet_enroll_username: { type: 'binary' }, - fleet_enroll_password: { type: 'binary' }, - config: { type: 'flattened' }, + name: OUTPUT_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + is_default: { type: 'boolean' }, + hosts: { type: 'keyword' }, + ca_sha256: { type: 'keyword' }, + fleet_enroll_username: { type: 'binary' }, + fleet_enroll_password: { type: 'binary' }, + config: { type: 'flattened' }, + }, }, }, [DATASOURCE_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - description: { type: 'text' }, - namespace: { type: 'keyword' }, - config_id: { type: 'keyword' }, - enabled: { type: 'boolean' }, - package: { - properties: { - name: { type: 'keyword' }, - title: { type: 'keyword' }, - version: { type: 'keyword' }, + name: DATASOURCE_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + description: { type: 'text' }, + namespace: { type: 'keyword' }, + config_id: { type: 'keyword' }, + enabled: { type: 'boolean' }, + package: { + properties: { + name: { type: 'keyword' }, + title: { type: 'keyword' }, + version: { type: 'keyword' }, + }, }, - }, - output_id: { type: 'keyword' }, - inputs: { - type: 'nested', - properties: { - type: { type: 'keyword' }, - enabled: { type: 'boolean' }, - processors: { type: 'keyword' }, - config: { type: 'flattened' }, - streams: { - type: 'nested', - properties: { - id: { type: 'keyword' }, - enabled: { type: 'boolean' }, - dataset: { type: 'keyword' }, - processors: { type: 'keyword' }, - config: { type: 'flattened' }, - pkg_stream: { type: 'flattened' }, + output_id: { type: 'keyword' }, + inputs: { + type: 'nested', + properties: { + type: { type: 'keyword' }, + enabled: { type: 'boolean' }, + processors: { type: 'keyword' }, + config: { type: 'flattened' }, + vars: { type: 'flattened' }, + streams: { + type: 'nested', + properties: { + id: { type: 'keyword' }, + enabled: { type: 'boolean' }, + dataset: { type: 'keyword' }, + processors: { type: 'keyword' }, + config: { type: 'flattened' }, + agent_stream: { type: 'flattened' }, + vars: { type: 'flattened' }, + }, }, }, }, + revision: { type: 'integer' }, }, - revision: { type: 'integer' }, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - version: { type: 'keyword' }, - internal: { type: 'boolean' }, - es_index_patterns: { - dynamic: false, - type: 'object', - }, - installed: { - type: 'nested', - properties: { - id: { type: 'keyword' }, - type: { type: 'keyword' }, + name: PACKAGES_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + version: { type: 'keyword' }, + internal: { type: 'boolean' }, + removable: { type: 'boolean' }, + es_index_patterns: { + dynamic: 'false', + type: 'object', + }, + installed: { + type: 'nested', + properties: { + id: { type: 'keyword' }, + type: { type: 'keyword' }, + }, }, }, }, }, }; + +export function registerSavedObjects(savedObjects: SavedObjectsServiceSetup) { + Object.values(savedObjectTypes).forEach(type => { + savedObjects.registerType(type); + }); +} + +export function registerEncryptedSavedObjects( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + // Encrypted saved objects + encryptedSavedObjects.registerType({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['api_key']), + attributesToExcludeFromAAD: new Set([ + 'name', + 'type', + 'api_key_id', + 'config_id', + 'created_at', + 'updated_at', + 'expire_at', + 'active', + ]), + }); + encryptedSavedObjects.registerType({ + type: OUTPUT_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['fleet_enroll_username', 'fleet_enroll_password']), + attributesToExcludeFromAAD: new Set([ + 'name', + 'type', + 'is_default', + 'hosts', + 'ca_sha256', + 'config', + ]), + }); + encryptedSavedObjects.registerType({ + type: AGENT_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['default_api_key']), + attributesToExcludeFromAAD: new Set([ + 'shared_id', + 'type', + 'active', + 'enrolled_at', + 'access_api_key_id', + 'version', + 'user_provided_metadata', + 'local_metadata', + 'config_id', + 'last_updated', + 'last_checkin', + 'config_revision', + 'config_newest_revision', + 'updated_at', + 'current_error_events', + ]), + }); + encryptedSavedObjects.registerType({ + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['data']), + attributesToExcludeFromAAD: new Set(['agent_id', 'type', 'sent_at', 'created_at']), + }); +} 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 0000000000000..17758f6e3d7f1 --- /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 309ddca3784c2..5ecbaff8ad71e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -6,7 +6,11 @@ import { uniq } from 'lodash'; import { SavedObjectsClientContract } from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; -import { DEFAULT_AGENT_CONFIG, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../constants'; +import { + DEFAULT_AGENT_CONFIG, + AGENT_CONFIG_SAVED_OBJECT_TYPE, + AGENT_SAVED_OBJECT_TYPE, +} from '../constants'; import { Datasource, NewAgentConfig, @@ -15,7 +19,8 @@ import { AgentConfigStatus, ListWithKuery, } from '../types'; -import { DeleteAgentConfigsResponse, storedDatasourceToAgentDatasource } from '../../common'; +import { DeleteAgentConfigResponse, storedDatasourceToAgentDatasource } from '../../common'; +import { listAgents } from './agents'; import { datasourceService } from './datasource'; import { outputService } from './output'; import { agentConfigUpdateEventHandler } from './agent_config_update'; @@ -67,7 +72,7 @@ class AgentConfigService { public async ensureDefaultAgentConfig(soClient: SavedObjectsClientContract) { const configs = await soClient.find<AgentConfig>({ type: AGENT_CONFIG_SAVED_OBJECT_TYPE, - filter: 'agent_configs.attributes.is_default:true', + filter: `${AGENT_CONFIG_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (configs.total === 0) { @@ -244,7 +249,7 @@ class AgentConfigService { public async getDefaultAgentConfigId(soClient: SavedObjectsClientContract) { const configs = await soClient.find({ type: AGENT_CONFIG_SAVED_OBJECT_TYPE, - filter: 'agent_configs.attributes.is_default:true', + filter: `${AGENT_CONFIG_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (configs.saved_objects.length === 0) { @@ -256,32 +261,40 @@ class AgentConfigService { public async delete( soClient: SavedObjectsClientContract, - ids: string[] - ): Promise<DeleteAgentConfigsResponse> { - const result: DeleteAgentConfigsResponse = []; - const defaultConfigId = await this.getDefaultAgentConfigId(soClient); + id: string + ): Promise<DeleteAgentConfigResponse> { + const config = await this.get(soClient, id, false); + if (!config) { + throw new Error('Agent configuration not found'); + } - if (ids.includes(defaultConfigId)) { + const defaultConfigId = await this.getDefaultAgentConfigId(soClient); + if (id === defaultConfigId) { throw new Error('The default agent configuration cannot be deleted'); } - for (const id of ids) { - try { - await soClient.delete(SAVED_OBJECT_TYPE, id); - await this.triggerAgentConfigUpdatedEvent(soClient, 'deleted', id); - result.push({ - id, - success: true, - }); - } catch (e) { - result.push({ - id, - success: false, - }); - } + const { total } = await listAgents(soClient, { + showInactive: false, + perPage: 0, + page: 1, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:${id}`, + }); + + if (total > 0) { + throw new Error('Cannot delete agent config that is assigned to agent(s)'); } - return result; + if (config.datasources && config.datasources.length) { + await datasourceService.delete(soClient, config.datasources as string[], { + skipUnassignFromAgentConfigs: true, + }); + } + await soClient.delete(SAVED_OBJECT_TYPE, id); + await this.triggerAgentConfigUpdatedEvent(soClient, 'deleted', id); + return { + id, + success: true, + }; } public async getFullConfig( @@ -301,28 +314,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/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts index b4c1f09015a69..ae0dedce178a8 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -5,19 +5,42 @@ */ import Boom from 'boom'; import { SavedObjectsBulkResponse } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks'; + import { Agent, AgentAction, AgentActionSOAttributes, AgentEvent, } from '../../../common/types/models'; -import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; +import { AGENT_TYPE_PERMANENT, AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { acknowledgeAgentActions } from './acks'; +import { appContextService } from '../app_context'; +import { IngestManagerAppContext } from '../../plugin'; describe('test agent acks services', () => { it('should succeed on valid and matched actions', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockStartEncryptedSOClient = encryptedSavedObjectsMock.createStart(); + appContextService.start(({ + encryptedSavedObjects: mockStartEncryptedSOClient, + } as unknown) as IngestManagerAppContext); + + mockStartEncryptedSOClient.getDecryptedAsInternalUser.mockReturnValue( + Promise.resolve({ + id: 'action1', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: { + type: 'CONFIG_CHANGE', + agent_id: 'id', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + }) + ); mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -25,7 +48,7 @@ describe('test agent acks services', () => { { id: 'action1', references: [], - type: 'agent_actions', + type: AGENT_ACTION_SAVED_OBJECT_TYPE, attributes: { type: 'CONFIG_CHANGE', agent_id: 'id', @@ -114,7 +137,7 @@ describe('test agent acks services', () => { { id: 'action1', references: [], - type: 'agent_actions', + type: AGENT_ACTION_SAVED_OBJECT_TYPE, attributes: { type: 'CONFIG_CHANGE', agent_id: 'id', diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts index f2e671c6dbaa8..c739007952389 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts @@ -6,17 +6,17 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; -import { AgentAction, AgentActionSOAttributes } from '../../../common/types/models'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { AgentAction } from '../../../common/types/models'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - const newAgentAction: AgentActionSOAttributes = { + const newAgentAction: Omit<AgentAction, 'id'> = { agent_id: 'agentid', type: 'CONFIG_CHANGE', - data: 'data', + data: { content: 'data' }, sent_at: '2020-03-14T19:45:02.620Z', created_at: '2020-03-14T19:45:02.620Z', }; @@ -31,7 +31,7 @@ describe('test agent actions services', () => { .calls[0][1] as unknown) as AgentAction; expect(createdAction).toBeDefined(); expect(createdAction?.type).toEqual(newAgentAction.type); - expect(createdAction?.data).toEqual(newAgentAction.data); + expect(createdAction?.data).toEqual(JSON.stringify(newAgentAction.data)); expect(createdAction?.sent_at).toEqual(newAgentAction.sent_at); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index a8ef0820f8d9f..1bb177e54282d 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -8,16 +8,21 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { Agent, AgentAction, AgentActionSOAttributes } from '../../../common/types/models'; import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { savedObjectToAgentAction } from './saved_objects'; +import { appContextService } from '../app_context'; export async function createAgentAction( soClient: SavedObjectsClientContract, - newAgentAction: AgentActionSOAttributes + newAgentAction: Omit<AgentAction, 'id'> ): Promise<AgentAction> { const so = await soClient.create<AgentActionSOAttributes>(AGENT_ACTION_SAVED_OBJECT_TYPE, { ...newAgentAction, + data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, }); - return savedObjectToAgentAction(so); + const agentAction = savedObjectToAgentAction(so); + agentAction.data = newAgentAction.data; + + return agentAction; } export async function getAgentActionsForCheckin( @@ -29,21 +34,47 @@ export async function getAgentActionsForCheckin( filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * and ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id:${agentId}`, }); - return res.saved_objects.map(savedObjectToAgentAction); + return Promise.all( + res.saved_objects.map(async so => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser<AgentActionSOAttributes>( + AGENT_ACTION_SAVED_OBJECT_TYPE, + so.id + ) + ); + }) + ); } export async function getAgentActionByIds( soClient: SavedObjectsClientContract, actionIds: string[] ) { - const res = await soClient.bulkGet<AgentActionSOAttributes>( - actionIds.map(actionId => ({ - id: actionId, - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - })) - ); + const actions = ( + await soClient.bulkGet<AgentActionSOAttributes>( + actionIds.map(actionId => ({ + id: actionId, + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + })) + ) + ).saved_objects.map(savedObjectToAgentAction); - return res.saved_objects.map(savedObjectToAgentAction); + return Promise.all( + actions.map(async action => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser<AgentActionSOAttributes>( + AGENT_ACTION_SAVED_OBJECT_TYPE, + action.id + ) + ); + }) + ); } export interface ActionsService { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts index d98052ea87e86..72a86d7c8158e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts @@ -53,12 +53,12 @@ describe('Agent checkin service', () => { agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config1', revision: 2, }, - }), + }, }, ] ); @@ -80,24 +80,24 @@ describe('Agent checkin service', () => { agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config2', revision: 2, }, - }), + }, }, { id: 'action1', agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config1', revision: 1, }, - }), + }, }, ] ); @@ -118,5 +118,19 @@ describe('Agent checkin service', () => { expect(res).toBeTruthy(); }); + + it('should return true if this agent has no revision currently set', () => { + const res = shouldCreateConfigAction( + getAgent({ + config_id: 'config1', + last_checkin: '2018-01-02T00:00:00', + config_revision: null, + config_newest_revision: 2, + }), + [] + ); + + expect(res).toBeTruthy(); + }); }); }); 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 9a2b3f22b9431..9b1565e7d74aa 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -11,12 +11,14 @@ import { AgentAction, AgentSOAttributes, AgentEventSOAttributes, + AgentMetadata, } from '../../types'; import { agentConfigService } from '../agent_config'; import * as APIKeysService from '../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgentActionsForCheckin, createAgentAction } from './actions'; +import { appContextService } from '../app_context'; export async function agentCheckin( soClient: SavedObjectsClientContract, @@ -27,8 +29,7 @@ export async function agentCheckin( const updateData: { last_checkin: string; default_api_key?: string; - actions?: AgentAction[]; - local_metadata?: string; + local_metadata?: AgentMetadata; current_error_events?: string; } = { last_checkin: new Date().toISOString(), @@ -38,11 +39,17 @@ export async function agentCheckin( // Generate new agent config if config is updated if (agent.config_id && shouldCreateConfigAction(agent, actions)) { + const { + attributes: { default_api_key: defaultApiKey }, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agent.id); + const config = await agentConfigService.getFullConfig(soClient, agent.config_id); if (config) { // Assign output API keys // We currently only support default ouput - if (!agent.default_api_key) { + if (!defaultApiKey) { updateData.default_api_key = await APIKeysService.generateOutputApiKey( soClient, 'default', @@ -50,7 +57,7 @@ export async function agentCheckin( ); } // Mutate the config to set the api token for this agent - config.outputs.default.api_key = agent.default_api_key || updateData.default_api_key; + config.outputs.default.api_key = defaultApiKey || updateData.default_api_key; const configChangeAction = await createAgentAction(soClient, { agent_id: agent.id, @@ -62,9 +69,6 @@ export async function agentCheckin( actions.push(configChangeAction); } } - if (localMetadata) { - updateData.local_metadata = JSON.stringify(localMetadata); - } const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, events); @@ -156,9 +160,13 @@ export function shouldCreateConfigAction(agent: Agent, actions: AgentAction[]): } const isAgentConfigOutdated = - agent.config_revision && - agent.config_newest_revision && - agent.config_revision < agent.config_newest_revision; + // Config reassignment + (!agent.config_revision && agent.config_newest_revision) || + // new revision of a config + (agent.config_revision && + agent.config_newest_revision && + agent.config_revision < agent.config_newest_revision); + if (!isAgentConfigOutdated) { return false; } @@ -168,7 +176,7 @@ export function shouldCreateConfigAction(agent: Agent, actions: AgentAction[]): return false; } - const data = JSON.parse(action.data); + const { data } = action; return ( data.config.id === agent.config_id && data.config.revision === agent.config_newest_revision 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 ec270884e62b4..43fd5a3ce0ac9 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -31,12 +31,17 @@ export async function listAgents( if (kuery && kuery !== '') { // To ensure users dont need to know about SO data structure... - filters.push(kuery.replace(/agents\./g, 'agents.attributes.')); + filters.push( + kuery.replace( + new RegExp(`${AGENT_SAVED_OBJECT_TYPE}\.`, 'g'), + `${AGENT_SAVED_OBJECT_TYPE}.attributes.` + ) + ); } if (showInactive === false) { - const agentActiveCondition = `agents.attributes.active:true AND not agents.attributes.type:${AGENT_TYPE_EPHEMERAL}`; - const recentlySeenEphemeralAgent = `agents.attributes.active:true AND agents.attributes.type:${AGENT_TYPE_EPHEMERAL} AND agents.attributes.last_checkin > ${Date.now() - + const agentActiveCondition = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true AND not ${AGENT_SAVED_OBJECT_TYPE}.attributes.type:${AGENT_TYPE_EPHEMERAL}`; + const recentlySeenEphemeralAgent = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true AND ${AGENT_SAVED_OBJECT_TYPE}.attributes.type:${AGENT_TYPE_EPHEMERAL} AND ${AGENT_SAVED_OBJECT_TYPE}.attributes.last_checkin > ${Date.now() - 3 * AGENT_POLLING_THRESHOLD_MS}`; filters.push(`(${agentActiveCondition}) OR (${recentlySeenEphemeralAgent})`); } @@ -98,7 +103,7 @@ export async function updateAgent( } ) { await soClient.update<AgentSOAttributes>(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 a34d2e03e9b3d..81afa70ecb818 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/events.ts b/x-pack/plugins/ingest_manager/server/services/agents/events.ts index 707229845531c..2758374eba65f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/events.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/events.ts @@ -23,7 +23,10 @@ export async function getAgentEvents( type: AGENT_EVENT_SAVED_OBJECT_TYPE, filter: kuery && kuery !== '' - ? kuery.replace(/agent_events\./g, 'agent_events.attributes.') + ? kuery.replace( + new RegExp(`${AGENT_EVENT_SAVED_OBJECT_TYPE}\.`, 'g'), + `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.` + ) : undefined, perPage, page, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts index c95c9ecc2a1d8..257091af0ebd0 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts @@ -13,3 +13,4 @@ export * from './status'; export * from './crud'; export * from './update'; export * from './actions'; +export * from './reassign'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts new file mode 100644 index 0000000000000..f8142af376eb3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts @@ -0,0 +1,28 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; +import Boom from 'boom'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentSOAttributes } from '../../types'; +import { agentConfigService } from '../agent_config'; + +export async function reassignAgent( + soClient: SavedObjectsClientContract, + agentId: string, + newConfigId: string +) { + const config = await agentConfigService.get(soClient, newConfigId); + if (!config) { + throw Boom.notFound(`Agent Configuration not found: ${newConfigId}`); + } + + await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agentId, { + config_id: newConfigId, + config_revision: null, + config_newest_revision: config.revision, + }); +} 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 aa88520740687..11beba1cd7e43 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<AgentSOAttributes>): 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, }; @@ -38,5 +38,6 @@ export function savedObjectToAgentAction(so: SavedObject<AgentActionSOAttributes return { id: so.id, ...so.attributes, + data: so.attributes.data ? JSON.parse(so.attributes.data) : 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 new file mode 100644 index 0000000000000..8140b1e6de470 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { getAgentStatusById } from './status'; +import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; +import { AgentSOAttributes } from '../../../common/types/models'; +import { SavedObject } from 'kibana/server'; + +describe('Agent status service', () => { + it('should return inactive when agent is not active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: false, + local_metadata: {}, + user_provided_metadata: {}, + }, + } as SavedObject<AgentSOAttributes>); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('inactive'); + }); + + it('should return online when agent is active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: true, + local_metadata: {}, + user_provided_metadata: {}, + }, + } as SavedObject<AgentSOAttributes>); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('online'); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 21e200d701e69..c4d4a8436e147 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -5,8 +5,8 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import { listAgents } from './crud'; -import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { getAgent, listAgents } from './crud'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentStatus, Agent } from '../../types'; import { @@ -17,6 +17,14 @@ import { } from '../../constants'; import { AgentStatusKueryHelper } from '../../../common/services'; +export async function getAgentStatusById( + soClient: SavedObjectsClientContract, + agentId: string +): Promise<AgentStatus> { + const agent = await getAgent(soClient, agentId); + return getAgentStatus(agent); +} + export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus { const { type, last_checkin: lastCheckIn } = agent; const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); @@ -59,13 +67,13 @@ export async function getAgentStatusForConfig( AgentStatusKueryHelper.buildKueryForOfflineAgents(), ].map(kuery => listAgents(soClient, { - showInactive: true, + showInactive: false, perPage: 0, page: 1, kuery: configId ? kuery - ? `(${kuery}) and (agents.config_id:"${configId}")` - : `agents.config_id:"${configId}"` + ? `(${kuery}) and (${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}")` + : `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"` : kuery, }) ) @@ -83,7 +91,9 @@ export async function getAgentStatusForConfig( async function getEventsCount(soClient: SavedObjectsClientContract, configId?: string) { const { total } = await soClient.find({ type: AGENT_EVENT_SAVED_OBJECT_TYPE, - filter: configId ? `agent_events.attributes.config_id:"${configId}"` : undefined, + filter: configId + ? `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.config_id:"${configId}"` + : undefined, perPage: 0, page: 1, sortField: 'timestamp', diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index 18af9fd4de73f..ee7e08d741035 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -10,42 +10,15 @@ import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; import * as APIKeyService from '../api_keys'; -export async function unenrollAgents( - soClient: SavedObjectsClientContract, - toUnenrollIds: string[] -) { - const response = []; - for (const id of toUnenrollIds) { - try { - await unenrollAgent(soClient, id); - response.push({ - id, - success: true, - }); - } catch (error) { - response.push({ - id, - error, - success: false, - }); - } - } - - return response; -} - -async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { +export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = await getAgent(soClient, agentId); await Promise.all([ agent.access_api_key_id ? APIKeyService.invalidateAPIKey(soClient, agent.access_api_key_id) : undefined, - agent.default_api_key - ? APIKeyService.invalidateAPIKey( - soClient, - APIKeyService.parseApiKey(agent.default_api_key).apiKeyId - ) + agent.default_api_key_id + ? APIKeyService.invalidateAPIKey(soClient, agent.default_api_key_id) : undefined, ]); await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agentId, { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index 59d0ad31d1a64..fd57e83d7421e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { unenrollAgents } from './unenroll'; +import { unenrollAgent } from './unenroll'; import { agentConfigService } from '../agent_config'; export async function updateAgentsForConfigId( @@ -22,7 +22,7 @@ export async function updateAgentsForConfigId( let page = 1; while (hasMore) { const { agents } = await listAgents(soClient, { - kuery: `agents.config_id:"${configId}"`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"`, page: page++, perPage: 1000, showInactive: true, @@ -46,7 +46,7 @@ export async function unenrollForConfigId(soClient: SavedObjectsClientContract, let page = 1; while (hasMore) { const { agents } = await listAgents(soClient, { - kuery: `agents.config_id:"${configId}"`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"`, page: page++, perPage: 1000, showInactive: true, @@ -55,9 +55,8 @@ export async function unenrollForConfigId(soClient: SavedObjectsClientContract, if (agents.length === 0) { hasMore = false; } - await unenrollAgents( - soClient, - agents.map(a => a.id) - ); + for (const agent of agents) { + await unenrollAgent(soClient, agent.id); + } } } diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index a6a2db8be4e9d..1ac812c3380cd 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -10,6 +10,7 @@ import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKey } from './security'; import { agentConfigService } from '../agent_config'; +import { appContextService } from '../app_context'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -30,7 +31,10 @@ export async function listEnrollmentApiKeys( sortOrder: 'DESC', filter: kuery && kuery !== '' - ? kuery.replace(/enrollment_api_keys\./g, 'enrollment_api_keys.attributes.') + ? kuery.replace( + new RegExp(`${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}\.`, 'g'), + `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.attributes.` + ) : undefined, }); @@ -45,9 +49,13 @@ export async function listEnrollmentApiKeys( } export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { - return savedObjectToEnrollmentApiKey( - await soClient.get<EnrollmentAPIKeySOAttributes>(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id) - ); + const so = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser<EnrollmentAPIKeySOAttributes>( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + id + ); + return savedObjectToEnrollmentApiKey(so); } /** @@ -75,7 +83,7 @@ export async function deleteEnrollmentApiKeyForConfigId( const { items } = await listEnrollmentApiKeys(soClient, { page: page++, perPage: 100, - kuery: `enrollment_api_keys.config_id:${configId}`, + kuery: `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.config_id:${configId}`, }); if (items.length === 0) { @@ -120,16 +128,19 @@ export async function generateEnrollmentAPIKey( const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); - return savedObjectToEnrollmentApiKey( - await soClient.create<EnrollmentAPIKeySOAttributes>(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, { + const so = await soClient.create<EnrollmentAPIKeySOAttributes>( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + { active: true, api_key_id: key.id, api_key: apiKey, name, config_id: configId, created_at: new Date().toISOString(), - }) + } ); + + return getEnrollmentAPIKey(soClient, so.id); } function savedObjectToEnrollmentApiKey({ diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index a0a7c8dd7c05a..e917d2edd1309 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -34,6 +34,9 @@ class AppContextService { public stop() {} public getEncryptedSavedObjects() { + if (!this.encryptedSavedObjects) { + throw new Error('Encrypted saved object start service not set.'); + } return this.encryptedSavedObjects; } diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts index 09c59998388d1..3682ae6d1167b 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.test.ts @@ -5,39 +5,38 @@ */ import { datasourceService } from './datasource'; +import { PackageInfo } from '../types'; -async function mockedGetAssetsData(_a: any, _b: any, dataset: string) { - if (dataset === 'dataset1') { - return [ - { - buffer: Buffer.from(` +const TEMPLATE = ` type: log metricset: ["dataset1"] paths: {{#each paths}} - {{this}} {{/each}} -`), - }, - ]; - } - return []; -} - -jest.mock('./epm/packages/assets', () => { - return { - getAssetsDataForPackageKey: mockedGetAssetsData, - }; -}); +`; describe('Datasource service', () => { describe('assignPackageStream', () => { - it('should work with cofig variables from the stream', async () => { + it('should work with config variables from the stream', async () => { const inputs = await datasourceService.assignPackageStream( - { - pkgName: 'package', - pkgVersion: '1.0.0', - }, + ({ + datasources: [ + { + inputs: [ + { + type: 'log', + streams: [ + { + dataset: 'package.dataset1', + template: TEMPLATE, + }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo, [ { type: 'log', @@ -47,7 +46,7 @@ describe('Datasource service', () => { id: 'dataset01', dataset: 'package.dataset1', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, @@ -67,12 +66,12 @@ describe('Datasource service', () => { id: 'dataset01', dataset: 'package.dataset1', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, }, - pkg_stream: { + agent_stream: { metricset: ['dataset1'], paths: ['/var/log/set.log'], type: 'log', @@ -85,15 +84,28 @@ describe('Datasource service', () => { it('should work with config variables at the input level', async () => { const inputs = await datasourceService.assignPackageStream( - { - pkgName: 'package', - pkgVersion: '1.0.0', - }, + ({ + datasources: [ + { + inputs: [ + { + type: 'log', + streams: [ + { + dataset: 'package.dataset1', + template: TEMPLATE, + }, + ], + }, + ], + }, + ], + } as unknown) as PackageInfo, [ { type: 'log', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, @@ -113,7 +125,7 @@ describe('Datasource service', () => { { type: 'log', enabled: true, - config: { + vars: { paths: { value: ['/var/log/set.log'], }, @@ -123,7 +135,7 @@ describe('Datasource service', () => { id: 'dataset01', dataset: 'package.dataset1', enabled: true, - pkg_stream: { + agent_stream: { metricset: ['dataset1'], paths: ['/var/log/set.log'], type: 'log', diff --git a/x-pack/plugins/ingest_manager/server/services/datasource.ts b/x-pack/plugins/ingest_manager/server/services/datasource.ts index f27252aaa9a84..affd9b2755881 100644 --- a/x-pack/plugins/ingest_manager/server/services/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/services/datasource.ts @@ -4,20 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract } from 'src/core/server'; -import { safeLoad } from 'js-yaml'; import { AuthenticatedUser } from '../../../security/server'; import { DeleteDatasourcesResponse, packageToConfigDatasource, DatasourceInput, DatasourceInputStream, + PackageInfo, } from '../../common'; import { DATASOURCE_SAVED_OBJECT_TYPE } from '../constants'; import { NewDatasource, Datasource, ListWithKuery } from '../types'; import { agentConfigService } from './agent_config'; import { getPackageInfo, getInstallation } from './epm/packages'; import { outputService } from './output'; -import { getAssetsDataForPackageKey } from './epm/packages/assets'; import { createStream } from './epm/agent/agent'; const SAVED_OBJECT_TYPE = DATASOURCE_SAVED_OBJECT_TYPE; @@ -146,7 +145,7 @@ class DatasourceService { public async delete( soClient: SavedObjectsClientContract, ids: string[], - options?: { user?: AuthenticatedUser } + options?: { user?: AuthenticatedUser; skipUnassignFromAgentConfigs?: boolean } ): Promise<DeleteDatasourcesResponse> { const result: DeleteDatasourcesResponse = []; @@ -156,14 +155,16 @@ class DatasourceService { if (!oldDatasource) { throw new Error('Datasource not found'); } - await agentConfigService.unassignDatasources( - soClient, - oldDatasource.config_id, - [oldDatasource.id], - { - user: options?.user, - } - ); + if (!options?.skipUnassignFromAgentConfigs) { + await agentConfigService.unassignDatasources( + soClient, + oldDatasource.config_id, + [oldDatasource.id], + { + user: options?.user, + } + ); + } await soClient.delete(SAVED_OBJECT_TYPE, id); result.push({ id, @@ -201,20 +202,16 @@ class DatasourceService { } public async assignPackageStream( - pkgInfo: { pkgName: string; pkgVersion: string }, + pkgInfo: PackageInfo, inputs: DatasourceInput[] ): Promise<DatasourceInput[]> { const inputsPromises = inputs.map(input => _assignPackageStreamToInput(pkgInfo, input)); + return Promise.all(inputsPromises); } } -const _isAgentStream = (p: string) => !!p.match(/agent\/stream\/stream\.yml/); - -async function _assignPackageStreamToInput( - pkgInfo: { pkgName: string; pkgVersion: string }, - input: DatasourceInput -) { +async function _assignPackageStreamToInput(pkgInfo: PackageInfo, input: DatasourceInput) { const streamsPromises = input.streams.map(stream => _assignPackageStreamToStream(pkgInfo, input, stream) ); @@ -224,35 +221,43 @@ async function _assignPackageStreamToInput( } async function _assignPackageStreamToStream( - pkgInfo: { pkgName: string; pkgVersion: string }, + pkgInfo: PackageInfo, input: DatasourceInput, stream: DatasourceInputStream ) { if (!stream.enabled) { - return { ...stream, pkg_stream: undefined }; + return { ...stream, agent_stream: undefined }; } const dataset = getDataset(stream.dataset); - const assetsData = await getAssetsDataForPackageKey(pkgInfo, _isAgentStream, dataset); + const datasource = pkgInfo.datasources?.[0]; + if (!datasource) { + throw new Error('Stream template not found, no datasource'); + } - const [pkgStream] = assetsData; - if (!pkgStream || !pkgStream.buffer) { - throw new Error(`Stream template not found for dataset ${dataset}`); + const inputFromPkg = datasource.inputs.find(pkgInput => pkgInput.type === input.type); + if (!inputFromPkg) { + throw new Error(`Stream template not found, unable to found input ${input.type}`); } - // Populate template variables from input config and stream config - const data: { [k: string]: string | string[] } = {}; - if (input.config) { - for (const key of Object.keys(input.config)) { - data[key] = input.config[key].value; - } + const streamFromPkg = inputFromPkg.streams.find( + pkgStream => pkgStream.dataset === stream.dataset + ); + if (!streamFromPkg) { + throw new Error(`Stream template not found, unable to found stream ${stream.dataset}`); } - if (stream.config) { - for (const key of Object.keys(stream.config)) { - data[key] = stream.config[key].value; - } + + if (!streamFromPkg.template) { + throw new Error(`Stream template not found for dataset ${dataset}`); } - const yaml = safeLoad(createStream(data, pkgStream.buffer.toString())); - stream.pkg_stream = yaml; + + const yaml = createStream( + // Populate template variables from input vars and stream vars + Object.assign({}, input.vars, stream.vars), + streamFromPkg.template + ); + + stream.agent_stream = yaml; + return { ...stream }; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts index 21de625532f03..db2e4fe474640 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.test.ts @@ -6,29 +6,61 @@ import { createStream } from './agent'; -test('Test creating a stream from template', () => { - const streamTemplate = ` -input: log -paths: -{{#each paths}} - - {{this}} -{{/each}} -exclude_files: [".gz$"] -processors: - - add_locale: ~ - `; - const vars = { - paths: ['/usr/local/var/log/nginx/access.log'], - }; +describe('createStream', () => { + it('should work', () => { + const streamTemplate = ` + input: log + paths: + {{#each paths}} + - {{this}} + {{/each}} + exclude_files: [".gz$"] + processors: + - add_locale: ~ + `; + const vars = { + paths: { value: ['/usr/local/var/log/nginx/access.log'] }, + }; - const output = createStream(vars, streamTemplate); + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'log', + paths: ['/usr/local/var/log/nginx/access.log'], + exclude_files: ['.gz$'], + processors: [{ add_locale: null }], + }); + }); - expect(output).toBe(` -input: log -paths: - - /usr/local/var/log/nginx/access.log -exclude_files: [".gz$"] -processors: - - add_locale: ~ - `); + it('should support yaml values', () => { + const streamTemplate = ` + input: redis/metrics + metricsets: ["key"] + test: null + {{#if key.patterns}} + key.patterns: {{key.patterns}} + {{/if}} + `; + const vars = { + 'key.patterns': { + type: 'yaml', + value: ` + - limit: 20 + pattern: '*' + `, + }, + }; + + const output = createStream(vars, streamTemplate); + expect(output).toEqual({ + input: 'redis/metrics', + metricsets: ['key'], + test: null, + 'key.patterns': [ + { + limit: 20, + pattern: '*', + }, + ], + }); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts index 5d9a6d409aa1a..8254c0d8aaa37 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/agent/agent.ts @@ -5,12 +5,71 @@ */ import Handlebars from 'handlebars'; +import { safeLoad } from 'js-yaml'; +import { DatasourceConfigRecord } from '../../../../common'; -interface StreamVars { - [k: string]: string | string[]; +function isValidKey(key: string) { + return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; } -export function createStream(vars: StreamVars, streamTemplate: string) { - const template = Handlebars.compile(streamTemplate); - return template(vars); +function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any) { + if (Object.keys(yamlVariables).length === 0 || !yaml) { + return yaml; + } + + Object.entries(yaml).forEach(([key, value]: [string, any]) => { + if (typeof value === 'object') { + yaml[key] = replaceVariablesInYaml(yamlVariables, value); + } + if (typeof value === 'string' && value in yamlVariables) { + yaml[key] = yamlVariables[value]; + } + }); + + return yaml; +} + +function buildTemplateVariables(variables: DatasourceConfigRecord) { + const yamlValues: { [k: string]: any } = {}; + const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => { + // support variables with . like key.patterns + const keyParts = key.split('.'); + const lastKeyPart = keyParts.pop(); + + if (!lastKeyPart || !isValidKey(lastKeyPart)) { + throw new Error('Invalid key'); + } + + let varPart = acc; + for (const keyPart of keyParts) { + if (!isValidKey(keyPart)) { + throw new Error('Invalid key'); + } + if (!varPart[keyPart]) { + varPart[keyPart] = {}; + } + varPart = varPart[keyPart]; + } + + if (recordEntry.type && recordEntry.type === 'yaml') { + const yamlKeyPlaceholder = `##${key}##`; + varPart[lastKeyPart] = `"${yamlKeyPlaceholder}"`; + yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null; + } else { + varPart[lastKeyPart] = recordEntry.value; + } + return acc; + }, {} as { [k: string]: any }); + + return { vars, yamlValues }; +} + +export function createStream(variables: DatasourceConfigRecord, streamTemplate: string) { + const { vars, yamlValues } = buildTemplateVariables(variables); + + const template = Handlebars.compile(streamTemplate, { noEscape: true }); + const stream = template(vars); + const yamlFromStream = safeLoad(stream, {}); + + return replaceVariablesInYaml(yamlValues, yamlFromStream); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 166983fbccc35..440060aff9616 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -2,859 +2,856 @@ exports[`tests loading base.yml: base.yml 1`] = ` { - "order": 1, + "priority": 1, "index_patterns": [ "foo-*" ], - "settings": { - "index": { - "lifecycle": { - "name": "logs-default" - }, - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "refresh_interval": "5s", - "number_of_shards": "1", - "query": { - "default_field": [ - "message" - ] - }, - "number_of_routing_shards": "30" - } - }, - "mappings": { - "_meta": { - "package": "foo" - }, - "dynamic_templates": [ - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } + "template": { + "settings": { + "index": { + "lifecycle": { + "name": "logs-default" + }, + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "refresh_interval": "5s", + "number_of_shards": "1", + "query": { + "default_field": [ + "message" + ] + }, + "number_of_routing_shards": "30" } - ], - "date_detection": false, - "properties": { - "user": { - "properties": { - "auid": { - "ignore_above": 1024, - "type": "keyword" - }, - "euid": { - "ignore_above": 1024, - "type": "keyword" + }, + "mappings": { + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" } } - }, - "long": { - "properties": { - "nested": { - "properties": { - "foo": { - "type": "text" - }, - "bar": { - "type": "long" + ], + "date_detection": false, + "properties": { + "user": { + "properties": { + "auid": { + "ignore_above": 1024, + "type": "keyword" + }, + "euid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "long": { + "properties": { + "nested": { + "properties": { + "foo": { + "type": "text" + }, + "bar": { + "type": "long" + } } } } - } - }, - "nested": { - "properties": { - "bar": { - "ignore_above": 1024, - "type": "keyword" - }, - "baz": { - "ignore_above": 1024, - "type": "keyword" + }, + "nested": { + "properties": { + "bar": { + "ignore_above": 1024, + "type": "keyword" + }, + "baz": { + "ignore_above": 1024, + "type": "keyword" + } } + }, + "myalias": { + "type": "alias", + "path": "user.euid" + }, + "validarray": { + "type": "integer" } - }, - "myalias": { - "type": "alias", - "path": "user.euid" - }, - "validarray": { - "type": "integer" } - } - }, - "aliases": {} + }, + "aliases": {} + } } `; exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` { - "order": 1, + "priority": 1, "index_patterns": [ "foo-*" ], - "settings": { - "index": { - "lifecycle": { - "name": "logs-default" - }, - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "refresh_interval": "5s", - "number_of_shards": "1", - "query": { - "default_field": [ - "message" - ] - }, - "number_of_routing_shards": "30" - } - }, - "mappings": { - "_meta": { - "package": "foo" + "template": { + "settings": { + "index": { + "lifecycle": { + "name": "logs-default" + }, + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "refresh_interval": "5s", + "number_of_shards": "1", + "query": { + "default_field": [ + "message" + ] + }, + "number_of_routing_shards": "30" + } }, - "dynamic_templates": [ - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" + "mappings": { + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } } - } - ], - "date_detection": false, - "properties": { - "coredns": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "query": { - "properties": { - "size": { - "type": "long" - }, - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" + ], + "date_detection": false, + "properties": { + "coredns": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "query": { + "properties": { + "size": { + "type": "long" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } } - } - }, - "response": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" + }, + "response": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "size": { + "type": "long" + } } + }, + "dnssec_ok": { + "type": "boolean" } - }, - "dnssec_ok": { - "type": "boolean" } } } - } - }, - "aliases": {} + }, + "aliases": {} + } } `; exports[`tests loading system.yml: system.yml 1`] = ` { - "order": 1, + "priority": 1, "index_patterns": [ "whatsthis-*" ], - "settings": { - "index": { - "lifecycle": { - "name": "metrics-default" - }, - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": "10000" - } - }, - "refresh_interval": "5s", - "number_of_shards": "1", - "query": { - "default_field": [ - "message" - ] - }, - "number_of_routing_shards": "30" - } - }, - "mappings": { - "_meta": { - "package": "foo" + "template": { + "settings": { + "index": { + "lifecycle": { + "name": "metrics-default" + }, + "codec": "best_compression", + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "refresh_interval": "5s", + "number_of_shards": "1", + "query": { + "default_field": [ + "message" + ] + }, + "number_of_routing_shards": "30" + } }, - "dynamic_templates": [ - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" + "mappings": { + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } } - } - ], - "date_detection": false, - "properties": { - "system": { - "properties": { - "core": { - "properties": { - "id": { - "type": "long" - }, - "user": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + ], + "date_detection": false, + "properties": { + "system": { + "properties": { + "core": { + "properties": { + "id": { + "type": "long" + }, + "user": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } - } - }, - "system": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "system": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } - } - }, - "nice": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "nice": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } - } - }, - "idle": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "idle": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } - } - }, - "iowait": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "iowait": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } - } - }, - "irq": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "irq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } - } - }, - "softirq": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "softirq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } - } - }, - "steal": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "ticks": { - "type": "long" + }, + "steal": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "ticks": { + "type": "long" + } } } } - } - }, - "cpu": { - "properties": { - "cores": { - "type": "long" - }, - "user": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "cpu": { + "properties": { + "cores": { + "type": "long" + }, + "user": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "system": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "system": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "nice": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "nice": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "idle": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "idle": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "iowait": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "iowait": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "irq": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "irq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "softirq": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "softirq": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "steal": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "steal": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "total": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "total": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } } } } } - } - }, - "diskio": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "read": { - "properties": { - "count": { - "type": "long" - }, - "bytes": { - "type": "long" - }, - "time": { - "type": "long" + }, + "diskio": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "read": { + "properties": { + "count": { + "type": "long" + }, + "bytes": { + "type": "long" + }, + "time": { + "type": "long" + } } - } - }, - "write": { - "properties": { - "count": { - "type": "long" - }, - "bytes": { - "type": "long" - }, - "time": { - "type": "long" + }, + "write": { + "properties": { + "count": { + "type": "long" + }, + "bytes": { + "type": "long" + }, + "time": { + "type": "long" + } } - } - }, - "io": { - "properties": { - "time": { - "type": "long" + }, + "io": { + "properties": { + "time": { + "type": "long" + } } - } - }, - "iostat": { - "properties": { - "read": { - "properties": { - "request": { - "properties": { - "merges_per_sec": { - "type": "float" - }, - "per_sec": { - "type": "float" + }, + "iostat": { + "properties": { + "read": { + "properties": { + "request": { + "properties": { + "merges_per_sec": { + "type": "float" + }, + "per_sec": { + "type": "float" + } } - } - }, - "per_sec": { - "properties": { - "bytes": { - "type": "float" + }, + "per_sec": { + "properties": { + "bytes": { + "type": "float" + } } + }, + "await": { + "type": "float" } - }, - "await": { - "type": "float" } - } - }, - "write": { - "properties": { - "request": { - "properties": { - "merges_per_sec": { - "type": "float" - }, - "per_sec": { - "type": "float" + }, + "write": { + "properties": { + "request": { + "properties": { + "merges_per_sec": { + "type": "float" + }, + "per_sec": { + "type": "float" + } } - } - }, - "per_sec": { - "properties": { - "bytes": { - "type": "float" + }, + "per_sec": { + "properties": { + "bytes": { + "type": "float" + } } + }, + "await": { + "type": "float" } - }, - "await": { - "type": "float" } - } - }, - "request": { - "properties": { - "avg_size": { - "type": "float" + }, + "request": { + "properties": { + "avg_size": { + "type": "float" + } } - } - }, - "queue": { - "properties": { - "avg_size": { - "type": "float" + }, + "queue": { + "properties": { + "avg_size": { + "type": "float" + } } + }, + "await": { + "type": "float" + }, + "service_time": { + "type": "float" + }, + "busy": { + "type": "float" } - }, - "await": { - "type": "float" - }, - "service_time": { - "type": "float" - }, - "busy": { - "type": "float" } } } - } - }, - "entropy": { - "properties": { - "available_bits": { - "type": "long" - }, - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "entropy": { + "properties": { + "available_bits": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } - } - }, - "filesystem": { - "properties": { - "available": { - "type": "long" - }, - "device_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mount_point": { - "ignore_above": 1024, - "type": "keyword" - }, - "files": { - "type": "long" - }, - "free": { - "type": "long" - }, - "free_files": { - "type": "long" - }, - "total": { - "type": "long" - }, - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "filesystem": { + "properties": { + "available": { + "type": "long" + }, + "device_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mount_point": { + "ignore_above": 1024, + "type": "keyword" + }, + "files": { + "type": "long" + }, + "free": { + "type": "long" + }, + "free_files": { + "type": "long" + }, + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } } } - } - }, - "fsstat": { - "properties": { - "count": { - "type": "long" - }, - "total_files": { - "type": "long" - }, - "total_size": { - "properties": { - "free": { - "type": "long" - }, - "used": { - "type": "long" - }, - "total": { - "type": "long" + }, + "fsstat": { + "properties": { + "count": { + "type": "long" + }, + "total_files": { + "type": "long" + }, + "total_size": { + "properties": { + "free": { + "type": "long" + }, + "used": { + "type": "long" + }, + "total": { + "type": "long" + } } } } - } - }, - "load": { - "properties": { - "1": { - "type": "scaled_float", - "scaling_factor": 100 - }, - "5": { - "type": "scaled_float", - "scaling_factor": 100 - }, - "15": { - "type": "scaled_float", - "scaling_factor": 100 - }, - "norm": { - "properties": { - "1": { - "type": "scaled_float", - "scaling_factor": 100 - }, - "5": { - "type": "scaled_float", - "scaling_factor": 100 - }, - "15": { - "type": "scaled_float", - "scaling_factor": 100 + }, + "load": { + "properties": { + "1": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "5": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "15": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "norm": { + "properties": { + "1": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "5": { + "type": "scaled_float", + "scaling_factor": 100 + }, + "15": { + "type": "scaled_float", + "scaling_factor": 100 + } } + }, + "cores": { + "type": "long" } - }, - "cores": { - "type": "long" } - } - }, - "memory": { - "properties": { - "total": { - "type": "long" - }, - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "memory": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } - } - }, - "free": { - "type": "long" - }, - "actual": { - "properties": { - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "free": { + "type": "long" + }, + "actual": { + "properties": { + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "free": { + "type": "long" } - }, - "free": { - "type": "long" } - } - }, - "swap": { - "properties": { - "total": { - "type": "long" - }, - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "swap": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } - } - }, - "free": { - "type": "long" - }, - "out": { - "properties": { - "pages": { - "type": "long" + }, + "free": { + "type": "long" + }, + "out": { + "properties": { + "pages": { + "type": "long" + } } - } - }, - "in": { - "properties": { - "pages": { - "type": "long" + }, + "in": { + "properties": { + "pages": { + "type": "long" + } } - } - }, - "readahead": { - "properties": { - "pages": { - "type": "long" - }, - "cached": { - "type": "long" + }, + "readahead": { + "properties": { + "pages": { + "type": "long" + }, + "cached": { + "type": "long" + } } } } - } - }, - "hugepages": { - "properties": { - "total": { - "type": "long" - }, - "used": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "long" + }, + "hugepages": { + "properties": { + "total": { + "type": "long" + }, + "used": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "long" + } } - } - }, - "free": { - "type": "long" - }, - "reserved": { - "type": "long" - }, - "surplus": { - "type": "long" - }, - "default_size": { - "type": "long" - }, - "swap": { - "properties": { - "out": { - "properties": { - "pages": { - "type": "long" - }, - "fallback": { - "type": "long" + }, + "free": { + "type": "long" + }, + "reserved": { + "type": "long" + }, + "surplus": { + "type": "long" + }, + "default_size": { + "type": "long" + }, + "swap": { + "properties": { + "out": { + "properties": { + "pages": { + "type": "long" + }, + "fallback": { + "type": "long" + } } } } @@ -862,743 +859,743 @@ exports[`tests loading system.yml: system.yml 1`] = ` } } } - } - }, - "network": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "out": { - "properties": { - "bytes": { - "type": "long" - }, - "packets": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "dropped": { - "type": "long" + }, + "network": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "out": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "dropped": { + "type": "long" + } } - } - }, - "in": { - "properties": { - "bytes": { - "type": "long" - }, - "packets": { - "type": "long" - }, - "errors": { - "type": "long" - }, - "dropped": { - "type": "long" + }, + "in": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + }, + "errors": { + "type": "long" + }, + "dropped": { + "type": "long" + } } } } - } - }, - "network_summary": { - "properties": { - "ip": { - "properties": { - "*": { - "type": "object" + }, + "network_summary": { + "properties": { + "ip": { + "properties": { + "*": { + "type": "object" + } } - } - }, - "tcp": { - "properties": { - "*": { - "type": "object" + }, + "tcp": { + "properties": { + "*": { + "type": "object" + } } - } - }, - "udp": { - "properties": { - "*": { - "type": "object" + }, + "udp": { + "properties": { + "*": { + "type": "object" + } } - } - }, - "udp_lite": { - "properties": { - "*": { - "type": "object" + }, + "udp_lite": { + "properties": { + "*": { + "type": "object" + } } - } - }, - "icmp": { - "properties": { - "*": { - "type": "object" + }, + "icmp": { + "properties": { + "*": { + "type": "object" + } } } } - } - }, - "process": { - "properties": { - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "cmdline": { - "ignore_above": 2048, - "type": "keyword" - }, - "env": { - "type": "object" - }, - "cpu": { - "properties": { - "user": { - "properties": { - "ticks": { - "type": "long" + }, + "process": { + "properties": { + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "cmdline": { + "ignore_above": 2048, + "type": "keyword" + }, + "env": { + "type": "object" + }, + "cpu": { + "properties": { + "user": { + "properties": { + "ticks": { + "type": "long" + } } - } - }, - "total": { - "properties": { - "value": { - "type": "long" - }, - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 - }, - "norm": { - "properties": { - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "total": { + "properties": { + "value": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + }, + "norm": { + "properties": { + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "ticks": { + "type": "long" } - }, - "ticks": { - "type": "long" } - } - }, - "system": { - "properties": { - "ticks": { - "type": "long" + }, + "system": { + "properties": { + "ticks": { + "type": "long" + } } + }, + "start_time": { + "type": "date" } - }, - "start_time": { - "type": "date" } - } - }, - "memory": { - "properties": { - "size": { - "type": "long" - }, - "rss": { - "properties": { - "bytes": { - "type": "long" - }, - "pct": { - "type": "scaled_float", - "scaling_factor": 1000 + }, + "memory": { + "properties": { + "size": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + }, + "pct": { + "type": "scaled_float", + "scaling_factor": 1000 + } } + }, + "share": { + "type": "long" } - }, - "share": { - "type": "long" } - } - }, - "fd": { - "properties": { - "open": { - "type": "long" - }, - "limit": { - "properties": { - "soft": { - "type": "long" - }, - "hard": { - "type": "long" + }, + "fd": { + "properties": { + "open": { + "type": "long" + }, + "limit": { + "properties": { + "soft": { + "type": "long" + }, + "hard": { + "type": "long" + } } } } - } - }, - "cgroup": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "cpu": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "cfs": { - "properties": { - "period": { - "properties": { - "us": { - "type": "long" + }, + "cgroup": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "cfs": { + "properties": { + "period": { + "properties": { + "us": { + "type": "long" + } } - } - }, - "quota": { - "properties": { - "us": { - "type": "long" + }, + "quota": { + "properties": { + "us": { + "type": "long" + } } + }, + "shares": { + "type": "long" } - }, - "shares": { - "type": "long" } - } - }, - "rt": { - "properties": { - "period": { - "properties": { - "us": { - "type": "long" + }, + "rt": { + "properties": { + "period": { + "properties": { + "us": { + "type": "long" + } } - } - }, - "runtime": { - "properties": { - "us": { - "type": "long" + }, + "runtime": { + "properties": { + "us": { + "type": "long" + } } } } - } - }, - "stats": { - "properties": { - "periods": { - "type": "long" - }, - "throttled": { - "properties": { - "periods": { - "type": "long" - }, - "ns": { - "type": "long" + }, + "stats": { + "properties": { + "periods": { + "type": "long" + }, + "throttled": { + "properties": { + "periods": { + "type": "long" + }, + "ns": { + "type": "long" + } } } } } } - } - }, - "cpuacct": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "total": { - "properties": { - "ns": { - "type": "long" + }, + "cpuacct": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "total": { + "properties": { + "ns": { + "type": "long" + } } - } - }, - "stats": { - "properties": { - "user": { - "properties": { - "ns": { - "type": "long" + }, + "stats": { + "properties": { + "user": { + "properties": { + "ns": { + "type": "long" + } } - } - }, - "system": { - "properties": { - "ns": { - "type": "long" + }, + "system": { + "properties": { + "ns": { + "type": "long" + } } } } + }, + "percpu": { + "type": "object" } - }, - "percpu": { - "type": "object" } - } - }, - "memory": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "mem": { - "properties": { - "usage": { - "properties": { - "bytes": { - "type": "long" - }, - "max": { - "properties": { - "bytes": { - "type": "long" + }, + "memory": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "mem": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } } } } - } - }, - "limit": { - "properties": { - "bytes": { - "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } } + }, + "failures": { + "type": "long" } - }, - "failures": { - "type": "long" } - } - }, - "memsw": { - "properties": { - "usage": { - "properties": { - "bytes": { - "type": "long" - }, - "max": { - "properties": { - "bytes": { - "type": "long" + }, + "memsw": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } } } } - } - }, - "limit": { - "properties": { - "bytes": { - "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } } + }, + "failures": { + "type": "long" } - }, - "failures": { - "type": "long" } - } - }, - "kmem": { - "properties": { - "usage": { - "properties": { - "bytes": { - "type": "long" - }, - "max": { - "properties": { - "bytes": { - "type": "long" + }, + "kmem": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } } } } - } - }, - "limit": { - "properties": { - "bytes": { - "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } } + }, + "failures": { + "type": "long" } - }, - "failures": { - "type": "long" } - } - }, - "kmem_tcp": { - "properties": { - "usage": { - "properties": { - "bytes": { - "type": "long" - }, - "max": { - "properties": { - "bytes": { - "type": "long" + }, + "kmem_tcp": { + "properties": { + "usage": { + "properties": { + "bytes": { + "type": "long" + }, + "max": { + "properties": { + "bytes": { + "type": "long" + } } } } - } - }, - "limit": { - "properties": { - "bytes": { - "type": "long" + }, + "limit": { + "properties": { + "bytes": { + "type": "long" + } } + }, + "failures": { + "type": "long" } - }, - "failures": { - "type": "long" } - } - }, - "stats": { - "properties": { - "active_anon": { - "properties": { - "bytes": { - "type": "long" + }, + "stats": { + "properties": { + "active_anon": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "active_file": { - "properties": { - "bytes": { - "type": "long" + }, + "active_file": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "cache": { - "properties": { - "bytes": { - "type": "long" + }, + "cache": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "hierarchical_memory_limit": { - "properties": { - "bytes": { - "type": "long" + }, + "hierarchical_memory_limit": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "hierarchical_memsw_limit": { - "properties": { - "bytes": { - "type": "long" + }, + "hierarchical_memsw_limit": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "inactive_anon": { - "properties": { - "bytes": { - "type": "long" + }, + "inactive_anon": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "inactive_file": { - "properties": { - "bytes": { - "type": "long" + }, + "inactive_file": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "mapped_file": { - "properties": { - "bytes": { - "type": "long" + }, + "mapped_file": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "page_faults": { - "type": "long" - }, - "major_page_faults": { - "type": "long" - }, - "pages_in": { - "type": "long" - }, - "pages_out": { - "type": "long" - }, - "rss": { - "properties": { - "bytes": { - "type": "long" + }, + "page_faults": { + "type": "long" + }, + "major_page_faults": { + "type": "long" + }, + "pages_in": { + "type": "long" + }, + "pages_out": { + "type": "long" + }, + "rss": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "rss_huge": { - "properties": { - "bytes": { - "type": "long" + }, + "rss_huge": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "swap": { - "properties": { - "bytes": { - "type": "long" + }, + "swap": { + "properties": { + "bytes": { + "type": "long" + } } - } - }, - "unevictable": { - "properties": { - "bytes": { - "type": "long" + }, + "unevictable": { + "properties": { + "bytes": { + "type": "long" + } } } } } } - } - }, - "blkio": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "total": { - "properties": { - "bytes": { - "type": "long" - }, - "ios": { - "type": "long" + }, + "blkio": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "total": { + "properties": { + "bytes": { + "type": "long" + }, + "ios": { + "type": "long" + } } } } } } - } - }, - "summary": { - "properties": { - "total": { - "type": "long" - }, - "running": { - "type": "long" - }, - "idle": { - "type": "long" - }, - "sleeping": { - "type": "long" - }, - "stopped": { - "type": "long" - }, - "zombie": { - "type": "long" - }, - "dead": { - "type": "long" - }, - "unknown": { - "type": "long" + }, + "summary": { + "properties": { + "total": { + "type": "long" + }, + "running": { + "type": "long" + }, + "idle": { + "type": "long" + }, + "sleeping": { + "type": "long" + }, + "stopped": { + "type": "long" + }, + "zombie": { + "type": "long" + }, + "dead": { + "type": "long" + }, + "unknown": { + "type": "long" + } } } } - } - }, - "raid": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "sync_action": { - "ignore_above": 1024, - "type": "keyword" - }, - "disks": { - "properties": { - "active": { - "type": "long" - }, - "total": { - "type": "long" - }, - "spare": { - "type": "long" - }, - "failed": { - "type": "long" - }, - "states": { - "properties": { - "*": { - "type": "object" + }, + "raid": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "level": { + "ignore_above": 1024, + "type": "keyword" + }, + "sync_action": { + "ignore_above": 1024, + "type": "keyword" + }, + "disks": { + "properties": { + "active": { + "type": "long" + }, + "total": { + "type": "long" + }, + "spare": { + "type": "long" + }, + "failed": { + "type": "long" + }, + "states": { + "properties": { + "*": { + "type": "object" + } } } } - } - }, - "blocks": { - "properties": { - "total": { - "type": "long" - }, - "synced": { - "type": "long" + }, + "blocks": { + "properties": { + "total": { + "type": "long" + }, + "synced": { + "type": "long" + } } } } - } - }, - "socket": { - "properties": { - "local": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" + }, + "socket": { + "properties": { + "local": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + } } - } - }, - "remote": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - }, - "host": { - "ignore_above": 1024, - "type": "keyword" - }, - "etld_plus_one": { - "ignore_above": 1024, - "type": "keyword" - }, - "host_error": { - "ignore_above": 1024, - "type": "keyword" + }, + "remote": { + "properties": { + "ip": { + "type": "ip" + }, + "port": { + "type": "long" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "etld_plus_one": { + "ignore_above": 1024, + "type": "keyword" + }, + "host_error": { + "ignore_above": 1024, + "type": "keyword" + } } - } - }, - "process": { - "properties": { - "cmdline": { - "ignore_above": 1024, - "type": "keyword" + }, + "process": { + "properties": { + "cmdline": { + "ignore_above": 1024, + "type": "keyword" + } } - } - }, - "user": { - "properties": {} - }, - "summary": { - "properties": { - "all": { - "properties": { - "count": { - "type": "long" - }, - "listening": { - "type": "long" + }, + "user": { + "properties": {} + }, + "summary": { + "properties": { + "all": { + "properties": { + "count": { + "type": "long" + }, + "listening": { + "type": "long" + } } - } - }, - "tcp": { - "properties": { - "memory": { - "type": "long" - }, - "all": { - "properties": { - "orphan": { - "type": "long" - }, - "count": { - "type": "long" - }, - "listening": { - "type": "long" - }, - "established": { - "type": "long" - }, - "close_wait": { - "type": "long" - }, - "time_wait": { - "type": "long" - }, - "syn_sent": { - "type": "long" - }, - "syn_recv": { - "type": "long" - }, - "fin_wait1": { - "type": "long" - }, - "fin_wait2": { - "type": "long" - }, - "last_ack": { - "type": "long" - }, - "closing": { - "type": "long" + }, + "tcp": { + "properties": { + "memory": { + "type": "long" + }, + "all": { + "properties": { + "orphan": { + "type": "long" + }, + "count": { + "type": "long" + }, + "listening": { + "type": "long" + }, + "established": { + "type": "long" + }, + "close_wait": { + "type": "long" + }, + "time_wait": { + "type": "long" + }, + "syn_sent": { + "type": "long" + }, + "syn_recv": { + "type": "long" + }, + "fin_wait1": { + "type": "long" + }, + "fin_wait2": { + "type": "long" + }, + "last_ack": { + "type": "long" + }, + "closing": { + "type": "long" + } } } } - } - }, - "udp": { - "properties": { - "memory": { - "type": "long" - }, - "all": { - "properties": { - "count": { - "type": "long" + }, + "udp": { + "properties": { + "memory": { + "type": "long" + }, + "all": { + "properties": { + "count": { + "type": "long" + } } } } @@ -1606,65 +1603,65 @@ exports[`tests loading system.yml: system.yml 1`] = ` } } } - } - }, - "uptime": { - "properties": { - "duration": { - "properties": { - "ms": { - "type": "long" + }, + "uptime": { + "properties": { + "duration": { + "properties": { + "ms": { + "type": "long" + } } } } - } - }, - "users": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "seat": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "ignore_above": 1024, - "type": "keyword" - }, - "remote": { - "type": "boolean" - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "leader": { - "type": "long" - }, - "remote_host": { - "ignore_above": 1024, - "type": "keyword" + }, + "users": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "seat": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "remote": { + "type": "boolean" + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "scope": { + "ignore_above": 1024, + "type": "keyword" + }, + "leader": { + "type": "long" + }, + "remote_host": { + "ignore_above": 1024, + "type": "keyword" + } } } } } } - } - }, - "aliases": {} + }, + "aliases": {} + } } `; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 560ddfc1f6885..6ef6f863753b5 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AssetReference, - Dataset, - RegistryPackage, - IngestAssetType, - ElasticsearchAssetType, -} from '../../../../types'; +import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types'; import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; @@ -22,15 +16,17 @@ export const installTemplates = async ( callCluster: CallESAsCurrentUser, pkgName: string, pkgVersion: string -) => { +): Promise<TemplateRef[]> => { // install any pre-built index template assets, - // atm, this is only the base package's global template + // atm, this is only the base package's global index templates + // Install component templates first, as they are used by the index templates + installPreBuiltComponentTemplates(pkgName, pkgVersion, callCluster); installPreBuiltTemplates(pkgName, pkgVersion, callCluster); // build templates per dataset from yml files const datasets = registryPackage.datasets; if (datasets) { - const templates = datasets.reduce<Array<Promise<AssetReference>>>((acc, dataset) => { + const installTemplatePromises = datasets.reduce<Array<Promise<TemplateRef>>>((acc, dataset) => { acc.push( installTemplateForDataset({ pkg: registryPackage, @@ -40,12 +36,13 @@ export const installTemplates = async ( ); return acc; }, []); - return Promise.all(templates).then(results => results.flat()); + + const res = await Promise.all(installTemplatePromises); + return res.flat(); } return []; }; -// this is temporary until we update the registry to use index templates v2 structure const installPreBuiltTemplates = async ( pkgName: string, pkgVersion: string, @@ -56,20 +53,91 @@ const installPreBuiltTemplates = async ( pkgVersion, (entry: Registry.ArchiveEntry) => isTemplate(entry) ); + // templatePaths.forEach(async path => { + // const { file } = Registry.pathParts(path); + // const templateName = file.substr(0, file.lastIndexOf('.')); + // const content = JSON.parse(Registry.getAsset(path).toString('utf8')); + // await callCluster('indices.putTemplate', { + // name: templateName, + // body: content, + // }); + // }); templatePaths.forEach(async path => { const { file } = Registry.pathParts(path); const templateName = file.substr(0, file.lastIndexOf('.')); const content = JSON.parse(Registry.getAsset(path).toString('utf8')); - await callCluster('indices.putTemplate', { - name: templateName, + let templateAPIPath = '_template'; + + // v2 index templates need to be installed through the new API endpoint. + // Checking for 'template' and 'composed_of' should catch them all. + // For the new v2 format, see https://github.com/elastic/elasticsearch/issues/53101 + if (content.hasOwnProperty('template') || content.hasOwnProperty('composed_of')) { + templateAPIPath = '_index_template'; + } + + const callClusterParams: { + method: string; + path: string; + ignore: number[]; + body: any; + } = { + method: 'PUT', + path: `/${templateAPIPath}/${templateName}`, + ignore: [404], body: content, - }); + }; + // This uses the catch-all endpoint 'transport.request' because there is no + // convenience endpoint using the new _index_template API yet. + // The existing convenience endpoint `indices.putTemplate` only sends to _template, + // which does not support v2 templates. + // See src/core/server/elasticsearch/api_types.ts for available endpoints. + await callCluster('transport.request', callClusterParams); + }); +}; + +const installPreBuiltComponentTemplates = async ( + pkgName: string, + pkgVersion: string, + callCluster: CallESAsCurrentUser +) => { + const templatePaths = await Registry.getArchiveInfo( + pkgName, + pkgVersion, + (entry: Registry.ArchiveEntry) => isComponentTemplate(entry) + ); + templatePaths.forEach(async path => { + const { file } = Registry.pathParts(path); + const templateName = file.substr(0, file.lastIndexOf('.')); + const content = JSON.parse(Registry.getAsset(path).toString('utf8')); + + const callClusterParams: { + method: string; + path: string; + ignore: number[]; + body: any; + } = { + method: 'PUT', + path: `/_component_template/${templateName}`, + ignore: [404], + body: content, + }; + // This uses the catch-all endpoint 'transport.request' because there is no + // convenience endpoint for component templates yet. + // See src/core/server/elasticsearch/api_types.ts for available endpoints. + await callCluster('transport.request', callClusterParams); }); }; + const isTemplate = ({ path }: Registry.ArchiveEntry) => { const pathParts = Registry.pathParts(path); return pathParts.type === ElasticsearchAssetType.indexTemplate; }; + +const isComponentTemplate = ({ path }: Registry.ArchiveEntry) => { + const pathParts = Registry.pathParts(path); + return pathParts.type === ElasticsearchAssetType.componentTemplate; +}; + /** * installTemplatesForDataset installs one template for each dataset * @@ -84,7 +152,7 @@ export async function installTemplateForDataset({ pkg: RegistryPackage; callCluster: CallESAsCurrentUser; dataset: Dataset; -}): Promise<AssetReference> { +}): Promise<TemplateRef> { const fields = await loadFieldsFromYaml(pkg, dataset.path); return installTemplate({ callCluster, @@ -104,7 +172,7 @@ export async function installTemplate({ fields: Field[]; dataset: Dataset; packageVersion: string; -}): Promise<AssetReference> { +}): Promise<TemplateRef> { const mappings = generateMappings(processFields(fields)); const templateName = generateTemplateName(dataset); let pipelineName; @@ -117,11 +185,26 @@ export async function installTemplate({ } const template = getTemplate(dataset.type, templateName, mappings, pipelineName); // TODO: Check return values for errors - await callCluster('indices.putTemplate', { - name: templateName, + const callClusterParams: { + method: string; + path: string; + ignore: number[]; + body: any; + } = { + method: 'PUT', + path: `/_index_template/${templateName}`, + ignore: [404], body: template, - }); + }; + // This uses the catch-all endpoint 'transport.request' because there is no + // convenience endpoint using the new _index_template API yet. + // The existing convenience endpoint `indices.putTemplate` only sends to _template, + // which does not support v2 templates. + // See src/core/server/elasticsearch/api_types.ts for available endpoints. + await callCluster('transport.request', callClusterParams); - // The id of a template is its name - return { id: templateName, type: IngestAssetType.IndexTemplate }; + return { + templateName, + indexTemplate: template, + }; } 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 1a73c9581a2de..cacf84381dd88 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 @@ -93,7 +93,7 @@ test('tests processing text field with multi fields', () => { const fields: Field[] = safeLoad(textWithMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(textWithMultiFieldsMapping)); + expect(mappings).toEqual(textWithMultiFieldsMapping); }); test('tests processing keyword field with multi fields', () => { @@ -127,7 +127,7 @@ test('tests processing keyword field with multi fields', () => { const fields: Field[] = safeLoad(keywordWithMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithMultiFieldsMapping)); + expect(mappings).toEqual(keywordWithMultiFieldsMapping); }); test('tests processing keyword field with multi fields with analyzed text field', () => { @@ -159,7 +159,7 @@ test('tests processing keyword field with multi fields with analyzed text field' const fields: Field[] = safeLoad(keywordWithAnalyzedMultiFieldsLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(keywordWithAnalyzedMultiFieldsMapping)); + expect(mappings).toEqual(keywordWithAnalyzedMultiFieldsMapping); }); test('tests processing object field with no other attributes', () => { @@ -177,7 +177,7 @@ test('tests processing object field with no other attributes', () => { const fields: Field[] = safeLoad(objectFieldLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldMapping)); + expect(mappings).toEqual(objectFieldMapping); }); test('tests processing object field with enabled set to false', () => { @@ -197,7 +197,7 @@ test('tests processing object field with enabled set to false', () => { const fields: Field[] = safeLoad(objectFieldEnabledFalseLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldEnabledFalseMapping)); + expect(mappings).toEqual(objectFieldEnabledFalseMapping); }); test('tests processing object field with dynamic set to false', () => { @@ -217,7 +217,7 @@ test('tests processing object field with dynamic set to false', () => { const fields: Field[] = safeLoad(objectFieldDynamicFalseLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicFalseMapping)); + expect(mappings).toEqual(objectFieldDynamicFalseMapping); }); test('tests processing object field with dynamic set to true', () => { @@ -237,7 +237,7 @@ test('tests processing object field with dynamic set to true', () => { const fields: Field[] = safeLoad(objectFieldDynamicTrueLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicTrueMapping)); + expect(mappings).toEqual(objectFieldDynamicTrueMapping); }); test('tests processing object field with dynamic set to strict', () => { @@ -257,5 +257,159 @@ test('tests processing object field with dynamic set to strict', () => { const fields: Field[] = safeLoad(objectFieldDynamicStrictLiteralYml); const processedFields = processFields(fields); const mappings = generateMappings(processedFields); - expect(JSON.stringify(mappings)).toEqual(JSON.stringify(objectFieldDynamicStrictMapping)); + expect(mappings).toEqual(objectFieldDynamicStrictMapping); +}); + +test('tests processing object field with property', () => { + const objectFieldWithPropertyLiteralYml = ` +- name: a + type: object +- name: a.b + type: keyword + `; + const objectFieldWithPropertyMapping = { + properties: { + a: { + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyMapping); +}); + +test('tests processing object field with property, reverse order', () => { + const objectFieldWithPropertyReversedLiteralYml = ` +- name: a.b + type: keyword +- name: a + type: object + dynamic: false + `; + const objectFieldWithPropertyReversedMapping = { + properties: { + a: { + dynamic: false, + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(objectFieldWithPropertyReversedLiteralYml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(objectFieldWithPropertyReversedMapping); +}); + +test('tests processing nested field with property', () => { + const nestedYaml = ` + - name: a.b + type: keyword + - name: a + type: nested + dynamic: false + `; + const expectedMapping = { + properties: { + a: { + dynamic: false, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +test('tests processing nested field with property, nested field first', () => { + const nestedYaml = ` + - name: a + type: nested + include_in_parent: true + - name: a.b + type: keyword + `; + const expectedMapping = { + properties: { + a: { + include_in_parent: true, + type: 'nested', + properties: { + b: { + ignore_above: 1024, + type: 'keyword', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +test('tests processing nested leaf field with properties', () => { + const nestedYaml = ` + - name: a + type: object + dynamic: false + - name: a.b + type: nested + enabled: false + `; + const expectedMapping = { + properties: { + a: { + dynamic: false, + properties: { + b: { + enabled: false, + type: 'nested', + }, + }, + }, + }, + }; + const fields: Field[] = safeLoad(nestedYaml); + const processedFields = processFields(fields); + const mappings = generateMappings(processedFields); + expect(mappings).toEqual(expectedMapping); +}); + +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/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 22a61d2bdfb7c..c45c7e706be58 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -5,24 +5,30 @@ */ import { Field, Fields } from '../../fields/field'; -import { Dataset, IndexTemplate } from '../../../../types'; +import { + Dataset, + CallESAsCurrentUser, + TemplateRef, + IndexTemplate, + IndexTemplateMappings, +} from '../../../../types'; import { getDatasetAssetBaseName } from '../index'; interface Properties { [key: string]: any; } -interface Mappings { - properties: any; -} - -interface Mapping { - [key: string]: any; -} interface MultiFields { [key: string]: object; } +export interface IndexTemplateMapping { + [key: string]: any; +} +export interface CurrentIndex { + indexName: string; + indexTemplate: IndexTemplate; +} const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; @@ -34,12 +40,12 @@ const DEFAULT_IGNORE_ABOVE = 1024; export function getTemplate( type: string, templateName: string, - mappings: Mappings, + mappings: IndexTemplateMappings, pipelineName?: string | undefined ): IndexTemplate { const template = getBaseTemplate(type, templateName, mappings); if (pipelineName) { - template.settings.index.default_pipeline = pipelineName; + template.template.settings.index.default_pipeline = pipelineName; } return template; } @@ -52,7 +58,7 @@ export function getTemplate( * * @param fields */ -export function generateMappings(fields: Field[]): Mappings { +export function generateMappings(fields: Field[]): IndexTemplateMappings { const props: Properties = {}; // TODO: this can happen when the fields property in fields.yml is present but empty // Maybe validation should be moved to fields/field.ts @@ -65,7 +71,14 @@ export function generateMappings(fields: Field[]): Mappings { switch (type) { case 'group': - fieldProps = generateMappings(field.fields!); + fieldProps = { ...generateMappings(field.fields!), ...generateDynamicAndEnabled(field) }; + break; + case 'group-nested': + fieldProps = { + ...generateMappings(field.fields!), + ...generateNestedProps(field), + type: 'nested', + }; break; case 'integer': fieldProps.type = 'long'; @@ -89,13 +102,10 @@ export function generateMappings(fields: Field[]): Mappings { } break; case 'object': - fieldProps.type = 'object'; - if (field.hasOwnProperty('enabled')) { - fieldProps.enabled = field.enabled; - } - if (field.hasOwnProperty('dynamic')) { - fieldProps.dynamic = field.dynamic; - } + fieldProps = { ...fieldProps, ...generateDynamicAndEnabled(field), type: 'object' }; + break; + case 'nested': + fieldProps = { ...fieldProps, ...generateNestedProps(field), type: 'nested' }; break; case 'array': // this assumes array fields were validated in an earlier step @@ -122,6 +132,29 @@ export function generateMappings(fields: Field[]): Mappings { return { properties: props }; } +function generateDynamicAndEnabled(field: Field) { + const props: Properties = {}; + if (field.hasOwnProperty('enabled')) { + props.enabled = field.enabled; + } + if (field.hasOwnProperty('dynamic')) { + props.dynamic = field.dynamic; + } + return props; +} + +function generateNestedProps(field: Field) { + const props = generateDynamicAndEnabled(field); + + if (field.hasOwnProperty('include_in_parent')) { + props.include_in_parent = field.include_in_parent; + } + if (field.hasOwnProperty('include_in_root')) { + props.include_in_root = field.include_in_root; + } + return props; +} + function generateMultiFields(fields: Fields): MultiFields { const multiFields: MultiFields = {}; if (fields) { @@ -140,8 +173,8 @@ function generateMultiFields(fields: Fields): MultiFields { return multiFields; } -function generateKeywordMapping(field: Field): Mapping { - const mapping: Mapping = { +function generateKeywordMapping(field: Field): IndexTemplateMapping { + const mapping: IndexTemplateMapping = { ignore_above: DEFAULT_IGNORE_ABOVE, }; if (field.ignore_above) { @@ -150,8 +183,8 @@ function generateKeywordMapping(field: Field): Mapping { return mapping; } -function generateTextMapping(field: Field): Mapping { - const mapping: Mapping = {}; +function generateTextMapping(field: Field): IndexTemplateMapping { + const mapping: IndexTemplateMapping = {}; if (field.analyzer) { mapping.analyzer = field.analyzer; } @@ -200,64 +233,175 @@ export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record return patterns; } -function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate { +function getBaseTemplate( + type: string, + templateName: string, + mappings: IndexTemplateMappings +): IndexTemplate { return { - // We need to decide which order we use for the templates - order: 1, + // This takes precedence over all index templates installed with the 'base' package + priority: 1, // To be completed with the correct index patterns index_patterns: [`${templateName}-*`], - settings: { - index: { - // ILM Policy must be added here, for now point to the default global ILM policy name - lifecycle: { - name: `${type}-default`, - }, - // What should be our default for the compression? - codec: 'best_compression', - // W - mapping: { - total_fields: { - limit: '10000', + template: { + settings: { + index: { + // ILM Policy must be added here, for now point to the default global ILM policy name + lifecycle: { + name: `${type}-default`, }, + // What should be our default for the compression? + codec: 'best_compression', + // W + mapping: { + total_fields: { + limit: '10000', + }, + }, + // This is the default from Beats? So far seems to be a good value + refresh_interval: '5s', + // Default in the stack now, still good to have it in + number_of_shards: '1', + // All the default fields which should be queried have to be added here. + // So far we add all keyword and text fields here. + query: { + default_field: ['message'], + }, + // We are setting 30 because it can be devided by several numbers. Useful when shrinking. + number_of_routing_shards: '30', }, - // This is the default from Beats? So far seems to be a good value - refresh_interval: '5s', - // Default in the stack now, still good to have it in - number_of_shards: '1', - // All the default fields which should be queried have to be added here. - // So far we add all keyword and text fields here. - query: { - default_field: ['message'], - }, - // We are setting 30 because it can be devided by several numbers. Useful when shrinking. - number_of_routing_shards: '30', - }, - }, - mappings: { - // To be filled with interesting information about this specific index - _meta: { - package: 'foo', }, - // All the dynamic field mappings - dynamic_templates: [ - // This makes sure all mappings are keywords by default - { - strings_as_keyword: { - mapping: { - ignore_above: 1024, - type: 'keyword', + mappings: { + // All the dynamic field mappings + dynamic_templates: [ + // This makes sure all mappings are keywords by default + { + strings_as_keyword: { + mapping: { + ignore_above: 1024, + type: 'keyword', + }, + match_mapping_type: 'string', }, - match_mapping_type: 'string', }, - }, - ], - // As we define fields ahead, we don't need any automatic field detection - // This makes sure all the fields are mapped to keyword by default to prevent mapping conflicts - date_detection: false, - // All the properties we know from the fields.yml file - properties: mappings.properties, + ], + // As we define fields ahead, we don't need any automatic field detection + // This makes sure all the fields are mapped to keyword by default to prevent mapping conflicts + date_detection: false, + // All the properties we know from the fields.yml file + properties: mappings.properties, + }, + // To be filled with the aliases that we need + aliases: {}, }, - // To be filled with the aliases that we need - aliases: {}, }; } + +export const updateCurrentWriteIndices = async ( + callCluster: CallESAsCurrentUser, + templates: TemplateRef[] +): Promise<void> => { + if (!templates) return; + + const allIndices = await queryIndicesFromTemplates(callCluster, templates); + return updateAllIndices(allIndices, callCluster); +}; + +const queryIndicesFromTemplates = async ( + callCluster: CallESAsCurrentUser, + templates: TemplateRef[] +): Promise<CurrentIndex[]> => { + const indexPromises = templates.map(template => { + return getIndices(callCluster, template); + }); + const indexObjects = await Promise.all(indexPromises); + return indexObjects.filter(item => item !== undefined).flat(); +}; + +const getIndices = async ( + callCluster: CallESAsCurrentUser, + template: TemplateRef +): Promise<CurrentIndex[] | undefined> => { + const { templateName, indexTemplate } = template; + const res = await callCluster('search', getIndexQuery(templateName)); + const indices: any[] = res?.aggregations?.index.buckets; + if (indices) { + return indices.map(index => ({ + indexName: index.key, + indexTemplate, + })); + } +}; + +const updateAllIndices = async ( + indexNameWithTemplates: CurrentIndex[], + callCluster: CallESAsCurrentUser +): Promise<void> => { + const updateIndexPromises = indexNameWithTemplates.map(({ indexName, indexTemplate }) => { + return updateExistingIndex({ indexName, callCluster, indexTemplate }); + }); + await Promise.all(updateIndexPromises); +}; +const updateExistingIndex = async ({ + indexName, + callCluster, + indexTemplate, +}: { + indexName: string; + callCluster: CallESAsCurrentUser; + indexTemplate: IndexTemplate; +}) => { + const { settings, mappings } = indexTemplate.template; + // try to update the mappings first + // for now we assume updates are compatible + try { + await callCluster('indices.putMapping', { + index: indexName, + body: mappings, + }); + } catch (err) { + throw new Error('incompatible mappings update'); + } + // update settings after mappings was successful to ensure + // pointing to theme new pipeline is safe + // for now, only update the pipeline + if (!settings.index.default_pipeline) return; + try { + await callCluster('indices.putSettings', { + index: indexName, + body: { index: { default_pipeline: settings.index.default_pipeline } }, + }); + } catch (err) { + throw new Error('incompatible settings update'); + } +}; + +const getIndexQuery = (templateName: string) => ({ + index: `${templateName}-*`, + size: 0, + body: { + query: { + bool: { + must: [ + { + exists: { + field: 'stream.namespace', + }, + }, + { + exists: { + field: 'stream.dataset', + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts index e3aef6077dbc3..f0ff4c6125452 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts @@ -179,4 +179,197 @@ describe('processFields', () => { JSON.stringify(mixedFieldsExpanded) ); }); + + const objectFieldWithProperty = [ + { + name: 'a', + type: 'object', + dynamic: true, + }, + { + name: 'a.b', + type: 'keyword', + }, + ]; + + const objectFieldWithPropertyExpanded = [ + { + name: 'a', + type: 'group', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + test('correctly handles properties of object type fields', () => { + expect(JSON.stringify(processFields(objectFieldWithProperty))).toEqual( + JSON.stringify(objectFieldWithPropertyExpanded) + ); + }); + + test('correctly handles properties of object type fields where object comes second', () => { + const nested = [ + { + name: 'a.b', + type: 'keyword', + }, + { + name: 'a', + type: 'object', + dynamic: true, + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('correctly handles properties of nested type fields', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a.b', + type: 'keyword', + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group-nested', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('correctly handles properties of nested type where nested top level comes second', () => { + const nested = [ + { + name: 'a.b', + type: 'keyword', + }, + { + name: 'a', + type: 'nested', + dynamic: true, + }, + ]; + + const nestedExpanded = [ + { + name: 'a', + type: 'group-nested', + dynamic: true, + fields: [ + { + name: 'b', + type: 'keyword', + }, + ], + }, + ]; + expect(processFields(nested)).toEqual(nestedExpanded); + }); + + test('ignores redefinitions of an object field', () => { + const object = [ + { + name: 'a', + type: 'object', + dynamic: true, + }, + { + name: 'a', + type: 'object', + dynamic: false, + }, + ]; + + const objectExpected = [ + { + name: 'a', + type: 'object', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(object)).toEqual(objectExpected); + }); + + test('ignores redefinitions of a nested field', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a', + type: 'nested', + dynamic: false, + }, + ]; + + const nestedExpected = [ + { + name: 'a', + type: 'nested', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(nested)).toEqual(nestedExpected); + }); + + test('ignores redefinitions of a nested and object field', () => { + const nested = [ + { + name: 'a', + type: 'nested', + dynamic: true, + }, + { + name: 'a', + type: 'object', + dynamic: false, + }, + ]; + + const nestedExpected = [ + { + name: 'a', + type: 'nested', + // should preserve the field that was parsed first which had dynamic: true + dynamic: true, + }, + ]; + expect(processFields(nested)).toEqual(nestedExpected); + }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index 9c9843e0454ab..abaf7ab5b0dfc 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -28,6 +28,8 @@ export interface Field { object_type?: string; scaling_factor?: number; dynamic?: 'strict' | boolean; + include_in_parent?: boolean; + include_in_root?: boolean; // Kibana specific analyzed?: boolean; @@ -108,10 +110,54 @@ function dedupFields(fields: Fields): Fields { return f.name === field.name; }); if (found) { - if (found.type === 'group' && field.type === 'group' && found.fields && field.fields) { - found.fields = dedupFields(found.fields.concat(field.fields)); + // remove name, type, and fields from `field` variable so we avoid merging them into `found` + const { name, type, fields: nestedFields, ...importantFieldProps } = field; + /** + * There are a couple scenarios this if is trying to account for: + * Example 1 + * - name: a.b + * - name: a + * In this scenario found will be `group` and field could be either `object` or `nested` + * Example 2 + * - name: a + * - name: a.b + * In this scenario found could be `object` or `nested` and field will be group + */ + if ( + // only merge if found is a group and field is object, nested, or group. + // Or if found is object, or nested, and field is a group. + // This is to avoid merging two objects, or nested, or object with a nested. + (found.type === 'group' && + (field.type === 'object' || field.type === 'nested' || field.type === 'group')) || + ((found.type === 'object' || found.type === 'nested') && field.type === 'group') + ) { + // if the new field has properties let's dedup and concat them with the already existing found variable in + // the array + if (field.fields) { + // if the found type was object or nested it won't have a fields array so let's initialize it + if (!found.fields) { + found.fields = []; + } + found.fields = dedupFields(found.fields.concat(field.fields)); + } + + // if found already had fields or got new ones from the new field coming in we need to assign the right + // type to it + if (found.fields) { + // If this field is supposed to be `nested` and we have fields, we need to preserve the fact that it is + // supposed to be `nested` for when the template is actually generated + if (found.type === 'nested' || field.type === 'nested') { + found.type = 'group-nested'; + } else { + // found was either `group` already or `object` so just set it to `group` + found.type = 'group'; + } + } + // we need to merge in other properties (like `dynamic`) that might exist + Object.assign(found, importantFieldProps); + // if `field.type` wasn't group object or nested, then there's a conflict in types, so lets ignore it } else { - // only 'group' fields can be merged in this way + // only `group`, `object`, and `nested` fields can be merged in this way // XXX: don't abort on error for now // see discussion in https://github.com/elastic/kibana/pull/59894 // throw new Error( 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 bc1694348b4c2..f1660fbc01591 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 05e64c6565dc6..ec657820a2225 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 { 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 d76584225877c..da8d79a04b97c 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<PackageInfo> { 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/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 0a7642752b3e9..632bc3ac9b69f 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 @@ -5,6 +5,7 @@ */ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; +import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, @@ -12,15 +13,19 @@ import { KibanaAssetType, CallESAsCurrentUser, DefaultPackages, + ElasticsearchAssetType, + IngestAssetType, } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getObject } from './get_objects'; -import { getInstallation } from './index'; +import { getInstallation, getInstallationObject } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; +import { deleteAssetsByType, deleteKibanaSavedObjectsAssets } from './remove'; +import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -89,44 +94,91 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); + + // see if some version of this package is already installed + // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge + // and be replaced by getPackageInfo after adjusting for it to not group/use archive assets + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); - const { internal = false } = registryPackageInfo; + const latestPackage = await Registry.fetchFindLatestPackage(pkgName); - const installKibanaAssetsPromise = installKibanaAssets({ - savedObjectsClient, - pkgName, - pkgVersion, - }); - const installPipelinePromises = installPipelines(registryPackageInfo, callCluster); - const installTemplatePromises = installTemplates( + if (pkgVersion < latestPackage.version) + throw Boom.badRequest('Cannot install or update to an out-of-date package'); + + const reinstall = pkgVersion === installedPkg?.attributes.version; + const { internal = false, removable = true } = registryPackageInfo; + + // delete the previous version's installation's SO kibana assets before installing new ones + // in case some assets were removed in the new version + if (installedPkg) { + try { + await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); + } catch (err) { + // log these errors, some assets may not exist if deleted during a failed update + } + } + + const [installedKibanaAssets, installedPipelines] = await Promise.all([ + installKibanaAssets({ + savedObjectsClient, + pkgName, + pkgVersion, + }), + installPipelines(registryPackageInfo, callCluster), + // index patterns and ilm policies are not currently associated with a particular package + // so we do not save them in the package saved object state. + installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), + // currenly only the base package has an ILM policy + // at some point ILM policies can be installed/modified + // per dataset and we should then save them + installILMPolicy(pkgName, pkgVersion, callCluster), + ]); + + // install or update the templates + const installedTemplates = await installTemplates( registryPackageInfo, callCluster, pkgName, pkgVersion ); + const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // index patterns and ilm policies are not currently associated with a particular package - // so we do not save them in the package saved object state. at some point ILM policies can be installed/modified - // per dataset and we should then save them - await installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); - // currenly only the base package has an ILM policy - await installILMPolicy(pkgName, pkgVersion, callCluster); - - const res = await Promise.all([ - installKibanaAssetsPromise, - installPipelinePromises, - installTemplatePromises, - ]); + // get template refs to save + const installedTemplateRefs = installedTemplates.map(template => ({ + id: template.templateName, + type: IngestAssetType.IndexTemplate, + })); - const toSaveAssetRefs: AssetReference[] = res.flat(); - const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // Save those references in the package manager's state saved object - return await saveInstallationReferences({ + if (installedPkg) { + // update current index for every index template created + await updateCurrentWriteIndices(callCluster, installedTemplates); + if (!reinstall) { + try { + // delete the previous version's installation's pipelines + // this must happen after the template is updated + await deleteAssetsByType({ + savedObjectsClient, + callCluster, + installedObjects: installedPkg.attributes.installed, + assetType: ElasticsearchAssetType.ingestPipeline, + }); + } catch (err) { + throw new Error(err.message); + } + } + } + const toSaveAssetRefs: AssetReference[] = [ + ...installedKibanaAssets, + ...installedPipelines, + ...installedTemplateRefs, + ]; + // Save references to installed assets in the package's saved object state + return saveInstallationReferences({ savedObjectsClient, - pkgkey, pkgName, pkgVersion, internal, + removable, toSaveAssetRefs, toSaveESIndexPatterns, }); @@ -154,10 +206,10 @@ export async function installKibanaAssets(options: { export async function saveInstallationReferences(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; pkgName: string; pkgVersion: string; internal: boolean; + removable: boolean; toSaveAssetRefs: AssetReference[]; toSaveESIndexPatterns: Record<string, string>; }) { @@ -166,36 +218,25 @@ export async function saveInstallationReferences(options: { pkgName, pkgVersion, internal, + removable, toSaveAssetRefs, toSaveESIndexPatterns, } = options; - const installation = await getInstallation({ savedObjectsClient, pkgName }); - const savedAssetRefs = installation?.installed || []; - const toInstallESIndexPatterns = Object.assign( - installation?.es_index_patterns || {}, - toSaveESIndexPatterns - ); - - const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => { - const hasRef = current.find(c => c.id === pending.id && c.type === pending.type); - if (!hasRef) current.push(pending); - return current; - }; - const toInstallAssetsRefs = toSaveAssetRefs.reduce(mergeRefsReducer, savedAssetRefs); await savedObjectsClient.create<Installation>( PACKAGES_SAVED_OBJECT_TYPE, { - installed: toInstallAssetsRefs, - es_index_patterns: toInstallESIndexPatterns, + installed: toSaveAssetRefs, + es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, internal, + removable, }, { id: pkgName, overwrite: true } ); - return toInstallAssetsRefs; + return toSaveAssetRefs; } async function installKibanaSavedObjects({ 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 a30acb97b99cf..0d9db1697d886 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 @@ -5,6 +5,7 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; +import Boom from 'boom'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types'; import { CallESAsCurrentUser } from '../../../types'; @@ -20,7 +21,10 @@ export async function removeInstallation(options: { // TODO: the epm api should change to /name/version so we don't need to do this const [pkgName] = pkgkey.split('-'); const installation = await getInstallation({ savedObjectsClient, pkgName }); - const installedObjects = installation?.installed || []; + if (!installation) throw Boom.badRequest(`${pkgName} is not installed`); + if (installation.removable === false) + throw Boom.badRequest(`${pkgName} is installed by default and cannot be removed`); + const installedObjects = installation.installed || []; // Delete the manager saved object with references to the asset objects // could also update with [] or some other state @@ -29,7 +33,17 @@ export async function removeInstallation(options: { // recreate or delete index patterns when a package is uninstalled await installIndexPatterns(savedObjectsClient); - // Delete the installed assets + // Delete the installed asset + await deleteAssets(installedObjects, savedObjectsClient, callCluster); + + // successful delete's in SO client return {}. return something more useful + return installedObjects; +} +async function deleteAssets( + installedObjects: AssetReference[], + savedObjectsClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser +) { const deletePromises = installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { @@ -40,22 +54,80 @@ export async function removeInstallation(options: { deleteTemplate(callCluster, id); } }); - await Promise.all([...deletePromises]); - - // successful delete's in SO client return {}. return something more useful - return installedObjects; + try { + await Promise.all([...deletePromises]); + } catch (err) { + throw new Error(err.message); + } } - async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise<void> { // '*' shouldn't ever appear here, but it still would delete all ingest pipelines if (id && id !== '*') { - await callCluster('ingest.deletePipeline', { id }); + try { + await callCluster('ingest.deletePipeline', { id }); + } catch (err) { + throw new Error(`error deleting pipeline ${id}`); + } } } async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): Promise<void> { // '*' shouldn't ever appear here, but it still would delete all templates if (name && name !== '*') { - await callCluster('indices.deleteTemplate', { name }); + try { + const callClusterParams: { + method: string; + path: string; + ignore: number[]; + } = { + method: 'DELETE', + path: `/_index_template/${name}`, + ignore: [404], + }; + // This uses the catch-all endpoint 'transport.request' because there is no + // convenience endpoint using the new _index_template API yet. + // The existing convenience endpoint `indices.putTemplate` only sends to _template, + // which does not support v2 templates. + // See src/core/server/elasticsearch/api_types.ts for available endpoints. + await callCluster('transport.request', callClusterParams); + } catch { + throw new Error(`error deleting template ${name}`); + } + } +} + +export async function deleteAssetsByType({ + savedObjectsClient, + callCluster, + installedObjects, + assetType, +}: { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + installedObjects: AssetReference[]; + assetType: ElasticsearchAssetType; +}) { + const toDelete = installedObjects.filter(asset => asset.type === assetType); + try { + await deleteAssets(toDelete, savedObjectsClient, callCluster); + } catch (err) { + throw new Error(err.message); + } +} + +export async function deleteKibanaSavedObjectsAssets( + savedObjectsClient: SavedObjectsClientContract, + installedObjects: AssetReference[] +) { + const deletePromises = installedObjects.map(({ id, type }) => { + const assetType = type as AssetType; + if (savedObjectTypes.includes(assetType)) { + return savedObjectsClient.delete(assetType, id); + } + }); + try { + await Promise.all(deletePromises); + } catch (err) { + throw new Error('error deleting saved object asset'); } } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index a96afc5eb7fa5..8e9b920875617 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -16,11 +16,11 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { appContextService } from '../../'; import { cacheGet, cacheSet } from './cache'; import { ArchiveEntry, untarBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; +import { getRegistryUrl } from './registry_url'; export { ArchiveEntry } from './extract'; @@ -32,7 +32,7 @@ export const pkgToPkgKey = ({ name, version }: { name: string; version: string } `${name}-${version}`; export async function fetchList(params?: SearchParams): Promise<RegistrySearchResults> { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/search`); if (params && params.category) { url.searchParams.set('category', params.category); @@ -45,7 +45,7 @@ export async function fetchFindLatestPackage( packageName: string, internal: boolean = true ): Promise<RegistrySearchResult> { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/search?package=${packageName}&internal=${internal}`); const res = await fetchUrl(url.toString()); const searchResults = JSON.parse(res); @@ -57,17 +57,17 @@ export async function fetchFindLatestPackage( } export async function fetchInfo(pkgName: string, pkgVersion: string): Promise<RegistryPackage> { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}`).then(JSON.parse); } export async function fetchFile(filePath: string): Promise<Response> { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return getResponse(`${registryUrl}${filePath}`); } export async function fetchCategories(): Promise<CategorySummaryList> { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return fetchUrl(`${registryUrl}/categories`).then(JSON.parse); } @@ -151,7 +151,7 @@ async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Pro async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise<Buffer> { const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return getResponseStream(`${registryUrl}${archivePath}`).then(streamToBuffer); } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts new file mode 100644 index 0000000000000..d92d6faf8472e --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DEFAULT_REGISTRY_URL } from '../../../constants'; +import { appContextService, licenseService } from '../../'; + +export const getRegistryUrl = (): string => { + const license = licenseService.getLicenseInformation(); + const customUrl = appContextService.getConfig()?.epm.registryUrl; + + if ( + customUrl && + license && + license.isAvailable && + license.hasAtLeast('gold') && + license.isActive + ) { + return customUrl; + } + + return DEFAULT_REGISTRY_URL; +}; diff --git a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts index 167e22873979c..fc2fe6d1c40e8 100644 --- a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts +++ b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts @@ -4,15 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract } from 'kibana/server'; -import { getInstallation } from './epm/packages/get'; - -export interface ESIndexPatternService { - getESIndexPattern( - savedObjectsClient: SavedObjectsClientContract, - pkgName: string, - datasetPath: string - ): Promise<string | undefined>; -} +import { getInstallation } from './epm/packages'; +import { ESIndexPatternService } from '../../server'; export class ESIndexPatternSavedObjectService implements ESIndexPatternService { public async getESIndexPattern( diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index d64f1b0c2b6fb..483661b9de915 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -3,10 +3,41 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { appContextService } from './app_context'; -export { ESIndexPatternService, ESIndexPatternSavedObjectService } from './es_index_pattern'; + +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentStatus } from '../../common/types/models'; +import * as settingsService from './settings'; +export { ESIndexPatternSavedObjectService } from './es_index_pattern'; + +/** + * Service to return the index pattern of EPM packages + */ +export interface ESIndexPatternService { + getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise<string | undefined>; +} + +/** + * A service that provides exported functions that return information about an Agent + */ +export interface AgentService { + /** + * Return the status by the Agent's id + * @param soClient + * @param agentId + */ + getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise<AgentStatus>; +} // Saved object services export { datasourceService } from './datasource'; export { agentConfigService } from './agent_config'; export { outputService } from './output'; +export { settingsService }; + +// Plugin services +export { appContextService } from './app_context'; +export { licenseService } from './license'; diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts index aebb8188db0cc..395c9af4a4ca2 100644 --- a/x-pack/plugins/ingest_manager/server/services/output.ts +++ b/x-pack/plugins/ingest_manager/server/services/output.ts @@ -14,7 +14,7 @@ class OutputService { public async ensureDefaultOutput(soClient: SavedObjectsClientContract) { const outputs = await soClient.find<Output>({ type: OUTPUT_SAVED_OBJECT_TYPE, - filter: 'outputs.attributes.is_default:true', + filter: `${OUTPUT_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (!outputs.saved_objects.length) { @@ -44,7 +44,7 @@ class OutputService { public async getDefaultOutputId(soClient: SavedObjectsClientContract) { const outputs = await soClient.find({ type: OUTPUT_SAVED_OBJECT_TYPE, - filter: 'outputs.attributes.is_default:true', + filter: `${OUTPUT_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (!outputs.saved_objects.length) { @@ -95,6 +95,34 @@ class OutputService { ...outputSO.attributes, }; } + + public async update(soClient: SavedObjectsClientContract, id: string, data: Partial<Output>) { + const outputSO = await soClient.update<Output>(SAVED_OBJECT_TYPE, id, data); + + if (outputSO.error) { + throw new Error(outputSO.error.message); + } + } + + public async list(soClient: SavedObjectsClientContract) { + const outputs = await soClient.find<Output>({ + type: SAVED_OBJECT_TYPE, + page: 1, + perPage: 1000, + }); + + return { + items: outputs.saved_objects.map<Output>(outputSO => { + return { + id: outputSO.id, + ...outputSO.attributes, + }; + }), + total: outputs.total, + page: 1, + perPage: 1000, + }; + } } export const outputService = new OutputService(); diff --git a/x-pack/plugins/ingest_manager/server/services/settings.ts b/x-pack/plugins/ingest_manager/server/services/settings.ts new file mode 100644 index 0000000000000..f1c09746d9abd --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/settings.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Boom from 'boom'; +import { SavedObjectsClientContract } from 'kibana/server'; +import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, SettingsSOAttributes, Settings } from '../../common'; + +export async function getSettings(soClient: SavedObjectsClientContract): Promise<Settings> { + const res = await soClient.find<SettingsSOAttributes>({ + type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + }); + + if (res.total === 0) { + throw Boom.notFound('Global settings not found'); + } + const settingsSo = res.saved_objects[0]; + return { + id: settingsSo.id, + ...settingsSo.attributes, + }; +} + +export async function saveSettings( + soClient: SavedObjectsClientContract, + newData: Partial<Omit<Settings, 'id'>> +): Promise<Settings> { + try { + const settings = await getSettings(soClient); + + const res = await soClient.update<SettingsSOAttributes>( + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + settings.id, + newData + ); + + return { + id: settings.id, + ...res.attributes, + }; + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + const res = await soClient.create<SettingsSOAttributes>( + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + newData + ); + + return { + id: res.id, + ...res.attributes, + }; + } + + throw e; + } +} diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 167a24481aba5..390e240841611 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -21,6 +21,8 @@ import { import { getPackageInfo } from './epm/packages'; import { datasourceService } from './datasource'; import { generateEnrollmentAPIKey } from './api_keys'; +import { settingsService } from '.'; +import { appContextService } from './app_context'; const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; @@ -34,6 +36,17 @@ export async function setupIngestManager( ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), agentConfigService.ensureDefaultAgentConfig(soClient), + settingsService.getSettings(soClient).catch((e: any) => { + if (e.isBoom && e.output.statusCode === 404) { + return settingsService.saveSettings(soClient, { + agent_auto_upgrade: true, + package_auto_upgrade: true, + kibana_url: appContextService.getConfig()?.fleet?.kibana?.host, + }); + } + + return Promise.reject(e); + }), ]); // ensure default packages are added to the default conifg @@ -132,7 +145,7 @@ async function addPackageToConfig( config.namespace ); newDatasource.inputs = await datasourceService.assignPackageStream( - { pkgName: packageToInstall.name, pkgVersion: packageToInstall.version }, + packageInfo, newDatasource.inputs ); diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 1cd5622c0c7b0..27ed1de849987 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, @@ -22,6 +23,7 @@ export { AgentConfig, NewAgentConfig, AgentConfigStatus, + DataStream, Output, NewOutput, OutputType, @@ -47,6 +49,10 @@ export { RegistrySearchResults, RegistrySearchResult, DefaultPackages, + TemplateRef, + IndexTemplateMappings, + Settings, + SettingsSOAttributes, } from '../../common'; export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser']; 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 040b2eb16289a..59cadf3bd7f74 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({ diff --git a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts index c0cfee8f231c9..e71016560f60c 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/datasource.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/datasource.ts @@ -6,6 +6,14 @@ import { schema } from '@kbn/config-schema'; export { Datasource, NewDatasource } from '../../../common'; +const ConfigRecordSchema = schema.recordOf( + schema.string(), + schema.object({ + type: schema.maybe(schema.string()), + value: schema.maybe(schema.any()), + }) +); + const DatasourceBaseSchema = { name: schema.string(), description: schema.maybe(schema.string()), @@ -25,6 +33,7 @@ const DatasourceBaseSchema = { type: schema.string(), enabled: schema.boolean(), processors: schema.maybe(schema.arrayOf(schema.string())), + vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( schema.string(), @@ -40,6 +49,7 @@ const DatasourceBaseSchema = { enabled: schema.boolean(), dataset: schema.string(), processors: schema.maybe(schema.arrayOf(schema.string())), + vars: schema.maybe(ConfigRecordSchema), config: schema.maybe( schema.recordOf( schema.string(), diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index f94c02ccee40b..ac1679101312e 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -62,14 +62,18 @@ export const PostNewAgentActionRequestSchema = { }; export const PostAgentUnenrollRequestSchema = { - body: schema.oneOf([ - schema.object({ - kuery: schema.string(), - }), - schema.object({ - ids: schema.arrayOf(schema.string()), - }), - ]), + params: schema.object({ + agentId: schema.string(), + }), +}; + +export const PutAgentReassignRequestSchema = { + params: schema.object({ + agentId: schema.string(), + }), + body: schema.object({ + config_id: schema.string(), + }), }; export const GetOneAgentEventsRequestSchema = { diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts index 0d223f028fc88..ab97ddc0ba723 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent_config.ts @@ -29,9 +29,9 @@ export const UpdateAgentConfigRequestSchema = { body: NewAgentConfigSchema, }; -export const DeleteAgentConfigsRequestSchema = { +export const DeleteAgentConfigRequestSchema = { body: schema.object({ - agentConfigIds: schema.arrayOf(schema.string()), + agentConfigId: schema.string(), }), }; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts deleted file mode 100644 index 2244bcd44043f..0000000000000 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts +++ /dev/null @@ -1,13 +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. - */ - -export const GetFleetSetupRequestSchema = {}; - -export const CreateFleetSetupRequestSchema = {}; - -export interface CreateFleetSetupResponse { - isInitialized: boolean; -} diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts index c143cd3b35f91..6976dae38d5f1 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts @@ -9,5 +9,6 @@ export * from './agent'; export * from './datasource'; export * from './epm'; export * from './enrollment_api_key'; -export * from './fleet_setup'; export * from './install_script'; +export * from './output'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/output.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/output.ts new file mode 100644 index 0000000000000..79a7c444dacdb --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/output.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +export const GetOneOutputRequestSchema = { + params: schema.object({ + outputId: schema.string(), + }), +}; + +export const GetOutputsRequestSchema = {}; + +export const PutOutputRequestSchema = { + params: schema.object({ + outputId: schema.string(), + }), + body: schema.object({ + hosts: schema.maybe(schema.arrayOf(schema.string())), + ca_sha256: schema.maybe(schema.string()), + }), +}; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts new file mode 100644 index 0000000000000..8b7500e4a9bd9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.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. + */ +import { schema } from '@kbn/config-schema'; + +export const GetSettingsRequestSchema = {}; + +export const PutSettingsRequestSchema = { + body: schema.object({ + agent_auto_upgrade: schema.maybe(schema.boolean()), + package_auto_upgrade: schema.maybe(schema.boolean()), + kibana_url: schema.maybe(schema.string()), + kibana_ca_sha256: schema.maybe(schema.string()), + }), +}; diff --git a/x-pack/plugins/ingest_pipelines/README.md b/x-pack/plugins/ingest_pipelines/README.md new file mode 100644 index 0000000000000..a469511bdbbd2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/README.md @@ -0,0 +1,24 @@ +# Ingest Node Pipelines UI + +## Summary +The `ingest_pipelines` plugin provides Kibana support for [Elasticsearch's ingest nodes](https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html). Please refer to the Elasticsearch documentation for more details. + +This plugin allows Kibana to create, edit, clone and delete ingest node pipelines. It also provides support to simulate a pipeline. + +It requires a Basic license and the following cluster privileges: `manage_pipeline` and `cluster:monitor/nodes/info`. + +--- + +## Development + +A new app called Ingest Node Pipelines is registered in the Management section and follows a typical CRUD UI pattern. The client-side portion of this app lives in [public/application](public/application) and uses endpoints registered in [server/routes/api](server/routes/api). + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions on setting up your development environment. + +### Test coverage + +The app has the following test coverage: + +- Complete API integration tests +- Smoke-level functional test +- Client-integration tests diff --git a/x-pack/plugins/ingest_pipelines/common/constants.ts b/x-pack/plugins/ingest_pipelines/common/constants.ts new file mode 100644 index 0000000000000..edf681c276a84 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseType } from '../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN_ID = 'ingest_pipelines'; + +export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; + +export const BASE_PATH = '/management/elasticsearch/ingest_pipelines'; + +export const API_BASE_PATH = '/api/ingest_pipelines'; + +export const APP_CLUSTER_REQUIRED_PRIVILEGES = ['manage_pipeline', 'cluster:monitor/nodes/info']; diff --git a/x-pack/plugins/ingest_pipelines/common/lib/index.ts b/x-pack/plugins/ingest_pipelines/common/lib/index.ts new file mode 100644 index 0000000000000..a976f66bc7c40 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { deserializePipelines } from './pipeline_serialization'; diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts new file mode 100644 index 0000000000000..65d6b6e30497f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { deserializePipelines } from './pipeline_serialization'; + +describe('pipeline_serialization', () => { + describe('deserializePipelines()', () => { + it('should deserialize pipelines', () => { + expect( + deserializePipelines({ + pipeline1: { + description: 'pipeline 1 description', + version: 1, + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + on_failure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], + }, + pipeline2: { + description: 'pipeline2 description', + version: 1, + processors: [], + }, + }) + ).toEqual([ + { + name: 'pipeline1', + description: 'pipeline 1 description', + version: 1, + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + on_failure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], + }, + { + name: 'pipeline2', + description: 'pipeline2 description', + version: 1, + processors: [], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts new file mode 100644 index 0000000000000..572f655076015 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/lib/pipeline_serialization.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PipelinesByName, Pipeline } from '../types'; + +export function deserializePipelines(pipelinesByName: PipelinesByName): Pipeline[] { + const pipelineNames: string[] = Object.keys(pipelinesByName); + + const deserializedPipelines = pipelineNames.map((name: string) => { + return { + ...pipelinesByName[name], + name, + }; + }); + + return deserializedPipelines; +} diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts new file mode 100644 index 0000000000000..8d77359a7c3c5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -0,0 +1,28 @@ +/* + * 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. + */ + +interface Processor { + [key: string]: { + [key: string]: unknown; + }; +} + +export interface Pipeline { + name: string; + description: string; + version?: number; + processors: Processor[]; + on_failure?: Processor[]; +} + +export interface PipelinesByName { + [key: string]: { + description: string; + version?: number; + processors: Processor[]; + on_failure?: Processor[]; + }; +} diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json new file mode 100644 index 0000000000000..ec02c5f80edf9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "ingestPipelines", + "version": "8.0.0", + "server": true, + "ui": true, + "requiredPlugins": [ + "licensing", + "management" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": [ + "xpack", + "ingest_pipelines" + ] +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx new file mode 100644 index 0000000000000..ba7675b507596 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/app.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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPageContent } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { HashRouter, Switch, Route } from 'react-router-dom'; + +import { BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../common/constants'; + +import { + SectionError, + useAuthorizationContext, + WithPrivileges, + SectionLoading, + NotAuthorizedSection, +} from '../shared_imports'; + +import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; + +export const AppWithoutRouter = () => ( + <Switch> + <Route exact path={BASE_PATH} component={PipelinesList} /> + <Route exact path={`${BASE_PATH}/create/:sourceName`} component={PipelinesClone} /> + <Route exact path={`${BASE_PATH}/create`} component={PipelinesCreate} /> + <Route exact path={`${BASE_PATH}/edit/:name`} component={PipelinesEdit} /> + {/* Catch all */} + <Route component={PipelinesList} /> + </Switch> +); + +export const App: FunctionComponent = () => { + const { apiError } = useAuthorizationContext(); + + if (apiError) { + return ( + <SectionError + title={ + <FormattedMessage + id="xpack.ingestPipelines.app.checkingPrivilegesErrorMessage" + defaultMessage="Error fetching user privileges from the server." + /> + } + error={apiError} + /> + ); + } + + return ( + <WithPrivileges + privileges={APP_CLUSTER_REQUIRED_PRIVILEGES.map(privilege => `cluster.${privilege}`)} + > + {({ isLoading, hasPrivileges, privilegesMissing }) => { + if (isLoading) { + return ( + <SectionLoading> + <FormattedMessage + id="xpack.ingestPipelines.app.checkingPrivilegesDescription" + defaultMessage="Checking privileges…" + /> + </SectionLoading> + ); + } + + if (!hasPrivileges) { + return ( + <EuiPageContent> + <NotAuthorizedSection + title={ + <FormattedMessage + id="xpack.ingestPipelines.app.deniedPrivilegeTitle" + defaultMessage="You're missing cluster privileges" + /> + } + message={ + <FormattedMessage + id="xpack.ingestPipelines.app.deniedPrivilegeDescription" + defaultMessage="To use Ingest Pipelines, you must have {privilegesCount, + plural, one {this cluster privilege} other {these cluster privileges}}: {missingPrivileges}." + values={{ + missingPrivileges: privilegesMissing.cluster!.join(', '), + privilegesCount: privilegesMissing.cluster!.length, + }} + /> + } + /> + </EuiPageContent> + ); + } + + return ( + <HashRouter> + <AppWithoutRouter /> + </HashRouter> + ); + }} + </WithPrivileges> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts new file mode 100644 index 0000000000000..21a2ee30a84e1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelineForm } from './pipeline_form'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts new file mode 100644 index 0000000000000..2b007a25667a1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelineFormProvider as PipelineForm } from './pipeline_form_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx new file mode 100644 index 0000000000000..9082196a48b39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -0,0 +1,166 @@ +/* + * 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 } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +import { useForm, Form, FormConfig } from '../../../shared_imports'; +import { Pipeline } from '../../../../common/types'; + +import { PipelineRequestFlyout } from './pipeline_request_flyout'; +import { PipelineTestFlyout } from './pipeline_test_flyout'; +import { PipelineFormFields } from './pipeline_form_fields'; +import { PipelineFormError } from './pipeline_form_error'; +import { pipelineFormSchema } from './schema'; + +export interface PipelineFormProps { + onSave: (pipeline: Pipeline) => void; + onCancel: () => void; + isSaving: boolean; + saveError: any; + defaultValue?: Pipeline; + isEditing?: boolean; +} + +export const PipelineForm: React.FunctionComponent<PipelineFormProps> = ({ + defaultValue = { + name: '', + description: '', + processors: '', + on_failure: '', + version: '', + }, + onSave, + isSaving, + saveError, + isEditing, + onCancel, +}) => { + const [isRequestVisible, setIsRequestVisible] = useState<boolean>(false); + + const [isTestingPipeline, setIsTestingPipeline] = useState<boolean>(false); + + const handleSave: FormConfig['onSubmit'] = (formData, isValid) => { + if (isValid) { + onSave(formData as Pipeline); + } + }; + + const handleTestPipelineClick = () => { + setIsTestingPipeline(true); + }; + + const { form } = useForm({ + schema: pipelineFormSchema, + defaultValue, + onSubmit: handleSave, + }); + + const saveButtonLabel = isSaving ? ( + <FormattedMessage + id="xpack.ingestPipelines.form.savingButtonLabel" + defaultMessage="Saving..." + /> + ) : isEditing ? ( + <FormattedMessage + id="xpack.ingestPipelines.form.saveButtonLabel" + defaultMessage="Save pipeline" + /> + ) : ( + <FormattedMessage + id="xpack.ingestPipelines.form.createButtonLabel" + defaultMessage="Create pipeline" + /> + ); + + return ( + <> + <Form + form={form} + data-test-subj="pipelineForm" + isInvalid={form.isSubmitted && !form.isValid} + error={form.getErrors()} + > + {/* Request error */} + {saveError && <PipelineFormError errorMessage={saveError.message} />} + + {/* All form fields */} + <PipelineFormFields + hasVersion={Boolean(defaultValue.version)} + isTestButtonDisabled={isTestingPipeline || form.isValid === false} + onTestPipelineClick={handleTestPipelineClick} + hasOnFailure={Boolean(defaultValue.on_failure)} + isEditing={isEditing} + /> + + {/* Form submission */} + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiButton + fill + color="secondary" + iconType="check" + onClick={form.submit} + data-test-subj="submitButton" + disabled={form.isSubmitted && form.isValid === false} + isLoading={isSaving} + > + {saveButtonLabel} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty color="primary" onClick={onCancel}> + <FormattedMessage + id="xpack.ingestPipelines.form.cancelButtonLabel" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + onClick={() => setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + > + {isRequestVisible ? ( + <FormattedMessage + id="xpack.ingestPipelines.form.hideRequestButtonLabel" + defaultMessage="Hide request" + /> + ) : ( + <FormattedMessage + id="xpack.ingestPipelines.form.showRequestButtonLabel" + defaultMessage="Show request" + /> + )} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + + {/* ES request flyout */} + {isRequestVisible ? ( + <PipelineRequestFlyout + closeFlyout={() => setIsRequestVisible(prevIsRequestVisible => !prevIsRequestVisible)} + /> + ) : null} + + {/* Test pipeline flyout */} + {isTestingPipeline ? ( + <PipelineTestFlyout + closeFlyout={() => { + setIsTestingPipeline(prevIsTestingPipeline => !prevIsTestingPipeline); + }} + /> + ) : null} + </Form> + + <EuiSpacer size="m" /> + </> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx new file mode 100644 index 0000000000000..ef0e2737df24d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_error.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +interface Props { + errorMessage: string; +} + +export const PipelineFormError: React.FunctionComponent<Props> = ({ errorMessage }) => { + return ( + <> + <EuiCallOut + title={ + <FormattedMessage + id="xpack.ingestPipelines.form.savePipelineError" + defaultMessage="Unable to create pipeline" + /> + } + color="danger" + iconType="alert" + data-test-subj="savePipelineError" + > + <p>{errorMessage}</p> + </EuiCallOut> + <EuiSpacer size="m" /> + </> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx new file mode 100644 index 0000000000000..045afd52204fa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -0,0 +1,223 @@ +/* + * 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 } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; + +import { + getUseField, + getFormRow, + Field, + JsonEditorField, + useKibana, +} from '../../../shared_imports'; + +interface Props { + hasVersion: boolean; + hasOnFailure: boolean; + isTestButtonDisabled: boolean; + onTestPipelineClick: () => void; + isEditing?: boolean; +} + +const UseField = getUseField({ component: Field }); +const FormRow = getFormRow({ titleTag: 'h3' }); + +export const PipelineFormFields: React.FunctionComponent<Props> = ({ + isEditing, + hasVersion, + hasOnFailure, + isTestButtonDisabled, + onTestPipelineClick, +}) => { + const { services } = useKibana(); + + const [isVersionVisible, setIsVersionVisible] = useState<boolean>(hasVersion); + const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState<boolean>(hasOnFailure); + + return ( + <> + {/* Name field with optional version field */} + <FormRow + title={<FormattedMessage id="xpack.ingestPipelines.form.nameTitle" defaultMessage="Name" />} + description={ + <> + <FormattedMessage + id="xpack.ingestPipelines.form.nameDescription" + defaultMessage="A unique identifier for this pipeline." + /> + <EuiSpacer size="m" /> + <EuiSwitch + label={ + <FormattedMessage + id="xpack.ingestPipelines.form.versionToggleDescription" + defaultMessage="Add version number" + /> + } + checked={isVersionVisible} + onChange={e => setIsVersionVisible(e.target.checked)} + data-test-subj="versionToggle" + /> + </> + } + > + <UseField + path="name" + componentProps={{ + ['data-test-subj']: 'nameField', + euiFieldProps: { disabled: Boolean(isEditing) }, + }} + /> + + {isVersionVisible && ( + <UseField + path="version" + componentProps={{ + ['data-test-subj']: 'versionField', + }} + /> + )} + </FormRow> + + {/* Description field */} + <FormRow + title={ + <FormattedMessage + id="xpack.ingestPipelines.form.descriptionFieldTitle" + defaultMessage="Description" + /> + } + description={ + <FormattedMessage + id="xpack.ingestPipelines.form.descriptionFieldDescription" + defaultMessage="The description to apply to the pipeline." + /> + } + > + <UseField + path="description" + componentProps={{ + ['data-test-subj']: 'descriptionField', + euiFieldProps: { + compressed: true, + }, + }} + /> + </FormRow> + + {/* Processors field */} + <FormRow + title={ + <FormattedMessage + id="xpack.ingestPipelines.form.processorsFieldTitle" + defaultMessage="Processors" + /> + } + description={ + <> + <FormattedMessage + id="xpack.ingestPipelines.form.processorsFieldDescription" + defaultMessage="The processors used to pre-process documents before indexing. {learnMoreLink}" + values={{ + learnMoreLink: ( + <EuiLink href={services.documentation.getProcessorsUrl()} target="_blank"> + {i18n.translate('xpack.ingestPipelines.form.processorsDocumentionLink', { + defaultMessage: 'Learn more.', + })} + </EuiLink> + ), + }} + /> + + <EuiSpacer /> + + <EuiButton size="s" onClick={onTestPipelineClick} disabled={isTestButtonDisabled}> + <FormattedMessage + id="xpack.ingestPipelines.form.testPipelineButtonLabel" + defaultMessage="Test pipeline" + /> + </EuiButton> + </> + } + > + <UseField + path="processors" + component={JsonEditorField} + componentProps={{ + ['data-test-subj']: 'processorsField', + euiCodeEditorProps: { + height: '300px', + 'aria-label': i18n.translate('xpack.ingestPipelines.form.processorsFieldAriaLabel', { + defaultMessage: 'Processors JSON editor', + }), + }, + }} + /> + </FormRow> + + {/* On-failure field */} + <FormRow + title={ + <FormattedMessage + id="xpack.ingestPipelines.form.onFailureTitle" + defaultMessage="Failure processors" + /> + } + description={ + <> + <FormattedMessage + id="xpack.ingestPipelines.form.onFailureDescription" + defaultMessage="The processors to be executed following a failed processor. {learnMoreLink}" + values={{ + learnMoreLink: ( + <EuiLink href={services.documentation.getHandlingFailureUrl()} target="_blank"> + {i18n.translate('xpack.ingestPipelines.form.onFailureDocumentionLink', { + defaultMessage: 'Learn more.', + })} + </EuiLink> + ), + }} + /> + <EuiSpacer size="m" /> + <EuiSwitch + label={ + <FormattedMessage + id="xpack.ingestPipelines.form.onFailureToggleDescription" + defaultMessage="Add on-failure processors" + /> + } + checked={isOnFailureEditorVisible} + onChange={e => setIsOnFailureEditorVisible(e.target.checked)} + data-test-subj="onFailureToggle" + /> + </> + } + > + {isOnFailureEditorVisible ? ( + <UseField + path="on_failure" + component={JsonEditorField} + componentProps={{ + ['data-test-subj']: 'onFailureEditor', + euiCodeEditorProps: { + height: '300px', + 'aria-label': i18n.translate('xpack.ingestPipelines.form.onFailureFieldAriaLabel', { + defaultMessage: 'On-failure processors JSON editor', + }), + }, + }} + /> + ) : ( + // <FormRow/> requires children or a field + // For now, we return an empty <div> if the editor is not visible + <div /> + )} + </FormRow> + </> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx new file mode 100644 index 0000000000000..57abea2309aa1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_provider.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { PipelineForm as PipelineFormUI, PipelineFormProps } from './pipeline_form'; +import { TestConfigContextProvider } from './test_config_context'; + +export const PipelineFormProvider: React.FunctionComponent<PipelineFormProps> = passThroughProps => { + return ( + <TestConfigContextProvider> + <PipelineFormUI {...passThroughProps} /> + </TestConfigContextProvider> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts new file mode 100644 index 0000000000000..9476b65557c54 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelineRequestFlyoutProvider as PipelineRequestFlyout } from './pipeline_request_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx new file mode 100644 index 0000000000000..58e86695808b1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useRef } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +import { Pipeline } from '../../../../../common/types'; + +interface Props { + pipeline: Pipeline; + closeFlyout: () => void; +} + +export const PipelineRequestFlyout: React.FunctionComponent<Props> = ({ + closeFlyout, + pipeline, +}) => { + const { name, ...pipelineBody } = pipeline; + const endpoint = `PUT _ingest/pipeline/${name || '<pipelineName>'}`; + const payload = JSON.stringify(pipelineBody, null, 2); + const request = `${endpoint}\n${payload}`; + // Hack so that copied-to-clipboard value updates as content changes + // Related issue: https://github.com/elastic/eui/issues/3321 + const uuid = useRef(0); + uuid.current++; + + return ( + <EuiFlyout maxWidth={550} onClose={closeFlyout}> + <EuiFlyoutHeader> + <EuiTitle> + <h2> + {name ? ( + <FormattedMessage + id="xpack.ingestPipelines.requestFlyout.namedTitle" + defaultMessage="Request for '{name}'" + values={{ name }} + /> + ) : ( + <FormattedMessage + id="xpack.ingestPipelines.requestFlyout.unnamedTitle" + defaultMessage="Request" + /> + )} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + + <EuiFlyoutBody> + <EuiText> + <p> + <FormattedMessage + id="xpack.ingestPipelines.requestFlyout.descriptionText" + defaultMessage="This Elasticsearch request will create or update this pipeline." + /> + </p> + </EuiText> + + <EuiSpacer /> + <EuiCodeBlock language="json" isCopyable key={uuid.current}> + {request} + </EuiCodeBlock> + </EuiFlyoutBody> + + <EuiFlyoutFooter> + <EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left"> + <FormattedMessage + id="xpack.ingestPipelines.requestFlyout.closeButtonLabel" + defaultMessage="Close" + /> + </EuiButtonEmpty> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx new file mode 100644 index 0000000000000..6dcedca6085af --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx @@ -0,0 +1,29 @@ +/* + * 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 { Pipeline } from '../../../../../common/types'; +import { useFormContext } from '../../../../shared_imports'; +import { PipelineRequestFlyout } from './pipeline_request_flyout'; + +export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { + const form = useFormContext(); + const [formData, setFormData] = useState<Pipeline>({} as Pipeline); + + useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + }); + + return subscription.unsubscribe; + }, [form]); + + return <PipelineRequestFlyout pipeline={formData} closeFlyout={closeFlyout} />; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts new file mode 100644 index 0000000000000..38bbc43b469a5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelineTestFlyoutProvider as PipelineTestFlyout } from './pipeline_test_flyout_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx new file mode 100644 index 0000000000000..16f39b2912c1d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout.tsx @@ -0,0 +1,203 @@ +/* + * 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, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiCallOut, +} from '@elastic/eui'; + +import { useKibana } from '../../../../shared_imports'; +import { Pipeline } from '../../../../../common/types'; +import { Tabs, Tab, OutputTab, DocumentsTab } from './tabs'; +import { useTestConfigContext } from '../test_config_context'; + +export interface PipelineTestFlyoutProps { + closeFlyout: () => void; + pipeline: Pipeline; + isPipelineValid: boolean; +} + +export const PipelineTestFlyout: React.FunctionComponent<PipelineTestFlyoutProps> = ({ + closeFlyout, + pipeline, + isPipelineValid, +}) => { + const { services } = useKibana(); + + const { testConfig } = useTestConfigContext(); + const { documents: cachedDocuments, verbose: cachedVerbose } = testConfig; + + const initialSelectedTab = cachedDocuments ? 'output' : 'documents'; + const [selectedTab, setSelectedTab] = useState<Tab>(initialSelectedTab); + + const [shouldExecuteImmediately, setShouldExecuteImmediately] = useState<boolean>(false); + const [isExecuting, setIsExecuting] = useState<boolean>(false); + const [executeError, setExecuteError] = useState<any>(null); + const [executeOutput, setExecuteOutput] = useState<any>(undefined); + + const handleExecute = useCallback( + async (documents: object[], verbose?: boolean) => { + const { name: pipelineName, ...pipelineDefinition } = pipeline; + + setIsExecuting(true); + setExecuteError(null); + + const { error, data: output } = await services.api.simulatePipeline({ + documents, + verbose, + pipeline: pipelineDefinition, + }); + + setIsExecuting(false); + + if (error) { + setExecuteError(error); + return; + } + + setExecuteOutput(output); + + services.notifications.toasts.addSuccess( + i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { + defaultMessage: 'Pipeline executed', + }), + { + toastLifeTimeMs: 1000, + } + ); + + setSelectedTab('output'); + }, + [pipeline, services.api, services.notifications.toasts] + ); + + useEffect(() => { + if (cachedDocuments) { + setShouldExecuteImmediately(true); + } + // We only want to know on initial mount if there are cached documents + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + // If the user has already tested the pipeline once, + // use the cached test config and automatically execute the pipeline + if (shouldExecuteImmediately && Object.entries(pipeline).length > 0) { + setShouldExecuteImmediately(false); + handleExecute(cachedDocuments!, cachedVerbose); + } + }, [ + pipeline, + handleExecute, + cachedDocuments, + cachedVerbose, + isExecuting, + shouldExecuteImmediately, + ]); + + let tabContent; + + if (selectedTab === 'output') { + tabContent = ( + <OutputTab + executeOutput={executeOutput} + handleExecute={handleExecute} + isExecuting={isExecuting} + /> + ); + } else { + // default to "documents" tab + tabContent = ( + <DocumentsTab + isExecuting={isExecuting} + isPipelineValid={isPipelineValid} + handleExecute={handleExecute} + /> + ); + } + + return ( + <EuiFlyout maxWidth={550} onClose={closeFlyout}> + <EuiFlyoutHeader> + <EuiTitle> + <h2> + {pipeline.name ? ( + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.withPipelineNameTitle" + defaultMessage="Test pipeline '{pipelineName}'" + values={{ + pipelineName: pipeline.name, + }} + /> + ) : ( + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.title" + defaultMessage="Test pipeline" + /> + )} + </h2> + </EuiTitle> + </EuiFlyoutHeader> + + <EuiFlyoutBody> + <Tabs + onTabChange={setSelectedTab} + selectedTab={selectedTab} + getIsDisabled={tabId => !executeOutput && tabId === 'output'} + /> + + <EuiSpacer /> + + {/* Execute error */} + {executeError ? ( + <> + <EuiCallOut + title={ + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.executePipelineError" + defaultMessage="Unable to execute pipeline" + /> + } + color="danger" + iconType="alert" + > + <p>{executeError.message}</p> + </EuiCallOut> + <EuiSpacer size="m" /> + </> + ) : null} + + {/* Invalid pipeline error */} + {!isPipelineValid ? ( + <> + <EuiCallOut + title={ + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.invalidPipelineErrorMessage" + defaultMessage="The pipeline to execute is invalid." + /> + } + color="danger" + iconType="alert" + /> + <EuiSpacer size="m" /> + </> + ) : null} + + {/* Documents or output tab content */} + {tabContent} + </EuiFlyoutBody> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx new file mode 100644 index 0000000000000..351478394595a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx @@ -0,0 +1,39 @@ +/* + * 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 { Pipeline } from '../../../../../common/types'; +import { useFormContext } from '../../../../shared_imports'; +import { PipelineTestFlyout, PipelineTestFlyoutProps } from './pipeline_test_flyout'; + +type Props = Omit<PipelineTestFlyoutProps, 'pipeline' | 'isPipelineValid'>; + +export const PipelineTestFlyoutProvider: React.FunctionComponent<Props> = ({ closeFlyout }) => { + const form = useFormContext(); + const [formData, setFormData] = useState<Pipeline>({} as Pipeline); + const [isFormDataValid, setIsFormDataValid] = useState<boolean>(false); + + useEffect(() => { + const subscription = form.subscribe(async ({ isValid, validate, data }) => { + const isFormValid = isValid ?? (await validate()); + if (isFormValid) { + setFormData(data.format() as Pipeline); + } + setIsFormDataValid(isFormValid); + }); + + return subscription.unsubscribe; + }, [form]); + + return ( + <PipelineTestFlyout + pipeline={formData} + closeFlyout={closeFlyout} + isPipelineValid={isFormDataValid} + /> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts new file mode 100644 index 0000000000000..ea8fe2cd92350 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { Tabs, Tab } from './pipeline_test_tabs'; + +export { DocumentsTab } from './tab_documents'; + +export { OutputTab } from './tab_output'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx new file mode 100644 index 0000000000000..f720b80122702 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/pipeline_test_tabs.tsx @@ -0,0 +1,60 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTab, EuiTabs } from '@elastic/eui'; + +export type Tab = 'documents' | 'output'; + +interface Props { + onTabChange: (tab: Tab) => void; + selectedTab: Tab; + getIsDisabled: (tab: Tab) => boolean; +} + +export const Tabs: React.FunctionComponent<Props> = ({ + onTabChange, + selectedTab, + getIsDisabled, +}) => { + const tabs: Array<{ + id: Tab; + name: React.ReactNode; + }> = [ + { + id: 'documents', + name: ( + <FormattedMessage + id="xpack.ingestPipelines.tabs.documentsTabTitle" + defaultMessage="Documents" + /> + ), + }, + { + id: 'output', + name: ( + <FormattedMessage id="xpack.ingestPipelines.tabs.outputTabTitle" defaultMessage="Output" /> + ), + }, + ]; + + return ( + <EuiTabs> + {tabs.map(tab => ( + <EuiTab + onClick={() => onTabChange(tab.id)} + isSelected={tab.id === selectedTab} + key={tab.id} + disabled={getIsDisabled(tab.id)} + data-test-subj={tab.id.toLowerCase() + '_tab'} + > + {tab.name} + </EuiTab> + ))} + </EuiTabs> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx new file mode 100644 index 0000000000000..de9910344bd4b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/schema.tsx @@ -0,0 +1,87 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { EuiCode } from '@elastic/eui'; + +import { FormSchema, fieldValidators, ValidationFuncArg } from '../../../../../shared_imports'; +import { parseJson, stringifyJson } from '../../../../lib'; + +const { emptyField, isJsonField } = fieldValidators; + +export const documentsSchema: FormSchema = { + documents: { + label: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel', + { + defaultMessage: 'Documents', + } + ), + helpText: ( + <FormattedMessage + id="xpack.ingestPipelines.form.onFailureFieldHelpText" + defaultMessage="Use JSON format: {code}" + values={{ + code: ( + <EuiCode> + {JSON.stringify([ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + ])} + </EuiCode> + ), + }} + /> + ), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.noDocumentsError', + { + defaultMessage: 'Documents are required.', + } + ) + ), + }, + { + validator: isJsonField( + i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError', + { + defaultMessage: 'The documents JSON is not valid.', + } + ) + ), + }, + { + validator: ({ value }: ValidationFuncArg<any, any>) => { + const parsedJSON = JSON.parse(value); + + if (!parsedJSON.length) { + return { + message: i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsForm.oneDocumentRequiredError', + { + defaultMessage: 'At least one document is required.', + } + ), + }; + } + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx new file mode 100644 index 0000000000000..97bf03dbdc068 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_documents.tsx @@ -0,0 +1,152 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { EuiSpacer, EuiText, EuiButton, EuiHorizontalRule, EuiLink } from '@elastic/eui'; + +import { + getUseField, + Field, + JsonEditorField, + Form, + useForm, + FormConfig, + useKibana, +} from '../../../../../shared_imports'; + +import { documentsSchema } from './schema'; +import { useTestConfigContext, TestConfig } from '../../test_config_context'; + +const UseField = getUseField({ component: Field }); + +interface Props { + handleExecute: (documents: object[], verbose: boolean) => void; + isPipelineValid: boolean; + isExecuting: boolean; +} + +export const DocumentsTab: React.FunctionComponent<Props> = ({ + isPipelineValid, + handleExecute, + isExecuting, +}) => { + const { services } = useKibana(); + + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); + const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + + const executePipeline: FormConfig['onSubmit'] = (formData, isValid) => { + if (!isValid || !isPipelineValid) { + return; + } + + const { documents } = formData as TestConfig; + + // Update context + setCurrentTestConfig({ + ...testConfig, + documents, + }); + + handleExecute(documents!, cachedVerbose); + }; + + const { form } = useForm({ + schema: documentsSchema, + defaultValue: { + documents: cachedDocuments || '', + verbose: cachedVerbose || false, + }, + onSubmit: executePipeline, + }); + + return ( + <> + <EuiText> + <p> + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.tabDescriptionText" + defaultMessage="Provide an array of documents to be ingested by the pipeline. {learnMoreLink}" + values={{ + learnMoreLink: ( + <EuiLink href={services.documentation.getSimulatePipelineApiUrl()} target="_blank"> + {i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink', + { + defaultMessage: 'Learn more.', + } + )} + </EuiLink> + ), + }} + /> + </p> + </EuiText> + + <EuiSpacer size="m" /> + + <Form + form={form} + data-test-subj="testPipelineForm" + isInvalid={form.isSubmitted && !form.isValid && !isPipelineValid} + error={form.getErrors()} + > + {/* Documents editor */} + <UseField + path="documents" + component={JsonEditorField} + componentProps={{ + ['data-test-subj']: 'documentsField', + euiCodeEditorProps: { + height: '300px', + 'aria-label': i18n.translate( + 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.editorFieldAriaLabel', + { + defaultMessage: 'Documents JSON editor', + } + ), + }, + }} + /> + + <EuiHorizontalRule /> + + <EuiText> + <p> + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.runDescriptionText" + defaultMessage="Execute the pipeline." + /> + </p> + </EuiText> + + <EuiSpacer size="m" /> + + <EuiButton + onClick={form.submit} + size="s" + isLoading={isExecuting} + disabled={(form.isSubmitted && !form.isValid) || !isPipelineValid} + > + {isExecuting ? ( + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.runningButtonLabel" + defaultMessage="Running" + /> + ) : ( + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.documentsTab.runButtonLabel" + defaultMessage="Run" + /> + )} + </EuiButton> + </Form> + </> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx new file mode 100644 index 0000000000000..aa80f8c86ad8b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/tabs/tab_output.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCodeBlock, + EuiSpacer, + EuiText, + EuiSwitch, + EuiLink, + EuiIcon, + EuiLoadingSpinner, + EuiIconTip, +} from '@elastic/eui'; +import { useTestConfigContext } from '../../test_config_context'; + +interface Props { + executeOutput?: { docs: object[] }; + handleExecute: (documents: object[], verbose: boolean) => void; + isExecuting: boolean; +} + +export const OutputTab: React.FunctionComponent<Props> = ({ + executeOutput, + handleExecute, + isExecuting, +}) => { + const { setCurrentTestConfig, testConfig } = useTestConfigContext(); + const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + + const onEnableVerbose = (isVerboseEnabled: boolean) => { + setCurrentTestConfig({ + ...testConfig, + verbose: isVerboseEnabled, + }); + + handleExecute(cachedDocuments!, isVerboseEnabled); + }; + + let content: React.ReactNode | undefined; + + if (isExecuting) { + content = <EuiLoadingSpinner size="m" />; + } else if (executeOutput) { + content = ( + <EuiCodeBlock language="json" isCopyable> + {JSON.stringify(executeOutput, null, 2)} + </EuiCodeBlock> + ); + } + + return ( + <> + <EuiText> + <p> + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionText" + defaultMessage="The output of the executed pipeline. {runLink}" + values={{ + runLink: ( + <EuiLink onClick={() => handleExecute(cachedDocuments!, cachedVerbose)}> + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.outputTab.descriptionLinkLabel" + defaultMessage="Refresh output" + />{' '} + <EuiIcon type="refresh" /> + </EuiLink> + ), + }} + /> + </p> + </EuiText> + + <EuiSpacer size="m" /> + + <EuiSwitch + label={ + <> + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.outputTab.verboseSwitchLabel" + defaultMessage="View verbose output" + />{' '} + <EuiIconTip + content={ + <FormattedMessage + id="xpack.ingestPipelines.testPipelineFlyout.outputTab.verboseSwitchTooltipLabel" + defaultMessage="Include output data for each processor in the executed pipeline response" + /> + } + /> + </> + } + checked={cachedVerbose} + onChange={e => onEnableVerbose(e.target.checked)} + /> + + <EuiSpacer size="m" /> + + {content} + </> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx new file mode 100644 index 0000000000000..2e2689f41527a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -0,0 +1,147 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; +import { parseJson, stringifyJson } from '../../lib'; + +const { emptyField, isJsonField } = fieldValidators; +const { toInt } = fieldFormatters; + +export const pipelineFormSchema: FormSchema = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { + defaultMessage: 'A pipeline name is required.', + }) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { + defaultMessage: 'Description', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineDescriptionRequiredError', { + defaultMessage: 'A pipeline description is required.', + }) + ), + }, + ], + }, + processors: { + label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { + defaultMessage: 'Processors', + }), + helpText: ( + <FormattedMessage + id="xpack.ingestPipelines.form.processorsFieldHelpText" + defaultMessage="Use JSON format: {code}" + values={{ + code: ( + <EuiCode> + {JSON.stringify([ + { + set: { + field: 'foo', + value: 'bar', + }, + }, + ])} + </EuiCode> + ), + }} + /> + ), + serializer: parseJson, + deserializer: stringifyJson, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.processorsRequiredError', { + defaultMessage: 'Processors are required.', + }) + ), + }, + { + validator: isJsonField( + i18n.translate('xpack.ingestPipelines.form.processorsJsonError', { + defaultMessage: 'The processors JSON is not valid.', + }) + ), + }, + ], + }, + on_failure: { + label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { + defaultMessage: 'On-failure processors (optional)', + }), + helpText: ( + <FormattedMessage + id="xpack.ingestPipelines.form.onFailureFieldHelpText" + defaultMessage="Use JSON format: {code}" + values={{ + code: ( + <EuiCode> + {JSON.stringify([ + { + set: { + field: '_index', + value: 'failed-{{ _index }}', + }, + }, + ])} + </EuiCode> + ), + }} + /> + ), + serializer: value => { + const result = parseJson(value); + // If an empty array was passed, strip out this value entirely. + if (!result.length) { + return undefined; + } + return result; + }, + deserializer: stringifyJson, + validations: [ + { + validator: validationArg => { + if (!validationArg.value) { + return; + } + return isJsonField( + i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { + defaultMessage: 'The on-failure processors JSON is not valid.', + }) + )(validationArg); + }, + }, + ], + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx new file mode 100644 index 0000000000000..6840ebef28796 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/test_config_context.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useContext } from 'react'; + +export interface TestConfig { + documents?: object[] | undefined; + verbose: boolean; +} + +interface TestConfigContext { + testConfig: TestConfig; + setCurrentTestConfig: (config: TestConfig) => void; +} + +const TEST_CONFIG_DEFAULT_VALUE = { + testConfig: { + verbose: false, + }, + setCurrentTestConfig: () => {}, +}; + +const TestConfigContext = React.createContext<TestConfigContext>(TEST_CONFIG_DEFAULT_VALUE); + +export const useTestConfigContext = () => { + const ctx = useContext(TestConfigContext); + if (!ctx) { + throw new Error( + '"useTestConfigContext" can only be called inside of TestConfigContext.Provider!' + ); + } + return ctx; +}; + +export const TestConfigContextProvider = ({ children }: { children: React.ReactNode }) => { + const [testConfig, setTestConfig] = useState<TestConfig>({ + verbose: false, + }); + + const setCurrentTestConfig = useCallback((currentTestConfig: TestConfig): void => { + setTestConfig(currentTestConfig); + }, []); + + return ( + <TestConfigContext.Provider + value={{ + testConfig, + setCurrentTestConfig, + }} + > + {children} + </TestConfigContext.Provider> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts new file mode 100644 index 0000000000000..776d44c825670 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/constants/index.ts @@ -0,0 +1,14 @@ +/* + * 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. + */ + +// UI metric constants +export const UIM_APP_NAME = 'ingest_pipelines'; +export const UIM_PIPELINES_LIST_LOAD = 'pipelines_list_load'; +export const UIM_PIPELINE_CREATE = 'pipeline_create'; +export const UIM_PIPELINE_UPDATE = 'pipeline_update'; +export const UIM_PIPELINE_DELETE = 'pipeline_delete'; +export const UIM_PIPELINE_DELETE_MANY = 'pipeline_delete_many'; +export const UIM_PIPELINE_SIMULATE = 'pipeline_simulate'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/index.tsx b/x-pack/plugins/ingest_pipelines/public/application/index.tsx new file mode 100644 index 0000000000000..e43dba4689b44 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/index.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from 'kibana/public'; +import React, { ReactNode } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { NotificationsSetup } from 'kibana/public'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; + +import { API_BASE_PATH } from '../../common/constants'; + +import { AuthorizationProvider } from '../shared_imports'; + +import { App } from './app'; +import { DocumentationService, UiMetricService, ApiService, BreadcrumbService } from './services'; + +export interface AppServices { + breadcrumbs: BreadcrumbService; + metric: UiMetricService; + documentation: DocumentationService; + api: ApiService; + notifications: NotificationsSetup; +} + +export interface CoreServices { + http: HttpSetup; +} + +export const renderApp = ( + element: HTMLElement, + I18nContext: ({ children }: { children: ReactNode }) => JSX.Element, + services: AppServices, + coreServices: CoreServices +) => { + render( + <AuthorizationProvider + privilegesEndpoint={`${API_BASE_PATH}/privileges`} + httpClient={coreServices.http} + > + <I18nContext> + <KibanaContextProvider services={services}> + <App /> + </KibanaContextProvider> + </I18nContext> + </AuthorizationProvider>, + element + ); + + return () => { + unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts new file mode 100644 index 0000000000000..1283033267a50 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { stringifyJson, parseJson } from './utils'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts new file mode 100644 index 0000000000000..e7eff3bd6ca33 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { stringifyJson, parseJson } from './utils'; + +describe('utils', () => { + describe('stringifyJson()', () => { + it('should stringify a valid JSON array', () => { + expect(stringifyJson([1, 2, 3])).toEqual(`[ + 1, + 2, + 3 +]`); + }); + + it('should return a stringified empty array if the value is not a valid JSON array', () => { + expect(stringifyJson({})).toEqual('[\n\n]'); + }); + }); + + describe('parseJson()', () => { + it('should parse a valid JSON string', () => { + expect(parseJson('[1,2,3]')).toEqual([1, 2, 3]); + expect(parseJson('[{"foo": "bar"}]')).toEqual([{ foo: 'bar' }]); + }); + + it('should convert valid JSON that is not an array to an array', () => { + expect(parseJson('{"foo": "bar"}')).toEqual([{ foo: 'bar' }]); + }); + + it('should return an empty array if invalid JSON string', () => { + expect(parseJson('{invalidJsonString}')).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts new file mode 100644 index 0000000000000..fe4e9e65f4b9a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/lib/utils.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const stringifyJson = (json: any): string => + Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]'; + +export const parseJson = (jsonString: string): object[] => { + let parsedJSON: any; + + try { + parsedJSON = JSON.parse(jsonString); + + if (!Array.isArray(parsedJSON)) { + // Convert object to array + parsedJSON = [parsedJSON]; + } + } catch { + parsedJSON = []; + } + + return parsedJSON; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts new file mode 100644 index 0000000000000..e36f27cbf5f62 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/mount_management_section.ts @@ -0,0 +1,35 @@ +/* + * 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 { CoreSetup } from 'src/core/public'; +import { ManagementAppMountParams } from 'src/plugins/management/public'; + +import { documentationService, uiMetricService, apiService, breadcrumbService } from './services'; +import { renderApp } from '.'; + +export async function mountManagementSection( + { http, getStartServices, notifications }: CoreSetup, + params: ManagementAppMountParams +) { + const { element, setBreadcrumbs } = params; + const [coreStart] = await getStartServices(); + const { + docLinks, + i18n: { Context: I18nContext }, + } = coreStart; + + documentationService.setup(docLinks); + breadcrumbService.setup(setBreadcrumbs); + + const services = { + breadcrumbs: breadcrumbService, + metric: uiMetricService, + documentation: documentationService, + api: apiService, + notifications, + }; + + return renderApp(element, I18nContext, services, { http }); +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts new file mode 100644 index 0000000000000..b2925666c5768 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { PipelinesList } from './pipelines_list'; + +export { PipelinesCreate } from './pipelines_create'; + +export { PipelinesEdit } from './pipelines_edit'; + +export { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts new file mode 100644 index 0000000000000..614a3598d407d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelinesClone } from './pipelines_clone'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx new file mode 100644 index 0000000000000..b3b1217caf834 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_clone/pipelines_clone.tsx @@ -0,0 +1,59 @@ +/* + * 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, { FunctionComponent, useEffect } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { SectionLoading, useKibana } from '../../../shared_imports'; + +import { PipelinesCreate } from '../pipelines_create'; + +export interface ParamProps { + sourceName: string; +} + +/** + * This section is a wrapper around the create section where we receive a pipeline name + * to load and set as the source pipeline for the {@link PipelinesCreate} form. + */ +export const PipelinesClone: FunctionComponent<RouteComponentProps<ParamProps>> = props => { + const { sourceName } = props.match.params; + const { services } = useKibana(); + + const { error, data: pipeline, isLoading, isInitialRequest } = services.api.useLoadPipeline( + decodeURIComponent(sourceName) + ); + + useEffect(() => { + if (error && !isLoading) { + services.notifications!.toasts.addError(error, { + title: i18n.translate('xpack.ingestPipelines.clone.loadSourcePipelineErrorTitle', { + defaultMessage: 'Cannot load {name}.', + values: { name: sourceName }, + }), + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [error, isLoading]); + + if (isLoading && isInitialRequest) { + return ( + <SectionLoading> + <FormattedMessage + id="xpack.ingestPipelines.clone.loadingPipelinesDescription" + defaultMessage="Loading pipeline…" + /> + </SectionLoading> + ); + } else { + // We still show the create form even if we were not able to load the + // latest pipeline data. + const sourcePipeline = pipeline ? { ...pipeline, name: `${pipeline.name}-copy` } : undefined; + return <PipelinesCreate {...props} sourcePipeline={sourcePipeline} />; + } +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts new file mode 100644 index 0000000000000..374defa869916 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelinesCreate } from './pipelines_create'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx new file mode 100644 index 0000000000000..34a362d596d92 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -0,0 +1,109 @@ +/* + * 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 { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; + +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; +import { useKibana } from '../../../shared_imports'; +import { PipelineForm } from '../../components'; + +interface Props { + /** + * This value may be passed in to prepopulate the creation form + */ + sourcePipeline?: Pipeline; +} + +export const PipelinesCreate: React.FunctionComponent<RouteComponentProps & Props> = ({ + history, + sourcePipeline, +}) => { + const { services } = useKibana(); + + const [isSaving, setIsSaving] = useState<boolean>(false); + const [saveError, setSaveError] = useState<any>(null); + + const onSave = async (pipeline: Pipeline) => { + setIsSaving(true); + setSaveError(null); + + const { error } = await services.api.createPipeline(pipeline); + + setIsSaving(false); + + if (error) { + setSaveError(error); + return; + } + + history.push(BASE_PATH + `?pipeline=${pipeline.name}`); + }; + + const onCancel = () => { + history.push(BASE_PATH); + }; + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('create'); + }, [services]); + + return ( + <EuiPageBody> + <EuiPageContent> + <EuiTitle size="l"> + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="l" data-test-subj="remoteClusterPageTitle"> + <h1 data-test-subj="pageTitle"> + <FormattedMessage + id="xpack.ingestPipelines.create.pageTitle" + defaultMessage="Create pipeline" + /> + </h1> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiButtonEmpty + size="s" + flush="right" + href={services.documentation.getPutPipelineApiUrl()} + target="_blank" + iconType="help" + > + <FormattedMessage + id="xpack.ingestPipelines.create.docsButtonLabel" + defaultMessage="Create pipeline docs" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiTitle> + + <EuiSpacer size="l" /> + + <PipelineForm + defaultValue={sourcePipeline} + onSave={onSave} + onCancel={onCancel} + isSaving={isSaving} + saveError={saveError} + /> + </EuiPageContent> + </EuiPageBody> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts new file mode 100644 index 0000000000000..26458d23fd6d8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelinesEdit } from './pipelines_edit'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx new file mode 100644 index 0000000000000..99cd8d7eef97b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -0,0 +1,151 @@ +/* + * 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 { RouteComponentProps } from 'react-router-dom'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { EuiCallOut } from '@elastic/eui'; +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; +import { useKibana, SectionLoading } from '../../../shared_imports'; +import { PipelineForm } from '../../components'; + +interface MatchParams { + name: string; +} + +export const PipelinesEdit: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({ + match: { + params: { name }, + }, + history, +}) => { + const { services } = useKibana(); + + const [isSaving, setIsSaving] = useState<boolean>(false); + const [saveError, setSaveError] = useState<any>(null); + + const decodedPipelineName = decodeURI(decodeURIComponent(name)); + + const { error, data: pipeline, isLoading } = services.api.useLoadPipeline(decodedPipelineName); + + const onSave = async (updatedPipeline: Pipeline) => { + setIsSaving(true); + setSaveError(null); + + const { error: savePipelineError } = await services.api.updatePipeline(updatedPipeline); + + setIsSaving(false); + + if (savePipelineError) { + setSaveError(savePipelineError); + return; + } + + history.push(BASE_PATH + `?pipeline=${updatedPipeline.name}`); + }; + + const onCancel = () => { + history.push(BASE_PATH); + }; + + useEffect(() => { + services.breadcrumbs.setBreadcrumbs('edit'); + }, [services.breadcrumbs]); + + let content: React.ReactNode; + + if (isLoading) { + content = ( + <SectionLoading> + <FormattedMessage + id="xpack.ingestPipelines.edit.loadingPipelinesDescription" + defaultMessage="Loading pipeline…" + /> + </SectionLoading> + ); + } else if (error) { + content = ( + <> + <EuiCallOut + title={ + <FormattedMessage + id="xpack.ingestPipelines.edit.fetchPipelineError" + defaultMessage="Error loading pipeline" + /> + } + color="danger" + iconType="alert" + data-test-subj="fetchPipelineError" + > + <p>{error.message}</p> + </EuiCallOut> + <EuiSpacer size="m" /> + </> + ); + } else if (pipeline) { + content = ( + <PipelineForm + onSave={onSave} + onCancel={onCancel} + isSaving={isSaving} + saveError={saveError} + defaultValue={pipeline} + isEditing={true} + /> + ); + } + + return ( + <EuiPageBody> + <EuiPageContent> + <EuiTitle size="l"> + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiTitle size="l" data-test-subj="remoteClusterPageTitle"> + <h1 data-test-subj="pageTitle"> + <FormattedMessage + id="xpack.ingestPipelines.edit.pageTitle" + defaultMessage="Edit pipeline '{name}'" + values={{ name: decodedPipelineName }} + /> + </h1> + </EuiTitle> + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <EuiButtonEmpty + size="s" + flush="right" + href={services.documentation.getPutPipelineApiUrl()} + target="_blank" + iconType="help" + > + <FormattedMessage + id="xpack.ingestPipelines.edit.docsButtonLabel" + defaultMessage="Edit pipeline docs" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiTitle> + + <EuiSpacer size="l" /> + + {content} + </EuiPageContent> + </EuiPageBody> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx new file mode 100644 index 0000000000000..c7736a6c19ba1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx @@ -0,0 +1,125 @@ +/* + * 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 { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { useKibana } from '../../../shared_imports'; + +export const PipelineDeleteModal = ({ + pipelinesToDelete, + callback, +}: { + pipelinesToDelete: string[]; + callback: (data?: { hasDeletedPipelines: boolean }) => void; +}) => { + const { services } = useKibana(); + + const numPipelinesToDelete = pipelinesToDelete.length; + + const handleDeletePipelines = () => { + services.api + .deletePipelines(pipelinesToDelete) + .then(({ data: { itemsDeleted, errors }, error }) => { + const hasDeletedPipelines = itemsDeleted && itemsDeleted.length; + + if (hasDeletedPipelines) { + const successMessage = + itemsDeleted.length === 1 + ? i18n.translate( + 'xpack.ingestPipelines.deleteModal.successDeleteSingleNotificationMessageText', + { + defaultMessage: "Deleted pipeline '{pipelineName}'", + values: { pipelineName: pipelinesToDelete[0] }, + } + ) + : i18n.translate( + 'xpack.ingestPipelines.deleteModal.successDeleteMultipleNotificationMessageText', + { + defaultMessage: + 'Deleted {numSuccesses, plural, one {# pipeline} other {# pipelines}}', + values: { numSuccesses: itemsDeleted.length }, + } + ); + + callback({ hasDeletedPipelines }); + services.notifications.toasts.addSuccess(successMessage); + } + + if (error || errors?.length) { + const hasMultipleErrors = errors?.length > 1 || (error && pipelinesToDelete.length > 1); + const errorMessage = hasMultipleErrors + ? i18n.translate( + 'xpack.ingestPipelines.deleteModal.multipleErrorsNotificationMessageText', + { + defaultMessage: 'Error deleting {count} pipelines', + values: { + count: errors?.length || pipelinesToDelete.length, + }, + } + ) + : i18n.translate('xpack.ingestPipelines.deleteModal.errorNotificationMessageText', { + defaultMessage: "Error deleting pipeline '{name}'", + values: { name: (errors && errors[0].name) || pipelinesToDelete[0] }, + }); + services.notifications.toasts.addDanger(errorMessage); + } + }); + }; + + const handleOnCancel = () => { + callback(); + }; + + return ( + <EuiOverlayMask> + <EuiConfirmModal + buttonColor="danger" + data-test-subj="deletePipelinesConfirmation" + title={ + <FormattedMessage + id="xpack.ingestPipelines.deleteModal.modalTitleText" + defaultMessage="Delete {numPipelinesToDelete, plural, one {pipeline} other {# pipelines}}" + values={{ numPipelinesToDelete }} + /> + } + onCancel={handleOnCancel} + onConfirm={handleDeletePipelines} + cancelButtonText={ + <FormattedMessage + id="xpack.ingestPipelines.deleteModal.cancelButtonLabel" + defaultMessage="Cancel" + /> + } + confirmButtonText={ + <FormattedMessage + id="xpack.ingestPipelines.deleteModal.confirmButtonLabel" + defaultMessage="Delete {numPipelinesToDelete, plural, one {pipeline} other {pipelines} }" + values={{ numPipelinesToDelete }} + /> + } + > + <> + <p> + <FormattedMessage + id="xpack.ingestPipelines.deleteModal.deleteDescription" + defaultMessage="You are about to delete {numPipelinesToDelete, plural, one {this pipeline} other {these pipelines} }:" + values={{ numPipelinesToDelete }} + /> + </p> + + <ul> + {pipelinesToDelete.map(name => ( + <li key={name}>{name}</li> + ))} + </ul> + </> + </EuiConfirmModal> + </EuiOverlayMask> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx new file mode 100644 index 0000000000000..98243a5149c0d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx @@ -0,0 +1,210 @@ +/* + * 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, { FunctionComponent, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiIcon, + EuiPopover, + EuiContextMenu, + EuiButton, +} from '@elastic/eui'; + +import { Pipeline } from '../../../../common/types'; + +import { PipelineDetailsJsonBlock } from './details_json_block'; + +export interface Props { + pipeline: Pipeline; + onEditClick: (pipelineName: string) => void; + onCloneClick: (pipelineName: string) => void; + onDeleteClick: (pipelineName: string[]) => void; + onClose: () => void; +} + +export const PipelineDetailsFlyout: FunctionComponent<Props> = ({ + pipeline, + onClose, + onEditClick, + onCloneClick, + onDeleteClick, +}) => { + const [showPopover, setShowPopover] = useState(false); + const actionMenuItems = [ + /** + * Edit pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.editActionLabel', { + defaultMessage: 'Edit', + }), + icon: <EuiIcon type="pencil" />, + onClick: () => onEditClick(pipeline.name), + }, + /** + * Clone pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.cloneActionLabel', { + defaultMessage: 'Clone', + }), + icon: <EuiIcon type="copy" />, + onClick: () => onCloneClick(pipeline.name), + }, + /** + * Delete pipeline + */ + { + name: i18n.translate('xpack.ingestPipelines.list.pipelineDetails.deleteActionLabel', { + defaultMessage: 'Delete', + }), + icon: <EuiIcon type="trash" />, + onClick: () => onDeleteClick([pipeline.name]), + }, + ]; + + const managePipelineButton = ( + <EuiButton + data-test-subj="managePipelineButton" + aria-label={i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.managePipelineActionsAriaLabel', + { + defaultMessage: 'Manage pipeline', + } + )} + onClick={() => setShowPopover(previousBool => !previousBool)} + iconType="arrowUp" + iconSide="right" + fill + > + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.managePipelineButtonLabel', { + defaultMessage: 'Manage', + })} + </EuiButton> + ); + + return ( + <EuiFlyout + onClose={onClose} + aria-labelledby="pipelineDetailsFlyoutTitle" + size="m" + maxWidth={550} + > + <EuiFlyoutHeader> + <EuiTitle id="pipelineDetailsFlyoutTitle"> + <h2>{pipeline.name}</h2> + </EuiTitle> + </EuiFlyoutHeader> + + <EuiFlyoutBody> + <EuiDescriptionList> + {/* Pipeline description */} + <EuiDescriptionListTitle> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { + defaultMessage: 'Description', + })} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + {pipeline.description ?? ''} + </EuiDescriptionListDescription> + + {/* Pipeline version */} + {pipeline.version && ( + <> + <EuiDescriptionListTitle> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.versionTitle', { + defaultMessage: 'Version', + })} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + {String(pipeline.version)} + </EuiDescriptionListDescription> + </> + )} + + {/* Processors JSON */} + <EuiDescriptionListTitle> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.processorsTitle', { + defaultMessage: 'Processors JSON', + })} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <PipelineDetailsJsonBlock json={pipeline.processors} /> + </EuiDescriptionListDescription> + + {/* On Failure Processor JSON */} + {pipeline.on_failure?.length && ( + <> + <EuiDescriptionListTitle> + {i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.failureProcessorsTitle', + { + defaultMessage: 'On failure processors JSON', + } + )} + </EuiDescriptionListTitle> + <EuiDescriptionListDescription> + <PipelineDetailsJsonBlock json={pipeline.on_failure} /> + </EuiDescriptionListDescription> + </> + )} + </EuiDescriptionList> + </EuiFlyoutBody> + + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty iconType="cross" onClick={onClose} flush="left"> + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.closeButtonLabel', { + defaultMessage: 'Close', + })} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexGroup gutterSize="none" alignItems="center" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiPopover + isOpen={showPopover} + closePopover={() => setShowPopover(false)} + button={managePipelineButton} + panelPaddingSize="none" + withTitle + repositionOnScroll + > + <EuiContextMenu + initialPanelId={0} + data-test-subj="autoFollowPatternActionContextMenu" + panels={[ + { + id: 0, + title: i18n.translate( + 'xpack.ingestPipelines.list.pipelineDetails.managePipelinePanelTitle', + { + defaultMessage: 'Pipeline options', + } + ), + items: actionMenuItems, + }, + ]} + /> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx new file mode 100644 index 0000000000000..6c44336c7547d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_json_block.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent, useRef } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; + +export interface Props { + json: Record<string, any>; +} + +export const PipelineDetailsJsonBlock: FunctionComponent<Props> = ({ json }) => { + // Hack so copied-to-clipboard value updates as content changes + // Related issue: https://github.com/elastic/eui/issues/3321 + const uuid = useRef(0); + uuid.current++; + + return ( + <EuiCodeBlock + paddingSize="s" + language="json" + overflowHeight={json.length > 0 ? 300 : undefined} + isCopyable + key={uuid.current} + > + {JSON.stringify(json, null, 2)} + </EuiCodeBlock> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx new file mode 100644 index 0000000000000..ef64fb33a6a55 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -0,0 +1,29 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; +import { BASE_PATH } from '../../../../common/constants'; + +export const EmptyList: FunctionComponent = () => ( + <EuiEmptyPrompt + iconType="managementApp" + title={ + <h2> + {i18n.translate('xpack.ingestPipelines.list.table.emptyPromptTitle', { + defaultMessage: 'Start by creating a pipeline', + })} + </h2> + } + actions={ + <EuiButton href={`#${BASE_PATH}/create`} iconType="plusInCircle" fill> + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { + defaultMessage: 'Create a pipeline', + })} + </EuiButton> + } + /> +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts new file mode 100644 index 0000000000000..a541e3bb85fd0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { PipelinesList } from './main'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx new file mode 100644 index 0000000000000..c90ac2714a95a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -0,0 +1,207 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Location } from 'history'; +import { parse } from 'query-string'; + +import { + EuiPageBody, + EuiPageContent, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiCallOut, +} from '@elastic/eui'; + +import { EuiSpacer, EuiText } from '@elastic/eui'; + +import { Pipeline } from '../../../../common/types'; +import { BASE_PATH } from '../../../../common/constants'; +import { useKibana, SectionLoading } from '../../../shared_imports'; +import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; + +import { EmptyList } from './empty_list'; +import { PipelineTable } from './table'; +import { PipelineDetailsFlyout } from './details_flyout'; +import { PipelineNotFoundFlyout } from './not_found_flyout'; +import { PipelineDeleteModal } from './delete_modal'; + +const getPipelineNameFromLocation = (location: Location) => { + const { pipeline } = parse(location.search.substring(1)); + return pipeline; +}; + +export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ + history, + location, +}) => { + const { services } = useKibana(); + const pipelineNameFromLocation = getPipelineNameFromLocation(location); + + const [selectedPipeline, setSelectedPipeline] = useState<Pipeline | undefined>(undefined); + const [showFlyout, setShowFlyout] = useState<boolean>(false); + + const [pipelinesToDelete, setPipelinesToDelete] = useState<string[]>([]); + + const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + + // Track component loaded + useEffect(() => { + services.metric.trackUiMetric(UIM_PIPELINES_LIST_LOAD); + services.breadcrumbs.setBreadcrumbs('home'); + }, [services.metric, services.breadcrumbs]); + + useEffect(() => { + if (pipelineNameFromLocation && data?.length) { + const pipeline = data.find(p => p.name === pipelineNameFromLocation); + setSelectedPipeline(pipeline); + setShowFlyout(true); + } + }, [pipelineNameFromLocation, data]); + + const goToEditPipeline = (name: string) => { + history.push(`${BASE_PATH}/edit/${encodeURIComponent(name)}`); + }; + + const goToClonePipeline = (name: string) => { + history.push(`${BASE_PATH}/create/${encodeURIComponent(name)}`); + }; + + const goHome = () => { + setShowFlyout(false); + history.push(BASE_PATH); + }; + + let content: React.ReactNode; + + if (isLoading) { + content = ( + <SectionLoading> + <FormattedMessage + id="xpack.ingestPipelines.list.loadingMessage" + defaultMessage="Loading pipelines..." + /> + </SectionLoading> + ); + } else if (data?.length) { + content = ( + <PipelineTable + onReloadClick={sendRequest} + onEditPipelineClick={goToEditPipeline} + onDeletePipelineClick={setPipelinesToDelete} + onClonePipelineClick={goToClonePipeline} + pipelines={data} + /> + ); + } else { + content = <EmptyList />; + } + + const renderFlyout = (): React.ReactNode => { + if (!showFlyout) { + return; + } + if (selectedPipeline) { + return ( + <PipelineDetailsFlyout + pipeline={selectedPipeline} + onClose={() => { + setSelectedPipeline(undefined); + goHome(); + }} + onEditClick={goToEditPipeline} + onCloneClick={goToClonePipeline} + onDeleteClick={setPipelinesToDelete} + /> + ); + } else { + // Somehow we triggered show pipeline details, but do not have a pipeline. + // We assume not found. + return ( + <PipelineNotFoundFlyout + onClose={() => { + goHome(); + }} + pipelineName={pipelineNameFromLocation} + /> + ); + } + }; + + return ( + <> + <EuiPageBody> + <EuiPageContent> + <EuiTitle size="l"> + <EuiFlexGroup alignItems="center"> + <EuiFlexItem> + <h1 data-test-subj="appTitle"> + <FormattedMessage + id="xpack.ingestPipelines.list.listTitle" + defaultMessage="Ingest Node Pipelines" + /> + </h1> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + href={services.documentation.getIngestNodeUrl()} + target="_blank" + iconType="help" + > + <FormattedMessage + id="xpack.ingestPipelines.list.pipelinesDocsLinkText" + defaultMessage="Ingest Node Pipelines docs" + /> + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiTitle> + <EuiSpacer size="s" /> + <EuiTitle size="s"> + <EuiText color="subdued"> + <FormattedMessage + id="xpack.ingestPipelines.list.pipelinesDescription" + defaultMessage="Use ingest node pipelines to pre-process documents before indexing." + /> + </EuiText> + </EuiTitle> + <EuiSpacer size="m" /> + {/* Error call out for pipeline table */} + {error ? ( + <EuiCallOut + iconType="faceSad" + color="danger" + title={i18n.translate('xpack.ingestPipelines.list.loadErrorTitle', { + defaultMessage: 'Cannot load pipelines, please refresh the page to try again.', + })} + /> + ) : ( + content + )} + </EuiPageContent> + </EuiPageBody> + {renderFlyout()} + {pipelinesToDelete?.length > 0 ? ( + <PipelineDeleteModal + callback={deleteResponse => { + if (deleteResponse?.hasDeletedPipelines) { + // reload pipelines list + sendRequest(); + } + setPipelinesToDelete([]); + setSelectedPipeline(undefined); + }} + pipelinesToDelete={pipelinesToDelete} + /> + ) : null} + </> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx new file mode 100644 index 0000000000000..b967e54187ced --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/not_found_flyout.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlyout, EuiFlyoutBody, EuiCallOut } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; + +interface Props { + onClose: () => void; + pipelineName: string | string[] | null | undefined; +} + +export const PipelineNotFoundFlyout: FunctionComponent<Props> = ({ onClose, pipelineName }) => { + return ( + <EuiFlyout onClose={onClose} size="m" maxWidth={550}> + <EuiFlyoutHeader> + {pipelineName && ( + <EuiTitle id="notFoundFlyoutTitle"> + <h2>{pipelineName}</h2> + </EuiTitle> + )} + </EuiFlyoutHeader> + + <EuiFlyoutBody> + <EuiCallOut + title={ + <FormattedMessage + id="xpack.ingestPipelines.list.notFoundFlyoutMessage" + defaultMessage="Pipeline not found" + /> + } + color="danger" + iconType="alert" + /> + </EuiFlyoutBody> + </EuiFlyout> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx new file mode 100644 index 0000000000000..c93285289ff39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/table.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiInMemoryTable, EuiLink, EuiButton, EuiInMemoryTableProps } from '@elastic/eui'; + +import { BASE_PATH } from '../../../../common/constants'; +import { Pipeline } from '../../../../common/types'; + +export interface Props { + pipelines: Pipeline[]; + onReloadClick: () => void; + onEditPipelineClick: (pipelineName: string) => void; + onClonePipelineClick: (pipelineName: string) => void; + onDeletePipelineClick: (pipelineName: string[]) => void; +} + +export const PipelineTable: FunctionComponent<Props> = ({ + pipelines, + onReloadClick, + onEditPipelineClick, + onClonePipelineClick, + onDeletePipelineClick, +}) => { + const [selection, setSelection] = useState<Pipeline[]>([]); + + const tableProps: EuiInMemoryTableProps<Pipeline> = { + itemId: 'name', + isSelectable: true, + sorting: { sort: { field: 'name', direction: 'asc' } }, + selection: { + onSelectionChange: setSelection, + }, + search: { + toolsLeft: + selection.length > 0 ? ( + <EuiButton + data-test-subj="deletePipelinesButton" + onClick={() => onDeletePipelineClick(selection.map(pipeline => pipeline.name))} + color="danger" + > + <FormattedMessage + id="xpack.ingestPipelines.list.table.deletePipelinesButtonLabel" + defaultMessage="Delete {count, plural, one {pipeline} other {pipelines} }" + values={{ count: selection.length }} + /> + </EuiButton> + ) : ( + undefined + ), + toolsRight: [ + <EuiButton + key="reloadButton" + iconType="refresh" + color="secondary" + data-test-subj="reloadButton" + onClick={onReloadClick} + > + {i18n.translate('xpack.ingestPipelines.list.table.reloadButtonLabel', { + defaultMessage: 'Reload', + })} + </EuiButton>, + <EuiButton + href={`#${BASE_PATH}/create`} + fill + iconType="plusInCircle" + data-test-subj="createPipelineButton" + key="createPipelineButton" + > + {i18n.translate('xpack.ingestPipelines.list.table.createPipelineButtonLabel', { + defaultMessage: 'Create a pipeline', + })} + </EuiButton>, + ], + box: { + incremental: true, + }, + }, + pagination: { + initialPageSize: 10, + pageSizeOptions: [10, 20, 50], + }, + columns: [ + { + field: 'name', + name: i18n.translate('xpack.ingestPipelines.list.table.nameColumnTitle', { + defaultMessage: 'Name', + }), + sortable: true, + render: (name: string) => <EuiLink href={`#${BASE_PATH}?pipeline=${name}`}>{name}</EuiLink>, + }, + { + name: ( + <FormattedMessage + id="xpack.ingestPipelines.list.table.actionColumnTitle" + defaultMessage="Actions" + /> + ), + actions: [ + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.editActionLabel', { + defaultMessage: 'Edit', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.editActionDescription', { + defaultMessage: 'Edit this pipeline', + }), + type: 'icon', + icon: 'pencil', + onClick: ({ name }) => onEditPipelineClick(name), + }, + { + name: i18n.translate('xpack.ingestPipelines.list.table.cloneActionLabel', { + defaultMessage: 'Clone', + }), + description: i18n.translate('xpack.ingestPipelines.list.table.cloneActionDescription', { + defaultMessage: 'Clone this pipeline', + }), + type: 'icon', + icon: 'copy', + onClick: ({ name }) => onClonePipelineClick(name), + }, + { + isPrimary: true, + name: i18n.translate('xpack.ingestPipelines.list.table.deleteActionLabel', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.ingestPipelines.list.table.deleteActionDescription', + { defaultMessage: 'Delete this pipeline' } + ), + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: ({ name }) => onDeletePipelineClick([name]), + }, + ], + }, + ], + items: pipelines ?? [], + }; + + return <EuiInMemoryTable {...tableProps} />; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts new file mode 100644 index 0000000000000..13eb96e78adae --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -0,0 +1,125 @@ +/* + * 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 { HttpSetup } from 'src/core/public'; + +import { Pipeline } from '../../../common/types'; +import { API_BASE_PATH } from '../../../common/constants'; +import { + UseRequestConfig, + SendRequestConfig, + SendRequestResponse, + sendRequest as _sendRequest, + useRequest as _useRequest, +} from '../../shared_imports'; +import { UiMetricService } from './ui_metric'; +import { + UIM_PIPELINE_CREATE, + UIM_PIPELINE_UPDATE, + UIM_PIPELINE_DELETE, + UIM_PIPELINE_DELETE_MANY, + UIM_PIPELINE_SIMULATE, +} from '../constants'; + +export class ApiService { + private client: HttpSetup | undefined; + private uiMetricService: UiMetricService | undefined; + + private useRequest<R = any, E = Error>(config: UseRequestConfig) { + if (!this.client) { + throw new Error('Api service has not be initialized.'); + } + return _useRequest<R, E>(this.client, config); + } + + private sendRequest<D = any, E = Error>( + config: SendRequestConfig + ): Promise<SendRequestResponse<D, E>> { + if (!this.client) { + throw new Error('Api service has not be initialized.'); + } + return _sendRequest<D, E>(this.client, config); + } + + private trackUiMetric(eventName: string) { + if (!this.uiMetricService) { + throw new Error('UI metric service has not be initialized.'); + } + return this.uiMetricService.trackUiMetric(eventName); + } + + public setup(httpClient: HttpSetup, uiMetricService: UiMetricService): void { + this.client = httpClient; + this.uiMetricService = uiMetricService; + } + + public useLoadPipelines() { + return this.useRequest<Pipeline[]>({ + path: API_BASE_PATH, + method: 'get', + }); + } + + public useLoadPipeline(name: string) { + return this.useRequest<Pipeline>({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'get', + }); + } + + public async createPipeline(pipeline: Pipeline) { + const result = await this.sendRequest({ + path: API_BASE_PATH, + method: 'post', + body: JSON.stringify(pipeline), + }); + + this.trackUiMetric(UIM_PIPELINE_CREATE); + + return result; + } + + public async updatePipeline(pipeline: Pipeline) { + const { name, ...body } = pipeline; + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/${encodeURIComponent(name)}`, + method: 'put', + body: JSON.stringify(body), + }); + + this.trackUiMetric(UIM_PIPELINE_UPDATE); + + return result; + } + + public async deletePipelines(names: string[]) { + const result = this.sendRequest({ + path: `${API_BASE_PATH}/${names.map(name => encodeURIComponent(name)).join(',')}`, + method: 'delete', + }); + + this.trackUiMetric(names.length > 1 ? UIM_PIPELINE_DELETE_MANY : UIM_PIPELINE_DELETE); + + return result; + } + + public async simulatePipeline(testConfig: { + documents: object[]; + verbose?: boolean; + pipeline: Omit<Pipeline, 'name'>; + }) { + const result = await this.sendRequest({ + path: `${API_BASE_PATH}/simulate`, + method: 'post', + body: JSON.stringify(testConfig), + }); + + this.trackUiMetric(UIM_PIPELINE_SIMULATE); + + return result; + } +} + +export const apiService = new ApiService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts new file mode 100644 index 0000000000000..1ccdbbad9b1bb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { BASE_PATH } from '../../../common/constants'; +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; + +type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; + +const homeBreadcrumbText = i18n.translate('xpack.ingestPipelines.breadcrumb.pipelinesLabel', { + defaultMessage: 'Ingest Node Pipelines', +}); + +export class BreadcrumbService { + private breadcrumbs: { + [key: string]: Array<{ + text: string; + href?: string; + }>; + } = { + home: [ + { + text: homeBreadcrumbText, + }, + ], + create: [ + { + text: homeBreadcrumbText, + href: `#${BASE_PATH}`, + }, + { + text: i18n.translate('xpack.ingestPipelines.breadcrumb.createPipelineLabel', { + defaultMessage: 'Create pipeline', + }), + }, + ], + edit: [ + { + text: homeBreadcrumbText, + href: `#${BASE_PATH}`, + }, + { + text: i18n.translate('xpack.ingestPipelines.breadcrumb.editPipelineLabel', { + defaultMessage: 'Edit pipeline', + }), + }, + ], + }; + + private setBreadcrumbsHandler?: SetBreadcrumbs; + + public setup(setBreadcrumbsHandler: SetBreadcrumbs): void { + this.setBreadcrumbsHandler = setBreadcrumbsHandler; + } + + public setBreadcrumbs(type: 'create' | 'home' | 'edit'): void { + if (!this.setBreadcrumbsHandler) { + throw new Error('Breadcrumb service has not been initialized'); + } + + const newBreadcrumbs = this.breadcrumbs[type] + ? [...this.breadcrumbs[type]] + : [...this.breadcrumbs.home]; + + this.setBreadcrumbsHandler(newBreadcrumbs); + } +} + +export const breadcrumbService = new BreadcrumbService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts new file mode 100644 index 0000000000000..05fdc4b1dfb84 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/documentation.ts @@ -0,0 +1,39 @@ +/* + * 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 { DocLinksStart } from 'src/core/public'; + +export class DocumentationService { + private esDocBasePath: string = ''; + + public setup(docLinks: DocLinksStart): void { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + + this.esDocBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + } + + public getIngestNodeUrl() { + return `${this.esDocBasePath}/ingest.html`; + } + + public getProcessorsUrl() { + return `${this.esDocBasePath}/ingest-processors.html`; + } + + public getHandlingFailureUrl() { + return `${this.esDocBasePath}/handling-failure-in-pipelines.html`; + } + + public getPutPipelineApiUrl() { + return `${this.esDocBasePath}/put-pipeline-api.html`; + } + + public getSimulatePipelineApiUrl() { + return `${this.esDocBasePath}/simulate-pipeline-api.html`; + } +} + +export const documentationService = new DocumentationService(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/index.ts b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts new file mode 100644 index 0000000000000..f03a7824f8364 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { documentationService, DocumentationService } from './documentation'; + +export { uiMetricService, UiMetricService } from './ui_metric'; + +export { apiService, ApiService } from './api'; + +export { breadcrumbService, BreadcrumbService } from './breadcrumbs'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts b/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts new file mode 100644 index 0000000000000..f99bb9ba331d2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/ui_metric.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +import { UIM_APP_NAME } from '../constants'; + +export class UiMetricService { + private usageCollection: UsageCollectionSetup | undefined; + + public setup(usageCollection: UsageCollectionSetup) { + this.usageCollection = usageCollection; + } + + private track(name: string) { + if (!this.usageCollection) { + // Usage collection is an optional plugin and might be disabled + return; + } + + const { reportUiStats, METRIC_TYPE } = this.usageCollection; + reportUiStats(UIM_APP_NAME, METRIC_TYPE.COUNT, name); + } + + public trackUiMetric(eventName: string) { + return this.track(eventName); + } +} + +export const uiMetricService = new UiMetricService(); diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts new file mode 100644 index 0000000000000..7247973703804 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { IngestPipelinesPlugin } from './plugin'; + +export function plugin() { + return new IngestPipelinesPlugin(); +} diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts new file mode 100644 index 0000000000000..e9f5fd6c7f57c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -0,0 +1,39 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { CoreSetup, Plugin } from 'src/core/public'; + +import { PLUGIN_ID } from '../common/constants'; +import { uiMetricService, apiService } from './application/services'; +import { Dependencies } from './types'; + +export class IngestPipelinesPlugin implements Plugin { + public setup(coreSetup: CoreSetup, plugins: Dependencies): void { + const { management, usageCollection } = plugins; + const { http } = coreSetup; + + // Initialize services + uiMetricService.setup(usageCollection); + apiService.setup(http, uiMetricService); + + management.sections.getSection('elasticsearch')!.registerApp({ + id: PLUGIN_ID, + title: i18n.translate('xpack.ingestPipelines.appTitle', { + defaultMessage: 'Ingest Node Pipelines', + }), + mount: async params => { + const { mountManagementSection } = await import('./application/mount_management_section'); + + return await mountManagementSection(coreSetup, params); + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts new file mode 100644 index 0000000000000..cfa946ff942ec --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -0,0 +1,54 @@ +/* + * 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 { useKibana as _useKibana } from '../../../../src/plugins/kibana_react/public'; +import { AppServices } from './application'; + +export { + SendRequestConfig, + SendRequestResponse, + UseRequestConfig, + sendRequest, + useRequest, +} from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; + +export { + FormSchema, + FIELD_TYPES, + FormConfig, + useForm, + Form, + getUseField, + ValidationFuncArg, + useFormContext, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; + +export { + fieldFormatters, + fieldValidators, +} from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; + +export { + getFormRow, + Field, + JsonEditorField, +} from '../../../../src/plugins/es_ui_shared/static/forms/components'; + +export { + isJSON, + isEmptyString, +} from '../../../../src/plugins/es_ui_shared/static/validators/string'; + +export { + SectionLoading, + WithPrivileges, + AuthorizationProvider, + SectionError, + Error, + useAuthorizationContext, + NotAuthorizedSection, +} from '../../../../src/plugins/es_ui_shared/public'; + +export const useKibana = () => _useKibana<AppServices>(); diff --git a/x-pack/plugins/ingest_pipelines/public/types.ts b/x-pack/plugins/ingest_pipelines/public/types.ts new file mode 100644 index 0000000000000..91783ea04fa9a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ManagementSetup } from 'src/plugins/management/public'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; + +export interface Dependencies { + management: ManagementSetup; + usageCollection: UsageCollectionSetup; +} diff --git a/x-pack/plugins/ingest_pipelines/server/index.ts b/x-pack/plugins/ingest_pipelines/server/index.ts new file mode 100644 index 0000000000000..dc162a5d67cb6 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/server'; +import { IngestPipelinesPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new IngestPipelinesPlugin(initializerContext); +} diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts b/x-pack/plugins/ingest_pipelines/server/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts rename to x-pack/plugins/ingest_pipelines/server/lib/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts b/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts rename to x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts diff --git a/x-pack/plugins/ingest_pipelines/server/plugin.ts b/x-pack/plugins/ingest_pipelines/server/plugin.ts new file mode 100644 index 0000000000000..b27ca417c3e3c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/plugin.ts @@ -0,0 +1,59 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server'; + +import { PLUGIN_ID, PLUGIN_MIN_LICENSE_TYPE } from '../common/constants'; + +import { License } from './services'; +import { ApiRoutes } from './routes'; +import { isEsError } from './lib'; +import { Dependencies } from './types'; + +export class IngestPipelinesPlugin implements Plugin<void, void, any, any> { + private readonly logger: Logger; + private readonly license: License; + private readonly apiRoutes: ApiRoutes; + + constructor({ logger }: PluginInitializerContext) { + this.logger = logger.get(); + this.license = new License(); + this.apiRoutes = new ApiRoutes(); + } + + public setup({ http, elasticsearch }: CoreSetup, { licensing }: Dependencies) { + this.logger.debug('ingest_pipelines: setup'); + + const router = http.createRouter(); + + this.license.setup( + { + pluginId: PLUGIN_ID, + minimumLicenseType: PLUGIN_MIN_LICENSE_TYPE, + defaultErrorMessage: i18n.translate('xpack.ingestPipelines.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + this.apiRoutes.setup({ + router, + license: this.license, + lib: { + isEsError, + }, + }); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts new file mode 100644 index 0000000000000..63637eaac765d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -0,0 +1,83 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { Pipeline } from '../../../common/types'; +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + name: schema.string(), + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), +}); + +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.post( + { + path: API_BASE_PATH, + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const pipeline = req.body as Pipeline; + + const { name, description, processors, version, on_failure } = pipeline; + + try { + // Check that a pipeline with the same name doesn't already exist + const pipelineByName = await callAsCurrentUser('ingest.getPipeline', { id: name }); + + if (pipelineByName[name]) { + return res.conflict({ + body: new Error( + i18n.translate('xpack.ingestPipelines.createRoute.duplicatePipelineIdErrorMessage', { + defaultMessage: "There is already a pipeline with name '{name}'.", + values: { + name, + }, + }) + ), + }); + } + } catch (e) { + // Silently swallow error + } + + try { + const response = await callAsCurrentUser('ingest.putPipeline', { + id: name, + body: { + description, + processors, + version, + on_failure, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts new file mode 100644 index 0000000000000..4664b49a08a50 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/delete.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const paramsSchema = schema.object({ + names: schema.string(), +}); + +export const registerDeleteRoute = ({ router, license }: RouteDependencies): void => { + router.delete( + { + path: `${API_BASE_PATH}/{names}`, + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { names } = req.params; + const pipelineNames = names.split(','); + + const response: { itemsDeleted: string[]; errors: any[] } = { + itemsDeleted: [], + errors: [], + }; + + await Promise.all( + pipelineNames.map(pipelineName => { + return callAsCurrentUser('ingest.deletePipeline', { id: pipelineName }) + .then(() => response.itemsDeleted.push(pipelineName)) + .catch(e => + response.errors.push({ + name: pipelineName, + error: e, + }) + ); + }) + ); + + return res.ok({ body: response }); + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts new file mode 100644 index 0000000000000..ec92262014272 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -0,0 +1,83 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { deserializePipelines } from '../../../common/lib'; +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerGetRoutes = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + // Get all pipelines + router.get( + { path: API_BASE_PATH, validate: false }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + + try { + const pipelines = await callAsCurrentUser('ingest.getPipeline'); + + return res.ok({ body: deserializePipelines(pipelines) }); + } catch (error) { + if (isEsError(error)) { + // ES returns 404 when there are no pipelines + // Instead, we return an empty array and 200 status back to the client + if (error.status === 404) { + return res.ok({ body: [] }); + } + + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); + + // Get single pipeline + router.get( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { name } = req.params; + + try { + const pipeline = await callAsCurrentUser('ingest.getPipeline', { id: name }); + + return res.ok({ + body: { + ...pipeline[name], + name, + }, + }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/index.ts new file mode 100644 index 0000000000000..58a4bf5617659 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/index.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 { registerGetRoutes } from './get'; + +export { registerCreateRoute } from './create'; + +export { registerUpdateRoute } from './update'; + +export { registerPrivilegesRoute } from './privileges'; + +export { registerDeleteRoute } from './delete'; + +export { registerSimulateRoute } from './simulate'; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts new file mode 100644 index 0000000000000..2e1c11928959f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -0,0 +1,62 @@ +/* + * 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 { RouteDependencies } from '../../types'; +import { API_BASE_PATH, APP_CLUSTER_REQUIRED_PRIVILEGES } from '../../../common/constants'; +import { Privileges } from '../../../../../../src/plugins/es_ui_shared/public'; + +const extractMissingPrivileges = (privilegesObject: { [key: string]: boolean } = {}): string[] => + Object.keys(privilegesObject).reduce((privileges: string[], privilegeName: string): string[] => { + if (!privilegesObject[privilegeName]) { + privileges.push(privilegeName); + } + return privileges; + }, []); + +export const registerPrivilegesRoute = ({ license, router }: RouteDependencies) => { + router.get( + { + path: `${API_BASE_PATH}/privileges`, + validate: false, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { + core: { + elasticsearch: { dataClient }, + }, + } = ctx; + + const privilegesResult: Privileges = { + hasAllPrivileges: true, + missingPrivileges: { + cluster: [], + }, + }; + + try { + const { has_all_requested: hasAllPrivileges, cluster } = await dataClient.callAsCurrentUser( + 'transport.request', + { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, + }, + } + ); + + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + return res.ok({ body: privilegesResult }); + } catch (e) { + return res.internalError(e); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts new file mode 100644 index 0000000000000..ca5fc78d118fd --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + pipeline: schema.object({ + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), + }), + documents: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + verbose: schema.maybe(schema.boolean()), +}); + +export const registerSimulateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.post( + { + path: `${API_BASE_PATH}/simulate`, + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + + const { pipeline, documents, verbose } = req.body; + + try { + const response = await callAsCurrentUser('ingest.simulate', { + verbose, + body: { + pipeline, + docs: documents, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts new file mode 100644 index 0000000000000..a6fdee47f0ecf --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.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 { schema } from '@kbn/config-schema'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; + +const bodySchema = schema.object({ + description: schema.string(), + processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), + version: schema.maybe(schema.number()), + on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), +}); + +const paramsSchema = schema.object({ + name: schema.string(), +}); + +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError }, +}: RouteDependencies): void => { + router.put( + { + path: `${API_BASE_PATH}/{name}`, + validate: { + body: bodySchema, + params: paramsSchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { callAsCurrentUser } = ctx.core.elasticsearch.dataClient; + const { name } = req.params; + const { description, processors, version, on_failure } = req.body; + + try { + // Verify pipeline exists; ES will throw 404 if it doesn't + await callAsCurrentUser('ingest.getPipeline', { id: name }); + + const response = await callAsCurrentUser('ingest.putPipeline', { + id: name, + body: { + description, + processors, + version, + on_failure, + }, + }); + + return res.ok({ body: response }); + } catch (error) { + if (isEsError(error)) { + return res.customError({ + statusCode: error.statusCode, + body: error, + }); + } + + return res.internalError({ body: error }); + } + }) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/index.ts b/x-pack/plugins/ingest_pipelines/server/routes/index.ts new file mode 100644 index 0000000000000..f703a460143f4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/routes/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../types'; + +import { + registerGetRoutes, + registerCreateRoute, + registerUpdateRoute, + registerPrivilegesRoute, + registerDeleteRoute, + registerSimulateRoute, +} from './api'; + +export class ApiRoutes { + setup(dependencies: RouteDependencies) { + registerGetRoutes(dependencies); + registerCreateRoute(dependencies); + registerUpdateRoute(dependencies); + registerPrivilegesRoute(dependencies); + registerDeleteRoute(dependencies); + registerSimulateRoute(dependencies); + } +} diff --git a/x-pack/plugins/ingest_pipelines/server/services/index.ts b/x-pack/plugins/ingest_pipelines/server/services/index.ts new file mode 100644 index 0000000000000..b7a45e59549eb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/services/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { License } from './license'; diff --git a/x-pack/plugins/ingest_pipelines/server/services/license.ts b/x-pack/plugins/ingest_pipelines/server/services/license.ts new file mode 100644 index 0000000000000..0a4748bd0ace0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/services/license.ts @@ -0,0 +1,82 @@ +/* + * 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 { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute<P, Q, B>(handler: RequestHandler<P, Q, B>) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest<P, Q, B>, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } +} diff --git a/x-pack/plugins/ingest_pipelines/server/types.ts b/x-pack/plugins/ingest_pipelines/server/types.ts new file mode 100644 index 0000000000000..0135ae8e2f07d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/server/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { License } from './services'; +import { isEsError } from './lib'; + +export interface Dependencies { + licensing: LicensingPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + }; +} diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx new file mode 100644 index 0000000000000..f295f88a58e5f --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -0,0 +1,118 @@ +/* + * 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 { AppMountParameters, CoreSetup } from 'kibana/public'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import rison from 'rison-node'; +import { DashboardConstants } from '../../../../../src/plugins/dashboard/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; + +import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry'; + +import { App } from './app'; +import { EditorFrameStart } from '../types'; +import { addEmbeddableToDashboardUrl, getUrlVars, isRisonObject } from '../helpers'; +import { addHelpMenuToAppChrome } from '../help_menu_util'; +import { SavedObjectIndexStore } from '../persistence'; +import { LensPluginStartDependencies } from '../plugin'; + +export async function mountApp( + core: CoreSetup<LensPluginStartDependencies, void>, + params: AppMountParameters, + createEditorFrame: EditorFrameStart['createInstance'] +) { + const [coreStart, startDependencies] = await core.getStartServices(); + const { data: dataStart, navigation } = startDependencies; + const savedObjectsClient = coreStart.savedObjects.client; + addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); + + const instance = await createEditorFrame(); + + setReportManager( + new LensReportManager({ + storage: new Storage(localStorage), + http: core.http, + }) + ); + const updateUrlTime = (urlVars: Record<string, string>): void => { + const decoded = rison.decode(urlVars._g); + if (!isRisonObject(decoded)) { + return; + } + // @ts-ignore + decoded.time = dataStart.query.timefilter.timefilter.getTime(); + urlVars._g = rison.encode(decoded); + }; + const redirectTo = ( + routeProps: RouteComponentProps<{ id?: string }>, + addToDashboardMode: boolean, + id?: string + ) => { + if (!id) { + routeProps.history.push('/lens'); + } else if (!addToDashboardMode) { + routeProps.history.push(`/lens/edit/${id}`); + } else if (addToDashboardMode && id) { + routeProps.history.push(`/lens/edit/${id}`); + const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); + if (!lastDashboardLink || !lastDashboardLink.url) { + throw new Error('Cannot get last dashboard url'); + } + const urlVars = getUrlVars(lastDashboardLink.url); + updateUrlTime(urlVars); // we need to pass in timerange in query params directly + const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); + window.history.pushState({}, '', dashboardUrl); + } + }; + + const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { + trackUiEvent('loaded'); + const addToDashboardMode = + !!routeProps.location.search && + routeProps.location.search.includes( + DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM + ); + return ( + <App + core={coreStart} + data={dataStart} + navigation={navigation} + editorFrame={instance} + storage={new Storage(localStorage)} + docId={routeProps.match.params.id} + docStorage={new SavedObjectIndexStore(savedObjectsClient)} + redirectTo={id => redirectTo(routeProps, addToDashboardMode, id)} + addToDashboardMode={addToDashboardMode} + /> + ); + }; + + function NotFound() { + trackUiEvent('loaded_404'); + return <FormattedMessage id="xpack.lens.app404" defaultMessage="404 Not Found" />; + } + + render( + <I18nProvider> + <HashRouter> + <Switch> + <Route exact path="/lens/edit/:id" render={renderEditor} /> + <Route exact path="/lens" render={renderEditor} /> + <Route path="/lens" component={NotFound} /> + </Switch> + </HashRouter> + </I18nProvider>, + params.element + ); + return () => { + instance.unmount(); + unmountComponentAtNode(params.element); + }; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap new file mode 100644 index 0000000000000..76063d230bdb6 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`datatable_expression DatatableComponent it renders the title and value 1`] = ` +<VisualizationContainer> + <EuiBasicTable + className="lnsDataTable" + columns={ + Array [ + Object { + "field": "a", + "name": "a", + "render": [Function], + }, + Object { + "field": "b", + "name": "b", + "render": [Function], + }, + Object { + "field": "c", + "name": "c", + "render": [Function], + }, + ] + } + data-test-subj="lnsDataTable" + items={ + Array [ + Object { + "a": 10110, + "b": 1588024800000, + "c": 3, + }, + ] + } + noItemsMessage="No items found" + responsive={true} + tableLayout="auto" + /> +</VisualizationContainer> +`; diff --git a/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss index e36326d710f72..7d95d73143870 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss +++ b/x-pack/plugins/lens/public/datatable_visualization/_visualization.scss @@ -1,3 +1,13 @@ .lnsDataTable { align-self: flex-start; } + +.lnsDataTable__filter { + opacity: 0; + transition: opacity $euiAnimSpeedNormal ease-in-out; +} + +.lnsDataTable__cell:hover .lnsDataTable__filter, +.lnsDataTable__filter:focus-within { + opacity: 1; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx new file mode 100644 index 0000000000000..6d5b1153ad1bc --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -0,0 +1,156 @@ +/* + * 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 { shallow } from 'enzyme'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { datatable, DatatableComponent } from './expression'; +import { LensMultiTable } from '../types'; +import { DatatableProps } from './expression'; +import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; +import { IFieldFormat } from '../../../../../src/plugins/data/public'; +import { IAggType } from 'src/plugins/data/public'; +const executeTriggerActions = jest.fn(); + +function sampleArgs() { + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + l1: { + type: 'kibana_datatable', + columns: [ + { id: 'a', name: 'a', meta: { type: 'count' } }, + { id: 'b', name: 'b', meta: { type: 'date_histogram', aggConfigParams: { field: 'b' } } }, + { id: 'c', name: 'c', meta: { type: 'cardinality' } }, + ], + rows: [{ a: 10110, b: 1588024800000, c: 3 }], + }, + }, + }; + + const args: DatatableProps['args'] = { + title: 'My fanci metric chart', + columns: { + columnIds: ['a', 'b', 'c'], + type: 'lens_datatable_columns', + }, + }; + + return { data, args }; +} + +describe('datatable_expression', () => { + describe('datatable renders', () => { + test('it renders with the specified data and args', () => { + const { data, args } = sampleArgs(); + const result = datatable.fn(data, args, createMockExecutionContext()); + + expect(result).toEqual({ + type: 'render', + as: 'lens_datatable_renderer', + value: { data, args }, + }); + }); + }); + + describe('DatatableComponent', () => { + test('it renders the title and value', () => { + const { data, args } = sampleArgs(); + + expect( + shallow( + <DatatableComponent + data={data} + args={args} + formatFactory={x => x as IFieldFormat} + executeTriggerActions={executeTriggerActions} + getType={jest.fn()} + /> + ) + ).toMatchSnapshot(); + }); + + test('it invokes executeTriggerActions with correct context on click on top value', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + <DatatableComponent + data={{ + ...data, + dateRange: { + fromDate: new Date('2020-04-20T05:00:00.000Z'), + toDate: new Date('2020-05-03T05:00:00.000Z'), + }, + }} + args={args} + formatFactory={x => x as IFieldFormat} + executeTriggerActions={executeTriggerActions} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + /> + ); + + wrapper + .find('[data-test-subj="lensDatatableFilterOut"]') + .first() + .simulate('click'); + + expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { + data: { + data: [ + { + column: 0, + row: 0, + table: data.tables.l1, + value: 10110, + }, + ], + negate: true, + }, + timeFieldName: undefined, + }); + }); + + test('it invokes executeTriggerActions with correct context on click on timefield', () => { + const { args, data } = sampleArgs(); + + const wrapper = mountWithIntl( + <DatatableComponent + data={{ + ...data, + dateRange: { + fromDate: new Date('2020-04-20T05:00:00.000Z'), + toDate: new Date('2020-05-03T05:00:00.000Z'), + }, + }} + args={args} + formatFactory={x => x as IFieldFormat} + executeTriggerActions={executeTriggerActions} + getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} + /> + ); + + wrapper + .find('[data-test-subj="lensDatatableFilterFor"]') + .at(3) + .simulate('click'); + + expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { + data: { + data: [ + { + column: 1, + row: 0, + table: data.tables.l1, + value: 1588024800000, + }, + ], + negate: false, + }, + timeFieldName: 'b', + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 772ee13168d02..71d29be1744bb 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -7,7 +7,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { EuiBasicTable } from '@elastic/eui'; +import { I18nProvider } from '@kbn/i18n/react'; +import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { IAggType } from 'src/plugins/data/public'; import { FormatFactory, LensMultiTable } from '../types'; import { ExpressionFunctionDefinition, @@ -15,7 +17,10 @@ import { IInterpreterRenderHandlers, } from '../../../../../src/plugins/expressions/public'; import { VisualizationContainer } from '../visualization_container'; - +import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { getExecuteTriggerActions } from '../services'; export interface DatatableColumns { columnIds: string[]; } @@ -30,6 +35,12 @@ export interface DatatableProps { args: Args; } +type DatatableRenderProps = DatatableProps & { + formatFactory: FormatFactory; + executeTriggerActions: UiActionsStart['executeTriggerActions']; + getType: (name: string) => IAggType; +}; + export interface DatatableRender { type: 'render'; as: 'lens_datatable_renderer'; @@ -100,9 +111,10 @@ export const datatableColumns: ExpressionFunctionDefinition< }, }; -export const getDatatableRenderer = ( - formatFactory: Promise<FormatFactory> -): ExpressionRenderDefinition<DatatableProps> => ({ +export const getDatatableRenderer = (dependencies: { + formatFactory: Promise<FormatFactory>; + getType: Promise<(name: string) => IAggType>; +}): ExpressionRenderDefinition<DatatableProps> => ({ name: 'lens_datatable_renderer', displayName: i18n.translate('xpack.lens.datatable.visualizationName', { defaultMessage: 'Datatable', @@ -115,9 +127,18 @@ export const getDatatableRenderer = ( config: DatatableProps, handlers: IInterpreterRenderHandlers ) => { - const resolvedFormatFactory = await formatFactory; + const resolvedFormatFactory = await dependencies.formatFactory; + const executeTriggerActions = getExecuteTriggerActions(); + const resolvedGetType = await dependencies.getType; ReactDOM.render( - <DatatableComponent {...config} formatFactory={resolvedFormatFactory} />, + <I18nProvider> + <DatatableComponent + {...config} + formatFactory={resolvedFormatFactory} + executeTriggerActions={executeTriggerActions} + getType={resolvedGetType} + /> + </I18nProvider>, domNode, () => { handlers.done(); @@ -127,7 +148,7 @@ export const getDatatableRenderer = ( }, }); -function DatatableComponent(props: DatatableProps & { formatFactory: FormatFactory }) { +export function DatatableComponent(props: DatatableRenderProps) { const [firstTable] = Object.values(props.data.tables); const formatters: Record<string, ReturnType<FormatFactory>> = {}; @@ -135,6 +156,29 @@ function DatatableComponent(props: DatatableProps & { formatFactory: FormatFacto formatters[column.id] = props.formatFactory(column.formatHint); }); + const handleFilterClick = (field: string, value: unknown, colIndex: number, negate = false) => { + const col = firstTable.columns[colIndex]; + const isDateHistogram = col.meta?.type === 'date_histogram'; + const timeFieldName = negate && isDateHistogram ? undefined : col?.meta?.aggConfigParams?.field; + const rowIndex = firstTable.rows.findIndex(row => row[field] === value); + + const context: ValueClickTriggerContext = { + data: { + negate, + data: [ + { + row: rowIndex, + column: colIndex, + value, + table: firstTable, + }, + ], + }, + timeFieldName, + }; + props.executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context); + }; + return ( <VisualizationContainer> <EuiBasicTable @@ -144,23 +188,87 @@ function DatatableComponent(props: DatatableProps & { formatFactory: FormatFacto columns={props.args.columns.columnIds .map(field => { const col = firstTable.columns.find(c => c.id === field); + const colIndex = firstTable.columns.findIndex(c => c.id === field); + + const filterable = col?.meta?.type && props.getType(col.meta.type)?.type === 'buckets'; return { field, name: (col && col.name) || '', + render: (value: unknown) => { + const formattedValue = formatters[field]?.convert(value); + const fieldName = col?.meta?.aggConfigParams?.field; + + if (filterable) { + return ( + <EuiFlexGroup + className="lnsDataTable__cell" + data-test-subj="lnsDataTableCellValueFilterable" + gutterSize="xs" + > + <EuiFlexItem grow={false}>{formattedValue}</EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup + responsive={false} + gutterSize="none" + alignItems="center" + className="lnsDataTable__filter" + > + <EuiToolTip + position="bottom" + content={i18n.translate('xpack.lens.includeValueButtonTooltip', { + defaultMessage: 'Include value', + })} + > + <EuiButtonIcon + iconType="plusInCircle" + color="text" + aria-label={i18n.translate('xpack.lens.includeValueButtonAriaLabel', { + defaultMessage: `Include {value}`, + values: { + value: `${fieldName ? `${fieldName}: ` : ''}${formattedValue}`, + }, + })} + data-test-subj="lensDatatableFilterFor" + onClick={() => handleFilterClick(field, value, colIndex)} + /> + </EuiToolTip> + <EuiFlexItem grow={false}> + <EuiToolTip + position="bottom" + content={i18n.translate('xpack.lens.excludeValueButtonTooltip', { + defaultMessage: 'Exclude value', + })} + > + <EuiButtonIcon + iconType="minusInCircle" + color="text" + aria-label={i18n.translate( + 'xpack.lens.excludeValueButtonAriaLabel', + { + defaultMessage: `Exclude {value}`, + values: { + value: `${ + fieldName ? `${fieldName}: ` : '' + }${formattedValue}`, + }, + } + )} + data-test-subj="lensDatatableFilterOut" + onClick={() => handleFilterClick(field, value, colIndex, true)} + /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); + } + return <span data-test-subj="lnsDataTableCellValue">{formattedValue}</span>; + }, }; }) .filter(({ field }) => !!field)} - items={ - firstTable - ? firstTable.rows.map(row => { - const formattedRow: Record<string, unknown> = {}; - Object.entries(formatters).forEach(([columnId, formatter]) => { - formattedRow[columnId] = formatter.convert(row[columnId]); - }); - return formattedRow; - }) - : [] - } + items={firstTable ? firstTable.rows : []} /> </VisualizationContainer> ); diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index ff036aadfd4cf..44894d31da51d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -4,12 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/public'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { datatableVisualization } from './visualization'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { datatable, datatableColumns, getDatatableRenderer } from './expression'; import { EditorFrameSetup, FormatFactory } from '../types'; +import { setExecuteTriggerActions } from '../services'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +interface DatatableVisualizationPluginStartPlugins { + uiActions: UiActionsStart; + data: DataPublicPluginStart; +} export interface DatatableVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; formatFactory: Promise<FormatFactory>; @@ -20,12 +27,22 @@ export class DatatableVisualization { constructor() {} setup( - _core: CoreSetup | null, + core: CoreSetup<DatatableVisualizationPluginStartPlugins, void>, { expressions, formatFactory, editorFrame }: DatatableVisualizationPluginSetupPlugins ) { expressions.registerFunction(() => datatableColumns); expressions.registerFunction(() => datatable); - expressions.registerRenderer(() => getDatatableRenderer(formatFactory)); + expressions.registerRenderer(() => + getDatatableRenderer({ + formatFactory, + getType: core + .getStartServices() + .then(([_, { data: dataStart }]) => dataStart.search.aggs.types.get), + }) + ); editorFrame.registerVisualization(datatableVisualization); } + start(core: CoreStart, { uiActions }: DatatableVisualizationPluginStartPlugins) { + setExecuteTriggerActions(uiActions.executeTriggerActions); + } } diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 359c06a6a9ebc..21bbcce68bf36 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); }, @@ -122,8 +126,8 @@ export const datatableVisualization: Visualization< ], }, previewIcon: chartTableSVG, - // dont show suggestions for reduced versions or single-line tables - hide: table.changeType === 'reduced' || !table.isMultiRow, + // tables are hidden from suggestion bar, but used for drag & drop and chart switching + hide: true, }, ]; }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss deleted file mode 100644 index 62a7f6b023f31..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss +++ /dev/null @@ -1,50 +0,0 @@ -.lnsConfigPanel__panel { - margin-bottom: $euiSizeS; -} - -.lnsConfigPanel__row { - background: $euiColorLightestShade; - padding: $euiSizeS; - border-radius: $euiBorderRadius; - - // Add margin to the top of the next same panel - & + & { - margin-top: $euiSizeS; - } -} - -.lnsConfigPanel__addLayerBtn { - color: transparentize($euiColorMediumShade, .3); - // Remove EuiButton's default shadow to make button more subtle - // sass-lint:disable-block no-important - box-shadow: none !important; - border: 1px dashed currentColor; -} - -.lnsConfigPanel__dimension { - @include euiFontSizeS; - background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); - border-radius: $euiBorderRadius; - display: flex; - align-items: center; - margin-top: $euiSizeXS; - overflow: hidden; -} - -.lnsConfigPanel__trigger { - max-width: 100%; - display: block; -} - -.lnsConfigPanel__triggerLink { - padding: $euiSizeS; - width: 100%; - display: flex; - align-items: center; - min-height: $euiSizeXXL; -} - -.lnsConfigPanel__popover { - line-height: 0; - flex-grow: 1; -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss similarity index 100% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss new file mode 100644 index 0000000000000..1965b51f97034 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss @@ -0,0 +1,7 @@ +.lnsConfigPanel__addLayerBtn { + color: transparentize($euiColorMediumShade, .3); + // Remove EuiButton's default shadow to make button more subtle + // sass-lint:disable-block no-important + box-shadow: none !important; + border: 1px dashed currentColor; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss new file mode 100644 index 0000000000000..254807d06d386 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss @@ -0,0 +1,9 @@ +.lnsDimensionPopover { + line-height: 0; + flex-grow: 1; +} + +.lnsDimensionPopover__trigger { + max-width: 100%; + display: block; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss new file mode 100644 index 0000000000000..8f09a358dd5e4 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss @@ -0,0 +1,4 @@ +@import 'chart_switch'; +@import 'config_panel'; +@import 'dimension_popover'; +@import 'layer_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss new file mode 100644 index 0000000000000..3fbc42f9a25a0 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -0,0 +1,33 @@ +.lnsLayerPanel { + margin-bottom: $euiSizeS; +} + +.lnsLayerPanel__row { + background: $euiColorLightestShade; + padding: $euiSizeS; + border-radius: $euiBorderRadius; + + // Add margin to the top of the next same panel + & + & { + margin-top: $euiSizeS; + } +} + +.lnsLayerPanel__dimension { + @include euiFontSizeS; + background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); + border-radius: $euiBorderRadius; + display: flex; + align-items: center; + margin-top: $euiSizeXS; + overflow: hidden; +} + +.lnsLayerPanel__triggerLink { + padding: $euiSizeS; + width: 100%; + display: flex; + align-items: center; + min-height: $euiSizeXXL; +} + diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx similarity index 92% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx index 6698c9e68b98c..c8d8064e60e38 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx @@ -5,13 +5,17 @@ */ import React from 'react'; -import { createMockVisualization, createMockFramePublicAPI, createMockDatasource } from '../mocks'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; +import { + createMockVisualization, + createMockFramePublicAPI, + createMockDatasource, +} from '../../mocks'; +import { EuiKeyPadMenuItem } from '@elastic/eui'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types'; +import { Action } from '../state_management'; import { ChartSwitch } from './chart_switch'; -import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; -import { EuiKeyPadMenuItemButton } from '@elastic/eui'; -import { Action } from './state_management'; describe('chart_switch', () => { function generateVisualization(id: string): jest.Mocked<Visualization> { @@ -58,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', + }, + ]; + }), }, }; } @@ -129,7 +151,7 @@ describe('chart_switch', () => { function getMenuItem(subType: string, component: ReactWrapper) { showFlyout(component); return component - .find(EuiKeyPadMenuItemButton) + .find(EuiKeyPadMenuItem) .find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`) .first(); } @@ -309,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; @@ -329,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([]); @@ -373,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; @@ -389,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/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx similarity index 91% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx index 5e2fced577724..d73f83e75c0e4 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx @@ -10,15 +10,15 @@ import { EuiPopover, EuiPopoverTitle, EuiKeyPadMenu, - EuiKeyPadMenuItemButton, + EuiKeyPadMenuItem, EuiButtonEmpty, } from '@elastic/eui'; import { flatten } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Visualization, FramePublicAPI, Datasource } from '../../types'; -import { Action } from './state_management'; -import { getSuggestions, switchToSuggestion, Suggestion } from './suggestion_helpers'; -import { trackUiEvent } from '../../lens_ui_telemetry'; +import { Visualization, FramePublicAPI, Datasource } from '../../../types'; +import { Action } from '../state_management'; +import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; interface VisualizationSelection { visualizationId: string; @@ -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) { @@ -215,7 +224,7 @@ export function ChartSwitch(props: Props) { </EuiPopoverTitle> <EuiKeyPadMenu> {(visualizationTypes || []).map(v => ( - <EuiKeyPadMenuItemButton + <EuiKeyPadMenuItem key={`${v.visualizationId}:${v.id}`} label={<span data-test-subj="visTypeTitle">{v.label}</span>} role="menuitem" @@ -238,7 +247,7 @@ export function ChartSwitch(props: Props) { betaBadgeIconType={v.selection.dataLoss !== 'nothing' ? 'alert' : undefined} > <EuiIcon className="lnsChartSwitch__chartIcon" type={v.icon || 'empty'} size="l" /> - </EuiKeyPadMenuItemButton> + </EuiKeyPadMenuItem> ))} </EuiKeyPadMenu> </EuiPopover> @@ -250,7 +259,8 @@ export function ChartSwitch(props: Props) { function getTopSuggestion( props: Props, visualizationId: string, - newVisualization: Visualization<unknown, unknown> + newVisualization: Visualization<unknown, unknown>, + 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/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx new file mode 100644 index 0000000000000..e5d3e93258c0a --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -0,0 +1,177 @@ +/* + * 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, { useMemo, memo } from 'react'; +import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Visualization } from '../../../types'; +import { ChartSwitch } from './chart_switch'; +import { LayerPanel } from './layer_panel'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { generateId } from '../../../id_generator'; +import { removeLayer, appendLayer } from './layer_actions'; +import { ConfigPanelWrapperProps } from './types'; + +export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { + const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; + const { visualizationState } = props; + + return ( + <> + <ChartSwitch + data-test-subj="lnsChartSwitcher" + visualizationMap={props.visualizationMap} + visualizationId={props.activeVisualizationId} + visualizationState={props.visualizationState} + datasourceMap={props.datasourceMap} + datasourceStates={props.datasourceStates} + dispatch={props.dispatch} + framePublicAPI={props.framePublicAPI} + /> + {activeVisualization && visualizationState && ( + <LayerPanels {...props} activeVisualization={activeVisualization} /> + )} + </> + ); +}); + +function LayerPanels( + props: ConfigPanelWrapperProps & { + activeDatasourceId: string; + activeVisualization: Visualization; + } +) { + const { + framePublicAPI, + activeVisualization, + visualizationState, + dispatch, + activeDatasourceId, + datasourceMap, + } = props; + const setVisualizationState = useMemo( + () => (newState: unknown) => { + props.dispatch({ + type: 'UPDATE_VISUALIZATION_STATE', + visualizationId: activeVisualization.id, + newState, + clearStagedPreview: false, + }); + }, + [props.dispatch, activeVisualization] + ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: false, + }); + }, + [props.dispatch] + ); + const updateAll = useMemo( + () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { + props.dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: prevState => { + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: newDatasourceState, + isLoading: false, + }, + }, + visualization: { + ...prevState.visualization, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [props.dispatch] + ); + const layerIds = activeVisualization.getLayerIds(visualizationState); + + return ( + <EuiForm className="lnsConfigPanel"> + {layerIds.map(layerId => ( + <LayerPanel + {...props} + key={layerId} + layerId={layerId} + activeVisualization={activeVisualization} + visualizationState={visualizationState} + updateVisualization={setVisualizationState} + updateDatasource={updateDatasource} + updateAll={updateAll} + frame={framePublicAPI} + isOnlyLayer={layerIds.length === 1} + onRemoveLayer={() => { + dispatch({ + type: 'UPDATE_STATE', + subType: 'REMOVE_OR_CLEAR_LAYER', + updater: state => + removeLayer({ + activeVisualization, + layerId, + trackUiEvent, + datasourceMap, + state, + }), + }); + }} + /> + ))} + {activeVisualization.appendLayer && visualizationState && ( + <EuiFlexItem grow={true}> + <EuiToolTip + className="eui-fullWidth" + content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', { + defaultMessage: + 'Use multiple layers to combine chart types or visualize different index patterns.', + })} + position="bottom" + > + <EuiButton + className="lnsConfigPanel__addLayerBtn" + fullWidth + size="s" + data-test-subj="lnsXY_layer_add" + aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { + defaultMessage: 'Add layer', + })} + title={i18n.translate('xpack.lens.xyChart.addLayerButton', { + defaultMessage: 'Add layer', + })} + onClick={() => { + dispatch({ + type: 'UPDATE_STATE', + subType: 'ADD_LAYER', + updater: state => + appendLayer({ + activeVisualization, + generateId, + trackUiEvent, + activeDatasource: datasourceMap[activeDatasourceId], + state, + }), + }); + }} + iconType="plusInCircleFilled" + /> + </EuiToolTip> + </EuiFlexItem> + )} + </EuiForm> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx new file mode 100644 index 0000000000000..36db13b74ac4f --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiPopover } from '@elastic/eui'; +import { VisualizationDimensionGroupConfig } from '../../../types'; +import { DimensionPopoverState } from './types'; + +export function DimensionPopover({ + popoverState, + setPopoverState, + groups, + accessor, + groupId, + trigger, + panel, +}: { + popoverState: DimensionPopoverState; + setPopoverState: (newState: DimensionPopoverState) => void; + groups: VisualizationDimensionGroupConfig[]; + accessor: string; + groupId: string; + trigger: React.ReactElement; + panel: React.ReactElement; +}) { + const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(accessor)) : false; + return ( + <EuiPopover + className="lnsDimensionPopover" + anchorClassName="lnsDimensionPopover__trigger" + isOpen={ + popoverState.isOpen && + (popoverState.openId === accessor || (noMatch && popoverState.addingToGroupId === groupId)) + } + closePopover={() => { + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + }} + button={trigger} + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + {panel} + </EuiPopover> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts new file mode 100644 index 0000000000000..754b3fb5c6fde --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ConfigPanelWrapper } from './config_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts similarity index 100% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts similarity index 95% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts index cc2cbb172d23e..3d1d590664238 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts @@ -5,8 +5,8 @@ */ import _ from 'lodash'; -import { EditorFrameState } from './state_management'; -import { Datasource, Visualization } from '../../types'; +import { EditorFrameState } from '../state_management'; +import { Datasource, Visualization } from '../../../types'; interface RemoveLayerOptions { trackUiEvent: (name: string) => void; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx new file mode 100644 index 0000000000000..f7be82dd34ba3 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -0,0 +1,405 @@ +/* + * 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, { useContext, useState } from 'react'; +import { + EuiPanel, + EuiSpacer, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NativeRenderer } from '../../../native_renderer'; +import { Visualization, FramePublicAPI, StateSetter } from '../../../types'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop'; +import { LayerSettings } from './layer_settings'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { generateId } from '../../../id_generator'; +import { ConfigPanelWrapperProps, DimensionPopoverState } from './types'; +import { DimensionPopover } from './dimension_popover'; + +export function LayerPanel( + props: Exclude<ConfigPanelWrapperProps, 'state' | 'setState'> & { + frame: FramePublicAPI; + layerId: string; + isOnlyLayer: boolean; + activeVisualization: Visualization; + visualizationState: unknown; + updateVisualization: StateSetter<unknown>; + updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; + onRemoveLayer: () => void; + } +) { + const dragDropContext = useContext(DragContext); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; + const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; + if (!datasourcePublicAPI) { + return null; + } + const layerVisualizationConfigProps = { + layerId, + dragDropContext, + state: props.visualizationState, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; + + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; + + const layerDatasourceConfigProps = { + ...layerDatasourceDropProps, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + + const [popoverState, setPopoverState] = useState<DimensionPopoverState>({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some(d => d.accessors.length > 0); + + return ( + <ChildDragDropProvider {...dragDropContext}> + <EuiPanel className="lnsLayerPanel" paddingSize="s"> + <EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}> + <EuiFlexItem grow={false}> + <LayerSettings + layerId={layerId} + layerConfigProps={{ + ...layerVisualizationConfigProps, + setState: props.updateVisualization, + }} + activeVisualization={activeVisualization} + /> + </EuiFlexItem> + + {layerDatasource && ( + <EuiFlexItem className="eui-textTruncate"> + <NativeRenderer + render={layerDatasource.renderLayerPanel} + nativeProps={{ + layerId, + state: layerDatasourceState, + setState: (updater: unknown) => { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + dateRange: props.framePublicAPI.dateRange, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter(columnId => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach(columnId => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); + }); + + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + + <EuiSpacer size="s" /> + + {groups.map((group, index) => { + const newId = generateId(); + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + <EuiFormRow + className="lnsLayerPanel__row" + label={group.groupLabel} + key={index} + isInvalid={isMissing} + error={ + isMissing + ? i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', + }) + : [] + } + > + <> + {group.accessors.map(accessor => ( + <DragDrop + key={accessor} + className="lnsLayerPanel__dimension" + data-test-subj={group.dataTestSubj} + droppable={ + dragDropContext.dragging && + layerDatasource.canHandleDrop({ + ...layerDatasourceDropProps, + columnId: accessor, + filterOperations: group.filterOperations, + }) + } + onDrop={droppedItem => { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); + }} + > + <DimensionPopover + popoverState={popoverState} + setPopoverState={setPopoverState} + groups={groups} + accessor={accessor} + groupId={group.groupId} + trigger={ + <NativeRenderer + render={props.datasourceMap[datasourceId].renderDimensionTrigger} + nativeProps={{ + ...layerDatasourceConfigProps, + columnId: accessor, + filterOperations: group.filterOperations, + suggestedPriority: group.suggestedPriority, + togglePopover: () => { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + }); + } + }, + }} + /> + } + panel={ + <NativeRenderer + render={props.datasourceMap[datasourceId].renderDimensionEditor} + nativeProps={{ + ...layerDatasourceConfigProps, + core: props.core, + columnId: accessor, + filterOperations: group.filterOperations, + }} + /> + } + /> + + <EuiButtonIcon + data-test-subj="indexPattern-dimensionPopover-remove" + iconType="cross" + iconSize="s" + size="s" + color="danger" + aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { + defaultMessage: 'Remove configuration', + })} + title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { + defaultMessage: 'Remove configuration', + })} + onClick={() => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + props.activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + </DragDrop> + ))} + {group.supportsMoreColumns ? ( + <DragDrop + className="lnsLayerPanel__dimension" + data-test-subj={group.dataTestSubj} + droppable={ + dragDropContext.dragging && + layerDatasource.canHandleDrop({ + ...layerDatasourceDropProps, + columnId: newId, + filterOperations: group.filterOperations, + }) + } + onDrop={droppedItem => { + const dropSuccess = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: group.filterOperations, + }); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } + }} + > + <DimensionPopover + popoverState={popoverState} + setPopoverState={setPopoverState} + groups={groups} + accessor={newId} + groupId={group.groupId} + trigger={ + <div className="lnsLayerPanel__triggerLink"> + <EuiButtonEmpty + iconType="plusInCircleFilled" + data-test-subj="lns-empty-dimension" + aria-label={i18n.translate('xpack.lens.configure.addConfig', { + defaultMessage: 'Add a configuration', + })} + title={i18n.translate('xpack.lens.configure.addConfig', { + defaultMessage: 'Add a configuration', + })} + onClick={() => { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: group.groupId, + }); + } + }} + size="xs" + > + <FormattedMessage + id="xpack.lens.configure.emptyConfig" + defaultMessage="Drop a field here" + /> + </EuiButtonEmpty> + </div> + } + panel={ + <NativeRenderer + render={props.datasourceMap[datasourceId].renderDimensionEditor} + nativeProps={{ + ...layerDatasourceConfigProps, + core: props.core, + columnId: newId, + filterOperations: group.filterOperations, + suggestedPriority: group.suggestedPriority, + + setState: (newState: unknown) => { + props.updateAll( + datasourceId, + newState, + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: null, // clear now that dimension exists + }); + }, + }} + /> + } + /> + </DragDrop> + ) : null} + </> + </EuiFormRow> + ); + })} + + <EuiSpacer size="s" /> + + <EuiFlexGroup justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + size="xs" + iconType="trash" + color="danger" + data-test-subj="lns_layer_remove" + onClick={() => { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: 'Delete layer', + })} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </ChildDragDropProvider> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx new file mode 100644 index 0000000000000..57588e31590b4 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx @@ -0,0 +1,53 @@ +/* + * 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 } from 'react'; +import { EuiPopover, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { NativeRenderer } from '../../../native_renderer'; +import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; + +export function LayerSettings({ + layerId, + activeVisualization, + layerConfigProps, +}: { + layerId: string; + activeVisualization: Visualization; + layerConfigProps: VisualizationLayerWidgetProps; +}) { + const [isOpen, setIsOpen] = useState(false); + + if (!activeVisualization.renderLayerContextMenu) { + return null; + } + + return ( + <EuiPopover + id={`lnsLayerPopover_${layerId}`} + panelPaddingSize="s" + ownFocus + button={ + <EuiButtonIcon + iconType={activeVisualization.getLayerContextMenuIcon?.(layerConfigProps) || 'gear'} + aria-label={i18n.translate('xpack.lens.editLayerSettings', { + defaultMessage: 'Edit layer settings', + })} + onClick={() => setIsOpen(!isOpen)} + data-test-subj="lns_layer_settings" + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + anchorPosition="leftUp" + > + <NativeRenderer + render={activeVisualization.renderLayerContextMenu} + nativeProps={layerConfigProps} + /> + </EuiPopover> + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts new file mode 100644 index 0000000000000..df510d3648f8c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from '../state_management'; +import { + Visualization, + FramePublicAPI, + Datasource, + DatasourceDimensionEditorProps, +} from '../../../types'; + +export interface ConfigPanelWrapperProps { + activeDatasourceId: string; + visualizationState: unknown; + visualizationMap: Record<string, Visualization>; + activeVisualizationId: string | null; + dispatch: (action: Action) => void; + framePublicAPI: FramePublicAPI; + datasourceMap: Record<string, Datasource>; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; + core: DatasourceDimensionEditorProps['core']; +} + +export interface DimensionPopoverState { + isOpen: boolean; + openId: string | null; + addingToGroupId: string | null; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx deleted file mode 100644 index da812e948b23f..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ /dev/null @@ -1,655 +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 React, { useMemo, useContext, memo, useState } from 'react'; -import { - EuiPanel, - EuiSpacer, - EuiPopover, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiToolTip, - EuiButton, - EuiForm, - EuiFormRow, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { NativeRenderer } from '../../native_renderer'; -import { Action } from './state_management'; -import { - Visualization, - FramePublicAPI, - Datasource, - VisualizationLayerWidgetProps, - DatasourceDimensionEditorProps, - StateSetter, -} from '../../types'; -import { DragContext, DragDrop, ChildDragDropProvider } from '../../drag_drop'; -import { ChartSwitch } from './chart_switch'; -import { trackUiEvent } from '../../lens_ui_telemetry'; -import { generateId } from '../../id_generator'; -import { removeLayer, appendLayer } from './layer_actions'; - -interface ConfigPanelWrapperProps { - activeDatasourceId: string; - visualizationState: unknown; - visualizationMap: Record<string, Visualization>; - activeVisualizationId: string | null; - dispatch: (action: Action) => void; - framePublicAPI: FramePublicAPI; - datasourceMap: Record<string, Datasource>; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; - core: DatasourceDimensionEditorProps['core']; -} - -export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { - const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; - const { visualizationState } = props; - - return ( - <> - <ChartSwitch - data-test-subj="lnsChartSwitcher" - visualizationMap={props.visualizationMap} - visualizationId={props.activeVisualizationId} - visualizationState={props.visualizationState} - datasourceMap={props.datasourceMap} - datasourceStates={props.datasourceStates} - dispatch={props.dispatch} - framePublicAPI={props.framePublicAPI} - /> - {activeVisualization && visualizationState && ( - <LayerPanels {...props} activeVisualization={activeVisualization} /> - )} - </> - ); -}); - -function LayerPanels( - props: ConfigPanelWrapperProps & { - activeDatasourceId: string; - activeVisualization: Visualization; - } -) { - const { - framePublicAPI, - activeVisualization, - visualizationState, - dispatch, - activeDatasourceId, - datasourceMap, - } = props; - const setVisualizationState = useMemo( - () => (newState: unknown) => { - props.dispatch({ - type: 'UPDATE_VISUALIZATION_STATE', - visualizationId: activeVisualization.id, - newState, - clearStagedPreview: false, - }); - }, - [props.dispatch, activeVisualization] - ); - const updateDatasource = useMemo( - () => (datasourceId: string, newState: unknown) => { - props.dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: () => newState, - datasourceId, - clearStagedPreview: false, - }); - }, - [props.dispatch] - ); - const updateAll = useMemo( - () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { - props.dispatch({ - type: 'UPDATE_STATE', - subType: 'UPDATE_ALL_STATES', - updater: prevState => { - return { - ...prevState, - datasourceStates: { - ...prevState.datasourceStates, - [datasourceId]: { - state: newDatasourceState, - isLoading: false, - }, - }, - visualization: { - ...prevState.visualization, - state: newVisualizationState, - }, - stagedPreview: undefined, - }; - }, - }); - }, - [props.dispatch] - ); - const layerIds = activeVisualization.getLayerIds(visualizationState); - - return ( - <EuiForm className="lnsConfigPanel"> - {layerIds.map(layerId => ( - <LayerPanel - {...props} - key={layerId} - layerId={layerId} - activeVisualization={activeVisualization} - visualizationState={visualizationState} - updateVisualization={setVisualizationState} - updateDatasource={updateDatasource} - updateAll={updateAll} - frame={framePublicAPI} - isOnlyLayer={layerIds.length === 1} - onRemoveLayer={() => { - dispatch({ - type: 'UPDATE_STATE', - subType: 'REMOVE_OR_CLEAR_LAYER', - updater: state => - removeLayer({ - activeVisualization, - layerId, - trackUiEvent, - datasourceMap, - state, - }), - }); - }} - /> - ))} - {activeVisualization.appendLayer && ( - <EuiFlexItem grow={true}> - <EuiToolTip - className="eui-fullWidth" - content={i18n.translate('xpack.lens.xyChart.addLayerTooltip', { - defaultMessage: - 'Use multiple layers to combine chart types or visualize different index patterns.', - })} - position="bottom" - > - <EuiButton - className="lnsConfigPanel__addLayerBtn" - fullWidth - size="s" - data-test-subj="lnsXY_layer_add" - aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', { - defaultMessage: 'Add layer', - })} - title={i18n.translate('xpack.lens.xyChart.addLayerButton', { - defaultMessage: 'Add layer', - })} - onClick={() => { - dispatch({ - type: 'UPDATE_STATE', - subType: 'ADD_LAYER', - updater: state => - appendLayer({ - activeVisualization, - generateId, - trackUiEvent, - activeDatasource: datasourceMap[activeDatasourceId], - state, - }), - }); - }} - iconType="plusInCircleFilled" - /> - </EuiToolTip> - </EuiFlexItem> - )} - </EuiForm> - ); -} - -function LayerPanel( - props: Exclude<ConfigPanelWrapperProps, 'state' | 'setState'> & { - frame: FramePublicAPI; - layerId: string; - isOnlyLayer: boolean; - activeVisualization: Visualization; - visualizationState: unknown; - updateVisualization: StateSetter<unknown>; - updateDatasource: (datasourceId: string, newState: unknown) => void; - updateAll: ( - datasourceId: string, - newDatasourcestate: unknown, - newVisualizationState: unknown - ) => void; - onRemoveLayer: () => void; - } -) { - const dragDropContext = useContext(DragContext); - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; - const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - if (!datasourcePublicAPI) { - return null; - } - const layerVisualizationConfigProps = { - layerId, - dragDropContext, - state: props.visualizationState, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - const datasourceId = datasourcePublicAPI.datasourceId; - const layerDatasourceState = props.datasourceStates[datasourceId].state; - const layerDatasource = props.datasourceMap[datasourceId]; - - const layerDatasourceDropProps = { - layerId, - dragDropContext, - state: layerDatasourceState, - setState: (newState: unknown) => { - props.updateDatasource(datasourceId, newState); - }, - }; - - const layerDatasourceConfigProps = { - ...layerDatasourceDropProps, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - - const [popoverState, setPopoverState] = useState<{ - isOpen: boolean; - openId: string | null; - addingToGroupId: string | null; - }>({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - - const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); - const isEmptyLayer = !groups.some(d => d.accessors.length > 0); - - function wrapInPopover( - id: string, - groupId: string, - trigger: React.ReactElement, - panel: React.ReactElement - ) { - const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false; - return ( - <EuiPopover - className="lnsConfigPanel__popover" - anchorClassName="lnsConfigPanel__trigger" - isOpen={ - popoverState.isOpen && - (popoverState.openId === id || (noMatch && popoverState.addingToGroupId === groupId)) - } - closePopover={() => { - setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); - }} - button={trigger} - anchorPosition="leftUp" - withTitle - panelPaddingSize="s" - > - {panel} - </EuiPopover> - ); - } - - return ( - <ChildDragDropProvider {...dragDropContext}> - <EuiPanel className="lnsConfigPanel__panel" paddingSize="s"> - <EuiFlexGroup gutterSize="s" alignItems="flexStart" responsive={false}> - <EuiFlexItem grow={false}> - <LayerSettings - layerId={layerId} - layerConfigProps={{ - ...layerVisualizationConfigProps, - setState: props.updateVisualization, - }} - activeVisualization={activeVisualization} - /> - </EuiFlexItem> - - {layerDatasource && ( - <EuiFlexItem className="eui-textTruncate"> - <NativeRenderer - render={layerDatasource.renderLayerPanel} - nativeProps={{ - layerId, - state: layerDatasourceState, - setState: (updater: unknown) => { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - dateRange: props.framePublicAPI.dateRange, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter(columnId => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach(columnId => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - </EuiFlexItem> - )} - </EuiFlexGroup> - - <EuiSpacer size="s" /> - - {groups.map((group, index) => { - const newId = generateId(); - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - <EuiFormRow - className="lnsConfigPanel__row" - label={group.groupLabel} - key={index} - isInvalid={isMissing} - error={ - isMissing - ? i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - }) - : [] - } - > - <> - {group.accessors.map(accessor => ( - <DragDrop - key={accessor} - className="lnsConfigPanel__dimension" - data-test-subj={group.dataTestSubj} - droppable={ - dragDropContext.dragging && - layerDatasource.canHandleDrop({ - ...layerDatasourceDropProps, - columnId: accessor, - filterOperations: group.filterOperations, - }) - } - onDrop={droppedItem => { - layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: accessor, - filterOperations: group.filterOperations, - }); - }} - > - {wrapInPopover( - accessor, - group.groupId, - <NativeRenderer - render={props.datasourceMap[datasourceId].renderDimensionTrigger} - nativeProps={{ - ...layerDatasourceConfigProps, - columnId: accessor, - filterOperations: group.filterOperations, - suggestedPriority: group.suggestedPriority, - togglePopover: () => { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: accessor, - addingToGroupId: null, // not set for existing dimension - }); - } - }, - }} - />, - <NativeRenderer - render={props.datasourceMap[datasourceId].renderDimensionEditor} - nativeProps={{ - ...layerDatasourceConfigProps, - core: props.core, - columnId: accessor, - filterOperations: group.filterOperations, - }} - /> - )} - - <EuiButtonIcon - data-test-subj="indexPattern-dimensionPopover-remove" - iconType="cross" - iconSize="s" - size="s" - color="danger" - aria-label={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { - defaultMessage: 'Remove configuration', - })} - title={i18n.translate('xpack.lens.indexPattern.removeColumnLabel', { - defaultMessage: 'Remove configuration', - })} - onClick={() => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: accessor, - prevState: layerDatasourceState, - }), - props.activeVisualization.removeDimension({ - layerId, - columnId: accessor, - prevState: props.visualizationState, - }) - ); - }} - /> - </DragDrop> - ))} - {group.supportsMoreColumns ? ( - <DragDrop - className="lnsConfigPanel__dimension" - data-test-subj={group.dataTestSubj} - droppable={ - dragDropContext.dragging && - layerDatasource.canHandleDrop({ - ...layerDatasourceDropProps, - columnId: newId, - filterOperations: group.filterOperations, - }) - } - onDrop={droppedItem => { - const dropSuccess = layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: newId, - filterOperations: group.filterOperations, - }); - if (dropSuccess) { - props.updateVisualization( - activeVisualization.setDimension({ - layerId, - groupId: group.groupId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - } - }} - > - {wrapInPopover( - newId, - group.groupId, - <div className="lnsConfigPanel__triggerLink"> - <EuiButtonEmpty - iconType="plusInCircleFilled" - data-test-subj="lns-empty-dimension" - aria-label={i18n.translate('xpack.lens.configure.addConfig', { - defaultMessage: 'Add a configuration', - })} - title={i18n.translate('xpack.lens.configure.addConfig', { - defaultMessage: 'Add a configuration', - })} - onClick={() => { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: newId, - addingToGroupId: group.groupId, - }); - } - }} - size="xs" - > - <FormattedMessage - id="xpack.lens.configure.emptyConfig" - defaultMessage="Drop a field here" - /> - </EuiButtonEmpty> - </div>, - <NativeRenderer - render={props.datasourceMap[datasourceId].renderDimensionEditor} - nativeProps={{ - ...layerDatasourceConfigProps, - core: props.core, - columnId: newId, - filterOperations: group.filterOperations, - suggestedPriority: group.suggestedPriority, - - setState: (newState: unknown) => { - props.updateAll( - datasourceId, - newState, - activeVisualization.setDimension({ - layerId, - groupId: group.groupId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - setPopoverState({ - isOpen: true, - openId: newId, - addingToGroupId: null, // clear now that dimension exists - }); - }, - }} - /> - )} - </DragDrop> - ) : null} - </> - </EuiFormRow> - ); - })} - - <EuiSpacer size="s" /> - - <EuiFlexGroup justifyContent="center"> - <EuiFlexItem grow={false}> - <EuiButtonEmpty - size="xs" - iconType="trash" - color="danger" - data-test-subj="lns_layer_remove" - onClick={() => { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; - - if (el?.blur) { - el.blur(); - } - - onRemoveLayer(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - </EuiButtonEmpty> - </EuiFlexItem> - </EuiFlexGroup> - </EuiPanel> - </ChildDragDropProvider> - ); -} - -function LayerSettings({ - layerId, - activeVisualization, - layerConfigProps, -}: { - layerId: string; - activeVisualization: Visualization; - layerConfigProps: VisualizationLayerWidgetProps; -}) { - const [isOpen, setIsOpen] = useState(false); - - if (!activeVisualization.renderLayerContextMenu) { - return null; - } - - return ( - <EuiPopover - id={`lnsLayerPopover_${layerId}`} - panelPaddingSize="s" - ownFocus - button={ - <EuiButtonIcon - iconType={activeVisualization.getLayerContextMenuIcon?.(layerConfigProps) || 'gear'} - aria-label={i18n.translate('xpack.lens.editLayerSettings', { - defaultMessage: 'Edit layer settings', - })} - onClick={() => setIsOpen(!isOpen)} - data-test-subj="lns_layer_settings" - /> - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - anchorPosition="leftUp" - > - <NativeRenderer - render={activeVisualization.renderLayerContextMenu} - nativeProps={layerConfigProps} - /> - </EuiPopover> - ); -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index b72d9081bbc91..5cd803e7cebbc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -16,7 +16,7 @@ import { } from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; -import { ConfigPanelWrapper } from './config_panel_wrapper'; +import { ConfigPanelWrapper } from './config_panel'; import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss index d4b27c6c98b3c..5e3726c953f11 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -1,8 +1,6 @@ -@import 'chart_switch'; -@import 'config_panel_wrapper'; +@import 'config_panel/index'; @import 'data_panel_wrapper'; @import 'expression_renderer'; @import 'frame_layout'; @import 'suggestion_panel'; @import 'workspace_panel_wrapper'; - 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 eabcdfa7a24ab..949ae1f43448e 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<string, Visualization>; 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/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 0ef5f6d1a5470..6cd15e3c93e4e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -96,9 +96,7 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe public supportedTriggers() { switch (this.savedVis.visualizationType) { case 'lnsXY': - // TODO: case 'lnsDatatable': - return [VIS_EVENT_TO_TRIGGER.filter]; - + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; case 'lnsMetric': default: return []; 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 50cd1ad8bd53a..e684fe8b3b5d6 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<Visualization> { 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/helpers/index.ts b/x-pack/plugins/lens/public/helpers/index.ts index f464b5dcc97a3..69a22d19ffbef 100644 --- a/x-pack/plugins/lens/public/helpers/index.ts +++ b/x-pack/plugins/lens/public/helpers/index.ts @@ -5,3 +5,4 @@ */ export { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; +export { isRisonObject } from './is_rison_object'; diff --git a/x-pack/plugins/lens/public/helpers/is_rison_object.ts b/x-pack/plugins/lens/public/helpers/is_rison_object.ts new file mode 100644 index 0000000000000..81976c9a83320 --- /dev/null +++ b/x-pack/plugins/lens/public/helpers/is_rison_object.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RisonObject, RisonValue } from 'rison-node'; +import { isObject } from 'lodash'; + +export const isRisonObject = (value: RisonValue): value is RisonObject => { + return isObject(value); +}; 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 5f35ef650a08c..0000000000000 --- 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<typeof getAutoDate>; - - 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 97a46f4a3e176..0000000000000 --- 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/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 074c40759f8d8..9df79aa9e0908 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/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index b3bd08d3bbfbe..583832aafcbe8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -192,7 +192,7 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens return ( <EuiLink id={columnId} - className="lnsConfigPanel__triggerLink" + className="lnsLayerPanel__triggerLink" onClick={() => { props.togglePopover(); }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index fe14f472341af..73fd144b9c7f8 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<IndexPatternDatasourceStartPlugins>, - { 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 e4f3677d0fe88..06635e663361d 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/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 2008b326a539c..f26fd39a60c0e 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 }), }), }, }), @@ -1549,6 +1552,62 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions[0].table.columns.length).toBe(1); expect(suggestions[0].table.columns[0].operation.label).toBe('Sum of field1'); }); + + it('contains a reordering suggestion when there are exactly 2 buckets', () => { + const initialState = testInitialState(); + const state: IndexPatternPrivateState = { + indexPatternRefs: [], + existingFields: {}, + currentIndexPatternId: '1', + indexPatterns: expectedIndexPatterns, + showEmptyFields: true, + layers: { + first: { + ...initialState.layers.first, + columns: { + id1: { + label: 'Date histogram', + dataType: 'date', + isBucketed: true, + + operationType: 'date_histogram', + sourceField: 'field2', + params: { + interval: 'd', + }, + }, + id2: { + label: 'Top 5', + dataType: 'string', + isBucketed: true, + + operationType: 'terms', + sourceField: 'field1', + params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'asc' }, + }, + id3: { + label: 'Average of field1', + dataType: 'number', + isBucketed: false, + + operationType: 'avg', + sourceField: 'field1', + }, + }, + columnOrder: ['id1', 'id2', 'id3'], + }, + }, + }; + + const suggestions = getDatasourceSuggestionsFromCurrentState(state); + expect(suggestions).toContainEqual( + expect.objectContaining({ + table: expect.objectContaining({ + changeType: 'reorder', + }), + }) + ); + }); }); }); 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 2b3e976a77ea7..487c1bf759fc2 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, @@ -478,7 +486,7 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId layerId, updatedLayer, label: getNestedTitle([layer.columns[secondBucket], layer.columns[firstBucket]]), - changeType: 'extended', + changeType: 'reorder', }); } 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 3ab51b5fa3f2b..1308fa3b7ca60 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/public/metric_visualization/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/metric_visualization.tsx index 73b8019a31eaa..04a1c3865f22d 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<State, PersistableState> = { }, ], + getVisualizationTypeId() { + return 'lnsMetric'; + }, + clearLayer(state) { return { ...state, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts new file mode 100644 index 0000000000000..a6acc61922177 --- /dev/null +++ b/x-pack/plugins/lens/public/plugin.ts @@ -0,0 +1,103 @@ +/* + * 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 { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; +import { VisualizationsSetup } from 'src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; +import { EditorFrameService } from './editor_frame_service'; +import { IndexPatternDatasource } from './indexpattern_datasource'; +import { XyVisualization } from './xy_visualization'; +import { MetricVisualization } from './metric_visualization'; +import { DatatableVisualization } from './datatable_visualization'; +import { stopReportManager } from './lens_ui_telemetry'; + +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { EditorFrameStart } from './types'; +import { getLensAliasConfig } from './vis_type_alias'; + +import './index.scss'; + +export interface LensPluginSetupDependencies { + kibanaLegacy: KibanaLegacySetup; + expressions: ExpressionsSetup; + data: DataPublicPluginSetup; + embeddable?: EmbeddableSetup; + visualizations: VisualizationsSetup; +} + +export interface LensPluginStartDependencies { + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + expressions: ExpressionsStart; + navigation: NavigationPublicPluginStart; + uiActions: UiActionsStart; +} + +export class LensPlugin { + private datatableVisualization: DatatableVisualization; + private editorFrameService: EditorFrameService; + private createEditorFrame: EditorFrameStart['createInstance'] | null = null; + private indexpatternDatasource: IndexPatternDatasource; + private xyVisualization: XyVisualization; + private metricVisualization: MetricVisualization; + + constructor() { + this.datatableVisualization = new DatatableVisualization(); + this.editorFrameService = new EditorFrameService(); + this.indexpatternDatasource = new IndexPatternDatasource(); + this.xyVisualization = new XyVisualization(); + this.metricVisualization = new MetricVisualization(); + } + + setup( + core: CoreSetup<LensPluginStartDependencies, void>, + { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies + ) { + const editorFrameSetupInterface = this.editorFrameService.setup(core, { + data, + embeddable, + expressions, + }); + const dependencies = { + expressions, + data, + editorFrame: editorFrameSetupInterface, + formatFactory: core + .getStartServices() + .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), + }; + this.indexpatternDatasource.setup(core, dependencies); + this.xyVisualization.setup(core, dependencies); + this.datatableVisualization.setup(core, dependencies); + this.metricVisualization.setup(core, dependencies); + + visualizations.registerAlias(getLensAliasConfig()); + + kibanaLegacy.registerLegacyApp({ + id: 'lens', + title: NOT_INTERNATIONALIZED_PRODUCT_NAME, + mount: async (params: AppMountParameters) => { + const { mountApp } = await import('./app_plugin/mounter'); + return mountApp(core, params, this.createEditorFrame!); + }, + }); + } + + start(core: CoreStart, startDependencies: LensPluginStartDependencies) { + this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; + this.xyVisualization.start(core, startDependencies); + this.datatableVisualization.start(core, startDependencies); + } + + stop() { + stopReportManager(); + } +} diff --git a/x-pack/plugins/lens/public/plugin.tsx b/x-pack/plugins/lens/public/plugin.tsx deleted file mode 100644 index 8d760eb0df501..0000000000000 --- a/x-pack/plugins/lens/public/plugin.tsx +++ /dev/null @@ -1,208 +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 React from 'react'; -import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; -import { render, unmountComponentAtNode } from 'react-dom'; -import rison, { RisonObject, RisonValue } from 'rison-node'; -import { isObject } from 'lodash'; - -import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; -import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; -import { VisualizationsSetup } from 'src/plugins/visualizations/public'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; -import { DashboardConstants } from '../../../../src/plugins/dashboard/public'; -import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { EditorFrameService } from './editor_frame_service'; -import { IndexPatternDatasource } from './indexpattern_datasource'; -import { addHelpMenuToAppChrome } from './help_menu_util'; -import { SavedObjectIndexStore } from './persistence'; -import { XyVisualization } from './xy_visualization'; -import { MetricVisualization } from './metric_visualization'; -import { DatatableVisualization } from './datatable_visualization'; -import { App } from './app_plugin'; -import { - LensReportManager, - setReportManager, - stopReportManager, - trackUiEvent, -} from './lens_ui_telemetry'; - -import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; -import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; -import { addEmbeddableToDashboardUrl, getUrlVars } from './helpers'; -import { EditorFrameStart } from './types'; -import { getLensAliasConfig } from './vis_type_alias'; - -import './index.scss'; - -export interface LensPluginSetupDependencies { - kibanaLegacy: KibanaLegacySetup; - expressions: ExpressionsSetup; - data: DataPublicPluginSetup; - embeddable?: EmbeddableSetup; - visualizations: VisualizationsSetup; -} - -export interface LensPluginStartDependencies { - data: DataPublicPluginStart; - embeddable: EmbeddableStart; - expressions: ExpressionsStart; - navigation: NavigationPublicPluginStart; - uiActions: UiActionsStart; -} - -export const isRisonObject = (value: RisonValue): value is RisonObject => { - return isObject(value); -}; -export class LensPlugin { - private datatableVisualization: DatatableVisualization; - private editorFrameService: EditorFrameService; - private createEditorFrame: EditorFrameStart['createInstance'] | null = null; - private indexpatternDatasource: IndexPatternDatasource; - private xyVisualization: XyVisualization; - private metricVisualization: MetricVisualization; - - constructor() { - this.datatableVisualization = new DatatableVisualization(); - this.editorFrameService = new EditorFrameService(); - this.indexpatternDatasource = new IndexPatternDatasource(); - this.xyVisualization = new XyVisualization(); - this.metricVisualization = new MetricVisualization(); - } - - setup( - core: CoreSetup<LensPluginStartDependencies, void>, - { kibanaLegacy, expressions, data, embeddable, visualizations }: LensPluginSetupDependencies - ) { - const editorFrameSetupInterface = this.editorFrameService.setup(core, { - data, - embeddable, - expressions, - }); - const dependencies = { - expressions, - data, - editorFrame: editorFrameSetupInterface, - formatFactory: core - .getStartServices() - .then(([_, { data: dataStart }]) => dataStart.fieldFormats.deserialize), - }; - this.indexpatternDatasource.setup(core, dependencies); - this.xyVisualization.setup(core, dependencies); - this.datatableVisualization.setup(core, dependencies); - this.metricVisualization.setup(core, dependencies); - - visualizations.registerAlias(getLensAliasConfig()); - - kibanaLegacy.registerLegacyApp({ - id: 'lens', - title: NOT_INTERNATIONALIZED_PRODUCT_NAME, - mount: async (params: AppMountParameters) => { - const [coreStart, startDependencies] = await core.getStartServices(); - const { data: dataStart, navigation } = startDependencies; - const savedObjectsClient = coreStart.savedObjects.client; - addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); - - const instance = await this.createEditorFrame!(); - - setReportManager( - new LensReportManager({ - storage: new Storage(localStorage), - http: core.http, - }) - ); - const updateUrlTime = (urlVars: Record<string, string>): void => { - const decoded = rison.decode(urlVars._g); - if (!isRisonObject(decoded)) { - return; - } - // @ts-ignore - decoded.time = dataStart.query.timefilter.timefilter.getTime(); - urlVars._g = rison.encode(decoded); - }; - const redirectTo = ( - routeProps: RouteComponentProps<{ id?: string }>, - addToDashboardMode: boolean, - id?: string - ) => { - if (!id) { - routeProps.history.push('/lens'); - } else if (!addToDashboardMode) { - routeProps.history.push(`/lens/edit/${id}`); - } else if (addToDashboardMode && id) { - routeProps.history.push(`/lens/edit/${id}`); - const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); - if (!lastDashboardLink || !lastDashboardLink.url) { - throw new Error('Cannot get last dashboard url'); - } - const urlVars = getUrlVars(lastDashboardLink.url); - updateUrlTime(urlVars); // we need to pass in timerange in query params directly - const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); - window.history.pushState({}, '', dashboardUrl); - } - }; - - const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { - trackUiEvent('loaded'); - const addToDashboardMode = - !!routeProps.location.search && - routeProps.location.search.includes( - DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM - ); - return ( - <App - core={coreStart} - data={dataStart} - navigation={navigation} - editorFrame={instance} - storage={new Storage(localStorage)} - docId={routeProps.match.params.id} - docStorage={new SavedObjectIndexStore(savedObjectsClient)} - redirectTo={id => redirectTo(routeProps, addToDashboardMode, id)} - addToDashboardMode={addToDashboardMode} - /> - ); - }; - - function NotFound() { - trackUiEvent('loaded_404'); - return <FormattedMessage id="xpack.lens.app404" defaultMessage="404 Not Found" />; - } - - render( - <I18nProvider> - <HashRouter> - <Switch> - <Route exact path="/lens/edit/:id" render={renderEditor} /> - <Route exact path="/lens" render={renderEditor} /> - <Route path="/lens" component={NotFound} /> - </Switch> - </HashRouter> - </I18nProvider>, - params.element - ); - return () => { - instance.unmount(); - unmountComponentAtNode(params.element); - }; - }, - }); - } - - start(core: CoreStart, startDependencies: LensPluginStartDependencies) { - this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; - this.xyVisualization.start(core, startDependencies); - } - - stop() { - stopReportManager(); - } -} diff --git a/x-pack/plugins/lens/public/services.ts b/x-pack/plugins/lens/public/services.ts new file mode 100644 index 0000000000000..a66743dde2661 --- /dev/null +++ b/x-pack/plugins/lens/public/services.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createGetterSetter } from '../../../../src/plugins/kibana_utils/public'; +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; + +export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter< + UiActionsStart['executeTriggerActions'] +>('executeTriggerActions'); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 0b432c0c70727..04efc642793b0 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -103,9 +103,16 @@ export interface TableSuggestion { * * `unchanged` means the table is the same in the currently active configuration * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) + * * `reorder` means the table columns have changed order, which change the data as well * * `layers` means the change is a change to the layer structure, not to the table */ -export type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended' | 'layers'; +export type TableChangeType = + | 'initial' + | 'unchanged' + | 'reduced' + | 'extended' + | 'reorder' + | 'layers'; export interface DatasourceSuggestion<T = unknown> { state: T; @@ -273,7 +280,7 @@ export type VisualizationLayerWidgetProps<T = unknown> = VisualizationConfigProp setState: (newState: T) => void; }; -type VisualizationDimensionGroupConfig = SharedDimensionProps & { +export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; /** ID is passed back to visualization. For example, `x` */ @@ -312,6 +319,10 @@ export interface SuggestionRequest<T = unknown> { * 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; } /** @@ -368,55 +379,91 @@ export interface VisualizationType { } export interface Visualization<T = unknown, P = unknown> { + /** Plugin ID, such as "lnsXY" */ id: string; + /** + * Initialize is allowed to modify the state stored in memory. The initialize function + * is called with a previous state in two cases: + * - Loadingn from a saved visualization + * - When using suggestions, the suggested state is passed in + */ + initialize: (frame: FramePublicAPI, state?: P) => T; + /** + * Can remove any state that should not be persisted to saved object, such as UI state + */ + getPersistableState: (state: T) => P; + + /** + * Visualizations must provide at least one type for the chart switcher, + * 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. + */ + switchVisualizationType?: (visualizationTypeId: string, state: T) => T; + /** Description is displayed as the clickable text in the chart switcher */ + getDescription: (state: T) => { icon?: IconType; label: string }; + /** Frame needs to know which layers the visualization is currently using */ getLayerIds: (state: T) => string[]; + /** Reset button on each layer triggers this */ clearLayer: (state: T, layerId: string) => T; + /** Optional, if the visualization supports multiple layers */ removeLayer?: (state: T, layerId: string) => T; + /** Track added layers in internal state */ appendLayer?: (state: T, layerId: string) => T; - // Layer context menu is used by visualizations for styling the entire layer - // For example, the XY visualization uses this to have multiple chart types - getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps<T>) => void; - + /** + * For consistency across different visualizations, the dimension configuration UI is standardized + */ getConfiguration: ( props: VisualizationConfigProps<T> ) => { groups: VisualizationDimensionGroupConfig[] }; - getDescription: ( - state: T - ) => { - icon?: IconType; - label: string; - }; - - switchVisualizationType?: (visualizationTypeId: string, state: T) => T; - - // For initializing from saved object - initialize: (frame: FramePublicAPI, state?: P) => T; - - getPersistableState: (state: T) => P; + /** + * Popover contents that open when the user clicks the contextMenuIcon. This can be used + * for extra configurability, such as for styling the legend or axis + */ + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps<T>) => void; + /** + * Visualizations can provide a custom icon which will open a layer-specific popover + * If no icon is provided, gear icon is default + */ + getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; - // Actions triggered by the frame which tell the datasource that a dimension is being changed - setDimension: ( - props: VisualizationDimensionChangeProps<T> & { - groupId: string; - } - ) => T; + /** + * The frame is telling the visualization to update or set a dimension based on user interaction + * groupId is coming from the groupId provided in getConfiguration + */ + setDimension: (props: VisualizationDimensionChangeProps<T> & { groupId: string }) => T; + /** + * The frame is telling the visualization to remove a dimension. The visualization needs to + * look at its internal state to determine which dimension is being affected. + */ removeDimension: (props: VisualizationDimensionChangeProps<T>) => T; - toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; + /** + * The frame will call this function on all visualizations at different times. The + * main use cases where visualization suggestions are requested are: + * - When dragging a field + * - When opening the chart switcher + * If the state is provided when requesting suggestions, the visualization is active. + * Most visualizations will apply stricter filtering to suggestions when they are active, + * because suggestions have the potential to remove the users's work in progress. + */ + getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>; + toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; /** - * Epression to render a preview version of the chart in very constraint space. + * Expression to render a preview version of the chart in very constrained space. * If there is no expression provided, the preview icon is used. */ toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; - - // The frame will call this function on all visualizations when the table changes, or when - // rendering additional ways of using the data - getSuggestions: (context: SuggestionRequest<T>) => Array<VisualizationSuggestion<T>>; } diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap index bef53c2fd266e..f8f467b25643b 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap @@ -6,6 +6,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` > <Connect(spec) legendPosition="top" + onBrushEnd={[Function]} onElementClick={[Function]} rotation={0} showLegend={false} @@ -73,6 +74,7 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` > <Connect(spec) legendPosition="top" + onBrushEnd={[Function]} onElementClick={[Function]} rotation={0} showLegend={false} @@ -140,6 +142,7 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` > <Connect(spec) legendPosition="top" + onBrushEnd={[Function]} onElementClick={[Function]} rotation={90} showLegend={false} @@ -207,6 +210,7 @@ exports[`xy_expression XYChart component it renders line 1`] = ` > <Connect(spec) legendPosition="top" + onBrushEnd={[Function]} onElementClick={[Function]} rotation={0} showLegend={false} @@ -274,6 +278,7 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` > <Connect(spec) legendPosition="top" + onBrushEnd={[Function]} onElementClick={[Function]} rotation={0} showLegend={false} @@ -345,6 +350,7 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` > <Connect(spec) legendPosition="top" + onBrushEnd={[Function]} onElementClick={[Function]} rotation={0} showLegend={false} @@ -416,6 +422,7 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = > <Connect(spec) legendPosition="top" + onBrushEnd={[Function]} onElementClick={[Function]} rotation={90} showLegend={false} diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 5dfae097be834..9a0819d4f01c4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -12,8 +12,8 @@ import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; import { EditorFrameSetup, FormatFactory } from '../types'; +import { setExecuteTriggerActions } from '../services'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { setExecuteTriggerActions } from './services'; export interface XyVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; @@ -53,6 +53,7 @@ export class XyVisualization { ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme, timeZone: getTimeZone(core.uiSettings), + histogramBarTarget: core.uiSettings.get<number>('histogram:barTarget'), }) ); diff --git a/x-pack/plugins/lens/public/xy_visualization/services.ts b/x-pack/plugins/lens/public/xy_visualization/services.ts deleted file mode 100644 index 51289fe0c63e7..0000000000000 --- a/x-pack/plugins/lens/public/xy_visualization/services.ts +++ /dev/null @@ -1,12 +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 { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; - -export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter< - UiActionsStart['executeTriggerActions'] ->('executeTriggerActions'); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 9b068b0ca5ef0..d28a803790822 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -142,7 +142,7 @@ export const buildExpression = ( .concat(layer.splitAccessor ? [layer.splitAccessor] : []) .forEach(accessor => { const operation = datasource.getOperationForColumnId(accessor); - if (operation && operation.label) { + if (operation?.label) { columnToLabel[accessor] = operation.label; } }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 80d33d1b95b61..dd2f32b63e34a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -27,6 +27,145 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; const executeTriggerActions = jest.fn(); +const dateHistogramData: LensMultiTable = { + type: 'lens_multitable', + tables: { + timeLayer: { + type: 'kibana_datatable', + rows: [ + { + xAccessorId: 1585758120000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Accessories", + yAccessorId: 1, + }, + { + xAccessorId: 1585758360000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585759380000, + splitAccessorId: "Women's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760700000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Clothing", + yAccessorId: 1, + }, + { + xAccessorId: 1585760760000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + { + xAccessorId: 1585761120000, + splitAccessorId: "Men's Shoes", + yAccessorId: 1, + }, + ], + columns: [ + { + id: 'xAccessorId', + name: 'order_date per minute', + meta: { + type: 'date_histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: { + field: 'order_date', + timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' }, + useNormalizedEsInterval: true, + scaleMetricValues: false, + interval: '1m', + drop_partials: false, + min_doc_count: 0, + extended_bounds: {}, + }, + }, + formatHint: { id: 'date', params: { pattern: 'HH:mm' } }, + }, + { + id: 'splitAccessorId', + name: 'Top values of category.keyword', + meta: { + type: 'terms', + indexPatternId: 'indexPatternId', + aggConfigParams: { + field: 'category.keyword', + orderBy: 'yAccessorId', + order: 'desc', + size: 3, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: 'Missing', + }, + }, + formatHint: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: 'Missing', + parsedUrl: { + origin: 'http://localhost:5601', + pathname: '/jiy/app/kibana', + basePath: '/jiy', + }, + }, + }, + }, + { + id: 'yAccessorId', + name: 'Count of records', + meta: { + type: 'count', + indexPatternId: 'indexPatternId', + aggConfigParams: {}, + }, + formatHint: { id: 'number' }, + }, + ], + }, + }, + dateRange: { + fromDate: new Date('2020-04-01T16:14:16.246Z'), + toDate: new Date('2020-04-01T17:15:41.263Z'), + }, +}; + +const dateHistogramLayer: LayerArgs = { + layerId: 'timeLayer', + hide: false, + xAccessor: 'xAccessorId', + yScaleType: 'linear', + xScaleType: 'time', + isHistogram: true, + splitAccessor: 'splitAccessorId', + seriesType: 'bar_stacked', + accessors: ['yAccessorId'], +}; + const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatatable => ({ type: 'kibana_datatable', columns: [ @@ -40,7 +179,7 @@ const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatata id: 'c', name: 'c', formatHint: { id: 'string' }, - meta: { type: 'date-histogram', aggConfigParams: { interval: '10s' } }, + meta: { type: 'date-histogram', aggConfigParams: { interval: 'auto' } }, }, { id: 'd', name: 'ColD', formatHint: { id: 'string' } }, ], @@ -156,6 +295,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -203,6 +343,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -237,15 +378,17 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); + // real auto interval is 30mins = 1800000 expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` Object { "max": 1546491600000, "min": 1546405200000, - "minInterval": 10000, + "minInterval": 1728000, } `); }); @@ -271,6 +414,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -279,7 +423,7 @@ describe('xy_expression', () => { Object { "max": 1546491600000, "min": 1546405200000, - "minInterval": 10000, + "minInterval": undefined, } `); }); @@ -307,6 +451,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -350,6 +495,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -383,6 +529,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -398,6 +545,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -414,6 +562,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -430,6 +579,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -438,8 +588,41 @@ describe('xy_expression', () => { expect(component.find(Settings).prop('rotation')).toEqual(90); }); + test('onBrushEnd returns correct context data for date histogram data', () => { + const { args } = sampleArgs(); + + const wrapper = mountWithIntl( + <XYChart + data={dateHistogramData} + args={{ + ...args, + layers: [dateHistogramLayer], + }} + formatFactory={getFormatSpy} + timeZone="UTC" + chartTheme={{}} + histogramBarTarget={50} + executeTriggerActions={executeTriggerActions} + /> + ); + + wrapper + .find(Settings) + .first() + .prop('onBrushEnd')!(1585757732783, 1585758880838); + + expect(executeTriggerActions).toHaveBeenCalledWith('SELECT_RANGE_TRIGGER', { + data: { + column: 0, + table: dateHistogramData.tables.timeLayer, + range: [1585757732783, 1585758880838], + }, + timeFieldName: 'order_date', + }); + }); + test('onElementClick returns correct context data', () => { - const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1' }; + const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null }; const series = { key: 'spec{d}yAccessor{d}splitAccessors{b-2}', specId: 'd', @@ -472,6 +655,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -510,6 +694,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -527,6 +712,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -547,6 +733,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -565,6 +752,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="CEST" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -582,6 +770,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -606,6 +795,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -624,6 +814,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -684,6 +875,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -878,6 +1070,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -894,6 +1087,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -910,6 +1104,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -927,6 +1122,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); @@ -943,6 +1139,7 @@ describe('xy_expression', () => { args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }} formatFactory={getFormatSpy} chartTheme={{}} + histogramBarTarget={50} timeZone="UTC" executeTriggerActions={executeTriggerActions} /> @@ -963,6 +1160,7 @@ describe('xy_expression', () => { formatFactory={getFormatSpy} timeZone="UTC" chartTheme={{}} + histogramBarTarget={50} executeTriggerActions={executeTriggerActions} /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index f12a0e5b907c7..2cd9ae7acb203 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -6,6 +6,7 @@ import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; +import moment from 'moment'; import { Chart, Settings, @@ -28,15 +29,18 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EmbeddableVisTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { + ValueClickTriggerContext, + RangeSelectTriggerContext, +} from '../../../../../src/plugins/embeddable/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; import { LensMultiTable, FormatFactory } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; +import { getExecuteTriggerActions } from '../services'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { parseInterval } from '../../../../../src/plugins/data/common'; -import { getExecuteTriggerActions } from './services'; type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T; type SeriesSpec = InferPropType<typeof LineSeries> & @@ -58,6 +62,7 @@ type XYChartRenderProps = XYChartProps & { chartTheme: PartialTheme; formatFactory: FormatFactory; timeZone: string; + histogramBarTarget: number; executeTriggerActions: UiActionsStart['executeTriggerActions']; }; @@ -110,6 +115,7 @@ export const xyChart: ExpressionFunctionDefinition< export const getXyChartRenderer = (dependencies: { formatFactory: Promise<FormatFactory>; chartTheme: PartialTheme; + histogramBarTarget: number; timeZone: string; }): ExpressionRenderDefinition<XYChartProps> => ({ name: 'lens_xy_chart_renderer', @@ -130,6 +136,7 @@ export const getXyChartRenderer = (dependencies: { formatFactory={formatFactory} chartTheme={dependencies.chartTheme} timeZone={dependencies.timeZone} + histogramBarTarget={dependencies.histogramBarTarget} executeTriggerActions={executeTriggerActions} /> </I18nProvider>, @@ -169,6 +176,7 @@ export function XYChart({ formatFactory, timeZone, chartTheme, + histogramBarTarget, executeTriggerActions, }: XYChartRenderProps) { const { legend, layers } = args; @@ -212,20 +220,54 @@ export function XYChart({ const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle; - // add minInterval only for single row value as it cannot be determined from dataset + function calculateMinInterval() { + // check all the tables to see if all of the rows have the same timestamp + // that would mean that chart will draw a single bar + const isSingleTimestampInXDomain = () => { + const nonEmptyLayers = layers.filter( + layer => data.tables[layer.layerId].rows.length && layer.xAccessor + ); + + if (!nonEmptyLayers.length) { + return; + } + + const firstRowValue = + data.tables[nonEmptyLayers[0].layerId].rows[0][nonEmptyLayers[0].xAccessor!]; + for (const layer of nonEmptyLayers) { + if ( + layer.xAccessor && + data.tables[layer.layerId].rows.some(row => row[layer.xAccessor!] !== firstRowValue) + ) { + return false; + } + } + return true; + }; + + // add minInterval only for single point in domain + if (data.dateRange && isSingleTimestampInXDomain()) { + if (xAxisColumn?.meta?.aggConfigParams?.interval !== 'auto') + return parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds(); - const minInterval = layers.every(layer => data.tables[layer.layerId].rows.length <= 1) - ? parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds() + const { fromDate, toDate } = data.dateRange; + const duration = moment(toDate).diff(moment(fromDate)); + const targetMs = duration / histogramBarTarget; + return isNaN(targetMs) ? 0 : Math.max(Math.floor(targetMs), 1); + } + return undefined; + } + + const isTimeViz = data.dateRange && layers.every(l => l.xScaleType === 'time'); + + const xDomain = isTimeViz + ? { + min: data.dateRange?.fromDate.getTime(), + max: data.dateRange?.toDate.getTime(), + minInterval: calculateMinInterval(), + } : undefined; - const xDomain = - data.dateRange && layers.every(l => l.xScaleType === 'time') - ? { - min: data.dateRange.fromDate.getTime(), - max: data.dateRange.toDate.getTime(), - minInterval, - } - : undefined; return ( <Chart> <Settings @@ -235,6 +277,31 @@ export function XYChart({ theme={chartTheme} rotation={shouldRotate ? 90 : 0} xDomain={xDomain} + onBrushEnd={(min: number, max: number) => { + // in the future we want to make it also for histogram + if (!xAxisColumn || !isTimeViz) { + return; + } + + const firstLayerWithData = + layers[layers.findIndex(layer => data.tables[layer.layerId].rows.length)]; + const table = data.tables[firstLayerWithData.layerId]; + + const xAxisColumnIndex = table.columns.findIndex( + el => el.id === firstLayerWithData.xAccessor + ); + const timeFieldName = table.columns[xAxisColumnIndex]?.meta?.aggConfigParams?.field; + + const context: RangeSelectTriggerContext = { + data: { + range: [min, max], + table, + column: xAxisColumnIndex, + }, + timeFieldName, + }; + executeTriggerActions(VIS_EVENT_TO_TRIGGER.brush, context); + }} onElementClick={([[geometry, series]]) => { // for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue const xySeries = series as XYChartSeriesIdentifier; @@ -271,13 +338,11 @@ export function XYChart({ }); } - const xAxisFieldName: string | undefined = table.columns.find( - col => col.id === layer.xAccessor - )?.meta?.aggConfigParams?.field; - + const xAxisFieldName = table.columns.find(el => el.id === layer.xAccessor)?.meta + ?.aggConfigParams?.field; const timeFieldName = xDomain && xAxisFieldName; - const context: EmbeddableVisTriggerContext = { + const context: ValueClickTriggerContext = { data: { data: points.map(point => ({ row: point.row, @@ -288,7 +353,6 @@ export function XYChart({ }, timeFieldName, }; - executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context); }} /> diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index ddbd9d11b5fad..73ff88e97f479 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -186,7 +186,7 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], layerId: 'first', - changeType: 'unchanged', + changeType: 'extended', label: 'Datasource title', }, keptLayerIds: [], @@ -196,6 +196,34 @@ describe('xy_suggestions', () => { expect(suggestion.title).toEqual('Datasource title'); }); + test('suggests only stacked bar chart when xy chart is inactive', () => { + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [dateCol('date'), numCol('price')], + layerId: 'first', + changeType: 'unchanged', + label: 'Datasource title', + }, + keptLayerIds: [], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.title).toEqual('Bar chart'); + expect(suggestion.state).toEqual( + expect.objectContaining({ + layers: [ + expect.objectContaining({ + seriesType: 'bar_stacked', + xAccessor: 'date', + accessors: ['price'], + splitAccessor: undefined, + }), + ], + }) + ); + }); + test('hides reduced suggestions if there is a current state', () => { const [suggestion, ...rest] = getSuggestions({ table: { @@ -224,7 +252,7 @@ describe('xy_suggestions', () => { expect(suggestion.hide).toBeTruthy(); }); - test('does not hide reduced suggestions if xy visualization is not active', () => { + test('hides reduced suggestions if xy visualization is not active', () => { const [suggestion, ...rest] = getSuggestions({ table: { isMultiRow: true, @@ -236,7 +264,7 @@ describe('xy_suggestions', () => { }); expect(rest).toHaveLength(0); - expect(suggestion.hide).toBeFalsy(); + expect(suggestion.hide).toBeTruthy(); }); test('only makes a seriesType suggestion for unchanged table without split', () => { @@ -419,6 +447,44 @@ describe('xy_suggestions', () => { }); }); + test('changes column mappings when suggestion is reorder', () => { + const currentState: XYState = { + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + accessors: ['price'], + layerId: 'first', + seriesType: 'bar', + splitAccessor: 'category', + xAccessor: 'product', + }, + ], + }; + const [suggestion, ...rest] = getSuggestions({ + table: { + isMultiRow: true, + columns: [strCol('category'), strCol('product'), numCol('price')], + layerId: 'first', + changeType: 'reorder', + }, + state: currentState, + keptLayerIds: [], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.state).toEqual({ + ...currentState, + layers: [ + { + ...currentState.layers[0], + xAccessor: 'category', + splitAccessor: 'product', + }, + ], + }); + }); + test('overwrites column to dimension mappings if a date dimension is added', () => { (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); const currentState: XYState = { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts index 5e9311bb1e928..abd7640344064 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts @@ -99,11 +99,14 @@ function getBucketMappings(table: TableSuggestion, currentState?: State) { // reverse the buckets before prioritization to always use the most inner // bucket of the highest-prioritized group as x value (don't use nested // buckets as split series) - const prioritizedBuckets = prioritizeColumns(buckets.reverse()); + const prioritizedBuckets = prioritizeColumns([...buckets].reverse()); if (!currentLayer || table.changeType === 'initial') { return prioritizedBuckets; } + if (table.changeType === 'reorder') { + return buckets; + } // if existing table is just modified, try to map buckets to the current dimensions const currentXColumnIndex = prioritizedBuckets.findIndex( @@ -175,12 +178,24 @@ function getSuggestionsForLayer({ keptLayerIds, }; - const isSameState = currentState && changeType === 'unchanged'; + // handles the simplest cases, acting as a chart switcher + if (!currentState && changeType === 'unchanged') { + return [ + { + ...buildSuggestion(options), + title: i18n.translate('xpack.lens.xySuggestions.barChartTitle', { + defaultMessage: 'Bar chart', + }), + }, + ]; + } + const isSameState = currentState && changeType === 'unchanged'; if (!isSameState) { return buildSuggestion(options); } + // Suggestions are either changing the data, or changing the way the data is used const sameStateSuggestions: Array<VisualizationSuggestion<State>> = []; // if current state is using the same data, suggest same chart with different presentational configuration @@ -374,8 +389,11 @@ function buildSuggestion({ return { title, score: getScore(yValues, splitBy, changeType), - // don't advertise chart of same type but with less data - hide: currentState && changeType === 'reduced', + hide: + // Only advertise very clear changes when XY chart is not active + (!currentState && changeType !== 'unchanged' && changeType !== 'extended') || + // Don't advertise removing dimensions + (currentState && changeType === 'reduced'), state, previewIcon: getIconForSeries(seriesType), }; 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 beccf0dc46eb4..d176905c65120 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 c72fa0fec24d7..e91edf9cc0183 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<State, PersistableState> = { id: 'lnsXY', visualizationTypes, + getVisualizationTypeId(state) { + const type = getVisualizationType(state); + return type === 'mixed' ? type : type.id; + }, getLayerIds(state) { return state.layers.map(l => l.layerId); diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations.test.ts index e80308cc9acdb..0541d9636577b 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 = ({ log: { warning: () => {} } } as unknown) 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 3d238723b7438..a15e2b3692d02 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations.ts @@ -5,6 +5,7 @@ */ import { cloneDeep } from 'lodash'; +import { fromExpression, toExpression, Ast, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { SavedObjectMigrationFn } from 'src/core/server'; interface XYLayerPre77 { @@ -14,7 +15,126 @@ interface XYLayerPre77 { accessors: string[]; } -export const migrations: Record<string, SavedObjectMigrationFn> = { +/** + * Removes the `lens_auto_date` subexpression from a stored expression + * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const removeLensAutoDate: SavedObjectMigrationFn<any, any> = (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 + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const addTimeFieldToEsaggs: SavedObjectMigrationFn<any, any> = (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 }; + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const migrations: Record<string, SavedObjectMigrationFn<any, any>> = { '7.7.0': doc => { const newDoc = cloneDeep(doc); if (newDoc.attributes?.visualizationType === 'lnsXY') { @@ -34,4 +154,7 @@ export const migrations: Record<string, SavedObjectMigrationFn> = { } 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': (doc, context) => addTimeFieldToEsaggs(removeLensAutoDate(doc, context), context), }; diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts index d622e3f71eff5..154692a2fd197 100644 --- a/x-pack/plugins/licensing/server/mocks.ts +++ b/x-pack/plugins/licensing/server/mocks.ts @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ import { BehaviorSubject } from 'rxjs'; -import { LicensingPluginSetup } from './types'; +import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { licenseMock } from '../common/licensing.mock'; +import { featureUsageMock } from './services/feature_usage_service.mock'; -const createSetupMock = () => { +const createSetupMock = (): jest.Mocked<LicensingPluginSetup> => { const license = licenseMock.createLicense(); - const mock: jest.Mocked<LicensingPluginSetup> = { + const mock = { license$: new BehaviorSubject(license), refresh: jest.fn(), createLicensePoller: jest.fn(), + featureUsage: featureUsageMock.createSetup(), }; mock.refresh.mockResolvedValue(license); mock.createLicensePoller.mockReturnValue({ @@ -23,7 +25,16 @@ const createSetupMock = () => { return mock; }; +const createStartMock = (): jest.Mocked<LicensingPluginStart> => { + const mock = { + featureUsage: featureUsageMock.createStart(), + }; + + return mock; +}; + export const licensingMock = { createSetup: createSetupMock, + createStart: createStartMock, ...licenseMock, }; diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 383245e6f4ee8..ee43ac0ce233c 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -20,12 +20,13 @@ import { } from 'src/core/server'; import { ILicense, PublicLicense, PublicFeatures } from '../common/types'; -import { LicensingPluginSetup } from './types'; +import { LicensingPluginSetup, LicensingPluginStart } from './types'; import { License } from '../common/license'; import { createLicenseUpdate } from '../common/license_update'; import { ElasticsearchError, RawLicense, RawFeatures } from './types'; import { registerRoutes } from './routes'; +import { FeatureUsageService } from './services'; import { LicenseConfigType } from './licensing_config'; import { createRouteHandlerContext } from './licensing_route_handler_context'; @@ -77,18 +78,19 @@ function sign({ * A plugin for fetching, refreshing, and receiving information about the license for the * current Kibana instance. */ -export class LicensingPlugin implements Plugin<LicensingPluginSetup> { +export class LicensingPlugin implements Plugin<LicensingPluginSetup, LicensingPluginStart, {}, {}> { private stop$ = new Subject(); private readonly logger: Logger; private readonly config$: Observable<LicenseConfigType>; private loggingSubscription?: Subscription; + private featureUsage = new FeatureUsageService(); constructor(private readonly context: PluginInitializerContext) { this.logger = this.context.logger.get(); this.config$ = this.context.config.create<LicenseConfigType>(); } - public async setup(core: CoreSetup) { + public async setup(core: CoreSetup<{}, LicensingPluginStart>) { this.logger.debug('Setting up Licensing plugin'); const config = await this.config$.pipe(take(1)).toPromise(); const pollingFrequency = config.api_polling_frequency; @@ -101,13 +103,14 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup> { core.http.registerRouteHandlerContext('licensing', createRouteHandlerContext(license$)); - registerRoutes(core.http.createRouter()); + registerRoutes(core.http.createRouter(), core.getStartServices); core.http.registerOnPreResponse(createOnPreResponseHandler(refresh, license$)); return { refresh, license$, createLicensePoller: this.createLicensePoller.bind(this), + featureUsage: this.featureUsage.setup(), }; } @@ -186,7 +189,11 @@ export class LicensingPlugin implements Plugin<LicensingPluginSetup> { return error.message; } - public async start(core: CoreStart) {} + public async start(core: CoreStart) { + return { + featureUsage: this.featureUsage.start(), + }; + } public stop() { this.stop$.next(); diff --git a/x-pack/plugins/licensing/server/routes/feature_usage.ts b/x-pack/plugins/licensing/server/routes/feature_usage.ts new file mode 100644 index 0000000000000..5fbfbc3f577b8 --- /dev/null +++ b/x-pack/plugins/licensing/server/routes/feature_usage.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { LicensingPluginStart } from '../types'; + +export function registerFeatureUsageRoute( + router: IRouter, + getStartServices: StartServicesAccessor<{}, LicensingPluginStart> +) { + router.get( + { path: '/api/licensing/feature_usage', validate: false }, + async (context, request, response) => { + const [, , { featureUsage }] = await getStartServices(); + return response.ok({ + body: [...featureUsage.getLastUsages().entries()].reduce( + (res, [featureName, lastUsage]) => { + return { + ...res, + [featureName]: new Date(lastUsage).toISOString(), + }; + }, + {} + ), + }); + } + ); +} diff --git a/x-pack/plugins/licensing/server/routes/index.ts b/x-pack/plugins/licensing/server/routes/index.ts index 26b3bc6292dd6..2d073a92e507e 100644 --- a/x-pack/plugins/licensing/server/routes/index.ts +++ b/x-pack/plugins/licensing/server/routes/index.ts @@ -3,9 +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 { IRouter } from 'src/core/server'; + +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { LicensingPluginStart } from '../types'; import { registerInfoRoute } from './info'; +import { registerFeatureUsageRoute } from './feature_usage'; -export function registerRoutes(router: IRouter) { +export function registerRoutes( + router: IRouter, + getStartServices: StartServicesAccessor<{}, LicensingPluginStart> +) { registerInfoRoute(router); + registerFeatureUsageRoute(router, getStartServices); } diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts new file mode 100644 index 0000000000000..f247c6ffcb526 --- /dev/null +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + FeatureUsageService, + FeatureUsageServiceSetup, + FeatureUsageServiceStart, +} from './feature_usage_service'; + +const createSetupMock = (): jest.Mocked<FeatureUsageServiceSetup> => { + const mock = { + register: jest.fn(), + }; + + return mock; +}; + +const createStartMock = (): jest.Mocked<FeatureUsageServiceStart> => { + const mock = { + notifyUsage: jest.fn(), + getLastUsages: jest.fn(), + }; + + return mock; +}; + +const createServiceMock = (): jest.Mocked<PublicMethodsOf<FeatureUsageService>> => { + const mock = { + setup: jest.fn(() => createSetupMock()), + start: jest.fn(() => createStartMock()), + }; + + return mock; +}; + +export const featureUsageMock = { + create: createServiceMock, + createSetup: createSetupMock, + createStart: createStartMock, +}; diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts new file mode 100644 index 0000000000000..f0ef0dbec0b22 --- /dev/null +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.test.ts @@ -0,0 +1,116 @@ +/* + * 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 { FeatureUsageService } from './feature_usage_service'; + +describe('FeatureUsageService', () => { + let service: FeatureUsageService; + + beforeEach(() => { + service = new FeatureUsageService(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + const toObj = (map: ReadonlyMap<string, any>): Record<string, any> => + Object.fromEntries(map.entries()); + + describe('#setup', () => { + describe('#register', () => { + it('throws when registering the same feature twice', () => { + const setup = service.setup(); + setup.register('foo'); + expect(() => { + setup.register('foo'); + }).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`); + }); + }); + }); + + describe('#start', () => { + describe('#notifyUsage', () => { + it('allows to notify a feature usage', () => { + const setup = service.setup(); + setup.register('feature'); + const start = service.start(); + start.notifyUsage('feature', 127001); + + expect(start.getLastUsages().get('feature')).toBe(127001); + }); + + it('can receive a Date object', () => { + const setup = service.setup(); + setup.register('feature'); + const start = service.start(); + + const usageTime = new Date(2015, 9, 21, 17, 54, 12); + start.notifyUsage('feature', usageTime); + expect(start.getLastUsages().get('feature')).toBe(usageTime.getTime()); + }); + + it('uses the current time when `usedAt` is unspecified', () => { + jest.spyOn(Date, 'now').mockReturnValue(42); + + const setup = service.setup(); + setup.register('feature'); + const start = service.start(); + start.notifyUsage('feature'); + + expect(start.getLastUsages().get('feature')).toBe(42); + }); + + it('throws when notifying for an unregistered feature', () => { + service.setup(); + const start = service.start(); + expect(() => { + start.notifyUsage('unregistered'); + }).toThrowErrorMatchingInlineSnapshot(`"Feature 'unregistered' is not registered."`); + }); + }); + + describe('#getLastUsages', () => { + it('returns the last usage for all used features', () => { + const setup = service.setup(); + setup.register('featureA'); + setup.register('featureB'); + const start = service.start(); + start.notifyUsage('featureA', 127001); + start.notifyUsage('featureB', 6666); + + expect(toObj(start.getLastUsages())).toEqual({ + featureA: 127001, + featureB: 6666, + }); + }); + + it('returns the last usage even after notifying for an older usage', () => { + const setup = service.setup(); + setup.register('featureA'); + const start = service.start(); + start.notifyUsage('featureA', 1000); + start.notifyUsage('featureA', 500); + + expect(toObj(start.getLastUsages())).toEqual({ + featureA: 1000, + }); + }); + + it('does not return entries for unused registered features', () => { + const setup = service.setup(); + setup.register('featureA'); + setup.register('featureB'); + const start = service.start(); + start.notifyUsage('featureA', 127001); + + expect(toObj(start.getLastUsages())).toEqual({ + featureA: 127001, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/licensing/server/services/feature_usage_service.ts b/x-pack/plugins/licensing/server/services/feature_usage_service.ts new file mode 100644 index 0000000000000..47ffe3a3d9f54 --- /dev/null +++ b/x-pack/plugins/licensing/server/services/feature_usage_service.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isDate } from 'lodash'; + +/** @public */ +export interface FeatureUsageServiceSetup { + /** + * Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. + */ + register(featureName: string): void; +} + +/** @public */ +export interface FeatureUsageServiceStart { + /** + * Notify of a registered feature usage at given time. + * + * @param featureName - the name of the feature to notify usage of + * @param usedAt - Either a `Date` or an unix timestamp with ms. If not specified, it will be set to the current time. + */ + notifyUsage(featureName: string, usedAt?: Date | number): void; + /** + * Return a map containing last usage timestamp for all features. + * Features that were not used yet do not appear in the map. + */ + getLastUsages(): ReadonlyMap<string, number>; +} + +export class FeatureUsageService { + private readonly features: string[] = []; + private readonly lastUsages = new Map<string, number>(); + + public setup(): FeatureUsageServiceSetup { + return { + register: featureName => { + if (this.features.includes(featureName)) { + throw new Error(`Feature '${featureName}' has already been registered.`); + } + this.features.push(featureName); + }, + }; + } + + public start(): FeatureUsageServiceStart { + return { + notifyUsage: (featureName, usedAt = Date.now()) => { + if (!this.features.includes(featureName)) { + throw new Error(`Feature '${featureName}' is not registered.`); + } + if (isDate(usedAt)) { + usedAt = usedAt.getTime(); + } + const currentValue = this.lastUsages.get(featureName) ?? 0; + this.lastUsages.set(featureName, Math.max(usedAt, currentValue)); + }, + getLastUsages: () => new Map(this.lastUsages.entries()), + }; + } +} diff --git a/x-pack/plugins/licensing/server/services/index.ts b/x-pack/plugins/licensing/server/services/index.ts new file mode 100644 index 0000000000000..fc890dd3c927d --- /dev/null +++ b/x-pack/plugins/licensing/server/services/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { + FeatureUsageService, + FeatureUsageServiceSetup, + FeatureUsageServiceStart, +} from './feature_usage_service'; diff --git a/x-pack/plugins/licensing/server/types.ts b/x-pack/plugins/licensing/server/types.ts index f46167a0d0a42..f11d9d5e69a58 100644 --- a/x-pack/plugins/licensing/server/types.ts +++ b/x-pack/plugins/licensing/server/types.ts @@ -6,6 +6,7 @@ import { Observable } from 'rxjs'; import { IClusterClient } from 'src/core/server'; import { ILicense, LicenseStatus, LicenseType } from '../common/types'; +import { FeatureUsageServiceSetup, FeatureUsageServiceStart } from './services'; export interface ElasticsearchError extends Error { status?: number; @@ -57,7 +58,6 @@ export interface LicensingPluginSetup { * Triggers licensing information re-fetch. */ refresh(): Promise<ILicense>; - /** * Creates a license poller to retrieve a license data with. * Allows a plugin to configure a cluster to retrieve data from at @@ -67,4 +67,16 @@ export interface LicensingPluginSetup { clusterClient: IClusterClient, pollingFrequency: number ) => { license$: Observable<ILicense>; refresh(): Promise<ILicense> }; + /** + * APIs to register licensed feature usage. + */ + featureUsage: FeatureUsageServiceSetup; +} + +/** @public */ +export interface LicensingPluginStart { + /** + * APIs to manage licensed feature usage. + */ + featureUsage: FeatureUsageServiceStart; } diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts new file mode 100644 index 0000000000000..dbe31fed66413 --- /dev/null +++ b/x-pack/plugins/lists/common/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Lists routes + */ +export const LIST_URL = `/api/lists`; +export const LIST_INDEX = `${LIST_URL}/index`; +export const LIST_ITEM_URL = `${LIST_URL}/items`; diff --git a/x-pack/plugins/lists/common/schemas/common/index.ts b/x-pack/plugins/lists/common/schemas/common/index.ts new file mode 100644 index 0000000000000..a05e97ded38ee --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/common/index.ts @@ -0,0 +1,7 @@ +/* + * 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 * from './schemas'; diff --git a/x-pack/plugins/lists/common/schemas/common/schemas.ts b/x-pack/plugins/lists/common/schemas/common/schemas.ts new file mode 100644 index 0000000000000..edc037ed7a0b1 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/common/schemas.ts @@ -0,0 +1,62 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { NonEmptyString } from '../types/non_empty_string'; + +export const name = t.string; +export type Name = t.TypeOf<typeof name>; +export const nameOrUndefined = t.union([name, t.undefined]); +export type NameOrUndefined = t.TypeOf<typeof nameOrUndefined>; + +export const description = t.string; +export type Description = t.TypeOf<typeof description>; +export const descriptionOrUndefined = t.union([description, t.undefined]); +export type DescriptionOrUndefined = t.TypeOf<typeof descriptionOrUndefined>; + +export const list_id = NonEmptyString; +export const list_idOrUndefined = t.union([list_id, t.undefined]); +export type List_idOrUndefined = t.TypeOf<typeof list_idOrUndefined>; + +export const item = t.string; +export const created_at = t.string; // TODO: Make this into an ISO Date string check +export const updated_at = t.string; // TODO: Make this into an ISO Date string check +export const updated_by = t.string; +export const created_by = t.string; +export const file = t.object; + +export const id = NonEmptyString; +export type Id = t.TypeOf<typeof id>; +export const idOrUndefined = t.union([id, t.undefined]); +export type IdOrUndefined = t.TypeOf<typeof idOrUndefined>; + +export const ip = t.string; +export const ipOrUndefined = t.union([ip, t.undefined]); + +export const keyword = t.string; +export const keywordOrUndefined = t.union([keyword, t.undefined]); + +export const value = t.string; +export const valueOrUndefined = t.union([value, t.undefined]); + +export const tie_breaker_id = t.string; // TODO: Use UUID for this instead of a string for validation +export const _index = t.string; + +export const type = t.keyof({ ip: null, keyword: null }); // TODO: Add the other data types here + +export const typeOrUndefined = t.union([type, t.undefined]); +export type Type = t.TypeOf<typeof type>; + +export const meta = t.object; +export type Meta = t.TypeOf<typeof meta>; +export const metaOrUndefined = t.union([meta, t.undefined]); +export type MetaOrUndefined = t.TypeOf<typeof metaOrUndefined>; + +export const esDataTypeUnion = t.union([t.type({ ip }), t.type({ keyword })]); +export type EsDataTypeUnion = t.TypeOf<typeof esDataTypeUnion>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.ts new file mode 100644 index 0000000000000..4a825382c06e4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/create_es_bulk_type.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. + */ + +import * as t from 'io-ts'; + +import { _index } from '../common/schemas'; + +export const createEsBulkTypeSchema = t.exact( + t.type({ + create: t.exact(t.type({ _index })), + }) +); + +export type CreateEsBulkTypeSchema = t.TypeOf<typeof createEsBulkTypeSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index.ts new file mode 100644 index 0000000000000..d70dd09849fa6 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './update_es_list_schema'; +export * from './index_es_list_schema'; +export * from './update_es_list_item_schema'; +export * from './index_es_list_item_schema'; +export * from './create_es_bulk_type'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts new file mode 100644 index 0000000000000..596498b64b771 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + esDataTypeUnion, + list_id, + metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, +} from '../common/schemas'; + +export const indexEsListItemSchema = t.intersection([ + t.exact( + t.type({ + created_at, + created_by, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, + }) + ), + esDataTypeUnion, +]); + +export type IndexEsListItemSchema = t.TypeOf<typeof indexEsListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts new file mode 100644 index 0000000000000..e0924392628a9 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/index_es_list_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const indexEsListSchema = t.exact( + t.type({ + created_at, + created_by, + description, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type IndexEsListSchema = t.TypeOf<typeof indexEsListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts new file mode 100644 index 0000000000000..e4cf46bc39429 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_item_schema.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { esDataTypeUnion, metaOrUndefined, updated_at, updated_by } from '../common/schemas'; + +export const updateEsListItemSchema = t.intersection([ + t.exact( + t.type({ + meta: metaOrUndefined, + updated_at, + updated_by, + }) + ), + esDataTypeUnion, +]); + +export type UpdateEsListItemSchema = t.TypeOf<typeof updateEsListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts new file mode 100644 index 0000000000000..8f23f3744e563 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_query/update_es_list_schema.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + descriptionOrUndefined, + metaOrUndefined, + nameOrUndefined, + updated_at, + updated_by, +} from '../common/schemas'; + +export const updateEsListSchema = t.exact( + t.type({ + description: descriptionOrUndefined, + meta: metaOrUndefined, + name: nameOrUndefined, + updated_at, + updated_by, + }) +); + +export type UpdateEsListSchema = t.TypeOf<typeof updateEsListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/index.ts b/x-pack/plugins/lists/common/schemas/elastic_response/index.ts new file mode 100644 index 0000000000000..6fbc6ef293064 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './search_es_list_item_schema'; +export * from './search_es_list_schema'; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts new file mode 100644 index 0000000000000..902d3e6a9896e --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + ipOrUndefined, + keywordOrUndefined, + list_id, + metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, +} from '../common/schemas'; + +export const searchEsListItemSchema = t.exact( + t.type({ + created_at, + created_by, + ip: ipOrUndefined, + keyword: keywordOrUndefined, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + updated_at, + updated_by, + }) +); + +export type SearchEsListItemSchema = t.TypeOf<typeof searchEsListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts new file mode 100644 index 0000000000000..00a7c6f321d38 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_schema.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const searchEsListSchema = t.exact( + t.type({ + created_at, + created_by, + description, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type SearchEsListSchema = t.TypeOf<typeof searchEsListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/index.ts b/x-pack/plugins/lists/common/schemas/index.ts new file mode 100644 index 0000000000000..6a60a6df55691 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './common'; +export * from './request'; +export * from './response'; +export * from './elastic_query'; +export * from './elastic_response'; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts new file mode 100644 index 0000000000000..8168e5a9838f2 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_item_schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_id, metaOrUndefined, value } from '../common/schemas'; + +export const createListItemSchema = t.exact( + t.type({ + id: idOrUndefined, + list_id, + meta: metaOrUndefined, + value, + }) +); + +export type CreateListItemSchema = t.TypeOf<typeof createListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts new file mode 100644 index 0000000000000..ba791a55d17eb --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getListRequest } from './mocks/utils'; +import { createListSchema } from './create_list_schema'; + +describe('create_list_schema', () => { + // TODO: Finish the tests for this + test('it should validate a typical lists request', () => { + const payload = getListRequest(); + const decoded = createListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + description: 'Description of a list item', + id: 'some-list-id', + name: 'Name of a list item', + type: 'ip', + }); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts new file mode 100644 index 0000000000000..353a4ecdafa0c --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/create_list_schema.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { description, idOrUndefined, metaOrUndefined, name, type } from '../common/schemas'; + +export const createListSchema = t.exact( + t.type({ + description, + id: idOrUndefined, + meta: metaOrUndefined, + name, + type, + }) +); + +export type CreateListSchema = t.TypeOf<typeof createListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts new file mode 100644 index 0000000000000..f4c1fb5c43eb0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const deleteListItemSchema = t.exact( + t.type({ + id: idOrUndefined, + list_id: list_idOrUndefined, + value: valueOrUndefined, + }) +); + +export type DeleteListItemSchema = t.TypeOf<typeof deleteListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts new file mode 100644 index 0000000000000..fd6aa5b85f81a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/delete_list_schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id } from '../common/schemas'; + +export const deleteListSchema = t.exact( + t.type({ + id, + }) +); + +export type DeleteListSchema = t.TypeOf<typeof deleteListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts new file mode 100644 index 0000000000000..14b201bf8089d --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/export_list_item_query_schema.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { list_id } from '../common/schemas'; + +export const exportListItemQuerySchema = t.exact( + t.type({ + list_id, + // TODO: Add file_name here with a default value + }) +); + +export type ExportListItemQuerySchema = t.TypeOf<typeof exportListItemQuerySchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.ts new file mode 100644 index 0000000000000..b8467d141bdd8 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_query_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { list_idOrUndefined, typeOrUndefined } from '../common/schemas'; + +export const importListItemQuerySchema = t.exact( + t.type({ list_id: list_idOrUndefined, type: typeOrUndefined }) +); + +export type ImportListItemQuerySchema = t.TypeOf<typeof importListItemQuerySchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts new file mode 100644 index 0000000000000..0cf01db8617f0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/import_list_item_schema.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import { Readable } from 'stream'; + +import * as t from 'io-ts'; + +import { file } from '../common/schemas'; + +export const importListItemSchema = t.exact( + t.type({ + file, + }) +); + +export interface HapiReadableStream extends Readable { + hapi: { + filename: string; + }; +} + +/** + * Special interface since we are streaming in a file through a reader + */ +export interface ImportListItemSchema { + file: HapiReadableStream; +} diff --git a/x-pack/plugins/lists/common/schemas/request/index.ts b/x-pack/plugins/lists/common/schemas/request/index.ts new file mode 100644 index 0000000000000..d332ab1eb1bab --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_list_item_schema'; +export * from './create_list_schema'; +export * from './delete_list_item_schema'; +export * from './delete_list_schema'; +export * from './export_list_item_query_schema'; +export * from './import_list_item_schema'; +export * from './patch_list_item_schema'; +export * from './patch_list_schema'; +export * from './read_list_item_schema'; +export * from './read_list_schema'; +export * from './import_list_item_query_schema'; +export * from './update_list_schema'; +export * from './update_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts b/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts new file mode 100644 index 0000000000000..e5d189db8490b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/mocks/utils.ts @@ -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 { CreateListSchema } from '../create_list_schema'; + +export const getListRequest = (): CreateListSchema => ({ + description: 'Description of a list item', + id: 'some-list-id', + meta: undefined, + name: 'Name of a list item', + type: 'ip', +}); diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts new file mode 100644 index 0000000000000..3e8198a5109b3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, metaOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const patchListItemSchema = t.exact( + t.type({ + id, + meta: metaOrUndefined, + value: valueOrUndefined, + }) +); + +export type PatchListItemSchema = t.TypeOf<typeof patchListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts new file mode 100644 index 0000000000000..efcb81fc8be2a --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/patch_list_schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { descriptionOrUndefined, id, metaOrUndefined, nameOrUndefined } from '../common/schemas'; + +export const patchListSchema = t.exact( + t.type({ + description: descriptionOrUndefined, + id, + meta: metaOrUndefined, + name: nameOrUndefined, + }) +); + +export type PatchListSchema = t.TypeOf<typeof patchListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.ts new file mode 100644 index 0000000000000..9ea14a2a21ed8 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_item_schema.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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { idOrUndefined, list_idOrUndefined, valueOrUndefined } from '../common/schemas'; + +export const readListItemSchema = t.exact( + t.type({ id: idOrUndefined, list_id: list_idOrUndefined, value: valueOrUndefined }) +); + +export type ReadListItemSchema = t.TypeOf<typeof readListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts new file mode 100644 index 0000000000000..8803346709c31 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/read_list_schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id } from '../common/schemas'; + +export const readListSchema = t.exact( + t.type({ + id, + }) +); + +export type ReadListSchema = t.TypeOf<typeof readListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts new file mode 100644 index 0000000000000..e1f88bae66e0f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_item_schema.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { id, metaOrUndefined, value } from '../common/schemas'; + +export const updateListItemSchema = t.exact( + t.type({ + id, + meta: metaOrUndefined, + value, + }) +); + +export type UpdateListItemSchema = t.TypeOf<typeof updateListItemSchema>; diff --git a/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts new file mode 100644 index 0000000000000..d51ed60c41b56 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/request/update_list_schema.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { description, id, metaOrUndefined, name } from '../common/schemas'; + +export const updateListSchema = t.exact( + t.type({ + description, + id, + meta: metaOrUndefined, + name, + }) +); + +export type UpdateListSchema = t.TypeOf<typeof updateListSchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts new file mode 100644 index 0000000000000..55aaf587ac06b --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/acknowledge_schema.ts @@ -0,0 +1,11 @@ +/* + * 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 * as t from 'io-ts'; + +export const acknowledgeSchema = t.type({ acknowledged: t.boolean }); + +export type AcknowledgeSchema = t.TypeOf<typeof acknowledgeSchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/index.ts b/x-pack/plugins/lists/common/schemas/response/index.ts new file mode 100644 index 0000000000000..3f11adf58d8d4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './list_item_schema'; +export * from './list_schema'; +export * from './acknowledge_schema'; +export * from './list_item_index_exist_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts new file mode 100644 index 0000000000000..bf2bf21d2c216 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_index_exist_schema.ts @@ -0,0 +1,14 @@ +/* + * 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 * as t from 'io-ts'; + +export const listItemIndexExistSchema = t.type({ + list_index: t.boolean, + list_item_index: t.boolean, +}); + +export type ListItemIndexExistSchema = t.TypeOf<typeof listItemIndexExistSchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts new file mode 100644 index 0000000000000..6c2f2ed9a7095 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_item_schema.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +/* eslint-disable @typescript-eslint/camelcase */ + +import { + created_at, + created_by, + id, + list_id, + metaOrUndefined, + tie_breaker_id, + type, + updated_at, + updated_by, + value, +} from '../common/schemas'; + +export const listItemSchema = t.exact( + t.type({ + created_at, + created_by, + id, + list_id, + meta: metaOrUndefined, + tie_breaker_id, + type, + updated_at, + updated_by, + value, + }) +); + +export type ListItemSchema = t.TypeOf<typeof listItemSchema>; + +export const listItemArraySchema = t.array(listItemSchema); +export type ListItemArraySchema = t.TypeOf<typeof listItemArraySchema>; diff --git a/x-pack/plugins/lists/common/schemas/response/list_schema.ts b/x-pack/plugins/lists/common/schemas/response/list_schema.ts new file mode 100644 index 0000000000000..cad449766ceb4 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/list_schema.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { + created_at, + created_by, + description, + id, + metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, +} from '../common/schemas'; + +export const listSchema = t.exact( + t.type({ + created_at, + created_by, + description, + id, + meta: metaOrUndefined, + name, + tie_breaker_id, + type, + updated_at, + updated_by, + }) +); + +export type ListSchema = t.TypeOf<typeof listSchema>; diff --git a/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts b/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts new file mode 100644 index 0000000000000..d1e2094bbcad3 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/types/non_empty_string.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +export type NonEmptyStringC = t.Type<string, string, unknown>; + +/** + * Types the NonEmptyString as: + * - A string that is not empty + */ +export const NonEmptyString: NonEmptyStringC = new t.Type<string, string, unknown>( + 'NonEmptyString', + t.string.is, + (input, context): Either<t.Errors, string> => { + if (typeof input === 'string' && input.trim() !== '') { + return t.success(input); + } else { + return t.failure(input, context); + } + }, + t.identity +); diff --git a/x-pack/plugins/lists/common/siem_common_deps.ts b/x-pack/plugins/lists/common/siem_common_deps.ts new file mode 100644 index 0000000000000..5e74753a6f0bd --- /dev/null +++ b/x-pack/plugins/lists/common/siem_common_deps.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getPaths, foldLeftRight } from '../../siem/server/utils/build_validation/__mocks__/utils'; +export { exactCheck } from '../../siem/server/utils/build_validation/exact_check'; diff --git a/x-pack/plugins/lists/kibana.json b/x-pack/plugins/lists/kibana.json new file mode 100644 index 0000000000000..b7aaac6d3fc76 --- /dev/null +++ b/x-pack/plugins/lists/kibana.json @@ -0,0 +1,10 @@ +{ + "configPath": ["xpack", "lists"], + "id": "lists", + "kibanaVersion": "kibana", + "requiredPlugins": [], + "optionalPlugins": ["spaces", "security"], + "server": true, + "ui": false, + "version": "8.0.0" +} diff --git a/x-pack/plugins/lists/server/config.ts b/x-pack/plugins/lists/server/config.ts new file mode 100644 index 0000000000000..3e7995b2ce8d0 --- /dev/null +++ b/x-pack/plugins/lists/server/config.ts @@ -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 { TypeOf, schema } from '@kbn/config-schema'; + +export const ConfigSchema = schema.object({ + enabled: schema.boolean({ defaultValue: false }), + listIndex: schema.string({ defaultValue: '.lists' }), + listItemIndex: schema.string({ defaultValue: '.items' }), +}); + +export type ConfigType = TypeOf<typeof ConfigSchema>; diff --git a/x-pack/plugins/lists/server/create_config.ts b/x-pack/plugins/lists/server/create_config.ts new file mode 100644 index 0000000000000..3158fabda935f --- /dev/null +++ b/x-pack/plugins/lists/server/create_config.ts @@ -0,0 +1,21 @@ +/* + * 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 { map } from 'rxjs/operators'; +import { PluginInitializerContext } from 'kibana/server'; +import { Observable } from 'rxjs'; + +import { ConfigType } from './config'; + +export const createConfig$ = ( + context: PluginInitializerContext +): Observable<Readonly<{ + enabled: boolean; + listIndex: string; + listItemIndex: string; +}>> => { + return context.config.create<ConfigType>().pipe(map(config => config)); +}; diff --git a/x-pack/plugins/lists/server/error_with_status_code.ts b/x-pack/plugins/lists/server/error_with_status_code.ts new file mode 100644 index 0000000000000..f9bbbc4abad27 --- /dev/null +++ b/x-pack/plugins/lists/server/error_with_status_code.ts @@ -0,0 +1,16 @@ +/* + * 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 class ErrorWithStatusCode extends Error { + private readonly statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } + + public getStatusCode = (): number => this.statusCode; +} 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 0000000000000..9c1d11b71984d --- /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/get_space_id.ts b/x-pack/plugins/lists/server/get_space_id.ts new file mode 100644 index 0000000000000..f224e37e04467 --- /dev/null +++ b/x-pack/plugins/lists/server/get_space_id.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. + */ + +import { KibanaRequest } from 'kibana/server'; + +import { SpacesServiceSetup } from '../../spaces/server'; + +export const getSpaceId = ({ + spaces, + request, +}: { + spaces: SpacesServiceSetup | undefined | null; + request: KibanaRequest; +}): string => spaces?.getSpaceId(request) ?? 'default'; 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 0000000000000..0992e3c361fcf --- /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/get_user.ts b/x-pack/plugins/lists/server/get_user.ts new file mode 100644 index 0000000000000..3b59853d0ab62 --- /dev/null +++ b/x-pack/plugins/lists/server/get_user.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaRequest } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../security/server'; + +export interface GetUserOptions { + security: SecurityPluginSetup | null | undefined; + request: KibanaRequest; +} + +export const getUser = ({ security, request }: GetUserOptions): string => { + 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/index.ts b/x-pack/plugins/lists/server/index.ts new file mode 100644 index 0000000000000..c1e577aa60195 --- /dev/null +++ b/x-pack/plugins/lists/server/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { PluginInitializerContext } from '../../../../src/core/server'; + +import { ConfigSchema } from './config'; +import { ListPlugin } from './plugin'; + +export const config = { schema: ConfigSchema }; +export const plugin = (initializerContext: PluginInitializerContext): ListPlugin => + new ListPlugin(initializerContext); diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts new file mode 100644 index 0000000000000..2498c36967a53 --- /dev/null +++ b/x-pack/plugins/lists/server/plugin.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { first } from 'rxjs/operators'; +import { Logger, PluginInitializerContext } from 'kibana/server'; +import { CoreSetup } from 'src/core/server'; + +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesServiceSetup } from '../../spaces/server'; + +import { ConfigType } from './config'; +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 security: SecurityPluginSetup | undefined | null; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); + } + + public async setup(core: CoreSetup, plugins: PluginsSetup): Promise<void> { + const config = await createConfig$(this.initializerContext) + .pipe(first()) + .toPromise(); + + this.logger.error( + 'You have activated the lists values feature flag which is NOT currently supported for Elastic Security! You should turn this feature flag off immediately by un-setting "xpack.lists.enabled: true" in kibana.yml and restarting Kibana' + ); + this.spaces = plugins.spaces?.spacesService; + this.config = config; + this.security = plugins.security; + + core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext()); + const router = core.http.createRouter(); + initRoutes(router); + } + + public start(): void { + this.logger.debug('Starting plugin'); + } + + public stop(): void { + this.logger.debug('Stopping plugin'); + } + + private createRouteHandlerContext = (): ContextProvider => { + return async (context, request): ContextProviderReturn => { + const { spaces, config, security } = this; + const { + core: { + elasticsearch: { + dataClient: { callAsCurrentUser }, + }, + }, + } = context; + if (config == null) { + throw new TypeError('Configuration 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, + request, + security, + spaceId, + user, + }), + }; + } + }; + }; +} diff --git a/x-pack/plugins/lists/server/routes/create_list_index_route.ts b/x-pack/plugins/lists/server/routes/create_list_index_route.ts new file mode 100644 index 0000000000000..1c893fb757c5d --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_index_route.ts @@ -0,0 +1,82 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { LIST_INDEX } from '../../common/constants'; +import { acknowledgeSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListIndexRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (listIndexExists && listItemIndexExists) { + return siemResponse.error({ + body: `index: "${lists.getListIndex()}" and "${lists.getListItemIndex()}" already exists`, + statusCode: 409, + }); + } else { + const policyExists = await lists.getListPolicyExists(); + const policyListItemExists = await lists.getListItemPolicyExists(); + + if (!policyExists) { + await lists.setListPolicy(); + } + if (!policyListItemExists) { + await lists.setListItemPolicy(); + } + + const templateExists = await lists.getListTemplateExists(); + const templateListItemsExists = await lists.getListItemTemplateExists(); + + if (!templateExists) { + await lists.setListTemplate(); + } + + if (!templateListItemsExists) { + await lists.setListItemTemplate(); + } + + if (!listIndexExists) { + await lists.createListBootStrapIndex(); + } + if (!listItemIndexExists) { + await lists.createListItemBootStrapIndex(); + } + + const [validated, errors] = validate({ acknowledged: true }, acknowledgeSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_list_item_route.ts b/x-pack/plugins/lists/server/routes/create_list_item_route.ts new file mode 100644 index 0000000000000..68622e98cbc52 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_item_route.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { createListItemSchema, listItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(createListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value, meta } = request.body; + const lists = getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const listItem = await lists.getListItemByValue({ listId, type: list.type, value }); + if (listItem.length !== 0) { + return siemResponse.error({ + body: `list_id: "${listId}" already contains the given value: ${value}`, + statusCode: 409, + }); + } else { + const createdListItem = await lists.createListItem({ + id, + listId, + meta, + type: list.type, + value, + }); + const [validated, errors] = validate(createdListItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/create_list_route.ts b/x-pack/plugins/lists/server/routes/create_list_route.ts new file mode 100644 index 0000000000000..0f3c404c53cfd --- /dev/null +++ b/x-pack/plugins/lists/server/routes/create_list_route.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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { createListSchema, listSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const createListRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(createListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, type, meta } = request.body; + const lists = getListClient(context); + const listExists = await lists.getListIndexExists(); + if (!listExists) { + return siemResponse.error({ + body: `To create a list, the index must exist first. Index "${lists.getListIndex()}" does not exist`, + statusCode: 400, + }); + } else { + if (id != null) { + const list = await lists.getList({ id }); + if (list != null) { + return siemResponse.error({ + body: `list id: "${id}" already exists`, + statusCode: 409, + }); + } + } + const list = await lists.createList({ description, id, meta, name, type }); + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_index_route.ts b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts new file mode 100644 index 0000000000000..424c3f45aac40 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_index_route.ts @@ -0,0 +1,97 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_INDEX } from '../../common/constants'; +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { acknowledgeSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +/** + * Deletes all of the indexes, template, ilm policies, and aliases. You can check + * this by looking at each of these settings from ES after a deletion: + * + * GET /_template/.lists-default + * GET /.lists-default-000001/ + * GET /_ilm/policy/.lists-default + * GET /_alias/.lists-default + * + * GET /_template/.items-default + * GET /.items-default-000001/ + * GET /_ilm/policy/.items-default + * GET /_alias/.items-default + * + * And ensuring they're all gone + */ +export const deleteListIndexRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (!listIndexExists && !listItemIndexExists) { + return siemResponse.error({ + body: `index: "${lists.getListIndex()}" and "${lists.getListItemIndex()}" does not exist`, + statusCode: 404, + }); + } else { + if (listIndexExists) { + await lists.deleteListIndex(); + } + if (listItemIndexExists) { + await lists.deleteListItemIndex(); + } + + const listsPolicyExists = await lists.getListPolicyExists(); + const listItemPolicyExists = await lists.getListItemPolicyExists(); + + if (listsPolicyExists) { + await lists.deleteListPolicy(); + } + if (listItemPolicyExists) { + await lists.deleteListItemPolicy(); + } + + const listsTemplateExists = await lists.getListTemplateExists(); + const listItemTemplateExists = await lists.getListItemTemplateExists(); + + if (listsTemplateExists) { + await lists.deleteListTemplate(); + } + if (listItemTemplateExists) { + await lists.deleteListItemTemplate(); + } + + const [validated, errors] = validate({ acknowledged: true }, acknowledgeSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_item_route.ts b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts new file mode 100644 index 0000000000000..51b4eb9f02cc2 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_item_route.ts @@ -0,0 +1,89 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { deleteListItemSchema, listItemArraySchema, listItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const deleteListItemRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + query: buildRouteValidation(deleteListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = getListClient(context); + if (id != null) { + const deleted = await lists.deleteListItem({ id }); + if (deleted == null) { + return siemResponse.error({ + body: `list item with id: "${id}" item not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const deleted = await lists.deleteListItemByValue({ listId, type: list.type, value }); + if (deleted == null || deleted.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" with ${value} was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listItemArraySchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: `Either "list_id" or "id" needs to be defined in the request`, + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/delete_list_route.ts b/x-pack/plugins/lists/server/routes/delete_list_route.ts new file mode 100644 index 0000000000000..e89355b7689c5 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/delete_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { deleteListSchema, listSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const deleteListRoute = (router: IRouter): void => { + router.delete( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + query: buildRouteValidation(deleteListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const lists = getListClient(context); + const { id } = request.query; + const deleted = await lists.deleteList({ id }); + if (deleted == null) { + return siemResponse.error({ + body: `list id: "${id}" was not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(deleted, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/export_list_item_route.ts b/x-pack/plugins/lists/server/routes/export_list_item_route.ts new file mode 100644 index 0000000000000..32b99bfc512bf --- /dev/null +++ b/x-pack/plugins/lists/server/routes/export_list_item_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Stream } from 'stream'; + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps'; +import { exportListItemQuerySchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const exportListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + tags: ['access:lists'], + }, + path: `${LIST_ITEM_URL}/_export`, + validate: { + query: buildRouteValidation(exportListItemQuerySchema), + // TODO: Do we want to add a body here like export_rules_route and allow a size limit? + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { list_id: listId } = request.query; + const lists = getListClient(context); + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list_id: ${listId} does not exist`, + statusCode: 400, + }); + } else { + // TODO: Allow the API to override the name of the file to export + const fileName = list.name; + + const stream = new Stream.PassThrough(); + lists.exportListItemsToStream({ listId, stream, stringToAppend: '\n' }); + return response.ok({ + body: stream, + headers: { + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Content-Type': 'text/plain', + }, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts new file mode 100644 index 0000000000000..a3b6a520a4ecf --- /dev/null +++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { + ImportListItemSchema, + importListItemQuerySchema, + importListItemSchema, + listSchema, +} from '../../common/schemas'; + +import { getListClient } from '.'; + +export const importListItemRoute = (router: IRouter): void => { + router.post( + { + options: { + body: { + output: 'stream', + }, + tags: ['access:lists'], + }, + path: `${LIST_ITEM_URL}/_import`, + validate: { + body: buildRouteValidation<typeof importListItemSchema, ImportListItemSchema>( + importListItemSchema + ), + query: buildRouteValidation(importListItemQuerySchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { list_id: listId, type } = request.query; + const lists = getListClient(context); + if (listId != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 409, + }); + } + await lists.importListItemsToStream({ + listId, + meta: undefined, + stream: request.body.file, + type: list.type, + }); + + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (type != null) { + const { filename } = request.body.file.hapi; + // TODO: Should we prevent the same file from being uploaded multiple times? + const list = await lists.createListIfItDoesNotExist({ + description: `File uploaded from file system of ${filename}`, + id: filename, + meta: undefined, + name: filename, + type, + }); + await lists.importListItemsToStream({ + listId: list.id, + meta: undefined, + stream: request.body.file, + type: list.type, + }); + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else { + return siemResponse.error({ + body: 'Either type or list_id need to be defined in the query', + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts new file mode 100644 index 0000000000000..4951cddc56939 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -0,0 +1,21 @@ +/* + * 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 * from './create_list_index_route'; +export * from './create_list_item_route'; +export * from './create_list_route'; +export * from './delete_list_index_route'; +export * from './delete_list_item_route'; +export * from './delete_list_route'; +export * from './export_list_item_route'; +export * from './import_list_item_route'; +export * from './init_routes'; +export * from './patch_list_item_route'; +export * from './patch_list_route'; +export * from './read_list_index_route'; +export * from './read_list_item_route'; +export * from './read_list_route'; +export * from './utils'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts new file mode 100644 index 0000000000000..924dd086ee708 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { updateListRoute } from './update_list_route'; +import { updateListItemRoute } from './update_list_item_route'; + +import { + createListIndexRoute, + createListItemRoute, + createListRoute, + deleteListIndexRoute, + deleteListItemRoute, + deleteListRoute, + exportListItemRoute, + importListItemRoute, + patchListItemRoute, + patchListRoute, + readListIndexRoute, + readListItemRoute, + readListRoute, +} from '.'; + +export const initRoutes = (router: IRouter): void => { + // lists + createListRoute(router); + readListRoute(router); + updateListRoute(router); + deleteListRoute(router); + patchListRoute(router); + + // lists items + createListItemRoute(router); + readListItemRoute(router); + updateListItemRoute(router); + deleteListItemRoute(router); + patchListItemRoute(router); + exportListItemRoute(router); + importListItemRoute(router); + + // indexes of lists + createListIndexRoute(router); + readListIndexRoute(router); + deleteListIndexRoute(router); +}; diff --git a/x-pack/plugins/lists/server/routes/patch_list_item_route.ts b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts new file mode 100644 index 0000000000000..e18fd0618b133 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/patch_list_item_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemSchema, patchListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const patchListItemRoute = (router: IRouter): void => { + router.patch( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(patchListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta } = request.body; + const lists = getListClient(context); + const listItem = await lists.updateListItem({ + id, + meta, + value, + }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/patch_list_route.ts b/x-pack/plugins/lists/server/routes/patch_list_route.ts new file mode 100644 index 0000000000000..9d3fa4db8ccd0 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/patch_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, patchListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const patchListRoute = (router: IRouter): void => { + router.patch( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(patchListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta } = request.body; + const lists = getListClient(context); + const list = await lists.updateList({ description, id, meta, name }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" found found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_index_route.ts b/x-pack/plugins/lists/server/routes/read_list_index_route.ts new file mode 100644 index 0000000000000..248fc72666d70 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_index_route.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 { IRouter } from 'kibana/server'; + +import { LIST_INDEX } from '../../common/constants'; +import { buildSiemResponse, transformError, validate } from '../siem_server_deps'; +import { listItemIndexExistSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListIndexRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_INDEX, + validate: false, + }, + async (context, _, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const lists = getListClient(context); + const listIndexExists = await lists.getListIndexExists(); + const listItemIndexExists = await lists.getListItemIndexExists(); + + if (listIndexExists || listItemIndexExists) { + const [validated, errors] = validate( + { list_index: listIndexExists, lists_item_index: listItemIndexExists }, + listItemIndexExistSchema + ); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } else if (!listIndexExists && listItemIndexExists) { + return siemResponse.error({ + body: `index ${lists.getListIndex()} does not exist`, + statusCode: 404, + }); + } else if (!listItemIndexExists && listIndexExists) { + return siemResponse.error({ + body: `index ${lists.getListItemIndex()} does not exist`, + statusCode: 404, + }); + } else { + return siemResponse.error({ + body: `index ${lists.getListIndex()} and index ${lists.getListItemIndex()} does not exist`, + statusCode: 404, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_item_route.ts b/x-pack/plugins/lists/server/routes/read_list_item_route.ts new file mode 100644 index 0000000000000..0a60cba786f04 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_item_route.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemArraySchema, listItemSchema, readListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListItemRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + query: buildRouteValidation(readListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id, list_id: listId, value } = request.query; + const lists = getListClient(context); + if (id != null) { + const listItem = await lists.getListItem({ id }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } else if (listId != null && value != null) { + const list = await lists.getList({ id: listId }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${listId}" does not exist`, + statusCode: 404, + }); + } else { + const listItem = await lists.getListItemByValue({ + listId, + type: list.type, + value, + }); + if (listItem.length === 0) { + return siemResponse.error({ + body: `list_id: "${listId}" item of ${value} does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemArraySchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } + } else { + return siemResponse.error({ + body: `Either "list_id" or "id" needs to be defined in the request`, + statusCode: 400, + }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/read_list_route.ts b/x-pack/plugins/lists/server/routes/read_list_route.ts new file mode 100644 index 0000000000000..c30eadfca0b65 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/read_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, readListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const readListRoute = (router: IRouter): void => { + router.get( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + query: buildRouteValidation(readListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { id } = request.query; + const lists = getListClient(context); + const list = await lists.getList({ id }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" does not exist`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_list_item_route.ts b/x-pack/plugins/lists/server/routes/update_list_item_route.ts new file mode 100644 index 0000000000000..494d57b93b8e4 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_list_item_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; + +import { LIST_ITEM_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listItemSchema, updateListItemSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const updateListItemRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: LIST_ITEM_URL, + validate: { + body: buildRouteValidation(updateListItemSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { value, id, meta } = request.body; + const lists = getListClient(context); + const listItem = await lists.updateListItem({ + id, + meta, + value, + }); + if (listItem == null) { + return siemResponse.error({ + body: `list item id: "${id}" not found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(listItem, listItemSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/update_list_route.ts b/x-pack/plugins/lists/server/routes/update_list_route.ts new file mode 100644 index 0000000000000..6ace61e46a780 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/update_list_route.ts @@ -0,0 +1,59 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +import { LIST_URL } from '../../common/constants'; +import { + buildRouteValidation, + buildSiemResponse, + transformError, + validate, +} from '../siem_server_deps'; +import { listSchema, updateListSchema } from '../../common/schemas'; + +import { getListClient } from '.'; + +export const updateListRoute = (router: IRouter): void => { + router.put( + { + options: { + tags: ['access:lists'], + }, + path: LIST_URL, + validate: { + body: buildRouteValidation(updateListSchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + try { + const { name, description, id, meta } = request.body; + const lists = getListClient(context); + const list = await lists.updateList({ description, id, meta, name }); + if (list == null) { + return siemResponse.error({ + body: `list id: "${id}" found found`, + statusCode: 404, + }); + } else { + const [validated, errors] = validate(list, listSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/utils/get_list_client.ts b/x-pack/plugins/lists/server/routes/utils/get_list_client.ts new file mode 100644 index 0000000000000..a16163ec0fa3a --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/get_list_client.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'kibana/server'; + +import { ListClient } from '../../services/lists/client'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +export const getListClient = (context: RequestHandlerContext): ListClient => { + const lists = context.lists?.getListClient(); + if (lists == null) { + throw new ErrorWithStatusCode('Lists is not found as a plugin', 404); + } else { + return lists; + } +}; diff --git a/x-pack/plugins/lists/server/routes/utils/index.ts b/x-pack/plugins/lists/server/routes/utils/index.ts new file mode 100644 index 0000000000000..a601bdfc003c5 --- /dev/null +++ b/x-pack/plugins/lists/server/routes/utils/index.ts @@ -0,0 +1,6 @@ +/* + * 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 * from './get_list_client'; diff --git a/x-pack/plugins/lists/server/scripts/check_env_variables.sh b/x-pack/plugins/lists/server/scripts/check_env_variables.sh new file mode 100755 index 0000000000000..fb3bbbe0fad18 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/check_env_variables.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# +# 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. +# + +# Add this to the start of any scripts to detect if env variables are set + +set -e + +if [ -z "${ELASTICSEARCH_USERNAME}" ]; then + echo "Set ELASTICSEARCH_USERNAME in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_PASSWORD}" ]; then + echo "Set ELASTICSEARCH_PASSWORD in your environment" + exit 1 +fi + +if [ -z "${ELASTICSEARCH_URL}" ]; then + echo "Set ELASTICSEARCH_URL in your environment" + exit 1 +fi + +if [ -z "${KIBANA_URL}" ]; then + echo "Set KIBANA_URL in your environment" + exit 1 +fi + +if [ -z "${TASK_MANAGER_INDEX}" ]; then + echo "Set TASK_MANAGER_INDEX in your environment" + exit 1 +fi + +if [ -z "${KIBANA_INDEX}" ]; then + echo "Set KIBANA_INDEX in your environment" + exit 1 +fi diff --git a/x-pack/plugins/lists/server/scripts/delete_all_lists.sh b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh new file mode 100755 index 0000000000000..5b65bb14414c7 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_all_lists.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_all_lists.sh +# https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html + + +# Delete all the main lists that have children items +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ + --data '{ + "query": { + "exists": { "field": "siem_list" } + } + }' \ + | jq . + +# Delete all the list children items as well +curl -s -k \ + -H "Content-Type: application/json" \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${ELASTICSEARCH_URL}/${KIBANA_INDEX}*/_delete_by_query \ + --data '{ + "query": { + "exists": { "field": "siem_list_item" } + } + }' \ + | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list.sh b/x-pack/plugins/lists/server/scripts/delete_list.sh new file mode 100755 index 0000000000000..9934ce61c7107 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_by_list_id.sh ${list_id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_index.sh b/x-pack/plugins/lists/server/scripts/delete_list_index.sh new file mode 100755 index 0000000000000..85f06ffbd6670 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_index.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_signal_index.sh +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh new file mode 100755 index 0000000000000..ab14d8c8a80ed --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_item_by_id.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_item_by_id.sh?id={id} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE ${KIBANA_URL}${SPACE_URL}/api/lists/items?id=$1 | jq . diff --git a/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh b/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh new file mode 100755 index 0000000000000..6d3213ccb8793 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/delete_list_item_by_value.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./delete_list_item_by_value.sh?list_id=${some_id}&value=${some_ip} +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X DELETE "${KIBANA_URL}${SPACE_URL}/api/lists/items?list_id=$1&value=$2" | jq . diff --git a/x-pack/plugins/lists/server/scripts/export_list_items.sh b/x-pack/plugins/lists/server/scripts/export_list_items.sh new file mode 100755 index 0000000000000..ba355854c77cc --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/export_list_items.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +LIST_ID=${1:-ips.txt} + +# Example to export +# ./export_list_items.sh > /tmp/ips.txt + +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=${LIST_ID}" diff --git a/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh new file mode 100755 index 0000000000000..5efad01e9a68e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/export_list_items_to_file.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +FOLDER=${1:-/tmp} + +# Example to export +# ./export_list_items_to_file.sh + +# Change current working directory as exports cause Kibana to restart +pushd ${FOLDER} > /dev/null + +curl -s -k -OJ \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_export?list_id=list-ip" + +popd > /dev/null diff --git a/x-pack/plugins/lists/server/scripts/get_list.sh b/x-pack/plugins/lists/server/scripts/get_list.sh new file mode 100755 index 0000000000000..7f0e4e3062266 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list.sh {list_id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/lists?id="$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh b/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh new file mode 100755 index 0000000000000..31d26e195815f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list_item_by_id.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list_item_by_id ${id} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items?id=$1" | jq . diff --git a/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh b/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh new file mode 100755 index 0000000000000..24ca27b0c949d --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/get_list_item_by_value.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_list_item_by_value.sh ${list_id} ${value} +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET "${KIBANA_URL}${SPACE_URL}/api/lists/items?list_id=$1&value=$2" | jq . diff --git a/x-pack/plugins/lists/server/scripts/hard_reset.sh b/x-pack/plugins/lists/server/scripts/hard_reset.sh new file mode 100755 index 0000000000000..861928866369b --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/hard_reset.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# re-create the list and list item indexes +./delete_list_index.sh +./post_list_index.sh diff --git a/x-pack/plugins/lists/server/scripts/import_list_items.sh b/x-pack/plugins/lists/server/scripts/import_list_items.sh new file mode 100755 index 0000000000000..a39409cd08267 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/import_list_items.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +LIST_ID=${1:-list-ip} +FILE=${2:-./lists/files/ips.txt} + +# ./import_list_items.sh list-ip ./lists/files/ips.txt +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_import?list_id=${LIST_ID}" \ + --form file=@${FILE} \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh b/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh new file mode 100755 index 0000000000000..4ec55cb4c5f7b --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/import_list_items_by_filename.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a defaults if no argument is specified +TYPE=${1:-ip} +FILE=${2:-./lists/files/ips.txt} + +# Example to import ips from ./lists/files/ips.txt +# ./import_list_items_by_filename.sh ip ./lists/files/ips.txt + +curl -s -k \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST "${KIBANA_URL}${SPACE_URL}/api/lists/items/_import?type=${TYPE}" \ + --form file=@${FILE} \ + | jq .; diff --git a/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt new file mode 100644 index 0000000000000..aee32e3a4bd92 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/files/hosts.txt @@ -0,0 +1,2 @@ +kibana +rock01 diff --git a/x-pack/plugins/lists/server/scripts/lists/files/ips.txt b/x-pack/plugins/lists/server/scripts/lists/files/ips.txt new file mode 100644 index 0000000000000..cf8ebcacae5a1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/files/ips.txt @@ -0,0 +1,9 @@ +127.0.0.1 +127.0.0.2 +127.0.0.3 +127.0.0.4 +127.0.0.5 +127.0.0.6 +127.0.0.7 +127.0.0.8 +127.0.0.9 diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json b/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json new file mode 100644 index 0000000000000..196b3b149ab82 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_everything.json @@ -0,0 +1,13 @@ +{ + "id": "list-ip-everything", + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip", + "meta": { + "level_1_meta": { + "level_2_meta": { + "level_3_key": "some_value_ui" + } + } + } +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json new file mode 100644 index 0000000000000..3e12ef1754f07 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip.json @@ -0,0 +1,6 @@ +{ + "id": "list-ip", + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json new file mode 100644 index 0000000000000..1516fa5057e50 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item.json @@ -0,0 +1,5 @@ +{ + "id": "hand_inserted_item_id", + "list_id": "list-ip", + "value": "127.0.0.1" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json new file mode 100644 index 0000000000000..9730c1b7523f1 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_item_everything.json @@ -0,0 +1,12 @@ +{ + "id": "hand_inserted_item_id_everything", + "list_id": "list-ip", + "value": "127.0.0.2", + "meta": { + "level_1_meta": { + "level_2_meta": { + "level_3_key": "some_value_ui" + } + } + } +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json new file mode 100644 index 0000000000000..4a95a62b67c3e --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_ip_no_id.json @@ -0,0 +1,5 @@ +{ + "name": "Simple list with an ip", + "description": "This list describes bad internet ip", + "type": "ip" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json new file mode 100644 index 0000000000000..e8f5fa7e38a06 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword.json @@ -0,0 +1,6 @@ +{ + "id": "list-keyword", + "name": "Simple list with a keyword", + "description": "This list describes bad host names", + "type": "keyword" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json new file mode 100644 index 0000000000000..b736e7b96ad98 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/new/list_keyword_item.json @@ -0,0 +1,4 @@ +{ + "list_id": "list-keyword", + "value": "kibana" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json new file mode 100644 index 0000000000000..00c3496e71b35 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/patches/list_ip_item.json @@ -0,0 +1,4 @@ +{ + "id": "hand_inserted_item_id", + "value": "255.255.255.255" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json b/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json new file mode 100644 index 0000000000000..1a57ab8b6a3b9 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/patches/simplest_updated_name.json @@ -0,0 +1,4 @@ +{ + "id": "list-ip", + "name": "Changed the name here to something else" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json b/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json new file mode 100644 index 0000000000000..00c3496e71b35 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/updates/list_ip_item.json @@ -0,0 +1,4 @@ +{ + "id": "hand_inserted_item_id", + "value": "255.255.255.255" +} diff --git a/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json b/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json new file mode 100644 index 0000000000000..936a070ede52c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists/updates/simple_update.json @@ -0,0 +1,5 @@ +{ + "id": "list-ip", + "name": "Changed the name here to something else", + "description": "Some other description here for you" +} diff --git a/x-pack/plugins/lists/server/scripts/lists_index_exists.sh b/x-pack/plugins/lists/server/scripts/lists_index_exists.sh new file mode 100755 index 0000000000000..7dfbd5b1bada5 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/lists_index_exists.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./lists_index_exists.sh +curl -s -k -f \ + -H 'Content-Type: application/json' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/patch_list.sh b/x-pack/plugins/lists/server/scripts/patch_list.sh new file mode 100755 index 0000000000000..3a517a52dbd21 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/patch_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/patches/simplest_updated_name.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/patches/simplest_updated_name.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/patch_list_item.sh b/x-pack/plugins/lists/server/scripts/patch_list_item.sh new file mode 100755 index 0000000000000..406b03dc6499c --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/patch_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/patches/list_ip_item.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/patches/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/post_list.sh b/x-pack/plugins/lists/server/scripts/post_list.sh new file mode 100755 index 0000000000000..6aaffee0bc4b2 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/new/list_ip.json}) + +# Example: ./post_list.sh +# Example: ./post_list.sh ./lists/new/list_ip.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/post_list_index.sh b/x-pack/plugins/lists/server/scripts/post_list_index.sh new file mode 100755 index 0000000000000..b7c372d3947e3 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list_index.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./post_signal_index.sh +curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists/index | jq . diff --git a/x-pack/plugins/lists/server/scripts/post_list_item.sh b/x-pack/plugins/lists/server/scripts/post_list_item.sh new file mode 100755 index 0000000000000..b55a60420674f --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/post_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/new/list_ip_item.json}) + +# Example: ./post_list.sh +# Example: ./post_list.sh ./lists/new/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_list.sh b/x-pack/plugins/lists/server/scripts/update_list.sh new file mode 100755 index 0000000000000..4d93544d568a8 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_list.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/updates/simple_update.json}) + +# Example: ./update_list.sh +# Example: ./update_list.sh ./lists/updates/simple_update.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PUT ${KIBANA_URL}${SPACE_URL}/api/lists \ + -d @${LIST} \ + | jq .; +} & +done + +wait diff --git a/x-pack/plugins/lists/server/scripts/update_list_item.sh b/x-pack/plugins/lists/server/scripts/update_list_item.sh new file mode 100755 index 0000000000000..e3153bfd25b19 --- /dev/null +++ b/x-pack/plugins/lists/server/scripts/update_list_item.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Uses a default if no argument is specified +LISTS=(${@:-./lists/updates/list_ip_item.json}) + +# Example: ./patch_list.sh +# Example: ./patch_list.sh ./lists/updates/list_ip_item.json +for LIST in "${LISTS[@]}" +do { + [ -e "$LIST" ] || continue + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X PATCH ${KIBANA_URL}${SPACE_URL}/api/lists/items \ + -d @${LIST} \ + | jq .; +} & +done + +wait 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 new file mode 100644 index 0000000000000..48deb3ee86820 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TestReadable } from '../mocks'; + +import { BufferLines } from './buffer_lines'; + +describe('buffer_lines', () => { + test('it can read a single line', done => { + const input = new TestReadable(); + input.push('line one\n'); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one']); + done(); + }); + }); + + test('it can read two lines', done => { + const input = new TestReadable(); + input.push('line one\n'); + input.push('line two\n'); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one', 'line two']); + done(); + }); + }); + + test('two identical lines are collapsed into just one line without duplicates', done => { + const input = new TestReadable(); + input.push('line one\n'); + input.push('line one\n'); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual(['line one']); + done(); + }); + }); + + test('it can close out without writing any lines', done => { + const input = new TestReadable(); + input.push(null); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest).toEqual([]); + done(); + }); + }); + + test('it can read 200 lines', done => { + const input = new TestReadable(); + const bufferedLine = new BufferLines({ input }); + let linesToTest: string[] = []; + const size200: string[] = new Array(200).fill(null).map((_, index) => `${index}\n`); + size200.forEach(element => input.push(element)); + input.push(null); + bufferedLine.on('lines', (lines: string[]) => { + linesToTest = [...linesToTest, ...lines]; + }); + bufferedLine.on('close', () => { + expect(linesToTest.length).toEqual(200); + done(); + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.ts new file mode 100644 index 0000000000000..fd8fe7077fd58 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/buffer_lines.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import readLine from 'readline'; +import { Readable } from 'stream'; + +const BUFFER_SIZE = 100; + +export class BufferLines extends Readable { + private set = new Set<string>(); + constructor({ input }: { input: NodeJS.ReadableStream }) { + super({ encoding: 'utf-8' }); + const readline = readLine.createInterface({ + input, + }); + + readline.on('line', line => { + this.push(line); + }); + + readline.on('close', () => { + this.push(null); + }); + } + + public _read(): void { + // No operation but this is required to be implemented + } + + public push(line: string | null): boolean { + if (line == null) { + this.emit('lines', Array.from(this.set)); + this.set.clear(); + this.emit('close'); + return true; + } else { + this.set.add(line); + if (this.set.size > BUFFER_SIZE) { + this.emit('lines', Array.from(this.set)); + this.set.clear(); + return true; + } else { + return true; + } + } + } +} 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 new file mode 100644 index 0000000000000..abbb270149955 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { + LIST_ITEM_ID, + LIST_ITEM_INDEX, + getCreateListItemOptionsMock, + getIndexESListItemMock, + getListItemResponseMock, +} from '../mocks'; + +import { createListItem } from './create_list_item'; + +describe('crete_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list item as expected with the id changed out for the elastic id', async () => { + const options = getCreateListItemOptionsMock(); + const listItem = await createListItem(options); + const expected = getListItemResponseMock(); + expected.id = 'elastic-id-123'; + expect(listItem).toEqual(expected); + }); + + test('It calls "callCluster" with body, index, and listIndex', async () => { + const options = getCreateListItemOptionsMock(); + await createListItem(options); + const body = getIndexESListItemMock(); + const expected = { + body, + id: LIST_ITEM_ID, + index: LIST_ITEM_INDEX, + }; + expect(options.callCluster).toBeCalledWith('index', expected); + }); + + test('It returns an auto-generated id if id is sent in undefined', async () => { + const options = getCreateListItemOptionsMock(); + options.id = undefined; + const list = await createListItem(options); + const expected = getListItemResponseMock(); + expected.id = 'elastic-id-123'; + expect(list).toEqual(expected); + }); +}); 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 new file mode 100644 index 0000000000000..83a118b795192 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts @@ -0,0 +1,73 @@ +/* + * 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 uuid from 'uuid'; +import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + IdOrUndefined, + IndexEsListItemSchema, + ListItemSchema, + MetaOrUndefined, + Type, +} from '../../../common/schemas'; +import { transformListItemToElasticQuery } from '../utils'; + +export interface CreateListItemOptions { + id: IdOrUndefined; + listId: string; + type: Type; + value: string; + callCluster: APICaller; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string; +} + +export const createListItem = async ({ + id, + listId, + type, + value, + callCluster, + listItemIndex, + user, + meta, + dateNow, + tieBreaker, +}: CreateListItemOptions): Promise<ListItemSchema> => { + const createdAt = dateNow ?? new Date().toISOString(); + const tieBreakerId = tieBreaker ?? uuid.v4(); + const baseBody = { + created_at: createdAt, + created_by: user, + list_id: listId, + meta, + tie_breaker_id: tieBreakerId, + updated_at: createdAt, + updated_by: user, + }; + const body: IndexEsListItemSchema = { + ...baseBody, + ...transformListItemToElasticQuery({ type, value }), + }; + + const response: CreateDocumentResponse = await callCluster('index', { + body, + id, + index: listItemIndex, + }); + + return { + id: response._id, + type, + value, + ...baseBody, + }; +}; 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 new file mode 100644 index 0000000000000..94cc57b53b4e2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndexEsListItemSchema } from '../../../common/schemas'; +import { + LIST_ITEM_INDEX, + TIE_BREAKERS, + VALUE_2, + getCreateListItemBulkOptionsMock, + getIndexESListItemMock, +} from '../mocks'; + +import { createListItemsBulk } from './create_list_items_bulk'; + +describe('crete_list_item_bulk', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + 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.callCluster).toBeCalledWith('bulk', { + body: [ + { create: { _index: LIST_ITEM_INDEX } }, + firstRecord, + { create: { _index: LIST_ITEM_INDEX } }, + secondRecord, + ], + index: LIST_ITEM_INDEX, + }); + }); + + test('It should not call the dataClient when the values are empty', async () => { + const options = getCreateListItemBulkOptionsMock(); + options.value = []; + 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 new file mode 100644 index 0000000000000..eac294c5f244a --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import { APICaller } from 'kibana/server'; + +import { transformListItemToElasticQuery } from '../utils'; +import { + CreateEsBulkTypeSchema, + IndexEsListItemSchema, + MetaOrUndefined, + Type, +} from '../../../common/schemas'; + +export interface CreateListItemsBulkOptions { + listId: string; + type: Type; + value: string[]; + callCluster: APICaller; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string[]; +} + +export const createListItemsBulk = async ({ + listId, + type, + value, + callCluster, + listItemIndex, + user, + meta, + dateNow, + tieBreaker, +}: CreateListItemsBulkOptions): Promise<void> => { + // It causes errors if you try to add items to bulk that do not exist within ES + if (!value.length) { + return; + } + const body = value.reduce<Array<IndexEsListItemSchema | CreateEsBulkTypeSchema>>( + (accum, singleValue, index) => { + const createdAt = dateNow ?? new Date().toISOString(); + const tieBreakerId = + tieBreaker != null && tieBreaker[index] != null ? tieBreaker[index] : uuid.v4(); + const elasticBody: IndexEsListItemSchema = { + created_at: createdAt, + created_by: user, + list_id: listId, + meta, + tie_breaker_id: tieBreakerId, + updated_at: createdAt, + updated_by: user, + ...transformListItemToElasticQuery({ type, value: singleValue }), + }; + const createBody: CreateEsBulkTypeSchema = { create: { _index: listItemIndex } }; + return [...accum, createBody, elasticBody]; + }, + [] + ); + + 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 new file mode 100644 index 0000000000000..00fcefb2c379f --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + LIST_ITEM_ID, + LIST_ITEM_INDEX, + getDeleteListItemOptionsMock, + getListItemResponseMock, +} from '../mocks'; + +import { getListItem } from './get_list_item'; +import { deleteListItem } from './delete_list_item'; + +jest.mock('./get_list_item', () => ({ + getListItem: jest.fn(), +})); + +describe('delete_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Delete returns a null if "getListItem" returns a null', async () => { + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getDeleteListItemOptionsMock(); + const deletedListItem = await deleteListItem(options); + expect(deletedListItem).toEqual(null); + }); + + test('Delete returns the same list item if a list item is returned from "getListItem"', async () => { + const listItem = getListItemResponseMock(); + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(listItem); + const options = getDeleteListItemOptionsMock(); + 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); + const options = getDeleteListItemOptionsMock(); + await deleteListItem(options); + const deleteQuery = { + id: LIST_ITEM_ID, + index: LIST_ITEM_INDEX, + }; + 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 new file mode 100644 index 0000000000000..9992f43387c89 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 { getListItem } from '.'; + +export interface DeleteListItemOptions { + id: Id; + callCluster: APICaller; + listItemIndex: string; +} + +export const deleteListItem = async ({ + id, + callCluster, + listItemIndex, +}: DeleteListItemOptions): Promise<ListItemSchema | null> => { + const listItem = await getListItem({ callCluster, id, listItemIndex }); + if (listItem == null) { + return null; + } else { + await callCluster('delete', { + id, + index: listItemIndex, + }); + } + return listItem; +}; 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 new file mode 100644 index 0000000000000..c7c80638e4c37 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getDeleteListItemByValueOptionsMock, getListItemResponseMock } from '../mocks'; + +import { getListItemByValues } from './get_list_item_by_values'; +import { deleteListItemByValue } from './delete_list_item_by_value'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('delete_list_item_by_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Delete returns a an empty array if the list items are also empty', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getDeleteListItemByValueOptionsMock(); + const deletedListItem = await deleteListItemByValue(options); + expect(deletedListItem).toEqual([]); + }); + + test('Delete returns the list item if a list item is returned from "getListByValues"', async () => { + const listItems = [getListItemResponseMock()]; + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce(listItems); + const options = getDeleteListItemByValueOptionsMock(); + const deletedListItem = await deleteListItemByValue(options); + expect(deletedListItem).toEqual(listItems); + }); + + test('Delete calls "deleteByQuery" if a list item is returned from "getListByValues"', async () => { + const listItems = [getListItemResponseMock()]; + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce(listItems); + const options = getDeleteListItemByValueOptionsMock(); + await deleteListItemByValue(options); + const deleteByQuery = { + body: { + query: { + bool: { + filter: [{ term: { list_id: 'some-list-id' } }, { terms: { ip: ['127.0.0.1'] } }], + }, + }, + }, + index: '.items', + }; + 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 new file mode 100644 index 0000000000000..ec29f14a0ff64 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts @@ -0,0 +1,53 @@ +/* + * 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 { APICaller } from 'kibana/server'; + +import { ListItemArraySchema, Type } from '../../../common/schemas'; +import { getQueryFilterFromTypeValue } from '../utils'; + +import { getListItemByValues } from './get_list_item_by_values'; + +export interface DeleteListItemByValueOptions { + listId: string; + type: Type; + value: string; + callCluster: APICaller; + listItemIndex: string; +} + +export const deleteListItemByValue = async ({ + listId, + value, + type, + callCluster, + listItemIndex, +}: DeleteListItemByValueOptions): Promise<ListItemArraySchema> => { + const listItems = await getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value: [value], + }); + const values = listItems.map(listItem => listItem.value); + const filter = getQueryFilterFromTypeValue({ + listId, + type, + value: values, + }); + await callCluster('deleteByQuery', { + body: { + query: { + bool: { + filter, + }, + }, + }, + index: listItemIndex, + }); + return listItems; +}; 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 new file mode 100644 index 0000000000000..31a421c2e31bf --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + getCallClusterMock, + getListItemResponseMock, + getSearchListItemMock, +} from '../mocks'; + +import { getListItem } from './get_list_item'; + +describe('get_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list item as expected if the list item is found', async () => { + const data = getSearchListItemMock(); + const callCluster = getCallClusterMock(data); + const list = await getListItem({ callCluster, id: LIST_ID, listItemIndex: LIST_INDEX }); + const expected = getListItemResponseMock(); + expect(list).toEqual(expected); + }); + + test('it returns null if the search is empty', async () => { + const data = getSearchListItemMock(); + data.hits.hits = []; + 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 new file mode 100644 index 0000000000000..83b30d336ccd4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { Id, ListItemSchema, SearchEsListItemSchema } from '../../../common/schemas'; +import { deriveTypeFromItem, transformElasticToListItem } from '../utils'; + +interface GetListItemOptions { + id: Id; + callCluster: APICaller; + listItemIndex: string; +} + +export const getListItem = async ({ + id, + callCluster, + listItemIndex, +}: GetListItemOptions): Promise<ListItemSchema | null> => { + const listItemES: SearchResponse<SearchEsListItemSchema> = await callCluster('search', { + body: { + query: { + term: { + _id: id, + }, + }, + }, + ignoreUnavailable: true, + index: listItemIndex, + }); + + if (listItemES.hits.hits.length) { + const type = deriveTypeFromItem({ item: listItemES.hits.hits[0]._source }); + const listItems = transformElasticToListItem({ response: listItemES, type }); + return listItems[0]; + } else { + return null; + } +}; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts new file mode 100644 index 0000000000000..d30b3c795550f --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { getListItemByValueOptionsMocks, getListItemResponseMock } from '../mocks'; + +import { getListItemByValues } from './get_list_item_by_values'; +import { getListItemByValue } from './get_list_item_by_value'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('get_list_by_value', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Calls get_list_item_by_values with its input', async () => { + const listItemMock = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([listItemMock]); + const options = getListItemByValueOptionsMocks(); + const listItem = await getListItemByValue(options); + const expected = getListItemResponseMock(); + expect(listItem).toEqual([expected]); + }); +}); 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 new file mode 100644 index 0000000000000..49bcf12043d7c --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_value.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * 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 { getListItemByValues } from '.'; + +export interface GetListItemByValueOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + type: Type; + value: string; +} + +export const getListItemByValue = async ({ + listId, + callCluster, + listItemIndex, + type, + value, +}: GetListItemByValueOptions): Promise<ListItemArraySchema> => + getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value: [value], + }); 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 new file mode 100644 index 0000000000000..7f5fff4dc3147 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.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 { + LIST_ID, + LIST_ITEM_INDEX, + TYPE, + VALUE, + VALUE_2, + getCallClusterMock, + getSearchListItemMock, +} from '../mocks'; + +import { getListItemByValues } from './get_list_item_by_values'; + +describe('get_list_item_by_values', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns a an empty array if the ES query is also empty', async () => { + const data = getSearchListItemMock(); + data.hits.hits = []; + const callCluster = getCallClusterMock(data); + const listItem = await getListItemByValues({ + 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 callCluster = getCallClusterMock(data); + const listItem = await getListItemByValues({ + 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', + created_by: 'some user', + id: 'some-list-item-id', + list_id: 'some-list-id', + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: '2020-04-20T15:25:31.830Z', + updated_by: 'some user', + value: '127.0.0.1', + }, + ]); + }); +}); 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 new file mode 100644 index 0000000000000..29b9b01754027 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_by_values.ts @@ -0,0 +1,41 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { getQueryFilterFromTypeValue, transformElasticToListItem } from '../utils'; + +export interface GetListItemByValuesOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + type: Type; + value: string[]; +} + +export const getListItemByValues = async ({ + listId, + callCluster, + listItemIndex, + type, + value, +}: GetListItemByValuesOptions): Promise<ListItemArraySchema> => { + const response: SearchResponse<SearchEsListItemSchema> = 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 + }); + 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 new file mode 100644 index 0000000000000..ffe2eff9f3ca7 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemIndex } from './get_list_item_index'; + +describe('get_list_item_index', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns the list item index when there is a space', async () => { + const listIndex = getListItemIndex({ + listsItemsIndexName: 'lists-items-index', + 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 new file mode 100644 index 0000000000000..4cd93df6d9bf4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_index.ts @@ -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. + */ + +export interface GetListItemIndexOptions { + spaceId: string; + listsItemsIndexName: string; +} + +export const getListItemIndex = ({ + spaceId, + listsItemsIndexName, +}: GetListItemIndexOptions): string => `${listsItemsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_template.test.ts b/x-pack/plugins/lists/server/services/items/get_list_item_template.test.ts new file mode 100644 index 0000000000000..9c85fa6ff0256 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_template.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListItemTemplate } from './get_list_item_template'; + +jest.mock('./list_item_mappings.json', () => ({ + listMappings: {}, +})); + +describe('get_list_item_template', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list template with the string filled in', async () => { + const template = getListItemTemplate('some_index'); + expect(template).toEqual({ + index_patterns: ['some_index-*'], + mappings: { listMappings: {} }, + settings: { index: { lifecycle: { name: 'some_index', rollover_alias: 'some_index' } } }, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/get_list_item_template.ts b/x-pack/plugins/lists/server/services/items/get_list_item_template.ts new file mode 100644 index 0000000000000..95f4a09b40648 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/get_list_item_template.ts @@ -0,0 +1,23 @@ +/* + * 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 listsItemsMappings from './list_item_mappings.json'; + +export const getListItemTemplate = (index: string): Record<string, unknown> => { + const template = { + index_patterns: [`${index}-*`], + mappings: listsItemsMappings, + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, + }; + return template; +}; diff --git a/x-pack/plugins/lists/server/services/items/index.ts b/x-pack/plugins/lists/server/services/items/index.ts new file mode 100644 index 0000000000000..ee1d83fabca31 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './buffer_lines'; +export * from './create_list_item'; +export * from './create_list_items_bulk'; +export * from './delete_list_item_by_value'; +export * from './get_list_item_by_value'; +export * from './get_list_item'; +export * from './get_list_item_by_values'; +export * from './update_list_item'; +export * from './write_lines_to_bulk_list_items'; +export * from './write_list_items_to_stream'; +export * from './get_list_item_template'; +export * from './delete_list_item'; +export * from './get_list_item_index'; diff --git a/x-pack/plugins/lists/server/services/items/list_item_mappings.json b/x-pack/plugins/lists/server/services/items/list_item_mappings.json new file mode 100644 index 0000000000000..ca69c26df52b5 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/list_item_mappings.json @@ -0,0 +1,33 @@ +{ + "dynamic": "strict", + "properties": { + "tie_breaker_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "keyword": { + "type": "keyword" + }, + "meta": { + "enabled": "false", + "type": "object" + }, + "created_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/list_item_policy.json b/x-pack/plugins/lists/server/services/items/list_item_policy.json new file mode 100644 index 0000000000000..a4c84f73e7896 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/list_item_policy.json @@ -0,0 +1,14 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb" + } + } + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.test.ts b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts new file mode 100644 index 0000000000000..4ef4110bc0742 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/update_list_item.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { getListItemResponseMock, getUpdateListItemOptionsMock } from '../mocks'; + +import { updateListItem } from './update_list_item'; +import { getListItem } from './get_list_item'; + +jest.mock('./get_list_item', () => ({ + getListItem: jest.fn(), +})); + +describe('update_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list item as expected with the id changed out for the elastic id when there is a list item to update', async () => { + const list = getListItemResponseMock(); + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getUpdateListItemOptionsMock(); + const updatedList = await updateListItem(options); + const expected = getListItemResponseMock(); + expected.id = 'elastic-id-123'; + expect(updatedList).toEqual(expected); + }); + + test('it returns null when there is not a list item to update', async () => { + ((getListItem as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getUpdateListItemOptionsMock(); + const updatedList = await updateListItem(options); + expect(updatedList).toEqual(null); + }); +}); 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 new file mode 100644 index 0000000000000..6a71b2a0caf41 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + Id, + ListItemSchema, + MetaOrUndefined, + UpdateEsListItemSchema, +} from '../../../common/schemas'; +import { transformListItemToElasticQuery } from '../utils'; + +import { getListItem } from './get_list_item'; + +export interface UpdateListItemOptions { + id: Id; + value: string | null | undefined; + callCluster: APICaller; + listItemIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; +} + +export const updateListItem = async ({ + id, + value, + callCluster, + listItemIndex, + user, + meta, + dateNow, +}: UpdateListItemOptions): Promise<ListItemSchema | null> => { + const updatedAt = dateNow ?? new Date().toISOString(); + const listItem = await getListItem({ callCluster, id, listItemIndex }); + if (listItem == null) { + return null; + } else { + const doc: UpdateEsListItemSchema = { + meta, + updated_at: updatedAt, + updated_by: user, + ...transformListItemToElasticQuery({ type: listItem.type, value: value ?? listItem.value }), + }; + + const response: CreateDocumentResponse = await callCluster('update', { + body: { + doc, + }, + id: listItem.id, + index: listItemIndex, + }); + return { + created_at: listItem.created_at, + created_by: listItem.created_by, + id: response._id, + list_id: listItem.list_id, + meta: meta ?? listItem.meta, + tie_breaker_id: listItem.tie_breaker_id, + type: listItem.type, + updated_at: updatedAt, + updated_by: listItem.updated_by, + value: value ?? listItem.value, + }; + } +}; diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts new file mode 100644 index 0000000000000..f064543f1ec93 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { + getImportListItemsToStreamOptionsMock, + getListItemResponseMock, + getWriteBufferToItemsOptionsMock, +} from '../mocks'; + +import { + LinesResult, + importListItemsToStream, + writeBufferToItems, +} from './write_lines_to_bulk_list_items'; + +import { getListItemByValues } from '.'; + +jest.mock('./get_list_item_by_values', () => ({ + getListItemByValues: jest.fn(), +})); + +describe('write_lines_to_bulk_list_items', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('importListItemsToStream', () => { + test('It imports a set of items to a write buffer by calling "getListItemByValues" with an empty buffer', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith(expect.objectContaining({ value: [] })); + }); + + test('It imports a set of items to a write buffer by calling "getListItemByValues" with a single value given', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push('127.0.0.1\n'); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith(expect.objectContaining({ value: ['127.0.0.1'] })); + }); + + test('It imports a set of items to a write buffer by calling "getListItemByValues" with two values given', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getImportListItemsToStreamOptionsMock(); + const promise = importListItemsToStream(options); + options.stream.push('127.0.0.1\n'); + options.stream.push('127.0.0.2\n'); + options.stream.push(null); + await promise; + expect(getListItemByValues).toBeCalledWith( + expect.objectContaining({ value: ['127.0.0.1', '127.0.0.2'] }) + ); + }); + }); + + describe('writeBufferToItems', () => { + test('It returns no duplicates and no lines processed when given empty items', async () => { + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([]); + const options = getWriteBufferToItemsOptionsMock(); + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It returns no lines processed when given items but no buffer', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It returns 1 lines processed when given a buffer item that is not a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255']; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 0, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = [data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 0, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate and processing a second value as not a duplicate', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255', data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters a duplicate value out and reports it as a duplicate and processing two other values', async () => { + const data = getListItemResponseMock(); + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([data]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = ['255.255.255.255', '192.168.0.1', data.value]; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 1, + linesProcessed: 2, + }; + expect(linesResult).toEqual(expected); + }); + + test('It filters two duplicate values out and reports processes a single value', async () => { + const dataItem1 = getListItemResponseMock(); + dataItem1.value = '127.0.0.1'; + const dataItem2 = getListItemResponseMock(); + dataItem2.value = '127.0.0.2'; + ((getListItemByValues as unknown) as jest.Mock).mockResolvedValueOnce([dataItem1, dataItem2]); + const options = getWriteBufferToItemsOptionsMock(); + options.buffer = [dataItem1.value, dataItem2.value, '192.168.0.0.1']; + const linesResult = await writeBufferToItems(options); + const expected: LinesResult = { + duplicatesFound: 2, + linesProcessed: 1, + }; + expect(linesResult).toEqual(expected); + }); + }); +}); 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 new file mode 100644 index 0000000000000..542c2bb12d8e5 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts @@ -0,0 +1,102 @@ +/* + * 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 { Readable } from 'stream'; + +import { APICaller } from 'kibana/server'; + +import { MetaOrUndefined, Type } from '../../../common/schemas'; + +import { BufferLines } from './buffer_lines'; +import { getListItemByValues } from './get_list_item_by_values'; +import { createListItemsBulk } from './create_list_items_bulk'; + +export interface ImportListItemsToStreamOptions { + listId: string; + stream: Readable; + callCluster: APICaller; + listItemIndex: string; + type: Type; + user: string; + meta: MetaOrUndefined; +} + +export const importListItemsToStream = ({ + listId, + stream, + callCluster, + listItemIndex, + type, + user, + meta, +}: ImportListItemsToStreamOptions): Promise<void> => { + return new Promise<void>(resolve => { + const readBuffer = new BufferLines({ input: stream }); + readBuffer.on('lines', async (lines: string[]) => { + await writeBufferToItems({ + buffer: lines, + callCluster, + listId, + listItemIndex, + meta, + type, + user, + }); + }); + + readBuffer.on('close', () => { + resolve(); + }); + }); +}; + +export interface WriteBufferToItemsOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + buffer: string[]; + type: Type; + user: string; + meta: MetaOrUndefined; +} + +export interface LinesResult { + linesProcessed: number; + duplicatesFound: number; +} + +export const writeBufferToItems = async ({ + listId, + callCluster, + listItemIndex, + buffer, + type, + user, + meta, +}: WriteBufferToItemsOptions): Promise<LinesResult> => { + const items = await getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value: buffer, + }); + const duplicatesRemoved = buffer.filter( + bufferedValue => !items.some(item => item.value === bufferedValue) + ); + const linesProcessed = duplicatesRemoved.length; + const duplicatesFound = buffer.length - duplicatesRemoved.length; + await createListItemsBulk({ + callCluster, + listId, + listItemIndex, + meta, + type, + user, + value: duplicatesRemoved, + }); + return { duplicatesFound, linesProcessed }; +}; 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 new file mode 100644 index 0000000000000..b08e5fa688b4b --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.test.ts @@ -0,0 +1,289 @@ +/* + * 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 { + LIST_ID, + LIST_ITEM_INDEX, + getCallClusterMock, + getExportListItemsToStreamOptionsMock, + getResponseOptionsMock, + getSearchListItemMock, + getWriteNextResponseOptions, + getWriteResponseHitsToStreamOptionsMock, +} from '../mocks'; + +import { + exportListItemsToStream, + getResponse, + getSearchAfterFromResponse, + writeNextResponse, + writeResponseHitsToStream, +} from '.'; + +describe('write_list_items_to_stream', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('exportListItemsToStream', () => { + test('It exports empty list items to the stream as an empty array', done => { + const options = getExportListItemsToStreamOptionsMock(); + const firstResponse = getSearchListItemMock(); + firstResponse.hits.hits = []; + options.callCluster = getCallClusterMock(firstResponse); + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual([]); + done(); + }); + }); + + test('It exports single list item to the stream', done => { + const options = getExportListItemsToStreamOptionsMock(); + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual(['127.0.0.1']); + done(); + }); + }); + + test('It exports two list items to the stream', done => { + const options = getExportListItemsToStreamOptionsMock(); + const firstResponse = getSearchListItemMock(); + const secondResponse = getSearchListItemMock(); + firstResponse.hits.hits = [...firstResponse.hits.hits, ...secondResponse.hits.hits]; + options.callCluster = getCallClusterMock(firstResponse); + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual(['127.0.0.1', '127.0.0.1']); + done(); + }); + }); + + test('It exports two list items to the stream with two separate calls', done => { + const options = getExportListItemsToStreamOptionsMock(); + + const firstResponse = getSearchListItemMock(); + firstResponse.hits.hits[0].sort = ['some-sort-value']; + + const secondResponse = getSearchListItemMock(); + secondResponse.hits.hits[0]._source.ip = '255.255.255.255'; + + options.callCluster = jest + .fn() + .mockResolvedValueOnce(firstResponse) + .mockResolvedValueOnce(secondResponse); + + exportListItemsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual(['127.0.0.1', '255.255.255.255']); + done(); + }); + }); + }); + + describe('writeNextResponse', () => { + test('It returns an empty searchAfter response when there is no sort defined', async () => { + const options = getWriteNextResponseOptions(); + const searchAfter = await writeNextResponse(options); + expect(searchAfter).toEqual(undefined); + }); + + test('It returns a searchAfter response when there is a sort defined', async () => { + const listItem = getSearchListItemMock(); + listItem.hits.hits[0].sort = ['sort-value-1']; + const options = getWriteNextResponseOptions(); + options.callCluster = getCallClusterMock(listItem); + const searchAfter = await writeNextResponse(options); + expect(searchAfter).toEqual(['sort-value-1']); + }); + + test('It returns a searchAfter response of undefined when the response is empty', async () => { + const listItem = getSearchListItemMock(); + listItem.hits.hits = []; + const options = getWriteNextResponseOptions(); + options.callCluster = getCallClusterMock(listItem); + const searchAfter = await writeNextResponse(options); + expect(searchAfter).toEqual(undefined); + }); + }); + + describe('getSearchAfterFromResponse', () => { + test('It returns undefined if the hits array is empty', () => { + const response = getSearchListItemMock(); + response.hits.hits = []; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(undefined); + }); + + test('It returns undefined if the hits array does not have a sort', () => { + const response = getSearchListItemMock(); + response.hits.hits[0].sort = undefined; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(undefined); + }); + + test('It returns a sort of a single array if that single item exists', () => { + const response = getSearchListItemMock(); + response.hits.hits[0].sort = ['sort-value-1', 'sort-value-2']; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(['sort-value-1', 'sort-value-2']); + }); + + test('It returns a sort of the last array element of size 2', () => { + const response = getSearchListItemMock(); + const response2 = getSearchListItemMock(); + response2.hits.hits[0].sort = ['sort-value']; + response.hits.hits = [...response.hits.hits, ...response2.hits.hits]; + const searchAfter = getSearchAfterFromResponse({ response }); + expect(searchAfter).toEqual(['sort-value']); + }); + }); + + describe('getResponse', () => { + test('It returns a simple response with the default size of 100', async () => { + const options = getResponseOptionsMock(); + options.searchAfter = ['string 1', 'string 2']; + await getResponse(options); + const expected = { + body: { + query: { term: { list_id: LIST_ID } }, + search_after: ['string 1', 'string 2'], + sort: [{ tie_breaker_id: 'asc' }], + }, + ignoreUnavailable: true, + index: LIST_ITEM_INDEX, + size: 100, + }; + expect(options.callCluster).toBeCalledWith('search', expected); + }); + + test('It returns a simple response with expected values and size changed', async () => { + const options = getResponseOptionsMock(); + options.searchAfter = ['string 1', 'string 2']; + options.size = 33; + await getResponse(options); + const expected = { + body: { + query: { term: { list_id: LIST_ID } }, + search_after: ['string 1', 'string 2'], + sort: [{ tie_breaker_id: 'asc' }], + }, + ignoreUnavailable: true, + index: LIST_ITEM_INDEX, + size: 33, + }; + expect(options.callCluster).toBeCalledWith('search', expected); + }); + }); + + describe('writeResponseHitsToStream', () => { + test('it will push into the stream the mock response', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.end(() => { + expect(chunks).toEqual(['127.0.0.1']); + done(); + }); + }); + + test('it will push into the stream an empty mock response', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + options.response.hits.hits = []; + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.on('finish', () => { + expect(chunks).toEqual([]); + done(); + }); + options.stream.end(); + }); + + test('it will push into the stream 2 mock responses', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + const secondResponse = getSearchListItemMock(); + options.response.hits.hits = [...options.response.hits.hits, ...secondResponse.hits.hits]; + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.end(() => { + expect(chunks).toEqual(['127.0.0.1', '127.0.0.1']); + done(); + }); + }); + + test('it will push an additional string given to it such as a new line character', done => { + const options = getWriteResponseHitsToStreamOptionsMock(); + const secondResponse = getSearchListItemMock(); + options.response.hits.hits = [...options.response.hits.hits, ...secondResponse.hits.hits]; + options.stringToAppend = '\n'; + writeResponseHitsToStream(options); + + let chunks: string[] = []; + options.stream.on('data', (chunk: Buffer) => { + chunks = [...chunks, chunk.toString()]; + }); + + options.stream.end(() => { + expect(chunks).toEqual(['127.0.0.1\n', '127.0.0.1\n']); + done(); + }); + }); + + test('it will throw an exception with a status code if the hit_source is not a data type we expect', () => { + const options = getWriteResponseHitsToStreamOptionsMock(); + options.response.hits.hits[0]._source.ip = undefined; + options.response.hits.hits[0]._source.keyword = undefined; + const expected = `Encountered an error where hit._source was an unexpected type: ${JSON.stringify( + options.response.hits.hits[0]._source + )}`; + expect(() => writeResponseHitsToStream(options)).toThrow(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 new file mode 100644 index 0000000000000..b81e4a4fc73c2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/write_list_items_to_stream.ts @@ -0,0 +1,160 @@ +/* + * 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 { PassThrough } from 'stream'; + +import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { SearchEsListItemSchema } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +/** + * How many results to page through from the network at a time + * using search_after + */ +export const SIZE = 100; + +export interface ExportListItemsToStreamOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + stream: PassThrough; + stringToAppend: string | null | undefined; +} + +export const exportListItemsToStream = ({ + listId, + callCluster, + stream, + listItemIndex, + stringToAppend, +}: ExportListItemsToStreamOptions): void => { + // Use a timeout to start the reading process on the next tick. + // and prevent the async await from bubbling up to the caller + setTimeout(async () => { + let searchAfter = await writeNextResponse({ + callCluster, + listId, + listItemIndex, + searchAfter: undefined, + stream, + stringToAppend, + }); + while (searchAfter != null) { + searchAfter = await writeNextResponse({ + callCluster, + listId, + listItemIndex, + searchAfter, + stream, + stringToAppend, + }); + } + stream.end(); + }); +}; + +export interface WriteNextResponseOptions { + listId: string; + callCluster: APICaller; + listItemIndex: string; + stream: PassThrough; + searchAfter: string[] | undefined; + stringToAppend: string | null | undefined; +} + +export const writeNextResponse = async ({ + listId, + callCluster, + stream, + listItemIndex, + searchAfter, + stringToAppend, +}: WriteNextResponseOptions): Promise<string[] | undefined> => { + const response = await getResponse({ + callCluster, + listId, + listItemIndex, + searchAfter, + }); + + if (response.hits.hits.length) { + writeResponseHitsToStream({ response, stream, stringToAppend }); + return getSearchAfterFromResponse({ response }); + } else { + return undefined; + } +}; + +export const getSearchAfterFromResponse = <T>({ + response, +}: { + response: SearchResponse<T>; +}): string[] | undefined => + response.hits.hits.length > 0 + ? response.hits.hits[response.hits.hits.length - 1].sort + : undefined; + +export interface GetResponseOptions { + callCluster: APICaller; + listId: string; + searchAfter: undefined | string[]; + listItemIndex: string; + size?: number; +} + +export const getResponse = async ({ + callCluster, + searchAfter, + listId, + listItemIndex, + size = SIZE, +}: GetResponseOptions): Promise<SearchResponse<SearchEsListItemSchema>> => { + return callCluster('search', { + body: { + query: { + term: { + list_id: listId, + }, + }, + search_after: searchAfter, + sort: [{ tie_breaker_id: 'asc' }], + }, + ignoreUnavailable: true, + index: listItemIndex, + size, + }); +}; + +export interface WriteResponseHitsToStreamOptions { + response: SearchResponse<SearchEsListItemSchema>; + stream: PassThrough; + stringToAppend: string | null | undefined; +} + +export const writeResponseHitsToStream = ({ + response, + stream, + stringToAppend, +}: WriteResponseHitsToStreamOptions): void => { + const stringToAppendOrEmpty = stringToAppend ?? ''; + + response.hits.hits.forEach(hit => { + if (hit._source.ip != null) { + stream.push(`${hit._source.ip}${stringToAppendOrEmpty}`); + } else if (hit._source.keyword != null) { + stream.push(`${hit._source.keyword}${stringToAppendOrEmpty}`); + } else { + throw new ErrorWithStatusCode( + `Encountered an error where hit._source was an unexpected type: ${JSON.stringify( + hit._source + )}`, + 400 + ); + } + }); +}; diff --git a/x-pack/plugins/lists/server/services/lists/client.ts b/x-pack/plugins/lists/server/services/lists/client.ts new file mode 100644 index 0000000000000..ba22bf72cc18c --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/client.ts @@ -0,0 +1,413 @@ +/* + * 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 { APICaller } from 'kibana/server'; + +import { ListItemArraySchema, ListItemSchema, ListSchema } from '../../../common/schemas'; +import { ConfigType } from '../../config'; +import { + createList, + deleteList, + getList, + getListIndex, + getListTemplate, + updateList, +} from '../../services/lists'; +import { + createListItem, + deleteListItem, + deleteListItemByValue, + exportListItemsToStream, + getListItem, + getListItemByValue, + getListItemByValues, + getListItemIndex, + getListItemTemplate, + importListItemsToStream, + updateListItem, +} from '../../services/items'; +import { + createBootstrapIndex, + deleteAllIndex, + deletePolicy, + deleteTemplate, + getIndexExists, + getPolicyExists, + getTemplateExists, + setPolicy, + setTemplate, +} from '../../siem_server_deps'; +import listsItemsPolicy from '../items/list_item_policy.json'; + +import listPolicy from './list_policy.json'; +import { + ConstructorOptions, + CreateListIfItDoesNotExistOptions, + CreateListItemOptions, + CreateListOptions, + DeleteListItemByValueOptions, + DeleteListItemOptions, + DeleteListOptions, + ExportListItemsToStreamOptions, + GetListItemByValueOptions, + GetListItemOptions, + GetListItemsByValueOptions, + GetListOptions, + ImportListItemsToStreamOptions, + UpdateListItemOptions, + UpdateListOptions, +} from './client_types'; + +export class ListClient { + private readonly spaceId: string; + private readonly user: string; + private readonly config: ConfigType; + private readonly callCluster: APICaller; + + constructor({ spaceId, user, config, callCluster }: ConstructorOptions) { + this.spaceId = spaceId; + this.user = user; + this.config = config; + this.callCluster = callCluster; + } + + public getListIndex = (): string => { + const { + spaceId, + config: { listIndex: listsIndexName }, + } = this; + return getListIndex({ listsIndexName, spaceId }); + }; + + public getListItemIndex = (): string => { + const { + spaceId, + config: { listItemIndex: listsItemsIndexName }, + } = this; + return getListItemIndex({ listsItemsIndexName, spaceId }); + }; + + public getList = async ({ id }: GetListOptions): Promise<ListSchema | null> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getList({ callCluster, id, listIndex }); + }; + + public createList = async ({ + id, + name, + description, + type, + meta, + }: CreateListOptions): Promise<ListSchema> => { + const { callCluster, user } = this; + const listIndex = this.getListIndex(); + return createList({ callCluster, description, id, listIndex, meta, name, type, user }); + }; + + public createListIfItDoesNotExist = async ({ + id, + name, + description, + type, + meta, + }: CreateListIfItDoesNotExistOptions): Promise<ListSchema> => { + const list = await this.getList({ id }); + if (list == null) { + return this.createList({ description, id, meta, name, type }); + } else { + return list; + } + }; + + public getListIndexExists = async (): Promise<boolean> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getIndexExists(callCluster, listIndex); + }; + + public getListItemIndexExists = async (): Promise<boolean> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getIndexExists(callCluster, listItemIndex); + }; + + public createListBootStrapIndex = async (): Promise<unknown> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return createBootstrapIndex(callCluster, listIndex); + }; + + public createListItemBootStrapIndex = async (): Promise<unknown> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return createBootstrapIndex(callCluster, listItemIndex); + }; + + public getListPolicyExists = async (): Promise<boolean> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getPolicyExists(callCluster, listIndex); + }; + + public getListItemPolicyExists = async (): Promise<boolean> => { + const { callCluster } = this; + const listsItemIndex = this.getListItemIndex(); + return getPolicyExists(callCluster, listsItemIndex); + }; + + public getListTemplateExists = async (): Promise<boolean> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return getTemplateExists(callCluster, listIndex); + }; + + public getListItemTemplateExists = async (): Promise<boolean> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getTemplateExists(callCluster, listItemIndex); + }; + + public getListTemplate = (): Record<string, unknown> => { + const listIndex = this.getListIndex(); + return getListTemplate(listIndex); + }; + + public getListItemTemplate = (): Record<string, unknown> => { + const listItemIndex = this.getListItemIndex(); + return getListItemTemplate(listItemIndex); + }; + + public setListTemplate = async (): Promise<unknown> => { + const { callCluster } = this; + const template = this.getListTemplate(); + const listIndex = this.getListIndex(); + return setTemplate(callCluster, listIndex, template); + }; + + public setListItemTemplate = async (): Promise<unknown> => { + const { callCluster } = this; + const template = this.getListItemTemplate(); + const listItemIndex = this.getListItemIndex(); + return setTemplate(callCluster, listItemIndex, template); + }; + + public setListPolicy = async (): Promise<unknown> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return setPolicy(callCluster, listIndex, listPolicy); + }; + + public setListItemPolicy = async (): Promise<unknown> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return setPolicy(callCluster, listItemIndex, listsItemsPolicy); + }; + + public deleteListIndex = async (): Promise<boolean> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return deleteAllIndex(callCluster, `${listIndex}-*`); + }; + + public deleteListItemIndex = async (): Promise<boolean> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteAllIndex(callCluster, `${listItemIndex}-*`); + }; + + public deleteListPolicy = async (): Promise<unknown> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return deletePolicy(callCluster, listIndex); + }; + + public deleteListItemPolicy = async (): Promise<unknown> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deletePolicy(callCluster, listItemIndex); + }; + + public deleteListTemplate = async (): Promise<unknown> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + return deleteTemplate(callCluster, listIndex); + }; + + public deleteListItemTemplate = async (): Promise<unknown> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteTemplate(callCluster, listItemIndex); + }; + + public deleteListItem = async ({ id }: DeleteListItemOptions): Promise<ListItemSchema | null> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteListItem({ callCluster, id, listItemIndex }); + }; + + public deleteListItemByValue = async ({ + listId, + value, + type, + }: DeleteListItemByValueOptions): Promise<ListItemArraySchema> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return deleteListItemByValue({ + callCluster, + listId, + listItemIndex, + type, + value, + }); + }; + + public deleteList = async ({ id }: DeleteListOptions): Promise<ListSchema | null> => { + const { callCluster } = this; + const listIndex = this.getListIndex(); + const listItemIndex = this.getListItemIndex(); + return deleteList({ + callCluster, + id, + listIndex, + listItemIndex, + }); + }; + + public exportListItemsToStream = ({ + stringToAppend, + listId, + stream, + }: ExportListItemsToStreamOptions): void => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + exportListItemsToStream({ + callCluster, + listId, + listItemIndex, + stream, + stringToAppend, + }); + }; + + public importListItemsToStream = async ({ + type, + listId, + stream, + meta, + }: ImportListItemsToStreamOptions): Promise<void> => { + const { callCluster, user } = this; + const listItemIndex = this.getListItemIndex(); + return importListItemsToStream({ + callCluster, + listId, + listItemIndex, + meta, + stream, + type, + user, + }); + }; + + public getListItemByValue = async ({ + listId, + value, + type, + }: GetListItemByValueOptions): Promise<ListItemArraySchema> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getListItemByValue({ + callCluster, + listId, + listItemIndex, + type, + value, + }); + }; + + public createListItem = async ({ + id, + listId, + value, + type, + meta, + }: CreateListItemOptions): Promise<ListItemSchema> => { + const { callCluster, user } = this; + const listItemIndex = this.getListItemIndex(); + return createListItem({ + callCluster, + id, + listId, + listItemIndex, + meta, + type, + user, + value, + }); + }; + + public updateListItem = async ({ + id, + value, + meta, + }: UpdateListItemOptions): Promise<ListItemSchema | null> => { + const { callCluster, user } = this; + const listItemIndex = this.getListItemIndex(); + return updateListItem({ + callCluster, + id, + listItemIndex, + meta, + user, + value, + }); + }; + + public updateList = async ({ + id, + name, + description, + meta, + }: UpdateListOptions): Promise<ListSchema | null> => { + const { callCluster, user } = this; + const listIndex = this.getListIndex(); + return updateList({ + callCluster, + description, + id, + listIndex, + meta, + name, + user, + }); + }; + + public getListItem = async ({ id }: GetListItemOptions): Promise<ListItemSchema | null> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getListItem({ + callCluster, + id, + listItemIndex, + }); + }; + + public getListItemByValues = async ({ + type, + listId, + value, + }: GetListItemsByValueOptions): Promise<ListItemArraySchema> => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return getListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value, + }); + }; +} diff --git a/x-pack/plugins/lists/server/services/lists/client_types.ts b/x-pack/plugins/lists/server/services/lists/client_types.ts new file mode 100644 index 0000000000000..2cc58c02dbfcf --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/client_types.ts @@ -0,0 +1,115 @@ +/* + * 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 { PassThrough, Readable } from 'stream'; + +import { APICaller, KibanaRequest } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../../../security/server'; +import { + Description, + DescriptionOrUndefined, + Id, + IdOrUndefined, + MetaOrUndefined, + Name, + NameOrUndefined, + Type, +} from '../../../common/schemas'; +import { ConfigType } from '../../config'; + +export interface ConstructorOptions { + callCluster: APICaller; + config: ConfigType; + request: KibanaRequest; + spaceId: string; + user: string; + security: SecurityPluginSetup | undefined | null; +} + +export interface GetListOptions { + id: Id; +} + +export interface DeleteListOptions { + id: Id; +} + +export interface DeleteListItemOptions { + id: Id; +} + +export interface CreateListOptions { + id: IdOrUndefined; + name: Name; + description: Description; + type: Type; + meta: MetaOrUndefined; +} + +export interface CreateListIfItDoesNotExistOptions { + id: Id; + name: Name; + description: Description; + type: Type; + meta: MetaOrUndefined; +} + +export interface DeleteListItemByValueOptions { + listId: string; + value: string; + type: Type; +} + +export interface GetListItemByValueOptions { + listId: string; + value: string; + type: Type; +} + +export interface ExportListItemsToStreamOptions { + stringToAppend: string | null | undefined; + listId: string; + stream: PassThrough; +} + +export interface ImportListItemsToStreamOptions { + listId: string; + type: Type; + stream: Readable; + meta: MetaOrUndefined; +} + +export interface CreateListItemOptions { + id: IdOrUndefined; + listId: string; + type: Type; + value: string; + meta: MetaOrUndefined; +} + +export interface UpdateListItemOptions { + id: Id; + value: string | null | undefined; + meta: MetaOrUndefined; +} + +export interface UpdateListOptions { + id: Id; + name: NameOrUndefined; + description: DescriptionOrUndefined; + meta: MetaOrUndefined; +} + +export interface GetListItemOptions { + id: Id; +} + +export interface GetListItemsByValueOptions { + type: Type; + listId: string; + value: string[]; +} 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 new file mode 100644 index 0000000000000..36284a70fb97d --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + getCreateListOptionsMock, + getIndexESListMock, + getListResponseMock, +} from '../mocks'; + +import { createList } from './create_list'; + +describe('crete_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list as expected with the id changed out for the elastic id', async () => { + const options = getCreateListOptionsMock(); + const list = await createList(options); + const expected = getListResponseMock(); + expected.id = 'elastic-id-123'; + expect(list).toEqual(expected); + }); + + test('It calls "callCluster" with body, index, and listIndex', async () => { + const options = getCreateListOptionsMock(); + await createList(options); + const body = getIndexESListMock(); + const expected = { + body, + id: LIST_ID, + index: LIST_INDEX, + }; + expect(options.callCluster).toBeCalledWith('index', expected); + }); + + test('It returns an auto-generated id if id is sent in undefined', async () => { + const options = getCreateListOptionsMock(); + options.id = undefined; + const list = await createList(options); + const expected = getListResponseMock(); + expected.id = 'elastic-id-123'; + expect(list).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts new file mode 100644 index 0000000000000..ddbc99c88a877 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/create_list.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 uuid from 'uuid'; +import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + Description, + IdOrUndefined, + IndexEsListSchema, + ListSchema, + MetaOrUndefined, + Name, + Type, +} from '../../../common/schemas'; + +export interface CreateListOptions { + id: IdOrUndefined; + type: Type; + name: Name; + description: Description; + callCluster: APICaller; + listIndex: string; + user: string; + meta: MetaOrUndefined; + dateNow?: string; + tieBreaker?: string; +} + +export const createList = async ({ + id, + name, + type, + description, + callCluster, + listIndex, + user, + meta, + dateNow, + tieBreaker, +}: CreateListOptions): Promise<ListSchema> => { + const createdAt = dateNow ?? new Date().toISOString(); + const body: IndexEsListSchema = { + created_at: createdAt, + created_by: user, + description, + meta, + name, + tie_breaker_id: tieBreaker ?? uuid.v4(), + type, + updated_at: createdAt, + updated_by: user, + }; + const response: CreateDocumentResponse = await callCluster('index', { + body, + id, + index: listIndex, + }); + return { + id: response._id, + ...body, + }; +}; 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 new file mode 100644 index 0000000000000..62b5e7c7aec4a --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + LIST_ITEM_INDEX, + getDeleteListOptionsMock, + getListResponseMock, +} from '../mocks'; + +import { getList } from './get_list'; +import { deleteList } from './delete_list'; + +jest.mock('./get_list', () => ({ + getList: jest.fn(), +})); + +describe('delete_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Delete returns a null if the list is also null', async () => { + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getDeleteListOptionsMock(); + const deletedList = await deleteList(options); + expect(deletedList).toEqual(null); + }); + + test('Delete returns the list if a list is returned from getList', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getDeleteListOptionsMock(); + const deletedList = await deleteList(options); + expect(deletedList).toEqual(list); + }); + + test('Delete calls "deleteByQuery" and "delete" if a list is returned from getList', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getDeleteListOptionsMock(); + await deleteList(options); + const deleteByQuery = { + body: { query: { term: { list_id: LIST_ID } } }, + index: LIST_ITEM_INDEX, + }; + expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery); + }); + + test('Delete calls "delete" second if a list is returned from getList', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getDeleteListOptionsMock(); + await deleteList(options); + const deleteQuery = { + id: LIST_ID, + index: LIST_INDEX, + }; + 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.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 new file mode 100644 index 0000000000000..bc66c88b082a3 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/delete_list.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 { APICaller } from 'kibana/server'; + +import { Id, ListSchema } from '../../../common/schemas'; + +import { getList } from './get_list'; + +export interface DeleteListOptions { + id: Id; + callCluster: APICaller; + listIndex: string; + listItemIndex: string; +} + +export const deleteList = async ({ + id, + callCluster, + listIndex, + listItemIndex, +}: DeleteListOptions): Promise<ListSchema | null> => { + const list = await getList({ callCluster, id, listIndex }); + if (list == null) { + return null; + } else { + await callCluster('deleteByQuery', { + body: { + query: { + term: { + list_id: id, + }, + }, + }, + index: listItemIndex, + }); + + await callCluster('delete', { + id, + index: listIndex, + }); + return list; + } +}; 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 new file mode 100644 index 0000000000000..c997d5325296a --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { + LIST_ID, + LIST_INDEX, + getCallClusterMock, + getListResponseMock, + getSearchListMock, +} from '../mocks'; + +import { getList } from './get_list'; + +describe('get_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list as expected if the list is found', async () => { + const data = getSearchListMock(); + const callCluster = getCallClusterMock(data); + const list = await getList({ callCluster, id: LIST_ID, listIndex: LIST_INDEX }); + const expected = getListResponseMock(); + expect(list).toEqual(expected); + }); + + test('it returns null if the search is empty', async () => { + const data = getSearchListMock(); + data.hits.hits = []; + 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 new file mode 100644 index 0000000000000..c04bd504ad8c0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { Id, ListSchema, SearchEsListSchema } from '../../../common/schemas'; + +interface GetListOptions { + id: Id; + callCluster: APICaller; + listIndex: string; +} + +export const getList = async ({ + id, + callCluster, + listIndex, +}: GetListOptions): Promise<ListSchema | null> => { + const result: SearchResponse<SearchEsListSchema> = await callCluster('search', { + body: { + query: { + term: { + _id: id, + }, + }, + }, + ignoreUnavailable: true, + index: listIndex, + }); + if (result.hits.hits.length) { + return { + id: result.hits.hits[0]._id, + ...result.hits.hits[0]._source, + }; + } else { + return null; + } +}; 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 new file mode 100644 index 0000000000000..f82928ffeddd2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.test.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListIndex } from './get_list_index'; + +describe('get_list_index', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns the list index when there is a space', async () => { + const listIndex = getListIndex({ + listsIndexName: 'lists-index', + 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 new file mode 100644 index 0000000000000..5086603fa8403 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface GetListIndexOptions { + spaceId: string; + listsIndexName: string; +} + +export const getListIndex = ({ spaceId, listsIndexName }: GetListIndexOptions): string => + `${listsIndexName}-${spaceId}`; diff --git a/x-pack/plugins/lists/server/services/lists/get_list_template.test.ts b/x-pack/plugins/lists/server/services/lists/get_list_template.test.ts new file mode 100644 index 0000000000000..e25eaaafd855e --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_template.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getListTemplate } from './get_list_template'; + +jest.mock('./list_mappings.json', () => ({ + listMappings: {}, +})); + +describe('get_list_template', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list template with the string filled in', async () => { + const template = getListTemplate('some_index'); + expect(template).toEqual({ + index_patterns: ['some_index-*'], + mappings: { listMappings: {} }, + settings: { index: { lifecycle: { name: 'some_index', rollover_alias: 'some_index' } } }, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/get_list_template.ts b/x-pack/plugins/lists/server/services/lists/get_list_template.ts new file mode 100644 index 0000000000000..9d93a051f2d10 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/get_list_template.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import listMappings from './list_mappings.json'; + +export const getListTemplate = (index: string): Record<string, unknown> => ({ + index_patterns: [`${index}-*`], + mappings: listMappings, + settings: { + index: { + lifecycle: { + name: index, + rollover_alias: index, + }, + }, + }, +}); diff --git a/x-pack/plugins/lists/server/services/lists/index.ts b/x-pack/plugins/lists/server/services/lists/index.ts new file mode 100644 index 0000000000000..f704ef0b05b82 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './create_list'; +export * from './delete_list'; +export * from './get_list'; +export * from './get_list_template'; +export * from './update_list'; +export * from './get_list_index'; diff --git a/x-pack/plugins/lists/server/services/lists/list_mappings.json b/x-pack/plugins/lists/server/services/lists/list_mappings.json new file mode 100644 index 0000000000000..1136a53da787d --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_mappings.json @@ -0,0 +1,33 @@ +{ + "dynamic": "strict", + "properties": { + "name": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "meta": { + "enabled": "false", + "type": "object" + }, + "created_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } +} diff --git a/x-pack/plugins/lists/server/services/lists/list_policy.json b/x-pack/plugins/lists/server/services/lists/list_policy.json new file mode 100644 index 0000000000000..a4c84f73e7896 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/list_policy.json @@ -0,0 +1,14 @@ +{ + "policy": { + "phases": { + "hot": { + "min_age": "0ms", + "actions": { + "rollover": { + "max_size": "50gb" + } + } + } + } + } +} diff --git a/x-pack/plugins/lists/server/services/lists/update_list.test.ts b/x-pack/plugins/lists/server/services/lists/update_list.test.ts new file mode 100644 index 0000000000000..09bf0ee69c981 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/update_list.test.ts @@ -0,0 +1,41 @@ +/* + * 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 { getListResponseMock, getUpdateListOptionsMock } from '../mocks'; + +import { updateList } from './update_list'; +import { getList } from './get_list'; + +jest.mock('./get_list', () => ({ + getList: jest.fn(), +})); + +describe('update_list', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns a list as expected with the id changed out for the elastic id when there is a list to update', async () => { + const list = getListResponseMock(); + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(list); + const options = getUpdateListOptionsMock(); + const updatedList = await updateList(options); + const expected = getListResponseMock(); + expected.id = 'elastic-id-123'; + expect(updatedList).toEqual(expected); + }); + + test('it returns null when there is not a list to update', async () => { + ((getList as unknown) as jest.Mock).mockResolvedValueOnce(null); + const options = getUpdateListOptionsMock(); + const updatedList = await updateList(options); + expect(updatedList).toEqual(null); + }); +}); diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts new file mode 100644 index 0000000000000..9859adf062485 --- /dev/null +++ b/x-pack/plugins/lists/server/services/lists/update_list.ts @@ -0,0 +1,72 @@ +/* + * 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 { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { + DescriptionOrUndefined, + Id, + ListSchema, + MetaOrUndefined, + NameOrUndefined, + UpdateEsListSchema, +} from '../../../common/schemas'; + +import { getList } from '.'; + +export interface UpdateListOptions { + id: Id; + callCluster: APICaller; + listIndex: string; + user: string; + name: NameOrUndefined; + description: DescriptionOrUndefined; + meta: MetaOrUndefined; + dateNow?: string; +} + +export const updateList = async ({ + id, + name, + description, + callCluster, + listIndex, + user, + meta, + dateNow, +}: UpdateListOptions): Promise<ListSchema | null> => { + const updatedAt = dateNow ?? new Date().toISOString(); + const list = await getList({ callCluster, id, listIndex }); + if (list == null) { + return null; + } else { + const doc: UpdateEsListSchema = { + description, + meta, + name, + updated_at: updatedAt, + updated_by: user, + }; + const response: CreateDocumentResponse = await callCluster('update', { + body: { doc }, + id, + index: listIndex, + }); + return { + created_at: list.created_at, + created_by: list.created_by, + description: description ?? list.description, + id: response._id, + meta, + name: name ?? list.name, + tie_breaker_id: list.tie_breaker_id, + type: list.type, + updated_at: updatedAt, + updated_by: user, + }; + } +}; diff --git a/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts new file mode 100644 index 0000000000000..180ecbb797339 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_call_cluster_mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CreateDocumentResponse } from 'elasticsearch'; +import { APICaller } from 'kibana/server'; + +import { LIST_INDEX } from './lists_services_mock_constants'; +import { getShardMock } from './get_shard_mock'; + +export const getEmptyCreateDocumentResponseMock = (): CreateDocumentResponse => ({ + _id: 'elastic-id-123', + _index: LIST_INDEX, + _shards: getShardMock(), + _type: '', + _version: 1, + created: true, + result: '', +}); + +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 new file mode 100644 index 0000000000000..fcdad66d65251 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_bulk_options_mock.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CreateListItemsBulkOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + LIST_ID, + LIST_ITEM_INDEX, + META, + TIE_BREAKERS, + TYPE, + USER, + VALUE, + VALUE_2, +} from './lists_services_mock_constants'; + +export const getCreateListItemBulkOptionsMock = (): CreateListItemsBulkOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + tieBreaker: TIE_BREAKERS, + type: TYPE, + user: USER, + value: [VALUE, VALUE_2], +}); 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 new file mode 100644 index 0000000000000..17e3ad2f8de08 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_item_options_mock.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CreateListItemOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + LIST_ID, + LIST_ITEM_ID, + LIST_ITEM_INDEX, + META, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getCreateListItemOptionsMock = (): CreateListItemOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + id: LIST_ITEM_ID, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + tieBreaker: TIE_BREAKER, + type: TYPE, + user: USER, + value: '127.0.0.1', +}); 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 new file mode 100644 index 0000000000000..0ea6533fc122a --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_create_list_options_mock.ts @@ -0,0 +1,33 @@ +/* + * 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 { CreateListOptions } from '../lists'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + LIST_INDEX, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getCreateListOptionsMock = (): CreateListOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + description: DESCRIPTION, + id: LIST_ID, + listIndex: LIST_INDEX, + meta: META, + name: NAME, + tieBreaker: TIE_BREAKER, + type: TYPE, + user: USER, +}); 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 new file mode 100644 index 0000000000000..f6859e72d71b3 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_by_value_options_mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DeleteListItemByValueOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; + +export const getDeleteListItemByValueOptionsMock = (): DeleteListItemByValueOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: VALUE, +}); 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 new file mode 100644 index 0000000000000..271c185860b07 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_item_options_mock.ts @@ -0,0 +1,16 @@ +/* + * 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 { DeleteListItemOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ITEM_ID, LIST_ITEM_INDEX } from './lists_services_mock_constants'; + +export const getDeleteListItemOptionsMock = (): DeleteListItemOptions => ({ + 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 new file mode 100644 index 0000000000000..8ec92dfa4ef77 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_delete_list_options_mock.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. + */ + +import { DeleteListOptions } from '../lists'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_INDEX, LIST_ITEM_INDEX } from './lists_services_mock_constants'; + +export const getDeleteListOptionsMock = (): DeleteListOptions => ({ + 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 new file mode 100644 index 0000000000000..d7541f3e09e6c --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_import_list_items_to_stream_options_mock.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ImportListItemsToStreamOptions } from '../items'; + +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 => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + stream: new TestReadable(), + type: TYPE, + user: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts new file mode 100644 index 0000000000000..574e4afcb36f0 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_item_mock.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IndexEsListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, META, TIE_BREAKER, USER, VALUE } from './lists_services_mock_constants'; + +export const getIndexESListItemMock = (ip = VALUE): IndexEsListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + ip, + list_id: LIST_ID, + meta: META, + tie_breaker_id: TIE_BREAKER, + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts new file mode 100644 index 0000000000000..4e4d8d9c572e4 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_index_es_list_mock.ts @@ -0,0 +1,29 @@ +/* + * 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 { IndexEsListSchema } from '../../../common/schemas'; + +import { + DATE_NOW, + DESCRIPTION, + META, + NAME, + TIE_BREAKER, + TYPE, + USER, +} from './lists_services_mock_constants'; + +export const getIndexESListMock = (): IndexEsListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + meta: META, + name: NAME, + tie_breaker_id: TIE_BREAKER, + type: TYPE, + updated_at: DATE_NOW, + updated_by: USER, +}); 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 new file mode 100644 index 0000000000000..96bc22ca7e6f2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_value_options_mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GetListItemByValueOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE } from './lists_services_mock_constants'; + +export const getListItemByValueOptionsMocks = (): GetListItemByValueOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: VALUE, +}); 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 new file mode 100644 index 0000000000000..f21f97dc8d15f --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_by_values_options_mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GetListItemByValuesOptions } from '../items'; + +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 => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts new file mode 100644 index 0000000000000..1a30282ddaeba --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_item_response_mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, LIST_ITEM_ID, USER, VALUE } from './lists_services_mock_constants'; + +export const getListItemResponseMock = (): ListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + id: LIST_ITEM_ID, + list_id: LIST_ID, + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, + value: VALUE, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts new file mode 100644 index 0000000000000..ea068d774c4ed --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_list_response_mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ListSchema } from '../../../common/schemas'; + +import { DATE_NOW, DESCRIPTION, LIST_ID, NAME, USER } from './lists_services_mock_constants'; + +export const getListResponseMock = (): ListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + id: LIST_ID, + meta: {}, + name: NAME, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts new file mode 100644 index 0000000000000..5e9fd8995c0eb --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_item_mock.ts @@ -0,0 +1,21 @@ +/* + * 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 { SearchEsListItemSchema } from '../../../common/schemas'; + +import { DATE_NOW, LIST_ID, USER, VALUE } from './lists_services_mock_constants'; + +export const getSearchEsListItemMock = (): SearchEsListItemSchema => ({ + created_at: DATE_NOW, + created_by: USER, + ip: VALUE, + keyword: undefined, + list_id: LIST_ID, + meta: {}, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts new file mode 100644 index 0000000000000..6a565437617ba --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_es_list_mock.ts @@ -0,0 +1,21 @@ +/* + * 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 { SearchEsListSchema } from '../../../common/schemas'; + +import { DATE_NOW, DESCRIPTION, NAME, USER } from './lists_services_mock_constants'; + +export const getSearchEsListMock = (): SearchEsListSchema => ({ + created_at: DATE_NOW, + created_by: USER, + description: DESCRIPTION, + meta: {}, + name: NAME, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: DATE_NOW, + updated_by: USER, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts new file mode 100644 index 0000000000000..9f877c8168cca --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_list_item_mock.ts @@ -0,0 +1,33 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; + +import { SearchEsListItemSchema } from '../../../common/schemas'; + +import { getShardMock } from './get_shard_mock'; +import { LIST_INDEX, LIST_ITEM_ID } from './lists_services_mock_constants'; +import { getSearchEsListItemMock } from './get_search_es_list_item_mock'; + +export const getSearchListItemMock = (): SearchResponse<SearchEsListItemSchema> => ({ + _scroll_id: '123', + _shards: getShardMock(), + hits: { + hits: [ + { + _id: LIST_ITEM_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListItemMock(), + _type: '', + }, + ], + max_score: 0, + total: 1, + }, + timed_out: false, + took: 10, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts new file mode 100644 index 0000000000000..9728139eab42a --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_search_list_mock.ts @@ -0,0 +1,33 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; + +import { SearchEsListSchema } from '../../../common/schemas'; + +import { getShardMock } from './get_shard_mock'; +import { LIST_ID, LIST_INDEX } from './lists_services_mock_constants'; +import { getSearchEsListMock } from './get_search_es_list_mock'; + +export const getSearchListMock = (): SearchResponse<SearchEsListSchema> => ({ + _scroll_id: '123', + _shards: getShardMock(), + hits: { + hits: [ + { + _id: LIST_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListMock(), + _type: '', + }, + ], + max_score: 0, + total: 1, + }, + timed_out: false, + took: 10, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts b/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts new file mode 100644 index 0000000000000..4cc6577d5e531 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_shard_mock.ts @@ -0,0 +1,14 @@ +/* + * 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 { ShardsResponse } from 'elasticsearch'; + +export const getShardMock = (): ShardsResponse => ({ + failed: 0, + skipped: 0, + successful: 0, + total: 0, +}); 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 new file mode 100644 index 0000000000000..0555997941baa --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_item_options_mock.ts @@ -0,0 +1,26 @@ +/* + * 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 { UpdateListItemOptions } from '../items'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + LIST_ITEM_ID, + LIST_ITEM_INDEX, + META, + USER, + VALUE, +} from './lists_services_mock_constants'; + +export const getUpdateListItemOptionsMock = (): UpdateListItemOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + id: LIST_ITEM_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + user: USER, + value: VALUE, +}); 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 new file mode 100644 index 0000000000000..fe6fc37eaf81e --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_update_list_options_mock.ts @@ -0,0 +1,28 @@ +/* + * 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 { UpdateListOptions } from '../lists'; + +import { getCallClusterMock } from './get_call_cluster_mock'; +import { + DATE_NOW, + DESCRIPTION, + LIST_ID, + LIST_INDEX, + META, + NAME, + USER, +} from './lists_services_mock_constants'; + +export const getUpdateListOptionsMock = (): UpdateListOptions => ({ + callCluster: getCallClusterMock(), + dateNow: DATE_NOW, + description: DESCRIPTION, + id: LIST_ID, + listIndex: LIST_INDEX, + meta: META, + name: NAME, + user: USER, +}); 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 new file mode 100644 index 0000000000000..d6b7d70c1aa77 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_write_buffer_to_items_options_mock.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { WriteBufferToItemsOptions } from '../items'; + +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: [], + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + meta: META, + type: TYPE, + user: USER, +}); 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 new file mode 100644 index 0000000000000..c945818a83e8a --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/get_write_list_items_to_stream_options_mock.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Stream } from 'stream'; + +import { + ExportListItemsToStreamOptions, + GetResponseOptions, + WriteNextResponseOptions, + WriteResponseHitsToStreamOptions, +} from '../items'; + +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 => ({ + callCluster: getCallClusterMock(getSearchListItemMock()), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); + +export const getWriteNextResponseOptions = (): WriteNextResponseOptions => ({ + callCluster: getCallClusterMock(getSearchListItemMock()), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + searchAfter: [], + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); + +export const getResponseOptionsMock = (): GetResponseOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + searchAfter: [], + size: 100, +}); + +export const getWriteResponseHitsToStreamOptionsMock = (): WriteResponseHitsToStreamOptions => ({ + response: getSearchListItemMock(), + stream: new Stream.PassThrough(), + stringToAppend: undefined, +}); diff --git a/x-pack/plugins/lists/server/services/mocks/index.ts b/x-pack/plugins/lists/server/services/mocks/index.ts new file mode 100644 index 0000000000000..c555ba322fa2b --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/index.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +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'; +export * from './get_search_list_mock'; +export * from './get_shard_mock'; +export * from './lists_services_mock_constants'; +export * from './get_update_list_options_mock'; +export * from './get_create_list_item_options_mock'; +export * from './get_list_item_response_mock'; +export * from './get_index_es_list_mock'; +export * from './get_index_es_list_item_mock'; +export * from './get_create_list_item_bulk_options_mock'; +export * from './get_delete_list_item_by_value_options_mock'; +export * from './get_delete_list_item_options_mock'; +export * from './get_list_item_by_values_options_mock'; +export * from './get_search_es_list_mock'; +export * from './get_search_es_list_item_mock'; +export * from './get_list_item_by_value_options_mock'; +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/mocks/lists_services_mock_constants.ts b/x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.ts new file mode 100644 index 0000000000000..d174211f348ea --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/lists_services_mock_constants.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export const DATE_NOW = '2020-04-20T15:25:31.830Z'; +export const USER = 'some user'; +export const LIST_INDEX = '.lists'; +export const LIST_ITEM_INDEX = '.items'; +export const NAME = 'some name'; +export const DESCRIPTION = 'some description'; +export const LIST_ID = 'some-list-id'; +export const LIST_ITEM_ID = 'some-list-item-id'; +export const TIE_BREAKER = '6a76b69d-80df-4ab2-8c3e-85f466b06a0e'; +export const TIE_BREAKERS = [ + '21530991-4051-46ec-bc35-2afa09a1b0b5', + '3c662054-ae37-4aa9-9936-3e8e2ea26775', + '60e49a20-3a23-48b6-8bf9-ed5e3b70f7a0', + '38814080-a40f-4358-992a-3b875f9b7dec', + '29fa61be-aaaf-411c-a78a-7059e3f723f1', + '9c19c959-cb9d-4cd2-99e4-1ea2baf0ef0e', + 'd409308c-f94b-4b3a-8234-bbd7a80c9140', + '87824c99-cd83-45c4-8aa6-4ad95dfea62c', + '7b940c17-9355-479f-b882-f3e575718f79', + '5983ad0c-4ef4-4fa0-8308-80ab9ecc4f74', +]; +export const META = {}; +export const TYPE = 'ip'; +export const VALUE = '127.0.0.1'; +export const VALUE_2 = '255.255.255'; diff --git a/x-pack/plugins/lists/server/services/mocks/test_readable.ts b/x-pack/plugins/lists/server/services/mocks/test_readable.ts new file mode 100644 index 0000000000000..52ad6de484005 --- /dev/null +++ b/x-pack/plugins/lists/server/services/mocks/test_readable.ts @@ -0,0 +1,11 @@ +/* + * 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 { Readable } from 'stream'; + +export class TestReadable extends Readable { + public _read(): void {} +} 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 new file mode 100644 index 0000000000000..3b6f58479a2f2 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.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 { getSearchEsListItemMock } from '../mocks'; +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 }); + const expected: Type = 'ip'; + expect(derivedType).toEqual(expected); + }); + + test('it returns the item keyword if it exists', () => { + const item = getSearchEsListItemMock(); + item.ip = undefined; + item.keyword = 'some keyword'; + const derivedType = deriveTypeFromItem({ item }); + const expected: Type = 'keyword'; + expect(derivedType).toEqual(expected); + }); + + test('it throws an error with status code if neither one exists', () => { + const item = getSearchEsListItemMock(); + item.ip = undefined; + item.keyword = undefined; + const expected = `Was expecting a valid type from the Elastic Search List Item such as ip or keyword but did not found one here ${JSON.stringify( + item + )}`; + expect(() => deriveTypeFromItem({ item })).toThrowError(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.ts b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.ts new file mode 100644 index 0000000000000..7a65e74bf4947 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/derive_type_from_es_type.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +interface DeriveTypeFromItemOptions { + item: SearchEsListItemSchema; +} + +export const deriveTypeFromItem = ({ item }: DeriveTypeFromItemOptions): Type => { + if (item.ip != null) { + return 'ip'; + } else if (item.keyword != null) { + return 'keyword'; + } else { + throw new ErrorWithStatusCode( + `Was expecting a valid type from the Elastic Search List Item such as ip or keyword but did not found one here ${JSON.stringify( + item + )}`, + 400 + ); + } +}; 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 0000000000000..3d48e44e26eaa --- /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/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts new file mode 100644 index 0000000000000..3f50efe0c6c56 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -0,0 +1,33 @@ +/* + * 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 { Type } from '../../../common/schemas'; + +export type QueryFilterType = Array< + { term: { list_id: string } } | { terms: { ip: string[] } } | { terms: { keyword: string[] } } +>; + +export const getQueryFilterFromTypeValue = ({ + type, + value, + listId, +}: { + type: Type; + value: string[]; + listId: string; + // We disable the consistent return since we want to use typescript for exhaustive type checks + // eslint-disable-next-line consistent-return +}): QueryFilterType => { + const filter: QueryFilterType = [{ term: { list_id: listId } }]; + switch (type) { + case 'ip': { + return [...filter, ...[{ terms: { ip: value } }]]; + } + case 'keyword': { + return [...filter, ...[{ terms: { keyword: value } }]]; + } + } +}; diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts new file mode 100644 index 0000000000000..8a44b5ab607bf --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +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 './derive_type_from_es_type'; 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 0000000000000..3b9864be6df53 --- /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 new file mode 100644 index 0000000000000..2dc0f4fe7a821 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; + +import { ListItemArraySchema, SearchEsListItemSchema, Type } from '../../../common/schemas'; +import { ErrorWithStatusCode } from '../../error_with_status_code'; + +export interface TransformElasticToListItemOptions { + response: SearchResponse<SearchEsListItemSchema>; + type: Type; +} + +export const transformElasticToListItem = ({ + response, + type, +}: TransformElasticToListItemOptions): ListItemArraySchema => { + return response.hits.hits.map(hit => { + const { + _id, + _source: { + created_at, + updated_at, + updated_by, + created_by, + list_id, + tie_breaker_id, + ip, + keyword, + meta, + }, + } = hit; + + const baseTypes = { + created_at, + created_by, + id: _id, + list_id, + meta, + tie_breaker_id, + type, + updated_at, + updated_by, + }; + + switch (type) { + case 'ip': { + if (ip == null) { + throw new ErrorWithStatusCode('Was expecting ip to not be null/undefined', 400); + } + return { + ...baseTypes, + value: ip, + }; + } + case 'keyword': { + if (keyword == null) { + throw new ErrorWithStatusCode('Was expecting keyword to not be null/undefined', 400); + } + return { + ...baseTypes, + value: keyword, + }; + } + } + return assertUnreachable(); + }); +}; + +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 0000000000000..217cad30bfdbb --- /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 new file mode 100644 index 0000000000000..051802cc41b5b --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_list_item_to_elastic_query.ts @@ -0,0 +1,33 @@ +/* + * 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'; + +export const transformListItemToElasticQuery = ({ + type, + value, +}: { + type: Type; + value: string; +}): EsDataTypeUnion => { + switch (type) { + case 'ip': { + return { + ip: value, + }; + } + case 'keyword': { + return { + keyword: value, + }; + } + } + return assertUnreachable(type); +}; + +const assertUnreachable = (type: string): never => { + throw new Error(`Unknown type: "${type}" in transformListItemToElasticQuery`); +}; diff --git a/x-pack/plugins/lists/server/siem_server_deps.ts b/x-pack/plugins/lists/server/siem_server_deps.ts new file mode 100644 index 0000000000000..e78debc8e4349 --- /dev/null +++ b/x-pack/plugins/lists/server/siem_server_deps.ts @@ -0,0 +1,21 @@ +/* + * 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 { + transformError, + buildSiemResponse, +} from '../../siem/server/lib/detection_engine/routes/utils'; +export { deleteTemplate } from '../../siem/server/lib/detection_engine/index/delete_template'; +export { deletePolicy } from '../../siem/server/lib/detection_engine/index/delete_policy'; +export { deleteAllIndex } from '../../siem/server/lib/detection_engine/index/delete_all_index'; +export { setPolicy } from '../../siem/server/lib/detection_engine/index/set_policy'; +export { setTemplate } from '../../siem/server/lib/detection_engine/index/set_template'; +export { getTemplateExists } from '../../siem/server/lib/detection_engine/index/get_template_exists'; +export { getPolicyExists } from '../../siem/server/lib/detection_engine/index/get_policy_exists'; +export { createBootstrapIndex } from '../../siem/server/lib/detection_engine/index/create_bootstrap_index'; +export { getIndexExists } from '../../siem/server/lib/detection_engine/index/get_index_exists'; +export { buildRouteValidation } from '../../siem/server/utils/build_validation/route_validation'; +export { validate } from '../../siem/server/lib/detection_engine/routes/rules/validate'; diff --git a/x-pack/plugins/lists/server/types.ts b/x-pack/plugins/lists/server/types.ts new file mode 100644 index 0000000000000..e0e4495d47c34 --- /dev/null +++ b/x-pack/plugins/lists/server/types.ts @@ -0,0 +1,28 @@ +/* + * 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 { IContextProvider, RequestHandler } from 'kibana/server'; + +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginSetup } from '../../spaces/server'; + +import { ListClient } from './services/lists/client'; + +export type ContextProvider = IContextProvider<RequestHandler<unknown, unknown, unknown>, 'lists'>; + +export interface PluginsSetup { + security: SecurityPluginSetup | undefined | null; + spaces: SpacesPluginSetup | undefined | null; +} + +export type ContextProviderReturn = Promise<{ getListClient: () => ListClient }>; +declare module 'src/core/server' { + interface RequestHandlerContext { + lists?: { + getListClient: () => ListClient; + }; + } +} diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json index bcc926535d3c2..97dbf58865a88 100644 --- a/x-pack/plugins/logstash/kibana.json +++ b/x-pack/plugins/logstash/kibana.json @@ -4,9 +4,13 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "logstash"], "requiredPlugins": [ - "licensing" + "licensing", + "management" + ], + "optionalPlugins": [ + "home", + "security" ], - "optionalPlugins": ["security"], "server": true, - "ui": false + "ui": true } diff --git a/x-pack/plugins/logstash/public/application/breadcrumbs.js b/x-pack/plugins/logstash/public/application/breadcrumbs.js new file mode 100644 index 0000000000000..322b9860b3785 --- /dev/null +++ b/x-pack/plugins/logstash/public/application/breadcrumbs.js @@ -0,0 +1,38 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export function getPipelineListBreadcrumbs() { + return [ + { + text: i18n.translate('xpack.logstash.pipelines.listBreadcrumb', { + defaultMessage: 'Pipelines', + }), + href: '#/management/logstash/pipelines', + }, + ]; +} + +export function getPipelineEditBreadcrumbs(pipelineId) { + return [ + ...getPipelineListBreadcrumbs(), + { + text: pipelineId, + }, + ]; +} + +export function getPipelineCreateBreadcrumbs() { + return [ + ...getPipelineListBreadcrumbs(), + { + text: i18n.translate('xpack.logstash.pipelines.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]; +} diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/flex_item_setting.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/form_label_with_icon_tip.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/pipeline_editor.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/confirm_delete_pipeline_modal.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/constants.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/constants.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/constants.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/constants.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/flex_item_setting.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/flex_item_setting.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/form_label_with_icon_tip.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/form_label_with_icon_tip.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/index.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/index.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/index.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js similarity index 97% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js index 5e430ccbd8ceb..e45820d56cc03 100644 --- a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.js @@ -13,7 +13,7 @@ import 'brace/mode/plain_text'; import 'brace/theme/github'; import { isEmpty } from 'lodash'; -import { TOOLTIPS } from '../../../../../../plugins/logstash/common/constants/tooltips'; +import { TOOLTIPS } from '../../../../common/constants/tooltips'; import { EuiButton, EuiButtonEmpty, @@ -40,7 +40,6 @@ class PipelineEditorUi extends React.Component { const { pipeline: { id, description, pipeline, settings }, - username, } = this.props; const pipelineWorkersSet = typeof settings['pipeline.workers'] === 'number'; @@ -60,7 +59,6 @@ class PipelineEditorUi extends React.Component { 'queue.max_bytes': settings['queue.max_bytes.number'] + settings['queue.max_bytes.units'], 'queue.type': settings['queue.type'], }, - username, }, pipelineIdErrors: [], pipelineIdPattern: /^[A-Za-z\_][A-Za-z0-9\-\_]*$/, @@ -236,15 +234,7 @@ class PipelineEditorUi extends React.Component { }; getPipelineHeadingText = () => { - const { - routeService: { - current: { - params: { clone, id }, - }, - }, - isNewPipeline, - intl, - } = this.props; + const { clone, id, isNewPipeline, intl } = this.props; if (!!clone && id) { return intl.formatMessage( @@ -502,6 +492,8 @@ class PipelineEditorUi extends React.Component { } PipelineEditorUi.propTypes = { + id: PropTypes.string, + clone: PropTypes.bool.isRequired, close: PropTypes.func.isRequired, isNewPipeline: PropTypes.bool.isRequired, licenseService: PropTypes.shape({ @@ -527,20 +519,11 @@ PipelineEditorUi.propTypes = { deletePipeline: PropTypes.func.isRequired, savePipeline: PropTypes.func.isRequired, }).isRequired, - routeService: PropTypes.shape({ - current: PropTypes.shape({ - params: PropTypes.shape({ - clone: PropTypes.oneOf([true, undefined]), - id: PropTypes.string, - }), - }), - }).isRequired, toastNotifications: PropTypes.shape({ addWarning: PropTypes.func.isRequired, addSuccess: PropTypes.func.isRequired, addError: PropTypes.func.isRequired, }).isRequired, - username: PropTypes.string, }; export const PipelineEditor = injectI18n(PipelineEditorUi); diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.test.js similarity index 96% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.test.js index 2d7ed5f257fbd..bb5961ce36120 100644 --- a/x-pack/legacy/plugins/logstash/public/components/pipeline_editor/pipeline_editor.test.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/pipeline_editor.test.js @@ -17,7 +17,6 @@ describe('PipelineEditor component', () => { let open; let pipeline; let pipelineService; - let routeService; let toastNotifications; let username; @@ -47,14 +46,6 @@ describe('PipelineEditor component', () => { deletePipeline: jest.fn(), savePipeline: jest.fn(), }; - routeService = { - current: { - params: { - clone: undefined, - id: undefined, - }, - }, - }; toastNotifications = { addWarning: jest.fn(), addSuccess: jest.fn(), @@ -62,13 +53,14 @@ describe('PipelineEditor component', () => { }; username = 'elastic'; props = { + clone: false, + id: 'pipelineId', close, isNewPipeline, licenseService, open, pipeline, pipelineService, - routeService, toastNotifications, username, }; @@ -79,10 +71,8 @@ describe('PipelineEditor component', () => { }); it('matches snapshot for clone pipeline', () => { - routeService.current.params = { - clone: true, - id: 'pipelineToClone', - }; + props.clone = true; + props.id = 'pipelineToClone'; expect(shallowWithIntl(<PipelineEditor.WrappedComponent {...props} />)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/add_role_alert.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/alert_call_out.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/enable_monitoring_alert.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/info_alerts.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/info_alerts.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/info_alerts.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/info_alerts.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap rename to x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/pipelines_table.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/add_role_alert.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/add_role_alert.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/alert_call_out.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/alert_call_out.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/confirm_delete_modal.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/constants.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/constants.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/constants.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/constants.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/enable_monitoring_alert.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/enable_monitoring_alert.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/index.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/index.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/index.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/info_alerts.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/info_alerts.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipeline_list.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipeline_list.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.js diff --git a/x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.test.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/pipeline_list/pipelines_table.test.js rename to x-pack/plugins/logstash/public/application/components/pipeline_list/pipelines_table.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_actions.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure_title.test.js.snap diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/constants.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/constants.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/constants.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/constants.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/index.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/index.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/index.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.test.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure.test.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.test.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_actions.test.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_actions.test.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.js diff --git a/x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.test.js b/x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.test.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/components/upgrade_failure/upgrade_failure_title.test.js rename to x-pack/plugins/logstash/public/application/components/upgrade_failure/upgrade_failure_title.test.js diff --git a/x-pack/plugins/logstash/public/application/index.tsx b/x-pack/plugins/logstash/public/application/index.tsx new file mode 100644 index 0000000000000..438038d6c885e --- /dev/null +++ b/x-pack/plugins/logstash/public/application/index.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; + +import { CoreStart } from 'src/core/public'; +import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; +import { + ClusterService, + MonitoringService, + PipelineService, + PipelinesService, + UpgradeService, + // @ts-ignore +} from '../services'; +// @ts-ignore +import { PipelineList } from './components/pipeline_list'; +import { PipelineEditView } from './pipeline_edit_view'; +// @ts-ignore +import { Pipeline } from '../models/pipeline'; +// @ts-ignore +import * as Breadcrumbs from './breadcrumbs'; + +export const renderApp = async ( + core: CoreStart, + { basePath, element, setBreadcrumbs }: ManagementAppMountParams, + licenseService$: Observable<any> +) => { + const logstashLicenseService = await licenseService$.pipe(first()).toPromise(); + const clusterService = new ClusterService(core.http); + const monitoringService = new MonitoringService( + core.http, + // When monitoring is migrated this should be fetched from monitoring's plugin contract + core.injectedMetadata.getInjectedVar('monitoringUiEnabled'), + clusterService + ); + const pipelinesService = new PipelinesService(core.http, monitoringService); + const pipelineService = new PipelineService(core.http, pipelinesService); + const upgradeService = new UpgradeService(core.http); + + ReactDOM.render( + <core.i18n.Context> + <HashRouter basename={basePath}> + <Switch> + <Route + path="/" + exact + render={({ history }) => { + setBreadcrumbs(Breadcrumbs.getPipelineListBreadcrumbs()); + return ( + <PipelineList + clusterService={clusterService} + isReadOnly={logstashLicenseService.isReadOnly} + isForbidden={true} + isLoading={false} + licenseService={logstashLicenseService} + monitoringService={monitoringService} + openPipeline={(id: string) => history.push(`/pipeline/${id}/edit`)} + clonePipeline={(id: string) => history.push(`/pipeline/${id}/edit?clone`)} + createPipeline={() => history.push(`/pipeline/new-pipeline`)} + pipelinesService={pipelinesService} + toastNotifications={core.notifications.toasts} + /> + ); + }} + /> + <Route + path="/pipeline/new-pipeline" + exact + render={({ history }) => ( + <PipelineEditView + history={history} + setBreadcrumbs={setBreadcrumbs} + logstashLicenseService={logstashLicenseService} + pipelineService={pipelineService} + toasts={core.notifications.toasts} + upgradeService={upgradeService} + /> + )} + /> + <Route + path="/pipeline/:id" + exact + render={({ match }) => <Redirect to={`/pipeline/${match.params.id}/edit`} />} + /> + <Route + path="/pipeline/:id/edit" + exact + render={({ match, history }) => ( + <PipelineEditView + history={history} + setBreadcrumbs={setBreadcrumbs} + logstashLicenseService={logstashLicenseService} + pipelineService={pipelineService} + toasts={core.notifications.toasts} + upgradeService={upgradeService} + id={match.params.id} + /> + )} + /> + </Switch> + </HashRouter> + </core.i18n.Context>, + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx b/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx new file mode 100644 index 0000000000000..c1b465febcd9b --- /dev/null +++ b/x-pack/plugins/logstash/public/application/pipeline_edit_view.tsx @@ -0,0 +1,153 @@ +/* + * 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, useLayoutEffect, useCallback } from 'react'; +import { usePromise } from 'react-use'; +import { History } from 'history'; + +import { i18n } from '@kbn/i18n'; +import { ToastsStart } from 'src/core/public'; + +// @ts-ignore +import { UpgradeFailure } from './components/upgrade_failure'; +// @ts-ignore +import { PipelineEditor } from './components/pipeline_editor'; +// @ts-ignore +import { Pipeline } from '../models/pipeline'; +import { ManagementAppMountParams } from '../../../../../src/plugins/management/public'; +// @ts-ignore +import * as Breadcrumbs from './breadcrumbs'; + +const usePipeline = ( + pipelineService: any, + logstashLicenseService: any, + toasts: ToastsStart, + shouldClone: boolean, + id?: string +) => { + const mounted = usePromise(); + const [pipeline, setPipeline] = useState<any>(null); + + useLayoutEffect(() => { + (async () => { + if (!id) { + return setPipeline(new Pipeline()); + } + + try { + const result = await mounted(pipelineService.loadPipeline(id) as Promise<any>); + setPipeline(shouldClone ? result.clone : result); + } catch (e) { + await logstashLicenseService.checkValidity(); + if (e.status !== 403) { + toasts.addDanger( + i18n.translate('xpack.logstash.couldNotLoadPipelineErrorNotification', { + defaultMessage: `Couldn't load pipeline. Error: '{errStatusText}'.`, + values: { + errStatusText: e.statusText, + }, + }) + ); + } + } + })(); + }, [pipelineService, id, mounted, shouldClone, logstashLicenseService, toasts]); + + return pipeline; +}; + +const useIsUpgraded = (upgradeService: any) => { + const [isUpgraded, setIsUpgraded] = useState<null | boolean>(null); + const mounted = usePromise(); + + useLayoutEffect(() => { + mounted(upgradeService.executeUpgrade() as Promise<boolean>).then(result => + setIsUpgraded(result) + ); + }, [mounted, upgradeService]); + + return isUpgraded; +}; + +interface EditProps { + pipelineService: any; + logstashLicenseService: any; + upgradeService: any; + toasts: ToastsStart; + history: History; + setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; + + // URL params + id?: string; +} + +export const PipelineEditView: React.FC<EditProps> = ({ + pipelineService, + logstashLicenseService, + upgradeService, + toasts, + history, + setBreadcrumbs, + id, +}) => { + const params = new URLSearchParams(history.location.search); + const shouldRetry = params.get('retry') === 'true'; + const shouldClone = params.get('clone') === ''; + + const pipeline = usePipeline(pipelineService, logstashLicenseService, toasts, shouldClone, id); + const isUpgraded = useIsUpgraded(upgradeService); + + const onRetry = useCallback(() => { + const newParams = new URLSearchParams(history.location.search); + newParams.set('retry', 'true'); + history.replace({ search: newParams.toString() }); + }, [history]); + const close = useCallback(() => { + history.push('/'); + }, [history]); + const open = useCallback( + (newId: string) => { + history.push(`/pipeline/${newId}/edit`); + }, + [history] + ); + + if (!pipeline || isUpgraded === null) { + return null; + } + + const isNewPipeline = !pipeline.id; + setBreadcrumbs( + isNewPipeline + ? Breadcrumbs.getPipelineCreateBreadcrumbs() + : Breadcrumbs.getPipelineEditBreadcrumbs(pipeline.id) + ); + + if (!isUpgraded) { + return ( + <UpgradeFailure + isNewPipeline={isNewPipeline} + isManualUpgrade={!!shouldRetry} + onRetry={onRetry} + onClose={close} + /> + ); + } + + return ( + <PipelineEditor + id={id} + clone={shouldClone} + close={close} + open={open} + isNewPipeline={isNewPipeline} + pipeline={pipeline} + pipelineService={pipelineService} + toastNotifications={toasts} + licenseService={logstashLicenseService} + /> + ); +}; diff --git a/x-pack/plugins/logstash/public/index.ts b/x-pack/plugins/logstash/public/index.ts new file mode 100644 index 0000000000000..26a1ca4e8c6c4 --- /dev/null +++ b/x-pack/plugins/logstash/public/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { LogstashPlugin } from './plugin'; + +export const plugin = () => new LogstashPlugin(); diff --git a/x-pack/legacy/plugins/logstash/public/lib/get_search_value/get_search_value.js b/x-pack/plugins/logstash/public/lib/get_search_value/get_search_value.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/lib/get_search_value/get_search_value.js rename to x-pack/plugins/logstash/public/lib/get_search_value/get_search_value.js diff --git a/x-pack/legacy/plugins/logstash/public/lib/get_search_value/index.js b/x-pack/plugins/logstash/public/lib/get_search_value/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/lib/get_search_value/index.js rename to x-pack/plugins/logstash/public/lib/get_search_value/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/cluster/cluster.js b/x-pack/plugins/logstash/public/models/cluster/cluster.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/cluster/cluster.js rename to x-pack/plugins/logstash/public/models/cluster/cluster.js diff --git a/x-pack/legacy/plugins/logstash/public/models/cluster/index.js b/x-pack/plugins/logstash/public/models/cluster/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/cluster/index.js rename to x-pack/plugins/logstash/public/models/cluster/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline/index.js b/x-pack/plugins/logstash/public/models/pipeline/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/pipeline/index.js rename to x-pack/plugins/logstash/public/models/pipeline/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline/pipeline.js b/x-pack/plugins/logstash/public/models/pipeline/pipeline.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/pipeline/pipeline.js rename to x-pack/plugins/logstash/public/models/pipeline/pipeline.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/index.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/index.js similarity index 100% rename from x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/index.js rename to x-pack/plugins/logstash/public/models/pipeline_list_item/index.js diff --git a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js similarity index 83% rename from x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js rename to x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js index 06d01a05bac27..3a304e467e0c0 100755 --- a/x-pack/legacy/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js +++ b/x-pack/plugins/logstash/public/models/pipeline_list_item/pipeline_list_item.js @@ -5,10 +5,10 @@ */ import { pick, capitalize } from 'lodash'; +import moment from 'moment'; -import { getSearchValue } from 'plugins/logstash/lib/get_search_value'; -import { getMoment } from 'plugins/logstash/../common/lib/get_moment'; -import { PIPELINE } from '../../../../../../plugins/logstash/common/constants'; +import { getSearchValue } from '../../lib/get_search_value'; +import { PIPELINE } from '../../../common/constants'; /** * Represents the model for listing pipelines in the UI @@ -25,7 +25,7 @@ export class PipelineListItem { this.username = props.username; if (props.lastModified) { - this.lastModified = getMoment(props.lastModified); + this.lastModified = getMomentDate(props.lastModified); this.lastModifiedHumanized = capitalize(this.lastModified.fromNow()); } } @@ -51,3 +51,11 @@ export class PipelineListItem { return new PipelineListItem(props); } } + +function getMomentDate(date) { + if (!date) { + return null; + } + + return moment(date); +} diff --git a/x-pack/plugins/logstash/public/plugin.ts b/x-pack/plugins/logstash/public/plugin.ts new file mode 100644 index 0000000000000..91d1a39d3970c --- /dev/null +++ b/x-pack/plugins/logstash/public/plugin.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 { i18n } from '@kbn/i18n'; +import { Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { once } from 'lodash'; + +import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { + HomePublicPluginSetup, + FeatureCatalogueCategory, +} from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; + +// @ts-ignore +import { LogstashLicenseService } from './services'; + +interface SetupDeps { + licensing: LicensingPluginSetup; + management: ManagementSetup; + + home?: HomePublicPluginSetup; +} + +export class LogstashPlugin implements Plugin<void, void, SetupDeps> { + private licenseSubscription?: Subscription; + + public setup(core: CoreSetup, plugins: SetupDeps) { + const logstashLicense$ = plugins.licensing.license$.pipe( + map(license => new LogstashLicenseService(license)) + ); + const section = plugins.management.sections.register({ + id: 'logstash', + title: 'Logstash', + order: 30, + euiIconType: 'logoLogstash', + }); + const managementApp = section.registerApp({ + id: 'pipelines', + title: i18n.translate('xpack.logstash.managementSection.pipelinesTitle', { + defaultMessage: 'Pipelines', + }), + order: 10, + mount: async params => { + const [coreStart] = await core.getStartServices(); + const { renderApp } = await import('./application'); + + return renderApp(coreStart, params, logstashLicense$); + }, + }); + + this.licenseSubscription = logstashLicense$.subscribe((license: any) => { + if (license.enableLinks) { + managementApp.enable(); + } else { + managementApp.disable(); + } + + if (plugins.home && license.enableLinks) { + // Ensure that we don't register the feature more than once + once(() => { + plugins.home!.featureCatalogue.register({ + id: 'management_logstash', + title: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesTitle', { + defaultMessage: 'Logstash Pipelines', + }), + description: i18n.translate('xpack.logstash.homeFeature.logstashPipelinesDescription', { + defaultMessage: 'Create, delete, update, and clone data ingestion pipelines.', + }), + icon: 'pipelineApp', + path: '/app/kibana#/management/logstash/pipelines', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + }); + } + }); + } + + public start(core: CoreStart) {} + + public stop() { + if (this.licenseSubscription) { + this.licenseSubscription.unsubscribe(); + } + } +} diff --git a/x-pack/plugins/logstash/public/services/cluster/cluster_service.js b/x-pack/plugins/logstash/public/services/cluster/cluster_service.js new file mode 100755 index 0000000000000..20f3b0d349c80 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/cluster/cluster_service.js @@ -0,0 +1,29 @@ +/* + * 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 { ROUTES } from '../../../common/constants'; +import { Cluster } from '../../models/cluster'; + +export class ClusterService { + constructor(http) { + this.http = http; + } + + loadCluster() { + return this.http.get(`${ROUTES.API_ROOT}/cluster`).then(response => { + if (!response) { + return; + } + return Cluster.fromUpstreamJSON(response.cluster); + }); + } + + isClusterInfoAvailable() { + return this.loadCluster() + .then(cluster => Boolean(cluster)) + .catch(() => false); + } +} diff --git a/x-pack/plugins/logstash/public/services/cluster/index.js b/x-pack/plugins/logstash/public/services/cluster/index.js new file mode 100755 index 0000000000000..4417262d9f442 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/cluster/index.js @@ -0,0 +1,7 @@ +/* + * 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 { ClusterService } from './cluster_service'; diff --git a/x-pack/plugins/logstash/public/services/index.js b/x-pack/plugins/logstash/public/services/index.js new file mode 100644 index 0000000000000..a7e8aa5c6259f --- /dev/null +++ b/x-pack/plugins/logstash/public/services/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ClusterService } from './cluster'; +export { LogstashLicenseService } from './license'; +export { MonitoringService } from './monitoring'; +export { PipelineService } from './pipeline'; +export { PipelinesService } from './pipelines'; +export { UpgradeService } from './upgrade'; diff --git a/x-pack/plugins/logstash/public/services/license/index.js b/x-pack/plugins/logstash/public/services/license/index.js new file mode 100755 index 0000000000000..64f39b1144cee --- /dev/null +++ b/x-pack/plugins/logstash/public/services/license/index.js @@ -0,0 +1,7 @@ +/* + * 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 { LogstashLicenseService } from './logstash_license_service'; diff --git a/x-pack/plugins/logstash/public/services/license/logstash_license_service.js b/x-pack/plugins/logstash/public/services/license/logstash_license_service.js new file mode 100755 index 0000000000000..b836b75b89cc7 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/license/logstash_license_service.js @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export class LogstashLicenseService { + constructor(license, navigateToApp, toasts) { + this.license = license; + this.navigateToApp = navigateToApp; + this.toasts = toasts; + } + + get enableLinks() { + return this.calculated.enableLinks; + } + + get isAvailable() { + return this.calculated.isAvailable; + } + + get isReadOnly() { + return this.calculated.isReadOnly; + } + + get message() { + return this.calculated.message; + } + + get isSecurityEnabled() { + return this.license.getFeature(`security`).isEnabled; + } + + /** + * Checks if the license is valid or the license can perform downgraded UI tasks. + * Rejects if the plugin is not available due to license. + */ + checkValidity() { + return new Promise((resolve, reject) => { + if (this.isAvailable) { + return resolve(); + } + + return reject(); + }); + } + + get calculated() { + if (!this.license) { + throw new Error(`No license available!`); + } + + if (!this.isSecurityEnabled) { + return { + isAvailable: false, + enableLinks: false, + isReadOnly: false, + message: i18n.translate('xpack.logstash.managementSection.enableSecurityDescription', { + defaultMessage: + 'Security must be enabled in order to use Logstash pipeline management features.' + + ' Please set xpack.security.enabled: true in your elasticsearch.yml.', + }), + }; + } + + if (!this.license.hasAtLeast('standard')) { + return { + isAvailable: false, + enableLinks: false, + isReadOnly: false, + message: i18n.translate( + 'xpack.logstash.managementSection.licenseDoesNotSupportDescription', + { + defaultMessage: + 'Your {licenseType} license does not support Logstash pipeline management features. Please upgrade your license.', + values: { licenseType: this.license.type }, + } + ), + }; + } + + if (!this.license.isActive) { + return { + isAvailable: true, + enableLinks: true, + isReadonly: true, + message: i18n.translate( + 'xpack.logstash.managementSection.pipelineCrudOperationsNotAllowedDescription', + { + defaultMessage: + 'You cannot edit, create, or delete your Logstash pipelines because your {licenseType} license has expired.', + values: { licenseType: this.license.type }, + } + ), + }; + } + + return { + isAvailable: true, + enableLinks: true, + isReadOnly: false, + }; + } +} diff --git a/x-pack/plugins/logstash/public/services/monitoring/index.js b/x-pack/plugins/logstash/public/services/monitoring/index.js new file mode 100755 index 0000000000000..bc0e8b6bc978a --- /dev/null +++ b/x-pack/plugins/logstash/public/services/monitoring/index.js @@ -0,0 +1,7 @@ +/* + * 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 { MonitoringService } from './monitoring_service'; diff --git a/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js new file mode 100755 index 0000000000000..d551f4fba61d2 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/monitoring/monitoring_service.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { ROUTES, MONITORING } from '../../../common/constants'; +import { PipelineListItem } from '../../models/pipeline_list_item'; + +export class MonitoringService { + constructor(http, monitoringUiEnabled, clusterService) { + this.http = http; + this.monitoringUiEnabled = monitoringUiEnabled; + this.clusterService = clusterService; + } + + isMonitoringEnabled() { + return this.monitoringUiEnabled; + } + + getPipelineList() { + if (!this.isMonitoringEnabled()) { + return Promise.resolve([]); + } + + return this.clusterService + .loadCluster() + .then(cluster => { + const url = `${ROUTES.MONITORING_API_ROOT}/v1/clusters/${cluster.uuid}/logstash/pipeline_ids`; + const now = moment.utc(); + const body = JSON.stringify({ + timeRange: { + max: now.toISOString(), + min: now.subtract(MONITORING.ACTIVE_PIPELINE_RANGE_S, 'seconds').toISOString(), + }, + }); + return this.http.post(url, { body }); + }) + .then(response => + response.map(pipeline => PipelineListItem.fromUpstreamMonitoringJSON(pipeline)) + ) + .catch(() => []); + } +} diff --git a/x-pack/plugins/logstash/public/services/pipeline/index.js b/x-pack/plugins/logstash/public/services/pipeline/index.js new file mode 100755 index 0000000000000..70d228b34860b --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipeline/index.js @@ -0,0 +1,7 @@ +/* + * 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 { PipelineService } from './pipeline_service'; diff --git a/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js b/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js new file mode 100755 index 0000000000000..7c3e18e745d82 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipeline/pipeline_service.js @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ROUTES } from '../../../common/constants'; +import { Pipeline } from '../../models/pipeline'; + +export class PipelineService { + constructor(http, pipelinesService) { + this.http = http; + this.pipelinesService = pipelinesService; + } + + loadPipeline(id) { + return this.http.get(`${ROUTES.API_ROOT}/pipeline/${id}`).then(response => { + return Pipeline.fromUpstreamJSON(response); + }); + } + + savePipeline(pipelineModel) { + return this.http + .put(`${ROUTES.API_ROOT}/pipeline/${pipelineModel.id}`, { + body: JSON.stringify(pipelineModel.upstreamJSON), + }) + .catch(e => { + throw e.message; + }); + } + + deletePipeline(id) { + return this.http + .delete(`${ROUTES.API_ROOT}/pipeline/${id}`) + .then(() => this.pipelinesService.addToRecentlyDeleted(id)) + .catch(e => { + throw e.message; + }); + } +} diff --git a/x-pack/plugins/logstash/public/services/pipelines/index.js b/x-pack/plugins/logstash/public/services/pipelines/index.js new file mode 100755 index 0000000000000..a932dd4b951f4 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipelines/index.js @@ -0,0 +1,7 @@ +/* + * 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 { PipelinesService } from './pipelines_service'; diff --git a/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js b/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js new file mode 100755 index 0000000000000..00610a23f2717 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/pipelines/pipelines_service.js @@ -0,0 +1,128 @@ +/* + * 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 { ROUTES, MONITORING } from '../../../common/constants'; +import { PipelineListItem } from '../../models/pipeline_list_item'; + +const RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY = 'xpack.logstash.recentlyDeletedPipelines'; + +export class PipelinesService { + constructor(http, monitoringService) { + this.http = http; + this.monitoringService = monitoringService; + } + + getPipelineList() { + return Promise.all([this.getManagementPipelineList(), this.getMonitoringPipelineList()]).then( + ([managementPipelines, monitoringPipelines]) => { + const now = Date.now(); + + // Monitoring will report centrally-managed pipelines as well, including recently-deleted centrally-managed ones. + // If there's a recently-deleted pipeline we're keeping track of BUT monitoring doesn't report it, that means + // it's not running in Logstash any more. So we can stop tracking it as a recently-deleted pipeline. + const monitoringPipelineIds = monitoringPipelines.map(pipeline => pipeline.id); + this.getRecentlyDeleted().forEach(recentlyDeletedPipeline => { + // We don't want to stop tracking the recently-deleted pipeline until Monitoring has had some + // time to report on it. Otherwise, if we stop tracking first, *then* Monitoring reports it, we'll + // still end up showing it in the list until Monitoring stops reporting it. + if (now - recentlyDeletedPipeline.deletedOn < MONITORING.ACTIVE_PIPELINE_RANGE_S * 1000) { + return; + } + + // If Monitoring is still reporting the pipeline, don't stop tracking it yet + if (monitoringPipelineIds.includes(recentlyDeletedPipeline.id)) { + return; + } + + this.removeFromRecentlyDeleted(recentlyDeletedPipeline.id); + }); + + // Merge centrally-managed pipelines with pipelines reported by monitoring. Take care to dedupe + // while merging because monitoring will (rightly) report centrally-managed pipelines as well, + // including recently-deleted ones! + const managementPipelineIds = managementPipelines.map(pipeline => pipeline.id); + return managementPipelines.concat( + monitoringPipelines.filter( + monitoringPipeline => + !managementPipelineIds.includes(monitoringPipeline.id) && + !this.isRecentlyDeleted(monitoringPipeline.id) + ) + ); + } + ); + } + + getManagementPipelineList() { + return this.http.get(`${ROUTES.API_ROOT}/pipelines`).then(response => { + return response.pipelines.map(pipeline => PipelineListItem.fromUpstreamJSON(pipeline)); + }); + } + + getMonitoringPipelineList() { + return this.monitoringService.getPipelineList(); + } + + /** + * Delete a collection of pipelines + * + * @param pipelineIds Array of pipeline IDs + * @return Promise { numSuccesses, numErrors } + */ + deletePipelines(pipelineIds) { + const body = JSON.stringify({ + pipelineIds, + }); + return this.http.post(`${ROUTES.API_ROOT}/pipelines/delete`, { body }).then(response => { + this.addToRecentlyDeleted(...pipelineIds); + return response.results; + }); + } + + addToRecentlyDeleted(...pipelineIds) { + const recentlyDeletedPipelines = this.getRecentlyDeleted(); + const recentlyDeletedPipelineIds = recentlyDeletedPipelines.map(pipeline => pipeline.id); + pipelineIds.forEach(pipelineId => { + if (!recentlyDeletedPipelineIds.includes(pipelineId)) { + recentlyDeletedPipelines.push({ + id: pipelineId, + deletedOn: Date.now(), + }); + } + }); + this.setRecentlyDeleted(recentlyDeletedPipelines); + } + + removeFromRecentlyDeleted(...pipelineIds) { + const recentlyDeletedPipelinesToKeep = this.getRecentlyDeleted().filter( + recentlyDeletedPipeline => !pipelineIds.includes(recentlyDeletedPipeline.id) + ); + this.setRecentlyDeleted(recentlyDeletedPipelinesToKeep); + } + + isRecentlyDeleted(pipelineId) { + return this.getRecentlyDeleted() + .map(pipeline => pipeline.id) + .includes(pipelineId); + } + + getRecentlyDeleted() { + const recentlyDeletedPipelines = window.localStorage.getItem( + RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY + ); + if (!recentlyDeletedPipelines) { + return []; + } + + return JSON.parse(recentlyDeletedPipelines); + } + + setRecentlyDeleted(recentlyDeletedPipelineIds) { + window.localStorage.setItem( + RECENTLY_DELETED_PIPELINE_IDS_STORAGE_KEY, + JSON.stringify(recentlyDeletedPipelineIds) + ); + } +} diff --git a/x-pack/plugins/logstash/public/services/upgrade/index.js b/x-pack/plugins/logstash/public/services/upgrade/index.js new file mode 100755 index 0000000000000..1c835b11ae423 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/upgrade/index.js @@ -0,0 +1,7 @@ +/* + * 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 { UpgradeService } from './upgrade_service'; diff --git a/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js b/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js new file mode 100755 index 0000000000000..7bd101ebee6b0 --- /dev/null +++ b/x-pack/plugins/logstash/public/services/upgrade/upgrade_service.js @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ROUTES } from '../../../common/constants'; + +export class UpgradeService { + constructor(http) { + this.http = http; + } + + executeUpgrade() { + return this.http + .post(`${ROUTES.API_ROOT}/upgrade`) + .then(response => response.is_upgraded) + .catch(e => { + throw e.message; + }); + } +} diff --git a/x-pack/plugins/logstash/server/routes/pipeline/save.ts b/x-pack/plugins/logstash/server/routes/pipeline/save.ts index 556c281944a85..e484d0e221b6d 100644 --- a/x-pack/plugins/logstash/server/routes/pipeline/save.ts +++ b/x-pack/plugins/logstash/server/routes/pipeline/save.ts @@ -25,7 +25,6 @@ export function registerPipelineSaveRoute(router: IRouter, security?: SecurityPl id: schema.string(), description: schema.string(), pipeline: schema.string(), - username: schema.string(), settings: schema.maybe(schema.object({}, { unknowns: 'allow' })), }), }, diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index a4006732224ce..fd972219563a8 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -166,6 +166,7 @@ export enum STYLE_TYPE { export enum LAYER_STYLE_TYPE { VECTOR = 'VECTOR', HEATMAP = 'HEATMAP', + TILE = 'TILE', } export const COLOR_MAP_TYPE = { @@ -214,3 +215,5 @@ export enum SCALING_TYPES { } export const RGBA_0000 = 'rgba(0,0,0,0)'; + +export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID'; diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index f8175b0ed3f10..6980f14d0788a 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -5,8 +5,9 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { Query } from 'src/plugins/data/public'; import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; -import { VectorStyleDescriptor } from './style_property_descriptor_types'; +import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; export type AttributionDescriptor = { @@ -17,6 +18,7 @@ export type AttributionDescriptor = { export type AbstractSourceDescriptor = { id?: string; type: string; + applyGlobalQuery?: boolean; }; export type EMSTMSSourceDescriptor = AbstractSourceDescriptor & { @@ -71,17 +73,15 @@ export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & { term: string; // term field name }; -export type KibanaRegionmapSourceDescriptor = { - type: string; +export type KibanaRegionmapSourceDescriptor = AbstractSourceDescriptor & { name: string; }; -export type KibanaTilemapSourceDescriptor = { - type: string; -}; +// This is for symmetry with other sources only. +// It takes no additional configuration since all params are in the .yml. +export type KibanaTilemapSourceDescriptor = AbstractSourceDescriptor; -export type WMSSourceDescriptor = { - type: string; +export type WMSSourceDescriptor = AbstractSourceDescriptor & { serviceUrl: string; layers: string; styles: string; @@ -111,6 +111,8 @@ export type JoinDescriptor = { right: ESTermSourceDescriptor; }; +// todo : this union type is incompatible with dynamic extensibility of sources. +// Reconsider using SourceDescriptor in type signatures for top-level classes export type SourceDescriptor = | XYZTMSSourceDescriptor | WMSSourceDescriptor @@ -121,7 +123,9 @@ export type SourceDescriptor = | ESGeoGridSourceDescriptor | EMSFileSourceDescriptor | ESPewPewSourceDescriptor - | TiledSingleLayerVectorSourceDescriptor; + | TiledSingleLayerVectorSourceDescriptor + | EMSTMSSourceDescriptor + | EMSFileSourceDescriptor; export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; @@ -129,12 +133,14 @@ export type LayerDescriptor = { __errorMessage?: string; alpha?: number; id: string; - label?: string; + label?: string | null; minZoom?: number; maxZoom?: number; - sourceDescriptor: SourceDescriptor; + sourceDescriptor: SourceDescriptor | null; type?: string; visible?: boolean; + style?: StyleDescriptor | null; + query?: Query; }; export type VectorLayerDescriptor = LayerDescriptor & { diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts index 47e56ff96d623..381bc5bba01c0 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts @@ -182,7 +182,11 @@ export type VectorStylePropertiesDescriptor = { [VECTOR_STYLES.LABEL_BORDER_SIZE]?: LabelBorderSizeStylePropertyDescriptor; }; -export type VectorStyleDescriptor = { +export type StyleDescriptor = { + type: string; +}; + +export type VectorStyleDescriptor = StyleDescriptor & { type: LAYER_STYLE_TYPE.VECTOR; properties: VectorStylePropertiesDescriptor; }; diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js rename to x-pack/plugins/maps/common/migrations/add_field_meta_options.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js rename to x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js rename to x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js rename to x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.test.ts similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/join_agg_key.test.ts rename to x-pack/plugins/maps/common/migrations/join_agg_key.test.ts diff --git a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.ts similarity index 97% rename from x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts rename to x-pack/plugins/maps/common/migrations/join_agg_key.ts index 29661aedb550c..97b9ee4692c25 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/join_agg_key.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.ts @@ -20,7 +20,7 @@ import { LayerDescriptor, VectorLayerDescriptor, } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; const GROUP_BY_DELIMITER = '_groupby_'; diff --git a/x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js rename to x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js rename to x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js b/x-pack/plugins/maps/common/migrations/move_apply_global_query.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.js rename to x-pack/plugins/maps/common/migrations/move_apply_global_query.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.test.js b/x-pack/plugins/maps/common/migrations/move_apply_global_query.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/move_apply_global_query.test.js rename to x-pack/plugins/maps/common/migrations/move_apply_global_query.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/references.js b/x-pack/plugins/maps/common/migrations/references.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/references.js rename to x-pack/plugins/maps/common/migrations/references.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/references.test.js b/x-pack/plugins/maps/common/migrations/references.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/references.test.js rename to x-pack/plugins/maps/common/migrations/references.test.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts b/x-pack/plugins/maps/common/migrations/scaling_type.test.ts similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/scaling_type.test.ts rename to x-pack/plugins/maps/common/migrations/scaling_type.test.ts diff --git a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts b/x-pack/plugins/maps/common/migrations/scaling_type.ts similarity index 93% rename from x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts rename to x-pack/plugins/maps/common/migrations/scaling_type.ts index 551975fbacea5..98a06a764f4ec 100644 --- a/x-pack/legacy/plugins/maps/common/migrations/scaling_type.ts +++ b/x-pack/plugins/maps/common/migrations/scaling_type.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { SOURCE_TYPES, SCALING_TYPES } from '../constants'; import { LayerDescriptor, ESSearchSourceDescriptor } from '../descriptor_types'; -import { MapSavedObjectAttributes } from '../../../../../plugins/maps/common/map_saved_object_type'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; function isEsDocumentSource(layerDescriptor: LayerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); diff --git a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.js rename to x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js diff --git a/x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.test.js b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/common/migrations/top_hits_time_to_sort.test.js rename to x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.test.js diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index b2aec30c113eb..b8bad47327f22 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -3,6 +3,16 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "maps"], - "requiredPlugins": ["inspector"], + "requiredPlugins": [ + "inspector", + "licensing", + "home", + "data", + "fileUpload", + "uiActions", + "navigation", + "visualizations", + "embeddable" + ], "ui": true } diff --git a/x-pack/legacy/plugins/maps/public/_main.scss b/x-pack/plugins/maps/public/_main.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/_main.scss rename to x-pack/plugins/maps/public/_main.scss diff --git a/x-pack/legacy/plugins/maps/public/_mapbox_hacks.scss b/x-pack/plugins/maps/public/_mapbox_hacks.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/_mapbox_hacks.scss rename to x-pack/plugins/maps/public/_mapbox_hacks.scss diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index debead3ad5c45..38c56405787eb 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -14,6 +14,7 @@ import { MapCenterAndZoom, MapRefreshConfig, } from '../../common/descriptor_types'; +import { MapSettings } from '../reducers/map'; export type SyncContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -62,3 +63,22 @@ export function hideViewControl(): AnyAction; export function setHiddenLayers(hiddenLayerIds: string[]): AnyAction; export function addLayerWithoutDataSync(layerDescriptor: unknown): AnyAction; + +export function setMapSettings(settings: MapSettings): AnyAction; + +export function rollbackMapSettings(): AnyAction; + +export function trackMapSettings(): AnyAction; + +export function updateMapSetting( + settingKey: string, + settingValue: string | boolean | number +): AnyAction; + +export function cloneLayer(layerId: string): AnyAction; + +export function fitToLayerExtent(layerId: string): AnyAction; + +export function removeLayer(layerId: string): AnyAction; + +export function toggleLayerVisible(layerId: string): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/map_actions.js b/x-pack/plugins/maps/public/actions/map_actions.js index 13cb3d5f89860..da6ba6b481054 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.js +++ b/x-pack/plugins/maps/public/actions/map_actions.js @@ -4,6 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import turf from 'turf'; +import turfBooleanContains from '@turf/boolean-contains'; +import uuid from 'uuid/v4'; +import { + getLayerList, + getLayerListRaw, + getDataFilters, + getSelectedLayerId, + getMapReady, + getWaitingForMapReadyLayerListRaw, + getTransientLayerId, + getOpenTooltips, + getQuery, + getDataRequestDescriptor, +} from '../selectors/map_selectors'; + +import { FLYOUT_STATE } from '../reducers/ui'; +import { + cancelRequest, + registerCancelCallback, + unregisterCancelCallback, + getEventHandlers, +} from '../reducers/non_serializable_instances'; +import { updateFlyout } from './ui_actions'; +import { + FEATURE_ID_PROPERTY_NAME, + LAYER_TYPE, + SOURCE_DATA_ID_ORIGIN, +} from '../../common/constants'; + export const SET_SELECTED_LAYER = 'SET_SELECTED_LAYER'; export const SET_TRANSIENT_LAYER = 'SET_TRANSIENT_LAYER'; export const UPDATE_LAYER_ORDER = 'UPDATE_LAYER_ORDER'; @@ -45,3 +76,924 @@ export const HIDE_TOOLBAR_OVERLAY = 'HIDE_TOOLBAR_OVERLAY'; export const HIDE_LAYER_CONTROL = 'HIDE_LAYER_CONTROL'; export const HIDE_VIEW_CONTROL = 'HIDE_VIEW_CONTROL'; export const SET_WAITING_FOR_READY_HIDDEN_LAYERS = 'SET_WAITING_FOR_READY_HIDDEN_LAYERS'; +export const SET_MAP_SETTINGS = 'SET_MAP_SETTINGS'; +export const ROLLBACK_MAP_SETTINGS = 'ROLLBACK_MAP_SETTINGS'; +export const TRACK_MAP_SETTINGS = 'TRACK_MAP_SETTINGS'; +export const UPDATE_MAP_SETTING = 'UPDATE_MAP_SETTING'; + +function getLayerLoadingCallbacks(dispatch, getState, layerId) { + return { + startLoading: (dataId, requestToken, meta) => + dispatch(startDataLoad(layerId, dataId, requestToken, meta)), + stopLoading: (dataId, requestToken, data, meta) => + dispatch(endDataLoad(layerId, dataId, requestToken, data, meta)), + onLoadError: (dataId, requestToken, errorMessage) => + dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)), + updateSourceData: newData => { + dispatch(updateSourceDataRequest(layerId, newData)); + }, + isRequestStillActive: (dataId, requestToken) => { + const dataRequest = getDataRequestDescriptor(getState(), layerId, dataId); + if (!dataRequest) { + return false; + } + return dataRequest.dataRequestToken === requestToken; + }, + registerCancelCallback: (requestToken, callback) => + dispatch(registerCancelCallback(requestToken, callback)), + }; +} + +function getLayerById(layerId, state) { + return getLayerList(state).find(layer => { + return layerId === layer.getId(); + }); +} + +async function syncDataForAllLayers(dispatch, getState, dataFilters) { + const state = getState(); + const layerList = getLayerList(state); + const syncs = layerList.map(layer => { + const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layer.getId()); + return layer.syncData({ ...loadingFunctions, dataFilters }); + }); + await Promise.all(syncs); +} + +export function cancelAllInFlightRequests() { + return (dispatch, getState) => { + getLayerList(getState()).forEach(layer => { + dispatch(clearDataRequests(layer)); + }); + }; +} + +function clearDataRequests(layer) { + return dispatch => { + layer.getInFlightRequestTokens().forEach(requestToken => { + dispatch(cancelRequest(requestToken)); + }); + dispatch({ + type: UPDATE_LAYER_PROP, + id: layer.getId(), + propName: '__dataRequests', + newValue: [], + }); + }; +} + +export function setMapInitError(errorMessage) { + return { + type: SET_MAP_INIT_ERROR, + errorMessage, + }; +} + +export function setMapSettings(settings) { + return { + type: SET_MAP_SETTINGS, + settings, + }; +} + +export function rollbackMapSettings() { + return { type: ROLLBACK_MAP_SETTINGS }; +} + +export function trackMapSettings() { + return { type: TRACK_MAP_SETTINGS }; +} + +export function updateMapSetting(settingKey, settingValue) { + return { + type: UPDATE_MAP_SETTING, + settingKey, + settingValue, + }; +} + +export function trackCurrentLayerState(layerId) { + return { + type: TRACK_CURRENT_LAYER_STATE, + layerId: layerId, + }; +} + +export function rollbackToTrackedLayerStateForSelectedLayer() { + return async (dispatch, getState) => { + const layerId = getSelectedLayerId(getState()); + await dispatch({ + type: ROLLBACK_TO_TRACKED_LAYER_STATE, + layerId: layerId, + }); + + // Ensure updateStyleMeta is triggered + // syncDataForLayer may not trigger endDataLoad if no re-fetch is required + dispatch(updateStyleMeta(layerId)); + + dispatch(syncDataForLayer(layerId)); + }; +} + +export function removeTrackedLayerStateForSelectedLayer() { + return (dispatch, getState) => { + const layerId = getSelectedLayerId(getState()); + dispatch({ + type: REMOVE_TRACKED_LAYER_STATE, + layerId: layerId, + }); + }; +} + +export function replaceLayerList(newLayerList) { + return (dispatch, getState) => { + const isMapReady = getMapReady(getState()); + if (!isMapReady) { + dispatch({ + type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, + }); + } else { + getLayerListRaw(getState()).forEach(({ id }) => { + dispatch(removeLayerFromLayerList(id)); + }); + } + + newLayerList.forEach(layerDescriptor => { + dispatch(addLayer(layerDescriptor)); + }); + }; +} + +export function cloneLayer(layerId) { + return async (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer) { + return; + } + + const clonedDescriptor = await layer.cloneDescriptor(); + dispatch(addLayer(clonedDescriptor)); + }; +} + +export function addLayer(layerDescriptor) { + return (dispatch, getState) => { + const isMapReady = getMapReady(getState()); + if (!isMapReady) { + dispatch({ + type: ADD_WAITING_FOR_MAP_READY_LAYER, + layer: layerDescriptor, + }); + return; + } + + dispatch({ + type: ADD_LAYER, + layer: layerDescriptor, + }); + dispatch(syncDataForLayer(layerDescriptor.id)); + }; +} + +// Do not use when rendering a map. Method exists to enable selectors for getLayerList when +// rendering is not needed. +export function addLayerWithoutDataSync(layerDescriptor) { + return { + type: ADD_LAYER, + layer: layerDescriptor, + }; +} + +function setLayerDataLoadErrorStatus(layerId, errorMessage) { + return dispatch => { + dispatch({ + type: SET_LAYER_ERROR_STATUS, + isInErrorState: errorMessage !== null, + layerId, + errorMessage, + }); + }; +} + +export function cleanTooltipStateForLayer(layerId, layerFeatures = []) { + return (dispatch, getState) => { + let featuresRemoved = false; + const openTooltips = getOpenTooltips(getState()) + .map(tooltipState => { + const nextFeatures = tooltipState.features.filter(tooltipFeature => { + if (tooltipFeature.layerId !== layerId) { + // feature from another layer, keep it + return true; + } + + // Keep feature if it is still in layer + return layerFeatures.some(layerFeature => { + return layerFeature.properties[FEATURE_ID_PROPERTY_NAME] === tooltipFeature.id; + }); + }); + + if (tooltipState.features.length !== nextFeatures.length) { + featuresRemoved = true; + } + + return { ...tooltipState, features: nextFeatures }; + }) + .filter(tooltipState => { + return tooltipState.features.length > 0; + }); + + if (featuresRemoved) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); + } + }; +} + +export function setLayerVisibility(layerId, makeVisible) { + return async (dispatch, getState) => { + //if the current-state is invisible, we also want to sync data + //e.g. if a layer was invisible at start-up, it won't have any data loaded + const layer = getLayerById(layerId, getState()); + + // If the layer visibility is already what we want it to be, do nothing + if (!layer || layer.isVisible() === makeVisible) { + return; + } + + if (!makeVisible) { + dispatch(cleanTooltipStateForLayer(layerId)); + } + + await dispatch({ + type: SET_LAYER_VISIBILITY, + layerId, + visibility: makeVisible, + }); + if (makeVisible) { + dispatch(syncDataForLayer(layerId)); + } + }; +} + +export function toggleLayerVisible(layerId) { + return async (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer) { + return; + } + const makeVisible = !layer.isVisible(); + + dispatch(setLayerVisibility(layerId, makeVisible)); + }; +} + +export function setSelectedLayer(layerId) { + return async (dispatch, getState) => { + const oldSelectedLayer = getSelectedLayerId(getState()); + if (oldSelectedLayer) { + await dispatch(rollbackToTrackedLayerStateForSelectedLayer()); + } + if (layerId) { + dispatch(trackCurrentLayerState(layerId)); + } + dispatch({ + type: SET_SELECTED_LAYER, + selectedLayerId: layerId, + }); + }; +} + +export function removeTransientLayer() { + return async (dispatch, getState) => { + const transientLayerId = getTransientLayerId(getState()); + if (transientLayerId) { + await dispatch(removeLayerFromLayerList(transientLayerId)); + await dispatch(setTransientLayer(null)); + } + }; +} + +export function setTransientLayer(layerId) { + return { + type: SET_TRANSIENT_LAYER, + transientLayerId: layerId, + }; +} + +export function clearTransientLayerStateAndCloseFlyout() { + return async dispatch => { + await dispatch(updateFlyout(FLYOUT_STATE.NONE)); + await dispatch(setSelectedLayer(null)); + await dispatch(removeTransientLayer()); + }; +} + +export function updateLayerOrder(newLayerOrder) { + return { + type: UPDATE_LAYER_ORDER, + newLayerOrder, + }; +} + +export function mapReady() { + return (dispatch, getState) => { + dispatch({ + type: MAP_READY, + }); + + getWaitingForMapReadyLayerListRaw(getState()).forEach(layerDescriptor => { + dispatch(addLayer(layerDescriptor)); + }); + + dispatch({ + type: CLEAR_WAITING_FOR_MAP_READY_LAYER_LIST, + }); + }; +} + +export function mapDestroyed() { + return { + type: MAP_DESTROYED, + }; +} + +export function mapExtentChanged(newMapConstants) { + return async (dispatch, getState) => { + const state = getState(); + const dataFilters = getDataFilters(state); + const { extent, zoom: newZoom } = newMapConstants; + const { buffer, zoom: currentZoom } = dataFilters; + + if (extent) { + let doesBufferContainExtent = false; + if (buffer) { + const bufferGeometry = turf.bboxPolygon([ + buffer.minLon, + buffer.minLat, + buffer.maxLon, + buffer.maxLat, + ]); + const extentGeometry = turf.bboxPolygon([ + extent.minLon, + extent.minLat, + extent.maxLon, + extent.maxLat, + ]); + + doesBufferContainExtent = turfBooleanContains(bufferGeometry, extentGeometry); + } + + if (!doesBufferContainExtent || currentZoom !== newZoom) { + const scaleFactor = 0.5; // TODO put scale factor in store and fetch with selector + const width = extent.maxLon - extent.minLon; + const height = extent.maxLat - extent.minLat; + dataFilters.buffer = { + minLon: extent.minLon - width * scaleFactor, + minLat: extent.minLat - height * scaleFactor, + maxLon: extent.maxLon + width * scaleFactor, + maxLat: extent.maxLat + height * scaleFactor, + }; + } + } + + dispatch({ + type: MAP_EXTENT_CHANGED, + mapState: { + ...dataFilters, + ...newMapConstants, + }, + }); + const newDataFilters = { ...dataFilters, ...newMapConstants }; + await syncDataForAllLayers(dispatch, getState, newDataFilters); + }; +} + +export function closeOnClickTooltip(tooltipId) { + return (dispatch, getState) => { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: getOpenTooltips(getState()).filter(({ id }) => { + return tooltipId !== id; + }), + }); + }; +} + +export function openOnClickTooltip(tooltipState) { + return (dispatch, getState) => { + const openTooltips = getOpenTooltips(getState()).filter(({ features, location, isLocked }) => { + return ( + isLocked && + !_.isEqual(location, tooltipState.location) && + !_.isEqual(features, tooltipState.features) + ); + }); + + openTooltips.push({ + ...tooltipState, + isLocked: true, + id: uuid(), + }); + + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips, + }); + }; +} + +export function closeOnHoverTooltip() { + return (dispatch, getState) => { + if (getOpenTooltips(getState()).length) { + dispatch({ + type: SET_OPEN_TOOLTIPS, + openTooltips: [], + }); + } + }; +} + +export function openOnHoverTooltip(tooltipState) { + return { + type: SET_OPEN_TOOLTIPS, + openTooltips: [ + { + ...tooltipState, + isLocked: false, + id: uuid(), + }, + ], + }; +} + +export function setMouseCoordinates({ lat, lon }) { + let safeLon = lon; + if (lon > 180) { + const overlapWestOfDateLine = lon - 180; + safeLon = -180 + overlapWestOfDateLine; + } else if (lon < -180) { + const overlapEastOfDateLine = Math.abs(lon) - 180; + safeLon = 180 - overlapEastOfDateLine; + } + + return { + type: SET_MOUSE_COORDINATES, + lat, + lon: safeLon, + }; +} + +export function clearMouseCoordinates() { + return { type: CLEAR_MOUSE_COORDINATES }; +} + +export function disableScrollZoom() { + return { type: SET_SCROLL_ZOOM, scrollZoom: false }; +} + +export function fitToLayerExtent(layerId) { + return async function(dispatch, getState) { + const targetLayer = getLayerById(layerId, getState()); + + if (targetLayer) { + const dataFilters = getDataFilters(getState()); + const bounds = await targetLayer.getBounds(dataFilters); + if (bounds) { + await dispatch(setGotoWithBounds(bounds)); + } + } + }; +} + +export function setGotoWithBounds(bounds) { + return { + type: SET_GOTO, + bounds: bounds, + }; +} + +export function setGotoWithCenter({ lat, lon, zoom }) { + return { + type: SET_GOTO, + center: { lat, lon, zoom }, + }; +} + +export function clearGoto() { + return { type: CLEAR_GOTO }; +} + +export function startDataLoad(layerId, dataId, requestToken, meta = {}) { + return (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (layer) { + dispatch(cancelRequest(layer.getPrevRequestToken(dataId))); + } + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoad) { + eventHandlers.onDataLoad({ + layerId, + dataId, + }); + } + + dispatch({ + meta, + type: LAYER_DATA_LOAD_STARTED, + layerId, + dataId, + requestToken, + }); + }; +} + +export function updateSourceDataRequest(layerId, newData) { + return dispatch => { + dispatch({ + type: UPDATE_SOURCE_DATA_REQUEST, + dataId: SOURCE_DATA_ID_ORIGIN, + layerId, + newData, + }); + + dispatch(updateStyleMeta(layerId)); + }; +} + +export function endDataLoad(layerId, dataId, requestToken, data, meta) { + return async (dispatch, getState) => { + dispatch(unregisterCancelCallback(requestToken)); + + const features = data && data.features ? data.features : []; + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadEnd) { + const layer = getLayerById(layerId, getState()); + const resultMeta = {}; + if (layer && layer.getType() === LAYER_TYPE.VECTOR) { + resultMeta.featuresCount = features.length; + } + + eventHandlers.onDataLoadEnd({ + layerId, + dataId, + resultMeta, + }); + } + + dispatch(cleanTooltipStateForLayer(layerId, features)); + dispatch({ + type: LAYER_DATA_LOAD_ENDED, + layerId, + dataId, + data, + meta, + requestToken, + }); + + //Clear any data-load errors when there is a succesful data return. + //Co this on end-data-load iso at start-data-load to avoid blipping the error status between true/false. + //This avoids jitter in the warning icon of the TOC when the requests continues to return errors. + dispatch(setLayerDataLoadErrorStatus(layerId, null)); + + dispatch(updateStyleMeta(layerId)); + }; +} + +export function onDataLoadError(layerId, dataId, requestToken, errorMessage) { + return async (dispatch, getState) => { + dispatch(unregisterCancelCallback(requestToken)); + + const eventHandlers = getEventHandlers(getState()); + if (eventHandlers && eventHandlers.onDataLoadError) { + eventHandlers.onDataLoadError({ + layerId, + dataId, + errorMessage, + }); + } + + dispatch(cleanTooltipStateForLayer(layerId)); + dispatch({ + type: LAYER_DATA_LOAD_ERROR, + data: null, + layerId, + dataId, + requestToken, + }); + + dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); + }; +} + +export function updateSourceProp(layerId, propName, value, newLayerType) { + return async dispatch => { + dispatch({ + type: UPDATE_SOURCE_PROP, + layerId, + propName, + value, + }); + if (newLayerType) { + dispatch(updateLayerType(layerId, newLayerType)); + } + await dispatch(clearMissingStyleProperties(layerId)); + dispatch(syncDataForLayer(layerId)); + }; +} + +function updateLayerType(layerId, newLayerType) { + return (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer || layer.getType() === newLayerType) { + return; + } + dispatch(clearDataRequests(layer)); + dispatch({ + type: UPDATE_LAYER_PROP, + id: layerId, + propName: 'type', + newValue: newLayerType, + }); + }; +} + +export function syncDataForLayer(layerId) { + return async (dispatch, getState) => { + const targetLayer = getLayerById(layerId, getState()); + if (targetLayer) { + const dataFilters = getDataFilters(getState()); + const loadingFunctions = getLayerLoadingCallbacks(dispatch, getState, layerId); + await targetLayer.syncData({ + ...loadingFunctions, + dataFilters, + }); + } + }; +} + +export function updateLayerLabel(id, newLabel) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'label', + newValue: newLabel, + }; +} + +export function updateLayerMinZoom(id, minZoom) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'minZoom', + newValue: minZoom, + }; +} + +export function updateLayerMaxZoom(id, maxZoom) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'maxZoom', + newValue: maxZoom, + }; +} + +export function updateLayerAlpha(id, alpha) { + return { + type: UPDATE_LAYER_PROP, + id, + propName: 'alpha', + newValue: alpha, + }; +} + +export function setLayerQuery(id, query) { + return dispatch => { + dispatch({ + type: UPDATE_LAYER_PROP, + id, + propName: 'query', + newValue: query, + }); + + dispatch(syncDataForLayer(id)); + }; +} + +export function removeSelectedLayer() { + return (dispatch, getState) => { + const state = getState(); + const layerId = getSelectedLayerId(state); + dispatch(removeLayer(layerId)); + }; +} + +export function removeLayer(layerId) { + return async (dispatch, getState) => { + const state = getState(); + const selectedLayerId = getSelectedLayerId(state); + if (layerId === selectedLayerId) { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + await dispatch(setSelectedLayer(null)); + } + dispatch(removeLayerFromLayerList(layerId)); + }; +} + +function removeLayerFromLayerList(layerId) { + return (dispatch, getState) => { + const layerGettingRemoved = getLayerById(layerId, getState()); + if (!layerGettingRemoved) { + return; + } + + layerGettingRemoved.getInFlightRequestTokens().forEach(requestToken => { + dispatch(cancelRequest(requestToken)); + }); + dispatch(cleanTooltipStateForLayer(layerId)); + layerGettingRemoved.destroy(); + dispatch({ + type: REMOVE_LAYER, + id: layerId, + }); + }; +} + +export function setQuery({ query, timeFilters, filters = [], refresh = false }) { + function generateQueryTimestamp() { + return new Date().toISOString(); + } + return async (dispatch, getState) => { + const prevQuery = getQuery(getState()); + const prevTriggeredAt = + prevQuery && prevQuery.queryLastTriggeredAt + ? prevQuery.queryLastTriggeredAt + : generateQueryTimestamp(); + + dispatch({ + type: SET_QUERY, + timeFilters, + query: { + ...query, + // ensure query changes to trigger re-fetch when "Refresh" clicked + queryLastTriggeredAt: refresh ? generateQueryTimestamp() : prevTriggeredAt, + }, + filters, + }); + + const dataFilters = getDataFilters(getState()); + await syncDataForAllLayers(dispatch, getState, dataFilters); + }; +} + +export function setRefreshConfig({ isPaused, interval }) { + return { + type: SET_REFRESH_CONFIG, + isPaused, + interval, + }; +} + +export function triggerRefreshTimer() { + return async (dispatch, getState) => { + dispatch({ + type: TRIGGER_REFRESH_TIMER, + }); + + const dataFilters = getDataFilters(getState()); + await syncDataForAllLayers(dispatch, getState, dataFilters); + }; +} + +export function clearMissingStyleProperties(layerId) { + return async (dispatch, getState) => { + const targetLayer = getLayerById(layerId, getState()); + if (!targetLayer) { + return; + } + + const style = targetLayer.getCurrentStyle(); + if (!style) { + return; + } + + const nextFields = await targetLayer.getFields(); //take into account all fields, since labels can be driven by any field (source or join) + const { hasChanges, nextStyleDescriptor } = style.getDescriptorWithMissingStylePropsRemoved( + nextFields + ); + if (hasChanges) { + dispatch(updateLayerStyle(layerId, nextStyleDescriptor)); + } + }; +} + +export function updateLayerStyle(layerId, styleDescriptor) { + return dispatch => { + dispatch({ + type: UPDATE_LAYER_STYLE, + layerId, + style: { + ...styleDescriptor, + }, + }); + + // Ensure updateStyleMeta is triggered + // syncDataForLayer may not trigger endDataLoad if no re-fetch is required + dispatch(updateStyleMeta(layerId)); + + // Style update may require re-fetch, for example ES search may need to retrieve field used for dynamic styling + dispatch(syncDataForLayer(layerId)); + }; +} + +export function updateStyleMeta(layerId) { + return async (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (!layer) { + return; + } + const sourceDataRequest = layer.getSourceDataRequest(); + const style = layer.getCurrentStyle(); + if (!style || !sourceDataRequest) { + return; + } + const styleMeta = await style.pluckStyleMetaFromSourceDataRequest(sourceDataRequest); + dispatch({ + type: SET_LAYER_STYLE_META, + layerId, + styleMeta, + }); + }; +} + +export function updateLayerStyleForSelectedLayer(styleDescriptor) { + return (dispatch, getState) => { + const selectedLayerId = getSelectedLayerId(getState()); + if (!selectedLayerId) { + return; + } + dispatch(updateLayerStyle(selectedLayerId, styleDescriptor)); + }; +} + +export function setJoinsForLayer(layer, joins) { + return async dispatch => { + await dispatch({ + type: SET_JOINS, + layer: layer, + joins: joins, + }); + + await dispatch(clearMissingStyleProperties(layer.getId())); + dispatch(syncDataForLayer(layer.getId())); + }; +} + +export function updateDrawState(drawState) { + return dispatch => { + if (drawState !== null) { + dispatch({ type: SET_OPEN_TOOLTIPS, openTooltips: [] }); // tooltips just get in the way + } + dispatch({ + type: UPDATE_DRAW_STATE, + drawState: drawState, + }); + }; +} + +export function disableInteractive() { + return { type: SET_INTERACTIVE, disableInteractive: true }; +} + +export function disableTooltipControl() { + return { type: DISABLE_TOOLTIP_CONTROL, disableTooltipControl: true }; +} + +export function hideToolbarOverlay() { + return { type: HIDE_TOOLBAR_OVERLAY, hideToolbarOverlay: true }; +} + +export function hideLayerControl() { + return { type: HIDE_LAYER_CONTROL, hideLayerControl: true }; +} +export function hideViewControl() { + return { type: HIDE_VIEW_CONTROL, hideViewControl: true }; +} + +export function setHiddenLayers(hiddenLayerIds) { + return (dispatch, getState) => { + const isMapReady = getMapReady(getState()); + + if (!isMapReady) { + dispatch({ type: SET_WAITING_FOR_READY_HIDDEN_LAYERS, hiddenLayerIds }); + } else { + getLayerListRaw(getState()).forEach(layer => + dispatch(setLayerVisibility(layer.id, !hiddenLayerIds.includes(layer.id))) + ); + } + }; +} diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js similarity index 98% rename from x-pack/legacy/plugins/maps/public/actions/map_actions.test.js rename to x-pack/plugins/maps/public/actions/map_actions.test.js index 7e2a3c827fa88..c280b8af7ab80 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -5,7 +5,7 @@ */ jest.mock('../selectors/map_selectors', () => ({})); -jest.mock('../../../../../plugins/maps/public/kibana_services', () => ({})); +jest.mock('../kibana_services', () => ({})); import { mapExtentChanged, setMouseCoordinates } from './map_actions'; diff --git a/x-pack/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/plugins/maps/public/actions/ui_actions.d.ts new file mode 100644 index 0000000000000..43cdcff7d2d69 --- /dev/null +++ b/x-pack/plugins/maps/public/actions/ui_actions.d.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnyAction } from 'redux'; +import { FLYOUT_STATE } from '../reducers/ui'; + +export const UPDATE_FLYOUT: string; +export const CLOSE_SET_VIEW: string; +export const OPEN_SET_VIEW: string; +export const SET_IS_LAYER_TOC_OPEN: string; +export const SET_FULL_SCREEN: string; +export const SET_READ_ONLY: string; +export const SET_OPEN_TOC_DETAILS: string; +export const SHOW_TOC_DETAILS: string; +export const HIDE_TOC_DETAILS: string; +export const UPDATE_INDEXING_STAGE: string; + +export function updateFlyout(display: FLYOUT_STATE): AnyAction; + +export function setOpenTOCDetails(layerIds?: string[]): AnyAction; + +export function setIsLayerTOCOpen(open: boolean): AnyAction; + +export function setReadOnly(readOnly: boolean): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/ui_actions.js b/x-pack/plugins/maps/public/actions/ui_actions.js index 59ae56c15056a..e2a36e33e7db0 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.js +++ b/x-pack/plugins/maps/public/actions/ui_actions.js @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getFlyoutDisplay } from '../selectors/ui_selectors'; +import { FLYOUT_STATE } from '../reducers/ui'; +import { setSelectedLayer, trackMapSettings } from './map_actions'; + export const UPDATE_FLYOUT = 'UPDATE_FLYOUT'; export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW'; export const OPEN_SET_VIEW = 'OPEN_SET_VIEW'; @@ -14,3 +18,84 @@ export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; export const UPDATE_INDEXING_STAGE = 'UPDATE_INDEXING_STAGE'; + +export function exitFullScreen() { + return { + type: SET_FULL_SCREEN, + isFullScreen: false, + }; +} + +export function updateFlyout(display) { + return { + type: UPDATE_FLYOUT, + display, + }; +} +export function openMapSettings() { + return (dispatch, getState) => { + const flyoutDisplay = getFlyoutDisplay(getState()); + if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { + return; + } + dispatch(setSelectedLayer(null)); + dispatch(trackMapSettings()); + dispatch(updateFlyout(FLYOUT_STATE.MAP_SETTINGS_PANEL)); + }; +} +export function closeSetView() { + return { + type: CLOSE_SET_VIEW, + }; +} +export function openSetView() { + return { + type: OPEN_SET_VIEW, + }; +} +export function setIsLayerTOCOpen(isLayerTOCOpen) { + return { + type: SET_IS_LAYER_TOC_OPEN, + isLayerTOCOpen, + }; +} +export function enableFullScreen() { + return { + type: SET_FULL_SCREEN, + isFullScreen: true, + }; +} +export function setReadOnly(isReadOnly) { + return { + type: SET_READ_ONLY, + isReadOnly, + }; +} + +export function setOpenTOCDetails(layerIds) { + return { + type: SET_OPEN_TOC_DETAILS, + layerIds, + }; +} + +export function showTOCDetails(layerId) { + return { + type: SHOW_TOC_DETAILS, + layerId, + }; +} + +export function hideTOCDetails(layerId) { + return { + type: HIDE_TOC_DETAILS, + layerId, + }; +} + +export function updateIndexingStage(stage) { + return { + type: UPDATE_INDEXING_STAGE, + stage, + }; +} diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.d.ts b/x-pack/plugins/maps/public/angular/get_initial_layers.d.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_layers.d.ts rename to x-pack/plugins/maps/public/angular/get_initial_layers.d.ts diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.js b/x-pack/plugins/maps/public/angular/get_initial_layers.js new file mode 100644 index 0000000000000..f02ded1704533 --- /dev/null +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.js @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +// Import each layer type, even those not used, to init in registry +import '../layers/sources/wms_source'; +import '../layers/sources/ems_file_source'; +import '../layers/sources/es_search_source'; +import '../layers/sources/es_pew_pew_source'; +import '../layers/sources/kibana_regionmap_source'; +import '../layers/sources/es_geo_grid_source'; +import '../layers/sources/xyz_tms_source'; +import { KibanaTilemapSource } from '../layers/sources/kibana_tilemap_source'; +import { TileLayer } from '../layers/tile_layer'; +import { EMSTMSSource } from '../layers/sources/ems_tms_source'; +import { VectorTileLayer } from '../layers/vector_tile_layer'; +import { getInjectedVarFunc } from '../kibana_services'; +import { getKibanaTileMap } from '../meta'; + +export function getInitialLayers(layerListJSON, initialLayers = []) { + if (layerListJSON) { + return JSON.parse(layerListJSON); + } + + const tilemapSourceFromKibana = getKibanaTileMap(); + if (_.get(tilemapSourceFromKibana, 'url')) { + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: KibanaTilemapSource.createDescriptor(), + }); + return [layerDescriptor, ...initialLayers]; + } + + const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); + if (isEmsEnabled) { + const layerDescriptor = VectorTileLayer.createDescriptor({ + sourceDescriptor: EMSTMSSource.createDescriptor({ isAutoSelect: true }), + }); + return [layerDescriptor, ...initialLayers]; + } + + return initialLayers; +} diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js similarity index 79% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js rename to x-pack/plugins/maps/public/angular/get_initial_layers.test.js index 8c9185a16ea0e..4b5cad8d19260 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_layers.test.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../../plugins/maps/public/meta', () => { +jest.mock('../meta', () => { return {}; }); -jest.mock('../../../../../plugins/maps/public/kibana_services'); +jest.mock('../kibana_services'); import { getInitialLayers } from './get_initial_layers'; @@ -15,8 +15,7 @@ const layerListNotProvided = undefined; describe('Saved object has layer list', () => { beforeEach(() => { - require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => - jest.fn(); + require('../kibana_services').getInjectedVarFunc = () => jest.fn(); }); it('Should get initial layers from saved object', () => { @@ -33,7 +32,7 @@ describe('Saved object has layer list', () => { describe('kibana.yml configured with map.tilemap.url', () => { beforeAll(() => { - require('../../../../../plugins/maps/public/meta').getKibanaTileMap = () => { + require('../meta').getKibanaTileMap = () => { return { url: 'myTileUrl', }; @@ -53,7 +52,7 @@ describe('kibana.yml configured with map.tilemap.url', () => { sourceDescriptor: { type: 'KIBANA_TILEMAP', }, - style: {}, + style: { type: 'TILE' }, type: 'TILE', visible: true, }, @@ -63,10 +62,10 @@ describe('kibana.yml configured with map.tilemap.url', () => { describe('EMS is enabled', () => { beforeAll(() => { - require('../../../../../plugins/maps/public/meta').getKibanaTileMap = () => { + require('../meta').getKibanaTileMap = () => { return null; }; - require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => key => { + require('../kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'emsTileLayerId': return { @@ -97,7 +96,7 @@ describe('EMS is enabled', () => { isAutoSelect: true, type: 'EMS_TMS', }, - style: {}, + style: { type: 'TILE' }, type: 'VECTOR_TILE', visible: true, }, @@ -107,11 +106,11 @@ describe('EMS is enabled', () => { describe('EMS is not enabled', () => { beforeAll(() => { - require('../../../../../plugins/maps/public/meta').getKibanaTileMap = () => { + require('../meta').getKibanaTileMap = () => { return null; }; - require('../../../../../plugins/maps/public/kibana_services').getInjectedVarFunc = () => key => { + require('../kibana_services').getInjectedVarFunc = () => key => { switch (key) { case 'isEmsEnabled': return false; diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js b/x-pack/plugins/maps/public/angular/get_initial_query.js similarity index 82% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_query.js rename to x-pack/plugins/maps/public/angular/get_initial_query.js index c50ecb2b05dc0..4f61142413671 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_query.js +++ b/x-pack/plugins/maps/public/angular/get_initial_query.js @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; +import { getUiSettings } from '../kibana_services'; export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage }) { const settings = getUiSettings(); diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js similarity index 84% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js rename to x-pack/plugins/maps/public/angular/get_initial_refresh_config.js index 8735d45debfc4..f13e435cd1d5c 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_refresh_config.js +++ b/x-pack/plugins/maps/public/angular/get_initial_refresh_config.js @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; + +import { getUiSettings } from '../kibana_services'; export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { const uiSettings = getUiSettings(); diff --git a/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js b/x-pack/plugins/maps/public/angular/get_initial_time_filters.js similarity index 80% rename from x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js rename to x-pack/plugins/maps/public/angular/get_initial_time_filters.js index 74fbf603e99f5..75d9f0e95ccf0 100644 --- a/x-pack/legacy/plugins/maps/public/angular/get_initial_time_filters.js +++ b/x-pack/plugins/maps/public/angular/get_initial_time_filters.js @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiSettings } from '../../../../../plugins/maps/public/kibana_services'; + +import { getUiSettings } from '../kibana_services'; export function getInitialTimeFilters({ mapStateJSON, globalState = {} }) { if (mapStateJSON) { diff --git a/x-pack/legacy/plugins/maps/public/angular/listing_ng_wrapper.html b/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html similarity index 100% rename from x-pack/legacy/plugins/maps/public/angular/listing_ng_wrapper.html rename to x-pack/plugins/maps/public/angular/listing_ng_wrapper.html diff --git a/x-pack/legacy/plugins/maps/public/angular/map.html b/x-pack/plugins/maps/public/angular/map.html similarity index 100% rename from x-pack/legacy/plugins/maps/public/angular/map.html rename to x-pack/plugins/maps/public/angular/map.html diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js similarity index 79% rename from x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js rename to x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index 710997a9c0d7f..2dcec35960b08 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -6,15 +6,14 @@ import _ from 'lodash'; import { createSavedGisMapClass } from './saved_gis_map'; -import { SavedObjectLoader } from '../../../../../../../src/plugins/saved_objects/public'; +import { SavedObjectLoader } from '../../../../../../src/plugins/saved_objects/public'; import { getCoreChrome, getSavedObjectsClient, getIndexPatternService, getCoreOverlays, getData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/maps/public/kibana_services'; +} from '../../kibana_services'; export const getMapsSavedObjectLoader = _.once(function() { const services = { diff --git a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js similarity index 88% rename from x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js rename to x-pack/plugins/maps/public/angular/services/saved_gis_map.js index 990a0613da681..1a58b0cefaed9 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { createSavedObjectClass } from '../../../../../../../src/plugins/saved_objects/public'; +import { createSavedObjectClass } from '../../../../../../src/plugins/saved_objects/public'; import { getTimeFilters, getMapZoom, @@ -15,12 +15,13 @@ import { getRefreshConfig, getQuery, getFilters, + getMapSettings, } from '../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { convertMapExtentToPolygon } from '../../../../../../plugins/maps/public/elasticsearch_geo_utils'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../../../../../../plugins/maps/public/reducers/util'; + +import { convertMapExtentToPolygon } from '../../elasticsearch_geo_utils'; + +import { copyPersistentState } from '../../reducers/util'; import { extractReferences, injectReferences } from '../../../common/migrations/references'; import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; @@ -98,6 +99,7 @@ export function createSavedGisMapClass(services) { refreshConfig: getRefreshConfig(state), query: _.omit(getQuery(state), 'queryLastTriggeredAt'), filters: getFilters(state), + settings: getMapSettings(state), }); this.uiStateJSON = JSON.stringify({ diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap rename to x-pack/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/components/_geometry_filter.scss b/x-pack/plugins/maps/public/components/_geometry_filter.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/_geometry_filter.scss rename to x-pack/plugins/maps/public/components/_geometry_filter.scss diff --git a/x-pack/plugins/maps/public/components/_index.scss b/x-pack/plugins/maps/public/components/_index.scss new file mode 100644 index 0000000000000..161b3fefdb8f9 --- /dev/null +++ b/x-pack/plugins/maps/public/components/_index.scss @@ -0,0 +1,3 @@ +@import 'metric_editors'; +@import './geometry_filter'; +@import 'tooltip_selector'; diff --git a/x-pack/legacy/plugins/maps/public/components/_metric_editors.scss b/x-pack/plugins/maps/public/components/_metric_editors.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/_metric_editors.scss rename to x-pack/plugins/maps/public/components/_metric_editors.scss diff --git a/x-pack/legacy/plugins/maps/public/components/_tooltip_selector.scss b/x-pack/plugins/maps/public/components/_tooltip_selector.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/_tooltip_selector.scss rename to x-pack/plugins/maps/public/components/_tooltip_selector.scss diff --git a/x-pack/plugins/maps/public/components/alpha_slider.tsx b/x-pack/plugins/maps/public/components/alpha_slider.tsx new file mode 100644 index 0000000000000..921c386292050 --- /dev/null +++ b/x-pack/plugins/maps/public/components/alpha_slider.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { ValidatedRange } from './validated_range'; + +interface Props { + alpha: number; + onChange: (alpha: number) => void; +} + +export function AlphaSlider({ alpha, onChange }: Props) { + const onAlphaChange = (newAlpha: number) => { + onChange(newAlpha / 100); + }; + + return ( + <EuiFormRow + label={i18n.translate('xpack.maps.layerPanel.settingsPanel.layerTransparencyLabel', { + defaultMessage: 'Opacity', + })} + display="columnCompressed" + > + <ValidatedRange + min={0} + max={100} + step={1} + value={Math.round(alpha * 100)} + onChange={onAlphaChange} + showInput + showRange + compressed + append={i18n.translate('xpack.maps.layerPanel.settingsPanel.percentageLabel', { + defaultMessage: '%', + description: 'Percentage', + })} + /> + </EuiFormRow> + ); +} diff --git a/x-pack/legacy/plugins/maps/public/components/distance_filter_form.tsx b/x-pack/plugins/maps/public/components/distance_filter_form.tsx similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/distance_filter_form.tsx rename to x-pack/plugins/maps/public/components/distance_filter_form.tsx diff --git a/x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts b/x-pack/plugins/maps/public/components/geo_field_with_index.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/geo_field_with_index.ts rename to x-pack/plugins/maps/public/components/geo_field_with_index.ts diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js b/x-pack/plugins/maps/public/components/geometry_filter_form.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/geometry_filter_form.js rename to x-pack/plugins/maps/public/components/geometry_filter_form.js diff --git a/x-pack/legacy/plugins/maps/public/components/geometry_filter_form.test.js b/x-pack/plugins/maps/public/components/geometry_filter_form.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/geometry_filter_form.test.js rename to x-pack/plugins/maps/public/components/geometry_filter_form.test.js diff --git a/x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js b/x-pack/plugins/maps/public/components/global_filter_checkbox.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js rename to x-pack/plugins/maps/public/components/global_filter_checkbox.js diff --git a/x-pack/legacy/plugins/maps/public/components/map_listing.js b/x-pack/plugins/maps/public/components/map_listing.js similarity index 98% rename from x-pack/legacy/plugins/maps/public/components/map_listing.js rename to x-pack/plugins/maps/public/components/map_listing.js index ef1d524cb91dd..ee10fe30130f3 100644 --- a/x-pack/legacy/plugins/maps/public/components/map_listing.js +++ b/x-pack/plugins/maps/public/components/map_listing.js @@ -7,8 +7,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getToasts } from '../../../../../plugins/maps/public/kibana_services'; + +import { getToasts } from '../kibana_services'; import { EuiTitle, EuiFieldSearch, diff --git a/x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx b/x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx similarity index 100% rename from x-pack/legacy/plugins/maps/public/components/multi_index_geo_field_select.tsx rename to x-pack/plugins/maps/public/components/multi_index_geo_field_select.tsx diff --git a/x-pack/plugins/maps/public/connected_components/_index.scss b/x-pack/plugins/maps/public/connected_components/_index.scss new file mode 100644 index 0000000000000..83042ae1d586c --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/_index.scss @@ -0,0 +1,6 @@ +@import 'gis_map/gis_map'; +@import 'layer_addpanel/source_select/index'; +@import 'layer_panel/index'; +@import 'widget_overlay/index'; +@import 'toolbar_overlay/index'; +@import 'map/features_tooltip/index'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/_gis_map.scss b/x-pack/plugins/maps/public/connected_components/gis_map/_gis_map.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/gis_map/_gis_map.scss rename to x-pack/plugins/maps/public/connected_components/gis_map/_gis_map.scss diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts b/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts new file mode 100644 index 0000000000000..92d92dfbd142d --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts @@ -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 React from 'react'; +import { Filter } from 'src/plugins/data/public'; + +import { RenderToolTipContent } from '../../layers/tooltips/tooltip_property'; + +export const GisMap: React.ComponentType<{ + addFilters: ((filters: Filter[]) => void) | null; + renderTooltipContent?: RenderToolTipContent; +}>; diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/plugins/maps/public/connected_components/gis_map/index.js new file mode 100644 index 0000000000000..f8769d0bb898a --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.js @@ -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 { connect } from 'react-redux'; +import { GisMap } from './view'; +import { exitFullScreen } from '../../actions/ui_actions'; +import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; +import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions'; +import { + areLayersLoaded, + getRefreshConfig, + getMapInitError, + getQueryableUniqueIndexPatternIds, + isToolbarOverlayHidden, +} from '../../selectors/map_selectors'; + +import { getCoreChrome } from '../../kibana_services'; + +function mapStateToProps(state = {}) { + return { + areLayersLoaded: areLayersLoaded(state), + flyoutDisplay: getFlyoutDisplay(state), + isFullScreen: getIsFullScreen(state), + refreshConfig: getRefreshConfig(state), + mapInitError: getMapInitError(state), + indexPatternIds: getQueryableUniqueIndexPatternIds(state), + hideToolbarOverlay: isToolbarOverlayHidden(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), + exitFullScreen: () => { + dispatch(exitFullScreen()); + getCoreChrome().setIsVisible(true); + }, + cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), + }; +} + +const connectedGisMap = connect(mapStateToProps, mapDispatchToProps)(GisMap); +export { connectedGisMap as GisMap }; diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js new file mode 100644 index 0000000000000..6eb173a001d01 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js @@ -0,0 +1,224 @@ +/* + * 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 _ from 'lodash'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { MBMapContainer } from '../map/mb'; +import { WidgetOverlay } from '../widget_overlay'; +import { ToolbarOverlay } from '../toolbar_overlay'; +import { LayerPanel } from '../layer_panel'; +import { AddLayerPanel } from '../layer_addpanel'; +import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; +import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; + +import { getIndexPatternsFromIds } from '../../index_pattern_util'; +import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; +import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public'; +import { i18n } from '@kbn/i18n'; +import uuid from 'uuid/v4'; +import { FLYOUT_STATE } from '../../reducers/ui'; +import { MapSettingsPanel } from '../map_settings_panel'; + +const RENDER_COMPLETE_EVENT = 'renderComplete'; + +export class GisMap extends Component { + state = { + isInitialLoadRenderTimeoutComplete: false, + domId: uuid(), + geoFields: [], + }; + + componentDidMount() { + this._isMounted = true; + this._isInitalLoadRenderTimerStarted = false; + this._setRefreshTimer(); + } + + componentDidUpdate() { + this._setRefreshTimer(); + if (this.props.areLayersLoaded && !this._isInitalLoadRenderTimerStarted) { + this._isInitalLoadRenderTimerStarted = true; + this._startInitialLoadRenderTimer(); + } + + if (!!this.props.addFilters) { + this._loadGeoFields(this.props.indexPatternIds); + } + } + + componentWillUnmount() { + this._isMounted = false; + this._clearRefreshTimer(); + this.props.cancelAllInFlightRequests(); + } + + // Reporting uses both a `data-render-complete` attribute and a DOM event listener to determine + // if a visualization is done loading. The process roughly is: + // - See if the `data-render-complete` attribute is "true". If so we're done! + // - If it's not, then reporting injects a listener into the browser for a custom "renderComplete" event. + // - When that event is fired, we snapshot the viz and move on. + // Failure to not have the dom attribute, or custom event, will timeout the job. + // See x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/wait_for_render.ts for more. + _onInitialLoadRenderComplete = () => { + const el = document.querySelector(`[data-dom-id="${this.state.domId}"]`); + + if (el) { + el.dispatchEvent(new CustomEvent(RENDER_COMPLETE_EVENT, { bubbles: true })); + } + }; + + _loadGeoFields = async nextIndexPatternIds => { + if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { + // all ready loaded index pattern ids + return; + } + + this._prevIndexPatternIds = nextIndexPatternIds; + + const geoFields = []; + try { + const indexPatterns = await getIndexPatternsFromIds(nextIndexPatternIds); + indexPatterns.forEach(indexPattern => { + indexPattern.fields.forEach(field => { + if ( + !indexPatternsUtils.isNestedField(field) && + (field.type === ES_GEO_FIELD_TYPE.GEO_POINT || + field.type === ES_GEO_FIELD_TYPE.GEO_SHAPE) + ) { + geoFields.push({ + geoFieldName: field.name, + geoFieldType: field.type, + indexPatternTitle: indexPattern.title, + indexPatternId: indexPattern.id, + }); + } + }); + }); + } catch (e) { + // swallow errors. + // the Layer-TOC will indicate which layers are disfunctional on a per-layer basis + } + + if (!this._isMounted) { + return; + } + + this.setState({ geoFields }); + }; + + _setRefreshTimer = () => { + const { isPaused, interval } = this.props.refreshConfig; + + if (this.isPaused === isPaused && this.interval === interval) { + // refreshConfig is the same, nothing to do + return; + } + + this.isPaused = isPaused; + this.interval = interval; + + this._clearRefreshTimer(); + + if (!isPaused && interval > 0) { + this.refreshTimerId = setInterval(() => { + this.props.triggerRefreshTimer(); + }, interval); + } + }; + + _clearRefreshTimer = () => { + if (this.refreshTimerId) { + clearInterval(this.refreshTimerId); + } + }; + + // Mapbox does not provide any feedback when rendering is complete. + // Temporary solution is just to wait set period of time after data has loaded. + _startInitialLoadRenderTimer = () => { + setTimeout(() => { + if (this._isMounted) { + this.setState({ isInitialLoadRenderTimeoutComplete: true }); + this._onInitialLoadRenderComplete(); + } + }, 5000); + }; + + render() { + const { + addFilters, + flyoutDisplay, + isFullScreen, + exitFullScreen, + mapInitError, + renderTooltipContent, + } = this.props; + + const { domId } = this.state; + + if (mapInitError) { + return ( + <div data-render-complete data-shared-item> + <EuiCallOut + title={i18n.translate('xpack.maps.map.initializeErrorTitle', { + defaultMessage: 'Unable to initialize map', + })} + color="danger" + iconType="cross" + > + <p>{mapInitError}</p> + </EuiCallOut> + </div> + ); + } + + let flyoutPanel = null; + if (flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD) { + flyoutPanel = <AddLayerPanel />; + } else if (flyoutDisplay === FLYOUT_STATE.LAYER_PANEL) { + flyoutPanel = <LayerPanel />; + } else if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { + flyoutPanel = <MapSettingsPanel />; + } + + let exitFullScreenButton; + if (isFullScreen) { + exitFullScreenButton = <ExitFullScreenButton onExitFullScreenMode={exitFullScreen} />; + } + return ( + <EuiFlexGroup + gutterSize="none" + responsive={false} + data-dom-id={domId} + data-render-complete={this.state.isInitialLoadRenderTimeoutComplete} + data-shared-item + > + <EuiFlexItem className="mapMapWrapper"> + <MBMapContainer + addFilters={addFilters} + geoFields={this.state.geoFields} + renderTooltipContent={renderTooltipContent} + /> + {!this.props.hideToolbarOverlay && ( + <ToolbarOverlay addFilters={addFilters} geoFields={this.state.geoFields} /> + )} + <WidgetOverlay /> + </EuiFlexItem> + + <EuiFlexItem + className={classNames('mapMapLayerPanel', { + 'mapMapLayerPanel-isVisible': !!flyoutPanel, + })} + grow={false} + > + {flyoutPanel} + </EuiFlexItem> + + {exitFullScreenButton} + </EuiFlexGroup> + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/flyout_footer/view.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js new file mode 100644 index 0000000000000..bff235a7d27fc --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/index.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { ImportEditor } from './view'; + +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; + +import { INDEXING_STAGE } from '../../../reducers/ui'; +import { updateIndexingStage } from '../../../actions/ui_actions'; +import { getIndexingStage } from '../../../selectors/ui_selectors'; + +function mapStateToProps(state = {}) { + return { + inspectorAdapters: getInspectorAdapters(state), + isIndexingTriggered: getIndexingStage(state) === INDEXING_STAGE.TRIGGERED, + }; +} + +const mapDispatchToProps = { + onIndexReady: indexReady => + indexReady ? updateIndexingStage(INDEXING_STAGE.READY) : updateIndexingStage(null), + importSuccessHandler: () => updateIndexingStage(INDEXING_STAGE.SUCCESS), + importErrorHandler: () => updateIndexingStage(INDEXING_STAGE.ERROR), +}; + +const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps)(ImportEditor); +export { connectedFlyOut as ImportEditor }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js new file mode 100644 index 0000000000000..8ebb17ac4fff5 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js @@ -0,0 +1,39 @@ +/* + * 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 { EuiPanel } from '@elastic/eui'; + +import { uploadLayerWizardConfig } from '../../../layers/sources/client_file_source'; + +export const ImportEditor = props => { + const editorProperties = getEditorProperties(props); + return ( + <EuiPanel style={{ position: 'relative' }}> + {uploadLayerWizardConfig.renderWizard(editorProperties)} + </EuiPanel> + ); +}; + +function getEditorProperties({ + previewLayer, + mapColors, + onRemove, + isIndexingTriggered, + onIndexReady, + importSuccessHandler, + importErrorHandler, +}) { + return { + previewLayer, + mapColors, + onRemove, + importSuccessHandler, + importErrorHandler, + isIndexingTriggered, + onIndexReady, + }; +} diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js new file mode 100644 index 0000000000000..a29898f8a2830 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js @@ -0,0 +1,60 @@ +/* + * 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 { connect } from 'react-redux'; +import { AddLayerPanel } from './view'; + +import { FLYOUT_STATE, INDEXING_STAGE } from '../../reducers/ui'; +import { updateFlyout, updateIndexingStage } from '../../actions/ui_actions'; +import { getFlyoutDisplay, getIndexingStage } from '../../selectors/ui_selectors'; +import { getMapColors } from '../../selectors/map_selectors'; + +import { getInspectorAdapters } from '../../reducers/non_serializable_instances'; +import { + setTransientLayer, + addLayer, + setSelectedLayer, + removeTransientLayer, +} from '../../actions/map_actions'; + +function mapStateToProps(state = {}) { + const indexingStage = getIndexingStage(state); + return { + inspectorAdapters: getInspectorAdapters(state), + flyoutVisible: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + mapColors: getMapColors(state), + isIndexingTriggered: indexingStage === INDEXING_STAGE.TRIGGERED, + isIndexingSuccess: indexingStage === INDEXING_STAGE.SUCCESS, + isIndexingReady: indexingStage === INDEXING_STAGE.READY, + }; +} + +function mapDispatchToProps(dispatch) { + return { + previewLayer: async layerDescriptor => { + await dispatch(setSelectedLayer(null)); + await dispatch(removeTransientLayer()); + dispatch(addLayer(layerDescriptor)); + dispatch(setSelectedLayer(layerDescriptor.id)); + dispatch(setTransientLayer(layerDescriptor.id)); + }, + removeTransientLayer: () => { + dispatch(setSelectedLayer(null)); + dispatch(removeTransientLayer()); + }, + selectLayerAndAdd: () => { + dispatch(setTransientLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); + }, + setIndexingTriggered: () => dispatch(updateIndexingStage(INDEXING_STAGE.TRIGGERED)), + resetIndexing: () => dispatch(updateIndexingStage(null)), + }; +} + +const connectedFlyOut = connect(mapStateToProps, mapDispatchToProps, null, { forwardRef: true })( + AddLayerPanel +); +export { connectedFlyOut as AddLayerPanel }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss new file mode 100644 index 0000000000000..8ae6970315e13 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_index.scss @@ -0,0 +1 @@ +@import 'source_select'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss rename to x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/_source_select.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js new file mode 100644 index 0000000000000..82df9237e6ed3 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js @@ -0,0 +1,41 @@ +/* + * 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, { Fragment } from 'react'; + +import { getLayerWizards } from '../../../layers/layer_wizard_registry'; +import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; +import _ from 'lodash'; + +export function SourceSelect({ updateSourceSelection }) { + const sourceCards = getLayerWizards().map(layerWizard => { + const icon = layerWizard.icon ? <EuiIcon type={layerWizard.icon} size="l" /> : null; + + const onClick = () => { + updateSourceSelection({ + layerWizard: layerWizard, + isIndexingSource: !!layerWizard.isIndexingSource, + }); + }; + + return ( + <Fragment key={layerWizard.title}> + <EuiSpacer size="s" /> + <EuiCard + className="mapLayerAddpanel__card" + title={layerWizard.title} + icon={icon} + onClick={onClick} + description={layerWizard.description} + layout="horizontal" + data-test-subj={_.camelCase(layerWizard.title)} + /> + </Fragment> + ); + }); + + return <Fragment>{sourceCards}</Fragment>; +} diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js new file mode 100644 index 0000000000000..127b99d730db5 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js @@ -0,0 +1,176 @@ +/* + * 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, { Component, Fragment } from 'react'; +import { SourceSelect } from './source_select/source_select'; +import { FlyoutFooter } from './flyout_footer'; +import { ImportEditor } from './import_editor'; +import { EuiButtonEmpty, EuiPanel, EuiTitle, EuiFlyoutHeader, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export class AddLayerPanel extends Component { + state = { + layerWizard: null, + layerDescriptor: null, // TODO get this from redux store instead of storing locally + isIndexingSource: false, + importView: false, + layerImportAddReady: false, + }; + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + if (!this.state.layerImportAddReady && this.props.isIndexingSuccess) { + this.setState({ layerImportAddReady: true }); + } + } + + _getPanelDescription() { + const { importView, layerImportAddReady } = this.state; + let panelDescription; + if (layerImportAddReady || !importView) { + panelDescription = i18n.translate('xpack.maps.addLayerPanel.addLayer', { + defaultMessage: 'Add layer', + }); + } else { + panelDescription = i18n.translate('xpack.maps.addLayerPanel.importFile', { + defaultMessage: 'Import file', + }); + } + return panelDescription; + } + + _previewLayer = async (layerDescriptor, isIndexingSource) => { + if (!this._isMounted) { + return; + } + if (!layerDescriptor) { + this.setState({ + layerDescriptor: null, + isIndexingSource: false, + }); + this.props.removeTransientLayer(); + return; + } + + this.setState({ layerDescriptor, isIndexingSource }); + this.props.previewLayer(layerDescriptor); + }; + + _clearLayerData = ({ keepSourceType = false }) => { + if (!this._isMounted) { + return; + } + + this.setState({ + layerDescriptor: null, + isIndexingSource: false, + ...(!keepSourceType ? { layerWizard: null, importView: false } : {}), + }); + this.props.removeTransientLayer(); + }; + + _onSourceSelectionChange = ({ layerWizard, isIndexingSource }) => { + this.setState({ layerWizard, importView: isIndexingSource }); + }; + + _layerAddHandler = () => { + if (this.state.isIndexingSource && !this.props.isIndexingTriggered) { + this.props.setIndexingTriggered(); + } else { + this.props.selectLayerAndAdd(); + if (this.state.importView) { + this.setState({ + layerImportAddReady: false, + }); + this.props.resetIndexing(); + } + } + }; + + _renderPanelBody() { + if (!this.state.layerWizard) { + return <SourceSelect updateSourceSelection={this._onSourceSelectionChange} />; + } + + const backButton = this.props.isIndexingTriggered ? null : ( + <Fragment> + <EuiButtonEmpty size="xs" flush="left" onClick={this._clearLayerData} iconType="arrowLeft"> + <FormattedMessage + id="xpack.maps.addLayerPanel.changeDataSourceButtonLabel" + defaultMessage="Change layer" + /> + </EuiButtonEmpty> + <EuiSpacer size="s" /> + </Fragment> + ); + + if (this.state.importView) { + return ( + <Fragment> + {backButton} + <ImportEditor + clearSource={this._clearLayerData} + previewLayer={this._previewLayer} + mapColors={this.props.mapColors} + onRemove={() => this._clearLayerData({ keepSourceType: true })} + /> + </Fragment> + ); + } + + return ( + <Fragment> + {backButton} + <EuiPanel> + {this.state.layerWizard.renderWizard({ + previewLayer: this._previewLayer, + mapColors: this.props.mapColors, + })} + </EuiPanel> + </Fragment> + ); + } + + render() { + if (!this.props.flyoutVisible) { + return null; + } + + const panelDescription = this._getPanelDescription(); + const isNextBtnEnabled = this.state.importView + ? this.props.isIndexingReady || this.props.isIndexingSuccess + : !!this.state.layerDescriptor; + + return ( + <Fragment> + <EuiFlyoutHeader hasBorder className="mapLayerPanel__header"> + <EuiTitle size="s"> + <h2>{panelDescription}</h2> + </EuiTitle> + </EuiFlyoutHeader> + + <div className="mapLayerPanel__body" data-test-subj="layerAddForm"> + <div className="mapLayerPanel__bodyOverflow">{this._renderPanelBody()}</div> + </div> + + <FlyoutFooter + showNextButton={!!this.state.layerWizard} + disableNextButton={!isNextBtnEnabled} + onClick={this._layerAddHandler} + nextButtonText={panelDescription} + /> + </Fragment> + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss new file mode 100644 index 0000000000000..41b4826a02c67 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss @@ -0,0 +1,4 @@ +@import 'layer_panel'; +@import 'filter_editor/filter_editor'; +@import 'join_editor/resources/join'; +@import 'style_settings/style_settings'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js similarity index 96% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 40fdac38493d4..fba2ec05d0b1d 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -20,12 +20,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { - getIndexPatternService, - getUiSettings, - getData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../plugins/maps/public/kibana_services'; +import { getIndexPatternService, getUiSettings, getData } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; export class FilterEditor extends Component { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js new file mode 100644 index 0000000000000..621ce209eb982 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/index.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { FlyoutFooter } from './view'; + +import { FLYOUT_STATE } from '../../../reducers/ui'; +import { updateFlyout } from '../../../actions/ui_actions'; +import { hasDirtyState } from '../../../selectors/map_selectors'; +import { + setSelectedLayer, + removeSelectedLayer, + removeTrackedLayerStateForSelectedLayer, +} from '../../../actions/map_actions'; + +function mapStateToProps(state = {}) { + return { + hasStateChanged: hasDirtyState(state), + }; +} + +const mapDispatchToProps = dispatch => { + return { + cancelLayerPanel: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(setSelectedLayer(null)); + }, + saveLayerEdits: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + dispatch(removeTrackedLayerStateForSelectedLayer()); + dispatch(setSelectedLayer(null)); + }, + removeLayer: () => { + dispatch(removeSelectedLayer()); + }, + }; +}; + +const connectedFlyoutFooter = connect(mapStateToProps, mapDispatchToProps)(FlyoutFooter); +export { connectedFlyoutFooter as FlyoutFooter }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js similarity index 95% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js index 9c4e1cfdb5467..0d26354e2449b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js @@ -13,9 +13,9 @@ import { MetricsExpression } from './metrics_expression'; import { WhereExpression } from './where_expression'; import { GlobalFilterCheckbox } from '../../../../components/global_filter_checkbox'; -import { indexPatterns } from '../../../../../../../../../src/plugins/data/public'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getIndexPatternService } from '../../../../../../../../plugins/maps/public/kibana_services'; +import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; + +import { getIndexPatternService } from '../../../../kibana_services'; export class Join extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js similarity index 93% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js index 73600c81d221e..12ca2f3c514a0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js @@ -16,16 +16,15 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SingleFieldSelect } from '../../../../../../../../plugins/maps/public/components/single_field_select'; + +import { SingleFieldSelect } from '../../../../components/single_field_select'; import { FormattedMessage } from '@kbn/i18n/react'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getTermsFields } from '../../../../../../../../plugins/maps/public/index_pattern_util'; + +import { getTermsFields } from '../../../../index_pattern_util'; import { getIndexPatternService, getIndexPatternSelectComponent, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/maps/public/kibana_services'; +} from '../../../../kibana_services'; export class JoinExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js similarity index 95% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js index c6a79a398f9af..8c83743ac4c96 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js @@ -14,8 +14,8 @@ import { EuiFormErrorText, EuiFormHelpText, } from '@elastic/eui'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MetricsEditor } from '../../../../../../../../plugins/maps/public/components/metrics_editor'; + +import { MetricsEditor } from '../../../../components/metrics_editor'; import { FormattedMessage } from '@kbn/i18n/react'; import { AGG_TYPE } from '../../../../../common/constants'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js similarity index 92% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js index d8bf862249448..3cd8a3c42879a 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../../../../../plugins/maps/public/components/metric_editor', () => ({ +jest.mock('../../../../components/metric_editor', () => ({ MetricsEditor: () => { return <div>mockMetricsEditor</div>; }, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js similarity index 94% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index 54ec0ac46fa3d..7c9b4f7b7b9a4 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -8,11 +8,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiPopover, EuiExpression, EuiFormHelpText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - getUiSettings, - getData, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/maps/public/kibana_services'; +import { getUiSettings, getData } from '../../../../kibana_services'; export class WhereExpression extends Component { state = { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/view.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js new file mode 100644 index 0000000000000..e2f22c584d3b3 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.js @@ -0,0 +1,41 @@ +/* + * 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 { connect } from 'react-redux'; +import { LayerSettings } from './layer_settings'; +import { getSelectedLayer } from '../../../selectors/map_selectors'; +import { + updateLayerLabel, + updateLayerMaxZoom, + updateLayerMinZoom, + updateLayerAlpha, +} from '../../../actions/map_actions'; +import { MAX_ZOOM } from '../../../../common/constants'; + +function mapStateToProps(state = {}) { + const selectedLayer = getSelectedLayer(state); + return { + minVisibilityZoom: selectedLayer.getMinSourceZoom(), + maxVisibilityZoom: MAX_ZOOM, + alpha: selectedLayer.getAlpha(), + label: selectedLayer.getLabel(), + layerId: selectedLayer.getId(), + maxZoom: selectedLayer.getMaxZoom(), + minZoom: selectedLayer.getMinZoom(), + }; +} + +function mapDispatchToProps(dispatch) { + return { + updateLabel: (id, label) => dispatch(updateLayerLabel(id, label)), + updateMinZoom: (id, minZoom) => dispatch(updateLayerMinZoom(id, minZoom)), + updateMaxZoom: (id, maxZoom) => dispatch(updateLayerMaxZoom(id, maxZoom)), + updateAlpha: (id, alpha) => dispatch(updateLayerAlpha(id, alpha)), + }; +} + +const connectedLayerSettings = connect(mapStateToProps, mapDispatchToProps)(LayerSettings); +export { connectedLayerSettings as LayerSettings }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js new file mode 100644 index 0000000000000..d84d05260f982 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -0,0 +1,87 @@ +/* + * 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, { Fragment } from 'react'; + +import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; + +import { AlphaSlider } from '../../../components/alpha_slider'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; +export function LayerSettings(props) { + const onLabelChange = event => { + const label = event.target.value; + props.updateLabel(props.layerId, label); + }; + + const onZoomChange = ([min, max]) => { + props.updateMinZoom(props.layerId, Math.max(props.minVisibilityZoom, parseInt(min, 10))); + props.updateMaxZoom(props.layerId, Math.min(props.maxVisibilityZoom, parseInt(max, 10))); + }; + + const onAlphaChange = alpha => { + props.updateAlpha(props.layerId, alpha); + }; + + const renderZoomSliders = () => { + return ( + <ValidatedDualRange + label={i18n.translate('xpack.maps.layerPanel.settingsPanel.visibleZoomLabel', { + defaultMessage: 'Visibility', + })} + formRowDisplay="columnCompressed" + min={props.minVisibilityZoom} + max={props.maxVisibilityZoom} + value={[props.minZoom, props.maxZoom]} + showInput="inputWithPopover" + showRange + showLabels + onChange={onZoomChange} + allowEmptyRange={false} + compressed + prepend={i18n.translate('xpack.maps.layerPanel.settingsPanel.visibleZoom', { + defaultMessage: 'Zoom levels', + })} + /> + ); + }; + + const renderLabel = () => { + return ( + <EuiFormRow + label={i18n.translate('xpack.maps.layerPanel.settingsPanel.layerNameLabel', { + defaultMessage: 'Name', + })} + display="columnCompressed" + > + <EuiFieldText value={props.label} onChange={onLabelChange} compressed /> + </EuiFormRow> + ); + }; + + return ( + <Fragment> + <EuiPanel> + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.maps.layerPanel.layerSettingsTitle" + defaultMessage="Layer settings" + /> + </h5> + </EuiTitle> + + <EuiSpacer size="m" /> + {renderLabel()} + {renderZoomSliders()} + <AlphaSlider alpha={props.alpha} onChange={onAlphaChange} /> + </EuiPanel> + + <EuiSpacer size="s" /> + </Fragment> + ); +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss rename to x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/index.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js new file mode 100644 index 0000000000000..f8b7c417e67fd --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.js @@ -0,0 +1,238 @@ +/* + * 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, { Fragment } from 'react'; + +import { FilterEditor } from './filter_editor'; +import { JoinEditor } from './join_editor'; +import { FlyoutFooter } from './flyout_footer'; +import { LayerErrors } from './layer_errors'; +import { LayerSettings } from './layer_settings'; +import { StyleSettings } from './style_settings'; +import { + EuiButtonIcon, + EuiFlexItem, + EuiTitle, + EuiPanel, + EuiFlexGroup, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiSpacer, + EuiAccordion, + EuiText, + EuiLink, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +import { getData, getCore } from '../../kibana_services'; + +const localStorage = new Storage(window.localStorage); + +export class LayerPanel extends React.Component { + state = { + displayName: '', + immutableSourceProps: [], + leftJoinFields: null, + }; + + componentDidMount() { + this._isMounted = true; + this.loadDisplayName(); + this.loadImmutableSourceProperties(); + this.loadLeftJoinFields(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + loadDisplayName = async () => { + if (!this.props.selectedLayer) { + return; + } + + const displayName = await this.props.selectedLayer.getDisplayName(); + if (this._isMounted) { + this.setState({ displayName }); + } + }; + + loadImmutableSourceProperties = async () => { + if (!this.props.selectedLayer) { + return; + } + + const immutableSourceProps = await this.props.selectedLayer.getImmutableSourceProperties(); + if (this._isMounted) { + this.setState({ immutableSourceProps }); + } + }; + + async loadLeftJoinFields() { + if (!this.props.selectedLayer || !this.props.selectedLayer.isJoinable()) { + return; + } + + let leftJoinFields; + try { + const leftFieldsInstances = await this.props.selectedLayer.getLeftJoinFields(); + const leftFieldPromises = leftFieldsInstances.map(async field => { + return { + name: field.getName(), + label: await field.getLabel(), + }; + }); + leftJoinFields = await Promise.all(leftFieldPromises); + } catch (error) { + leftJoinFields = []; + } + if (this._isMounted) { + this.setState({ leftJoinFields }); + } + } + + _onSourceChange = ({ propName, value, newLayerType }) => { + this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); + }; + + _renderFilterSection() { + if (!this.props.selectedLayer.supportsElasticsearchFilters()) { + return null; + } + + return ( + <Fragment> + <EuiPanel> + <FilterEditor /> + </EuiPanel> + <EuiSpacer size="s" /> + </Fragment> + ); + } + + _renderJoinSection() { + if (!this.props.selectedLayer.isJoinable()) { + return null; + } + + return ( + <Fragment> + <EuiPanel> + <JoinEditor + leftJoinFields={this.state.leftJoinFields} + layerDisplayName={this.state.displayName} + /> + </EuiPanel> + <EuiSpacer size="s" /> + </Fragment> + ); + } + + _renderSourceProperties() { + return this.state.immutableSourceProps.map(({ label, value, link }) => { + function renderValue() { + if (link) { + return ( + <EuiLink href={link} target="_blank"> + {value} + </EuiLink> + ); + } + return <span>{value}</span>; + } + return ( + <p key={label} className="mapLayerPanel__sourceDetail"> + <strong>{label}</strong> {renderValue()} + </p> + ); + }); + } + + render() { + const { selectedLayer } = this.props; + + if (!selectedLayer) { + return null; + } + + return ( + <KibanaContextProvider + services={{ + appName: 'maps', + storage: localStorage, + data: getData(), + ...getCore(), + }} + > + <EuiFlexGroup direction="column" gutterSize="none"> + <EuiFlyoutHeader hasBorder className="mapLayerPanel__header"> + <EuiFlexGroup responsive={false} alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButtonIcon + aria-label={i18n.translate('xpack.maps.layerPanel.fitToBoundsAriaLabel', { + defaultMessage: 'Fit to bounds', + })} + iconType={selectedLayer.getLayerTypeIconName()} + onClick={this.props.fitToBounds} + > + <FormattedMessage + id="xpack.maps.layerPanel.fitToBoundsButtonLabel" + defaultMessage="Fit" + /> + </EuiButtonIcon> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size="s"> + <h2>{this.state.displayName}</h2> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="xs" /> + <div className="mapLayerPanel__sourceDetails"> + <EuiAccordion + id="accordion1" + buttonContent={i18n.translate('xpack.maps.layerPanel.sourceDetailsLabel', { + defaultMessage: 'Source details', + })} + > + <EuiText color="subdued" size="s"> + <EuiSpacer size="xs" /> + {this._renderSourceProperties()} + </EuiText> + </EuiAccordion> + </div> + </EuiFlyoutHeader> + + <div className="mapLayerPanel__body"> + <div className="mapLayerPanel__bodyOverflow"> + <LayerErrors /> + + <LayerSettings /> + + {this.props.selectedLayer.renderSourceSettingsEditor({ + onChange: this._onSourceChange, + })} + + {this._renderFilterSection()} + + {this._renderJoinSection()} + + <StyleSettings /> + </div> + </div> + + <EuiFlyoutFooter className="mapLayerPanel__footer"> + <FlyoutFooter /> + </EuiFlyoutFooter> + </EuiFlexGroup> + </KibanaContextProvider> + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js rename to x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/feature_properties.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/__snapshots__/tooltip_header.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/_index.scss b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/_index.scss rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/_index.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js similarity index 92% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index 15824b82965e8..b103fb43af97c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -8,12 +8,12 @@ import React, { Component, Fragment } from 'react'; import { EuiIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createSpatialFilterWithGeometry } from '../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; + +import { createSpatialFilterWithGeometry } from '../../../elasticsearch_geo_utils'; import { GEO_JSON_TYPE } from '../../../../common/constants'; import { GeometryFilterForm } from '../../../components/geometry_filter_form'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { UrlOverflowService } from '../../../../../../../../src/plugins/kibana_legacy/public'; + +import { UrlOverflowService } from '../../../../../../../src/plugins/kibana_legacy/public'; import rison from 'rison-node'; // over estimated and imprecise value to ensure filter has additional room for any meta keys added when filter is mapped. diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_properties.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/features_tooltip.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js rename to x-pack/plugins/maps/public/connected_components/map/features_tooltip/tooltip_header.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_circle.ts diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js similarity index 86% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index cc0e665525036..a69e06458a6a0 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -15,8 +15,7 @@ import { createSpatialFilterWithGeometry, getBoundingBoxGeometry, roundCoordinates, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../../../plugins/maps/public/elasticsearch_geo_utils'; +} from '../../../../elasticsearch_geo_utils'; import { DrawTooltip } from './draw_tooltip'; const mbDrawModes = MapboxDraw.modes; @@ -65,13 +64,28 @@ export class DrawControl extends React.Component { if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { const circle = e.features[0]; - roundCoordinates(circle.properties.center); + const distanceKm = _.round( + circle.properties.radiusKm, + circle.properties.radiusKm > 10 ? 0 : 2 + ); + // Only include as much precision as needed for distance + let precision = 2; + if (distanceKm <= 1) { + precision = 5; + } else if (distanceKm <= 10) { + precision = 4; + } else if (distanceKm <= 100) { + precision = 3; + } const filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel, - distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2), + distanceKm, geoFieldName: this.props.drawState.geoFieldName, indexPatternId: this.props.drawState.indexPatternId, - point: circle.properties.center, + point: [ + _.round(circle.properties.center[0], precision), + _.round(circle.properties.center[1], precision), + ], }); this.props.addFilters([filter]); this.props.disableDrawState(); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_tooltip.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/draw_control/index.js rename to x-pack/plugins/maps/public/connected_components/map/mb/draw_control/index.js diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/index.js new file mode 100644 index 0000000000000..f8daf0804265b --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map/mb/index.js @@ -0,0 +1,79 @@ +/* + * 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 { connect } from 'react-redux'; +import { MBMapContainer } from './view'; +import { + mapExtentChanged, + mapReady, + mapDestroyed, + setMouseCoordinates, + clearMouseCoordinates, + clearGoto, + setMapInitError, +} from '../../../actions/map_actions'; +import { + getLayerList, + getMapReady, + getGoto, + getScrollZoom, + isInteractiveDisabled, + isTooltipControlDisabled, + isViewControlHidden, + getSpatialFiltersLayer, + getMapSettings, +} from '../../../selectors/map_selectors'; + +import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; + +function mapStateToProps(state = {}) { + return { + isMapReady: getMapReady(state), + settings: getMapSettings(state), + layerList: getLayerList(state), + spatialFiltersLayer: getSpatialFiltersLayer(state), + goto: getGoto(state), + inspectorAdapters: getInspectorAdapters(state), + scrollZoom: getScrollZoom(state), + disableInteractive: isInteractiveDisabled(state), + disableTooltipControl: isTooltipControlDisabled(state), + disableTooltipControl: isTooltipControlDisabled(state), + hideViewControl: isViewControlHidden(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + extentChanged: e => { + dispatch(mapExtentChanged(e)); + }, + onMapReady: e => { + dispatch(clearGoto()); + dispatch(mapExtentChanged(e)); + dispatch(mapReady()); + }, + onMapDestroyed: () => { + dispatch(mapDestroyed()); + }, + setMouseCoordinates: ({ lat, lon }) => { + dispatch(setMouseCoordinates({ lat, lon })); + }, + clearMouseCoordinates: () => { + dispatch(clearMouseCoordinates()); + }, + clearGoto: () => { + dispatch(clearGoto()); + }, + setMapInitError(errorMessage) { + dispatch(setMapInitError(errorMessage)); + }, + }; +} + +const connectedMBMapContainer = connect(mapStateToProps, mapDispatchToProps, null, { + forwardRef: true, +})(MBMapContainer); +export { connectedMBMapContainer as MBMapContainer }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js similarity index 82% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js rename to x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js index a8c4f61a00da3..4774cdc556c24 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/mb.utils.test.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js @@ -5,6 +5,7 @@ */ import { removeOrphanedSourcesAndLayers, syncLayerOrderForSingleLayer } from './utils'; +import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; import _ from 'lodash'; class MockMbMap { @@ -121,7 +122,8 @@ function makeMultiSourceMockLayer(layerId) { ); } -describe('mb/utils', () => { +describe('removeOrphanedSourcesAndLayers', () => { + const spatialFilterLayer = makeMultiSourceMockLayer(SPATIAL_FILTERS_LAYER_ID); test('should remove foo and bar layer', async () => { const bazLayer = makeSingleSourceMockLayer('baz'); const fooLayer = makeSingleSourceMockLayer('foo'); @@ -133,7 +135,7 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); @@ -151,7 +153,7 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); @@ -169,13 +171,23 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); expect(removedStyle).toEqual(nextStyle); }); + test('should not remove spatial filter layer and sources when spatialFilterLayer is provided', async () => { + const styleWithSpatialFilters = getMockStyle([spatialFilterLayer]); + const mockMbMap = new MockMbMap(styleWithSpatialFilters); + + removeOrphanedSourcesAndLayers(mockMbMap, [], spatialFilterLayer); + expect(mockMbMap.getStyle()).toEqual(styleWithSpatialFilters); + }); +}); + +describe('syncLayerOrderForSingleLayer', () => { test('should move bar layer in front of foo layer', async () => { const fooLayer = makeSingleSourceMockLayer('foo'); const barLayer = makeSingleSourceMockLayer('bar'); @@ -250,40 +262,4 @@ describe('mb/utils', () => { const nextStyle = getMockStyle(nextLayerListOrder); expect(orderedStyle).toEqual(nextStyle); }); - - test('should reorder foo and bar and remove baz', async () => { - const bazLayer = makeSingleSourceMockLayer('baz'); - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeSingleSourceMockLayer('bar'); - - const currentLayerOrder = [bazLayer, fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); - - test('should reorder foo and bar and remove baz, when having multi-source multi-layer data', async () => { - const bazLayer = makeMultiSourceMockLayer('baz'); - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeMultiSourceMockLayer('bar'); - - const currentLayerOrder = [bazLayer, fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); }); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_control.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/__snapshots__/tooltip_popover.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_control.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js rename to x-pack/plugins/maps/public/connected_components/map/mb/tooltip_control/tooltip_popover.test.js diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js index 15aacfbf1f38d..adf109a087d27 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js @@ -4,8 +4,130 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import { RGBAImage } from './image_utils'; +export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLayer) { + const mbStyle = mbMap.getStyle(); + + const mbLayerIdsToRemove = []; + mbStyle.layers.forEach(mbLayer => { + // ignore mapbox layers from spatial filter layer + if (spatialFilterLayer.ownsMbLayerId(mbLayer.id)) { + return; + } + + const layer = layerList.find(layer => { + return layer.ownsMbLayerId(mbLayer.id); + }); + if (!layer) { + mbLayerIdsToRemove.push(mbLayer.id); + } + }); + mbLayerIdsToRemove.forEach(mbLayerId => mbMap.removeLayer(mbLayerId)); + + const mbSourcesToRemove = []; + for (const mbSourceId in mbStyle.sources) { + if (mbStyle.sources.hasOwnProperty(mbSourceId)) { + // ignore mapbox sources from spatial filter layer + if (spatialFilterLayer.ownsMbSourceId(mbSourceId)) { + return; + } + + const layer = layerList.find(layer => { + return layer.ownsMbSourceId(mbSourceId); + }); + if (!layer) { + mbSourcesToRemove.push(mbSourceId); + } + } + } + mbSourcesToRemove.forEach(mbSourceId => mbMap.removeSource(mbSourceId)); +} + +export function moveLayerToTop(mbMap, layer) { + const mbStyle = mbMap.getStyle(); + + if (!mbStyle.layers || mbStyle.layers.length === 0) { + return; + } + + layer.getMbLayerIds().forEach(mbLayerId => { + const mbLayer = mbMap.getLayer(mbLayerId); + if (mbLayer) { + mbMap.moveLayer(mbLayerId); + } + }); +} + +/** + * This is function assumes only a single layer moved in the layerList, compared to mbMap + * It is optimized to minimize the amount of mbMap.moveLayer calls. + * @param mbMap + * @param layerList + */ +export function syncLayerOrderForSingleLayer(mbMap, layerList) { + if (!layerList || layerList.length === 0) { + return; + } + + const mbLayers = mbMap.getStyle().layers.slice(); + const layerIds = []; + mbLayers.forEach(mbLayer => { + const layer = layerList.find(layer => layer.ownsMbLayerId(mbLayer.id)); + if (layer) { + layerIds.push(layer.getId()); + } + }); + + const currentLayerOrderLayerIds = _.uniq(layerIds); + + const newLayerOrderLayerIdsUnfiltered = layerList.map(l => l.getId()); + const newLayerOrderLayerIds = newLayerOrderLayerIdsUnfiltered.filter(layerId => + currentLayerOrderLayerIds.includes(layerId) + ); + + let netPos = 0; + let netNeg = 0; + const movementArr = currentLayerOrderLayerIds.reduce((accu, id, idx) => { + const movement = newLayerOrderLayerIds.findIndex(newOId => newOId === id) - idx; + movement > 0 ? netPos++ : movement < 0 && netNeg++; + accu.push({ id, movement }); + return accu; + }, []); + if (netPos === 0 && netNeg === 0) { + return; + } + const movedLayerId = + (netPos >= netNeg && movementArr.find(l => l.movement < 0).id) || + (netPos < netNeg && movementArr.find(l => l.movement > 0).id); + const nextLayerIdx = newLayerOrderLayerIds.findIndex(layerId => layerId === movedLayerId) + 1; + + let nextMbLayerId; + if (nextLayerIdx === newLayerOrderLayerIds.length) { + nextMbLayerId = null; + } else { + const foundLayer = mbLayers.find(({ id: mbLayerId }) => { + const layerId = newLayerOrderLayerIds[nextLayerIdx]; + const layer = layerList.find(layer => layer.getId() === layerId); + return layer.ownsMbLayerId(mbLayerId); + }); + nextMbLayerId = foundLayer.id; + } + + const movedLayer = layerList.find(layer => layer.getId() === movedLayerId); + mbLayers.forEach(({ id: mbLayerId }) => { + if (movedLayer.ownsMbLayerId(mbLayerId)) { + mbMap.moveLayer(mbLayerId, nextMbLayerId); + } + }); +} + +export async function addSpritesheetToMap(json, imgUrl, mbMap) { + const imgData = await loadSpriteSheetImageData(imgUrl); + addSpriteSheetToMapFromImageData(json, imgData, mbMap); +} + function getImageData(img) { const canvas = window.document.createElement('canvas'); const context = canvas.getContext('2d'); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js new file mode 100644 index 0000000000000..6bb5a4fed6e52 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -0,0 +1,337 @@ +/* + * 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 _ from 'lodash'; +import React from 'react'; +import { ResizeChecker } from '../../../../../../../src/plugins/kibana_utils/public'; +import { + syncLayerOrderForSingleLayer, + removeOrphanedSourcesAndLayers, + addSpritesheetToMap, + moveLayerToTop, +} from './utils'; +import { getGlyphUrl, isRetina } from '../../../meta'; +import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; +import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; +import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; +import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; +import { spritesheet } from '@elastic/maki'; +import sprites1 from '@elastic/maki/dist/sprite@1.png'; +import sprites2 from '@elastic/maki/dist/sprite@2.png'; +import { DrawControl } from './draw_control'; +import { TooltipControl } from './tooltip_control'; +import { clampToLatBounds, clampToLonBounds } from '../../../elasticsearch_geo_utils'; + +import { getInjectedVarFunc } from '../../../kibana_services'; + +mapboxgl.workerUrl = mbWorkerUrl; +mapboxgl.setRTLTextPlugin(mbRtlPlugin); + +export class MBMapContainer extends React.Component { + state = { + prevLayerList: undefined, + hasSyncedLayerList: false, + mbMap: undefined, + }; + + static getDerivedStateFromProps(nextProps, prevState) { + const nextLayerList = nextProps.layerList; + if (nextLayerList !== prevState.prevLayerList) { + return { + prevLayerList: nextLayerList, + hasSyncedLayerList: false, + }; + } + + return null; + } + + componentDidMount() { + this._initializeMap(); + this._isMounted = true; + } + + componentDidUpdate() { + if (this.state.mbMap) { + // do not debounce syncing of map-state + this._syncMbMapWithMapState(); + this._debouncedSync(); + } + } + + componentWillUnmount() { + this._isMounted = false; + if (this._checker) { + this._checker.destroy(); + } + if (this.state.mbMap) { + this.state.mbMap.remove(); + this.state.mbMap = null; + } + this.props.onMapDestroyed(); + } + + _debouncedSync = _.debounce(() => { + if (this._isMounted && this.props.isMapReady) { + if (!this.state.hasSyncedLayerList) { + this.setState( + { + hasSyncedLayerList: true, + }, + () => { + this._syncMbMapWithLayerList(); + this._syncMbMapWithInspector(); + } + ); + } + this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap); + this._syncSettings(); + } + }, 256); + + _getMapState() { + const zoom = this.state.mbMap.getZoom(); + const mbCenter = this.state.mbMap.getCenter(); + const mbBounds = this.state.mbMap.getBounds(); + return { + zoom: _.round(zoom, ZOOM_PRECISION), + center: { + lon: _.round(mbCenter.lng, DECIMAL_DEGREES_PRECISION), + lat: _.round(mbCenter.lat, DECIMAL_DEGREES_PRECISION), + }, + extent: { + minLon: _.round(mbBounds.getWest(), DECIMAL_DEGREES_PRECISION), + minLat: _.round(mbBounds.getSouth(), DECIMAL_DEGREES_PRECISION), + maxLon: _.round(mbBounds.getEast(), DECIMAL_DEGREES_PRECISION), + maxLat: _.round(mbBounds.getNorth(), DECIMAL_DEGREES_PRECISION), + }, + }; + } + + async _createMbMapInstance() { + return new Promise(resolve => { + const mbStyle = { + version: 8, + sources: {}, + layers: [], + }; + const glyphUrl = getGlyphUrl(); + if (glyphUrl) { + mbStyle.glyphs = glyphUrl; + } + + const options = { + attributionControl: false, + container: this.refs.mapContainer, + style: mbStyle, + scrollZoom: this.props.scrollZoom, + preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false), + interactive: !this.props.disableInteractive, + maxZoom: this.props.settings.maxZoom, + minZoom: this.props.settings.minZoom, + }; + const initialView = _.get(this.props.goto, 'center'); + if (initialView) { + options.zoom = initialView.zoom; + options.center = { + lng: initialView.lon, + lat: initialView.lat, + }; + } else { + options.bounds = [-170, -60, 170, 75]; + } + const mbMap = new mapboxgl.Map(options); + mbMap.dragRotate.disable(); + mbMap.touchZoomRotate.disableRotation(); + if (!this.props.disableInteractive) { + mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); + } + + let emptyImage; + mbMap.on('styleimagemissing', e => { + if (emptyImage) { + mbMap.addImage(e.id, emptyImage); + } + }); + mbMap.on('load', () => { + emptyImage = new Image(); + + emptyImage.src = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; + emptyImage.crossOrigin = 'anonymous'; + resolve(mbMap); + }); + }); + } + + async _initializeMap() { + let mbMap; + try { + mbMap = await this._createMbMapInstance(); + } catch (error) { + this.props.setMapInitError(error.message); + return; + } + + if (!this._isMounted) { + return; + } + + this.setState({ mbMap }, () => { + this._loadMakiSprites(); + this._initResizerChecker(); + this._registerMapEventListeners(); + this.props.onMapReady(this._getMapState()); + }); + } + + _registerMapEventListeners() { + // moveend callback is debounced to avoid updating map extent state while map extent is still changing + // moveend is fired while the map extent is still changing in the following scenarios + // 1) During opening/closing of layer details panel, the EUI animation results in 8 moveend events + // 2) Setting map zoom and center from goto is done in 2 API calls, resulting in 2 moveend events + this.state.mbMap.on( + 'moveend', + _.debounce(() => { + this.props.extentChanged(this._getMapState()); + }, 100) + ); + // Attach event only if view control is visible, which shows lat/lon + if (!this.props.hideViewControl) { + const throttledSetMouseCoordinates = _.throttle(e => { + this.props.setMouseCoordinates({ + lat: e.lngLat.lat, + lon: e.lngLat.lng, + }); + }, 100); + this.state.mbMap.on('mousemove', throttledSetMouseCoordinates); + this.state.mbMap.on('mouseout', () => { + throttledSetMouseCoordinates.cancel(); // cancel any delayed setMouseCoordinates invocations + this.props.clearMouseCoordinates(); + }); + } + } + + _initResizerChecker() { + this._checker = new ResizeChecker(this.refs.mapContainer); + this._checker.on('resize', () => { + this.state.mbMap.resize(); + }); + } + + _loadMakiSprites() { + const sprites = isRetina() ? sprites2 : sprites1; + const json = isRetina() ? spritesheet[2] : spritesheet[1]; + addSpritesheetToMap(json, sprites, this.state.mbMap); + } + + _syncMbMapWithMapState = () => { + const { isMapReady, goto, clearGoto } = this.props; + + if (!isMapReady || !goto) { + return; + } + + clearGoto(); + + if (goto.bounds) { + //clamping ot -89/89 latitudes since Mapboxgl does not seem to handle bounds that contain the poles (logs errors to the console when using -90/90) + const lnLatBounds = new mapboxgl.LngLatBounds( + new mapboxgl.LngLat( + clampToLonBounds(goto.bounds.minLon), + clampToLatBounds(goto.bounds.minLat) + ), + new mapboxgl.LngLat( + clampToLonBounds(goto.bounds.maxLon), + clampToLatBounds(goto.bounds.maxLat) + ) + ); + //maxZoom ensure we're not zooming in too far on single points or small shapes + //the padding is to avoid too tight of a fit around edges + this.state.mbMap.fitBounds(lnLatBounds, { maxZoom: 17, padding: 16 }); + } else if (goto.center) { + this.state.mbMap.setZoom(goto.center.zoom); + this.state.mbMap.setCenter({ + lng: goto.center.lon, + lat: goto.center.lat, + }); + } + }; + + _syncMbMapWithLayerList = () => { + removeOrphanedSourcesAndLayers( + this.state.mbMap, + this.props.layerList, + this.props.spatialFiltersLayer + ); + this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap)); + syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList); + moveLayerToTop(this.state.mbMap, this.props.spatialFiltersLayer); + }; + + _syncMbMapWithInspector = () => { + if (!this.props.inspectorAdapters.map) { + return; + } + + const stats = { + center: this.state.mbMap.getCenter().toArray(), + zoom: this.state.mbMap.getZoom(), + }; + this.props.inspectorAdapters.map.setMapState({ + stats, + style: this.state.mbMap.getStyle(), + }); + }; + + _syncSettings() { + let zoomRangeChanged = false; + if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) { + this.state.mbMap.setMinZoom(this.props.settings.minZoom); + zoomRangeChanged = true; + } + if (this.props.settings.maxZoom !== this.state.mbMap.getMaxZoom()) { + this.state.mbMap.setMaxZoom(this.props.settings.maxZoom); + zoomRangeChanged = true; + } + + // 'moveend' event not fired when map moves from setMinZoom or setMaxZoom + // https://github.com/mapbox/mapbox-gl-js/issues/9610 + // hack to update extent after zoom update finishes moving map. + if (zoomRangeChanged) { + setTimeout(() => { + this.props.extentChanged(this._getMapState()); + }, 300); + } + } + + render() { + let drawControl; + let tooltipControl; + if (this.state.mbMap) { + drawControl = <DrawControl mbMap={this.state.mbMap} addFilters={this.props.addFilters} />; + tooltipControl = !this.props.disableTooltipControl ? ( + <TooltipControl + mbMap={this.state.mbMap} + addFilters={this.props.addFilters} + geoFields={this.props.geoFields} + renderTooltipContent={this.props.renderTooltipContent} + /> + ) : null; + } + return ( + <div + id="mapContainer" + className="mapContainer" + ref="mapContainer" + data-test-subj="mapContainer" + > + {drawControl} + {tooltipControl} + </div> + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts new file mode 100644 index 0000000000000..329fac28d7d2e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { FLYOUT_STATE } from '../../reducers/ui'; +import { MapStoreState } from '../../reducers/store'; +import { MapSettingsPanel } from './map_settings_panel'; +import { rollbackMapSettings, updateMapSetting } from '../../actions/map_actions'; +import { getMapSettings, hasMapSettingsChanges } from '../../selectors/map_selectors'; +import { updateFlyout } from '../../actions/ui_actions'; + +function mapStateToProps(state: MapStoreState) { + return { + settings: getMapSettings(state), + hasMapSettingsChanges: hasMapSettingsChanges(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch<AnyAction>) { + return { + cancelChanges: () => { + dispatch(rollbackMapSettings()); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + }, + keepChanges: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + }, + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => { + dispatch(updateMapSetting(settingKey, settingValue)); + }, + }; +} + +const connectedMapSettingsPanel = connect(mapStateToProps, mapDispatchToProps)(MapSettingsPanel); +export { connectedMapSettingsPanel as MapSettingsPanel }; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx new file mode 100644 index 0000000000000..a89f4461fff06 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -0,0 +1,100 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { NavigationPanel } from './navigation_panel'; +import { SpatialFiltersPanel } from './spatial_filters_panel'; + +interface Props { + cancelChanges: () => void; + hasMapSettingsChanges: boolean; + keepChanges: () => void; + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function MapSettingsPanel({ + cancelChanges, + hasMapSettingsChanges, + keepChanges, + settings, + updateMapSetting, +}: Props) { + // TODO move common text like Cancel and Close to common i18n translation + const closeBtnLabel = hasMapSettingsChanges + ? i18n.translate('xpack.maps.mapSettingsPanel.cancelLabel', { + defaultMessage: 'Cancel', + }) + : i18n.translate('xpack.maps.mapSettingsPanel.closeLabel', { + defaultMessage: 'Close', + }); + + return ( + <EuiFlexGroup direction="column" gutterSize="none"> + <EuiFlyoutHeader hasBorder className="mapLayerPanel__header"> + <EuiTitle size="s"> + <h2> + <FormattedMessage + id="xpack.maps.mapSettingsPanel.title" + defaultMessage="Map settings" + /> + </h2> + </EuiTitle> + </EuiFlyoutHeader> + + <div className="mapLayerPanel__body"> + <div className="mapLayerPanel__bodyOverflow"> + <NavigationPanel settings={settings} updateMapSetting={updateMapSetting} /> + <EuiSpacer size="s" /> + <SpatialFiltersPanel settings={settings} updateMapSetting={updateMapSetting} /> + </div> + </div> + + <EuiFlyoutFooter className="mapLayerPanel__footer"> + <EuiFlexGroup responsive={false}> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + onClick={cancelChanges} + flush="left" + data-test-subj="layerPanelCancelButton" + > + {closeBtnLabel} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem> + <EuiSpacer /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + disabled={!hasMapSettingsChanges} + iconType="check" + onClick={keepChanges} + fill + > + <FormattedMessage + id="xpack.maps.mapSettingsPanel.keepChangesButtonLabel" + defaultMessage="Keep changes" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </EuiFlexGroup> + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx new file mode 100644 index 0000000000000..ed83e838f44f6 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { ValidatedDualRange, Value } from '../../../../../../src/plugins/kibana_react/public'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; + +interface Props { + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function NavigationPanel({ settings, updateMapSetting }: Props) { + const onZoomChange = (value: Value) => { + updateMapSetting('minZoom', Math.max(MIN_ZOOM, parseInt(value[0] as string, 10))); + updateMapSetting('maxZoom', Math.min(MAX_ZOOM, parseInt(value[1] as string, 10))); + }; + + return ( + <EuiPanel> + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.maps.mapSettingsPanel.navigationTitle" + defaultMessage="Navigation" + /> + </h5> + </EuiTitle> + + <EuiSpacer size="m" /> + <ValidatedDualRange + label={i18n.translate('xpack.maps.mapSettingsPanel.zoomRangeLabel', { + defaultMessage: 'Zoom range', + })} + formRowDisplay="columnCompressed" + min={MIN_ZOOM} + max={MAX_ZOOM} + value={[settings.minZoom, settings.maxZoom]} + showInput="inputWithPopover" + showRange + showLabels + onChange={onZoomChange} + allowEmptyRange={false} + compressed + /> + </EuiPanel> + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx new file mode 100644 index 0000000000000..cae703e982966 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx @@ -0,0 +1,98 @@ +/* + * 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 { EuiFormRow, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { AlphaSlider } from '../../components/alpha_slider'; +import { MbValidatedColorPicker } from '../../layers/styles/vector/components/color/mb_validated_color_picker'; + +interface Props { + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function SpatialFiltersPanel({ settings, updateMapSetting }: Props) { + const onAlphaChange = (alpha: number) => { + updateMapSetting('spatialFiltersAlpa', alpha); + }; + + const onFillColorChange = (color: string) => { + updateMapSetting('spatialFiltersFillColor', color); + }; + + const onLineColorChange = (color: string) => { + updateMapSetting('spatialFiltersLineColor', color); + }; + + const onShowSpatialFiltersChange = (event: EuiSwitchEvent) => { + updateMapSetting('showSpatialFilters', event.target.checked); + }; + + const renderStyleInputs = () => { + if (!settings.showSpatialFilters) { + return null; + } + + return ( + <> + <AlphaSlider alpha={settings.spatialFiltersAlpa} onChange={onAlphaChange} /> + + <EuiFormRow + label={i18n.translate('xpack.maps.mapSettingsPanel.spatialFiltersFillColorLabel', { + defaultMessage: 'Fill color', + })} + display="columnCompressed" + > + <MbValidatedColorPicker + color={settings.spatialFiltersFillColor} + onChange={onFillColorChange} + /> + </EuiFormRow> + + <EuiFormRow + label={i18n.translate('xpack.maps.mapSettingsPanel.spatialFiltersLineColorLabel', { + defaultMessage: 'Border color', + })} + display="columnCompressed" + > + <MbValidatedColorPicker + color={settings.spatialFiltersLineColor} + onChange={onLineColorChange} + /> + </EuiFormRow> + </> + ); + }; + + return ( + <EuiPanel> + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.maps.mapSettingsPanel.spatialFiltersTitle" + defaultMessage="Spatial filters" + /> + </h5> + </EuiTitle> + + <EuiSpacer size="m" /> + <EuiFormRow> + <EuiSwitch + label={i18n.translate('xpack.maps.mapSettingsPanel.showSpatialFiltersLabel', { + defaultMessage: 'Show spatial filters on map', + })} + checked={settings.showSpatialFilters} + onChange={onShowSpatialFiltersChange} + compressed + /> + </EuiFormRow> + {renderStyleInputs()} + </EuiPanel> + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss new file mode 100644 index 0000000000000..2754a3e204263 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/_index.scss @@ -0,0 +1,21 @@ +@import 'tools_control/index'; + +.mapToolbarOverlay { + position: absolute; + top: ($euiSizeM + $euiSizeS) + ($euiSizeXL * 2); // Position and height of mapbox controls plus margin + left: $euiSizeM; + z-index: 2; // Sit on top of mapbox controls shadow +} + +.mapToolbarOverlay__button { + @include size($euiSizeXL); + // sass-lint:disable-block no-important + background-color: $euiColorEmptyShade !important; + pointer-events: all; + + &:enabled, + &:enabled:hover, + &:enabled:focus { + @include euiBottomShadowLarge; + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/index.js diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js new file mode 100644 index 0000000000000..c3cc4090ab952 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js @@ -0,0 +1,39 @@ +/* + * 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 { connect } from 'react-redux'; +import { SetViewControl } from './set_view_control'; +import { setGotoWithCenter } from '../../../actions/map_actions'; +import { getMapZoom, getMapCenter, getMapSettings } from '../../../selectors/map_selectors'; +import { closeSetView, openSetView } from '../../../actions/ui_actions'; +import { getIsSetViewOpen } from '../../../selectors/ui_selectors'; + +function mapStateToProps(state = {}) { + return { + settings: getMapSettings(state), + isSetViewOpen: getIsSetViewOpen(state), + zoom: getMapZoom(state), + center: getMapCenter(state), + }; +} + +function mapDispatchToProps(dispatch) { + return { + onSubmit: ({ lat, lon, zoom }) => { + dispatch(closeSetView()); + dispatch(setGotoWithCenter({ lat, lon, zoom })); + }, + closeSetView: () => { + dispatch(closeSetView()); + }, + openSetView: () => { + dispatch(openSetView()); + }, + }; +} + +const connectedSetViewControl = connect(mapStateToProps, mapDispatchToProps)(SetViewControl); +export { connectedSetViewControl as SetViewControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js similarity index 97% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js index 9c983447bfbf6..2c10728f78e5c 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js @@ -18,7 +18,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; function getViewString(lat, lon, zoom) { return `${lat},${lon},${zoom}`; @@ -118,8 +117,8 @@ export class SetViewControl extends Component { const { isInvalid: isZoomInvalid, component: zoomFormRow } = this._renderNumberFormRow({ value: this.state.zoom, - min: MIN_ZOOM, - max: MAX_ZOOM, + min: this.props.settings.minZoom, + max: this.props.settings.maxZoom, onChange: this._onZoomChange, label: i18n.translate('xpack.maps.setViewControl.zoomLabel', { defaultMessage: 'Zoom', diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/__snapshots__/tools_control.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/_index.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js rename to x-pack/plugins/maps/public/connected_components/toolbar_overlay/tools_control/tools_control.test.js diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/_index.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/_index.scss new file mode 100644 index 0000000000000..5e5086bed2763 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/_index.scss @@ -0,0 +1,6 @@ +@import 'mixins'; + +@import 'widget_overlay'; +@import 'attribution_control/attribution_control'; +@import 'layer_control/index'; +@import 'view_control/view_control'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_mixins.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/_mixins.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_mixins.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/_mixins.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/_widget_overlay.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/__snapshots__/view.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/_attribution_control.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/attribution_control/view.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/index.js diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap new file mode 100644 index 0000000000000..0af4eb0793f03 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap @@ -0,0 +1,266 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LayerControl is rendered 1`] = ` +<Fragment> + <EuiPanel + className="mapWidgetControl mapWidgetControl-hasShadow" + grow={false} + paddingSize="none" + > + <EuiFlexItem + className="mapWidgetControl__headerFlexItem" + grow={false} + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="spaceBetween" + responsive={false} + > + <EuiFlexItem> + <EuiTitle + className="mapWidgetControl__header" + size="xxxs" + > + <h2> + <FormattedMessage + defaultMessage="Layers" + id="xpack.maps.layerControl.layersTitle" + values={Object {}} + /> + </h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiToolTip + content="Collapse layers panel" + delay="long" + position="top" + > + <EuiButtonIcon + aria-label="Collapse layers panel" + className="mapLayerControl__closeLayerTOCButton" + color="text" + data-test-subj="mapToggleLegendButton" + iconType="menuRight" + onClick={[Function]} + /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem + className="mapLayerControl" + > + <LayerTOC /> + </EuiFlexItem> + </EuiPanel> + <EuiSpacer + size="s" + /> + <EuiButton + className="mapLayerControl__addLayerButton" + data-test-subj="addLayerButton" + fill={true} + fullWidth={true} + isDisabled={false} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Add layer" + id="xpack.maps.layerControl.addLayerButtonLabel" + values={Object {}} + /> + </EuiButton> +</Fragment> +`; + +exports[`LayerControl isLayerTOCOpen Should render expand button 1`] = ` +<EuiToolTip + content="Expand layers panel" + delay="long" + position="left" +> + <EuiButtonIcon + aria-label="Expand layers panel" + className="mapLayerControl__openLayerTOCButton" + color="text" + iconType="menuLeft" + onClick={[Function]} + /> +</EuiToolTip> +`; + +exports[`LayerControl isLayerTOCOpen Should render expand button with error icon when layer has error 1`] = ` +<EuiToolTip + content="Expand layers panel" + delay="long" + position="left" +> + <EuiButtonIcon + aria-label="Expand layers panel" + className="mapLayerControl__openLayerTOCButton" + color="text" + iconType="alert" + onClick={[Function]} + /> +</EuiToolTip> +`; + +exports[`LayerControl isLayerTOCOpen Should render expand button with loading icon when layer is loading 1`] = ` +<EuiToolTip + content="Expand layers panel" + delay="long" + position="left" +> + <button + aria-label="Expand layers panel" + className="euiButtonIcon euiButtonIcon--text mapLayerControl__openLayerTOCButton" + onClick={[Function]} + type="button" + > + <EuiLoadingSpinner + size="m" + /> + </button> +</EuiToolTip> +`; + +exports[`LayerControl isReadOnly 1`] = ` +<Fragment> + <EuiPanel + className="mapWidgetControl mapWidgetControl-hasShadow" + grow={false} + paddingSize="none" + > + <EuiFlexItem + className="mapWidgetControl__headerFlexItem" + grow={false} + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="spaceBetween" + responsive={false} + > + <EuiFlexItem> + <EuiTitle + className="mapWidgetControl__header" + size="xxxs" + > + <h2> + <FormattedMessage + defaultMessage="Layers" + id="xpack.maps.layerControl.layersTitle" + values={Object {}} + /> + </h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiToolTip + content="Collapse layers panel" + delay="long" + position="top" + > + <EuiButtonIcon + aria-label="Collapse layers panel" + className="mapLayerControl__closeLayerTOCButton" + color="text" + data-test-subj="mapToggleLegendButton" + iconType="menuRight" + onClick={[Function]} + /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem + className="mapLayerControl" + > + <LayerTOC /> + </EuiFlexItem> + </EuiPanel> +</Fragment> +`; + +exports[`LayerControl should disable buttons when flyout is open 1`] = ` +<Fragment> + <EuiPanel + className="mapWidgetControl mapWidgetControl-hasShadow" + grow={false} + paddingSize="none" + > + <EuiFlexItem + className="mapWidgetControl__headerFlexItem" + grow={false} + > + <EuiFlexGroup + alignItems="center" + gutterSize="none" + justifyContent="spaceBetween" + responsive={false} + > + <EuiFlexItem> + <EuiTitle + className="mapWidgetControl__header" + size="xxxs" + > + <h2> + <FormattedMessage + defaultMessage="Layers" + id="xpack.maps.layerControl.layersTitle" + values={Object {}} + /> + </h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem + grow={false} + > + <EuiToolTip + content="Collapse layers panel" + delay="long" + position="top" + > + <EuiButtonIcon + aria-label="Collapse layers panel" + className="mapLayerControl__closeLayerTOCButton" + color="text" + data-test-subj="mapToggleLegendButton" + iconType="menuRight" + onClick={[Function]} + /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem + className="mapLayerControl" + > + <LayerTOC /> + </EuiFlexItem> + </EuiPanel> + <EuiSpacer + size="s" + /> + <EuiButton + className="mapLayerControl__addLayerButton" + data-test-subj="addLayerButton" + fill={true} + fullWidth={true} + isDisabled={true} + onClick={[Function]} + > + <FormattedMessage + defaultMessage="Add layer" + id="xpack.maps.layerControl.addLayerButtonLabel" + values={Object {}} + /> + </EuiButton> +</Fragment> +`; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss new file mode 100644 index 0000000000000..9a3e3a45d6c4e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_index.scss @@ -0,0 +1,2 @@ +@import 'layer_control'; +@import 'layer_toc/toc_entry/toc_entry'; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/_layer_control.scss diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js new file mode 100644 index 0000000000000..915f808b8e358 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { LayerControl } from './view'; + +import { FLYOUT_STATE } from '../../../reducers/ui'; +import { updateFlyout, setIsLayerTOCOpen } from '../../../actions/ui_actions'; +import { setSelectedLayer } from '../../../actions/map_actions'; +import { + getIsReadOnly, + getIsLayerTOCOpen, + getFlyoutDisplay, +} from '../../../selectors/ui_selectors'; +import { getLayerList } from '../../../selectors/map_selectors'; + +function mapStateToProps(state = {}) { + return { + isReadOnly: getIsReadOnly(state), + isLayerTOCOpen: getIsLayerTOCOpen(state), + layerList: getLayerList(state), + isFlyoutOpen: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, + }; +} + +function mapDispatchToProps(dispatch) { + return { + showAddLayerWizard: async () => { + await dispatch(setSelectedLayer(null)); + dispatch(updateFlyout(FLYOUT_STATE.ADD_LAYER_WIZARD)); + }, + closeLayerTOC: () => { + dispatch(setIsLayerTOCOpen(false)); + }, + openLayerTOC: () => { + dispatch(setIsLayerTOCOpen(true)); + }, + }; +} + +const connectedLayerControl = connect(mapStateToProps, mapDispatchToProps)(LayerControl); +export { connectedLayerControl as LayerControl }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/__snapshots__/view.test.js.snap diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/index.js diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap new file mode 100644 index 0000000000000..f1cb1a8864753 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/__snapshots__/view.test.js.snap @@ -0,0 +1,302 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TOCEntry is rendered 1`] = ` +<div + className="mapTocEntry" + data-layerid="1" + id="1" +> + <div + className="mapTocEntry-visible" + > + <Connect(TOCEntryActionsPopover) + displayName="layer 1" + editLayer={[Function]} + escapedDisplayName="layer_1" + layer={ + Object { + "getDisplayName": [Function], + "getId": [Function], + "hasErrors": [Function], + "hasLegendDetails": [Function], + "isVisible": [Function], + "renderLegendDetails": [Function], + "showAtZoomLevel": [Function], + } + } + /> + <div + className="mapTocEntry__layerIcons" + > + <EuiButtonIcon + aria-label="Edit layer" + iconType="pencil" + onClick={[Function]} + title="Edit layer" + /> + <EuiButtonIcon + aria-label="Reorder layer" + className="mapTocEntry__grab" + color="subdued" + iconType="grab" + title="Reorder layer" + /> + </div> + </div> + <span + className="mapTocEntry__detailsToggle" + > + <button + aria-label="Show layer details" + className="mapTocEntry__detailsToggleButton" + onClick={[Function]} + title="Show layer details" + > + <EuiIcon + className="eui-alignBaseline" + size="s" + type="arrowDown" + /> + </button> + </span> +</div> +`; + +exports[`TOCEntry props Should shade background when not selected layer 1`] = ` +<div + className="mapTocEntry" + data-layerid="1" + id="1" +> + <div + className="mapTocEntry-visible" + > + <Connect(TOCEntryActionsPopover) + displayName="layer 1" + editLayer={[Function]} + escapedDisplayName="layer_1" + layer={ + Object { + "getDisplayName": [Function], + "getId": [Function], + "hasErrors": [Function], + "hasLegendDetails": [Function], + "isVisible": [Function], + "renderLegendDetails": [Function], + "showAtZoomLevel": [Function], + } + } + /> + <div + className="mapTocEntry__layerIcons" + > + <EuiButtonIcon + aria-label="Edit layer" + iconType="pencil" + onClick={[Function]} + title="Edit layer" + /> + <EuiButtonIcon + aria-label="Reorder layer" + className="mapTocEntry__grab" + color="subdued" + iconType="grab" + title="Reorder layer" + /> + </div> + </div> + <span + className="mapTocEntry__detailsToggle" + > + <button + aria-label="Show layer details" + className="mapTocEntry__detailsToggleButton" + onClick={[Function]} + title="Show layer details" + > + <EuiIcon + className="eui-alignBaseline" + size="s" + type="arrowDown" + /> + </button> + </span> +</div> +`; + +exports[`TOCEntry props Should shade background when selected layer 1`] = ` +<div + className="mapTocEntry mapTocEntry-isSelected" + data-layerid="1" + id="1" +> + <div + className="mapTocEntry-visible" + > + <Connect(TOCEntryActionsPopover) + displayName="layer 1" + editLayer={[Function]} + escapedDisplayName="layer_1" + layer={ + Object { + "getDisplayName": [Function], + "getId": [Function], + "hasErrors": [Function], + "hasLegendDetails": [Function], + "isVisible": [Function], + "renderLegendDetails": [Function], + "showAtZoomLevel": [Function], + } + } + /> + <div + className="mapTocEntry__layerIcons" + > + <EuiButtonIcon + aria-label="Edit layer" + iconType="pencil" + onClick={[Function]} + title="Edit layer" + /> + <EuiButtonIcon + aria-label="Reorder layer" + className="mapTocEntry__grab" + color="subdued" + iconType="grab" + title="Reorder layer" + /> + </div> + </div> + <span + className="mapTocEntry__detailsToggle" + > + <button + aria-label="Show layer details" + className="mapTocEntry__detailsToggleButton" + onClick={[Function]} + title="Show layer details" + > + <EuiIcon + className="eui-alignBaseline" + size="s" + type="arrowDown" + /> + </button> + </span> +</div> +`; + +exports[`TOCEntry props isReadOnly 1`] = ` +<div + className="mapTocEntry" + data-layerid="1" + id="1" +> + <div + className="mapTocEntry-visible" + > + <Connect(TOCEntryActionsPopover) + displayName="layer 1" + editLayer={[Function]} + escapedDisplayName="layer_1" + layer={ + Object { + "getDisplayName": [Function], + "getId": [Function], + "hasErrors": [Function], + "hasLegendDetails": [Function], + "isVisible": [Function], + "renderLegendDetails": [Function], + "showAtZoomLevel": [Function], + } + } + /> + </div> + <span + className="mapTocEntry__detailsToggle" + > + <button + aria-label="Show layer details" + className="mapTocEntry__detailsToggleButton" + onClick={[Function]} + title="Show layer details" + > + <EuiIcon + className="eui-alignBaseline" + size="s" + type="arrowDown" + /> + </button> + </span> +</div> +`; + +exports[`TOCEntry props should display layer details when isLegendDetailsOpen is true 1`] = ` +<div + className="mapTocEntry" + data-layerid="1" + id="1" +> + <div + className="mapTocEntry-visible" + > + <Connect(TOCEntryActionsPopover) + displayName="layer 1" + editLayer={[Function]} + escapedDisplayName="layer_1" + layer={ + Object { + "getDisplayName": [Function], + "getId": [Function], + "hasErrors": [Function], + "hasLegendDetails": [Function], + "isVisible": [Function], + "renderLegendDetails": [Function], + "showAtZoomLevel": [Function], + } + } + /> + <div + className="mapTocEntry__layerIcons" + > + <EuiButtonIcon + aria-label="Edit layer" + iconType="pencil" + onClick={[Function]} + title="Edit layer" + /> + <EuiButtonIcon + aria-label="Reorder layer" + className="mapTocEntry__grab" + color="subdued" + iconType="grab" + title="Reorder layer" + /> + </div> + </div> + <div + className="mapTocEntry__layerDetails" + data-test-subj="mapLayerTOCDetailslayer_1" + > + <div> + TOC details mock + </div> + </div> + <span + className="mapTocEntry__detailsToggle" + > + <button + aria-label="Hide layer details" + className="mapTocEntry__detailsToggleButton" + onClick={[Function]} + title="Hide layer details" + > + <EuiIcon + className="eui-alignBaseline" + size="s" + type="arrowUp" + /> + </button> + </span> +</div> +`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/_toc_entry.scss diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js new file mode 100644 index 0000000000000..b0d4d9d357988 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js @@ -0,0 +1,53 @@ +/* + * 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 { connect } from 'react-redux'; +import { TOCEntry } from './view'; +import { FLYOUT_STATE } from '../../../../../reducers/ui'; +import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions'; +import { + getMapZoom, + hasDirtyState, + getSelectedLayer, +} from '../../../../../selectors/map_selectors'; +import { + getIsReadOnly, + getOpenTOCDetails, + getFlyoutDisplay, +} from '../../../../../selectors/ui_selectors'; +import { setSelectedLayer, removeTransientLayer } from '../../../../../actions/map_actions'; + +function mapStateToProps(state = {}, ownProps) { + const flyoutDisplay = getFlyoutDisplay(state); + return { + isReadOnly: getIsReadOnly(state), + zoom: getMapZoom(state), + selectedLayer: getSelectedLayer(state), + hasDirtyStateSelector: hasDirtyState(state), + isLegendDetailsOpen: getOpenTOCDetails(state).includes(ownProps.layer.getId()), + isEditButtonDisabled: + flyoutDisplay !== FLYOUT_STATE.NONE && flyoutDisplay !== FLYOUT_STATE.LAYER_PANEL, + }; +} + +function mapDispatchToProps(dispatch) { + return { + openLayerPanel: async layerId => { + await dispatch(removeTransientLayer()); + await dispatch(setSelectedLayer(layerId)); + dispatch(updateFlyout(FLYOUT_STATE.LAYER_PANEL)); + }, + hideTOCDetails: layerId => { + dispatch(hideTOCDetails(layerId)); + }, + showTOCDetails: layerId => { + dispatch(showTOCDetails(layerId)); + }, + }; +} + +const connectedTOCEntry = connect(mapStateToProps, mapDispatchToProps)(TOCEntry); +export { connectedTOCEntry as TOCEntry }; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap new file mode 100644 index 0000000000000..b8c652909408a --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/__snapshots__/toc_entry_actions_popover.test.tsx.snap @@ -0,0 +1,354 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TOCEntryActionsPopover is rendered 1`] = ` +<EuiPopover + anchorClassName="mapLayTocActions__popoverAnchor" + anchorPosition="leftUp" + button={ + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + content={ + <React.Fragment> + simulated tooltip content at zoom: 0 + <div> + <span> + mockFootnoteIcon + </span> + + simulated footnote at isUsingSearch: true + </div> + </React.Fragment> + } + delay="regular" + position="top" + title="layer 1" + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + color="text" + data-test-subj="layerTocActionsPanelToggleButtonlayer1" + flush="left" + onClick={[Function]} + size="xs" + > + <span + className="mapTocEntry__layerNameIcon" + > + <span> + mockIcon + </span> + </span> + layer 1 + + <React.Fragment> + + <span> + mockFootnoteIcon + </span> + </React.Fragment> + </EuiButtonEmpty> + </EuiToolTip> + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="contextMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + <EuiContextMenu + data-test-subj="layerTocActionsPanellayer1" + initialPanelId={0} + panels={ + Array [ + Object { + "id": 0, + "items": Array [ + Object { + "data-test-subj": "fitToBoundsButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="search" + />, + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": <EuiIcon + size="m" + type="eye" + />, + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "editLayerButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="pencil" + />, + "name": "Edit layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "cloneLayerButton", + "icon": <EuiIcon + size="m" + type="copy" + />, + "name": "Clone layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "removeLayerButton", + "icon": <EuiIcon + size="m" + type="trash" + />, + "name": "Remove layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + /> +</EuiPopover> +`; + +exports[`TOCEntryActionsPopover should disable fit to data when supportsFitToBounds is false 1`] = ` +<EuiPopover + anchorClassName="mapLayTocActions__popoverAnchor" + anchorPosition="leftUp" + button={ + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + content={ + <React.Fragment> + simulated tooltip content at zoom: 0 + <div> + <span> + mockFootnoteIcon + </span> + + simulated footnote at isUsingSearch: true + </div> + </React.Fragment> + } + delay="regular" + position="top" + title="layer 1" + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + color="text" + data-test-subj="layerTocActionsPanelToggleButtonlayer1" + flush="left" + onClick={[Function]} + size="xs" + > + <span + className="mapTocEntry__layerNameIcon" + > + <span> + mockIcon + </span> + </span> + layer 1 + + <React.Fragment> + + <span> + mockFootnoteIcon + </span> + </React.Fragment> + </EuiButtonEmpty> + </EuiToolTip> + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="contextMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + <EuiContextMenu + data-test-subj="layerTocActionsPanellayer1" + initialPanelId={0} + panels={ + Array [ + Object { + "id": 0, + "items": Array [ + Object { + "data-test-subj": "fitToBoundsButton", + "disabled": true, + "icon": <EuiIcon + size="m" + type="search" + />, + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": "Layer does not support fit to data", + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": <EuiIcon + size="m" + type="eye" + />, + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "editLayerButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="pencil" + />, + "name": "Edit layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "cloneLayerButton", + "icon": <EuiIcon + size="m" + type="copy" + />, + "name": "Clone layer", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "removeLayerButton", + "icon": <EuiIcon + size="m" + type="trash" + />, + "name": "Remove layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + /> +</EuiPopover> +`; + +exports[`TOCEntryActionsPopover should not show edit actions in read only mode 1`] = ` +<EuiPopover + anchorClassName="mapLayTocActions__popoverAnchor" + anchorPosition="leftUp" + button={ + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + content={ + <React.Fragment> + simulated tooltip content at zoom: 0 + <div> + <span> + mockFootnoteIcon + </span> + + simulated footnote at isUsingSearch: true + </div> + </React.Fragment> + } + delay="regular" + position="top" + title="layer 1" + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + color="text" + data-test-subj="layerTocActionsPanelToggleButtonlayer1" + flush="left" + onClick={[Function]} + size="xs" + > + <span + className="mapTocEntry__layerNameIcon" + > + <span> + mockIcon + </span> + </span> + layer 1 + + <React.Fragment> + + <span> + mockFootnoteIcon + </span> + </React.Fragment> + </EuiButtonEmpty> + </EuiToolTip> + } + className="mapLayTocActions" + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="contextMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="none" + withTitle={true} +> + <EuiContextMenu + data-test-subj="layerTocActionsPanellayer1" + initialPanelId={0} + panels={ + Array [ + Object { + "id": 0, + "items": Array [ + Object { + "data-test-subj": "fitToBoundsButton", + "disabled": false, + "icon": <EuiIcon + size="m" + type="search" + />, + "name": "Fit to data", + "onClick": [Function], + "toolTipContent": null, + }, + Object { + "data-test-subj": "layerVisibilityToggleButton", + "icon": <EuiIcon + size="m" + type="eye" + />, + "name": "Hide layer", + "onClick": [Function], + "toolTipContent": null, + }, + ], + "title": "Layer actions", + }, + ] + } + /> +</EuiPopover> +`; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts new file mode 100644 index 0000000000000..1437370557efc --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { MapStoreState } from '../../../../../../reducers/store'; +import { + fitToLayerExtent, + toggleLayerVisible, + cloneLayer, + removeLayer, +} from '../../../../../../actions/map_actions'; +import { getMapZoom, isUsingSearch } from '../../../../../../selectors/map_selectors'; +import { getIsReadOnly } from '../../../../../../selectors/ui_selectors'; +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; + +function mapStateToProps(state: MapStoreState) { + return { + isReadOnly: getIsReadOnly(state), + isUsingSearch: isUsingSearch(state), + zoom: getMapZoom(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch<AnyAction>) { + return { + cloneLayer: (layerId: string) => { + dispatch(cloneLayer(layerId)); + }, + fitToBounds: (layerId: string) => { + dispatch(fitToLayerExtent(layerId)); + }, + removeLayer: (layerId: string) => { + dispatch(removeLayer(layerId)); + }, + toggleVisible: (layerId: string) => { + dispatch(toggleLayerVisible(layerId)); + }, + }; +} + +const connectedTOCEntryActionsPopover = connect( + mapStateToProps, + mapDispatchToProps +)(TOCEntryActionsPopover); +export { connectedTOCEntryActionsPopover as TOCEntryActionsPopover }; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx new file mode 100644 index 0000000000000..b873119fd7d13 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable max-classes-per-file */ + +import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { AbstractLayer, ILayer } from '../../../../../../layers/layer'; +import { AbstractSource, ISource } from '../../../../../../layers/sources/source'; +import { AbstractStyle, IStyle } from '../../../../../../layers/styles/style'; + +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; + +let supportsFitToBounds: boolean; + +class MockSource extends AbstractSource implements ISource {} + +class MockStyle extends AbstractStyle implements IStyle {} + +class LayerMock extends AbstractLayer implements ILayer { + constructor() { + const sourceDescriptor = { + type: 'mySourceType', + }; + const source = new MockSource(sourceDescriptor); + const style = new MockStyle({ type: 'myStyleType' }); + const layerDescriptor = { + id: 'testLayer', + sourceDescriptor, + }; + super({ layerDescriptor, source, style }); + } + + async supportsFitToBounds(): Promise<boolean> { + return supportsFitToBounds; + } + + isVisible() { + return true; + } + + getIconAndTooltipContent(zoom: number, isUsingSearch: boolean) { + return { + icon: <span>mockIcon</span>, + tooltipContent: `simulated tooltip content at zoom: ${zoom}`, + footnotes: [ + { + icon: <span>mockFootnoteIcon</span>, + message: `simulated footnote at isUsingSearch: ${isUsingSearch}`, + }, + ], + }; + } +} + +const defaultProps = { + cloneLayer: () => {}, + displayName: 'layer 1', + editLayer: () => {}, + escapedDisplayName: 'layer1', + fitToBounds: () => {}, + isEditButtonDisabled: false, + isReadOnly: false, + isUsingSearch: true, + layer: new LayerMock(), + removeLayer: () => {}, + toggleVisible: () => {}, + zoom: 0, +}; + +describe('TOCEntryActionsPopover', () => { + beforeEach(() => { + supportsFitToBounds = true; + }); + + test('is rendered', async () => { + const component = shallowWithIntl(<TOCEntryActionsPopover {...defaultProps} />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should not show edit actions in read only mode', async () => { + const component = shallowWithIntl( + <TOCEntryActionsPopover {...defaultProps} isReadOnly={true} /> + ); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + + test('should disable fit to data when supportsFitToBounds is false', async () => { + supportsFitToBounds = false; + const component = shallowWithIntl(<TOCEntryActionsPopover {...defaultProps} />); + + // Ensure all promises resolve + await new Promise(resolve => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx new file mode 100644 index 0000000000000..d628cca61de11 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -0,0 +1,241 @@ +/* + * 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, { Component, Fragment } from 'react'; + +import { EuiButtonEmpty, EuiPopover, EuiContextMenu, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ILayer } from '../../../../../../layers/layer'; + +interface Props { + cloneLayer: (layerId: string) => void; + displayName: string; + editLayer: () => void; + escapedDisplayName: string; + fitToBounds: (layerId: string) => void; + isEditButtonDisabled: boolean; + isReadOnly: boolean; + isUsingSearch: boolean; + layer: ILayer; + removeLayer: (layerId: string) => void; + toggleVisible: (layerId: string) => void; + zoom: number; +} + +interface State { + isPopoverOpen: boolean; + supportsFitToBounds: boolean; +} + +export class TOCEntryActionsPopover extends Component<Props, State> { + private _isMounted: boolean = false; + + state = { + isPopoverOpen: false, + supportsFitToBounds: false, + }; + + componentDidMount() { + this._isMounted = true; + this._loadSupportsFitToBounds(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + async _loadSupportsFitToBounds() { + const supportsFitToBounds = await this.props.layer.supportsFitToBounds(); + if (this._isMounted) { + this.setState({ supportsFitToBounds }); + } + } + + _togglePopover = () => { + this.setState(prevState => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState(() => ({ + isPopoverOpen: false, + })); + }; + + _cloneLayer() { + this.props.cloneLayer(this.props.layer.getId()); + } + + _fitToBounds() { + this.props.fitToBounds(this.props.layer.getId()); + } + + _removeLayer() { + this.props.fitToBounds(this.props.layer.getId()); + } + + _toggleVisible() { + this.props.toggleVisible(this.props.layer.getId()); + } + + _renderPopoverToggleButton() { + const { icon, tooltipContent, footnotes } = this.props.layer.getIconAndTooltipContent( + this.props.zoom, + this.props.isUsingSearch + ); + + const footnoteIcons = footnotes.map((footnote, index) => { + return ( + <Fragment key={index}> + {''} + {footnote.icon} + </Fragment> + ); + }); + const footnoteTooltipContent = footnotes.map((footnote, index) => { + return ( + <div key={index}> + {footnote.icon} {footnote.message} + </div> + ); + }); + + return ( + <EuiToolTip + anchorClassName="mapLayTocActions__tooltipAnchor" + position="top" + title={this.props.displayName} + content={ + <Fragment> + {tooltipContent} + {footnoteTooltipContent} + </Fragment> + } + > + <EuiButtonEmpty + className="mapTocEntry__layerName eui-textLeft" + size="xs" + flush="left" + color="text" + onClick={this._togglePopover} + data-test-subj={`layerTocActionsPanelToggleButton${this.props.escapedDisplayName}`} + > + <span className="mapTocEntry__layerNameIcon">{icon}</span> + {this.props.displayName} {footnoteIcons} + </EuiButtonEmpty> + </EuiToolTip> + ); + } + + _getActionsPanel() { + const actionItems = [ + { + name: i18n.translate('xpack.maps.layerTocActions.fitToDataTitle', { + defaultMessage: 'Fit to data', + }), + icon: <EuiIcon type="search" size="m" />, + 'data-test-subj': 'fitToBoundsButton', + toolTipContent: this.state.supportsFitToBounds + ? null + : i18n.translate('xpack.maps.layerTocActions.noFitSupportTooltip', { + defaultMessage: 'Layer does not support fit to data', + }), + disabled: !this.state.supportsFitToBounds, + onClick: () => { + this._closePopover(); + this._fitToBounds(); + }, + }, + { + name: this.props.layer.isVisible() + ? i18n.translate('xpack.maps.layerTocActions.hideLayerTitle', { + defaultMessage: 'Hide layer', + }) + : i18n.translate('xpack.maps.layerTocActions.showLayerTitle', { + defaultMessage: 'Show layer', + }), + icon: <EuiIcon type={this.props.layer.isVisible() ? 'eye' : 'eyeClosed'} size="m" />, + 'data-test-subj': 'layerVisibilityToggleButton', + toolTipContent: null, + onClick: () => { + this._closePopover(); + this._toggleVisible(); + }, + }, + ]; + + if (!this.props.isReadOnly) { + actionItems.push({ + disabled: this.props.isEditButtonDisabled, + name: i18n.translate('xpack.maps.layerTocActions.editLayerTitle', { + defaultMessage: 'Edit layer', + }), + icon: <EuiIcon type="pencil" size="m" />, + 'data-test-subj': 'editLayerButton', + toolTipContent: null, + onClick: () => { + this._closePopover(); + this.props.editLayer(); + }, + }); + actionItems.push({ + name: i18n.translate('xpack.maps.layerTocActions.cloneLayerTitle', { + defaultMessage: 'Clone layer', + }), + icon: <EuiIcon type="copy" size="m" />, + toolTipContent: null, + 'data-test-subj': 'cloneLayerButton', + onClick: () => { + this._closePopover(); + this._cloneLayer(); + }, + }); + actionItems.push({ + name: i18n.translate('xpack.maps.layerTocActions.removeLayerTitle', { + defaultMessage: 'Remove layer', + }), + icon: <EuiIcon type="trash" size="m" />, + toolTipContent: null, + 'data-test-subj': 'removeLayerButton', + onClick: () => { + this._closePopover(); + this._removeLayer(); + }, + }); + } + + return { + id: 0, + title: i18n.translate('xpack.maps.layerTocActions.layerActionsTitle', { + defaultMessage: 'Layer actions', + }), + items: actionItems, + }; + } + + render() { + return ( + <EuiPopover + id="contextMenu" + className="mapLayTocActions" + button={this._renderPopoverToggleButton()} + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} + panelPaddingSize="none" + withTitle + anchorPosition="leftUp" + anchorClassName="mapLayTocActions__popoverAnchor" + > + <EuiContextMenu + initialPanelId={0} + panels={[this._getActionsPanel()]} + data-test-subj={`layerTocActionsPanel${this.props.escapedDisplayName}`} + /> + </EuiPopover> + ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js new file mode 100644 index 0000000000000..c0ce24fef9cd8 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -0,0 +1,261 @@ +/* + * 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 classNames from 'classnames'; + +import { EuiIcon, EuiOverlayMask, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; +import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; +import { i18n } from '@kbn/i18n'; + +function escapeLayerName(name) { + return name ? name.split(' ').join('_') : ''; +} + +export class TOCEntry extends React.Component { + state = { + displayName: null, + hasLegendDetails: false, + shouldShowModal: false, + }; + + componentDidMount() { + this._isMounted = true; + this._updateDisplayName(); + this._loadHasLegendDetails(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + this._updateDisplayName(); + this._loadHasLegendDetails(); + } + + _toggleLayerDetailsVisibility = () => { + if (this.props.isLegendDetailsOpen) { + this.props.hideTOCDetails(this.props.layer.getId()); + } else { + this.props.showTOCDetails(this.props.layer.getId()); + } + }; + + async _loadHasLegendDetails() { + const hasLegendDetails = + (await this.props.layer.hasLegendDetails()) && + this.props.layer.isVisible() && + this.props.layer.showAtZoomLevel(this.props.zoom); + if (this._isMounted && hasLegendDetails !== this.state.hasLegendDetails) { + this.setState({ hasLegendDetails }); + } + } + + async _updateDisplayName() { + const label = await this.props.layer.getDisplayName(); + if (this._isMounted) { + if (label !== this.state.displayName) { + this.setState({ + displayName: label, + }); + } + } + } + + _openLayerPanelWithCheck = () => { + const { selectedLayer, hasDirtyStateSelector } = this.props; + if (selectedLayer && selectedLayer.getId() === this.props.layer.getId()) { + return; + } + + if (hasDirtyStateSelector) { + this.setState({ + shouldShowModal: true, + }); + return; + } + + this.props.openLayerPanel(this.props.layer.getId()); + }; + + _renderCancelModal() { + if (!this.state.shouldShowModal) { + return null; + } + + const closeModal = () => { + this.setState({ + shouldShowModal: false, + }); + }; + + const openPanel = () => { + closeModal(); + this.props.openLayerPanel(this.props.layer.getId()); + }; + + return ( + <EuiOverlayMask> + <EuiConfirmModal + title="Discard changes" + onCancel={closeModal} + onConfirm={openPanel} + cancelButtonText="Do not proceed" + confirmButtonText="Proceed and discard changes" + buttonColor="danger" + defaultFocusedButton="cancel" + > + <p>There are unsaved changes to your layer.</p> + <p>Are you sure you want to proceed?</p> + </EuiConfirmModal> + </EuiOverlayMask> + ); + } + + _renderLayerIcons() { + if (this.props.isReadOnly) { + return null; + } + + return ( + <div className="mapTocEntry__layerIcons"> + <EuiButtonIcon + isDisabled={this.props.isEditButtonDisabled} + iconType="pencil" + aria-label={i18n.translate('xpack.maps.layerControl.tocEntry.editButtonAriaLabel', { + defaultMessage: 'Edit layer', + })} + title={i18n.translate('xpack.maps.layerControl.tocEntry.editButtonTitle', { + defaultMessage: 'Edit layer', + })} + onClick={this._openLayerPanelWithCheck} + /> + + <EuiButtonIcon + iconType="grab" + color="subdued" + title={i18n.translate('xpack.maps.layerControl.tocEntry.grabButtonTitle', { + defaultMessage: 'Reorder layer', + })} + aria-label={i18n.translate('xpack.maps.layerControl.tocEntry.grabButtonAriaLabel', { + defaultMessage: 'Reorder layer', + })} + className="mapTocEntry__grab" + {...this.props.dragHandleProps} + /> + </div> + ); + } + + _renderDetailsToggle() { + if (!this.state.hasLegendDetails) { + return null; + } + + const { isLegendDetailsOpen } = this.props; + return ( + <span className="mapTocEntry__detailsToggle"> + <button + className="mapTocEntry__detailsToggleButton" + aria-label={ + isLegendDetailsOpen + ? i18n.translate('xpack.maps.layerControl.tocEntry.hideDetailsButtonAriaLabel', { + defaultMessage: 'Hide layer details', + }) + : i18n.translate('xpack.maps.layerControl.tocEntry.showDetailsButtonAriaLabel', { + defaultMessage: 'Show layer details', + }) + } + title={ + isLegendDetailsOpen + ? i18n.translate('xpack.maps.layerControl.tocEntry.hideDetailsButtonTitle', { + defaultMessage: 'Hide layer details', + }) + : i18n.translate('xpack.maps.layerControl.tocEntry.showDetailsButtonTitle', { + defaultMessage: 'Show layer details', + }) + } + onClick={this._toggleLayerDetailsVisibility} + > + <EuiIcon + className="eui-alignBaseline" + type={isLegendDetailsOpen ? 'arrowUp' : 'arrowDown'} + size="s" + /> + </button> + </span> + ); + } + + _renderLayerHeader() { + const { layer, zoom } = this.props; + return ( + <div + className={ + layer.isVisible() && layer.showAtZoomLevel(zoom) && !layer.hasErrors() + ? 'mapTocEntry-visible' + : 'mapTocEntry-notVisible' + } + > + <TOCEntryActionsPopover + layer={layer} + displayName={this.state.displayName} + escapedDisplayName={escapeLayerName(this.state.displayName)} + editLayer={this._openLayerPanelWithCheck} + isEditButtonDisabled={this.props.isEditButtonDisabled} + /> + + {this._renderLayerIcons()} + </div> + ); + } + + _renderLegendDetails = () => { + if (!this.props.isLegendDetailsOpen || !this.state.hasLegendDetails) { + return null; + } + + const tocDetails = this.props.layer.renderLegendDetails(); + if (!tocDetails) { + return null; + } + + return ( + <div + className="mapTocEntry__layerDetails" + data-test-subj={`mapLayerTOCDetails${escapeLayerName(this.state.displayName)}`} + > + {tocDetails} + </div> + ); + }; + + render() { + const classes = classNames('mapTocEntry', { + 'mapTocEntry-isDragging': this.props.isDragging, + 'mapTocEntry-isDraggingOver': this.props.isDraggingOver, + 'mapTocEntry-isSelected': + this.props.selectedLayer && this.props.selectedLayer.getId() === this.props.layer.getId(), + }); + + return ( + <div + className={classes} + id={this.props.layer.getId()} + data-layerid={this.props.layer.getId()} + > + {this._renderLayerHeader()} + + {this._renderLegendDetails()} + + {this._renderDetailsToggle()} + + {this._renderCancelModal()} + </div> + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.test.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/view.test.js diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js new file mode 100644 index 0000000000000..180dc2e3933c3 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js @@ -0,0 +1,162 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiButton, + EuiTitle, + EuiSpacer, + EuiButtonIcon, + EuiToolTip, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { LayerTOC } from './layer_toc'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +function renderExpandButton({ hasErrors, isLoading, onClick }) { + const expandLabel = i18n.translate('xpack.maps.layerControl.openLayerTOCButtonAriaLabel', { + defaultMessage: 'Expand layers panel', + }); + + if (isLoading) { + // Can not use EuiButtonIcon with spinner because spinner is a class and not an icon + return ( + <button + className="euiButtonIcon euiButtonIcon--text mapLayerControl__openLayerTOCButton" + type="button" + onClick={onClick} + aria-label={expandLabel} + > + <EuiLoadingSpinner size="m" /> + </button> + ); + } + + return ( + <EuiButtonIcon + className="mapLayerControl__openLayerTOCButton" + color="text" + onClick={onClick} + iconType={hasErrors ? 'alert' : 'menuLeft'} + aria-label={expandLabel} + /> + ); +} + +export function LayerControl({ + isReadOnly, + isLayerTOCOpen, + showAddLayerWizard, + closeLayerTOC, + openLayerTOC, + layerList, + isFlyoutOpen, +}) { + if (!isLayerTOCOpen) { + const hasErrors = layerList.some(layer => { + return layer.hasErrors(); + }); + const isLoading = layerList.some(layer => { + return layer.isLayerLoading(); + }); + + return ( + <EuiToolTip + delay="long" + content={i18n.translate('xpack.maps.layerControl.openLayerTOCButtonAriaLabel', { + defaultMessage: 'Expand layers panel', + })} + position="left" + > + {renderExpandButton({ hasErrors, isLoading, onClick: openLayerTOC })} + </EuiToolTip> + ); + } + + let addLayer; + if (!isReadOnly) { + addLayer = ( + <Fragment> + <EuiSpacer size="s" /> + <EuiButton + isDisabled={isFlyoutOpen} + className="mapLayerControl__addLayerButton" + fill + fullWidth + onClick={showAddLayerWizard} + data-test-subj="addLayerButton" + > + <FormattedMessage + id="xpack.maps.layerControl.addLayerButtonLabel" + defaultMessage="Add layer" + /> + </EuiButton> + </Fragment> + ); + } + + return ( + <Fragment> + <EuiPanel + className="mapWidgetControl mapWidgetControl-hasShadow" + paddingSize="none" + grow={false} + > + <EuiFlexItem className="mapWidgetControl__headerFlexItem" grow={false}> + <EuiFlexGroup + justifyContent="spaceBetween" + alignItems="center" + responsive={false} + gutterSize="none" + > + <EuiFlexItem> + <EuiTitle size="xxxs" className="mapWidgetControl__header"> + <h2> + <FormattedMessage + id="xpack.maps.layerControl.layersTitle" + defaultMessage="Layers" + /> + </h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiToolTip + delay="long" + content={i18n.translate('xpack.maps.layerControl.closeLayerTOCButtonAriaLabel', { + defaultMessage: 'Collapse layers panel', + })} + > + <EuiButtonIcon + className="mapLayerControl__closeLayerTOCButton" + onClick={closeLayerTOC} + iconType="menuRight" + color="text" + aria-label={i18n.translate( + 'xpack.maps.layerControl.closeLayerTOCButtonAriaLabel', + { + defaultMessage: 'Collapse layers panel', + } + )} + data-test-subj="mapToggleLegendButton" + /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + + <EuiFlexItem className="mapLayerControl"> + <LayerTOC /> + </EuiFlexItem> + </EuiPanel> + + {addLayer} + </Fragment> + ); +} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js similarity index 90% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js index ee5745efe5180..2f19b1b0c8c45 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.test.js @@ -21,6 +21,7 @@ const defaultProps = { openLayerTOC: () => {}, isLayerTOCOpen: true, layerList: [], + isFlyoutOpen: false, }; describe('LayerControl', () => { @@ -30,6 +31,12 @@ describe('LayerControl', () => { expect(component).toMatchSnapshot(); }); + test('should disable buttons when flyout is open', () => { + const component = shallow(<LayerControl {...defaultProps} isFlyoutOpen={true} />); + + expect(component).toMatchSnapshot(); + }); + test('isReadOnly', () => { const component = shallow(<LayerControl {...defaultProps} isReadOnly={true} />); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss b/x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss rename to x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/_view_control.scss diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/index.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/index.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/index.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/view_control/view_control.js diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js similarity index 100% rename from x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js rename to x-pack/plugins/maps/public/connected_components/widget_overlay/widget_overlay.js diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js index 617cf537fd5c3..888fce7e7afe0 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js @@ -18,6 +18,7 @@ import { } from '../common/constants'; import { getEsSpatialRelationLabel } from '../common/i18n_getters'; import { SPATIAL_FILTER_TYPE } from './kibana_services'; +import turfCircle from '@turf/circle'; function ensureGeoField(type) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; @@ -64,7 +65,7 @@ function ensureGeometryType(type, expectedTypes) { * @param {string} geoFieldType Geometry field type ["geo_point", "geo_shape"] * @returns {number} */ -export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { +export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epochMillisFields) { const features = []; const tmpGeometriesAccumulator = []; @@ -80,6 +81,16 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType) { geoShapeToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); } + // There is a bug in Elasticsearch API where epoch_millis are returned as a string instead of a number + // https://github.com/elastic/elasticsearch/issues/50622 + // Convert these field values to integers. + for (let i = 0; i < epochMillisFields.length; i++) { + const fieldName = epochMillisFields[i]; + if (typeof properties[fieldName] === 'string') { + properties[fieldName] = parseInt(properties[fieldName]); + } + } + // don't include geometry field value in properties delete properties[geoFieldName]; @@ -320,7 +331,7 @@ export function createDistanceFilterWithMeta({ values: { distanceKm, geoFieldName, - pointLabel: point.join(','), + pointLabel: point.join(', '), }, }), }; @@ -441,3 +452,40 @@ export function clamp(val, min, max) { return val; } } + +export function extractFeaturesFromFilters(filters) { + const features = []; + filters + .filter(filter => { + return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE; + }) + .forEach(filter => { + let geometry; + if (filter.geo_distance && filter.geo_distance[filter.meta.key]) { + const distanceSplit = filter.geo_distance.distance.split('km'); + const distance = parseFloat(distanceSplit[0]); + const circleFeature = turfCircle(filter.geo_distance[filter.meta.key], distance); + geometry = circleFeature.geometry; + } else if ( + filter.geo_shape && + filter.geo_shape[filter.meta.key] && + filter.geo_shape[filter.meta.key].shape + ) { + geometry = filter.geo_shape[filter.meta.key].shape; + } else { + // do not know how to convert spatial filter to geometry + // this includes pre-indexed shapes + return; + } + + features.push({ + type: 'Feature', + geometry, + properties: { + filter: filter.meta.alias, + }, + }); + }); + + return features; +} diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js index 5db7556be4639..d13291a8e2ba5 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -19,6 +19,7 @@ import { createExtentFilter, convertMapExtentToPolygon, roundCoordinates, + extractFeaturesFromFilters, } from './elasticsearch_geo_utils'; import { indexPatterns } from '../../../../src/plugins/data/public'; @@ -66,7 +67,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.type).toBe('FeatureCollection'); expect(geojson.features.length).toBe(2); expect(geojson.features[0]).toEqual({ @@ -94,7 +95,7 @@ describe('hitsToGeoJson', () => { _source: {}, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.type).toBe('FeatureCollection'); expect(geojson.features.length).toBe(1); }); @@ -111,7 +112,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.features.length).toBe(1); const feature = geojson.features[0]; expect(feature.properties.myField).toBe(8); @@ -128,7 +129,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point'); + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); expect(geojson.type).toBe('FeatureCollection'); expect(geojson.features.length).toBe(2); expect(geojson.features[0]).toEqual({ @@ -159,6 +160,23 @@ describe('hitsToGeoJson', () => { }); }); + it('Should convert epoch_millis value from string to integer', () => { + const hits = [ + { + _id: 'doc1', + _index: 'index1', + _source: { + [geoFieldName]: '20,100', + myDateField: '1587156257081', + }, + }, + ]; + const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', ['myDateField']); + expect(geojson.type).toBe('FeatureCollection'); + expect(geojson.features.length).toBe(1); + expect(geojson.features[0].properties.myDateField).toBe(1587156257081); + }); + describe('dot in geoFieldName', () => { const indexPatternMock = { fields: { @@ -184,7 +202,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point'); + const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point', []); expect(geojson.features[0].geometry).toEqual({ coordinates: [100, 20], type: 'Point', @@ -199,7 +217,7 @@ describe('hitsToGeoJson', () => { }, }, ]; - const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point'); + const geojson = hitsToGeoJson(hits, indexPatternFlattenHit, 'my.location', 'geo_point', []); expect(geojson.features[0].geometry).toEqual({ coordinates: [100, 20], type: 'Point', @@ -486,3 +504,131 @@ describe('roundCoordinates', () => { ]); }); }); + +describe('extractFeaturesFromFilters', () => { + it('should ignore non-spatial filers', () => { + const phraseFilter = { + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'machine.os', + negate: false, + params: { + query: 'ios', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'machine.os': 'ios', + }, + }, + }; + expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]); + }); + + it('should convert geo_distance filter to feature', () => { + const spatialFilter = { + geo_distance: { + distance: '1096km', + 'geo.coordinates': [-89.87125, 53.49454], + }, + meta: { + alias: 'geo.coordinates within 1096km of -89.87125,53.49454', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + const features = extractFeaturesFromFilters([spatialFilter]); + expect(features[0].geometry.coordinates[0][0]).toEqual([-89.87125, 63.35109118642093]); + expect(features[0].properties).toEqual({ + filter: 'geo.coordinates within 1096km of -89.87125,53.49454', + }); + }); + + it('should convert geo_shape filter to feature', () => { + const spatialFilter = { + geo_shape: { + 'geo.coordinates': { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + }, + ignore_unmapped: true, + }, + meta: { + alias: 'geo.coordinates in bounds', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + }, + properties: { + filter: 'geo.coordinates in bounds', + }, + }, + ]); + }); + + it('should ignore geo_shape filter with pre-index shape', () => { + const spatialFilter = { + geo_shape: { + 'geo.coordinates': { + indexed_shape: { + id: 's5gldXEBkTB2HMwpC8y0', + index: 'world_countries_v1', + path: 'coordinates', + }, + relation: 'INTERSECTS', + }, + ignore_unmapped: true, + }, + meta: { + alias: 'geo.coordinates in multipolygon', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/embeddable/README.md b/x-pack/plugins/maps/public/embeddable/README.md similarity index 100% rename from x-pack/legacy/plugins/maps/public/embeddable/README.md rename to x-pack/plugins/maps/public/embeddable/README.md diff --git a/x-pack/legacy/plugins/maps/public/embeddable/index.ts b/x-pack/plugins/maps/public/embeddable/index.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/embeddable/index.ts rename to x-pack/plugins/maps/public/embeddable/index.ts diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx similarity index 90% rename from x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx rename to x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index b8e4c84ad56a1..467cf4727edb7 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; import 'mapbox-gl/dist/mapbox-gl.css'; - -import { I18nContext } from 'ui/i18n'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; import { @@ -18,8 +16,8 @@ import { IContainer, EmbeddableInput, EmbeddableOutput, -} from '../../../../../../src/plugins/embeddable/public'; -import { APPLY_FILTER_TRIGGER } from '../../../../../../src/plugins/ui_actions/public'; +} from '../../../../../src/plugins/embeddable/public'; +import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; import { esFilters, IIndexPattern, @@ -27,11 +25,10 @@ import { Filter, Query, RefreshInterval, -} from '../../../../../../src/plugins/data/public'; - +} from '../../../../../src/plugins/data/public'; import { GisMap } from '../connected_components/gis_map'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createMapStore, MapStore } from '../../../../../plugins/maps/public/reducers/store'; +import { createMapStore, MapStore } from '../reducers/store'; +import { MapSettings } from '../reducers/map'; import { setGotoWithCenter, replaceLayerList, @@ -44,22 +41,20 @@ import { hideLayerControl, hideViewControl, setHiddenLayers, + setMapSettings, } from '../actions/map_actions'; -import { MapCenterAndZoom } from '../../../../../plugins/maps/common/descriptor_types'; +import { MapCenterAndZoom } from '../../common/descriptor_types'; import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { getInspectorAdapters, setEventHandlers, EventHandlers, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; +} from '../reducers/non_serializable_instances'; import { getMapCenter, getMapZoom, getHiddenLayerIds } from '../selectors/map_selectors'; import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { RenderToolTipContent } from '../../../../../plugins/maps/public/layers/tooltips/tooltip_property'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getUiActions } from '../../../../../plugins/maps/public/kibana_services'; +import { RenderToolTipContent } from '../layers/tooltips/tooltip_property'; +import { getUiActions, getCoreI18n } from '../kibana_services'; interface MapEmbeddableConfig { editUrl?: string; @@ -67,6 +62,7 @@ interface MapEmbeddableConfig { editable: boolean; title?: string; layerList: unknown[]; + settings?: MapSettings; } export interface MapEmbeddableInput extends EmbeddableInput { @@ -104,6 +100,7 @@ export class MapEmbeddable extends Embeddable<MapEmbeddableInput, MapEmbeddableO private _prevFilters?: Filter[]; private _domNode?: HTMLElement; private _unsubscribeFromStore?: Unsubscribe; + private _settings?: MapSettings; constructor( config: MapEmbeddableConfig, @@ -126,6 +123,7 @@ export class MapEmbeddable extends Embeddable<MapEmbeddableInput, MapEmbeddableO this._renderTooltipContent = renderTooltipContent; this._eventHandlers = eventHandlers; this._layerList = config.layerList; + this._settings = config.settings; this._store = createMapStore(); this._subscription = this.getInput$().subscribe(input => this.onContainerStateChanged(input)); @@ -201,6 +199,10 @@ export class MapEmbeddable extends Embeddable<MapEmbeddableInput, MapEmbeddableO this._store.dispatch(setReadOnly(true)); this._store.dispatch(disableScrollZoom()); + if (this._settings) { + this._store.dispatch(setMapSettings(this._settings)); + } + if (_.has(this.input, 'isLayerTOCOpen')) { this._store.dispatch(setIsLayerTOCOpen(this.input.isLayerTOCOpen)); } @@ -247,6 +249,8 @@ export class MapEmbeddable extends Embeddable<MapEmbeddableInput, MapEmbeddableO this._domNode = domNode; + const I18nContext = getCoreI18n().Context; + render( <Provider store={this._store}> <I18nContext> diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts similarity index 80% rename from x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts rename to x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 96c3baf634a83..abfaba80c33d1 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -6,35 +6,24 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { npSetup, npStart } from 'ui/new_platform'; import { IIndexPattern } from 'src/plugins/data/public'; // @ts-ignore import { getMapsSavedObjectLoader } from '../angular/services/gis_map_saved_object_loader'; import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable'; -import { - getIndexPatternService, - getHttp, - getMapsCapabilities, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/kibana_services'; +import { getIndexPatternService, getHttp, getMapsCapabilities } from '../kibana_services'; import { EmbeddableFactoryDefinition, IContainer, -} from '../../../../../../src/plugins/embeddable/public'; +} from '../../../../../src/plugins/embeddable/public'; import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { createMapStore } from '../../../../../plugins/maps/public/reducers/store'; + +import { createMapStore } from '../reducers/store'; import { addLayerWithoutDataSync } from '../actions/map_actions'; import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; import { getInitialLayers } from '../angular/get_initial_layers'; import { mergeInputWithSavedMap } from './merge_input_with_saved_map'; -import '../angular/services/gis_map_saved_object_loader'; -// @ts-ignore -import { - bindSetupCoreAndPlugins as bindNpSetupCoreAndPlugins, - bindStartCoreAndPlugins as bindNpStartCoreAndPlugins, -} from '../../../../../plugins/maps/public/plugin'; // eslint-disable-line @kbn/eslint/no-restricted-paths +import '../index.scss'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; @@ -45,11 +34,6 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type: MAP_SAVED_OBJECT_TYPE, getIconForSavedObject: () => APP_ICON, }; - constructor() { - // Init required services. Necessary while in legacy - bindNpSetupCoreAndPlugins(npSetup.core, npSetup.plugins); - bindNpStartCoreAndPlugins(npStart.core, npStart.plugins); - } async isEditable() { return getMapsCapabilities().save as boolean; @@ -110,6 +94,14 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { const layerList = getInitialLayers(savedMap.layerListJSON); const indexPatterns = await this._getIndexPatterns(layerList); + let settings; + if (savedMap.mapStateJSON) { + const mapState = JSON.parse(savedMap.mapStateJSON); + if (mapState.settings) { + settings = mapState.settings; + } + } + const embeddable = new MapEmbeddable( { layerList, @@ -117,6 +109,7 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { editUrl: getHttp().basePath.prepend(createMapPath(savedObjectId)), indexPatterns, editable: await this.isEditable(), + settings, }, input, parent @@ -151,8 +144,3 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { ); }; } - -npSetup.plugins.embeddable.registerEmbeddableFactory( - MAP_SAVED_OBJECT_TYPE, - new MapEmbeddableFactory() -); diff --git a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts similarity index 100% rename from x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts rename to x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.d.ts diff --git a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js similarity index 89% rename from x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js rename to x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js index 8e3e0a9168e30..d91c91b3b223c 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/merge_input_with_saved_map.js +++ b/x-pack/plugins/maps/public/embeddable/merge_input_with_saved_map.js @@ -5,8 +5,8 @@ */ import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../../../plugins/maps/public/reducers/ui'; + +import { DEFAULT_IS_LAYER_TOC_OPEN } from '../reducers/ui'; const MAP_EMBEDDABLE_INPUT_KEYS = [ 'hideFilterActions', diff --git a/x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts b/x-pack/plugins/maps/public/feature_catalogue_entry.ts similarity index 89% rename from x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts rename to x-pack/plugins/maps/public/feature_catalogue_entry.ts index fdda76b4e1212..6c2579bd3e4e2 100644 --- a/x-pack/legacy/plugins/maps/public/feature_catalogue_entry.ts +++ b/x-pack/plugins/maps/public/feature_catalogue_entry.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { APP_ID, APP_ICON } from '../common/constants'; import { getAppTitle } from '../common/i18n_getters'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; export const featureCatalogueEntry = { id: APP_ID, diff --git a/x-pack/legacy/plugins/maps/public/help_menu_util.js b/x-pack/plugins/maps/public/help_menu_util.js similarity index 81% rename from x-pack/legacy/plugins/maps/public/help_menu_util.js rename to x-pack/plugins/maps/public/help_menu_util.js index 70b9340b562cd..053caf6688309 100644 --- a/x-pack/legacy/plugins/maps/public/help_menu_util.js +++ b/x-pack/plugins/maps/public/help_menu_util.js @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getDocLinks, getCoreChrome } from '../../../../plugins/maps/public/kibana_services'; + +import { getDocLinks, getCoreChrome } from './kibana_services'; export function addHelpMenuToAppChrome() { const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = getDocLinks(); diff --git a/x-pack/legacy/plugins/maps/public/icon.svg b/x-pack/plugins/maps/public/icon.svg similarity index 100% rename from x-pack/legacy/plugins/maps/public/icon.svg rename to x-pack/plugins/maps/public/icon.svg diff --git a/x-pack/plugins/maps/public/index.scss b/x-pack/plugins/maps/public/index.scss new file mode 100644 index 0000000000000..8b2f6d3cb6156 --- /dev/null +++ b/x-pack/plugins/maps/public/index.scss @@ -0,0 +1,17 @@ +/* GIS plugin styles */ + +// Import the EUI global scope so we can use EUI constants +@import 'src/legacy/ui/public/styles/_styling_constants'; + +// Prefix all styles with "map" to avoid conflicts. +// Examples +// mapChart +// mapChart__legend +// mapChart__legend--small +// mapChart__legend-isLoading + +@import 'main'; +@import 'mapbox_hacks'; +@import 'connected_components/index'; +@import 'components/index'; +@import 'layers/index'; diff --git a/x-pack/plugins/maps/public/kibana_services.d.ts b/x-pack/plugins/maps/public/kibana_services.d.ts index 867557c296292..3d346fe1acdd5 100644 --- a/x-pack/plugins/maps/public/kibana_services.d.ts +++ b/x-pack/plugins/maps/public/kibana_services.d.ts @@ -3,8 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { IIndexPattern } from 'src/plugins/data/public'; +import { IIndexPattern, DataPublicPluginStart } from 'src/plugins/data/public'; export function getLicenseId(): any; export function getInspector(): any; @@ -30,6 +29,7 @@ export function getUiActions(): any; export function getCore(): any; export function getNavigation(): any; export function getCoreI18n(): any; +export function getSearchService(): DataPublicPluginStart['search']; export function setLicenseId(args: unknown): void; export function setInspector(args: unknown): void; @@ -53,3 +53,4 @@ export function setUiActions(args: unknown): void; export function setCore(args: unknown): void; export function setNavigation(args: unknown): void; export function setCoreI18n(args: unknown): void; +export function setSearchService(args: DataPublicPluginStart['search']): void; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index dcbd54a09381f..431d7a3b339b7 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -5,8 +5,6 @@ */ import { esFilters, search } from '../../../../src/plugins/data/public'; -export { SearchSource } from '../../../../src/plugins/data/public'; - export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; const { getRequestInspectorStats, getResponseInspectorStats } = search; @@ -137,3 +135,7 @@ export const getNavigation = () => navigation; let coreI18n; export const setCoreI18n = kibanaCoreI18n => (coreI18n = kibanaCoreI18n); export const getCoreI18n = () => coreI18n; + +let dataSearchService; +export const setSearchService = searchService => (dataSearchService = searchService); +export const getSearchService = () => dataSearchService; diff --git a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts index 9a9ea2968ceeb..5c486200977d7 100644 --- a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts @@ -24,7 +24,7 @@ import { } from '../../common/constants'; import { ESGeoGridSource } from './sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from './util/can_skip_fetch'; -import { IVectorLayer, VectorLayerArguments } from './vector_layer'; +import { IVectorLayer } from './vector_layer'; import { IESSource } from './sources/es_source'; import { IESAggSource } from './sources/es_agg_source'; import { ISource } from './sources/source'; @@ -36,6 +36,8 @@ import { DynamicStylePropertyOptions, VectorLayerDescriptor, } from '../../common/descriptor_types'; +import { IStyle } from './styles/style'; +import { IVectorSource } from './sources/vector_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; @@ -145,11 +147,16 @@ function getClusterStyleDescriptor( return clusterStyleDescriptor; } +export interface BlendedVectorLayerArguments { + source: IVectorSource; + layerDescriptor: VectorLayerDescriptor; +} + export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { static type = LAYER_TYPE.BLENDED_VECTOR; static createDescriptor( - options: VectorLayerDescriptor, + options: Partial<VectorLayerDescriptor>, mapColors: string[] ): VectorLayerDescriptor { const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); @@ -163,11 +170,14 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { private readonly _documentSource: IESSource; private readonly _documentStyle: IVectorStyle; - constructor(options: VectorLayerArguments) { - super(options); + constructor(options: BlendedVectorLayerArguments) { + super({ + ...options, + joins: [], + }); this._documentSource = this._source as IESSource; // VectorLayer constructor sets _source as document source - this._documentStyle = this._style; // VectorLayer constructor sets _style as document source + this._documentStyle = this._style as IVectorStyle; // VectorLayer constructor sets _style as document source this._clusterSource = getClusterSource(this._documentSource, this._documentStyle); const clusterStyleDescriptor = getClusterStyleDescriptor( @@ -229,11 +239,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { return this._documentSource; } - getCurrentStyle() { + getCurrentStyle(): IStyle { return this._isClustered ? this._clusterStyle : this._documentStyle; } - getStyleForEditing() { + getStyleForEditing(): IStyle { return this._documentStyle; } @@ -242,8 +252,8 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { const requestToken = Symbol(`layer-active-count:${this.getId()}`); const searchFilters = this._getSearchFilters( syncContext.dataFilters, - this.getSource(), - this.getCurrentStyle() + this.getSource() as IVectorSource, + this.getCurrentStyle() as IVectorStyle ); const canSkipFetch = await canSkipSourceUpdate({ source: this.getSource(), diff --git a/x-pack/plugins/maps/public/layers/layer.d.ts b/x-pack/plugins/maps/public/layers/layer.d.ts deleted file mode 100644 index e8fc5d473626c..0000000000000 --- a/x-pack/plugins/maps/public/layers/layer.d.ts +++ /dev/null @@ -1,51 +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 { LayerDescriptor, MapExtent, MapFilters, MapQuery } from '../../common/descriptor_types'; -import { ISource } from './sources/source'; -import { DataRequest } from './util/data_request'; -import { SyncContext } from '../actions/map_actions'; - -export interface ILayer { - getBounds(mapFilters: MapFilters): Promise<MapExtent>; - getDataRequest(id: string): DataRequest | undefined; - getDisplayName(source?: ISource): Promise<string>; - getId(): string; - getSourceDataRequest(): DataRequest | undefined; - getSource(): ISource; - getSourceForEditing(): ISource; - syncData(syncContext: SyncContext): Promise<void>; - isVisible(): boolean; - showAtZoomLevel(zoomLevel: number): boolean; - getMinZoom(): number; - getMaxZoom(): number; - getMinSourceZoom(): number; -} - -export interface ILayerArguments { - layerDescriptor: LayerDescriptor; - source: ISource; -} - -export class AbstractLayer implements ILayer { - static createDescriptor(options: Partial<LayerDescriptor>, mapColors?: string[]): LayerDescriptor; - constructor(layerArguments: ILayerArguments); - getBounds(mapFilters: MapFilters): Promise<MapExtent>; - getDataRequest(id: string): DataRequest | undefined; - getDisplayName(source?: ISource): Promise<string>; - getId(): string; - getSourceDataRequest(): DataRequest | undefined; - getSource(): ISource; - getSourceForEditing(): ISource; - syncData(syncContext: SyncContext): Promise<void>; - isVisible(): boolean; - showAtZoomLevel(zoomLevel: number): boolean; - getMinZoom(): number; - getMaxZoom(): number; - getMinSourceZoom(): number; - getQuery(): MapQuery; - _removeStaleMbSourcesAndLayers(mbMap: unknown): void; - _requiresPrevSourceCleanup(mbMap: unknown): boolean; -} diff --git a/x-pack/plugins/maps/public/layers/layer.js b/x-pack/plugins/maps/public/layers/layer.js deleted file mode 100644 index 19dcbaf1dfcfd..0000000000000 --- a/x-pack/plugins/maps/public/layers/layer.js +++ /dev/null @@ -1,373 +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 _ from 'lodash'; -import React from 'react'; -import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import { DataRequest } from './util/data_request'; -import { - MAX_ZOOM, - MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, - MIN_ZOOM, - SOURCE_DATA_ID_ORIGIN, -} from '../../common/constants'; -import uuid from 'uuid/v4'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../reducers/util.js'; -import { i18n } from '@kbn/i18n'; - -export class AbstractLayer { - constructor({ layerDescriptor, source }) { - this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); - this._source = source; - if (this._descriptor.__dataRequests) { - this._dataRequests = this._descriptor.__dataRequests.map( - dataRequest => new DataRequest(dataRequest) - ); - } else { - this._dataRequests = []; - } - } - - static getBoundDataForSource(mbMap, sourceId) { - const mbStyle = mbMap.getStyle(); - return mbStyle.sources[sourceId].data; - } - - static createDescriptor(options = {}) { - const layerDescriptor = { ...options }; - - layerDescriptor.__dataRequests = _.get(options, '__dataRequests', []); - layerDescriptor.id = _.get(options, 'id', uuid()); - layerDescriptor.label = options.label && options.label.length > 0 ? options.label : null; - layerDescriptor.minZoom = _.get(options, 'minZoom', MIN_ZOOM); - layerDescriptor.maxZoom = _.get(options, 'maxZoom', MAX_ZOOM); - layerDescriptor.alpha = _.get(options, 'alpha', 0.75); - layerDescriptor.visible = _.get(options, 'visible', true); - layerDescriptor.style = _.get(options, 'style', {}); - - return layerDescriptor; - } - - destroy() { - if (this._source) { - this._source.destroy(); - } - } - - async cloneDescriptor() { - const clonedDescriptor = copyPersistentState(this._descriptor); - // layer id is uuid used to track styles/layers in mapbox - clonedDescriptor.id = uuid(); - const displayName = await this.getDisplayName(); - clonedDescriptor.label = `Clone of ${displayName}`; - clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); - if (clonedDescriptor.joins) { - clonedDescriptor.joins.forEach(joinDescriptor => { - // right.id is uuid used to track requests in inspector - joinDescriptor.right.id = uuid(); - }); - } - return clonedDescriptor; - } - - makeMbLayerId(layerNameSuffix) { - return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; - } - - isJoinable() { - return this.getSource().isJoinable(); - } - - supportsElasticsearchFilters() { - return this.getSource().isESSource(); - } - - async supportsFitToBounds() { - return await this.getSource().supportsFitToBounds(); - } - - async getDisplayName(source) { - if (this._descriptor.label) { - return this._descriptor.label; - } - - const sourceDisplayName = source - ? await source.getDisplayName() - : await this.getSource().getDisplayName(); - return sourceDisplayName || `Layer ${this._descriptor.id}`; - } - - async getAttributions() { - if (!this.hasErrors()) { - return await this.getSource().getAttributions(); - } - return []; - } - - getLabel() { - return this._descriptor.label ? this._descriptor.label : ''; - } - - getCustomIconAndTooltipContent() { - return { - icon: <EuiIcon size="m" type={this.getLayerTypeIconName()} />, - }; - } - - getIconAndTooltipContent(zoomLevel, isUsingSearch) { - let icon; - let tooltipContent = null; - const footnotes = []; - if (this.hasErrors()) { - icon = ( - <EuiIcon - aria-label={i18n.translate('xpack.maps.layer.loadWarningAriaLabel', { - defaultMessage: 'Load warning', - })} - size="m" - type="alert" - color="warning" - /> - ); - tooltipContent = this.getErrors(); - } else if (this.isLayerLoading()) { - icon = <EuiLoadingSpinner size="m" />; - } else if (!this.isVisible()) { - icon = <EuiIcon size="m" type="eyeClosed" />; - tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { - defaultMessage: `Layer is hidden.`, - }); - } else if (!this.showAtZoomLevel(zoomLevel)) { - const minZoom = this.getMinZoom(); - const maxZoom = this.getMaxZoom(); - icon = <EuiIcon size="m" type="expand" />; - tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { - defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, - values: { minZoom, maxZoom }, - }); - } else { - const customIconAndTooltipContent = this.getCustomIconAndTooltipContent(); - if (customIconAndTooltipContent) { - icon = customIconAndTooltipContent.icon; - if (!customIconAndTooltipContent.areResultsTrimmed) { - tooltipContent = customIconAndTooltipContent.tooltipContent; - } else { - footnotes.push({ - icon: <EuiIcon color="subdued" type="partial" size="s" />, - message: customIconAndTooltipContent.tooltipContent, - }); - } - } - - if (isUsingSearch && this.getQueryableIndexPatternIds().length) { - footnotes.push({ - icon: <EuiIcon color="subdued" type="filter" size="s" />, - message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { - defaultMessage: 'Results narrowed by search bar', - }), - }); - } - } - - return { - icon, - tooltipContent, - footnotes, - }; - } - - async hasLegendDetails() { - return false; - } - - renderLegendDetails() { - return null; - } - - getId() { - return this._descriptor.id; - } - - getSource() { - return this._source; - } - - getSourceForEditing() { - return this._source; - } - - isVisible() { - return this._descriptor.visible; - } - - showAtZoomLevel(zoom) { - return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); - } - - getMinZoom() { - return this._descriptor.minZoom; - } - - getMaxZoom() { - return this._descriptor.maxZoom; - } - - getMinSourceZoom() { - return this._source.getMinZoom(); - } - - _requiresPrevSourceCleanup() { - return false; - } - - _removeStaleMbSourcesAndLayers(mbMap) { - if (this._requiresPrevSourceCleanup(mbMap)) { - const mbStyle = mbMap.getStyle(); - mbStyle.layers.forEach(mbLayer => { - if (this.ownsMbLayerId(mbLayer.id)) { - mbMap.removeLayer(mbLayer.id); - } - }); - Object.keys(mbStyle.sources).some(mbSourceId => { - if (this.ownsMbSourceId(mbSourceId)) { - mbMap.removeSource(mbSourceId); - } - }); - } - } - - getAlpha() { - return this._descriptor.alpha; - } - - getQuery() { - return this._descriptor.query; - } - - getCurrentStyle() { - return this._style; - } - - getStyleForEditing() { - return this._style; - } - - async getImmutableSourceProperties() { - return this.getSource().getImmutableProperties(); - } - - renderSourceSettingsEditor = ({ onChange }) => { - return this.getSourceForEditing().renderSourceSettingsEditor({ onChange }); - }; - - getPrevRequestToken(dataId) { - const prevDataRequest = this.getDataRequest(dataId); - if (!prevDataRequest) { - return; - } - - return prevDataRequest.getRequestToken(); - } - - getInFlightRequestTokens() { - if (!this._dataRequests) { - return []; - } - - const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken()); - return _.compact(requestTokens); - } - - getSourceDataRequest() { - return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); - } - - getDataRequest(id) { - return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id); - } - - isLayerLoading() { - return this._dataRequests.some(dataRequest => dataRequest.isLoading()); - } - - hasErrors() { - return _.get(this._descriptor, '__isInErrorState', false); - } - - getErrors() { - return this.hasErrors() ? this._descriptor.__errorMessage : ''; - } - - toLayerDescriptor() { - return this._descriptor; - } - - async syncData() { - //no-op by default - } - - getMbLayerIds() { - throw new Error('Should implement AbstractLayer#getMbLayerIds'); - } - - ownsMbLayerId() { - throw new Error('Should implement AbstractLayer#ownsMbLayerId'); - } - - ownsMbSourceId() { - throw new Error('Should implement AbstractLayer#ownsMbSourceId'); - } - - canShowTooltip() { - return false; - } - - syncLayerWithMB() { - throw new Error('Should implement AbstractLayer#syncLayerWithMB'); - } - - getLayerTypeIconName() { - throw new Error('should implement Layer#getLayerTypeIconName'); - } - - isDataLoaded() { - const sourceDataRequest = this.getSourceDataRequest(); - return sourceDataRequest && sourceDataRequest.hasData(); - } - - async getBounds(/* mapFilters: MapFilters */) { - return { - minLon: -180, - maxLon: 180, - minLat: -89, - maxLat: 89, - }; - } - - renderStyleEditor({ onStyleDescriptorChange }) { - const style = this.getStyleForEditing(); - if (!style) { - return null; - } - return style.renderEditor({ layer: this, onStyleDescriptorChange }); - } - - getIndexPatternIds() { - return []; - } - - getQueryableIndexPatternIds() { - return []; - } - - syncVisibilityWithMb(mbMap, mbLayerId) { - mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); - } - - getType() { - return this._descriptor.type; - } -} diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx new file mode 100644 index 0000000000000..dccf413b489f1 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/layer.tsx @@ -0,0 +1,489 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { Query } from 'src/plugins/data/public'; +import _ from 'lodash'; +import React, { ReactElement } from 'react'; +import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import uuid from 'uuid/v4'; +import { i18n } from '@kbn/i18n'; +import { FeatureCollection } from 'geojson'; +import { DataRequest } from './util/data_request'; +import { + MAX_ZOOM, + MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, + MIN_ZOOM, + SOURCE_DATA_ID_ORIGIN, +} from '../../common/constants'; +// @ts-ignore +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../reducers/util.js'; +import { + LayerDescriptor, + MapExtent, + MapFilters, + StyleDescriptor, +} from '../../common/descriptor_types'; +import { Attribution, ImmutableSourceProperty, ISource } from './sources/source'; +import { SyncContext } from '../actions/map_actions'; +import { IStyle } from './styles/style'; + +export interface ILayer { + getBounds(mapFilters: MapFilters): Promise<MapExtent>; + getDataRequest(id: string): DataRequest | undefined; + getDisplayName(source?: ISource): Promise<string>; + getId(): string; + getSourceDataRequest(): DataRequest | undefined; + getSource(): ISource; + getSourceForEditing(): ISource; + syncData(syncContext: SyncContext): void; + supportsElasticsearchFilters(): boolean; + supportsFitToBounds(): Promise<boolean>; + getAttributions(): Promise<Attribution[]>; + getLabel(): string; + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent; + getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent; + renderLegendDetails(): ReactElement<any> | null; + showAtZoomLevel(zoom: number): boolean; + getMinZoom(): number; + getMaxZoom(): number; + getMinSourceZoom(): number; + getAlpha(): number; + getQuery(): Query | null; + getStyle(): IStyle; + getStyleForEditing(): IStyle; + getCurrentStyle(): IStyle; + getImmutableSourceProperties(): Promise<ImmutableSourceProperty[]>; + renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement<any> | null; + isLayerLoading(): boolean; + hasErrors(): boolean; + getErrors(): string; + getMbLayerIds(): string[]; + ownsMbLayerId(mbLayerId: string): boolean; + ownsMbSourceId(mbSourceId: string): boolean; + canShowTooltip(): boolean; + syncLayerWithMB(mbMap: unknown): void; + getLayerTypeIconName(): string; + isDataLoaded(): boolean; + getIndexPatternIds(): string[]; + getQueryableIndexPatternIds(): string[]; + getType(): string | undefined; + isVisible(): boolean; + cloneDescriptor(): Promise<LayerDescriptor>; + renderStyleEditor({ + onStyleDescriptorChange, + }: { + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement<any> | null; +} +export type Footnote = { + icon: ReactElement<any>; + message?: string | null; +}; +export type IconAndTooltipContent = { + icon?: ReactElement<any> | null; + tooltipContent?: string | null; + footnotes: Footnote[]; +}; +export type CustomIconAndTooltipContent = { + icon: ReactElement<any> | null; + tooltipContent?: string | null; + areResultsTrimmed?: boolean; +}; + +export interface ILayerArguments { + layerDescriptor: LayerDescriptor; + source: ISource; + style: IStyle; +} + +export class AbstractLayer implements ILayer { + protected readonly _descriptor: LayerDescriptor; + protected readonly _source: ISource; + protected readonly _style: IStyle; + protected readonly _dataRequests: DataRequest[]; + + static createDescriptor(options: Partial<LayerDescriptor>): LayerDescriptor { + return { + ...options, + sourceDescriptor: options.sourceDescriptor ? options.sourceDescriptor : null, + __dataRequests: _.get(options, '__dataRequests', []), + id: _.get(options, 'id', uuid()), + label: options.label && options.label.length > 0 ? options.label : null, + minZoom: _.get(options, 'minZoom', MIN_ZOOM), + maxZoom: _.get(options, 'maxZoom', MAX_ZOOM), + alpha: _.get(options, 'alpha', 0.75), + visible: _.get(options, 'visible', true), + style: _.get(options, 'style', null), + }; + } + + destroy() { + if (this._source) { + this._source.destroy(); + } + } + + constructor({ layerDescriptor, source, style }: ILayerArguments) { + this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); + this._source = source; + this._style = style; + if (this._descriptor.__dataRequests) { + this._dataRequests = this._descriptor.__dataRequests.map( + dataRequest => new DataRequest(dataRequest) + ); + } else { + this._dataRequests = []; + } + } + + static getBoundDataForSource(mbMap: unknown, sourceId: string): FeatureCollection { + // @ts-ignore + const mbStyle = mbMap.getStyle(); + return mbStyle.sources[sourceId].data; + } + + async cloneDescriptor(): Promise<LayerDescriptor> { + // @ts-ignore + const clonedDescriptor = copyPersistentState(this._descriptor); + // layer id is uuid used to track styles/layers in mapbox + clonedDescriptor.id = uuid(); + const displayName = await this.getDisplayName(); + clonedDescriptor.label = `Clone of ${displayName}`; + clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); + + // todo: remove this + // This should not be in AbstractLayer. It relies on knowledge of VectorLayerDescriptor + // @ts-ignore + if (clonedDescriptor.joins) { + // @ts-ignore + clonedDescriptor.joins.forEach(joinDescriptor => { + // right.id is uuid used to track requests in inspector + // @ts-ignore + joinDescriptor.right.id = uuid(); + }); + } + return clonedDescriptor; + } + + makeMbLayerId(layerNameSuffix: string): string { + return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; + } + + isJoinable(): boolean { + return this.getSource().isJoinable(); + } + + supportsElasticsearchFilters(): boolean { + return this.getSource().isESSource(); + } + + async supportsFitToBounds(): Promise<boolean> { + return await this.getSource().supportsFitToBounds(); + } + + async getDisplayName(source?: ISource): Promise<string> { + if (this._descriptor.label) { + return this._descriptor.label; + } + + const sourceDisplayName = source + ? await source.getDisplayName() + : await this.getSource().getDisplayName(); + return sourceDisplayName || `Layer ${this._descriptor.id}`; + } + + async getAttributions(): Promise<Attribution[]> { + if (!this.hasErrors()) { + return await this.getSource().getAttributions(); + } + return []; + } + + getStyleForEditing(): IStyle { + return this._style; + } + + getStyle() { + return this._style; + } + + getLabel(): string { + return this._descriptor.label ? this._descriptor.label : ''; + } + + getCustomIconAndTooltipContent(): CustomIconAndTooltipContent { + return { + icon: <EuiIcon size="m" type={this.getLayerTypeIconName()} />, + }; + } + + getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent { + let icon; + let tooltipContent = null; + const footnotes = []; + if (this.hasErrors()) { + icon = ( + <EuiIcon + aria-label={i18n.translate('xpack.maps.layer.loadWarningAriaLabel', { + defaultMessage: 'Load warning', + })} + size="m" + type="alert" + color="warning" + /> + ); + tooltipContent = this.getErrors(); + } else if (this.isLayerLoading()) { + icon = <EuiLoadingSpinner size="m" />; + } else if (!this.isVisible()) { + icon = <EuiIcon size="m" type="eyeClosed" />; + tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { + defaultMessage: `Layer is hidden.`, + }); + } else if (!this.showAtZoomLevel(zoomLevel)) { + const minZoom = this.getMinZoom(); + const maxZoom = this.getMaxZoom(); + icon = <EuiIcon size="m" type="expand" />; + tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { + defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, + values: { minZoom, maxZoom }, + }); + } else { + const customIconAndTooltipContent = this.getCustomIconAndTooltipContent(); + if (customIconAndTooltipContent) { + icon = customIconAndTooltipContent.icon; + if (!customIconAndTooltipContent.areResultsTrimmed) { + tooltipContent = customIconAndTooltipContent.tooltipContent; + } else { + footnotes.push({ + icon: <EuiIcon color="subdued" type="partial" size="s" />, + message: customIconAndTooltipContent.tooltipContent, + }); + } + } + + if (isUsingSearch && this.getQueryableIndexPatternIds().length) { + footnotes.push({ + icon: <EuiIcon color="subdued" type="filter" size="s" />, + message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { + defaultMessage: 'Results narrowed by search bar', + }), + }); + } + } + + return { + icon, + tooltipContent, + footnotes, + }; + } + + async hasLegendDetails(): Promise<boolean> { + return false; + } + + renderLegendDetails(): ReactElement<any> | null { + return null; + } + + getId(): string { + return this._descriptor.id; + } + + getSource(): ISource { + return this._source; + } + + getSourceForEditing(): ISource { + return this._source; + } + + isVisible(): boolean { + return !!this._descriptor.visible; + } + + showAtZoomLevel(zoom: number): boolean { + return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); + } + + getMinZoom(): number { + return typeof this._descriptor.minZoom === 'number' ? this._descriptor.minZoom : MIN_ZOOM; + } + + getMaxZoom(): number { + return typeof this._descriptor.maxZoom === 'number' ? this._descriptor.maxZoom : MAX_ZOOM; + } + + getMinSourceZoom(): number { + return this._source.getMinZoom(); + } + + _requiresPrevSourceCleanup(mbMap: unknown) { + return false; + } + + _removeStaleMbSourcesAndLayers(mbMap: unknown) { + if (this._requiresPrevSourceCleanup(mbMap)) { + // @ts-ignore + const mbStyle = mbMap.getStyle(); + // @ts-ignore + mbStyle.layers.forEach(mbLayer => { + // @ts-ignore + if (this.ownsMbLayerId(mbLayer.id)) { + // @ts-ignore + mbMap.removeLayer(mbLayer.id); + } + }); + // @ts-ignore + Object.keys(mbStyle.sources).some(mbSourceId => { + // @ts-ignore + if (this.ownsMbSourceId(mbSourceId)) { + // @ts-ignore + mbMap.removeSource(mbSourceId); + } + }); + } + } + + getAlpha(): number { + return typeof this._descriptor.alpha === 'number' ? this._descriptor.alpha : 1; + } + + getQuery(): Query | null { + return this._descriptor.query ? this._descriptor.query : null; + } + + getCurrentStyle(): IStyle { + return this._style; + } + + async getImmutableSourceProperties() { + const source = this.getSource(); + return await source.getImmutableProperties(); + } + + renderSourceSettingsEditor({ onChange }: { onChange: () => void }) { + const source = this.getSourceForEditing(); + return source.renderSourceSettingsEditor({ onChange }); + } + + getPrevRequestToken(dataId: string): symbol | undefined { + const prevDataRequest = this.getDataRequest(dataId); + if (!prevDataRequest) { + return; + } + + return prevDataRequest.getRequestToken(); + } + + getInFlightRequestTokens(): symbol[] { + if (!this._dataRequests) { + return []; + } + + const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken()); + + // Compact removes all the undefineds + // @ts-ignore + return _.compact(requestTokens); + } + + getSourceDataRequest(): DataRequest | undefined { + return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); + } + + getDataRequest(id: string): DataRequest | undefined { + return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id); + } + + isLayerLoading(): boolean { + return this._dataRequests.some(dataRequest => dataRequest.isLoading()); + } + + hasErrors(): boolean { + return _.get(this._descriptor, '__isInErrorState', false); + } + + getErrors(): string { + return this.hasErrors() && this._descriptor.__errorMessage + ? this._descriptor.__errorMessage + : ''; + } + + async syncData(syncContext: SyncContext) { + // no-op by default + } + + getMbLayerIds(): string[] { + throw new Error('Should implement AbstractLayer#getMbLayerIds'); + } + + ownsMbLayerId(layerId: string): boolean { + throw new Error('Should implement AbstractLayer#ownsMbLayerId'); + } + + ownsMbSourceId(sourceId: string): boolean { + throw new Error('Should implement AbstractLayer#ownsMbSourceId'); + } + + canShowTooltip() { + return false; + } + + syncLayerWithMB(mbMap: unknown) { + throw new Error('Should implement AbstractLayer#syncLayerWithMB'); + } + + getLayerTypeIconName(): string { + throw new Error('should implement Layer#getLayerTypeIconName'); + } + + isDataLoaded(): boolean { + const sourceDataRequest = this.getSourceDataRequest(); + return sourceDataRequest ? sourceDataRequest.hasData() : false; + } + + async getBounds(mapFilters: MapFilters): Promise<MapExtent> { + return { + minLon: -180, + maxLon: 180, + minLat: -89, + maxLat: 89, + }; + } + + renderStyleEditor({ + onStyleDescriptorChange, + }: { + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement<any> | null { + const style = this.getStyleForEditing(); + if (!style) { + return null; + } + return style.renderEditor({ layer: this, onStyleDescriptorChange }); + } + + getIndexPatternIds(): string[] { + return []; + } + + getQueryableIndexPatternIds(): string[] { + return []; + } + + syncVisibilityWithMb(mbMap: unknown, mbLayerId: string) { + // @ts-ignore + mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); + } + + getType(): string | undefined { + return this._descriptor.type; + } +} 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 cb87aeaa9da3f..7715541b1c52d 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts @@ -6,16 +6,21 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { ReactElement } from 'react'; -import { ISource } from './sources/source'; - -export type PreviewSourceHandler = (source: ISource | null) => void; +import { LayerDescriptor } from '../../common/descriptor_types'; export type RenderWizardArguments = { - onPreviewSource: PreviewSourceHandler; - inspectorAdapters: object; + previewLayer: (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => void; + mapColors: string[]; + // upload arguments + isIndexingTriggered: boolean; + onRemove: () => void; + onIndexReady: () => void; + importSuccessHandler: (indexResponses: unknown) => void; + importErrorHandler: (indexResponses: unknown) => void; }; export type LayerWizard = { + checkVisibility?: () => boolean; description: string; icon: string; isIndexingSource?: boolean; @@ -30,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/load_layer_wizards.ts b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts index 49d128257fe20..0b83f3bbdc613 100644 --- a/x-pack/plugins/maps/public/layers/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts @@ -12,7 +12,7 @@ import { esDocumentsLayerWizardConfig } from './sources/es_search_source'; // @ts-ignore import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from './sources/es_geo_grid_source'; // @ts-ignore -import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source/es_pew_pew_source'; +import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source'; // @ts-ignore import { emsBoundariesLayerWizardConfig } from './sources/ems_file_source'; // @ts-ignore diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 137513ad7c612..3c9c71d2a1875 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -5,24 +5,12 @@ */ import { AbstractVectorSource } from '../vector_source'; -import React from 'react'; -import { - ES_GEO_FIELD_TYPE, - SOURCE_TYPES, - DEFAULT_MAX_RESULT_WINDOW, - SCALING_TYPES, -} from '../../../../common/constants'; -import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; -import { ESSearchSource } from '../es_search_source'; -import uuid from 'uuid/v4'; -import { i18n } from '@kbn/i18n'; +import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; export class GeojsonFileSource extends AbstractVectorSource { static type = SOURCE_TYPES.GEOJSON_FILE; - static isIndexingSource = true; - static createDescriptor(geoJson, name) { // Wrap feature as feature collection if needed let featureCollection; @@ -68,103 +56,9 @@ export class GeojsonFileSource extends AbstractVectorSource { canFormatFeatureProperties() { return true; } - - shouldBeIndexed() { - return GeojsonFileSource.isIndexingSource; - } } -const viewIndexedData = ( - addAndViewSource, - inspectorAdapters, - importSuccessHandler, - importErrorHandler -) => { - return (indexResponses = {}) => { - const { indexDataResp, indexPatternResp } = indexResponses; - - const indexCreationFailed = !(indexDataResp && indexDataResp.success); - const allDocsFailed = indexDataResp.failures.length === indexDataResp.docCount; - const indexPatternCreationFailed = !(indexPatternResp && indexPatternResp.success); - - if (indexCreationFailed || allDocsFailed || indexPatternCreationFailed) { - importErrorHandler(indexResponses); - return; - } - const { fields, id: indexPatternId } = indexPatternResp; - const geoField = fields.find(field => Object.values(ES_GEO_FIELD_TYPE).includes(field.type)); - if (!indexPatternId || !geoField) { - addAndViewSource(null); - } else { - const source = new ESSearchSource( - { - id: uuid(), - indexPatternId, - geoField: geoField.name, - // Only turn on bounds filter for large doc counts - filterByMapBounds: indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW, - scalingType: - geoField.type === ES_GEO_FIELD_TYPE.GEO_POINT - ? SCALING_TYPES.CLUSTERS - : SCALING_TYPES.LIMIT, - }, - inspectorAdapters - ); - addAndViewSource(source); - importSuccessHandler(indexResponses); - } - }; -}; - -const previewGeojsonFile = (onPreviewSource, inspectorAdapters) => { - return (geojsonFile, name) => { - if (!geojsonFile) { - onPreviewSource(null); - return; - } - const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name); - const source = new GeojsonFileSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; -}; - registerSource({ ConstructorFunction: GeojsonFileSource, type: SOURCE_TYPES.GEOJSON_FILE, }); - -export const uploadLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.geojsonFileDescription', { - defaultMessage: 'Index GeoJSON data in Elasticsearch', - }), - icon: 'importAction', - isIndexingSource: true, - renderWizard: ({ - onPreviewSource, - inspectorAdapters, - addAndViewSource, - isIndexingTriggered, - onRemove, - onIndexReady, - importSuccessHandler, - importErrorHandler, - }) => { - return ( - <ClientFileCreateSourceEditor - previewGeojsonFile={previewGeojsonFile(onPreviewSource, inspectorAdapters)} - isIndexingTriggered={isIndexingTriggered} - onIndexingComplete={viewIndexedData( - addAndViewSource, - inspectorAdapters, - importSuccessHandler, - importErrorHandler - )} - onRemove={onRemove} - onIndexReady={onIndexReady} - /> - ); - }, - title: i18n.translate('xpack.maps.source.geojsonFileTitle', { - defaultMessage: 'Upload GeoJSON', - }), -}; diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js index a6a31def4b231..5c2a0afd31885 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { GeojsonFileSource, uploadLayerWizardConfig } from './geojson_file_source'; +export { GeojsonFileSource } from './geojson_file_source'; +export { uploadLayerWizardConfig } from './upload_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx new file mode 100644 index 0000000000000..2f8aa67d74b52 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { IFieldType } from 'src/plugins/data/public'; +import { + ES_GEO_FIELD_TYPE, + DEFAULT_MAX_RESULT_WINDOW, + SCALING_TYPES, +} from '../../../../common/constants'; +// @ts-ignore +import { ESSearchSource, createDefaultLayerDescriptor } from '../es_search_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; +// @ts-ignore +import { GeojsonFileSource } from './geojson_file_source'; +import { VectorLayer } from '../../vector_layer'; + +export const uploadLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.geojsonFileDescription', { + defaultMessage: 'Index GeoJSON data in Elasticsearch', + }), + icon: 'importAction', + isIndexingSource: true, + renderWizard: ({ + previewLayer, + mapColors, + isIndexingTriggered, + onRemove, + onIndexReady, + importSuccessHandler, + importErrorHandler, + }: RenderWizardArguments) => { + function previewGeojsonFile(geojsonFile: unknown, name: string) { + if (!geojsonFile) { + previewLayer(null); + return; + } + const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name); + const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + // TODO figure out a better way to handle passing this information back to layer_addpanel + previewLayer(layerDescriptor, true); + } + + function viewIndexedData(indexResponses: { + indexDataResp: unknown; + indexPatternResp: unknown; + }) { + const { indexDataResp, indexPatternResp } = indexResponses; + + // @ts-ignore + const indexCreationFailed = !(indexDataResp && indexDataResp.success); + // @ts-ignore + const allDocsFailed = indexDataResp.failures.length === indexDataResp.docCount; + // @ts-ignore + const indexPatternCreationFailed = !(indexPatternResp && indexPatternResp.success); + + if (indexCreationFailed || allDocsFailed || indexPatternCreationFailed) { + importErrorHandler(indexResponses); + return; + } + // @ts-ignore + const { fields, id: indexPatternId } = indexPatternResp; + const geoField = fields.find((field: IFieldType) => + [ES_GEO_FIELD_TYPE.GEO_POINT as string, ES_GEO_FIELD_TYPE.GEO_SHAPE as string].includes( + field.type + ) + ); + if (!indexPatternId || !geoField) { + previewLayer(null); + } else { + const esSearchSourceConfig = { + indexPatternId, + geoField: geoField.name, + // Only turn on bounds filter for large doc counts + // @ts-ignore + filterByMapBounds: indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW, + scalingType: + geoField.type === ES_GEO_FIELD_TYPE.GEO_POINT + ? SCALING_TYPES.CLUSTERS + : SCALING_TYPES.LIMIT, + }; + previewLayer(createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)); + importSuccessHandler(indexResponses); + } + } + + return ( + <ClientFileCreateSourceEditor + previewGeojsonFile={previewGeojsonFile} + isIndexingTriggered={isIndexingTriggered} + onIndexingComplete={viewIndexedData} + onRemove={onRemove} + onIndexReady={onIndexReady} + /> + ); + }, + title: i18n.translate('xpack.maps.source.geojsonFileTitle', { + defaultMessage: 'Upload GeoJSON', + }), +}; 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 new file mode 100644 index 0000000000000..a6e2e7f42657c --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -0,0 +1,36 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { VectorLayer } from '../../vector_layer'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +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', + }), + icon: 'emsApp', + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + // @ts-ignore + const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); + const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayer(layerDescriptor); + }; + return <EMSFileCreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index e8af17b911939..5802a223e4846 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -9,14 +9,13 @@ import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; -import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; import { EMSFileField } from '../../fields/ems_file_field'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', { defaultMessage: 'EMS Boundaries', }); @@ -161,19 +160,3 @@ registerSource({ ConstructorFunction: EMSFileSource, type: SOURCE_TYPES.EMS_FILE, }); - -export const emsBoundariesLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.emsFileDescription', { - defaultMessage: 'Administrative boundaries from Elastic Maps Service', - }), - icon: 'emsApp', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); - const source = new EMSFileSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - return <EMSFileCreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js index 28fbc04a1a032..e9bf592c6d2b7 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EMSFileSource, emsBoundariesLayerWizardConfig } from './ems_file_source'; +export { emsBoundariesLayerWizardConfig } from './ems_boundaries_layer_wizard'; +export { EMSFileSource } from './ems_file_source'; 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 new file mode 100644 index 0000000000000..fc745edbabee8 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { EMSTMSSource, sourceTitle } from './ems_tms_source'; +// @ts-ignore +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', + }), + icon: 'emsApp', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + const layerDescriptor = VectorTileLayer.createDescriptor({ + sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); + }; + + return <TileServiceSelect onTileSelect={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 79121c4cdb31f..3bed9b2c09570 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -7,10 +7,8 @@ import _ from 'lodash'; import React from 'react'; import { AbstractTMSSource } from '../tms_source'; -import { VectorTileLayer } from '../../vector_tile_layer'; import { getEMSClient } from '../../../meta'; -import { TileServiceSelect } from './tile_service_select'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -18,7 +16,7 @@ import { SOURCE_TYPES } from '../../../../common/constants'; import { getInjectedVarFunc, getUiSettings } from '../../../kibana_services'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { defaultMessage: 'EMS Basemaps', }); @@ -84,20 +82,6 @@ export class EMSTMSSource extends AbstractTMSSource { return tmsService; } - _createDefaultLayerDescriptor(options) { - return VectorTileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - createDefaultLayer(options) { - return new VectorTileLayer({ - layerDescriptor: this._createDefaultLayerDescriptor(options), - source: this, - }); - } - async getDisplayName() { try { const emsTMSService = await this._getEMSTMSService(); @@ -150,20 +134,3 @@ registerSource({ ConstructorFunction: EMSTMSSource, type: SOURCE_TYPES.EMS_TMS, }); - -export const emsBaseMapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.emsTileDescription', { - defaultMessage: 'Tile map service from Elastic Maps Service', - }), - icon: 'emsApp', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - const descriptor = EMSTMSSource.createDescriptor(sourceConfig); - const source = new EMSTMSSource(descriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return <TileServiceSelect onTileSelect={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js index 60a4c9b1de891..704bcfd370a85 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EMSTMSSource, emsBaseMapLayerWizardConfig } from './ems_tms_source'; +export { emsBaseMapLayerWizardConfig } from './ems_base_map_layer_wizard'; +export { EMSTMSSource } from './ems_tms_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx new file mode 100644 index 0000000000000..f9092e64833f1 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -0,0 +1,108 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { VectorLayer } from '../../vector_layer'; +import { + ESGeoGridSourceDescriptor, + ColorDynamicOptions, + SizeDynamicOptions, +} from '../../../../common/descriptor_types'; +import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { + COUNT_PROP_NAME, + COLOR_MAP_TYPE, + FIELD_ORIGIN, + RENDER_AS, + VECTOR_STYLES, + STYLE_TYPE, +} from '../../../../common/constants'; +// @ts-ignore +import { COLOR_GRADIENTS } from '../../styles/color_utils'; + +export const clustersLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.esGridClustersDescription', { + defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: Partial<ESGeoGridSourceDescriptor>) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const defaultDynamicProperties = getDefaultDynamicProperties(); + const layerDescriptor = VectorLayer.createDescriptor({ + sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), + style: VectorStyle.createDescriptor({ + // @ts-ignore + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]! + .options as ColorDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + color: COLOR_GRADIENTS[0].value, + type: COLOR_MAP_TYPE.ORDINAL, + }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: '#FFF', + }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { + type: STYLE_TYPE.STATIC, + options: { + size: 0, + }, + }, + [VECTOR_STYLES.ICON_SIZE]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + }, + }, + [VECTOR_STYLES.LABEL_TEXT]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT]!.options, + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + }, + }, + }), + }); + previewLayer(layerDescriptor); + }; + + return ( + <CreateSourceEditor + requestType={RENDER_AS.POINT} + onSourceConfigChange={onSourceConfigChange} + /> + ); + }, + title: clustersTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 96347c444dd5b..51ee15e7ea5af 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -6,7 +6,7 @@ import { AbstractESAggSource } from '../es_agg_source'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; -import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; +import { GRID_RESOLUTION } from '../../../../common/constants'; export class ESGeoGridSource extends AbstractESAggSource { static createDescriptor({ @@ -14,12 +14,7 @@ export class ESGeoGridSource extends AbstractESAggSource { geoField, requestType, resolution, - }: { - indexPatternId: string; - geoField: string; - requestType: RENDER_AS; - resolution?: GRID_RESOLUTION; - }): ESGeoGridSourceDescriptor; + }: Partial<ESGeoGridSourceDescriptor>): ESGeoGridSourceDescriptor; constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index b9ef13e520bf8..17fad2f2e6453 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -8,39 +8,27 @@ import React from 'react'; import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; -import { HeatmapLayer } from '../../heatmap_layer'; -import { VectorLayer } from '../../vector_layer'; import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; -import { VectorStyle } from '../../styles/vector/vector_style'; -import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { COLOR_GRADIENTS } from '../../styles/color_utils'; -import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { SOURCE_TYPES, DEFAULT_MAX_BUCKETS_LIMIT, - COUNT_PROP_NAME, - COLOR_MAP_TYPE, RENDER_AS, GRID_RESOLUTION, - VECTOR_STYLES, - FIELD_ORIGIN, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { AbstractESAggSource } from '../es_agg_source'; -import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -import { StaticStyleProperty } from '../../styles/vector/properties/static_style_property'; import { DataRequestAbortError } from '../../util/data_request'; import { registerSource } from '../source_registry'; export const MAX_GEOTILE_LEVEL = 29; -const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', { +export const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', { defaultMessage: 'Clusters and grids', }); -const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle', { +export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle', { defaultMessage: 'Heat map', }); @@ -320,87 +308,6 @@ export class ESGeoGridSource extends AbstractESAggSource { return true; } - _createHeatmapLayerDescriptor(options) { - return HeatmapLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - _createVectorLayerDescriptor(options) { - const descriptor = VectorLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - - const defaultDynamicProperties = getDefaultDynamicProperties(); - - descriptor.style = VectorStyle.createDescriptor({ - [VECTOR_STYLES.FILL_COLOR]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - color: COLOR_GRADIENTS[0].value, - type: COLOR_MAP_TYPE.ORDINAL, - }, - }, - [VECTOR_STYLES.LINE_COLOR]: { - type: StaticStyleProperty.type, - options: { - color: '#FFF', - }, - }, - [VECTOR_STYLES.LINE_WIDTH]: { - type: StaticStyleProperty.type, - options: { - size: 0, - }, - }, - [VECTOR_STYLES.ICON_SIZE]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - }, - }, - [VECTOR_STYLES.LABEL_TEXT]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - }, - }, - }); - return descriptor; - } - - createDefaultLayer(options) { - if (this._descriptor.requestType === RENDER_AS.HEATMAP) { - return new HeatmapLayer({ - layerDescriptor: this._createHeatmapLayerDescriptor(options), - source: this, - }); - } - - const layerDescriptor = this._createVectorLayerDescriptor(options); - const style = new VectorStyle(layerDescriptor.style, this); - return new VectorLayer({ - layerDescriptor, - source: this, - style, - }); - } - canFormatFeatureProperties() { return true; } @@ -422,57 +329,3 @@ registerSource({ ConstructorFunction: ESGeoGridSource, type: SOURCE_TYPES.ES_GEO_GRID, }); - -export const clustersLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.esGridClustersDescription', { - defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = ESGeoGridSource.createDescriptor(sourceConfig); - const source = new ESGeoGridSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return ( - <CreateSourceEditor - requestType={RENDER_AS.POINT} - onSourceConfigChange={onSourceConfigChange} - /> - ); - }, - title: clustersTitle, -}; - -export const heatmapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', { - defaultMessage: 'Geospatial data grouped in grids to show density', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = ESGeoGridSource.createDescriptor(sourceConfig); - const source = new ESGeoGridSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return ( - <CreateSourceEditor - requestType={RENDER_AS.HEATMAP} - onSourceConfigChange={onSourceConfigChange} - /> - ); - }, - title: heatmapTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx new file mode 100644 index 0000000000000..fee1a81a5c63a --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { ESGeoGridSource, heatmapTitle } from './es_geo_grid_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { HeatmapLayer } from '../../heatmap_layer'; +import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; +import { RENDER_AS } from '../../../../common/constants'; + +export const heatmapLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', { + defaultMessage: 'Geospatial data grouped in grids to show density', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: Partial<ESGeoGridSourceDescriptor>) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const layerDescriptor = HeatmapLayer.createDescriptor({ + sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); + }; + + return ( + <CreateSourceEditor + requestType={RENDER_AS.HEATMAP} + onSourceConfigChange={onSourceConfigChange} + /> + ); + }, + title: heatmapTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js index c2fa2356b1a3e..2db66ff411627 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - ESGeoGridSource, - clustersLayerWizardConfig, - heatmapLayerWizardConfig, -} from './es_geo_grid_source'; +export { clustersLayerWizardConfig } from './clusters_layer_wizard'; +export { ESGeoGridSource } from './es_geo_grid_source'; +export { heatmapLayerWizardConfig } from './heatmap_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 57e5afb99404b..0d15cff032410 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -8,29 +8,18 @@ import React from 'react'; import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; -import { VectorLayer } from '../../vector_layer'; -import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; -import { VectorStyle } from '../../styles/vector/vector_style'; -import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; import { i18n } from '@kbn/i18n'; -import { - FIELD_ORIGIN, - SOURCE_TYPES, - COUNT_PROP_NAME, - VECTOR_STYLES, -} from '../../../../common/constants'; +import { SOURCE_TYPES } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; import { AbstractESAggSource } from '../es_agg_source'; -import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -import { COLOR_GRADIENTS } from '../../styles/color_utils'; import { indexPatterns } from '../../../../../../../src/plugins/data/public'; import { registerSource } from '../source_registry'; const MAX_GEOTILE_LEVEL = 29; -const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { defaultMessage: 'Point to point', }); @@ -109,43 +98,6 @@ export class ESPewPewSource extends AbstractESAggSource { ]; } - createDefaultLayer(options) { - const defaultDynamicProperties = getDefaultDynamicProperties(); - const styleDescriptor = VectorStyle.createDescriptor({ - [VECTOR_STYLES.LINE_COLOR]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - color: COLOR_GRADIENTS[0].value, - }, - }, - [VECTOR_STYLES.LINE_WIDTH]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - }, - }, - }); - - return new VectorLayer({ - layerDescriptor: VectorLayer.createDescriptor({ - ...options, - sourceDescriptor: this._descriptor, - style: styleDescriptor, - }), - source: this, - style: new VectorStyle(styleDescriptor, this), - }); - } - getGeoGridPrecision(zoom) { const targetGeotileLevel = Math.ceil(zoom) + 2; return Math.min(targetGeotileLevel, MAX_GEOTILE_LEVEL); @@ -234,25 +186,3 @@ registerSource({ ConstructorFunction: ESPewPewSource, type: SOURCE_TYPES.ES_PEW_PEW, }); - -export const point2PointLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.pewPewDescription', { - defaultMessage: 'Aggregated data paths between the source and destination', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = ESPewPewSource.createDescriptor(sourceConfig); - const source = new ESPewPewSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js new file mode 100644 index 0000000000000..fabde578085ab --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ESPewPewSource } from './es_pew_pew_source'; +export { point2PointLayerWizardConfig } from './point_2_point_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx new file mode 100644 index 0000000000000..3ad6d64903d4a --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; +import { VectorLayer } from '../../vector_layer'; +// @ts-ignore +import { ESPewPewSource, sourceTitle } from './es_pew_pew_source'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { + FIELD_ORIGIN, + COUNT_PROP_NAME, + VECTOR_STYLES, + STYLE_TYPE, +} from '../../../../common/constants'; +// @ts-ignore +import { COLOR_GRADIENTS } from '../../styles/color_utils'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types'; + +export const point2PointLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.pewPewDescription', { + defaultMessage: 'Aggregated data paths between the source and destination', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const defaultDynamicProperties = getDefaultDynamicProperties(); + const layerDescriptor = VectorLayer.createDescriptor({ + sourceDescriptor: ESPewPewSource.createDescriptor(sourceConfig), + style: VectorStyle.createDescriptor({ + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR]! + .options as ColorDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + color: COLOR_GRADIENTS[0].value, + }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH]! + .options as SizeDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + }, + }, + }), + }); + previewLayer(layerDescriptor); + }; + + return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx new file mode 100644 index 0000000000000..4a775dd78f787 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { ESSearchSource, sourceTitle } from './es_search_source'; +import { BlendedVectorLayer } from '../../blended_vector_layer'; +import { VectorLayer } from '../../vector_layer'; +import { SCALING_TYPES } from '../../../../common/constants'; + +export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) { + const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); + + return sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS + ? BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors) + : VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); +} + +export const esDocumentsLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.esSearchDescription', { + defaultMessage: 'Vector data from a Kibana index pattern', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + previewLayer(createDefaultLayerDescriptor(sourceConfig, mapColors)); + }; + return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts index c904280a38c85..23e3c759d73c3 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts @@ -8,6 +8,8 @@ import { AbstractESSource } from '../es_source'; import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; export class ESSearchSource extends AbstractESSource { + static createDescriptor(sourceConfig: unknown): ESSearchSourceDescriptor; + constructor(sourceDescriptor: Partial<ESSearchSourceDescriptor>, inspectorAdapters: unknown); getFieldNames(): string[]; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 96679f0e85941..a412c49faceac 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -6,15 +6,11 @@ import _ from 'lodash'; import React from 'react'; -import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { AbstractESSource } from '../es_source'; -import { SearchSource } from '../../../kibana_services'; -import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../../vector_layer'; +import { getSearchService } from '../../../kibana_services'; import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; -import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { SOURCE_TYPES, @@ -27,14 +23,14 @@ import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { getSourceFields } from '../../../index_pattern_util'; import { loadIndexSettings } from './load_index_settings'; -import { BlendedVectorLayer } from '../../blended_vector_layer'; +import uuid from 'uuid/v4'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; import { getField, addFieldToDSL } from '../../util/es_agg_utils'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); @@ -71,56 +67,31 @@ function getDocValueAndSourceFields(indexPattern, fieldNames) { export class ESSearchSource extends AbstractESSource { static type = SOURCE_TYPES.ES_SEARCH; + static createDescriptor(descriptor) { + return { + ...descriptor, + id: descriptor.id ? descriptor.id : uuid(), + type: ESSearchSource.type, + indexPatternId: descriptor.indexPatternId, + geoField: descriptor.geoField, + filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS), + tooltipProperties: _.get(descriptor, 'tooltipProperties', []), + sortField: _.get(descriptor, 'sortField', ''), + sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC), + scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT), + topHitsSplitField: descriptor.topHitsSplitField, + topHitsSize: _.get(descriptor, 'topHitsSize', 1), + }; + } + constructor(descriptor, inspectorAdapters) { - super( - { - ...descriptor, - id: descriptor.id, - type: ESSearchSource.type, - indexPatternId: descriptor.indexPatternId, - geoField: descriptor.geoField, - filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS), - tooltipProperties: _.get(descriptor, 'tooltipProperties', []), - sortField: _.get(descriptor, 'sortField', ''), - sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC), - scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT), - topHitsSplitField: descriptor.topHitsSplitField, - topHitsSize: _.get(descriptor, 'topHitsSize', 1), - }, - inspectorAdapters - ); + super(ESSearchSource.createDescriptor(descriptor), inspectorAdapters); this._tooltipFields = this._descriptor.tooltipProperties.map(property => this.createField({ fieldName: property }) ); } - createDefaultLayer(options, mapColors) { - if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { - const layerDescriptor = BlendedVectorLayer.createDescriptor( - { - sourceDescriptor: this._descriptor, - ...options, - }, - mapColors - ); - const style = new VectorStyle(layerDescriptor.style, this); - return new BlendedVectorLayer({ - layerDescriptor: layerDescriptor, - source: this, - style, - }); - } - - const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); - const style = new VectorStyle(layerDescriptor.style, this); - return new VectorLayer({ - layerDescriptor: layerDescriptor, - source: this, - style, - }); - } - createField({ fieldName }) { return new ESDocField({ fieldName, @@ -387,11 +358,21 @@ export class ESSearchSource extends AbstractESSource { }); return properties; }; + const epochMillisFields = searchFilters.fieldNames.filter(fieldName => { + const field = getField(indexPattern, fieldName); + return field.readFromDocValues && field.type === 'date'; + }); let featureCollection; try { const geoField = await this._getGeoField(); - featureCollection = hitsToGeoJson(hits, flattenHit, geoField.name, geoField.type); + featureCollection = hitsToGeoJson( + hits, + flattenHit, + geoField.name, + geoField.type, + epochMillisFields + ); } catch (error) { throw new Error( i18n.translate('xpack.maps.source.esSearch.convertToGeoJsonErrorMsg', { @@ -417,13 +398,17 @@ export class ESSearchSource extends AbstractESSource { return {}; } - const searchSource = new SearchSource(); + const searchService = getSearchService(); + const searchSource = searchService.searchSource.create(); + searchSource.setField('index', indexPattern); searchSource.setField('size', 1); + const query = { language: 'kuery', query: `_id:"${docId}" and _index:"${index}"`, }; + searchSource.setField('query', query); searchSource.setField('fields', this._getTooltipPropertyNames()); @@ -572,29 +557,3 @@ registerSource({ ConstructorFunction: ESSearchSource, type: SOURCE_TYPES.ES_SEARCH, }); - -export const esDocumentsLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.esSearchDescription', { - defaultMessage: 'Vector data from a Kibana index pattern', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const source = new ESSearchSource( - { - id: uuid(), - ...sourceConfig, - }, - inspectorAdapters - ); - onPreviewSource(source); - }; - return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts deleted file mode 100644 index 66cc2ddd85404..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts +++ /dev/null @@ -1,33 +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. - */ -jest.mock('ui/new_platform'); -jest.mock('../../../kibana_services'); - -import { ESSearchSource } from './es_search_source'; -import { VectorLayer } from '../../vector_layer'; -import { SCALING_TYPES, SOURCE_TYPES } from '../../../../common/constants'; -import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; - -const descriptor: ESSearchSourceDescriptor = { - type: SOURCE_TYPES.ES_SEARCH, - id: '1234', - indexPatternId: 'myIndexPattern', - geoField: 'myLocation', - scalingType: SCALING_TYPES.LIMIT, -}; - -describe('ES Search Source', () => { - beforeEach(() => { - require('../../../kibana_services').getUiSettings = () => ({ - get: jest.fn(), - }); - }); - it('should create a vector layer', () => { - const source = new ESSearchSource(descriptor, null); - const layer = source.createDefaultLayer(); - expect(layer instanceof VectorLayer).toEqual(true); - }); -}); diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js b/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js index 2c401ac92567e..6ae327a18b7c2 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ESSearchSource, esDocumentsLayerWizardConfig } from './es_search_source'; +export { ESSearchSource } from './es_search_source'; +export { + createDefaultLayerDescriptor, + esDocumentsLayerWizardConfig, +} from './es_documents_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts index 092dc3bf0d5a8..d95ec5a64e6c3 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts @@ -6,8 +6,10 @@ import { AbstractVectorSource } from '../vector_source'; import { IVectorSource } from '../vector_source'; -import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/public'; +import { IndexPattern, ISearchSource } from '../../../../../../../src/plugins/data/public'; import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; export interface IESSource extends IVectorSource { getId(): string; @@ -19,7 +21,14 @@ export interface IESSource extends IVectorSource { searchFilters: VectorSourceRequestMeta, limit: number, initialSearchContext?: object - ): Promise<SearchSource>; + ): Promise<ISearchSource>; + loadStylePropsMeta( + layerName: string, + style: VectorStyle, + dynamicStyleProps: IDynamicStyleProperty[], + registerCancelCallback: (requestToken: symbol, callback: () => void) => void, + searchFilters: VectorSourceRequestMeta + ): Promise<unknown>; } export class AbstractESSource extends AbstractVectorSource implements IESSource { @@ -32,5 +41,12 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource searchFilters: VectorSourceRequestMeta, limit: number, initialSearchContext?: object - ): Promise<SearchSource>; + ): Promise<ISearchSource>; + loadStylePropsMeta( + layerName: string, + style: VectorStyle, + dynamicStyleProps: IDynamicStyleProperty[], + registerCancelCallback: (requestToken: symbol, callback: () => void) => void, + searchFilters: VectorSourceRequestMeta + ): Promise<unknown>; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js index 3402e367cbd73..87733e347aa2a 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js @@ -9,16 +9,15 @@ import { getAutocompleteService, fetchSearchSourceAndRecordWithInspector, getIndexPatternService, - SearchSource, getTimeFilter, + getSearchService, } from '../../../kibana_services'; import { createExtentFilter } from '../../../elasticsearch_geo_utils'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths + import { copyPersistentState } from '../../../reducers/util'; -import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; import { DataRequestAbortError } from '../../util/data_request'; import { expandToTileBoundaries } from '../es_geo_grid_source/geo_tile_utils'; @@ -125,8 +124,9 @@ export class AbstractESSource extends AbstractVectorSource { if (isTimeAware) { allFilters.push(getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters)); } + const searchService = getSearchService(); + const searchSource = searchService.searchSource.create(initialSearchContext); - const searchSource = new SearchSource(initialSearchContext); searchSource.setField('index', indexPattern); searchSource.setField('size', limit); searchSource.setField('filter', allFilters); @@ -135,7 +135,8 @@ export class AbstractESSource extends AbstractVectorSource { } if (searchFilters.sourceQuery) { - const layerSearchSource = new SearchSource(); + const layerSearchSource = searchService.searchSource.create(); + layerSearchSource.setField('index', indexPattern); layerSearchSource.setField('query', searchFilters.sourceQuery); searchSource.setParent(layerSearchSource); @@ -174,9 +175,11 @@ export class AbstractESSource extends AbstractVectorSource { }; } + const minLon = esBounds.top_left.lon; + const maxLon = esBounds.bottom_right.lon; return { - minLon: esBounds.top_left.lon, - maxLon: esBounds.bottom_right.lon, + minLon: minLon > maxLon ? minLon - 360 : minLon, + maxLon, minLat: esBounds.bottom_right.lat, maxLat: esBounds.top_left.lat, }; @@ -221,9 +224,7 @@ export class AbstractESSource extends AbstractVectorSource { async supportsFitToBounds() { try { const geoField = await this._getGeoField(); - // geo_bounds aggregation only supports geo_point - // there is currently no backend support for getting bounding box of geo_shape field - return geoField.type !== ES_GEO_FIELD_TYPE.GEO_SHAPE; + return geoField.aggregatable; } catch (error) { return false; } @@ -294,7 +295,9 @@ export class AbstractESSource extends AbstractVectorSource { }, {}); const indexPattern = await this.getIndexPattern(); - const searchSource = new SearchSource(); + const searchService = getSearchService(); + const searchSource = searchService.searchSource.create(); + searchSource.setField('index', indexPattern); searchSource.setField('size', 0); searchSource.setField('aggs', aggs); diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts index 701bd5e2c8b5e..248ca2b9212b4 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts @@ -9,4 +9,5 @@ import { IESAggSource } from '../es_agg_source'; export interface IESTermSource extends IESAggSource { getTermField(): IField; + hasCompleteConfig(): boolean; } diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js index 00c3bfc5f17c6..6e9d7ad1a613b 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { KibanaRegionmapSource, kibanaRegionMapLayerWizardConfig } from './kibana_regionmap_source'; +export { kibanaRegionMapLayerWizardConfig } from './kibana_regionmap_layer_wizard'; +export { KibanaRegionmapSource } from './kibana_regionmap_source'; 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 new file mode 100644 index 0000000000000..a9adec2bda2c8 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +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', + }), + icon: 'logoKibana', + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig); + const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayer(layerDescriptor); + }; + + return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index be333f8ee85a4..fb5a2e4f42f1d 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -5,8 +5,6 @@ */ import { AbstractVectorSource } from '../vector_source'; -import React from 'react'; -import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -14,7 +12,7 @@ import { FIELD_ORIGIN, SOURCE_TYPES } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { defaultMessage: 'Configured GeoJSON', }); @@ -101,20 +99,3 @@ registerSource({ ConstructorFunction: KibanaRegionmapSource, type: SOURCE_TYPES.REGIONMAP_FILE, }); - -export const kibanaRegionMapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { - defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', - }), - icon: 'logoKibana', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig); - const source = new KibanaRegionmapSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js index 9fd7f088032ca..cc89091605456 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { KibanaTilemapSource, kibanaBasemapLayerWizardConfig } from './kibana_tilemap_source'; +export { kibanaBasemapLayerWizardConfig } from './kibana_base_map_layer_wizard'; +export { KibanaTilemapSource } from './kibana_tilemap_source'; 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 new file mode 100644 index 0000000000000..141fabeedd3e5 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +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', + }), + icon: 'logoKibana', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = () => { + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: KibanaTilemapSource.createDescriptor(), + }); + previewLayer(layerDescriptor); + }; + return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js index bbb653eff32e2..7dc1d454a1c52 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js @@ -3,10 +3,8 @@ * 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 { AbstractTMSSource } from '../tms_source'; -import { TileLayer } from '../../tile_layer'; -import { CreateSourceEditor } from './create_source_editor'; import { getKibanaTileMap } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -14,7 +12,7 @@ import _ from 'lodash'; import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.kbnTMSTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.kbnTMSTitle', { defaultMessage: 'Configured Tile Map Service', }); @@ -42,20 +40,6 @@ export class KibanaTilemapSource extends AbstractTMSSource { ]; } - _createDefaultLayerDescriptor(options) { - return TileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - createDefaultLayer(options) { - return new TileLayer({ - layerDescriptor: this._createDefaultLayerDescriptor(options), - source: this, - }); - } - async getUrlTemplate() { const tilemap = getKibanaTileMap(); if (!tilemap.url) { @@ -88,19 +72,3 @@ registerSource({ ConstructorFunction: KibanaTilemapSource, type: SOURCE_TYPES.KIBANA_TILEMAP, }); - -export const kibanaBasemapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.kbnTMSDescription', { - defaultMessage: 'Tile map service configured in kibana.yml', - }), - icon: 'logoKibana', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = () => { - const sourceDescriptor = KibanaTilemapSource.createDescriptor(); - const source = new KibanaTilemapSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - return <CreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx index dfdea1489d50c..c94fec3deac67 100644 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -12,30 +12,20 @@ import { } from './mvt_single_layer_vector_source_editor'; import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; -import { SOURCE_TYPES } from '../../../../common/constants'; +import { TiledVectorLayer } from '../../tiled_vector_layer'; export const mvtVectorSourceWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { defaultMessage: 'Vector source wizard', }), icon: 'grid', - renderWizard: ({ onPreviewSource, inspectorAdapters }: RenderWizardArguments) => { - const onSourceConfigChange = ({ - urlTemplate, - layerName, - minSourceZoom, - maxSourceZoom, - }: MVTSingleLayerVectorSourceConfig) => { - const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor({ - urlTemplate, - layerName, - minSourceZoom, - maxSourceZoom, - type: SOURCE_TYPES.MVT_SINGLE_LAYER, - }); - const source = new MVTSingleLayerVectorSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { + const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); + const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayer(layerDescriptor); }; + return <MVTSingleLayerVectorSourceEditor onSourceConfigChange={onSourceConfigChange} />; }, title: sourceTitle, diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 0bfda6be72203..58e6e39aaa1f9 100644 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { AbstractSource, ImmutableSourceProperty } from '../source'; -import { TiledVectorLayer } from '../../tiled_vector_layer'; import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; @@ -15,13 +14,12 @@ import { IField } from '../../fields/field'; import { registerSource } from '../source_registry'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { - LayerDescriptor, MapExtent, TiledSingleLayerVectorSourceDescriptor, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; -import { VectorLayerArguments } from '../../vector_layer'; +import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor'; export const sourceTitle = i18n.translate( 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', @@ -37,7 +35,7 @@ export class MVTSingleLayerVectorSource extends AbstractSource layerName, minSourceZoom, maxSourceZoom, - }: TiledSingleLayerVectorSourceDescriptor) { + }: MVTSingleLayerVectorSourceConfig) { return { type: SOURCE_TYPES.MVT_SINGLE_LAYER, id: uuid(), @@ -66,19 +64,6 @@ export class MVTSingleLayerVectorSource extends AbstractSource return []; } - createDefaultLayer(options: LayerDescriptor): TiledVectorLayer { - const layerDescriptor = { - sourceDescriptor: this._descriptor, - ...options, - }; - const normalizedLayerDescriptor = TiledVectorLayer.createDescriptor(layerDescriptor, []); - const vectorLayerArguments: VectorLayerArguments = { - layerDescriptor: normalizedLayerDescriptor, - source: this, - }; - return new TiledVectorLayer(vectorLayerArguments); - } - getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], diff --git a/x-pack/plugins/maps/public/layers/sources/source.d.ts b/x-pack/plugins/maps/public/layers/sources/source.d.ts deleted file mode 100644 index 5a01da02adaae..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/source.d.ts +++ /dev/null @@ -1,56 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -import { AbstractSourceDescriptor, LayerDescriptor } from '../../../common/descriptor_types'; -import { ILayer } from '../layer'; - -export type ImmutableSourceProperty = { - label: string; - value: string; -}; - -export type Attribution = { - url: string; - label: string; -}; - -export interface ISource { - createDefaultLayer(options?: LayerDescriptor): ILayer; - destroy(): void; - getDisplayName(): Promise<string>; - getInspectorAdapters(): object; - isFieldAware(): boolean; - isFilterByMapBounds(): boolean; - isGeoGridPrecisionAware(): boolean; - isQueryAware(): boolean; - isRefreshTimerAware(): Promise<boolean>; - isTimeAware(): Promise<boolean>; - getImmutableProperties(): Promise<ImmutableSourceProperty[]>; - getAttributions(): Promise<Attribution[]>; - getMinZoom(): number; - getMaxZoom(): number; -} - -export class AbstractSource implements ISource { - readonly _descriptor: AbstractSourceDescriptor; - constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters?: object); - - destroy(): void; - createDefaultLayer(options?: LayerDescriptor, mapColors?: string[]): ILayer; - getDisplayName(): Promise<string>; - getInspectorAdapters(): object; - isFieldAware(): boolean; - isFilterByMapBounds(): boolean; - isGeoGridPrecisionAware(): boolean; - isQueryAware(): boolean; - isRefreshTimerAware(): Promise<boolean>; - isTimeAware(): Promise<boolean>; - getImmutableProperties(): Promise<ImmutableSourceProperty[]>; - getAttributions(): Promise<Attribution[]>; - getMinZoom(): number; - getMaxZoom(): number; -} diff --git a/x-pack/plugins/maps/public/layers/sources/source.js b/x-pack/plugins/maps/public/layers/sources/source.js deleted file mode 100644 index 555b8999d6284..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/source.js +++ /dev/null @@ -1,160 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../../reducers/util'; -import { MIN_ZOOM, MAX_ZOOM } from '../../../common/constants'; - -export class AbstractSource { - static isIndexingSource = false; - - static renderEditor() { - throw new Error('Must implement Source.renderEditor'); - } - - static createDescriptor() { - throw new Error('Must implement Source.createDescriptor'); - } - - constructor(descriptor, inspectorAdapters) { - this._descriptor = descriptor; - this._inspectorAdapters = inspectorAdapters; - } - - destroy() {} - - cloneDescriptor() { - return copyPersistentState(this._descriptor); - } - - async supportsFitToBounds() { - return true; - } - - /** - * return list of immutable source properties. - * Immutable source properties are properties that can not be edited by the user. - */ - async getImmutableProperties() { - return []; - } - - getInspectorAdapters() { - return this._inspectorAdapters; - } - - _createDefaultLayerDescriptor() { - throw new Error(`Source#createDefaultLayerDescriptor not implemented`); - } - - createDefaultLayer() { - throw new Error(`Source#createDefaultLayer not implemented`); - } - - async getDisplayName() { - console.warn('Source should implement Source#getDisplayName'); - return ''; - } - - /** - * return attribution for this layer as array of objects with url and label property. - * e.g. [{ url: 'example.com', label: 'foobar' }] - * @return {Promise<null>} - */ - async getAttributions() { - return []; - } - - isFieldAware() { - return false; - } - - isRefreshTimerAware() { - return false; - } - - isGeoGridPrecisionAware() { - return false; - } - - async isTimeAware() { - return false; - } - - getFieldNames() { - return []; - } - - hasCompleteConfig() { - throw new Error(`Source#hasCompleteConfig not implemented`); - } - - renderSourceSettingsEditor() { - return null; - } - - getApplyGlobalQuery() { - return !!this._descriptor.applyGlobalQuery; - } - - getIndexPatternIds() { - return []; - } - - getQueryableIndexPatternIds() { - return []; - } - - isFilterByMapBounds() { - return false; - } - - isQueryAware() { - return false; - } - - getGeoGridPrecision() { - return 0; - } - - isJoinable() { - return false; - } - - shouldBeIndexed() { - return AbstractSource.isIndexingSource; - } - - isESSource() { - return false; - } - - // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes - async getPreIndexedShape(/* properties */) { - return null; - } - - // Returns function used to format value - async createFieldFormatter(/* field */) { - return null; - } - - async loadStylePropsMeta() { - throw new Error(`Source#loadStylePropsMeta not implemented`); - } - - async getValueSuggestions(/* field, query */) { - return []; - } - - getMinZoom() { - return MIN_ZOOM; - } - - getMaxZoom() { - return MAX_ZOOM; - } -} diff --git a/x-pack/plugins/maps/public/layers/sources/source.ts b/x-pack/plugins/maps/public/layers/sources/source.ts new file mode 100644 index 0000000000000..af934d7464f61 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/source.ts @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { ReactElement } from 'react'; + +import { Adapters } from 'src/plugins/inspector/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +// @ts-ignore +import { copyPersistentState } from '../../reducers/util'; + +import { SourceDescriptor } from '../../../common/descriptor_types'; +import { IField } from '../fields/field'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; + +export type ImmutableSourceProperty = { + label: string; + value: string; +}; + +export type Attribution = { + url: string; + label: string; +}; + +export type PreIndexedShape = { + index: string; + id: string | number; + path: string; +}; + +export type FieldFormatter = (value: string | number | null | undefined | boolean) => string; + +export interface ISource { + destroy(): void; + getDisplayName(): Promise<string>; + getInspectorAdapters(): Adapters | undefined; + isFieldAware(): boolean; + isFilterByMapBounds(): boolean; + isGeoGridPrecisionAware(): boolean; + isQueryAware(): boolean; + isRefreshTimerAware(): boolean; + isTimeAware(): Promise<boolean>; + getImmutableProperties(): Promise<ImmutableSourceProperty[]>; + getAttributions(): Promise<Attribution[]>; + isESSource(): boolean; + renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement<any> | null; + supportsFitToBounds(): Promise<boolean>; + isJoinable(): boolean; + cloneDescriptor(): SourceDescriptor; + getFieldNames(): string[]; + getApplyGlobalQuery(): boolean; + getIndexPatternIds(): string[]; + getQueryableIndexPatternIds(): string[]; + getGeoGridPrecision(zoom: number): number; + getPreIndexedShape(): Promise<PreIndexedShape | null>; + createFieldFormatter(field: IField): Promise<FieldFormatter | null>; + getValueSuggestions(field: IField, query: string): Promise<string[]>; + getMinZoom(): number; + getMaxZoom(): number; +} + +export class AbstractSource implements ISource { + readonly _descriptor: SourceDescriptor; + readonly _inspectorAdapters?: Adapters | undefined; + + constructor(descriptor: SourceDescriptor, inspectorAdapters?: Adapters) { + this._descriptor = descriptor; + this._inspectorAdapters = inspectorAdapters; + } + + destroy(): void {} + + cloneDescriptor(): SourceDescriptor { + // @ts-ignore + return copyPersistentState(this._descriptor); + } + + async supportsFitToBounds(): Promise<boolean> { + return true; + } + + /** + * return list of immutable source properties. + * Immutable source properties are properties that can not be edited by the user. + */ + async getImmutableProperties(): Promise<ImmutableSourceProperty[]> { + return []; + } + + getInspectorAdapters(): Adapters | undefined { + return this._inspectorAdapters; + } + + async getDisplayName(): Promise<string> { + return ''; + } + + async getAttributions(): Promise<Attribution[]> { + return []; + } + + isFieldAware(): boolean { + return false; + } + + isRefreshTimerAware(): boolean { + return false; + } + + isGeoGridPrecisionAware(): boolean { + return false; + } + + isQueryAware(): boolean { + return false; + } + + getFieldNames(): string[] { + return []; + } + + renderSourceSettingsEditor() { + return null; + } + + getApplyGlobalQuery(): boolean { + return !!this._descriptor.applyGlobalQuery; + } + + getIndexPatternIds(): string[] { + return []; + } + + getQueryableIndexPatternIds(): string[] { + return []; + } + + getGeoGridPrecision(zoom: number): number { + return 0; + } + + isJoinable(): boolean { + return false; + } + + isESSource(): boolean { + return false; + } + + // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes + async getPreIndexedShape(/* properties */): Promise<PreIndexedShape | null> { + return null; + } + + // Returns function used to format value + async createFieldFormatter(field: IField): Promise<FieldFormatter | null> { + return null; + } + + async getValueSuggestions(field: IField, query: string): Promise<string[]> { + return []; + } + + async isTimeAware(): Promise<boolean> { + return false; + } + + isFilterByMapBounds(): boolean { + return false; + } + + getMinZoom() { + return MIN_ZOOM; + } + + getMaxZoom() { + return MAX_ZOOM; + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js index 509584cbc415a..12e1780d9cad5 100644 --- a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { VectorLayer } from '../../vector_layer'; import { TooltipProperty } from '../../tooltips/tooltip_property'; -import { VectorStyle } from '../../styles/vector/vector_style'; import { AbstractSource } from './../source'; import * as topojson from 'topojson-client'; import _ from 'lodash'; @@ -74,30 +72,10 @@ export class AbstractVectorSource extends AbstractSource { return this.createField({ fieldName: name }); } - _createDefaultLayerDescriptor(options, mapColors) { - return VectorLayer.createDescriptor( - { - sourceDescriptor: this._descriptor, - ...options, - }, - mapColors - ); - } - _getTooltipPropertyNames() { return this._tooltipFields.map(field => field.getName()); } - createDefaultLayer(options, mapColors) { - const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); - const style = new VectorStyle(layerDescriptor.style, this); - return new VectorLayer({ - layerDescriptor: layerDescriptor, - source: this, - style, - }); - } - isFilterByMapBounds() { return false; } diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/index.js b/x-pack/plugins/maps/public/layers/sources/wms_source/index.js index daae552a6f772..792e7e862826c 100644 --- a/x-pack/plugins/maps/public/layers/sources/wms_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/wms_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { WMSSource, wmsLayerWizardConfig } from './wms_source'; +export { wmsLayerWizardConfig } from './wms_layer_wizard'; +export { WMSSource } from './wms_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx new file mode 100644 index 0000000000000..fbf5e25c78b17 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx @@ -0,0 +1,36 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +// @ts-ignore +import { WMSCreateSourceEditor } from './wms_create_source_editor'; +// @ts-ignore +import { sourceTitle, WMSSource } from './wms_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { TileLayer } from '../../tile_layer'; + +export const wmsLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.wmsDescription', { + defaultMessage: 'Maps from OGC Standard WMS', + }), + icon: 'grid', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: WMSSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); + }; + return <WMSCreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js index 33f764784124e..cb8f9c34e2b57 100644 --- a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js @@ -4,18 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - import { AbstractTMSSource } from '../tms_source'; -import { TileLayer } from '../../tile_layer'; -import { WMSCreateSourceEditor } from './wms_create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { WmsClient } from './wms_client'; import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.wmsTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.wmsTitle', { defaultMessage: 'Web Map Service', }); @@ -52,20 +48,6 @@ export class WMSSource extends AbstractTMSSource { ]; } - _createDefaultLayerDescriptor(options) { - return TileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - createDefaultLayer(options) { - return new TileLayer({ - layerDescriptor: this._createDefaultLayerDescriptor(options), - source: this, - }); - } - async getDisplayName() { return this._descriptor.serviceUrl; } @@ -94,24 +76,3 @@ registerSource({ ConstructorFunction: WMSSource, type: SOURCE_TYPES.WMS, }); - -export const wmsLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.wmsDescription', { - defaultMessage: 'Maps from OGC Standard WMS', - }), - icon: 'grid', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = WMSSource.createDescriptor(sourceConfig); - const source = new WMSSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - return <WMSCreateSourceEditor onSourceConfigChange={onSourceConfigChange} />; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx index 8b1ed588c8dd1..e970c75fa7adf 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx @@ -9,17 +9,19 @@ import React from 'react'; import { XYZTMSEditor, XYZTMSSourceConfig } from './xyz_tms_editor'; import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { TileLayer } from '../../tile_layer'; export const tmsLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.ems_xyzDescription', { defaultMessage: 'Tile map service configured in interface', }), icon: 'grid', - renderWizard: ({ onPreviewSource }: RenderWizardArguments) => { + renderWizard: ({ previewLayer }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { - const sourceDescriptor = XYZTMSSource.createDescriptor(sourceConfig); - const source = new XYZTMSSource(sourceDescriptor); - onPreviewSource(source); + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); }; return <XYZTMSEditor onSourceConfigChange={onSourceConfigChange} />; }, diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts index 4031a18bff7cb..b1ba6fc6f6f8e 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts @@ -5,8 +5,6 @@ */ import { XYZTMSSource } from './xyz_tms_source'; -import { ILayer } from '../../layer'; -import { TileLayer } from '../../tile_layer'; import { SOURCE_TYPES } from '../../../../common/constants'; import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; @@ -16,12 +14,6 @@ const descriptor: XYZTMSSourceDescriptor = { id: 'foobar', }; describe('xyz Tilemap Source', () => { - it('should create a tile-layer', () => { - const source = new XYZTMSSource(descriptor); - const layer: ILayer = source.createDefaultLayer(); - expect(layer instanceof TileLayer).toEqual(true); - }); - it('should echo url template for url template', async () => { const source = new XYZTMSSource(descriptor); const template = await source.getUrlTemplate(); diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts index 8b64480f92961..b1003d25fb759 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts @@ -5,12 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { TileLayer } from '../../tile_layer'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; import { AbstractTMSSource } from '../tms_source'; -import { LayerDescriptor, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; +import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; import { Attribution, ImmutableSourceProperty } from '../source'; import { XYZTMSSourceConfig } from './xyz_tms_editor'; @@ -48,17 +47,6 @@ export class XYZTMSSource extends AbstractTMSSource { ]; } - createDefaultLayer(options?: LayerDescriptor): TileLayer { - const layerDescriptor: LayerDescriptor = TileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - return new TileLayer({ - layerDescriptor, - source: this, - }); - } - async getDisplayName(): Promise<string> { return this._descriptor.urlTemplate; } diff --git a/x-pack/plugins/maps/public/layers/styles/abstract_style.js b/x-pack/plugins/maps/public/layers/styles/abstract_style.js deleted file mode 100644 index 3e7a3dbf7ed20..0000000000000 --- a/x-pack/plugins/maps/public/layers/styles/abstract_style.js +++ /dev/null @@ -1,29 +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. - */ - -export class AbstractStyle { - getDescriptorWithMissingStylePropsRemoved(/* nextOrdinalFields */) { - return { - hasChanges: false, - }; - } - - async pluckStyleMetaFromSourceDataRequest(/* sourceDataRequest */) { - return {}; - } - - getDescriptor() { - return this._descriptor; - } - - renderEditor(/* { layer, onStyleDescriptorChange } */) { - return null; - } - - getSourceFieldNames() { - return []; - } -} diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index d769fe0da9ec2..1fa24943c5e51 100644 --- a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { AbstractStyle } from '../abstract_style'; +import { AbstractStyle } from '../style'; import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; diff --git a/x-pack/plugins/maps/public/layers/styles/style.ts b/x-pack/plugins/maps/public/layers/styles/style.ts new file mode 100644 index 0000000000000..38fdc36904412 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/styles/style.ts @@ -0,0 +1,59 @@ +/* + * 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 { ReactElement } from 'react'; +import { StyleDescriptor, StyleMetaDescriptor } from '../../../common/descriptor_types'; +import { ILayer } from '../layer'; +import { IField } from '../fields/field'; +import { DataRequest } from '../util/data_request'; + +export interface IStyle { + getDescriptor(): StyleDescriptor | null; + getDescriptorWithMissingStylePropsRemoved( + nextFields: IField[] + ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor }; + pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor; + renderEditor({ + layer, + onStyleDescriptorChange, + }: { + layer: ILayer; + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement<any> | null; + getSourceFieldNames(): string[]; +} + +export class AbstractStyle implements IStyle { + readonly _descriptor: StyleDescriptor | null; + + constructor(descriptor: StyleDescriptor | null) { + this._descriptor = descriptor; + } + + getDescriptorWithMissingStylePropsRemoved( + nextFields: IField[] + ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor } { + return { + hasChanges: false, + }; + } + + pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor { + return { fieldMeta: {} }; + } + + getDescriptor(): StyleDescriptor | null { + return this._descriptor; + } + + renderEditor(/* { layer, onStyleDescriptorChange } */) { + return null; + } + + getSourceFieldNames(): string[] { + return []; + } +} diff --git a/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts new file mode 100644 index 0000000000000..f658d0821edf2 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts @@ -0,0 +1,16 @@ +/* + * 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 { AbstractStyle } from '../style'; +import { LAYER_STYLE_TYPE } from '../../../../common/constants'; + +export class TileStyle extends AbstractStyle { + constructor() { + super({ + type: LAYER_STYLE_TYPE.TILE, + }); + } +} diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index ea521f8749d80..8cef78f9a8f21 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -279,6 +279,10 @@ export class DynamicStyleProperty extends AbstractStyleProperty { } getNumericalMbFeatureStateValue(value) { + if (typeof value === 'number') { + return value; + } + const valueAsFloat = parseFloat(value); return isNaN(valueAsFloat) ? null : valueAsFloat; } diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts index e010d5ac7d7a3..762322b8e09f9 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts @@ -7,24 +7,23 @@ import { IStyleProperty } from './properties/style_property'; import { IDynamicStyleProperty } from './properties/dynamic_style_property'; import { IVectorLayer } from '../../vector_layer'; import { IVectorSource } from '../../sources/vector_source'; +import { AbstractStyle, IStyle } from '../style'; import { VectorStyleDescriptor, VectorStylePropertiesDescriptor, } from '../../../../common/descriptor_types'; -export interface IVectorStyle { +export interface IVectorStyle extends IStyle { getAllStyleProperties(): IStyleProperty[]; - getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; getSourceFieldNames(): string[]; } -export class VectorStyle implements IVectorStyle { +export class VectorStyle extends AbstractStyle implements IVectorStyle { static createDescriptor(properties: VectorStylePropertiesDescriptor): VectorStyleDescriptor; static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor; constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer); getSourceFieldNames(): string[]; getAllStyleProperties(): IStyleProperty[]; - getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; } diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js index b044c98d44d41..5a4edd9c93a05 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { VectorStyleEditor } from './components/vector_style_editor'; import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES } from './vector_style_defaults'; -import { AbstractStyle } from '../abstract_style'; +import { AbstractStyle } from '../style'; import { GEO_JSON_TYPE, FIELD_ORIGIN, @@ -60,6 +60,7 @@ export class VectorStyle extends AbstractStyle { constructor(descriptor = {}, source, layer) { super(); + descriptor = descriptor === null ? {} : descriptor; this._source = source; this._layer = layer; this._descriptor = { diff --git a/x-pack/plugins/maps/public/layers/tile_layer.d.ts b/x-pack/plugins/maps/public/layers/tile_layer.d.ts index 53e8c388ee4c2..8a1ef0f172717 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.d.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractLayer, ILayerArguments } from './layer'; +import { AbstractLayer } from './layer'; import { ITMSSource } from './sources/tms_source'; import { LayerDescriptor } from '../../common/descriptor_types'; -interface ITileLayerArguments extends ILayerArguments { +interface ITileLayerArguments { source: ITMSSource; layerDescriptor: LayerDescriptor; } diff --git a/x-pack/plugins/maps/public/layers/tile_layer.js b/x-pack/plugins/maps/public/layers/tile_layer.js index 2ac60e12d137a..baded3c287637 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/plugins/maps/public/layers/tile_layer.js @@ -6,7 +6,8 @@ import { AbstractLayer } from './layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; +import { TileStyle } from './styles/tile/tile_style'; export class TileLayer extends AbstractLayer { static type = LAYER_TYPE.TILE; @@ -15,9 +16,14 @@ export class TileLayer extends AbstractLayer { const tileLayerDescriptor = super.createDescriptor(options, mapColors); tileLayerDescriptor.type = TileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } + constructor({ source, layerDescriptor }) { + super({ source, layerDescriptor, style: new TileStyle() }); + } + async syncData({ startLoading, stopLoading, onLoadError, dataFilters }) { if (!this.isVisible() || !this.showAtZoomLevel(dataFilters.zoom)) { return; diff --git a/x-pack/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/plugins/maps/public/layers/tile_layer.test.ts index f8c2fd9db60fa..d536b18af4aad 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TileLayer } from './tile_layer'; +// eslint-disable-next-line max-classes-per-file +import { ITileLayerArguments, TileLayer } from './tile_layer'; import { SOURCE_TYPES } from '../../common/constants'; import { XYZTMSSourceDescriptor } from '../../common/descriptor_types'; import { ITMSSource, AbstractTMSSource } from './sources/tms_source'; @@ -22,9 +23,6 @@ class MockTileSource extends AbstractTMSSource implements ITMSSource { super(descriptor, {}); this._descriptor = descriptor; } - createDefaultLayer(): ILayer { - throw new Error('not implemented'); - } async getDisplayName(): Promise<string> { return this._descriptor.urlTemplate; @@ -38,10 +36,13 @@ class MockTileSource extends AbstractTMSSource implements ITMSSource { describe('TileLayer', () => { it('should use display-label from source', async () => { const source = new MockTileSource(sourceDescriptor); - const layer: ILayer = new TileLayer({ + + const args: ITileLayerArguments = { source, layerDescriptor: { id: 'layerid', sourceDescriptor }, - }); + }; + + const layer: ILayer = new TileLayer(args); expect(await source.getDisplayName()).toEqual(await layer.getDisplayName()); }); diff --git a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx index c47cae5641e56..06c5ef579b221 100644 --- a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx @@ -20,7 +20,7 @@ export class TiledVectorLayer extends VectorLayer { static type = LAYER_TYPE.TILED_VECTOR; static createDescriptor( - descriptor: VectorLayerDescriptor, + descriptor: Partial<VectorLayerDescriptor>, mapColors: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); diff --git a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js index 2a4843c78635f..bf1f9de6b2d2b 100644 --- a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js +++ b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -8,101 +8,74 @@ import { canSkipSourceUpdate, updateDueToExtent } from './can_skip_fetch'; import { DataRequest } from './data_request'; describe('updateDueToExtent', () => { - it('should be false when the source is not extent aware', async () => { - const sourceMock = { - isFilterByMapBounds: () => { - return false; - }, + it('should be false when buffers are the same', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, }; - expect(updateDueToExtent(sourceMock)).toBe(false); + const newBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + expect(updateDueToExtent({ buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); }); - describe('source is extent aware', () => { - const sourceMock = { - isFilterByMapBounds: () => { - return true; - }, + it('should be false when the new buffer is contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(updateDueToExtent({ buffer: oldBuffer }, { buffer: newBuffer })).toBe(false); + }); - it('should be false when buffers are the same', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe( - false - ); - }); - - it('should be false when the new buffer is contained in the old buffer', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe( - false - ); - }); - - it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }; - expect( - updateDueToExtent( - sourceMock, - { buffer: oldBuffer, areResultsTrimmed: true }, - { buffer: newBuffer } - ) - ).toBe(true); - }); + it('should be true when the new buffer is contained in the old buffer and the past results were truncated', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect( + updateDueToExtent({ buffer: oldBuffer, areResultsTrimmed: true }, { buffer: newBuffer }) + ).toBe(true); + }); - it('should be true when meta has no old buffer', async () => { - expect(updateDueToExtent(sourceMock)).toBe(true); - }); + it('should be true when meta has no old buffer', async () => { + expect(updateDueToExtent()).toBe(true); + }); - it('should be true when the new buffer is not contained in the old buffer', async () => { - const oldBuffer = { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, - }; - const newBuffer = { - maxLat: 7.5, - maxLon: 92.5, - minLat: -2.5, - minLon: 82.5, - }; - expect(updateDueToExtent(sourceMock, { buffer: oldBuffer }, { buffer: newBuffer })).toBe( - true - ); - }); + it('should be true when the new buffer is not contained in the old buffer', async () => { + const oldBuffer = { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }; + const newBuffer = { + maxLat: 7.5, + maxLon: 92.5, + minLat: -2.5, + minLon: 82.5, + }; + expect(updateDueToExtent({ buffer: oldBuffer }, { buffer: newBuffer })).toBe(true); }); }); diff --git a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts index 758cc35f41fbb..8398bd7af39ad 100644 --- a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts @@ -15,16 +15,7 @@ import { DataRequest } from './data_request'; const SOURCE_UPDATE_REQUIRED = true; const NO_SOURCE_UPDATE_REQUIRED = false; -export function updateDueToExtent( - source: ISource, - prevMeta: DataMeta = {}, - nextMeta: DataMeta = {} -) { - const extentAware = source.isFilterByMapBounds(); - if (!extentAware) { - return NO_SOURCE_UPDATE_REQUIRED; - } - +export function updateDueToExtent(prevMeta: DataMeta = {}, nextMeta: DataMeta = {}) { const { buffer: previousBuffer } = prevMeta; const { buffer: newBuffer } = nextMeta; @@ -134,7 +125,10 @@ export async function canSkipSourceUpdate({ updateDueToPrecisionChange = !_.isEqual(prevMeta.geogridPrecision, nextMeta.geogridPrecision); } - const updateDueToExtentChange = updateDueToExtent(source, prevMeta, nextMeta); + let updateDueToExtentChange = false; + if (extentAware) { + updateDueToExtentChange = updateDueToExtent(prevMeta, nextMeta); + } const updateDueToSourceMetaChange = !_.isEqual(prevMeta.sourceMeta, nextMeta.sourceMeta); diff --git a/x-pack/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/plugins/maps/public/layers/vector_layer.d.ts index 3d5b8054ff3fd..efc1f3011c687 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/vector_layer.d.ts @@ -19,7 +19,7 @@ import { IVectorStyle } from './styles/vector/vector_style'; import { IField } from './fields/field'; import { SyncContext } from '../actions/map_actions'; -type VectorLayerArguments = { +export type VectorLayerArguments = { source: IVectorSource; joins?: IJoin[]; layerDescriptor: VectorLayerDescriptor; @@ -33,14 +33,12 @@ export interface IVectorLayer extends ILayer { } export class VectorLayer extends AbstractLayer implements IVectorLayer { + protected readonly _style: IVectorStyle; static createDescriptor( options: Partial<LayerDescriptor>, mapColors?: string[] ): VectorLayerDescriptor; - protected readonly _source: IVectorSource; - protected readonly _style: IVectorStyle; - constructor(options: VectorLayerArguments); getLayerTypeIconName(): string; getFields(): Promise<IField[]>; diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/layers/vector_layer.js index c5947a63587ea..582e34bce2e98 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_layer.js @@ -175,8 +175,7 @@ export class VectorLayer extends AbstractLayer { } async getBounds(dataFilters) { - const isStaticLayer = - !this.getSource().isBoundsAware() || !this.getSource().isFilterByMapBounds(); + const isStaticLayer = !this.getSource().isBoundsAware(); if (isStaticLayer) { return this._getBoundsBasedOnData(); } @@ -484,6 +483,8 @@ export class VectorLayer extends AbstractLayer { try { startLoading(dataRequestId, requestToken, nextMeta); const layerName = await this.getDisplayName(source); + + //todo: cast source to ESSource when migrating to TS const styleMeta = await source.loadStylePropsMeta( layerName, style, diff --git a/x-pack/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/plugins/maps/public/layers/vector_tile_layer.js index c620ec6c56dc3..fc7812a2c86c7 100644 --- a/x-pack/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_tile_layer.js @@ -6,7 +6,7 @@ import { TileLayer } from './tile_layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; import { isRetina } from '../meta'; import { addSpriteSheetToMapFromImageData, @@ -28,6 +28,7 @@ export class VectorTileLayer extends TileLayer { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = VectorTileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.js b/x-pack/plugins/maps/public/maps_vis_type_alias.js new file mode 100644 index 0000000000000..85613f4608c6f --- /dev/null +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.js @@ -0,0 +1,41 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { APP_ID, APP_ICON, MAP_BASE_URL } from '../common/constants'; +import { getInjectedVarFunc, getVisualizations } from './kibana_services'; + +export function getMapsVisTypeAlias() { + const showMapVisualizationTypes = getInjectedVarFunc()('showMapVisualizationTypes', false); + if (!showMapVisualizationTypes) { + getVisualizations().hideTypes(['region_map', 'tile_map']); + } + + const description = i18n.translate('xpack.maps.visTypeAlias.description', { + defaultMessage: 'Create and style maps with multiple layers and indices.', + }); + + const legacyMapVisualizationWarning = i18n.translate( + 'xpack.maps.visTypeAlias.legacyMapVizWarning', + { + defaultMessage: `Use the Maps app instead of Coordinate Map and Region Map. +The Maps app offers more functionality and is easier to use.`, + } + ); + + return { + aliasUrl: MAP_BASE_URL, + name: APP_ID, + title: i18n.translate('xpack.maps.visTypeAlias.title', { + defaultMessage: 'Maps', + }), + description: showMapVisualizationTypes + ? `${description} ${legacyMapVisualizationWarning}` + : description, + icon: APP_ICON, + stage: 'production', + }; +} diff --git a/x-pack/plugins/maps/public/meta.js b/x-pack/plugins/maps/public/meta.js index d4612554cf00b..c3245e8e98db2 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) diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index d3b9626dc8366..bdcd14ea98782 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -31,12 +31,23 @@ import { setUiActions, setUiSettings, setVisualizations, - // @ts-ignore + setSearchService, } from './kibana_services'; +import { featureCatalogueEntry } from './feature_catalogue_entry'; +// @ts-ignore +import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import { registerLayerWizards } from './layers/load_layer_wizards'; +import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { MapEmbeddableFactory } from './embeddable'; +import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; + home: HomePublicPluginSetup; + visualizations: VisualizationsSetup; + embeddable: EmbeddableSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsPluginStartDependencies {} @@ -61,6 +72,7 @@ export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { setFileUpload(fileUpload); setIndexPatternSelect(data.ui.IndexPatternSelect); setTimeFilter(data.query.timefilter.timefilter); + setSearchService(data.search); setIndexPatternService(data.indexPatterns); setAutocompleteService(data.autocomplete); setCore(core); @@ -94,8 +106,16 @@ export class MapsPlugin MapsPluginStartDependencies > { public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { - plugins.inspector.registerView(MapView); + const { inspector, home, visualizations, embeddable } = plugins; + bindSetupCoreAndPlugins(core, plugins); + + inspector.registerView(MapView); + home.featureCatalogue.register(featureCatalogueEntry); + visualizations.registerAlias(getMapsVisTypeAlias()); + embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); } - public start(core: CoreStart, plugins: any) {} + public start(core: CoreStart, plugins: any) { + bindStartCoreAndPlugins(core, plugins); + } } diff --git a/x-pack/plugins/maps/public/reducers/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts new file mode 100644 index 0000000000000..fe21b37434edd --- /dev/null +++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MAX_ZOOM, MIN_ZOOM } from '../../common/constants'; +import { MapSettings } from './map'; + +export function getDefaultMapSettings(): MapSettings { + return { + maxZoom: MAX_ZOOM, + minZoom: MIN_ZOOM, + showSpatialFilters: true, + spatialFiltersAlpa: 0.3, + spatialFiltersFillColor: '#DA8B45', + spatialFiltersLineColor: '#DA8B45', + }; +} diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 30271d4d5fa8b..be0700d4bdd6d 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -39,6 +39,15 @@ export type MapContext = { hideViewControl: boolean; }; +export type MapSettings = { + maxZoom: number; + minZoom: number; + showSpatialFilters: boolean; + spatialFiltersAlpa: number; + spatialFiltersFillColor: string; + spatialFiltersLineColor: string; +}; + export type MapState = { ready: boolean; mapInitError?: string | null; @@ -49,4 +58,6 @@ export type MapState = { __transientLayerId: string | null; layerList: LayerDescriptor[]; waitingForMapReadyLayerList: LayerDescriptor[]; + settings: MapSettings; + __rollbackSettings: MapSettings | null; }; diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 251a2304538ed..a76267bb7095e 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -46,8 +46,13 @@ import { HIDE_LAYER_CONTROL, HIDE_VIEW_CONTROL, SET_WAITING_FOR_READY_HIDDEN_LAYERS, + SET_MAP_SETTINGS, + ROLLBACK_MAP_SETTINGS, + TRACK_MAP_SETTINGS, + UPDATE_MAP_SETTING, } from '../actions/map_actions'; +import { getDefaultMapSettings } from './default_map_settings'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util'; import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; @@ -124,6 +129,8 @@ const INITIAL_STATE = { __transientLayerId: null, layerList: [], waitingForMapReadyLayerList: [], + settings: getDefaultMapSettings(), + __rollbackSettings: null, }; export function map(state = INITIAL_STATE, action) { @@ -179,6 +186,32 @@ export function map(state = INITIAL_STATE, action) { ...state, goto: null, }; + case SET_MAP_SETTINGS: + return { + ...state, + settings: { ...getDefaultMapSettings(), ...action.settings }, + }; + case ROLLBACK_MAP_SETTINGS: + return state.__rollbackSettings + ? { + ...state, + settings: { ...state.__rollbackSettings }, + __rollbackSettings: null, + } + : state; + case TRACK_MAP_SETTINGS: + return { + ...state, + __rollbackSettings: state.settings, + }; + case UPDATE_MAP_SETTING: + return { + ...state, + settings: { + ...(state.settings ? state.settings : {}), + [action.settingKey]: action.settingValue, + }, + }; case SET_LAYER_ERROR_STATUS: const { layerList } = state; const layerIdx = getLayerIndex(layerList, action.layerId); diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index 7429545ec0e46..537ea7fc7b24b 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -16,13 +16,13 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, UPDATE_INDEXING_STAGE, - // @ts-ignore } from '../actions/ui_actions'; export enum FLYOUT_STATE { NONE = 'NONE', LAYER_PANEL = 'LAYER_PANEL', ADD_LAYER_WIZARD = 'ADD_LAYER_WIZARD', + MAP_SETTINGS_PANEL = 'MAP_SETTINGS_PANEL', } export enum INDEXING_STAGE { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts new file mode 100644 index 0000000000000..bc881d06f62ce --- /dev/null +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AnyAction } from 'redux'; +import { MapCenter } from '../../common/descriptor_types'; +import { MapStoreState } from '../reducers/store'; +import { MapSettings } from '../reducers/map'; +import { IVectorLayer } from '../layers/vector_layer'; + +export function getHiddenLayerIds(state: MapStoreState): string[]; + +export function getMapZoom(state: MapStoreState): number; + +export function getMapCenter(state: MapStoreState): MapCenter; + +export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[]; + +export function getMapSettings(state: MapStoreState): MapSettings; + +export function hasMapSettingsChanges(state: MapStoreState): boolean; + +export function isUsingSearch(state: MapStoreState): boolean; + +export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer; diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js similarity index 76% rename from x-pack/legacy/plugins/maps/public/selectors/map_selectors.js rename to x-pack/plugins/maps/public/selectors/map_selectors.js index 1e71025935519..f43c92d4c9945 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -6,31 +6,26 @@ import { createSelector } from 'reselect'; import _ from 'lodash'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TileLayer } from '../../../../../plugins/maps/public/layers/tile_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { VectorTileLayer } from '../../../../../plugins/maps/public/layers/vector_tile_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { VectorLayer } from '../../../../../plugins/maps/public/layers/vector_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { HeatmapLayer } from '../../../../../plugins/maps/public/layers/heatmap_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BlendedVectorLayer } from '../../../../../plugins/maps/public/layers/blended_vector_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getTimeFilter } from '../../../../../plugins/maps/public/kibana_services'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { TiledVectorLayer } from '../../../../../plugins/maps/public/layers/tiled_vector_layer'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getInspectorAdapters } from '../../../../../plugins/maps/public/reducers/non_serializable_instances'; +import { TileLayer } from '../layers/tile_layer'; +import { VectorTileLayer } from '../layers/vector_tile_layer'; +import { VectorLayer } from '../layers/vector_layer'; +import { HeatmapLayer } from '../layers/heatmap_layer'; +import { BlendedVectorLayer } from '../layers/blended_vector_layer'; +import { getTimeFilter } from '../kibana_services'; +import { getInspectorAdapters } from '../reducers/non_serializable_instances'; +import { TiledVectorLayer } from '../layers/tiled_vector_layer'; +import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; +import { InnerJoin } from '../layers/joins/inner_join'; +import { getSourceByType } from '../layers/sources/source_registry'; +import { GeojsonFileSource } from '../layers/sources/client_file_source'; import { - copyPersistentState, - TRACKED_LAYER_DESCRIPTOR, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/reducers/util'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { InnerJoin } from '../../../../../plugins/maps/public/layers/joins/inner_join'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getSourceByType } from '../../../../../plugins/maps/public/layers/sources/source_registry'; + LAYER_TYPE, + SOURCE_DATA_ID_ORIGIN, + STYLE_TYPE, + VECTOR_STYLES, + SPATIAL_FILTERS_LAYER_ID, +} from '../../common/constants'; +import { extractFeaturesFromFilters } from '../elasticsearch_geo_utils'; function createLayerInstance(layerDescriptor, inspectorAdapters) { const source = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); @@ -68,6 +63,18 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new source.ConstructorFunction(sourceDescriptor, inspectorAdapters); } +export const getMapSettings = ({ map }) => map.settings; + +const getRollbackMapSettings = ({ map }) => map.__rollbackSettings; + +export const hasMapSettingsChanges = createSelector( + getMapSettings, + getRollbackMapSettings, + (settings, rollbackSettings) => { + return rollbackSettings ? !_.isEqual(settings, rollbackSettings) : false; + } +); + export const getOpenTooltips = ({ map }) => { return map && map.openTooltips ? map.openTooltips : []; }; @@ -187,6 +194,53 @@ export const getDataFilters = createSelector( } ); +export const getSpatialFiltersLayer = createSelector( + getFilters, + getMapSettings, + (filters, settings) => { + const featureCollection = { + type: 'FeatureCollection', + features: extractFeaturesFromFilters(filters), + }; + const geoJsonSourceDescriptor = GeojsonFileSource.createDescriptor( + featureCollection, + 'spatialFilters' + ); + + return new VectorLayer({ + layerDescriptor: { + id: SPATIAL_FILTERS_LAYER_ID, + visible: settings.showSpatialFilters, + alpha: settings.spatialFiltersAlpa, + type: LAYER_TYPE.VECTOR, + __dataRequests: [ + { + dataId: SOURCE_DATA_ID_ORIGIN, + data: featureCollection, + }, + ], + style: { + properties: { + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: settings.spatialFiltersFillColor, + }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: settings.spatialFiltersLineColor, + }, + }, + }, + }, + }, + source: new GeojsonFileSource(geoJsonSourceDescriptor), + }); + } +); + export const getLayerList = createSelector( getLayerListRaw, getInspectorAdapters, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/plugins/maps/public/selectors/map_selectors.test.js new file mode 100644 index 0000000000000..fec16251914ea --- /dev/null +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.js @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../layers/vector_layer', () => {}); +jest.mock('../layers/tiled_vector_layer', () => {}); +jest.mock('../layers/blended_vector_layer', () => {}); +jest.mock('../layers/heatmap_layer', () => {}); +jest.mock('../layers/vector_tile_layer', () => {}); +jest.mock('../layers/joins/inner_join', () => {}); +jest.mock('../reducers/non_serializable_instances', () => ({ + getInspectorAdapters: () => { + return {}; + }, +})); +jest.mock('../kibana_services', () => ({ + getTimeFilter: () => ({ + getTime: () => { + return { + to: 'now', + from: 'now-15m', + }; + }, + }), +})); + +import { getTimeFilters } from './map_selectors'; + +describe('getTimeFilters', () => { + it('should return timeFilters when contained in state', () => { + const state = { + map: { + mapState: { + timeFilters: { + to: '2001-01-01', + from: '2001-12-31', + }, + }, + }, + }; + expect(getTimeFilters(state)).toEqual({ to: '2001-01-01', from: '2001-12-31' }); + }); + + it('should return kibana time filters when not contained in state', () => { + const state = { + map: { + mapState: { + timeFilters: null, + }, + }, + }; + expect(getTimeFilters(state)).toEqual({ to: 'now', from: 'now-15m' }); + }); +}); diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts new file mode 100644 index 0000000000000..32d4beeb381d7 --- /dev/null +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MapStoreState } from '../reducers/store'; + +import { FLYOUT_STATE, INDEXING_STAGE } from '../reducers/ui'; + +export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; +export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; +export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; +export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; +export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; +export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getIndexingStage = ({ ui }: MapStoreState): INDEXING_STAGE | null => + ui.importIndexingStage; diff --git a/x-pack/plugins/ml/common/constants/settings.ts b/x-pack/plugins/ml/common/constants/settings.ts new file mode 100644 index 0000000000000..2df2ecd22e078 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/settings.ts @@ -0,0 +1,7 @@ +/* + * 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 const FILE_DATA_VISUALIZER_MAX_FILE_SIZE = 'ml:fileDataVisualizerMaxFileSize'; diff --git a/x-pack/plugins/ml/common/license/index.ts b/x-pack/plugins/ml/common/license/index.ts index e901a9545897b..e87986a26a3bd 100644 --- a/x-pack/plugins/ml/common/license/index.ts +++ b/x-pack/plugins/ml/common/license/index.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MlLicense, LicenseStatus, MINIMUM_FULL_LICENSE, MINIMUM_LICENSE } from './ml_license'; +export { + MlLicense, + LicenseStatus, + MINIMUM_FULL_LICENSE, + MINIMUM_LICENSE, + isFullLicense, + isMinimumLicense, +} from './ml_license'; diff --git a/x-pack/plugins/ml/common/license/ml_license.ts b/x-pack/plugins/ml/common/license/ml_license.ts index 2a60887310447..25b5b4992b227 100644 --- a/x-pack/plugins/ml/common/license/ml_license.ts +++ b/x-pack/plugins/ml/common/license/ml_license.ts @@ -38,8 +38,8 @@ export class MlLicense { this._isSecurityEnabled = securityIsEnabled; this._hasLicenseExpired = this._license.status === 'expired'; this._isMlEnabled = this._license.getFeature(PLUGIN_ID).isEnabled; - this._isMinimumLicense = this._license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; - this._isFullLicense = this._license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; + this._isMinimumLicense = isMinimumLicense(this._license); + this._isFullLicense = isFullLicense(this._license); if (this._initialized === false && postInitFunctions !== undefined) { postInitFunctions.forEach(f => f(this)); @@ -74,3 +74,11 @@ export class MlLicense { return this._isFullLicense; } } + +export function isFullLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; +} + +export function isMinimumLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; +} diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts new file mode 100644 index 0000000000000..572217ce16eee --- /dev/null +++ b/x-pack/plugins/ml/common/types/capabilities.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 { KibanaRequest } from 'kibana/server'; + +export const userMlCapabilities = { + canAccessML: false, + // Anomaly Detection + canGetJobs: false, + canGetDatafeeds: false, + // Calendars + canGetCalendars: false, + // File Data Visualizer + canFindFileStructure: false, + // Data Frame Analytics + canGetDataFrameAnalytics: false, + // Annotations + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, +}; + +export const adminMlCapabilities = { + // Anomaly Detection + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + // Filters + canGetFilters: false, + // Calendars + canCreateCalendar: false, + canDeleteCalendar: false, + // Filters + canCreateFilter: false, + canDeleteFilter: false, + // Data Frame Analytics + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, +}; + +export type UserMlCapabilities = typeof userMlCapabilities; +export type AdminMlCapabilities = typeof adminMlCapabilities; +export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; + +export const basicLicenseMlCapabilities = ['canAccessML', 'canFindFileStructure'] as Array< + keyof MlCapabilities +>; + +export function getDefaultCapabilities(): MlCapabilities { + return { + ...userMlCapabilities, + ...adminMlCapabilities, + }; +} + +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; + isPlatinumOrTrialLicense: boolean; + mlFeatureEnabledInSpace: boolean; +} + +export type ResolveMlCapabilities = (request: KibanaRequest) => Promise<MlCapabilities | null>; diff --git a/x-pack/plugins/ml/common/types/ml_config.ts b/x-pack/plugins/ml/common/types/ml_config.ts deleted file mode 100644 index f2ddadccb2170..0000000000000 --- a/x-pack/plugins/ml/common/types/ml_config.ts +++ /dev/null @@ -1,16 +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 { schema, TypeOf } from '@kbn/config-schema'; -import { MAX_FILE_SIZE } from '../constants/file_datavisualizer'; - -export const configSchema = schema.object({ - file_data_visualizer: schema.object({ - max_file_size: schema.string({ defaultValue: MAX_FILE_SIZE }), - }), -}); - -export type MlConfigType = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/ml/common/types/privileges.ts b/x-pack/plugins/ml/common/types/privileges.ts deleted file mode 100644 index d9089c751b81b..0000000000000 --- a/x-pack/plugins/ml/common/types/privileges.ts +++ /dev/null @@ -1,75 +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. - */ -// - -export interface Privileges { - // Anomaly Detection - canGetJobs: boolean; - canCreateJob: boolean; - canDeleteJob: boolean; - canOpenJob: boolean; - canCloseJob: boolean; - canForecastJob: boolean; - canGetDatafeeds: boolean; - canStartStopDatafeed: boolean; - canUpdateJob: boolean; - canUpdateDatafeed: boolean; - canPreviewDatafeed: boolean; - // Calendars - canGetCalendars: boolean; - canCreateCalendar: boolean; - canDeleteCalendar: boolean; - // Filters - canGetFilters: boolean; - canCreateFilter: boolean; - canDeleteFilter: boolean; - // File Data Visualizer - canFindFileStructure: boolean; - // Data Frame Analytics - canGetDataFrameAnalytics: boolean; - canDeleteDataFrameAnalytics: boolean; - canCreateDataFrameAnalytics: boolean; - canStartStopDataFrameAnalytics: boolean; -} - -export function getDefaultPrivileges(): Privileges { - return { - // Anomaly Detection - canGetJobs: false, - canCreateJob: false, - canDeleteJob: false, - canOpenJob: false, - canCloseJob: false, - canForecastJob: false, - canGetDatafeeds: false, - canStartStopDatafeed: false, - canUpdateJob: false, - canUpdateDatafeed: false, - canPreviewDatafeed: false, - // Calendars - canGetCalendars: false, - canCreateCalendar: false, - canDeleteCalendar: false, - // Filters - canGetFilters: false, - canCreateFilter: false, - canDeleteFilter: false, - // File Data Visualizer - canFindFileStructure: false, - // Data Frame Analytics - canGetDataFrameAnalytics: false, - canDeleteDataFrameAnalytics: false, - canCreateDataFrameAnalytics: false, - canStartStopDataFrameAnalytics: false, - }; -} - -export interface PrivilegesResponse { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} diff --git a/x-pack/plugins/ml/common/util/job_utils.d.ts b/x-pack/plugins/ml/common/util/job_utils.d.ts index bfad422e0ab48..4528fbfbb774d 100644 --- a/x-pack/plugins/ml/common/util/job_utils.d.ts +++ b/x-pack/plugins/ml/common/util/job_utils.d.ts @@ -52,3 +52,5 @@ export function getLatestDataOrBucketTimestamp( ): number; export function prefixDatafeedId(datafeedId: string, prefix: string): string; + +export function splitIndexPatternNames(indexPatternName: string): string[]; diff --git a/x-pack/plugins/ml/common/util/job_utils.js b/x-pack/plugins/ml/common/util/job_utils.js index de0aa4b886629..8fe5733ce67bd 100644 --- a/x-pack/plugins/ml/common/util/job_utils.js +++ b/x-pack/plugins/ml/common/util/job_utils.js @@ -588,3 +588,9 @@ export function processCreatedBy(customSettings) { delete customSettings.created_by; } } + +export function splitIndexPatternNames(indexPatternName) { + return indexPatternName.includes(',') + ? indexPatternName.split(',').map(i => i.trim()) + : [indexPatternName]; +} diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index f1facd18b9da5..e9796fcbb0fe4 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -15,14 +15,10 @@ import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/p import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setLicenseCache } from './license'; import { MlSetupDependencies, MlStartDependencies } from '../plugin'; -import { MlConfigType } from '../../common/types/ml_config'; import { MlRouter } from './routing'; -type MlDependencies = MlSetupDependencies & - MlStartDependencies & { - mlConfig: MlConfigType; - }; +type MlDependencies = MlSetupDependencies & MlStartDependencies; interface AppProps { coreStart: CoreStart; @@ -78,7 +74,6 @@ export const renderApp = ( http: coreStart.http, security: deps.security, urlGenerators: deps.share.urlGenerators, - mlConfig: deps.mlConfig, }); const mlLicense = setLicenseCache(deps.licensing); diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts new file mode 100644 index 0000000000000..1ca176d8d09ce --- /dev/null +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { hasLicenseExpired } from '../license'; + +import { MlCapabilities, getDefaultCapabilities } from '../../../common/types/capabilities'; +import { getCapabilities, getManageMlCapabilities } from './get_capabilities'; +import { ACCESS_DENIED_PATH } from '../management/management_urls'; + +let _capabilities: MlCapabilities = getDefaultCapabilities(); + +export function checkGetManagementMlJobsResolver() { + return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => { + getManageMlCapabilities().then( + ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { + _capabilities = capabilities; + // Loop through all capabilities to ensure they are all set to true. + const isManageML = Object.values(_capabilities).every(p => p === true); + + if (isManageML === true && isPlatinumOrTrialLicense === true) { + return resolve({ mlFeatureEnabledInSpace }); + } else { + window.location.href = ACCESS_DENIED_PATH; + return reject(); + } + } + ); + }); +} + +export function checkGetJobsCapabilitiesResolver(): Promise<MlCapabilities> { + return new Promise((resolve, reject) => { + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; + // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. + // all other functionality is controlled by the return capabilities object. + // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, + // allow the promise to resolve as the separate license check will redirect then user to + // a basic feature + if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); + } else { + window.location.href = '#/access-denied'; + return reject(); + } + }); + }); +} + +export function checkCreateJobsCapabilitiesResolver(): Promise<MlCapabilities> { + return new Promise((resolve, reject) => { + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; + // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, + // allow the promise to resolve as the separate license check will redirect then user to + // a basic feature + if (_capabilities.canCreateJob || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); + } else { + // if the user has no permission to create a job, + // redirect them back to the Transforms Management page + window.location.href = '#/jobs'; + return reject(); + } + }); + }); +} + +export function checkFindFileStructurePrivilegeResolver(): Promise<MlCapabilities> { + return new Promise((resolve, reject) => { + getCapabilities().then(({ capabilities }) => { + _capabilities = capabilities; + // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. + // all other functionality is controlled by the return _capabilities object + if (_capabilities.canFindFileStructure) { + return resolve(_capabilities); + } else { + window.location.href = '#/access-denied'; + return reject(); + } + }); + }); +} + +// check the privilege type and the license to see whether a user has permission to access a feature. +// takes the name of the privilege variable as specified in get_privileges.js +export function checkPermission(capability: keyof MlCapabilities) { + const licenseHasExpired = hasLicenseExpired(); + return _capabilities[capability] === true && licenseHasExpired !== true; +} + +// create the text for the button's tooltips if the user's license has +// expired or if they don't have the privilege to press that button +export function createPermissionFailureMessage(privilegeType: keyof MlCapabilities) { + let message = ''; + const licenseHasExpired = hasLicenseExpired(); + if (licenseHasExpired) { + message = i18n.translate('xpack.ml.privilege.licenseHasExpiredTooltip', { + defaultMessage: 'Your license has expired.', + }); + } else if (privilegeType === 'canCreateJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.createMLJobsTooltip', { + defaultMessage: 'You do not have permission to create Machine Learning jobs.', + }); + } else if (privilegeType === 'canStartStopDatafeed') { + message = i18n.translate('xpack.ml.privilege.noPermission.startOrStopDatafeedsTooltip', { + defaultMessage: 'You do not have permission to start or stop datafeeds.', + }); + } else if (privilegeType === 'canUpdateJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.editJobsTooltip', { + defaultMessage: 'You do not have permission to edit jobs.', + }); + } else if (privilegeType === 'canDeleteJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.deleteJobsTooltip', { + defaultMessage: 'You do not have permission to delete jobs.', + }); + } else if (privilegeType === 'canCreateCalendar') { + message = i18n.translate('xpack.ml.privilege.noPermission.createCalendarsTooltip', { + defaultMessage: 'You do not have permission to create calendars.', + }); + } else if (privilegeType === 'canDeleteCalendar') { + message = i18n.translate('xpack.ml.privilege.noPermission.deleteCalendarsTooltip', { + defaultMessage: 'You do not have permission to delete calendars.', + }); + } else if (privilegeType === 'canForecastJob') { + message = i18n.translate('xpack.ml.privilege.noPermission.runForecastsTooltip', { + defaultMessage: 'You do not have permission to run forecasts.', + }); + } + return i18n.translate('xpack.ml.privilege.pleaseContactAdministratorTooltip', { + defaultMessage: '{message} Please contact your administrator.', + values: { + message, + }, + }); +} diff --git a/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts new file mode 100644 index 0000000000000..c9561ed254544 --- /dev/null +++ b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ml } from '../services/ml_api_service'; + +import { setUpgradeInProgress } from '../services/upgrade_service'; +import { MlCapabilitiesResponse } from '../../../common/types/capabilities'; + +export function getCapabilities(): Promise<MlCapabilitiesResponse> { + return new Promise((resolve, reject) => { + ml.checkMlCapabilities() + .then((resp: MlCapabilitiesResponse) => { + if (resp.upgradeInProgress === true) { + setUpgradeInProgress(true); + } + resolve(resp); + }) + .catch(() => { + reject(); + }); + }); +} + +export function getManageMlCapabilities(): Promise<MlCapabilitiesResponse> { + return new Promise((resolve, reject) => { + ml.checkManageMLCapabilities() + .then((resp: MlCapabilitiesResponse) => { + if (resp.upgradeInProgress === true) { + setUpgradeInProgress(true); + } + resolve(resp); + }) + .catch(() => { + reject(); + }); + }); +} diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index b881bfe4f1fe6..7696143a9ce71 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -8,14 +8,14 @@ import React from 'react'; import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json'; import { getColumns } from './anomalies_table_columns'; -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => false, })); jest.mock('../../license', () => ({ hasLicenseExpired: () => false, })); -jest.mock('../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../services/field_format_service', () => ({ getFieldFormat: () => {}, diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 0c6c959927140..8f79ce4a6c08a 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -23,7 +23,7 @@ import { DetectorCell } from './detector_cell'; import { EntityCell } from '../entity_cell'; import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlFieldFormatService } from '../../services/field_format_service'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { formatValue } from '../../formatters/format_value'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 2a34f12330a75..5d2b8e35fbc0f 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { parseInterval } from '../../../../common/util/parse_interval'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts new file mode 100644 index 0000000000000..4bb670ad02dfc --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { EuiDataGridSorting } from '@elastic/eui'; + +import { multiColumnSortFactory } from './common'; + +describe('Transform: Define Pivot Common', () => { + test('multiColumnSortFactory()', () => { + const data = [ + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + ]; + + const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; + const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); + data.sort(multiColumnSort1); + + expect(data).toStrictEqual([ + { s: 'b', n: 3 }, + { s: 'b', n: 4 }, + { s: 'a', n: 1 }, + { s: 'a', n: 2 }, + ]); + + const sortingColumns2: EuiDataGridSorting['columns'] = [ + { id: 's', direction: 'asc' }, + { id: 'n', direction: 'desc' }, + ]; + const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); + data.sort(multiColumnSort2); + + expect(data).toStrictEqual([ + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + ]); + + const sortingColumns3: EuiDataGridSorting['columns'] = [ + { id: 'n', direction: 'desc' }, + { id: 's', direction: 'desc' }, + ]; + const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); + data.sort(multiColumnSort3); + + expect(data).toStrictEqual([ + { s: 'b', n: 4 }, + { s: 'b', n: 3 }, + { s: 'a', n: 2 }, + { s: 'a', n: 1 }, + ]); + }); +}); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts new file mode 100644 index 0000000000000..d141b68b5d03f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -0,0 +1,287 @@ +/* + * 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 moment from 'moment-timezone'; +import { useEffect, useMemo } from 'react'; + +import { + EuiDataGridCellValueElementProps, + EuiDataGridSorting, + EuiDataGridStyle, +} from '@elastic/eui'; + +import { + IndexPattern, + IFieldType, + ES_FIELD_TYPES, + KBN_FIELD_TYPES, +} from '../../../../../../../src/plugins/data/public'; + +import { + BASIC_NUMERICAL_TYPES, + EXTENDED_NUMERICAL_TYPES, +} from '../../data_frame_analytics/common/fields'; + +import { + FEATURE_IMPORTANCE, + FEATURE_INFLUENCE, + OUTLIER_SCORE, +} from '../../data_frame_analytics/common/constants'; +import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; +import { getNestedProperty } from '../../util/object_utils'; +import { mlFieldFormatService } from '../../services/field_format_service'; + +import { DataGridItem, IndexPagination, RenderCellValue } from './types'; + +export const INIT_MAX_COLUMNS = 20; + +export const euiDataGridStyle: EuiDataGridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + stripes: false, + rowHover: 'none', + header: 'shade', +}; + +export const euiDataGridToolbarSettings = { + showColumnSelector: true, + showStyleSelector: false, + showSortSelector: true, + showFullScreenSelector: false, +}; + +export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): string[] => { + const allFields = indexPattern.fields.map(f => f.name); + const indexPatternFields: string[] = allFields.filter(f => { + if (indexPattern.metaFields.includes(f)) { + return false; + } + + const fieldParts = f.split('.'); + const lastPart = fieldParts.pop(); + if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { + return false; + } + + return true; + }); + + return indexPatternFields; +}; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, resultsField: string) => { + return Object.keys(fieldTypes).map(field => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + const isSortable = true; + const type = fieldTypes[field]; + + const isNumber = + type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + if (isNumber) { + schema = 'numeric'; + } + + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; + } + + if ( + field === `${resultsField}.${OUTLIER_SCORE}` || + field.includes(`${resultsField}.${FEATURE_INFLUENCE}`) + ) { + schema = 'numeric'; + } + + if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) { + schema = 'json'; + } + + return { id: field, schema, isSortable }; + }); +}; + +export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case KBN_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case KBN_FIELD_TYPES.DATE: + schema = 'datetime'; + break; + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case KBN_FIELD_TYPES.NUMBER: + schema = 'numeric'; + break; + } + + return schema; +}; + +export const useRenderCellValue = ( + indexPattern: IndexPattern | undefined, + pagination: IndexPagination, + tableItems: DataGridItem[], + resultsField?: string, + cellPropsCallback?: ( + columnId: string, + cellValue: any, + fullItem: Record<string, any>, + setCellProps: EuiDataGridCellValueElementProps['setCellProps'] + ) => void +): RenderCellValue => { + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: EuiDataGridCellValueElementProps['setCellProps']; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + if (indexPattern === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + function getCellValue(cId: string) { + if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) { + const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null); + return results[cId.replace(`${resultsField}.`, '')]; + } + + return tableItems.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(tableItems[adjustedRowIndex], cId, null) + : null; + } + + const cellValue = getCellValue(columnId); + + // React by default doesn't all us to use a hook in a callback. + // However, this one will be passed on to EuiDataGrid and its docs + // recommend wrapping `setCellProps` in a `useEffect()` hook + // so we're ignoring the linting rule here. + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (typeof cellPropsCallback === 'function') { + cellPropsCallback(columnId, cellValue, fullItem, setCellProps); + } + }, [columnId, cellValue]); + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + const field = indexPattern.fields.getByName(columnId); + if (field?.type === KBN_FIELD_TYPES.DATE) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]); + return renderCellValue; +}; + +/** + * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. + * `sortFn()` is recursive to support sorting on multiple columns. + * + * @param sortingColumns - The EUI data grid sorting configuration + * @returns The sorting function which can be used with an array's sort() function. + */ +export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { + const isString = (arg: any): arg is string => { + return typeof arg === 'string'; + }; + + const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { + const sort = sortingColumns[sortingColumnIndex]; + const aValue = getNestedProperty(a, sort.id, null); + const bValue = getNestedProperty(b, sort.id, null); + + if (typeof aValue === 'number' && typeof bValue === 'number') { + if (aValue < bValue) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (isString(aValue) && isString(bValue)) { + if (aValue.localeCompare(bValue) === -1) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue.localeCompare(bValue) === 1) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (sortingColumnIndex + 1 < sortingColumns.length) { + return sortFn(a, b, sortingColumnIndex + 1); + } + + return 0; + }; + + return sortFn; +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx new file mode 100644 index 0000000000000..a5b301902cc75 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -0,0 +1,181 @@ +/* + * 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, { useEffect, FC } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiCodeBlock, + EuiCopy, + EuiDataGrid, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { CoreSetup } from 'src/core/public'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; +import { UseIndexDataReturnType } from './types'; + +export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( + <EuiTitle size="xs"> + <span>{title}</span> + </EuiTitle> +); + +interface PropsWithoutHeader extends UseIndexDataReturnType { + dataTestSubj: string; + toastNotifications: CoreSetup['notifications']['toasts']; +} + +interface PropsWithHeader extends PropsWithoutHeader { + copyToClipboard: string; + copyToClipboardDescription: string; + title: string; +} + +function isWithHeader(arg: any): arg is PropsWithHeader { + return typeof arg?.title === 'string' && arg?.title !== ''; +} + +type Props = PropsWithHeader | PropsWithoutHeader; + +export const DataGrid: FC<Props> = props => { + const { + columns, + dataTestSubj, + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems: data, + toastNotifications, + visibleColumns, + } = props; + + useEffect(() => { + if (invalidSortingColumnns.length > 0) { + invalidSortingColumnns.forEach(columnId => { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataGrid.invalidSortingColumnError', { + defaultMessage: `The column '{columnId}' cannot be used for sorting.`, + values: { columnId }, + }) + ); + }); + } + }, [invalidSortingColumnns, toastNotifications]); + + if (status === INDEX_STATUS.LOADED && data.length === 0) { + return ( + <div data-test-subj={`${dataTestSubj} empty`}> + {isWithHeader(props) && <DataGridTitle title={props.title} />} + <EuiCallOut + title={i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutTitle', { + defaultMessage: 'Empty index query result.', + })} + color="primary" + > + <p> + {i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', + })} + </p> + </EuiCallOut> + </div> + ); + } + + if (noDataMessage !== '') { + return ( + <div data-test-subj={`${dataTestSubj} empty`}> + {isWithHeader(props) && <DataGridTitle title={props.title} />} + <EuiCallOut + title={i18n.translate('xpack.ml.dataGrid.dataGridNoDataCalloutTitle', { + defaultMessage: 'Index preview not available', + })} + color="primary" + > + <p>{noDataMessage}</p> + </EuiCallOut> + </div> + ); + } + + return ( + <div data-test-subj={`${dataTestSubj} ${status === INDEX_STATUS.ERROR ? 'error' : 'loaded'}`}> + {isWithHeader(props) && ( + <EuiFlexGroup alignItems="center" justifyContent="spaceBetween"> + <EuiFlexItem> + <DataGridTitle title={props.title} /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiCopy + beforeMessage={props.copyToClipboardDescription} + textToCopy={props.copyToClipboard} + > + {(copy: () => void) => ( + <EuiButtonIcon + onClick={copy} + iconType="copyClipboard" + aria-label={props.copyToClipboardDescription} + /> + )} + </EuiCopy> + </EuiFlexItem> + </EuiFlexGroup> + )} + {status === INDEX_STATUS.ERROR && ( + <div data-test-subj={`${dataTestSubj} error`}> + <EuiCallOut + title={i18n.translate('xpack.ml.dataGrid.indexDataError', { + defaultMessage: 'An error occurred loading the index data.', + })} + color="danger" + iconType="cross" + > + <EuiCodeBlock language="json" fontSize="s" paddingSize="s" isCopyable> + {errorMessage} + </EuiCodeBlock> + </EuiCallOut> + <EuiSpacer size="m" /> + </div> + )} + <EuiDataGrid + aria-label={isWithHeader(props) ? props.title : ''} + columns={columns} + columnVisibility={{ visibleColumns, setVisibleColumns }} + gridStyle={euiDataGridStyle} + rowCount={rowCount} + renderCellValue={renderCellValue} + sorting={{ columns: sortingColumns, onSort }} + toolbarVisibility={euiDataGridToolbarSettings} + pagination={{ + ...pagination, + pageSizeOptions: [5, 10, 25], + onChangeItemsPerPage, + onChangePage, + }} + /> + </div> + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts new file mode 100644 index 0000000000000..2472878d1b0c1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { + getDataGridSchemasFromFieldTypes, + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useRenderCellValue, +} from './common'; +export { useDataGrid } from './use_data_grid'; +export { DataGrid } from './data_grid'; +export { + DataGridItem, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts new file mode 100644 index 0000000000000..5fa038edf7815 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -0,0 +1,98 @@ +/* + * 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 { Dispatch, SetStateAction } from 'react'; +import { SearchResponse } from 'elasticsearch'; + +import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { Dictionary } from '../../../../common/types/common'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; + +export type ColumnId = string; +export type DataGridItem = Record<string, any>; + +export type IndexPagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; + +export type OnChangeItemsPerPage = (pageSize: any) => void; +export type OnChangePage = (pageIndex: any) => void; +export type OnSort = ( + sc: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> +) => void; + +export type RenderCellValue = ({ + rowIndex, + columnId, + setCellProps, +}: { + rowIndex: number; + columnId: string; + setCellProps: any; +}) => any; + +export type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +export interface SearchResponse7 extends SearchResponse<any> { + hits: SearchResponse<any>['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + +export interface UseIndexDataReturnType + extends Pick< + UseDataGridReturnType, + | 'errorMessage' + | 'invalidSortingColumnns' + | 'noDataMessage' + | 'onChangeItemsPerPage' + | 'onChangePage' + | 'onSort' + | 'pagination' + | 'setPagination' + | 'setVisibleColumns' + | 'rowCount' + | 'sortingColumns' + | 'status' + | 'tableItems' + | 'visibleColumns' + > { + columns: EuiDataGridColumn[]; + renderCellValue: RenderCellValue; +} + +export interface UseDataGridReturnType { + errorMessage: string; + invalidSortingColumnns: ColumnId[]; + noDataMessage: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onChangePage: OnChangePage; + onSort: OnSort; + pagination: IndexPagination; + resetPagination: () => void; + rowCount: number; + setErrorMessage: Dispatch<SetStateAction<string>>; + setNoDataMessage: Dispatch<SetStateAction<string>>; + setPagination: Dispatch<SetStateAction<IndexPagination>>; + setRowCount: Dispatch<SetStateAction<number>>; + setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; + setStatus: Dispatch<SetStateAction<INDEX_STATUS>>; + setTableItems: Dispatch<SetStateAction<DataGridItem[]>>; + setVisibleColumns: Dispatch<SetStateAction<ColumnId[]>>; + sortingColumns: EuiDataGridSorting['columns']; + status: INDEX_STATUS; + tableItems: DataGridItem[]; + visibleColumns: ColumnId[]; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts new file mode 100644 index 0000000000000..c7c4f46031b6e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useEffect, useState } from 'react'; + +import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { INIT_MAX_COLUMNS } from './common'; +import { + ColumnId, + DataGridItem, + IndexPagination, + OnChangeItemsPerPage, + OnChangePage, + OnSort, + UseDataGridReturnType, +} from './types'; + +export const useDataGrid = ( + columns: EuiDataGridColumn[], + defaultPageSize = 5, + defaultVisibleColumnsCount = INIT_MAX_COLUMNS, + defaultVisibleColumnsFilter?: (id: string) => boolean +): UseDataGridReturnType => { + const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; + + const [noDataMessage, setNoDataMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState<DataGridItem[]>([]); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, []); + + const onChangePage: OnChangePage = useCallback( + pageIndex => setPagination(p => ({ ...p, pageIndex })), + [] + ); + + const resetPagination = () => setPagination(defaultPagination); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState<ColumnId[]>([]); + + const columnIds = columns.map(c => c.id); + const filteredColumnIds = + defaultVisibleColumnsFilter !== undefined + ? columnIds.filter(defaultVisibleColumnsFilter) + : columnIds; + const defaultVisibleColumns = filteredColumnIds.splice(0, defaultVisibleColumnsCount); + + useEffect(() => { + setVisibleColumns(defaultVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultVisibleColumns.join()]); + + const [invalidSortingColumnns, setInvalidSortingColumnns] = useState<string[]>([]); + + const onSort: OnSort = useCallback( + sc => { + // Check if an unsupported column type for sorting was selected. + const updatedInvalidSortingColumnns = sc.reduce<string[]>((arr, current) => { + const columnType = columns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + setInvalidSortingColumnns(updatedInvalidSortingColumnns); + if (updatedInvalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } + }, + [columns] + ); + + return { + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + resetPagination, + rowCount, + setErrorMessage, + setNoDataMessage, + setPagination, + setRowCount, + setSortingColumns, + setStatus, + setTableItems, + setVisibleColumns, + sortingColumns, + status, + tableItems, + visibleColumns, + }; +}; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index f8868ec099985..f259a1f1ffb02 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -30,7 +30,7 @@ import { import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { ConditionsSection } from './conditions_section'; import { ScopeSection } from './scope_section'; import { SelectRuleAction } from './select_rule_action'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index 5c43c558a3333..3e8a17eeb8617 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -45,7 +45,7 @@ jest.mock('../../services/job_service', () => ({ }, })); jest.mock('../../services/ml_api_service', () => 'ml'); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js index 8dca6db96d45b..48e0da72f067c 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js @@ -14,7 +14,7 @@ import React from 'react'; import { EuiCallOut, EuiCheckbox, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; import { ScopeExpression } from './scope_expression'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { getScopeFieldDefaults } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js index 3d7861fc7de0a..189fb6a35d3e6 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js @@ -11,7 +11,7 @@ jest.mock('../../services/job_service.js', () => 'mlJobService'); // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 475e44af3669c..c65d872212ad6 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -15,7 +15,7 @@ import { LicenseManagementUIPluginSetup } from '../../../../../license_managemen interface StartPlugins { data: DataPublicPluginStart; - security: SecurityPluginSetup; + security?: SecurityPluginSetup; licenseManagement?: LicenseManagementUIPluginSetup; } export type StartServices = CoreStart & StartPlugins; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 3c959b827bb1c..fb3b2b3519947 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -52,7 +52,7 @@ export interface ClassificationAnalysis { classification: Classification; } -export interface LoadRegressionExploreDataArg { +export interface LoadExploreDataArg { filterByIsTraining?: boolean; searchQuery: SavedSearchQuery; } @@ -409,11 +409,11 @@ export function getEvalQueryBody({ ignoreDefaultQuery, }: { resultsField: string; - isTraining: boolean; + isTraining?: boolean; searchQuery?: ResultsSearchQuery; ignoreDefaultQuery?: boolean; }) { - let query; + let query: any; const trainingQuery: ResultsSearchQuery = { term: { [`${resultsField}.is_training`]: { value: isTraining } }, @@ -426,19 +426,25 @@ export function getEvalQueryBody({ searchQueryClone.bool.must = []; } - searchQueryClone.bool.must.push(trainingQuery); + if (isTraining !== undefined) { + searchQueryClone.bool.must.push(trainingQuery); + } + query = searchQueryClone; } else if (isQueryStringQuery(searchQueryClone)) { query = { bool: { - must: [searchQueryClone, trainingQuery], + must: [searchQueryClone], }, }; + if (isTraining !== undefined) { + query.bool.must.push(trainingQuery); + } } else { // Not a bool or string query so we need to create it so can add the trainingQuery query = { bool: { - must: [trainingQuery], + must: isTraining !== undefined ? [trainingQuery] : [], }, }; } @@ -456,7 +462,7 @@ interface EvaluateMetrics { } interface LoadEvalDataConfig { - isTraining: boolean; + isTraining?: boolean; index: string; dependentVariable: string; resultsField: string; @@ -535,7 +541,7 @@ interface TrackTotalHitsSearchResponse { interface LoadDocsCountConfig { ignoreDefaultQuery?: boolean; - isTraining: boolean; + isTraining?: boolean; searchQuery: SavedSearchQuery; resultsField: string; destIndex: string; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts new file mode 100644 index 0000000000000..51b2918012c8d --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DEFAULT_RESULTS_FIELD = 'ml'; +export const FEATURE_IMPORTANCE = 'feature_importance'; +export const FEATURE_INFLUENCE = 'feature_influence'; +export const OUTLIER_SCORE = 'outlier_score'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts deleted file mode 100644 index 2b6d733837562..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts +++ /dev/null @@ -1,23 +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 { EuiDataGridStyle } from '@elastic/eui'; - -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'none', - header: 'shade', -}; - -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index f165669bdd674..8423bc1b94a09 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getNestedProperty } from '../../util/object_utils'; import { - DataFrameAnalyticsConfig, getNumTopFeatureImportanceValues, getPredictedFieldName, getDependentVar, getPredictionFieldName, + isClassificationAnalysis, + isOutlierAnalysis, + isRegressionAnalysis, + DataFrameAnalyticsConfig, } from './analytics'; import { Field } from '../../../../common/types/fields'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE } from './constants'; + export type EsId = string; export type EsDocSource = Record<string, any>; export type EsFieldName = string; @@ -42,7 +46,7 @@ export const EXTENDED_NUMERICAL_TYPES = new Set([ ES_FIELD_TYPES.SCALED_FLOAT, ]); -const ML__ID_COPY = 'ml__id_copy'; +export const ML__ID_COPY = 'ml__id_copy'; export const isKeywordAndTextType = (fieldName: string): boolean => { const { fields } = newJobCapsService; @@ -64,32 +68,61 @@ export const isKeywordAndTextType = (fieldName: string): boolean => { }; // Used to sort columns: +// - Anchor on the left ml.outlier_score, ml.is_training, <predictedField>, <actual> // - string based columns are moved to the left -// - followed by the outlier_score column -// - feature_influence fields get moved next to the corresponding field column +// - feature_influence/feature_importance fields get moved next to the corresponding field column // - overall fields get sorted alphabetically -export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: string, b: string) => { - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; +export const sortExplorationResultsFields = ( + a: string, + b: string, + jobConfig: DataFrameAnalyticsConfig +) => { + const resultsField = jobConfig.dest.results_field; - if (typeofA !== 'string' && typeofB === 'string') { - return 1; - } - if (typeofA === 'string' && typeofB !== 'string') { - return -1; - } - if (typeofA === 'string' && typeofB === 'string') { - return a.localeCompare(b); - } + if (isOutlierAnalysis(jobConfig.analysis)) { + if (a === `${resultsField}.${OUTLIER_SCORE}`) { + return -1; + } - if (a === `${resultsField}.outlier_score`) { - return -1; + if (b === `${resultsField}.${OUTLIER_SCORE}`) { + return 1; + } } - if (b === `${resultsField}.outlier_score`) { - return 1; + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); + + if (a === `${resultsField}.is_training`) { + return -1; + } + if (b === `${resultsField}.is_training`) { + return 1; + } + if (a === predictedField) { + return -1; + } + if (b === predictedField) { + return 1; + } + if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { + return -1; + } + if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { + return 1; + } + + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + if (b === `${resultsField}.prediction_probability`) { + return 1; + } } + const typeofA = typeof a; + const typeofB = typeof b; + const tokensA = a.split('.'); const prefixA = tokensA[0]; const tokensB = b.split('.'); @@ -109,91 +142,6 @@ export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: strin return a.localeCompare(tokensB.join('.')); } - return a.localeCompare(b); -}; - -export const sortRegressionResultsFields = ( - a: string, - b: string, - jobConfig: DataFrameAnalyticsConfig -) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - if (a === `${resultsField}.is_training`) { - return -1; - } - if (b === `${resultsField}.is_training`) { - return 1; - } - if (a === predictedField) { - return -1; - } - if (b === predictedField) { - return 1; - } - if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { - return -1; - } - if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - - return a.localeCompare(b); -}; - -// Used to sort columns: -// Anchor on the left ml.is_training, <predictedField>, <actual> -export const sortRegressionResultsColumns = ( - obj: EsDocSource, - jobConfig: DataFrameAnalyticsConfig -) => (a: string, b: string) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; - - if (a === `${resultsField}.is_training`) { - return -1; - } - - if (b === `${resultsField}.is_training`) { - return 1; - } - - if (a === predictedField) { - return -1; - } - - if (b === predictedField) { - return 1; - } - - if (a === dependentVariable) { - return -1; - } - - if (b === dependentVariable) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - if (typeofA !== 'string' && typeofB === 'string') { return 1; } @@ -204,44 +152,9 @@ export const sortRegressionResultsColumns = ( return a.localeCompare(b); } - const tokensA = a.split('.'); - const prefixA = tokensA[0]; - const tokensB = b.split('.'); - const prefixB = tokensB[0]; - - if (prefixA === resultsField && tokensA.length > 1 && prefixB !== resultsField) { - tokensA.shift(); - tokensA.shift(); - if (tokensA.join('.') === b) return 1; - return tokensA.join('.').localeCompare(b); - } - - if (prefixB === resultsField && tokensB.length > 1 && prefixA !== resultsField) { - tokensB.shift(); - tokensB.shift(); - if (tokensB.join('.') === a) return -1; - return a.localeCompare(tokensB.join('.')); - } - return a.localeCompare(b); }; -export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFieldName[] { - const flatDocFields: EsFieldName[] = []; - const newDocFields = Object.keys(obj); - newDocFields.forEach(f => { - const fieldValue = getNestedProperty(obj, f); - if (typeof fieldValue !== 'object' || fieldValue === null || Array.isArray(fieldValue)) { - flatDocFields.push(f); - } else { - const innerFields = getFlattenedFields(fieldValue, resultsField); - const flattenedFields = innerFields.map(d => `${f}.${d}`); - flatDocFields.push(...flattenedFields); - } - }); - return flatDocFields.filter(f => f !== ML__ID_COPY); -} - export const getDefaultFieldsFromJobCaps = ( fields: Field[], jobConfig: DataFrameAnalyticsConfig, @@ -259,49 +172,72 @@ export const getDefaultFieldsFromJobCaps = ( return fieldsObj; } - const dependentVariable = getDependentVar(jobConfig.analysis); - const type = newJobCapsService.getFieldById(dependentVariable)?.type; - const predictionFieldName = getPredictionFieldName(jobConfig.analysis); - const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); // default is 'ml' const resultsField = jobConfig.dest.results_field; - const defaultPredictionField = `${dependentVariable}_prediction`; - const predictedField = `${resultsField}.${ - predictionFieldName ? predictionFieldName : defaultPredictionField - }`; - const featureImportanceFields = []; - - if ((numTopFeatureImportanceValues ?? 0) > 0) { - featureImportanceFields.push({ - id: `${resultsField}.feature_importance`, - name: `${resultsField}.feature_importance`, - type: KBN_FIELD_TYPES.NUMBER, - }); + const featureInfluenceFields = []; + const allFields: any = []; + let type: ES_FIELD_TYPES | undefined; + let predictedField: string | undefined; + + if (isOutlierAnalysis(jobConfig.analysis)) { + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push({ + id: `${resultsField}.${OUTLIER_SCORE}`, + name: `${resultsField}.${OUTLIER_SCORE}`, + type: KBN_FIELD_TYPES.NUMBER, + }); + + featureInfluenceFields.push( + ...fields + .filter(d => !jobConfig.analyzed_fields.excludes.includes(d.id)) + .map(d => ({ + id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`, + name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`, + type: KBN_FIELD_TYPES.NUMBER, + })) + ); + } } - let allFields: any = []; - // Only need to add these fields if we didn't use dest index pattern to get the fields - if (needsDestIndexFields === true) { - allFields.push( - { - id: `${resultsField}.is_training`, - name: `${resultsField}.is_training`, - type: ES_FIELD_TYPES.BOOLEAN, - }, - { id: predictedField, name: predictedField, type } - ); + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + type = newJobCapsService.getFieldById(dependentVariable)?.type; + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); + + const defaultPredictionField = `${dependentVariable}_prediction`; + predictedField = `${resultsField}.${ + predictionFieldName ? predictionFieldName : defaultPredictionField + }`; + + if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { + featureImportanceFields.push({ + id: `${resultsField}.${FEATURE_IMPORTANCE}`, + name: `${resultsField}.${FEATURE_IMPORTANCE}`, + type: KBN_FIELD_TYPES.UNKNOWN, + }); + } + + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push( + { + id: `${resultsField}.is_training`, + name: `${resultsField}.is_training`, + type: ES_FIELD_TYPES.BOOLEAN, + }, + { id: predictedField, name: predictedField, type } + ); + } } - allFields.push(...fields, ...featureImportanceFields); + allFields.push(...fields, ...featureImportanceFields, ...featureInfluenceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => - sortRegressionResultsFields(a, b, jobConfig) + sortExplorationResultsFields(a, b, jobConfig) ); - // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid - if (needsDestIndexFields === false) { - allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); - } let selectedFields = allFields.filter( (field: any) => field.name === predictedField || !field.name.includes('.keyword') @@ -317,145 +253,3 @@ export const getDefaultFieldsFromJobCaps = ( depVarType: type, }; }; - -export const getDefaultClassificationFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - const resultsField = jobConfig.dest.results_field; - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - - if (k === `${resultsField}.prediction_probability`) { - return true; - } - - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultRegressionFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - const resultsField = jobConfig.dest.results_field; - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultSelectableFields = (docs: EsDoc[], resultsField: string): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields.filter(k => { - if (k === `${resultsField}.outlier_score`) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }); -}; - -export const toggleSelectedFieldSimple = ( - selectedFields: EsFieldName[], - column: EsFieldName -): EsFieldName[] => { - const index = selectedFields.indexOf(column); - - if (index === -1) { - selectedFields.push(column); - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; -// Fields starting with 'ml' or custom result name not included in newJobCapsService fields so -// need to recreate the field with correct type and add to selected fields -export const toggleSelectedField = ( - selectedFields: Field[], - column: EsFieldName, - resultsField: string, - depVarType?: ES_FIELD_TYPES -): Field[] => { - const index = selectedFields.map(field => field.name).indexOf(column); - if (index === -1) { - const columnField = newJobCapsService.getFieldById(column); - if (columnField !== null) { - selectedFields.push(columnField); - } else { - const resultFieldPattern = `^${resultsField}\.`; - const regex = new RegExp(resultFieldPattern); - const isResultField = column.match(regex) !== null; - let newField; - - if (isResultField && column.includes('is_training')) { - newField = { - id: column, - name: column, - type: ES_FIELD_TYPES.BOOLEAN, - }; - } else if (isResultField && depVarType !== undefined) { - newField = { - id: column, - name: column, - type: depVarType, - }; - } - - if (newField) selectedFields.push(newField); - } - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts new file mode 100644 index 0000000000000..87b8c15aeaa78 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getErrorMessage } from '../../../../common/util/errors'; + +import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid'; +import { ml } from '../../services/ml_api_service'; + +import { isKeywordAndTextType } from '../common/fields'; +import { SavedSearchQuery } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig, INDEX_STATUS } from './analytics'; + +export const getIndexData = async ( + jobConfig: DataFrameAnalyticsConfig | undefined, + dataGrid: UseDataGridReturnType, + searchQuery: SavedSearchQuery +) => { + if (jobConfig !== undefined) { + const { + pagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + } = dataGrid; + + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const { pageIndex, pageSize } = pagination; + const resp: SearchResponse7 = await ml.esSearch({ + index: jobConfig.dest.index, + body: { + query: searchQuery, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }); + + setRowCount(resp.hits.total.value); + + const docs = resp.hits.hits.map(d => d._source); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts new file mode 100644 index 0000000000000..12ae4a586e949 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { newJobCapsService } from '../../services/new_job_capabilities_service'; + +import { getDefaultFieldsFromJobCaps, DataFrameAnalyticsConfig } from '../common'; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getIndexFields = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + needsDestIndexFields: boolean +) => { + const { fields } = newJobCapsService; + if (jobConfig !== undefined) { + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: FieldTypes = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + return { + defaultSelectedFields: defaultSelected.map(field => field.id), + fieldTypes: types, + tableFields: allFields, + }; + } else { + return { + defaultSelectedFields: [], + fieldTypes: {}, + tableFields: [], + }; + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 7b76faf613ce8..400902c152c9e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -30,16 +30,8 @@ export { } from './analytics'; export { - getDefaultSelectableFields, - getDefaultRegressionFields, - getDefaultClassificationFields, getDefaultFieldsFromJobCaps, - getFlattenedFields, - sortColumns, - sortRegressionResultsColumns, - sortRegressionResultsFields, - toggleSelectedField, - toggleSelectedFieldSimple, + sortExplorationResultsFields, EsId, EsDoc, EsDocSource, @@ -47,4 +39,7 @@ export { MAX_COLUMNS, } from './fields'; -export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; +export { getIndexData } from './get_index_data'; +export { getIndexFields } from './get_index_fields'; + +export { useResultsViewConfig } from './use_results_view_config'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts new file mode 100644 index 0000000000000..0bc9e78207596 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -0,0 +1,104 @@ +/* + * 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 { useEffect, useState } from 'react'; + +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; + +import { getErrorMessage } from '../../../../common/util/errors'; + +import { getIndexPatternIdFromName } from '../../util/index_utils'; +import { ml } from '../../services/ml_api_service'; +import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { useMlContext } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig } from '../common'; + +import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics'; +import { DATA_FRAME_TASK_STATE } from '../pages/analytics_management/components/analytics_list/common'; + +export const useResultsViewConfig = (jobId: string) => { + const mlContext = useMlContext(); + const [indexPattern, setIndexPattern] = useState<IndexPattern | undefined>(undefined); + const [isInitialized, setIsInitialized] = useState<boolean>(false); + const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false); + const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined); + const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState<undefined | string>( + undefined + ); + const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined); + const [jobStatus, setJobStatus] = useState<DATA_FRAME_TASK_STATE | undefined>(undefined); + + // get analytics configuration, index pattern and field caps + useEffect(() => { + (async function() { + setIsLoadingJobConfig(false); + + try { + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + const jobConfigUpdate = analyticsConfigs.data_frame_analytics[0]; + + try { + const destIndex = Array.isArray(jobConfigUpdate.dest.index) + ? jobConfigUpdate.dest.index[0] + : jobConfigUpdate.dest.index; + const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; + let indexP: IndexPattern | undefined; + + try { + indexP = await mlContext.indexPatterns.get(destIndexPatternId); + } catch (e) { + indexP = undefined; + } + + if (indexP === undefined) { + const sourceIndex = jobConfigUpdate.source.index[0]; + const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } + + if (indexP !== undefined) { + await newJobCapsService.initializeFromIndexPattern(indexP, false, false); + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIndexPattern(indexP); + setIsInitialized(true); + setIsLoadingJobConfig(false); + } + } catch (e) { + setJobCapsServiceErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + } + } catch (e) { + setJobConfigErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + })(); + }, []); + + return { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 5c151166829ab..ccac9a697210b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -4,183 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - <EuiTitle size="xs"> - <span> - {i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for classification job ID {jobId}', - values: { jobId }, - })} - </span> - </EuiTitle> -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const ClassificationExploration: FC<Props> = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined); - const [jobStatus, setJobStatus] = useState<DATA_FRAME_TASK_STATE | undefined>(undefined); - const [indexPattern, setIndexPattern] = useState<IndexPattern | undefined>(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false); - const [isInitialized, setIsInitialized] = useState<boolean>(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState<undefined | string>( - undefined - ); - const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } else { - setJobConfigErrorMessage( - i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage', - { - defaultMessage: 'No results found.', - } - ) - ); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - <EuiPanel grow={false}> - <ExplorationTitle jobId={jobId} /> - <EuiSpacer /> - <EuiCallOut - title={jobConfigErrorMessage ? jobConfigErrorTitle : jobCapsErrorTitle} - color="danger" - iconType="cross" - > - <p>{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}</p> - </EuiCallOut> - </EuiPanel> - ); - } - return ( - <Fragment> - {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - <EvaluatePanel jobConfig={jobConfig} jobStatus={jobStatus} searchQuery={searchQuery} /> + <ExplorationPageWrapper + jobId={jobId} + title={i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', + { + defaultMessage: 'Destination index for classification job ID {jobId}', + values: { jobId }, + } )} - <EuiSpacer /> - {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - <ResultsTable - jobConfig={jobConfig} - indexPattern={indexPattern} - jobStatus={jobStatus} - setEvaluateSearchQuery={setSearchQuery} - /> - )} - </Fragment> + EvaluatePanel={EvaluatePanel} + /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 91dae49ba5c49..af90547606f82 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -117,13 +117,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) const resultsField = jobConfig.dest.results_field; let requiresKeyword = false; - const loadData = async ({ - isTrainingClause, - ignoreDefaultQuery = true, - }: { - isTrainingClause: { query: string; operator: string }; - ignoreDefaultQuery?: boolean; - }) => { + const loadData = async ({ isTraining }: { isTraining: boolean | undefined }) => { setIsLoading(true); try { @@ -134,19 +128,18 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) } const evalData = await loadEvalData({ - isTraining: false, + isTraining, index, dependentVariable, resultsField, predictionFieldName, searchQuery, - ignoreDefaultQuery, jobType: ANALYSIS_CONFIG_TYPE.CLASSIFICATION, requiresKeyword, }); const docsCountResp = await loadDocsCount({ - isTraining: false, + isTraining, searchQuery, resultsField, destIndex: jobConfig.dest.index, @@ -225,29 +218,46 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) }, [confusionMatrixData]); useEffect(() => { - const hasIsTrainingClause = - isResultsSearchBoolQuery(searchQuery) && - searchQuery.bool.must.filter( - (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined - ); - const isTrainingClause = - hasIsTrainingClause && - hasIsTrainingClause[0] && - hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + let isTraining: boolean | undefined; + const query = + isResultsSearchBoolQuery(searchQuery) && (searchQuery.bool.should || searchQuery.bool.filter); - const noTrainingQuery = isTrainingClause === false || isTrainingClause === undefined; + if (query !== undefined && query !== false) { + for (let i = 0; i < query.length; i++) { + const clause = query[i]; - if (noTrainingQuery) { + if (clause.match && clause.match[`${resultsField}.is_training`] !== undefined) { + isTraining = clause.match[`${resultsField}.is_training`]; + break; + } else if ( + clause.bool && + (clause.bool.should !== undefined || clause.bool.filter !== undefined) + ) { + const innerQuery = clause.bool.should || clause.bool.filter; + if (innerQuery !== undefined) { + for (let j = 0; j < innerQuery.length; j++) { + const innerClause = innerQuery[j]; + if ( + innerClause.match && + innerClause.match[`${resultsField}.is_training`] !== undefined + ) { + isTraining = innerClause.match[`${resultsField}.is_training`]; + break; + } + } + } + } + } + } + if (isTraining === undefined) { setDataSubsetTitle(SUBSET_TITLE.ENTIRE); } else { setDataSubsetTitle( - isTrainingClause && isTrainingClause.query === 'true' - ? SUBSET_TITLE.TRAINING - : SUBSET_TITLE.TESTING + isTraining && isTraining === true ? SUBSET_TITLE.TRAINING : SUBSET_TITLE.TESTING ); } - loadData({ isTrainingClause }); + loadData({ isTraining }); }, [JSON.stringify(searchQuery)]); const renderCellValue = ({ diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx deleted file mode 100644 index 9758dd969b443..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ /dev/null @@ -1,549 +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 React, { Fragment, FC, useEffect, useState } from 'react'; -import moment from 'moment-timezone'; - -import { i18n } from '@kbn/i18n'; -import { - EuiBadge, - EuiButtonIcon, - EuiCallOut, - EuiCheckbox, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiPopover, - EuiPopoverTitle, - EuiProgress, - EuiSpacer, - EuiText, - EuiToolTip, - Query, -} from '@elastic/eui'; - -import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { mlFieldFormatService } from '../../../../../services/field_format_service'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { - ColumnType, - mlInMemoryTableBasicFactory, - OnTableChangeArg, - SortingPropType, - SORT_DIRECTION, -} from '../../../../../components/ml_in_memory_table'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; -import { Field } from '../../../../../../../common/types/fields'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - isKeywordAndTextType, - sortRegressionResultsFields, -} from '../../../../common/fields'; - -import { - toggleSelectedField, - EsDoc, - DataFrameAnalyticsConfig, - EsFieldName, - MAX_COLUMNS, - getPredictedFieldName, - INDEX_STATUS, - SEARCH_SIZE, - defaultSearchQuery, - getDependentVar, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData, TableItem } from './use_explore_data'; -import { ExplorationTitle } from './classification_exploration'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>(); - -const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText', - { - defaultMessage: 'Showing documents for which predictions exist', - } -); - -const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText', - { - defaultMessage: 'Showing first {searchSize} documents for which predictions exist', - values: { searchSize: SEARCH_SIZE }, - } -); - -interface Props { - indexPattern: IndexPattern; - jobConfig: DataFrameAnalyticsConfig; - jobStatus?: DATA_FRAME_TASK_STATE; - setEvaluateSearchQuery: React.Dispatch<React.SetStateAction<object>>; -} - -export const ResultsTable: FC<Props> = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(25); - const [selectedFields, setSelectedFields] = useState([] as Field[]); - const [docFields, setDocFields] = useState([] as Field[]); - const [depVarType, setDepVarType] = useState<ES_FIELD_TYPES | undefined>(undefined); - const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); - const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); - const [searchError, setSearchError] = useState<any>(undefined); - const [searchString, setSearchString] = useState<string | undefined>(undefined); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - - const dependentVariable = getDependentVar(jobConfig.analysis); - - function toggleColumnsPopover() { - setColumnsPopoverVisible(!isColumnsPopoverVisible); - } - - function closeColumnsPopover() { - setColumnsPopoverVisible(false); - } - - function toggleColumn(column: EsFieldName) { - if (tableItems.length > 0 && jobConfig !== undefined) { - // spread to a new array otherwise the component wouldn't re-render - setSelectedFields([ - ...toggleSelectedField(selectedFields, column, jobConfig.dest.results_field, depVarType), - ]); - } - } - - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - - const { - errorMessage, - loadExploreData, - sortField, - sortDirection, - status, - tableItems, - } = useExploreData( - jobConfig, - needsDestIndexFields, - selectedFields, - setSelectedFields, - setDocFields, - setDepVarType - ); - - const columns: Array<ColumnType<TableItem>> = selectedFields - .sort(({ name: a }, { name: b }) => sortRegressionResultsFields(a, b, jobConfig)) - .map(field => { - const { type } = field; - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, field.id, ''); - } - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - const column: ColumnType<TableItem> = { - field: field.name, - name: field.name, - sortable: true, - truncateText: true, - }; - - const render = (d: any, fullItem: EsDoc) => { - if (format !== undefined) { - d = format.convert(d, 'text'); - return d; - } - - if (Array.isArray(d) && d.every(item => typeof item === 'string')) { - // If the cells data is an array of strings, return as a comma separated list. - // The list will get limited to 5 items with `…` at the end if there's more in the original array. - return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; - } else if (Array.isArray(d)) { - // If the cells data is an array of e.g. objects, display a 'array' badge with a - // tooltip that explains that this type of field is not supported in this table. - return ( - <EuiToolTip - content={i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent', - { - defaultMessage: - 'The full content of this array based column cannot be displayed.', - } - )} - > - <EuiBadge> - {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent', - { - defaultMessage: 'array', - } - )} - </EuiBadge> - </EuiToolTip> - ); - } - - return d; - }; - - if (isNumber) { - column.dataType = 'number'; - column.render = render; - } else if (typeof type !== 'undefined') { - switch (type) { - case ES_FIELD_TYPES.BOOLEAN: - column.dataType = ES_FIELD_TYPES.BOOLEAN; - column.render = d => (d ? 'true' : 'false'); - break; - case ES_FIELD_TYPES.DATE: - column.align = 'right'; - if (format !== undefined) { - column.render = render; - } else { - column.render = (d: any) => { - if (d !== undefined) { - return formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); - } - return d; - }; - } - break; - default: - column.render = render; - break; - } - } else { - column.render = render; - } - - return column; - }); - - const docFieldsCount = docFields.length; - - useEffect(() => { - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - sortField !== undefined && - sortDirection !== undefined && - selectedFields.some(field => field.name === sortField) - ) { - let field = sortField; - // If sorting by predictedField use dependentVar type - if (predictedFieldName === sortField) { - field = dependentVariable; - } - const requiresKeyword = isKeywordAndTextType(field); - - loadExploreData({ - field: sortField, - direction: sortDirection, - searchQuery, - requiresKeyword, - }); - } - }, [JSON.stringify(searchQuery)]); - - useEffect(() => { - // By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`). - // if that's not available sort ascending on the first column. Check if the current sorting field is still available. - if ( - jobConfig !== undefined && - columns.length > 0 && - selectedFields.length > 0 && - !selectedFields.some(field => field.name === sortField) - ) { - const predictedFieldSelected = selectedFields.some( - field => field.name === predictedFieldName - ); - - // CHECK IF keyword suffix is needed (if predicted field is selected we have to check the dependent variable type) - let sortByField = predictedFieldSelected ? dependentVariable : selectedFields[0].name; - - const requiresKeyword = isKeywordAndTextType(sortByField); - - sortByField = predictedFieldSelected ? predictedFieldName : sortByField; - - const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; - loadExploreData({ field: sortByField, direction, searchQuery, requiresKeyword }); - } - }, [ - jobConfig, - columns.length, - selectedFields.length, - sortField, - sortDirection, - tableItems.length, - ]); - - let sorting: SortingPropType = false; - let onTableChange; - - if (columns.length > 0 && sortField !== '' && sortField !== undefined) { - sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - - onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: sortField, direction: sortDirection }, - }: OnTableChangeArg) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - if (sort.field !== sortField || sort.direction !== sortDirection) { - let field = sort.field; - // If sorting by predictedField use depVar for type check - if (predictedFieldName === sort.field) { - field = dependentVariable; - } - - loadExploreData({ - ...sort, - searchQuery, - requiresKeyword: isKeywordAndTextType(field), - }); - } - }; - } - - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: tableItems.length, - pageSizeOptions: PAGE_SIZE_OPTIONS, - hidePerPageOptions: false, - }; - - const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { - if (error) { - setSearchError(error.message); - } else { - try { - const esQueryDsl = Query.toESQuery(query); - setSearchQuery(esQueryDsl); - setSearchString(query.text); - setSearchError(undefined); - // set query for use in evaluate panel - setEvaluateSearchQuery(esQueryDsl); - } catch (e) { - setSearchError(e.toString()); - } - } - }; - - const search = { - onChange: onQueryChange, - defaultQuery: searchString, - box: { - incremental: false, - placeholder: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', - { - defaultMessage: 'E.g. avg>0.5', - } - ), - }, - filters: [ - { - type: 'field_value_toggle_group', - field: `${jobConfig.dest.results_field}.is_training`, - items: [ - { - value: false, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', - { - defaultMessage: 'Testing', - } - ), - }, - { - value: true, - name: i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', - { - defaultMessage: 'Training', - } - ), - }, - ], - }, - ], - }; - - if (jobConfig === undefined) { - return null; - } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { - return ( - <EuiPanel grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem grow={false}> - <ExplorationTitle jobId={jobConfig.id} /> - </EuiFlexItem> - {jobStatus !== undefined && ( - <EuiFlexItem grow={false}> - <span>{getTaskStateBadge(jobStatus)}</span> - </EuiFlexItem> - )} - </EuiFlexGroup> - <EuiCallOut - title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.indexError', { - defaultMessage: 'An error occurred loading the index data.', - })} - color="danger" - iconType="cross" - > - <p>{errorMessage}</p> - </EuiCallOut> - </EuiPanel> - ); - } - - const tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : searchError; - - return ( - <EuiPanel - grow={false} - id="mlDataFrameAnalyticsTableResultsPanel" - data-test-subj="mlDFAnalyticsClassificationExplorationTablePanel" - > - <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}> - <EuiFlexItem grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem grow={false}> - <ExplorationTitle jobId={jobConfig.id} /> - </EuiFlexItem> - {jobStatus !== undefined && ( - <EuiFlexItem grow={false}> - <span>{getTaskStateBadge(jobStatus)}</span> - </EuiFlexItem> - )} - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem> - <EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}> - <EuiFlexItem style={{ textAlign: 'right' }}> - {docFieldsCount > MAX_COLUMNS && ( - <EuiText size="s"> - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - } - )} - </EuiText> - )} - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiText size="s"> - <EuiPopover - id="popover" - button={ - <EuiButtonIcon - iconType="gear" - onClick={toggleColumnsPopover} - aria-label={i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel', - { - defaultMessage: 'Select columns', - } - )} - /> - } - isOpen={isColumnsPopoverVisible} - closePopover={closeColumnsPopover} - ownFocus - > - <EuiPopoverTitle> - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', - { - defaultMessage: 'Select fields', - } - )} - </EuiPopoverTitle> - <div style={{ maxHeight: '400px', overflowY: 'scroll' }}> - {docFields.map(({ name }) => ( - <EuiCheckbox - key={name} - id={name} - label={name} - checked={selectedFields.some(field => field.name === name)} - onChange={() => toggleColumn(name)} - disabled={ - selectedFields.some(field => field.name === name) && - selectedFields.length === 1 - } - /> - ))} - </div> - </EuiPopover> - </EuiText> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - {status === INDEX_STATUS.LOADING && <EuiProgress size="xs" color="accent" />} - {status !== INDEX_STATUS.LOADING && ( - <EuiProgress size="xs" color="accent" max={1} value={0} /> - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - <Fragment> - <EuiFormRow - helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs} - > - <Fragment /> - </EuiFormRow> - <EuiSpacer /> - <MlInMemoryTableBasic - allowNeutralSort={false} - columns={columns} - compressed - hasActions={false} - isSelectable={false} - items={tableItems} - onTableChange={onTableChange} - pagination={pagination} - responsive={false} - search={search} - error={tableError} - sorting={sorting} - /> - </Fragment> - )} - </EuiPanel> - ); - } -); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts deleted file mode 100644 index 9527a9adb98ce..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts +++ /dev/null @@ -1,208 +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. - */ -/* - * 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, { useEffect, useState } from 'react'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { Field } from '../../../../../../../common/types/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, -} from '../../../../common/analytics'; - -import { - getDefaultFieldsFromJobCaps, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, - SEARCH_SIZE, - SearchQuery, -} from '../../../../common'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -interface LoadClassificationExploreDataArg { - direction: SortDirection; - filterByIsTraining?: boolean; - field: string; - searchQuery: SavedSearchQuery; - requiresKeyword?: boolean; - pageIndex?: number; - pageSize?: number; -} - -export type TableItem = Record<string, any>; - -export interface UseExploreDataReturnType { - errorMessage: string; - loadExploreData: (arg: LoadClassificationExploreDataArg) => void; - sortField: EsFieldName; - sortDirection: SortDirection; - status: INDEX_STATUS; - tableItems: TableItem[]; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig | undefined, - needsDestIndexFields: boolean, - selectedFields: Field[], - setSelectedFields: React.Dispatch<React.SetStateAction<Field[]>>, - setDocFields: React.Dispatch<React.SetStateAction<Field[]>>, - setDepVarType: React.Dispatch<React.SetStateAction<ES_FIELD_TYPES | undefined>> -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - const [tableItems, setTableItems] = useState<TableItem[]>([]); - const [sortField, setSortField] = useState<string>(''); - const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { - selectedFields: defaultSelected, - docFields, - depVarType, - } = getDefaultFieldsFromJobCaps(fields, jobConfig, needsDestIndexFields); - - setDepVarType(depVarType); - setSelectedFields(defaultSelected); - setDocFields(docFields); - } - }; - - const loadExploreData = async ({ - field, - direction, - searchQuery, - requiresKeyword, - }: LoadClassificationExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(searchQuery); - let query: ResultsSearchQuery; - - if (JSON.stringify(searchQuery) === JSON.stringify(defaultSearchQuery)) { - query = { - exists: { - field: resultsField, - }, - }; - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const body: SearchQuery = { - query, - }; - - if (field !== undefined) { - body.sort = [ - { - [`${field}${requiresKeyword ? '.keyword' : ''}`]: { - order: direction, - }, - }, - ]; - } - - const resp: SearchResponse<any> = await ml.esSearch({ - index: jobConfig.dest.index, - size: SEARCH_SIZE, - body, - }); - - setSortField(field); - setSortDirection(direction); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - if (jobConfig !== undefined) { - getDefaultSelectedFields(); - } - }, [jobConfig && jobConfig.id]); - - return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx index 9765192f0e446..839587c47289a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx @@ -15,7 +15,7 @@ interface Props { export const ErrorCallout: FC<Props> = ({ error }) => { let errorCallout = ( <EuiCallOut - title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.generalError', { + title={i18n.translate('xpack.ml.dataframe.analytics.errorCallout.generalErrorTitle', { defaultMessage: 'An error occurred loading the data.', })} color="danger" @@ -28,14 +28,14 @@ export const ErrorCallout: FC<Props> = ({ error }) => { if (error.includes('index_not_found')) { errorCallout = ( <EuiCallOut - title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.evaluateError', { + title={i18n.translate('xpack.ml.dataframe.analytics.errorCallout.evaluateErrorTitle', { defaultMessage: 'An error occurred loading the data.', })} color="danger" iconType="cross" > <p> - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noIndexCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the destination index exists and contains documents.', })} @@ -46,16 +46,13 @@ export const ErrorCallout: FC<Props> = ({ error }) => { // Job was started but no results have been written yet errorCallout = ( <EuiCallOut - title={i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle', - { - defaultMessage: 'Empty index query result.', - } - )} + title={i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noDataCalloutTitle', { + defaultMessage: 'Empty index query result.', + })} color="primary" > <p> - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noDataCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the job has completed and the index contains documents.', })} @@ -66,22 +63,16 @@ export const ErrorCallout: FC<Props> = ({ error }) => { // query bar syntax is incorrect errorCallout = ( <EuiCallOut - title={i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage', - { - defaultMessage: 'Unable to parse query.', - } - )} + title={i18n.translate('xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorTitle', { + defaultMessage: 'Unable to parse query.', + })} color="primary" > <p> - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody', - { - defaultMessage: - 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', - } - )} + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorBody', { + defaultMessage: + 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', + })} </p> </EuiCallOut> ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx deleted file mode 100644 index e88bc1cd06f95..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx +++ /dev/null @@ -1,161 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const FEATURE_INFLUENCE = 'feature_influence'; -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; -type TableItem = Record<string, any>; - -interface ExplorationDataGridProps { - colorRange: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch<SetStateAction<Pagination>>; - setSelectedFields: Dispatch<SetStateAction<string[]>>; - setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ExplorationDataGrid: FC<ExplorationDataGridProps> = ({ - colorRange, - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - const split = columnId.split('.'); - let backgroundColor; - - // column with feature values get color coded by its corresponding influencer value - if (fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { - backgroundColor = colorRange(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`]); - } - - // column with influencer values get color coded by its own value - if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { - backgroundColor = colorRange(cellValue); - } - - if (backgroundColor !== undefined) { - setCellProps({ - style: { backgroundColor }, - }); - } - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - <EuiDataGrid - aria-label={i18n.translate('xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel', { - defaultMessage: 'Outlier detection results table', - })} - columns={columns} - columnVisibility={{ - visibleColumns: selectedFields, - setVisibleColumns: setSelectedFields, - }} - gridStyle={euiDataGridStyle} - rowCount={rowCount} - renderCellValue={renderCellValue} - sorting={{ columns: sortingColumns, onSort }} - toolbarVisibility={euiDataGridToolbarSettings} - pagination={{ - ...pagination, - pageSizeOptions: PAGE_SIZE_OPTIONS, - onChangeItemsPerPage, - onChangePage, - }} - /> - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts deleted file mode 100644 index ea89e91de5046..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { ExplorationDataGrid } from './exploration_data_grid'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx new file mode 100644 index 0000000000000..1986c486974c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -0,0 +1,76 @@ +/* + * 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, { FC, useState } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { useResultsViewConfig, DataFrameAnalyticsConfig } from '../../../../common'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; + +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { ExplorationResultsTable } from '../exploration_results_table'; +import { JobConfigErrorCallout } from '../job_config_error_callout'; +import { LoadingPanel } from '../loading_panel'; + +export interface EvaluatePanelProps { + jobConfig: DataFrameAnalyticsConfig; + jobStatus?: DATA_FRAME_TASK_STATE; + searchQuery: ResultsSearchQuery; +} + +interface Props { + jobId: string; + title: string; + EvaluatePanel: FC<EvaluatePanelProps>; +} + +export const ExplorationPageWrapper: FC<Props> = ({ jobId, title, EvaluatePanel }) => { + const { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery); + + if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { + return ( + <JobConfigErrorCallout + jobCapsServiceErrorMessage={jobCapsServiceErrorMessage} + jobConfigErrorMessage={jobConfigErrorMessage} + title={title} + /> + ); + } + + return ( + <> + {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} + {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( + <EvaluatePanel jobConfig={jobConfig} jobStatus={jobStatus} searchQuery={searchQuery} /> + )} + <EuiSpacer /> + {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} + {isLoadingJobConfig === false && + jobConfig !== undefined && + indexPattern !== undefined && + isInitialized === true && ( + <ExplorationResultsTable + jobConfig={jobConfig} + indexPattern={indexPattern} + jobStatus={jobStatus} + setEvaluateSearchQuery={setSearchQuery} + title={title} + /> + )} + </> + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts new file mode 100644 index 0000000000000..bf294a3cd08c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { EvaluatePanelProps, ExplorationPageWrapper } from './exploration_page_wrapper'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx new file mode 100644 index 0000000000000..24e5785c6e808 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -0,0 +1,172 @@ +/* + * 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, { Fragment, FC, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; + +import { + DataFrameAnalyticsConfig, + MAX_COLUMNS, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, +} from '../../../../common'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { ExplorationTitle } from '../exploration_title'; +import { ExplorationQueryBar } from '../exploration_query_bar'; + +import { useExplorationResults } from './use_exploration_results'; + +const showingDocs = i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', + { + defaultMessage: 'Showing documents for which predictions exist', + } +); + +const showingFirstDocs = i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText', + { + defaultMessage: 'Showing first {searchSize} documents for which predictions exist', + values: { searchSize: SEARCH_SIZE }, + } +); + +interface Props { + indexPattern: IndexPattern; + jobConfig: DataFrameAnalyticsConfig; + jobStatus?: DATA_FRAME_TASK_STATE; + setEvaluateSearchQuery: React.Dispatch<React.SetStateAction<object>>; + title: string; +} + +export const ExplorationResultsTable: FC<Props> = React.memo( + ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { + const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); + + useEffect(() => { + setEvaluateSearchQuery(searchQuery); + }, [JSON.stringify(searchQuery)]); + + const classificationData = useExplorationResults(indexPattern, jobConfig, searchQuery); + const docFieldsCount = classificationData.columns.length; + const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; + + if (jobConfig === undefined || classificationData === undefined) { + return null; + } + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + return ( + <EuiPanel grow={false}> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow={false}> + <ExplorationTitle title={title} /> + </EuiFlexItem> + {jobStatus !== undefined && ( + <EuiFlexItem grow={false}> + <span>{getTaskStateBadge(jobStatus)}</span> + </EuiFlexItem> + )} + </EuiFlexGroup> + <EuiCallOut + title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.indexError', { + defaultMessage: 'An error occurred loading the index data.', + })} + color="danger" + iconType="cross" + > + <p>{errorMessage}</p> + </EuiCallOut> + </EuiPanel> + ); + } + + return ( + <EuiPanel + grow={false} + id="mlDataFrameAnalyticsTableResultsPanel" + data-test-subj="mlDFAnalyticsExplorationTablePanel" + > + <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow={false}> + <ExplorationTitle title={title} /> + </EuiFlexItem> + {jobStatus !== undefined && ( + <EuiFlexItem grow={false}> + <span>{getTaskStateBadge(jobStatus)}</span> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}> + <EuiFlexItem style={{ textAlign: 'right' }}> + {docFieldsCount > MAX_COLUMNS && ( + <EuiText size="s"> + {i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.fieldSelection', + { + defaultMessage: + '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', + values: { selectedFieldsLength: visibleColumns.length, docFieldsCount }, + } + )} + </EuiText> + )} + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + <EuiFlexGroup direction="column"> + <EuiFlexItem grow={false}> + <EuiSpacer size="s" /> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem> + <ExplorationQueryBar + indexPattern={indexPattern} + setSearchQuery={setSearchQuery} + /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiFormRow + helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs} + > + <Fragment /> + </EuiFormRow> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <DataGrid + {...classificationData} + dataTestSubj="mlExplorationDataGrid" + toastNotifications={getToastNotifications()} + /> + </EuiFlexItem> + </EuiFlexGroup> + )} + </EuiPanel> + ); + } +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts new file mode 100644 index 0000000000000..19308640c8b02 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ExplorationResultsTable } from './exploration_results_table'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts new file mode 100644 index 0000000000000..6f9dc694d8172 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -0,0 +1,72 @@ +/* + * 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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_IMPORTANCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +export const useExplorationResults = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-importance columns and the doc id copy + d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== ML__ID_COPY + ); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx new file mode 100644 index 0000000000000..f06c88c73df71 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.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 React, { FC } from 'react'; + +import { EuiTitle } from '@elastic/eui'; + +export const ExplorationTitle: FC<{ title: string }> = ({ title }) => ( + <EuiTitle size="xs"> + <span>{title}</span> + </EuiTitle> +); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts new file mode 100644 index 0000000000000..b34e61b3b5e76 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { ExplorationTitle } from './exploration_title'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts new file mode 100644 index 0000000000000..a5991f4325d12 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { JobConfigErrorCallout } from './job_config_error_callout'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx new file mode 100644 index 0000000000000..945d6654067c0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx @@ -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 React, { FC } from 'react'; + +import { EuiCallOut, EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ExplorationTitle } from '../exploration_title'; + +const jobConfigErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobConfig.errorTitle', { + defaultMessage: 'Unable to fetch results. An error occurred loading the job configuration data.', +}); + +const jobCapsErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobCaps.errorTitle', { + defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", +}); + +interface Props { + jobCapsServiceErrorMessage: string | undefined; + jobConfigErrorMessage: string | undefined; + title: string; +} + +export const JobConfigErrorCallout: FC<Props> = ({ + jobCapsServiceErrorMessage, + jobConfigErrorMessage, + title, +}) => { + return ( + <EuiPanel grow={false}> + <ExplorationTitle title={title} /> + <EuiSpacer /> + <EuiCallOut + title={jobConfigErrorMessage ? jobConfigErrorTitle : jobCapsErrorTitle} + color="danger" + iconType="cross" + > + <p>{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}</p> + </EuiCallOut> + </EuiPanel> + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts similarity index 100% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts new file mode 100644 index 0000000000000..bfd3dd33995aa --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -0,0 +1,23 @@ +/* + * 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 { DataGridItem } from '../../../../../components/data_grid'; + +import { DataFrameAnalyticsConfig } from '../../../../common'; +import { FEATURE_INFLUENCE, OUTLIER_SCORE } from '../../../../common/constants'; + +export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => + `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; + +export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] = []) => { + if (tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) + ).length; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index fdcb7d9d237e3..0154f92576c4a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -15,127 +15,51 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { useColorRange, - ColorRangeLegend, COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; +import { ColorRangeLegend } from '../../../../../components/color_range_legend'; +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; -import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common'; +import { defaultSearchQuery, useResultsViewConfig, INDEX_STATUS } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { useExploreData, TableItem } from '../../hooks/use_explore_data'; - -import { ExplorationDataGrid } from '../exploration_data_grid'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { ExplorationTitle } from '../exploration_title'; -const FEATURE_INFLUENCE = 'feature_influence'; +import { getFeatureCount } from './common'; +import { useOutlierData } from './use_outlier_data'; -const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( - <EuiTitle size="xs"> - <span> - {i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { - defaultMessage: 'Outlier detection job ID {jobId}', - values: { jobId }, - })} - </span> - </EuiTitle> -); +export type TableItem = Record<string, any>; interface ExplorationProps { jobId: string; } -const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { - if (tableItems.length === 0) { - return 0; - } - - return Object.keys(tableItems[0]).filter(key => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; -}; - export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) => { - const { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - searchQuery, - selectedFields, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - rowCount, - status, - tableFields, - tableItems, - } = useExploreData(jobId); - - const columns = []; - - if ( - jobConfig !== undefined && - indexPattern !== undefined && - selectedFields.length > 0 && - tableItems.length > 0 - ) { - const resultsField = jobConfig.dest.results_field; - const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); - columns.push( - ...tableFields.sort(sortColumns(tableItems[0], resultsField)).map(id => { - const idWithoutPrefix = id.replace(removePrefix, ''); - const field = indexPattern.fields.getByName(idWithoutPrefix); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'number': - schema = 'numeric'; - break; - } - - if (id === `${resultsField}.outlier_score`) { - schema = 'numeric'; - } - - return { id, schema }; - }) - ); - } + const explorationTitle = i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { + defaultMessage: 'Outlier detection job ID {jobId}', + values: { jobId }, + }); - const colorRange = useColorRange( - COLOR_RANGE.BLUE, - COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 - ); + const { indexPattern, jobConfig, jobStatus } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); + const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); - if (jobConfig === undefined || indexPattern === undefined) { - return null; - } + const { columns, errorMessage, status, tableItems } = outlierData; // if it's a searchBar syntax error leave the table visible so they can try again if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( <EuiPanel grow={false}> - <ExplorationTitle jobId={jobConfig.id} /> + <ExplorationTitle title={explorationTitle} /> <EuiCallOut title={i18n.translate('xpack.ml.dataframe.analytics.exploration.indexError', { defaultMessage: 'An error occurred loading the index data.', @@ -149,17 +73,11 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) = ); } - let tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : undefined; - - if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) { - tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', - }); - } + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 + ); return ( <EuiPanel data-test-subj="mlDFAnalyticsOutlierExplorationTablePanel"> @@ -170,7 +88,7 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) = gutterSize="s" > <EuiFlexItem grow={false}> - <ExplorationTitle jobId={jobConfig.id} /> + <ExplorationTitle title={explorationTitle} /> </EuiFlexItem> {jobStatus !== undefined && ( <EuiFlexItem grow={false}> @@ -179,7 +97,7 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) = )} </EuiFlexGroup> <EuiHorizontalRule margin="xs" /> - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && indexPattern !== undefined && ( <> <EuiFlexGroup justifyContent="spaceBetween"> <EuiFlexItem> @@ -200,19 +118,10 @@ export const OutlierExploration: FC<ExplorationProps> = React.memo(({ jobId }) = </EuiFlexGroup> <EuiSpacer size="s" /> {columns.length > 0 && tableItems.length > 0 && ( - <ExplorationDataGrid - colorRange={colorRange} - columns={columns} - indexPattern={indexPattern} - pagination={pagination} - resultsField={jobConfig.dest.results_field} - rowCount={rowCount} - selectedFields={selectedFields} - setPagination={setPagination} - setSelectedFields={setSelectedFields} - setSortingColumns={setSortingColumns} - sortingColumns={sortingColumns} - tableItems={tableItems} + <DataGrid + {...outlierData} + dataTestSubj="mlExplorationDataGrid" + toastNotifications={getToastNotifications()} /> )} </> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts new file mode 100644 index 0000000000000..91a5ba2db6908 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { DataFrameAnalyticsConfig } from '../../../../common'; + +import { getOutlierScoreFieldName } from './common'; + +describe('Data Frame Analytics: <Exploration /> common utils', () => { + test('getOutlierScoreFieldName()', () => { + const jobConfig: DataFrameAnalyticsConfig = { + id: 'the-id', + analysis: { outlier_detection: {} }, + dest: { + index: 'the-dest-index', + results_field: 'the-results-field', + }, + source: { + index: 'the-source-index', + }, + analyzed_fields: { includes: [], excludes: [] }, + model_memory_limit: '50mb', + create_time: 1234, + version: '1.0.0', + }; + + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + + expect(outlierScoreFieldName).toMatch('the-results-field.outlier_score'); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts new file mode 100644 index 0000000000000..0d06bc0d43307 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + useColorRange, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from '../../../../../components/color_range_legend'; +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +import { getFeatureCount, getOutlierScoreFieldName } from './common'; + +export const useOutlierData = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined && indexPattern !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-influence columns and the doc id copy + d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY + ); + + // initialize sorting: reverse sort on outlier score column + useEffect(() => { + if (jobConfig !== undefined) { + dataGrid.setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); + } + }, [jobConfig && jobConfig.id]); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, dataGrid.tableItems) : 1 + ); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD, + (columnId, cellValue, fullItem, setCellProps) => { + const resultsField = jobConfig?.dest.results_field ?? ''; + + const split = columnId.split('.'); + let backgroundColor; + + // column with feature values get color coded by its corresponding influencer value + if ( + fullItem[resultsField] !== undefined && + fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`] !== undefined + ) { + backgroundColor = colorRange(fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`]); + } + + // column with influencer values get color coded by its own value + if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { + backgroundColor = colorRange(cellValue); + } + + if (backgroundColor !== undefined) { + setCellProps({ + style: { backgroundColor }, + }); + } + } + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 6ef6666be5ec6..f6e8e0047671f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -235,7 +235,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} > {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink', + 'xpack.ml.dataframe.analytics.regressionExploration.regressionDocsLink', { defaultMessage: 'Regression evaluation docs ', } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index bfeca76a2b1c7..36d91f6f41d44 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -4,174 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - <EuiTitle size="xs"> - <span> - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for regression job ID {jobId}', - values: { jobId }, - })} - </span> - </EuiTitle> -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const RegressionExploration: FC<Props> = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined); - const [jobStatus, setJobStatus] = useState<DATA_FRAME_TASK_STATE | undefined>(undefined); - const [indexPattern, setIndexPattern] = useState<any | undefined>(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false); - const [isInitialized, setIsInitialized] = useState<boolean>(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState<undefined | string>( - undefined - ); - const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IIndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - <EuiPanel grow={false}> - <ExplorationTitle jobId={jobId} /> - <EuiSpacer /> - <EuiCallOut - title={jobConfigErrorMessage ? jobConfigErrorTitle : jobCapsErrorTitle} - color="danger" - iconType="cross" - > - <p>{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}</p> - </EuiCallOut> - </EuiPanel> - ); - } - return ( - <Fragment> - {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - <EvaluatePanel jobConfig={jobConfig} jobStatus={jobStatus} searchQuery={searchQuery} /> - )} - <EuiSpacer /> - {isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />} - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - <ResultsTable - jobConfig={jobConfig} - indexPattern={indexPattern} - jobStatus={jobStatus} - setEvaluateSearchQuery={setSearchQuery} - /> - )} - </Fragment> + <ExplorationPageWrapper + jobId={jobId} + title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { + defaultMessage: 'Destination index for regression job ID {jobId}', + values: { jobId }, + })} + EvaluatePanel={EvaluatePanel} + /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx deleted file mode 100644 index 0fcb1ed600719..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +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 React, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; -type TableItem = Record<string, any>; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch<SetStateAction<Pagination>>; - setSelectedFields: Dispatch<SetStateAction<string[]>>; - setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const RegressionExplorationDataGrid: FC<ExplorationDataGridProps> = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - <EuiDataGrid - aria-label={i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.dataGridAriaLabel', - { - defaultMessage: 'Regression results table', - } - )} - columns={columns} - columnVisibility={{ - visibleColumns: selectedFields, - setVisibleColumns: setSelectedFields, - }} - gridStyle={euiDataGridStyle} - rowCount={rowCount} - renderCellValue={renderCellValue} - sorting={{ columns: sortingColumns, onSort }} - toolbarVisibility={euiDataGridToolbarSettings} - pagination={{ - ...pagination, - pageSizeOptions: PAGE_SIZE_OPTIONS, - onChangeItemsPerPage, - onChangePage, - }} - /> - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx deleted file mode 100644 index 43fa50b2e4df5..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ /dev/null @@ -1,233 +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 React, { Fragment, FC, useEffect } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiProgress, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; - -import { - DataFrameAnalyticsConfig, - MAX_COLUMNS, - INDEX_STATUS, - SEARCH_SIZE, - defaultSearchQuery, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { useExploreData } from './use_explore_data'; -import { ExplorationTitle } from './regression_exploration'; -import { RegressionExplorationDataGrid } from './regression_exploration_data_grid'; -import { ExplorationQueryBar } from '../exploration_query_bar'; - -const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', - { - defaultMessage: 'Showing documents for which predictions exist', - } -); - -const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText', - { - defaultMessage: 'Showing first {searchSize} documents for which predictions exist', - values: { searchSize: SEARCH_SIZE }, - } -); - -interface Props { - indexPattern: IndexPattern; - jobConfig: DataFrameAnalyticsConfig; - jobStatus?: DATA_FRAME_TASK_STATE; - setEvaluateSearchQuery: React.Dispatch<React.SetStateAction<object>>; -} - -export const ResultsTable: FC<Props> = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); - - useEffect(() => { - setEvaluateSearchQuery(searchQuery); - }, [JSON.stringify(searchQuery)]); - - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; - - if (jobConfig === undefined) { - return null; - } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { - return ( - <EuiPanel grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem grow={false}> - <ExplorationTitle jobId={jobConfig.id} /> - </EuiFlexItem> - {jobStatus !== undefined && ( - <EuiFlexItem grow={false}> - <span>{getTaskStateBadge(jobStatus)}</span> - </EuiFlexItem> - )} - </EuiFlexGroup> - <EuiCallOut - title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.indexError', { - defaultMessage: 'An error occurred loading the index data.', - })} - color="danger" - iconType="cross" - > - <p>{errorMessage}</p> - </EuiCallOut> - </EuiPanel> - ); - } - - return ( - <EuiPanel grow={false} data-test-subj="mlDFAnalyticsRegressionExplorationTablePanel"> - <EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}> - <EuiFlexItem grow={false}> - <EuiFlexGroup gutterSize="s"> - <EuiFlexItem grow={false}> - <ExplorationTitle jobId={jobConfig.id} /> - </EuiFlexItem> - {jobStatus !== undefined && ( - <EuiFlexItem grow={false}> - <span>{getTaskStateBadge(jobStatus)}</span> - </EuiFlexItem> - )} - </EuiFlexGroup> - </EuiFlexItem> - <EuiFlexItem> - <EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}> - <EuiFlexItem style={{ textAlign: 'right' }}> - {docFieldsCount > MAX_COLUMNS && ( - <EuiText size="s"> - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - } - )} - </EuiText> - )} - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - </EuiFlexGroup> - {status === INDEX_STATUS.LOADING && <EuiProgress size="xs" color="accent" />} - {status !== INDEX_STATUS.LOADING && ( - <EuiProgress size="xs" color="accent" max={1} value={0} /> - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - <EuiFlexGroup direction="column"> - <EuiFlexItem grow={false}> - <EuiSpacer size="s" /> - <EuiFlexGroup justifyContent="spaceBetween"> - <EuiFlexItem> - <ExplorationQueryBar - indexPattern={indexPattern} - setSearchQuery={setSearchQuery} - /> - </EuiFlexItem> - </EuiFlexGroup> - <EuiFormRow - helpText={tableItems.length === SEARCH_SIZE ? showingFirstDocs : showingDocs} - > - <Fragment /> - </EuiFormRow> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <RegressionExplorationDataGrid - columns={columns} - indexPattern={indexPattern} - pagination={pagination} - resultsField={jobConfig.dest.results_field} - rowCount={rowCount} - selectedFields={selectedFields} - setPagination={setPagination} - setSelectedFields={setSelectedFields} - setSortingColumns={setSortingColumns} - sortingColumns={sortingColumns} - tableItems={tableItems} - /> - </EuiFlexItem> - </EuiFlexGroup> - )} - </EuiPanel> - ); - } -); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts deleted file mode 100644 index c68fe5b2cbee8..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ /dev/null @@ -1,291 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - LoadRegressionExploreDataArg, - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, -} from '../../../../common/analytics'; - -export type TableItem = Record<string, any>; -type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch<SetStateAction<undefined | boolean>>; - setPagination: Dispatch<SetStateAction<Pagination>>; - setSearchQuery: Dispatch<SetStateAction<SavedSearchQuery>>; - setSelectedFields: Dispatch<SetStateAction<EsFieldName[]>>; - setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse<any> { - hits: SearchResponse<any>['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState<string[]>([]); - const [tableItems, setTableItems] = useState<TableItem[]>([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState<undefined | boolean>(undefined); - const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadRegressionExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts deleted file mode 100644 index 48db8e34c934e..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts +++ /dev/null @@ -1,12 +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 { DataFrameAnalyticsConfig } from '../../../../common'; - -const OUTLIER_SCORE = 'outlier_score'; - -export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => - `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts deleted file mode 100644 index dd896ca02f7f7..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { useExploreData, TableItem } from './use_explore_data'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts deleted file mode 100644 index a0a9eb8312499..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts +++ /dev/null @@ -1,267 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { Dictionary } from '../../../../../../../common/types/common'; - -import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { ml } from '../../../../../services/ml_api_service'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; - -import { - getDefaultSelectableFields, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, - MAX_COLUMNS, - defaultSearchQuery, -} from '../../../../common'; -import { isKeywordAndTextType } from '../../../../common/fields'; - -import { getOutlierScoreFieldName } from './common'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; - -export type TableItem = Record<string, any>; - -type Pagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; - -interface UseExploreDataReturnType { - errorMessage: string; - indexPattern: IndexPattern | undefined; - jobConfig: DataFrameAnalyticsConfig | undefined; - jobStatus: DATA_FRAME_TASK_STATE | undefined; - pagination: Pagination; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setJobConfig: Dispatch<SetStateAction<DataFrameAnalyticsConfig | undefined>>; - setPagination: Dispatch<SetStateAction<Pagination>>; - setSearchQuery: Dispatch<SetStateAction<SavedSearchQuery>>; - setSelectedFields: Dispatch<SetStateAction<EsFieldName[]>>; - setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse<any> { - hits: SearchResponse<any>['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = (jobId: string): UseExploreDataReturnType => { - const mlContext = useMlContext(); - - const [indexPattern, setIndexPattern] = useState<IndexPattern | undefined>(undefined); - const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined); - const [jobStatus, setJobStatus] = useState<DATA_FRAME_TASK_STATE | undefined>(undefined); - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState<string[]>([]); - const [tableItems, setTableItems] = useState<TableItem[]>([]); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery); - const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]); - - // get analytics configuration - useEffect(() => { - (async function() { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - } - })(); - }, []); - - // get index pattern and field caps - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - } catch (e) { - // eslint-disable-next-line - console.log('Error loading index field data', e); - } - } - })(); - }, [jobConfig && jobConfig.id]); - - // initialize sorting: reverse sort on outlier score column - useEffect(() => { - if (jobConfig !== undefined) { - setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); - } - }, [jobConfig && jobConfig.id]); - - // update data grid data - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const { pageIndex, pageSize } = pagination; - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query: searchQuery, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultSelectableFields(docs, resultsField); - setSelectedFields(newSelectedFields.sort().splice(0, MAX_COLUMNS)); - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableFields(flattenedFields); - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - })(); - }, [jobConfig && jobConfig.id, pagination, searchQuery, selectedFields, sortingColumns]); - - return { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - rowCount, - searchQuery, - selectedFields, - setJobConfig, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - }; -}; 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 eb1871c98764b..cc75ddbe08cfb 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 @@ -11,12 +11,14 @@ import { i18n } from '@kbn/i18n'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, } 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 { /** @@ -214,7 +216,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo }, results_field: { optional: true, - defaultValue: 'ml', + defaultValue: DEFAULT_RESULTS_FIELD, }, }, model_memory_limit: { @@ -321,6 +323,8 @@ interface CloneActionProps { * to support EuiContext with a valid DOM structure without nested buttons. */ export const CloneAction: FC<CloneActionProps> = ({ createAnalyticsForm, item }) => { + const canCreateDataFrameAnalytics: boolean = checkPermission('canCreateDataFrameAnalytics'); + const buttonText = i18n.translate('xpack.ml.dataframe.analyticsList.cloneJobButtonLabel', { defaultMessage: 'Clone job', }); @@ -337,6 +341,7 @@ export const CloneAction: FC<CloneActionProps> = ({ createAnalyticsForm, item }) iconType="copy" onClick={onClick} aria-label={buttonText} + disabled={canCreateDataFrameAnalytics === false} > {buttonText} </EuiButtonEmpty> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 6d1db5033863b..3e5224b76329e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { render } from '@testing-library/react'; -import * as CheckPrivilige from '../../../../../privilege/check_privilege'; +import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; import { DeleteAction } from './action_delete'; import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; -jest.mock('../../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index 47fc84cf450c0..2923938ae68ac 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -19,7 +19,7 @@ import { deleteAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx index 40664a1413845..bbbda85c45e49 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx @@ -19,7 +19,7 @@ import { startAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 4e19df9ae22a8..72514c91ff58b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -12,7 +12,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { getAnalysisType, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index d2e5f582d23f6..58bc75bd7309b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { getTaskStateBadge } from './columns'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index e5054e8a6ad2c..3c8c3c3b3aa55 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { createPermissionFailureMessage } from '../../../../../privilege/check_privilege'; +import { createPermissionFailureMessage } from '../../../../../capabilities/check_capabilities'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; export const CreateAnalyticsButton: FC<CreateAnalyticsFormProps> = props => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index e121268e65e86..70840a442f6f6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -6,7 +6,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap index 46428ff9c351a..59481cb086566 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/edit_flyout/__snapshots__/overrides.test.js.snap @@ -70,6 +70,7 @@ exports[`Overrides render overrides 1`] = ` "asPlainText": true, } } + sortMatchesBy="none" /> </EuiFormRow> <EuiFormRow @@ -322,6 +323,7 @@ exports[`Overrides render overrides 1`] = ` "asPlainText": true, } } + sortMatchesBy="none" /> </EuiFormRow> <EuiFormRow @@ -358,6 +360,7 @@ exports[`Overrides render overrides 1`] = ` "asPlainText": true, } } + sortMatchesBy="none" /> </EuiFormRow> </EuiForm> diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx index 30fc74acbabf4..32b51c8b7d4ee 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx @@ -55,9 +55,11 @@ export const FilebeatConfigFlyout: FC<Props> = ({ } = useMlKibana(); useEffect(() => { - security.authc.getCurrentUser().then(user => { - setUsername(user.username === undefined ? null : user.username); - }); + if (security !== undefined) { + security.authc.getCurrentUser().then(user => { + setUsername(user.username === undefined ? null : user.username); + }); + } }, []); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx index dddf64ce2cfd3..c05ca63df3a75 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { ml } from '../../../../services/ml_api_service'; import { isFullLicense } from '../../../../license'; -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { useMlKibana } from '../../../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts index 7b6464570e55c..7d966949624c1 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.ts @@ -9,11 +9,13 @@ import numeral from '@elastic/numeral'; import { ml } from '../../../../services/ml_api_service'; import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer'; import { + MAX_FILE_SIZE, MAX_FILE_SIZE_BYTES, ABSOLUTE_MAX_FILE_SIZE_BYTES, FILE_SIZE_DISPLAY_FORMAT, } from '../../../../../../common/constants/file_datavisualizer'; -import { getMlConfig } from '../../../../util/dependency_cache'; +import { getUiSettings } from '../../../../util/dependency_cache'; +import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../../../../../common/constants/settings'; const DEFAULT_LINES_TO_SAMPLE = 1000; const UPLOAD_SIZE_MB = 5; @@ -62,13 +64,13 @@ export function readFile(file: File) { } export function getMaxBytes() { - const maxFileSize = getMlConfig().file_data_visualizer.max_file_size; + const maxFileSize = getUiSettings().get(FILE_DATA_VISUALIZER_MAX_FILE_SIZE, MAX_FILE_SIZE); // @ts-ignore const maxBytes = numeral(maxFileSize.toUpperCase()).value(); if (maxBytes < MAX_FILE_SIZE_BYTES) { return MAX_FILE_SIZE_BYTES; } - return maxBytes < ABSOLUTE_MAX_FILE_SIZE_BYTES ? maxBytes : ABSOLUTE_MAX_FILE_SIZE_BYTES; + return maxBytes <= ABSOLUTE_MAX_FILE_SIZE_BYTES ? maxBytes : ABSOLUTE_MAX_FILE_SIZE_BYTES; } export function getMaxBytesFormatted() { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 3a37274edbc16..86ffc4a2614b9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -33,7 +33,7 @@ import { NavigationMenu } from '../../components/navigation_menu'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isFullLicense } from '../../license'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap index 1b27d09965c4e..06ee16f264756 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap @@ -150,6 +150,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe placeholder="Select entities" selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> </EuiFormRow> <EuiSpacer @@ -356,6 +357,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with an enti ] } singleSelection={false} + sortMatchesBy="none" /> </EuiFormRow> <EuiSpacer @@ -594,6 +596,7 @@ exports[`CustomUrlEditor renders the editor for a discover type URL with valid t ] } singleSelection={false} + sortMatchesBy="none" /> </EuiFormRow> <EuiSpacer @@ -826,6 +829,7 @@ exports[`CustomUrlEditor renders the editor for a new dashboard type URL with no placeholder="Select entities" selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> </EuiFormRow> <EuiSpacer diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js index 92ad842686b05..bb4bed93f922e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { getIndexPatternNames } from '../../../../util/index_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index 216c416f30a6b..9406f1b3456cf 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -10,7 +10,7 @@ import React, { Component } from 'react'; import { EuiSpacer, EuiCallOut, EuiLoadingSpinner } from '@elastic/eui'; import { mlJobService } from '../../../../services/job_service'; -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { ML_DATA_PREVIEW_COUNT } from '../../../../../../common/util/job_utils'; import { MLJobEditor } from '../ml_job_editor'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index a5509c0f79a36..2e530a66cd83d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index 5f91ba9b6f107..e7b6e3a771a85 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -22,7 +22,7 @@ import { import { cloneDeep } from 'lodash'; import { ml } from '../../../../../services/ml_api_service'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; import { mlMessageBarService } from '../../../../../components/messagebar'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index 1297ca5b9afd1..fdffa8b38ae04 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts index 306fd82dc8758..9dda8eec206e4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/default_configs.ts @@ -7,6 +7,7 @@ import { IndexPatternTitle } from '../../../../../../../common/types/kibana'; import { Field, Aggregation, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from '../../../../../../../common/types/anomaly_detection_jobs'; +import { splitIndexPatternNames } from '../../../../../../../common/util/job_utils'; export function createEmptyJob(): Job { return { @@ -28,7 +29,7 @@ export function createEmptyDatafeed(indexPatternTitle: IndexPatternTitle): Dataf return { datafeed_id: '', job_id: '', - indices: [indexPatternTitle], + indices: splitIndexPatternNames(indexPatternTitle), query: {}, }; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 50a84eb3d11cb..69df2773f9f8d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -50,7 +50,7 @@ function getWizardUrlFromCloningJob(job: CombinedJob) { break; } - const indexPatternId = getIndexPatternIdFromName(job.datafeed_config.indices[0]); + const indexPatternId = getIndexPatternIdFromName(job.datafeed_config.indices.join()); return `jobs/new_job/${page}?index=${indexPatternId}&_g=()`; } diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index a6fe9e1d11953..6bc5c9b15074f 100644 --- a/x-pack/plugins/ml/public/application/management/index.ts +++ b/x-pack/plugins/ml/public/application/management/index.ts @@ -25,8 +25,11 @@ export function initManagementSection( ) { const licensing = pluginsSetup.licensing.license$.pipe(take(1)); licensing.subscribe(license => { - if (license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid') { - const management = pluginsSetup.management; + const management = pluginsSetup.management; + if ( + management !== undefined && + license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid' + ) { const mlSection = management.sections.register({ id: PLUGIN_ID, title: i18n.translate('xpack.ml.management.mlTitle', { diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index dca1235a96cb6..4a41f3e45001d 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -20,7 +20,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { checkGetManagementMlJobs } from '../../../../privilege/check_privilege'; +import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module @@ -75,7 +75,7 @@ export const JobsListPage: FC<{ I18nContext: CoreStart['i18n']['Context'] }> = ( const check = async () => { try { - const checkPrivilege = await checkGetManagementMlJobs(); + const checkPrivilege = await checkGetManagementMlJobsResolver(); setInitialized(true); setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); } catch (e) { diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index b2eda12abc578..c379cd702daee 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -82,14 +82,14 @@ export const AnalyticsPanel: FC<Props> = ({ jobCreationDisabled }) => { title={ <h2> {i18n.translate('xpack.ml.overview.analyticsList.createFirstJobMessage', { - defaultMessage: 'Create your first analytics job', + defaultMessage: 'Create your first data frame analytics job', })} </h2> } body={ <p> {i18n.translate('xpack.ml.overview.analyticsList.emptyPromptText', { - defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotate it with the results. The analytics job stores the annotated data, as well as a copy of the source data, in a new index.`, + defaultMessage: `Data frame analytics enable you to perform different analyses of your data and annotates it with the results. The job puts the annotated data and a copy of the source data in a new index.`, })} </p> } diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 5f5c3f7c28670..dac39b1a2071d 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -172,7 +172,7 @@ export const AnomalyDetectionPanel: FC<Props> = ({ jobCreationDisabled }) => { <Fragment> <p> {i18n.translate('xpack.ml.overview.anomalyDetection.emptyPromptText', { - defaultMessage: `Machine learning makes it easy to detect anomalies in time series data stored in Elasticsearch. Track one metric from a single machine or hundreds of metrics across thousands of machines. Start automatically spotting the anomalies hiding in your data and resolve issues faster.`, + defaultMessage: `Anomaly detection enables you to find unusual behavior in time series data. Start automatically spotting the anomalies hiding in your data and resolve issues faster.`, })} </p> </Fragment> diff --git a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx index 219c195bab111..3e4e9cfbd2b66 100644 --- a/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/sidebar.tsx @@ -11,7 +11,6 @@ import { useMlKibana } from '../../contexts/kibana'; const createJobLink = '#/jobs/new_job/step/index_or_search'; const feedbackLink = 'https://www.elastic.co/community/'; -const whatIsMachineLearningLink = 'https://www.elastic.co/what-is/elasticsearch-machine-learning'; interface Props { createAnomalyDetectionJobDisabled: boolean; @@ -60,7 +59,7 @@ export const OverviewSideBar: FC<Props> = ({ createAnomalyDetectionJobDisabled } <p> <FormattedMessage id="xpack.ml.overview.gettingStartedSectionText" - defaultMessage="Welcome to Machine Learning. Get started by reviewing our {docs} or {createJob}. For more information about machine learning in the Elastic stack please see {whatIsMachineLearning}. We recommend using {transforms} to create feature indices for analytics jobs." + defaultMessage="Welcome to Machine Learning. Get started by reviewing our {docs} or {createJob}. We recommend using {transforms} to create feature indices for analytics jobs." values={{ docs: ( <EuiLink href={docsLink} target="blank"> @@ -79,14 +78,6 @@ export const OverviewSideBar: FC<Props> = ({ createAnomalyDetectionJobDisabled } /> </EuiLink> ), - whatIsMachineLearning: ( - <EuiLink href={whatIsMachineLearningLink} target="blank"> - <FormattedMessage - id="xpack.ml.overview.gettingStartedSectionWhatIsMachineLearning" - defaultMessage="here" - /> - </EuiLink> - ), }} /> </p> @@ -96,7 +87,7 @@ export const OverviewSideBar: FC<Props> = ({ createAnomalyDetectionJobDisabled } <p> <FormattedMessage id="xpack.ml.overview.feedbackSectionText" - defaultMessage="If you have input or suggestions regarding your experience with Machine Learning please feel free to submit {feedbackLink}." + defaultMessage="If you have input or suggestions regarding your experience, please submit {feedbackLink}." values={{ feedbackLink: ( <EuiLink href={feedbackLink} target="blank"> diff --git a/x-pack/plugins/ml/public/application/overview/overview_page.tsx b/x-pack/plugins/ml/public/application/overview/overview_page.tsx index 7c995c8a568d7..9a852c491ee27 100644 --- a/x-pack/plugins/ml/public/application/overview/overview_page.tsx +++ b/x-pack/plugins/ml/public/application/overview/overview_page.tsx @@ -6,7 +6,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui'; -import { checkPermission } from '../privilege/check_privilege'; +import { checkPermission } from '../capabilities/check_capabilities'; import { mlNodesAvailable } from '../ml_nodes_check/check_ml_nodes'; import { NavigationMenu } from '../components/navigation_menu'; import { OverviewSideBar } from './components/sidebar'; diff --git a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts b/x-pack/plugins/ml/public/application/privilege/check_privilege.ts deleted file mode 100644 index 4de8c6eb703ff..0000000000000 --- a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts +++ /dev/null @@ -1,141 +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 { i18n } from '@kbn/i18n'; - -import { hasLicenseExpired } from '../license'; - -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { getPrivileges, getManageMlPrivileges } from './get_privileges'; -import { ACCESS_DENIED_PATH } from '../management/management_urls'; - -let privileges: Privileges = getDefaultPrivileges(); -// manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53 -export function checkGetManagementMlJobs() { - return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => { - getManageMlPrivileges().then( - ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { - privileges = capabilities; - // Loop through all privileges to ensure they are all set to true. - const isManageML = Object.values(privileges).every(p => p === true); - - if (isManageML === true && isPlatinumOrTrialLicense === true) { - return resolve({ mlFeatureEnabledInSpace }); - } else { - window.location.href = ACCESS_DENIED_PATH; - return reject(); - } - } - ); - }); -} - -export function checkGetJobsPrivilege(): Promise<Privileges> { - return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; - // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. - // all other functionality is controlled by the return privileges object. - // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, - // allow the promise to resolve as the separate license check will redirect then user to - // a basic feature - if (privileges.canGetJobs || isPlatinumOrTrialLicense === false) { - return resolve(privileges); - } else { - window.location.href = '#/access-denied'; - return reject(); - } - }); - }); -} - -export function checkCreateJobsPrivilege(): Promise<Privileges> { - return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; - // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, - // allow the promise to resolve as the separate license check will redirect then user to - // a basic feature - if (privileges.canCreateJob || isPlatinumOrTrialLicense === false) { - return resolve(privileges); - } else { - // if the user has no permission to create a job, - // redirect them back to the Transforms Management page - window.location.href = '#/jobs'; - return reject(); - } - }); - }); -} - -export function checkFindFileStructurePrivilege(): Promise<Privileges> { - return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities }) => { - privileges = capabilities; - // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. - // all other functionality is controlled by the return privileges object - if (privileges.canFindFileStructure) { - return resolve(privileges); - } else { - window.location.href = '#/access-denied'; - return reject(); - } - }); - }); -} - -// check the privilege type and the license to see whether a user has permission to access a feature. -// takes the name of the privilege variable as specified in get_privileges.js -export function checkPermission(privilegeType: keyof Privileges) { - const licenseHasExpired = hasLicenseExpired(); - return privileges[privilegeType] === true && licenseHasExpired !== true; -} - -// create the text for the button's tooltips if the user's license has -// expired or if they don't have the privilege to press that button -export function createPermissionFailureMessage(privilegeType: keyof Privileges) { - let message = ''; - const licenseHasExpired = hasLicenseExpired(); - if (licenseHasExpired) { - message = i18n.translate('xpack.ml.privilege.licenseHasExpiredTooltip', { - defaultMessage: 'Your license has expired.', - }); - } else if (privilegeType === 'canCreateJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.createMLJobsTooltip', { - defaultMessage: 'You do not have permission to create Machine Learning jobs.', - }); - } else if (privilegeType === 'canStartStopDatafeed') { - message = i18n.translate('xpack.ml.privilege.noPermission.startOrStopDatafeedsTooltip', { - defaultMessage: 'You do not have permission to start or stop datafeeds.', - }); - } else if (privilegeType === 'canUpdateJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.editJobsTooltip', { - defaultMessage: 'You do not have permission to edit jobs.', - }); - } else if (privilegeType === 'canDeleteJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.deleteJobsTooltip', { - defaultMessage: 'You do not have permission to delete jobs.', - }); - } else if (privilegeType === 'canCreateCalendar') { - message = i18n.translate('xpack.ml.privilege.noPermission.createCalendarsTooltip', { - defaultMessage: 'You do not have permission to create calendars.', - }); - } else if (privilegeType === 'canDeleteCalendar') { - message = i18n.translate('xpack.ml.privilege.noPermission.deleteCalendarsTooltip', { - defaultMessage: 'You do not have permission to delete calendars.', - }); - } else if (privilegeType === 'canForecastJob') { - message = i18n.translate('xpack.ml.privilege.noPermission.runForecastsTooltip', { - defaultMessage: 'You do not have permission to run forecasts.', - }); - } - return i18n.translate('xpack.ml.privilege.pleaseContactAdministratorTooltip', { - defaultMessage: '{message} Please contact your administrator.', - values: { - message, - }, - }); -} diff --git a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts b/x-pack/plugins/ml/public/application/privilege/get_privileges.ts deleted file mode 100644 index a3811779333d9..0000000000000 --- a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts +++ /dev/null @@ -1,40 +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 { ml } from '../services/ml_api_service'; - -import { setUpgradeInProgress } from '../services/upgrade_service'; -import { PrivilegesResponse } from '../../../common/types/privileges'; - -export function getPrivileges(): Promise<PrivilegesResponse> { - return new Promise((resolve, reject) => { - ml.checkMlPrivileges() - .then((resp: PrivilegesResponse) => { - if (resp.upgradeInProgress === true) { - setUpgradeInProgress(true); - } - resolve(resp); - }) - .catch(() => { - reject(); - }); - }); -} - -export function getManageMlPrivileges(): Promise<PrivilegesResponse> { - return new Promise((resolve, reject) => { - ml.checkManageMLPrivileges() - .then((resp: PrivilegesResponse) => { - if (resp.upgradeInProgress === true) { - setUpgradeInProgress(true); - } - resolve(resp); - }) - .catch(() => { - reject(); - }); - }); -} diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts index 776f0727389dd..958221df8a636 100644 --- a/x-pack/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts @@ -6,7 +6,7 @@ import { loadIndexPatterns, loadSavedSearches } from '../util/index_utils'; import { checkFullLicense } from '../license'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../capabilities/check_capabilities'; import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; @@ -28,6 +28,6 @@ export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Re getMlNodeCount, loadMlServerInfo, loadIndexPatterns: () => loadIndexPatterns(indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, loadSavedSearches, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index d257a9c080c35..fc2d517b2edb1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -16,7 +16,7 @@ import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; @@ -30,7 +30,7 @@ export const selectorRoute: MlRoute = { const PageWrapper: FC<PageProps> = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( <PageLoader context={context}> diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 174b3e3b4b338..1115a38870821 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -17,7 +17,7 @@ import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -43,7 +43,7 @@ const PageWrapper: FC<PageProps> = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( <PageLoader context={context}> diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index a3dbc9f97124c..1ec73fced82fe 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -12,7 +12,7 @@ import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; import { checkBasicLicense } from '../../../license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -39,7 +39,7 @@ const PageWrapper: FC<PageProps> = ({ location, deps }) => { const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index 9411b415e4e4d..b630b09b1a46d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -13,7 +13,7 @@ import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/inde import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; enum MODE { @@ -71,7 +71,7 @@ const PageWrapper: FC<IndexOrSearchPageProps> = ({ nextStepPath, deps, mode }) = const dataVizResolvers = { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index b1256e21888d9..1c91d7e94b241 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -15,7 +15,7 @@ import { Page } from '../../../jobs/new_job/pages/new_job'; import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; -import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { @@ -115,7 +115,7 @@ const PageWrapper: FC<WizardPageProps> = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record<string, any> = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), - privileges: checkCreateJobsPrivilege, + privileges: checkCreateJobsCapabilitiesResolver, jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index ccb99985cb70c..9b08bbf35c448 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -13,7 +13,7 @@ import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; import { checkFullLicense } from '../../license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; @@ -38,7 +38,7 @@ export const overviewRoute: MlRoute = { const PageWrapper: FC<PageProps> = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, loadMlServerInfo, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index 9d5c4e9c0b0a0..e015a3292acc4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -42,7 +45,7 @@ export const calendarListRoute: MlRoute = { const PageWrapper: FC<PageProps> = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index bf039e3bd2354..ebd58120853a9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC<NewCalendarPageProps> = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 6839ad833cb06..25bded1a52db1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; @@ -43,7 +46,7 @@ export const filterListRoute: MlRoute = { const PageWrapper: FC<PageProps> = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 7b8bd6c3c81ac..2f4ccecf2f1a2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC<NewFilterPageProps> = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index 10ccc0987fe5d..7cb943c091c4e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -16,7 +16,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { Settings } from '../../../settings'; import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; @@ -32,7 +35,7 @@ export const settingsRoute: MlRoute = { const PageWrapper: FC<PageProps> = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index e160126833801..6e3fd08e90e38 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -15,7 +15,7 @@ import { jobs } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info'; -import { PrivilegesResponse } from '../../../../common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { Job, @@ -300,18 +300,17 @@ export const ml = { }); }, - checkMlPrivileges() { - return http<PrivilegesResponse>({ + checkMlCapabilities() { + return http<MlCapabilitiesResponse>({ path: `${basePath()}/ml_capabilities`, method: 'GET', }); }, - checkManageMLPrivileges() { - return http<PrivilegesResponse>({ + checkManageMLCapabilities() { + return http<MlCapabilitiesResponse>({ path: `${basePath()}/ml_capabilities`, method: 'GET', - query: { ignoreSpaces: true }, }); }, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 7e2d6814c0b23..7f5ade64e7f14 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -7,15 +7,15 @@ jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () => <div id="mockNavigationMenu" />, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index 8750927ac1ee7..b2fce2c1474cb 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -13,15 +13,15 @@ import { CalendarsList } from './calendars_list'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () => <div id="mockNavigationMenu" />, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js index 8b1ab853676cf..0266bc2a55318 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js @@ -8,7 +8,7 @@ // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); jest.mock('../../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index dbc815b5fc099..c1bcee4acdd37 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -12,7 +12,7 @@ import { FilterLists } from './filter_lists'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () => <div id="mockNavigationMenu" />, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js index a5c1c872bfa5a..29b1185ddd4ab 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js @@ -6,7 +6,7 @@ // Create a mock for the privilege check used within the table to // enable/disable the 'New Filter' button. -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index c04ac40b962d5..7dd06268f7f8d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -32,7 +32,7 @@ import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { checkPermission, createPermissionFailureMessage, -} from '../../../privilege/check_privilege'; +} from '../../../capabilities/check_capabilities'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 5e505757dd2aa..42742f6efcb2a 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -150,7 +150,7 @@ function getTimeseriesexplorerDefaultState() { }; } -const containerPadding = 24; +const containerPadding = 34; export class TimeSeriesExplorer extends React.Component { static propTypes = { diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 3fd228377c57e..5a062320ca6c5 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -107,7 +107,7 @@ export function drawLineChartDots(data, lineChartGroup, lineChartValuesLine, rad } // this replicates Kibana's filterAxisLabels() behavior -// which can be found in src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js +// which can be found in src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_labels.js // axis labels which overflow the chart's boundaries will be removed export function filterAxisLabels(selection, chartWidth) { if (selection === undefined || selection.selectAll === undefined) { diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index 934a0a5e9ae3a..356da38d5ad08 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -23,7 +23,6 @@ import { } from 'kibana/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { SecurityPluginSetup } from '../../../../security/public'; -import { MlConfigType } from '../../../common/types/ml_config'; export interface DependencyCache { timefilter: DataPublicPluginSetup['query']['timefilter'] | null; @@ -40,10 +39,9 @@ export interface DependencyCache { savedObjectsClient: SavedObjectsClientContract | null; application: ApplicationStart | null; http: HttpStart | null; - security: SecurityPluginSetup | null; + security: SecurityPluginSetup | undefined | null; i18n: I18nStart | null; urlGenerators: SharePluginStart['urlGenerators'] | null; - mlConfig: MlConfigType | null; } const cache: DependencyCache = { @@ -64,7 +62,6 @@ const cache: DependencyCache = { security: null, i18n: null, urlGenerators: null, - mlConfig: null, }; export function setDependencyCache(deps: Partial<DependencyCache>) { @@ -85,7 +82,6 @@ export function setDependencyCache(deps: Partial<DependencyCache>) { cache.security = deps.security || null; cache.i18n = deps.i18n || null; cache.urlGenerators = deps.urlGenerators || null; - cache.mlConfig = deps.mlConfig || null; } export function getTimefilter() { @@ -206,13 +202,6 @@ export function getGetUrlGenerator() { return cache.urlGenerators.getUrlGenerator; } -export function getMlConfig() { - if (cache.mlConfig === null) { - throw new Error("mlConfig hasn't been initialized"); - } - return cache.mlConfig; -} - export function clearCache() { console.log('clearing dependency cache'); // eslint-disable-line no-console Object.keys(cache).forEach(k => { diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 4697496270edf..8070f94a1264d 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializer, PluginInitializerContext } from 'kibana/public'; +import { PluginInitializer } from 'kibana/public'; import './index.scss'; import { MlPlugin, @@ -19,6 +19,6 @@ export const plugin: PluginInitializer< MlPluginStart, MlSetupDependencies, MlStartDependencies -> = (context: PluginInitializerContext) => new MlPlugin(context); +> = () => new MlPlugin(); export { MlPluginSetup, MlPluginStart }; diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index b51be4d248683..d37d1fd815eb5 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -5,13 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { - Plugin, - CoreStart, - CoreSetup, - AppMountParameters, - PluginInitializerContext, -} from 'kibana/public'; +import { Plugin, CoreStart, CoreSetup, AppMountParameters } from 'kibana/public'; import { ManagementSetup } from 'src/plugins/management/public'; import { SharePluginStart } from 'src/plugins/share/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -25,26 +19,22 @@ import { LicenseManagementUIPluginSetup } from '../../license_management/public' import { setDependencyCache } from './application/util/dependency_cache'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; import { registerFeature } from './register_feature'; -import { MlConfigType } from '../common/types/ml_config'; export interface MlStartDependencies { data: DataPublicPluginStart; share: SharePluginStart; } export interface MlSetupDependencies { - security: SecurityPluginSetup; + security?: SecurityPluginSetup; licensing: LicensingPluginSetup; - management: ManagementSetup; + management?: ManagementSetup; usageCollection: UsageCollectionSetup; licenseManagement?: LicenseManagementUIPluginSetup; home: HomePublicPluginSetup; } export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> { - constructor(private readonly initializerContext: PluginInitializerContext) {} - setup(core: CoreSetup<MlStartDependencies, MlPluginStart>, pluginsSetup: MlSetupDependencies) { - const mlConfig = this.initializerContext.config.get<MlConfigType>(); core.application.register({ id: PLUGIN_ID, title: i18n.translate('xpack.ml.plugin.title', { @@ -67,7 +57,6 @@ export class MlPlugin implements Plugin<MlPluginSetup, MlPluginStart> { usageCollection: pluginsSetup.usageCollection, licenseManagement: pluginsSetup.licenseManagement, home: pluginsSetup.home, - mlConfig, }, { element: params.element, diff --git a/x-pack/plugins/ml/server/config.ts b/x-pack/plugins/ml/server/config.ts deleted file mode 100644 index 7cef6f17bbefb..0000000000000 --- a/x-pack/plugins/ml/server/config.ts +++ /dev/null @@ -1,15 +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 { PluginConfigDescriptor } from 'kibana/server'; -import { MlConfigType, configSchema } from '../common/types/ml_config'; - -export const config: PluginConfigDescriptor<MlConfigType> = { - exposeToBrowser: { - file_data_visualizer: true, - }, - schema: configSchema, -}; diff --git a/x-pack/plugins/ml/server/index.ts b/x-pack/plugins/ml/server/index.ts index 6e638d647a387..175c20bf49c94 100644 --- a/x-pack/plugins/ml/server/index.ts +++ b/x-pack/plugins/ml/server/index.ts @@ -9,5 +9,3 @@ import { MlServerPlugin } from './plugin'; export { MlPluginSetup, MlPluginStart } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => new MlServerPlugin(ctx); - -export { config } from './config'; diff --git a/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts new file mode 100644 index 0000000000000..8e350b8382276 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts @@ -0,0 +1,29 @@ +/* + * 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 { + adminMlCapabilities, + userMlCapabilities, + MlCapabilities, + getDefaultCapabilities, +} from '../../../../common/types/capabilities'; + +export function getAdminCapabilities() { + const caps: any = {}; + Object.keys(adminMlCapabilities).forEach(k => { + caps[k] = true; + }); + return { ...getUserCapabilities(), ...caps } as MlCapabilities; +} + +export function getUserCapabilities() { + const caps: any = {}; + Object.keys(userMlCapabilities).forEach(k => { + caps[k] = true; + }); + + return { ...getDefaultCapabilities(), ...caps } as MlCapabilities; +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts new file mode 100644 index 0000000000000..aef22debf3642 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts @@ -0,0 +1,58 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; +import { ILicense } from '../../../../licensing/common/types'; +import { isFullLicense, isMinimumLicense } from '../../../common/license'; +import { MlCapabilities, basicLicenseMlCapabilities } from '../../../common/types/capabilities'; + +export const setupCapabilitiesSwitcher = ( + coreSetup: CoreSetup, + license$: Observable<ILicense>, + logger: Logger +) => { + coreSetup.capabilities.registerSwitcher(getSwitcher(license$, logger)); +}; + +function getSwitcher(license$: Observable<ILicense>, logger: Logger): CapabilitiesSwitcher { + return async (request, capabilities) => { + const isAnonymousRequest = !request.route.options.authRequired; + + if (isAnonymousRequest) { + return capabilities; + } + + try { + const license = await license$.pipe(take(1)).toPromise(); + + // full license, leave capabilities as they were + if (isFullLicense(license)) { + return capabilities; + } + + const mlCaps = capabilities.ml as MlCapabilities; + const originalCapabilities = cloneDeep(mlCaps); + + // not full licence, switch off all capabilities + Object.keys(mlCaps).forEach(k => { + mlCaps[k as keyof MlCapabilities] = false; + }); + + // for a basic license, reapply the original capabilities for the basic license features + if (isMinimumLicense(license)) { + basicLicenseMlCapabilities.forEach(c => (mlCaps[c] = originalCapabilities[c])); + } + + return capabilities; + } catch (e) { + logger.debug(`Error updating capabilities for ML based on licensing: ${e}`); + return capabilities; + } + }; +} 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 new file mode 100644 index 0000000000000..746c9da47d0ad --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -0,0 +1,340 @@ +/* + * 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 { getAdminCapabilities, getUserCapabilities } from './__mocks__/ml_capabilities'; +import { capabilitiesProvider } from './check_capabilities'; +import { MlLicense } from '../../../common/license'; +import { getDefaultCapabilities } from '../../../common/types/capabilities'; + +const mlLicense = { + isSecurityEnabled: () => true, + isFullLicense: () => true, +} as MlLicense; + +const mlLicenseBasic = { + isSecurityEnabled: () => true, + isFullLicense: () => false, +} as MlLicense; + +const mlIsEnabled = async () => true; +const mlIsNotEnabled = async () => false; + +const callWithRequestNonUpgrade = async () => ({ upgrade_mode: false }); +const callWithRequestUpgrade = async () => ({ upgrade_mode: true }); + +describe('check_capabilities', () => { + describe('getCapabilities() - right number of capabilities', () => { + test('kibana capabilities count', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities } = await getCapabilities(); + const count = Object.keys(capabilities).length; + expect(count).toBe(28); + done(); + }); + }); + + describe('getCapabilities() with security', () => { + test('ml_user capabilities only', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + 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.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.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.canGetFilters).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + 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.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.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.canGetFilters).toBe(true); + expect(capabilities.canCreateCalendar).toBe(true); + expect(capabilities.canDeleteCalendar).toBe(true); + expect(capabilities.canCreateFilter).toBe(true); + expect(capabilities.canDeleteFilter).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); + expect(capabilities.canCreateDataFrameAnalytics).toBe(true); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); + done(); + }); + + test('upgrade in progress with full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + 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.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.canStartStopDatafeed).toBe(false); + 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); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('upgrade in progress with partial capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + 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.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.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.canGetFilters).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicense, + mlIsNotEnabled + ); + 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.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.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.canGetFilters).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + }); + + test('full capabilities, basic license, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicenseBasic, + mlIsNotEnabled + ); + 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.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.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.canGetFilters).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts new file mode 100644 index 0000000000000..d955cf981faca --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import { + MlCapabilities, + adminMlCapabilities, + MlCapabilitiesResponse, +} from '../../../common/types/capabilities'; +import { upgradeCheckProvider } from './upgrade'; +import { MlLicense } from '../../../common/license'; + +export function capabilitiesProvider( + callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], + capabilities: MlCapabilities, + mlLicense: MlLicense, + isMlEnabledInSpace: () => Promise<boolean> +) { + const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); + async function getCapabilities(): Promise<MlCapabilitiesResponse> { + const upgradeInProgress = await isUpgradeInProgress(); + const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); + const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); + + if (upgradeInProgress === true) { + // if an upgrade is in progress, set all admin capabilities to false + disableAdminPrivileges(capabilities); + } + + return { + capabilities, + upgradeInProgress, + isPlatinumOrTrialLicense, + mlFeatureEnabledInSpace, + }; + } + return { getCapabilities }; +} + +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/lib/capabilities/index.ts b/x-pack/plugins/ml/server/lib/capabilities/index.ts new file mode 100644 index 0000000000000..b73c6b87f6839 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { capabilitiesProvider } from './check_capabilities'; +export { setupCapabilitiesSwitcher } from './capabilities_switcher'; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts similarity index 100% rename from x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts rename to x-pack/plugins/ml/server/lib/capabilities/upgrade.ts diff --git a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts b/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts deleted file mode 100644 index ef82003ec80d6..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts +++ /dev/null @@ -1,154 +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. - */ - -export function callWithRequestProvider(testType: string) { - switch (testType) { - case 'partialPrivileges': - return partialPrivileges; - case 'fullPrivileges': - return fullPrivileges; - case 'upgradeWithFullPrivileges': - return upgradeWithFullPrivileges; - case 'upgradeWithPartialPrivileges': - return upgradeWithPartialPrivileges; - - default: - return fullPrivileges; - } -} - -const fullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const partialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const upgradeWithFullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'elastic', - has_all_requested: true, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const upgradeWithPartialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const fullClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': true, - 'cluster:admin/xpack/ml/datafeeds/update': true, - 'cluster:admin/xpack/ml/job/forecast': true, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': true, - 'cluster:admin/xpack/ml/datafeeds/preview': true, - 'cluster:admin/xpack/ml/datafeeds/start': true, - 'cluster:admin/xpack/ml/filters/put': true, - 'cluster:admin/xpack/ml/datafeeds/stop': true, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': true, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': true, - 'cluster:admin/xpack/ml/calendars/events/post': true, - 'cluster:admin/xpack/ml/job/close': true, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': true, - 'cluster:admin/xpack/ml/calendars/put': true, - 'cluster:admin/xpack/ml/calendars/events/delete': true, - 'cluster:admin/xpack/ml/datafeeds/put': true, - 'cluster:admin/xpack/ml/job/open': true, - 'cluster:admin/xpack/ml/job/delete': true, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': true, - 'cluster:admin/xpack/ml/job/update': true, - 'cluster:admin/xpack/ml/calendars/delete': true, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; - -// the same as ml_user role -const partialClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': false, - 'cluster:admin/xpack/ml/datafeeds/update': false, - 'cluster:admin/xpack/ml/job/forecast': false, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': false, - 'cluster:admin/xpack/ml/datafeeds/preview': false, - 'cluster:admin/xpack/ml/datafeeds/start': false, - 'cluster:admin/xpack/ml/filters/put': false, - 'cluster:admin/xpack/ml/datafeeds/stop': false, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': false, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': false, - 'cluster:admin/xpack/ml/calendars/events/post': false, - 'cluster:admin/xpack/ml/job/close': false, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': false, - 'cluster:admin/xpack/ml/calendars/put': false, - 'cluster:admin/xpack/ml/calendars/events/delete': false, - 'cluster:admin/xpack/ml/datafeeds/put': false, - 'cluster:admin/xpack/ml/job/open': false, - 'cluster:admin/xpack/ml/job/delete': false, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': false, - 'cluster:admin/xpack/ml/job/update': false, - 'cluster:admin/xpack/ml/calendars/delete': false, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts deleted file mode 100644 index d8435e9026250..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts +++ /dev/null @@ -1,515 +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 { callWithRequestProvider } from './__mocks__/call_with_request'; -import { privilegesProvider } from './check_privileges'; -import { mlPrivileges } from './privileges'; -import { MlLicense } from '../../../common/license'; - -const mlLicenseWithSecurity = { - isSecurityEnabled: () => true, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurity = { - isSecurityEnabled: () => false, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurityBasicLicense = { - isSecurityEnabled: () => false, - isFullLicense: () => false, -} as MlLicense; - -const mlLicenseWithSecurityBasicLicense = { - isSecurityEnabled: () => true, - isFullLicense: () => false, -} as MlLicense; - -const mlIsEnabled = async () => true; -const mlIsNotEnabled = async () => false; - -describe('check_privileges', () => { - describe('getPrivileges() - right number of capabilities', () => { - test('es capabilities count', async done => { - const count = mlPrivileges.cluster.length; - expect(count).toBe(27); - done(); - }); - - test('kibana capabilities count', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities } = await getPrivileges(); - const count = Object.keys(capabilities).length; - expect(count).toBe(22); - done(); - }); - }); - - describe('getPrivileges() with security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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(false); - 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); - done(); - }); - - test('full capabilities', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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(false); - 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); - done(); - }); - - test('ml_user capabilities with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full capabilities, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).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.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); - done(); - }); - }); - - describe('getPrivileges() without security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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); - done(); - }); - - test('ml_user capabilities without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).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.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(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities only, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).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.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); - done(); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts deleted file mode 100644 index df61ad0111a03..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ /dev/null @@ -1,269 +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 { IScopedClusterClient } from 'kibana/server'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { upgradeCheckProvider } from './upgrade'; -import { MlLicense } from '../../../common/license'; - -import { mlPrivileges } from './privileges'; - -type ClusterPrivilege = Record<string, boolean>; - -export interface MlCapabilities { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} - -export function privilegesProvider( - callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], - mlLicense: MlLicense, - isMlEnabledInSpace: () => Promise<boolean>, - ignoreSpaces: boolean = false -) { - const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); - async function getPrivileges(): Promise<MlCapabilities> { - // get the default privileges, forced to be false. - const privileges = getDefaultPrivileges(); - - const upgradeInProgress = await isUpgradeInProgress(); - const isSecurityEnabled = mlLicense.isSecurityEnabled(); - - const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); - const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); - - const setGettingPrivileges = isPlatinumOrTrialLicense - ? setFullGettingPrivileges - : setBasicGettingPrivileges; - - const setActionPrivileges = isPlatinumOrTrialLicense - ? setFullActionPrivileges - : setBasicActionPrivileges; - - if (mlFeatureEnabledInSpace === false && ignoreSpaces === false) { - // if ML isn't enabled in the current space, - // return with the default privileges (all false) - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - - if (isSecurityEnabled === false) { - if (upgradeInProgress === true) { - // if security is disabled and an upgrade in is progress, - // force all "getting" privileges to be true - // leaving all "setting" privileges to be the default false - setGettingPrivileges({}, privileges, true); - } else { - // if no upgrade is in progress, - // get all privileges forced to true - setGettingPrivileges({}, privileges, true); - setActionPrivileges({}, privileges, true); - } - } else { - // security enabled - // load all ml privileges for this user. - const { cluster } = await callAsCurrentUser('ml.privilegeCheck', { body: mlPrivileges }); - setGettingPrivileges(cluster, privileges); - if (upgradeInProgress === false) { - // if an upgrade is in progress, don't apply the "setting" - // privileges. leave them to be the default false. - setActionPrivileges(cluster, privileges); - } - } - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - return { getPrivileges }; -} - -function setFullGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetJobs = true; - } - - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/datafeeds/get'] && - cluster['cluster:monitor/xpack/ml/datafeeds/stats/get']) - ) { - privileges.canGetDatafeeds = true; - } - - // Calendars - if (forceTrue || cluster['cluster:monitor/xpack/ml/calendars/get']) { - privileges.canGetCalendars = true; - } - - // Filters - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/get']) { - privileges.canGetFilters = true; - } - - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetDataFrameAnalytics = true; - } -} - -function setFullActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/update']) { - privileges.canUpdateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/open']) { - privileges.canOpenJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/close']) { - privileges.canCloseJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/forecast']) { - privileges.canForecastJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/update']) { - privileges.canUpdateDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/preview']) { - privileges.canPreviewDatafeed = true; - } - - // Calendars - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/put'] && - cluster['cluster:admin/xpack/ml/calendars/jobs/update'] && - cluster['cluster:admin/xpack/ml/calendars/events/post']) - ) { - privileges.canCreateCalendar = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/delete'] && - cluster['cluster:admin/xpack/ml/calendars/events/delete']) - ) { - privileges.canDeleteCalendar = true; - } - - // Filters - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/filters/put'] && - cluster['cluster:admin/xpack/ml/filters/update']) - ) { - privileges.canCreateFilter = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/delete']) { - privileges.canDeleteFilter = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDataFrameAnalytics = true; - } -} - -function setBasicGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } -} - -function setBasicActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) {} diff --git a/x-pack/plugins/ml/server/lib/check_privileges/index.ts b/x-pack/plugins/ml/server/lib/check_privileges/index.ts deleted file mode 100644 index 67b435116aa00..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { privilegesProvider, MlCapabilities } from './check_privileges'; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts deleted file mode 100644 index 9fcd28dd68105..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts +++ /dev/null @@ -1,37 +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. - */ - -export const mlPrivileges = { - cluster: [ - 'cluster:monitor/xpack/ml/job/get', - 'cluster:monitor/xpack/ml/job/stats/get', - 'cluster:monitor/xpack/ml/datafeeds/get', - 'cluster:monitor/xpack/ml/datafeeds/stats/get', - 'cluster:monitor/xpack/ml/calendars/get', - 'cluster:admin/xpack/ml/job/put', - 'cluster:admin/xpack/ml/job/delete', - 'cluster:admin/xpack/ml/job/update', - 'cluster:admin/xpack/ml/job/open', - 'cluster:admin/xpack/ml/job/close', - 'cluster:admin/xpack/ml/job/forecast', - 'cluster:admin/xpack/ml/datafeeds/put', - 'cluster:admin/xpack/ml/datafeeds/delete', - 'cluster:admin/xpack/ml/datafeeds/start', - 'cluster:admin/xpack/ml/datafeeds/stop', - 'cluster:admin/xpack/ml/datafeeds/update', - 'cluster:admin/xpack/ml/datafeeds/preview', - 'cluster:admin/xpack/ml/calendars/put', - 'cluster:admin/xpack/ml/calendars/delete', - 'cluster:admin/xpack/ml/calendars/jobs/update', - 'cluster:admin/xpack/ml/calendars/events/post', - 'cluster:admin/xpack/ml/calendars/events/delete', - 'cluster:admin/xpack/ml/filters/put', - 'cluster:admin/xpack/ml/filters/get', - 'cluster:admin/xpack/ml/filters/update', - 'cluster:admin/xpack/ml/filters/delete', - 'cluster:monitor/xpack/ml/findfilestructure', - ], -}; diff --git a/x-pack/plugins/ml/server/lib/register_settings.ts b/x-pack/plugins/ml/server/lib/register_settings.ts new file mode 100644 index 0000000000000..38b1f5e3fc083 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/register_settings.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { FILE_DATA_VISUALIZER_MAX_FILE_SIZE } from '../../common/constants/settings'; +import { MAX_FILE_SIZE } from '../../common/constants/file_datavisualizer'; + +export function registerKibanaSettings(coreSetup: CoreSetup) { + coreSetup.uiSettings.register({ + [FILE_DATA_VISUALIZER_MAX_FILE_SIZE]: { + name: i18n.translate('xpack.ml.maxFileSizeSettingsName', { + defaultMessage: 'File Data Visualizer maximum file upload size', + }), + value: MAX_FILE_SIZE, + description: i18n.translate('xpack.ml.maxFileSizeSettingsDescription', { + defaultMessage: + 'Sets the file size limit when importing data in the File Data Visualizer. The highest supported value for this setting is 1GB.', + }), + category: ['Machine Learning'], + schema: schema.string(), + validation: { + regexString: '\\d+[mMgG][bB]', + message: i18n.translate('xpack.ml.maxFileSizeSettingsError', { + defaultMessage: 'Should be a valid data size. e.g. 200MB, 1GB', + }), + }, + }, + }); +} diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index 64d750f511f3a..581770e59043f 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,6 @@ */ import { difference } from 'lodash'; -import Boom from 'boom'; import { IScopedClusterClient } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; @@ -33,43 +32,31 @@ export class CalendarManager { } async getCalendar(calendarId: string) { - try { - const resp = await this._client('ml.calendars', { - calendarId, - }); - - const calendars = resp.calendars; - if (calendars.length) { - const calendar = calendars[0]; - calendar.events = await this._eventManager.getCalendarEvents(calendarId); - return calendar; - } else { - throw Boom.notFound(`Calendar with the id "${calendarId}" not found`); - } - } catch (error) { - throw Boom.badRequest(error); - } + const resp = await this._client('ml.calendars', { + calendarId, + }); + + const calendars = resp.calendars; + const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found. + calendar.events = await this._eventManager.getCalendarEvents(calendarId); + return calendar; } async getAllCalendars() { - try { - const calendarsResp = await this._client('ml.calendars'); - - const events: CalendarEvent[] = await this._eventManager.getAllEvents(); - const calendars: Calendar[] = calendarsResp.calendars; - calendars.forEach(cal => (cal.events = [])); - - // loop events and combine with related calendars - events.forEach(event => { - const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); - if (calendar) { - calendar.events.push(event); - } - }); - return calendars; - } catch (error) { - throw Boom.badRequest(error); - } + const calendarsResp = await this._client('ml.calendars'); + + const events: CalendarEvent[] = await this._eventManager.getAllEvents(); + const calendars: Calendar[] = calendarsResp.calendars; + calendars.forEach(cal => (cal.events = [])); + + // loop events and combine with related calendars + events.forEach(event => { + const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); + if (calendar) { + calendar.events.push(event); + } + }); + return calendars; } /** @@ -78,12 +65,8 @@ export class CalendarManager { * @returns {Promise<*>} */ async getCalendarsByIds(calendarIds: string) { - try { - const calendars: Calendar[] = await this.getAllCalendars(); - return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); - } catch (error) { - throw Boom.badRequest(error); - } + const calendars: Calendar[] = await this.getAllCalendars(); + return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); } async newCalendar(calendar: FormCalendar) { @@ -91,75 +74,67 @@ export class CalendarManager { const events = calendar.events; delete calendar.calendarId; delete calendar.events; - try { - await this._client('ml.addCalendar', { - calendarId, - body: calendar, - }); - - if (events.length) { - await this._eventManager.addEvents(calendarId, events); - } + await this._client('ml.addCalendar', { + calendarId, + body: calendar, + }); - // return the newly created calendar - return await this.getCalendar(calendarId); - } catch (error) { - throw Boom.badRequest(error); + if (events.length) { + await this._eventManager.addEvents(calendarId, events); } + + // return the newly created calendar + return await this.getCalendar(calendarId); } async updateCalendar(calendarId: string, calendar: Calendar) { const origCalendar: Calendar = await this.getCalendar(calendarId); - try { - // update job_ids - const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); - const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); - - // workout the differences between the original events list and the new one - // if an event has no event_id, it must be new - const eventsToAdd = calendar.events.filter( - event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // if an event in the original calendar cannot be found, it must have been deleted - const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( - event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // note, both of the loops below could be removed if the add and delete endpoints - // allowed multiple job_ids - - // add all new jobs - if (jobsToAdd.length) { - await this._client('ml.addJobToCalendar', { - calendarId, - jobId: jobsToAdd.join(','), - }); - } - - // remove all removed jobs - if (jobsToRemove.length) { - await this._client('ml.removeJobFromCalendar', { - calendarId, - jobId: jobsToRemove.join(','), - }); - } + // update job_ids + const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); + const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); + + // workout the differences between the original events list and the new one + // if an event has no event_id, it must be new + const eventsToAdd = calendar.events.filter( + event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // if an event in the original calendar cannot be found, it must have been deleted + const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( + event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // note, both of the loops below could be removed if the add and delete endpoints + // allowed multiple job_ids + + // add all new jobs + if (jobsToAdd.length) { + await this._client('ml.addJobToCalendar', { + calendarId, + jobId: jobsToAdd.join(','), + }); + } - // add all new events - if (eventsToAdd.length !== 0) { - await this._eventManager.addEvents(calendarId, eventsToAdd); - } + // remove all removed jobs + if (jobsToRemove.length) { + await this._client('ml.removeJobFromCalendar', { + calendarId, + jobId: jobsToRemove.join(','), + }); + } - // remove all removed events - await Promise.all( - eventsToRemove.map(async event => { - await this._eventManager.deleteEvent(calendarId, event.event_id); - }) - ); - } catch (error) { - throw Boom.badRequest(error); + // add all new events + if (eventsToAdd.length !== 0) { + await this._eventManager.addEvents(calendarId, eventsToAdd); } + // remove all removed events + await Promise.all( + eventsToRemove.map(async event => { + await this._eventManager.deleteEvent(calendarId, event.event_id); + }) + ); + // return the updated calendar return await this.getCalendar(calendarId); } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index 488839f68b3fe..41240e2695f6f 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -23,41 +21,29 @@ export class EventManager { } async getCalendarEvents(calendarId: string) { - try { - const resp = await this._client('ml.events', { calendarId }); + const resp = await this._client('ml.events', { calendarId }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - try { - const resp = await this._client('ml.events', { - calendarId, - jobId, - }); + const resp = await this._client('ml.events', { + calendarId, + jobId, + }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - try { - return await this._client('ml.addEvent', { - calendarId, - body, - }); - } catch (error) { - throw Boom.badRequest(error); - } + return await this._client('ml.addEvent', { + calendarId, + body, + }); } async deleteEvent(calendarId: string, eventId: string) { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 84f81a30f36b8..40b2a524151b3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -29,7 +29,11 @@ import { JobSpecificOverride, isGeneralJobOverride, } from '../../../common/types/modules'; -import { getLatestDataOrBucketTimestamp, prefixDatafeedId } from '../../../common/util/job_utils'; +import { + getLatestDataOrBucketTimestamp, + prefixDatafeedId, + splitIndexPatternNames, +} from '../../../common/util/job_utils'; import { mlLog } from '../../client/log'; import { calculateModelMemoryLimitProvider } from '../calculate_model_memory_limit'; import { fieldsServiceProvider } from '../fields_service'; @@ -828,9 +832,7 @@ export class DataRecognizer { updateDatafeedIndices(moduleConfig: Module) { // if the supplied index pattern contains a comma, split into multiple indices and // add each one to the datafeed - const indexPatternNames = this.indexPatternName.includes(',') - ? this.indexPatternName.split(',').map(i => i.trim()) - : [this.indexPatternName]; + const indexPatternNames = splitIndexPatternNames(this.indexPatternName); moduleConfig.datafeeds.forEach(df => { const newIndices: string[] = []; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index c7add12be142c..969b74194148b 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -7,14 +7,18 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, + CoreStart, Plugin, IScopedClusterClient, + KibanaRequest, Logger, PluginInitializerContext, ICustomClusterClient, + CapabilitiesStart, } from 'kibana/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; +import { MlCapabilities } from '../common/types/capabilities'; import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; import { initMlTelemetry } from './lib/telemetry'; @@ -41,6 +45,9 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; +import { getPluginPrivileges } from '../common/types/capabilities'; +import { setupCapabilitiesSwitcher } from './lib/capabilities'; +import { registerKibanaSettings } from './lib/register_settings'; declare module 'kibana/server' { interface RequestHandlerContext { @@ -59,6 +66,7 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug private log: Logger; private version: string; private mlLicense: MlServerLicense; + private capabilities: CapabilitiesStart | null = null; constructor(ctx: PluginInitializerContext) { this.log = ctx.logger.get(); @@ -67,6 +75,8 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug } public setup(coreSetup: CoreSetup, plugins: PluginsSetup): MlPluginSetup { + const { user, admin } = getPluginPrivileges(); + plugins.features.registerFeature({ id: PLUGIN_ID, name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { @@ -87,35 +97,41 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug { id: 'ml_user', privilege: { + api: user.api, app: [PLUGIN_ID, 'kibana'], catalogue: [PLUGIN_ID], savedObject: { all: [], read: [], }, - ui: [], + ui: user.ui, }, }, { id: 'ml_admin', privilege: { + api: admin.api, app: [PLUGIN_ID, 'kibana'], catalogue: [PLUGIN_ID], savedObject: { all: [], read: [], }, - ui: [], + ui: admin.ui, }, }, ], }, }); + registerKibanaSettings(coreSetup); this.mlLicense.setup(plugins.licensing.license$, [ (mlLicense: MlLicense) => initSampleDataSets(mlLicense, plugins), ]); + // initialize capabilities switcher to add license filter to ml capabilities + setupCapabilitiesSwitcher(coreSetup, plugins.licensing.license$, this.log); + // Can access via router's handler function 'context' parameter - context.ml.mlClient const mlClient = coreSetup.elasticsearch.createClient(PLUGIN_ID, { plugins: [elasticsearchJsPlugin], @@ -132,6 +148,14 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug mlLicense: this.mlLicense, }; + const resolveMlCapabilities = async (request: KibanaRequest) => { + if (this.capabilities === null) { + return null; + } + const capabilities = await this.capabilities.resolveCapabilities(request); + return capabilities.ml as MlCapabilities; + }; + annotationRoutes(routeInit, plugins.security); calendars(routeInit); dataFeedRoutes(routeInit); @@ -151,17 +175,20 @@ export class MlServerPlugin implements Plugin<MlPluginSetup, MlPluginStart, Plug systemRoutes(routeInit, { spaces: plugins.spaces, cloud: plugins.cloud, + resolveMlCapabilities, }); initMlServerLog({ log: this.log }); initMlTelemetry(coreSetup, plugins.usageCollection); return { - ...createSharedServices(this.mlLicense, plugins.spaces, plugins.cloud), + ...createSharedServices(this.mlLicense, plugins.spaces, plugins.cloud, resolveMlCapabilities), mlClient, }; } - public start(): MlPluginStart {} + public start(coreStart: CoreStart): MlPluginStart { + this.capabilities = coreStart.capabilities; + } public stop() { this.mlLicense.unsubscribe(); diff --git a/x-pack/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts index d5abebda00caa..e6d082cd74aab 100644 --- a/x-pack/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -54,6 +54,9 @@ export function annotationRoutes( validate: { body: getAnnotationsSchema, }, + options: { + tags: ['access:ml:canGetAnnotations'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { 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 a675eb58dc792..63cd5498231af 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 { @@ -138,12 +150,14 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * - * @api {put} /api/ml/anomaly_detectors/:jobId Instantiate an anomaly detection job + * @api {put} /api/ml/anomaly_detectors/:jobId Create an anomaly detection job * @apiName CreateAnomalyDetectors * @apiDescription Creates an anomaly detection job. * * @apiSchema (params) jobIdSchema * @apiSchema (body) anomalyDetectionJobSchema + * + * @apiSuccess {Object} job the configuration of the job that has been created. */ router.put( { @@ -152,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 { @@ -186,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 { @@ -218,6 +238,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canOpenJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -249,6 +272,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -284,6 +310,9 @@ export function jobRoutes({ router, mlLicense }: RouteInitialization) { validate: { params: jobIdSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -317,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 { @@ -349,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 { @@ -387,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 { @@ -423,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 { @@ -460,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 { @@ -494,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/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 4848de6db7049..cd311c285d0df 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -49,6 +49,7 @@ "GetCategoryExamples", "GetPartitionFieldsValues", + "Modules", "DataRecognizer", "RecognizeIndex", "GetModule", @@ -100,7 +101,7 @@ "SystemRoutes", "HasPrivileges", - "MlCapabilities", + "MlCapabilitiesResponse", "MlNodeCount", "MlInfo", "MlEsSearch", diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index a17601f74ae93..9c80651a13999 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 dd9e0ea66aa9d..32cb2b343f876 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 a4c0d5553a4b2..04008a896a1a2 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -74,10 +74,12 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * * @api {post} /api/ml/data_visualizer/get_field_stats/:indexPatternTitle Get stats for fields * @apiName GetStatsForFields - * @apiDescription Returns fields stats of the index pattern. + * @apiDescription Returns the stats on individual fields in the specified index pattern. * * @apiSchema (params) indexPatternTitleSchema * @apiSchema (body) dataVisualizerFieldStatsSchema + * + * @apiSuccess {Object} fieldName stats by field, keyed on the name of the field. */ router.post( { @@ -86,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 { @@ -130,10 +135,16 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) * * @api {post} /api/ml/data_visualizer/get_overall_stats/:indexPatternTitle Get overall stats * @apiName GetOverallStats - * @apiDescription Returns overall stats of the index pattern. + * @apiDescription Returns the top level overall stats for the specified index pattern. * * @apiSchema (params) indexPatternTitleSchema * @apiSchema (body) dataVisualizerOverallStatsSchema + * + * @apiSuccess {number} totalCount total count of documents. + * @apiSuccess {Object} aggregatableExistsFields stats on aggregatable fields that exist in documents. + * @apiSuccess {Object} aggregatableNotExistsFields stats on aggregatable fields that do not exist in documents. + * @apiSuccess {Object} nonAggregatableExistsFields stats on non-aggregatable fields that exist in documents. + * @apiSuccess {Object} nonAggregatableNotExistsFields stats on non-aggregatable fields that do not exist in documents. */ router.post( { @@ -142,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 ec667e1d305f5..1fa1d408372da 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 9a5f47409c8a0..b0f13df294145 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -37,6 +37,8 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * @apiDescription Returns the cardinality of one or more fields. Returns an Object whose keys are the names of the fields, with values equal to the cardinality of the field * * @apiSchema (body) getCardinalityOfFieldsSchema + * + * @apiSuccess {number} fieldName cardinality of the field. */ router.post( { @@ -44,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); @@ -64,9 +68,12 @@ export function fieldsService({ router, mlLicense }: RouteInitialization) { * * @api {post} /api/ml/fields_service/time_field_range Get time field range * @apiName GetTimeFieldRange - * @apiDescription Returns the timefield range for the given index + * @apiDescription Returns the time range for the given index and query using the specified time range. * * @apiSchema (body) getTimeFieldRangeSchema + * + * @apiSuccess {Object} start start of time range with epoch and string properties. + * @apiSuccess {Object} end end of time range with epoch and string properties. */ router.post( { @@ -74,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 3f3fc3f547b6a..0f389f9505943 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 738c25070358d..d5287c349a8fc 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 e434936beba63..fb3ef7fc41c76 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 71499748691f6..5acc89e7d13be 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -7,7 +7,10 @@ import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; -import { jobAuditMessagesQuerySchema, jobIdSchema } from './schemas/job_audit_messages_schema'; +import { + jobAuditMessagesQuerySchema, + jobAuditMessagesJobIdSchema, +} from './schemas/job_audit_messages_schema'; /** * Routes for job audit message routes @@ -20,16 +23,19 @@ export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitializatio * @apiName GetJobAuditMessages * @apiDescription Returns audit messages for specified job ID * - * @apiSchema (params) jobIdSchema + * @apiSchema (params) jobAuditMessagesJobIdSchema * @apiSchema (query) jobAuditMessagesQuerySchema */ router.get( { path: '/api/ml/job_audit_messages/messages/{jobId}', validate: { - params: jobIdSchema, + params: jobAuditMessagesJobIdSchema, query: jobAuditMessagesQuerySchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -64,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 493974cbafe36..05c44e1da9757 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; import { schema } from '@kbn/config-schema'; -import { IScopedClusterClient } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -28,25 +26,6 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; * Routes for job service */ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { - async function hasPermissionToCreateJobs( - callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] - ) { - if (mlLicense.isSecurityEnabled() === false) { - return true; - } - - const resp = await callAsCurrentUser('ml.privilegeCheck', { - body: { - cluster: [ - 'cluster:admin/xpack/ml/job/put', - 'cluster:admin/xpack/ml/job/open', - 'cluster:admin/xpack/ml/datafeeds/put', - ], - }, - }); - return resp.has_all_requested; - } - /** * @apiGroup JobService * @@ -62,6 +41,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: forceStartDatafeedSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -93,6 +75,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: datafeedIdsSchema, }, + options: { + tags: ['access:ml:canStartStopDatafeed'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -124,6 +109,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canDeleteJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -155,6 +143,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canCloseJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -176,9 +167,14 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { * * @api {post} /api/ml/jobs/jobs_summary Jobs summary * @apiName JobsSummary - * @apiDescription Creates a summary jobs list. Jobs include job stats, datafeed stats, and calendars. + * @apiDescription Returns a list of anomaly detection jobs, with summary level information for every job. + * For any supplied job IDs, full job information will be returned, which include the analysis configuration, + * job stats, datafeed stats, and calendars. * * @apiSchema (body) jobIdsSchema + * + * @apiSuccess {Array} jobsList list of jobs. For any supplied job IDs, the job object will contain a fullJob property + * which includes the full configuration and stats for the job. */ router.post( { @@ -186,6 +182,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -217,6 +216,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.object(jobsWithTimerangeSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -247,6 +249,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -274,6 +279,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/jobs/groups', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -304,6 +312,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.object(updateGroupsSchema), }, + options: { + tags: ['access:ml:canUpdateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -331,6 +342,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -361,6 +375,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: jobIdsSchema, }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -391,6 +408,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { 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 { @@ -424,6 +444,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -476,6 +499,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.object(chartSchema), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -524,6 +550,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { { path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -554,6 +583,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { validate: { body: schema.object(lookBackProgressSchema), }, + options: { + tags: ['access:ml:canCreateJob'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -585,17 +617,12 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { 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(context.ml!.mlClient.callAsCurrentUser)) === false) { - throw Boom.forbidden( - 'Insufficient privileges, the machine_learning_admin role is required.' - ); - } - const { validateCategoryExamples } = categorizationExamplesProvider( context.ml!.mlClient.callAsCurrentUser, context.ml!.mlClient.callAsInternalUser @@ -646,6 +673,9 @@ export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { 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 dd2bd9deadf43..632166d6d5fb8 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 2d462b6dc207a..622ae66ede426 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -81,7 +81,7 @@ function dataRecognizerJobsExist(context: RequestHandlerContext, moduleId: strin */ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {get} /api/ml/modules/recognize/:indexPatternTitle Recognize index pattern * @apiName RecognizeIndex @@ -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 { @@ -111,7 +114,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ); /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {get} /api/ml/modules/get_module/:moduleId Get module * @apiName GetModule @@ -127,6 +130,9 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ...getModuleIdParamSchema(true), }), }, + options: { + tags: ['access:ml:canGetJobs'], + }, }, mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { @@ -146,7 +152,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ); /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {post} /api/ml/modules/setup/:moduleId Setup module * @apiName SetupModule @@ -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 { @@ -204,7 +213,7 @@ export function dataRecognizer({ router, mlLicense }: RouteInitialization) { ); /** - * @apiGroup DataRecognizer + * @apiGroup Modules * * @api {post} /api/ml/modules/jobs_exist/:moduleId Check if module jobs exist * @apiName CheckExistingModuleJobs @@ -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 59458b1e486db..e4a9abb0784be 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 89c267340fe52..94ca0827ccfa5 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/schemas/anomaly_detectors_schema.ts b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts index ab1305d9bc354..9b86e3e06096e 100644 --- a/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/anomaly_detectors_schema.ts @@ -119,7 +119,7 @@ export const anomalyDetectionJobSchema = { }; export const jobIdSchema = schema.object({ - /** Job id */ + /** Job ID. */ jobId: schema.string(), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts index 1a1d02f991b55..b2d665954bd4d 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts @@ -7,26 +7,41 @@ import { schema } from '@kbn/config-schema'; export const indexPatternTitleSchema = schema.object({ + /** Title of the index pattern for which to return stats. */ indexPatternTitle: schema.string(), }); export const dataVisualizerFieldStatsSchema = schema.object({ + /** Query to match documents in the index. */ query: schema.any(), fields: schema.arrayOf(schema.any()), + /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ samplerShardSize: schema.number(), + /** Name of the time field in the index (optional). */ timeFieldName: schema.maybe(schema.string()), + /** Earliest timestamp for search, as epoch ms (optional). */ earliest: schema.maybe(schema.number()), + /** Latest timestamp for search, as epoch ms (optional). */ latest: schema.maybe(schema.number()), + /** Aggregation interval to use for obtaining document counts over time (optional). */ interval: schema.maybe(schema.string()), + /** Maximum number of examples to return for text type fields. */ maxExamples: schema.number(), }); export const dataVisualizerOverallStatsSchema = schema.object({ + /** Query to match documents in the index. */ query: schema.any(), + /** Names of aggregatable fields for which to return stats. */ aggregatableFields: schema.arrayOf(schema.string()), + /** Names of non-aggregatable fields for which to return stats. */ nonAggregatableFields: schema.arrayOf(schema.string()), + /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ samplerShardSize: schema.number(), + /** Name of the time field in the index (optional). */ timeFieldName: schema.maybe(schema.string()), + /** Earliest timestamp for search, as epoch ms (optional). */ earliest: schema.maybe(schema.number()), + /** Latest timestamp for search, as epoch ms (optional). */ latest: schema.maybe(schema.number()), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts index e0fba498e0d58..ba397e0084e27 100644 --- a/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/fields_service_schema.ts @@ -7,16 +7,25 @@ import { schema } from '@kbn/config-schema'; export const getCardinalityOfFieldsSchema = schema.object({ + /** Index or indexes for which to return the time range. */ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + /** Name(s) of the field(s) to return cardinality information. */ fieldNames: schema.maybe(schema.arrayOf(schema.string())), + /** Query to match documents in the index(es) (optional). */ query: schema.maybe(schema.any()), + /** Name of the time field in the index. */ timeFieldName: schema.maybe(schema.string()), + /** Earliest timestamp for search, as epoch ms (optional). */ earliestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])), + /** Latest timestamp for search, as epoch ms (optional). */ latestMs: schema.maybe(schema.oneOf([schema.number(), schema.string()])), }); export const getTimeFieldRangeSchema = schema.object({ + /** Index or indexes for which to return the time range. */ index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + /** Name of the time field in the index. */ timeFieldName: schema.maybe(schema.string()), + /** Query to match documents in the index(es). */ query: schema.maybe(schema.any()), }); diff --git a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts index b94a004384eb1..ac489b3a6ce6f 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_audit_messages_schema.ts @@ -6,7 +6,10 @@ import { schema } from '@kbn/config-schema'; -export const jobIdSchema = schema.object({ jobId: schema.maybe(schema.string()) }); +export const jobAuditMessagesJobIdSchema = schema.object({ + /** Job ID. */ + jobId: schema.maybe(schema.string()), +}); export const jobAuditMessagesQuerySchema = schema.maybe( schema.object({ from: schema.maybe(schema.any()) }) diff --git a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts index d2036b8a7c0fa..1ca1e5287e9d0 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_service_schema.ts @@ -40,6 +40,7 @@ export const forceStartDatafeedSchema = schema.object({ }); export const jobIdsSchema = schema.object({ + /** Optional list of job ID(s). */ jobIds: schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.maybe(schema.string()))]) ), diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 2a0a760e94f79..7ae7dd8eef065 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -10,7 +10,7 @@ import { Request } from 'hapi'; import { RequestHandlerContext } from 'kibana/server'; import { wrapError } from '../client/error_wrapper'; import { mlLog } from '../client/log'; -import { privilegesProvider } from '../lib/check_privileges'; +import { capabilitiesProvider } from '../lib/capabilities'; import { spacesUtilsProvider } from '../lib/spaces_utils'; import { RouteInitialization, SystemRouteDeps } from '../types'; @@ -19,7 +19,7 @@ import { RouteInitialization, SystemRouteDeps } from '../types'; */ export function systemRoutes( { router, mlLicense }: RouteInitialization, - { spaces, cloud }: SystemRouteDeps + { spaces, cloud, resolveMlCapabilities }: SystemRouteDeps ) { async function getNodeCount(context: RequestHandlerContext) { const filterPath = 'nodes.*.attributes'; @@ -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 { @@ -103,35 +106,38 @@ export function systemRoutes( * @apiGroup SystemRoutes * * @api {get} /api/ml/ml_capabilities Check ML capabilities - * @apiName MlCapabilities + * @apiName MlCapabilitiesResponse * @apiDescription Checks ML capabilities */ router.get( { path: '/api/ml/ml_capabilities', - validate: { - query: schema.object({ - ignoreSpaces: schema.maybe(schema.string()), - }), + validate: false, + options: { + tags: ['access:ml:canAccessML'], }, }, mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { - const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; // if spaces is disabled force isMlEnabledInSpace to be true const { isMlEnabledInSpace } = spaces !== undefined ? spacesUtilsProvider(spaces, (request as unknown) as Request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + return response.customError(wrapError(new Error('resolveMlCapabilities is not defined'))); + } + + const { getCapabilities } = capabilitiesProvider( context.ml!.mlClient.callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); return response.ok({ - body: await getPrivileges(), + body: await getCapabilities(), }); } catch (error) { return response.customError(wrapError(error)); @@ -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/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index cedf18e80906f..698ac8e6261e5 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; +import { APICaller, KibanaRequest } from 'kibana/server'; import { SearchResponse, SearchParams } from 'elasticsearch'; import { MlServerLicense } from '../../lib/license'; import { CloudSetup } from '../../../../cloud/server'; import { LicenseCheck } from '../license_checks'; -import { spacesUtilsProvider, RequestFacade } from '../../lib/spaces_utils'; +import { spacesUtilsProvider } from '../../lib/spaces_utils'; import { SpacesPluginSetup } from '../../../../spaces/server'; -import { privilegesProvider, MlCapabilities } from '../../lib/check_privileges'; +import { capabilitiesProvider } from '../../lib/capabilities'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities'; export interface MlSystemProvider { mlSystemProvider( callAsCurrentUser: APICaller, - request: RequestFacade + request: KibanaRequest ): { - mlCapabilities(ignoreSpaces?: boolean): Promise<MlCapabilities>; + mlCapabilities(): Promise<MlCapabilitiesResponse>; mlInfo(): Promise<MlInfoResponse>; mlSearch<T>(searchParams: SearchParams): Promise<SearchResponse<T>>; }; @@ -31,12 +32,13 @@ export function getMlSystemProvider( isFullLicense: LicenseCheck, mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup | undefined + cloud: CloudSetup | undefined, + resolveMlCapabilities: ResolveMlCapabilities ): MlSystemProvider { return { - mlSystemProvider(callAsCurrentUser: APICaller, request: RequestFacade) { + mlSystemProvider(callAsCurrentUser: APICaller, request: KibanaRequest) { return { - mlCapabilities(ignoreSpaces?: boolean) { + async mlCapabilities() { isMinimumLicense(); const { isMlEnabledInSpace } = @@ -44,13 +46,18 @@ export function getMlSystemProvider( ? spacesUtilsProvider(spaces, request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + throw new Error('resolveMlCapabilities is not defined'); + } + + const { getCapabilities } = capabilitiesProvider( callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); - return getPrivileges(); + return getCapabilities(); }, async mlInfo(): Promise<MlInfoResponse> { isMinimumLicense(); diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index f08eb5c23b272..f2d20a72444be 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -17,6 +17,7 @@ import { AnomalyDetectorsProvider, getAnomalyDetectorsProvider, } from './providers/anomaly_detectors'; +import { ResolveMlCapabilities } from '../../common/types/capabilities'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -27,14 +28,22 @@ export type SharedServices = JobServiceProvider & export function createSharedServices( mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup + cloud: CloudSetup, + resolveMlCapabilities: ResolveMlCapabilities ): SharedServices { const { isFullLicense, isMinimumLicense } = licenseChecks(mlLicense); return { ...getJobServiceProvider(isFullLicense), ...getAnomalyDetectorsProvider(isFullLicense), - ...getMlSystemProvider(isMinimumLicense, isFullLicense, mlLicense, spaces, cloud), + ...getMlSystemProvider( + isMinimumLicense, + isFullLicense, + mlLicense, + spaces, + cloud, + resolveMlCapabilities + ), ...getModulesProvider(isFullLicense), ...getResultsServiceProvider(isFullLicense), }; diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index ff4d07bd79e42..678e81d3526ac 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -13,6 +13,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { MlServerLicense } from './lib/license'; +import { ResolveMlCapabilities } from '../common/types/capabilities'; export interface LicenseCheckResult { isAvailable: boolean; @@ -26,6 +27,7 @@ export interface LicenseCheckResult { export interface SystemRouteDeps { cloud: CloudSetup; spaces?: SpacesPluginSetup; + resolveMlCapabilities: ResolveMlCapabilities; } export interface PluginsSetup { diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index edd6142455dfb..eeed7b4d5acf6 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -245,7 +245,7 @@ export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION, ALERT_TYPE_CLUSTER_ST /** * Matches the id for the built-in in email action type - * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + * See x-pack/plugins/actions/server/builtin_action_types/email.ts */ export const ALERT_ACTION_TYPE_EMAIL = '.email'; diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json index bbdf1a2e7cb76..115cc08871ea4 100644 --- a/x-pack/plugins/monitoring/kibana.json +++ b/x-pack/plugins/monitoring/kibana.json @@ -3,8 +3,8 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["monitoring"], - "requiredPlugins": ["licensing", "features"], - "optionalPlugins": ["alerting", "actions", "infra", "telemetryCollectionManager", "usageCollection"], + "requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"], + "optionalPlugins": ["alerting", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home"], "server": true, - "ui": false + "ui": true } diff --git a/x-pack/legacy/plugins/monitoring/public/_hacks.scss b/x-pack/plugins/monitoring/public/_hacks.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/_hacks.scss rename to x-pack/plugins/monitoring/public/_hacks.scss diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts new file mode 100644 index 0000000000000..3fa79cedf4ce7 --- /dev/null +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import angular, { IWindowService } from 'angular'; +import '../views/all'; +// required for `ngSanitize` angular module +import 'angular-sanitize'; +import 'angular-route'; +import '../index.scss'; +import { capitalize } from 'lodash'; +import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import { AppMountContext } from 'kibana/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { + createTopNavDirective, + createTopNavHelper, +} from '../../../../../src/plugins/kibana_legacy/public'; +import { MonitoringPluginDependencies } from '../types'; +import { GlobalState } from '../url_state'; +import { getSafeForExternalLink } from '../lib/get_safe_for_external_link'; + +// @ts-ignore +import { formatNumber, formatMetric } from '../lib/format_number'; +// @ts-ignore +import { extractIp } from '../lib/extract_ip'; +// @ts-ignore +import { PrivateProvider } from './providers/private'; +// @ts-ignore +import { KbnUrlProvider } from './providers/url'; +// @ts-ignore +import { breadcrumbsProvider } from '../services/breadcrumbs'; +// @ts-ignore +import { monitoringClustersProvider } from '../services/clusters'; +// @ts-ignore +import { executorProvider } from '../services/executor'; +// @ts-ignore +import { featuresProvider } from '../services/features'; +// @ts-ignore +import { licenseProvider } from '../services/license'; +// @ts-ignore +import { titleProvider } from '../services/title'; +// @ts-ignore +import { monitoringBeatsBeatProvider } from '../directives/beats/beat'; +// @ts-ignore +import { monitoringBeatsOverviewProvider } from '../directives/beats/overview'; +// @ts-ignore +import { monitoringMlListingProvider } from '../directives/elasticsearch/ml_job_listing'; +// @ts-ignore +import { monitoringMainProvider } from '../directives/main'; + +export const appModuleName = 'monitoring'; + +type IPrivate = <T>(provider: (...injectable: unknown[]) => T) => T; + +const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.bootstrap']; + +export const localAppModule = ({ + core, + data: { query }, + navigation, + externalConfig, +}: MonitoringPluginDependencies) => { + createLocalI18nModule(); + createLocalPrivateModule(); + createLocalStorage(); + createLocalConfigModule(core); + createLocalKbnUrlModule(); + createLocalStateModule(query); + createLocalTopNavModule(navigation); + createHrefModule(core); + createMonitoringAppServices(); + createMonitoringAppDirectives(); + createMonitoringAppConfigConstants(externalConfig); + createMonitoringAppFilters(); + + const appModule = angular.module(appModuleName, [ + ...thirdPartyAngularDependencies, + 'monitoring/I18n', + 'monitoring/Private', + 'monitoring/KbnUrl', + 'monitoring/Storage', + 'monitoring/Config', + 'monitoring/State', + 'monitoring/TopNav', + 'monitoring/href', + 'monitoring/constants', + 'monitoring/services', + 'monitoring/filters', + 'monitoring/directives', + ]); + return appModule; +}; + +function createMonitoringAppConfigConstants(keys: MonitoringPluginDependencies['externalConfig']) { + let constantsModule = angular.module('monitoring/constants', []); + keys.map(([key, value]) => (constantsModule = constantsModule.constant(key as string, value))); +} + +function createLocalStateModule(query: any) { + angular + .module('monitoring/State', ['monitoring/Private']) + .service('globalState', function( + Private: IPrivate, + $rootScope: ng.IRootScopeService, + $location: ng.ILocationService + ) { + function GlobalStateProvider(this: any) { + const state = new GlobalState(query, $rootScope, $location, this); + const initialState: any = state.getState(); + for (const key in initialState) { + if (!initialState.hasOwnProperty(key)) { + continue; + } + this[key] = initialState[key]; + } + this.save = () => { + const newState = { ...this }; + delete newState.save; + state.setState(newState); + }; + } + return Private(GlobalStateProvider); + }); +} + +function createLocalKbnUrlModule() { + angular + .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute']) + .service('kbnUrl', function(Private: IPrivate) { + return Private(KbnUrlProvider); + }); +} + +function createMonitoringAppServices() { + angular + .module('monitoring/services', ['monitoring/Private']) + .service('breadcrumbs', function(Private: IPrivate) { + return Private(breadcrumbsProvider); + }) + .service('monitoringClusters', function(Private: IPrivate) { + return Private(monitoringClustersProvider); + }) + .service('$executor', function(Private: IPrivate) { + return Private(executorProvider); + }) + .service('features', function(Private: IPrivate) { + return Private(featuresProvider); + }) + .service('license', function(Private: IPrivate) { + return Private(licenseProvider); + }) + .service('title', function(Private: IPrivate) { + return Private(titleProvider); + }); +} + +function createMonitoringAppDirectives() { + angular + .module('monitoring/directives', []) + .directive('monitoringBeatsBeat', monitoringBeatsBeatProvider) + .directive('monitoringBeatsOverview', monitoringBeatsOverviewProvider) + .directive('monitoringMlListing', monitoringMlListingProvider) + .directive('monitoringMain', monitoringMainProvider); +} + +function createMonitoringAppFilters() { + angular + .module('monitoring/filters', []) + .filter('capitalize', function() { + return function(input: string) { + return capitalize(input?.toLowerCase()); + }; + }) + .filter('formatNumber', function() { + return formatNumber; + }) + .filter('formatMetric', function() { + return formatMetric; + }) + .filter('extractIp', function() { + return extractIp; + }); +} + +function createLocalConfigModule(core: MonitoringPluginDependencies['core']) { + angular.module('monitoring/Config', []).provider('config', function() { + return { + $get: () => ({ + get: (key: string) => core.uiSettings?.get(key), + }), + }; + }); +} + +function createLocalStorage() { + angular + .module('monitoring/Storage', []) + .service('localStorage', function($window: IWindowService) { + return new Storage($window.localStorage); + }) + .service('sessionStorage', function($window: IWindowService) { + return new Storage($window.sessionStorage); + }) + .service('sessionTimeout', function() { + return {}; + }); +} + +function createLocalPrivateModule() { + angular.module('monitoring/Private', []).provider('Private', PrivateProvider); +} + +function createLocalTopNavModule({ ui }: MonitoringPluginDependencies['navigation']) { + angular + .module('monitoring/TopNav', ['react']) + .directive('kbnTopNav', createTopNavDirective) + .directive('kbnTopNavHelper', createTopNavHelper(ui)); +} + +function createLocalI18nModule() { + angular + .module('monitoring/I18n', []) + .provider('i18n', I18nProvider) + .filter('i18n', i18nFilter) + .directive('i18nId', i18nDirective); +} + +function createHrefModule(core: AppMountContext['core']) { + const name: string = 'kbnHref'; + angular.module('monitoring/href', []).directive(name, function() { + return { + restrict: 'A', + link: { + pre: (_$scope, _$el, $attr) => { + $attr.$observe(name, val => { + if (val) { + const url = getSafeForExternalLink(val as string); + $attr.$set('href', core.http.basePath.prepend(url)); + } + }); + }, + }, + }; + }); +} diff --git a/x-pack/plugins/monitoring/public/angular/helpers/routes.ts b/x-pack/plugins/monitoring/public/angular/helpers/routes.ts new file mode 100644 index 0000000000000..b9307e8594a7a --- /dev/null +++ b/x-pack/plugins/monitoring/public/angular/helpers/routes.ts @@ -0,0 +1,38 @@ +/* + * 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. + */ + +type RouteObject = [string, { reloadOnSearch: boolean }]; +interface Redirect { + redirectTo: string; +} + +class Routes { + private routes: RouteObject[] = []; + public redirect?: Redirect = { redirectTo: '/no-data' }; + + public when = (...args: RouteObject) => { + const [, routeOptions] = args; + routeOptions.reloadOnSearch = false; + this.routes.push(args); + return this; + }; + + public otherwise = (redirect: Redirect) => { + this.redirect = redirect; + return this; + }; + + public addToProvider = ($routeProvider: any) => { + this.routes.forEach(args => { + $routeProvider.when.apply(this, args); + }); + + if (this.redirect) { + $routeProvider.otherwise(this.redirect); + } + }; +} +export const uiRoutes = new Routes(); diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/ui/utils.ts b/x-pack/plugins/monitoring/public/angular/helpers/utils.ts similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/np_imports/ui/utils.ts rename to x-pack/plugins/monitoring/public/angular/helpers/utils.ts diff --git a/x-pack/plugins/monitoring/public/angular/index.ts b/x-pack/plugins/monitoring/public/angular/index.ts new file mode 100644 index 0000000000000..b371503fdb7c9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/angular/index.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import angular, { IModule } from 'angular'; +import { uiRoutes } from './helpers/routes'; +import { Legacy } from '../legacy_shims'; +import { configureAppAngularModule } from '../../../../../src/plugins/kibana_legacy/public'; +import { localAppModule, appModuleName } from './app_modules'; + +import { MonitoringPluginDependencies } from '../types'; + +const APP_WRAPPER_CLASS = 'monitoringApplicationWrapper'; +export class AngularApp { + private injector?: angular.auto.IInjectorService; + + constructor(deps: MonitoringPluginDependencies) { + const { + core, + element, + data, + navigation, + isCloud, + pluginInitializerContext, + externalConfig, + } = deps; + const app: IModule = localAppModule(deps); + app.run(($injector: angular.auto.IInjectorService) => { + this.injector = $injector; + Legacy.init( + { core, element, data, navigation, isCloud, pluginInitializerContext, externalConfig }, + this.injector + ); + }); + + app.config(($routeProvider: unknown) => uiRoutes.addToProvider($routeProvider)); + + const np = { core, env: pluginInitializerContext.env }; + configureAppAngularModule(app, np, true); + const appElement = document.createElement('div'); + appElement.setAttribute('style', 'height: 100%'); + appElement.innerHTML = '<div ng-view style="height: 100%" id="monitoring-angular-app"></div>'; + + if (!element.classList.contains(APP_WRAPPER_CLASS)) { + element.classList.add(APP_WRAPPER_CLASS); + } + + angular.bootstrap(appElement, [appModuleName]); + angular.element(element).append(appElement); + } + + public destroy = () => { + if (this.injector) { + this.injector.get('$rootScope').$destroy(); + } + }; + + public applyScope = () => { + if (!this.injector) { + return; + } + + const rootScope = this.injector.get('$rootScope'); + rootScope.$applyAsync(); + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/private.js b/x-pack/plugins/monitoring/public/angular/providers/private.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/private.js rename to x-pack/plugins/monitoring/public/angular/providers/private.js index 6eae978b828b3..e456f2617f7b8 100644 --- a/x-pack/legacy/plugins/monitoring/public/np_imports/angular/providers/private.js +++ b/x-pack/plugins/monitoring/public/angular/providers/private.js @@ -193,4 +193,6 @@ export function PrivateProvider() { return Private; }, ]; + + return provider; } diff --git a/x-pack/plugins/monitoring/public/angular/providers/url.js b/x-pack/plugins/monitoring/public/angular/providers/url.js new file mode 100644 index 0000000000000..57b63708b546e --- /dev/null +++ b/x-pack/plugins/monitoring/public/angular/providers/url.js @@ -0,0 +1,217 @@ +/* + * 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 _ from 'lodash'; + +export function KbnUrlProvider($injector, $location, $rootScope, $parse) { + /** + * the `kbnUrl` service was created to smooth over some of the + * inconsistent behavior that occurs when modifying the url via + * the `$location` api. In general it is recommended that you use + * the `kbnUrl` service any time you want to modify the url. + * + * "features" that `kbnUrl` does it's best to guarantee, which + * are not guaranteed with the `$location` service: + * - calling `kbnUrl.change()` with a url that resolves to the current + * route will force a full transition (rather than just updating the + * properties of the $route object) + * + * Additional features of `kbnUrl` + * - parameterized urls + * - easily include an app state with the url + * + * @type {KbnUrl} + */ + const self = this; + + /** + * Navigate to a url + * + * @param {String} url - the new url, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the url template + * @return {undefined} + */ + self.change = function(url, paramObj, appState) { + self._changeLocation('url', url, paramObj, false, appState); + }; + + /** + * Same as #change except only changes the url's path, + * leaving the search string and such intact + * + * @param {String} path - the new path, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the path template + * @return {undefined} + */ + self.changePath = function(path, paramObj) { + self._changeLocation('path', path, paramObj); + }; + + /** + * Same as #change except that it removes the current url from history + * + * @param {String} url - the new url, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the url template + * @return {undefined} + */ + self.redirect = function(url, paramObj, appState) { + self._changeLocation('url', url, paramObj, true, appState); + }; + + /** + * Same as #redirect except only changes the url's path, + * leaving the search string and such intact + * + * @param {String} path - the new path, can be a template. See #eval + * @param {Object} [paramObj] - optional set of parameters for the path template + * @return {undefined} + */ + self.redirectPath = function(path, paramObj) { + self._changeLocation('path', path, paramObj, true); + }; + + /** + * Evaluate a url template. templates can contain double-curly wrapped + * expressions that are evaluated in the context of the paramObj + * + * @param {String} template - the url template to evaluate + * @param {Object} [paramObj] - the variables to expose to the template + * @return {String} - the evaluated result + * @throws {Error} If any of the expressions can't be parsed. + */ + self.eval = function(template, paramObj) { + paramObj = paramObj || {}; + + return template.replace(/\{\{([^\}]+)\}\}/g, function(match, expr) { + // remove filters + const key = expr.split('|')[0].trim(); + + // verify that the expression can be evaluated + const p = $parse(key)(paramObj); + + // if evaluation can't be made, throw + if (_.isUndefined(p)) { + throw new Error(`Replacement failed, unresolved expression: ${expr}`); + } + + return encodeURIComponent($parse(expr)(paramObj)); + }); + }; + + /** + * convert an object's route to an href, compatible with + * window.location.href= and <a href=""> + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {string} - the computed href + */ + self.getRouteHref = function(obj, route) { + return '#' + self.getRouteUrl(obj, route); + }; + + /** + * convert an object's route to a url, compatible with url.change() or $location.url() + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {string} - the computed url + */ + self.getRouteUrl = function(obj, route) { + const template = obj && obj.routes && obj.routes[route]; + if (template) return self.eval(template, obj); + }; + + /** + * Similar to getRouteUrl, supports objects which list their routes, + * and redirects to the named route. See #redirect + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {undefined} + */ + self.redirectToRoute = function(obj, route) { + self.redirect(self.getRouteUrl(obj, route)); + }; + + /** + * Similar to getRouteUrl, supports objects which list their routes, + * and changes the url to the named route. See #change + * + * @param {Object} obj - any object that list's it's routes at obj.routes{} + * @param {string} route - the route name + * @return {undefined} + */ + self.changeToRoute = function(obj, route) { + self.change(self.getRouteUrl(obj, route)); + }; + + /** + * Removes the given parameter from the url. Does so without modifying the browser + * history. + * @param param + */ + self.removeParam = function(param) { + $location.search(param, null).replace(); + }; + + ///// + // private api + ///// + let reloading; + + self._changeLocation = function(type, url, paramObj, replace, appState) { + const prev = { + path: $location.path(), + search: $location.search(), + }; + + url = self.eval(url, paramObj); + $location[type](url); + if (replace) $location.replace(); + + if (appState) { + $location.search(appState.getQueryParamName(), appState.toQueryParam()); + } + + const next = { + path: $location.path(), + search: $location.search(), + }; + + if ($injector.has('$route')) { + const $route = $injector.get('$route'); + + if (self._shouldForceReload(next, prev, $route)) { + reloading = $rootScope.$on('$locationChangeSuccess', function() { + // call the "unlisten" function returned by $on + reloading(); + reloading = false; + + $route.reload(); + }); + } + } + }; + + // determine if the router will automatically reload the route + self._shouldForceReload = function(next, prev, $route) { + if (reloading) return false; + + const route = $route.current && $route.current.$$route; + if (!route) return false; + + // for the purposes of determining whether the router will + // automatically be reloading, '' and '/' are equal + const nextPath = next.path || '/'; + const prevPath = prev.path || '/'; + if (nextPath !== prevPath) return false; + + const reloadOnSearch = route.reloadOnSearch; + const searchSame = _.isEqual(next.search, prev.search); + return (reloadOnSearch && searchSame) || !reloadOnSearch; + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__tests__/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/__tests__/map_severity.js rename to x-pack/plugins/monitoring/public/components/alerts/__tests__/map_severity.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js rename to x-pack/plugins/monitoring/public/components/alerts/alerts.js index 95c1af5549198..a86fdb1041a5c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/alerts.js +++ b/x-pack/plugins/monitoring/public/components/alerts/alerts.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import chrome from '../../np_imports/ui/chrome'; +import { Legacy } from '../../legacy_shims'; import { capitalize, get } from 'lodash'; import { formatDateTimeLocal } from '../../../common/formatting'; import { formatTimestampToDuration } from '../../../common'; @@ -16,8 +16,8 @@ import { ALERT_TYPE_CLUSTER_STATE, } from '../../../common/constants'; import { mapSeverity } from './map_severity'; -import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; +import { FormattedAlert } from '../../components/alerts/formatted_alert'; +import { EuiMonitoringTable } from '../../components/table'; import { EuiHealth, EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -162,7 +162,7 @@ export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) category: get(alert, 'metadata.link', get(alert, 'type', null)), })); - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx similarity index 91% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx index 6b7e2391e0301..2c2d7c6464e1b 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx @@ -7,11 +7,15 @@ import React from 'react'; import { mockUseEffects } from '../../../jest.helpers'; import { shallow, ShallowWrapper } from 'enzyme'; -import { kfetch } from 'ui/kfetch'; +import { Legacy } from '../../../legacy_shims'; import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; -jest.mock('ui/kfetch', () => ({ - kfetch: jest.fn(), +jest.mock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn(), + }, + }, })); const defaultProps: AlertsConfigurationProps = { @@ -61,7 +65,7 @@ describe('Configuration', () => { beforeEach(async () => { mockUseEffects(2); - (kfetch as jest.Mock).mockImplementation(() => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { return { data: [ { @@ -101,7 +105,7 @@ describe('Configuration', () => { describe('edit action', () => { let component: ShallowWrapper; beforeEach(async () => { - (kfetch as jest.Mock).mockImplementation(() => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { return { data: [], }; @@ -124,7 +128,7 @@ describe('Configuration', () => { describe('no email address', () => { let component: ShallowWrapper; beforeEach(async () => { - (kfetch as jest.Mock).mockImplementation(() => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(() => { return { data: [ { diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx index eaa474ba177b1..61f86b0f9b609 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -5,10 +5,10 @@ */ import React, { ReactNode } from 'react'; -import { kfetch } from 'ui/kfetch'; import { EuiSteps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { Legacy } from '../../../legacy_shims'; +import { ActionResult } from '../../../../../../plugins/actions/common'; import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; import { getMissingFieldErrors } from '../../../lib/form_validation'; import { Step1 } from './step1'; @@ -59,7 +59,7 @@ export const AlertsConfiguration: React.FC<AlertsConfigurationProps> = ( }, [emailAddress]); async function fetchEmailActions() { - const kibanaActions = await kfetch({ + const kibanaActions = await Legacy.shims.kfetch({ method: 'GET', pathname: `/api/action/_getAll`, }); @@ -84,7 +84,7 @@ export const AlertsConfiguration: React.FC<AlertsConfigurationProps> = ( setShowFormErrors(false); try { - await kfetch({ + await Legacy.shims.kfetch({ method: 'POST', pathname: `/api/monitoring/v1/alerts`, body: JSON.stringify({ selectedEmailActionId, emailAddress }), diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts rename to x-pack/plugins/monitoring/public/components/alerts/configuration/index.ts diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx index 19a1a61d00a42..5734d379dfb0c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -49,9 +49,13 @@ describe('Step1', () => { beforeEach(() => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: () => { - return {}; + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: () => { + return {}; + }, + }, }, })); setModules(); @@ -97,8 +101,12 @@ describe('Step1', () => { it('should send up the create to the server', async () => { const kfetch = jest.fn().mockImplementation(() => {}); jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch, + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch, + }, + }, })); setModules(); }); @@ -152,8 +160,12 @@ describe('Step1', () => { it('should send up the edit to the server', async () => { const kfetch = jest.fn().mockImplementation(() => {}); jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch, + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch, + }, + }, })); setModules(); }); @@ -194,13 +206,17 @@ describe('Step1', () => { describe('testing', () => { it('should allow for testing', async () => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: jest.fn().mockImplementation(arg => { - if (arg.pathname === '/api/action/1/_execute') { - return { status: 'ok' }; - } - return {}; - }), + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn().mockImplementation(arg => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }), + }, + }, })); setModules(); }); @@ -234,12 +250,16 @@ describe('Step1', () => { it('should show a successful test', async () => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: (arg: any) => { - if (arg.pathname === '/api/action/1/_execute') { - return { status: 'ok' }; - } - return {}; + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }, + }, }, })); setModules(); @@ -257,12 +277,16 @@ describe('Step1', () => { it('should show a failed test error', async () => { jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch: (arg: any) => { - if (arg.pathname === '/api/action/1/_execute') { - return { message: 'Very detailed error message' }; - } - return {}; + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { message: 'Very detailed error message' }; + } + return {}; + }, + }, }, })); setModules(); @@ -304,8 +328,12 @@ describe('Step1', () => { it('should send up the delete to the server', async () => { const kfetch = jest.fn().mockImplementation(() => {}); jest.isolateModules(() => { - jest.doMock('ui/kfetch', () => ({ - kfetch, + jest.doMock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch, + }, + }, })); setModules(); }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx index a69bf29dd9874..7953010005885 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/configuration/step1.tsx @@ -16,10 +16,10 @@ import { EuiToolTip, EuiCallOut, } from '@elastic/eui'; -import { kfetch } from 'ui/kfetch'; import { omit, pick } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ActionResult, BASE_ACTION_API_PATH } from '../../../../../../../plugins/actions/common'; +import { Legacy } from '../../../legacy_shims'; +import { ActionResult, BASE_ACTION_API_PATH } from '../../../../../../plugins/actions/common'; import { ManageEmailAction, EmailActionData } from '../manage_email_action'; import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; import { NEW_ACTION_ID } from './configuration'; @@ -42,7 +42,7 @@ export const Step1: React.FC<GetStep1Props> = (props: GetStep1Props) => { async function createEmailAction(data: EmailActionData) { if (props.editAction) { - await kfetch({ + await Legacy.shims.kfetch({ method: 'PUT', pathname: `${BASE_ACTION_API_PATH}/${props.editAction.id}`, body: JSON.stringify({ @@ -53,7 +53,7 @@ export const Step1: React.FC<GetStep1Props> = (props: GetStep1Props) => { }); props.setEditAction(null); } else { - await kfetch({ + await Legacy.shims.kfetch({ method: 'POST', pathname: BASE_ACTION_API_PATH, body: JSON.stringify({ @@ -73,7 +73,7 @@ export const Step1: React.FC<GetStep1Props> = (props: GetStep1Props) => { async function deleteEmailAction(id: string) { setIsDeleting(true); - await kfetch({ + await Legacy.shims.kfetch({ method: 'DELETE', pathname: `${BASE_ACTION_API_PATH}/${id}`, }); @@ -99,7 +99,7 @@ export const Step1: React.FC<GetStep1Props> = (props: GetStep1Props) => { to: [props.emailAddress], }; - const result = await kfetch({ + const result = await Legacy.shims.kfetch({ method: 'POST', pathname: `${BASE_ACTION_API_PATH}/${props.selectedEmailActionId}/_execute`, body: JSON.stringify({ params }), diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step2.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx rename to x-pack/plugins/monitoring/public/components/alerts/configuration/step3.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/formatted_alert.js b/x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/formatted_alert.js rename to x-pack/plugins/monitoring/public/components/alerts/formatted_alert.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/index.js b/x-pack/plugins/monitoring/public/components/alerts/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/index.js rename to x-pack/plugins/monitoring/public/components/alerts/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx rename to x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx index 2bd9804795cb5..3ef9654076340 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/manage_email_action.tsx @@ -21,7 +21,7 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ActionResult } from '../../../../../../plugins/actions/common'; +import { ActionResult } from '../../../../../plugins/actions/common'; import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/map_severity.js b/x-pack/plugins/monitoring/public/components/alerts/map_severity.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/map_severity.js rename to x-pack/plugins/monitoring/public/components/alerts/map_severity.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx similarity index 83% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx rename to x-pack/plugins/monitoring/public/components/alerts/status.test.tsx index d3cf4b463a2cc..a0031f50951bd 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/status.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { kfetch } from 'ui/kfetch'; +import { Legacy } from '../../legacy_shims'; import { AlertsStatus, AlertsStatusProps } from './status'; import { ALERT_TYPES } from '../../../common/constants'; import { getSetupModeState } from '../../lib/setup_mode'; @@ -18,8 +18,16 @@ jest.mock('../../lib/setup_mode', () => ({ toggleSetupMode: jest.fn(), })); -jest.mock('ui/kfetch', () => ({ - kfetch: jest.fn(), +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn(), + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'current', + }, + }, + }, })); const defaultProps: AlertsStatusProps = { @@ -35,7 +43,7 @@ describe('Status', () => { enabled: false, }); - (kfetch as jest.Mock).mockImplementation(({ pathname }) => { + (Legacy.shims.kfetch as jest.Mock).mockImplementation(({ pathname }) => { if (pathname === '/internal/security/api_key/privileges') { return { areApiKeysEnabled: true }; } @@ -62,7 +70,7 @@ describe('Status', () => { }); it('should render a success message if all alerts have been migrated and in setup mode', async () => { - (kfetch as jest.Mock).mockReturnValue({ + (Legacy.shims.kfetch as jest.Mock).mockReturnValue({ data: ALERT_TYPES.map(type => ({ alertTypeId: type })), }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/plugins/monitoring/public/components/alerts/status.tsx similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx rename to x-pack/plugins/monitoring/public/components/alerts/status.tsx index 5f5329bf7fff8..cdddbf1031303 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx +++ b/x-pack/plugins/monitoring/public/components/alerts/status.tsx @@ -5,7 +5,6 @@ */ import React, { Fragment } from 'react'; -import { kfetch } from 'ui/kfetch'; import { EuiSpacer, EuiCallOut, @@ -18,8 +17,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { Alert, BASE_ALERT_API_PATH } from '../../../../../../plugins/alerting/common'; +import { Legacy } from '../../legacy_shims'; +import { Alert, BASE_ALERT_API_PATH } from '../../../../../plugins/alerting/common'; import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; import { AlertsConfiguration } from './configuration'; @@ -39,7 +38,10 @@ export const AlertsStatus: React.FC<AlertsStatusProps> = (props: AlertsStatusPro React.useEffect(() => { async function fetchAlertsStatus() { - const alerts = await kfetch({ method: 'GET', pathname: `${BASE_ALERT_API_PATH}/_find` }); + const alerts = await Legacy.shims.kfetch({ + method: 'GET', + pathname: `${BASE_ALERT_API_PATH}/_find`, + }); const monitoringAlerts = alerts.data.filter((alert: Alert) => alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) ); @@ -57,7 +59,9 @@ export const AlertsStatus: React.FC<AlertsStatusProps> = (props: AlertsStatusPro }, [setupModeEnabled, showMigrationFlyout]); async function fetchSecurityConfigured() { - const response = await kfetch({ pathname: '/internal/security/api_key/privileges' }); + const response = await Legacy.shims.kfetch({ + pathname: '/internal/security/api_key/privileges', + }); setIsSecurityConfigured(response.areApiKeysEnabled); } @@ -72,7 +76,7 @@ export const AlertsStatus: React.FC<AlertsStatusProps> = (props: AlertsStatusPro if (isSecurityConfigured) { return null; } - + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; return ( <Fragment> diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instance/index.js b/x-pack/plugins/monitoring/public/components/apm/instance/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instance/index.js rename to x-pack/plugins/monitoring/public/components/apm/instance/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instance/instance.js rename to x-pack/plugins/monitoring/public/components/apm/instance/instance.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instance/status.js b/x-pack/plugins/monitoring/public/components/apm/instance/status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instance/status.js rename to x-pack/plugins/monitoring/public/components/apm/instance/status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/index.js b/x-pack/plugins/monitoring/public/components/apm/instances/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instances/index.js rename to x-pack/plugins/monitoring/public/components/apm/instances/index.js diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js new file mode 100644 index 0000000000000..a48fbc51341f1 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -0,0 +1,199 @@ +/* + * 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, { Fragment } from 'react'; +import moment from 'moment'; +import { uniq, get } from 'lodash'; +import { EuiMonitoringTable } from '../../table'; +import { + EuiLink, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import { Status } from './status'; +import { formatMetric } from '../../../lib/format_number'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { formatTimestampToDuration } from '../../../../common'; +import { i18n } from '@kbn/i18n'; +import { APM_SYSTEM_ID } from '../../../../common/constants'; +import { ListingCallOut } from '../../setup_mode/listing_callout'; +import { SetupModeBadge } from '../../setup_mode/badge'; +import { FormattedMessage } from '@kbn/i18n/react'; + +function getColumns(setupMode) { + return [ + { + name: i18n.translate('xpack.monitoring.apm.instances.nameTitle', { + defaultMessage: 'Name', + }), + field: 'name', + render: (name, apm) => { + let setupModeStatus = null; + if (setupMode && setupMode.enabled) { + const list = get(setupMode, 'data.byUuid', {}); + const status = list[apm.uuid] || {}; + const instance = { + uuid: apm.uuid, + name: apm.name, + }; + + setupModeStatus = ( + <div className="monTableCell__setupModeStatus"> + <SetupModeBadge + setupMode={setupMode} + status={status} + instance={instance} + productName={APM_SYSTEM_ID} + /> + </div> + ); + } + + return ( + <Fragment> + <EuiLink + href={getSafeForExternalLink(`#/apm/instances/${apm.uuid}`)} + data-test-subj={`apmLink-${name}`} + > + {name} + </EuiLink> + {setupModeStatus} + </Fragment> + ); + }, + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.outputEnabledTitle', { + defaultMessage: 'Output Enabled', + }), + field: 'output', + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.totalEventsRateTitle', { + defaultMessage: 'Total Events Rate', + }), + field: 'total_events_rate', + render: value => formatMetric(value, '', '/s'), + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.bytesSentRateTitle', { + defaultMessage: 'Bytes Sent Rate', + }), + field: 'bytes_sent_rate', + render: value => formatMetric(value, 'byte', '/s'), + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.outputErrorsTitle', { + defaultMessage: 'Output Errors', + }), + field: 'errors', + render: value => formatMetric(value, '0'), + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.lastEventTitle', { + defaultMessage: 'Last Event', + }), + field: 'time_of_last_event', + render: value => + i18n.translate('xpack.monitoring.apm.instances.lastEventValue', { + defaultMessage: '{timeOfLastEvent} ago', + values: { + timeOfLastEvent: formatTimestampToDuration(+moment(value), 'since'), + }, + }), + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.allocatedMemoryTitle', { + defaultMessage: 'Allocated Memory', + }), + field: 'memory', + render: value => formatMetric(value, 'byte'), + }, + { + name: i18n.translate('xpack.monitoring.apm.instances.versionTitle', { + defaultMessage: 'Version', + }), + field: 'version', + }, + ]; +} + +export function ApmServerInstances({ apms, setupMode }) { + const { pagination, sorting, onTableChange, data } = apms; + + let setupModeCallout = null; + if (setupMode.enabled && setupMode.data) { + setupModeCallout = ( + <ListingCallOut + setupModeData={setupMode.data} + useNodeIdentifier={false} + productName={APM_SYSTEM_ID} + /> + ); + } + + const versions = uniq(data.apms.map(item => item.version)).map(version => { + return { value: version }; + }); + + return ( + <EuiPage> + <EuiPageBody> + <EuiScreenReaderOnly> + <h1> + <FormattedMessage + id="xpack.monitoring.apm.instances.heading" + defaultMessage="APM Instances" + /> + </h1> + </EuiScreenReaderOnly> + <EuiPageContent> + <Status stats={data.stats} /> + <EuiSpacer size="m" /> + {setupModeCallout} + <EuiMonitoringTable + className="apmInstancesTable" + rows={data.apms} + columns={getColumns(setupMode)} + sorting={sorting} + pagination={pagination} + setupMode={setupMode} + productName={APM_SYSTEM_ID} + search={{ + box: { + incremental: true, + placeholder: i18n.translate( + 'xpack.monitoring.apm.instances.filterInstancesPlaceholder', + { + defaultMessage: 'Filter Instances…', + } + ), + }, + filters: [ + { + type: 'field_value_selection', + field: 'version', + name: i18n.translate('xpack.monitoring.apm.instances.versionFilter', { + defaultMessage: 'Version', + }), + options: versions, + multiSelect: 'or', + }, + ], + }} + onTableChange={onTableChange} + executeQueryOptions={{ + defaultFields: ['name'], + }} + /> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/instances/status.js b/x-pack/plugins/monitoring/public/components/apm/instances/status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/instances/status.js rename to x-pack/plugins/monitoring/public/components/apm/instances/status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/apm/overview/index.js b/x-pack/plugins/monitoring/public/components/apm/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/apm/overview/index.js rename to x-pack/plugins/monitoring/public/components/apm/overview/index.js diff --git a/x-pack/plugins/monitoring/public/components/apm/status_icon.js b/x-pack/plugins/monitoring/public/components/apm/status_icon.js new file mode 100644 index 0000000000000..073c56217e8df --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/apm/status_icon.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { StatusIcon } from '../../components/status_icon'; +import { i18n } from '@kbn/i18n'; + +export function ApmStatusIcon({ status, availability = true }) { + const type = (() => { + if (!availability) { + return StatusIcon.TYPES.GRAY; + } + + const statusKey = status.toUpperCase(); + return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; + })(); + + return ( + <StatusIcon + type={type} + label={i18n.translate('xpack.monitoring.apm.healthStatusLabel', { + defaultMessage: 'Health: {status}', + values: { + status, + }, + })} + /> + ); +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/beat/beat.js b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/beat/beat.js rename to x-pack/plugins/monitoring/public/components/beats/beat/beat.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/beat/index.js b/x-pack/plugins/monitoring/public/components/beats/beat/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/beat/index.js rename to x-pack/plugins/monitoring/public/components/beats/beat/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/index.js b/x-pack/plugins/monitoring/public/components/beats/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/index.js rename to x-pack/plugins/monitoring/public/components/beats/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/listing/index.js b/x-pack/plugins/monitoring/public/components/beats/listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/listing/index.js rename to x-pack/plugins/monitoring/public/components/beats/listing/index.js diff --git a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js new file mode 100644 index 0000000000000..5863f6e5161ad --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js @@ -0,0 +1,204 @@ +/* + * 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, { PureComponent } from 'react'; +import { uniq, get } from 'lodash'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiLink, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import { Stats } from '../../beats'; +import { formatMetric } from '../../../lib/format_number'; +import { EuiMonitoringTable } from '../../table'; +import { i18n } from '@kbn/i18n'; +import { BEATS_SYSTEM_ID } from '../../../../common/constants'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { ListingCallOut } from '../../setup_mode/listing_callout'; +import { SetupModeBadge } from '../../setup_mode/badge'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export class Listing extends PureComponent { + getColumns() { + const setupMode = this.props.setupMode; + + return [ + { + name: i18n.translate('xpack.monitoring.beats.instances.nameTitle', { + defaultMessage: 'Name', + }), + field: 'name', + render: (name, beat) => { + let setupModeStatus = null; + if (setupMode && setupMode.enabled) { + const list = get(setupMode, 'data.byUuid', {}); + const status = list[beat.uuid] || {}; + const instance = { + uuid: beat.uuid, + name: beat.name, + }; + + setupModeStatus = ( + <div className="monTableCell__setupModeStatus"> + <SetupModeBadge + setupMode={setupMode} + status={status} + instance={instance} + productName={BEATS_SYSTEM_ID} + /> + </div> + ); + } + + return ( + <div> + <EuiLink + href={getSafeForExternalLink(`#/beats/beat/${beat.uuid}`)} + data-test-subj={`beatLink-${name}`} + > + {name} + </EuiLink> + {setupModeStatus} + </div> + ); + }, + }, + { + name: i18n.translate('xpack.monitoring.beats.instances.typeTitle', { + defaultMessage: 'Type', + }), + field: 'type', + }, + { + name: i18n.translate('xpack.monitoring.beats.instances.outputEnabledTitle', { + defaultMessage: 'Output Enabled', + }), + field: 'output', + }, + { + name: i18n.translate('xpack.monitoring.beats.instances.totalEventsRateTitle', { + defaultMessage: 'Total Events Rate', + }), + field: 'total_events_rate', + render: value => formatMetric(value, '', '/s'), + }, + { + name: i18n.translate('xpack.monitoring.beats.instances.bytesSentRateTitle', { + defaultMessage: 'Bytes Sent Rate', + }), + field: 'bytes_sent_rate', + render: value => formatMetric(value, 'byte', '/s'), + }, + { + name: i18n.translate('xpack.monitoring.beats.instances.outputErrorsTitle', { + defaultMessage: 'Output Errors', + }), + field: 'errors', + render: value => formatMetric(value, '0'), + }, + { + name: i18n.translate('xpack.monitoring.beats.instances.allocatedMemoryTitle', { + defaultMessage: 'Allocated Memory', + }), + field: 'memory', + render: value => formatMetric(value, 'byte'), + }, + { + name: i18n.translate('xpack.monitoring.beats.instances.versionTitle', { + defaultMessage: 'Version', + }), + field: 'version', + }, + ]; + } + + render() { + const { stats, data, sorting, pagination, onTableChange, setupMode } = this.props; + + let setupModeCallOut = null; + if (setupMode.enabled && setupMode.data) { + setupModeCallOut = ( + <ListingCallOut + setupModeData={setupMode.data} + useNodeIdentifier={false} + productName={BEATS_SYSTEM_ID} + /> + ); + } + + const types = uniq(data.map(item => item.type)).map(type => { + return { value: type }; + }); + + const versions = uniq(data.map(item => item.version)).map(version => { + return { value: version }; + }); + + return ( + <EuiPage> + <EuiPageBody> + <EuiScreenReaderOnly> + <h1> + <FormattedMessage + id="xpack.monitoring.beats.listing.heading" + defaultMessage="Beats listing" + /> + </h1> + </EuiScreenReaderOnly> + <EuiPageContent> + <Stats stats={stats} /> + <EuiSpacer size="m" /> + {setupModeCallOut} + <EuiMonitoringTable + className="beatsTable" + rows={data} + setupMode={setupMode} + productName={BEATS_SYSTEM_ID} + columns={this.getColumns()} + sorting={sorting} + pagination={pagination} + search={{ + box: { + incremental: true, + placeholder: i18n.translate('xpack.monitoring.beats.filterBeatsPlaceholder', { + defaultMessage: 'Filter Beats...', + }), + }, + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('xpack.monitoring.beats.instances.typeFilter', { + defaultMessage: 'Type', + }), + options: types, + multiSelect: 'or', + }, + { + type: 'field_value_selection', + field: 'version', + name: i18n.translate('xpack.monitoring.beats.instances.versionFilter', { + defaultMessage: 'Version', + }), + options: versions, + multiSelect: 'or', + }, + ], + }} + onTableChange={onTableChange} + executeQueryOptions={{ + defaultFields: ['name', 'type'], + }} + /> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_active.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_types.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/latest_versions.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap b/x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap rename to x-pack/plugins/monitoring/public/components/beats/overview/__snapshots__/overview.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/index.js b/x-pack/plugins/monitoring/public/components/beats/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/index.js rename to x-pack/plugins/monitoring/public/components/beats/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_active.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_active.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_active.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_types.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_types.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_types.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_types.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_types.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/latest_versions.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/latest_versions.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.js rename to x-pack/plugins/monitoring/public/components/beats/overview/overview.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.test.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.test.js rename to x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js index 1947f042b09b7..006f6ce3db975 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/beats/overview/overview.test.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.test.js @@ -10,16 +10,10 @@ import { shallow } from 'enzyme'; jest.mock('../stats', () => ({ Stats: () => 'Stats', })); -jest.mock('../../', () => ({ +jest.mock('../../chart', () => ({ MonitoringTimeseriesContainer: () => 'MonitoringTimeseriesContainer', })); -jest.mock('../../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - import { BeatsOverview } from './overview'; describe('Overview', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/beats/stats.js b/x-pack/plugins/monitoring/public/components/beats/stats.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/beats/stats.js rename to x-pack/plugins/monitoring/public/components/beats/stats.js index 672d8a79ca64a..89ec10bbaf1bb 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/beats/stats.js +++ b/x-pack/plugins/monitoring/public/components/beats/stats.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../lib/format_number'; import { SummaryStatus } from '../summary_status'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_color.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_color.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_color.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_color.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_last_value.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_last_value.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_last_value.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_last_value.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_title.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_title.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_title.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_title.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js b/x-pack/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js rename to x-pack/plugins/monitoring/public/components/chart/__tests__/get_values_for_legend.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss b/x-pack/plugins/monitoring/public/components/chart/_chart.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/_chart.scss rename to x-pack/plugins/monitoring/public/components/chart/_chart.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/_index.scss b/x-pack/plugins/monitoring/public/components/chart/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/_index.scss rename to x-pack/plugins/monitoring/public/components/chart/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js b/x-pack/plugins/monitoring/public/components/chart/chart_target.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js rename to x-pack/plugins/monitoring/public/components/chart/chart_target.js index 5c9cddf9c2902..e6a6cc4b77755 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/chart_target.js +++ b/x-pack/plugins/monitoring/public/components/chart/chart_target.js @@ -6,7 +6,7 @@ import _ from 'lodash'; import React from 'react'; -import $ from 'plugins/xpack_main/jquery_flot'; +import $ from '../../lib/jquery_flot'; import { eventBus } from './event_bus'; import { getChartOptions } from './get_chart_options'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/event_bus.js b/x-pack/plugins/monitoring/public/components/chart/event_bus.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/event_bus.js rename to x-pack/plugins/monitoring/public/components/chart/event_bus.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js rename to x-pack/plugins/monitoring/public/components/chart/get_chart_options.js index 661d51e068201..81a3260447600 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from '../../np_imports/ui/chrome'; +import { Legacy } from '../../legacy_shims'; import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; export async function getChartOptions(axisOptions) { - const $injector = chrome.dangerouslyGetActiveInjector(); + const $injector = Legacy.shims.getAngularInjector(); const timezone = $injector.get('config').get('dateFormat:tz'); const opts = { legend: { diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_color.js b/x-pack/plugins/monitoring/public/components/chart/get_color.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_color.js rename to x-pack/plugins/monitoring/public/components/chart/get_color.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_last_value.js b/x-pack/plugins/monitoring/public/components/chart/get_last_value.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_last_value.js rename to x-pack/plugins/monitoring/public/components/chart/get_last_value.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_title.js b/x-pack/plugins/monitoring/public/components/chart/get_title.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_title.js rename to x-pack/plugins/monitoring/public/components/chart/get_title.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_units.js b/x-pack/plugins/monitoring/public/components/chart/get_units.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_units.js rename to x-pack/plugins/monitoring/public/components/chart/get_units.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/get_values_for_legend.js b/x-pack/plugins/monitoring/public/components/chart/get_values_for_legend.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/get_values_for_legend.js rename to x-pack/plugins/monitoring/public/components/chart/get_values_for_legend.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js b/x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/horizontal_legend.js rename to x-pack/plugins/monitoring/public/components/chart/horizontal_legend.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/index.js b/x-pack/plugins/monitoring/public/components/chart/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/index.js rename to x-pack/plugins/monitoring/public/components/chart/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/info_tooltip.js b/x-pack/plugins/monitoring/public/components/chart/info_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/info_tooltip.js rename to x-pack/plugins/monitoring/public/components/chart/info_tooltip.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries.js rename to x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js rename to x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries_container.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_container.js b/x-pack/plugins/monitoring/public/components/chart/timeseries_container.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_container.js rename to x-pack/plugins/monitoring/public/components/chart/timeseries_container.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js b/x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/chart/timeseries_visualization.js rename to x-pack/plugins/monitoring/public/components/chart/timeseries_visualization.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js rename to x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js index 948f743ba0183..68d7a5a94e42f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/alerts_indicator.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; +import { mapSeverity } from '../../alerts/map_severity'; import { EuiHealth, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/listing/index.js b/x-pack/plugins/monitoring/public/components/cluster/listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/listing/index.js rename to x-pack/plugins/monitoring/public/components/cluster/listing/index.js diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js new file mode 100644 index 0000000000000..feda891c1ce29 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -0,0 +1,458 @@ +/* + * 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, { Fragment, Component } from 'react'; +import { Legacy } from '../../../legacy_shims'; +import moment from 'moment'; +import numeral from '@elastic/numeral'; +import { capitalize, partial } from 'lodash'; +import { + EuiHealth, + EuiLink, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiToolTip, + EuiCallOut, + EuiSpacer, + EuiIcon, +} from '@elastic/eui'; +import { EuiMonitoringTable } from '../../table'; +import { AlertsIndicator } from '../../cluster/listing/alerts_indicator'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; + +const IsClusterSupported = ({ isSupported, children }) => { + return isSupported ? children : '-'; +}; + +const STANDALONE_CLUSTER_STORAGE_KEY = 'viewedStandaloneCluster'; + +/* + * This checks if alerts feature is supported via monitoring cluster + * license. If the alerts feature is not supported because the prod cluster + * license is basic, IsClusterSupported makes the status col hidden + * completely + */ +const IsAlertsSupported = props => { + const { alertsMeta = { enabled: true }, clusterMeta = { enabled: true } } = props.cluster.alerts; + if (alertsMeta.enabled && clusterMeta.enabled) { + return <span>{props.children}</span>; + } + + const message = + alertsMeta.message || + clusterMeta.message || + i18n.translate('xpack.monitoring.cluster.listing.unknownHealthMessage', { + defaultMessage: 'Unknown', + }); + + return ( + <EuiToolTip content={message} position="bottom"> + <EuiHealth color="subdued" data-test-subj="alertIcon"> + N/A + </EuiHealth> + </EuiToolTip> + ); +}; + +const getColumns = ( + showLicenseExpiration, + changeCluster, + handleClickIncompatibleLicense, + handleClickInvalidLicense +) => { + return [ + { + name: i18n.translate('xpack.monitoring.cluster.listing.nameColumnTitle', { + defaultMessage: 'Name', + }), + field: 'cluster_name', + sortable: true, + render: (value, cluster) => { + if (cluster.isSupported) { + return ( + <EuiLink + onClick={() => changeCluster(cluster.cluster_uuid, cluster.ccs)} + data-test-subj="clusterLink" + > + {value} + </EuiLink> + ); + } + + // not supported because license is basic/not compatible with multi-cluster + if (cluster.license) { + return ( + <EuiLink + onClick={() => handleClickIncompatibleLicense(cluster.cluster_name)} + data-test-subj="clusterLink" + > + {value} + </EuiLink> + ); + } + + // not supported because license is invalid + return ( + <EuiLink + onClick={() => handleClickInvalidLicense(cluster.cluster_name)} + data-test-subj="clusterLink" + > + {value} + </EuiLink> + ); + }, + }, + { + name: i18n.translate('xpack.monitoring.cluster.listing.statusColumnTitle', { + defaultMessage: 'Status', + }), + field: 'status', + 'data-test-subj': 'alertsStatus', + sortable: true, + render: (_status, cluster) => ( + <IsClusterSupported {...cluster}> + <IsAlertsSupported cluster={cluster}> + <AlertsIndicator alerts={cluster.alerts} /> + </IsAlertsSupported> + </IsClusterSupported> + ), + }, + { + name: i18n.translate('xpack.monitoring.cluster.listing.nodesColumnTitle', { + defaultMessage: 'Nodes', + }), + field: 'elasticsearch.cluster_stats.nodes.count.total', + 'data-test-subj': 'nodesCount', + sortable: true, + render: (total, cluster) => ( + <IsClusterSupported {...cluster}>{numeral(total).format('0,0')}</IsClusterSupported> + ), + }, + { + name: i18n.translate('xpack.monitoring.cluster.listing.indicesColumnTitle', { + defaultMessage: 'Indices', + }), + field: 'elasticsearch.cluster_stats.indices.count', + 'data-test-subj': 'indicesCount', + sortable: true, + render: (count, cluster) => ( + <IsClusterSupported {...cluster}>{numeral(count).format('0,0')}</IsClusterSupported> + ), + }, + { + name: i18n.translate('xpack.monitoring.cluster.listing.dataColumnTitle', { + defaultMessage: 'Data', + }), + field: 'elasticsearch.cluster_stats.indices.store.size_in_bytes', + 'data-test-subj': 'dataSize', + sortable: true, + render: (size, cluster) => ( + <IsClusterSupported {...cluster}>{numeral(size).format('0,0[.]0 b')}</IsClusterSupported> + ), + }, + { + name: i18n.translate('xpack.monitoring.cluster.listing.logstashColumnTitle', { + defaultMessage: 'Logstash', + }), + field: 'logstash.node_count', + 'data-test-subj': 'logstashCount', + sortable: true, + render: (count, cluster) => ( + <IsClusterSupported {...cluster}>{numeral(count).format('0,0')}</IsClusterSupported> + ), + }, + { + name: i18n.translate('xpack.monitoring.cluster.listing.kibanaColumnTitle', { + defaultMessage: 'Kibana', + }), + field: 'kibana.count', + 'data-test-subj': 'kibanaCount', + sortable: true, + render: (count, cluster) => ( + <IsClusterSupported {...cluster}>{numeral(count).format('0,0')}</IsClusterSupported> + ), + }, + { + name: i18n.translate('xpack.monitoring.cluster.listing.licenseColumnTitle', { + defaultMessage: 'License', + }), + field: 'license.type', + 'data-test-subj': 'clusterLicense', + sortable: true, + render: (licenseType, cluster) => { + const license = cluster.license; + + if (!licenseType) { + return ( + <div> + <div className="monTableCell__clusterCellLiscense">N/A</div> + </div> + ); + } + + if (license) { + const licenseExpiry = () => { + if (license.expiry_date_in_millis < moment().valueOf()) { + // license is expired + return <span className="monTableCell__clusterCellExpired">Expired</span>; + } + + // license is fine + return <span>Expires {moment(license.expiry_date_in_millis).format('D MMM YY')}</span>; + }; + + return ( + <div> + <div className="monTableCell__clusterCellLiscense">{capitalize(licenseType)}</div> + <div className="monTableCell__clusterCellExpiration"> + {showLicenseExpiration ? licenseExpiry() : null} + </div> + </div> + ); + } + + // there is no license! + return ( + <EuiLink onClick={() => handleClickInvalidLicense(cluster.cluster_name)}> + <EuiHealth color="subdued" data-test-subj="alertIcon"> + N/A + </EuiHealth> + </EuiLink> + ); + }, + }, + ]; +}; + +const changeCluster = (scope, globalState, kbnUrl, clusterUuid, ccs) => { + scope.$evalAsync(() => { + globalState.cluster_uuid = clusterUuid; + globalState.ccs = ccs; + globalState.save(); + kbnUrl.redirect('/overview'); + }); +}; + +const licenseWarning = (scope, { title, text }) => { + scope.$evalAsync(() => { + Legacy.shims.toastNotifications.addWarning({ + title, + text, + 'data-test-subj': 'monitoringLicenseWarning', + }); + }); +}; + +const handleClickIncompatibleLicense = (scope, clusterName) => { + licenseWarning(scope, { + title: toMountPoint( + <FormattedMessage + id="xpack.monitoring.cluster.listing.incompatibleLicense.warningMessageTitle" + defaultMessage="You can't view the {clusterName} cluster" + values={{ clusterName: '"' + clusterName + '"' }} + /> + ), + text: toMountPoint( + <Fragment> + <p> + <FormattedMessage + id="xpack.monitoring.cluster.listing.incompatibleLicense.noMultiClusterSupportMessage" + defaultMessage="The Basic license does not support multi-cluster monitoring." + /> + </p> + <p> + <FormattedMessage + id="xpack.monitoring.cluster.listing.incompatibleLicense.infoMessage" + defaultMessage="Need to monitor multiple clusters? {getLicenseInfoLink} to enjoy multi-cluster monitoring." + values={{ + getLicenseInfoLink: ( + <EuiLink href="https://www.elastic.co/subscriptions" target="_blank"> + <FormattedMessage + id="xpack.monitoring.cluster.listing.incompatibleLicense.getLicenseLinkLabel" + defaultMessage="Get a license with full functionality" + /> + </EuiLink> + ), + }} + /> + </p> + </Fragment> + ), + }); +}; + +const handleClickInvalidLicense = (scope, clusterName) => { + const licensingPath = `${Legacy.shims.getBasePath()}/app/kibana#/management/elasticsearch/license_management/home`; + + licenseWarning(scope, { + title: toMountPoint( + <FormattedMessage + id="xpack.monitoring.cluster.listing.invalidLicense.warningMessageTitle" + defaultMessage="You can't view the {clusterName} cluster" + values={{ clusterName: '"' + clusterName + '"' }} + /> + ), + text: toMountPoint( + <Fragment> + <p> + <FormattedMessage + id="xpack.monitoring.cluster.listing.invalidLicense.invalidInfoMessage" + defaultMessage="The license information is invalid." + /> + </p> + <p> + <FormattedMessage + id="xpack.monitoring.cluster.listing.invalidLicense.infoMessage" + defaultMessage="Need a license? {getBasicLicenseLink} or {getLicenseInfoLink} to enjoy multi-cluster monitoring." + values={{ + getBasicLicenseLink: ( + <EuiLink href={licensingPath}> + <FormattedMessage + id="xpack.monitoring.cluster.listing.invalidLicense.getBasicLicenseLinkLabel" + defaultMessage="Get a free Basic license" + /> + </EuiLink> + ), + getLicenseInfoLink: ( + <EuiLink href="https://www.elastic.co/subscriptions" target="_blank"> + <FormattedMessage + id="xpack.monitoring.cluster.listing.invalidLicense.getLicenseLinkLabel" + defaultMessage="Get a license with full functionality" + /> + </EuiLink> + ), + }} + /> + </p> + </Fragment> + ), + }); +}; + +export class Listing extends Component { + constructor(props) { + super(props); + this.state = { + [STANDALONE_CLUSTER_STORAGE_KEY]: false, + }; + } + + renderStandaloneClusterCallout(changeCluster, storage) { + if (storage.get(STANDALONE_CLUSTER_STORAGE_KEY)) { + return null; + } + + return ( + <div> + <EuiCallOut + color="warning" + title={i18n.translate('xpack.monitoring.cluster.listing.standaloneClusterCallOutTitle', { + defaultMessage: + "It looks like you have instances that aren't connected to an Elasticsearch cluster.", + })} + iconType="link" + > + <p> + <EuiLink + onClick={() => changeCluster(STANDALONE_CLUSTER_CLUSTER_UUID)} + data-test-subj="standaloneClusterLink" + > + <FormattedMessage + id="xpack.monitoring.cluster.listing.standaloneClusterCallOutLink" + defaultMessage="View these instances." + /> + </EuiLink> +   + <FormattedMessage + id="xpack.monitoring.cluster.listing.standaloneClusterCallOutText" + defaultMessage="Or, click Standalone Cluster in the table below" + /> + </p> + <p> + <EuiLink + onClick={() => { + storage.set(STANDALONE_CLUSTER_STORAGE_KEY, true); + this.setState({ [STANDALONE_CLUSTER_STORAGE_KEY]: true }); + }} + > + <EuiIcon type="cross" /> +   + <FormattedMessage + id="xpack.monitoring.cluster.listing.standaloneClusterCallOutDismiss" + defaultMessage="Dismiss" + /> + </EuiLink> + </p> + </EuiCallOut> + <EuiSpacer /> + </div> + ); + } + + render() { + const { angular, clusters, sorting, pagination, onTableChange } = this.props; + + const _changeCluster = partial( + changeCluster, + angular.scope, + angular.globalState, + angular.kbnUrl + ); + const _handleClickIncompatibleLicense = partial(handleClickIncompatibleLicense, angular.scope); + const _handleClickInvalidLicense = partial(handleClickInvalidLicense, angular.scope); + const hasStandaloneCluster = !!clusters.find( + cluster => cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID + ); + + return ( + <EuiPage> + <EuiPageBody> + <EuiPageContent> + {hasStandaloneCluster + ? this.renderStandaloneClusterCallout(_changeCluster, angular.storage) + : null} + <EuiMonitoringTable + className="clusterTable" + rows={clusters} + columns={getColumns( + angular.showLicenseExpiration, + _changeCluster, + _handleClickIncompatibleLicense, + _handleClickInvalidLicense + )} + rowProps={item => { + return { + 'data-test-subj': `clusterRow_${item.cluster_uuid}`, + }; + }} + sorting={{ + ...sorting, + sort: { + ...sorting.sort, + field: 'cluster_name', + }, + }} + pagination={pagination} + search={{ + box: { + incremental: true, + placeholder: angular.scope.filterText, + }, + }} + onTableChange={onTableChange} + executeQueryOptions={{ + defaultFields: ['cluster_name'], + }} + /> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap b/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap rename to x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/__snapshots__/helpers.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js b/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js index fea8f0001540a..e09c42bc59429 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/__tests__/helpers.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { BytesUsage, BytesPercentageUsage } from '../helpers'; describe('Bytes Usage', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index d87ff98e79be0..6dcd64f875e1c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -6,8 +6,8 @@ import React, { Fragment } from 'react'; import moment from 'moment-timezone'; -import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; -import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; +import { FormattedAlert } from '../../alerts/formatted_alert'; +import { mapSeverity } from '../../alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; import { CALCULATE_DURATION_SINCE, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/apm_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index 84dc13e9da1de..97205e0fcd732 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -7,7 +7,7 @@ import React from 'react'; import moment from 'moment'; import { get } from 'lodash'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../../lib/format_number'; import { ClusterItemContainer, BytesUsage, DisabledIfNoDataAndInSetupModeLink } from './helpers'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js index 7406c15f3cf1d..c20770bdda6b7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/beats_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/beats_panel.js @@ -6,7 +6,7 @@ import { get } from 'lodash'; import React from 'react'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../../lib/format_number'; import { EuiFlexGrid, EuiFlexItem, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js index 7b08c89f53881..41dc662c94211 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/elasticsearch_panel.js @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { get, capitalize } from 'lodash'; -import { formatNumber } from 'plugins/monitoring/lib/format_number'; +import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, HealthStatusIndicator, @@ -32,6 +32,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from '../../logs/reason'; import { SetupModeTooltip } from '../../setup_mode/tooltip'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; const calculateShards = shards => { @@ -168,7 +169,7 @@ export function ElasticsearchPanel(props) { const showMlJobs = () => { // if license doesn't support ML, then `ml === null` if (props.ml) { - const gotoURL = '#/elasticsearch/ml_jobs'; + const gotoURL = getSafeForExternalLink('#/elasticsearch/ml_jobs'); return ( <> <EuiDescriptionListTitle> diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/helpers.js b/x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/helpers.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/helpers.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/plugins/monitoring/public/components/cluster/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/kibana_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index ee17ce446dd6d..541c240b3c35a 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { formatNumber } from 'plugins/monitoring/lib/format_number'; +import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, HealthStatusIndicator, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js b/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js similarity index 90% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js index c6fb386c755f3..012c81e63931e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/license_text.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/license_text.js @@ -6,6 +6,7 @@ import React from 'react'; import moment from 'moment-timezone'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { capitalize } from 'lodash'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,7 +19,7 @@ export function LicenseText({ license, showLicenseExpiration }) { } return ( - <EuiLink href="#/license"> + <EuiLink href={getSafeForExternalLink('#/license')}> <FormattedMessage id="xpack.monitoring.cluster.overview.licenseText.toLicensePageLinkLabel" defaultMessage="{licenseType} license {willExpireOn}" diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js rename to x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js index 9e21ef1074ed3..19a318642aab7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/logstash_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/logstash_panel.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { formatNumber } from 'plugins/monitoring/lib/format_number'; +import { formatNumber } from '../../../lib/format_number'; import { ClusterItemContainer, BytesPercentageUsage, diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/status_icon.js b/x-pack/plugins/monitoring/public/components/cluster/status_icon.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/cluster/status_icon.js rename to x-pack/plugins/monitoring/public/components/cluster/status_icon.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/__snapshots__/ccr.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js index 8e87e94943ee4..5aad73d7a131e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.js @@ -17,10 +17,9 @@ import { EuiTextColor, EuiScreenReaderOnly, } from '@elastic/eui'; - -import './ccr.css'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; function toSeconds(ms) { return Math.floor(ms / 1000) + 's'; @@ -65,7 +64,9 @@ export class Ccr extends Component { ), render: shardId => { return ( - <EuiLink href={`#/elasticsearch/ccr/${index}/shard/${shardId}`}> + <EuiLink + href={getSafeForExternalLink(`#/elasticsearch/ccr/${index}/shard/${shardId}`)} + > {shardId} </EuiLink> ); diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/ccr.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.css b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr/ccr.css rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr/index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/__snapshots__/ccr_shard.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js index af0ff323b7ba8..a8aa931bad254 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.js @@ -5,7 +5,7 @@ */ import React, { Fragment, PureComponent } from 'react'; -import chrome from '../../../np_imports/ui/chrome'; +import { Legacy } from '../../../legacy_shims'; import { EuiPage, EuiPageBody, @@ -93,7 +93,7 @@ export class CcrShard extends PureComponent { renderLatestStat() { const { stat, timestamp } = this.props; - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return ( diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js similarity index 88% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js index b950c2ca0a6d2..ceb1fd1186eed 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/ccr_shard.test.js @@ -8,13 +8,18 @@ import React from 'react'; import { shallow } from 'enzyme'; import { CcrShard } from './ccr_shard'; -jest.mock('../../../np_imports/ui/chrome', () => { +jest.mock('../../../legacy_shims', () => { return { - getBasePath: () => '', - dangerouslyGetActiveInjector: () => ({ get: () => ({ get: () => 'utc' }) }), + Legacy: { + shims: { getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }) }, + }, }; }); +jest.mock('../../chart', () => ({ + MonitoringTimeseriesContainer: () => 'MonitoringTimeseriesContainer', +})); + describe('CcrShard', () => { const props = { formattedLeader: 'leader on remote', diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/ccr_shard/status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/advanced.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index/advanced.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index_detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js index f8dd7b0af7a17..a73dfbf8cd321 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/indices/indices.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.js @@ -8,6 +8,7 @@ import React from 'react'; import { capitalize } from 'lodash'; import { LARGE_FLOAT, LARGE_BYTES, LARGE_ABBREVIATED } from '../../../../common/formatting'; import { formatMetric } from '../../../lib/format_number'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ElasticsearchStatusIcon } from '../status_icon'; import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringTable } from '../../table'; @@ -34,7 +35,10 @@ const columns = [ sortable: true, render: value => ( <div data-test-subj="name"> - <EuiLink href={`#/elasticsearch/indices/${value}`} data-test-subj={`indexLink-${value}`}> + <EuiLink + href={getSafeForExternalLink(`#/elasticsearch/indices/${value}`)} + data-test-subj={`indexLink-${value}`} + > {value} </EuiLink> </div> diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js new file mode 100644 index 0000000000000..4d457c943c872 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.js @@ -0,0 +1,36 @@ +/* + * 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 { StatusIcon } from '../../status_icon'; +import { i18n } from '@kbn/i18n'; + +export function MachineLearningJobStatusIcon({ status }) { + const type = (() => { + const statusKey = status.toUpperCase(); + + if (statusKey === 'OPENED') { + return StatusIcon.TYPES.GREEN; + } else if (statusKey === 'CLOSED') { + return StatusIcon.TYPES.GRAY; + } else if (statusKey === 'FAILED') { + return StatusIcon.TYPES.RED; + } + + // basically a "changing" state like OPENING or CLOSING + return StatusIcon.TYPES.YELLOW; + })(); + + return ( + <StatusIcon + type={type} + label={i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.statusIconLabel', { + defaultMessage: 'Job Status: {status}', + values: { status }, + })} + /> + ); +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/advanced.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/node.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node/status_icon.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node_detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js index 50abdcc718bc8..0c4b4b2b3c3f4 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { MetricCell } from '../cells'; describe('Node Listing Metric Cell', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/cells.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/cells.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index d9cf29f73ce0d..9e6252315f429 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -7,6 +7,7 @@ import React, { Fragment } from 'react'; import { NodeStatusIcon } from '../node'; import { extractIp } from '../../../lib/extract_ip'; // TODO this is only used for elasticsearch nodes summary / node detail, so it should be moved to components/elasticsearch/nodes/lib +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import { ClusterStatus } from '../cluster_status'; import { EuiMonitoringSSPTable } from '../../table'; import { MetricCell, OfflineCell } from './cells'; @@ -75,7 +76,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid) => { render: (value, node) => { let nameLink = ( <EuiLink - href={`#/elasticsearch/nodes/${node.resolver}`} + href={getSafeForExternalLink(`#/elasticsearch/nodes/${node.resolver}`)} data-test-subj={`nodeLink-${node.resolver}`} > {value} diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/overview.js b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/overview/overview.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js index 133b520947b1b..1aaba96ed3da4 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from '../../../np_imports/ui/chrome'; +import { Legacy } from '../../../legacy_shims'; import { capitalize } from 'lodash'; -import { formatMetric } from 'plugins/monitoring/lib/format_number'; +import { formatMetric } from '../../../lib/format_number'; import { formatDateTimeLocal } from '../../../../common/formatting'; const getIpAndPort = transport => { @@ -39,7 +39,7 @@ export const parseProps = props => { } = props; const { files, size } = index; - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return { diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/progress.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js similarity index 85% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js index 7a9d096437d52..27598bee6d841 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/recovery_index.js @@ -8,13 +8,14 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; import { Snapshot } from './snapshot'; import { FormattedMessage } from '@kbn/i18n/react'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; export const RecoveryIndex = props => { const { name, shard, relocationType } = props; return ( <div> - <EuiLink href={`#/elasticsearch/indices/${name}`}>{name}</EuiLink> + <EuiLink href={getSafeForExternalLink(`#/elasticsearch/indices/${name}`)}>{name}</EuiLink> <br /> <FormattedMessage id="xpack.monitoring.elasticsearch.shardActivity.recoveryIndex.shardDescription" diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js index 6607d236590c1..d228144778c02 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity.js @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { EuiText, EuiTitle, EuiLink, EuiSpacer, EuiSwitch } from '@elastic/eui'; -import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; +import { EuiMonitoringTable } from '../../table'; import { RecoveryIndex } from './recovery_index'; import { TotalTime } from './total_time'; import { SourceDestination } from './source_destination'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/snapshot.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_destination.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/source_tooltip.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/total_time.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/_shard_allocation.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/__snapshots__/shard.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/shard.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/unassigned.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/calculate_class.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/decorate_shards.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_primary_children.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/has_unassigned.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/labels.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/vents.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/indices_by_nodes.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/elasticsearch/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/elasticsearch/status_icon.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/index.js b/x-pack/plugins/monitoring/public/components/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/index.js rename to x-pack/plugins/monitoring/public/components/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/cluster_status/index.js rename to x-pack/plugins/monitoring/public/components/kibana/cluster_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/detail_status/index.js b/x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/detail_status/index.js rename to x-pack/plugins/monitoring/public/components/kibana/detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/kibana/instances/index.js b/x-pack/plugins/monitoring/public/components/kibana/instances/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/kibana/instances/index.js rename to x-pack/plugins/monitoring/public/components/kibana/instances/index.js diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js new file mode 100644 index 0000000000000..ed562c7da995b --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.js @@ -0,0 +1,291 @@ +/* + * 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, { PureComponent, Fragment } from 'react'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPanel, + EuiSpacer, + EuiLink, + EuiCallOut, + EuiScreenReaderOnly, +} from '@elastic/eui'; +import { capitalize, get } from 'lodash'; +import { ClusterStatus } from '../cluster_status'; +import { EuiMonitoringTable } from '../../table'; +import { KibanaStatusIcon } from '../status_icon'; +import { StatusIcon } from '../../status_icon'; +import { formatMetric, formatNumber } from '../../../lib/format_number'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SetupModeBadge } from '../../setup_mode/badge'; +import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; +import { ListingCallOut } from '../../setup_mode/listing_callout'; + +const getColumns = setupMode => { + const columns = [ + { + name: i18n.translate('xpack.monitoring.kibana.listing.nameColumnTitle', { + defaultMessage: 'Name', + }), + field: 'name', + render: (name, kibana) => { + let setupModeStatus = null; + if (setupMode && setupMode.enabled) { + const list = get(setupMode, 'data.byUuid', {}); + const uuid = get(kibana, 'kibana.uuid'); + const status = list[uuid] || {}; + const instance = { + uuid, + name: kibana.name, + }; + + setupModeStatus = ( + <div className="monTableCell__setupModeStatus"> + <SetupModeBadge + setupMode={setupMode} + status={status} + instance={instance} + productName={KIBANA_SYSTEM_ID} + /> + </div> + ); + if (status.isNetNewUser) { + return ( + <div> + {name} + {setupModeStatus} + </div> + ); + } + } + + return ( + <div> + <EuiLink + href={getSafeForExternalLink(`#/kibana/instances/${kibana.kibana.uuid}`)} + data-test-subj={`kibanaLink-${name}`} + > + {name} + </EuiLink> + {setupModeStatus} + </div> + ); + }, + }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.statusColumnTitle', { + defaultMessage: 'Status', + }), + field: 'status', + render: (status, kibana) => ( + <div + title={i18n.translate('xpack.monitoring.kibana.listing.instanceStatusTitle', { + defaultMessage: 'Instance status: {kibanaStatus}', + values: { + kibanaStatus: status, + }, + })} + className="monTableCell__status" + > + <KibanaStatusIcon status={status} availability={kibana.availability} /> +   + {!kibana.availability ? ( + <FormattedMessage + id="xpack.monitoring.kibana.listing.instanceStatus.offlineLabel" + defaultMessage="Offline" + /> + ) : ( + capitalize(status) + )} + </div> + ), + }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.loadAverageColumnTitle', { + defaultMessage: 'Load Average', + }), + field: 'os.load.1m', + render: value => <span>{formatMetric(value, '0.00')}</span>, + }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.memorySizeColumnTitle', { + defaultMessage: 'Memory Size', + }), + field: 'process.memory.resident_set_size_in_bytes', + render: value => <span>{formatNumber(value, 'byte')}</span>, + }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.requestsColumnTitle', { + defaultMessage: 'Requests', + }), + field: 'requests.total', + render: value => <span>{formatNumber(value, 'int_commas')}</span>, + }, + { + name: i18n.translate('xpack.monitoring.kibana.listing.responseTimeColumnTitle', { + defaultMessage: 'Response Times', + }), + // It is possible this does not exist through MB collection + field: 'response_times.average', + render: (value, kibana) => { + if (!value) { + return null; + } + + return ( + <div> + <div className="monTableCell__splitNumber"> + {formatNumber(value, 'int_commas') + ' ms avg'} + </div> + <div className="monTableCell__splitNumber"> + {formatNumber(kibana.response_times.max, 'int_commas')} ms max + </div> + </div> + ); + }, + }, + ]; + + return columns; +}; + +export class KibanaInstances extends PureComponent { + render() { + const { clusterStatus, angular, setupMode, sorting, pagination, onTableChange } = this.props; + + let setupModeCallOut = null; + // Merge the instances data with the setup data if enabled + const instances = this.props.instances || []; + if (setupMode.enabled && setupMode.data) { + // We want to create a seamless experience for the user by merging in the setup data + // and the node data from monitoring indices in the likely scenario where some instances + // are using MB collection and some are using no collection + const instancesByUuid = instances.reduce( + (byUuid, instance) => ({ + ...byUuid, + [get(instance, 'kibana.uuid')]: instance, + }), + {} + ); + + instances.push( + ...Object.entries(setupMode.data.byUuid).reduce((instances, [nodeUuid, instance]) => { + if (!instancesByUuid[nodeUuid]) { + instances.push({ + kibana: { + ...instance.instance.kibana, + status: StatusIcon.TYPES.GRAY, + }, + }); + } + return instances; + }, []) + ); + + setupModeCallOut = ( + <ListingCallOut + setupModeData={setupMode.data} + useNodeIdentifier={false} + productName={KIBANA_SYSTEM_ID} + customRenderer={() => { + const customRenderResponse = { + shouldRender: false, + componentToRender: null, + }; + + const hasInstances = setupMode.data.totalUniqueInstanceCount > 0; + if (!hasInstances) { + customRenderResponse.shouldRender = true; + customRenderResponse.componentToRender = ( + <Fragment> + <EuiCallOut + title={i18n.translate( + 'xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeTitle', + { + defaultMessage: 'Kibana instance detected', + } + )} + color="warning" + iconType="flag" + > + <p> + {i18n.translate( + 'xpack.monitoring.kibana.instances.metricbeatMigration.detectedNodeDescription', + { + defaultMessage: `The following instances are not monitored. + Click 'Monitor with Metricbeat' below to start monitoring.`, + } + )} + </p> + </EuiCallOut> + <EuiSpacer size="m" /> + </Fragment> + ); + } + + return customRenderResponse; + }} + /> + ); + } + + const dataFlattened = instances.map(item => ({ + ...item, + name: item.kibana.name, + status: item.kibana.status, + })); + + return ( + <EuiPage> + <EuiPageBody> + <EuiScreenReaderOnly> + <h1> + <FormattedMessage + id="xpack.monitoring.kibana.instances.heading" + defaultMessage="Kibana instances" + /> + </h1> + </EuiScreenReaderOnly> + <EuiPanel> + <ClusterStatus stats={clusterStatus} /> + </EuiPanel> + <EuiSpacer size="m" /> + {setupModeCallOut} + <EuiPageContent> + <EuiMonitoringTable + className="kibanaInstancesTable" + rows={dataFlattened} + columns={getColumns(angular.kbnUrl, angular.$scope, setupMode)} + sorting={sorting} + pagination={pagination} + setupMode={setupMode} + productName={KIBANA_SYSTEM_ID} + search={{ + box: { + incremental: true, + placeholder: i18n.translate( + 'xpack.monitoring.kibana.listing.filterInstancesPlaceholder', + { + defaultMessage: 'Filter Instances…', + } + ), + }, + }} + onTableChange={onTableChange} + executeQueryOptions={{ + defaultFields: ['name'], + }} + /> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } +} diff --git a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js new file mode 100644 index 0000000000000..3c18197170b50 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { StatusIcon } from '../status_icon'; +import { i18n } from '@kbn/i18n'; + +export function KibanaStatusIcon({ status, availability = true }) { + const type = (() => { + if (!availability) { + return StatusIcon.TYPES.GRAY; + } + + const statusKey = status.toUpperCase(); + return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; + })(); + + return ( + <StatusIcon + type={type} + label={i18n.translate('xpack.monitoring.kibana.statusIconLabel', { + defaultMessage: 'Health: {status}', + values: { + status, + }, + })} + /> + ); +} diff --git a/x-pack/plugins/monitoring/public/components/license/index.js b/x-pack/plugins/monitoring/public/components/license/index.js new file mode 100644 index 0000000000000..085cc9082cf53 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/license/index.js @@ -0,0 +1,201 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiPage, + EuiPageBody, + EuiSpacer, + EuiCodeBlock, + EuiPanel, + EuiText, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiScreenReaderOnly, + EuiCard, + EuiButton, + EuiIcon, + EuiTitle, + EuiTextAlign, +} from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { Legacy } from '../../legacy_shims'; + +export const AddLicense = ({ uploadPath }) => { + return ( + <EuiCard + title={ + <FormattedMessage + id="xpack.monitoring.updateLicenseTitle" + defaultMessage="Update your license" + /> + } + description={ + <FormattedMessage + id="xpack.monitoring.useAvailableLicenseDescription" + defaultMessage="If you already have a new license, upload it now." + /> + } + footer={ + <EuiButton data-test-subj="updateLicenseButton" href={uploadPath}> + <FormattedMessage + id="xpack.monitoring.updateLicenseButtonLabel" + defaultMessage="Update license" + /> + </EuiButton> + } + /> + ); +}; + +export class LicenseStatus extends React.PureComponent { + render() { + const { isExpired, status, type, expiryDate } = this.props; + const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); + let icon; + let title; + let message; + if (isExpired) { + icon = <EuiIcon color="danger" type="alert" />; + message = ( + <Fragment> + <FormattedMessage + id="xpack.monitoring.expiredLicenseStatusDescription" + defaultMessage="Your license expired on {expiryDate}" + values={{ + expiryDate: <strong>{expiryDate}</strong>, + }} + /> + </Fragment> + ); + title = ( + <FormattedMessage + id="xpack.monitoring.expiredLicenseStatusTitle" + defaultMessage="Your {typeTitleCase} license has expired" + values={{ + typeTitleCase, + }} + /> + ); + } else { + icon = <EuiIcon color="success" type="checkInCircleFilled" size="l" />; + message = expiryDate ? ( + <Fragment> + <FormattedMessage + id="xpack.monitoring.activeLicenseStatusDescription" + defaultMessage="Your license will expire on {expiryDate}" + values={{ + expiryDate: <strong>{expiryDate}</strong>, + }} + /> + </Fragment> + ) : ( + <Fragment> + <FormattedMessage + id="xpack.monitoring.permanentActiveLicenseStatusDescription" + defaultMessage="Your license will never expire." + /> + </Fragment> + ); + title = ( + <FormattedMessage + id="xpack.monitoring.activeLicenseStatusTitle" + defaultMessage="Your {typeTitleCase} license is {status}" + values={{ + typeTitleCase, + status: status.toLowerCase(), + }} + /> + ); + } + return ( + <EuiTextAlign textAlign="center"> + <EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}>{icon}</EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size="m"> + <h1 data-test-subj="licenseText">{title}</h1> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer /> + + <EuiText data-test-subj="licenseSubText">{message}</EuiText> + </EuiTextAlign> + ); + } +} + +const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => { + if (!isPrimaryCluster) { + return null; + } + + // viewed license is for the cluster directly connected to Kibana + return <AddLicense uploadPath={uploadLicensePath} />; +}; + +const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { + if (isPrimaryCluster) { + return null; + } + + // viewed license is for a remote monitored cluster not directly connected to Kibana + return ( + <EuiPanel> + <p> + <FormattedMessage + id="xpack.monitoring.license.howToUpdateLicenseDescription" + defaultMessage="To update the license for this cluster, provide the license file through + the Elasticsearch {apiText}:" + values={{ + apiText: 'API', + }} + /> + </p> + <EuiSpacer /> + <EuiCodeBlock> + {`curl -XPUT -u <user> 'https://<host>:<port>/_license' -H 'Content-Type: application/json' -d @license.json`} + </EuiCodeBlock> + </EuiPanel> + ); +}; + +export function License(props) { + const { status, type, isExpired, expiryDate } = props; + const licenseManagement = `${Legacy.shims.getBasePath()}/app/kibana#/management/elasticsearch/license_management`; + return ( + <EuiPage> + <EuiScreenReaderOnly> + <h1> + <FormattedMessage id="xpack.monitoring.license.heading" defaultMessage="License" /> + </h1> + </EuiScreenReaderOnly> + <EuiPageBody> + <LicenseStatus isExpired={isExpired} status={status} type={type} expiryDate={expiryDate} /> + <EuiSpacer /> + + <EuiFlexGroup justifyContent="center"> + <EuiFlexItem grow={false}> + <LicenseUpdateInfoForPrimary {...props} /> + <LicenseUpdateInfoForRemote {...props} /> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer /> + <EuiText size="s" textAlign="center"> + <p> + For more license options please visit  + <EuiLink href={licenseManagement}>License Management</EuiLink>. + </p> + </EuiText> + </EuiPageBody> + </EuiPage> + ); +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap rename to x-pack/plugins/monitoring/public/components/logs/__snapshots__/logs.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap b/x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap rename to x-pack/plugins/monitoring/public/components/logs/__snapshots__/reason.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/index.js b/x-pack/plugins/monitoring/public/components/logs/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logs/index.js rename to x-pack/plugins/monitoring/public/components/logs/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js b/x-pack/plugins/monitoring/public/components/logs/logs.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/logs/logs.js rename to x-pack/plugins/monitoring/public/components/logs/logs.js index 3590199048352..eaf92f72103e0 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.js @@ -5,17 +5,16 @@ */ import React, { PureComponent } from 'react'; import { capitalize } from 'lodash'; -import chrome from '../../np_imports/ui/chrome'; +import { Legacy } from '../../legacy_shims'; import { EuiBasicTable, EuiTitle, EuiSpacer, EuiText, EuiCallOut, EuiLink } from '@elastic/eui'; import { INFRA_SOURCE_ID } from '../../../common/constants'; import { formatDateTimeLocal } from '../../../common/formatting'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from './reason'; -import { capabilities } from '../../np_imports/ui/capabilities'; const getFormattedDateTimeLocal = timestamp => { - const injector = chrome.dangerouslyGetActiveInjector(); + const injector = Legacy.shims.getAngularInjector(); const timezone = injector.get('config').get('dateFormat:tz'); return formatDateTimeLocal(timestamp, timezone); }; @@ -110,7 +109,7 @@ const clusterColumns = [ ]; function getLogsUiLink(clusterUuid, nodeId, indexUuid) { - const base = `${chrome.getBasePath()}/app/logs/link-to/${INFRA_SOURCE_ID}/logs`; + const base = `${Legacy.shims.getBasePath()}/app/logs/link-to/${INFRA_SOURCE_ID}/logs`; const params = []; if (clusterUuid) { @@ -158,7 +157,7 @@ export class Logs extends PureComponent { } renderCallout() { - const uiCapabilities = capabilities.get(); + const uiCapabilities = Legacy.shims.capabilities.get(); const show = uiCapabilities.logs && uiCapabilities.logs.show; const { logs: { enabled }, diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.test.js b/x-pack/plugins/monitoring/public/components/logs/logs.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/logs/logs.test.js rename to x-pack/plugins/monitoring/public/components/logs/logs.test.js index 63af8b208fbec..c0497ee351e34 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/logs.test.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.test.js @@ -8,21 +8,16 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Logs } from './logs'; -jest.mock('../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - -jest.mock( - '../../np_imports/ui/capabilities', - () => ({ - capabilities: { - get: () => ({ logs: { show: true } }), +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + getBasePath: () => '', + capabilities: { + get: () => ({ logs: { show: true } }), + }, }, - }), - { virtual: true } -); + }, +})); const logs = { enabled: true, diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.js b/x-pack/plugins/monitoring/public/components/logs/reason.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/logs/reason.js rename to x-pack/plugins/monitoring/public/components/logs/reason.js index 91c62ae53a1ca..ad21f7f81d9bd 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.js @@ -8,10 +8,11 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../legacy_shims'; import { Monospace } from '../metricbeat_migration/instruction_steps/components/monospace/monospace'; export const Reason = ({ reason }) => { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; let title = i18n.translate('xpack.monitoring.logs.reason.defaultTitle', { defaultMessage: 'No log data found', }); diff --git a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.test.js b/x-pack/plugins/monitoring/public/components/logs/reason.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/logs/reason.test.js rename to x-pack/plugins/monitoring/public/components/logs/reason.test.js index c8ed05bd73ade..bd56c733268b7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logs/reason.test.js +++ b/x-pack/plugins/monitoring/public/components/logs/reason.test.js @@ -8,9 +8,15 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Reason } from './reason'; -jest.mock('ui/documentation_links', () => ({ - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'current', + }, + }, + }, })); describe('Logs', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/cluster_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/cluster_status/index.js rename to x-pack/plugins/monitoring/public/components/logstash/cluster_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/detail_status/index.js b/x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/detail_status/index.js rename to x-pack/plugins/monitoring/public/components/logstash/detail_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/listing/__snapshots__/listing.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/index.js b/x-pack/plugins/monitoring/public/components/logstash/listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/index.js rename to x-pack/plugins/monitoring/public/components/logstash/listing/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.js rename to x-pack/plugins/monitoring/public/components/logstash/listing/listing.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.test.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/listing/listing.test.js rename to x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/overview/index.js b/x-pack/plugins/monitoring/public/components/logstash/overview/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/overview/index.js rename to x-pack/plugins/monitoring/public/components/logstash/overview/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/overview/overview.js b/x-pack/plugins/monitoring/public/components/logstash/overview/overview.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/overview/overview.js rename to x-pack/plugins/monitoring/public/components/logstash/overview/overview.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js index f8df93d6ee8fb..132cc7ce131cf 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { formatMetric } from '../../../lib/format_number'; import { ClusterStatus } from '../cluster_status'; -import { Sparkline } from 'plugins/monitoring/components/sparkline'; +import { Sparkline } from '../../../components/sparkline'; import { EuiMonitoringSSPTable } from '../../table'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/config.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/__tests__/pipeline_state.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/config.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/boolean_edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/edge_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/if_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/plugin_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/queue_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/__tests__/vertex_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/boolean_edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/edge_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/if_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/plugin_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/queue_vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/graph/vertex_factory.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/else_element.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/flatten_pipeline_section.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/if_element.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/list.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/list/plugin_element.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/if_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/make_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/pipeline.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/plugin_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/queue.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/__tests__/utils.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/if_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/make_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/pipeline.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/plugin_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/queue.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline/utils.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/models/pipeline_state.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/collapsible_statement.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/detail_drawer.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/metric.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/pipeline_viewer.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/plugin_statement.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/queue.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_list_heading.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/__snapshots__/statement_section.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/collapsible_statement.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js index 2a65110f81169..09f4d03953038 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/detail_drawer.test.js @@ -8,6 +8,10 @@ import React from 'react'; import { DetailDrawer } from '../detail_drawer'; import { shallow } from 'enzyme'; +jest.mock('../../../../sparkline', () => ({ + Sparkline: () => 'Sparkline', +})); + describe('DetailDrawer component', () => { let onHide; let timeseriesTooltipXValueFormatter; diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/metric.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js similarity index 94% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js index 5013c38ac1921..8c2558bee4e44 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js +++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/pipeline_viewer.test.js @@ -8,6 +8,10 @@ import React from 'react'; import { PipelineViewer } from '../pipeline_viewer'; import { shallow } from 'enzyme'; +jest.mock('../../../../sparkline', () => ({ + Sparkline: () => 'Sparkline', +})); + describe('PipelineViewer component', () => { let pipeline; let component; diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/plugin_statement.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/queue.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_list_heading.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/__test__/statement_section.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/_pipeline_viewer.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/collapsible_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/detail_drawer.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/metric.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/pipeline_viewer.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/plugin_statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/queue.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_list_heading.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js rename to x-pack/plugins/monitoring/public/components/logstash/pipeline_viewer/views/statement_section.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/constants.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/constants.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/constants.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/constants.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/__snapshots__/flyout.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js index 0a284c102dc9d..42d0eec7cbed0 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.js @@ -25,7 +25,7 @@ import { EuiCheckbox, } from '@elastic/eui'; import { getInstructionSteps } from '../instruction_steps'; -import { Storage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { STORAGE_KEY, ELASTICSEARCH_SYSTEM_ID, @@ -38,7 +38,7 @@ import { INSTRUCTION_STEP_ENABLE_METRICBEAT, INSTRUCTION_STEP_DISABLE_INTERNAL, } from '../constants'; -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Legacy } from '../../../legacy_shims'; import { getIdentifier, formatProductName } from '../../setup_mode/formatting'; const storage = new Storage(window.localStorage); @@ -223,6 +223,7 @@ export class Flyout extends Component { getDocumentationTitle() { const { productName } = this.props; + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; let documentationUrl = null; if (productName === KIBANA_SYSTEM_ID) { diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js index 3587381f977cb..61cfb491f0bd0 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/flyout.test.js @@ -16,9 +16,16 @@ import { LOGSTASH_SYSTEM_ID, } from '../../../../common/constants'; -jest.mock('ui/documentation_links', () => ({ - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - DOC_LINK_VERSION: 'current', +jest.mock('../../../legacy_shims', () => ({ + Legacy: { + shims: { + kfetch: jest.fn(), + docLinks: { + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', + DOC_LINK_VERSION: 'current', + }, + }, + }, })); jest.mock('../../../../common', () => ({ diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/flyout/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/disable_internal_collection_instructions.js diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js new file mode 100644 index 0000000000000..51a7a139aafd0 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/enable_metricbeat_instructions.js @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; +import { Monospace } from '../components/monospace'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Legacy } from '../../../../legacy_shims'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; + +export function getApmInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-configuration.html` + ); + + const installMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatTitle', + { + defaultMessage: 'Install Metricbeat on the same server as the APM server', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.apmInstructions.installMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const enableMetricbeatModuleStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleTitle', + { + defaultMessage: 'Enable and configure the Beat x-pack module in Metricbeat', + } + ), + children: ( + <Fragment> + <EuiCodeBlock isCopyable language="bash"> + metricbeat modules enable beat-xpack + </EuiCodeBlock> + <EuiSpacer size="s" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.apmInstructions.enableMetricbeatModuleDescription" + defaultMessage="By default the module will collect APM server monitoring metrics from http://localhost:5066. If the local APM server has a different address, you must specify it via the {hosts} setting in the {file} file." + values={{ + hosts: <Monospace>hosts</Monospace>, + file: <Monospace>modules.d/beat-xpack.yml</Monospace>, + }} + /> + </p> + </EuiText> + {securitySetup} + </Fragment> + ), + }; + + const configureMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatTitle', + { + defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', + } + ), + children: ( + <Fragment> + <EuiText> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.apmInstructions.configureMetricbeatDescription" + defaultMessage="Make these changes in your {file}." + values={{ + file: <Monospace>metricbeat.yml</Monospace>, + }} + /> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock isCopyable> + {`output.elasticsearch: + hosts: [${esMonitoringUrl}] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" +`} + </EuiCodeBlock> + {securitySetup} + </Fragment> + ), + }; + + const startMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatTitle', + { + defaultMessage: 'Start Metricbeat', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.apmInstructions.startMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const migrationStatusStep = getMigrationStatusStep(product); + + return [ + installMetricbeatStep, + enableMetricbeatModuleStep, + configureMetricbeatStep, + startMetricbeatStep, + migrationStatusStep, + ]; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/apm/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/common_beats_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/disable_internal_collection_instructions.js diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js new file mode 100644 index 0000000000000..ddaa610b874a8 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/enable_metricbeat_instructions.js @@ -0,0 +1,180 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiCodeBlock, EuiLink, EuiCallOut, EuiText } from '@elastic/eui'; +import { Monospace } from '../components/monospace'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { UNDETECTED_BEAT_TYPE, DEFAULT_BEAT_FOR_URLS } from './common_beats_instructions'; +import { Legacy } from '../../../../legacy_shims'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; + +export function getBeatsInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const beatType = product.beatType; + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-configuration.html` + ); + + const installMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatTitle', + { + defaultMessage: 'Install Metricbeat on the same server as this {beatType}', + values: { + beatType: beatType || UNDETECTED_BEAT_TYPE, + }, + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.beatsInstructions.installMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const httpEndpointUrl = + `${ELASTIC_WEBSITE_URL}guide/en/beats/${beatType || DEFAULT_BEAT_FOR_URLS}` + + `/${DOC_LINK_VERSION}/http-endpoint.html`; + + const enableMetricbeatModuleStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleTitle', + { + defaultMessage: 'Enable and configure the Beat x-pack module in Metricbeat', + } + ), + children: ( + <Fragment> + <EuiCodeBlock isCopyable language="bash"> + metricbeat modules enable beat-xpack + </EuiCodeBlock> + <EuiSpacer size="s" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleDescription" + defaultMessage="By default the module will collect {beatType} monitoring metrics from http://localhost:5066. If the {beatType} instance being monitored has a different address, you must specify it via the {hosts} setting in the {file} file." + values={{ + hosts: <Monospace>hosts</Monospace>, + file: <Monospace>modules.d/beat-xpack.yml</Monospace>, + beatType: beatType || UNDETECTED_BEAT_TYPE, + }} + /> + </p> + </EuiText> + <EuiSpacer size="m" /> + <EuiCallOut + color="warning" + iconType="help" + title={ + <EuiText> + <p> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirections" + defaultMessage="In order for Metricbeat to collect metrics from the running {beatType}, you need to {link}." + values={{ + link: ( + <EuiLink href={httpEndpointUrl} target="_blank"> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.beatsInstructions.enableMetricbeatModuleHttpEnabledDirectionsLinkText" + defaultMessage="enable an HTTP endpoint for the {beatType} instance being monitored" + values={{ + beatType, + }} + /> + </EuiLink> + ), + beatType: beatType || UNDETECTED_BEAT_TYPE, + }} + /> + </p> + </EuiText> + } + /> + {securitySetup} + </Fragment> + ), + }; + + const configureMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatTitle', + { + defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', + } + ), + children: ( + <Fragment> + <EuiText> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.beatsInstructions.configureMetricbeatDescription" + defaultMessage="Make these changes in your {file}." + values={{ + file: <Monospace>metricbeat.yml</Monospace>, + }} + /> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock isCopyable> + {`output.elasticsearch: + hosts: [${esMonitoringUrl}] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" +`} + </EuiCodeBlock> + {securitySetup} + </Fragment> + ), + }; + + const startMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatTitle', + { + defaultMessage: 'Start Metricbeat', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.beatsInstructions.startMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const migrationStatusStep = getMigrationStatusStep(product); + + return [ + installMetricbeatStep, + enableMetricbeatModuleStep, + configureMetricbeatStep, + startMetricbeatStep, + migrationStatusStep, + ]; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/beats/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/common_instructions.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/components/monospace/monospace.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/disable_internal_collection_instructions.js diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js new file mode 100644 index 0000000000000..8607739e7090c --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/enable_metricbeat_instructions.js @@ -0,0 +1,157 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; +import { Monospace } from '../components/monospace'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Legacy } from '../../../../legacy_shims'; +import { getSecurityStep, getMigrationStatusStep } from '../common_instructions'; + +export function getElasticsearchInstructionsForEnablingMetricbeat( + product, + _meta, + { esMonitoringUrl } +) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/configuring-metricbeat.html` + ); + + const installMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatTitle', + { + defaultMessage: 'Install Metricbeat on the same server as Elasticsearch', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.installMetricbeatLinkText" + defaultMessage="Follow these instructions." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const enableMetricbeatModuleStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleTitle', + { + defaultMessage: 'Enable and configure the Elasticsearch x-pack module in Metricbeat', + } + ), + children: ( + <Fragment> + <EuiText> + <p> + {i18n.translate( + 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleInstallDirectory', + { + defaultMessage: 'From the installation directory, run:', + } + )} + </p> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock isCopyable language="bash"> + metricbeat modules enable elasticsearch-xpack + </EuiCodeBlock> + <EuiSpacer size="s" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.enableMetricbeatModuleDescription" + defaultMessage="By default the module collects Elasticsearch metrics from {url}. + If the local server has a different address, add it to the hosts setting in {module}." + values={{ + module: <Monospace>modules.d/elasticsearch-xpack.yml</Monospace>, + url: <Monospace>http://localhost:9200</Monospace>, + }} + /> + </p> + </EuiText> + {securitySetup} + </Fragment> + ), + }; + + const configureMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatTitle', + { + defaultMessage: 'Configure Metricbeat to send data to the monitoring cluster', + } + ), + children: ( + <Fragment> + <EuiText> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.configureMetricbeatDescription" + defaultMessage="Modify {file} to set the connection information." + values={{ + file: <Monospace>metricbeat.yml</Monospace>, + }} + /> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock isCopyable> + {`output.elasticsearch: + hosts: [${esMonitoringUrl}] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" +`} + </EuiCodeBlock> + {securitySetup} + </Fragment> + ), + }; + + const startMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatTitle', + { + defaultMessage: 'Start Metricbeat', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.elasticsearchInstructions.startMetricbeatLinkText" + defaultMessage="Follow these instructions." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const migrationStatusStep = getMigrationStatusStep(product); + + return [ + installMetricbeatStep, + enableMetricbeatModuleStep, + configureMetricbeatStep, + startMetricbeatStep, + migrationStatusStep, + ]; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/elasticsearch/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/get_instruction_steps.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/disable_internal_collection_instructions.js diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js new file mode 100644 index 0000000000000..eb1aedd47c568 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/enable_metricbeat_instructions.js @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; +import { Monospace } from '../components/monospace'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Legacy } from '../../../../legacy_shims'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; + +export function getKibanaInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html` + ); + + const installMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatTitle', + { + defaultMessage: 'Install Metricbeat on the same server as Kibana', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.kibanaInstructions.installMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const enableMetricbeatModuleStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleTitle', + { + defaultMessage: 'Enable and configure the Kibana x-pack module in Metricbeat', + } + ), + children: ( + <Fragment> + <EuiCodeBlock isCopyable language="bash"> + metricbeat modules enable kibana-xpack + </EuiCodeBlock> + <EuiSpacer size="s" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.kibanaInstructions.enableMetricbeatModuleDescription" + defaultMessage="By default the module will collect Kibana monitoring metrics from http://localhost:5601. If the local Kibana instance has a different address, you must specify it via the {hosts} setting in the {file} file." + values={{ + hosts: <Monospace>hosts</Monospace>, + file: <Monospace>modules.d/kibana-xpack.yml</Monospace>, + }} + /> + </p> + </EuiText> + {securitySetup} + </Fragment> + ), + }; + + const configureMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatTitle', + { + defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', + } + ), + children: ( + <Fragment> + <EuiText> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.kibanaInstructions.configureMetricbeatDescription" + defaultMessage="Make these changes in your {file}." + values={{ + file: <Monospace>metricbeat.yml</Monospace>, + }} + /> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock isCopyable> + {`output.elasticsearch: + hosts: [${esMonitoringUrl}] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" +`} + </EuiCodeBlock> + {securitySetup} + </Fragment> + ), + }; + + const startMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatTitle', + { + defaultMessage: 'Start Metricbeat', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.kibanaInstructions.startMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const migrationStatusStep = getMigrationStatusStep(product); + + return [ + installMetricbeatStep, + enableMetricbeatModuleStep, + configureMetricbeatStep, + startMetricbeatStep, + migrationStatusStep, + ]; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/kibana/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/disable_internal_collection_instructions.js diff --git a/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js new file mode 100644 index 0000000000000..ce542fa8a05e5 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/enable_metricbeat_instructions.js @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { Fragment } from 'react'; +import { EuiSpacer, EuiCodeBlock, EuiLink, EuiText } from '@elastic/eui'; +import { Monospace } from '../components/monospace'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Legacy } from '../../../../legacy_shims'; +import { getMigrationStatusStep, getSecurityStep } from '../common_instructions'; + +export function getLogstashInstructionsForEnablingMetricbeat(product, _meta, { esMonitoringUrl }) { + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; + const securitySetup = getSecurityStep( + `${ELASTIC_WEBSITE_URL}guide/en/logstash/${DOC_LINK_VERSION}/monitoring-with-metricbeat.html` + ); + + const installMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatTitle', + { + defaultMessage: 'Install Metricbeat on the same server as Logstash', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-installation.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.logstashInstructions.installMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const enableMetricbeatModuleStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleTitle', + { + defaultMessage: 'Enable and configure the Logstash x-pack module in Metricbeat', + } + ), + children: ( + <Fragment> + <EuiCodeBlock isCopyable language="bash"> + metricbeat modules enable logstash-xpack + </EuiCodeBlock> + <EuiSpacer size="s" /> + <EuiText> + <p> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.logstashInstructions.enableMetricbeatModuleDescription" + defaultMessage="By default the module will collect Logstash monitoring metrics from http://localhost:9600. If the local Logstash instance has a different address, you must specify it via the {hosts} setting in the {file} file." + values={{ + hosts: <Monospace>hosts</Monospace>, + file: <Monospace>modules.d/logstash-xpack.yml</Monospace>, + }} + /> + </p> + </EuiText> + {securitySetup} + </Fragment> + ), + }; + + const configureMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatTitle', + { + defaultMessage: 'Configure Metricbeat to send to the monitoring cluster', + } + ), + children: ( + <Fragment> + <EuiText> + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.logstashInstructions.configureMetricbeatDescription" + defaultMessage="Make these changes in your {file}." + values={{ + file: <Monospace>metricbeat.yml</Monospace>, + }} + /> + </EuiText> + <EuiSpacer size="s" /> + <EuiCodeBlock isCopyable> + {`output.elasticsearch: + hosts: [${esMonitoringUrl}] ## Monitoring cluster + + # Optional protocol and basic auth credentials. + #protocol: "https" + #username: "elastic" + #password: "changeme" +`} + </EuiCodeBlock> + {securitySetup} + </Fragment> + ), + }; + + const startMetricbeatStep = { + title: i18n.translate( + 'xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatTitle', + { + defaultMessage: 'Start Metricbeat', + } + ), + children: ( + <EuiText> + <p> + <EuiLink + href={`${ELASTIC_WEBSITE_URL}guide/en/beats/metricbeat/${DOC_LINK_VERSION}/metricbeat-starting.html`} + target="_blank" + > + <FormattedMessage + id="xpack.monitoring.metricbeatMigration.logstashInstructions.startMetricbeatLinkText" + defaultMessage="Follow the instructions here." + /> + </EuiLink> + </p> + </EuiText> + ), + }; + + const migrationStatusStep = getMigrationStatusStep(product); + + return [ + installMetricbeatStep, + enableMetricbeatModuleStep, + configureMetricbeatStep, + startMetricbeatStep, + migrationStatusStep, + ]; +} diff --git a/x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js b/x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js rename to x-pack/plugins/monitoring/public/components/metricbeat_migration/instruction_steps/logstash/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/checker_errors.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/__snapshots__/no_data.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js b/x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js index 8462d2a6fc87b..f3cd5ccb9703d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/__tests__/checker_errors.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { boomify, forbidden } from 'boom'; -import { renderWithIntl } from '../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { CheckerErrors } from '../checker_errors'; describe('CheckerErrors', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js b/x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js similarity index 84% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js rename to x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js index 81a412a680bc6..5b54df3a82812 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/__tests__/no_data.test.js @@ -5,17 +5,11 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { NoData } from '../'; const enabler = {}; -jest.mock('../../../np_imports/ui/chrome', () => { - return { - getBasePath: () => '', - }; -}); - describe('NoData', () => { test('should show text next to the spinner while checking a setting', () => { const component = renderWithIntl( diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/_index.scss b/x-pack/plugins/monitoring/public/components/no_data/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/_index.scss rename to x-pack/plugins/monitoring/public/components/no_data/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/_no_data.scss b/x-pack/plugins/monitoring/public/components/no_data/_no_data.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/_no_data.scss rename to x-pack/plugins/monitoring/public/components/no_data/_no_data.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/changes_needed.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/cloud_deployment.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/index.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/index.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/looking_for.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/looking_for.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/looking_for.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/looking_for.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/what_is.js b/x-pack/plugins/monitoring/public/components/no_data/blurbs/what_is.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/blurbs/what_is.js rename to x-pack/plugins/monitoring/public/components/no_data/blurbs/what_is.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/checker_errors.js b/x-pack/plugins/monitoring/public/components/no_data/checker_errors.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/checker_errors.js rename to x-pack/plugins/monitoring/public/components/no_data/checker_errors.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/checking_settings.js b/x-pack/plugins/monitoring/public/components/no_data/checking_settings.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/checking_settings.js rename to x-pack/plugins/monitoring/public/components/no_data/checking_settings.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap index 43ff42b6a80e8..fa266b30fcb94 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/__snapshots__/collection_enabled.test.js.snap @@ -192,6 +192,7 @@ exports[`ExplainCollectionEnabled should explain about xpack.monitoring.collecti isCopyable={false} paddingSize="l" transparentBackground={false} + whiteSpace="pre-wrap" > <Portal containerInfo={ @@ -220,6 +221,7 @@ exports[`ExplainCollectionEnabled should explain about xpack.monitoring.collecti isCopyable={false} paddingSize="l" transparentBackground={false} + whiteSpace="pre-wrap" > <Portal containerInfo={ diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js index d2be217ca8498..13a49b1c7b200 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/__tests__/collection_enabled.test.js @@ -6,7 +6,7 @@ import React from 'react'; import sinon from 'sinon'; -import { mountWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainCollectionEnabled } from '../collection_enabled'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_enabled/collection_enabled.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap similarity index 99% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap index 3cf35609acd07..8b3675bf32753 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/__snapshots__/collection_interval.test.js.snap @@ -365,6 +365,7 @@ exports[`ExplainCollectionInterval collection interval setting updates should sh isCopyable={false} paddingSize="l" transparentBackground={false} + whiteSpace="pre-wrap" > <Portal containerInfo={ @@ -393,6 +394,7 @@ exports[`ExplainCollectionInterval collection interval setting updates should sh isCopyable={false} paddingSize="l" transparentBackground={false} + whiteSpace="pre-wrap" > <Portal containerInfo={ @@ -695,6 +697,7 @@ exports[`ExplainCollectionInterval should explain about xpack.monitoring.collect isCopyable={false} paddingSize="l" transparentBackground={false} + whiteSpace="pre-wrap" > <Portal containerInfo={ @@ -723,6 +726,7 @@ exports[`ExplainCollectionInterval should explain about xpack.monitoring.collect isCopyable={false} paddingSize="l" transparentBackground={false} + whiteSpace="pre-wrap" > <Portal containerInfo={ diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js index 0a50ad44c8b4f..88185283b6faa 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/__tests__/collection_interval.test.js @@ -6,7 +6,7 @@ import React from 'react'; import sinon from 'sinon'; -import { mountWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainCollectionInterval } from '../collection_interval'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/collection_interval/collection_interval.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/__snapshots__/exporters.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js similarity index 93% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js index c9147037f0022..88f1eedfc3bcc 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/__tests__/exporters.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainExporters, ExplainExportersCloud } from '../exporters'; // Mocking to prevent errors with React portal. diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/exporters/exporters.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/index.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/index.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/__snapshots__/plugin_enabled.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js similarity index 92% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js index 56536a8e4270b..ece58ad6aeed2 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/__tests__/plugin_enabled.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { ExplainPluginEnabled } from '../plugin_enabled'; // Mocking to prevent errors with React portal. diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js b/x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js rename to x-pack/plugins/monitoring/public/components/no_data/explanations/plugin_enabled/plugin_enabled.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/index.js b/x-pack/plugins/monitoring/public/components/no_data/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/index.js rename to x-pack/plugins/monitoring/public/components/no_data/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js b/x-pack/plugins/monitoring/public/components/no_data/no_data.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/no_data.js rename to x-pack/plugins/monitoring/public/components/no_data/no_data.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/reason_found.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/__snapshots__/we_tried.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js similarity index 96% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js index e9b2ff11538ab..7f15cacb1ebb9 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/reason_found.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { ReasonFound } from '../'; // Mocking to prevent errors with React portal. diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js similarity index 85% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js index e382a1c9ea8db..95970453d4b7c 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__tests__/we_tried.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { WeTried } from '../'; describe('WeTried', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/index.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/index.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/reason_found.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/reason_found.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/reason_found.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/reason_found.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/we_tried.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/no_data/reasons/we_tried.js rename to x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap rename to x-pack/plugins/monitoring/public/components/page_loading/__tests__/__snapshots__/page_loading.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js similarity index 85% rename from x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js rename to x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js index b1ad00ffcc3c6..41f3ef4be969e 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js +++ b/x-pack/plugins/monitoring/public/components/page_loading/__tests__/page_loading.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { PageLoading } from '../'; describe('PageLoading', () => { diff --git a/x-pack/legacy/plugins/monitoring/public/components/page_loading/index.js b/x-pack/plugins/monitoring/public/components/page_loading/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/page_loading/index.js rename to x-pack/plugins/monitoring/public/components/page_loading/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap b/x-pack/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap rename to x-pack/plugins/monitoring/public/components/renderers/__snapshots__/setup_mode.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/index.js b/x-pack/plugins/monitoring/public/components/renderers/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/index.js rename to x-pack/plugins/monitoring/public/components/renderers/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js b/x-pack/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js rename to x-pack/plugins/monitoring/public/components/renderers/lib/find_new_uuid.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.js rename to x-pack/plugins/monitoring/public/components/renderers/setup_mode.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.test.js b/x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/renderers/setup_mode.test.js rename to x-pack/plugins/monitoring/public/components/renderers/setup_mode.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/badge.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/enter_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/formatting.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/listing_callout.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap rename to x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss b/x-pack/plugins/monitoring/public/components/setup_mode/_enter_button.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/_enter_button.scss rename to x-pack/plugins/monitoring/public/components/setup_mode/_enter_button.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss b/x-pack/plugins/monitoring/public/components/setup_mode/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/_index.scss rename to x-pack/plugins/monitoring/public/components/setup_mode/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js b/x-pack/plugins/monitoring/public/components/setup_mode/badge.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.js rename to x-pack/plugins/monitoring/public/components/setup_mode/badge.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/badge.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/badge.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/badge.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx b/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx rename to x-pack/plugins/monitoring/public/components/setup_mode/enter_button.test.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx b/x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/enter_button.tsx rename to x-pack/plugins/monitoring/public/components/setup_mode/enter_button.tsx diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.js rename to x-pack/plugins/monitoring/public/components/setup_mode/formatting.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/formatting.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/formatting.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js b/x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.js rename to x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/listing_callout.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/listing_callout.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js b/x-pack/plugins/monitoring/public/components/setup_mode/tooltip.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.js rename to x-pack/plugins/monitoring/public/components/setup_mode/tooltip.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.test.js b/x-pack/plugins/monitoring/public/components/setup_mode/tooltip.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/setup_mode/tooltip.test.js rename to x-pack/plugins/monitoring/public/components/setup_mode/tooltip.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js b/x-pack/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js rename to x-pack/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap b/x-pack/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap rename to x-pack/plugins/monitoring/public/components/sparkline/__test__/__snapshots__/index.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/index.test.js b/x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/index.test.js rename to x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js index 22f4787593fec..ab2cf10b4615d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/sparkline/__test__/index.test.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/__test__/index.test.js @@ -9,6 +9,10 @@ import renderer from 'react-test-renderer'; import { shallow } from 'enzyme'; import { Sparkline } from '../'; +jest.mock('../sparkline_flot_chart', () => ({ + SparklineFlotChart: () => 'SparklineFlotChart', +})); + describe('Sparkline component', () => { let component; let renderedComponent; diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/_index.scss b/x-pack/plugins/monitoring/public/components/sparkline/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/_index.scss rename to x-pack/plugins/monitoring/public/components/sparkline/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/_sparkline.scss b/x-pack/plugins/monitoring/public/components/sparkline/_sparkline.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/_sparkline.scss rename to x-pack/plugins/monitoring/public/components/sparkline/_sparkline.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js b/x-pack/plugins/monitoring/public/components/sparkline/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/index.js rename to x-pack/plugins/monitoring/public/components/sparkline/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js b/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js similarity index 98% rename from x-pack/legacy/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js rename to x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js index bb17f464a155a..82d5f53f9fbd7 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js +++ b/x-pack/plugins/monitoring/public/components/sparkline/sparkline_flot_chart.js @@ -5,7 +5,7 @@ */ import { last, isFunction, debounce } from 'lodash'; -import $ from 'plugins/xpack_main/jquery_flot'; +import $ from '../../lib/jquery_flot'; import { DEBOUNCE_FAST_MS } from '../../../common/constants'; /** diff --git a/x-pack/legacy/plugins/monitoring/public/components/status_icon/_index.scss b/x-pack/plugins/monitoring/public/components/status_icon/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/status_icon/_index.scss rename to x-pack/plugins/monitoring/public/components/status_icon/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/status_icon/_status_icon.scss b/x-pack/plugins/monitoring/public/components/status_icon/_status_icon.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/status_icon/_status_icon.scss rename to x-pack/plugins/monitoring/public/components/status_icon/_status_icon.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js b/x-pack/plugins/monitoring/public/components/status_icon/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/status_icon/index.js rename to x-pack/plugins/monitoring/public/components/status_icon/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap b/x-pack/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap rename to x-pack/plugins/monitoring/public/components/summary_status/__snapshots__/summary_status.test.js.snap diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/_index.scss b/x-pack/plugins/monitoring/public/components/summary_status/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/_index.scss rename to x-pack/plugins/monitoring/public/components/summary_status/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/_summary_status.scss b/x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/_summary_status.scss rename to x-pack/plugins/monitoring/public/components/summary_status/_summary_status.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/index.js b/x-pack/plugins/monitoring/public/components/summary_status/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/index.js rename to x-pack/plugins/monitoring/public/components/summary_status/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.js rename to x-pack/plugins/monitoring/public/components/summary_status/summary_status.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.test.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.test.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.test.js rename to x-pack/plugins/monitoring/public/components/summary_status/summary_status.test.js index fe709af266349..5f4dced47ee6f 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/summary_status/summary_status.test.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.test.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { renderWithIntl } from '../../../../../../test_utils/enzyme_helpers'; +import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { SummaryStatus } from './summary_status'; jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/_index.scss b/x-pack/plugins/monitoring/public/components/table/_index.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/_index.scss rename to x-pack/plugins/monitoring/public/components/table/_index.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/_table.scss b/x-pack/plugins/monitoring/public/components/table/_table.scss similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/_table.scss rename to x-pack/plugins/monitoring/public/components/table/_table.scss diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/eui_table.js rename to x-pack/plugins/monitoring/public/components/table/eui_table.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/eui_table_ssp.js b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/eui_table_ssp.js rename to x-pack/plugins/monitoring/public/components/table/eui_table_ssp.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/index.js b/x-pack/plugins/monitoring/public/components/table/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/index.js rename to x-pack/plugins/monitoring/public/components/table/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/components/table/storage.js b/x-pack/plugins/monitoring/public/components/table/storage.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/components/table/storage.js rename to x-pack/plugins/monitoring/public/components/table/storage.js diff --git a/x-pack/plugins/monitoring/public/directives/beats/beat/index.js b/x-pack/plugins/monitoring/public/directives/beats/beat/index.js new file mode 100644 index 0000000000000..103cac98ba564 --- /dev/null +++ b/x-pack/plugins/monitoring/public/directives/beats/beat/index.js @@ -0,0 +1,36 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import { Beat } from '../../../components/beats/beat'; + +//monitoringBeatsBeat +export function monitoringBeatsBeatProvider() { + return { + restrict: 'E', + scope: { + data: '=', + onBrush: '<', + zoomInfo: '<', + }, + link(scope, $el) { + scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); + + scope.$watch('data', (data = {}) => { + render( + <Beat + summary={data.summary} + metrics={data.metrics} + onBrush={scope.onBrush} + zoomInfo={scope.zoomInfo} + />, + $el[0] + ); + }); + }, + }; +} diff --git a/x-pack/plugins/monitoring/public/directives/beats/overview/index.js b/x-pack/plugins/monitoring/public/directives/beats/overview/index.js new file mode 100644 index 0000000000000..4faf69e13d02c --- /dev/null +++ b/x-pack/plugins/monitoring/public/directives/beats/overview/index.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { BeatsOverview } from '../../../components/beats/overview'; + +export function monitoringBeatsOverviewProvider() { + return { + restrict: 'E', + scope: { + data: '=', + onBrush: '<', + zoomInfo: '<', + }, + link(scope, $el) { + scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); + + scope.$watch('data', (data = {}) => { + render( + <BeatsOverview {...data} onBrush={scope.onBrush} zoomInfo={scope.zoomInfo} />, + $el[0] + ); + }); + }, + }; +} diff --git a/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js new file mode 100644 index 0000000000000..706d1ac4c0e33 --- /dev/null +++ b/x-pack/plugins/monitoring/public/directives/elasticsearch/ml_job_listing/index.js @@ -0,0 +1,163 @@ +/* + * 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 { capitalize } from 'lodash'; +import numeral from '@elastic/numeral'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { EuiMonitoringTable } from '../../../components/table'; +import { MachineLearningJobStatusIcon } from '../../../components/elasticsearch/ml_job_listing/status_icon'; +import { LARGE_ABBREVIATED, LARGE_BYTES } from '../../../../common/formatting'; +import { EuiLink, EuiPage, EuiPageContent, EuiPageBody, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { ClusterStatus } from '../../../components/elasticsearch/cluster_status'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +const getColumns = (kbnUrl, scope) => [ + { + name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.jobIdTitle', { + defaultMessage: 'Job ID', + }), + field: 'job_id', + sortable: true, + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.stateTitle', { + defaultMessage: 'State', + }), + field: 'state', + sortable: true, + render: state => ( + <div> + <MachineLearningJobStatusIcon status={state} /> +   + {capitalize(state)} + </div> + ), + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.processedRecordsTitle', { + defaultMessage: 'Processed Records', + }), + field: 'data_counts.processed_record_count', + sortable: true, + render: value => <span>{numeral(value).format(LARGE_ABBREVIATED)}</span>, + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.modelSizeTitle', { + defaultMessage: 'Model Size', + }), + field: 'model_size_stats.model_bytes', + sortable: true, + render: value => <span>{numeral(value).format(LARGE_BYTES)}</span>, + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.forecastsTitle', { + defaultMessage: 'Forecasts', + }), + field: 'forecasts_stats.total', + sortable: true, + render: value => <span>{numeral(value).format(LARGE_ABBREVIATED)}</span>, + }, + { + name: i18n.translate('xpack.monitoring.elasticsearch.mlJobListing.nodeTitle', { + defaultMessage: 'Node', + }), + field: 'node.name', + sortable: true, + render: (name, node) => { + if (node) { + return ( + <EuiLink + onClick={() => { + scope.$evalAsync(() => kbnUrl.changePath(`/elasticsearch/nodes/${node.id}`)); + }} + > + {name} + </EuiLink> + ); + } + + return ( + <FormattedMessage + id="xpack.monitoring.elasticsearch.mlJobListing.noDataLabel" + defaultMessage="N/A" + /> + ); + }, + }, +]; + +//monitoringMlListing +export function monitoringMlListingProvider(kbnUrl) { + return { + restrict: 'E', + scope: { + jobs: '=', + paginationSettings: '=', + sorting: '=', + onTableChange: '=', + status: '=', + }, + link(scope, $el) { + scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); + const columns = getColumns(kbnUrl, scope); + + const filterJobsPlaceholder = i18n.translate( + 'xpack.monitoring.elasticsearch.mlJobListing.filterJobsPlaceholder', + { + defaultMessage: 'Filter Jobs…', + } + ); + + scope.$watch('jobs', (jobs = []) => { + const mlTable = ( + <EuiPage> + <EuiPageBody> + <EuiPanel> + <ClusterStatus stats={scope.status} /> + </EuiPanel> + <EuiSpacer size="m" /> + <EuiPageContent> + <EuiMonitoringTable + className="mlJobsTable" + rows={jobs} + columns={columns} + sorting={{ + ...scope.sorting, + sort: { + ...scope.sorting.sort, + field: 'job_id', + }, + }} + pagination={scope.paginationSettings} + message={i18n.translate( + 'xpack.monitoring.elasticsearch.mlJobListing.noJobsDescription', + { + defaultMessage: + 'There are no Machine Learning Jobs that match your query. Try changing the time range selection.', + } + )} + search={{ + box: { + incremental: true, + placeholder: filterJobsPlaceholder, + }, + }} + onTableChange={scope.onTableChange} + executeQueryOptions={{ + defaultFields: ['job_id'], + }} + /> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + render(mlTable, $el[0]); + }); + }, + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js b/x-pack/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js rename to x-pack/plugins/monitoring/public/directives/main/__tests__/monitoring_main_controller.js diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html new file mode 100644 index 0000000000000..f5c4bf5c757af --- /dev/null +++ b/x-pack/plugins/monitoring/public/directives/main/index.html @@ -0,0 +1,317 @@ +<div class="app-container"> + <div id="setupModeNav"></div> + <kbn-top-nav + name="monitoringMain.navName" + app-name="'monitoring'" + show-search-bar="true" + show-auto-refresh-only="!monitoringMain.datePicker.enableTimeFilter" + show-date-picker="monitoringMain.datePicker.enableTimeFilter" + date-range-from="monitoringMain.datePicker.timeRange.from" + date-range-to="monitoringMain.datePicker.timeRange.to" + is-refresh-paused="monitoringMain.datePicker.refreshInterval.pause" + refresh-interval="monitoringMain.datePicker.refreshInterval.value" + on-refresh-change="monitoringMain.datePicker.onRefreshChange" + on-query-submit="monitoringMain.datePicker.onTimeUpdate" + > + </kbn-top-nav> + <div> + <div ng-if="monitoringMain.inElasticsearch" class="euiTabs" role="navigation"> + <a + ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')" + kbn-href="#/elasticsearch" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.esNavigation.overviewLinkText" + i18n-default-message="Overview" + ></a> + <a + ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')" + kbn-href="" + class="euiTab euiTab-isDisabled" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.esNavigation.overviewLinkText" + i18n-default-message="Overview" + ></a> + <a + ng-if="!monitoringMain.instance" + kbn-href="#/elasticsearch/nodes" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('nodes')}" + i18n-id="xpack.monitoring.esNavigation.nodesLinkText" + i18n-default-message="Nodes" + ></a> + <a + ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('elasticsearch')" + kbn-href="#/elasticsearch/indices" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}" + i18n-id="xpack.monitoring.esNavigation.indicesLinkText" + i18n-default-message="Indices" + ></a> + <a + ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('elasticsearch')" + kbn-href="" + class="euiTab euiTab-isDisabled" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('indices')}" + i18n-id="xpack.monitoring.esNavigation.indicesLinkText" + i18n-default-message="Indices" + ></a> + <a + ng-if="!monitoringMain.instance && monitoringMain.isMlSupported()" + kbn-href="#/elasticsearch/ml_jobs" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('ml')}" + i18n-id="xpack.monitoring.esNavigation.jobsLinkText" + i18n-default-message="Jobs" + ></a> + <a + ng-if="(monitoringMain.isCcrEnabled || monitoringMain.isActiveTab('ccr')) && !monitoringMain.instance" + kbn-href="#/elasticsearch/ccr" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('ccr')}" + i18n-id="xpack.monitoring.esNavigation.ccrLinkText" + i18n-default-message="CCR" + ></a> + <a + ng-if="monitoringMain.instance && (monitoringMain.name === 'nodes' || monitoringMain.name === 'indices')" + kbn-href="#/elasticsearch/{{ monitoringMain.name }}/{{ monitoringMain.resolver }}" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}" + > + <span + ng-if="monitoringMain.tabIconClass" + class="fa {{ monitoringMain.tabIconClass }}" + title="{{ monitoringMain.tabIconLabel }}" + ></span> + <span + i18n-id="xpack.monitoring.esNavigation.instance.overviewLinkText" + i18n-default-message="Overview" + ></span> + </a> + <a + ng-if="monitoringMain.instance && (monitoringMain.name === 'nodes' || monitoringMain.name === 'indices')" + kbn-href="#/elasticsearch/{{ monitoringMain.name }}/{{ monitoringMain.resolver }}/advanced" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.page === 'advanced'}" + i18n-id="xpack.monitoring.esNavigation.instance.advancedLinkText" + i18n-default-message="Advanced" + > + </a> + <!-- ML Instance (for use later) --> + <a + ng-if="monitoringMain.instance && monitoringMain.name !== 'nodes' && monitoringMain.name !== 'indices'" + class="euiTab" + >{{ monitoringMain.instance }}</a + > + </div> + + <div ng-if="monitoringMain.inKibana" class="euiTabs" role="navigation"> + <a + ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('kibana')" + kbn-href="#/kibana" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.kibanaNavigation.overviewLinkText" + i18n-default-message="Overview" + ></a> + <a + ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('kibana')" + kbn-href="" + class="euiTab euiTab-isDisabled" + ng-class="{ + 'euiTab-isSelected': monitoringMain.isActiveTab('overview'), + }" + i18n-id="xpack.monitoring.kibanaNavigation.overviewLinkText" + i18n-default-message="Overview" + ></a> + <a + ng-if="!monitoringMain.instance" + kbn-href="#/kibana/instances" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('kibanas')}" + i18n-id="xpack.monitoring.kibanaNavigation.instancesLinkText" + i18n-default-message="Instances" + ></a> + <a ng-if="monitoringMain.instance" class="euiTab">{{ monitoringMain.instance }}</a> + </div> + + <div ng-if="monitoringMain.inApm" class="euiTabs" role="navigation"> + <a + ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('apm')" + kbn-href="#/apm" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.apmNavigation.overviewLinkText" + i18n-default-message="Overview" + ></a> + <a + ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('apm')" + kbn-href="" + class="euiTab euiTab-isDisabled" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.apmNavigation.overviewLinkText" + i18n-default-message="Overview" + ></a> + <a + ng-if="!monitoringMain.instance" + kbn-href="#/apm/instances" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('apms')}" + i18n-id="xpack.monitoring.apmNavigation.instancesLinkText" + i18n-default-message="Instances" + ></a> + <a ng-if="monitoringMain.instance" class="euiTab">{{ monitoringMain.instance }}</a> + </div> + + <div ng-if="monitoringMain.inBeats" class="euiTabs" role="navigation"> + <a + ng-if="!monitoringMain.instance && !monitoringMain.isDisabledTab('beats')" + kbn-href="#/beats" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.beatsNavigation.overviewLinkText" + i18n-default-message="Overview" + > + </a> + <a + ng-if="!monitoringMain.instance && monitoringMain.isDisabledTab('beats')" + kbn-href="" + class="euiTab euiTab-isDisabled" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.beatsNavigation.overviewLinkText" + i18n-default-message="Overview" + > + </a> + <a + ng-if="!monitoringMain.instance" + kbn-href="#/beats/beats" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('beats')}" + i18n-id="xpack.monitoring.beatsNavigation.instancesLinkText" + i18n-default-message="Instances" + > + </a> + <a + ng-if="monitoringMain.instance" + kbn-href="#/beats/beat/{{ monitoringMain.resolver }}" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}" + i18n-id="xpack.monitoring.beatsNavigation.instance.overviewLinkText" + i18n-default-message="Overview" + > + </a> + </div> + + <div ng-if="monitoringMain.inLogstash" class="euiTabs" role="navigation"> + <a + ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && !monitoringMain.isDisabledTab('logstash')" + kbn-href="#/logstash" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.logstashNavigation.overviewLinkText" + i18n-default-message="Overview" + > + </a> + <a + ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && monitoringMain.isDisabledTab('logstash')" + kbn-href="" + class="euiTab euiTab-isDisabled" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('overview')}" + i18n-id="xpack.monitoring.logstashNavigation.overviewLinkText" + i18n-default-message="Overview" + > + </a> + <a + ng-if="!monitoringMain.instance && !monitoringMain.pipelineId" + kbn-href="#/logstash/nodes" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('nodes')}" + i18n-id="xpack.monitoring.logstashNavigation.nodesLinkText" + i18n-default-message="Nodes" + > + </a> + <a + ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && !monitoringMain.isDisabledTab('logstash')" + kbn-href="#/logstash/pipelines" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('pipelines')}" + > + <span + i18n-id="xpack.monitoring.logstashNavigation.pipelinesLinkText" + i18n-default-message="Pipelines" + ></span> + <span class="kuiIcon fa-flask monTabs--icon" tooltip="Beta feature" /> + </a> + <a + ng-if="!monitoringMain.instance && !monitoringMain.pipelineId && monitoringMain.isDisabledTab('logstash')" + kbn-href="" + class="euiTab euiTab-isDisabled" + ng-class="{'euiTab-isSelected': monitoringMain.isActiveTab('pipelines')}" + > + <span + i18n-id="xpack.monitoring.logstashNavigation.pipelinesLinkText" + i18n-default-message="Pipelines" + ></span> + <span class="kuiIcon fa-flask monTabs--icon" tooltip="Beta feature" /> + </a> + <a + ng-if="monitoringMain.instance" + kbn-href="#/logstash/node/{{ monitoringMain.resolver }}" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.page === 'overview'}" + i18n-id="xpack.monitoring.logstashNavigation.instance.overviewLinkText" + i18n-default-message="Overview" + > + </a> + <a + ng-if="monitoringMain.instance" + kbn-href="#/logstash/node/{{ monitoringMain.resolver }}/pipelines" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.page === 'pipelines'}" + > + <span + i18n-id="xpack.monitoring.logstashNavigation.instance.pipelinesLinkText" + i18n-default-message="Pipelines" + ></span> + <span class="kuiIcon fa-flask fa-sm monTabs--icon" tooltip="Beta feature" /> + </a> + <a + ng-if="monitoringMain.instance" + kbn-href="#/logstash/node/{{ monitoringMain.resolver }}/advanced" + class="euiTab" + ng-class="{'euiTab-isSelected': monitoringMain.page === 'advanced'}" + i18n-id="xpack.monitoring.logstashNavigation.instance.advancedLinkText" + i18n-default-message="Advanced" + > + </a> + <div + class="euiTab" + ng-if="monitoringMain.pipelineVersions.length" + id="dropdown-elm" + ng-init="monitoringMain.dropdownLoadedHandler()" + ></div> + </div> + + <div ng-if="monitoringMain.inOverview" class="euiTabs" role="navigation"> + <a class="euiTab" data-test-subj="clusterName">{{ pageData.cluster_name }}</a> + </div> + + <div ng-if="monitoringMain.inAlerts" class="euiTabs" role="navigation"> + <a + class="euiTab" + data-test-subj="clusterAlertsListingPage" + i18n-id="xpack.monitoring.clusterAlertsNavigation.clusterAlertsLinkText" + i18n-default-message="Cluster Alerts" + ></a> + </div> + + <div ng-if="monitoringMain.inListing" class="euiTabs" role="navigation"> + <a + class="euiTab" + i18n-id="xpack.monitoring.clustersNavigation.clustersLinkText" + i18n-default-message="Clusters" + ></a> + </div> + </div> + <div ng-transclude></div> +</div> diff --git a/x-pack/plugins/monitoring/public/directives/main/index.js b/x-pack/plugins/monitoring/public/directives/main/index.js new file mode 100644 index 0000000000000..8c28ab6103868 --- /dev/null +++ b/x-pack/plugins/monitoring/public/directives/main/index.js @@ -0,0 +1,266 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import { EuiSelect, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; +import template from './index.html'; +import { Legacy } from '../../legacy_shims'; +import { shortenPipelineHash } from '../../../common/formatting'; +import { getSetupModeState, initSetupModeState } from '../../lib/setup_mode'; +import { Subscription } from 'rxjs'; + +const setOptions = controller => { + if ( + !controller.pipelineVersions || + !controller.pipelineVersions.length || + !controller.pipelineDropdownElement + ) { + return; + } + + render( + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiTitle + style={{ maxWidth: 400, lineHeight: '40px', overflow: 'hidden', whiteSpace: 'nowrap' }} + > + <h2>{controller.pipelineId}</h2> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiSelect + value={controller.pipelineHash} + options={controller.pipelineVersions.map(option => { + return { + text: i18n.translate( + 'xpack.monitoring.logstashNavigation.pipelineVersionDescription', + { + defaultMessage: + 'Version active {relativeLastSeen} and first seen {relativeFirstSeen}', + values: { + relativeLastSeen: option.relativeLastSeen, + relativeFirstSeen: option.relativeFirstSeen, + }, + } + ), + value: option.hash, + }; + })} + onChange={controller.onChangePipelineHash} + /> + </EuiFlexItem> + </EuiFlexGroup>, + controller.pipelineDropdownElement + ); +}; + +/* + * Manage data and provide helper methods for the "main" directive's template + */ +export class MonitoringMainController { + // called internally by Angular + constructor() { + this.inListing = false; + this.inAlerts = false; + this.inOverview = false; + this.inElasticsearch = false; + this.inKibana = false; + this.inLogstash = false; + this.inBeats = false; + this.inApm = false; + } + + addTimerangeObservers = () => { + const timefilter = Legacy.shims.timefilter; + this.subscriptions = new Subscription(); + + const refreshIntervalUpdated = () => { + const { value: refreshInterval, pause: isPaused } = timefilter.getRefreshInterval(); + this.datePicker.onRefreshChange({ refreshInterval, isPaused }, true); + }; + + const timeUpdated = () => { + this.datePicker.onTimeUpdate({ dateRange: timefilter.getTime() }, true); + }; + + this.subscriptions.add( + timefilter.getRefreshIntervalUpdate$().subscribe(refreshIntervalUpdated) + ); + this.subscriptions.add(timefilter.getTimeUpdate$().subscribe(timeUpdated)); + }; + + dropdownLoadedHandler() { + this.pipelineDropdownElement = document.querySelector('#dropdown-elm'); + setOptions(this); + } + + // kick things off from the directive link function + setup(options) { + const timefilter = Legacy.shims.timefilter; + this._licenseService = options.licenseService; + this._breadcrumbsService = options.breadcrumbsService; + this._kbnUrlService = options.kbnUrlService; + this._executorService = options.executorService; + + Object.assign(this, options.attributes); + + this.navName = `${this.name}-nav`; + + // set the section we're navigated in + if (this.product) { + this.inElasticsearch = this.product === 'elasticsearch'; + this.inKibana = this.product === 'kibana'; + this.inLogstash = this.product === 'logstash'; + this.inBeats = this.product === 'beats'; + this.inApm = this.product === 'apm'; + } else { + this.inOverview = this.name === 'overview'; + this.inAlerts = this.name === 'alerts'; + this.inListing = this.name === 'listing'; // || this.name === 'no-data'; + } + + if (!this.inListing) { + // no breadcrumbs in cluster listing page + this.breadcrumbs = this._breadcrumbsService(options.clusterName, this); + } + + if (this.pipelineHash) { + this.pipelineHashShort = shortenPipelineHash(this.pipelineHash); + this.onChangePipelineHash = () => { + return this._kbnUrlService.changePath( + `/logstash/pipelines/${this.pipelineId}/${this.pipelineHash}` + ); + }; + } + + this.datePicker = { + enableTimeFilter: timefilter.isTimeRangeSelectorEnabled(), + timeRange: timefilter.getTime(), + refreshInterval: timefilter.getRefreshInterval(), + onRefreshChange: ({ isPaused, refreshInterval }, skipSet = false) => { + this.datePicker.refreshInterval = { + pause: isPaused, + value: refreshInterval, + }; + if (!skipSet) { + timefilter.setRefreshInterval({ + pause: isPaused, + value: refreshInterval ? refreshInterval : this.datePicker.refreshInterval.value, + }); + } + }, + onTimeUpdate: ({ dateRange }, skipSet = false) => { + this.datePicker.timeRange = { + ...dateRange, + }; + if (!skipSet) { + timefilter.setTime(dateRange); + } + this._executorService.cancel(); + this._executorService.run(); + }, + }; + } + + // check whether to "highlight" a tab + isActiveTab(testPath) { + return this.name === testPath; + } + + // check whether to show ML tab + isMlSupported() { + return this._licenseService.mlIsSupported(); + } + + isDisabledTab(product) { + const setupMode = getSetupModeState(); + if (!setupMode.enabled || !setupMode.data) { + return false; + } + + const data = setupMode.data[product] || {}; + if (data.totalUniqueInstanceCount === 0) { + return true; + } + if ( + data.totalUniqueInternallyCollectedCount === 0 && + data.totalUniqueFullyMigratedCount === 0 && + data.totalUniquePartiallyMigratedCount === 0 + ) { + return true; + } + return false; + } +} + +export function monitoringMainProvider(breadcrumbs, license, kbnUrl, $injector) { + const $executor = $injector.get('$executor'); + + return { + restrict: 'E', + transclude: true, + template, + controller: MonitoringMainController, + controllerAs: 'monitoringMain', + bindToController: true, + link(scope, _element, attributes, controller) { + scope.$applyAsync(() => { + controller.addTimerangeObservers(); + const setupObj = getSetupObj(); + controller.setup(setupObj); + Object.keys(setupObj.attributes).forEach(key => { + attributes.$observe(key, () => controller.setup(getSetupObj())); + }); + }); + + initSetupModeState(scope, $injector, () => { + controller.setup(getSetupObj()); + }); + if (!scope.cluster) { + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + scope.cluster = ($route.current.locals.clusters || []).find( + cluster => cluster.cluster_uuid === globalState.cluster_uuid + ); + } + + function getSetupObj() { + return { + licenseService: license, + breadcrumbsService: breadcrumbs, + executorService: $executor, + kbnUrlService: kbnUrl, + attributes: { + name: attributes.name, + product: attributes.product, + instance: attributes.instance, + resolver: attributes.resolver, + page: attributes.page, + tabIconClass: attributes.tabIconClass, + tabIconLabel: attributes.tabIconLabel, + pipelineId: attributes.pipelineId, + pipelineHash: attributes.pipelineHash, + pipelineVersions: get(scope, 'pageData.versions'), + isCcrEnabled: attributes.isCcrEnabled === 'true' || attributes.isCcrEnabled === true, + }, + clusterName: get(scope, 'cluster.cluster_name'), + }; + } + + scope.$on('$destroy', () => { + controller.pipelineDropdownElement && + unmountComponentAtNode(controller.pipelineDropdownElement); + controller.subscriptions && controller.subscriptions.unsubscribe(); + }); + scope.$watch('pageData.versions', versions => { + controller.pipelineVersions = versions; + setOptions(controller); + }); + }, + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg b/x-pack/plugins/monitoring/public/icons/health-gray.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-gray.svg rename to x-pack/plugins/monitoring/public/icons/health-gray.svg diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-green.svg b/x-pack/plugins/monitoring/public/icons/health-green.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-green.svg rename to x-pack/plugins/monitoring/public/icons/health-green.svg diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-red.svg b/x-pack/plugins/monitoring/public/icons/health-red.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-red.svg rename to x-pack/plugins/monitoring/public/icons/health-red.svg diff --git a/x-pack/legacy/plugins/monitoring/public/icons/health-yellow.svg b/x-pack/plugins/monitoring/public/icons/health-yellow.svg similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/icons/health-yellow.svg rename to x-pack/plugins/monitoring/public/icons/health-yellow.svg diff --git a/x-pack/plugins/monitoring/public/index.scss b/x-pack/plugins/monitoring/public/index.scss new file mode 100644 index 0000000000000..4dda80ee7454b --- /dev/null +++ b/x-pack/plugins/monitoring/public/index.scss @@ -0,0 +1,30 @@ +// Import the EUI global scope so we can use EUI constants +@import 'src/legacy/ui/public/styles/_styling_constants'; + +// Temporary hacks +@import 'hacks'; + +// Monitoring plugin styles + +// Prefix all styles with "mon" to avoid conflicts. +// Examples +// monChart +// monChart__legend +// monChart__legend--small +// monChart__legend-isLoading + +@import 'components/chart/index'; +@import 'components/no_data/index'; +@import 'components/sparkline/index'; +@import 'components/summary_status/index'; +@import 'components/table/index'; +@import 'components/logstash/pipeline_viewer/views/index'; +@import 'components/elasticsearch/shard_allocation/index'; +@import 'components/setup_mode/index'; +@import 'components/elasticsearch/ccr/index'; + +.monitoringApplicationWrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/x-pack/plugins/monitoring/public/index.ts b/x-pack/plugins/monitoring/public/index.ts new file mode 100644 index 0000000000000..71c98e8e8b131 --- /dev/null +++ b/x-pack/plugins/monitoring/public/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/public'; +import { MonitoringPlugin } from './plugin'; + +export function plugin(ctx: PluginInitializerContext) { + return new MonitoringPlugin(ctx); +} diff --git a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts b/x-pack/plugins/monitoring/public/jest.helpers.ts similarity index 80% rename from x-pack/legacy/plugins/monitoring/public/jest.helpers.ts rename to x-pack/plugins/monitoring/public/jest.helpers.ts index 46ba603d30138..96fb70e480f53 100644 --- a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts +++ b/x-pack/plugins/monitoring/public/jest.helpers.ts @@ -25,12 +25,3 @@ export function mockUseEffects(count = 1) { spy.mockImplementationOnce(f => f()); } } - -// export function mockUseEffectForDeps(deps, count = 1) { -// const spy = jest.spyOn(React, 'useEffect'); -// for (let i = 0; i < count; i++) { -// spy.mockImplementationOnce((f, depList) => { - -// }); -// } -// } diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts new file mode 100644 index 0000000000000..47aa1048c5130 --- /dev/null +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -0,0 +1,83 @@ +/* + * 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 { CoreStart } from 'kibana/public'; +import angular from 'angular'; +import { HttpRequestInit } from '../../../../src/core/public'; +import { MonitoringPluginDependencies } from './types'; + +export interface KFetchQuery { + [key: string]: string | number | boolean | undefined; +} + +export interface KFetchOptions extends HttpRequestInit { + pathname: string; + query?: KFetchQuery; + asSystemRequest?: boolean; +} + +export interface KFetchKibanaOptions { + prependBasePath?: boolean; +} + +export interface IShims { + toastNotifications: CoreStart['notifications']['toasts']; + capabilities: { get: () => CoreStart['application']['capabilities'] }; + getAngularInjector: () => angular.auto.IInjectorService; + getBasePath: () => string; + getInjected: (name: string, defaultValue?: unknown) => unknown; + breadcrumbs: { set: () => void }; + I18nContext: CoreStart['i18n']['Context']; + docLinks: CoreStart['docLinks']; + docTitle: CoreStart['chrome']['docTitle']; + timefilter: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + kfetch: ( + { pathname, ...options }: KFetchOptions, + kfetchOptions?: KFetchKibanaOptions | undefined + ) => Promise<any>; + isCloud: boolean; +} + +export class Legacy { + private static _shims: IShims; + + public static init( + { core, data, isCloud }: MonitoringPluginDependencies, + ngInjector: angular.auto.IInjectorService + ) { + this._shims = { + toastNotifications: core.notifications.toasts, + capabilities: { get: () => core.application.capabilities }, + getAngularInjector: (): angular.auto.IInjectorService => ngInjector, + getBasePath: (): string => core.http.basePath.get(), + getInjected: (name: string, defaultValue?: unknown): string | unknown => + core.injectedMetadata.getInjectedVar(name, defaultValue), + breadcrumbs: { + set: (...args: any[0]) => core.chrome.setBreadcrumbs.apply(this, args), + }, + I18nContext: core.i18n.Context, + docLinks: core.docLinks, + docTitle: core.chrome.docTitle, + timefilter: data.query.timefilter.timefilter, + kfetch: async ( + { pathname, ...options }: KFetchOptions, + kfetchOptions?: KFetchKibanaOptions + ) => + await core.http.fetch(pathname, { + prependBasePath: kfetchOptions?.prependBasePath, + ...options, + }), + isCloud, + }; + } + + public static get shims(): Readonly<IShims> { + if (!Legacy._shims) { + throw new Error('Legacy needs to be initiated with Legacy.init(...) before use'); + } + return Legacy._shims; + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/lib/__tests__/format_number.js b/x-pack/plugins/monitoring/public/lib/__tests__/format_number.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/__tests__/format_number.js rename to x-pack/plugins/monitoring/public/lib/__tests__/format_number.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx similarity index 88% rename from x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx rename to x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx index c09014b9a9627..d729457f60df1 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx +++ b/x-pack/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -6,12 +6,11 @@ import React from 'react'; import { contains } from 'lodash'; -import { toastNotifications } from 'ui/notify'; -// @ts-ignore -import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; // eslint-disable-line import/order import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { Legacy } from '../legacy_shims'; +import { formatMsg } from '../../../../../src/plugins/kibana_legacy/public'; +import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; export function formatMonitoringError(err: any) { // TODO: We should stop using Boom for errors and instead write a custom handler to return richer error objects @@ -43,7 +42,7 @@ export function ajaxErrorHandlersProvider($injector: any) { kbnUrl.redirect('access-denied'); } else if (err.status === 404 && !contains(window.location.hash, 'no-data')) { // pass through if this is a 404 and we're already on the no-data page - toastNotifications.addDanger({ + Legacy.shims.toastNotifications.addDanger({ title: toMountPoint( <FormattedMessage id="xpack.monitoring.ajaxErrorHandler.requestFailedNotificationTitle" @@ -64,7 +63,7 @@ export function ajaxErrorHandlersProvider($injector: any) { ), }); } else { - toastNotifications.addDanger({ + Legacy.shims.toastNotifications.addDanger({ title: toMountPoint( <FormattedMessage id="xpack.monitoring.ajaxErrorHandler.requestErrorNotificationTitle" diff --git a/x-pack/legacy/plugins/monitoring/public/lib/calculate_shard_stats.js b/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/calculate_shard_stats.js rename to x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/enabler.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/settings_checker.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/__tests__/start_checks.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/checkers/cluster_settings.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/checkers/cluster_settings.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/checkers/cluster_settings.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/checkers/cluster_settings.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/checkers/node_settings.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/checkers/node_settings.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/checkers/node_settings.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/checkers/node_settings.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/checkers/settings_checker.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/checkers/settings_checker.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/checkers/settings_checker.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/checkers/settings_checker.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/enabler.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/index.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/index.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js b/x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js rename to x-pack/plugins/monitoring/public/lib/elasticsearch_settings/start_checks.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ensure_minimum_time.js b/x-pack/plugins/monitoring/public/lib/ensure_minimum_time.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/ensure_minimum_time.js rename to x-pack/plugins/monitoring/public/lib/ensure_minimum_time.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ensure_minimum_time.test.js b/x-pack/plugins/monitoring/public/lib/ensure_minimum_time.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/ensure_minimum_time.test.js rename to x-pack/plugins/monitoring/public/lib/ensure_minimum_time.test.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/extract_ip.js b/x-pack/plugins/monitoring/public/lib/extract_ip.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/extract_ip.js rename to x-pack/plugins/monitoring/public/lib/extract_ip.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts b/x-pack/plugins/monitoring/public/lib/form_validation.ts similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts rename to x-pack/plugins/monitoring/public/lib/form_validation.ts diff --git a/x-pack/legacy/plugins/monitoring/public/lib/format_number.js b/x-pack/plugins/monitoring/public/lib/format_number.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/format_number.js rename to x-pack/plugins/monitoring/public/lib/format_number.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/get_cluster_from_clusters.js b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/get_cluster_from_clusters.js rename to x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js diff --git a/x-pack/plugins/monitoring/public/lib/get_page_data.js b/x-pack/plugins/monitoring/public/lib/get_page_data.js new file mode 100644 index 0000000000000..872cac5922334 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/get_page_data.js @@ -0,0 +1,29 @@ +/* + * 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 { ajaxErrorHandlersProvider } from './ajax_error_handler'; +import { Legacy } from '../legacy_shims'; + +export function getPageData($injector, api) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(api, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/plugins/monitoring/public/lib/get_safe_for_external_link.ts b/x-pack/plugins/monitoring/public/lib/get_safe_for_external_link.ts new file mode 100644 index 0000000000000..6f3e398c1414f --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/get_safe_for_external_link.ts @@ -0,0 +1,9 @@ +/* + * 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 function getSafeForExternalLink(url: string) { + return `${url.split('?')[0]}?${location.hash.split('?')[1]}`; +} diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md new file mode 100644 index 0000000000000..699e2500f4942 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/API.md @@ -0,0 +1,1498 @@ +# Flot Reference # + +**Table of Contents** + +[Introduction](#introduction) +| [Data Format](#data-format) +| [Plot Options](#plot-options) +| [Customizing the legend](#customizing-the-legend) +| [Customizing the axes](#customizing-the-axes) +| [Multiple axes](#multiple-axes) +| [Time series data](#time-series-data) +| [Customizing the data series](#customizing-the-data-series) +| [Customizing the grid](#customizing-the-grid) +| [Specifying gradients](#specifying-gradients) +| [Plot Methods](#plot-methods) +| [Hooks](#hooks) +| [Plugins](#plugins) +| [Version number](#version-number) + +--- + +## Introduction ## + +Consider a call to the plot function: + +```js +var plot = $.plot(placeholder, data, options) +``` + +The placeholder is a jQuery object or DOM element or jQuery expression +that the plot will be put into. This placeholder needs to have its +width and height set as explained in the [README](README.md) (go read that now if +you haven't, it's short). The plot will modify some properties of the +placeholder so it's recommended you simply pass in a div that you +don't use for anything else. Make sure you check any fancy styling +you apply to the div, e.g. background images have been reported to be a +problem on IE 7. + +The plot function can also be used as a jQuery chainable property. This form +naturally can't return the plot object directly, but you can still access it +via the 'plot' data key, like this: + +```js +var plot = $("#placeholder").plot(data, options).data("plot"); +``` + +The format of the data is documented below, as is the available +options. The plot object returned from the call has some methods you +can call. These are documented separately below. + +Note that in general Flot gives no guarantees if you change any of the +objects you pass in to the plot function or get out of it since +they're not necessarily deep-copied. + + +## Data Format ## + +The data is an array of data series: + +```js +[ series1, series2, ... ] +``` + +A series can either be raw data or an object with properties. The raw +data format is an array of points: + +```js +[ [x1, y1], [x2, y2], ... ] +``` + +E.g. + +```js +[ [1, 3], [2, 14.01], [3.5, 3.14] ] +``` + +Note that to simplify the internal logic in Flot both the x and y +values must be numbers (even if specifying time series, see below for +how to do this). This is a common problem because you might retrieve +data from the database and serialize them directly to JSON without +noticing the wrong type. If you're getting mysterious errors, double +check that you're inputting numbers and not strings. + +If a null is specified as a point or if one of the coordinates is null +or couldn't be converted to a number, the point is ignored when +drawing. As a special case, a null value for lines is interpreted as a +line segment end, i.e. the points before and after the null value are +not connected. + +Lines and points take two coordinates. For filled lines and bars, you +can specify a third coordinate which is the bottom of the filled +area/bar (defaults to 0). + +The format of a single series object is as follows: + +```js +{ + color: color or number + data: rawdata + label: string + lines: specific lines options + bars: specific bars options + points: specific points options + xaxis: number + yaxis: number + clickable: boolean + hoverable: boolean + shadowSize: number + highlightColor: color or number +} +``` + +You don't have to specify any of them except the data, the rest are +options that will get default values. Typically you'd only specify +label and data, like this: + +```js +{ + label: "y = 3", + data: [[0, 3], [10, 3]] +} +``` + +The label is used for the legend, if you don't specify one, the series +will not show up in the legend. + +If you don't specify color, the series will get a color from the +auto-generated colors. The color is either a CSS color specification +(like "rgb(255, 100, 123)") or an integer that specifies which of +auto-generated colors to select, e.g. 0 will get color no. 0, etc. + +The latter is mostly useful if you let the user add and remove series, +in which case you can hard-code the color index to prevent the colors +from jumping around between the series. + +The "xaxis" and "yaxis" options specify which axis to use. The axes +are numbered from 1 (default), so { yaxis: 2} means that the series +should be plotted against the second y axis. + +"clickable" and "hoverable" can be set to false to disable +interactivity for specific series if interactivity is turned on in +the plot, see below. + +The rest of the options are all documented below as they are the same +as the default options passed in via the options parameter in the plot +command. When you specify them for a specific data series, they will +override the default options for the plot for that data series. + +Here's a complete example of a simple data specification: + +```js +[ { label: "Foo", data: [ [10, 1], [17, -14], [30, 5] ] }, + { label: "Bar", data: [ [11, 13], [19, 11], [30, -7] ] } +] +``` + + +## Plot Options ## + +All options are completely optional. They are documented individually +below, to change them you just specify them in an object, e.g. + +```js +var options = { + series: { + lines: { show: true }, + points: { show: true } + } +}; + +$.plot(placeholder, data, options); +``` + + +## Customizing the legend ## + +```js +legend: { + show: boolean + labelFormatter: null or (fn: string, series object -> string) + labelBoxBorderColor: color + noColumns: number + position: "ne" or "nw" or "se" or "sw" + margin: number of pixels or [x margin, y margin] + backgroundColor: null or color + backgroundOpacity: number between 0 and 1 + container: null or jQuery object/DOM element/jQuery expression + sorted: null/false, true, "ascending", "descending", "reverse", or a comparator +} +``` + +The legend is generated as a table with the data series labels and +small label boxes with the color of the series. If you want to format +the labels in some way, e.g. make them to links, you can pass in a +function for "labelFormatter". Here's an example that makes them +clickable: + +```js +labelFormatter: function(label, series) { + // series is the series object for the label + return '<a href="#' + label + '">' + label + '</a>'; +} +``` + +To prevent a series from showing up in the legend, simply have the function +return null. + +"noColumns" is the number of columns to divide the legend table into. +"position" specifies the overall placement of the legend within the +plot (top-right, top-left, etc.) and margin the distance to the plot +edge (this can be either a number or an array of two numbers like [x, +y]). "backgroundColor" and "backgroundOpacity" specifies the +background. The default is a partly transparent auto-detected +background. + +If you want the legend to appear somewhere else in the DOM, you can +specify "container" as a jQuery object/expression to put the legend +table into. The "position" and "margin" etc. options will then be +ignored. Note that Flot will overwrite the contents of the container. + +Legend entries appear in the same order as their series by default. If "sorted" +is "reverse" then they appear in the opposite order from their series. To sort +them alphabetically, you can specify true, "ascending" or "descending", where +true and "ascending" are equivalent. + +You can also provide your own comparator function that accepts two +objects with "label" and "color" properties, and returns zero if they +are equal, a positive value if the first is greater than the second, +and a negative value if the first is less than the second. + +```js +sorted: function(a, b) { + // sort alphabetically in ascending order + return a.label == b.label ? 0 : ( + a.label > b.label ? 1 : -1 + ) +} +``` + + +## Customizing the axes ## + +```js +xaxis, yaxis: { + show: null or true/false + position: "bottom" or "top" or "left" or "right" + mode: null or "time" ("time" requires jquery.flot.time.js plugin) + timezone: null, "browser" or timezone (only makes sense for mode: "time") + + color: null or color spec + tickColor: null or color spec + font: null or font spec object + + min: null or number + max: null or number + autoscaleMargin: null or number + + transform: null or fn: number -> number + inverseTransform: null or fn: number -> number + + ticks: null or number or ticks array or (fn: axis -> ticks array) + tickSize: number or array + minTickSize: number or array + tickFormatter: (fn: number, object -> string) or string + tickDecimals: null or number + + labelWidth: null or number + labelHeight: null or number + reserveSpace: null or true + + tickLength: null or number + + alignTicksWithAxis: null or number +} +``` + +All axes have the same kind of options. The following describes how to +configure one axis, see below for what to do if you've got more than +one x axis or y axis. + +If you don't set the "show" option (i.e. it is null), visibility is +auto-detected, i.e. the axis will show up if there's data associated +with it. You can override this by setting the "show" option to true or +false. + +The "position" option specifies where the axis is placed, bottom or +top for x axes, left or right for y axes. The "mode" option determines +how the data is interpreted, the default of null means as decimal +numbers. Use "time" for time series data; see the time series data +section. The time plugin (jquery.flot.time.js) is required for time +series support. + +The "color" option determines the color of the line and ticks for the axis, and +defaults to the grid color with transparency. For more fine-grained control you +can also set the color of the ticks separately with "tickColor". + +You can customize the font and color used to draw the axis tick labels with CSS +or directly via the "font" option. When "font" is null - the default - each +tick label is given the 'flot-tick-label' class. For compatibility with Flot +0.7 and earlier the labels are also given the 'tickLabel' class, but this is +deprecated and scheduled to be removed with the release of version 1.0.0. + +To enable more granular control over styles, labels are divided between a set +of text containers, with each holding the labels for one axis. These containers +are given the classes 'flot-[x|y]-axis', and 'flot-[x|y]#-axis', where '#' is +the number of the axis when there are multiple axes. For example, the x-axis +labels for a simple plot with only a single x-axis might look like this: + +```html +<div class='flot-x-axis flot-x1-axis'> + <div class='flot-tick-label'>January 2013</div> + ... +</div> +``` + +For direct control over label styles you can also provide "font" as an object +with this format: + +```js +{ + size: 11, + lineHeight: 13, + style: "italic", + weight: "bold", + family: "sans-serif", + variant: "small-caps", + color: "#545454" +} +``` + +The size and lineHeight must be expressed in pixels; CSS units such as 'em' +or 'smaller' are not allowed. + +The options "min"/"max" are the precise minimum/maximum value on the +scale. If you don't specify either of them, a value will automatically +be chosen based on the minimum/maximum data values. Note that Flot +always examines all the data values you feed to it, even if a +restriction on another axis may make some of them invisible (this +makes interactive use more stable). + +The "autoscaleMargin" is a bit esoteric: it's the fraction of margin +that the scaling algorithm will add to avoid that the outermost points +ends up on the grid border. Note that this margin is only applied when +a min or max value is not explicitly set. If a margin is specified, +the plot will furthermore extend the axis end-point to the nearest +whole tick. The default value is "null" for the x axes and 0.02 for y +axes which seems appropriate for most cases. + +"transform" and "inverseTransform" are callbacks you can put in to +change the way the data is drawn. You can design a function to +compress or expand certain parts of the axis non-linearly, e.g. +suppress weekends or compress far away points with a logarithm or some +other means. When Flot draws the plot, each value is first put through +the transform function. Here's an example, the x axis can be turned +into a natural logarithm axis with the following code: + +```js +xaxis: { + transform: function (v) { return Math.log(v); }, + inverseTransform: function (v) { return Math.exp(v); } +} +``` + +Similarly, for reversing the y axis so the values appear in inverse +order: + +```js +yaxis: { + transform: function (v) { return -v; }, + inverseTransform: function (v) { return -v; } +} +``` + +Note that for finding extrema, Flot assumes that the transform +function does not reorder values (it should be monotone). + +The inverseTransform is simply the inverse of the transform function +(so v == inverseTransform(transform(v)) for all relevant v). It is +required for converting from canvas coordinates to data coordinates, +e.g. for a mouse interaction where a certain pixel is clicked. If you +don't use any interactive features of Flot, you may not need it. + + +The rest of the options deal with the ticks. + +If you don't specify any ticks, a tick generator algorithm will make +some for you. The algorithm has two passes. It first estimates how +many ticks would be reasonable and uses this number to compute a nice +round tick interval size. Then it generates the ticks. + +You can specify how many ticks the algorithm aims for by setting +"ticks" to a number. The algorithm always tries to generate reasonably +round tick values so even if you ask for three ticks, you might get +five if that fits better with the rounding. If you don't want any +ticks at all, set "ticks" to 0 or an empty array. + +Another option is to skip the rounding part and directly set the tick +interval size with "tickSize". If you set it to 2, you'll get ticks at +2, 4, 6, etc. Alternatively, you can specify that you just don't want +ticks at a size less than a specific tick size with "minTickSize". +Note that for time series, the format is an array like [2, "month"], +see the next section. + +If you want to completely override the tick algorithm, you can specify +an array for "ticks", either like this: + +```js +ticks: [0, 1.2, 2.4] +``` + +Or like this where the labels are also customized: + +```js +ticks: [[0, "zero"], [1.2, "one mark"], [2.4, "two marks"]] +``` + +You can mix the two if you like. + +For extra flexibility you can specify a function as the "ticks" +parameter. The function will be called with an object with the axis +min and max and should return a ticks array. Here's a simplistic tick +generator that spits out intervals of pi, suitable for use on the x +axis for trigonometric functions: + +```js +function piTickGenerator(axis) { + var res = [], i = Math.floor(axis.min / Math.PI); + do { + var v = i * Math.PI; + res.push([v, i + "\u03c0"]); + ++i; + } while (v < axis.max); + return res; +} +``` + +You can control how the ticks look like with "tickDecimals", the +number of decimals to display (default is auto-detected). + +Alternatively, for ultimate control over how ticks are formatted you can +provide a function to "tickFormatter". The function is passed two +parameters, the tick value and an axis object with information, and +should return a string. The default formatter looks like this: + +```js +function formatter(val, axis) { + return val.toFixed(axis.tickDecimals); +} +``` + +The axis object has "min" and "max" with the range of the axis, +"tickDecimals" with the number of decimals to round the value to and +"tickSize" with the size of the interval between ticks as calculated +by the automatic axis scaling algorithm (or specified by you). Here's +an example of a custom formatter: + +```js +function suffixFormatter(val, axis) { + if (val > 1000000) + return (val / 1000000).toFixed(axis.tickDecimals) + " MB"; + else if (val > 1000) + return (val / 1000).toFixed(axis.tickDecimals) + " kB"; + else + return val.toFixed(axis.tickDecimals) + " B"; +} +``` + +"labelWidth" and "labelHeight" specifies a fixed size of the tick +labels in pixels. They're useful in case you need to align several +plots. "reserveSpace" means that even if an axis isn't shown, Flot +should reserve space for it - it is useful in combination with +labelWidth and labelHeight for aligning multi-axis charts. + +"tickLength" is the length of the tick lines in pixels. By default, the +innermost axes will have ticks that extend all across the plot, while +any extra axes use small ticks. A value of null means use the default, +while a number means small ticks of that length - set it to 0 to hide +the lines completely. + +If you set "alignTicksWithAxis" to the number of another axis, e.g. +alignTicksWithAxis: 1, Flot will ensure that the autogenerated ticks +of this axis are aligned with the ticks of the other axis. This may +improve the looks, e.g. if you have one y axis to the left and one to +the right, because the grid lines will then match the ticks in both +ends. The trade-off is that the forced ticks won't necessarily be at +natural places. + + +## Multiple axes ## + +If you need more than one x axis or y axis, you need to specify for +each data series which axis they are to use, as described under the +format of the data series, e.g. { data: [...], yaxis: 2 } specifies +that a series should be plotted against the second y axis. + +To actually configure that axis, you can't use the xaxis/yaxis options +directly - instead there are two arrays in the options: + +```js +xaxes: [] +yaxes: [] +``` + +Here's an example of configuring a single x axis and two y axes (we +can leave options of the first y axis empty as the defaults are fine): + +```js +{ + xaxes: [ { position: "top" } ], + yaxes: [ { }, { position: "right", min: 20 } ] +} +``` + +The arrays get their default values from the xaxis/yaxis settings, so +say you want to have all y axes start at zero, you can simply specify +yaxis: { min: 0 } instead of adding a min parameter to all the axes. + +Generally, the various interfaces in Flot dealing with data points +either accept an xaxis/yaxis parameter to specify which axis number to +use (starting from 1), or lets you specify the coordinate directly as +x2/x3/... or x2axis/x3axis/... instead of "x" or "xaxis". + + +## Time series data ## + +Please note that it is now required to include the time plugin, +jquery.flot.time.js, for time series support. + +Time series are a bit more difficult than scalar data because +calendars don't follow a simple base 10 system. For many cases, Flot +abstracts most of this away, but it can still be a bit difficult to +get the data into Flot. So we'll first discuss the data format. + +The time series support in Flot is based on JavaScript timestamps, +i.e. everywhere a time value is expected or handed over, a JavaScript +timestamp number is used. This is a number, not a Date object. A +JavaScript timestamp is the number of milliseconds since January 1, +1970 00:00:00 UTC. This is almost the same as Unix timestamps, except it's +in milliseconds, so remember to multiply by 1000! + +You can see a timestamp like this + +```js +alert((new Date()).getTime()) +``` + +There are different schools of thought when it comes to display of +timestamps. Many will want the timestamps to be displayed according to +a certain time zone, usually the time zone in which the data has been +produced. Some want the localized experience, where the timestamps are +displayed according to the local time of the visitor. Flot supports +both. Optionally you can include a third-party library to get +additional timezone support. + +Default behavior is that Flot always displays timestamps according to +UTC. The reason being that the core JavaScript Date object does not +support other fixed time zones. Often your data is at another time +zone, so it may take a little bit of tweaking to work around this +limitation. + +The easiest way to think about it is to pretend that the data +production time zone is UTC, even if it isn't. So if you have a +datapoint at 2002-02-20 08:00, you can generate a timestamp for eight +o'clock UTC even if it really happened eight o'clock UTC+0200. + +In PHP you can get an appropriate timestamp with: + +```php +strtotime("2002-02-20 UTC") * 1000 +``` + +In Python you can get it with something like: + +```python +calendar.timegm(datetime_object.timetuple()) * 1000 +``` +In Ruby you can get it using the `#to_i` method on the +[`Time`](http://apidock.com/ruby/Time/to_i) object. If you're using the +`active_support` gem (default for Ruby on Rails applications) `#to_i` is also +available on the `DateTime` and `ActiveSupport::TimeWithZone` objects. You +simply need to multiply the result by 1000: + +```ruby +Time.now.to_i * 1000 # => 1383582043000 +# ActiveSupport examples: +DateTime.now.to_i * 1000 # => 1383582043000 +ActiveSupport::TimeZone.new('Asia/Shanghai').now.to_i * 1000 +# => 1383582043000 +``` + +In .NET you can get it with something like: + +```aspx +public static int GetJavaScriptTimestamp(System.DateTime input) +{ + System.TimeSpan span = new System.TimeSpan(System.DateTime.Parse("1/1/1970").Ticks); + System.DateTime time = input.Subtract(span); + return (long)(time.Ticks / 10000); +} +``` + +JavaScript also has some support for parsing date strings, so it is +possible to generate the timestamps manually client-side. + +If you've already got the real UTC timestamp, it's too late to use the +pretend trick described above. But you can fix up the timestamps by +adding the time zone offset, e.g. for UTC+0200 you would add 2 hours +to the UTC timestamp you got. Then it'll look right on the plot. Most +programming environments have some means of getting the timezone +offset for a specific date (note that you need to get the offset for +each individual timestamp to account for daylight savings). + +The alternative with core JavaScript is to interpret the timestamps +according to the time zone that the visitor is in, which means that +the ticks will shift with the time zone and daylight savings of each +visitor. This behavior is enabled by setting the axis option +"timezone" to the value "browser". + +If you need more time zone functionality than this, there is still +another option. If you include the "timezone-js" library +<https://github.com/mde/timezone-js> in the page and set axis.timezone +to a value recognized by said library, Flot will use timezone-js to +interpret the timestamps according to that time zone. + +Once you've gotten the timestamps into the data and specified "time" +as the axis mode, Flot will automatically generate relevant ticks and +format them. As always, you can tweak the ticks via the "ticks" option +- just remember that the values should be timestamps (numbers), not +Date objects. + +Tick generation and formatting can also be controlled separately +through the following axis options: + +```js +minTickSize: array +timeformat: null or format string +monthNames: null or array of size 12 of strings +dayNames: null or array of size 7 of strings +twelveHourClock: boolean +``` + +Here "timeformat" is a format string to use. You might use it like +this: + +```js +xaxis: { + mode: "time", + timeformat: "%Y/%m/%d" +} +``` + +This will result in tick labels like "2000/12/24". A subset of the +standard strftime specifiers are supported (plus the nonstandard %q): + +```js +%a: weekday name (customizable) +%b: month name (customizable) +%d: day of month, zero-padded (01-31) +%e: day of month, space-padded ( 1-31) +%H: hours, 24-hour time, zero-padded (00-23) +%I: hours, 12-hour time, zero-padded (01-12) +%m: month, zero-padded (01-12) +%M: minutes, zero-padded (00-59) +%q: quarter (1-4) +%S: seconds, zero-padded (00-59) +%y: year (two digits) +%Y: year (four digits) +%p: am/pm +%P: AM/PM (uppercase version of %p) +%w: weekday as number (0-6, 0 being Sunday) +``` + +Flot 0.8 switched from %h to the standard %H hours specifier. The %h specifier +is still available, for backwards-compatibility, but is deprecated and +scheduled to be removed permanently with the release of version 1.0. + +You can customize the month names with the "monthNames" option. For +instance, for Danish you might specify: + +```js +monthNames: ["jan", "feb", "mar", "apr", "maj", "jun", "jul", "aug", "sep", "okt", "nov", "dec"] +``` + +Similarly you can customize the weekday names with the "dayNames" +option. An example in French: + +```js +dayNames: ["dim", "lun", "mar", "mer", "jeu", "ven", "sam"] +``` + +If you set "twelveHourClock" to true, the autogenerated timestamps +will use 12 hour AM/PM timestamps instead of 24 hour. This only +applies if you have not set "timeformat". Use the "%I" and "%p" or +"%P" options if you want to build your own format string with 12-hour +times. + +If the Date object has a strftime property (and it is a function), it +will be used instead of the built-in formatter. Thus you can include +a strftime library such as http://hacks.bluesmoon.info/strftime/ for +more powerful date/time formatting. + +If everything else fails, you can control the formatting by specifying +a custom tick formatter function as usual. Here's a simple example +which will format December 24 as 24/12: + +```js +tickFormatter: function (val, axis) { + var d = new Date(val); + return d.getUTCDate() + "/" + (d.getUTCMonth() + 1); +} +``` + +Note that for the time mode "tickSize" and "minTickSize" are a bit +special in that they are arrays on the form "[value, unit]" where unit +is one of "second", "minute", "hour", "day", "month" and "year". So +you can specify + +```js +minTickSize: [1, "month"] +``` + +to get a tick interval size of at least 1 month and correspondingly, +if axis.tickSize is [2, "day"] in the tick formatter, the ticks have +been produced with two days in-between. + + +## Customizing the data series ## + +```js +series: { + lines, points, bars: { + show: boolean + lineWidth: number + fill: boolean or number + fillColor: null or color/gradient + } + + lines, bars: { + zero: boolean + } + + points: { + radius: number + symbol: "circle" or function + } + + bars: { + barWidth: number + align: "left", "right" or "center" + horizontal: boolean + } + + lines: { + steps: boolean + } + + shadowSize: number + highlightColor: color or number +} + +colors: [ color1, color2, ... ] +``` + +The options inside "series: {}" are copied to each of the series. So +you can specify that all series should have bars by putting it in the +global options, or override it for individual series by specifying +bars in a particular the series object in the array of data. + +The most important options are "lines", "points" and "bars" that +specify whether and how lines, points and bars should be shown for +each data series. In case you don't specify anything at all, Flot will +default to showing lines (you can turn this off with +lines: { show: false }). You can specify the various types +independently of each other, and Flot will happily draw each of them +in turn (this is probably only useful for lines and points), e.g. + +```js +var options = { + series: { + lines: { show: true, fill: true, fillColor: "rgba(255, 255, 255, 0.8)" }, + points: { show: true, fill: false } + } +}; +``` + +"lineWidth" is the thickness of the line or outline in pixels. You can +set it to 0 to prevent a line or outline from being drawn; this will +also hide the shadow. + +"fill" is whether the shape should be filled. For lines, this produces +area graphs. You can use "fillColor" to specify the color of the fill. +If "fillColor" evaluates to false (default for everything except +points which are filled with white), the fill color is auto-set to the +color of the data series. You can adjust the opacity of the fill by +setting fill to a number between 0 (fully transparent) and 1 (fully +opaque). + +For bars, fillColor can be a gradient, see the gradient documentation +below. "barWidth" is the width of the bars in units of the x axis (or +the y axis if "horizontal" is true), contrary to most other measures +that are specified in pixels. For instance, for time series the unit +is milliseconds so 24 * 60 * 60 * 1000 produces bars with the width of +a day. "align" specifies whether a bar should be left-aligned +(default), right-aligned or centered on top of the value it represents. +When "horizontal" is on, the bars are drawn horizontally, i.e. from the +y axis instead of the x axis; note that the bar end points are still +defined in the same way so you'll probably want to swap the +coordinates if you've been plotting vertical bars first. + +Area and bar charts normally start from zero, regardless of the data's range. +This is because they convey information through size, and starting from a +different value would distort their meaning. In cases where the fill is purely +for decorative purposes, however, "zero" allows you to override this behavior. +It defaults to true for filled lines and bars; setting it to false tells the +series to use the same automatic scaling as an un-filled line. + +For lines, "steps" specifies whether two adjacent data points are +connected with a straight (possibly diagonal) line or with first a +horizontal and then a vertical line. Note that this transforms the +data by adding extra points. + +For points, you can specify the radius and the symbol. The only +built-in symbol type is circles, for other types you can use a plugin +or define them yourself by specifying a callback: + +```js +function cross(ctx, x, y, radius, shadow) { + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.moveTo(x - size, y - size); + ctx.lineTo(x + size, y + size); + ctx.moveTo(x - size, y + size); + ctx.lineTo(x + size, y - size); +} +``` + +The parameters are the drawing context, x and y coordinates of the +center of the point, a radius which corresponds to what the circle +would have used and whether the call is to draw a shadow (due to +limited canvas support, shadows are currently faked through extra +draws). It's good practice to ensure that the area covered by the +symbol is the same as for the circle with the given radius, this +ensures that all symbols have approximately the same visual weight. + +"shadowSize" is the default size of shadows in pixels. Set it to 0 to +remove shadows. + +"highlightColor" is the default color of the translucent overlay used +to highlight the series when the mouse hovers over it. + +The "colors" array specifies a default color theme to get colors for +the data series from. You can specify as many colors as you like, like +this: + +```js +colors: ["#d18b2c", "#dba255", "#919733"] +``` + +If there are more data series than colors, Flot will try to generate +extra colors by lightening and darkening colors in the theme. + + +## Customizing the grid ## + +```js +grid: { + show: boolean + aboveData: boolean + color: color + backgroundColor: color/gradient or null + margin: number or margin object + labelMargin: number + axisMargin: number + markings: array of markings or (fn: axes -> array of markings) + borderWidth: number or object with "top", "right", "bottom" and "left" properties with different widths + borderColor: color or null or object with "top", "right", "bottom" and "left" properties with different colors + minBorderMargin: number or null + clickable: boolean + hoverable: boolean + autoHighlight: boolean + mouseActiveRadius: number +} + +interaction: { + redrawOverlayInterval: number or -1 +} +``` + +The grid is the thing with the axes and a number of ticks. Many of the +things in the grid are configured under the individual axes, but not +all. "color" is the color of the grid itself whereas "backgroundColor" +specifies the background color inside the grid area, here null means +that the background is transparent. You can also set a gradient, see +the gradient documentation below. + +You can turn off the whole grid including tick labels by setting +"show" to false. "aboveData" determines whether the grid is drawn +above the data or below (below is default). + +"margin" is the space in pixels between the canvas edge and the grid, +which can be either a number or an object with individual margins for +each side, in the form: + +```js +margin: { + top: top margin in pixels + left: left margin in pixels + bottom: bottom margin in pixels + right: right margin in pixels +} +``` + +"labelMargin" is the space in pixels between tick labels and axis +line, and "axisMargin" is the space in pixels between axes when there +are two next to each other. + +"borderWidth" is the width of the border around the plot. Set it to 0 +to disable the border. Set it to an object with "top", "right", +"bottom" and "left" properties to use different widths. You can +also set "borderColor" if you want the border to have a different color +than the grid lines. Set it to an object with "top", "right", "bottom" +and "left" properties to use different colors. "minBorderMargin" controls +the default minimum margin around the border - it's used to make sure +that points aren't accidentally clipped by the canvas edge so by default +the value is computed from the point radius. + +"markings" is used to draw simple lines and rectangular areas in the +background of the plot. You can either specify an array of ranges on +the form { xaxis: { from, to }, yaxis: { from, to } } (with multiple +axes, you can specify coordinates for other axes instead, e.g. as +x2axis/x3axis/...) or with a function that returns such an array given +the axes for the plot in an object as the first parameter. + +You can set the color of markings by specifying "color" in the ranges +object. Here's an example array: + +```js +markings: [ { xaxis: { from: 0, to: 2 }, yaxis: { from: 10, to: 10 }, color: "#bb0000" }, ... ] +``` + +If you leave out one of the values, that value is assumed to go to the +border of the plot. So for example if you only specify { xaxis: { +from: 0, to: 2 } } it means an area that extends from the top to the +bottom of the plot in the x range 0-2. + +A line is drawn if from and to are the same, e.g. + +```js +markings: [ { yaxis: { from: 1, to: 1 } }, ... ] +``` + +would draw a line parallel to the x axis at y = 1. You can control the +line width with "lineWidth" in the range object. + +An example function that makes vertical stripes might look like this: + +```js +markings: function (axes) { + var markings = []; + for (var x = Math.floor(axes.xaxis.min); x < axes.xaxis.max; x += 2) + markings.push({ xaxis: { from: x, to: x + 1 } }); + return markings; +} +``` + +If you set "clickable" to true, the plot will listen for click events +on the plot area and fire a "plotclick" event on the placeholder with +a position and a nearby data item object as parameters. The coordinates +are available both in the unit of the axes (not in pixels) and in +global screen coordinates. + +Likewise, if you set "hoverable" to true, the plot will listen for +mouse move events on the plot area and fire a "plothover" event with +the same parameters as the "plotclick" event. If "autoHighlight" is +true (the default), nearby data items are highlighted automatically. +If needed, you can disable highlighting and control it yourself with +the highlight/unhighlight plot methods described elsewhere. + +You can use "plotclick" and "plothover" events like this: + +```js +$.plot($("#placeholder"), [ d ], { grid: { clickable: true } }); + +$("#placeholder").bind("plotclick", function (event, pos, item) { + alert("You clicked at " + pos.x + ", " + pos.y); + // axis coordinates for other axes, if present, are in pos.x2, pos.x3, ... + // if you need global screen coordinates, they are pos.pageX, pos.pageY + + if (item) { + highlight(item.series, item.datapoint); + alert("You clicked a point!"); + } +}); +``` + +The item object in this example is either null or a nearby object on the form: + +```js +item: { + datapoint: the point, e.g. [0, 2] + dataIndex: the index of the point in the data array + series: the series object + seriesIndex: the index of the series + pageX, pageY: the global screen coordinates of the point +} +``` + +For instance, if you have specified the data like this + +```js +$.plot($("#placeholder"), [ { label: "Foo", data: [[0, 10], [7, 3]] } ], ...); +``` + +and the mouse is near the point (7, 3), "datapoint" is [7, 3], +"dataIndex" will be 1, "series" is a normalized series object with +among other things the "Foo" label in series.label and the color in +series.color, and "seriesIndex" is 0. Note that plugins and options +that transform the data can shift the indexes from what you specified +in the original data array. + +If you use the above events to update some other information and want +to clear out that info in case the mouse goes away, you'll probably +also need to listen to "mouseout" events on the placeholder div. + +"mouseActiveRadius" specifies how far the mouse can be from an item +and still activate it. If there are two or more points within this +radius, Flot chooses the closest item. For bars, the top-most bar +(from the latest specified data series) is chosen. + +If you want to disable interactivity for a specific data series, you +can set "hoverable" and "clickable" to false in the options for that +series, like this: + +```js +{ data: [...], label: "Foo", clickable: false } +``` + +"redrawOverlayInterval" specifies the maximum time to delay a redraw +of interactive things (this works as a rate limiting device). The +default is capped to 60 frames per second. You can set it to -1 to +disable the rate limiting. + + +## Specifying gradients ## + +A gradient is specified like this: + +```js +{ colors: [ color1, color2, ... ] } +``` + +For instance, you might specify a background on the grid going from +black to gray like this: + +```js +grid: { + backgroundColor: { colors: ["#000", "#999"] } +} +``` + +For the series you can specify the gradient as an object that +specifies the scaling of the brightness and the opacity of the series +color, e.g. + +```js +{ colors: [{ opacity: 0.8 }, { brightness: 0.6, opacity: 0.8 } ] } +``` + +where the first color simply has its alpha scaled, whereas the second +is also darkened. For instance, for bars the following makes the bars +gradually disappear, without outline: + +```js +bars: { + show: true, + lineWidth: 0, + fill: true, + fillColor: { colors: [ { opacity: 0.8 }, { opacity: 0.1 } ] } +} +``` + +Flot currently only supports vertical gradients drawn from top to +bottom because that's what works with IE. + + +## Plot Methods ## + +The Plot object returned from the plot function has some methods you +can call: + + - highlight(series, datapoint) + + Highlight a specific datapoint in the data series. You can either + specify the actual objects, e.g. if you got them from a + "plotclick" event, or you can specify the indices, e.g. + highlight(1, 3) to highlight the fourth point in the second series + (remember, zero-based indexing). + + - unhighlight(series, datapoint) or unhighlight() + + Remove the highlighting of the point, same parameters as + highlight. + + If you call unhighlight with no parameters, e.g. as + plot.unhighlight(), all current highlights are removed. + + - setData(data) + + You can use this to reset the data used. Note that axis scaling, + ticks, legend etc. will not be recomputed (use setupGrid() to do + that). You'll probably want to call draw() afterwards. + + You can use this function to speed up redrawing a small plot if + you know that the axes won't change. Put in the new data with + setData(newdata), call draw(), and you're good to go. Note that + for large datasets, almost all the time is consumed in draw() + plotting the data so in this case don't bother. + + - setupGrid() + + Recalculate and set axis scaling, ticks, legend etc. + + Note that because of the drawing model of the canvas, this + function will immediately redraw (actually reinsert in the DOM) + the labels and the legend, but not the actual tick lines because + they're drawn on the canvas. You need to call draw() to get the + canvas redrawn. + + - draw() + + Redraws the plot canvas. + + - triggerRedrawOverlay() + + Schedules an update of an overlay canvas used for drawing + interactive things like a selection and point highlights. This + is mostly useful for writing plugins. The redraw doesn't happen + immediately, instead a timer is set to catch multiple successive + redraws (e.g. from a mousemove). You can get to the overlay by + setting up a drawOverlay hook. + + - width()/height() + + Gets the width and height of the plotting area inside the grid. + This is smaller than the canvas or placeholder dimensions as some + extra space is needed (e.g. for labels). + + - offset() + + Returns the offset of the plotting area inside the grid relative + to the document, useful for instance for calculating mouse + positions (event.pageX/Y minus this offset is the pixel position + inside the plot). + + - pointOffset({ x: xpos, y: ypos }) + + Returns the calculated offset of the data point at (x, y) in data + space within the placeholder div. If you are working with multiple + axes, you can specify the x and y axis references, e.g. + + ```js + o = pointOffset({ x: xpos, y: ypos, xaxis: 2, yaxis: 3 }) + // o.left and o.top now contains the offset within the div + ```` + + - resize() + + Tells Flot to resize the drawing canvas to the size of the + placeholder. You need to run setupGrid() and draw() afterwards as + canvas resizing is a destructive operation. This is used + internally by the resize plugin. + + - shutdown() + + Cleans up any event handlers Flot has currently registered. This + is used internally. + +There are also some members that let you peek inside the internal +workings of Flot which is useful in some cases. Note that if you change +something in the objects returned, you're changing the objects used by +Flot to keep track of its state, so be careful. + + - getData() + + Returns an array of the data series currently used in normalized + form with missing settings filled in according to the global + options. So for instance to find out what color Flot has assigned + to the data series, you could do this: + + ```js + var series = plot.getData(); + for (var i = 0; i < series.length; ++i) + alert(series[i].color); + ``` + + A notable other interesting field besides color is datapoints + which has a field "points" with the normalized data points in a + flat array (the field "pointsize" is the increment in the flat + array to get to the next point so for a dataset consisting only of + (x,y) pairs it would be 2). + + - getAxes() + + Gets an object with the axes. The axes are returned as the + attributes of the object, so for instance getAxes().xaxis is the + x axis. + + Various things are stuffed inside an axis object, e.g. you could + use getAxes().xaxis.ticks to find out what the ticks are for the + xaxis. Two other useful attributes are p2c and c2p, functions for + transforming from data point space to the canvas plot space and + back. Both returns values that are offset with the plot offset. + Check the Flot source code for the complete set of attributes (or + output an axis with console.log() and inspect it). + + With multiple axes, the extra axes are returned as x2axis, x3axis, + etc., e.g. getAxes().y2axis is the second y axis. You can check + y2axis.used to see whether the axis is associated with any data + points and y2axis.show to see if it is currently shown. + + - getPlaceholder() + + Returns placeholder that the plot was put into. This can be useful + for plugins for adding DOM elements or firing events. + + - getCanvas() + + Returns the canvas used for drawing in case you need to hack on it + yourself. You'll probably need to get the plot offset too. + + - getPlotOffset() + + Gets the offset that the grid has within the canvas as an object + with distances from the canvas edges as "left", "right", "top", + "bottom". I.e., if you draw a circle on the canvas with the center + placed at (left, top), its center will be at the top-most, left + corner of the grid. + + - getOptions() + + Gets the options for the plot, normalized, with default values + filled in. You get a reference to actual values used by Flot, so + if you modify the values in here, Flot will use the new values. + If you change something, you probably have to call draw() or + setupGrid() or triggerRedrawOverlay() to see the change. + + +## Hooks ## + +In addition to the public methods, the Plot object also has some hooks +that can be used to modify the plotting process. You can install a +callback function at various points in the process, the function then +gets access to the internal data structures in Flot. + +Here's an overview of the phases Flot goes through: + + 1. Plugin initialization, parsing options + + 2. Constructing the canvases used for drawing + + 3. Set data: parsing data specification, calculating colors, + copying raw data points into internal format, + normalizing them, finding max/min for axis auto-scaling + + 4. Grid setup: calculating axis spacing, ticks, inserting tick + labels, the legend + + 5. Draw: drawing the grid, drawing each of the series in turn + + 6. Setting up event handling for interactive features + + 7. Responding to events, if any + + 8. Shutdown: this mostly happens in case a plot is overwritten + +Each hook is simply a function which is put in the appropriate array. +You can add them through the "hooks" option, and they are also available +after the plot is constructed as the "hooks" attribute on the returned +plot object, e.g. + +```js + // define a simple draw hook + function hellohook(plot, canvascontext) { alert("hello!"); }; + + // pass it in, in an array since we might want to specify several + var plot = $.plot(placeholder, data, { hooks: { draw: [hellohook] } }); + + // we can now find it again in plot.hooks.draw[0] unless a plugin + // has added other hooks +``` + +The available hooks are described below. All hook callbacks get the +plot object as first parameter. You can find some examples of defined +hooks in the plugins bundled with Flot. + + - processOptions [phase 1] + + ```function(plot, options)``` + + Called after Flot has parsed and merged options. Useful in the + instance where customizations beyond simple merging of default + values is needed. A plugin might use it to detect that it has been + enabled and then turn on or off other options. + + + - processRawData [phase 3] + + ```function(plot, series, data, datapoints)``` + + Called before Flot copies and normalizes the raw data for the given + series. If the function fills in datapoints.points with normalized + points and sets datapoints.pointsize to the size of the points, + Flot will skip the copying/normalization step for this series. + + In any case, you might be interested in setting datapoints.format, + an array of objects for specifying how a point is normalized and + how it interferes with axis scaling. It accepts the following options: + + ```js + { + x, y: boolean, + number: boolean, + required: boolean, + defaultValue: value, + autoscale: boolean + } + ``` + + "x" and "y" specify whether the value is plotted against the x or y axis, + and is currently used only to calculate axis min-max ranges. The default + format array, for example, looks like this: + + ```js + [ + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ] + ``` + + This indicates that a point, i.e. [0, 25], consists of two values, with the + first being plotted on the x axis and the second on the y axis. + + If "number" is true, then the value must be numeric, and is set to null if + it cannot be converted to a number. + + "defaultValue" provides a fallback in case the original value is null. This + is for instance handy for bars, where one can omit the third coordinate + (the bottom of the bar), which then defaults to zero. + + If "required" is true, then the value must exist (be non-null) for the + point as a whole to be valid. If no value is provided, then the entire + point is cleared out with nulls, turning it into a gap in the series. + + "autoscale" determines whether the value is considered when calculating an + automatic min-max range for the axes that the value is plotted against. + + - processDatapoints [phase 3] + + ```function(plot, series, datapoints)``` + + Called after normalization of the given series but before finding + min/max of the data points. This hook is useful for implementing data + transformations. "datapoints" contains the normalized data points in + a flat array as datapoints.points with the size of a single point + given in datapoints.pointsize. Here's a simple transform that + multiplies all y coordinates by 2: + + ```js + function multiply(plot, series, datapoints) { + var points = datapoints.points, ps = datapoints.pointsize; + for (var i = 0; i < points.length; i += ps) + points[i + 1] *= 2; + } + ``` + + Note that you must leave datapoints in a good condition as Flot + doesn't check it or do any normalization on it afterwards. + + - processOffset [phase 4] + + ```function(plot, offset)``` + + Called after Flot has initialized the plot's offset, but before it + draws any axes or plot elements. This hook is useful for customizing + the margins between the grid and the edge of the canvas. "offset" is + an object with attributes "top", "bottom", "left" and "right", + corresponding to the margins on the four sides of the plot. + + - drawBackground [phase 5] + + ```function(plot, canvascontext)``` + + Called before all other drawing operations. Used to draw backgrounds + or other custom elements before the plot or axes have been drawn. + + - drawSeries [phase 5] + + ```function(plot, canvascontext, series)``` + + Hook for custom drawing of a single series. Called just before the + standard drawing routine has been called in the loop that draws + each series. + + - draw [phase 5] + + ```function(plot, canvascontext)``` + + Hook for drawing on the canvas. Called after the grid is drawn + (unless it's disabled or grid.aboveData is set) and the series have + been plotted (in case any points, lines or bars have been turned + on). For examples of how to draw things, look at the source code. + + - bindEvents [phase 6] + + ```function(plot, eventHolder)``` + + Called after Flot has setup its event handlers. Should set any + necessary event handlers on eventHolder, a jQuery object with the + canvas, e.g. + + ```js + function (plot, eventHolder) { + eventHolder.mousedown(function (e) { + alert("You pressed the mouse at " + e.pageX + " " + e.pageY); + }); + } + ``` + + Interesting events include click, mousemove, mouseup/down. You can + use all jQuery events. Usually, the event handlers will update the + state by drawing something (add a drawOverlay hook and call + triggerRedrawOverlay) or firing an externally visible event for + user code. See the crosshair plugin for an example. + + Currently, eventHolder actually contains both the static canvas + used for the plot itself and the overlay canvas used for + interactive features because some versions of IE get the stacking + order wrong. The hook only gets one event, though (either for the + overlay or for the static canvas). + + Note that custom plot events generated by Flot are not generated on + eventHolder, but on the div placeholder supplied as the first + argument to the plot call. You can get that with + plot.getPlaceholder() - that's probably also the one you should use + if you need to fire a custom event. + + - drawOverlay [phase 7] + + ```function (plot, canvascontext)``` + + The drawOverlay hook is used for interactive things that need a + canvas to draw on. The model currently used by Flot works the way + that an extra overlay canvas is positioned on top of the static + canvas. This overlay is cleared and then completely redrawn + whenever something interesting happens. This hook is called when + the overlay canvas is to be redrawn. + + "canvascontext" is the 2D context of the overlay canvas. You can + use this to draw things. You'll most likely need some of the + metrics computed by Flot, e.g. plot.width()/plot.height(). See the + crosshair plugin for an example. + + - shutdown [phase 8] + + ```function (plot, eventHolder)``` + + Run when plot.shutdown() is called, which usually only happens in + case a plot is overwritten by a new plot. If you're writing a + plugin that adds extra DOM elements or event handlers, you should + add a callback to clean up after you. Take a look at the section in + the [PLUGINS](PLUGINS.md) document for more info. + + +## Plugins ## + +Plugins extend the functionality of Flot. To use a plugin, simply +include its JavaScript file after Flot in the HTML page. + +If you're worried about download size/latency, you can concatenate all +the plugins you use, and Flot itself for that matter, into one big file +(make sure you get the order right), then optionally run it through a +JavaScript minifier such as YUI Compressor. + +Here's a brief explanation of how the plugin plumbings work: + +Each plugin registers itself in the global array $.plot.plugins. When +you make a new plot object with $.plot, Flot goes through this array +calling the "init" function of each plugin and merging default options +from the "option" attribute of the plugin. The init function gets a +reference to the plot object created and uses this to register hooks +and add new public methods if needed. + +See the [PLUGINS](PLUGINS.md) document for details on how to write a plugin. As the +above description hints, it's actually pretty easy. + + +## Version number ## + +The version number of Flot is available in ```$.plot.version```. diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js new file mode 100644 index 0000000000000..613939256cfc9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/index.js @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* @notice + * + * This product includes code that is based on flot-charts, which was available + * under a "MIT" license. + * + * The MIT License (MIT) + * + * Copyright (c) 2007-2014 IOLA and Ole Laursen + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import $ from 'jquery'; +if (window) window.jQuery = $; +require('./jquery.flot'); +require('./jquery.flot.time'); +require('./jquery.flot.canvas'); +require('./jquery.flot.symbol'); +require('./jquery.flot.crosshair'); +require('./jquery.flot.selection'); +require('./jquery.flot.pie'); +require('./jquery.flot.stack'); +require('./jquery.flot.threshold'); +require('./jquery.flot.fillbetween'); +require('./jquery.flot.log'); +module.exports = $; diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js new file mode 100644 index 0000000000000..b2f6dc4e433a3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.colorhelpers.js @@ -0,0 +1,180 @@ +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ + +(function($) { + $.color = {}; + + // construct color object with some convenient chainable helpers + $.color.make = function (r, g, b, a) { + var o = {}; + o.r = r || 0; + o.g = g || 0; + o.b = b || 0; + o.a = a != null ? a : 1; + + o.add = function (c, d) { + for (var i = 0; i < c.length; ++i) + o[c.charAt(i)] += d; + return o.normalize(); + }; + + o.scale = function (c, f) { + for (var i = 0; i < c.length; ++i) + o[c.charAt(i)] *= f; + return o.normalize(); + }; + + o.toString = function () { + if (o.a >= 1.0) { + return "rgb("+[o.r, o.g, o.b].join(",")+")"; + } else { + return "rgba("+[o.r, o.g, o.b, o.a].join(",")+")"; + } + }; + + o.normalize = function () { + function clamp(min, value, max) { + return value < min ? min: (value > max ? max: value); + } + + o.r = clamp(0, parseInt(o.r), 255); + o.g = clamp(0, parseInt(o.g), 255); + o.b = clamp(0, parseInt(o.b), 255); + o.a = clamp(0, o.a, 1); + return o; + }; + + o.clone = function () { + return $.color.make(o.r, o.b, o.g, o.a); + }; + + return o.normalize(); + } + + // extract CSS color property from element, going up in the DOM + // if it's "transparent" + $.color.extract = function (elem, css) { + var c; + + do { + c = elem.css(css).toLowerCase(); + // keep going until we find an element that has color, or + // we hit the body or root (have no parent) + if (c != '' && c != 'transparent') + break; + elem = elem.parent(); + } while (elem.length && !$.nodeName(elem.get(0), "body")); + + // catch Safari's way of signalling transparent + if (c == "rgba(0, 0, 0, 0)") + c = "transparent"; + + return $.color.parse(c); + } + + // parse CSS color string (like "rgb(10, 32, 43)" or "#fff"), + // returns color object, if parsing failed, you get black (0, 0, + // 0) out + $.color.parse = function (str) { + var res, m = $.color.make; + + // Look for rgb(num,num,num) + if (res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)) + return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10)); + + // Look for rgba(num,num,num,num) + if (res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) + return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4])); + + // Look for rgb(num%,num%,num%) + if (res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)) + return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55); + + // Look for rgba(num%,num%,num%,num) + if (res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)) + return m(parseFloat(res[1])*2.55, parseFloat(res[2])*2.55, parseFloat(res[3])*2.55, parseFloat(res[4])); + + // Look for #a0b1c2 + if (res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)) + return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)); + + // Look for #fff + if (res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)) + return m(parseInt(res[1]+res[1], 16), parseInt(res[2]+res[2], 16), parseInt(res[3]+res[3], 16)); + + // Otherwise, we're most likely dealing with a named color + var name = $.trim(str).toLowerCase(); + if (name == "transparent") + return m(255, 255, 255, 0); + else { + // default to black + res = lookupColors[name] || [0, 0, 0]; + return m(res[0], res[1], res[2]); + } + } + + var lookupColors = { + aqua:[0,255,255], + azure:[240,255,255], + beige:[245,245,220], + black:[0,0,0], + blue:[0,0,255], + brown:[165,42,42], + cyan:[0,255,255], + darkblue:[0,0,139], + darkcyan:[0,139,139], + darkgrey:[169,169,169], + darkgreen:[0,100,0], + darkkhaki:[189,183,107], + darkmagenta:[139,0,139], + darkolivegreen:[85,107,47], + darkorange:[255,140,0], + darkorchid:[153,50,204], + darkred:[139,0,0], + darksalmon:[233,150,122], + darkviolet:[148,0,211], + fuchsia:[255,0,255], + gold:[255,215,0], + green:[0,128,0], + indigo:[75,0,130], + khaki:[240,230,140], + lightblue:[173,216,230], + lightcyan:[224,255,255], + lightgreen:[144,238,144], + lightgrey:[211,211,211], + lightpink:[255,182,193], + lightyellow:[255,255,224], + lime:[0,255,0], + magenta:[255,0,255], + maroon:[128,0,0], + navy:[0,0,128], + olive:[128,128,0], + orange:[255,165,0], + pink:[255,192,203], + purple:[128,0,128], + violet:[128,0,128], + red:[255,0,0], + silver:[192,192,192], + white:[255,255,255], + yellow:[255,255,0] + }; +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js new file mode 100644 index 0000000000000..29328d5812127 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.canvas.js @@ -0,0 +1,345 @@ +/* Flot plugin for drawing all elements of a plot on the canvas. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Flot normally produces certain elements, like axis labels and the legend, using +HTML elements. This permits greater interactivity and customization, and often +looks better, due to cross-browser canvas text inconsistencies and limitations. + +It can also be desirable to render the plot entirely in canvas, particularly +if the goal is to save it as an image, or if Flot is being used in a context +where the HTML DOM does not exist, as is the case within Node.js. This plugin +switches out Flot's standard drawing operations for canvas-only replacements. + +Currently the plugin supports only axis labels, but it will eventually allow +every element of the plot to be rendered directly to canvas. + +The plugin supports these options: + +{ + canvas: boolean +} + +The "canvas" option controls whether full canvas drawing is enabled, making it +possible to toggle on and off. This is useful when a plot uses HTML text in the +browser, but needs to redraw with canvas text when exporting as an image. + +*/ + +(function($) { + + var options = { + canvas: true + }; + + var render, getTextInfo, addText; + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + function init(plot, classes) { + + var Canvas = classes.Canvas; + + // We only want to replace the functions once; the second time around + // we would just get our new function back. This whole replacing of + // prototype functions is a disaster, and needs to be changed ASAP. + + if (render == null) { + getTextInfo = Canvas.prototype.getTextInfo, + addText = Canvas.prototype.addText, + render = Canvas.prototype.render; + } + + // Finishes rendering the canvas, including overlaid text + + Canvas.prototype.render = function() { + + if (!plot.getOptions().canvas) { + return render.call(this); + } + + var context = this.context, + cache = this._textCache; + + // For each text layer, render elements marked as active + + context.save(); + context.textBaseline = "middle"; + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + var layerCache = cache[layerKey]; + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey], + updateStyles = true; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var info = styleCache[key], + positions = info.positions, + lines = info.lines; + + // Since every element at this level of the cache have the + // same font and fill styles, we can just change them once + // using the values from the first element. + + if (updateStyles) { + context.fillStyle = info.font.color; + context.font = info.font.definition; + updateStyles = false; + } + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + for (var j = 0, line; line = position.lines[j]; j++) { + context.fillText(lines[j].text, line[0], line[1]); + } + } else { + positions.splice(i--, 1); + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + } + } + + context.restore(); + }; + + // Creates (if necessary) and returns a text info object. + // + // When the canvas option is set, the object looks like this: + // + // { + // width: Width of the text's bounding box. + // height: Height of the text's bounding box. + // positions: Array of positions at which this text is drawn. + // lines: [{ + // height: Height of this line. + // widths: Width of this line. + // text: Text on this line. + // }], + // font: { + // definition: Canvas font property string. + // color: Color of the text. + // }, + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // lines: Array of [x, y] coordinates at which to draw the line. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + if (!plot.getOptions().canvas) { + return getTextInfo.call(this, layer, text, font, angle, width); + } + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number + + text = "" + text; + + // If the font is a font-spec object, generate a CSS definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + if (info == null) { + + var context = this.context; + + // If the font was provided as CSS, create a div with those + // classes and examine it to generate a canvas font spec. + + if (typeof font !== "object") { + + var element = $("<div> </div>") + .css("position", "absolute") + .addClass(typeof font === "string" ? font : null) + .appendTo(this.getTextLayer(layer)); + + font = { + lineHeight: element.height(), + style: element.css("font-style"), + variant: element.css("font-variant"), + weight: element.css("font-weight"), + family: element.css("font-family"), + color: element.css("color") + }; + + // Setting line-height to 1, without units, sets it equal + // to the font-size, even if the font-size is abstract, + // like 'smaller'. This enables us to read the real size + // via the element's height, working around browsers that + // return the literal 'smaller' value. + + font.size = element.css("line-height", 1).height(); + + element.remove(); + } + + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px " + font.family; + + // Create a new info object, initializing the dimensions to + // zero so we can count them up line-by-line. + + info = styleCache[text] = { + width: 0, + height: 0, + positions: [], + lines: [], + font: { + definition: textStyle, + color: font.color + } + }; + + context.save(); + context.font = textStyle; + + // Canvas can't handle multi-line strings; break on various + // newlines, including HTML brs, to build a list of lines. + // Note that we could split directly on regexps, but IE < 9 is + // broken; revisit when we drop IE 7/8 support. + + var lines = (text + "").replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n"); + + for (var i = 0; i < lines.length; ++i) { + + var lineText = lines[i], + measured = context.measureText(lineText); + + info.width = Math.max(measured.width, info.width); + info.height += font.lineHeight; + + info.lines.push({ + text: lineText, + width: measured.width, + height: font.lineHeight + }); + } + + context.restore(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + if (!plot.getOptions().canvas) { + return addText.call(this, layer, x, y, text, font, angle, width, halign, valign); + } + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions, + lines = info.lines; + + // Text is drawn with baseline 'middle', which we need to account + // for by adding half a line's height to the y position. + + y += info.height / lines.length / 2; + + // Tweak the initial y-position to match vertical alignment + + if (valign == "middle") { + y = Math.round(y - info.height / 2); + } else if (valign == "bottom") { + y = Math.round(y - info.height); + } else { + y = Math.round(y); + } + + // FIXME: LEGACY BROWSER FIX + // AFFECTS: Opera < 12.00 + + // Offset the y coordinate, since Opera is off pretty + // consistently compared to the other browsers. + + if (!!(window.opera && window.opera.version().split(".")[0] < 12)) { + y -= 2; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + position = { + active: true, + lines: [], + x: x, + y: y + }; + + positions.push(position); + + // Fill in the x & y positions of each line, adjusting them + // individually for horizontal alignment. + + for (var i = 0, line; line = lines[i]; i++) { + if (halign == "center") { + position.lines.push([Math.round(x - line.width / 2), y]); + } else if (halign == "right") { + position.lines.push([Math.round(x - line.width), y]); + } else { + position.lines.push([Math.round(x), y]); + } + y += line.height; + } + }; + } + + $.plot.plugins.push({ + init: init, + options: options, + name: "canvas", + version: "1.0" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js new file mode 100644 index 0000000000000..2f9b257971499 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.categories.js @@ -0,0 +1,190 @@ +/* Flot plugin for plotting textual data or categories. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin +allows you to plot such a dataset directly. + +To enable it, you must specify mode: "categories" on the axis with the textual +labels, e.g. + + $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); + +By default, the labels are ordered as they are met in the data series. If you +need a different ordering, you can specify "categories" on the axis options +and list the categories there: + + xaxis: { + mode: "categories", + categories: ["February", "March", "April"] + } + +If you need to customize the distances between the categories, you can specify +"categories" as an object mapping labels to values + + xaxis: { + mode: "categories", + categories: { "February": 1, "March": 3, "April": 4 } + } + +If you don't specify all categories, the remaining categories will be numbered +from the max value plus 1 (with a spacing of 1 between each). + +Internally, the plugin works by transforming the input data through an auto- +generated mapping where the first category becomes 0, the second 1, etc. +Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this +is visible in hover and click events that return numbers rather than the +category labels). The plugin also overrides the tick generator to spit out the +categories as ticks instead of the values. + +If you need to map a value back to its label, the mapping is always accessible +as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories. + +*/ + +(function ($) { + var options = { + xaxis: { + categories: null + }, + yaxis: { + categories: null + } + }; + + function processRawData(plot, series, data, datapoints) { + // if categories are enabled, we need to disable + // auto-transformation to numbers so the strings are intact + // for later processing + + var xCategories = series.xaxis.options.mode == "categories", + yCategories = series.yaxis.options.mode == "categories"; + + if (!(xCategories || yCategories)) + return; + + var format = datapoints.format; + + if (!format) { + // FIXME: auto-detection should really not be defined here + var s = series; + format = []; + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + datapoints.format = format; + } + + for (var m = 0; m < format.length; ++m) { + if (format[m].x && xCategories) + format[m].number = false; + + if (format[m].y && yCategories) + format[m].number = false; + } + } + + function getNextIndex(categories) { + var index = -1; + + for (var v in categories) + if (categories[v] > index) + index = categories[v]; + + return index + 1; + } + + function categoriesTickGenerator(axis) { + var res = []; + for (var label in axis.categories) { + var v = axis.categories[label]; + if (v >= axis.min && v <= axis.max) + res.push([v, label]); + } + + res.sort(function (a, b) { return a[0] - b[0]; }); + + return res; + } + + function setupCategoriesForAxis(series, axis, datapoints) { + if (series[axis].options.mode != "categories") + return; + + if (!series[axis].categories) { + // parse options + var c = {}, o = series[axis].options.categories || {}; + if ($.isArray(o)) { + for (var i = 0; i < o.length; ++i) + c[o[i]] = i; + } + else { + for (var v in o) + c[v] = o[v]; + } + + series[axis].categories = c; + } + + // fix ticks + if (!series[axis].options.ticks) + series[axis].options.ticks = categoriesTickGenerator; + + transformPointsOnAxis(datapoints, axis, series[axis].categories); + } + + function transformPointsOnAxis(datapoints, axis, categories) { + // go through the points, transforming them + var points = datapoints.points, + ps = datapoints.pointsize, + format = datapoints.format, + formatColumn = axis.charAt(0), + index = getNextIndex(categories); + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + + for (var m = 0; m < ps; ++m) { + var val = points[i + m]; + + if (val == null || !format[m][formatColumn]) + continue; + + if (!(val in categories)) { + categories[val] = index; + ++index; + } + + points[i + m] = categories[val]; + } + } + } + + function processDatapoints(plot, series, datapoints) { + setupCategoriesForAxis(series, "xaxis", datapoints); + setupCategoriesForAxis(series, "yaxis", datapoints); + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.processDatapoints.push(processDatapoints); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'categories', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js new file mode 100644 index 0000000000000..5111695e3d12c --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.crosshair.js @@ -0,0 +1,176 @@ +/* Flot plugin for showing crosshairs when the mouse hovers over the plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + + crosshair: { + mode: null or "x" or "y" or "xy" + color: color + lineWidth: number + } + +Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical +crosshair that lets you trace the values on the x axis, "y" enables a +horizontal crosshair and "xy" enables them both. "color" is the color of the +crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of +the drawn lines (default is 1). + +The plugin also adds four public methods: + + - setCrosshair( pos ) + + Set the position of the crosshair. Note that this is cleared if the user + moves the mouse. "pos" is in coordinates of the plot and should be on the + form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple + axes), which is coincidentally the same format as what you get from a + "plothover" event. If "pos" is null, the crosshair is cleared. + + - clearCrosshair() + + Clear the crosshair. + + - lockCrosshair(pos) + + Cause the crosshair to lock to the current location, no longer updating if + the user moves the mouse. Optionally supply a position (passed on to + setCrosshair()) to move it to. + + Example usage: + + var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } }; + $("#graph").bind( "plothover", function ( evt, position, item ) { + if ( item ) { + // Lock the crosshair to the data point being hovered + myFlot.lockCrosshair({ + x: item.datapoint[ 0 ], + y: item.datapoint[ 1 ] + }); + } else { + // Return normal crosshair operation + myFlot.unlockCrosshair(); + } + }); + + - unlockCrosshair() + + Free the crosshair to move again after locking it. +*/ + +(function ($) { + var options = { + crosshair: { + mode: null, // one of null, "x", "y" or "xy", + color: "rgba(170, 0, 0, 0.80)", + lineWidth: 1 + } + }; + + function init(plot) { + // position of crosshair in pixels + var crosshair = { x: -1, y: -1, locked: false }; + + plot.setCrosshair = function setCrosshair(pos) { + if (!pos) + crosshair.x = -1; + else { + var o = plot.p2c(pos); + crosshair.x = Math.max(0, Math.min(o.left, plot.width())); + crosshair.y = Math.max(0, Math.min(o.top, plot.height())); + } + + plot.triggerRedrawOverlay(); + }; + + plot.clearCrosshair = plot.setCrosshair; // passes null for pos + + plot.lockCrosshair = function lockCrosshair(pos) { + if (pos) + plot.setCrosshair(pos); + crosshair.locked = true; + }; + + plot.unlockCrosshair = function unlockCrosshair() { + crosshair.locked = false; + }; + + function onMouseOut(e) { + if (crosshair.locked) + return; + + if (crosshair.x != -1) { + crosshair.x = -1; + plot.triggerRedrawOverlay(); + } + } + + function onMouseMove(e) { + if (crosshair.locked) + return; + + if (plot.getSelection && plot.getSelection()) { + crosshair.x = -1; // hide the crosshair while selecting + return; + } + + var offset = plot.offset(); + crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width())); + crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height())); + plot.triggerRedrawOverlay(); + } + + plot.hooks.bindEvents.push(function (plot, eventHolder) { + if (!plot.getOptions().crosshair.mode) + return; + + eventHolder.mouseout(onMouseOut); + eventHolder.mousemove(onMouseMove); + }); + + plot.hooks.drawOverlay.push(function (plot, ctx) { + var c = plot.getOptions().crosshair; + if (!c.mode) + return; + + var plotOffset = plot.getPlotOffset(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + if (crosshair.x != -1) { + var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0; + + ctx.strokeStyle = c.color; + ctx.lineWidth = c.lineWidth; + ctx.lineJoin = "round"; + + ctx.beginPath(); + if (c.mode.indexOf("x") != -1) { + var drawX = Math.floor(crosshair.x) + adj; + ctx.moveTo(drawX, 0); + ctx.lineTo(drawX, plot.height()); + } + if (c.mode.indexOf("y") != -1) { + var drawY = Math.floor(crosshair.y) + adj; + ctx.moveTo(0, drawY); + ctx.lineTo(plot.width(), drawY); + } + ctx.stroke(); + } + ctx.restore(); + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mouseout", onMouseOut); + eventHolder.unbind("mousemove", onMouseMove); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'crosshair', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js new file mode 100644 index 0000000000000..655036e0db846 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.errorbars.js @@ -0,0 +1,353 @@ +/* Flot plugin for plotting error bars. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Error bars are used to show standard deviation and other statistical +properties in a plot. + +* Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com + +This plugin allows you to plot error-bars over points. Set "errorbars" inside +the points series to the axis name over which there will be error values in +your data array (*even* if you do not intend to plot them later, by setting +"show: null" on xerr/yerr). + +The plugin supports these options: + + series: { + points: { + errorbars: "x" or "y" or "xy", + xerr: { + show: null/false or true, + asymmetric: null/false or true, + upperCap: null or "-" or function, + lowerCap: null or "-" or function, + color: null or color, + radius: null or number + }, + yerr: { same options as xerr } + } + } + +Each data point array is expected to be of the type: + + "x" [ x, y, xerr ] + "y" [ x, y, yerr ] + "xy" [ x, y, xerr, yerr ] + +Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and +equivalently for yerr. E.g., a datapoint for the "xy" case with symmetric +error-bars on X and asymmetric on Y would be: + + [ x, y, xerr, yerr_lower, yerr_upper ] + +By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will +draw a small cap perpendicular to the error bar. They can also be set to a +user-defined drawing function, with (ctx, x, y, radius) as parameters, as e.g.: + + function drawSemiCircle( ctx, x, y, radius ) { + ctx.beginPath(); + ctx.arc( x, y, radius, 0, Math.PI, false ); + ctx.moveTo( x - radius, y ); + ctx.lineTo( x + radius, y ); + ctx.stroke(); + } + +Color and radius both default to the same ones of the points series if not +set. The independent radius parameter on xerr/yerr is useful for the case when +we may want to add error-bars to a line, without showing the interconnecting +points (with radius: 0), and still showing end caps on the error-bars. +shadowSize and lineWidth are derived as well from the points series. + +*/ + +(function ($) { + var options = { + series: { + points: { + errorbars: null, //should be 'x', 'y' or 'xy' + xerr: { err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}, + yerr: { err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null} + } + } + }; + + function processRawData(plot, series, data, datapoints){ + if (!series.points.errorbars) + return; + + // x,y values + var format = [ + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ]; + + var errors = series.points.errorbars; + // error bars - first X then Y + if (errors == 'x' || errors == 'xy') { + // lower / upper error + if (series.points.xerr.asymmetric) { + format.push({ x: true, number: true, required: true }); + format.push({ x: true, number: true, required: true }); + } else + format.push({ x: true, number: true, required: true }); + } + if (errors == 'y' || errors == 'xy') { + // lower / upper error + if (series.points.yerr.asymmetric) { + format.push({ y: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + } else + format.push({ y: true, number: true, required: true }); + } + datapoints.format = format; + } + + function parseErrors(series, i){ + + var points = series.datapoints.points; + + // read errors from points array + var exl = null, + exu = null, + eyl = null, + eyu = null; + var xerr = series.points.xerr, + yerr = series.points.yerr; + + var eb = series.points.errorbars; + // error bars - first X + if (eb == 'x' || eb == 'xy') { + if (xerr.asymmetric) { + exl = points[i + 2]; + exu = points[i + 3]; + if (eb == 'xy') + if (yerr.asymmetric){ + eyl = points[i + 4]; + eyu = points[i + 5]; + } else eyl = points[i + 4]; + } else { + exl = points[i + 2]; + if (eb == 'xy') + if (yerr.asymmetric) { + eyl = points[i + 3]; + eyu = points[i + 4]; + } else eyl = points[i + 3]; + } + // only Y + } else if (eb == 'y') + if (yerr.asymmetric) { + eyl = points[i + 2]; + eyu = points[i + 3]; + } else eyl = points[i + 2]; + + // symmetric errors? + if (exu == null) exu = exl; + if (eyu == null) eyu = eyl; + + var errRanges = [exl, exu, eyl, eyu]; + // nullify if not showing + if (!xerr.show){ + errRanges[0] = null; + errRanges[1] = null; + } + if (!yerr.show){ + errRanges[2] = null; + errRanges[3] = null; + } + return errRanges; + } + + function drawSeriesErrors(plot, ctx, s){ + + var points = s.datapoints.points, + ps = s.datapoints.pointsize, + ax = [s.xaxis, s.yaxis], + radius = s.points.radius, + err = [s.points.xerr, s.points.yerr]; + + //sanity check, in case some inverted axis hack is applied to flot + var invertX = false; + if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) { + invertX = true; + var tmp = err[0].lowerCap; + err[0].lowerCap = err[0].upperCap; + err[0].upperCap = tmp; + } + + var invertY = false; + if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) { + invertY = true; + var tmp = err[1].lowerCap; + err[1].lowerCap = err[1].upperCap; + err[1].upperCap = tmp; + } + + for (var i = 0; i < s.datapoints.points.length; i += ps) { + + //parse + var errRanges = parseErrors(s, i); + + //cycle xerr & yerr + for (var e = 0; e < err.length; e++){ + + var minmax = [ax[e].min, ax[e].max]; + + //draw this error? + if (errRanges[e * err.length]){ + + //data coordinates + var x = points[i], + y = points[i + 1]; + + //errorbar ranges + var upper = [x, y][e] + errRanges[e * err.length + 1], + lower = [x, y][e] - errRanges[e * err.length]; + + //points outside of the canvas + if (err[e].err == 'x') + if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) + continue; + if (err[e].err == 'y') + if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) + continue; + + // prevent errorbars getting out of the canvas + var drawUpper = true, + drawLower = true; + + if (upper > minmax[1]) { + drawUpper = false; + upper = minmax[1]; + } + if (lower < minmax[0]) { + drawLower = false; + lower = minmax[0]; + } + + //sanity check, in case some inverted axis hack is applied to flot + if ((err[e].err == 'x' && invertX) || (err[e].err == 'y' && invertY)) { + //swap coordinates + var tmp = lower; + lower = upper; + upper = tmp; + tmp = drawLower; + drawLower = drawUpper; + drawUpper = tmp; + tmp = minmax[0]; + minmax[0] = minmax[1]; + minmax[1] = tmp; + } + + // convert to pixels + x = ax[0].p2c(x), + y = ax[1].p2c(y), + upper = ax[e].p2c(upper); + lower = ax[e].p2c(lower); + minmax[0] = ax[e].p2c(minmax[0]); + minmax[1] = ax[e].p2c(minmax[1]); + + //same style as points by default + var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth, + sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize; + + //shadow as for points + if (lw > 0 && sw > 0) { + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w/2, minmax); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w/2, minmax); + } + + ctx.strokeStyle = err[e].color? err[e].color: s.color; + ctx.lineWidth = lw; + //draw it + drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax); + } + } + } + } + + function drawError(ctx,err,x,y,upper,lower,drawUpper,drawLower,radius,offset,minmax){ + + //shadow offset + y += offset; + upper += offset; + lower += offset; + + // error bar - avoid plotting over circles + if (err.err == 'x'){ + if (upper > x + radius) drawPath(ctx, [[upper,y],[Math.max(x + radius,minmax[0]),y]]); + else drawUpper = false; + if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius,minmax[1]),y],[lower,y]] ); + else drawLower = false; + } + else { + if (upper < y - radius) drawPath(ctx, [[x,upper],[x,Math.min(y - radius,minmax[0])]] ); + else drawUpper = false; + if (lower > y + radius) drawPath(ctx, [[x,Math.max(y + radius,minmax[1])],[x,lower]] ); + else drawLower = false; + } + + //internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps + //this is a way to get errorbars on lines without visible connecting dots + radius = err.radius != null? err.radius: radius; + + // upper cap + if (drawUpper) { + if (err.upperCap == '-'){ + if (err.err=='x') drawPath(ctx, [[upper,y - radius],[upper,y + radius]] ); + else drawPath(ctx, [[x - radius,upper],[x + radius,upper]] ); + } else if ($.isFunction(err.upperCap)){ + if (err.err=='x') err.upperCap(ctx, upper, y, radius); + else err.upperCap(ctx, x, upper, radius); + } + } + // lower cap + if (drawLower) { + if (err.lowerCap == '-'){ + if (err.err=='x') drawPath(ctx, [[lower,y - radius],[lower,y + radius]] ); + else drawPath(ctx, [[x - radius,lower],[x + radius,lower]] ); + } else if ($.isFunction(err.lowerCap)){ + if (err.err=='x') err.lowerCap(ctx, lower, y, radius); + else err.lowerCap(ctx, x, lower, radius); + } + } + } + + function drawPath(ctx, pts){ + ctx.beginPath(); + ctx.moveTo(pts[0][0], pts[0][1]); + for (var p=1; p < pts.length; p++) + ctx.lineTo(pts[p][0], pts[p][1]); + ctx.stroke(); + } + + function draw(plot, ctx){ + var plotOffset = plot.getPlotOffset(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + $.each(plot.getData(), function (i, s) { + if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) + drawSeriesErrors(plot, ctx, s); + }); + ctx.restore(); + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.draw.push(draw); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'errorbars', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js new file mode 100644 index 0000000000000..18b15d26db8c9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.fillbetween.js @@ -0,0 +1,226 @@ +/* Flot plugin for computing bottoms for filled line and bar charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The case: you've got two series that you want to fill the area between. In Flot +terms, you need to use one as the fill bottom of the other. You can specify the +bottom of each data point as the third coordinate manually, or you can use this +plugin to compute it for you. + +In order to name the other series, you need to give it an id, like this: + + var dataset = [ + { data: [ ... ], id: "foo" } , // use default bottom + { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom + ]; + + $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); + +As a convenience, if the id given is a number that doesn't appear as an id in +the series, it is interpreted as the index in the array instead (so fillBetween: +0 can also mean the first series). + +Internally, the plugin modifies the datapoints in each series. For line series, +extra data points might be inserted through interpolation. Note that at points +where the bottom line is not defined (due to a null point or start/end of line), +the current line will show a gap too. The algorithm comes from the +jquery.flot.stack.js plugin, possibly some code could be shared. + +*/ + +(function ( $ ) { + + var options = { + series: { + fillBetween: null // or number + } + }; + + function init( plot ) { + + function findBottomSeries( s, allseries ) { + + var i; + + for ( i = 0; i < allseries.length; ++i ) { + if ( allseries[ i ].id === s.fillBetween ) { + return allseries[ i ]; + } + } + + if ( typeof s.fillBetween === "number" ) { + if ( s.fillBetween < 0 || s.fillBetween >= allseries.length ) { + return null; + } + return allseries[ s.fillBetween ]; + } + + return null; + } + + function computeFillBottoms( plot, s, datapoints ) { + + if ( s.fillBetween == null ) { + return; + } + + var other = findBottomSeries( s, plot.getData() ); + + if ( !other ) { + return; + } + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + withbottom = ps > 2 && datapoints.format[2].y, + withsteps = withlines && s.lines.steps, + fromgap = true, + i = 0, + j = 0, + l, m; + + while ( true ) { + + if ( i >= points.length ) { + break; + } + + l = newpoints.length; + + if ( points[ i ] == null ) { + + // copy gaps + + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + + i += ps; + + } else if ( j >= otherpoints.length ) { + + // for lines, we can't use the rest of the points + + if ( !withlines ) { + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + } + + i += ps; + + } else if ( otherpoints[ j ] == null ) { + + // oops, got a gap + + for ( m = 0; m < ps; ++m ) { + newpoints.push( null ); + } + + fromgap = true; + j += otherps; + + } else { + + // cases where we actually got two points + + px = points[ i ]; + py = points[ i + 1 ]; + qx = otherpoints[ j ]; + qy = otherpoints[ j + 1 ]; + bottom = 0; + + if ( px === qx ) { + + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + + //newpoints[ l + 1 ] += qy; + bottom = qy; + + i += ps; + j += otherps; + + } else if ( px > qx ) { + + // we got past point below, might need to + // insert interpolated extra point + + if ( withlines && i > 0 && points[ i - ps ] != null ) { + intery = py + ( points[ i - ps + 1 ] - py ) * ( qx - px ) / ( points[ i - ps ] - px ); + newpoints.push( qx ); + newpoints.push( intery ); + for ( m = 2; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + bottom = qy; + } + + j += otherps; + + } else { // px < qx + + // if we come from a gap, we just skip this point + + if ( fromgap && withlines ) { + i += ps; + continue; + } + + for ( m = 0; m < ps; ++m ) { + newpoints.push( points[ i + m ] ); + } + + // we might be able to interpolate a point below, + // this can give us a better y + + if ( withlines && j > 0 && otherpoints[ j - otherps ] != null ) { + bottom = qy + ( otherpoints[ j - otherps + 1 ] - qy ) * ( px - qx ) / ( otherpoints[ j - otherps ] - qx ); + } + + //newpoints[l + 1] += bottom; + + i += ps; + } + + fromgap = false; + + if ( l !== newpoints.length && withbottom ) { + newpoints[ l + 2 ] = bottom; + } + } + + // maintain the line steps invariant + + if ( withsteps && l !== newpoints.length && l > 0 && + newpoints[ l ] !== null && + newpoints[ l ] !== newpoints[ l - ps ] && + newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ] ) { + for (m = 0; m < ps; ++m) { + newpoints[ l + ps + m ] = newpoints[ l + m ]; + } + newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push( computeFillBottoms ); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: "fillbetween", + version: "1.0" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js new file mode 100644 index 0000000000000..178f0e69069ef --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.image.js @@ -0,0 +1,241 @@ +/* Flot plugin for plotting images. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and +(x2, y2) are where you intend the two opposite corners of the image to end up +in the plot. Image must be a fully loaded JavaScript image (you can make one +with new Image()). If the image is not complete, it's skipped when plotting. + +There are two helpers included for retrieving images. The easiest work the way +that you put in URLs instead of images in the data, like this: + + [ "myimage.png", 0, 0, 10, 10 ] + +Then call $.plot.image.loadData( data, options, callback ) where data and +options are the same as you pass in to $.plot. This loads the images, replaces +the URLs in the data with the corresponding images and calls "callback" when +all images are loaded (or failed loading). In the callback, you can then call +$.plot with the data set. See the included example. + +A more low-level helper, $.plot.image.load(urls, callback) is also included. +Given a list of URLs, it calls callback with an object mapping from URL to +Image object when all images are loaded or have failed loading. + +The plugin supports these options: + + series: { + images: { + show: boolean + anchor: "corner" or "center" + alpha: [ 0, 1 ] + } + } + +They can be specified for a specific series: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + images: { ... } + ]) + +Note that because the data format is different from usual data points, you +can't use images with anything else in a specific data series. + +Setting "anchor" to "center" causes the pixels in the image to be anchored at +the corner pixel centers inside of at the pixel corners, effectively letting +half a pixel stick out to each side in the plot. + +A possible future direction could be support for tiling for large images (like +Google Maps). + +*/ + +(function ($) { + var options = { + series: { + images: { + show: false, + alpha: 1, + anchor: "corner" // or "center" + } + } + }; + + $.plot.image = {}; + + $.plot.image.loadDataImages = function (series, options, callback) { + var urls = [], points = []; + + var defaultShow = options.series.images.show; + + $.each(series, function (i, s) { + if (!(defaultShow || s.images.show)) + return; + + if (s.data) + s = s.data; + + $.each(s, function (i, p) { + if (typeof p[0] == "string") { + urls.push(p[0]); + points.push(p); + } + }); + }); + + $.plot.image.load(urls, function (loadedImages) { + $.each(points, function (i, p) { + var url = p[0]; + if (loadedImages[url]) + p[0] = loadedImages[url]; + }); + + callback(); + }); + } + + $.plot.image.load = function (urls, callback) { + var missing = urls.length, loaded = {}; + if (missing == 0) + callback({}); + + $.each(urls, function (i, url) { + var handler = function () { + --missing; + + loaded[url] = this; + + if (missing == 0) + callback(loaded); + }; + + $('<img />').load(handler).error(handler).attr('src', url); + }); + }; + + function drawSeries(plot, ctx, series) { + var plotOffset = plot.getPlotOffset(); + + if (!series.images || !series.images.show) + return; + + var points = series.datapoints.points, + ps = series.datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var img = points[i], + x1 = points[i + 1], y1 = points[i + 2], + x2 = points[i + 3], y2 = points[i + 4], + xaxis = series.xaxis, yaxis = series.yaxis, + tmp; + + // actually we should check img.complete, but it + // appears to be a somewhat unreliable indicator in + // IE6 (false even after load event) + if (!img || img.width <= 0 || img.height <= 0) + continue; + + if (x1 > x2) { + tmp = x2; + x2 = x1; + x1 = tmp; + } + if (y1 > y2) { + tmp = y2; + y2 = y1; + y1 = tmp; + } + + // if the anchor is at the center of the pixel, expand the + // image by 1/2 pixel in each direction + if (series.images.anchor == "center") { + tmp = 0.5 * (x2-x1) / (img.width - 1); + x1 -= tmp; + x2 += tmp; + tmp = 0.5 * (y2-y1) / (img.height - 1); + y1 -= tmp; + y2 += tmp; + } + + // clip + if (x1 == x2 || y1 == y2 || + x1 >= xaxis.max || x2 <= xaxis.min || + y1 >= yaxis.max || y2 <= yaxis.min) + continue; + + var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height; + if (x1 < xaxis.min) { + sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1); + x1 = xaxis.min; + } + + if (x2 > xaxis.max) { + sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1); + x2 = xaxis.max; + } + + if (y1 < yaxis.min) { + sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1); + y1 = yaxis.min; + } + + if (y2 > yaxis.max) { + sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1); + y2 = yaxis.max; + } + + x1 = xaxis.p2c(x1); + x2 = xaxis.p2c(x2); + y1 = yaxis.p2c(y1); + y2 = yaxis.p2c(y2); + + // the transformation may have swapped us + if (x1 > x2) { + tmp = x2; + x2 = x1; + x1 = tmp; + } + if (y1 > y2) { + tmp = y2; + y2 = y1; + y1 = tmp; + } + + tmp = ctx.globalAlpha; + ctx.globalAlpha *= series.images.alpha; + ctx.drawImage(img, + sx1, sy1, sx2 - sx1, sy2 - sy1, + x1 + plotOffset.left, y1 + plotOffset.top, + x2 - x1, y2 - y1); + ctx.globalAlpha = tmp; + } + } + + function processRawData(plot, series, data, datapoints) { + if (!series.images.show) + return; + + // format is Image, x1, y1, x2, y2 (opposite corners) + datapoints.format = [ + { required: true }, + { x: true, number: true, required: true }, + { y: true, number: true, required: true }, + { x: true, number: true, required: true }, + { y: true, number: true, required: true } + ]; + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.drawSeries.push(drawSeries); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'image', + version: '1.1' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js new file mode 100644 index 0000000000000..43db1cc3d93db --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.js @@ -0,0 +1,3168 @@ +/* JavaScript plotting library for jQuery, version 0.8.3. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ + +// first an inline dependency, jquery.colorhelpers.js, we inline it here +// for convenience + +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ +(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i<c.length;++i)o[c.charAt(i)]+=d;return o.normalize()};o.scale=function(c,f){for(var i=0;i<c.length;++i)o[c.charAt(i)]*=f;return o.normalize()};o.toString=function(){if(o.a>=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return value<min?min:value>max?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); + +// the actual Flot code +(function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + // A shim to provide 'detach' to jQuery versions prior to 1.4. Using a DOM + // operation produces the same effect as detach, i.e. removing the element + // without touching its jQuery data. + + // Do not merge this into Flot 0.9, since it requires jQuery 1.4.4+. + + if (!$.fn.detach) { + $.fn.detach = function() { + return this.each(function() { + if (this.parentNode) { + this.parentNode.removeChild( this ); + } + }); + }; + } + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 <canvas> tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("<div class='flot-text'></div>") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("<div></div>") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("<div></div>").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + }; + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of columns in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85, // set to 0 to avoid background + sorted: null // default to no legend sorting + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null // number or [number, "unit"] + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // "left", "right", or "center" + horizontal: false, + zero: true + }, + shadowSize: 3, + highlightColor: null + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + margin: 0, // distance from the canvas edge to the grid + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + interaction: { + redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow + }, + hooks: {} + }, + surface = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + processOffset: [], + drawBackground: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return surface.element; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10) + }; + }; + plot.shutdown = shutdown; + plot.destroy = function () { + shutdown(); + placeholder.removeData("plot").empty(); + + series = []; + options = null; + surface = null; + overlay = null; + eventHolder = null; + ctx = null; + octx = null; + xaxes = []; + yaxes = []; + hooks = null; + highlights = []; + plot = null; + }; + plot.resize = function () { + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot, classes); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + + $.extend(true, options, opts); + + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. + + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontSize = placeholder.css("font-size"), + fontSizeDefault = fontSize ? +fontSize.replace("px", "") : 13, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * fontSizeDefault), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + if (!axisOptions.font.lineHeight) { + axisOptions.font.lineHeight = Math.round(axisOptions.font.size * 1.15); + } + } + } + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + // Override the inherit to allow the axis to auto-scale + if (options.x2axis.min == null) { + options.xaxes[1].min = null; + } + if (options.x2axis.max == null) { + options.xaxes[1].max = null; + } + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + // Override the inherit to allow the axis to auto-scale + if (options.y2axis.min == null) { + options.yaxes[1].min = null; + } + if (options.y2axis.max == null) { + options.yaxes[1].max = null; + } + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + if (options.highlightColor != null) + options.series.highlightColor = options.highlightColor; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + + var neededColors = series.length, maxIndex = -1, i; + + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. + + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + neededColors--; + if (typeof sc == "number" && sc > maxIndex) { + maxIndex = sc; + } + } + } + + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. + + if (neededColors <= maxIndex) { + neededColors = maxIndex + 1; + } + + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. + + var c, colors = [], colorPool = options.colors, + colorPoolSize = colorPool.length, variation = 0; + + for (i = 0; i < neededColors; i++) { + + c = $.color.parse(colorPool[i % colorPoolSize] || "#666"); + + // Each time we exhaust the colors in the pool we adjust + // a scaling factor used to produce more variations on + // those colors. The factor alternates negative/positive + // to produce lighter/darker colors. + + // Reset the variation after every few cycles, or else + // it will end up producing only white or black colors. + + if (i % colorPoolSize == 0 && i) { + if (variation >= 0) { + if (variation < 0.5) { + variation = -variation - 0.2; + } else variation = 0; + } else variation = -variation; + } + + colors[i] = c.scale('rgb', 1 + variation); + } + + // Finalize the series options, filling in their colors + + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p, + data, format; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + data = s.data; + format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + var insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.autoscale !== false) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points; + ps = s.datapoints.pointsize; + format = s.datapoints.format; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta; + + switch (s.bars.align) { + case "left": + delta = 0; + break; + case "right": + delta = -s.bars.barWidth; + break; + default: + delta = -s.bars.barWidth / 2; + } + + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function setupCanvases() { + + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. + + placeholder.css("padding", 0) // padding messes up the positioning + .children().filter(function(){ + return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + }).remove(); + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features + + ctx = surface.context; + octx = overlay.context; + + // define which element we're listening for events on + eventHolder = $(overlay.element).unbind(); + + // If we're re-using a plot object, shut down the old one + + var existing = placeholder.data("plot"); + + if (existing) { + existing.shutdown(); + overlay.clear(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || (axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null), + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; + + for (var i = 0; i < ticks.length; ++i) { + + var t = ticks[i]; + + if (!t.label) + continue; + + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); + + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); + } + + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + isXAxis = axis.direction === "x", + tickLength = axis.options.tickLength, + axisMargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + innermost = true, + outermost = true, + first = true, + found = false; + + // Determine the axis's position in its direction and on its side + + $.each(isXAxis ? xaxes : yaxes, function(i, a) { + if (a && (a.show || a.reserveSpace)) { + if (a === axis) { + found = true; + } else if (a.options.position === pos) { + if (found) { + outermost = false; + } else { + innermost = false; + } + } + if (!found) { + first = false; + } + } + }); + + // The outermost axis on each side has no margin + + if (outermost) { + axisMargin = 0; + } + + // The ticks for the first axis in each direction stretch across + + if (tickLength == null) { + tickLength = first ? "full" : 5; + } + + if (!isNaN(+tickLength)) + padding += +tickLength; + + if (isXAxis) { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axisMargin; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; + } + else { + plotOffset.right += lw + axisMargin; + axis.box = { left: surface.width - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; + } + else { + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; + } + } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + axis, i; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + var margins = { + left: minMargin, + right: minMargin, + top: minMargin, + bottom: minMargin + }; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + if (axis.reserveSpace && axis.ticks && axis.ticks.length) { + if (axis.direction === "x") { + margins.left = Math.max(margins.left, axis.labelWidth / 2); + margins.right = Math.max(margins.right, axis.labelWidth / 2); + } else { + margins.bottom = Math.max(margins.bottom, axis.labelHeight / 2); + margins.top = Math.max(margins.top, axis.labelHeight / 2); + } + } + }); + + plotOffset.left = Math.ceil(Math.max(margins.left, plotOffset.left)); + plotOffset.right = Math.ceil(Math.max(margins.right, plotOffset.right)); + plotOffset.top = Math.ceil(Math.max(margins.top, plotOffset.top)); + plotOffset.bottom = Math.ceil(Math.max(margins.bottom, plotOffset.bottom)); + } + + function setupGrid() { + var i, axes = allAxes(), showGrid = options.grid.show; + + // Initialize the plot's offset from the edge of the canvas + + for (var a in plotOffset) { + var margin = options.grid.margin || 0; + plotOffset[a] = typeof margin == "number" ? margin : margin[a] || 0; + } + + executeHooks(hooks.processOffset, [plotOffset]); + + // If the grid is visible, add its border width to the offset + + for (var a in plotOffset) { + if(typeof(options.grid.borderWidth) == "object") { + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; + } + else { + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; + } + } + + $.each(axes, function (_, axis) { + var axisOpts = axis.options; + axis.show = axisOpts.show == null ? axis.used : axisOpts.show; + axis.reserveSpace = axisOpts.reserveSpace == null ? axis.show : axisOpts.reserveSpace; + setRange(axis); + }); + + if (showGrid) { + + var allocatedAxes = $.grep(axes, function (axis) { + return axis.show || axis.reserveSpace; + }); + + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + } + + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; + + // now we got the proper plot dimensions, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (showGrid) { + drawAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); + + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + // Time mode was moved to a plug-in in 0.8, and since so many people use it + // we'll add an especially friendly reminder to make sure they included it. + + if (opts.mode == "time" && !axis.tickGenerator) { + throw new Error("Time mode requires the flot.time plugin."); + } + + // Flot supports base-10 axes; any other mode else is handled by a plug-in, + // like flot.time.js. + + if (!axis.tickGenerator) { + + axis.tickGenerator = function (axis) { + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + var formatted = "" + Math.round(value * factor) / factor; + + // If tickDecimals was specified, ensure that we have exactly that + // much precision; otherwise default to the value's own precision. + + if (axis.tickDecimals != null) { + var decimal = formatted.indexOf("."); + var precision = decimal == -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + + return formatted; + }; + } + + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = axis.tickGenerator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + axis.tickGenerator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (!axis.mode && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(axis.delta) / Math.LN10) + 1), + ts = axis.tickGenerator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks(axis); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + + surface.clear(); + + executeHooks(hooks.drawBackground, [ctx]); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) { + drawGrid(); + } + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) { + drawGrid(); + } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (var i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i, axes, bw, bc; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + var xequal = xrange.from === xrange.to, + yequal = yrange.from === yrange.to; + + if (xequal && yequal) { + continue; + } + + // then draw + xrange.from = Math.floor(xrange.axis.p2c(xrange.from)); + xrange.to = Math.floor(xrange.axis.p2c(xrange.to)); + yrange.from = Math.floor(yrange.axis.p2c(yrange.from)); + yrange.to = Math.floor(yrange.axis.p2c(yrange.to)); + + if (xequal || yequal) { + var lineWidth = m.lineWidth || options.grid.markingsLineWidth, + subPixel = lineWidth % 2 ? 0.5 : 0; + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = lineWidth; + if (xequal) { + ctx.moveTo(xrange.to + subPixel, yrange.from); + ctx.lineTo(xrange.to + subPixel, yrange.to); + } else { + ctx.moveTo(xrange.from, yrange.to + subPixel); + ctx.lineTo(xrange.to, yrange.to + subPixel); + } + ctx.stroke(); + } else { + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + axes = allAxes(); + bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue; + + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth + 1; + else + yoff = plotHeight + 1; + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (isNaN(v) || v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" + && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + // If either borderWidth or borderColor is an object, then draw the border + // line by line instead of as one rectangle + bc = options.grid.borderColor; + if(typeof bw == "object" || typeof bc == "object") { + if (typeof bw !== "object") { + bw = {top: bw, right: bw, bottom: bw, left: bw}; + } + if (typeof bc !== "object") { + bc = {top: bc, right: bc, bottom: bc, left: bc}; + } + + if (bw.top > 0) { + ctx.strokeStyle = bc.top; + ctx.lineWidth = bw.top; + ctx.beginPath(); + ctx.moveTo(0 - bw.left, 0 - bw.top/2); + ctx.lineTo(plotWidth, 0 - bw.top/2); + ctx.stroke(); + } + + if (bw.right > 0) { + ctx.strokeStyle = bc.right; + ctx.lineWidth = bw.right; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right / 2, 0 - bw.top); + ctx.lineTo(plotWidth + bw.right / 2, plotHeight); + ctx.stroke(); + } + + if (bw.bottom > 0) { + ctx.strokeStyle = bc.bottom; + ctx.lineWidth = bw.bottom; + ctx.beginPath(); + ctx.moveTo(plotWidth + bw.right, plotHeight + bw.bottom / 2); + ctx.lineTo(0, plotHeight + bw.bottom / 2); + ctx.stroke(); + } + + if (bw.left > 0) { + ctx.strokeStyle = bc.left; + ctx.lineWidth = bw.left; + ctx.beginPath(); + ctx.moveTo(0 - bw.left/2, plotHeight + bw.bottom); + ctx.lineTo(0- bw.left/2, 0); + ctx.stroke(); + } + } + else { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + } + + ctx.restore(); + } + + function drawAxisLabels() { + + $.each(allAxes(), function (_, axis) { + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; + + // Remove text before checking for axis.show and ticks.length; + // otherwise plugins, like flot-tickrotor, that draw their own + // tick labels will end up with both theirs and the defaults. + + surface.removeText(layer); + + if (!axis.show || axis.ticks.length == 0) + return; + + for (var i = 0; i < axis.ticks.length; ++i) { + + tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; + } + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; + } + } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); + } + }); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.fillStyle = fillStyleCallback(bottom, top); + c.fillRect(left, top, right - left, bottom - top) + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom); + if (drawLeft) + c.lineTo(left, top); + else + c.moveTo(left, top); + if (drawTop) + c.lineTo(right, top); + else + c.moveTo(right, top); + if (drawRight) + c.lineTo(right, bottom); + else + c.moveTo(right, bottom); + if (drawBottom) + c.lineTo(left, bottom); + else + c.moveTo(left, bottom); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + + var barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + + if (options.legend.container != null) { + $(options.legend.container).html(""); + } else { + placeholder.find(".legend").remove(); + } + + if (!options.legend.show) { + return; + } + + var fragments = [], entries = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + + // Build a list of legend entries, with each having a label and a color + + for (var i = 0; i < series.length; ++i) { + s = series[i]; + if (s.label) { + label = lf ? lf(s.label, s) : s.label; + if (label) { + entries.push({ + label: label, + color: s.color + }); + } + } + } + + // Sort the legend using either the default or a custom comparator + + if (options.legend.sorted) { + if ($.isFunction(options.legend.sorted)) { + entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); + } else { + var ascending = options.legend.sorted != "descending"; + entries.sort(function(a, b) { + return a.label == b.label ? 0 : ( + (a.label < b.label) != ascending ? 1 : -1 // Logical XOR + ); + }); + } + } + + // Generate markup for the list of entries, in their final order + + for (var i = 0; i < entries.length; ++i) { + + var entry = entries[i]; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push('</tr>'); + fragments.push('<tr>'); + rowStarted = true; + } + + fragments.push( + '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' + + '<td class="legendLabel" data-test-subj="flotLegendLabel">' + entry.label + '</td>' + ); + } + + if (rowStarted) + fragments.push('</tr>'); + + if (fragments.length == 0) + return; + + var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>'; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j, ps; + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + ps = s.datapoints.pointsize; + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + + var barLeft, barRight; + + switch (s.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -s.bars.barWidth; + break; + default: + barLeft = -s.bars.barWidth / 2; + } + + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + var t = options.interaction.redrawOverlayInterval; + if (t == -1) { // skip event queue + drawOverlay(); + return; + } + + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, t); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + overlay.clear(); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + return; + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis, + highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = highlightColor; + var radius = 1.5 * pointRadius; + x = axisx.p2c(x); + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(), + fillStyle = highlightColor, + barLeft; + + switch (series.bars.align) { + case "left": + barLeft = 0; + break; + case "right": + barLeft = -series.bars.barWidth; + break; + default: + barLeft = -series.bars.barWidth / 2; + } + + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = highlightColor; + + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness); + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + // Add the plot function to the top level of the jQuery object + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.8.3"; + + $.plot.plugins = []; + + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js new file mode 100644 index 0000000000000..e32bf5cf7e817 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.log.js @@ -0,0 +1,163 @@ +/* @notice + * + * Pretty handling of logarithmic axes. + * Copyright (c) 2007-2014 IOLA and Ole Laursen. + * Licensed under the MIT license. + * Created by Arne de Laat + * Set axis.mode to "log" and make the axis logarithmic using transform: + * axis: { + * mode: 'log', + * transform: function(v) {v <= 0 ? Math.log(v) / Math.LN10 : null}, + * inverseTransform: function(v) {Math.pow(10, v)} + * } + * The transform filters negative and zero values, because those are + * invalid on logarithmic scales. + * This plugin tries to create good looking logarithmic ticks, using + * unicode superscript characters. If all data to be plotted is between two + * powers of ten then the default flot tick generator and renderer are + * used. Logarithmic ticks are places at powers of ten and at half those + * values if there are not to many ticks already (e.g. [1, 5, 10, 50, 100]). + * For details, see https://github.com/flot/flot/pull/1328 +*/ + +(function($) { + + function log10(value) { + /* Get the Log10 of the value + */ + return Math.log(value) / Math.LN10; + } + + function floorAsLog10(value) { + /* Get power of the first power of 10 below the value + */ + return Math.floor(log10(value)); + } + + function ceilAsLog10(value) { + /* Get power of the first power of 10 above the value + */ + return Math.ceil(log10(value)); + } + + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + function getUnicodePower(power) { + var superscripts = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"], + result = "", + str_power = "" + power; + for (var i = 0; i < str_power.length; i++) { + if (str_power[i] === "+") { + } + else if (str_power[i] === "-") { + result += "⁻"; + } + else { + result += superscripts[str_power[i]]; + } + } + return result; + } + + function init(plot) { + plot.hooks.processOptions.push(function (plot) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode === "log") { + + axis.tickGenerator = function (axis) { + + var ticks = [], + end = ceilAsLog10(axis.max), + start = floorAsLog10(axis.min), + tick = Number.NaN, + i = 0; + + if (axis.min === null || axis.min <= 0) { + // Bad minimum, make ticks from 1 (10**0) to max + start = 0; + axis.min = 0.6; + } + + if (end <= start) { + // Start less than end?! + ticks = [1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, + 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, + 1e7, 1e8, 1e9]; + } + else if (log10(axis.max) - log10(axis.datamin) < 1) { + // Default flot generator incase no powers of 10 + // are between start and end + var prev; + start = floorInBase(axis.min, axis.tickSize); + do { + prev = tick; + tick = start + i * axis.tickSize; + ticks.push(tick); + ++i; + } while (tick < axis.max && tick !== prev); + } + else { + // Make ticks at each power of ten + for (; i <= (end - start); i++) { + tick = Math.pow(10, start + i); + ticks.push(tick); + } + + var length = ticks.length; + + // If not to many ticks also put a tick between + // the powers of ten + if (end - start < 6) { + for (var j = 1; j < length * 2 - 1; j += 2) { + tick = ticks[j - 1] * 5; + ticks.splice(j, 0, tick); + } + } + } + return ticks; + }; + + axis.tickFormatter = function (value, axis) { + var formatted; + if (log10(axis.max) - log10(axis.datamin) < 1) { + // Default flot formatter + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; + formatted = "" + Math.round(value * factor) / factor; + if (axis.tickDecimals !== null) { + var decimal = formatted.indexOf("."); + var precision = decimal === -1 ? 0 : formatted.length - decimal - 1; + if (precision < axis.tickDecimals) { + return (precision ? formatted : formatted + ".") + ("" + factor).substr(1, axis.tickDecimals - precision); + } + } + } + else { + var multiplier = "", + exponential = parseFloat(value).toExponential(0), + power = getUnicodePower(exponential.slice(2)); + if (exponential[0] !== "1") { + multiplier = exponential[0] + "x"; + } + formatted = multiplier + "10" + power; + } + return formatted; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + name: "log", + version: "0.9" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js new file mode 100644 index 0000000000000..13fb7f17d04b2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.navigate.js @@ -0,0 +1,346 @@ +/* Flot plugin for adding the ability to pan and zoom the plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The default behaviour is double click and scrollwheel up/down to zoom in, drag +to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and +plot.pan( offset ) so you easily can add custom controls. It also fires +"plotpan" and "plotzoom" events, useful for synchronizing plots. + +The plugin supports these options: + + zoom: { + interactive: false + trigger: "dblclick" // or "click" for single click + amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out) + } + + pan: { + interactive: false + cursor: "move" // CSS mouse cursor value used when dragging, e.g. "pointer" + frameRate: 20 + } + + xaxis, yaxis, x2axis, y2axis: { + zoomRange: null // or [ number, number ] (min range, max range) or false + panRange: null // or [ number, number ] (min, max) or false + } + +"interactive" enables the built-in drag/click behaviour. If you enable +interactive for pan, then you'll have a basic plot that supports moving +around; the same for zoom. + +"amount" specifies the default amount to zoom in (so 1.5 = 150%) relative to +the current viewport. + +"cursor" is a standard CSS mouse cursor string used for visual feedback to the +user when dragging. + +"frameRate" specifies the maximum number of times per second the plot will +update itself while the user is panning around on it (set to null to disable +intermediate pans, the plot will then not update until the mouse button is +released). + +"zoomRange" is the interval in which zooming can happen, e.g. with zoomRange: +[1, 100] the zoom will never scale the axis so that the difference between min +and max is smaller than 1 or larger than 100. You can set either end to null +to ignore, e.g. [1, null]. If you set zoomRange to false, zooming on that axis +will be disabled. + +"panRange" confines the panning to stay within a range, e.g. with panRange: +[-10, 20] panning stops at -10 in one end and at 20 in the other. Either can +be null, e.g. [-10, null]. If you set panRange to false, panning on that axis +will be disabled. + +Example API usage: + + plot = $.plot(...); + + // zoom default amount in on the pixel ( 10, 20 ) + plot.zoom({ center: { left: 10, top: 20 } }); + + // zoom out again + plot.zoomOut({ center: { left: 10, top: 20 } }); + + // zoom 200% in on the pixel (10, 20) + plot.zoom({ amount: 2, center: { left: 10, top: 20 } }); + + // pan 100 pixels to the left and 20 down + plot.pan({ left: -100, top: 20 }) + +Here, "center" specifies where the center of the zooming should happen. Note +that this is defined in pixel space, not the space of the data points (you can +use the p2c helpers on the axes in Flot to help you convert between these). + +"amount" is the amount to zoom the viewport relative to the current range, so +1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You +can set the default in the options. + +*/ + +// First two dependencies, jquery.event.drag.js and +// jquery.mousewheel.js, we put them inline here to save people the +// effort of downloading them. + +/* +jquery.event.drag.js ~ v1.5 ~ Copyright (c) 2008, Three Dub Media (http://threedubmedia.com) +Licensed under the MIT License ~ http://threedubmedia.googlecode.com/files/MIT-LICENSE.txt +*/ +(function(a){function e(h){var k,j=this,l=h.data||{};if(l.elem)j=h.dragTarget=l.elem,h.dragProxy=d.proxy||j,h.cursorOffsetX=l.pageX-l.left,h.cursorOffsetY=l.pageY-l.top,h.offsetX=h.pageX-h.cursorOffsetX,h.offsetY=h.pageY-h.cursorOffsetY;else if(d.dragging||l.which>0&&h.which!=l.which||a(h.target).is(l.not))return;switch(h.type){case"mousedown":return a.extend(l,a(j).offset(),{elem:j,target:h.target,pageX:h.pageX,pageY:h.pageY}),b.add(document,"mousemove mouseup",e,l),i(j,!1),d.dragging=null,!1;case!d.dragging&&"mousemove":if(g(h.pageX-l.pageX)+g(h.pageY-l.pageY)<l.distance)break;h.target=l.target,k=f(h,"dragstart",j),k!==!1&&(d.dragging=j,d.proxy=h.dragProxy=a(k||j)[0]);case"mousemove":if(d.dragging){if(k=f(h,"drag",j),c.drop&&(c.drop.allowed=k!==!1,c.drop.handler(h)),k!==!1)break;h.type="mouseup"}case"mouseup":b.remove(document,"mousemove mouseup",e),d.dragging&&(c.drop&&c.drop.handler(h),f(h,"dragend",j)),i(j,!0),d.dragging=d.proxy=l.elem=!1}return!0}function f(b,c,d){b.type=c;var e=a.event.dispatch.call(d,b);return e===!1?!1:e||b.result}function g(a){return Math.pow(a,2)}function h(){return d.dragging===!1}function i(a,b){a&&(a.unselectable=b?"off":"on",a.onselectstart=function(){return b},a.style&&(a.style.MozUserSelect=b?"":"none"))}a.fn.drag=function(a,b,c){return b&&this.bind("dragstart",a),c&&this.bind("dragend",c),a?this.bind("drag",b?b:a):this.trigger("drag")};var b=a.event,c=b.special,d=c.drag={not:":input",distance:0,which:1,dragging:!1,setup:function(c){c=a.extend({distance:d.distance,which:d.which,not:d.not},c||{}),c.distance=g(c.distance),b.add(this,"mousedown",e,c),this.attachEvent&&this.attachEvent("ondragstart",h)},teardown:function(){b.remove(this,"mousedown",e),this===d.dragging&&(d.dragging=d.proxy=!1),i(this,!0),this.detachEvent&&this.detachEvent("ondragstart",h)}};c.dragstart=c.dragend={setup:function(){},teardown:function(){}}})(jQuery); + +/* jquery.mousewheel.min.js + * Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) + * Licensed under the MIT License (LICENSE.txt). + * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. + * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. + * Thanks to: Seamus Leahy for adding deltaX and deltaY + * + * Version: 3.0.6 + * + * Requires: 1.2.2+ + */ +(function(d){function e(a){var b=a||window.event,c=[].slice.call(arguments,1),f=0,e=0,g=0,a=d.event.fix(b);a.type="mousewheel";b.wheelDelta&&(f=b.wheelDelta/120);b.detail&&(f=-b.detail/3);g=f;void 0!==b.axis&&b.axis===b.HORIZONTAL_AXIS&&(g=0,e=-1*f);void 0!==b.wheelDeltaY&&(g=b.wheelDeltaY/120);void 0!==b.wheelDeltaX&&(e=-1*b.wheelDeltaX/120);c.unshift(a,f,e,g);return(d.event.dispatch||d.event.handle).apply(this,c)}var c=["DOMMouseScroll","mousewheel"];if(d.event.fixHooks)for(var h=c.length;h;)d.event.fixHooks[c[--h]]=d.event.mouseHooks;d.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a=c.length;a;)this.addEventListener(c[--a],e,!1);else this.onmousewheel=e},teardown:function(){if(this.removeEventListener)for(var a=c.length;a;)this.removeEventListener(c[--a],e,!1);else this.onmousewheel=null}};d.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery); + + + + +(function ($) { + var options = { + xaxis: { + zoomRange: null, // or [number, number] (min range, max range) + panRange: null // or [number, number] (min, max) + }, + zoom: { + interactive: false, + trigger: "dblclick", // or "click" for single click + amount: 1.5 // how much to zoom relative to current position, 2 = 200% (zoom in), 0.5 = 50% (zoom out) + }, + pan: { + interactive: false, + cursor: "move", + frameRate: 20 + } + }; + + function init(plot) { + function onZoomClick(e, zoomOut) { + var c = plot.offset(); + c.left = e.pageX - c.left; + c.top = e.pageY - c.top; + if (zoomOut) + plot.zoomOut({ center: c }); + else + plot.zoom({ center: c }); + } + + function onMouseWheel(e, delta) { + e.preventDefault(); + onZoomClick(e, delta < 0); + return false; + } + + var prevCursor = 'default', prevPageX = 0, prevPageY = 0, + panTimeout = null; + + function onDragStart(e) { + if (e.which != 1) // only accept left-click + return false; + var c = plot.getPlaceholder().css('cursor'); + if (c) + prevCursor = c; + plot.getPlaceholder().css('cursor', plot.getOptions().pan.cursor); + prevPageX = e.pageX; + prevPageY = e.pageY; + } + + function onDrag(e) { + var frameRate = plot.getOptions().pan.frameRate; + if (panTimeout || !frameRate) + return; + + panTimeout = setTimeout(function () { + plot.pan({ left: prevPageX - e.pageX, + top: prevPageY - e.pageY }); + prevPageX = e.pageX; + prevPageY = e.pageY; + + panTimeout = null; + }, 1 / frameRate * 1000); + } + + function onDragEnd(e) { + if (panTimeout) { + clearTimeout(panTimeout); + panTimeout = null; + } + + plot.getPlaceholder().css('cursor', prevCursor); + plot.pan({ left: prevPageX - e.pageX, + top: prevPageY - e.pageY }); + } + + function bindEvents(plot, eventHolder) { + var o = plot.getOptions(); + if (o.zoom.interactive) { + eventHolder[o.zoom.trigger](onZoomClick); + eventHolder.mousewheel(onMouseWheel); + } + + if (o.pan.interactive) { + eventHolder.bind("dragstart", { distance: 10 }, onDragStart); + eventHolder.bind("drag", onDrag); + eventHolder.bind("dragend", onDragEnd); + } + } + + plot.zoomOut = function (args) { + if (!args) + args = {}; + + if (!args.amount) + args.amount = plot.getOptions().zoom.amount; + + args.amount = 1 / args.amount; + plot.zoom(args); + }; + + plot.zoom = function (args) { + if (!args) + args = {}; + + var c = args.center, + amount = args.amount || plot.getOptions().zoom.amount, + w = plot.width(), h = plot.height(); + + if (!c) + c = { left: w / 2, top: h / 2 }; + + var xf = c.left / w, + yf = c.top / h, + minmax = { + x: { + min: c.left - xf * w / amount, + max: c.left + (1 - xf) * w / amount + }, + y: { + min: c.top - yf * h / amount, + max: c.top + (1 - yf) * h / amount + } + }; + + $.each(plot.getAxes(), function(_, axis) { + var opts = axis.options, + min = minmax[axis.direction].min, + max = minmax[axis.direction].max, + zr = opts.zoomRange, + pr = opts.panRange; + + if (zr === false) // no zooming on this axis + return; + + min = axis.c2p(min); + max = axis.c2p(max); + if (min > max) { + // make sure min < max + var tmp = min; + min = max; + max = tmp; + } + + //Check that we are in panRange + if (pr) { + if (pr[0] != null && min < pr[0]) { + min = pr[0]; + } + if (pr[1] != null && max > pr[1]) { + max = pr[1]; + } + } + + var range = max - min; + if (zr && + ((zr[0] != null && range < zr[0] && amount >1) || + (zr[1] != null && range > zr[1] && amount <1))) + return; + + opts.min = min; + opts.max = max; + }); + + plot.setupGrid(); + plot.draw(); + + if (!args.preventEvent) + plot.getPlaceholder().trigger("plotzoom", [ plot, args ]); + }; + + plot.pan = function (args) { + var delta = { + x: +args.left, + y: +args.top + }; + + if (isNaN(delta.x)) + delta.x = 0; + if (isNaN(delta.y)) + delta.y = 0; + + $.each(plot.getAxes(), function (_, axis) { + var opts = axis.options, + min, max, d = delta[axis.direction]; + + min = axis.c2p(axis.p2c(axis.min) + d), + max = axis.c2p(axis.p2c(axis.max) + d); + + var pr = opts.panRange; + if (pr === false) // no panning on this axis + return; + + if (pr) { + // check whether we hit the wall + if (pr[0] != null && pr[0] > min) { + d = pr[0] - min; + min += d; + max += d; + } + + if (pr[1] != null && pr[1] < max) { + d = pr[1] - max; + min += d; + max += d; + } + } + + opts.min = min; + opts.max = max; + }); + + plot.setupGrid(); + plot.draw(); + + if (!args.preventEvent) + plot.getPlaceholder().trigger("plotpan", [ plot, args ]); + }; + + function shutdown(plot, eventHolder) { + eventHolder.unbind(plot.getOptions().zoom.trigger, onZoomClick); + eventHolder.unbind("mousewheel", onMouseWheel); + eventHolder.unbind("dragstart", onDragStart); + eventHolder.unbind("drag", onDrag); + eventHolder.unbind("dragend", onDragEnd); + if (panTimeout) + clearTimeout(panTimeout); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'navigate', + version: '1.3' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js new file mode 100644 index 0000000000000..24148c0a2e223 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.pie.js @@ -0,0 +1,824 @@ +/* Flot plugin for rendering pie charts. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes that each series has a single data value, and that each +value is a positive integer or zero. Negative numbers don't make sense for a +pie chart, and have unpredictable results. The values do NOT need to be +passed in as percentages; the plugin will calculate the total and per-slice +percentages internally. + +* Created by Brian Medendorp + +* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars + +The plugin supports these options: + + series: { + pie: { + show: true/false + radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' + innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect + startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result + tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) + offset: { + top: integer value to move the pie up or down + left: integer value to move the pie left or right, or 'auto' + }, + stroke: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#FFF') + width: integer pixel width of the stroke + }, + label: { + show: true/false, or 'auto' + formatter: a user-defined function that modifies the text/style of the label text + radius: 0-1 for percentage of fullsize, or a specified pixel length + background: { + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#000') + opacity: 0-1 + }, + threshold: 0-1 for the percentage value at which to hide labels (if they're too small) + }, + combine: { + threshold: 0-1 for the percentage value at which to combine slices (if they're too small) + color: any hexadecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined + label: any text value of what the combined slice should be labeled + } + highlight: { + opacity: 0-1 + } + } + } + +More detail and specific examples can be found in the included HTML file. + +*/ + +import { i18n } from '@kbn/i18n'; + +(function($) { + // Maximum redraw attempts when fitting labels within the plot + + var REDRAW_ATTEMPTS = 10; + + // Factor by which to shrink the pie when fitting labels within the plot + + var REDRAW_SHRINK = 0.95; + + function init(plot) { + + var canvas = null, + target = null, + options = null, + maxRadius = null, + centerLeft = null, + centerTop = null, + processed = false, + ctx = null; + + // interactive variables + + var highlights = []; + + // add hook to determine if pie plugin in enabled, and then perform necessary operations + + plot.hooks.processOptions.push(function(plot, options) { + if (options.series.pie.show) { + + options.grid.show = false; + + // set labels.show + + if (options.series.pie.label.show == "auto") { + if (options.legend.show) { + options.series.pie.label.show = false; + } else { + options.series.pie.label.show = true; + } + } + + // set radius + + if (options.series.pie.radius == "auto") { + if (options.series.pie.label.show) { + options.series.pie.radius = 3/4; + } else { + options.series.pie.radius = 1; + } + } + + // ensure sane tilt + + if (options.series.pie.tilt > 1) { + options.series.pie.tilt = 1; + } else if (options.series.pie.tilt < 0) { + options.series.pie.tilt = 0; + } + } + }); + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var options = plot.getOptions(); + if (options.series.pie.show) { + if (options.grid.hoverable) { + eventHolder.unbind("mousemove").mousemove(onMouseMove); + } + if (options.grid.clickable) { + eventHolder.unbind("click").click(onClick); + } + } + }); + + plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { + var options = plot.getOptions(); + if (options.series.pie.show) { + processDatapoints(plot, series, data, datapoints); + } + }); + + plot.hooks.drawOverlay.push(function(plot, octx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + drawOverlay(plot, octx); + } + }); + + plot.hooks.draw.push(function(plot, newCtx) { + var options = plot.getOptions(); + if (options.series.pie.show) { + draw(plot, newCtx); + } + }); + + function processDatapoints(plot, series, datapoints) { + if (!processed) { + processed = true; + canvas = plot.getCanvas(); + target = $(canvas).parent(); + options = plot.getOptions(); + plot.setData(combine(plot.getData())); + } + } + + function combine(data) { + + var total = 0, + combined = 0, + numCombined = 0, + color = options.series.pie.combine.color, + newdata = []; + + // Fix up the raw data from Flot, ensuring the data is numeric + + for (var i = 0; i < data.length; ++i) { + + var value = data[i].data; + + // If the data is an array, we'll assume that it's a standard + // Flot x-y pair, and are concerned only with the second value. + + // Note how we use the original array, rather than creating a + // new one; this is more efficient and preserves any extra data + // that the user may have stored in higher indexes. + + if ($.isArray(value) && value.length == 1) { + value = value[0]; + } + + if ($.isArray(value)) { + // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 + if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { + value[1] = +value[1]; + } else { + value[1] = 0; + } + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + value = [1, +value]; + } else { + value = [1, 0]; + } + + data[i].data = [value]; + } + + // Sum up all the slices, so we can calculate percentages for each + + for (var i = 0; i < data.length; ++i) { + total += data[i].data[0][1]; + } + + // Count the number of slices with percentages below the combine + // threshold; if it turns out to be just one, we won't combine. + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (value / total <= options.series.pie.combine.threshold) { + combined += value; + numCombined++; + if (!color) { + color = data[i].color; + } + } + } + + for (var i = 0; i < data.length; ++i) { + var value = data[i].data[0][1]; + if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { + newdata.push( + $.extend(data[i], { /* extend to allow keeping all other original data values + and using them e.g. in labelFormatter. */ + data: [[1, value]], + color: data[i].color, + label: data[i].label, + angle: value * Math.PI * 2 / total, + percent: value / (total / 100) + }) + ); + } + } + + if (numCombined > 1) { + newdata.push({ + data: [[1, combined]], + color: color, + label: options.series.pie.combine.label, + angle: combined * Math.PI * 2 / total, + percent: combined / (total / 100) + }); + } + + return newdata; + } + + function draw(plot, newCtx) { + + if (!target) { + return; // if no series were passed + } + + var canvasWidth = plot.getPlaceholder().width(), + canvasHeight = plot.getPlaceholder().height(), + legendWidth = target.children().filter(".legend").children().width() || 0; + + ctx = newCtx; + + // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! + + // When combining smaller slices into an 'other' slice, we need to + // add a new series. Since Flot gives plugins no way to modify the + // list of series, the pie plugin uses a hack where the first call + // to processDatapoints results in a call to setData with the new + // list of series, then subsequent processDatapoints do nothing. + + // The plugin-global 'processed' flag is used to control this hack; + // it starts out false, and is set to true after the first call to + // processDatapoints. + + // Unfortunately this turns future setData calls into no-ops; they + // call processDatapoints, the flag is true, and nothing happens. + + // To fix this we'll set the flag back to false here in draw, when + // all series have been processed, so the next sequence of calls to + // processDatapoints once again starts out with a slice-combine. + // This is really a hack; in 0.9 we need to give plugins a proper + // way to modify series before any processing begins. + + processed = false; + + // calculate maximum radius and center point + + maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; + centerTop = canvasHeight / 2 + options.series.pie.offset.top; + centerLeft = canvasWidth / 2; + + if (options.series.pie.offset.left == "auto") { + if (options.legend.position.match("w")) { + centerLeft += legendWidth / 2; + } else { + centerLeft -= legendWidth / 2; + } + if (centerLeft < maxRadius) { + centerLeft = maxRadius; + } else if (centerLeft > canvasWidth - maxRadius) { + centerLeft = canvasWidth - maxRadius; + } + } else { + centerLeft += options.series.pie.offset.left; + } + + var slices = plot.getData(), + attempts = 0; + + // Keep shrinking the pie's radius until drawPie returns true, + // indicating that all the labels fit, or we try too many times. + + do { + if (attempts > 0) { + maxRadius *= REDRAW_SHRINK; + } + attempts += 1; + clear(); + if (options.series.pie.tilt <= 0.8) { + drawShadow(); + } + } while (!drawPie() && attempts < REDRAW_ATTEMPTS) + + if (attempts >= REDRAW_ATTEMPTS) { + clear(); + const errorMessage = i18n.translate('xpack.monitoring.pie.unableToDrawLabelsInsideCanvasErrorMessage', { + defaultMessage: 'Could not draw pie with labels contained inside canvas', + }); + target.prepend(`<div class='error'>${errorMessage}</div>`); + } + + if (plot.setSeries && plot.insertLegend) { + plot.setSeries(slices); + plot.insertLegend(); + } + + // we're actually done at this point, just defining internal functions at this point + + function clear() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + target.children().filter(".pieLabel, .pieLabelBackground").remove(); + } + + function drawShadow() { + + var shadowLeft = options.series.pie.shadow.left; + var shadowTop = options.series.pie.shadow.top; + var edge = 10; + var alpha = options.series.pie.shadow.alpha; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { + return; // shadow would be outside canvas, so don't draw it + } + + ctx.save(); + ctx.translate(shadowLeft,shadowTop); + ctx.globalAlpha = alpha; + ctx.fillStyle = "#000"; + + // center and rotate to starting position + + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + + //radius -= edge; + + for (var i = 1; i <= edge; i++) { + ctx.beginPath(); + ctx.arc(0, 0, radius, 0, Math.PI * 2, false); + ctx.fill(); + radius -= i; + } + + ctx.restore(); + } + + function drawPie() { + + var startAngle = Math.PI * options.series.pie.startAngle; + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + // center and rotate to starting position + + ctx.save(); + ctx.translate(centerLeft,centerTop); + ctx.scale(1, options.series.pie.tilt); + //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera + + // draw slices + + ctx.save(); + var currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + slices[i].startAngle = currentAngle; + drawSlice(slices[i].angle, slices[i].color, true); + } + ctx.restore(); + + // draw slice outlines + + if (options.series.pie.stroke.width > 0) { + ctx.save(); + ctx.lineWidth = options.series.pie.stroke.width; + currentAngle = startAngle; + for (var i = 0; i < slices.length; ++i) { + drawSlice(slices[i].angle, options.series.pie.stroke.color, false); + } + ctx.restore(); + } + + // draw donut hole + + drawDonutHole(ctx); + + ctx.restore(); + + // Draw the labels, returning true if they fit within the plot + + if (options.series.pie.label.show) { + return drawLabels(); + } else return true; + + function drawSlice(angle, color, fill) { + + if (angle <= 0 || isNaN(angle)) { + return; + } + + if (fill) { + ctx.fillStyle = color; + } else { + ctx.strokeStyle = color; + ctx.lineJoin = "round"; + } + + ctx.beginPath(); + if (Math.abs(angle - Math.PI * 2) > 0.000000001) { + ctx.moveTo(0, 0); // Center of the pie + } + + //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera + ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); + ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); + ctx.closePath(); + //ctx.rotate(angle); // This doesn't work properly in Opera + currentAngle += angle; + + if (fill) { + ctx.fill(); + } else { + ctx.stroke(); + } + } + + function drawLabels() { + + var currentAngle = startAngle; + var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; + + for (var i = 0; i < slices.length; ++i) { + if (slices[i].percent >= options.series.pie.label.threshold * 100) { + if (!drawLabel(slices[i], currentAngle, i)) { + return false; + } + } + currentAngle += slices[i].angle; + } + + return true; + + function drawLabel(slice, startAngle, index) { + + if (slice.data[0][1] == 0) { + return true; + } + + // format label text + + var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; + + if (lf) { + text = lf(slice.label, slice); + } else { + text = slice.label; + } + + if (plf) { + text = plf(text, slice); + } + + var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; + var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); + var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; + + var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>"; + target.append(html); + + var label = target.children("#pieLabel" + index); + var labelTop = (y - label.height() / 2); + var labelLeft = (x - label.width() / 2); + + label.css("top", labelTop); + label.css("left", labelLeft); + + // check to make sure that the label is not outside the canvas + + if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { + return false; + } + + if (options.series.pie.label.background.opacity != 0) { + + // put in the transparent background separately to avoid blended labels and label boxes + + var c = options.series.pie.label.background.color; + + if (c == null) { + c = slice.color; + } + + var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; + $("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>") + .css("opacity", options.series.pie.label.background.opacity) + .insertBefore(label); + } + + return true; + } // end individual label function + } // end drawLabels function + } // end drawPie function + } // end draw function + + // Placed here because it needs to be accessed from multiple locations + + function drawDonutHole(layer) { + if (options.series.pie.innerRadius > 0) { + + // subtract the center + + layer.save(); + var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; + layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color + layer.beginPath(); + layer.fillStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.fill(); + layer.closePath(); + layer.restore(); + + // add inner stroke + + layer.save(); + layer.beginPath(); + layer.strokeStyle = options.series.pie.stroke.color; + layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); + layer.stroke(); + layer.closePath(); + layer.restore(); + + // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. + } + } + + //-- Additional Interactive related functions -- + + function isPointInPoly(poly, pt) { + for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) + ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) + && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) + && (c = !c); + return c; + } + + function findNearbySlice(mouseX, mouseY) { + + var slices = plot.getData(), + options = plot.getOptions(), + radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, + x, y; + + for (var i = 0; i < slices.length; ++i) { + + var s = slices[i]; + + if (s.pie.show) { + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(0, 0); // Center of the pie + //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. + ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); + ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); + ctx.closePath(); + x = mouseX - centerLeft; + y = mouseY - centerTop; + + if (ctx.isPointInPath) { + if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } else { + + // excanvas for IE doesn;t support isPointInPath, this is a workaround. + + var p1X = radius * Math.cos(s.startAngle), + p1Y = radius * Math.sin(s.startAngle), + p2X = radius * Math.cos(s.startAngle + s.angle / 4), + p2Y = radius * Math.sin(s.startAngle + s.angle / 4), + p3X = radius * Math.cos(s.startAngle + s.angle / 2), + p3Y = radius * Math.sin(s.startAngle + s.angle / 2), + p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), + p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), + p5X = radius * Math.cos(s.startAngle + s.angle), + p5Y = radius * Math.sin(s.startAngle + s.angle), + arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], + arrPoint = [x, y]; + + // TODO: perhaps do some mathematical trickery here with the Y-coordinate to compensate for pie tilt? + + if (isPointInPoly(arrPoly, arrPoint)) { + ctx.restore(); + return { + datapoint: [s.percent, s.data], + dataIndex: 0, + series: s, + seriesIndex: i + }; + } + } + + ctx.restore(); + } + } + + return null; + } + + function onMouseMove(e) { + triggerClickHoverEvent("plothover", e); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e); + } + + // trigger click or hover event (they send the same parameters so we share their code) + + function triggerClickHoverEvent(eventname, e) { + + var offset = plot.offset(); + var canvasX = parseInt(e.pageX - offset.left); + var canvasY = parseInt(e.pageY - offset.top); + var item = findNearbySlice(canvasX, canvasY); + + if (options.grid.autoHighlight) { + + // clear auto-highlights + + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && !(item && h.series == item.series)) { + unhighlight(h.series); + } + } + } + + // highlight the slice + + if (item) { + highlight(item.series, eventname); + } + + // trigger any hover bind events + + var pos = { pageX: e.pageX, pageY: e.pageY }; + target.trigger(eventname, [pos, item]); + } + + function highlight(s, auto) { + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i == -1) { + highlights.push({ series: s, auto: auto }); + plot.triggerRedrawOverlay(); + } else if (!auto) { + highlights[i].auto = false; + } + } + + function unhighlight(s) { + if (s == null) { + highlights = []; + plot.triggerRedrawOverlay(); + } + + //if (typeof s == "number") { + // s = series[s]; + //} + + var i = indexOfHighlight(s); + + if (i != -1) { + highlights.splice(i, 1); + plot.triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s) + return i; + } + return -1; + } + + function drawOverlay(plot, octx) { + + var options = plot.getOptions(); + + var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; + + octx.save(); + octx.translate(centerLeft, centerTop); + octx.scale(1, options.series.pie.tilt); + + for (var i = 0; i < highlights.length; ++i) { + drawHighlight(highlights[i].series); + } + + drawDonutHole(octx); + + octx.restore(); + + function drawHighlight(series) { + + if (series.angle <= 0 || isNaN(series.angle)) { + return; + } + + //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); + octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor + octx.beginPath(); + if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { + octx.moveTo(0, 0); // Center of the pie + } + octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); + octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); + octx.closePath(); + octx.fill(); + } + } + } // end init (plugin body) + + // define pie specific options and their default values + + var options = { + series: { + pie: { + show: false, + radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) + innerRadius: 0, /* for donut */ + startAngle: 3/2, + tilt: 1, + shadow: { + left: 5, // shadow left offset + top: 15, // shadow top offset + alpha: 0.02 // shadow alpha + }, + offset: { + top: 0, + left: "auto" + }, + stroke: { + color: "#fff", + width: 1 + }, + label: { + show: "auto", + formatter: function(label, slice) { + return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>"; + }, // formatter function + radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) + background: { + color: null, + opacity: 0 + }, + threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) + }, + combine: { + threshold: -1, // percentage at which to combine little slices into one larger slice + color: null, // color to give the new slice (auto-generated if null) + label: "Other" // label to give the new slice + }, + highlight: { + //color: "#fff", // will add this functionality once parseColor is available + opacity: 0.5 + } + } + } + }; + + $.plot.plugins.push({ + init: init, + options: options, + name: "pie", + version: "1.1" + }); + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js new file mode 100644 index 0000000000000..8a626dda0addb --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.resize.js @@ -0,0 +1,59 @@ +/* Flot plugin for automatically redrawing plots as the placeholder resizes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +It works by listening for changes on the placeholder div (through the jQuery +resize event plugin) - if the size changes, it will redraw the plot. + +There are no options. If you need to disable the plugin for some plots, you +can just fix the size of their placeholders. + +*/ + +/* Inline dependency: + * jQuery resize event - v1.1 - 3/14/2010 + * http://benalman.com/projects/jquery-resize-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this); + +(function ($) { + var options = { }; // no options + + function init(plot) { + function onResize() { + var placeholder = plot.getPlaceholder(); + + // somebody might have hidden us and we can't plot + // when we don't have the dimensions + if (placeholder.width() == 0 || placeholder.height() == 0) + return; + + plot.resize(); + plot.setupGrid(); + plot.draw(); + } + + function bindEvents(plot, eventHolder) { + plot.getPlaceholder().resize(onResize); + } + + function shutdown(plot, eventHolder) { + plot.getPlaceholder().unbind("resize", onResize); + } + + plot.hooks.bindEvents.push(bindEvents); + plot.hooks.shutdown.push(shutdown); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'resize', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js new file mode 100644 index 0000000000000..c8707b30f4e6f --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.selection.js @@ -0,0 +1,360 @@ +/* Flot plugin for selecting regions of a plot. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + +selection: { + mode: null or "x" or "y" or "xy", + color: color, + shape: "round" or "miter" or "bevel", + minSize: number of pixels +} + +Selection support is enabled by setting the mode to one of "x", "y" or "xy". +In "x" mode, the user will only be able to specify the x range, similarly for +"y" mode. For "xy", the selection becomes a rectangle where both ranges can be +specified. "color" is color of the selection (if you need to change the color +later on, you can get to it with plot.getOptions().selection.color). "shape" +is the shape of the corners of the selection. + +"minSize" is the minimum size a selection can be in pixels. This value can +be customized to determine the smallest size a selection can be and still +have the selection rectangle be displayed. When customizing this value, the +fact that it refers to pixels, not axis units must be taken into account. +Thus, for example, if there is a bar graph in time mode with BarWidth set to 1 +minute, setting "minSize" to 1 will not make the minimum selection size 1 +minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent +"plotunselected" events from being fired when the user clicks the mouse without +dragging. + +When selection support is enabled, a "plotselected" event will be emitted on +the DOM element you passed into the plot function. The event handler gets a +parameter with the ranges selected on the axes, like this: + + placeholder.bind( "plotselected", function( event, ranges ) { + alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) + // similar for yaxis - with multiple axes, the extra ones are in + // x2axis, x3axis, ... + }); + +The "plotselected" event is only fired when the user has finished making the +selection. A "plotselecting" event is fired during the process with the same +parameters as the "plotselected" event, in case you want to know what's +happening while it's happening, + +A "plotunselected" event with no arguments is emitted when the user clicks the +mouse to remove the selection. As stated above, setting "minSize" to 0 will +destroy this behavior. + +The plugin also adds the following methods to the plot object: + +- setSelection( ranges, preventEvent ) + + Set the selection rectangle. The passed in ranges is on the same form as + returned in the "plotselected" event. If the selection mode is "x", you + should put in either an xaxis range, if the mode is "y" you need to put in + an yaxis range and both xaxis and yaxis if the selection mode is "xy", like + this: + + setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); + + setSelection will trigger the "plotselected" event when called. If you don't + want that to happen, e.g. if you're inside a "plotselected" handler, pass + true as the second parameter. If you are using multiple axes, you can + specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of + xaxis, the plugin picks the first one it sees. + +- clearSelection( preventEvent ) + + Clear the selection rectangle. Pass in true to avoid getting a + "plotunselected" event. + +- getSelection() + + Returns the current selection in the same format as the "plotselected" + event. If there's currently no selection, the function returns null. + +*/ + +(function ($) { + function init(plot) { + var selection = { + first: { x: -1, y: -1}, second: { x: -1, y: -1}, + show: false, + active: false + }; + + // FIXME: The drag handling implemented here should be + // abstracted out, there's some similar code from a library in + // the navigation plugin, this should be massaged a bit to fit + // the Flot cases here better and reused. Doing this would + // make this plugin much slimmer. + var savedhandlers = {}; + + var mouseUpHandler = null; + + function onMouseMove(e) { + if (selection.active) { + updateSelection(e); + + plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]); + } + } + + function onMouseDown(e) { + if (e.which != 1) // only accept left-click + return; + + // cancel out any text selections + document.body.focus(); + + // prevent text selection and drag in old-school browsers + if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) { + savedhandlers.onselectstart = document.onselectstart; + document.onselectstart = function () { return false; }; + } + if (document.ondrag !== undefined && savedhandlers.ondrag == null) { + savedhandlers.ondrag = document.ondrag; + document.ondrag = function () { return false; }; + } + + setSelectionPos(selection.first, e); + + selection.active = true; + + // this is a bit silly, but we have to use a closure to be + // able to whack the same handler again + mouseUpHandler = function (e) { onMouseUp(e); }; + + $(document).one("mouseup", mouseUpHandler); + } + + function onMouseUp(e) { + mouseUpHandler = null; + + // revert drag stuff for old-school browsers + if (document.onselectstart !== undefined) + document.onselectstart = savedhandlers.onselectstart; + if (document.ondrag !== undefined) + document.ondrag = savedhandlers.ondrag; + + // no more dragging + selection.active = false; + updateSelection(e); + + if (selectionIsSane()) + triggerSelectedEvent(); + else { + // this counts as a clear + plot.getPlaceholder().trigger("plotunselected", [ ]); + plot.getPlaceholder().trigger("plotselecting", [ null ]); + } + + return false; + } + + function getSelection() { + if (!selectionIsSane()) + return null; + + if (!selection.show) return null; + + var r = {}, c1 = selection.first, c2 = selection.second; + $.each(plot.getAxes(), function (name, axis) { + if (axis.used) { + var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]); + r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) }; + } + }); + return r; + } + + function triggerSelectedEvent() { + var r = getSelection(); + + plot.getPlaceholder().trigger("plotselected", [ r ]); + + // backwards-compat stuff, to be removed in future + if (r.xaxis && r.yaxis) + plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); + } + + function clamp(min, value, max) { + return value < min ? min: (value > max ? max: value); + } + + function setSelectionPos(pos, e) { + var o = plot.getOptions(); + var offset = plot.getPlaceholder().offset(); + var plotOffset = plot.getPlotOffset(); + pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width()); + pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height()); + + if (o.selection.mode == "y") + pos.x = pos == selection.first ? 0 : plot.width(); + + if (o.selection.mode == "x") + pos.y = pos == selection.first ? 0 : plot.height(); + } + + function updateSelection(pos) { + if (pos.pageX == null) + return; + + setSelectionPos(selection.second, pos); + if (selectionIsSane()) { + selection.show = true; + plot.triggerRedrawOverlay(); + } + else + clearSelection(true); + } + + function clearSelection(preventEvent) { + if (selection.show) { + selection.show = false; + plot.triggerRedrawOverlay(); + if (!preventEvent) + plot.getPlaceholder().trigger("plotunselected", [ ]); + } + } + + // function taken from markings support in Flot + function extractRange(ranges, coord) { + var axis, from, to, key, axes = plot.getAxes(); + + for (var k in axes) { + axis = axes[k]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? plot.getXAxes()[0] : plot.getYAxes()[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function setSelection(ranges, preventEvent) { + var axis, range, o = plot.getOptions(); + + if (o.selection.mode == "y") { + selection.first.x = 0; + selection.second.x = plot.width(); + } + else { + range = extractRange(ranges, "x"); + + selection.first.x = range.axis.p2c(range.from); + selection.second.x = range.axis.p2c(range.to); + } + + if (o.selection.mode == "x") { + selection.first.y = 0; + selection.second.y = plot.height(); + } + else { + range = extractRange(ranges, "y"); + + selection.first.y = range.axis.p2c(range.from); + selection.second.y = range.axis.p2c(range.to); + } + + selection.show = true; + plot.triggerRedrawOverlay(); + if (!preventEvent && selectionIsSane()) + triggerSelectedEvent(); + } + + function selectionIsSane() { + var minSize = plot.getOptions().selection.minSize; + return Math.abs(selection.second.x - selection.first.x) >= minSize && + Math.abs(selection.second.y - selection.first.y) >= minSize; + } + + plot.clearSelection = clearSelection; + plot.setSelection = setSelection; + plot.getSelection = getSelection; + + plot.hooks.bindEvents.push(function(plot, eventHolder) { + var o = plot.getOptions(); + if (o.selection.mode != null) { + eventHolder.mousemove(onMouseMove); + eventHolder.mousedown(onMouseDown); + } + }); + + + plot.hooks.drawOverlay.push(function (plot, ctx) { + // draw selection + if (selection.show && selectionIsSane()) { + var plotOffset = plot.getPlotOffset(); + var o = plot.getOptions(); + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var c = $.color.parse(o.selection.color); + + ctx.strokeStyle = c.scale('a', 0.8).toString(); + ctx.lineWidth = 1; + ctx.lineJoin = o.selection.shape; + ctx.fillStyle = c.scale('a', 0.4).toString(); + + var x = Math.min(selection.first.x, selection.second.x) + 0.5, + y = Math.min(selection.first.y, selection.second.y) + 0.5, + w = Math.abs(selection.second.x - selection.first.x) - 1, + h = Math.abs(selection.second.y - selection.first.y) - 1; + + ctx.fillRect(x, y, w, h); + ctx.strokeRect(x, y, w, h); + + ctx.restore(); + } + }); + + plot.hooks.shutdown.push(function (plot, eventHolder) { + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mousedown", onMouseDown); + + if (mouseUpHandler) + $(document).unbind("mouseup", mouseUpHandler); + }); + + } + + $.plot.plugins.push({ + init: init, + options: { + selection: { + mode: null, // one of null, "x", "y" or "xy" + color: "#e8cfac", + shape: "round", // one of "round", "miter", or "bevel" + minSize: 5 // minimum number of pixels + } + }, + name: 'selection', + version: '1.1' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js new file mode 100644 index 0000000000000..0d91c0f3c0160 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.stack.js @@ -0,0 +1,188 @@ +/* Flot plugin for stacking data sets rather than overlaying them. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin assumes the data is sorted on x (or y if stacking horizontally). +For line charts, it is assumed that if a line has an undefined gap (from a +null point), then the line above it should have the same gap - insert zeros +instead of "null" if you want another behaviour. This also holds for the start +and end of the chart. Note that stacking a mix of positive and negative values +in most instances doesn't make sense (so it looks weird). + +Two or more series are stacked when their "stack" attribute is set to the same +key (which can be any number or string or just "true"). To specify the default +stack, you can set the stack option like this: + + series: { + stack: null/false, true, or a key (number/string) + } + +You can also specify it for a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + stack: true + }]) + +The stacking order is determined by the order of the data series in the array +(later series end up on top of the previous). + +Internally, the plugin modifies the datapoints in each series, adding an +offset to the y value. For line series, extra data points are inserted through +interpolation. If there's a second y value, it's also adjusted (e.g for bar +charts or filled areas). + +*/ + +(function ($) { + var options = { + series: { stack: null } // or number/string + }; + + function init(plot) { + function findMatchingSeries(s, allseries) { + var res = null; + for (var i = 0; i < allseries.length; ++i) { + if (s == allseries[i]) + break; + + if (allseries[i].stack == s.stack) + res = allseries[i]; + } + + return res; + } + + function stackData(plot, s, datapoints) { + if (s.stack == null || s.stack === false) + return; + + var other = findMatchingSeries(s, plot.getData()); + if (!other) + return; + + var ps = datapoints.pointsize, + points = datapoints.points, + otherps = other.datapoints.pointsize, + otherpoints = other.datapoints.points, + newpoints = [], + px, py, intery, qx, qy, bottom, + withlines = s.lines.show, + horizontal = s.bars.horizontal, + withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y), + withsteps = withlines && s.lines.steps, + fromgap = true, + keyOffset = horizontal ? 1 : 0, + accumulateOffset = horizontal ? 0 : 1, + i = 0, j = 0, l, m; + + while (true) { + if (i >= points.length) + break; + + l = newpoints.length; + + if (points[i] == null) { + // copy gaps + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + i += ps; + } + else if (j >= otherpoints.length) { + // for lines, we can't use the rest of the points + if (!withlines) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + } + i += ps; + } + else if (otherpoints[j] == null) { + // oops, got a gap + for (m = 0; m < ps; ++m) + newpoints.push(null); + fromgap = true; + j += otherps; + } + else { + // cases where we actually got two points + px = points[i + keyOffset]; + py = points[i + accumulateOffset]; + qx = otherpoints[j + keyOffset]; + qy = otherpoints[j + accumulateOffset]; + bottom = 0; + + if (px == qx) { + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + newpoints[l + accumulateOffset] += qy; + bottom = qy; + + i += ps; + j += otherps; + } + else if (px > qx) { + // we got past point below, might need to + // insert interpolated extra point + if (withlines && i > 0 && points[i - ps] != null) { + intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); + newpoints.push(qx); + newpoints.push(intery + qy); + for (m = 2; m < ps; ++m) + newpoints.push(points[i + m]); + bottom = qy; + } + + j += otherps; + } + else { // px < qx + if (fromgap && withlines) { + // if we come from a gap, we just skip this point + i += ps; + continue; + } + + for (m = 0; m < ps; ++m) + newpoints.push(points[i + m]); + + // we might be able to interpolate a point below, + // this can give us a better y + if (withlines && j > 0 && otherpoints[j - otherps] != null) + bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); + + newpoints[l + accumulateOffset] += bottom; + + i += ps; + } + + fromgap = false; + + if (l != newpoints.length && withbottom) + newpoints[l + 2] += bottom; + } + + // maintain the line steps invariant + if (withsteps && l != newpoints.length && l > 0 + && newpoints[l] != null + && newpoints[l] != newpoints[l - ps] + && newpoints[l + 1] != newpoints[l - ps + 1]) { + for (m = 0; m < ps; ++m) + newpoints[l + ps + m] = newpoints[l + m]; + newpoints[l + 1] = newpoints[l - ps + 1]; + } + } + + datapoints.points = newpoints; + } + + plot.hooks.processDatapoints.push(stackData); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'stack', + version: '1.2' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js new file mode 100644 index 0000000000000..79f634971b6fa --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.symbol.js @@ -0,0 +1,71 @@ +/* Flot plugin that adds some extra symbols for plotting points. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The symbols are accessed as strings through the standard symbol options: + + series: { + points: { + symbol: "square" // or "diamond", "triangle", "cross" + } + } + +*/ + +(function ($) { + function processRawData(plot, series, datapoints) { + // we normalize the area of each symbol so it is approximately the + // same as a circle of the given radius + + var handlers = { + square: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.rect(x - size, y - size, size + size, size + size); + }, + diamond: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 2s^2 => s = r * sqrt(pi/2) + var size = radius * Math.sqrt(Math.PI / 2); + ctx.moveTo(x - size, y); + ctx.lineTo(x, y - size); + ctx.lineTo(x + size, y); + ctx.lineTo(x, y + size); + ctx.lineTo(x - size, y); + }, + triangle: function (ctx, x, y, radius, shadow) { + // pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3)) + var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); + var height = size * Math.sin(Math.PI / 3); + ctx.moveTo(x - size/2, y + height/2); + ctx.lineTo(x + size/2, y + height/2); + if (!shadow) { + ctx.lineTo(x, y - height/2); + ctx.lineTo(x - size/2, y + height/2); + } + }, + cross: function (ctx, x, y, radius, shadow) { + // pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2 + var size = radius * Math.sqrt(Math.PI) / 2; + ctx.moveTo(x - size, y - size); + ctx.lineTo(x + size, y + size); + ctx.moveTo(x - size, y + size); + ctx.lineTo(x + size, y - size); + } + }; + + var s = series.points.symbol; + if (handlers[s]) + series.points.symbol = handlers[s]; + } + + function init(plot) { + plot.hooks.processDatapoints.push(processRawData); + } + + $.plot.plugins.push({ + init: init, + name: 'symbols', + version: '1.0' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js new file mode 100644 index 0000000000000..8c99c401d87e5 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.threshold.js @@ -0,0 +1,142 @@ +/* Flot plugin for thresholding data. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +The plugin supports these options: + + series: { + threshold: { + below: number + color: colorspec + } + } + +It can also be applied to a single series, like this: + + $.plot( $("#placeholder"), [{ + data: [ ... ], + threshold: { ... } + }]) + +An array can be passed for multiple thresholding, like this: + + threshold: [{ + below: number1 + color: color1 + },{ + below: number2 + color: color2 + }] + +These multiple threshold objects can be passed in any order since they are +sorted by the processing function. + +The data points below "below" are drawn with the specified color. This makes +it easy to mark points below 0, e.g. for budget data. + +Internally, the plugin works by splitting the data into two series, above and +below the threshold. The extra series below the threshold will have its label +cleared and the special "originSeries" attribute set to the original series. +You may need to check for this in hover events. + +*/ + +(function ($) { + var options = { + series: { threshold: null } // or { below: number, color: color spec} + }; + + function init(plot) { + function thresholdData(plot, s, datapoints, below, color) { + var ps = datapoints.pointsize, i, x, y, p, prevp, + thresholded = $.extend({}, s); // note: shallow copy + + thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format }; + thresholded.label = null; + thresholded.color = color; + thresholded.threshold = null; + thresholded.originSeries = s; + thresholded.data = []; + + var origpoints = datapoints.points, + addCrossingPoints = s.lines.show; + + var threspoints = []; + var newpoints = []; + var m; + + for (i = 0; i < origpoints.length; i += ps) { + x = origpoints[i]; + y = origpoints[i + 1]; + + prevp = p; + if (y < below) + p = threspoints; + else + p = newpoints; + + if (addCrossingPoints && prevp != p && x != null + && i > 0 && origpoints[i - ps] != null) { + var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]); + prevp.push(interx); + prevp.push(below); + for (m = 2; m < ps; ++m) + prevp.push(origpoints[i + m]); + + p.push(null); // start new segment + p.push(null); + for (m = 2; m < ps; ++m) + p.push(origpoints[i + m]); + p.push(interx); + p.push(below); + for (m = 2; m < ps; ++m) + p.push(origpoints[i + m]); + } + + p.push(x); + p.push(y); + for (m = 2; m < ps; ++m) + p.push(origpoints[i + m]); + } + + datapoints.points = newpoints; + thresholded.datapoints.points = threspoints; + + if (thresholded.datapoints.points.length > 0) { + var origIndex = $.inArray(s, plot.getData()); + // Insert newly-generated series right after original one (to prevent it from becoming top-most) + plot.getData().splice(origIndex + 1, 0, thresholded); + } + + // FIXME: there are probably some edge cases left in bars + } + + function processThresholds(plot, s, datapoints) { + if (!s.threshold) + return; + + if (s.threshold instanceof Array) { + s.threshold.sort(function(a, b) { + return a.below - b.below; + }); + + $(s.threshold).each(function(i, th) { + thresholdData(plot, s, datapoints, th.below, th.color); + }); + } + else { + thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color); + } + } + + plot.hooks.processDatapoints.push(processThresholds); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'threshold', + version: '1.2' + }); +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js new file mode 100644 index 0000000000000..991e87d364e8a --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/flot-charts/jquery.flot.time.js @@ -0,0 +1,473 @@ +/* Pretty handling of time axes. + +Copyright (c) 2007-2014 IOLA and Ole Laursen. +Licensed under the MIT license. + +Set axis.mode to "time" to enable. See the section "Time series data" in +API.txt for details. + +*/ + +import { i18n } from '@kbn/i18n'; + +(function($) { + + var options = { + xaxis: { + timezone: null, // "browser" for local to the client or timezone for timezone-js + timeformat: null, // format string to use + twelveHourClock: false, // 12 or 24 time in time mode + monthNames: null // list of names of months + } + }; + + // round to nearby lower multiple of base + + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + // Returns a string with the date d formatted according to fmt. + // A subset of the Open Group's strftime format is supported. + + function formatDate(d, fmt, monthNames, dayNames) { + + if (typeof d.strftime == "function") { + return d.strftime(fmt); + } + + var leftPad = function(n, pad) { + n = "" + n; + pad = "" + (pad == null ? "0" : pad); + return n.length == 1 ? pad + n : n; + }; + + var r = []; + var escape = false; + var hours = d.getHours(); + var isAM = hours < 12; + + if (monthNames == null) { + monthNames = [ + i18n.translate('xpack.monitoring.janLabel', { + defaultMessage: 'Jan', + }), i18n.translate('xpack.monitoring.febLabel', { + defaultMessage: 'Feb', + }), i18n.translate('xpack.monitoring.marLabel', { + defaultMessage: 'Mar', + }), i18n.translate('xpack.monitoring.aprLabel', { + defaultMessage: 'Apr', + }), i18n.translate('xpack.monitoring.mayLabel', { + defaultMessage: 'May', + }), i18n.translate('xpack.monitoring.junLabel', { + defaultMessage: 'Jun', + }), i18n.translate('xpack.monitoring.julLabel', { + defaultMessage: 'Jul', + }), i18n.translate('xpack.monitoring.augLabel', { + defaultMessage: 'Aug', + }), i18n.translate('xpack.monitoring.sepLabel', { + defaultMessage: 'Sep', + }), i18n.translate('xpack.monitoring.octLabel', { + defaultMessage: 'Oct', + }), i18n.translate('xpack.monitoring.novLabel', { + defaultMessage: 'Nov', + }), i18n.translate('xpack.monitoring.decLabel', { + defaultMessage: 'Dec', + })]; + } + + if (dayNames == null) { + dayNames = [i18n.translate('xpack.monitoring.sunLabel', { + defaultMessage: 'Sun', + }), i18n.translate('xpack.monitoring.monLabel', { + defaultMessage: 'Mon', + }), i18n.translate('xpack.monitoring.tueLabel', { + defaultMessage: 'Tue', + }), i18n.translate('xpack.monitoring.wedLabel', { + defaultMessage: 'Wed', + }), i18n.translate('xpack.monitoring.thuLabel', { + defaultMessage: 'Thu', + }), i18n.translate('xpack.monitoring.friLabel', { + defaultMessage: 'Fri', + }), i18n.translate('xpack.monitoring.satLabel', { + defaultMessage: 'Sat', + })]; + } + + var hours12; + + if (hours > 12) { + hours12 = hours - 12; + } else if (hours == 0) { + hours12 = 12; + } else { + hours12 = hours; + } + + for (var i = 0; i < fmt.length; ++i) { + + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'a': c = "" + dayNames[d.getDay()]; break; + case 'b': c = "" + monthNames[d.getMonth()]; break; + case 'd': c = leftPad(d.getDate()); break; + case 'e': c = leftPad(d.getDate(), " "); break; + case 'h': // For back-compat with 0.7; remove in 1.0 + case 'H': c = leftPad(hours); break; + case 'I': c = leftPad(hours12); break; + case 'l': c = leftPad(hours12, " "); break; + case 'm': c = leftPad(d.getMonth() + 1); break; + case 'M': c = leftPad(d.getMinutes()); break; + // quarters not in Open Group's strftime specification + case 'q': + c = "" + (Math.floor(d.getMonth() / 3) + 1); break; + case 'S': c = leftPad(d.getSeconds()); break; + case 'y': c = leftPad(d.getFullYear() % 100); break; + case 'Y': c = "" + d.getFullYear(); break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case 'w': c = "" + d.getDay(); break; + } + r.push(c); + escape = false; + } else { + if (c == "%") { + escape = true; + } else { + r.push(c); + } + } + } + + return r.join(""); + } + + // To have a consistent view of time-based data independent of which time + // zone the client happens to be in we need a date-like object independent + // of time zones. This is done through a wrapper that only calls the UTC + // versions of the accessor methods. + + function makeUtcWrapper(d) { + + function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { + sourceObj[sourceMethod] = function() { + return targetObj[targetMethod].apply(targetObj, arguments); + }; + }; + + var utc = { + date: d + }; + + // support strftime, if found + + if (d.strftime != undefined) { + addProxyMethod(utc, "strftime", d, "strftime"); + } + + addProxyMethod(utc, "getTime", d, "getTime"); + addProxyMethod(utc, "setTime", d, "setTime"); + + var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; + + for (var p = 0; p < props.length; p++) { + addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); + addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); + } + + return utc; + }; + + // select time zone strategy. This returns a date-like object tied to the + // desired timezone + + function dateGenerator(ts, opts) { + if (opts.timezone == "browser") { + return new Date(ts); + } else if (!opts.timezone || opts.timezone == "utc") { + return makeUtcWrapper(new Date(ts)); + } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { + var d = new timezoneJS.Date(); + // timezone-js is fickle, so be sure to set the time zone before + // setting the time. + d.setTimezone(opts.timezone); + d.setTime(ts); + return d; + } else { + return makeUtcWrapper(new Date(ts)); + } + } + + // map of app. size of time units in milliseconds + + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + + var baseSpec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"] + ]; + + // we don't know which variant(s) we'll need yet, but generating both is + // cheap + + var specMonths = baseSpec.concat([[3, "month"], [6, "month"], + [1, "year"]]); + var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], + [1, "year"]]); + + function init(plot) { + plot.hooks.processOptions.push(function (plot, options) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode == "time") { + axis.tickGenerator = function(axis) { + + var ticks = []; + var d = dateGenerator(axis.min, opts); + var minSize = 0; + + // make quarter use a possibility if quarters are + // mentioned in either of these options + + var spec = (opts.tickSize && opts.tickSize[1] === + "quarter") || + (opts.minTickSize && opts.minTickSize[1] === + "quarter") ? specQuarters : specMonths; + + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") { + minSize = opts.tickSize; + } else { + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + } + + for (var i = 0; i < spec.length - 1; ++i) { + if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 + && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { + break; + } + } + + var size = spec[i][0]; + var unit = spec[i][1]; + + // special-case the possibility of several years + + if (unit == "year") { + + // if given a minTickSize in years, just use it, + // ensuring that it's an integer + + if (opts.minTickSize != null && opts.minTickSize[1] == "year") { + size = Math.floor(opts.minTickSize[0]); + } else { + + var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); + var norm = (axis.delta / timeUnitSize.year) / magn; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + } + + // minimum size for years is 1 + + if (size < 1) { + size = 1; + } + } + + axis.tickSize = opts.tickSize || [size, unit]; + var tickSize = axis.tickSize[0]; + unit = axis.tickSize[1]; + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") { + d.setSeconds(floorInBase(d.getSeconds(), tickSize)); + } else if (unit == "minute") { + d.setMinutes(floorInBase(d.getMinutes(), tickSize)); + } else if (unit == "hour") { + d.setHours(floorInBase(d.getHours(), tickSize)); + } else if (unit == "month") { + d.setMonth(floorInBase(d.getMonth(), tickSize)); + } else if (unit == "quarter") { + d.setMonth(3 * floorInBase(d.getMonth() / 3, + tickSize)); + } else if (unit == "year") { + d.setFullYear(floorInBase(d.getFullYear(), tickSize)); + } + + // reset smaller components + + d.setMilliseconds(0); + + if (step >= timeUnitSize.minute) { + d.setSeconds(0); + } + if (step >= timeUnitSize.hour) { + d.setMinutes(0); + } + if (step >= timeUnitSize.day) { + d.setHours(0); + } + if (step >= timeUnitSize.day * 4) { + d.setDate(1); + } + if (step >= timeUnitSize.month * 2) { + d.setMonth(floorInBase(d.getMonth(), 3)); + } + if (step >= timeUnitSize.quarter * 2) { + d.setMonth(floorInBase(d.getMonth(), 6)); + } + if (step >= timeUnitSize.year) { + d.setMonth(0); + } + + var carry = 0; + var v = Number.NaN; + var prev; + + do { + + prev = v; + v = d.getTime(); + ticks.push(v); + + if (unit == "month" || unit == "quarter") { + if (tickSize < 1) { + + // a bit complicated - we'll divide the + // month/quarter up but we need to take + // care of fractions so we don't end up in + // the middle of a day + + d.setDate(1); + var start = d.getTime(); + d.setMonth(d.getMonth() + + (unit == "quarter" ? 3 : 1)); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getHours(); + d.setHours(0); + } else { + d.setMonth(d.getMonth() + + tickSize * (unit == "quarter" ? 3 : 1)); + } + } else if (unit == "year") { + d.setFullYear(d.getFullYear() + tickSize); + } else { + d.setTime(v + step); + } + } while (v < axis.max && v != prev); + + return ticks; + }; + + axis.tickFormatter = function (v, axis) { + + var d = dateGenerator(v, axis.options); + + // first check global format + + if (opts.timeformat != null) { + return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); + } + + // possibly use quarters if quarters are mentioned in + // any of these places + + var useQuarters = (axis.options.tickSize && + axis.options.tickSize[1] == "quarter") || + (axis.options.minTickSize && + axis.options.minTickSize[1] == "quarter"); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; + var fmt; + + if (t < timeUnitSize.minute) { + fmt = hourCode + ":%M:%S" + suffix; + } else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) { + fmt = hourCode + ":%M" + suffix; + } else { + fmt = "%b %d " + hourCode + ":%M" + suffix; + } + } else if (t < timeUnitSize.month) { + fmt = "%b %d"; + } else if ((useQuarters && t < timeUnitSize.quarter) || + (!useQuarters && t < timeUnitSize.year)) { + if (span < timeUnitSize.year) { + fmt = "%b"; + } else { + fmt = "%b %Y"; + } + } else if (useQuarters && t < timeUnitSize.year) { + if (span < timeUnitSize.year) { + fmt = "Q%q"; + } else { + fmt = "Q%q %Y"; + } + } else { + fmt = "%Y"; + } + + var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); + + return rt; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'time', + version: '1.0' + }); + + // Time-axis support used to be in Flot core, which exposed the + // formatDate function on the plot object. Various plugins depend + // on the function, so we need to re-expose it here. + + $.plot.formatDate = formatDate; + $.plot.dateGenerator = dateGenerator; + +})(jQuery); diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js new file mode 100644 index 0000000000000..abf060aca8c08 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/index.js @@ -0,0 +1,7 @@ +/* + * 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 { default } from './jquery_flot'; diff --git a/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js b/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js new file mode 100644 index 0000000000000..28a4d5f56df15 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/jquery_flot/jquery_flot.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import $ from 'jquery'; +if (window) { + window.jQuery = $; +} +import './flot-charts/jquery.flot'; + +// load flot plugins +// avoid the `canvas` plugin, it causes blurry fonts +import './flot-charts/jquery.flot.time'; +import './flot-charts/jquery.flot.crosshair'; +import './flot-charts/jquery.flot.selection'; + +export default $; diff --git a/x-pack/legacy/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js b/x-pack/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js rename to x-pack/plugins/monitoring/public/lib/logstash/__tests__/pipelines.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/logstash/pipelines.js b/x-pack/plugins/monitoring/public/lib/logstash/pipelines.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/lib/logstash/pipelines.js rename to x-pack/plugins/monitoring/public/lib/logstash/pipelines.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/route_init.js b/x-pack/plugins/monitoring/public/lib/route_init.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/lib/route_init.js rename to x-pack/plugins/monitoring/public/lib/route_init.js index 97a55303dae67..2c928334ef63e 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/route_init.js +++ b/x-pack/plugins/monitoring/public/lib/route_init.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; +import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { isInSetupMode } from './setup_mode'; import { getClusterFromClusters } from './get_cluster_from_clusters'; diff --git a/x-pack/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js new file mode 100644 index 0000000000000..c54c29df09685 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.test.js @@ -0,0 +1,229 @@ +/* + * 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. + */ + +let toggleSetupMode; +let initSetupModeState; +let getSetupModeState; +let updateSetupModeData; +let setSetupModeMenuItem; + +jest.mock('./ajax_error_handler', () => ({ + ajaxErrorHandlersProvider: err => { + throw err; + }, +})); + +jest.mock('react-dom', () => ({ + render: jest.fn(), +})); + +jest.mock('../legacy_shims', () => { + return { + Legacy: { + shims: { getAngularInjector: () => ({ get: () => ({ get: () => 'utc' }) }) }, + }, + }; +}); + +let data = {}; + +const injectorModulesMock = { + globalState: { + save: jest.fn(), + }, + Private: module => module, + $http: { + post: jest.fn().mockImplementation(() => { + return { data }; + }), + }, + $executor: { + run: jest.fn(), + }, +}; + +const angularStateMock = { + injector: { + get: module => { + return injectorModulesMock[module] || {}; + }, + }, + scope: { + $apply: fn => fn && fn(), + $evalAsync: fn => fn && fn(), + }, +}; + +// We are no longer waiting for setup mode data to be fetched when enabling +// so we need to wait for the next tick for the async action to finish +function waitForSetupModeData(action) { + process.nextTick(action); +} + +function setModulesAndMocks() { + jest.clearAllMocks().resetModules(); + injectorModulesMock.globalState.inSetupMode = false; + + const setupMode = require('./setup_mode'); + toggleSetupMode = setupMode.toggleSetupMode; + initSetupModeState = setupMode.initSetupModeState; + getSetupModeState = setupMode.getSetupModeState; + updateSetupModeData = setupMode.updateSetupModeData; + setSetupModeMenuItem = setupMode.setSetupModeMenuItem; +} + +describe('setup_mode', () => { + beforeEach(async () => { + setModulesAndMocks(); + }); + + describe('setup', () => { + it('should require angular state', async () => { + let error; + try { + toggleSetupMode(true); + } catch (err) { + error = err; + } + expect(error.message).toEqual( + 'Unable to interact with setup ' + + 'mode because the angular injector was not previously set. This needs to be ' + + 'set by calling `initSetupModeState`.' + ); + }); + + it('should enable toggle mode', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + expect(injectorModulesMock.globalState.inSetupMode).toBe(true); + }); + + it('should disable toggle mode', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(false); + expect(injectorModulesMock.globalState.inSetupMode).toBe(false); + }); + + it('should set top nav config', async () => { + const render = require('react-dom').render; + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + setSetupModeMenuItem(); + await toggleSetupMode(true); + expect(render.mock.calls.length).toBe(2); + }); + }); + + describe('in setup mode', () => { + afterEach(async () => { + data = {}; + }); + + it('should not fetch data if the user does not have sufficient permissions', async done => { + const addDanger = jest.fn(); + jest.doMock('../legacy_shims', () => ({ + Legacy: { + shims: { + toastNotifications: { + addDanger, + }, + }, + }, + })); + data = { + _meta: { + hasPermissions: false, + }, + }; + setModulesAndMocks(); + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + waitForSetupModeData(() => { + const state = getSetupModeState(); + expect(state.enabled).toBe(false); + expect(addDanger).toHaveBeenCalledWith({ + title: 'Setup mode is not available', + text: 'You do not have the necessary permissions to do this.', + }); + done(); + }); + }); + + it('should set the newly discovered cluster uuid', async done => { + const clusterUuid = '1ajy'; + data = { + _meta: { + liveClusterUuid: clusterUuid, + hasPermissions: true, + }, + elasticsearch: { + byUuid: { + 123: { + isPartiallyMigrated: true, + }, + }, + }, + }; + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + waitForSetupModeData(() => { + expect(injectorModulesMock.globalState.cluster_uuid).toBe(clusterUuid); + done(); + }); + }); + + it('should fetch data for a given cluster', async done => { + const clusterUuid = '1ajy'; + data = { + _meta: { + liveClusterUuid: clusterUuid, + hasPermissions: true, + }, + elasticsearch: { + byUuid: { + 123: { + isPartiallyMigrated: true, + }, + }, + }, + }; + + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + waitForSetupModeData(() => { + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( + `../api/monitoring/v1/setup/collection/cluster/${clusterUuid}`, + { + ccs: undefined, + } + ); + done(); + }); + }); + + it('should fetch data for a single node', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + injectorModulesMock.$http.post.mockClear(); + await updateSetupModeData('45asd'); + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith( + '../api/monitoring/v1/setup/collection/node/45asd', + { + ccs: undefined, + } + ); + }); + + it('should fetch data without a cluster uuid', async () => { + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + injectorModulesMock.$http.post.mockClear(); + await updateSetupModeData(undefined, true); + const url = '../api/monitoring/v1/setup/collection/cluster'; + const args = { ccs: undefined }; + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx similarity index 82% rename from x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx rename to x-pack/plugins/monitoring/public/lib/setup_mode.tsx index 7b081b79d6acd..5afb382b7cda8 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/lib/setup_mode.tsx @@ -7,19 +7,11 @@ import React from 'react'; import { render } from 'react-dom'; import { get, contains } from 'lodash'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { npSetup } from 'ui/new_platform'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import chrome from '../np_imports/ui/chrome'; -import { CloudSetup } from '../../../../../plugins/cloud/public'; +import { Legacy } from '../legacy_shims'; import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; -interface PluginsSetupWithCloud extends PluginsSetup { - cloud: CloudSetup; -} - function isOnPage(hash: string) { return contains(window.location.hash, hash); } @@ -46,12 +38,12 @@ const checkAngularState = () => { interface ISetupModeState { enabled: boolean; data: any; - callbacks: Function[]; + callback?: (() => void) | null; } const setupModeState: ISetupModeState = { enabled: false, data: null, - callbacks: [], + callback: null, }; export const getSetupModeState = () => setupModeState; @@ -93,18 +85,13 @@ export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid } }; -const notifySetupModeDataChange = (oldData?: any) => { - setupModeState.callbacks.forEach((cb: Function) => cb(oldData)); -}; +const notifySetupModeDataChange = () => setupModeState.callback && setupModeState.callback(); export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { - const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - const { cloud } = npSetup.plugins as PluginsSetupWithCloud; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const hasPermissions = get(data, '_meta.hasPermissions', false); - if (isCloudEnabled || !hasPermissions) { + if (Legacy.shims.isCloud || !hasPermissions) { let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { @@ -117,7 +104,7 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid } angularState.scope.$evalAsync(() => { - toastNotifications.addDanger({ + Legacy.shims.toastNotifications.addDanger({ title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { defaultMessage: 'Setup mode is not available', }), @@ -126,7 +113,7 @@ export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid }); return toggleSetupMode(false); // eslint-disable-line no-use-before-define } - notifySetupModeDataChange(oldData); + notifySetupModeDataChange(); const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; @@ -182,9 +169,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const { cloud } = npSetup.plugins as PluginsSetupWithCloud; - const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); - const enabled = !globalState.inSetupMode && !isCloudEnabled; + const enabled = !globalState.inSetupMode && !Legacy.shims.isCloud; render( <SetupModeEnterButton enabled={enabled} toggleSetupMode={toggleSetupMode} />, @@ -192,13 +177,13 @@ export const setSetupModeMenuItem = () => { ); }; -export const addSetupModeCallback = (callback: Function) => setupModeState.callbacks.push(callback); +export const addSetupModeCallback = (callback: () => void) => (setupModeState.callback = callback); -export const initSetupModeState = async ($scope: any, $injector: any, callback?: Function) => { +export const initSetupModeState = async ($scope: any, $injector: any, callback?: () => void) => { angularState.scope = $scope; angularState.injector = $injector; if (callback) { - setupModeState.callbacks.push(callback); + setupModeState.callback = callback; } const globalState = $injector.get('globalState'); @@ -212,7 +197,7 @@ export const isInSetupMode = () => { return true; } - const $injector = angularState.injector || chrome.dangerouslyGetActiveInjector(); + const $injector = angularState.injector || Legacy.shims.getAngularInjector(); const globalState = $injector.get('globalState'); return globalState.inSetupMode; }; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts new file mode 100644 index 0000000000000..63f0c46c14096 --- /dev/null +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -0,0 +1,147 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + App, + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'kibana/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; +import { MonitoringPluginDependencies, MonitoringConfig } from './types'; +import { + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from '../common/constants'; + +export class MonitoringPlugin + implements Plugin<boolean, void, MonitoringPluginDependencies, MonitoringPluginDependencies> { + constructor(private initializerContext: PluginInitializerContext<MonitoringConfig>) {} + + public setup( + core: CoreSetup<MonitoringPluginDependencies>, + plugins: object & { home?: HomePublicPluginSetup; cloud?: { isCloudEnabled: boolean } } + ) { + const { home } = plugins; + const id = 'monitoring'; + const icon = 'monitoringApp'; + const title = i18n.translate('xpack.monitoring.stackMonitoringTitle', { + defaultMessage: 'Stack Monitoring', + }); + const monitoring = this.initializerContext.config.get(); + + if (!monitoring.ui.enabled || !monitoring.enabled) { + return false; + } + + if (home) { + home.featureCatalogue.register({ + id, + title, + icon, + path: '/app/monitoring', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + description: i18n.translate('xpack.monitoring.monitoringDescription', { + defaultMessage: 'Track the real-time health and performance of your Elastic Stack.', + }), + }); + } + + const app: App = { + id, + title, + order: 9002, + euiIconType: icon, + category: DEFAULT_APP_CATEGORIES.management, + mount: async (params: AppMountParameters) => { + const [coreStart, pluginsStart] = await core.getStartServices(); + const { AngularApp } = await import('./angular'); + const deps: MonitoringPluginDependencies = { + navigation: pluginsStart.navigation, + element: params.element, + core: coreStart, + data: pluginsStart.data, + isCloud: Boolean(plugins.cloud?.isCloudEnabled), + pluginInitializerContext: this.initializerContext, + externalConfig: this.getExternalConfig(), + }; + + this.setInitialTimefilter(deps); + this.overrideAlertingEmailDefaults(deps); + + const monitoringApp = new AngularApp(deps); + const removeHistoryListener = params.history.listen(location => { + if (location.pathname === '' && location.hash === '') { + monitoringApp.applyScope(); + } + }); + + return () => { + removeHistoryListener(); + monitoringApp.destroy(); + }; + }, + }; + + core.application.register(app); + return true; + } + + public start(core: CoreStart, plugins: any) {} + + public stop() {} + + private setInitialTimefilter({ core: coreContext, data }: MonitoringPluginDependencies) { + const { timefilter } = data.query.timefilter; + const { uiSettings } = coreContext; + const refreshInterval = { value: 10000, pause: false }; + const time = { from: 'now-1h', to: 'now' }; + timefilter.setRefreshInterval(refreshInterval); + timefilter.setTime(time); + uiSettings.overrideLocalDefault( + 'timepicker:refreshIntervalDefaults', + JSON.stringify(refreshInterval) + ); + uiSettings.overrideLocalDefault('timepicker:timeDefaults', JSON.stringify(time)); + } + + private overrideAlertingEmailDefaults({ core: coreContext }: MonitoringPluginDependencies) { + const { uiSettings } = coreContext; + if (KIBANA_ALERTING_ENABLED && !uiSettings.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS)) { + uiSettings.overrideLocalDefault( + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + JSON.stringify({ + name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { + defaultMessage: 'Alerting email address', + }), + value: '', + description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { + defaultMessage: `The default email address to receive alerts from Stack Monitoring`, + }), + category: ['monitoring'], + }) + ); + } + } + + private getExternalConfig() { + const monitoring = this.initializerContext.config.get(); + return [ + ['minIntervalSeconds', monitoring.ui.min_interval_seconds], + ['showLicenseExpiration', monitoring.ui.show_license_expiration], + ['showCgroupMetricsElasticsearch', monitoring.ui.container.elasticsearch.enabled], + ['showCgroupMetricsLogstash', monitoring.ui.container.logstash.enabled], + ]; + } +} diff --git a/x-pack/plugins/monitoring/public/services/__tests__/breadcrumbs.js b/x-pack/plugins/monitoring/public/services/__tests__/breadcrumbs.js new file mode 100644 index 0000000000000..d4493d4d39e58 --- /dev/null +++ b/x-pack/plugins/monitoring/public/services/__tests__/breadcrumbs.js @@ -0,0 +1,130 @@ +/* + * 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 expect from '@kbn/expect'; +import { breadcrumbsProvider } from '../breadcrumbs'; +import { MonitoringMainController } from '../../directives/main'; + +describe('Monitoring Breadcrumbs Service', () => { + it('in Cluster Alerts', () => { + const controller = new MonitoringMainController(); + controller.setup({ + clusterName: 'test-cluster-foo', + licenseService: {}, + breadcrumbsService: breadcrumbsProvider(), + attributes: { + name: 'alerts', + }, + }); + expect(controller.breadcrumbs).to.eql([ + { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, + { url: '#/overview', label: 'test-cluster-foo' }, + ]); + }); + + it('in Cluster Overview', () => { + const controller = new MonitoringMainController(); + controller.setup({ + clusterName: 'test-cluster-foo', + licenseService: {}, + breadcrumbsService: breadcrumbsProvider(), + attributes: { + name: 'overview', + }, + }); + expect(controller.breadcrumbs).to.eql([ + { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, + ]); + }); + + it('in ES Node - Advanced', () => { + const controller = new MonitoringMainController(); + controller.setup({ + clusterName: 'test-cluster-foo', + licenseService: {}, + breadcrumbsService: breadcrumbsProvider(), + attributes: { + product: 'elasticsearch', + name: 'nodes', + instance: 'es-node-name-01', + resolver: 'es-node-resolver-01', + page: 'advanced', + tabIconClass: 'fa star', + tabIconLabel: 'Master Node', + }, + }); + expect(controller.breadcrumbs).to.eql([ + { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, + { url: '#/overview', label: 'test-cluster-foo' }, + { url: '#/elasticsearch', label: 'Elasticsearch' }, + { url: '#/elasticsearch/nodes', label: 'Nodes', testSubj: 'breadcrumbEsNodes' }, + { url: null, label: 'es-node-name-01' }, + ]); + }); + + it('in Kibana Overview', () => { + const controller = new MonitoringMainController(); + controller.setup({ + clusterName: 'test-cluster-foo', + licenseService: {}, + breadcrumbsService: breadcrumbsProvider(), + attributes: { + product: 'kibana', + name: 'overview', + }, + }); + expect(controller.breadcrumbs).to.eql([ + { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, + { url: '#/overview', label: 'test-cluster-foo' }, + { url: null, label: 'Kibana' }, + ]); + }); + + /** + * <monitoring-main product="logstash" name="nodes"> + */ + it('in Logstash Listing', () => { + const controller = new MonitoringMainController(); + controller.setup({ + clusterName: 'test-cluster-foo', + licenseService: {}, + breadcrumbsService: breadcrumbsProvider(), + attributes: { + product: 'logstash', + name: 'listing', + }, + }); + expect(controller.breadcrumbs).to.eql([ + { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, + { url: '#/overview', label: 'test-cluster-foo' }, + { url: null, label: 'Logstash' }, + ]); + }); + + /** + * <monitoring-main product="logstash" page="pipeline"> + */ + it('in Logstash Pipeline Viewer', () => { + const controller = new MonitoringMainController(); + controller.setup({ + clusterName: 'test-cluster-foo', + licenseService: {}, + breadcrumbsService: breadcrumbsProvider(), + attributes: { + product: 'logstash', + page: 'pipeline', + pipelineId: 'main', + pipelineHash: '42ee890af9...', + }, + }); + expect(controller.breadcrumbs).to.eql([ + { url: '#/home', label: 'Clusters', testSubj: 'breadcrumbClusters' }, + { url: '#/overview', label: 'test-cluster-foo' }, + { url: '#/logstash', label: 'Logstash' }, + { url: '#/logstash/pipelines', label: 'Pipelines' }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/public/services/__tests__/executor.js b/x-pack/plugins/monitoring/public/services/__tests__/executor.js new file mode 100644 index 0000000000000..1113f9e32bdc7 --- /dev/null +++ b/x-pack/plugins/monitoring/public/services/__tests__/executor.js @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ngMock from 'ng_mock'; +import expect from '@kbn/expect'; +import sinon from 'sinon'; +import { executorProvider } from '../executor'; +import Bluebird from 'bluebird'; +import { Legacy } from '../../legacy_shims'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; + +describe('$executor service', () => { + let scope; + let executor; + let $timeout; + let timefilter; + + beforeEach(() => { + const data = dataPluginMock.createStartContract(); + Legacy._shims = { timefilter }; + timefilter = data.query.timefilter.timefilter; + }); + + beforeEach(ngMock.module('kibana')); + + beforeEach( + ngMock.inject(function(_$rootScope_) { + scope = _$rootScope_.$new(); + }) + ); + + beforeEach(() => { + $timeout = sinon.spy(setTimeout); + $timeout.cancel = id => clearTimeout(id); + + timefilter.setRefreshInterval({ + value: 0, + }); + + executor = executorProvider(Bluebird, $timeout); + }); + + afterEach(() => executor.destroy()); + + it('should not call $timeout if the timefilter is not paused and set to zero', () => { + executor.start(scope); + expect($timeout.callCount).to.equal(0); + }); + + it('should call $timeout if the timefilter is not paused and set to 1000ms', () => { + timefilter.setRefreshInterval({ + pause: false, + value: 1000, + }); + executor.start(scope); + expect($timeout.callCount).to.equal(1); + }); + + it('should execute function if timefilter is not paused and interval set to 1000ms', done => { + timefilter.setRefreshInterval({ + pause: false, + value: 1000, + }); + executor.register({ execute: () => Bluebird.resolve().then(() => done(), done) }); + executor.start(scope); + }); + + it('should execute function multiple times', done => { + let calls = 0; + timefilter.setRefreshInterval({ + pause: false, + value: 10, + }); + executor.register({ + execute: () => { + if (calls++ > 1) { + done(); + } + return Bluebird.resolve(); + }, + }); + executor.start(scope); + }); + + it('should call handleResponse', done => { + timefilter.setRefreshInterval({ + pause: false, + value: 10, + }); + executor.register({ + execute: () => Bluebird.resolve(), + handleResponse: () => done(), + }); + executor.start(scope); + }); + + it('should call handleError', done => { + timefilter.setRefreshInterval({ + pause: false, + value: 10, + }); + executor.register({ + execute: () => Bluebird.reject(new Error('reject test')), + handleError: () => done(), + }); + executor.start(scope); + }); +}); diff --git a/x-pack/plugins/monitoring/public/services/breadcrumbs.js b/x-pack/plugins/monitoring/public/services/breadcrumbs.js new file mode 100644 index 0000000000000..f2867180e9c4c --- /dev/null +++ b/x-pack/plugins/monitoring/public/services/breadcrumbs.js @@ -0,0 +1,208 @@ +/* + * 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 { Legacy } from '../legacy_shims'; +import { i18n } from '@kbn/i18n'; + +// Helper for making objects to use in a link element +const createCrumb = (url, label, testSubj) => { + const crumb = { url, label }; + if (testSubj) { + crumb.testSubj = testSubj; + } + return crumb; +}; + +// generate Elasticsearch breadcrumbs +function getElasticsearchBreadcrumbs(mainInstance) { + const breadcrumbs = []; + if (mainInstance.instance) { + breadcrumbs.push(createCrumb('#/elasticsearch', 'Elasticsearch')); + if (mainInstance.name === 'indices') { + breadcrumbs.push( + createCrumb( + '#/elasticsearch/indices', + i18n.translate('xpack.monitoring.breadcrumbs.es.indicesLabel', { + defaultMessage: 'Indices', + }), + 'breadcrumbEsIndices' + ) + ); + } else if (mainInstance.name === 'nodes') { + breadcrumbs.push( + createCrumb( + '#/elasticsearch/nodes', + i18n.translate('xpack.monitoring.breadcrumbs.es.nodesLabel', { defaultMessage: 'Nodes' }), + 'breadcrumbEsNodes' + ) + ); + } else if (mainInstance.name === 'ml') { + // ML Instance (for user later) + breadcrumbs.push( + createCrumb( + '#/elasticsearch/ml_jobs', + i18n.translate('xpack.monitoring.breadcrumbs.es.jobsLabel', { defaultMessage: 'Jobs' }) + ) + ); + } else if (mainInstance.name === 'ccr_shard') { + breadcrumbs.push( + createCrumb( + '#/elasticsearch/ccr', + i18n.translate('xpack.monitoring.breadcrumbs.es.ccrLabel', { defaultMessage: 'CCR' }) + ) + ); + } + breadcrumbs.push(createCrumb(null, mainInstance.instance)); + } else { + // don't link to Overview when we're possibly on Overview or its sibling tabs + breadcrumbs.push(createCrumb(null, 'Elasticsearch')); + } + return breadcrumbs; +} + +// generate Kibana breadcrumbs +function getKibanaBreadcrumbs(mainInstance) { + const breadcrumbs = []; + if (mainInstance.instance) { + breadcrumbs.push(createCrumb('#/kibana', 'Kibana')); + breadcrumbs.push( + createCrumb( + '#/kibana/instances', + i18n.translate('xpack.monitoring.breadcrumbs.kibana.instancesLabel', { + defaultMessage: 'Instances', + }) + ) + ); + } else { + // don't link to Overview when we're possibly on Overview or its sibling tabs + breadcrumbs.push(createCrumb(null, 'Kibana')); + } + return breadcrumbs; +} + +// generate Logstash breadcrumbs +function getLogstashBreadcrumbs(mainInstance) { + const logstashLabel = i18n.translate('xpack.monitoring.breadcrumbs.logstashLabel', { + defaultMessage: 'Logstash', + }); + const breadcrumbs = []; + if (mainInstance.instance) { + breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); + if (mainInstance.name === 'nodes') { + breadcrumbs.push( + createCrumb( + '#/logstash/nodes', + i18n.translate('xpack.monitoring.breadcrumbs.logstash.nodesLabel', { + defaultMessage: 'Nodes', + }) + ) + ); + } + breadcrumbs.push(createCrumb(null, mainInstance.instance)); + } else if (mainInstance.page === 'pipeline') { + breadcrumbs.push(createCrumb('#/logstash', logstashLabel)); + breadcrumbs.push( + createCrumb( + '#/logstash/pipelines', + i18n.translate('xpack.monitoring.breadcrumbs.logstash.pipelinesLabel', { + defaultMessage: 'Pipelines', + }) + ) + ); + } else { + // don't link to Overview when we're possibly on Overview or its sibling tabs + breadcrumbs.push(createCrumb(null, logstashLabel)); + } + + return breadcrumbs; +} + +// generate Beats breadcrumbs +function getBeatsBreadcrumbs(mainInstance) { + const beatsLabel = i18n.translate('xpack.monitoring.breadcrumbs.beatsLabel', { + defaultMessage: 'Beats', + }); + const breadcrumbs = []; + if (mainInstance.instance) { + breadcrumbs.push(createCrumb('#/beats', beatsLabel)); + breadcrumbs.push( + createCrumb( + '#/beats/beats', + i18n.translate('xpack.monitoring.breadcrumbs.beats.instancesLabel', { + defaultMessage: 'Instances', + }) + ) + ); + breadcrumbs.push(createCrumb(null, mainInstance.instance)); + } else { + breadcrumbs.push(createCrumb(null, beatsLabel)); + } + + return breadcrumbs; +} + +// generate Apm breadcrumbs +function getApmBreadcrumbs(mainInstance) { + const apmLabel = i18n.translate('xpack.monitoring.breadcrumbs.apmLabel', { + defaultMessage: 'APM', + }); + const breadcrumbs = []; + if (mainInstance.instance) { + breadcrumbs.push(createCrumb('#/apm', apmLabel)); + breadcrumbs.push( + createCrumb( + '#/apm/instances', + i18n.translate('xpack.monitoring.breadcrumbs.apm.instancesLabel', { + defaultMessage: 'Instances', + }) + ) + ); + } else { + // don't link to Overview when we're possibly on Overview or its sibling tabs + breadcrumbs.push(createCrumb(null, apmLabel)); + } + return breadcrumbs; +} + +export function breadcrumbsProvider() { + return function createBreadcrumbs(clusterName, mainInstance) { + const homeCrumb = i18n.translate('xpack.monitoring.breadcrumbs.clustersLabel', { + defaultMessage: 'Clusters', + }); + + let breadcrumbs = [createCrumb('#/home', homeCrumb, 'breadcrumbClusters')]; + + if (!mainInstance.inOverview && clusterName) { + breadcrumbs.push(createCrumb('#/overview', clusterName)); + } + + if (mainInstance.inElasticsearch) { + breadcrumbs = breadcrumbs.concat(getElasticsearchBreadcrumbs(mainInstance)); + } + if (mainInstance.inKibana) { + breadcrumbs = breadcrumbs.concat(getKibanaBreadcrumbs(mainInstance)); + } + if (mainInstance.inLogstash) { + breadcrumbs = breadcrumbs.concat(getLogstashBreadcrumbs(mainInstance)); + } + if (mainInstance.inBeats) { + breadcrumbs = breadcrumbs.concat(getBeatsBreadcrumbs(mainInstance)); + } + if (mainInstance.inApm) { + breadcrumbs = breadcrumbs.concat(getApmBreadcrumbs(mainInstance)); + } + + Legacy.shims.breadcrumbs.set( + breadcrumbs.map(b => ({ + text: b.label, + href: b.url, + 'data-test-subj': b.testSubj, + })) + ); + + return breadcrumbs; + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/services/clusters.js b/x-pack/plugins/monitoring/public/services/clusters.js similarity index 77% rename from x-pack/legacy/plugins/monitoring/public/services/clusters.js rename to x-pack/plugins/monitoring/public/services/clusters.js index 40d6fa59228f8..dfa538b458054 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/clusters.js +++ b/x-pack/plugins/monitoring/public/services/clusters.js @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; -import { timefilter } from 'plugins/monitoring/np_imports/ui/timefilter'; +import { ajaxErrorHandlersProvider } from '../lib/ajax_error_handler'; +import { Legacy } from '../legacy_shims'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../common/constants'; function formatClusters(clusters) { @@ -20,10 +19,9 @@ function formatCluster(cluster) { return cluster; } -const uiModule = uiModules.get('monitoring/clusters'); -uiModule.service('monitoringClusters', $injector => { +export function monitoringClustersProvider($injector) { return (clusterUuid, ccs, codePaths) => { - const { min, max } = timefilter.getBounds(); + const { min, max } = Legacy.shims.timefilter.getBounds(); // append clusterUuid if the parameter is given let url = '../api/monitoring/v1/clusters'; @@ -51,4 +49,4 @@ uiModule.service('monitoringClusters', $injector => { return ajaxErrorHandlers(err); }); }; -}); +} diff --git a/x-pack/plugins/monitoring/public/services/executor.js b/x-pack/plugins/monitoring/public/services/executor.js new file mode 100644 index 0000000000000..7c5e9d652b64e --- /dev/null +++ b/x-pack/plugins/monitoring/public/services/executor.js @@ -0,0 +1,129 @@ +/* + * 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 { Legacy } from '../legacy_shims'; +import { subscribeWithScope } from '../angular/helpers/utils'; +import { Subscription } from 'rxjs'; + +export function executorProvider($timeout, $q) { + const queue = []; + const subscriptions = new Subscription(); + let executionTimer; + let ignorePaused = false; + + /** + * Resets the timer to start again + * @returns {void} + */ + function reset() { + cancel(); + start(); + } + + function killTimer() { + if (executionTimer) { + $timeout.cancel(executionTimer); + } + } + + /** + * Cancels the execution timer + * @returns {void} + */ + function cancel() { + killTimer(); + } + + /** + * Registers a service with the executor + * @param {object} service The service to register + * @returns {void} + */ + function register(service) { + queue.push(service); + } + + /** + * Stops the executor and empties the service queue + * @returns {void} + */ + function destroy() { + subscriptions.unsubscribe(); + cancel(); + ignorePaused = false; + queue.splice(0, queue.length); + } + + /** + * Runs the queue (all at once) + * @returns {Promise} a promise of all the services + */ + function run() { + const noop = () => $q.resolve(); + return $q + .all( + queue.map(service => { + return service + .execute() + .then(service.handleResponse || noop) + .catch(service.handleError || noop); + }) + ) + .finally(reset); + } + + function reFetch() { + cancel(); + run(); + } + + function killIfPaused() { + if (Legacy.shims.timefilter.getRefreshInterval().pause) { + killTimer(); + } + } + + /** + * Starts the executor service if the timefilter is not paused + * @returns {void} + */ + function start() { + const timefilter = Legacy.shims.timefilter; + if ( + (ignorePaused || timefilter.getRefreshInterval().pause === false) && + timefilter.getRefreshInterval().value > 0 + ) { + executionTimer = $timeout(run, timefilter.getRefreshInterval().value); + } + } + + /** + * Expose the methods + */ + return { + register, + start($scope) { + $scope.$applyAsync(() => { + const timefilter = Legacy.shims.timefilter; + subscriptions.add( + subscribeWithScope($scope, timefilter.getFetch$(), { + next: reFetch, + }) + ); + subscriptions.add( + subscribeWithScope($scope, timefilter.getRefreshIntervalUpdate$(), { + next: killIfPaused, + }) + ); + start(); + }); + }, + run, + destroy, + reset, + cancel, + }; +} diff --git a/x-pack/legacy/plugins/monitoring/public/services/features.js b/x-pack/plugins/monitoring/public/services/features.js similarity index 86% rename from x-pack/legacy/plugins/monitoring/public/services/features.js rename to x-pack/plugins/monitoring/public/services/features.js index e2357ef08d7df..f98af10f8dfb4 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/features.js +++ b/x-pack/plugins/monitoring/public/services/features.js @@ -5,10 +5,8 @@ */ import _ from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; -const uiModule = uiModules.get('monitoring/features', []); -uiModule.service('features', function($window) { +export function featuresProvider($window) { function getData() { let returnData = {}; const monitoringData = $window.localStorage.getItem('xpack.monitoring.data'); @@ -45,4 +43,4 @@ uiModule.service('features', function($window) { isEnabled, update, }; -}); +} diff --git a/x-pack/legacy/plugins/monitoring/public/services/license.js b/x-pack/plugins/monitoring/public/services/license.js similarity index 88% rename from x-pack/legacy/plugins/monitoring/public/services/license.js rename to x-pack/plugins/monitoring/public/services/license.js index 94078b799fdf1..341309004b110 100644 --- a/x-pack/legacy/plugins/monitoring/public/services/license.js +++ b/x-pack/plugins/monitoring/public/services/license.js @@ -5,11 +5,9 @@ */ import { contains } from 'lodash'; -import { uiModules } from 'plugins/monitoring/np_imports/ui/modules'; import { ML_SUPPORTED_LICENSES } from '../../common/constants'; -const uiModule = uiModules.get('monitoring/license', []); -uiModule.service('license', () => { +export function licenseProvider() { return new (class LicenseService { constructor() { // do not initialize with usable state @@ -50,4 +48,4 @@ uiModule.service('license', () => { return false; } })(); -}); +} diff --git a/x-pack/plugins/monitoring/public/services/title.js b/x-pack/plugins/monitoring/public/services/title.js new file mode 100644 index 0000000000000..0715f4dc9e0b6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/services/title.js @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { Legacy } from '../legacy_shims'; + +export function titleProvider($rootScope) { + return function changeTitle(cluster, suffix) { + let clusterName = _.get(cluster, 'cluster_name'); + clusterName = clusterName ? `- ${clusterName}` : ''; + suffix = suffix ? `- ${suffix}` : ''; + $rootScope.$applyAsync(() => { + Legacy.shims.docTitle.change( + i18n.translate('xpack.monitoring.stackMonitoringDocTitle', { + defaultMessage: 'Stack Monitoring {clusterName} {suffix}', + values: { clusterName, suffix }, + }) + ); + }); + }; +} diff --git a/x-pack/plugins/monitoring/public/types.ts b/x-pack/plugins/monitoring/public/types.ts new file mode 100644 index 0000000000000..5fcb6b50f5d83 --- /dev/null +++ b/x-pack/plugins/monitoring/public/types.ts @@ -0,0 +1,21 @@ +/* + * 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 { PluginInitializerContext, CoreStart } from 'kibana/public'; +import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; + +export { MonitoringConfig } from '../server'; + +export interface MonitoringPluginDependencies { + navigation: NavigationStart; + data: DataPublicPluginStart; + element: HTMLElement; + core: CoreStart; + isCloud: boolean; + pluginInitializerContext: PluginInitializerContext; + externalConfig: Array<Array<string | number> | Array<string | boolean>>; +} diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts new file mode 100644 index 0000000000000..e66d5462c2bb5 --- /dev/null +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -0,0 +1,169 @@ +/* + * 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 { Subscription } from 'rxjs'; +import { History } from 'history'; +import { createHashHistory } from 'history'; +import { MonitoringPluginDependencies } from './types'; + +import { + RefreshInterval, + TimeRange, + syncQueryStateWithUrl, +} from '../../../../src/plugins/data/public'; + +import { + createStateContainer, + createKbnUrlStateStorage, + StateContainer, + INullableBaseStateContainer, + IKbnUrlStateStorage, + ISyncStateRef, + syncState, +} from '../../../../src/plugins/kibana_utils/public'; + +interface Route { + params: { _g: unknown }; +} + +interface RawObject { + [key: string]: unknown; +} + +export interface MonitoringAppState { + [key: string]: unknown; + cluster_uuid?: string; + ccs?: boolean; + inSetupMode?: boolean; + refreshInterval?: RefreshInterval; + time?: TimeRange; + filters?: any[]; +} + +export interface MonitoringAppStateTransitions { + set: ( + state: MonitoringAppState + ) => <T extends keyof MonitoringAppState>( + prop: T, + value: MonitoringAppState[T] + ) => MonitoringAppState; +} + +const GLOBAL_STATE_KEY = '_g'; +const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB); + +export class GlobalState { + private readonly stateSyncRef: ISyncStateRef; + private readonly stateContainer: StateContainer< + MonitoringAppState, + MonitoringAppStateTransitions + >; + private readonly stateStorage: IKbnUrlStateStorage; + private readonly stateContainerChangeSub: Subscription; + private readonly syncQueryStateWithUrlManager: { stop: () => void }; + private readonly timefilterRef: MonitoringPluginDependencies['data']['query']['timefilter']['timefilter']; + + private lastAssignedState: MonitoringAppState = {}; + private lastKnownGlobalState?: string; + + constructor( + queryService: MonitoringPluginDependencies['data']['query'], + rootScope: ng.IRootScopeService, + ngLocation: ng.ILocationService, + externalState: RawObject + ) { + this.timefilterRef = queryService.timefilter.timefilter; + + const history: History = createHashHistory(); + this.stateStorage = createKbnUrlStateStorage({ useHash: false, history }); + + const initialStateFromUrl = this.stateStorage.get(GLOBAL_STATE_KEY) as MonitoringAppState; + + this.stateContainer = createStateContainer(initialStateFromUrl, { + set: state => (prop, value) => ({ ...state, [prop]: value }), + }); + + this.stateSyncRef = syncState({ + storageKey: GLOBAL_STATE_KEY, + stateContainer: this.stateContainer as INullableBaseStateContainer<MonitoringAppState>, + stateStorage: this.stateStorage, + }); + + this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { + this.lastAssignedState = this.getState(); + if (!this.stateContainer.get() && this.lastKnownGlobalState) { + rootScope.$applyAsync(() => + ngLocation.search(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace() + ); + } + this.syncExternalState(externalState); + }); + + this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage); + this.stateSyncRef.start(); + this.startHashSync(rootScope, ngLocation); + this.lastAssignedState = this.getState(); + + rootScope.$on('$destroy', () => this.destroy()); + } + + private syncExternalState(externalState: { [key: string]: unknown }) { + const currentState = this.stateContainer.get(); + for (const key in currentState) { + if ( + ({ save: 1, time: 1, refreshInterval: 1, filters: 1 } as { [key: string]: number })[key] + ) { + continue; + } + if (currentState[key] !== externalState[key]) { + externalState[key] = currentState[key]; + } + } + } + + private startHashSync(rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService) { + rootScope.$on( + '$routeChangeStart', + (_: { preventDefault: () => void }, newState: Route, oldState: Route) => { + const currentGlobalState = oldState?.params?._g; + const nextGlobalState = newState?.params?._g; + if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') { + newState.params._g = currentGlobalState; + ngLocation.search(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); + } + this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string; + } + ); + } + + public setState(state?: { [key: string]: unknown }) { + const currentAppState = this.getState(); + const newAppState = { ...currentAppState, ...state }; + if (state && objectEquals(newAppState, currentAppState)) { + return; + } + const newState = { + ...newAppState, + refreshInterval: this.timefilterRef.getRefreshInterval(), + time: this.timefilterRef.getTime(), + }; + this.lastAssignedState = newState; + this.stateContainer.set(newState); + } + + public getState(): MonitoringAppState { + const currentState = { ...this.lastAssignedState, ...this.stateContainer.get() }; + delete currentState.filters; + const { refreshInterval: _nullA, time: _nullB, ...currentAppState } = currentState; + return currentAppState || {}; + } + + public destroy() { + this.syncQueryStateWithUrlManager.stop(); + this.stateContainerChangeSub.unsubscribe(); + this.stateSyncRef.stop(); + } +} diff --git a/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js b/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js new file mode 100644 index 0000000000000..4f7180b138aa5 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/__tests__/base_controller.js @@ -0,0 +1,209 @@ +/* + * 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 { spy, stub } from 'sinon'; +import expect from '@kbn/expect'; +import { MonitoringViewBaseController } from '../'; +import { Legacy } from '../../legacy_shims'; +import { PromiseWithCancel, Status } from '../../../common/cancel_promise'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; + +/* + * Mostly copied from base_table_controller test, with modifications + */ +describe('MonitoringViewBaseController', function() { + let ctrl; + let $injector; + let $scope; + let opts; + let titleService; + let executorService; + let configService; + let timefilter; + const httpCall = ms => new Promise(resolve => setTimeout(() => resolve(), ms)); + + before(() => { + const data = dataPluginMock.createStartContract(); + Legacy._shims = { timefilter }; + timefilter = data.query.timefilter.timefilter; + titleService = spy(); + executorService = { + register: spy(), + start: spy(), + cancel: spy(), + run: spy(), + }; + configService = { + get: spy(), + }; + + const windowMock = () => { + const events = {}; + const targetEvent = 'popstate'; + return { + removeEventListener: stub(), + addEventListener: (name, handler) => name === targetEvent && (events[name] = handler), + history: { + back: () => events[targetEvent] && events[targetEvent](), + }, + }; + }; + + const injectorGetStub = stub(); + injectorGetStub.withArgs('title').returns(titleService); + injectorGetStub.withArgs('$executor').returns(executorService); + injectorGetStub + .withArgs('localStorage') + .throws('localStorage should not be used by this class'); + injectorGetStub.withArgs('$window').returns(windowMock()); + injectorGetStub.withArgs('config').returns(configService); + $injector = { get: injectorGetStub }; + + $scope = { + cluster: { cluster_uuid: 'foo' }, + $on: stub(), + $apply: stub(), + }; + + opts = { + title: 'testo', + getPageData: () => Promise.resolve({ data: { test: true } }), + $injector, + $scope, + }; + + ctrl = new MonitoringViewBaseController(opts); + }); + + it('show/hide zoom-out button based on interaction', done => { + const xaxis = { from: 1562089923880, to: 1562090159676 }; + const timeRange = { xaxis }; + const { zoomInfo } = ctrl; + + ctrl.onBrush(timeRange); + + expect(zoomInfo.showZoomOutBtn()).to.be(true); + + /* + Need to do this async, since we are delaying event adding + */ + setTimeout(() => { + zoomInfo.zoomOutHandler(); + expect(zoomInfo.showZoomOutBtn()).to.be(false); + done(); + }, 15); + }); + + it('creates functions for fetching data', () => { + expect(ctrl.updateData).to.be.a('function'); + expect(ctrl.onBrush).to.be.a('function'); + }); + + it('sets page title', () => { + expect(titleService.calledOnce).to.be(true); + const { args } = titleService.getCall(0); + expect(args).to.eql([{ cluster_uuid: 'foo' }, 'testo']); + }); + + it('starts data poller', () => { + expect(executorService.register.calledOnce).to.be(true); + expect(executorService.start.calledOnce).to.be(true); + }); + + it('does not allow for a new request if one is inflight', done => { + let counter = 0; + const opts = { + title: 'testo', + getPageData: ms => httpCall(ms), + $injector, + $scope, + }; + + const ctrl = new MonitoringViewBaseController(opts); + ctrl.updateData(30).then(() => ++counter); + ctrl.updateData(60).then(() => { + expect(counter).to.be(0); + done(); + }); + }); + + describe('time filter', () => { + it('enables timepicker and auto refresh #1', () => { + expect(timefilter.isTimeRangeSelectorEnabled()).to.be(true); + expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(true); + }); + + it('enables timepicker and auto refresh #2', () => { + opts = { + ...opts, + options: {}, + }; + ctrl = new MonitoringViewBaseController(opts); + + expect(timefilter.isTimeRangeSelectorEnabled()).to.be(true); + expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(true); + }); + + it('disables timepicker and enables auto refresh', () => { + opts = { + ...opts, + options: { enableTimeFilter: false }, + }; + ctrl = new MonitoringViewBaseController(opts); + + expect(timefilter.isTimeRangeSelectorEnabled()).to.be(false); + expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(true); + }); + + it('enables timepicker and disables auto refresh', () => { + opts = { + ...opts, + options: { enableAutoRefresh: false }, + }; + ctrl = new MonitoringViewBaseController(opts); + + expect(timefilter.isTimeRangeSelectorEnabled()).to.be(true); + expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(false); + }); + + it('disables timepicker and auto refresh', () => { + opts = { + ...opts, + options: { + enableTimeFilter: false, + enableAutoRefresh: false, + }, + }; + ctrl = new MonitoringViewBaseController(opts); + + expect(timefilter.isTimeRangeSelectorEnabled()).to.be(false); + expect(timefilter.isAutoRefreshSelectorEnabled()).to.be(false); + }); + + it('disables timepicker and auto refresh', done => { + opts = { + title: 'test', + getPageData: () => httpCall(60), + $injector, + $scope, + }; + + ctrl = new MonitoringViewBaseController({ ...opts }); + ctrl.updateDataPromise = new PromiseWithCancel(httpCall(50)); + + let shouldBeFalse = false; + ctrl.updateDataPromise.promise().then(() => (shouldBeFalse = true)); + + const lastUpdateDataPromise = ctrl.updateDataPromise; + + ctrl.updateData().then(() => { + expect(shouldBeFalse).to.be(false); + expect(lastUpdateDataPromise.status()).to.be(Status.Canceled); + done(); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/__tests__/base_table_controller.js b/x-pack/plugins/monitoring/public/views/__tests__/base_table_controller.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/__tests__/base_table_controller.js rename to x-pack/plugins/monitoring/public/views/__tests__/base_table_controller.js diff --git a/x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html b/x-pack/plugins/monitoring/public/views/access_denied/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/access_denied/index.html rename to x-pack/plugins/monitoring/public/views/access_denied/index.html diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.js b/x-pack/plugins/monitoring/public/views/access_denied/index.js new file mode 100644 index 0000000000000..856e59702963a --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.js @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noop } from 'lodash'; +import { uiRoutes } from '../../angular/helpers/routes'; +import { Legacy } from '../../legacy_shims'; +import template from './index.html'; + +const tryPrivilege = ($http, kbnUrl) => { + return $http + .get('../api/monitoring/v1/check_access') + .then(() => kbnUrl.redirect('/home')) + .catch(noop); +}; + +uiRoutes.when('/access-denied', { + template, + resolve: { + /* + * The user may have been granted privileges in between leaving Monitoring + * and before coming back to Monitoring. That means, they just be on this + * page because Kibana remembers the "last app URL". We check for the + * privilege one time up front (doing it in the resolve makes it happen + * before the template renders), and then keep retrying every 5 seconds. + */ + initialCheck($http, kbnUrl) { + return tryPrivilege($http, kbnUrl); + }, + }, + controllerAs: 'accessDenied', + controller($scope, $injector) { + const $window = $injector.get('$window'); + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const $http = $injector.get('$http'); + const kbnUrl = $injector.get('kbnUrl'); + const $interval = $injector.get('$interval'); + + // The template's "Back to Kibana" button click handler + this.goToKibana = () => { + $window.location.href = Legacy.shims.getBasePath() + kbnBaseUrl; + }; + + // keep trying to load data in the background + const accessPoller = $interval(() => tryPrivilege($http, kbnUrl), 5 * 1000); // every 5 seconds + $scope.$on('$destroy', () => $interval.cancel(accessPoller)); + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.html b/x-pack/plugins/monitoring/public/views/alerts/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/alerts/index.html rename to x-pack/plugins/monitoring/public/views/alerts/index.html diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.js b/x-pack/plugins/monitoring/public/views/alerts/index.js new file mode 100644 index 0000000000000..2e7a34c0aef29 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/alerts/index.js @@ -0,0 +1,128 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { render } from 'react-dom'; +import { find, get } from 'lodash'; +import { uiRoutes } from '../../angular/helpers/routes'; +import template from './index.html'; +import { routeInitProvider } from '../../lib/route_init'; +import { ajaxErrorHandlersProvider } from '../../lib/ajax_error_handler'; +import { Legacy } from '../../legacy_shims'; +import { Alerts } from '../../components/alerts'; +import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui'; +import { CODE_PATH_ALERTS, KIBANA_ALERTING_ENABLED } from '../../../common/constants'; + +function getPageData($injector) { + const globalState = $injector.get('globalState'); + const $http = $injector.get('$http'); + const Private = $injector.get('Private'); + const url = KIBANA_ALERTING_ENABLED + ? `../api/monitoring/v1/alert_status` + : `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; + + const timeBounds = Legacy.shims.timefilter.getBounds(); + const data = { + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }; + + if (!KIBANA_ALERTING_ENABLED) { + data.ccs = globalState.ccs; + } + + return $http + .post(url, data) + .then(response => { + const result = get(response, 'data', []); + if (KIBANA_ALERTING_ENABLED) { + return result.alerts; + } + return result; + }) + .catch(err => { + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/alerts', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ALERTS] }); + }, + alerts: getPageData, + }, + controllerAs: 'alerts', + controller: class AlertsView extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + const kbnUrl = $injector.get('kbnUrl'); + + // breadcrumbs + page title + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + super({ + title: i18n.translate('xpack.monitoring.alerts.clusterAlertsTitle', { + defaultMessage: 'Cluster Alerts', + }), + getPageData, + $scope, + $injector, + storageKey: 'alertsTable', + reactNodeId: 'monitoringAlertsApp', + }); + + this.data = $route.current.locals.alerts; + + const renderReact = data => { + const app = data.message ? ( + <p>{data.message}</p> + ) : ( + <Alerts + alerts={data} + angular={{ kbnUrl, scope: $scope }} + sorting={this.sorting} + pagination={this.pagination} + onTableChange={this.onTableChange} + /> + ); + + render( + <EuiPage> + <EuiPageBody> + <EuiPageContent> + {app} + <EuiSpacer size="m" /> + <EuiLink href="#/overview"> + <FormattedMessage + id="xpack.monitoring.alerts.clusterOverviewLinkLabel" + defaultMessage="« Cluster Overview" + /> + </EuiLink> + </EuiPageContent> + </EuiPageBody> + </EuiPage>, + document.getElementById('monitoringAlertsApp') + ); + }; + $scope.$watch( + () => this.data, + data => renderReact(data) + ); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/all.js b/x-pack/plugins/monitoring/public/views/all.js new file mode 100644 index 0000000000000..51dcce751863c --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/all.js @@ -0,0 +1,38 @@ +/* + * 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 './no_data'; +import './access_denied'; +import './alerts'; +import './license'; +import './cluster/listing'; +import './cluster/overview'; +import './elasticsearch/overview'; +import './elasticsearch/indices'; +import './elasticsearch/index'; +import './elasticsearch/index/advanced'; +import './elasticsearch/nodes'; +import './elasticsearch/node'; +import './elasticsearch/node/advanced'; +import './elasticsearch/ccr'; +import './elasticsearch/ccr/shard'; +import './elasticsearch/ml_jobs'; +import './kibana/overview'; +import './kibana/instances'; +import './kibana/instance'; +import './logstash/overview'; +import './logstash/nodes'; +import './logstash/node'; +import './logstash/node/advanced'; +import './logstash/node/pipelines'; +import './logstash/pipelines'; +import './logstash/pipeline'; +import './beats/overview'; +import './beats/listing'; +import './beats/beat'; +import './apm/overview'; +import './apm/instances'; +import './apm/instance'; diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.html b/x-pack/plugins/monitoring/public/views/apm/instance/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/apm/instance/index.html rename to x-pack/plugins/monitoring/public/views/apm/instance/index.html diff --git a/x-pack/plugins/monitoring/public/views/apm/instance/index.js b/x-pack/plugins/monitoring/public/views/apm/instance/index.js new file mode 100644 index 0000000000000..982857ab5aea4 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/apm/instance/index.js @@ -0,0 +1,76 @@ +/* + * 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. + */ + +/* + * 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 { i18n } from '@kbn/i18n'; +import { find, get } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { ApmServerInstance } from '../../../components/apm/instance'; +import { CODE_PATH_APM } from '../../../../common/constants'; + +uiRoutes.when('/apm/instances/:uuid', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_APM] }); + }, + }, + + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const title = $injector.get('title'); + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + super({ + title: i18n.translate('xpack.monitoring.apm.instance.routeTitle', { + defaultMessage: '{apm} - Instance', + values: { + apm: 'APM', + }, + }), + api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm/${$route.current.params.uuid}`, + defaultData: {}, + reactNodeId: 'apmInstanceReact', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + title($scope.cluster, `APM - ${get(data, 'apmSummary.name')}`); + this.renderReact(data); + } + ); + } + + renderReact(data) { + const component = ( + <ApmServerInstance + summary={data.apmSummary || {}} + metrics={data.metrics || {}} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + ); + super.renderReact(component); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.html b/x-pack/plugins/monitoring/public/views/apm/instances/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/apm/instances/index.html rename to x-pack/plugins/monitoring/public/views/apm/instances/index.html diff --git a/x-pack/plugins/monitoring/public/views/apm/instances/index.js b/x-pack/plugins/monitoring/public/views/apm/instances/index.js new file mode 100644 index 0000000000000..8cd0b03e89e04 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/apm/instances/index.js @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { ApmServerInstances } from '../../../components/apm/instances'; +import { MonitoringViewBaseEuiTableController } from '../..'; +import { SetupModeRenderer } from '../../../components/renderers'; +import { APM_SYSTEM_ID, CODE_PATH_APM } from '../../../../common/constants'; + +uiRoutes.when('/apm/instances', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_APM] }); + }, + }, + controller: class extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + super({ + title: i18n.translate('xpack.monitoring.apm.instances.routeTitle', { + defaultMessage: '{apm} - Instances', + values: { + apm: 'APM', + }, + }), + storageKey: 'apm.instances', + api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm/instances`, + defaultData: {}, + reactNodeId: 'apmInstancesReact', + $scope, + $injector, + }); + + this.scope = $scope; + this.injector = $injector; + + $scope.$watch( + () => this.data, + data => { + this.renderReact(data); + } + ); + } + + renderReact(data) { + const { pagination, sorting, onTableChange } = this; + + const component = ( + <SetupModeRenderer + scope={this.scope} + injector={this.injector} + productName={APM_SYSTEM_ID} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( + <Fragment> + {flyoutComponent} + <ApmServerInstances + setupMode={setupMode} + apms={{ + pagination, + sorting, + onTableChange, + data, + }} + /> + {bottomBarComponent} + </Fragment> + )} + /> + ); + super.renderReact(component); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.html b/x-pack/plugins/monitoring/public/views/apm/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/apm/overview/index.html rename to x-pack/plugins/monitoring/public/views/apm/overview/index.html diff --git a/x-pack/plugins/monitoring/public/views/apm/overview/index.js b/x-pack/plugins/monitoring/public/views/apm/overview/index.js new file mode 100644 index 0000000000000..1fdd441e62e22 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/apm/overview/index.js @@ -0,0 +1,54 @@ +/* + * 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 { find } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { ApmOverview } from '../../../components/apm/overview'; +import { CODE_PATH_APM } from '../../../../common/constants'; + +uiRoutes.when('/apm', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_APM] }); + }, + }, + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + super({ + title: 'APM', + api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/apm`, + defaultData: {}, + reactNodeId: 'apmOverviewReact', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + this.renderReact(data); + } + ); + } + + renderReact(data) { + const component = <ApmOverview {...data} onBrush={this.onBrush} zoomInfo={this.zoomInfo} />; + super.renderReact(component); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js new file mode 100644 index 0000000000000..e5e59f2f8e826 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import moment from 'moment'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { getPageData } from '../lib/get_page_data'; +import { PageLoading } from '../components'; +import { Legacy } from '../legacy_shims'; +import { PromiseWithCancel } from '../../common/cancel_promise'; +import { updateSetupModeData, getSetupModeState } from '../lib/setup_mode'; + +/** + * Given a timezone, this function will calculate the offset in milliseconds + * from UTC time. + * + * @param {string} timezone + */ +const getOffsetInMS = timezone => { + if (timezone === 'Browser') { + return 0; + } + const offsetInMinutes = moment.tz(timezone).utcOffset(); + const offsetInMS = offsetInMinutes * 1 * 60 * 1000; + return offsetInMS; +}; + +/** + * Class to manage common instantiation behaviors in a view controller + * + * This is expected to be extended, and behavior enabled using super(); + * + * Example: + * uiRoutes.when('/myRoute', { + * template: importedTemplate, + * controllerAs: 'myView', + * controller: class MyView extends MonitoringViewBaseController { + * constructor($injector, $scope) { + * super({ + * title: 'Hello World', + * api: '../api/v1/monitoring/foo/bar', + * defaultData, + * reactNodeId, + * $scope, + * $injector, + * options: { + * enableTimeFilter: false // this will have just the page auto-refresh control show + * } + * }); + * } + * } + * }); + */ +export class MonitoringViewBaseController { + /** + * Create a view controller + * @param {String} title - Title of the page + * @param {String} api - Back-end API endpoint to poll for getting the page + * data using POST and time range data in the body. Whenever possible, use + * this method for data polling rather than supply the getPageData param. + * @param {Function} apiUrlFn - Function that returns a string for the back-end + * API endpoint, in case the string has dynamic query parameters (e.g. + * show_system_indices) rather than supply the getPageData param. + * @param {Function} getPageData - (Optional) Function to fetch page data, if + * simply passing the API string isn't workable. + * @param {Object} defaultData - Initial model data to populate + * @param {String} reactNodeId - DOM element ID of the element for mounting + * the view's main React component + * @param {Service} $injector - Angular dependency injection service + * @param {Service} $scope - Angular view data binding service + * @param {Boolean} options.enableTimeFilter - Whether to show the time filter + * @param {Boolean} options.enableAutoRefresh - Whether to show the auto + * refresh control + */ + constructor({ + title = '', + api = '', + apiUrlFn, + getPageData: _getPageData = getPageData, + defaultData, + reactNodeId = null, // WIP: https://github.com/elastic/x-pack-kibana/issues/5198 + $scope, + $injector, + options = {}, + fetchDataImmediately = true, + }) { + const titleService = $injector.get('title'); + const $executor = $injector.get('$executor'); + const $window = $injector.get('$window'); + const config = $injector.get('config'); + + titleService($scope.cluster, title); + + $scope.pageData = this.data = { ...defaultData }; + this._isDataInitialized = false; + this.reactNodeId = reactNodeId; + + let deferTimer; + let zoomInLevel = 0; + + const popstateHandler = () => zoomInLevel > 0 && --zoomInLevel; + const removePopstateHandler = () => $window.removeEventListener('popstate', popstateHandler); + const addPopstateHandler = () => $window.addEventListener('popstate', popstateHandler); + + this.zoomInfo = { + zoomOutHandler: () => $window.history.back(), + showZoomOutBtn: () => zoomInLevel > 0, + }; + + const { enableTimeFilter = true, enableAutoRefresh = true } = options; + + this.updateData = () => { + if (this.updateDataPromise) { + // Do not sent another request if one is inflight + // See https://github.com/elastic/kibana/issues/24082 + this.updateDataPromise.cancel(); + this.updateDataPromise = null; + } + const _api = apiUrlFn ? apiUrlFn() : api; + const promises = [_getPageData($injector, _api, this.getPaginationRouteOptions())]; + const setupMode = getSetupModeState(); + if (setupMode.enabled) { + promises.push(updateSetupModeData()); + } + this.updateDataPromise = new PromiseWithCancel(Promise.all(promises)); + return this.updateDataPromise.promise().then(([pageData]) => { + $scope.$apply(() => { + this._isDataInitialized = true; // render will replace loading screen with the react component + $scope.pageData = this.data = pageData; // update the view's data with the fetch result + }); + }); + }; + + $scope.$applyAsync(() => { + const timefilter = Legacy.shims.timefilter; + + if (enableTimeFilter === false) { + timefilter.disableTimeRangeSelector(); + } else { + timefilter.enableTimeRangeSelector(); + } + + if (enableAutoRefresh === false) { + timefilter.disableAutoRefreshSelector(); + } else { + timefilter.enableAutoRefreshSelector(); + } + + // needed for chart pages + this.onBrush = ({ xaxis }) => { + removePopstateHandler(); + const { to, from } = xaxis; + const timezone = config.get('dateFormat:tz'); + const offset = getOffsetInMS(timezone); + timefilter.setTime({ + from: moment(from - offset), + to: moment(to - offset), + mode: 'absolute', + }); + $executor.cancel(); + $executor.run(); + ++zoomInLevel; + clearTimeout(deferTimer); + /* + Needed to defer 'popstate' event, so it does not fire immediately after it's added. + 10ms is to make sure the event is not added with the same code digest + */ + deferTimer = setTimeout(() => addPopstateHandler(), 10); + }; + + fetchDataImmediately && this.updateData(); + }); + + $executor.register({ + execute: () => this.updateData(), + }); + $executor.start($scope); + $scope.$on('$destroy', () => { + clearTimeout(deferTimer); + removePopstateHandler(); + const targetElement = document.getElementById(this.reactNodeId); + if (targetElement) { + // WIP https://github.com/elastic/x-pack-kibana/issues/5198 + unmountComponentAtNode(targetElement); + } + $executor.destroy(); + }); + + this.setTitle = title => titleService($scope.cluster, title); + } + + renderReact(component) { + const renderElement = document.getElementById(this.reactNodeId); + if (!renderElement) { + console.warn(`"#${this.reactNodeId}" element has not been added to the DOM yet`); + return; + } + const I18nContext = Legacy.shims.I18nContext; + const wrappedComponent = ( + <I18nContext>{!this._isDataInitialized ? <PageLoading /> : component}</I18nContext> + ); + render(wrappedComponent, renderElement); + } + + getPaginationRouteOptions() { + return {}; + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/base_eui_table_controller.js b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js similarity index 97% rename from x-pack/legacy/plugins/monitoring/public/views/base_eui_table_controller.js rename to x-pack/plugins/monitoring/public/views/base_eui_table_controller.js index 0460adaeecd10..ac5ba45af8614 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/base_eui_table_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js @@ -5,7 +5,7 @@ */ import { MonitoringViewBaseController } from './'; -import { euiTableStorageGetter, euiTableStorageSetter } from 'plugins/monitoring/components/table'; +import { euiTableStorageGetter, euiTableStorageSetter } from '../components/table'; import { EUI_SORT_ASCENDING } from '../../common/constants'; const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; diff --git a/x-pack/legacy/plugins/monitoring/public/views/base_table_controller.js b/x-pack/plugins/monitoring/public/views/base_table_controller.js similarity index 95% rename from x-pack/legacy/plugins/monitoring/public/views/base_table_controller.js rename to x-pack/plugins/monitoring/public/views/base_table_controller.js index 6ae486eae96fc..2275608c473dd 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/base_table_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_table_controller.js @@ -5,7 +5,7 @@ */ import { MonitoringViewBaseController } from './'; -import { tableStorageGetter, tableStorageSetter } from 'plugins/monitoring/components/table'; +import { tableStorageGetter, tableStorageSetter } from '../components/table'; /** * Class to manage common instantiation behaviors in a view controller diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js new file mode 100644 index 0000000000000..7c9c459218529 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/beats/beat/get_page_data.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beat/${$route.current.params.beatUuid}`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.html b/x-pack/plugins/monitoring/public/views/beats/beat/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/beats/beat/index.html rename to x-pack/plugins/monitoring/public/views/beats/beat/index.html diff --git a/x-pack/plugins/monitoring/public/views/beats/beat/index.js b/x-pack/plugins/monitoring/public/views/beats/beat/index.js new file mode 100644 index 0000000000000..9d5f9b4d562a6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/beats/beat/index.js @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { find } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseController } from '../../'; +import { getPageData } from './get_page_data'; +import template from './index.html'; +import { CODE_PATH_BEATS } from '../../../../common/constants'; + +uiRoutes.when('/beats/beat/:beatUuid', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_BEATS] }); + }, + pageData: getPageData, + }, + controllerAs: 'beat', + controller: class BeatDetail extends MonitoringViewBaseController { + constructor($injector, $scope) { + // breadcrumbs + page title + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + const pageData = $route.current.locals.pageData; + super({ + title: i18n.translate('xpack.monitoring.beats.instance.routeTitle', { + defaultMessage: 'Beats - {instanceName} - Overview', + values: { + instanceName: pageData.summary.name, + }, + }), + getPageData, + $scope, + $injector, + }); + + this.data = pageData; + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js new file mode 100644 index 0000000000000..77942303afcc2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/beats/listing/get_page_data.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats/beats`; + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.html b/x-pack/plugins/monitoring/public/views/beats/listing/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/beats/listing/index.html rename to x-pack/plugins/monitoring/public/views/beats/listing/index.html diff --git a/x-pack/plugins/monitoring/public/views/beats/listing/index.js b/x-pack/plugins/monitoring/public/views/beats/listing/index.js new file mode 100644 index 0000000000000..7d011089fdd7d --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/beats/listing/index.js @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { find } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseEuiTableController } from '../../'; +import { getPageData } from './get_page_data'; +import template from './index.html'; +import React, { Fragment } from 'react'; +import { Listing } from '../../../components/beats/listing/listing'; +import { SetupModeRenderer } from '../../../components/renderers'; +import { CODE_PATH_BEATS, BEATS_SYSTEM_ID } from '../../../../common/constants'; + +uiRoutes.when('/beats/beats', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_BEATS] }); + }, + pageData: getPageData, + }, + controllerAs: 'beats', + controller: class BeatsListing extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + // breadcrumbs + page title + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + super({ + title: i18n.translate('xpack.monitoring.beats.routeTitle', { defaultMessage: 'Beats' }), + storageKey: 'beats.beats', + getPageData, + reactNodeId: 'monitoringBeatsInstancesApp', + $scope, + $injector, + }); + + this.data = $route.current.locals.pageData; + this.scope = $scope; + this.injector = $injector; + this.kbnUrl = $injector.get('kbnUrl'); + + //Bypassing super.updateData, since this controller loads its own data + this._isDataInitialized = true; + + $scope.$watch( + () => this.data, + () => this.renderComponent() + ); + } + + renderComponent() { + const { sorting, pagination, onTableChange } = this.scope.beats; + this.renderReact( + <SetupModeRenderer + scope={this.scope} + injector={this.injector} + productName={BEATS_SYSTEM_ID} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( + <Fragment> + {flyoutComponent} + <Listing + stats={this.data.stats} + data={this.data.listing} + setupMode={setupMode} + sorting={this.sorting || sorting} + pagination={this.pagination || pagination} + onTableChange={this.onTableChange || onTableChange} + angular={{ + kbnUrl: this.kbnUrl, + scope: this.scope, + }} + /> + {bottomBarComponent} + </Fragment> + )} + /> + ); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js b/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js new file mode 100644 index 0000000000000..e5d576961b797 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/beats/overview/get_page_data.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/beats`; + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.html b/x-pack/plugins/monitoring/public/views/beats/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/beats/overview/index.html rename to x-pack/plugins/monitoring/public/views/beats/overview/index.html diff --git a/x-pack/plugins/monitoring/public/views/beats/overview/index.js b/x-pack/plugins/monitoring/public/views/beats/overview/index.js new file mode 100644 index 0000000000000..0eb39ef372263 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/beats/overview/index.js @@ -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 { find } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseController } from '../../'; +import { getPageData } from './get_page_data'; +import template from './index.html'; +import { CODE_PATH_BEATS } from '../../../../common/constants'; + +uiRoutes.when('/beats', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_BEATS] }); + }, + pageData: getPageData, + }, + controllerAs: 'beats', + controller: class BeatsOverview extends MonitoringViewBaseController { + constructor($injector, $scope) { + // breadcrumbs + page title + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + super({ + title: i18n.translate('xpack.monitoring.beats.overview.routeTitle', { + defaultMessage: 'Beats - Overview', + }), + getPageData, + $scope, + $injector, + }); + + this.data = $route.current.locals.pageData; + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.html b/x-pack/plugins/monitoring/public/views/cluster/listing/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/cluster/listing/index.html rename to x-pack/plugins/monitoring/public/views/cluster/listing/index.html diff --git a/x-pack/plugins/monitoring/public/views/cluster/listing/index.js b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js new file mode 100644 index 0000000000000..42be4f02f5c94 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/cluster/listing/index.js @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseEuiTableController } from '../../'; +import template from './index.html'; +import { Listing } from '../../../components/cluster/listing'; +import { CODE_PATH_ALL } from '../../../../common/constants'; + +const CODE_PATHS = [CODE_PATH_ALL]; + +const getPageData = $injector => { + const monitoringClusters = $injector.get('monitoringClusters'); + return monitoringClusters(undefined, undefined, CODE_PATHS); +}; + +uiRoutes + .when('/home', { + template, + resolve: { + clusters: (Private, kbnUrl) => { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: CODE_PATHS, fetchAllClusters: true }).then(clusters => { + if (!clusters || !clusters.length) { + kbnUrl.changePath('/no-data'); + return Promise.reject(); + } + if (clusters.length === 1) { + // Bypass the cluster listing if there is just 1 cluster + kbnUrl.redirect('/overview'); + return Promise.reject(); + } + return clusters; + }); + }, + }, + controllerAs: 'clusters', + controller: class ClustersList extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + super({ + storageKey: 'clusters', + getPageData, + $scope, + $injector, + reactNodeId: 'monitoringClusterListingApp', + }); + + const $route = $injector.get('$route'); + const kbnUrl = $injector.get('kbnUrl'); + const globalState = $injector.get('globalState'); + const storage = $injector.get('localStorage'); + const showLicenseExpiration = $injector.get('showLicenseExpiration'); + + this.data = $route.current.locals.clusters; + + $scope.$watch( + () => this.data, + data => { + this.renderReact( + <Listing + clusters={data} + angular={{ + scope: $scope, + globalState, + kbnUrl, + storage, + showLicenseExpiration, + }} + sorting={this.sorting} + pagination={this.pagination} + onTableChange={this.onTableChange} + /> + ); + } + ); + } + }, + }) + .otherwise({ redirectTo: '/no-data' }); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.html b/x-pack/plugins/monitoring/public/views/cluster/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.html rename to x-pack/plugins/monitoring/public/views/cluster/overview/index.html diff --git a/x-pack/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js new file mode 100644 index 0000000000000..3f6fb77f02288 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/cluster/overview/index.js @@ -0,0 +1,100 @@ +/* + * 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, { Fragment } from 'react'; +import { isEmpty } from 'lodash'; +import { Legacy } from '../../../legacy_shims'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { MonitoringViewBaseController } from '../../'; +import { Overview } from '../../../components/cluster/overview'; +import { SetupModeRenderer } from '../../../components/renderers'; +import { + CODE_PATH_ALL, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; + +const CODE_PATHS = [CODE_PATH_ALL]; + +uiRoutes.when('/overview', { + template, + resolve: { + clusters(Private) { + // checks license info of all monitored clusters for multi-cluster monitoring usage and capability + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: CODE_PATHS }); + }, + }, + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + const kbnUrl = $injector.get('kbnUrl'); + const monitoringClusters = $injector.get('monitoringClusters'); + const globalState = $injector.get('globalState'); + const showLicenseExpiration = $injector.get('showLicenseExpiration'); + const config = $injector.get('config'); + + super({ + title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { + defaultMessage: 'Overview', + }), + defaultData: {}, + getPageData: async () => { + const clusters = await monitoringClusters( + globalState.cluster_uuid, + globalState.ccs, + CODE_PATHS + ); + return clusters[0]; + }, + reactNodeId: 'monitoringClusterOverviewApp', + $scope, + $injector, + }); + + const changeUrl = target => { + $scope.$evalAsync(() => { + kbnUrl.changePath(target); + }); + }; + + $scope.$watch( + () => this.data, + async data => { + if (isEmpty(data)) { + return; + } + + let emailAddress = Legacy.shims.getInjected('monitoringLegacyEmailAddress') || ''; + if (KIBANA_ALERTING_ENABLED) { + emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; + } + + this.renderReact( + <SetupModeRenderer + scope={$scope} + injector={$injector} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( + <Fragment> + {flyoutComponent} + <Overview + cluster={data} + emailAddress={emailAddress} + setupMode={setupMode} + changeUrl={changeUrl} + showLicenseExpiration={showLicenseExpiration} + /> + {bottomBarComponent} + </Fragment> + )} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js new file mode 100644 index 0000000000000..d4a86b00a7505 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/get_page_data.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr`; + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js new file mode 100644 index 0000000000000..88d3a3614243f --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/index.js @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { getPageData } from './get_page_data'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { Ccr } from '../../../components/elasticsearch/ccr'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; + +uiRoutes.when('/elasticsearch/ccr', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + pageData: getPageData, + }, + controllerAs: 'elasticsearchCcr', + controller: class ElasticsearchCcrController extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.ccr.routeTitle', { + defaultMessage: 'Elasticsearch - Ccr', + }), + reactNodeId: 'elasticsearchCcrReact', + getPageData, + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + this.renderReact(data); + } + ); + + this.renderReact = ({ data }) => { + super.renderReact(<Ccr data={data} />); + }; + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js new file mode 100644 index 0000000000000..20f39edbb6a75 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/get_page_data.js @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { Legacy } from '../../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ccr/${$route.current.params.index}/shard/${$route.current.params.shardId}`; + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js new file mode 100644 index 0000000000000..260422d322a2d --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js @@ -0,0 +1,60 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { getPageData } from './get_page_data'; +import { routeInitProvider } from '../../../../lib/route_init'; +import template from './index.html'; +import { MonitoringViewBaseController } from '../../../base_controller'; +import { CcrShard } from '../../../../components/elasticsearch/ccr_shard'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; + +uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + pageData: getPageData, + }, + controllerAs: 'elasticsearchCcr', + controller: class ElasticsearchCcrController extends MonitoringViewBaseController { + constructor($injector, $scope, pageData) { + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.routeTitle', { + defaultMessage: 'Elasticsearch - Ccr - Shard', + }), + reactNodeId: 'elasticsearchCcrShardReact', + getPageData, + $scope, + $injector, + }); + + $scope.instance = i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.instanceTitle', { + defaultMessage: 'Index: {followerIndex} Shard: {shardId}', + values: { + followerIndex: get(pageData, 'stat.follower_index'), + shardId: get(pageData, 'stat.shard_id'), + }, + }); + + $scope.$watch( + () => this.data, + data => { + this.renderReact(data); + } + ); + + this.renderReact = props => { + super.renderReact(<CcrShard {...props} />); + }; + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js new file mode 100644 index 0000000000000..9fbaf6c00725d --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/advanced/index.js @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Controller for Advanced Index Detail + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../../legacy_shims'; +import { AdvancedIndex } from '../../../../components/elasticsearch/index/advanced'; +import { MonitoringViewBaseController } from '../../../base_controller'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; + +function getPageData($injector) { + const globalState = $injector.get('globalState'); + const $route = $injector.get('$route'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; + const $http = $injector.get('$http'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + is_advanced: true, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/elasticsearch/indices/:index/advanced', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + pageData: getPageData, + }, + controllerAs: 'monitoringElasticsearchAdvancedIndexApp', + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const indexName = $route.current.params.index; + + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.indices.advanced.routeTitle', { + defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', + values: { + indexName, + }, + }), + defaultData: {}, + getPageData, + reactNodeId: 'monitoringElasticsearchAdvancedIndexApp', + $scope, + $injector, + }); + + this.indexName = indexName; + + $scope.$watch( + () => this.data, + data => { + this.renderReact( + <AdvancedIndex + indexSummary={data.indexSummary} + metrics={data.metrics} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/index/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/index/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js new file mode 100644 index 0000000000000..d2f49d2280e15 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/index/index.js @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Controller for single index detail + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import template from './index.html'; +import { Legacy } from '../../../legacy_shims'; +import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; +import { indicesByNodes } from '../../../components/elasticsearch/shard_allocation/transformers/indices_by_nodes'; +import { Index } from '../../../components/elasticsearch/index/index'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/indices/${$route.current.params.index}`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + is_advanced: false, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/elasticsearch/indices/:index', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + pageData: getPageData, + }, + controllerAs: 'monitoringElasticsearchIndexApp', + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const kbnUrl = $injector.get('kbnUrl'); + const indexName = $route.current.params.index; + + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.indices.overview.routeTitle', { + defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', + values: { + indexName, + }, + }), + defaultData: {}, + getPageData, + reactNodeId: 'monitoringElasticsearchIndexApp', + $scope, + $injector, + }); + + this.indexName = indexName; + const transformer = indicesByNodes(); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.shards) { + return; + } + + const shards = data.shards; + $scope.totalCount = shards.length; + $scope.showing = transformer(shards, data.nodes); + $scope.labels = labels.node; + if (shards.some(shard => shard.state === 'UNASSIGNED')) { + $scope.labels = labels.indexWithUnassigned; + } else { + $scope.labels = labels.index; + } + + this.renderReact( + <Index + scope={$scope} + kbnUrl={kbnUrl} + onBrush={this.onBrush} + indexUuid={this.indexName} + clusterUuid={$scope.cluster.cluster_uuid} + zoomInfo={this.zoomInfo} + {...data} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/indices/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js new file mode 100644 index 0000000000000..ee177c3e8ed04 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/indices/index.js @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseEuiTableController } from '../../'; +import { ElasticsearchIndices } from '../../../components'; +import template from './index.html'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; + +uiRoutes.when('/elasticsearch/indices', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + }, + controllerAs: 'elasticsearchIndices', + controller: class ElasticsearchIndicesController extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + const features = $injector.get('features'); + + const { cluster_uuid: clusterUuid } = globalState; + $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: clusterUuid }); + + let showSystemIndices = features.isEnabled('showSystemIndices', false); + + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.indices.routeTitle', { + defaultMessage: 'Elasticsearch - Indices', + }), + storageKey: 'elasticsearch.indices', + apiUrlFn: () => + `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices?show_system_indices=${showSystemIndices}`, + reactNodeId: 'elasticsearchIndicesReact', + defaultData: {}, + $scope, + $injector, + $scope, + $injector, + }); + + this.isCcrEnabled = $scope.cluster.isCcrEnabled; + + // for binding + const toggleShowSystemIndices = isChecked => { + // flip the boolean + showSystemIndices = isChecked; + // preserve setting in localStorage + features.update('showSystemIndices', isChecked); + // update the page (resets pagination and sorting) + this.updateData(); + }; + + $scope.$watch( + () => this.data, + data => { + this.renderReact(data); + } + ); + + this.renderReact = ({ clusterStatus, indices }) => { + super.renderReact( + <ElasticsearchIndices + clusterStatus={clusterStatus} + indices={indices} + showSystemIndices={showSystemIndices} + toggleShowSystemIndices={toggleShowSystemIndices} + sorting={this.sorting} + pagination={this.pagination} + onTableChange={this.onTableChange} + /> + ); + }; + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js new file mode 100644 index 0000000000000..0b50a04d53036 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/get_page_data.js @@ -0,0 +1,29 @@ +/* + * 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 { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/ml_jobs`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js new file mode 100644 index 0000000000000..d1dd81223ad5e --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ml_jobs/index.js @@ -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 { find } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseEuiTableController } from '../../'; +import { getPageData } from './get_page_data'; +import template from './index.html'; +import { CODE_PATH_ELASTICSEARCH, CODE_PATH_ML } from '../../../../common/constants'; + +uiRoutes.when('/elasticsearch/ml_jobs', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH, CODE_PATH_ML] }); + }, + pageData: getPageData, + }, + controllerAs: 'mlJobs', + controller: class MlJobsList extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.mlJobs.routeTitle', { + defaultMessage: 'Elasticsearch - Machine Learning Jobs', + }), + storageKey: 'elasticsearch.mlJobs', + getPageData, + $scope, + $injector, + }); + + const $route = $injector.get('$route'); + this.data = $route.current.locals.pageData; + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + this.isCcrEnabled = Boolean($scope.cluster && $scope.cluster.isCcrEnabled); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js new file mode 100644 index 0000000000000..5c45509fce37c --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/advanced/index.js @@ -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. + */ + +/** + * Controller for Advanced Node Detail + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../../legacy_shims'; +import { AdvancedNode } from '../../../../components/elasticsearch/node/advanced'; +import { MonitoringViewBaseController } from '../../../base_controller'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const $route = $injector.get('$route'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + is_advanced: true, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/elasticsearch/nodes/:node/advanced', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + pageData: getPageData, + }, + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + defaultData: {}, + getPageData, + reactNodeId: 'monitoringElasticsearchAdvancedNodeApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.nodeSummary) { + return; + } + + this.setTitle( + i18n.translate('xpack.monitoring.elasticsearch.node.advanced.routeTitle', { + defaultMessage: 'Elasticsearch - Nodes - {nodeSummaryName} - Advanced', + values: { + nodeSummaryName: data.nodeSummary.name, + }, + }) + ); + + this.renderReact( + <AdvancedNode + nodeSummary={data.nodeSummary} + metrics={data.metrics} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js new file mode 100644 index 0000000000000..6aaa8aa452f68 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/get_page_data.js @@ -0,0 +1,35 @@ +/* + * 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 { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const $route = $injector.get('$route'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes/${$route.current.params.node}`; + const features = $injector.get('features'); + const showSystemIndices = features.isEnabled('showSystemIndices', false); + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + showSystemIndices, + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + is_advanced: false, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/node/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/node/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js new file mode 100644 index 0000000000000..c34be490d1711 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -0,0 +1,95 @@ +/* + * 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. + */ + +/** + * Controller for Node Detail + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { partial } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { getPageData } from './get_page_data'; +import template from './index.html'; +import { Node } from '../../../components/elasticsearch/node/node'; +import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; +import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; + +uiRoutes.when('/elasticsearch/nodes/:node', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + pageData: getPageData, + }, + controllerAs: 'monitoringElasticsearchNodeApp', + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const kbnUrl = $injector.get('kbnUrl'); + const nodeName = $route.current.params.node; + + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.node.overview.routeTitle', { + defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Overview', + values: { + nodeName, + }, + }), + defaultData: {}, + getPageData, + reactNodeId: 'monitoringElasticsearchNodeApp', + $scope, + $injector, + }); + + this.nodeName = nodeName; + + const features = $injector.get('features'); + const callPageData = partial(getPageData, $injector); + // show/hide system indices in shard allocation view + $scope.showSystemIndices = features.isEnabled('showSystemIndices', false); + $scope.toggleShowSystemIndices = isChecked => { + $scope.showSystemIndices = isChecked; + // preserve setting in localStorage + features.update('showSystemIndices', isChecked); + // update the page + callPageData().then(data => (this.data = data)); + }; + + const transformer = nodesByIndices(); + $scope.$watch( + () => this.data, + data => { + if (!data || !data.shards) { + return; + } + + const shards = data.shards; + $scope.totalCount = shards.length; + $scope.showing = transformer(shards, data.nodes); + $scope.labels = labels.node; + + this.renderReact( + <Node + scope={$scope} + kbnUrl={kbnUrl} + nodeId={this.nodeName} + clusterUuid={$scope.cluster.cluster_uuid} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + {...data} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/nodes/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js new file mode 100644 index 0000000000000..db4aaca15c00e --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/nodes/index.js @@ -0,0 +1,116 @@ +/* + * 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, { Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { Legacy } from '../../../legacy_shims'; +import template from './index.html'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseEuiTableController } from '../../'; +import { ElasticsearchNodes } from '../../../components'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { SetupModeRenderer } from '../../../components/renderers'; +import { ELASTICSEARCH_SYSTEM_ID, CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; + +uiRoutes.when('/elasticsearch/nodes', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + }, + controllerAs: 'elasticsearchNodes', + controller: class ElasticsearchNodesController extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + const showCgroupMetricsElasticsearch = $injector.get('showCgroupMetricsElasticsearch'); + + $scope.cluster = + find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }) || {}; + + const getPageData = ($injector, _api = undefined, routeOptions = {}) => { + _api; // to fix eslint + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const timeBounds = Legacy.shims.timefilter.getBounds(); + + const getNodes = (clusterUuid = globalState.cluster_uuid) => + $http.post(`../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + ...routeOptions, + }); + + const promise = globalState.cluster_uuid ? getNodes() : new Promise(resolve => resolve({})); + return promise + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); + }; + + super({ + title: i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { + defaultMessage: 'Elasticsearch - Nodes', + }), + storageKey: 'elasticsearch.nodes', + reactNodeId: 'elasticsearchNodesReact', + defaultData: {}, + getPageData, + $scope, + $injector, + fetchDataImmediately: false, // We want to apply pagination before sending the first request + }); + + this.isCcrEnabled = $scope.cluster.isCcrEnabled; + + $scope.$watch( + () => this.data, + () => this.renderReact(this.data || {}) + ); + + this.renderReact = ({ clusterStatus, nodes, totalNodeCount }) => { + const pagination = { + ...this.pagination, + totalItemCount: totalNodeCount, + }; + + super.renderReact( + <SetupModeRenderer + scope={$scope} + injector={$injector} + productName={ELASTICSEARCH_SYSTEM_ID} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( + <Fragment> + {flyoutComponent} + <ElasticsearchNodes + clusterStatus={clusterStatus} + clusterUuid={globalState.cluster_uuid} + setupMode={setupMode} + nodes={nodes} + showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch} + {...this.getPaginationTableProps(pagination)} + /> + {bottomBarComponent} + </Fragment> + )} + /> + ); + }; + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js new file mode 100644 index 0000000000000..7cdd7dfae0af0 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/controller.js @@ -0,0 +1,95 @@ +/* + * 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 { find } from 'lodash'; +import { MonitoringViewBaseController } from '../../'; +import { ElasticsearchOverview } from '../../../components'; + +export class ElasticsearchOverviewController extends MonitoringViewBaseController { + constructor($injector, $scope) { + // breadcrumbs + page title + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + super({ + title: 'Elasticsearch', + api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch`, + defaultData: { + clusterStatus: { status: '' }, + metrics: null, + shardActivity: null, + }, + reactNodeId: 'elasticsearchOverviewReact', + $scope, + $injector, + }); + + this.isCcrEnabled = $scope.cluster.isCcrEnabled; + this.showShardActivityHistory = false; + this.toggleShardActivityHistory = () => { + this.showShardActivityHistory = !this.showShardActivityHistory; + $scope.$evalAsync(() => { + this.renderReact(this.data, $scope.cluster); + }); + }; + + this.initScope($scope); + } + + initScope($scope) { + $scope.$watch( + () => this.data, + data => { + this.renderReact(data, $scope.cluster); + } + ); + + // HACK to force table to re-render even if data hasn't changed. This + // happens when the data remains empty after turning on showHistory. The + // button toggle needs to update the "no data" message based on the value of showHistory + $scope.$watch( + () => this.showShardActivityHistory, + () => { + const { data } = this; + const dataWithShardActivityLoading = { ...data, shardActivity: null }; + // force shard activity to rerender by manipulating and then re-setting its data prop + this.renderReact(dataWithShardActivityLoading, $scope.cluster); + this.renderReact(data, $scope.cluster); + } + ); + } + + filterShardActivityData(shardActivity) { + return shardActivity.filter(row => { + return this.showShardActivityHistory || row.stage !== 'DONE'; + }); + } + + renderReact(data, cluster) { + // All data needs to originate in this view, and get passed as a prop to the components, for statelessness + const { clusterStatus, metrics, shardActivity, logs } = data; + const shardActivityData = shardActivity && this.filterShardActivityData(shardActivity); // no filter on data = null + const component = ( + <ElasticsearchOverview + clusterStatus={clusterStatus} + metrics={metrics} + logs={logs} + cluster={cluster} + shardActivity={shardActivityData} + onBrush={this.onBrush} + showShardActivityHistory={this.showShardActivityHistory} + toggleShardActivityHistory={this.toggleShardActivityHistory} + zoomInfo={this.zoomInfo} + /> + ); + + super.renderReact(component); + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.html b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/elasticsearch/overview/index.html rename to x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.html diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js new file mode 100644 index 0000000000000..1c27a4d004abe --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/overview/index.js @@ -0,0 +1,23 @@ +/* + * 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 { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { ElasticsearchOverviewController } from './controller'; +import { CODE_PATH_ELASTICSEARCH } from '../../../../common/constants'; + +uiRoutes.when('/elasticsearch', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_ELASTICSEARCH] }); + }, + }, + controllerAs: 'elasticsearchOverview', + controller: ElasticsearchOverviewController, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/index.js b/x-pack/plugins/monitoring/public/views/index.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/index.js rename to x-pack/plugins/monitoring/public/views/index.js diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.html b/x-pack/plugins/monitoring/public/views/kibana/instance/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/instance/index.html rename to x-pack/plugins/monitoring/public/views/kibana/instance/index.html diff --git a/x-pack/plugins/monitoring/public/views/kibana/instance/index.js b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js new file mode 100644 index 0000000000000..b743b4e49f096 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/kibana/instance/index.js @@ -0,0 +1,150 @@ +/* + * 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. + */ + +/* + * Kibana Instance + */ +import React from 'react'; +import { get } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../legacy_shims'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiFlexGrid, + EuiFlexItem, + EuiPanel, +} from '@elastic/eui'; +import { MonitoringTimeseriesContainer } from '../../../components/chart'; +import { DetailStatus } from '../../../components/kibana/detail_status'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_KIBANA } from '../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const $route = $injector.get('$route'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/${$route.current.params.uuid}`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/kibana/instances/:uuid', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_KIBANA] }); + }, + pageData: getPageData, + }, + controllerAs: 'monitoringKibanaInstanceApp', + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + title: `Kibana - ${get($scope.pageData, 'kibanaSummary.name')}`, + defaultData: {}, + getPageData, + reactNodeId: 'monitoringKibanaInstanceApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.metrics) { + return; + } + + this.setTitle(`Kibana - ${get(data, 'kibanaSummary.name')}`); + + this.renderReact( + <EuiPage> + <EuiPageBody> + <EuiPanel> + <DetailStatus stats={data.kibanaSummary} /> + </EuiPanel> + <EuiSpacer size="m" /> + <EuiPageContent> + <EuiFlexGrid columns={2} gutterSize="s"> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_requests} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + <EuiSpacer /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_response_times} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + <EuiSpacer /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_memory} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + <EuiSpacer /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_average_concurrent_connections} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + <EuiSpacer /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_os_load} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + <EuiSpacer /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_process_delay} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + <EuiSpacer /> + </EuiFlexItem> + </EuiFlexGrid> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } + ); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js b/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js new file mode 100644 index 0000000000000..9f78bd07ecbf8 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/get_page_data.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana/instances`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.html b/x-pack/plugins/monitoring/public/views/kibana/instances/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/instances/index.html rename to x-pack/plugins/monitoring/public/views/kibana/instances/index.html diff --git a/x-pack/plugins/monitoring/public/views/kibana/instances/index.js b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js new file mode 100644 index 0000000000000..d179928ded693 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/kibana/instances/index.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseEuiTableController } from '../../'; +import { getPageData } from './get_page_data'; +import template from './index.html'; +import { KibanaInstances } from '../../../components/kibana/instances'; +import { SetupModeRenderer } from '../../../components/renderers'; +import { KIBANA_SYSTEM_ID, CODE_PATH_KIBANA } from '../../../../common/constants'; + +uiRoutes.when('/kibana/instances', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_KIBANA] }); + }, + pageData: getPageData, + }, + controllerAs: 'kibanas', + controller: class KibanaInstancesList extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + super({ + title: 'Kibana Instances', + storageKey: 'kibana.instances', + getPageData, + reactNodeId: 'monitoringKibanaInstancesApp', + $scope, + $injector, + }); + + const kbnUrl = $injector.get('kbnUrl'); + + const renderReact = () => { + this.renderReact( + <SetupModeRenderer + scope={$scope} + injector={$injector} + productName={KIBANA_SYSTEM_ID} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( + <Fragment> + {flyoutComponent} + <KibanaInstances + instances={this.data.kibanas} + setupMode={setupMode} + sorting={this.sorting} + pagination={this.pagination} + onTableChange={this.onTableChange} + clusterStatus={this.data.clusterStatus} + angular={{ + $scope, + kbnUrl, + }} + /> + {bottomBarComponent} + </Fragment> + )} + /> + ); + }; + + $scope.$watch( + () => this.data, + data => { + if (!data) { + return; + } + + renderReact(); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.html b/x-pack/plugins/monitoring/public/views/kibana/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/kibana/overview/index.html rename to x-pack/plugins/monitoring/public/views/kibana/overview/index.html diff --git a/x-pack/plugins/monitoring/public/views/kibana/overview/index.js b/x-pack/plugins/monitoring/public/views/kibana/overview/index.js new file mode 100644 index 0000000000000..b0be4f7862a91 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/kibana/overview/index.js @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Kibana Overview + */ +import React from 'react'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { MonitoringTimeseriesContainer } from '../../../components/chart'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../legacy_shims'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPanel, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { ClusterStatus } from '../../../components/kibana/cluster_status'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_KIBANA } from '../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/kibana`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/kibana', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_KIBANA] }); + }, + pageData: getPageData, + }, + controllerAs: 'monitoringKibanaOverviewApp', + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + title: `Kibana`, + defaultData: {}, + getPageData, + reactNodeId: 'monitoringKibanaOverviewApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.clusterStatus) { + return; + } + + this.renderReact( + <EuiPage> + <EuiPageBody> + <EuiPanel> + <ClusterStatus stats={data.clusterStatus} /> + </EuiPanel> + <EuiSpacer size="m" /> + <EuiPageContent> + <EuiFlexGroup> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_cluster_requests} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <MonitoringTimeseriesContainer + series={data.metrics.kibana_cluster_response_times} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } + ); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/license/controller.js b/x-pack/plugins/monitoring/public/views/license/controller.js new file mode 100644 index 0000000000000..1d6a179db98c4 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/license/controller.js @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, find } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Legacy } from '../../legacy_shims'; +import { formatDateTimeLocal } from '../../../common/formatting'; +import { BASE_PATH as MANAGEMENT_BASE_PATH } from '../../../../../plugins/license_management/common/constants'; +import { License } from '../../components'; + +const REACT_NODE_ID = 'licenseReact'; + +export class LicenseViewController { + constructor($injector, $scope) { + Legacy.shims.timefilter.disableTimeRangeSelector(); + Legacy.shims.timefilter.disableAutoRefreshSelector(); + + $scope.$on('$destroy', () => { + unmountComponentAtNode(document.getElementById(REACT_NODE_ID)); + }); + + this.init($injector, $scope, i18n); + } + + init($injector, $scope) { + const globalState = $injector.get('globalState'); + const title = $injector.get('title'); + const $route = $injector.get('$route'); + + const cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + $scope.cluster = cluster; + const routeTitle = i18n.translate('xpack.monitoring.license.licenseRouteTitle', { + defaultMessage: 'License', + }); + title($scope.cluster, routeTitle); + + this.license = cluster.license; + this.isExpired = Date.now() > get(cluster, 'license.expiry_date_in_millis'); + this.isPrimaryCluster = cluster.isPrimary; + + const basePath = Legacy.shims.getBasePath(); + this.uploadLicensePath = basePath + '/app/kibana#' + MANAGEMENT_BASE_PATH + 'upload_license'; + + this.renderReact($scope); + } + + renderReact($scope) { + const injector = Legacy.shims.getAngularInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + $scope.$evalAsync(() => { + const { isPrimaryCluster, license, isExpired, uploadLicensePath } = this; + let expiryDate = license.expiry_date_in_millis; + if (license.expiry_date_in_millis !== undefined) { + expiryDate = formatDateTimeLocal(license.expiry_date_in_millis, timezone); + } + + // Mount the React component to the template + render( + <License + isPrimaryCluster={isPrimaryCluster} + status={license.status} + type={license.type} + isExpired={isExpired} + expiryDate={expiryDate} + uploadLicensePath={uploadLicensePath} + />, + document.getElementById(REACT_NODE_ID) + ); + }); + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/license/index.html b/x-pack/plugins/monitoring/public/views/license/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/license/index.html rename to x-pack/plugins/monitoring/public/views/license/index.html diff --git a/x-pack/plugins/monitoring/public/views/license/index.js b/x-pack/plugins/monitoring/public/views/license/index.js new file mode 100644 index 0000000000000..46e93a8f01f45 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/license/index.js @@ -0,0 +1,23 @@ +/* + * 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 { uiRoutes } from '../../angular/helpers/routes'; +import { routeInitProvider } from '../../lib/route_init'; +import template from './index.html'; +import { LicenseViewController } from './controller'; +import { CODE_PATH_LICENSE } from '../../../common/constants'; + +uiRoutes.when('/license', { + template, + resolve: { + clusters: Private => { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LICENSE] }); + }, + }, + controllerAs: 'licenseView', + controller: LicenseViewController, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/advanced/index.html rename to x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.html diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js new file mode 100644 index 0000000000000..4099a99f122f2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/node/advanced/index.js @@ -0,0 +1,127 @@ +/* + * 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. + */ + +/* + * Logstash Node Advanced View + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../../legacy_shims'; +import { MonitoringViewBaseController } from '../../../base_controller'; +import { DetailStatus } from '../../../../components/logstash/detail_status'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPanel, + EuiSpacer, + EuiFlexGrid, + EuiFlexItem, +} from '@elastic/eui'; +import { MonitoringTimeseriesContainer } from '../../../../components/chart'; +import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const $route = $injector.get('$route'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + is_advanced: true, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/logstash/node/:uuid/advanced', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); + }, + pageData: getPageData, + }, + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + defaultData: {}, + getPageData, + reactNodeId: 'monitoringLogstashNodeAdvancedApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.nodeSummary) { + return; + } + + this.setTitle( + i18n.translate('xpack.monitoring.logstash.node.advanced.routeTitle', { + defaultMessage: 'Logstash - {nodeName} - Advanced', + values: { + nodeName: data.nodeSummary.name, + }, + }) + ); + + const metricsToShow = [ + data.metrics.logstash_node_cpu_utilization, + data.metrics.logstash_queue_events_count, + data.metrics.logstash_node_cgroup_cpu, + data.metrics.logstash_pipeline_queue_size, + data.metrics.logstash_node_cgroup_stats, + ]; + + this.renderReact( + <EuiPage> + <EuiPageBody> + <EuiPanel> + <DetailStatus stats={data.nodeSummary} /> + </EuiPanel> + <EuiSpacer size="m" /> + <EuiPageContent> + <EuiFlexGrid columns={2} gutterSize="s"> + {metricsToShow.map((metric, index) => ( + <EuiFlexItem key={index}> + <MonitoringTimeseriesContainer + series={metric} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + {...data} + /> + <EuiSpacer /> + </EuiFlexItem> + ))} + </EuiFlexGrid> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/index.html rename to x-pack/plugins/monitoring/public/views/logstash/node/index.html diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/index.js new file mode 100644 index 0000000000000..141761d8cc11a --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/node/index.js @@ -0,0 +1,128 @@ +/* + * 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. + */ + +/* + * Logstash Node + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../legacy_shims'; +import { DetailStatus } from '../../../components/logstash/detail_status'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPanel, + EuiSpacer, + EuiFlexGrid, + EuiFlexItem, +} from '@elastic/eui'; +import { MonitoringTimeseriesContainer } from '../../../components/chart'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const $route = $injector.get('$route'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${$route.current.params.uuid}`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + is_advanced: false, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/logstash/node/:uuid', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); + }, + pageData: getPageData, + }, + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + defaultData: {}, + getPageData, + reactNodeId: 'monitoringLogstashNodeApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.nodeSummary) { + return; + } + + this.setTitle( + i18n.translate('xpack.monitoring.logstash.node.routeTitle', { + defaultMessage: 'Logstash - {nodeName}', + values: { + nodeName: data.nodeSummary.name, + }, + }) + ); + + const metricsToShow = [ + data.metrics.logstash_events_input_rate, + data.metrics.logstash_jvm_usage, + data.metrics.logstash_events_output_rate, + data.metrics.logstash_node_cpu_metric, + data.metrics.logstash_events_latency, + data.metrics.logstash_os_load, + ]; + + this.renderReact( + <EuiPage> + <EuiPageBody> + <EuiPanel> + <DetailStatus stats={data.nodeSummary} /> + </EuiPanel> + <EuiSpacer size="m" /> + <EuiPageContent> + <EuiFlexGrid columns={2} gutterSize="s"> + {metricsToShow.map((metric, index) => ( + <EuiFlexItem key={index}> + <MonitoringTimeseriesContainer + series={metric} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + {...data} + /> + <EuiSpacer /> + </EuiFlexItem> + ))} + </EuiFlexGrid> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.html b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/node/pipelines/index.html rename to x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.html diff --git a/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js new file mode 100644 index 0000000000000..442e8533c18f6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/node/pipelines/index.js @@ -0,0 +1,129 @@ +/* + * 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. + */ + +/* + * Logstash Node Pipelines Listing + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { uiRoutes } from '../../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../../lib/route_init'; +import { isPipelineMonitoringSupportedInVersion } from '../../../../lib/logstash/pipelines'; +import template from './index.html'; +import { Legacy } from '../../../../legacy_shims'; +import { MonitoringViewBaseEuiTableController } from '../../../'; +import { PipelineListing } from '../../../../components/logstash/pipeline_listing/pipeline_listing'; +import { DetailStatus } from '../../../../components/logstash/detail_status'; +import { CODE_PATH_LOGSTASH } from '../../../../../common/constants'; + +const getPageData = ($injector, _api = undefined, routeOptions = {}) => { + _api; // fixing eslint + const $route = $injector.get('$route'); + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const Private = $injector.get('Private'); + + const logstashUuid = $route.current.params.uuid; + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/node/${logstashUuid}/pipelines`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + ...routeOptions, + }) + .then(response => response.data) + .catch(err => { + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +}; + +function makeUpgradeMessage(logstashVersion) { + if (isPipelineMonitoringSupportedInVersion(logstashVersion)) { + return null; + } + + return i18n.translate('xpack.monitoring.logstash.node.pipelines.notAvailableDescription', { + defaultMessage: + 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher. This node is running version {logstashVersion}.', + values: { + logstashVersion, + }, + }); +} + +uiRoutes.when('/logstash/node/:uuid/pipelines', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); + }, + }, + controller: class extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + const kbnUrl = $injector.get('kbnUrl'); + const config = $injector.get('config'); + + super({ + defaultData: {}, + getPageData, + reactNodeId: 'monitoringLogstashNodePipelinesApp', + $scope, + $injector, + fetchDataImmediately: false, // We want to apply pagination before sending the first request + }); + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.nodeSummary) { + return; + } + + this.setTitle( + i18n.translate('xpack.monitoring.logstash.node.pipelines.routeTitle', { + defaultMessage: 'Logstash - {nodeName} - Pipelines', + values: { + nodeName: data.nodeSummary.name, + }, + }) + ); + + const pagination = { + ...this.pagination, + totalItemCount: data.totalPipelineCount, + }; + + this.renderReact( + <PipelineListing + className="monitoringLogstashPipelinesTable" + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + stats={data.nodeSummary} + statusComponent={DetailStatus} + data={data.pipelines} + {...this.getPaginationTableProps(pagination)} + dateFormat={config.get('dateFormat')} + upgradeMessage={makeUpgradeMessage(data.nodeSummary.version, i18n)} + angular={{ + kbnUrl, + scope: $scope, + }} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js new file mode 100644 index 0000000000000..1d5d6007814f4 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/get_page_data.js @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { Legacy } from '../../../legacy_shims'; + +export function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/nodes`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.html b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/nodes/index.html rename to x-pack/plugins/monitoring/public/views/logstash/nodes/index.html diff --git a/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js new file mode 100644 index 0000000000000..49b2a20f11ea2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/nodes/index.js @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { routeInitProvider } from '../../../lib/route_init'; +import { MonitoringViewBaseEuiTableController } from '../../'; +import { getPageData } from './get_page_data'; +import template from './index.html'; +import { Listing } from '../../../components/logstash/listing'; +import { SetupModeRenderer } from '../../../components/renderers'; +import { CODE_PATH_LOGSTASH, LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; + +uiRoutes.when('/logstash/nodes', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); + }, + pageData: getPageData, + }, + controllerAs: 'lsNodes', + controller: class LsNodesList extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + const kbnUrl = $injector.get('kbnUrl'); + + super({ + title: 'Logstash - Nodes', + storageKey: 'logstash.nodes', + getPageData, + reactNodeId: 'monitoringLogstashNodesApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + this.renderReact( + <SetupModeRenderer + scope={$scope} + injector={$injector} + productName={LOGSTASH_SYSTEM_ID} + render={({ setupMode, flyoutComponent, bottomBarComponent }) => ( + <Fragment> + {flyoutComponent} + <Listing + data={data.nodes} + setupMode={setupMode} + stats={data.clusterStatus} + sorting={this.sorting} + pagination={this.pagination} + onTableChange={this.onTableChange} + angular={{ kbnUrl, scope: $scope }} + /> + {bottomBarComponent} + </Fragment> + )} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.html b/x-pack/plugins/monitoring/public/views/logstash/overview/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/overview/index.html rename to x-pack/plugins/monitoring/public/views/logstash/overview/index.html diff --git a/x-pack/plugins/monitoring/public/views/logstash/overview/index.js b/x-pack/plugins/monitoring/public/views/logstash/overview/index.js new file mode 100644 index 0000000000000..05b1747f4cfff --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/overview/index.js @@ -0,0 +1,76 @@ +/* + * 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. + */ + +/** + * Logstash Overview + */ +import React from 'react'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import template from './index.html'; +import { Legacy } from '../../../legacy_shims'; +import { Overview } from '../../../components/logstash/overview'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; + +function getPageData($injector) { + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + }) + .then(response => response.data) + .catch(err => { + const Private = $injector.get('Private'); + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/logstash', { + template, + resolve: { + clusters: function(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); + }, + pageData: getPageData, + }, + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + super({ + title: 'Logstash', + getPageData, + reactNodeId: 'monitoringLogstashOverviewApp', + $scope, + $injector, + }); + + $scope.$watch( + () => this.data, + data => { + this.renderReact( + <Overview + stats={data.clusterStatus} + metrics={data.metrics} + onBrush={this.onBrush} + zoomInfo={this.zoomInfo} + /> + ); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.html b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/pipeline/index.html rename to x-pack/plugins/monitoring/public/views/logstash/pipeline/index.html diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js new file mode 100644 index 0000000000000..0dd24d68540ce --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/pipeline/index.js @@ -0,0 +1,172 @@ +/* + * 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. + */ + +/* + * Logstash Node Pipeline View + */ +import React from 'react'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import moment from 'moment'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import { CALCULATE_DURATION_SINCE, CODE_PATH_LOGSTASH } from '../../../../common/constants'; +import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; +import template from './index.html'; +import { i18n } from '@kbn/i18n'; +import { List } from '../../../components/logstash/pipeline_viewer/models/list'; +import { PipelineState } from '../../../components/logstash/pipeline_viewer/models/pipeline_state'; +import { PipelineViewer } from '../../../components/logstash/pipeline_viewer'; +import { Pipeline } from '../../../components/logstash/pipeline_viewer/models/pipeline'; +import { vertexFactory } from '../../../components/logstash/pipeline_viewer/models/graph/vertex_factory'; +import { MonitoringViewBaseController } from '../../base_controller'; +import { EuiPageBody, EuiPage, EuiPageContent } from '@elastic/eui'; + +let previousPipelineHash = undefined; +let detailVertexId = undefined; + +function getPageData($injector) { + const $route = $injector.get('$route'); + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const minIntervalSeconds = $injector.get('minIntervalSeconds'); + const Private = $injector.get('Private'); + + const { ccs, cluster_uuid: clusterUuid } = globalState; + const pipelineId = $route.current.params.id; + const pipelineHash = $route.current.params.hash || ''; + + // Pipeline version was changed, so clear out detailVertexId since that vertex won't + // exist in the updated pipeline version + if (pipelineHash !== previousPipelineHash) { + previousPipelineHash = pipelineHash; + detailVertexId = undefined; + } + + const url = pipelineHash + ? `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}/${pipelineHash}` + : `../api/monitoring/v1/clusters/${clusterUuid}/logstash/pipeline/${pipelineId}`; + return $http + .post(url, { + ccs, + detailVertexId, + }) + .then(response => response.data) + .then(data => { + data.versions = data.versions.map(version => { + const relativeFirstSeen = formatTimestampToDuration( + version.firstSeen, + CALCULATE_DURATION_SINCE + ); + const relativeLastSeen = formatTimestampToDuration( + version.lastSeen, + CALCULATE_DURATION_SINCE + ); + + const fudgeFactorSeconds = 2 * minIntervalSeconds; + const isLastSeenCloseToNow = Date.now() - version.lastSeen <= fudgeFactorSeconds * 1000; + + return { + ...version, + relativeFirstSeen: i18n.translate( + 'xpack.monitoring.logstash.pipeline.relativeFirstSeenAgoLabel', + { + defaultMessage: '{relativeFirstSeen} ago', + values: { relativeFirstSeen }, + } + ), + relativeLastSeen: isLastSeenCloseToNow + ? i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenNowLabel', { + defaultMessage: 'now', + }) + : i18n.translate('xpack.monitoring.logstash.pipeline.relativeLastSeenAgoLabel', { + defaultMessage: 'until {relativeLastSeen} ago', + values: { relativeLastSeen }, + }), + }; + }); + + return data; + }) + .catch(err => { + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +} + +uiRoutes.when('/logstash/pipelines/:id/:hash?', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); + }, + pageData: getPageData, + }, + controller: class extends MonitoringViewBaseController { + constructor($injector, $scope) { + const config = $injector.get('config'); + const dateFormat = config.get('dateFormat'); + + super({ + title: i18n.translate('xpack.monitoring.logstash.pipeline.routeTitle', { + defaultMessage: 'Logstash - Pipeline', + }), + storageKey: 'logstash.pipelines', + getPageData, + reactNodeId: 'monitoringLogstashPipelineApp', + $scope, + options: { + enableTimeFilter: false, + }, + $injector, + }); + + const timeseriesTooltipXValueFormatter = xValue => moment(xValue).format(dateFormat); + + const setDetailVertexId = vertex => { + if (!vertex) { + detailVertexId = undefined; + } else { + detailVertexId = vertex.id; + } + + return this.updateData(); + }; + + $scope.$watch( + () => this.data, + data => { + if (!data || !data.pipeline) { + return; + } + this.pipelineState = new PipelineState(data.pipeline); + this.detailVertex = data.vertex ? vertexFactory(null, data.vertex) : null; + this.renderReact( + <EuiPage> + <EuiPageBody> + <EuiPageContent> + <PipelineViewer + pipeline={List.fromPipeline( + Pipeline.fromPipelineGraph(this.pipelineState.config.graph) + )} + timeseriesTooltipXValueFormatter={timeseriesTooltipXValueFormatter} + setDetailVertexId={setDetailVertexId} + detailVertex={this.detailVertex} + /> + </EuiPageContent> + </EuiPageBody> + </EuiPage> + ); + } + ); + + $scope.$on('$destroy', () => { + previousPipelineHash = undefined; + detailVertexId = undefined; + }); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.html b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/logstash/pipelines/index.html rename to x-pack/plugins/monitoring/public/views/logstash/pipelines/index.html diff --git a/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js new file mode 100644 index 0000000000000..4ddaba1e0a7c9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/logstash/pipelines/index.js @@ -0,0 +1,129 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { uiRoutes } from '../../../angular/helpers/routes'; +import { ajaxErrorHandlersProvider } from '../../../lib/ajax_error_handler'; +import { routeInitProvider } from '../../../lib/route_init'; +import { isPipelineMonitoringSupportedInVersion } from '../../../lib/logstash/pipelines'; +import template from './index.html'; +import { Legacy } from '../../../legacy_shims'; +import { PipelineListing } from '../../../components/logstash/pipeline_listing/pipeline_listing'; +import { MonitoringViewBaseEuiTableController } from '../..'; +import { CODE_PATH_LOGSTASH } from '../../../../common/constants'; + +/* + * Logstash Pipelines Listing page + */ + +const getPageData = ($injector, _api = undefined, routeOptions = {}) => { + _api; // to fix eslint + const $http = $injector.get('$http'); + const globalState = $injector.get('globalState'); + const Private = $injector.get('Private'); + + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/logstash/pipelines`; + const timeBounds = Legacy.shims.timefilter.getBounds(); + + return $http + .post(url, { + ccs: globalState.ccs, + timeRange: { + min: timeBounds.min.toISOString(), + max: timeBounds.max.toISOString(), + }, + ...routeOptions, + }) + .then(response => response.data) + .catch(err => { + const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider); + return ajaxErrorHandlers(err); + }); +}; + +function makeUpgradeMessage(logstashVersions) { + if ( + !Array.isArray(logstashVersions) || + logstashVersions.length === 0 || + logstashVersions.some(isPipelineMonitoringSupportedInVersion) + ) { + return null; + } + + return 'Pipeline monitoring is only available in Logstash version 6.0.0 or higher.'; +} + +uiRoutes.when('/logstash/pipelines', { + template, + resolve: { + clusters(Private) { + const routeInit = Private(routeInitProvider); + return routeInit({ codePaths: [CODE_PATH_LOGSTASH] }); + }, + }, + controller: class LogstashPipelinesList extends MonitoringViewBaseEuiTableController { + constructor($injector, $scope) { + super({ + title: 'Logstash Pipelines', + storageKey: 'logstash.pipelines', + getPageData, + reactNodeId: 'monitoringLogstashPipelinesApp', + $scope, + $injector, + fetchDataImmediately: false, // We want to apply pagination before sending the first request + }); + + const $route = $injector.get('$route'); + const kbnUrl = $injector.get('kbnUrl'); + const config = $injector.get('config'); + this.data = $route.current.locals.pageData; + const globalState = $injector.get('globalState'); + $scope.cluster = find($route.current.locals.clusters, { + cluster_uuid: globalState.cluster_uuid, + }); + + const renderReact = pageData => { + if (!pageData) { + return; + } + + const upgradeMessage = pageData + ? makeUpgradeMessage(pageData.clusterStatus.versions, i18n) + : null; + + const pagination = { + ...this.pagination, + totalItemCount: pageData.totalPipelineCount, + }; + + super.renderReact( + <PipelineListing + className="monitoringLogstashPipelinesTable" + onBrush={xaxis => this.onBrush({ xaxis })} + stats={pageData.clusterStatus} + data={pageData.pipelines} + {...this.getPaginationTableProps(pagination)} + upgradeMessage={upgradeMessage} + dateFormat={config.get('dateFormat')} + angular={{ + kbnUrl, + scope: $scope, + }} + /> + ); + }; + + $scope.$watch( + () => this.data, + pageData => { + renderReact(pageData); + } + ); + } + }, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js b/x-pack/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js rename to x-pack/plugins/monitoring/public/views/no_data/__tests__/model_updater.test.js diff --git a/x-pack/plugins/monitoring/public/views/no_data/controller.js b/x-pack/plugins/monitoring/public/views/no_data/controller.js new file mode 100644 index 0000000000000..14c30da2ce999 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/no_data/controller.js @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + ClusterSettingsChecker, + NodeSettingsChecker, + Enabler, + startChecks, +} from '../../lib/elasticsearch_settings'; +import { ModelUpdater } from './model_updater'; +import { NoData } from '../../components'; +import { CODE_PATH_LICENSE } from '../../../common/constants'; +import { MonitoringViewBaseController } from '../base_controller'; +import { i18n } from '@kbn/i18n'; +import { Legacy } from '../../legacy_shims'; + +export class NoDataController extends MonitoringViewBaseController { + constructor($injector, $scope) { + window.injectorThree = $injector; + const monitoringClusters = $injector.get('monitoringClusters'); + const kbnUrl = $injector.get('kbnUrl'); + const $http = $injector.get('$http'); + const checkers = [new ClusterSettingsChecker($http), new NodeSettingsChecker($http)]; + + const getData = async () => { + let catchReason; + try { + const monitoringClustersData = await monitoringClusters(undefined, undefined, [ + CODE_PATH_LICENSE, + ]); + if (monitoringClustersData && monitoringClustersData.length) { + kbnUrl.redirect('/home'); + return monitoringClustersData; + } + } catch (err) { + if (err && err.status === 503) { + catchReason = { + property: 'custom', + message: err.data.message, + }; + } + } + + this.errors.length = 0; + if (catchReason) { + this.reason = catchReason; + } else if (!this.isCollectionEnabledUpdating && !this.isCollectionIntervalUpdating) { + /** + * `no-use-before-define` is fine here, since getData is an async function. + * Needs to be done this way, since there is no `this` before super is executed + * */ + await startChecks(checkers, updateModel); // eslint-disable-line no-use-before-define + } + }; + + super({ + title: i18n.translate('xpack.monitoring.noData.routeTitle', { + defaultMessage: 'Setup Monitoring', + }), + getPageData: async () => await getData(), + reactNodeId: 'noDataReact', + $scope, + $injector, + }); + Object.assign(this, this.getDefaultModel()); + + //Need to set updateModel after super since there is no `this` otherwise + const { updateModel } = new ModelUpdater($scope, this); + const enabler = new Enabler($http, updateModel); + $scope.$watch( + () => this, + () => { + if (this.isCollectionEnabledUpdated && !this.reason) { + return; + } + this.render(enabler); + }, + true + ); + + this.changePath = path => kbnUrl.changePath(path); + } + + getDefaultModel() { + return { + errors: [], // errors can happen from trying to check or set ES settings + checkMessage: null, // message to show while waiting for api response + isLoading: true, // flag for in-progress state of checking for no data reason + isCollectionEnabledUpdating: false, // flags to indicate whether to show a spinner while waiting for ajax + isCollectionEnabledUpdated: false, + isCollectionIntervalUpdating: false, + isCollectionIntervalUpdated: false, + }; + } + + render(enabler) { + const props = this; + this.renderReact( + <NoData + {...props} + enabler={enabler} + changePath={this.changePath} + isCloudEnabled={Legacy.shims.isCloud} + /> + ); + } +} diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/index.html b/x-pack/plugins/monitoring/public/views/no_data/index.html similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/index.html rename to x-pack/plugins/monitoring/public/views/no_data/index.html diff --git a/x-pack/plugins/monitoring/public/views/no_data/index.js b/x-pack/plugins/monitoring/public/views/no_data/index.js new file mode 100644 index 0000000000000..9876739dfcbbe --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/no_data/index.js @@ -0,0 +1,14 @@ +/* + * 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 { uiRoutes } from '../../angular/helpers/routes'; +import template from './index.html'; +import { NoDataController } from './controller'; + +uiRoutes.when('/no-data', { + template, + controller: NoDataController, +}); diff --git a/x-pack/legacy/plugins/monitoring/public/views/no_data/model_updater.js b/x-pack/plugins/monitoring/public/views/no_data/model_updater.js similarity index 100% rename from x-pack/legacy/plugins/monitoring/public/views/no_data/model_updater.js rename to x-pack/plugins/monitoring/public/views/no_data/model_updater.js diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index dfe8ab31f972c..3a3ec6ac799d2 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -16,8 +16,33 @@ import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../common/constants'; * major version! * @return {Array} array of rename operations and callback function for rename logging */ -export const deprecations = ({ rename }: ConfigDeprecationFactory): ConfigDeprecation[] => { +export const deprecations = ({ + rename, + renameFromRoot, +}: ConfigDeprecationFactory): ConfigDeprecation[] => { return [ + // This order matters. The "blanket rename" needs to happen at the end + renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), + renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), + renameFromRoot( + 'xpack.monitoring.show_license_expiration', + 'monitoring.ui.show_license_expiration' + ), + renameFromRoot( + 'xpack.monitoring.ui.container.elasticsearch.enabled', + 'monitoring.ui.container.elasticsearch.enabled' + ), + renameFromRoot( + 'xpack.monitoring.ui.container.logstash.enabled', + 'monitoring.ui.container.logstash.enabled' + ), + renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch'), + renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), + renameFromRoot( + 'xpack.monitoring.elasticsearch.logFetchCount', + 'monitoring.ui.elasticsearch.logFetchCount' + ), + renameFromRoot('xpack.monitoring', 'monitoring'), (config, fromPath, logger) => { const clusterAlertsEnabled = get(config, 'cluster_alerts.enabled'); const emailNotificationsEnabled = diff --git a/x-pack/plugins/monitoring/server/index.ts b/x-pack/plugins/monitoring/server/index.ts index a992037fc6087..60f04c535ebf1 100644 --- a/x-pack/plugins/monitoring/server/index.ts +++ b/x-pack/plugins/monitoring/server/index.ts @@ -10,8 +10,14 @@ import { Plugin } from './plugin'; import { configSchema } from './config'; import { deprecations } from './deprecations'; +export { MonitoringConfig } from './config'; export const plugin = (initContext: PluginInitializerContext) => new Plugin(initContext); export const config: PluginConfigDescriptor<TypeOf<typeof configSchema>> = { schema: configSchema, deprecations, + exposeToBrowser: { + enabled: true, + ui: true, + kibana: true, + }, }; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index 1a9f2a4da32c2..f0ad6399c6c72 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -182,7 +182,6 @@ describe('get_all_stats', () => { }, { logger: coreMock.createPluginInitializerContext().logger.get('test'), - isDev: true, version: 'version', maxBucketSize: 1, } @@ -208,7 +207,6 @@ describe('get_all_stats', () => { }, { logger: coreMock.createPluginInitializerContext().logger.get('test'), - isDev: true, version: 'version', maxBucketSize: 1, } diff --git a/x-pack/plugins/remote_clusters/public/application/_hacks.scss b/x-pack/plugins/remote_clusters/public/application/_hacks.scss new file mode 100644 index 0000000000000..b7d81885e716d --- /dev/null +++ b/x-pack/plugins/remote_clusters/public/application/_hacks.scss @@ -0,0 +1,25 @@ +// Remote clusters plugin hacks + +// Prefix all styles with "remoteClusters" to avoid conflicts. +// Examples +// remoteClustersChart +// remoteClustersChart__legend +// remoteClustersChart__legend--small +// remoteClustersChart__legend-isLoading + +/** + * 1. Override EuiFormRow styles. Otherwise the switch will jump around when toggled on and off, + * as the 'Reset to defaults' link is added to and removed from the DOM. + * 2. Fix the positioning. + */ +.remoteClusterSkipIfUnavailableSwitch { + justify-content: flex-start !important; /* 1 */ + padding-top: $euiSizeS !important; +} + +/** + * 1. Prevent inherited flexbox layout from compressing this element on IE. + */ + .remoteClustersConnectionStatus__message { + flex-basis: auto !important; /* 1 */ +} diff --git a/x-pack/plugins/remote_clusters/public/application/index.js b/x-pack/plugins/remote_clusters/public/application/index.js index f2d788c741342..cf6e855ba58df 100644 --- a/x-pack/plugins/remote_clusters/public/application/index.js +++ b/x-pack/plugins/remote_clusters/public/application/index.js @@ -13,6 +13,8 @@ import { App } from './app'; import { remoteClustersStore } from './store'; import { AppContextProvider } from './app_context'; +import './_hacks.scss'; + export const renderApp = (elem, I18nContext, appDependencies) => { render( <I18nContext> diff --git a/x-pack/plugins/remote_clusters/public/index.ts b/x-pack/plugins/remote_clusters/public/index.ts index 6ba021b157c3e..127ec2a670645 100644 --- a/x-pack/plugins/remote_clusters/public/index.ts +++ b/x-pack/plugins/remote_clusters/public/index.ts @@ -3,8 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { PluginInitializerContext } from 'kibana/public'; import { RemoteClustersUIPlugin } from './plugin'; +export { RemoteClustersPluginSetup } from './plugin'; + export const plugin = (initializerContext: PluginInitializerContext) => new RemoteClustersUIPlugin(initializerContext); diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index d110c461c1e3f..22f98e94748d8 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -14,7 +14,12 @@ import { init as initNotification } from './application/services/notification'; import { init as initRedirect } from './application/services/redirect'; import { Dependencies, ClientConfigType } from './types'; -export class RemoteClustersUIPlugin implements Plugin<void, void, Dependencies, any> { +export interface RemoteClustersPluginSetup { + isUiEnabled: boolean; +} + +export class RemoteClustersUIPlugin + implements Plugin<RemoteClustersPluginSetup, void, Dependencies, any> { constructor(private readonly initializerContext: PluginInitializerContext) {} setup( @@ -55,6 +60,10 @@ export class RemoteClustersUIPlugin implements Plugin<void, void, Dependencies, }, }); } + + return { + isUiEnabled: isRemoteClustersUiEnabled, + }; } start({ application }: CoreStart) { diff --git a/x-pack/plugins/remote_clusters/server/index.ts b/x-pack/plugins/remote_clusters/server/index.ts index 927fa768fc9fd..ba7eb56a7f958 100644 --- a/x-pack/plugins/remote_clusters/server/index.ts +++ b/x-pack/plugins/remote_clusters/server/index.ts @@ -7,5 +7,6 @@ import { PluginInitializerContext } from 'kibana/server'; import { RemoteClustersServerPlugin } from './plugin'; export { config } from './config'; +export { RemoteClustersPluginSetup } from './plugin'; export const plugin = (ctx: PluginInitializerContext) => new RemoteClustersServerPlugin(ctx); diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index fca4a5dbc5f94..a7ca30a6bf96d 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -18,19 +19,26 @@ import { registerDeleteRoute, } from './routes/api'; -export class RemoteClustersServerPlugin implements Plugin<void, void, any, any> { +export interface RemoteClustersPluginSetup { + isUiEnabled: boolean; +} + +export class RemoteClustersServerPlugin + implements Plugin<RemoteClustersPluginSetup, void, any, any> { licenseStatus: LicenseStatus; log: Logger; - config: Observable<ConfigType>; + config$: Observable<ConfigType>; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config = config.create(); + this.config$ = config.create(); this.licenseStatus = { valid: false }; } async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) { const router = http.createRouter(); + const config = await this.config$.pipe(first()).toPromise(); + const routeDependencies: RouteDependencies = { router, getLicenseStatus: () => this.licenseStatus, @@ -64,6 +72,10 @@ export class RemoteClustersServerPlugin implements Plugin<void, void, any, any> } } }); + + return { + isUiEnabled: config.ui.enabled, + }; } start() {} diff --git a/x-pack/plugins/reporting/common/types.d.ts b/x-pack/plugins/reporting/common/types.d.ts new file mode 100644 index 0000000000000..34f0bc9ac8a36 --- /dev/null +++ b/x-pack/plugins/reporting/common/types.d.ts @@ -0,0 +1,7 @@ +/* + * 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 { ConfigType } from '../server/config'; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 5756d29face12..8079c5b1d9887 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -7,13 +7,6 @@ export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; -export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { - jobCompletionNotifier: { - interval: 10000, - intervalErrorMultiplier: 5, - }, -}; - // Routes export const API_BASE_URL = '/api/reporting'; export const API_LIST_URL = `${API_BASE_URL}/jobs`; diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 26d661e29bd94..77faf837e6505 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -4,15 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CoreSetup, - CoreStart, - HttpSetup, - Plugin, - PluginInitializerContext, - NotificationsStart, -} from '../../../src/core/public'; - export type JobId = string; export type JobStatus = | 'completed' @@ -21,9 +12,6 @@ export type JobStatus = | 'processing' | 'failed'; -export type HttpService = HttpSetup; -export type NotificationsService = NotificationsStart; - export interface SourceJob { _id: JobId; _source: { diff --git a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap index 271d61c224210..b05e74c516cd4 100644 --- a/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/__snapshots__/report_listing.test.tsx.snap @@ -219,11 +219,14 @@ Array [ <div className="euiTableCellContent" > - <span - className="euiTableCellContent__text" - > - Report - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Report" + > + Report + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> @@ -246,11 +249,14 @@ Array [ <div className="euiTableCellContent" > - <span - className="euiTableCellContent__text" - > - Created at - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Created at" + > + Created at + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> @@ -273,11 +279,14 @@ Array [ <div className="euiTableCellContent" > - <span - className="euiTableCellContent__text" - > - Status - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Status" + > + Status + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> @@ -298,11 +307,14 @@ Array [ <div className="euiTableCellContent euiTableCellContent--alignRight" > - <span - className="euiTableCellContent__text" - > - Actions - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Actions" + > + Actions + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> @@ -514,11 +526,14 @@ Array [ <div className="euiTableCellContent" > - <span - className="euiTableCellContent__text" - > - Report - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Report" + > + Report + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> @@ -541,11 +556,14 @@ Array [ <div className="euiTableCellContent" > - <span - className="euiTableCellContent__text" - > - Created at - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Created at" + > + Created at + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> @@ -568,11 +586,14 @@ Array [ <div className="euiTableCellContent" > - <span - className="euiTableCellContent__text" - > - Status - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Status" + > + Status + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> @@ -593,11 +614,14 @@ Array [ <div className="euiTableCellContent euiTableCellContent--alignRight" > - <span - className="euiTableCellContent__text" - > - Actions - </span> + <EuiInnerText> + <span + className="euiTableCellContent__text" + title="Actions" + > + Actions + </span> + </EuiInnerText> </div> </th> </EuiTableHeaderCell> diff --git a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx index 3f7c210eb3696..e8916ff82e121 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx @@ -83,7 +83,12 @@ export class ReportDeleteButton extends PureComponent<Props, State> { return ( <Fragment> - <EuiButton onClick={() => this.showConfirm()} iconType="trash" color={'danger'}> + <EuiButton + onClick={() => this.showConfirm()} + iconType="trash" + color={'danger'} + data-test-subj="deleteReportButton" + > {intl.formatMessage( { id: 'xpack.reporting.listing.table.deleteReportButton', diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx index 380a3b3295b9f..787279e6caf9b 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -47,12 +47,24 @@ const toasts = { addDanger: jest.fn(), } as any; +const mockPollConfig = { + jobCompletionNotifier: { + interval: 5000, + intervalErrorMultiplier: 3, + }, + jobsRefresh: { + interval: 5000, + intervalErrorMultiplier: 3, + }, +}; + describe('ReportListing', () => { it('Report job listing with some items', () => { const wrapper = mountWithIntl( <ReportListing apiClient={reportingAPIClient as ReportingAPIClient} license$={license$} + pollConfig={mockPollConfig} redirect={jest.fn()} toasts={toasts} /> @@ -74,6 +86,7 @@ describe('ReportListing', () => { <ReportListing apiClient={reportingAPIClient as ReportingAPIClient} license$={subMock as Observable<ILicense>} + pollConfig={mockPollConfig} redirect={jest.fn()} toasts={toasts} /> diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 885e9577471a0..d8f9b7d37cfbf 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import moment from 'moment'; -import { Component, Fragment, default as React } from 'react'; +import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { Poller } from '../../common/poller'; -import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; +import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; +import { ClientConfigType } from '../plugin'; import { ReportDeleteButton, ReportDownloadButton, @@ -53,6 +54,7 @@ export interface Props { intl: InjectedIntl; apiClient: ReportingAPIClient; license$: LicensingPluginSetup['license$']; + pollConfig: ClientConfigType['poll']; redirect: ApplicationStart['navigateToApp']; toasts: ToastsSetup; } @@ -167,12 +169,10 @@ class ReportListingUi extends Component<Props, State> { functionToPoll: () => { return this.fetchJobs(); }, - pollFrequencyInMillis: - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.interval, + pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval, trailing: false, continuePollingOnError: true, - pollFrequencyErrorMultiplier: - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.intervalErrorMultiplier, + pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier, }); this.poller.start(); this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 3c121f1712685..eed6d5dd141e7 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -4,30 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; +import { NotificationsSetup } from 'src/core/public'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, JOB_STATUS_WARNINGS, } from '../../constants'; - -import { - JobId, - JobSummary, - JobStatusBuckets, - NotificationsService, - SourceJob, -} from '../../index.d'; - +import { JobId, JobStatusBuckets, JobSummary, SourceJob } from '../../index.d'; import { - getSuccessToast, getFailureToast, + getGeneralErrorToast, + getSuccessToast, getWarningFormulasToast, getWarningMaxSizeToast, - getGeneralErrorToast, } from '../components'; import { ReportingAPIClient } from './reporting_api_client'; @@ -47,7 +40,7 @@ function summarizeJob(src: SourceJob): JobSummary { } export class ReportingNotifierStreamHandler { - constructor(private notifications: NotificationsService, private apiClient: ReportingAPIClient) {} + constructor(private notifications: NotificationsSetup, private apiClient: ReportingAPIClient) {} /* * Use Kibana Toast API to show our messages diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 08ba10ff69207..c40e7ad373eaf 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -4,44 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; import ReactDOM from 'react-dom'; +import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; +import { + CoreSetup, + CoreStart, + NotificationsSetup, + Plugin, + PluginInitializerContext, +} from 'src/core/public'; import { ManagementSetup } from 'src/plugins/management/public'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import { I18nProvider } from '@kbn/i18n/react'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; - -import { ReportListing } from './components/report_listing'; -import { getGeneralErrorToast } from './components'; - -import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; -import { ReportingAPIClient } from './lib/reporting_api_client'; -import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; -import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; -import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; - -import { LicensingPluginSetup } from '../../licensing/public'; +import { JobId, JobStatusBuckets } from '../'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; -import { SharePluginSetup } from '../../../../src/plugins/share/public'; - import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; +import { SharePluginSetup } from '../../../../src/plugins/share/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ConfigType } from '../common/types'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; +import { getGeneralErrorToast } from './components'; +import { ReportListing } from './components/report_listing'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; +import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; +import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; +import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; -import { - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG, - JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, -} from '../constants'; - -import { JobId, JobStatusBuckets, NotificationsService } from '..'; - -const { - jobCompletionNotifier: { interval: JOBS_REFRESH_INTERVAL }, -} = JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG; +export interface ClientConfigType { + poll: ConfigType['poll']; +} function getStored(): JobId[] { const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY); @@ -49,7 +47,7 @@ function getStored(): JobId[] { } function handleError( - notifications: NotificationsService, + notifications: NotificationsSetup, err: Error ): Rx.Observable<JobStatusBuckets> { notifications.toasts.addDanger( @@ -64,18 +62,19 @@ function handleError( return Rx.of({ completed: [], failed: [] }); } -export class ReportingPublicPlugin implements Plugin<any, any> { +export class ReportingPublicPlugin implements Plugin<void, void> { + private config: ClientConfigType; private readonly stop$ = new Rx.ReplaySubject(1); - private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', }); - private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { defaultMessage: 'Reporting', }); - constructor(initializerContext: PluginInitializerContext) {} + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get<ClientConfigType>(); + } public setup( core: CoreSetup, @@ -130,6 +129,7 @@ export class ReportingPublicPlugin implements Plugin<any, any> { <ReportListing toasts={toasts} license$={license$} + pollConfig={this.config.poll} redirect={start.application.navigateToApp} apiClient={apiClient} /> @@ -163,8 +163,9 @@ export class ReportingPublicPlugin implements Plugin<any, any> { const { http, notifications } = core; const apiClient = new ReportingAPIClient(http); const streamHandler = new StreamHandler(notifications, apiClient); + const { interval } = this.config.poll.jobsRefresh; - Rx.timer(0, JOBS_REFRESH_INTERVAL) + Rx.timer(0, interval) .pipe( takeUntil(this.stop$), // stop the interval when stop method is called map(() => getStored()), // read all pending job IDs from session storage diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index f0a0a093aa8c0..a0d7618322c65 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -10,6 +10,7 @@ import { ConfigSchema, ConfigType } from './schema'; export { createConfig$ } from './create_config'; export const config: PluginConfigDescriptor<ConfigType> = { + exposeToBrowser: { poll: true }, schema: ConfigSchema, deprecations: ({ unused }) => [ unused('capture.browser.chromium.maxScreenshotDimension'), diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 67d70c1513c15..402fddcb5e014 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -114,6 +114,7 @@ const CaptureSchema = schema.object({ const CsvSchema = schema.object({ checkForFormulas: schema.boolean({ defaultValue: true }), + escapeFormulaValues: schema.boolean({ defaultValue: false }), enablePanelActionDownload: schema.boolean({ defaultValue: true }), maxSizeBytes: schema.number({ defaultValue: 1024 * 1024 * 10, // 10MB diff --git a/x-pack/plugins/rollup/README.md b/x-pack/plugins/rollup/README.md new file mode 100644 index 0000000000000..b43f4d5981409 --- /dev/null +++ b/x-pack/plugins/rollup/README.md @@ -0,0 +1,52 @@ +## Summary +Welcome to the Kibana rollup plugin! This plugin provides Kibana support for [Elasticsearch's rollup feature](https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-rollup.html). Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. + +This plugin allows Kibana to: + +* Create and manage rollup jobs +* Create rollup index patterns +* Create visualizations from rollup index patterns +* Identify rollup indices in Index Management + +The rest of this doc dives into the implementation details of each of the above functionality. + +--- + +## Create and manage rollup jobs + +The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives in [public/crud_app](public/crud_app) and uses endpoints registered in [(server/routes/api/jobs](server/routes/api/jobs). + +Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-getting-started.html) to understand rollup indices and how to create rollup jobs. + +## Create rollup index patterns + +Kibana uses index patterns to consume and visualize rollup indices. Typically, Kibana can inspect the indices captured by an index pattern, identify its aggregations and fields, and determine how to consume the data. Rollup indices don't contain this type of information, so we predefine how to consume a rollup index pattern with the type and typeMeta fields on the index pattern saved object. All rollup index patterns have `type` defined as "rollup" and `typeMeta` defined as an object of the index pattern's capabilities. + +In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard: + +1. Adds a `Rollup` badge to rollup indices using `getIndexTags()`. +2. Enforces index pattern rules using `checkIndicesForErrors()`. Rollup index patterns must match **one** rollup index, and optionally, any number of regular indices. A rollup index pattern configured with one or more regular indices is known as a "hybrid" index pattern. This allows the user to visualize historical (rollup) data and live (regular) data in the same visualization. +3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns/register_fields_for_wildcard_route.ts), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names. +4. Writes additional information about aggregations, fields, histogram interval, and date histogram interval and timezone to the rollup index pattern saved object using `getIndexPatternMappings()`. This collection of information is referred to as its "capabilities". + +Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](public/index_pattern_list/rollup_index_pattern_list_config.js) extension points. + +## Create visualizations from rollup index patterns + +This plugin enables the user to create visualizations from rollup data using the Visualize app, excluding TSVB, Vega, and Timelion. When Visualize sends search requests, this plugin routes the requests to the [Elasticsearch rollup search endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html), which searches the special document structure within rollup indices. The visualization options available to users are based on the capabilities of the rollup index pattern they're visualizing. + +Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js). + +Limiting visualization editor options is done by [registering configs](public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit: +* Available aggregation types +* Available fields for a particular aggregation +* Default and base interval for histogram aggregation +* Default and base interval, and time zone, for date histogram aggregation + +## Identify rollup indices in Index Management + +In Index Management, similar to system indices, rollup indices are hidden by default. A toggle is provided to show rollup indices and add a badge to the table rows. This is done by using Index Management's extension points. + +The toggle and badge are registered on the client-side in [public/extend_index_management](public/extend_index_management). + +Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.ts). \ No newline at end of file diff --git a/x-pack/plugins/rollup/common/index.ts b/x-pack/plugins/rollup/common/index.ts index aeffa3dc3959f..e94726a6f3d95 100644 --- a/x-pack/plugins/rollup/common/index.ts +++ b/x-pack/plugins/rollup/common/index.ts @@ -4,6 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LicenseType } from '../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN = { + ID: 'rollup', + minimumLicenseType: basicLicense, +}; + export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; export const API_BASE_PATH = '/api/rollup'; diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index 8f832f6c6a345..f897051d3ed8a 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -4,6 +4,16 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "optionalPlugins": ["home", "indexManagement", "indexPatternManagement", "usageCollection"], - "requiredPlugins": ["management", "data"] + "requiredPlugins": [ + "indexPatternManagement", + "management", + "licensing" + ], + "optionalPlugins": [ + "home", + "indexManagement", + "usageCollection", + "visTypeTimeseries" + ], + "configPath": ["xpack", "rollup"] } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 4a55c4679c3d8..eca624e16cb86 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -42,7 +42,7 @@ export const stepIds = [ * 1. getDefaultFields: (overrides) => object * 2. fieldValidations * - * See x-pack/plugins/rollup/public/crud_app/services/jobs.js for more information on override's shape + * See rollup/public/crud_app/services/jobs.js for more information on override's shape */ export const stepIdToStepConfigMap = { [STEP_LOGISTICS]: { diff --git a/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts b/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts index aa1cc2dfea323..5d9340a140500 100644 --- a/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts +++ b/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts @@ -16,6 +16,9 @@ export { METRIC_TYPE }; export function trackUserRequest<TResponse>(request: Promise<TResponse>, actionType: string) { // Only track successful actions. return request.then(response => { + // NOTE: METRIC_TYPE.LOADED is probably the wrong metric type here. The correct metric type + // is more likely METRIC_TYPE.APPLICATION_USAGE. This change was introduced in + // https://github.com/elastic/kibana/pull/41113/files#diff-58ac12bdd1a3a05a24e69ff20633c482R20 trackUiMetric(METRIC_TYPE.LOADED, actionType); // We return the response immediately without waiting for the tracking request to resolve, // to avoid adding additional latency. diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js index 42c950f0b0d74..9d81abf70a55d 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js +++ b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js @@ -5,21 +5,34 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiCallOut } from '@elastic/eui'; export const RollupPrompt = () => ( <EuiCallOut color="warning" iconType="help" title="Beta feature"> <p> - Kibana's support for rollup index patterns is in beta. You might encounter issues using - these patterns in saved searches, visualizations, and dashboards. They are not supported in - some advanced features, such as Timelion, and Machine Learning. + {i18n.translate( + 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text', + { + defaultMessage: + "Kibana's support for rollup index patterns is in beta. You might encounter issues using " + + 'these patterns in saved searches, visualizations, and dashboards. They are not supported in ' + + 'some advanced features, such as Timelion, and Machine Learning.', + } + )} </p> <p> - You can match a rollup index pattern against one rollup index and zero or more regular - indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A - rollup index is limited to indices that have one job configuration, or multiple jobs with - compatible configurations. + {i18n.translate( + 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text', + { + defaultMessage: + 'You can match a rollup index pattern against one rollup index and zero or more regular ' + + 'indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A ' + + 'rollup index is limited to indices that have one job configuration, or multiple jobs with ' + + 'compatible configurations.', + } + )} </p> </EuiCallOut> ); diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts index fd1b90fbc9855..5bb678ac35d06 100644 --- a/x-pack/plugins/rollup/public/plugin.ts +++ b/x-pack/plugins/rollup/public/plugin.ts @@ -11,10 +11,6 @@ import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_mana import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; // @ts-ignore import { RollupIndexPatternListConfig } from './index_pattern_list/rollup_index_pattern_list_config'; -// @ts-ignore -import { initAggTypeFilter } from './visualize/agg_type_filter'; -// @ts-ignore -import { initAggTypeFieldFilter } from './visualize/agg_type_field_filter'; import { CONFIG_ROLLUPS, UIM_APP_NAME } from '../common'; import { FeatureCatalogueCategory, @@ -25,7 +21,6 @@ import { CRUD_APP_BASE_PATH } from './crud_app/constants'; import { ManagementSetup } from '../../../../src/plugins/management/public'; import { IndexManagementPluginSetup } from '../../index_management/public'; import { IndexPatternManagementSetup } from '../../../../src/plugins/index_pattern_management/public'; -import { DataPublicPluginStart, search } from '../../../../src/plugins/data/public'; // @ts-ignore import { setEsBaseAndXPackBase, setHttp } from './crud_app/services/index'; import { setNotifications, setFatalErrors, setUiStatsReporter } from './kibana_services'; @@ -39,10 +34,6 @@ export interface RollupPluginSetupDependencies { usageCollection?: UsageCollectionSetup; } -export interface RollupPluginStartDependencies { - data: DataPublicPluginStart; -} - export class RollupPlugin implements Plugin { setup( core: CoreSetup, @@ -108,16 +99,9 @@ export class RollupPlugin implements Plugin { } } - start(core: CoreStart, plugins: RollupPluginStartDependencies) { + start(core: CoreStart) { setHttp(core.http); setNotifications(core.notifications); setEsBaseAndXPackBase(core.docLinks.ELASTIC_WEBSITE_URL, core.docLinks.DOC_LINK_VERSION); - - const isRollupIndexPatternsEnabled = core.uiSettings.get(CONFIG_ROLLUPS); - - if (isRollupIndexPatternsEnabled) { - initAggTypeFilter(search.aggs.aggTypeFilters); - initAggTypeFieldFilter(plugins.data.search.__LEGACY.aggTypeFieldFilters); - } } } diff --git a/x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js b/x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js deleted file mode 100644 index 6f44e0ef90efd..0000000000000 --- a/x-pack/plugins/rollup/public/visualize/agg_type_field_filter.js +++ /dev/null @@ -1,22 +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. - */ - -export function initAggTypeFieldFilter(aggTypeFieldFilters) { - /** - * If rollup index pattern, check its capabilities - * and limit available fields for a given aggType based on that. - */ - aggTypeFieldFilters.addFilter((field, aggConfig) => { - const indexPattern = aggConfig.getIndexPattern(); - if (!indexPattern || indexPattern.type !== 'rollup') { - return true; - } - const aggName = aggConfig.type && aggConfig.type.name; - const aggFields = - indexPattern.typeMeta && indexPattern.typeMeta.aggs && indexPattern.typeMeta.aggs[aggName]; - return aggFields && aggFields[field.name]; - }); -} diff --git a/x-pack/plugins/rollup/public/visualize/agg_type_filter.js b/x-pack/plugins/rollup/public/visualize/agg_type_filter.js deleted file mode 100644 index 5f9fab3061a19..0000000000000 --- a/x-pack/plugins/rollup/public/visualize/agg_type_filter.js +++ /dev/null @@ -1,23 +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. - */ - -export function initAggTypeFilter(aggTypeFilters) { - /** - * If rollup index pattern, check its capabilities - * and limit available aggregations based on that. - */ - aggTypeFilters.addFilter((aggType, indexPattern) => { - if (indexPattern.type !== 'rollup') { - return true; - } - const aggName = aggType.name; - const aggs = indexPattern.typeMeta && indexPattern.typeMeta.aggs; - - // Return doc_count (which is collected by default for rollup date histogram, histogram, and terms) - // and the rest of the defined metrics from capabilities. - return aggName === 'count' || Object.keys(aggs).includes(aggName); - }); -} diff --git a/x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.ts b/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.ts rename to x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts diff --git a/x-pack/legacy/plugins/rollup/server/collectors/index.ts b/x-pack/plugins/rollup/server/collectors/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/collectors/index.ts rename to x-pack/plugins/rollup/server/collectors/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/collectors/register.ts rename to x-pack/plugins/rollup/server/collectors/register.ts diff --git a/x-pack/plugins/rollup/server/config.ts b/x-pack/plugins/rollup/server/config.ts new file mode 100644 index 0000000000000..6d02600521c3a --- /dev/null +++ b/x-pack/plugins/rollup/server/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type RollupConfig = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/rollup/server/index.ts b/x-pack/plugins/rollup/server/index.ts index 4056842453776..78859a959a1e0 100644 --- a/x-pack/plugins/rollup/server/index.ts +++ b/x-pack/plugins/rollup/server/index.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { RollupPlugin } from './plugin'; +import { configSchema, RollupConfig } from './config'; -export const plugin = (initContext: PluginInitializerContext) => new RollupPlugin(initContext); +export const plugin = (pluginInitializerContext: PluginInitializerContext) => + new RollupPlugin(pluginInitializerContext); -export { RollupSetup } from './plugin'; +export const config: PluginConfigDescriptor<RollupConfig> = { + schema: configSchema, +}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/index.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/index.js rename to x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js diff --git a/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js new file mode 100644 index 0000000000000..c03b7c33abe0a --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js @@ -0,0 +1,98 @@ +/* + * 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 const jobs = [ + { + job_id: 'foo1', + rollup_index: 'foo_rollup', + index_pattern: 'foo-*', + fields: { + node: [ + { + agg: 'terms', + }, + ], + temperature: [ + { + agg: 'min', + }, + { + agg: 'max', + }, + { + agg: 'sum', + }, + ], + timestamp: [ + { + agg: 'date_histogram', + time_zone: 'UTC', + interval: '1h', + delay: '7d', + }, + ], + voltage: [ + { + agg: 'histogram', + interval: 5, + }, + { + agg: 'sum', + }, + ], + }, + }, + { + job_id: 'foo2', + rollup_index: 'foo_rollup', + index_pattern: 'foo-*', + fields: { + host: [ + { + agg: 'terms', + }, + ], + timestamp: [ + { + agg: 'date_histogram', + time_zone: 'UTC', + interval: '1h', + delay: '7d', + }, + ], + voltage: [ + { + agg: 'histogram', + interval: 20, + }, + ], + }, + }, + { + job_id: 'foo3', + rollup_index: 'foo_rollup', + index_pattern: 'foo-*', + fields: { + timestamp: [ + { + agg: 'date_histogram', + time_zone: 'PST', + interval: '1h', + delay: '7d', + }, + ], + voltage: [ + { + agg: 'histogram', + interval: 5, + }, + { + agg: 'sum', + }, + ], + }, + }, +]; diff --git a/x-pack/legacy/plugins/rollup/server/lib/__tests__/jobs_compatibility.js b/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/__tests__/jobs_compatibility.js rename to x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js diff --git a/x-pack/plugins/rollup/server/lib/format_es_error.ts b/x-pack/plugins/rollup/server/lib/format_es_error.ts new file mode 100644 index 0000000000000..9dde027cd6949 --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/format_es_error.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +function extractCausedByChain( + causedBy: Record<string, any> = {}, + accumulator: string[] = [] +): string[] { + const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase + + if (reason) { + accumulator.push(reason); + } + + // eslint-disable-next-line @typescript-eslint/camelcase + if (caused_by) { + return extractCausedByChain(caused_by, accumulator); + } + + return accumulator; +} + +/** + * Wraps an error thrown by the ES JS client into a Boom error response and returns it + * + * @param err Object Error thrown by ES JS client + * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages + */ +export function wrapEsError( + err: any, + statusCodeToMessageMap: Record<string, string> = {} +): { message: string; body?: { cause?: string[] }; statusCode: number } { + const { statusCode, response } = err; + + const { + error: { + root_cause = [], // eslint-disable-line @typescript-eslint/camelcase + caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase + } = {}, + } = JSON.parse(response); + + // If no custom message if specified for the error's status code, just + // wrap the error as a Boom error response and return it + if (!statusCodeToMessageMap[statusCode]) { + // The caused_by chain has the most information so use that if it's available. If not then + // settle for the root_cause. + const causedByChain = extractCausedByChain(caused_by); + const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; + + return { + message: err.message, + statusCode, + body: { + cause: causedByChain.length ? causedByChain : defaultCause, + }, + }; + } + + // Otherwise, use the custom message to create a Boom error response and + // return it + const message = statusCodeToMessageMap[statusCode]; + return { message, statusCode }; +} + +export function formatEsError(err: any): any { + const { statusCode, message, body } = wrapEsError(err); + return { + statusCode, + body: { + message, + attributes: { + cause: body?.cause, + }, + }, + }; +} diff --git a/x-pack/plugins/rollup/server/lib/is_es_error.ts b/x-pack/plugins/rollup/server/lib/is_es_error.ts new file mode 100644 index 0000000000000..4137293cf39c0 --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/is_es_error.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as legacyElasticsearch from 'elasticsearch'; + +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.ts b/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.ts rename to x-pack/plugins/rollup/server/lib/jobs_compatibility.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/map_capabilities.ts b/x-pack/plugins/rollup/server/lib/map_capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/map_capabilities.ts rename to x-pack/plugins/rollup/server/lib/map_capabilities.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.ts b/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.ts rename to x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.ts b/x-pack/plugins/rollup/server/lib/search_strategies/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts b/x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts similarity index 76% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts index 93c4c1b52140b..333863979ba95 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts @@ -3,18 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getRollupSearchStrategy } from './rollup_search_strategy'; -import { getRollupSearchRequest } from './rollup_search_request'; -import { getRollupSearchCapabilities } from './rollup_search_capabilities'; + import { AbstractSearchRequest, DefaultSearchCapabilities, AbstractSearchStrategy, -} from '../../../../../../../src/plugins/vis_type_timeseries/server'; -import { RouteDependencies } from '../../types'; +} from '../../../../../../src/plugins/vis_type_timeseries/server'; +import { CallWithRequestFactoryShim } from '../../types'; +import { getRollupSearchStrategy } from './rollup_search_strategy'; +import { getRollupSearchRequest } from './rollup_search_request'; +import { getRollupSearchCapabilities } from './rollup_search_capabilities'; export const registerRollupSearchStrategy = ( - { elasticsearchService }: RouteDependencies, + callWithRequestFactory: CallWithRequestFactoryShim, addSearchStrategy: (searchStrategy: any) => void ) => { const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest); @@ -22,8 +23,9 @@ export const registerRollupSearchStrategy = ( const RollupSearchStrategy = getRollupSearchStrategy( AbstractSearchStrategy, RollupSearchRequest, - RollupSearchCapabilities + RollupSearchCapabilities, + callWithRequestFactory ); - addSearchStrategy(new RollupSearchStrategy(elasticsearchService)); + addSearchStrategy(new RollupSearchStrategy()); }; diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts similarity index 98% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts index 5a57129aa6039..151afe660847f 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get, has } from 'lodash'; -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'src/core/server'; import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper'; export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) => diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts similarity index 84% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts index 9d5aad2c2d3bc..815fe163411b3 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { indexBy, isString } from 'lodash'; -import { ElasticsearchServiceSetup, KibanaRequest } from 'kibana/server'; -import { callWithRequestFactory } from '../call_with_request_factory'; +import { KibanaRequest } from 'src/core/server'; + +import { CallWithRequestFactoryShim } from '../../types'; import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields'; import { getCapabilitiesForRollupIndices } from '../map_capabilities'; @@ -20,13 +21,16 @@ const isIndexPatternValid = (indexPattern: string) => export const getRollupSearchStrategy = ( AbstractSearchStrategy: any, RollupSearchRequest: any, - RollupSearchCapabilities: any + RollupSearchCapabilities: any, + callWithRequestFactory: CallWithRequestFactoryShim ) => class RollupSearchStrategy extends AbstractSearchStrategy { name = 'rollup'; - constructor(elasticsearchService: ElasticsearchServiceSetup) { - super(elasticsearchService, callWithRequestFactory, RollupSearchRequest); + constructor() { + // TODO: When vis_type_timeseries and AbstractSearchStrategy are migrated to the NP, it + // shouldn't require elasticsearchService to be injected, and we can remove this null argument. + super(null, callWithRequestFactory, RollupSearchRequest); } getRollupData(req: KibanaRequest, indexPattern: string) { diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index ea6d197e22029..ee9a1844c7468 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -4,20 +4,98 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +declare module 'src/core/server' { + interface RequestHandlerContext { + rollup?: RollupContext; + } +} + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { + CoreSetup, + Plugin, + Logger, + KibanaRequest, + PluginInitializerContext, + IScopedClusterClient, + APICaller, + SharedGlobalConfig, +} from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { CONFIG_ROLLUPS } from '../common'; -export class RollupPlugin implements Plugin<RollupSetup> { - private readonly initContext: PluginInitializerContext; +import { PLUGIN, CONFIG_ROLLUPS } from '../common'; +import { Dependencies, CallWithRequestFactoryShim } from './types'; +import { registerApiRoutes } from './routes'; +import { License } from './services'; +import { registerRollupUsageCollector } from './collectors'; +import { rollupDataEnricher } from './rollup_data_enricher'; +import { IndexPatternsFetcher } from './shared_imports'; +import { registerRollupSearchStrategy } from './lib/search_strategies'; +import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; + +interface RollupContext { + client: IScopedClusterClient; +} + +export class RollupPlugin implements Plugin<void, void, any, any> { + private readonly logger: Logger; + private readonly globalConfig$: Observable<SharedGlobalConfig>; + private readonly license: License; - constructor(initContext: PluginInitializerContext) { - this.initContext = initContext; + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.globalConfig$ = initializerContext.config.legacy.globalConfig$; + this.license = new License(); } - public setup(core: CoreSetup) { - core.uiSettings.register({ + public setup( + { http, uiSettings, elasticsearch }: CoreSetup, + { licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies + ) { + this.license.setup( + { + pluginId: PLUGIN.ID, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate('xpack.rollupJobs.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + // Extend the elasticsearchJs client with additional endpoints. + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + const rollupEsClient = elasticsearch.createClient('rollup', esClientConfig); + http.registerRouteHandlerContext('rollup', (context, request) => { + return { + client: rollupEsClient.asScoped(request), + }; + }); + + registerApiRoutes({ + router: http.createRouter(), + license: this.license, + lib: { + isEsError, + formatEsError, + getCapabilitiesForRollupIndices, + mergeCapabilitiesWithFields, + }, + sharedImports: { + IndexPatternsFetcher, + }, + }); + + uiSettings.register({ [CONFIG_ROLLUPS]: { name: i18n.translate('xpack.rollupJobs.rollupIndexPatternsTitle', { defaultMessage: 'Enable rollup index patterns', @@ -33,22 +111,34 @@ export class RollupPlugin implements Plugin<RollupSetup> { }, }); - return { - __legacy: { - config: this.initContext.config, - logger: this.initContext.logger, - }, - }; - } + if (visTypeTimeseries) { + // TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim. + const callWithRequestFactoryShim = ( + elasticsearchServiceShim: CallWithRequestFactoryShim, + request: KibanaRequest + ): APICaller => rollupEsClient.asScoped(request).callAsCurrentUser; - public start() {} - public stop() {} -} + const { addSearchStrategy } = visTypeTimeseries; + registerRollupSearchStrategy(callWithRequestFactoryShim, addSearchStrategy); + } + + if (usageCollection) { + this.globalConfig$ + .pipe(first()) + .toPromise() + .then(globalConfig => { + registerRollupUsageCollector(usageCollection, globalConfig.kibana.index); + }) + .catch((e: any) => { + this.logger.warn(`Registering Rollup collector failed: ${e}`); + }); + } + + if (indexManagement && indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(rollupDataEnricher); + } + } -export interface RollupSetup { - /** @deprecated */ - __legacy: { - config: PluginInitializerContext['config']; - logger: PluginInitializerContext['logger']; - }; + start() {} + stop() {} } diff --git a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts b/x-pack/plugins/rollup/server/rollup_data_enricher.ts similarity index 92% rename from x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts rename to x-pack/plugins/rollup/server/rollup_data_enricher.ts index ad621f2d9ba80..b06cf971a6460 100644 --- a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts +++ b/x-pack/plugins/rollup/server/rollup_data_enricher.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Index } from '../../../../plugins/index_management/server'; +import { Index } from '../../../plugins/index_management/server'; export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => { if (!indicesList || !indicesList.length) { diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts new file mode 100644 index 0000000000000..7bf525ca4aa98 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerFieldsForWildcardRoute } from './register_fields_for_wildcard_route'; + +export function registerIndexPatternsRoutes(dependencies: RouteDependencies) { + registerFieldsForWildcardRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts new file mode 100644 index 0000000000000..32f23314c5259 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts @@ -0,0 +1,141 @@ +/* + * 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 { indexBy } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { Field } from '../../../lib/merge_capabilities_with_fields'; +import { RouteDependencies } from '../../../types'; + +const parseMetaFields = (metaFields: string | string[]) => { + let parsedFields: string[] = []; + if (typeof metaFields === 'string') { + parsedFields = JSON.parse(metaFields); + } else { + parsedFields = metaFields; + } + return parsedFields; +}; + +const getFieldsForWildcardRequest = async ( + context: any, + request: any, + response: any, + IndexPatternsFetcher: any +) => { + const { callAsCurrentUser } = context.core.elasticsearch.dataClient; + const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { pattern, meta_fields: metaFields } = request.query; + + let parsedFields: string[] = []; + try { + parsedFields = parseMetaFields(metaFields); + } catch (error) { + return response.badRequest({ + body: error, + }); + } + + try { + const fields = await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields: parsedFields, + }); + + return response.ok({ + body: { fields }, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + return response.notFound(); + } +}; + +/** + * Get list of fields for rollup index pattern, in the format of regular index pattern fields + */ +export const registerFieldsForWildcardRoute = ({ + router, + license, + lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices, mergeCapabilitiesWithFields }, + sharedImports: { IndexPatternsFetcher }, +}: RouteDependencies) => { + const querySchema = schema.object({ + pattern: schema.string(), + meta_fields: schema.arrayOf(schema.string(), { + defaultValue: [], + }), + params: schema.string({ + validate(value) { + try { + const params = JSON.parse(value); + const keys = Object.keys(params); + const { rollup_index: rollupIndex } = params; + + if (!rollupIndex) { + return '[request query.params]: "rollup_index" is required'; + } else if (keys.length > 1) { + const invalidParams = keys.filter(key => key !== 'rollup_index'); + return `[request query.params]: ${invalidParams.join(', ')} is not allowed`; + } + } catch (err) { + return '[request query.params]: expected JSON string'; + } + }, + }), + }); + + router.get( + { + path: '/api/index_patterns/rollup/_fields_for_wildcard', + validate: { + query: querySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { params, meta_fields: metaFields } = request.query; + + try { + // Make call and use field information from response + const { payload } = await getFieldsForWildcardRequest( + context, + request, + response, + IndexPatternsFetcher + ); + const fields = payload.fields; + const parsedParams = JSON.parse(params); + const rollupIndex = parsedParams.rollup_index; + const rollupFields: Field[] = []; + const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name'); + const rollupIndexCapabilities = getCapabilitiesForRollupIndices( + await context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { + indexPattern: rollupIndex, + }) + )[rollupIndex].aggs; + + // Keep meta fields + metaFields.forEach( + (field: string) => + fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) + ); + + const mergedRollupFields = mergeCapabilitiesWithFields( + rollupIndexCapabilities, + fieldsFromFieldCapsApi, + rollupFields + ); + return response.ok({ body: { fields: mergedRollupFields } }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/indices/index.ts b/x-pack/plugins/rollup/server/routes/api/indices/index.ts new file mode 100644 index 0000000000000..0aa5772b56991 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/indices/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { RouteDependencies } from '../../../types'; +import { registerGetRoute } from './register_get_route'; +import { registerValidateIndexPatternRoute } from './register_validate_index_pattern_route'; + +export function registerIndicesRoutes(dependencies: RouteDependencies) { + registerGetRoute(dependencies); + registerValidateIndexPatternRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts new file mode 100644 index 0000000000000..3521650c1dc3e --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -0,0 +1,39 @@ +/* + * 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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a list of all rollup index names + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/indices'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const data = await context.rollup!.client.callAsCurrentUser( + 'rollup.rollupIndexCapabilities', + { + indexPattern: '_all', + } + ); + return response.ok({ body: getCapabilitiesForRollupIndices(data) }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts new file mode 100644 index 0000000000000..9e22060b9beb7 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts @@ -0,0 +1,142 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +type NumericField = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'scaled_float' + | 'double' + | 'float' + | 'half_float'; + +interface FieldCapability { + date?: any; + keyword?: any; + long?: any; + integer?: any; + short?: any; + byte?: any; + double?: any; + float?: any; + half_float?: any; + scaled_float?: any; +} + +interface FieldCapabilities { + fields: FieldCapability[]; +} + +function isNumericField(fieldCapability: FieldCapability) { + const numericTypes = [ + 'long', + 'integer', + 'short', + 'byte', + 'double', + 'float', + 'half_float', + 'scaled_float', + ]; + return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null); +} + +/** + * Returns information on validity of an index pattern for creating a rollup job: + * - Does the index pattern match any indices? + * - Does the index pattern match rollup indices? + * - Which date fields, numeric fields, and keyword fields are available in the matching indices? + */ +export const registerValidateIndexPatternRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/index_pattern_validity/{indexPattern}'), + validate: { + params: schema.object({ + indexPattern: schema.string(), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { indexPattern } = request.params; + const [fieldCapabilities, rollupIndexCapabilities]: [ + FieldCapabilities, + { [key: string]: any } + ] = await Promise.all([ + context.rollup!.client.callAsCurrentUser('rollup.fieldCapabilities', { indexPattern }), + context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { + indexPattern, + }), + ]); + + const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; + const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0; + + const dateFields: string[] = []; + const numericFields: string[] = []; + const keywordFields: string[] = []; + + const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); + + fieldCapabilitiesEntries.forEach( + ([fieldName, fieldCapability]: [string, FieldCapability]) => { + if (fieldCapability.date) { + dateFields.push(fieldName); + return; + } + + if (isNumericField(fieldCapability)) { + numericFields.push(fieldName); + return; + } + + if (fieldCapability.keyword) { + keywordFields.push(fieldName); + } + } + ); + + const body = { + doesMatchIndices, + doesMatchRollupIndices, + dateFields, + numericFields, + keywordFields, + }; + + return response.ok({ body }); + } catch (err) { + // 404s are still valid results. + if (err.statusCode === 404) { + const notFoundBody = { + doesMatchIndices: false, + doesMatchRollupIndices: false, + dateFields: [], + numericFields: [], + keywordFields: [], + }; + return response.ok({ body: notFoundBody }); + } + + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/index.ts b/x-pack/plugins/rollup/server/routes/api/jobs/index.ts new file mode 100644 index 0000000000000..fe1d1c6109a88 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerDeleteRoute } from './register_delete_route'; +import { registerGetRoute } from './register_get_route'; +import { registerStartRoute } from './register_start_route'; +import { registerStopRoute } from './register_stop_route'; + +export function registerJobsRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerDeleteRoute(dependencies); + registerGetRoute(dependencies); + registerStartRoute(dependencies); + registerStopRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts new file mode 100644 index 0000000000000..adf8c1da0af0e --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.put( + { + path: addBasePath('/create'), + validate: { + body: schema.object({ + job: schema.object( + { + id: schema.string(), + }, + { unknowns: 'allow' } + ), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { id, ...rest } = request.body.job; + // Create job. + await context.rollup!.client.callAsCurrentUser('rollup.createJob', { + id, + body: rest, + }); + // Then request the newly created job. + const results = await context.rollup!.client.callAsCurrentUser('rollup.job', { id }); + return response.ok({ body: results.jobs[0] }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts new file mode 100644 index 0000000000000..32f7b3f35e163 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerDeleteRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/delete'), + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { jobIds } = request.body; + const data = await Promise.all( + jobIds.map((id: string) => + context.rollup!.client.callAsCurrentUser('rollup.deleteJob', { id }) + ) + ).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + // There is an issue opened on ES to handle the following error correctly + // https://github.com/elastic/elasticsearch/issues/42908 + // Until then we'll modify the response here. + if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { + err.status = 400; + err.statusCode = 400; + err.displayName = 'Bad request'; + err.message = JSON.parse(err.response).task_failures[0].reason.reason; + } + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts new file mode 100644 index 0000000000000..a8d51f4639fc6 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/jobs'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const data = await context.rollup!.client.callAsCurrentUser('rollup.jobs'); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts new file mode 100644 index 0000000000000..fb6f2b12ba52e --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerStartRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/start'), + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + query: schema.maybe( + schema.object({ + waitForCompletion: schema.maybe(schema.string()), + }) + ), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { jobIds } = request.body; + + const data = await Promise.all( + jobIds.map((id: string) => + context.rollup!.client.callAsCurrentUser('rollup.startJob', { id }) + ) + ).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts new file mode 100644 index 0000000000000..118d98e36e03c --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerStopRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/stop'), + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + query: schema.object({ + waitForCompletion: schema.maybe(schema.string()), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { jobIds } = request.body; + // For our API integration tests we need to wait for the jobs to be stopped + // in order to be able to delete them sequentially. + const { waitForCompletion } = request.query; + const stopRollupJob = (id: string) => + context.rollup!.client.callAsCurrentUser('rollup.stopJob', { + id, + waitForCompletion: waitForCompletion === 'true', + }); + const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/search/index.ts b/x-pack/plugins/rollup/server/routes/api/search/index.ts new file mode 100644 index 0000000000000..2a2d823e79bc6 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/search/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerSearchRoute } from './register_search_route'; + +export function registerSearchRoutes(dependencies: RouteDependencies) { + registerSearchRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts new file mode 100644 index 0000000000000..c5c56336def1a --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerSearchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/search'), + validate: { + body: schema.arrayOf( + schema.object({ + index: schema.string(), + query: schema.any(), + }) + ), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const requests = request.body.map(({ index, query }: { index: string; query?: any }) => + context.rollup!.client.callAsCurrentUser('rollup.search', { + index, + rest_total_hits_as_int: true, + body: query, + }) + ); + const data = await Promise.all(requests); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/index.ts b/x-pack/plugins/rollup/server/routes/index.ts new file mode 100644 index 0000000000000..b25480855b4a2 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../types'; + +import { registerIndexPatternsRoutes } from './api/index_patterns'; +import { registerIndicesRoutes } from './api/indices'; +import { registerJobsRoutes } from './api/jobs'; +import { registerSearchRoutes } from './api/search'; + +export function registerApiRoutes(dependencies: RouteDependencies) { + registerIndexPatternsRoutes(dependencies); + registerIndicesRoutes(dependencies); + registerJobsRoutes(dependencies); + registerSearchRoutes(dependencies); +} diff --git a/x-pack/plugins/rollup/server/services/add_base_path.ts b/x-pack/plugins/rollup/server/services/add_base_path.ts new file mode 100644 index 0000000000000..7d7cce3aab334 --- /dev/null +++ b/x-pack/plugins/rollup/server/services/add_base_path.ts @@ -0,0 +1,9 @@ +/* + * 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 { API_BASE_PATH } from '../../common'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/plugins/rollup/server/services/index.ts b/x-pack/plugins/rollup/server/services/index.ts new file mode 100644 index 0000000000000..7f79c4f446546 --- /dev/null +++ b/x-pack/plugins/rollup/server/services/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { addBasePath } from './add_base_path'; +export { License } from './license'; diff --git a/x-pack/plugins/rollup/server/services/license.ts b/x-pack/plugins/rollup/server/services/license.ts new file mode 100644 index 0000000000000..bfd357867c3e2 --- /dev/null +++ b/x-pack/plugins/rollup/server/services/license.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + private _isEsSecurityEnabled: boolean = false; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + // Retrieving security checks the results of GET /_xpack as well as license state, + // so we're also checking whether the security is disabled in elasticsearch.yml. + this._isEsSecurityEnabled = license.getFeature('security').isEnabled; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute<P, Q, B>(handler: RequestHandler<P, Q, B>) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest<P, Q, B>, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + get isEsSecurityEnabled() { + return this._isEsSecurityEnabled; + } +} diff --git a/x-pack/plugins/rollup/server/shared_imports.ts b/x-pack/plugins/rollup/server/shared_imports.ts new file mode 100644 index 0000000000000..09842f529abed --- /dev/null +++ b/x-pack/plugins/rollup/server/shared_imports.ts @@ -0,0 +1,7 @@ +/* + * 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 { IndexPatternsFetcher } from '../../../../src/plugins/data/server'; diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts new file mode 100644 index 0000000000000..c21d76400164e --- /dev/null +++ b/x-pack/plugins/rollup/server/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, APICaller, KibanaRequest } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; + +import { IndexManagementPluginSetup } from '../../index_management/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { License } from './services'; +import { IndexPatternsFetcher } from './shared_imports'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; + +export interface Dependencies { + indexManagement?: IndexManagementPluginSetup; + visTypeTimeseries?: VisTypeTimeseriesSetup; + usageCollection?: UsageCollectionSetup; + licensing: LicensingPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + formatEsError: typeof formatEsError; + getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices; + mergeCapabilitiesWithFields: typeof mergeCapabilitiesWithFields; + }; + sharedImports: { + IndexPatternsFetcher: typeof IndexPatternsFetcher; + }; +} + +// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim. +export type CallWithRequestFactoryShim = ( + elasticsearchServiceShim: CallWithRequestFactoryShim, + request: KibanaRequest +) => APICaller; diff --git a/x-pack/plugins/security/common/licensing/license_features.ts b/x-pack/plugins/security/common/licensing/license_features.ts index 5184ab0e962bd..571d2630b2b17 100644 --- a/x-pack/plugins/security/common/licensing/license_features.ts +++ b/x-pack/plugins/security/common/licensing/license_features.ts @@ -33,6 +33,11 @@ export interface SecurityLicenseFeatures { */ readonly showRoleMappingsManagement: boolean; + /** + * Indicates whether we allow users to access agreement UI and acknowledge it. + */ + readonly allowAccessAgreement: boolean; + /** * Indicates whether we allow users to define document level security in roles. */ diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts index 5bdfa7d4886aa..9dec665614635 100644 --- a/x-pack/plugins/security/common/licensing/license_service.test.ts +++ b/x-pack/plugins/security/common/licensing/license_service.test.ts @@ -18,6 +18,7 @@ describe('license features', function() { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, layout: 'error-es-unavailable', @@ -37,6 +38,7 @@ describe('license features', function() { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, layout: 'error-xpack-unavailable', @@ -60,6 +62,7 @@ describe('license features', function() { expect(subscriptionHandler.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { + "allowAccessAgreement": false, "allowLogin": false, "allowRbac": false, "allowRoleDocumentLevelSecurity": false, @@ -78,6 +81,7 @@ describe('license features', function() { expect(subscriptionHandler.mock.calls[1]).toMatchInlineSnapshot(` Array [ Object { + "allowAccessAgreement": true, "allowLogin": true, "allowRbac": true, "allowRoleDocumentLevelSecurity": true, @@ -94,7 +98,7 @@ describe('license features', function() { } }); - it('should show login page and other security elements, allow RBAC but forbid role mappings, DLS, and sub-feature privileges if license is basic.', () => { + it('should show login page and other security elements, allow RBAC but forbid paid features if license is basic.', () => { const mockRawLicense = licensingMock.createLicense({ features: { security: { isEnabled: true, isAvailable: true } }, }); @@ -109,6 +113,7 @@ describe('license features', function() { allowLogin: true, showLinks: true, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: true, @@ -131,6 +136,7 @@ describe('license features', function() { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -138,7 +144,7 @@ describe('license features', function() { }); }); - it('should allow role mappings and sub-feature privileges, but not DLS/FLS if license = gold', () => { + it('should allow role mappings, access agreement and sub-feature privileges, but not DLS/FLS if license = gold', () => { const mockRawLicense = licensingMock.createLicense({ license: { mode: 'gold', type: 'gold' }, features: { security: { isEnabled: true, isAvailable: true } }, @@ -152,6 +158,7 @@ describe('license features', function() { allowLogin: true, showLinks: true, showRoleMappingsManagement: true, + allowAccessAgreement: true, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: true, @@ -159,7 +166,7 @@ describe('license features', function() { }); }); - it('should allow to login, allow RBAC, role mappings, sub-feature privileges, and DLS if license >= platinum', () => { + it('should allow to login, allow RBAC, role mappings, access agreement, sub-feature privileges, and DLS if license >= platinum', () => { const mockRawLicense = licensingMock.createLicense({ license: { mode: 'platinum', type: 'platinum' }, features: { security: { isEnabled: true, isAvailable: true } }, @@ -173,6 +180,7 @@ describe('license features', function() { allowLogin: true, showLinks: true, showRoleMappingsManagement: true, + allowAccessAgreement: true, allowRoleDocumentLevelSecurity: true, allowRoleFieldLevelSecurity: true, allowRbac: true, diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts index 34bc44b88e40d..7815798d6a9f3 100644 --- a/x-pack/plugins/security/common/licensing/license_service.ts +++ b/x-pack/plugins/security/common/licensing/license_service.ts @@ -71,6 +71,7 @@ export class SecurityLicenseService { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -88,6 +89,7 @@ export class SecurityLicenseService { allowLogin: false, showLinks: false, showRoleMappingsManagement: false, + allowAccessAgreement: false, allowRoleDocumentLevelSecurity: false, allowRoleFieldLevelSecurity: false, allowRbac: false, @@ -102,6 +104,7 @@ export class SecurityLicenseService { allowLogin: true, showLinks: true, showRoleMappingsManagement: isLicenseGoldOrBetter, + allowAccessAgreement: isLicenseGoldOrBetter, allowSubFeaturePrivileges: isLicenseGoldOrBetter, // Only platinum and trial licenses are compliant with field- and document-level security. allowRoleDocumentLevelSecurity: isLicensePlatinumOrBetter, diff --git a/x-pack/plugins/security/common/login_state.ts b/x-pack/plugins/security/common/login_state.ts index 4342e82d2f90b..fd2b1cb8d1cf7 100644 --- a/x-pack/plugins/security/common/login_state.ts +++ b/x-pack/plugins/security/common/login_state.ts @@ -6,15 +6,24 @@ import { LoginLayout } from './licensing'; +export interface LoginSelectorProvider { + type: string; + name: string; + usesLoginForm: boolean; + description?: string; + hint?: string; + icon?: string; +} + export interface LoginSelector { enabled: boolean; - providers: Array<{ type: string; name: string; description?: string }>; + providers: LoginSelectorProvider[]; } export interface LoginState { layout: LoginLayout; allowLogin: boolean; - showLoginForm: boolean; requiresSecureConnection: boolean; + loginHelp?: string; selector: LoginSelector; } diff --git a/x-pack/plugins/security/common/types.ts b/x-pack/plugins/security/common/types.ts new file mode 100644 index 0000000000000..c668c6ccf71d1 --- /dev/null +++ b/x-pack/plugins/security/common/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Type and name tuple to identify provider used to authenticate user. + */ +export interface AuthenticationProvider { + type: string; + name: string; +} + +export interface SessionInfo { + now: number; + idleTimeoutExpiration: number | null; + lifespanExpiration: number | null; + provider: AuthenticationProvider; +} diff --git a/x-pack/plugins/security/public/account_management/account_management_app.ts b/x-pack/plugins/security/public/account_management/account_management_app.ts index cd3ef34858b19..41567a04fe030 100644 --- a/x-pack/plugins/security/public/account_management/account_management_app.ts +++ b/x-pack/plugins/security/public/account_management/account_management_app.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public'; import { AuthenticationServiceSetup } from '../authentication'; -import { UserAPIClient } from '../management'; interface CreateDeps { application: ApplicationSetup; @@ -28,9 +27,14 @@ export const accountManagementApp = Object.freeze({ navLinkStatus: 3, appRoute: '/security/account', async mount({ element }: AppMountParameters) { - const [[coreStart], { renderAccountManagementPage }] = await Promise.all([ + const [ + [coreStart], + { renderAccountManagementPage }, + { UserAPIClient }, + ] = await Promise.all([ getStartServices(), import('./account_management_page'), + import('../management'), ]); coreStart.chrome.setBreadcrumbs([{ text: title }]); diff --git a/x-pack/plugins/security/public/authentication/_index.scss b/x-pack/plugins/security/public/authentication/_index.scss deleted file mode 100644 index 0a423c00f0218..0000000000000 --- a/x-pack/plugins/security/public/authentication/_index.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Component styles -@import './components/index'; - -// Login styles -@import './login/index'; diff --git a/x-pack/plugins/security/public/authentication/access_agreement/__snapshots__/access_agreement_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/access_agreement/__snapshots__/access_agreement_page.test.tsx.snap new file mode 100644 index 0000000000000..2227cbe8a495c --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/__snapshots__/access_agreement_page.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccessAgreementPage renders as expected when state is available 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" + > + <p + key="paragraph-1-1" + > + This is + <a + href="../link" + key="link-1-9" + > + link + </a> + </p> + </div> +</ReactMarkdown> +`; diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts new file mode 100644 index 0000000000000..add2db6a3c170 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.test.ts @@ -0,0 +1,62 @@ +/* + * 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. + */ + +jest.mock('./access_agreement_page'); + +import { AppMount, ScopedHistory } from 'src/core/public'; +import { accessAgreementApp } from './access_agreement_app'; + +import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; + +describe('accessAgreementApp', () => { + it('properly registers application', () => { + const coreSetupMock = coreMock.createSetup(); + + accessAgreementApp.create({ + application: coreSetupMock.application, + getStartServices: coreSetupMock.getStartServices, + }); + + expect(coreSetupMock.application.register).toHaveBeenCalledTimes(1); + + const [[appRegistration]] = coreSetupMock.application.register.mock.calls; + expect(appRegistration).toEqual({ + id: 'security_access_agreement', + chromeless: true, + appRoute: '/security/access_agreement', + title: 'Access Agreement', + mount: expect.any(Function), + }); + }); + + it('properly renders application', async () => { + const coreSetupMock = coreMock.createSetup(); + const coreStartMock = coreMock.createStart(); + coreSetupMock.getStartServices.mockResolvedValue([coreStartMock, {}, {}]); + const containerMock = document.createElement('div'); + + accessAgreementApp.create({ + application: coreSetupMock.application, + getStartServices: coreSetupMock.getStartServices, + }); + + const [[{ mount }]] = coreSetupMock.application.register.mock.calls; + await (mount as AppMount)({ + element: containerMock, + appBasePath: '', + onAppLeave: jest.fn(), + history: (scopedHistoryMock.create() as unknown) as ScopedHistory, + }); + + const mockRenderApp = jest.requireMock('./access_agreement_page').renderAccessAgreementPage; + expect(mockRenderApp).toHaveBeenCalledTimes(1); + expect(mockRenderApp).toHaveBeenCalledWith(coreStartMock.i18n, containerMock, { + http: coreStartMock.http, + notifications: coreStartMock.notifications, + fatalErrors: coreStartMock.fatalErrors, + }); + }); +}); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.ts b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.ts new file mode 100644 index 0000000000000..156a76542a28f --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_app.ts @@ -0,0 +1,38 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { StartServicesAccessor, ApplicationSetup, AppMountParameters } from 'src/core/public'; + +interface CreateDeps { + application: ApplicationSetup; + getStartServices: StartServicesAccessor; +} + +export const accessAgreementApp = Object.freeze({ + id: 'security_access_agreement', + create({ application, getStartServices }: CreateDeps) { + application.register({ + id: this.id, + title: i18n.translate('xpack.security.accessAgreementAppTitle', { + defaultMessage: 'Access Agreement', + }), + chromeless: true, + appRoute: '/security/access_agreement', + async mount({ element }: AppMountParameters) { + const [[coreStart], { renderAccessAgreementPage }] = await Promise.all([ + getStartServices(), + import('./access_agreement_page'), + ]); + return renderAccessAgreementPage(coreStart.i18n, element, { + http: coreStart.http, + notifications: coreStart.notifications, + fatalErrors: coreStart.fatalErrors, + }); + }, + }); + }, +}); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss new file mode 100644 index 0000000000000..08e7be248619f --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.scss @@ -0,0 +1,21 @@ +.secAccessAgreementPage .secAuthenticationStatePage__content { + max-width: 600px; +} + +.secAccessAgreementPage__textWrapper { + overflow-y: hidden; +} + +.secAccessAgreementPage__text { + @include euiYScrollWithShadows; + max-height: 400px; + padding: $euiSize $euiSizeL 0; +} + +.secAccessAgreementPage__footer { + padding: $euiSize $euiSizeL $euiSizeL; +} + +.secAccessAgreementPage__footerInner { + text-align: left; +} diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.test.tsx b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.test.tsx new file mode 100644 index 0000000000000..89b7489d45ebb --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.test.tsx @@ -0,0 +1,160 @@ +/* + * 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 ReactMarkdown from 'react-markdown'; +import { EuiLoadingContent } from '@elastic/eui'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { AccessAgreementPage } from './access_agreement_page'; + +describe('AccessAgreementPage', () => { + beforeAll(() => { + Object.defineProperty(window, 'location', { + value: { href: 'http://some-host/bar', protocol: 'http' }, + writable: true, + }); + }); + + afterAll(() => { + delete (window as any).location; + }); + + it('renders as expected when state is available', async () => { + const coreStartMock = coreMock.createStart(); + coreStartMock.http.get.mockResolvedValue({ accessAgreement: 'This is [link](../link)' }); + + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + expect(wrapper.exists(EuiLoadingContent)).toBe(true); + expect(wrapper.exists(ReactMarkdown)).toBe(false); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(ReactMarkdown)).toMatchSnapshot(); + expect(wrapper.exists(EuiLoadingContent)).toBe(false); + + expect(coreStartMock.http.get).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.get).toHaveBeenCalledWith( + '/internal/security/access_agreement/state' + ); + expect(coreStartMock.fatalErrors.add).not.toHaveBeenCalled(); + }); + + it('fails when state is not available', async () => { + const coreStartMock = coreMock.createStart(); + const error = Symbol(); + coreStartMock.http.get.mockRejectedValue(error); + + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(coreStartMock.http.get).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.get).toHaveBeenCalledWith( + '/internal/security/access_agreement/state' + ); + expect(coreStartMock.fatalErrors.add).toHaveBeenCalledTimes(1); + expect(coreStartMock.fatalErrors.add).toHaveBeenCalledWith(error); + }); + + it('properly redirects after successful acknowledgement', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.get.mockResolvedValue({ accessAgreement: 'This is [link](../link)' }); + coreStartMock.http.post.mockResolvedValue(undefined); + + window.location.href = `https://some-host/security/access_agreement?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + findTestSubject(wrapper, 'accessAgreementAcknowledge').simulate('click'); + + await act(async () => { + await nextTick(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith( + '/internal/security/access_agreement/acknowledge' + ); + + expect(window.location.href).toBe('/some-base-path/app/kibana#/home?_g=()'); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + + it('shows error toast if acknowledgement fails', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + + const failureReason = new Error('Oh no!'); + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + coreStartMock.http.get.mockResolvedValue({ accessAgreement: 'This is [link](../link)' }); + coreStartMock.http.post.mockRejectedValue(failureReason); + + window.location.href = currentURL; + const wrapper = mountWithIntl( + <AccessAgreementPage + http={coreStartMock.http} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + findTestSubject(wrapper, 'accessAgreementAcknowledge').simulate('click'); + + await act(async () => { + await nextTick(); + }); + + expect(coreStartMock.http.post).toHaveBeenCalledTimes(1); + expect(coreStartMock.http.post).toHaveBeenCalledWith( + '/internal/security/access_agreement/acknowledge' + ); + + expect(window.location.href).toBe(currentURL); + expect(coreStartMock.notifications.toasts.addError).toHaveBeenCalledWith(failureReason, { + title: 'Could not acknowledge access agreement.', + }); + }); +}); diff --git a/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx new file mode 100644 index 0000000000000..a34dcb18d2b9c --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/access_agreement_page.tsx @@ -0,0 +1,133 @@ +/* + * 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 './access_agreement_page.scss'; + +import React, { FormEvent, MouseEvent, useCallback, useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; +import ReactMarkdown from 'react-markdown'; +import { + EuiButton, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart, FatalErrorsStart, HttpStart, NotificationsStart } from 'src/core/public'; + +import { parseNext } from '../../../common/parse_next'; +import { AuthenticationStatePage } from '../components'; + +interface Props { + http: HttpStart; + notifications: NotificationsStart; + fatalErrors: FatalErrorsStart; +} + +export function AccessAgreementPage({ http, fatalErrors, notifications }: Props) { + const [isLoading, setIsLoading] = useState<boolean>(false); + + const [accessAgreement, setAccessAgreement] = useState<string | null>(null); + useEffect(() => { + http + .get<{ accessAgreement: string }>('/internal/security/access_agreement/state') + .then(response => setAccessAgreement(response.accessAgreement)) + .catch(err => fatalErrors.add(err)); + }, [http, fatalErrors]); + + const onAcknowledge = useCallback( + async (e: MouseEvent<HTMLButtonElement> | FormEvent<HTMLFormElement>) => { + e.preventDefault(); + + try { + setIsLoading(true); + await http.post('/internal/security/access_agreement/acknowledge'); + window.location.href = parseNext(window.location.href, http.basePath.serverBasePath); + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.security.accessAgreement.acknowledgeErrorMessage', { + defaultMessage: 'Could not acknowledge access agreement.', + }), + }); + + setIsLoading(false); + } + }, + [http, notifications] + ); + + const content = accessAgreement ? ( + <form onSubmit={onAcknowledge}> + <EuiPanel paddingSize="none"> + <EuiFlexGroup gutterSize="none" direction="column"> + <EuiFlexItem className="secAccessAgreementPage__textWrapper"> + <div className="secAccessAgreementPage__text"> + <EuiText textAlign="left"> + <ReactMarkdown>{accessAgreement}</ReactMarkdown> + </EuiText> + </div> + </EuiFlexItem> + <EuiFlexItem className="secAccessAgreementPage__footer"> + <div className="secAccessAgreementPage__footerInner"> + <EuiButton + fill + type="submit" + color="primary" + onClick={onAcknowledge} + isDisabled={isLoading} + isLoading={isLoading} + data-test-subj="accessAgreementAcknowledge" + > + <FormattedMessage + id="xpack.security.accessAgreement.acknowledgeButtonText" + defaultMessage="Acknowledge and continue" + /> + </EuiButton> + </div> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </form> + ) : ( + <EuiPanel paddingSize="l"> + <EuiLoadingContent lines={10} /> + </EuiPanel> + ); + + return ( + <AuthenticationStatePage + className="secAccessAgreementPage" + title={ + <FormattedMessage + id="xpack.security.accessAgreement.title" + defaultMessage="Access Agreement" + /> + } + > + {content} + <EuiSpacer size="xxl" /> + </AuthenticationStatePage> + ); +} + +export function renderAccessAgreementPage( + i18nStart: CoreStart['i18n'], + element: Element, + props: Props +) { + ReactDOM.render( + <i18nStart.Context> + <AccessAgreementPage {...props} /> + </i18nStart.Context>, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +} diff --git a/x-pack/plugins/security/public/authentication/access_agreement/index.ts b/x-pack/plugins/security/public/authentication/access_agreement/index.ts new file mode 100644 index 0000000000000..8f7661a89a269 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/access_agreement/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { accessAgreementApp } from './access_agreement_app'; diff --git a/x-pack/plugins/security/public/authentication/authentication_service.ts b/x-pack/plugins/security/public/authentication/authentication_service.ts index 979f7095cf933..6657f5c0a900c 100644 --- a/x-pack/plugins/security/public/authentication/authentication_service.ts +++ b/x-pack/plugins/security/public/authentication/authentication_service.ts @@ -8,6 +8,7 @@ import { ApplicationSetup, StartServicesAccessor, HttpSetup } from 'src/core/pub import { AuthenticatedUser } from '../../common/model'; import { ConfigType } from '../config'; import { PluginStartDependencies } from '../plugin'; +import { accessAgreementApp } from './access_agreement'; import { loginApp } from './login'; import { logoutApp } from './logout'; import { loggedOutApp } from './logged_out'; @@ -25,6 +26,11 @@ export interface AuthenticationServiceSetup { * Returns currently authenticated user and throws if current user isn't authenticated. */ getCurrentUser: () => Promise<AuthenticatedUser>; + + /** + * Determines if API Keys are currently enabled. + */ + areAPIKeysEnabled: () => Promise<boolean>; } export class AuthenticationService { @@ -37,11 +43,16 @@ export class AuthenticationService { const getCurrentUser = async () => (await http.get('/internal/security/me', { asSystemRequest: true })) as AuthenticatedUser; + const areAPIKeysEnabled = async () => + ((await http.get('/internal/security/api_key/_enabled')) as { apiKeysEnabled: boolean }) + .apiKeysEnabled; + + accessAgreementApp.create({ application, getStartServices }); loginApp.create({ application, config, getStartServices, http }); logoutApp.create({ application, http }); loggedOutApp.create({ application, getStartServices, http }); overwrittenSessionApp.create({ application, authc: { getCurrentUser }, getStartServices }); - return { getCurrentUser }; + return { getCurrentUser, areAPIKeysEnabled }; } } diff --git a/x-pack/plugins/security/public/authentication/components/_index.scss b/x-pack/plugins/security/public/authentication/components/_index.scss deleted file mode 100644 index dfa258d523c5a..0000000000000 --- a/x-pack/plugins/security/public/authentication/components/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './authentication_state_page/index'; diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap index 3590fa460a401..585dc368da707 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/__snapshots__/authentication_state_page.test.tsx.snap @@ -2,7 +2,7 @@ exports[`AuthenticationStatePage renders 1`] = ` <div - className="secAuthenticationStatePage" + className="secAuthenticationStatePage " > <header className="secAuthenticationStatePage__header" @@ -18,7 +18,7 @@ exports[`AuthenticationStatePage renders 1`] = ` > <EuiIcon size="xxl" - type="logoKibana" + type="logoElastic" /> </span> <EuiTitle diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/_index.scss b/x-pack/plugins/security/public/authentication/components/authentication_state_page/_index.scss deleted file mode 100644 index f7cdd75143791..0000000000000 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './authentication_state_page'; diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/_authentication_state_page.scss b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss similarity index 100% rename from x-pack/plugins/security/public/authentication/components/authentication_state_page/_authentication_state_page.scss rename to x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.scss diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx index e1292c5b21536..946c58a1c8e99 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.test.tsx @@ -18,4 +18,14 @@ describe('AuthenticationStatePage', () => { ) ).toMatchSnapshot(); }); + + it('renders with custom CSS class', () => { + expect( + shallowWithIntl( + <AuthenticationStatePage className="customClassName" title={'foo'}> + <span>hello world</span> + </AuthenticationStatePage> + ).exists('.secAuthenticationStatePage.customClassName') + ).toBe(true); + }); }); diff --git a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx index aa30661129978..35be650d127fb 100644 --- a/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx +++ b/x-pack/plugins/security/public/authentication/components/authentication_state_page/authentication_state_page.tsx @@ -4,20 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import './authentication_state_page.scss'; + import { EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; interface Props { + className?: string; title: React.ReactNode; } export const AuthenticationStatePage: React.FC<Props> = props => ( - <div className="secAuthenticationStatePage"> + <div className={`secAuthenticationStatePage ${props.className || ''}`}> <header className="secAuthenticationStatePage__header"> <div className="secAuthenticationStatePage__content eui-textCenter"> <EuiSpacer size="xxl" /> <span className="secAuthenticationStatePage__logo"> - <EuiIcon type="logoKibana" size="xxl" /> + <EuiIcon type="logoElastic" size="xxl" /> </span> <EuiTitle size="l" className="secAuthenticationStatePage__title"> <h1>{props.title}</h1> diff --git a/x-pack/plugins/security/public/authentication/index.mock.ts b/x-pack/plugins/security/public/authentication/index.mock.ts index c8d77a5b62c6f..dee0a24ab27c2 100644 --- a/x-pack/plugins/security/public/authentication/index.mock.ts +++ b/x-pack/plugins/security/public/authentication/index.mock.ts @@ -9,5 +9,6 @@ import { AuthenticationServiceSetup } from './authentication_service'; export const authenticationMock = { createSetup: (): jest.Mocked<AuthenticationServiceSetup> => ({ getCurrentUser: jest.fn(), + areAPIKeysEnabled: jest.fn(), }), }; diff --git a/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap index ecbdfedac1dd3..bbc6bfa1faddc 100644 --- a/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/login/__snapshots__/login_page.test.tsx.snap @@ -121,10 +121,15 @@ exports[`LoginPage enabled form state renders as expected 1`] = ` selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> `; @@ -155,10 +160,15 @@ exports[`LoginPage enabled form state renders as expected when info message is s selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> `; @@ -189,10 +199,55 @@ exports[`LoginPage enabled form state renders as expected when loginAssistanceMe selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], + } + } +/> +`; + +exports[`LoginPage enabled form state renders as expected when loginHelp is set 1`] = ` +<LoginForm + http={ + Object { + "addLoadingCountSource": [MockFunction], + "get": [MockFunction], + } + } + infoMessage="Your session has timed out. Please log in again." + loginAssistanceMessage="" + loginHelp="**some-help**" + notifications={ + Object { + "toasts": Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + }, + } + } + selector={ + Object { + "enabled": false, + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> `; @@ -279,10 +334,15 @@ exports[`LoginPage page renders as expected 1`] = ` selector={ Object { "enabled": false, - "providers": Array [], + "providers": Array [ + Object { + "name": "basic1", + "type": "basic", + "usesLoginForm": true, + }, + ], } } - showLoginForm={true} /> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/security/public/authentication/login/_index.scss b/x-pack/plugins/security/public/authentication/login/_index.scss deleted file mode 100644 index 4dd2c0cabfb5e..0000000000000 --- a/x-pack/plugins/security/public/authentication/login/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './login_page'; diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap index 7b8283b7bec0e..072a025aa06a0 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/__snapshots__/login_form.test.tsx.snap @@ -1,170 +1,91 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LoginForm login selector renders as expected with login form 1`] = ` -<Fragment> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="saml1" - onClick={[Function]} - > - Login w/SAML - </EuiButton> - <EuiSpacer - size="m" - /> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="pki1" - onClick={[Function]} - > - Login w/PKI - </EuiButton> - <EuiSpacer - size="m" - /> - <EuiText - color="subdued" - textAlign="center" +exports[`LoginForm login selector properly switches to login form -> login help and back: Login Help 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" > - ―――   - <FormattedMessage - defaultMessage="OR" - id="xpack.security.loginPage.loginSelectorOR" - values={Object {}} - /> -   ――― - </EuiText> - <EuiSpacer - size="m" - /> - <EuiPanel> - <form - onSubmit={[Function]} + <p + key="paragraph-1-1" > - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={false} - hasChildLabel={true} - hasEmptyLabelSpace={false} - isInvalid={false} - label={ - <FormattedMessage - defaultMessage="Username" - id="xpack.security.login.basicLoginForm.usernameFormRowLabel" - values={Object {}} - /> - } - labelType="label" + <strong + key="strong-1-1" > - <EuiFieldText - aria-required={true} - data-test-subj="loginUsername" - disabled={false} - id="username" - inputRef={[Function]} - isInvalid={false} - name="username" - onChange={[Function]} - value="" - /> - </EuiFormRow> - <EuiFormRow - describedByIds={Array []} - display="row" - fullWidth={false} - hasChildLabel={true} - hasEmptyLabelSpace={false} - isInvalid={false} - label={ - <FormattedMessage - defaultMessage="Password" - id="xpack.security.login.basicLoginForm.passwordFormRowLabel" - values={Object {}} - /> - } - labelType="label" - > - <EuiFieldPassword - aria-required={true} - autoComplete="off" - compressed={false} - data-test-subj="loginPassword" - disabled={false} - fullWidth={false} - id="password" - isInvalid={false} - isLoading={false} - name="password" - onChange={[Function]} - value="" - /> - </EuiFormRow> - <EuiButton - color="primary" - data-test-subj="loginSubmit" - fill={true} - isDisabled={false} - isLoading={false} - onClick={[Function]} - type="submit" - > - <FormattedMessage - defaultMessage="Log in" - id="xpack.security.login.basicLoginForm.logInButtonLabel" - values={Object {}} - /> - </EuiButton> - </form> - </EuiPanel> -</Fragment> + some help + </strong> + </p> + </div> +</ReactMarkdown> `; -exports[`LoginForm login selector renders as expected without login form for providers with and without description 1`] = ` -<Fragment> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="saml1" - onClick={[Function]} +exports[`LoginForm login selector properly switches to login help: Login Help 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" > - Login w/SAML - </EuiButton> - <EuiSpacer - size="m" - /> - <EuiButton - fullWidth={true} - isDisabled={false} - isLoading={false} - key="pki1" - onClick={[Function]} + <p + key="paragraph-1-1" + > + <strong + key="strong-1-1" + > + some help + </strong> + </p> + </div> +</ReactMarkdown> +`; + +exports[`LoginForm properly switches to login help: Login Help 1`] = ` +<ReactMarkdown + astPlugins={Array []} + escapeHtml={true} + plugins={Array []} + rawSourcePos={false} + renderers={Object {}} + skipHtml={false} + sourcePos={false} + transformLinkUri={[Function]} +> + <div + key="root-1-1" > - <FormattedMessage - defaultMessage="Login with {providerType}/{providerName}" - id="xpack.security.loginPage.loginProviderDescription" - values={ - Object { - "providerName": "pki1", - "providerType": "pki", - } - } - /> - </EuiButton> - <EuiSpacer - size="m" - /> -</Fragment> + <p + key="paragraph-1-1" + > + <strong + key="strong-1-1" + > + some help + </strong> + </p> + </div> +</ReactMarkdown> `; exports[`LoginForm renders as expected 1`] = ` <Fragment> - <EuiPanel> + <EuiPanel + data-test-subj="loginForm" + > <form onSubmit={[Function]} > @@ -227,21 +148,32 @@ exports[`LoginForm renders as expected 1`] = ` value="" /> </EuiFormRow> - <EuiButton - color="primary" - data-test-subj="loginSubmit" - fill={true} - isDisabled={false} - isLoading={false} - onClick={[Function]} - type="submit" + <EuiSpacer /> + <EuiFlexGroup + alignItems="center" + gutterSize="s" + responsive={false} > - <FormattedMessage - defaultMessage="Log in" - id="xpack.security.login.basicLoginForm.logInButtonLabel" - values={Object {}} - /> - </EuiButton> + <EuiFlexItem + grow={false} + > + <EuiButton + color="primary" + data-test-subj="loginSubmit" + fill={true} + isDisabled={false} + isLoading={false} + onClick={[Function]} + type="submit" + > + <FormattedMessage + defaultMessage="Log in" + id="xpack.security.login.basicLoginForm.logInButtonLabel" + values={Object {}} + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> </form> </EuiPanel> </Fragment> diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss new file mode 100644 index 0000000000000..6784052ef4337 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.scss @@ -0,0 +1,55 @@ +.secLoginCard { + display: block; + box-shadow: none; + padding: $euiSize; + text-align: left; + width: 100%; + + &:hover { + .secLoginCard__title { + text-decoration: underline; + } + } + + &:disabled { + pointer-events: none; + } + + &:not(.secLoginCard-isLoading):disabled { + .secLoginCard__title, + .secLoginCard__hint { + color: $euiColorMediumShade; + } + } + + &:focus { + border-color: transparent; + border-radius: $euiBorderRadius; + @include euiFocusRing; + + .secLoginCard__title { + text-decoration: underline; + } + + // Make the focus ring clean and without borders + + .secLoginCard { + border-color: transparent; + } + } + + + .secLoginCard { + border-top: $euiBorderThin; + } +} + +.secLoginCard__hint { + @include euiFontSizeXS; + color: $euiColorDarkShade; + margin-top: $euiSizeXS; +} + +.secLoginAssistanceMessage { + // This tightens up the layout if message is present + margin-top: -($euiSizeXXL + $euiSizeS); + padding: 0 $euiSize; +} diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx index c17c10a2c5148..4e172cdde0eed 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.test.tsx @@ -5,12 +5,39 @@ */ import React from 'react'; +import ReactMarkdown from 'react-markdown'; import { act } from '@testing-library/react'; -import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiIcon } from '@elastic/eui'; import { mountWithIntl, nextTick, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { LoginForm } from './login_form'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { LoginForm, PageMode } from './login_form'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { ReactWrapper } from 'enzyme'; + +function expectPageMode(wrapper: ReactWrapper, mode: PageMode) { + const assertions: Array<[string, boolean]> = + mode === PageMode.Form + ? [ + ['loginForm', true], + ['loginSelector', false], + ['loginHelp', false], + ] + : mode === PageMode.Selector + ? [ + ['loginForm', false], + ['loginSelector', true], + ['loginHelp', false], + ] + : [ + ['loginForm', false], + ['loginSelector', false], + ['loginHelp', true], + ]; + for (const [selector, exists] of assertions) { + expect(findTestSubject(wrapper, selector).exists()).toBe(exists); + } +} describe('LoginForm', () => { beforeAll(() => { @@ -32,8 +59,10 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ) ).toMatchSnapshot(); @@ -41,20 +70,44 @@ describe('LoginForm', () => { it('renders an info message when provided.', () => { const coreStartMock = coreMock.createStart(); - const wrapper = shallowWithIntl( + const wrapper = mountWithIntl( <LoginForm http={coreStartMock.http} notifications={coreStartMock.notifications} infoMessage={'Hey this is an info message'} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + expect(wrapper.find(EuiCallOut).props().title).toEqual('Hey this is an info message'); }); + it('renders `Need help?` link if login help text is provided.', () => { + const coreStartMock = coreMock.createStart(); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginHelp={'**Hey this is a login help message**'} + loginAssistanceMessage="" + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Form); + + expect(findTestSubject(wrapper, 'loginHelpLink').text()).toEqual('Need help?'); + }); + it('renders an invalid credentials message', async () => { const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); coreStartMock.http.post.mockRejectedValue({ response: { status: 401 } }); @@ -64,11 +117,15 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } }); wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } }); wrapper.find(EuiButton).simulate('click'); @@ -92,11 +149,15 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username' } }); wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password' } }); wrapper.find(EuiButton).simulate('click'); @@ -121,11 +182,15 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: false, providers: [] }} + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} /> ); + expectPageMode(wrapper, PageMode.Form); + wrapper.find('input[name="username"]').simulate('change', { target: { value: 'username1' } }); wrapper.find('input[name="password"]').simulate('change', { target: { value: 'password1' } }); wrapper.find(EuiButton).simulate('click'); @@ -144,47 +209,125 @@ describe('LoginForm', () => { expect(wrapper.find(EuiCallOut).exists()).toBe(false); }); + it('properly switches to login help', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + loginHelp="**some help**" + selector={{ + enabled: false, + providers: [{ type: 'basic', name: 'basic', usesLoginForm: true }], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Form); + expect(findTestSubject(wrapper, 'loginBackToSelector').exists()).toBe(false); + + // Going to login help. + findTestSubject(wrapper, 'loginHelpLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.LoginHelp); + + expect(findTestSubject(wrapper, 'loginHelp').find(ReactMarkdown)).toMatchSnapshot('Login Help'); + + // Going back to login form. + findTestSubject(wrapper, 'loginBackToLoginLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + expect(findTestSubject(wrapper, 'loginBackToSelector').exists()).toBe(false); + }); + describe('login selector', () => { - it('renders as expected with login form', async () => { + it('renders as expected with providers that use login form', async () => { const coreStartMock = coreMock.createStart(); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + selector={{ + enabled: true, + providers: [ + { + type: 'basic', + name: 'basic', + usesLoginForm: true, + hint: 'Basic hint', + icon: 'logoElastic', + }, + { type: 'saml', name: 'saml1', description: 'Log in w/SAML', usesLoginForm: false }, + { + type: 'pki', + name: 'pki1', + description: 'Log in w/PKI', + hint: 'PKI hint', + usesLoginForm: false, + }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + expect( - shallowWithIntl( - <LoginForm - http={coreStartMock.http} - notifications={coreStartMock.notifications} - loginAssistanceMessage="" - showLoginForm={true} - selector={{ - enabled: true, - providers: [ - { type: 'saml', name: 'saml1', description: 'Login w/SAML' }, - { type: 'pki', name: 'pki1', description: 'Login w/PKI' }, - ], - }} - /> - ) - ).toMatchSnapshot(); + wrapper.find('.secLoginCard').map(card => { + const hint = card.find('.secLoginCard__hint'); + return { + title: card.find('p.secLoginCard__title').text(), + hint: hint.exists() ? hint.text() : '', + icon: card.find(EuiIcon).props().type, + }; + }) + ).toEqual([ + { title: 'Log in with basic/basic', hint: 'Basic hint', icon: 'logoElastic' }, + { title: 'Log in w/SAML', hint: '', icon: 'empty' }, + { title: 'Log in w/PKI', hint: 'PKI hint', icon: 'empty' }, + ]); }); - it('renders as expected without login form for providers with and without description', async () => { + it('renders as expected without providers that use login form', async () => { const coreStartMock = coreMock.createStart(); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + selector={{ + enabled: true, + providers: [ + { + type: 'saml', + name: 'saml1', + description: 'Login w/SAML', + hint: 'SAML hint', + usesLoginForm: false, + }, + { type: 'pki', name: 'pki1', icon: 'some-icon', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + expect( - shallowWithIntl( - <LoginForm - http={coreStartMock.http} - notifications={coreStartMock.notifications} - loginAssistanceMessage="" - showLoginForm={false} - selector={{ - enabled: true, - providers: [ - { type: 'saml', name: 'saml1', description: 'Login w/SAML' }, - { type: 'pki', name: 'pki1' }, - ], - }} - /> - ) - ).toMatchSnapshot(); + wrapper.find('.secLoginCard').map(card => { + const hint = card.find('.secLoginCard__hint'); + return { + title: card.find('p.secLoginCard__title').text(), + hint: hint.exists() ? hint.text() : '', + icon: card.find(EuiIcon).props().type, + }; + }) + ).toEqual([ + { title: 'Login w/SAML', hint: 'SAML hint', icon: 'empty' }, + { title: 'Log in with pki/pki1', hint: '', icon: 'some-icon' }, + ]); }); it('properly redirects after successful login', async () => { @@ -203,17 +346,19 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} selector={{ enabled: true, providers: [ - { type: 'saml', name: 'saml1', description: 'Login w/SAML' }, - { type: 'pki', name: 'pki1', description: 'Login w/PKI' }, + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', description: 'Login w/SAML', usesLoginForm: false }, + { type: 'pki', name: 'pki1', description: 'Login w/PKI', usesLoginForm: false }, ], }} /> ); + expectPageMode(wrapper, PageMode.Selector); + wrapper.findWhere(node => node.key() === 'saml1').simulate('click'); await act(async () => { @@ -246,11 +391,18 @@ describe('LoginForm', () => { http={coreStartMock.http} notifications={coreStartMock.notifications} loginAssistanceMessage="" - showLoginForm={true} - selector={{ enabled: true, providers: [{ type: 'saml', name: 'saml1' }] }} + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} /> ); + expectPageMode(wrapper, PageMode.Selector); + wrapper.findWhere(node => node.key() === 'saml1').simulate('click'); await act(async () => { @@ -268,5 +420,123 @@ describe('LoginForm', () => { title: 'Could not perform login.', }); }); + + it('properly switches to login form', async () => { + const currentURL = `https://some-host/login?next=${encodeURIComponent( + '/some-base-path/app/kibana#/home?_g=()' + )}`; + + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + window.location.href = currentURL; + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + + wrapper.findWhere(node => node.key() === 'basic').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + + expect(coreStartMock.http.post).not.toHaveBeenCalled(); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + expect(window.location.href).toBe(currentURL); + }); + + it('properly switches to login help', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + loginHelp="**some help**" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + + findTestSubject(wrapper, 'loginHelpLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.LoginHelp); + + expect(findTestSubject(wrapper, 'loginHelp').find(ReactMarkdown)).toMatchSnapshot( + 'Login Help' + ); + + // Going back to login selector. + findTestSubject(wrapper, 'loginBackToLoginLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Selector); + + expect(coreStartMock.http.post).not.toHaveBeenCalled(); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); + + it('properly switches to login form -> login help and back', async () => { + const coreStartMock = coreMock.createStart({ basePath: '/some-base-path' }); + const wrapper = mountWithIntl( + <LoginForm + http={coreStartMock.http} + notifications={coreStartMock.notifications} + loginAssistanceMessage="" + loginHelp="**some help**" + selector={{ + enabled: true, + providers: [ + { type: 'basic', name: 'basic', usesLoginForm: true }, + { type: 'saml', name: 'saml1', usesLoginForm: false }, + ], + }} + /> + ); + + expectPageMode(wrapper, PageMode.Selector); + + // Going to login form. + wrapper.findWhere(node => node.key() === 'basic').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + + // Going to login help. + findTestSubject(wrapper, 'loginHelpLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.LoginHelp); + + expect(findTestSubject(wrapper, 'loginHelp').find(ReactMarkdown)).toMatchSnapshot( + 'Login Help' + ); + + // Going back to login form. + findTestSubject(wrapper, 'loginBackToLoginLink').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Form); + + // Going back to login selector. + findTestSubject(wrapper, 'loginBackToSelector').simulate('click'); + wrapper.update(); + expectPageMode(wrapper, PageMode.Selector); + + expect(coreStartMock.http.post).not.toHaveBeenCalled(); + expect(coreStartMock.notifications.toasts.addError).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index 01f5c40a69aeb..460c6550085a4 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import './login_form.scss'; + import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; import ReactMarkdown from 'react-markdown'; import { EuiButton, + EuiIcon, EuiCallOut, EuiFieldPassword, EuiFieldText, @@ -15,21 +18,28 @@ import { EuiPanel, EuiSpacer, EuiText, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiLoadingSpinner, + EuiLink, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public'; -import { LoginValidator, LoginValidationResult } from './validate_login'; import { parseNext } from '../../../../../common/parse_next'; import { LoginSelector } from '../../../../../common/login_state'; +import { LoginValidator } from './validate_login'; interface Props { http: HttpStart; notifications: NotificationsStart; selector: LoginSelector; - showLoginForm: boolean; infoMessage?: string; loginAssistanceMessage: string; + loginHelp?: string; } interface State { @@ -42,7 +52,8 @@ interface State { message: | { type: MessageType.None } | { type: MessageType.Danger | MessageType.Info; content: string }; - formError: LoginValidationResult | null; + mode: PageMode; + previousMode: PageMode; } enum LoadingStateType { @@ -57,12 +68,21 @@ enum MessageType { Danger, } +export enum PageMode { + Selector, + Form, + LoginHelp, +} + export class LoginForm extends Component<Props, State> { private readonly validator: LoginValidator; constructor(props: Props) { super(props); this.validator = new LoginValidator({ shouldValidate: false }); + + const mode = this.showLoginSelector() ? PageMode.Selector : PageMode.Form; + this.state = { loadingState: { type: LoadingStateType.None }, username: '', @@ -70,7 +90,8 @@ export class LoginForm extends Component<Props, State> { message: this.props.infoMessage ? { type: MessageType.Info, content: this.props.infoMessage } : { type: MessageType.None }, - formError: null, + mode, + previousMode: mode, }; } @@ -79,19 +100,91 @@ export class LoginForm extends Component<Props, State> { <Fragment> {this.renderLoginAssistanceMessage()} {this.renderMessage()} - {this.renderSelector()} - {this.renderLoginForm()} + {this.renderContent()} + {this.renderPageModeSwitchLink()} </Fragment> ); } - private renderLoginForm = () => { - if (!this.props.showLoginForm) { + private renderLoginAssistanceMessage = () => { + if (!this.props.loginAssistanceMessage) { return null; } return ( - <EuiPanel> + <div className="secLoginAssistanceMessage"> + <EuiHorizontalRule size="half" /> + <EuiText size="xs"> + <ReactMarkdown>{this.props.loginAssistanceMessage}</ReactMarkdown> + </EuiText> + </div> + ); + }; + + private renderMessage = () => { + const { message } = this.state; + if (message.type === MessageType.Danger) { + return ( + <Fragment> + <EuiCallOut + size="s" + color="danger" + data-test-subj="loginErrorMessage" + title={message.content} + role="alert" + /> + <EuiSpacer size="l" /> + </Fragment> + ); + } + + if (message.type === MessageType.Info) { + return ( + <Fragment> + <EuiCallOut + size="s" + color="primary" + data-test-subj="loginInfoMessage" + title={message.content} + role="status" + /> + <EuiSpacer size="l" /> + </Fragment> + ); + } + + return null; + }; + + public renderContent() { + switch (this.state.mode) { + case PageMode.Form: + return this.renderLoginForm(); + case PageMode.Selector: + return this.renderSelector(); + case PageMode.LoginHelp: + return this.renderLoginHelp(); + } + } + + private renderLoginForm = () => { + const loginSelectorLink = this.showLoginSelector() ? ( + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="loginBackToSelector" + size="xs" + onClick={() => this.onPageModeChange(PageMode.Selector)} + > + <FormattedMessage + id="xpack.security.loginPage.loginSelectorLinkText" + defaultMessage="See more login options" + /> + </EuiButtonEmpty> + </EuiFlexItem> + ) : null; + + return ( + <EuiPanel data-test-subj="loginForm"> <form onSubmit={this.submitLoginForm}> <EuiFormRow label={ @@ -137,67 +230,128 @@ export class LoginForm extends Component<Props, State> { /> </EuiFormRow> - <EuiButton - fill - type="submit" - color="primary" - onClick={this.submitLoginForm} - isDisabled={!this.isLoadingState(LoadingStateType.None)} - isLoading={this.isLoadingState(LoadingStateType.Form)} - data-test-subj="loginSubmit" - > - <FormattedMessage - id="xpack.security.login.basicLoginForm.logInButtonLabel" - defaultMessage="Log in" - /> - </EuiButton> + <EuiSpacer /> + + <EuiFlexGroup responsive={false} alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButton + fill + type="submit" + color="primary" + onClick={this.submitLoginForm} + isDisabled={!this.isLoadingState(LoadingStateType.None)} + isLoading={this.isLoadingState(LoadingStateType.Form)} + data-test-subj="loginSubmit" + > + <FormattedMessage + id="xpack.security.login.basicLoginForm.logInButtonLabel" + defaultMessage="Log in" + /> + </EuiButton> + </EuiFlexItem> + {loginSelectorLink} + </EuiFlexGroup> </form> </EuiPanel> ); }; - private renderLoginAssistanceMessage = () => { - if (!this.props.loginAssistanceMessage) { - return null; - } + private renderSelector = () => { + return ( + <EuiPanel data-test-subj="loginSelector" paddingSize="none"> + {this.props.selector.providers.map(provider => ( + <button + key={provider.name} + disabled={!this.isLoadingState(LoadingStateType.None)} + onClick={() => + provider.usesLoginForm + ? this.onPageModeChange(PageMode.Form) + : this.loginWithSelector(provider.type, provider.name) + } + className={`secLoginCard ${ + this.isLoadingState(LoadingStateType.Selector, provider.name) + ? 'secLoginCard-isLoading' + : '' + }`} + > + <EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}> + <EuiFlexItem grow={false}> + <EuiIcon size="xl" type={provider.icon ? provider.icon : 'empty'} /> + </EuiFlexItem> + <EuiFlexItem> + <EuiTitle size="xs" className="secLoginCard__title"> + <p> + {provider.description ?? ( + <FormattedMessage + id="xpack.security.loginPage.loginProviderDescription" + defaultMessage="Log in with {providerType}/{providerName}" + values={{ + providerType: provider.type, + providerName: provider.name, + }} + /> + )} + </p> + </EuiTitle> + {provider.hint ? <p className="secLoginCard__hint">{provider.hint}</p> : null} + </EuiFlexItem> + {this.isLoadingState(LoadingStateType.Selector, provider.name) ? ( + <EuiFlexItem grow={false}> + <EuiLoadingSpinner size="m" /> + </EuiFlexItem> + ) : null} + </EuiFlexGroup> + </button> + ))} + </EuiPanel> + ); + }; + private renderLoginHelp = () => { return ( - <Fragment> - <EuiText size="s"> - <ReactMarkdown>{this.props.loginAssistanceMessage}</ReactMarkdown> + <EuiPanel data-test-subj="loginHelp"> + <EuiText> + <ReactMarkdown>{this.props.loginHelp || ''}</ReactMarkdown> </EuiText> - </Fragment> + </EuiPanel> ); }; - private renderMessage = () => { - const { message } = this.state; - if (message.type === MessageType.Danger) { + private renderPageModeSwitchLink = () => { + if (this.state.mode === PageMode.LoginHelp) { return ( <Fragment> - <EuiCallOut - size="s" - color="danger" - data-test-subj="loginErrorMessage" - title={message.content} - role="alert" - /> - <EuiSpacer size="l" /> + <EuiSpacer /> + <EuiText size="xs" className="eui-textCenter"> + <EuiLink + data-test-subj="loginBackToLoginLink" + onClick={() => this.onPageModeChange(this.state.previousMode)} + > + <FormattedMessage + id="xpack.security.loginPage.goBackToLoginLink" + defaultMessage="Take me back to Login" + /> + </EuiLink> + </EuiText> </Fragment> ); } - if (message.type === MessageType.Info) { + if (this.props.loginHelp) { return ( <Fragment> - <EuiCallOut - size="s" - color="primary" - data-test-subj="loginInfoMessage" - title={message.content} - role="status" - /> - <EuiSpacer size="l" /> + <EuiSpacer /> + <EuiText size="xs" className="eui-textCenter"> + <EuiLink + data-test-subj="loginHelpLink" + onClick={() => this.onPageModeChange(PageMode.LoginHelp)} + > + <FormattedMessage + id="xpack.security.loginPage.loginHelpLinkText" + defaultMessage="Need help?" + /> + </EuiLink> + </EuiText> </Fragment> ); } @@ -205,60 +359,16 @@ export class LoginForm extends Component<Props, State> { return null; }; - private renderSelector = () => { - const showLoginSelector = - this.props.selector.enabled && this.props.selector.providers.length > 0; - if (!showLoginSelector) { - return null; - } - - const loginSelectorAndLoginFormSeparator = showLoginSelector && this.props.showLoginForm && ( - <> - <EuiText textAlign="center" color="subdued"> - ―――   - <FormattedMessage id="xpack.security.loginPage.loginSelectorOR" defaultMessage="OR" /> -   ――― - </EuiText> - <EuiSpacer size="m" /> - </> - ); - - return ( - <> - {this.props.selector.providers.map((provider, index) => ( - <Fragment key={index}> - <EuiButton - key={provider.name} - fullWidth={true} - isDisabled={!this.isLoadingState(LoadingStateType.None)} - isLoading={this.isLoadingState(LoadingStateType.Selector, provider.name)} - onClick={() => this.loginWithSelector(provider.type, provider.name)} - > - {provider.description ?? ( - <FormattedMessage - id="xpack.security.loginPage.loginProviderDescription" - defaultMessage="Login with {providerType}/{providerName}" - values={{ - providerType: provider.type, - providerName: provider.name, - }} - /> - )} - </EuiButton> - <EuiSpacer size="m" /> - </Fragment> - ))} - {loginSelectorAndLoginFormSeparator} - </> - ); - }; - private setUsernameInputRef(ref: HTMLInputElement) { if (ref) { ref.focus(); } } + private onPageModeChange = (mode: PageMode) => { + this.setState({ message: { type: MessageType.None }, mode, previousMode: this.state.mode }); + }; + private onUsernameChange = (e: ChangeEvent<HTMLInputElement>) => { this.setState({ username: e.target.value, @@ -279,12 +389,10 @@ export class LoginForm extends Component<Props, State> { this.validator.enableValidation(); const { username, password } = this.state; - const result = this.validator.validateForLogin(username, password); - if (result.isInvalid) { - this.setState({ formError: result }); - return; - } else { - this.setState({ formError: null }); + if (this.validator.validateForLogin(username, password).isInvalid) { + // Since validation is enabled now, we should ask React to re-render form and display + // validation error messages if any. + return this.forceUpdate(); } this.setState({ @@ -351,4 +459,11 @@ export class LoginForm extends Component<Props, State> { loadingState.type !== LoadingStateType.Selector || loadingState.providerName === providerName ); } + + private showLoginSelector() { + return ( + this.props.selector.enabled && + this.props.selector.providers.some(provider => !provider.usesLoginForm) + ); + } } diff --git a/x-pack/plugins/security/public/authentication/login/_login_page.scss b/x-pack/plugins/security/public/authentication/login/login_page.scss similarity index 100% rename from x-pack/plugins/security/public/authentication/login/_login_page.scss rename to x-pack/plugins/security/public/authentication/login/login_page.scss diff --git a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx index c4be57d8d7db7..ab107e46dfff6 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.test.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.test.tsx @@ -18,8 +18,10 @@ const createLoginState = (options?: Partial<LoginState>) => { allowLogin: true, layout: 'form', requiresSecureConnection: false, - showLoginForm: true, - selector: { enabled: false, providers: [] }, + selector: { + enabled: false, + providers: [{ type: 'basic', name: 'basic1', usesLoginForm: true }], + }, ...options, } as LoginState; }; @@ -163,7 +165,9 @@ describe('LoginPage', () => { it('renders as expected when login is not enabled', async () => { const coreStartMock = coreMock.createStart(); - httpMock.get.mockResolvedValue(createLoginState({ showLoginForm: false })); + httpMock.get.mockResolvedValue( + createLoginState({ selector: { enabled: false, providers: [] } }) + ); const wrapper = shallow( <LoginPage @@ -250,6 +254,28 @@ describe('LoginPage', () => { expect(wrapper.find(LoginForm)).toMatchSnapshot(); }); + + it('renders as expected when loginHelp is set', async () => { + const coreStartMock = coreMock.createStart(); + httpMock.get.mockResolvedValue(createLoginState({ loginHelp: '**some-help**' })); + + const wrapper = shallow( + <LoginPage + http={httpMock} + notifications={coreStartMock.notifications} + fatalErrors={coreStartMock.fatalErrors} + loginAssistanceMessage="" + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + resetHttpMock(); // so the calls don't show in the BasicLoginForm snapshot + }); + + expect(wrapper.find(LoginForm)).toMatchSnapshot(); + }); }); describe('API calls', () => { diff --git a/x-pack/plugins/security/public/authentication/login/login_page.tsx b/x-pack/plugins/security/public/authentication/login/login_page.tsx index 70f8f76ee0a9c..d24a301ed24ec 100644 --- a/x-pack/plugins/security/public/authentication/login/login_page.tsx +++ b/x-pack/plugins/security/public/authentication/login/login_page.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './login_page.scss'; + import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; @@ -120,10 +122,9 @@ export class LoginPage extends Component<Props, State> { requiresSecureConnection, isSecureConnection, selector, - showLoginForm, + loginHelp, }: LoginState & { isSecureConnection: boolean }) => { - const isLoginExplicitlyDisabled = - !showLoginForm && (!selector.enabled || selector.providers.length === 0); + const isLoginExplicitlyDisabled = selector.providers.length === 0; if (isLoginExplicitlyDisabled) { return ( <DisabledLoginForm @@ -223,10 +224,10 @@ export class LoginPage extends Component<Props, State> { <LoginForm http={this.props.http} notifications={this.props.notifications} - showLoginForm={showLoginForm} selector={selector} infoMessage={infoMessageMap.get(parse(window.location.href, true).query.msg?.toString())} loginAssistanceMessage={this.props.loginAssistanceMessage} + loginHelp={loginHelp} /> ); }; diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap index 2ff760891fa4e..02b1a7d0d3fa0 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap +++ b/x-pack/plugins/security/public/authentication/overwritten_session/__snapshots__/overwritten_session_page.test.tsx.snap @@ -11,7 +11,7 @@ exports[`OverwrittenSessionPage renders as expected 1`] = ` } > <div - className="secAuthenticationStatePage" + className="secAuthenticationStatePage " > <header className="secAuthenticationStatePage__header" @@ -31,10 +31,10 @@ exports[`OverwrittenSessionPage renders as expected 1`] = ` > <EuiIcon size="xxl" - type="logoKibana" + type="logoElastic" > <div - data-euiicon-type="logoKibana" + data-euiicon-type="logoElastic" size="xxl" /> </EuiIcon> diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts index 8e0ee73dfb613..213c26d5287dc 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts @@ -10,7 +10,7 @@ import { AuthenticationServiceSetup } from '../authentication_service'; interface CreateDeps { application: ApplicationSetup; - authc: AuthenticationServiceSetup; + authc: Pick<AuthenticationServiceSetup, 'getCurrentUser'>; getStartServices: StartServicesAccessor; } diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx index 1093957761d1c..5b77266068ebf 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx @@ -14,7 +14,7 @@ import { AuthenticationStatePage } from '../components'; interface Props { basePath: IBasePath; - authc: AuthenticationServiceSetup; + authc: Pick<AuthenticationServiceSetup, 'getCurrentUser'>; } export function OverwrittenSessionPage({ authc, basePath }: Props) { diff --git a/x-pack/plugins/security/public/index.scss b/x-pack/plugins/security/public/index.scss deleted file mode 100644 index 999639ba22eb7..0000000000000 --- a/x-pack/plugins/security/public/index.scss +++ /dev/null @@ -1,7 +0,0 @@ -$secFormWidth: 460px; - -// Authentication styles -@import './authentication/index'; - -// Management styles -@import './management/index'; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index 458f7ab801fdf..8016c94224060 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import './index.scss'; import { PluginInitializer, PluginInitializerContext } from 'src/core/public'; import { SecurityPlugin, @@ -15,7 +14,6 @@ import { } from './plugin'; export { SecurityPluginSetup, SecurityPluginStart }; -export { SessionInfo } from './types'; export { AuthenticatedUser } from '../common/model'; export { SecurityLicense, SecurityLicenseFeatures } from '../common/licensing'; diff --git a/x-pack/plugins/security/public/management/_index.scss b/x-pack/plugins/security/public/management/_index.scss deleted file mode 100644 index 5d419b5323079..0000000000000 --- a/x-pack/plugins/security/public/management/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './roles/index'; -@import './users/index'; -@import './role_mappings/index'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts index 372b1e56a73c4..a127379d97241 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -10,6 +10,7 @@ import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; isAdmin: boolean; + canManage: boolean; } interface InvalidateApiKeysResponse { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index ae6ef4aa0fc34..dea04a0eac396 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -18,7 +18,6 @@ import { APIKeysGridPage } from './api_keys_grid_page'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { apiKeysAPIClientMock } from '../index.mock'; -const mock403 = () => ({ body: { statusCode: 403 } }); const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( @@ -48,6 +47,7 @@ describe('APIKeysGridPage', () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, areApiKeysEnabled: true, + canManage: true, }); apiClientMock.getApiKeys.mockResolvedValue({ apiKeys: [ @@ -82,6 +82,7 @@ describe('APIKeysGridPage', () => { it('renders a callout when API keys are not enabled', async () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, + canManage: true, areApiKeysEnabled: false, }); @@ -95,7 +96,11 @@ describe('APIKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + apiClientMock.checkPrivileges.mockResolvedValue({ + canManage: false, + isAdmin: false, + areApiKeysEnabled: true, + }); const wrapper = mountWithIntl(<APIKeysGridPage {...getViewProperties()} />); @@ -152,6 +157,7 @@ describe('APIKeysGridPage', () => { beforeEach(() => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: false, + canManage: true, areApiKeysEnabled: true, }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 698c0d37dbc64..9db09a34d3c3f 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -26,7 +26,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; -import _ from 'lodash'; import { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; @@ -47,10 +46,10 @@ interface State { isLoadingApp: boolean; isLoadingTable: boolean; isAdmin: boolean; + canManage: boolean; areApiKeysEnabled: boolean; apiKeys: ApiKey[]; selectedItems: ApiKey[]; - permissionDenied: boolean; error: any; } @@ -63,9 +62,9 @@ export class APIKeysGridPage extends Component<Props, State> { isLoadingApp: true, isLoadingTable: false, isAdmin: false, + canManage: false, areApiKeysEnabled: false, apiKeys: [], - permissionDenied: false, selectedItems: [], error: undefined, }; @@ -77,19 +76,15 @@ export class APIKeysGridPage extends Component<Props, State> { public render() { const { - permissionDenied, isLoadingApp, isLoadingTable, areApiKeysEnabled, isAdmin, + canManage, error, apiKeys, } = this.state; - if (permissionDenied) { - return <PermissionDenied />; - } - if (isLoadingApp) { return ( <EuiPageContent> @@ -103,6 +98,10 @@ export class APIKeysGridPage extends Component<Props, State> { ); } + if (!canManage) { + return <PermissionDenied />; + } + if (error) { const { body: { error: errorTitle, message, statusCode }, @@ -495,26 +494,25 @@ export class APIKeysGridPage extends Component<Props, State> { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); - this.setState({ isAdmin, areApiKeysEnabled }); + const { + isAdmin, + canManage, + areApiKeysEnabled, + } = await this.props.apiKeysAPIClient.checkPrivileges(); + this.setState({ isAdmin, canManage, areApiKeysEnabled }); - if (areApiKeysEnabled) { - this.initiallyLoadApiKeys(); - } else { - // We're done loading and will just show the "Disabled" error. + if (!canManage || !areApiKeysEnabled) { this.setState({ isLoadingApp: false }); - } - } catch (e) { - if (_.get(e, 'body.statusCode') === 403) { - this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - }) - ); + this.initiallyLoadApiKeys(); } + } catch (e) { + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: e.body?.message ?? '' }, + }) + ); } } diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index 272fc9cfc2fe6..b9ec5b35b3f9d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -10,8 +10,6 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { PluginStartDependencies } from '../../plugin'; -import { APIKeysGridPage } from './api_keys_grid'; -import { APIKeysAPIClient } from './api_keys_api_client'; import { DocumentationLinksService } from './documentation_links'; interface CreateParams { @@ -28,7 +26,6 @@ export const apiKeysManagementApp = Object.freeze({ defaultMessage: 'API Keys', }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); setBreadcrumbs([ { text: i18n.translate('xpack.security.apiKeys.breadcrumb', { @@ -38,6 +35,16 @@ export const apiKeysManagementApp = Object.freeze({ }, ]); + const [ + [{ docLinks, http, notifications, i18n: i18nStart }], + { APIKeysGridPage }, + { APIKeysAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./api_keys_grid'), + import('./api_keys_api_client'), + ]); + render( <i18nStart.Context> <APIKeysGridPage diff --git a/x-pack/plugins/security/public/management/role_mappings/_index.scss b/x-pack/plugins/security/public/management/role_mappings/_index.scss deleted file mode 100644 index bae6effcd2ec5..0000000000000 --- a/x-pack/plugins/security/public/management/role_mappings/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_role_mapping/index'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/_index.scss deleted file mode 100644 index 3f240b8a2b2a2..0000000000000 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './rule_editor_panel/index'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss deleted file mode 100644 index c3b2764e64713..0000000000000 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './rule_editor_group'; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.scss similarity index 100% rename from x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.scss diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx index c17a853a65467..b10a6dd8d183f 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './rule_group_editor.scss'; + import React, { Component, Fragment } from 'react'; import { EuiPanel, diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index ea090520fdd46..ffb6d6d98f180 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -11,11 +11,7 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { PluginStartDependencies } from '../../plugin'; -import { RolesAPIClient } from '../roles'; -import { RoleMappingsAPIClient } from './role_mappings_api_client'; import { DocumentationLinksService } from './documentation_links'; -import { RoleMappingsGridPage } from './role_mappings_grid'; -import { EditRoleMappingPage } from './edit_role_mapping'; interface CreateParams { getStartServices: StartServicesAccessor<PluginStartDependencies>; @@ -31,7 +27,6 @@ export const roleMappingsManagementApp = Object.freeze({ defaultMessage: 'Role Mappings', }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); const roleMappingsBreadcrumbs = [ { text: i18n.translate('xpack.security.roleMapping.breadcrumb', { @@ -41,6 +36,20 @@ export const roleMappingsManagementApp = Object.freeze({ }, ]; + const [ + [{ docLinks, http, notifications, i18n: i18nStart }], + { RoleMappingsGridPage }, + { EditRoleMappingPage }, + { RoleMappingsAPIClient }, + { RolesAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./role_mappings_grid'), + import('./edit_role_mapping'), + import('./role_mappings_api_client'), + import('../roles'), + ]); + const roleMappingsAPIClient = new RoleMappingsAPIClient(http); const dockLinksService = new DocumentationLinksService(docLinks); const RoleMappingsGridPageWithBreadcrumbs = () => { diff --git a/x-pack/plugins/security/public/management/roles/_index.scss b/x-pack/plugins/security/public/management/roles/_index.scss deleted file mode 100644 index 5256c79f01f10..0000000000000 --- a/x-pack/plugins/security/public/management/roles/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss deleted file mode 100644 index 0153b1734ceba..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './collapsible_panel/index'; -@import './spaces_popover_list/index'; -@import './privileges/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss deleted file mode 100644 index c0f4f8ab9a870..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './collapsible_panel'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx index 01af7cb4509f6..eb1417600e19b 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './collapsible_panel.scss'; + import { EuiFlexGroup, EuiFlexItem, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss deleted file mode 100644 index a1a9d038065e6..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './privilege_feature_icon'; -@import './kibana/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap index a52438ca93638..dd27fe13e84a3 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap @@ -29,6 +29,7 @@ exports[`it renders without crashing 1`] = ` } selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index b306de5b84093..46fb9b8572679 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -119,6 +119,7 @@ exports[`it renders without crashing 1`] = ` placeholder="Add a user…" selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> </EuiFormRow> </EuiDescribedFormGroup> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap index bbf90d0f64bd2..d23a6da13a3bb 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap @@ -37,6 +37,7 @@ exports[`it renders without crashing 1`] = ` options={Array []} selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> </EuiFormRow> </EuiFlexItem> @@ -82,6 +83,7 @@ exports[`it renders without crashing 1`] = ` } selectedOptions={Array []} singleSelection={false} + sortMatchesBy="none" /> </EuiFormRow> </EuiFlexItem> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss deleted file mode 100644 index 19547c0e1953e..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import './feature_table/index'; -@import './space_aware_privilege_section/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss deleted file mode 100644 index 6a96553742819..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './change_all_privileges'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx index 2083778e53998..5d7b13acf79da 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './change_all_privileges.scss'; + import { EuiContextMenuItem, EuiContextMenuPanel, EuiLink, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx index 9e4a3a8a99b56..77445952f3d69 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table_cell/feature_table_cell.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './feature_table_cell.scss'; + import React from 'react'; import { EuiText, EuiIconTip, EuiIcon, IconType } from '@elastic/eui'; import { SecuredFeature } from '../../../../model'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss deleted file mode 100644 index 3f40f21e102a1..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './privilege_matrix'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss deleted file mode 100644 index 8f47727fdf8d6..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 1. Allow table to scroll both directions - */ - -.secPrivilegeMatrix__modal, -.secPrivilegeMatrix__modal .euiModal__flex { - overflow: hidden; /* 1 */ -} - -.secPrivilegeMatrix__row--isBasePrivilege, -.secPrivilegeMatrix__cell--isGlobalPrivilege, -.secPrivilegeTable__row--isGlobalSpace { - background-color: $euiColorLightestShade; -} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss new file mode 100644 index 0000000000000..8e2a3b0512afb --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.scss @@ -0,0 +1,3 @@ +.secPrivilegeTable__row--isGlobalSpace { + background-color: $euiColorLightestShade; +} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index ccb5398a11b23..30a275876fdc7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './privilege_space_table.scss'; + import { EuiBadge, EuiBadgeProps, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss deleted file mode 100644 index b40a32cb8df96..0000000000000 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './spaces_popover_list'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.scss similarity index 100% rename from x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index 92e42ec811afc..63ee311f3155e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './spaces_popover_list.scss'; + import { EuiButtonEmpty, EuiContextMenuItem, diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index e1a10fdc2b8c3..9aaa3b47f3b19 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -12,13 +12,7 @@ import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { SecurityLicense } from '../../../common/licensing'; import { PluginStartDependencies } from '../../plugin'; -import { UserAPIClient } from '../users'; -import { RolesAPIClient } from './roles_api_client'; -import { RolesGridPage } from './roles_grid'; -import { EditRolePage } from './edit_role'; import { DocumentationLinksService } from './documentation_links'; -import { IndicesAPIClient } from './indices_api_client'; -import { PrivilegesAPIClient } from './privileges_api_client'; interface CreateParams { fatalErrors: FatalErrorsSetup; @@ -34,11 +28,6 @@ export const rolesManagementApp = Object.freeze({ order: 20, title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), async mount({ basePath, element, setBreadcrumbs }) { - const [ - { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, - { data, features }, - ] = await getStartServices(); - const rolesBreadcrumbs = [ { text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), @@ -46,6 +35,27 @@ export const rolesManagementApp = Object.freeze({ }, ]; + const [ + [ + { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, + { data, features }, + ], + { RolesGridPage }, + { EditRolePage }, + { RolesAPIClient }, + { IndicesAPIClient }, + { PrivilegesAPIClient }, + { UserAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./roles_grid'), + import('./edit_role'), + import('./roles_api_client'), + import('./indices_api_client'), + import('./privileges_api_client'), + import('../users'), + ]); + const rolesAPIClient = new RolesAPIClient(http); const RolesGridPageWithBreadcrumbs = () => { setBreadcrumbs(rolesBreadcrumbs); diff --git a/x-pack/plugins/security/public/management/users/_index.scss b/x-pack/plugins/security/public/management/users/_index.scss deleted file mode 100644 index 35df0c1b96583..0000000000000 --- a/x-pack/plugins/security/public/management/users/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_user/index'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss deleted file mode 100644 index 7b24b74aceba0..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.secUsersEditPage__content { - max-width: $secFormWidth; - margin-left: auto; - margin-right: auto; - flex-grow: 0; -} diff --git a/x-pack/plugins/security/public/management/users/edit_user/_index.scss b/x-pack/plugins/security/public/management/users/edit_user/_index.scss deleted file mode 100644 index 734ba7882ba72..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './edit_user_page'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss new file mode 100644 index 0000000000000..727fac4782752 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss @@ -0,0 +1,6 @@ +.secUsersEditPage__content { + max-width: 460px; + margin-left: auto; + margin-right: auto; + flex-grow: 0; +} diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 6417ce81b647d..1c8130029bb50 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import './edit_user_page.scss'; + import { get } from 'lodash'; import React, { Component, Fragment, ChangeEvent } from 'react'; import { diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 82a2b8d2a98ad..9d337c1508ad4 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -12,10 +12,6 @@ import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; -import { RolesAPIClient } from '../roles'; -import { UserAPIClient } from './user_api_client'; -import { UsersGridPage } from './users_grid'; -import { EditUserPage } from './edit_user'; interface CreateParams { authc: AuthenticationServiceSetup; @@ -30,7 +26,6 @@ export const usersManagementApp = Object.freeze({ order: 10, title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), async mount({ basePath, element, setBreadcrumbs }) { - const [{ http, notifications, i18n: i18nStart }] = await getStartServices(); const usersBreadcrumbs = [ { text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), @@ -38,6 +33,20 @@ export const usersManagementApp = Object.freeze({ }, ]; + const [ + [{ http, notifications, i18n: i18nStart }], + { UsersGridPage }, + { EditUserPage }, + { UserAPIClient }, + { RolesAPIClient }, + ] = await Promise.all([ + getStartServices(), + import('./users_grid'), + import('./edit_user'), + import('./user_api_client'), + import('../roles'), + ]); + const userAPIClient = new UserAPIClient(http); const rolesAPIClient = new RolesAPIClient(http); const UsersGridPageWithBreadcrumbs = () => { diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 122b26378d22b..7c57c4dd997a2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -37,7 +37,7 @@ describe('Security Plugin', () => { ) ).toEqual({ __legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' }, - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), @@ -63,7 +63,7 @@ describe('Security Plugin', () => { expect(setupManagementServiceMock).toHaveBeenCalledTimes(1); expect(setupManagementServiceMock).toHaveBeenCalledWith({ - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), diff --git a/x-pack/plugins/security/public/session/session_timeout.test.tsx b/x-pack/plugins/security/public/session/session_timeout.test.tsx index eca3e7d6727df..11aadcff377ef 100644 --- a/x-pack/plugins/security/public/session/session_timeout.test.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.test.tsx @@ -74,6 +74,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: now + 2 * 60 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }; let notifications: ReturnType<typeof coreMock.createSetup>['notifications']; let http: ReturnType<typeof coreMock.createSetup>['http']; @@ -192,6 +193,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: null, lifespanExpiration: now + 2 * 60 * 1000, + provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); await sessionTimeout.start(); @@ -225,6 +227,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: null, lifespanExpiration: now + 2 * 60 * 1000, + provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); await sessionTimeout.start(); @@ -251,6 +254,7 @@ describe('Session Timeout', () => { now: now + elapsed, idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }); await sessionTimeout.extend('/foo'); expect(http.fetch).toHaveBeenCalledTimes(3); @@ -303,6 +307,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: now + 64 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }); await sessionTimeout.start(); expect(http.fetch).toHaveBeenCalled(); @@ -336,6 +341,7 @@ describe('Session Timeout', () => { now: now + elapsed, idleTimeoutExpiration: now + elapsed + 2 * 60 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }; http.fetch.mockResolvedValue(sessionInfo); await sessionTimeout.extend('/foo'); @@ -358,6 +364,7 @@ describe('Session Timeout', () => { now, idleTimeoutExpiration: now + 4 * 1000, lifespanExpiration: null, + provider: { type: 'basic', name: 'basic1' }, }); await sessionTimeout.start(); diff --git a/x-pack/plugins/security/public/session/session_timeout.tsx b/x-pack/plugins/security/public/session/session_timeout.tsx index bd6dbad7dbf14..b06d8fffd4b62 100644 --- a/x-pack/plugins/security/public/session/session_timeout.tsx +++ b/x-pack/plugins/security/public/session/session_timeout.tsx @@ -6,10 +6,10 @@ import { NotificationsSetup, Toast, HttpSetup, ToastInput } from 'src/core/public'; import { BroadcastChannel } from 'broadcast-channel'; +import { SessionInfo } from '../../common/types'; import { createToast as createIdleTimeoutToast } from './session_idle_timeout_warning'; import { createToast as createLifespanToast } from './session_lifespan_warning'; import { ISessionExpired } from './session_expired'; -import { SessionInfo } from '../types'; /** * Client session timeout is decreased by this number so that Kibana server @@ -127,7 +127,7 @@ export class SessionTimeout implements ISessionTimeout { this.sessionInfo = sessionInfo; // save the provider name in session storage, we will need it when we log out const key = `${this.tenant}/session_provider`; - sessionStorage.setItem(key, sessionInfo.provider); + sessionStorage.setItem(key, sessionInfo.provider.name); const { timeout, isLifespanTimeout } = this.getTimeout(); if (timeout == null) { diff --git a/x-pack/plugins/security/public/types.ts b/x-pack/plugins/security/public/types.ts deleted file mode 100644 index e9c4b6e281cf3..0000000000000 --- a/x-pack/plugins/security/public/types.ts +++ /dev/null @@ -1,12 +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. - */ - -export interface SessionInfo { - now: number; - idleTimeoutExpiration: number | null; - lifespanExpiration: number | null; - provider: string; -} diff --git a/x-pack/plugins/security/server/audit/audit_logger.test.ts b/x-pack/plugins/security/server/audit/audit_logger.test.ts index f7ee210a21a74..4dfd69a2ccb1f 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.test.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.test.ts @@ -62,7 +62,7 @@ describe(`#savedObjectsAuthorizationFailure`, () => { }); describe(`#savedObjectsAuthorizationSuccess`, () => { - test('logs via auditLogger when xpack.security.audit.enabled is true', () => { + test('logs via auditLogger', () => { const auditLogger = createMockAuditLogger(); const securityAuditLogger = new SecurityAuditLogger(() => auditLogger); const username = 'foo-user'; @@ -92,3 +92,21 @@ describe(`#savedObjectsAuthorizationSuccess`, () => { ); }); }); + +describe(`#accessAgreementAcknowledged`, () => { + test('logs via auditLogger', () => { + const auditLogger = createMockAuditLogger(); + const securityAuditLogger = new SecurityAuditLogger(() => auditLogger); + const username = 'foo-user'; + const provider = { type: 'saml', name: 'saml1' }; + + securityAuditLogger.accessAgreementAcknowledged(username, provider); + + expect(auditLogger.log).toHaveBeenCalledTimes(1); + expect(auditLogger.log).toHaveBeenCalledWith( + 'access_agreement_acknowledged', + 'foo-user acknowledged access agreement (saml/saml1).', + { username, provider } + ); + }); +}); diff --git a/x-pack/plugins/security/server/audit/audit_logger.ts b/x-pack/plugins/security/server/audit/audit_logger.ts index 40b525b5d2188..d7243ecbe13f8 100644 --- a/x-pack/plugins/security/server/audit/audit_logger.ts +++ b/x-pack/plugins/security/server/audit/audit_logger.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AuthenticationProvider } from '../../common/types'; import { LegacyAPI } from '../plugin'; export class SecurityAuditLogger { @@ -57,4 +58,12 @@ export class SecurityAuditLogger { } ); } + + accessAgreementAcknowledged(username: string, provider: AuthenticationProvider) { + this.getAuditLogger().log( + 'access_agreement_acknowledged', + `${username} acknowledged access agreement (${provider.type}/${provider.name}).`, + { username, provider } + ); + } } diff --git a/x-pack/plugins/security/server/audit/index.mock.ts b/x-pack/plugins/security/server/audit/index.mock.ts index c14b98ed4781e..888aa3361faf0 100644 --- a/x-pack/plugins/security/server/audit/index.mock.ts +++ b/x-pack/plugins/security/server/audit/index.mock.ts @@ -11,6 +11,7 @@ export const securityAuditLoggerMock = { return ({ savedObjectsAuthorizationFailure: jest.fn(), savedObjectsAuthorizationSuccess: jest.fn(), + accessAgreementAcknowledged: jest.fn(), } as unknown) as jest.Mocked<SecurityAuditLogger>; }, }; diff --git a/x-pack/plugins/security/server/authentication/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys.test.ts index 836740d0a547f..9f2a628b575d5 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.test.ts @@ -40,6 +40,82 @@ describe('API Keys', () => { }); }); + describe('areAPIKeysEnabled()', () => { + it('returns false when security feature is disabled', async () => { + mockLicense.isEnabled.mockReturnValue(false); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(false); + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('returns false when the exception metadata indicates api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(false); + }); + + it('returns true when the operation completes without error', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValue({}); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(true); + }); + + it('throws the original error when exception metadata does not indicate that api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'something_else' }, + }; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception metadata does not contain `disabled.feature`', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = {}; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception contains no metadata', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('calls callCluster with proper parameters', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValueOnce({}); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(true); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', { + body: { + id: 'kibana-api-key-service-test', + }, + }); + }); + }); + describe('create()', () => { it('returns null when security feature is disabled', async () => { mockLicense.isEnabled.mockReturnValue(false); diff --git a/x-pack/plugins/security/server/authentication/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys.ts index 9df7219cec334..29ff7e1f69f95 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.ts @@ -125,6 +125,35 @@ export class APIKeys { this.license = license; } + /** + * Determines if API Keys are enabled in Elasticsearch. + */ + async areAPIKeysEnabled(): Promise<boolean> { + if (!this.license.isEnabled()) { + return false; + } + + const id = `kibana-api-key-service-test`; + + this.logger.debug( + `Testing if API Keys are enabled by attempting to invalidate a non-existant key: ${id}` + ); + + try { + await this.clusterClient.callAsInternalUser('shield.invalidateAPIKey', { + body: { + id, + }, + }); + return true; + } catch (e) { + if (this.doesErrorIndicateAPIKeysAreDisabled(e)) { + return false; + } + throw e; + } + } + /** * Tries to create an API key for the current user. * @param request Request instance. @@ -247,6 +276,11 @@ export class APIKeys { return result; } + private doesErrorIndicateAPIKeysAreDisabled(e: Record<string, any>) { + const disabledFeature = e.body?.error?.['disabled.feature']; + return disabledFeature === 'api_keys'; + } + private getGrantParams(authorizationHeader: HTTPAuthorizationHeader): GrantAPIKeyParams { if (authorizationHeader.scheme.toLowerCase() === 'bearer') { return { diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index a595b63faaf9b..49b7b40659cfc 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -20,7 +20,10 @@ import { elasticsearchServiceMock, sessionStorageMock, } from '../../../../../src/core/server/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +import { securityAuditLoggerMock } from '../audit/index.mock'; +import { SecurityLicenseFeatures } from '../../common/licensing'; import { ConfigSchema, createConfig } from '../config'; import { AuthenticationResult } from './authentication_result'; import { Authenticator, AuthenticatorOptions, ProviderSession } from './authenticator'; @@ -39,8 +42,11 @@ function getMockOptions({ selector?: AuthenticatorOptions['config']['authc']['selector']; } = {}) { return { + auditLogger: securityAuditLoggerMock.create(), + getCurrentUser: jest.fn(), clusterClient: elasticsearchServiceMock.createClusterClient(), basePath: httpServiceMock.createSetupContract().basePath, + license: licenseMock.create(), loggers: loggingServiceMock.create(), config: createConfig( ConfigSchema.validate({ session, authc: { selector, providers, http } }), @@ -1108,6 +1114,141 @@ describe('Authenticator', () => { expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled(); }); }); + + describe('with Access Agreement', () => { + const mockUser = mockAuthenticatedUser(); + beforeEach(() => { + mockOptions = getMockOptions({ + providers: { + basic: { basic1: { order: 0, accessAgreement: { message: 'some notice' } } }, + }, + }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.succeeded(mockUser) + ); + + authenticator = new Authenticator(mockOptions); + }); + + it('does not redirect to Access Agreement if there is no active session', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(null); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect AJAX requests to Access Agreement', async () => { + const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement if request cannot be handled', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.notHandled() + ); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.notHandled() + ); + }); + + it('does not redirect to Access Agreement if authentication fails', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + const failureReason = new Error('something went wrong'); + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.failed(failureReason) + ); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.failed(failureReason) + ); + }); + + it('does not redirect to Access Agreement if redirect is required to complete authentication', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + mockBasicAuthenticationProvider.authenticate.mockResolvedValue( + AuthenticationResult.redirectTo('/some-url') + ); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo('/some-url') + ); + }); + + it('does not redirect to Access Agreement if user has already acknowledged it', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue({ + ...mockSessVal, + accessAgreementAcknowledged: true, + }); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement its own requests', async () => { + const request = httpServerMock.createKibanaRequest({ path: '/security/access_agreement' }); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement if it is not configured', async () => { + mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + authenticator = new Authenticator(mockOptions); + + const request = httpServerMock.createKibanaRequest(); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('does not redirect to Access Agreement if license doesnt allow it.', async () => { + const request = httpServerMock.createKibanaRequest(); + mockSessionStorage.get.mockResolvedValue(mockSessVal); + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.succeeded(mockUser) + ); + }); + + it('redirects to Access Agreement when needed.', async () => { + mockSessionStorage.get.mockResolvedValue(mockSessVal); + + const request = httpServerMock.createKibanaRequest(); + await expect(authenticator.authenticate(request)).resolves.toEqual( + AuthenticationResult.redirectTo( + '/mock-server-basepath/security/access_agreement?next=%2Fmock-server-basepath%2Fpath' + ) + ); + }); + }); }); describe('`logout` method', () => { @@ -1228,13 +1369,13 @@ describe('Authenticator', () => { now: currentDate, idleTimeoutExpiration: currentDate + 60000, lifespanExpiration: currentDate + 120000, - provider: 'basic1', + provider: { type: 'basic' as 'basic', name: 'basic1' }, }; mockSessionStorage.get.mockResolvedValue({ idleTimeoutExpiration: mockInfo.idleTimeoutExpiration, lifespanExpiration: mockInfo.lifespanExpiration, state, - provider: { type: 'basic', name: mockInfo.provider }, + provider: mockInfo.provider, path: mockOptions.basePath.serverBasePath, }); jest.spyOn(Date, 'now').mockImplementation(() => currentDate); @@ -1274,4 +1415,84 @@ describe('Authenticator', () => { expect(authenticator.isProviderTypeEnabled('saml')).toBe(true); }); }); + + describe('`acknowledgeAccessAgreement` method', () => { + let authenticator: Authenticator; + let mockOptions: ReturnType<typeof getMockOptions>; + let mockSessionStorage: jest.Mocked<SessionStorage<ProviderSession>>; + let mockSessionValue: any; + beforeEach(() => { + mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } }); + mockSessionStorage = sessionStorageMock.create(); + mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); + mockSessionValue = { + idleTimeoutExpiration: null, + lifespanExpiration: null, + state: { authorization: 'Basic xxx' }, + provider: { type: 'basic', name: 'basic1' }, + path: mockOptions.basePath.serverBasePath, + }; + mockSessionStorage.get.mockResolvedValue(mockSessionValue); + mockOptions.getCurrentUser.mockReturnValue(mockAuthenticatedUser()); + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + authenticator = new Authenticator(mockOptions); + }); + + it('fails if user is not authenticated', async () => { + mockOptions.getCurrentUser.mockReturnValue(null); + + await expect( + authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot acknowledge access agreement for unauthenticated user."` + ); + + expect(mockSessionStorage.set).not.toHaveBeenCalled(); + }); + + it('fails if cannot retrieve user session', async () => { + mockSessionStorage.get.mockResolvedValue(null); + + await expect( + authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot acknowledge access agreement for unauthenticated user."` + ); + + expect(mockSessionStorage.set).not.toHaveBeenCalled(); + }); + + it('fails if license doesn allow access agreement acknowledgement', async () => { + mockOptions.license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await expect( + authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Current license does not allow access agreement acknowledgement."` + ); + + expect(mockSessionStorage.set).not.toHaveBeenCalled(); + }); + + it('properly acknowledges access agreement for the authenticated user', async () => { + await authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest()); + + expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); + expect(mockSessionStorage.set).toHaveBeenCalledWith({ + ...mockSessionValue, + accessAgreementAcknowledged: true, + }); + + expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledTimes(1); + expect(mockOptions.auditLogger.accessAgreementAcknowledged).toHaveBeenCalledWith('user', { + type: 'basic', + name: 'basic1', + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index caf5b485d05e3..58dea2b23e546 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -14,6 +14,10 @@ import { HttpServiceSetup, IClusterClient, } from '../../../../../src/core/server'; +import { SecurityLicense } from '../../common/licensing'; +import { AuthenticatedUser } from '../../common/model'; +import { AuthenticationProvider, SessionInfo } from '../../common/types'; +import { SecurityAuditLogger } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; @@ -32,7 +36,6 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; -import { SessionInfo } from '../../public'; import { canRedirectRequest } from './can_redirect_request'; import { HTTPAuthorizationHeader } from './http_authentication'; @@ -43,7 +46,7 @@ export interface ProviderSession { /** * Name and type of the provider this session belongs to. */ - provider: { type: string; name: string }; + provider: AuthenticationProvider; /** * The Unix time in ms when the session should be considered expired. If `null`, session will stay @@ -67,6 +70,11 @@ export interface ProviderSession { * Cookie "Path" attribute that is validated against the current Kibana server configuration. */ path: string; + + /** + * Indicates whether user acknowledged access agreement or not. + */ + accessAgreementAcknowledged?: boolean; } /** @@ -76,7 +84,7 @@ export interface ProviderLoginAttempt { /** * Name or type of the provider this login attempt is targeted for. */ - provider: { name: string } | { type: string }; + provider: Pick<AuthenticationProvider, 'name'> | Pick<AuthenticationProvider, 'type'>; /** * Login attempt can have any form and defined by the specific provider. @@ -85,8 +93,11 @@ export interface ProviderLoginAttempt { } export interface AuthenticatorOptions { + auditLogger: SecurityAuditLogger; + getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null; config: Pick<ConfigType, 'session' | 'authc'>; basePath: HttpServiceSetup['basePath']; + license: SecurityLicense; loggers: LoggerFactory; clusterClient: IClusterClient; sessionStorageFactory: SessionStorageFactory<ProviderSession>; @@ -109,6 +120,11 @@ const providerMap = new Map< [PKIAuthenticationProvider.type, PKIAuthenticationProvider], ]); +/** + * The route to the access agreement UI. + */ +const ACCESS_AGREEMENT_ROUTE = '/security/access_agreement'; + function assertRequest(request: KibanaRequest) { if (!(request instanceof KibanaRequest)) { throw new Error(`Request should be a valid "KibanaRequest" instance, was [${typeof request}].`); @@ -135,7 +151,7 @@ function isLoginAttemptWithProviderName( function isLoginAttemptWithProviderType( attempt: unknown -): attempt is { value: unknown; provider: { type: string } } { +): attempt is { value: unknown; provider: Pick<AuthenticationProvider, 'type'> } { return ( typeof attempt === 'object' && (attempt as any)?.provider?.type && @@ -341,14 +357,7 @@ export class Authenticator { const sessionStorage = this.options.sessionStorageFactory.asScoped(request); const existingSession = await this.getSessionValue(sessionStorage); - // If request doesn't have any session information, isn't attributed with HTTP Authorization - // header and Login Selector is enabled, we must redirect user to the login selector. - const useLoginSelector = - !existingSession && - this.options.config.authc.selector.enabled && - canRedirectRequest(request) && - HTTPAuthorizationHeader.parseFromRequest(request) == null; - if (useLoginSelector) { + if (this.shouldRedirectToLoginSelector(request, existingSession)) { this.logger.debug('Redirecting request to Login Selector.'); return AuthenticationResult.redirectTo( `${this.options.basePath.serverBasePath}/login?next=${encodeURIComponent( @@ -368,7 +377,7 @@ export class Authenticator { ownsSession ? existingSession!.state : null ); - this.updateSessionValue(sessionStorage, { + const updatedSession = this.updateSessionValue(sessionStorage, { provider: { type: provider.type, name: providerName }, isSystemRequest: request.isSystemRequest, authenticationResult, @@ -376,6 +385,20 @@ export class Authenticator { }); if (!authenticationResult.notHandled()) { + if ( + authenticationResult.succeeded() && + this.shouldRedirectToAccessAgreement(request, updatedSession) + ) { + this.logger.debug('Redirecting user to the access agreement screen.'); + return AuthenticationResult.redirectTo( + `${ + this.options.basePath.serverBasePath + }${ACCESS_AGREEMENT_ROUTE}?next=${encodeURIComponent( + `${this.options.basePath.get(request)}${request.url.path}` + )}` + ); + } + return authenticationResult; } } @@ -441,7 +464,7 @@ export class Authenticator { now: Date.now(), idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, lifespanExpiration: sessionValue.lifespanExpiration, - provider: sessionValue.provider.name, + provider: sessionValue.provider, }; } return null; @@ -455,6 +478,32 @@ export class Authenticator { return [...this.providers.values()].some(provider => provider.type === providerType); } + /** + * Acknowledges access agreement on behalf of the currently authenticated user. + * @param request Request instance. + */ + async acknowledgeAccessAgreement(request: KibanaRequest) { + assertRequest(request); + + const sessionStorage = this.options.sessionStorageFactory.asScoped(request); + const existingSession = await this.getSessionValue(sessionStorage); + const currentUser = this.options.getCurrentUser(request); + if (!existingSession || !currentUser) { + throw new Error('Cannot acknowledge access agreement for unauthenticated user.'); + } + + if (!this.options.license.getFeatures().allowAccessAgreement) { + throw new Error('Current license does not allow access agreement acknowledgement.'); + } + + sessionStorage.set({ ...existingSession, accessAgreementAcknowledged: true }); + + this.options.auditLogger.accessAgreementAcknowledged( + currentUser.username, + existingSession.provider + ); + } + /** * Initializes HTTP Authentication provider and appends it to the end of the list of enabled * authentication providers. @@ -538,14 +587,14 @@ export class Authenticator { existingSession, isSystemRequest, }: { - provider: { type: string; name: string }; + provider: AuthenticationProvider; authenticationResult: AuthenticationResult; existingSession: ProviderSession | null; isSystemRequest: boolean; } ) { if (!existingSession && !authenticationResult.shouldUpdateState()) { - return; + return null; } // If authentication succeeds or requires redirect we should automatically extend existing user session, @@ -563,9 +612,12 @@ export class Authenticator { (authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401) ) { sessionStorage.clear(); - } else if (sessionCanBeUpdated) { + return null; + } + + if (sessionCanBeUpdated) { const { idleTimeoutExpiration, lifespanExpiration } = this.calculateExpiry(existingSession); - sessionStorage.set({ + const updatedSession = { state: authenticationResult.shouldUpdateState() ? authenticationResult.state : existingSession!.state, @@ -573,8 +625,13 @@ export class Authenticator { idleTimeoutExpiration, lifespanExpiration, path: this.serverBasePath, - }); + accessAgreementAcknowledged: existingSession?.accessAgreementAcknowledged, + }; + sessionStorage.set(updatedSession); + return updatedSession; } + + return existingSession; } private getProviderName(query: any): string | null { @@ -600,4 +657,48 @@ export class Authenticator { return { idleTimeoutExpiration, lifespanExpiration }; } + + /** + * Checks whether request should be redirected to the Login Selector UI. + * @param request Request instance. + * @param session Current session value if any. + */ + private shouldRedirectToLoginSelector(request: KibanaRequest, session: ProviderSession | null) { + // Request should be redirected to Login Selector UI only if all following conditions are met: + // 1. Request can be redirected (not API call) + // 2. Request is not authenticated yet + // 3. Login Selector UI is enabled + // 4. Request isn't attributed with HTTP Authorization header + return ( + canRedirectRequest(request) && + !session && + this.options.config.authc.selector.enabled && + HTTPAuthorizationHeader.parseFromRequest(request) == null + ); + } + + /** + * Checks whether request should be redirected to the Access Agreement UI. + * @param request Request instance. + * @param session Current session value if any. + */ + private shouldRedirectToAccessAgreement(request: KibanaRequest, session: ProviderSession | null) { + // Request should be redirected to Access Agreement UI only if all following conditions are met: + // 1. Request can be redirected (not API call) + // 2. Request is authenticated, but user hasn't acknowledged access agreement in the current + // session yet (based on the flag we store in the session) + // 3. Request is authenticated by the provider that has `accessAgreement` configured + // 4. Current license allows access agreement + // 5. And it's not a request to the Access Agreement UI itself + return ( + canRedirectRequest(request) && + session != null && + !session.accessAgreementAcknowledged && + (this.options.config.authc.providers as Record<string, any>)[session.provider.type]?.[ + session.provider.name + ]?.accessAgreement && + this.options.license.getFeatures().allowAccessAgreement && + request.url.pathname !== ACCESS_AGREEMENT_ROUTE + ); + } } diff --git a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts index 1c9b936692f9e..9f1d6b27aa9d7 100644 --- a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts +++ b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks'; +import { httpServerMock } from 'src/core/server/mocks'; import { canRedirectRequest } from './can_redirect_request'; diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 8092c1c81017b..7cd3ac18634f7 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -11,6 +11,7 @@ export const authenticationMock = { login: jest.fn(), logout: jest.fn(), isProviderTypeEnabled: jest.fn(), + areAPIKeysEnabled: jest.fn(), createAPIKey: jest.fn(), getCurrentUser: jest.fn(), grantAPIKeyAsInternalUser: jest.fn(), @@ -18,5 +19,6 @@ export const authenticationMock = { invalidateAPIKeyAsInternalUser: jest.fn(), isAuthenticated: jest.fn(), getSessionInfo: jest.fn(), + acknowledgeAccessAgreement: jest.fn(), }), }; diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index 6609f8707976b..1c1e0ed781f18 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -19,6 +19,7 @@ import { elasticsearchServiceMock, } from '../../../../../src/core/server/mocks'; import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock'; +import { securityAuditLoggerMock } from '../audit/index.mock'; import { AuthenticationHandler, @@ -40,9 +41,11 @@ import { InvalidateAPIKeyParams, } from './api_keys'; import { SecurityLicense } from '../../common/licensing'; +import { SecurityAuditLogger } from '../audit'; describe('setupAuthentication()', () => { let mockSetupAuthenticationParams: { + auditLogger: jest.Mocked<SecurityAuditLogger>; config: ConfigType; loggers: LoggerFactory; http: jest.Mocked<CoreSetup['http']>; @@ -52,6 +55,7 @@ describe('setupAuthentication()', () => { let mockScopedClusterClient: jest.Mocked<PublicMethodsOf<ScopedClusterClient>>; beforeEach(() => { mockSetupAuthenticationParams = { + auditLogger: securityAuditLoggerMock.create(), http: coreMock.createSetup().http, config: createConfig( ConfigSchema.validate({ diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 5d7b49de68d28..779b852195b02 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -10,12 +10,13 @@ import { KibanaRequest, LoggerFactory, } from '../../../../../src/core/server'; +import { SecurityLicense } from '../../common/licensing'; import { AuthenticatedUser } from '../../common/model'; +import { SecurityAuditLogger } from '../audit'; import { ConfigType } from '../config'; import { getErrorStatusCode } from '../errors'; import { Authenticator, ProviderSession } from './authenticator'; import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys'; -import { SecurityLicense } from '../../common/licensing'; export { canRedirectRequest } from './can_redirect_request'; export { Authenticator, ProviderLoginAttempt } from './authenticator'; @@ -35,6 +36,7 @@ export { } from './http_authentication'; interface SetupAuthenticationParams { + auditLogger: SecurityAuditLogger; http: CoreSetup['http']; clusterClient: IClusterClient; config: ConfigType; @@ -45,6 +47,7 @@ interface SetupAuthenticationParams { export type Authentication = UnwrapPromise<ReturnType<typeof setupAuthentication>>; export async function setupAuthentication({ + auditLogger, http, clusterClient, config, @@ -82,9 +85,12 @@ export async function setupAuthentication({ }; const authenticator = new Authenticator({ + auditLogger, + getCurrentUser, clusterClient, basePath: http.basePath, config: { session: config.session, authc: config.authc }, + license, loggers, sessionStorageFactory: await http.createCookieSessionStorageFactory({ encryptionKey: config.encryptionKey, @@ -171,7 +177,9 @@ export async function setupAuthentication({ logout: authenticator.logout.bind(authenticator), getSessionInfo: authenticator.getSessionInfo.bind(authenticator), isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator), + acknowledgeAccessAgreement: authenticator.acknowledgeAccessAgreement.bind(authenticator), getCurrentUser, + areAPIKeysEnabled: () => apiKeys.areAPIKeysEnabled(), createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), grantAPIKeyAsInternalUser: (request: KibanaRequest) => apiKeys.grantAsInternalUser(request), diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index a7a43a3031571..ec50ac090f1e7 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -315,117 +315,123 @@ describe('SAMLAuthenticationProvider', () => { }); }); - it('redirects to the home page if new SAML Response is for the same user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; - - const user = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', - }); - - mockOptions.tokens.invalidate.mockResolvedValue(undefined); - - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/base-path/', { - state: { - username: 'user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); - - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + for (const [description, response] of [ + ['session is valid', Promise.resolve({ username: 'user' })], + [ + 'session is is expired', + Promise.reject(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())), + ], + ] as Array<[string, Promise<any>]>) { + it(`redirects to the home page if new SAML Response is for the same user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, - }); - }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + username: 'user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - const existingUser = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'new-user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - mockOptions.tokens.invalidate.mockResolvedValue(undefined); + it(`redirects to \`overwritten_session\` if new SAML Response is for the another user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { - state: { - username: 'new-user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'new-user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { + state: { + username: 'new-user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - }); + } }); describe('User initiated login with captured redirect URL', () => { diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index e14d34d1901eb..5c5ec49890901 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -158,10 +158,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { return await this.loginWithSAMLResponse(request, samlResponse, state); } - if (authenticationResult.succeeded()) { - // If user has been authenticated via session, but request also includes SAML payload - // we should check whether this payload is for the exactly same user and if not - // we'll re-authenticate user and forward to a page with the respective warning. + // If user has been authenticated via session or failed to do so because of expired access token, + // but request also includes SAML payload we should check whether this payload is for the exactly + // same user and if not we'll re-authenticate user and forward to a page with the respective warning. + if ( + authenticationResult.succeeded() || + (authenticationResult.failed() && + Tokens.isAccessTokenExpiredError(authenticationResult.error)) + ) { return await this.loginWithNewSAMLResponse( request, samlResponse, diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts index 82f29310c04c0..57366183050d7 100644 --- a/x-pack/plugins/security/server/authentication/tokens.test.ts +++ b/x-pack/plugins/security/server/authentication/tokens.test.ts @@ -25,7 +25,7 @@ describe('Tokens', () => { tokens = new Tokens(tokensOptions); }); - it('isAccessTokenExpiredError() returns `true` only if token expired or its document is missing', () => { + it('isAccessTokenExpiredError() returns `true` only if token expired', () => { const nonExpirationErrors = [ {}, new Error(), @@ -91,55 +91,66 @@ describe('Tokens', () => { }); describe('invalidate()', () => { - it('throws if call to delete access token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + for (const [description, failureReason] of [ + ['an unknown error', new Error('failed to delete token')], + ['a 404 error without body', { statusCode: 404 }], + ] as Array<[string, object]>) { + it(`throws if call to delete access token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); - - it('throws if call to delete refresh token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.refresh_token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + it(`throws if call to delete refresh token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.refresh_token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + } it('invalidates all provided tokens', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; @@ -187,23 +198,35 @@ describe('Tokens', () => { ); }); - it('does not fail if none of the tokens were invalidated', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 0 }); - - await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + for (const [description, response] of [ + ['none of the tokens were invalidated', Promise.resolve({ invalidated_tokens: 0 })], + [ + '404 error is returned', + Promise.reject({ statusCode: 404, body: { invalidated_tokens: 0 } }), + ], + ] as Array<[string, Promise<any>]>) { + it(`does not fail if ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation(() => response); + + await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); + }); + } it('does not fail if more than one token per access or refresh token were invalidated', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index ea7b5d5a9ff38..9117c9a679a4a 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -103,8 +103,15 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate refresh token: ${err.message}`); - // We don't re-throw the error here to have a chance to invalidate access token if it's provided. - invalidationError = err; + + // When using already deleted refresh token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + // We don't re-throw the error here to have a chance to invalidate access token if it's provided. + invalidationError = err; + } } if (invalidatedTokensCount === 0) { @@ -128,7 +135,14 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate access token: ${err.message}`); - invalidationError = err; + + // When using already deleted access token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + invalidationError = err; + } } if (invalidatedTokensCount === 0) { diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 46a7ee79ee60c..2c24864649977 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -27,8 +27,11 @@ describe('config schema', () => { "providers": Object { "basic": Object { "basic": Object { + "accessAgreement": undefined, "description": undefined, "enabled": true, + "hint": undefined, + "icon": undefined, "order": 0, "showInSelector": true, }, @@ -69,8 +72,11 @@ describe('config schema', () => { "providers": Object { "basic": Object { "basic": Object { + "accessAgreement": undefined, "description": undefined, "enabled": true, + "hint": undefined, + "icon": undefined, "order": 0, "showInSelector": true, }, @@ -111,8 +117,11 @@ describe('config schema', () => { "providers": Object { "basic": Object { "basic": Object { + "accessAgreement": undefined, "description": undefined, "enabled": true, + "hint": undefined, + "icon": undefined, "order": 0, "showInSelector": true, }, @@ -361,20 +370,6 @@ describe('config schema', () => { `); }); - it('does not allow custom description', () => { - expect(() => - ConfigSchema.validate({ - authc: { - providers: { basic: { basic1: { order: 0, description: 'Some description' } } }, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.basic.basic1.description]: \`basic\` provider does not support custom description." -`); - }); - it('cannot be hidden from selector', () => { expect(() => ConfigSchema.validate({ @@ -410,7 +405,9 @@ describe('config schema', () => { Object { "basic": Object { "basic1": Object { + "description": "Log in with Elasticsearch", "enabled": true, + "icon": "logoElastic", "order": 0, "showInSelector": true, }, @@ -433,20 +430,6 @@ describe('config schema', () => { `); }); - it('does not allow custom description', () => { - expect(() => - ConfigSchema.validate({ - authc: { - providers: { token: { token1: { order: 0, description: 'Some description' } } }, - }, - }) - ).toThrowErrorMatchingInlineSnapshot(` -"[authc.providers]: types that failed validation: -- [authc.providers.0]: expected value of type [array] but got [Object] -- [authc.providers.1.token.token1.description]: \`token\` provider does not support custom description." -`); - }); - it('cannot be hidden from selector', () => { expect(() => ConfigSchema.validate({ @@ -482,7 +465,9 @@ describe('config schema', () => { Object { "token": Object { "token1": Object { + "description": "Log in with Elasticsearch", "enabled": true, + "icon": "logoElastic", "order": 0, "showInSelector": true, }, @@ -759,12 +744,16 @@ describe('config schema', () => { Object { "basic": Object { "basic1": Object { + "description": "Log in with Elasticsearch", "enabled": true, + "icon": "logoElastic", "order": 0, "showInSelector": true, }, "basic2": Object { + "description": "Log in with Elasticsearch", "enabled": false, + "icon": "logoElastic", "order": 1, "showInSelector": true, }, @@ -911,20 +900,12 @@ describe('createConfig()', () => { "sortedProviders": Array [ Object { "name": "saml", - "options": Object { - "description": undefined, - "order": 0, - "showInSelector": true, - }, + "order": 0, "type": "saml", }, Object { "name": "basic", - "options": Object { - "description": undefined, - "order": 1, - "showInSelector": true, - }, + "order": 1, "type": "basic", }, ], @@ -1015,47 +996,27 @@ describe('createConfig()', () => { Array [ Object { "name": "oidc1", - "options": Object { - "description": undefined, - "order": 0, - "showInSelector": true, - }, + "order": 0, "type": "oidc", }, Object { "name": "saml2", - "options": Object { - "description": undefined, - "order": 1, - "showInSelector": true, - }, + "order": 1, "type": "saml", }, Object { "name": "saml1", - "options": Object { - "description": undefined, - "order": 2, - "showInSelector": true, - }, + "order": 2, "type": "saml", }, Object { "name": "basic1", - "options": Object { - "description": undefined, - "order": 3, - "showInSelector": true, - }, + "order": 3, "type": "basic", }, Object { "name": "oidc2", - "options": Object { - "description": undefined, - "order": 4, - "showInSelector": true, - }, + "order": 4, "type": "oidc", }, ] diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 97ff7d00a4336..8fe79a788ac51 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -6,6 +6,7 @@ import crypto from 'crypto'; import { schema, Type, TypeOf } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; import { Logger } from '../../../../src/core/server'; export type ConfigType = ReturnType<typeof createConfig>; @@ -21,7 +22,7 @@ const providerOptionsSchema = (providerType: string, optionsSchema: Type<any>) = ); type ProvidersCommonConfigType = Record< - 'enabled' | 'showInSelector' | 'order' | 'description', + 'enabled' | 'showInSelector' | 'order' | 'description' | 'hint' | 'icon', Type<any> >; function getCommonProviderSchemaProperties(overrides: Partial<ProvidersCommonConfigType> = {}) { @@ -30,6 +31,9 @@ function getCommonProviderSchemaProperties(overrides: Partial<ProvidersCommonCon showInSelector: schema.boolean({ defaultValue: true }), order: schema.number({ min: 0 }), description: schema.maybe(schema.string()), + hint: schema.maybe(schema.string()), + icon: schema.maybe(schema.string()), + accessAgreement: schema.maybe(schema.object({ message: schema.string() })), ...overrides, }; } @@ -53,11 +57,12 @@ type ProvidersConfigType = TypeOf<typeof providersConfigSchema>; const providersConfigSchema = schema.object( { basic: getUniqueProviderSchema('basic', { - description: schema.maybe( - schema.any({ - validate: () => '`basic` provider does not support custom description.', - }) - ), + description: schema.string({ + defaultValue: i18n.translate('xpack.security.loginWithElasticsearchLabel', { + defaultMessage: 'Log in with Elasticsearch', + }), + }), + icon: schema.string({ defaultValue: 'logoElastic' }), showInSelector: schema.boolean({ defaultValue: true, validate: value => { @@ -68,11 +73,12 @@ const providersConfigSchema = schema.object( }), }), token: getUniqueProviderSchema('token', { - description: schema.maybe( - schema.any({ - validate: () => '`token` provider does not support custom description.', - }) - ), + description: schema.string({ + defaultValue: i18n.translate('xpack.security.loginWithElasticsearchLabel', { + defaultMessage: 'Log in with Elasticsearch', + }), + }), + icon: schema.string({ defaultValue: 'logoElastic' }), showInSelector: schema.boolean({ defaultValue: true, validate: value => { @@ -131,6 +137,7 @@ const providersConfigSchema = schema.object( export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), loginAssistanceMessage: schema.string({ defaultValue: '' }), + loginHelp: schema.maybe(schema.string()), cookieName: schema.string({ defaultValue: 'sid' }), encryptionKey: schema.conditional( schema.contextRef('dist'), @@ -147,7 +154,17 @@ export const ConfigSchema = schema.object({ selector: schema.object({ enabled: schema.maybe(schema.boolean()) }), providers: schema.oneOf([schema.arrayOf(schema.string()), providersConfigSchema], { defaultValue: { - basic: { basic: { enabled: true, showInSelector: true, order: 0, description: undefined } }, + basic: { + basic: { + enabled: true, + showInSelector: true, + order: 0, + description: undefined, + hint: undefined, + icon: undefined, + accessAgreement: undefined, + }, + }, token: undefined, saml: undefined, oidc: undefined, @@ -225,25 +242,19 @@ export function createConfig( const sortedProviders: Array<{ type: keyof ProvidersConfigType; name: string; - options: { order: number; showInSelector: boolean; description?: string }; + order: number; }> = []; for (const [type, providerGroup] of Object.entries(providers)) { - for (const [name, { enabled, showInSelector, order, description }] of Object.entries( - providerGroup ?? {} - )) { + for (const [name, { enabled, order }] of Object.entries(providerGroup ?? {})) { if (!enabled) { delete providerGroup![name]; } else { - sortedProviders.push({ - type: type as any, - name, - options: { order, showInSelector, description }, - }); + sortedProviders.push({ type: type as any, name, order }); } } } - sortedProviders.sort(({ options: { order: orderA } }, { options: { order: orderB } }) => + sortedProviders.sort(({ order: orderA }, { order: orderB }) => orderA < orderB ? -1 : orderA > orderB ? 1 : 0 ); @@ -253,7 +264,8 @@ export function createConfig( typeof config.authc.selector.enabled === 'boolean' ? config.authc.selector.enabled : !isUsingLegacyProvidersFormat && - sortedProviders.filter(provider => provider.options.showInSelector).length > 1; + sortedProviders.filter(({ type, name }) => providers[type]?.[name].showInSelector).length > + 1; return { ...config, diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index ababf12c2be60..a6407366bbd3b 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -8,6 +8,7 @@ import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; +import { licenseMock } from '../common/licensing/index.mock'; function createSetupMock() { const mockAuthz = authorizationMock.create(); @@ -19,6 +20,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), + license: licenseMock.create(), __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 4767f57de764c..d58c999ddccdf 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -50,35 +50,17 @@ describe('Security Plugin', () => { await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(` Object { "__legacyCompat": Object { - "license": Object { - "features$": Observable { - "_isScalar": false, - "operator": MapOperator { - "project": [Function], - "thisArg": undefined, - }, - "source": Observable { - "_isScalar": false, - "_subscribe": [Function], - }, - }, - "getFeatures": [Function], - "isEnabled": [Function], - }, "registerLegacyAPI": [Function], "registerPrivilegesWithCluster": [Function], }, "authc": Object { + "areAPIKeysEnabled": [Function], "createAPIKey": [Function], "getCurrentUser": [Function], - "getSessionInfo": [Function], "grantAPIKeyAsInternalUser": [Function], "invalidateAPIKey": [Function], "invalidateAPIKeyAsInternalUser": [Function], "isAuthenticated": [Function], - "isProviderTypeEnabled": [Function], - "login": [Function], - "logout": [Function], }, "authz": Object { "actions": Actions { @@ -106,6 +88,21 @@ describe('Security Plugin', () => { "useRbacForRequest": [Function], }, }, + "license": Object { + "features$": Observable { + "_isScalar": false, + "operator": MapOperator { + "project": [Function], + "thisArg": undefined, + }, + "source": Observable { + "_isScalar": false, + "_subscribe": [Function], + }, + }, + "getFeatures": [Function], + "isEnabled": [Function], + }, "registerSpacesService": [Function], } `); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 9dd4aaafa3494..97f5aea888dc7 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -48,8 +48,18 @@ export interface LegacyAPI { * Describes public Security plugin contract returned at the `setup` stage. */ export interface SecurityPluginSetup { - authc: Authentication; + authc: Pick< + Authentication, + | 'isAuthenticated' + | 'getCurrentUser' + | 'areAPIKeysEnabled' + | 'createAPIKey' + | 'invalidateAPIKey' + | 'grantAPIKeyAsInternalUser' + | 'invalidateAPIKeyAsInternalUser' + >; authz: Pick<Authorization, 'actions' | 'checkPrivilegesWithRequest' | 'mode'>; + license: SecurityLicense; /** * If Spaces plugin is available it's supposed to register its SpacesService with Security plugin @@ -64,7 +74,6 @@ export interface SecurityPluginSetup { __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void; registerPrivilegesWithCluster: () => void; - license: SecurityLicense; }; } @@ -126,7 +135,9 @@ export class Plugin { license$: licensing.license$, }); + const auditLogger = new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger); const authc = await setupAuthentication({ + auditLogger, http: core.http, clusterClient: this.clusterClient, config, @@ -146,7 +157,7 @@ export class Plugin { }); setupSavedObjects({ - auditLogger: new SecurityAuditLogger(() => this.getLegacyAPI().auditLogger), + auditLogger, authz, savedObjects: core.savedObjects, getSpacesService: this.getSpacesService, @@ -167,7 +178,15 @@ export class Plugin { }); return deepFreeze<SecurityPluginSetup>({ - authc, + authc: { + isAuthenticated: authc.isAuthenticated, + getCurrentUser: authc.getCurrentUser, + areAPIKeysEnabled: authc.areAPIKeysEnabled, + createAPIKey: authc.createAPIKey, + invalidateAPIKey: authc.invalidateAPIKey, + grantAPIKeyAsInternalUser: authc.grantAPIKeyAsInternalUser, + invalidateAPIKeyAsInternalUser: authc.invalidateAPIKeyAsInternalUser, + }, authz: { actions: authz.actions, @@ -175,6 +194,8 @@ export class Plugin { mode: authz.mode, }, + license, + registerSpacesService: service => { if (this.wasSpacesServiceAccessed()) { throw new Error('Spaces service has been accessed before registration.'); @@ -187,8 +208,6 @@ export class Plugin { registerLegacyAPI: (legacyAPI: LegacyAPI) => (this.legacyAPI = legacyAPI), registerPrivilegesWithCluster: async () => await authz.registerPrivilegesWithCluster(), - - license, }, }); } diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts new file mode 100644 index 0000000000000..3c6dc3c0d7bda --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LicenseCheck } from '../../../../licensing/server'; + +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import Boom from 'boom'; +import { defineEnabledApiKeysRoutes } from './enabled'; +import { APIKeys } from '../../authentication/api_keys'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponse?: () => Promise<unknown>; + asserts: { statusCode: number; result?: Record<string, any> }; +} + +describe('API keys enabled', () => { + const enabledApiKeysTest = ( + description: string, + { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + + if (apiResponse) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse); + } + + defineEnabledApiKeysRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key/_enabled', + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.invalidateAPIKey', + { + body: { + id: expect.any(String), + }, + } + ); + } else { + expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + enabledApiKeysTest('returns result of license checker', { + licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + enabledApiKeysTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, + asserts: { statusCode: 406, result: error }, + }); + }); + + describe('success', () => { + enabledApiKeysTest('returns true if API Keys are enabled', { + apiResponse: async () => ({}), + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: true, + }, + }, + }); + enabledApiKeysTest('returns false if API Keys are disabled', { + apiResponse: async () => { + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + throw error; + }, + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: false, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.ts new file mode 100644 index 0000000000000..2f5b8343bcd89 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineEnabledApiKeysRoutes({ router, authc }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key/_enabled', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const apiKeysEnabled = await authc.areAPIKeysEnabled(); + + return response.ok({ body: { apiKeysEnabled } }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts index d75eb1bcbe961..7ac37bbead613 100644 --- a/x-pack/plugins/security/server/routes/api_keys/index.ts +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -7,9 +7,11 @@ import { defineGetApiKeysRoutes } from './get'; import { defineCheckPrivilegesRoutes } from './privileges'; import { defineInvalidateApiKeysRoutes } from './invalidate'; +import { defineEnabledApiKeysRoutes } from './enabled'; import { RouteDefinitionParams } from '..'; export function defineApiKeysRoutes(params: RouteDefinitionParams) { + defineEnabledApiKeysRoutes(params); defineGetApiKeysRoutes(params); defineCheckPrivilegesRoutes(params); defineInvalidateApiKeysRoutes(params); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index 311d50e9eb169..afb67dc3bbfca 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -11,25 +11,53 @@ import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import { defineCheckPrivilegesRoutes } from './privileges'; +import { APIKeys } from '../../authentication/api_keys'; interface TestOptions { licenseCheckResult?: LicenseCheck; - apiResponses?: Array<() => Promise<unknown>>; - asserts: { statusCode: number; result?: Record<string, any>; apiArguments?: unknown[][] }; + callAsInternalUserResponses?: Array<() => Promise<unknown>>; + callAsCurrentUserResponses?: Array<() => Promise<unknown>>; + asserts: { + statusCode: number; + result?: Record<string, any>; + callAsInternalUserAPIArguments?: unknown[][]; + callAsCurrentUserAPIArguments?: unknown[][]; + }; } describe('Check API keys privileges', () => { const getPrivilegesTest = ( description: string, - { licenseCheckResult = { state: 'valid' }, apiResponses = [], asserts }: TestOptions + { + licenseCheckResult = { state: 'valid' }, + callAsInternalUserResponses = [], + callAsCurrentUserResponses = [], + asserts, + }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of apiResponses) { + for (const apiResponse of callAsCurrentUserResponses) { mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); } + for (const apiResponse of callAsInternalUserResponses) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce( + apiResponse + ); + } defineCheckPrivilegesRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; @@ -48,8 +76,8 @@ describe('Check API keys privileges', () => { expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { + if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) { + for (const apiArguments of asserts.callAsCurrentUserAPIArguments) { expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( mockRequest ); @@ -58,6 +86,17 @@ describe('Check API keys privileges', () => { } else { expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } + + if (Array.isArray(asserts.callAsInternalUserAPIArguments)) { + for (const apiArguments of asserts.callAsInternalUserAPIArguments) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + ...apiArguments + ); + } + } else { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); }; @@ -70,16 +109,21 @@ describe('Check API keys privileges', () => { const error = Boom.notAcceptable('test not acceptable message'); getPrivilegesTest('returns error from cluster client', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => { throw error; }, - async () => {}, ], + callAsInternalUserResponses: [async () => {}], asserts: { - apiArguments: [ - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], - ['shield.getAPIKeys', { owner: true }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 406, result: error, @@ -89,14 +133,16 @@ describe('Check API keys privileges', () => { describe('success', () => { getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => ({ api_keys: [ { @@ -112,71 +158,108 @@ describe('Check API keys privileges', () => { }), ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: true }, + result: { areApiKeysEnabled: true, isAdmin: true, canManage: true }, }, }); getPrivilegesTest( - 'returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', + 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => { - throw Boom.unauthorized('api keys are not enabled'); + const error = new Error(); + (error as any).body = { + error: { + 'disabled.feature': 'api_keys', + }, + }; + throw error; }, ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: false, isAdmin: true }, + result: { areApiKeysEnabled: false, isAdmin: true, canManage: true }, }, } ); getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false }, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, index: {}, application: {}, }), - async () => ({ - api_keys: [ - { - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved', - }, + ], + callAsInternalUserResponses: [async () => ({})], + asserts: { + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: false }, + }, + }); + + getPrivilegesTest('returns canManage=true when user can manage their own API Keys', { + callAsCurrentUserResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, + index: {}, + application: {}, }), ], + callAsInternalUserResponses: [async () => ({})], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: false }, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: true }, }, }); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts index 216d1ef1bf4a4..9cccb96752772 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -8,7 +8,11 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineCheckPrivilegesRoutes({ + router, + clusterClient, + authc, +}: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key/privileges', @@ -20,26 +24,25 @@ export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefi const [ { - cluster: { manage_security: manageSecurity, manage_api_key: manageApiKey }, + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, }, - { areApiKeysEnabled }, + areApiKeysEnabled, ] = await Promise.all([ scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { - body: { cluster: ['manage_security', 'manage_api_key'] }, + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, }), - scopedClusterClient.callAsCurrentUser('shield.getAPIKeys', { owner: true }).then( - // If the API returns a truthy result that means it's enabled. - result => ({ areApiKeysEnabled: !!result }), - // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. - e => - e.message.includes('api keys are not enabled') - ? Promise.resolve({ areApiKeysEnabled: false }) - : Promise.reject(e) - ), + authc.areAPIKeysEnabled(), ]); + const isAdmin = manageSecurity || manageApiKey; + const canManage = manageSecurity || manageApiKey || manageOwnApiKey; + return response.ok({ - body: { areApiKeysEnabled, isAdmin: manageSecurity || manageApiKey }, + body: { areApiKeysEnabled, isAdmin, canManage }, }); } catch (error) { return response.customError(wrapIntoCustomErrorResponse(error)); diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 156c03e90210b..5a0401e6320b4 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -12,6 +12,7 @@ import { RequestHandlerContext, RouteConfig, } from '../../../../../../src/core/server'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; import { Authentication, AuthenticationResult, @@ -28,11 +29,13 @@ import { routeDefinitionParamsMock } from '../index.mock'; describe('Common authentication routes', () => { let router: jest.Mocked<IRouter>; let authc: jest.Mocked<Authentication>; + let license: jest.Mocked<SecurityLicense>; let mockContext: RequestHandlerContext; beforeEach(() => { const routeParamsMock = routeDefinitionParamsMock.create(); router = routeParamsMock.router; authc = routeParamsMock.authc; + license = routeParamsMock.license; mockContext = ({ licensing: { @@ -433,4 +436,61 @@ describe('Common authentication routes', () => { }); }); }); + + describe('acknowledge access agreement', () => { + let routeHandler: RequestHandler<any, any, any>; + let routeConfig: RouteConfig<any, any, any, any>; + beforeEach(() => { + const [acsRouteConfig, acsRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/security/access_agreement/acknowledge' + )!; + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + routeConfig = acsRouteConfig; + routeHandler = acsRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it(`returns 403 if current license doesn't allow access agreement acknowledgement.`, async () => { + license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + const request = httpServerMock.createKibanaRequest(); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 403, + payload: { message: `Current license doesn't support access agreement.` }, + options: { body: { message: `Current license doesn't support access agreement.` } }, + }); + }); + + it('returns 500 if acknowledge throws unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.acknowledgeAccessAgreement.mockRejectedValue(unhandledException); + + const request = httpServerMock.createKibanaRequest(); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + payload: 'Internal Error', + options: {}, + }); + }); + + it('returns 204 if successfully acknowledged.', async () => { + authc.acknowledgeAccessAgreement.mockResolvedValue(undefined); + + const request = httpServerMock.createKibanaRequest(); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 204, + options: {}, + }); + }); + }); }); diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index abab67c9cd1d2..91783140539a5 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -18,7 +18,13 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes that are common to various authentication mechanisms. */ -export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDefinitionParams) { +export function defineCommonRoutes({ + router, + authc, + basePath, + license, + logger, +}: RouteDefinitionParams) { // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. for (const path of ['/api/security/logout', '/api/security/v1/logout']) { router.get( @@ -135,4 +141,26 @@ export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDef } }) ); + + router.post( + { path: '/internal/security/access_agreement/acknowledge', validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + // If license doesn't allow access agreement we shouldn't handle request. + if (!license.getFeatures().allowAccessAgreement) { + logger.warn(`Attempted to acknowledge access agreement when license doesn't allow it.`); + return response.forbidden({ + body: { message: `Current license doesn't support access agreement.` }, + }); + } + + try { + await authc.acknowledgeAccessAgreement(request); + } catch (err) { + logger.error(err); + return response.internalError(); + } + + return response.noContent(); + }) + ); } diff --git a/x-pack/plugins/security/server/routes/licensed_route_handler.ts b/x-pack/plugins/security/server/routes/licensed_route_handler.ts index b113b2ca59e3e..d8c212aa2d217 100644 --- a/x-pack/plugins/security/server/routes/licensed_route_handler.ts +++ b/x-pack/plugins/security/server/routes/licensed_route_handler.ts @@ -4,10 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { KibanaResponseFactory, RequestHandler, RouteMethod } from 'kibana/server'; -export const createLicensedRouteHandler = <P, Q, B>(handler: RequestHandler<P, Q, B>) => { - const licensedRouteHandler: RequestHandler<P, Q, B> = (context, request, responseToolkit) => { +export const createLicensedRouteHandler = < + P, + Q, + B, + M extends RouteMethod, + R extends KibanaResponseFactory +>( + handler: RequestHandler<P, Q, B, M, R> +) => { + const licensedRouteHandler: RequestHandler<P, Q, B, M, R> = ( + context, + request, + responseToolkit + ) => { const { license } = context.licensing; const licenseCheck = license.check('security', 'basic'); if (licenseCheck.state === 'unavailable' || licenseCheck.state === 'invalid') { diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts index fd05821f9d520..c163ff4e256cd 100644 --- a/x-pack/plugins/security/server/routes/users/change_password.test.ts +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -53,7 +53,7 @@ describe('Change password', () => { now: Date.now(), idleTimeoutExpiration: null, lifespanExpiration: null, - provider: 'basic', + provider: { type: 'basic', name: 'basic' }, }); mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.test.ts b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts new file mode 100644 index 0000000000000..3d616575b8413 --- /dev/null +++ b/x-pack/plugins/security/server/routes/views/access_agreement.test.ts @@ -0,0 +1,177 @@ +/* + * 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 { + RequestHandler, + RouteConfig, + kibanaResponseFactory, + IRouter, + HttpResources, + HttpResourcesRequestHandler, + RequestHandlerContext, +} from '../../../../../../src/core/server'; +import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing'; +import { AuthenticationProvider } from '../../../common/types'; +import { ConfigType } from '../../config'; +import { defineAccessAgreementRoutes } from './access_agreement'; + +import { httpResourcesMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { Authentication } from '../../authentication'; + +describe('Access agreement view routes', () => { + let httpResources: jest.Mocked<HttpResources>; + let router: jest.Mocked<IRouter>; + let config: ConfigType; + let authc: jest.Mocked<Authentication>; + let license: jest.Mocked<SecurityLicense>; + let mockContext: RequestHandlerContext; + beforeEach(() => { + const routeParamsMock = routeDefinitionParamsMock.create(); + router = routeParamsMock.router; + httpResources = routeParamsMock.httpResources; + authc = routeParamsMock.authc; + config = routeParamsMock.config; + license = routeParamsMock.license; + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, + } as SecurityLicenseFeatures); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: 'valid' }) }, + }, + } as unknown) as RequestHandlerContext; + + defineAccessAgreementRoutes(routeParamsMock); + }); + + describe('View route', () => { + let routeHandler: HttpResourcesRequestHandler<any, any, any>; + let routeConfig: RouteConfig<any, any, any, 'get'>; + beforeEach(() => { + const [viewRouteConfig, viewRouteHandler] = httpResources.register.mock.calls.find( + ([{ path }]) => path === '/security/access_agreement' + )!; + + routeConfig = viewRouteConfig; + routeHandler = viewRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('does not render view if current license does not allow access agreement.', async () => { + const request = httpServerMock.createKibanaRequest(); + const responseFactory = httpResourcesMock.createResponseFactory(); + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await routeHandler(mockContext, request, responseFactory); + + expect(responseFactory.renderCoreApp).not.toHaveBeenCalledWith(); + expect(responseFactory.forbidden).toHaveBeenCalledTimes(1); + }); + + it('renders view.', async () => { + const request = httpServerMock.createKibanaRequest(); + const responseFactory = httpResourcesMock.createResponseFactory(); + + await routeHandler(mockContext, request, responseFactory); + + expect(responseFactory.renderCoreApp).toHaveBeenCalledWith(); + }); + }); + + describe('Access agreement state route', () => { + let routeHandler: RequestHandler<any, any, any, 'get'>; + let routeConfig: RouteConfig<any, any, any, 'get'>; + beforeEach(() => { + const [loginStateRouteConfig, loginStateRouteHandler] = router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/access_agreement/state' + )!; + + routeConfig = loginStateRouteConfig; + routeHandler = loginStateRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('returns `403` if current license does not allow access agreement.', async () => { + const request = httpServerMock.createKibanaRequest(); + + license.getFeatures.mockReturnValue({ + allowAccessAgreement: false, + } as SecurityLicenseFeatures); + + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + status: 403, + payload: { message: `Current license doesn't support access agreement.` }, + options: { body: { message: `Current license doesn't support access agreement.` } }, + }); + }); + + it('returns empty `accessAgreement` if session info is not available.', async () => { + const request = httpServerMock.createKibanaRequest(); + + authc.getSessionInfo.mockResolvedValue(null); + + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + options: { body: { accessAgreement: '' } }, + payload: { accessAgreement: '' }, + status: 200, + }); + }); + + it('returns non-empty `accessAgreement` only if it is configured.', async () => { + const request = httpServerMock.createKibanaRequest(); + + config.authc = routeDefinitionParamsMock.create({ + authc: { + providers: { + basic: { basic1: { order: 0 } }, + saml: { + saml1: { + order: 1, + realm: 'realm1', + accessAgreement: { message: 'Some access agreement' }, + }, + }, + }, + }, + }).config.authc; + + const cases: Array<[AuthenticationProvider, string]> = [ + [{ type: 'basic', name: 'basic1' }, ''], + [{ type: 'saml', name: 'saml1' }, 'Some access agreement'], + [{ type: 'unknown-type', name: 'unknown-name' }, ''], + ]; + + for (const [sessionProvider, expectedAccessAgreement] of cases) { + authc.getSessionInfo.mockResolvedValue({ + now: Date.now(), + idleTimeoutExpiration: null, + lifespanExpiration: null, + provider: sessionProvider, + }); + + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ + options: { body: { accessAgreement: expectedAccessAgreement } }, + payload: { accessAgreement: expectedAccessAgreement }, + status: 200, + }); + } + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts new file mode 100644 index 0000000000000..49e1ff42a28a2 --- /dev/null +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ConfigType } from '../../config'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for the Access Agreement view. + */ +export function defineAccessAgreementRoutes({ + authc, + httpResources, + license, + config, + router, + logger, +}: RouteDefinitionParams) { + // If license doesn't allow access agreement we shouldn't handle request. + const canHandleRequest = () => license.getFeatures().allowAccessAgreement; + + httpResources.register( + { path: '/security/access_agreement', validate: false }, + createLicensedRouteHandler(async (context, request, response) => + canHandleRequest() + ? response.renderCoreApp() + : response.forbidden({ + body: { message: `Current license doesn't support access agreement.` }, + }) + ) + ); + + router.get( + { path: '/internal/security/access_agreement/state', validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + if (!canHandleRequest()) { + return response.forbidden({ + body: { message: `Current license doesn't support access agreement.` }, + }); + } + + // It's not guaranteed that we'll have session for the authenticated user (e.g. when user is + // authenticated with the help of HTTP authentication), that means we should safely check if + // we have it and can get a corresponding configuration. + try { + const session = await authc.getSessionInfo(request); + const accessAgreement = + (session && + config.authc.providers[ + session.provider.type as keyof ConfigType['authc']['providers'] + ]?.[session.provider.name]?.accessAgreement?.message) || + ''; + + return response.ok({ body: { accessAgreement } }); + } catch (err) { + logger.error(err); + return response.internalError(); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/views/index.test.ts b/x-pack/plugins/security/server/routes/views/index.test.ts index a8e7e905b119a..7cddef9bf2b98 100644 --- a/x-pack/plugins/security/server/routes/views/index.test.ts +++ b/x-pack/plugins/security/server/routes/views/index.test.ts @@ -20,15 +20,18 @@ describe('View routes', () => { expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path)) .toMatchInlineSnapshot(` Array [ + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", "/security/overwritten_session", ] `); - expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot( - `Array []` - ); + expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` + Array [ + "/internal/security/access_agreement/state", + ] + `); }); it('registers Login routes if `basic` provider is enabled', () => { @@ -43,6 +46,7 @@ describe('View routes', () => { .toMatchInlineSnapshot(` Array [ "/login", + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", @@ -52,6 +56,7 @@ describe('View routes', () => { expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` Array [ "/internal/security/login_state", + "/internal/security/access_agreement/state", ] `); }); @@ -68,6 +73,7 @@ describe('View routes', () => { .toMatchInlineSnapshot(` Array [ "/login", + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", @@ -77,6 +83,7 @@ describe('View routes', () => { expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` Array [ "/internal/security/login_state", + "/internal/security/access_agreement/state", ] `); }); @@ -93,6 +100,7 @@ describe('View routes', () => { .toMatchInlineSnapshot(` Array [ "/login", + "/security/access_agreement", "/security/account", "/security/logged_out", "/logout", @@ -102,6 +110,7 @@ describe('View routes', () => { expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(` Array [ "/internal/security/login_state", + "/internal/security/access_agreement/state", ] `); }); diff --git a/x-pack/plugins/security/server/routes/views/index.ts b/x-pack/plugins/security/server/routes/views/index.ts index 255989dfeb90c..b9de58d47fe40 100644 --- a/x-pack/plugins/security/server/routes/views/index.ts +++ b/x-pack/plugins/security/server/routes/views/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { defineAccessAgreementRoutes } from './access_agreement'; import { defineAccountManagementRoutes } from './account_management'; import { defineLoggedOutRoutes } from './logged_out'; import { defineLoginRoutes } from './login'; @@ -20,6 +21,7 @@ export function defineViewRoutes(params: RouteDefinitionParams) { defineLoginRoutes(params); } + defineAccessAgreementRoutes(params); defineAccountManagementRoutes(params); defineLoggedOutRoutes(params); defineLogoutRoutes(params); diff --git a/x-pack/plugins/security/server/routes/views/logged_out.test.ts b/x-pack/plugins/security/server/routes/views/logged_out.test.ts index 3ff05d242d9dd..7cb73c49f9cbc 100644 --- a/x-pack/plugins/security/server/routes/views/logged_out.test.ts +++ b/x-pack/plugins/security/server/routes/views/logged_out.test.ts @@ -39,7 +39,7 @@ describe('LoggedOut view routes', () => { it('redirects user to the root page if they have a session already.', async () => { authc.getSessionInfo.mockResolvedValue({ - provider: 'basic', + provider: { type: 'basic', name: 'basic' }, now: 0, idleTimeoutExpiration: null, lifespanExpiration: null, diff --git a/x-pack/plugins/security/server/routes/views/login.test.ts b/x-pack/plugins/security/server/routes/views/login.test.ts index d43319efbdfb9..014ad390a3d53 100644 --- a/x-pack/plugins/security/server/routes/views/login.test.ts +++ b/x-pack/plugins/security/server/routes/views/login.test.ts @@ -15,7 +15,7 @@ import { RouteConfig, } from '../../../../../../src/core/server'; import { SecurityLicense } from '../../../common/licensing'; -import { LoginState } from '../../../common/login_state'; +import { LoginSelectorProvider } from '../../../common/login_state'; import { ConfigType } from '../../config'; import { defineLoginRoutes } from './login'; @@ -141,6 +141,10 @@ describe('Login view routes', () => { }); describe('Login state route', () => { + function getAuthcConfig(authcConfig: Record<string, unknown> = {}) { + return routeDefinitionParamsMock.create({ authc: { ...authcConfig } }).config.authc; + } + let routeHandler: RequestHandler<any, any, any, 'get'>; let routeConfig: RouteConfig<any, any, any, 'get'>; beforeEach(() => { @@ -159,6 +163,7 @@ describe('Login view routes', () => { it('returns only required license features.', async () => { license.getFeatures.mockReturnValue({ + allowAccessAgreement: true, allowLogin: true, allowRbac: false, allowRoleDocumentLevelSecurity: true, @@ -176,9 +181,11 @@ describe('Login view routes', () => { const expectedPayload = { allowLogin: true, layout: 'error-es-unavailable', - showLoginForm: true, requiresSecureConnection: false, - selector: { enabled: false, providers: [] }, + selector: { + enabled: false, + providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }], + }, }; await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) @@ -198,9 +205,11 @@ describe('Login view routes', () => { const expectedPayload = { allowLogin: true, layout: 'form', - showLoginForm: true, requiresSecureConnection: false, - selector: { enabled: false, providers: [] }, + selector: { + enabled: false, + providers: [{ name: 'basic', type: 'basic', usesLoginForm: true }], + }, }; await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) @@ -229,22 +238,46 @@ describe('Login view routes', () => { }); }); - it('returns `showLoginForm: true` only if either `basic` or `token` provider is enabled.', async () => { + it('returns `useLoginForm: true` for `basic` and `token` providers.', async () => { license.getFeatures.mockReturnValue({ allowLogin: true, showLogin: true } as any); const request = httpServerMock.createKibanaRequest(); const contextMock = coreMock.createRequestHandlerContext(); - const cases: Array<[boolean, ConfigType['authc']['sortedProviders']]> = [ - [false, []], - [true, [{ type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }]], - [true, [{ type: 'token', name: 'token1', options: { order: 0, showInSelector: true } }]], + const cases: Array<[LoginSelectorProvider[], ConfigType['authc']]> = [ + [[], getAuthcConfig({ providers: { basic: { basic1: { order: 0, enabled: false } } } })], + [ + [ + { + name: 'basic1', + type: 'basic', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, + ], + getAuthcConfig({ providers: { basic: { basic1: { order: 0 } } } }), + ], + [ + [ + { + name: 'token1', + type: 'token', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, + ], + getAuthcConfig({ providers: { token: { token1: { order: 0 } } } }), + ], ]; - for (const [showLoginForm, sortedProviders] of cases) { - config.authc.sortedProviders = sortedProviders; + for (const [providers, authcConfig] of cases) { + config.authc = authcConfig; - const expectedPayload = expect.objectContaining({ showLoginForm }); + const expectedPayload = expect.objectContaining({ + selector: { enabled: false, providers }, + }); await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) ).resolves.toEqual({ @@ -261,81 +294,142 @@ describe('Login view routes', () => { const request = httpServerMock.createKibanaRequest(); const contextMock = coreMock.createRequestHandlerContext(); - const cases: Array<[ - boolean, - ConfigType['authc']['sortedProviders'], - LoginState['selector']['providers'] - ]> = [ - // selector is disabled, providers shouldn't be returned. + const cases: Array<[ConfigType['authc'], LoginSelectorProvider[]]> = [ + // selector is disabled, multiple providers, but only basic provider should be returned. [ - false, + getAuthcConfig({ + selector: { enabled: false }, + providers: { + basic: { basic1: { order: 0 } }, + saml: { saml1: { order: 1, realm: 'realm1' } }, + }, + }), [ - { type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }, - { type: 'saml', name: 'saml1', options: { order: 1, showInSelector: true } }, + { + name: 'basic1', + type: 'basic', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, ], - [], ], - // selector is enabled, but only basic/token is available, providers shouldn't be returned. + // selector is enabled, but only basic/token is available and should be returned. [ - true, - [{ type: 'basic', name: 'basic1', options: { order: 0, showInSelector: true } }], - [], + getAuthcConfig({ + selector: { enabled: true }, + providers: { basic: { basic1: { order: 0 } } }, + }), + [ + { + name: 'basic1', + type: 'basic', + usesLoginForm: true, + icon: 'logoElastic', + description: 'Log in with Elasticsearch', + }, + ], ], - // selector is enabled, non-basic/token providers should be returned + // selector is enabled, all providers should be returned [ - true, + getAuthcConfig({ + selector: { enabled: true }, + providers: { + basic: { + basic1: { + order: 0, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'logoElastic', + }, + }, + saml: { + saml1: { order: 1, description: 'some-desc2', realm: 'realm1', icon: 'some-icon2' }, + saml2: { order: 2, description: 'some-desc3', hint: 'some-hint3', realm: 'realm2' }, + }, + }, + }), [ { type: 'basic', name: 'basic1', - options: { order: 0, showInSelector: true, description: 'some-desc1' }, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'logoElastic', + usesLoginForm: true, }, { type: 'saml', name: 'saml1', - options: { order: 1, showInSelector: true, description: 'some-desc2' }, + description: 'some-desc2', + icon: 'some-icon2', + usesLoginForm: false, }, { type: 'saml', name: 'saml2', - options: { order: 2, showInSelector: true, description: 'some-desc3' }, + description: 'some-desc3', + hint: 'some-hint3', + usesLoginForm: false, }, ], - [ - { type: 'saml', name: 'saml1', description: 'some-desc2' }, - { type: 'saml', name: 'saml2', description: 'some-desc3' }, - ], ], - // selector is enabled, only non-basic/token providers that are enabled in selector should be returned. + // selector is enabled, only providers that are enabled should be returned. [ - true, + getAuthcConfig({ + selector: { enabled: true }, + providers: { + basic: { + basic1: { + order: 0, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'some-icon1', + }, + }, + saml: { + saml1: { + order: 1, + description: 'some-desc2', + realm: 'realm1', + showInSelector: false, + }, + saml2: { + order: 2, + description: 'some-desc3', + hint: 'some-hint3', + icon: 'some-icon3', + realm: 'realm2', + }, + }, + }, + }), [ { type: 'basic', name: 'basic1', - options: { order: 0, showInSelector: true, description: 'some-desc1' }, - }, - { - type: 'saml', - name: 'saml1', - options: { order: 1, showInSelector: false, description: 'some-desc2' }, + description: 'some-desc1', + hint: 'some-hint1', + icon: 'some-icon1', + usesLoginForm: true, }, { type: 'saml', name: 'saml2', - options: { order: 2, showInSelector: true, description: 'some-desc3' }, + description: 'some-desc3', + hint: 'some-hint3', + icon: 'some-icon3', + usesLoginForm: false, }, ], - [{ type: 'saml', name: 'saml2', description: 'some-desc3' }], ], ]; - for (const [selectorEnabled, sortedProviders, expectedProviders] of cases) { - config.authc.selector.enabled = selectorEnabled; - config.authc.sortedProviders = sortedProviders; + for (const [authcConfig, expectedProviders] of cases) { + config.authc = authcConfig; const expectedPayload = expect.objectContaining({ - selector: { enabled: selectorEnabled, providers: expectedProviders }, + selector: { enabled: authcConfig.selector.enabled, providers: expectedProviders }, }); await expect( routeHandler({ core: contextMock } as any, request, kibanaResponseFactory) diff --git a/x-pack/plugins/security/server/routes/views/login.ts b/x-pack/plugins/security/server/routes/views/login.ts index 4d6747de713f7..f72facb2e24cc 100644 --- a/x-pack/plugins/security/server/routes/views/login.ts +++ b/x-pack/plugins/security/server/routes/views/login.ts @@ -55,15 +55,16 @@ export function defineLoginRoutes({ const { allowLogin, layout = 'form' } = license.getFeatures(); const { sortedProviders, selector } = config.authc; - let showLoginForm = false; const providers = []; - for (const { type, name, options } of sortedProviders) { - if (options.showInSelector) { - if (type === 'basic' || type === 'token') { - showLoginForm = true; - } else if (selector.enabled) { - providers.push({ type, name, description: options.description }); - } + for (const { type, name } of sortedProviders) { + // Since `config.authc.sortedProviders` is based on `config.authc.providers` config we can + // be sure that config is present for every provider in `config.authc.sortedProviders`. + const { showInSelector, description, hint, icon } = config.authc.providers[type]?.[name]!; + + // Include provider into the list if either selector is enabled or provider uses login form. + const usesLoginForm = type === 'basic' || type === 'token'; + if (showInSelector && (usesLoginForm || selector.enabled)) { + providers.push({ type, name, usesLoginForm, description, hint, icon }); } } @@ -71,7 +72,7 @@ export function defineLoginRoutes({ allowLogin, layout, requiresSecureConnection: config.secureCookies, - showLoginForm, + loginHelp: config.loginHelp, selector: { enabled: selector.enabled, providers }, }; diff --git a/x-pack/plugins/siem/.gitattributes b/x-pack/plugins/siem/.gitattributes index 96ab5dadbda10..8dc2df600b211 100644 --- a/x-pack/plugins/siem/.gitattributes +++ b/x-pack/plugins/siem/.gitattributes @@ -1,4 +1,6 @@ # Auto-collapse generated files in GitHub # https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github x-pack/plugins/siem/server/graphql/types.ts linguist-generated=true +x-pack/plugins/siem/public/graphql/types.ts linguist-generated=true +x-pack/plugins/siem/public/graphql/introspection.json linguist-generated=true diff --git a/x-pack/plugins/siem/common/constants.ts b/x-pack/plugins/siem/common/constants.ts index edde5c6b8fa0d..bcf5c78be3644 100644 --- a/x-pack/plugins/siem/common/constants.ts +++ b/x-pack/plugins/siem/common/constants.ts @@ -6,6 +6,8 @@ export const APP_ID = 'siem'; export const APP_NAME = 'SIEM'; +export const APP_ICON = 'securityAnalyticsApp'; +export const APP_PATH = `/app/${APP_ID}`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; diff --git a/x-pack/plugins/siem/common/default_index_pattern.ts b/x-pack/plugins/siem/common/default_index_pattern.ts deleted file mode 100644 index 4d53aeb000c55..0000000000000 --- a/x-pack/plugins/siem/common/default_index_pattern.ts +++ /dev/null @@ -1,15 +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. - */ - -/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ -export const defaultIndexPattern = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', -]; diff --git a/x-pack/plugins/siem/common/types/timeline/index.ts b/x-pack/plugins/siem/common/types/timeline/index.ts new file mode 100644 index 0000000000000..55b4f9c6aca4d --- /dev/null +++ b/x-pack/plugins/siem/common/types/timeline/index.ts @@ -0,0 +1,280 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; +import { SavedObjectsClient } from 'kibana/server'; + +import { unionWithNullType } from '../../utility_types'; +import { NoteSavedObject, NoteSavedObjectToReturnRuntimeType } from './note'; +import { PinnedEventToReturnSavedObjectRuntimeType, PinnedEventSavedObject } from './pinned_event'; + +/* + * ColumnHeader Types + */ +const SavedColumnHeaderRuntimeType = runtimeTypes.partial({ + aggregatable: unionWithNullType(runtimeTypes.boolean), + category: unionWithNullType(runtimeTypes.string), + columnHeaderType: unionWithNullType(runtimeTypes.string), + description: unionWithNullType(runtimeTypes.string), + example: unionWithNullType(runtimeTypes.string), + indexes: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + placeholder: unionWithNullType(runtimeTypes.string), + searchable: unionWithNullType(runtimeTypes.boolean), + type: unionWithNullType(runtimeTypes.string), +}); + +/* + * DataProvider Types + */ +const SavedDataProviderQueryMatchBasicRuntimeType = runtimeTypes.partial({ + field: unionWithNullType(runtimeTypes.string), + displayField: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), + displayValue: unionWithNullType(runtimeTypes.string), + operator: unionWithNullType(runtimeTypes.string), +}); + +const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), +}); + +const SavedDataProviderRuntimeType = runtimeTypes.partial({ + id: unionWithNullType(runtimeTypes.string), + name: unionWithNullType(runtimeTypes.string), + enabled: unionWithNullType(runtimeTypes.boolean), + excluded: unionWithNullType(runtimeTypes.boolean), + kqlQuery: unionWithNullType(runtimeTypes.string), + queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), + and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), +}); + +/* + * Filters Types + */ +const SavedFilterMetaRuntimeType = runtimeTypes.partial({ + alias: unionWithNullType(runtimeTypes.string), + controlledBy: unionWithNullType(runtimeTypes.string), + disabled: unionWithNullType(runtimeTypes.boolean), + field: unionWithNullType(runtimeTypes.string), + formattedValue: unionWithNullType(runtimeTypes.string), + index: unionWithNullType(runtimeTypes.string), + key: unionWithNullType(runtimeTypes.string), + negate: unionWithNullType(runtimeTypes.boolean), + params: unionWithNullType(runtimeTypes.string), + type: unionWithNullType(runtimeTypes.string), + value: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterRuntimeType = runtimeTypes.partial({ + exists: unionWithNullType(runtimeTypes.string), + meta: unionWithNullType(SavedFilterMetaRuntimeType), + match_all: unionWithNullType(runtimeTypes.string), + missing: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + range: unionWithNullType(runtimeTypes.string), + script: unionWithNullType(runtimeTypes.string), +}); + +/* + * kqlQuery -> filterQuery Types + */ +const SavedKueryFilterQueryRuntimeType = runtimeTypes.partial({ + kind: unionWithNullType(runtimeTypes.string), + expression: unionWithNullType(runtimeTypes.string), +}); + +const SavedSerializedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + kuery: unionWithNullType(SavedKueryFilterQueryRuntimeType), + serializedQuery: unionWithNullType(runtimeTypes.string), +}); + +const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ + filterQuery: unionWithNullType(SavedSerializedFilterQueryQueryRuntimeType), +}); + +/* + * DatePicker Range Types + */ +const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ + start: unionWithNullType(runtimeTypes.number), + end: unionWithNullType(runtimeTypes.number), +}); + +/* + * Favorite Types + */ +const SavedFavoriteRuntimeType = runtimeTypes.partial({ + keySearch: unionWithNullType(runtimeTypes.string), + favoriteDate: unionWithNullType(runtimeTypes.number), + fullName: unionWithNullType(runtimeTypes.string), + userName: unionWithNullType(runtimeTypes.string), +}); + +/* + * Sort Types + */ +const SavedSortRuntimeType = runtimeTypes.partial({ + columnId: unionWithNullType(runtimeTypes.string), + sortDirection: unionWithNullType(runtimeTypes.string), +}); + +/* + * Timeline Types + */ + +export enum TimelineType { + default = 'default', + template = 'template', +} + +export const TimelineTypeLiteralRt = runtimeTypes.union([ + runtimeTypes.literal(TimelineType.template), + runtimeTypes.literal(TimelineType.default), +]); + +export const SavedTimelineRuntimeType = runtimeTypes.partial({ + columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), + dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), + description: unionWithNullType(runtimeTypes.string), + eventType: unionWithNullType(runtimeTypes.string), + favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), + filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), + kqlMode: unionWithNullType(runtimeTypes.string), + kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), + title: unionWithNullType(runtimeTypes.string), + templateTimelineId: unionWithNullType(runtimeTypes.string), + templateTimelineVersion: unionWithNullType(runtimeTypes.number), + timelineType: unionWithNullType(TimelineTypeLiteralRt), + dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), + savedQueryId: unionWithNullType(runtimeTypes.string), + sort: unionWithNullType(SavedSortRuntimeType), + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), +}); + +export interface SavedTimeline extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} + +export interface SavedTimelineNote extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} + +/** + * Timeline Saved object type with metadata + */ + +export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedTimelineRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + savedObjectId: runtimeTypes.string, + }), +]); + +export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ + SavedTimelineRuntimeType, + runtimeTypes.type({ + savedObjectId: runtimeTypes.string, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + noteIds: runtimeTypes.array(runtimeTypes.string), + notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), + pinnedEventIds: runtimeTypes.array(runtimeTypes.string), + pinnedEventsSaveObject: runtimeTypes.array(PinnedEventToReturnSavedObjectRuntimeType), + }), +]); + +export interface TimelineSavedObject + extends runtimeTypes.TypeOf<typeof TimelineSavedToReturnObjectRuntimeType> {} + +/** + * All Timeline Saved object type with metadata + */ +export const TimelineResponseType = runtimeTypes.type({ + data: runtimeTypes.type({ + persistTimeline: runtimeTypes.intersection([ + runtimeTypes.partial({ + code: unionWithNullType(runtimeTypes.number), + message: unionWithNullType(runtimeTypes.string), + }), + runtimeTypes.type({ + timeline: TimelineSavedToReturnObjectRuntimeType, + }), + ]), + }), +}); + +export interface TimelineResponse extends runtimeTypes.TypeOf<typeof TimelineResponseType> {} + +/** + * All Timeline Saved object type with metadata + */ + +export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ + total: runtimeTypes.number, + data: TimelineSavedToReturnObjectRuntimeType, +}); + +export interface AllTimelineSavedObject + extends runtimeTypes.TypeOf<typeof AllTimelineSavedObjectRuntimeType> {} + +/** + * Import/export timelines + */ + +export type ExportTimelineSavedObjectsClient = Pick< + SavedObjectsClient, + | 'get' + | 'errors' + | 'create' + | 'bulkCreate' + | 'delete' + | 'find' + | 'bulkGet' + | 'update' + | 'bulkUpdate' +>; + +export type ExportedGlobalNotes = Array<Exclude<NoteSavedObject, 'eventId'>>; +export type ExportedEventNotes = NoteSavedObject[]; + +export interface ExportedNotes { + eventNotes: ExportedEventNotes; + globalNotes: ExportedGlobalNotes; +} + +export type ExportedTimelines = TimelineSavedObject & + ExportedNotes & { + pinnedEventIds: string[]; + }; + +export interface ExportTimelineNotFoundError { + statusCode: number; + message: string; +} + +export interface BulkGetInput { + type: string; + id: string; +} + +export type NotesAndPinnedEventsByTimelineId = Record< + string, + { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } +>; diff --git a/x-pack/plugins/siem/common/types/timeline/note/index.ts b/x-pack/plugins/siem/common/types/timeline/note/index.ts new file mode 100644 index 0000000000000..c8e674997c19c --- /dev/null +++ b/x-pack/plugins/siem/common/types/timeline/note/index.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; + +import { unionWithNullType } from '../../../utility_types'; + +/* + * Note Types + */ +export const SavedNoteRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + timelineId: unionWithNullType(runtimeTypes.string), + }), + runtimeTypes.partial({ + eventId: unionWithNullType(runtimeTypes.string), + note: unionWithNullType(runtimeTypes.string), + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface SavedNote extends runtimeTypes.TypeOf<typeof SavedNoteRuntimeType> {} + +/** + * Note Saved object type with metadata + */ + +export const NoteSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedNoteRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + noteId: runtimeTypes.string, + timelineVersion: runtimeTypes.union([ + runtimeTypes.string, + runtimeTypes.null, + runtimeTypes.undefined, + ]), + }), +]); + +export const NoteSavedObjectToReturnRuntimeType = runtimeTypes.intersection([ + SavedNoteRuntimeType, + runtimeTypes.type({ + noteId: runtimeTypes.string, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface NoteSavedObject + extends runtimeTypes.TypeOf<typeof NoteSavedObjectToReturnRuntimeType> {} diff --git a/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts b/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts new file mode 100644 index 0000000000000..89a619598f7c1 --- /dev/null +++ b/x-pack/plugins/siem/common/types/timeline/pinned_event/index.ts @@ -0,0 +1,59 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import * as runtimeTypes from 'io-ts'; + +import { unionWithNullType } from '../../../utility_types'; + +/* + * Note Types + */ +export const SavedPinnedEventRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + timelineId: runtimeTypes.string, + eventId: runtimeTypes.string, + }), + runtimeTypes.partial({ + created: unionWithNullType(runtimeTypes.number), + createdBy: unionWithNullType(runtimeTypes.string), + updated: unionWithNullType(runtimeTypes.number), + updatedBy: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface SavedPinnedEvent extends runtimeTypes.TypeOf<typeof SavedPinnedEventRuntimeType> {} + +/** + * Note Saved object type with metadata + */ + +export const PinnedEventSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + id: runtimeTypes.string, + attributes: SavedPinnedEventRuntimeType, + version: runtimeTypes.string, + }), + runtimeTypes.partial({ + pinnedEventId: unionWithNullType(runtimeTypes.string), + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export const PinnedEventToReturnSavedObjectRuntimeType = runtimeTypes.intersection([ + runtimeTypes.type({ + pinnedEventId: runtimeTypes.string, + version: runtimeTypes.string, + }), + SavedPinnedEventRuntimeType, + runtimeTypes.partial({ + timelineVersion: unionWithNullType(runtimeTypes.string), + }), +]); + +export interface PinnedEventSavedObject + extends runtimeTypes.TypeOf<typeof PinnedEventToReturnSavedObjectRuntimeType> {} diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts index c7bbdbfccf082..a12dd926a9181 100644 --- a/x-pack/plugins/siem/common/utility_types.ts +++ b/x-pack/plugins/siem/common/utility_types.ts @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import * as runtimeTypes from 'io-ts'; import { ReactNode } from 'react'; -export type Pick3<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]> = { - [P1 in K1]: { [P2 in K2]: { [P3 in K3]: T[K1][K2][P3] } }; -}; - -export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; - // This type is for typing EuiDescriptionList export interface DescriptionList { title: NonNullable<ReactNode>; description: NonNullable<ReactNode>; } + +export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) => + runtimeTypes.union([type, runtimeTypes.null]); diff --git a/x-pack/plugins/siem/cypress/integration/cases.spec.ts b/x-pack/plugins/siem/cypress/integration/cases.spec.ts new file mode 100644 index 0000000000000..f541555d56440 --- /dev/null +++ b/x-pack/plugins/siem/cypress/integration/cases.spec.ts @@ -0,0 +1,115 @@ +/* + * 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 { case1 } from '../objects/case'; + +import { + ALL_CASES_CLOSE_ACTION, + ALL_CASES_CLOSED_CASES_COUNT, + ALL_CASES_CLOSED_CASES_STATS, + ALL_CASES_COMMENTS_COUNT, + ALL_CASES_DELETE_ACTION, + ALL_CASES_NAME, + ALL_CASES_OPEN_CASES_COUNT, + ALL_CASES_OPEN_CASES_STATS, + ALL_CASES_OPENED_ON, + ALL_CASES_PAGE_TITLE, + ALL_CASES_REPORTER, + ALL_CASES_REPORTERS_COUNT, + ALL_CASES_SERVICE_NOW_INCIDENT, + ALL_CASES_TAGS, + ALL_CASES_TAGS_COUNT, +} from '../screens/all_cases'; +import { + ACTION, + CASE_DETAILS_DESCRIPTION, + CASE_DETAILS_PAGE_TITLE, + CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN, + CASE_DETAILS_STATUS, + CASE_DETAILS_TAGS, + CASE_DETAILS_TIMELINE_MARKDOWN, + CASE_DETAILS_USER_ACTION, + CASE_DETAILS_USERNAMES, + PARTICIPANTS, + REPORTER, + USER, +} from '../screens/case_details'; +import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens/timeline'; + +import { goToCaseDetails, goToCreateNewCase } from '../tasks/all_cases'; +import { openCaseTimeline } from '../tasks/case_details'; +import { backToCases, createNewCase } from '../tasks/create_new_case'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; + +import { CASES } from '../urls/navigation'; + +describe('Cases', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Creates a new case with timeline and opens the timeline', () => { + loginAndWaitForPageWithoutDateRange(CASES); + goToCreateNewCase(); + createNewCase(case1); + backToCases(); + + cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta'); + cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1'); + cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', 'Closed cases0'); + cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open cases (1)'); + cy.get(ALL_CASES_CLOSED_CASES_COUNT).should('have.text', 'Closed cases (0)'); + cy.get(ALL_CASES_REPORTERS_COUNT).should('have.text', 'Reporter1'); + cy.get(ALL_CASES_TAGS_COUNT).should('have.text', 'Tags2'); + cy.get(ALL_CASES_NAME).should('have.text', case1.name); + cy.get(ALL_CASES_REPORTER).should('have.text', case1.reporter); + case1.tags.forEach((tag, index) => { + cy.get(ALL_CASES_TAGS(index)).should('have.text', tag); + }); + cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); + cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago'); + cy.get(ALL_CASES_SERVICE_NOW_INCIDENT).should('have.text', 'Not pushed'); + cy.get(ALL_CASES_DELETE_ACTION).should('exist'); + cy.get(ALL_CASES_CLOSE_ACTION).should('exist'); + + goToCaseDetails(); + + const expectedTags = case1.tags.join(''); + cy.get(CASE_DETAILS_PAGE_TITLE).should('have.text', case1.name); + cy.get(CASE_DETAILS_STATUS).should('have.text', 'open'); + cy.get(CASE_DETAILS_USER_ACTION) + .eq(USER) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_USER_ACTION) + .eq(ACTION) + .should('have.text', 'added description'); + cy.get(CASE_DETAILS_DESCRIPTION).should( + 'have.text', + `${case1.description} ${case1.timeline.title}` + ); + cy.get(CASE_DETAILS_USERNAMES) + .eq(REPORTER) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_USERNAMES) + .eq(PARTICIPANTS) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_TAGS).should('have.text', expectedTags); + cy.get(CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN).should('have.attr', 'disabled'); + cy.get(CASE_DETAILS_TIMELINE_MARKDOWN).then($element => { + const timelineLink = $element.prop('href').match(/http(s?):\/\/\w*:\w*(\S*)/)[0]; + openCaseTimeline(timelineLink); + + cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title); + cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description); + cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query); + }); + }); +}); diff --git a/x-pack/plugins/siem/cypress/objects/case.ts b/x-pack/plugins/siem/cypress/objects/case.ts new file mode 100644 index 0000000000000..1c7bc34bca417 --- /dev/null +++ b/x-pack/plugins/siem/cypress/objects/case.ts @@ -0,0 +1,29 @@ +/* + * 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 { Timeline } from './timeline'; + +export interface TestCase { + name: string; + tags: string[]; + description: string; + timeline: Timeline; + reporter: string; +} + +const caseTimeline: Timeline = { + title: 'SIEM test', + description: 'description', + query: 'host.name:*', +}; + +export const case1: TestCase = { + name: 'This is the title of the case', + tags: ['Tag1', 'Tag2'], + description: 'This is the case description', + timeline: caseTimeline, + reporter: 'elastic', +}; diff --git a/x-pack/plugins/siem/cypress/objects/timeline.ts b/x-pack/plugins/siem/cypress/objects/timeline.ts index bca99bfa9266a..060a1376b46ce 100644 --- a/x-pack/plugins/siem/cypress/objects/timeline.ts +++ b/x-pack/plugins/siem/cypress/objects/timeline.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Timeline { +export interface Timeline { title: string; + description: string; query: string; } diff --git a/x-pack/plugins/siem/cypress/screens/all_cases.ts b/x-pack/plugins/siem/cypress/screens/all_cases.ts new file mode 100644 index 0000000000000..b1e4c66515352 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/all_cases.ts @@ -0,0 +1,41 @@ +/* + * 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 const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]'; + +export const ALL_CASES_CLOSED_CASES_COUNT = '[data-test-subj="closed-case-count"]'; + +export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"]'; + +export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]'; + +export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn"]'; + +export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]'; + +export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]'; + +export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="open-case-count"]'; + +export const ALL_CASES_OPEN_CASES_STATS = '[data-test-subj="openStatsHeader"]'; + +export const ALL_CASES_OPENED_ON = '[data-test-subj="case-table-column-createdAt"]'; + +export const ALL_CASES_PAGE_TITLE = '[data-test-subj="header-page-title"]'; + +export const ALL_CASES_REPORTER = '[data-test-subj="case-table-column-createdBy"]'; + +export const ALL_CASES_REPORTERS_COUNT = + '[data-test-subj="options-filter-popover-button-Reporter"]'; + +export const ALL_CASES_SERVICE_NOW_INCIDENT = + '[data-test-subj="case-table-column-external-notPushed"]'; + +export const ALL_CASES_TAGS = (index: number) => { + return `[data-test-subj="case-table-column-tags-${index}"]`; +}; + +export const ALL_CASES_TAGS_COUNT = '[data-test-subj="options-filter-popover-button-Tags"]'; diff --git a/x-pack/plugins/siem/cypress/screens/case_details.ts b/x-pack/plugins/siem/cypress/screens/case_details.ts new file mode 100644 index 0000000000000..3bd180b1d588f --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/case_details.ts @@ -0,0 +1,29 @@ +/* + * 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 const ACTION = 2; + +export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="markdown-root"]'; + +export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]'; + +export const CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN = '[data-test-subj="push-to-service-now"]'; + +export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status"]'; + +export const CASE_DETAILS_TAGS = '[data-test-subj="case-tags"]'; + +export const CASE_DETAILS_TIMELINE_MARKDOWN = '[data-test-subj="markdown-link"]'; + +export const CASE_DETAILS_USER_ACTION = '[data-test-subj="user-action-title"] .euiFlexItem'; + +export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]'; + +export const PARTICIPANTS = 1; + +export const REPORTER = 0; + +export const USER = 1; diff --git a/x-pack/plugins/siem/cypress/screens/create_new_case.ts b/x-pack/plugins/siem/cypress/screens/create_new_case.ts new file mode 100644 index 0000000000000..6e2beb78fff19 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/create_new_case.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]'; + +export const DESCRIPTION_INPUT = + '[data-test-subj="caseDescription"] [data-test-subj="textAreaInput"]'; + +export const INSERT_TIMELINE_BTN = '[data-test-subj="insert-timeline-button"]'; + +export const LOADING_SPINNER = '[data-test-subj="create-case-loading-spinner"]'; + +export const SUBMIT_BTN = '[data-test-subj="create-case-submit"]'; + +export const TAGS_INPUT = '[data-test-subj="caseTags"] [data-test-subj="comboBoxSearchInput"]'; + +export const TIMELINE = '[data-test-subj="timeline"]'; + +export const TIMELINE_SEARCHBOX = '[data-test-subj="timeline-super-select-search-box"]'; + +export const TITLE_INPUT = '[data-test-subj="caseTitle"] [data-test-subj="input"]'; diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts index 53d8273d9ce6b..58d2568084f7c 100644 --- a/x-pack/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/plugins/siem/cypress/screens/timeline.ts @@ -42,6 +42,8 @@ export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]' export const TIMELINE_NOT_READY_TO_DROP_BUTTON = '[data-test-subj="flyout-button-not-ready-to-drop"]'; +export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; + export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; diff --git a/x-pack/plugins/siem/cypress/tasks/all_cases.ts b/x-pack/plugins/siem/cypress/tasks/all_cases.ts new file mode 100644 index 0000000000000..f374532201324 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/all_cases.ts @@ -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 { ALL_CASES_NAME, ALL_CASES_CREATE_NEW_CASE_BTN } from '../screens/all_cases'; + +export const goToCreateNewCase = () => { + cy.get(ALL_CASES_CREATE_NEW_CASE_BTN).click({ force: true }); +}; + +export const goToCaseDetails = () => { + cy.get(ALL_CASES_NAME).click({ force: true }); +}; diff --git a/x-pack/plugins/siem/cypress/tasks/case_details.ts b/x-pack/plugins/siem/cypress/tasks/case_details.ts new file mode 100644 index 0000000000000..a28f8b8010adb --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/case_details.ts @@ -0,0 +1,14 @@ +/* + * 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 { TIMELINE_TITLE } from '../screens/timeline'; + +export const openCaseTimeline = (link: string) => { + cy.visit('/app/kibana'); + cy.visit(link); + cy.contains('a', 'SIEM'); + cy.get(TIMELINE_TITLE).should('exist'); +}; diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_case.ts b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts new file mode 100644 index 0000000000000..b7078a1033de8 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TestCase } from '../objects/case'; + +import { + BACK_TO_CASES_BTN, + DESCRIPTION_INPUT, + SUBMIT_BTN, + INSERT_TIMELINE_BTN, + LOADING_SPINNER, + TAGS_INPUT, + TIMELINE, + TIMELINE_SEARCHBOX, + TITLE_INPUT, +} from '../screens/create_new_case'; + +export const backToCases = () => { + cy.get(BACK_TO_CASES_BTN).click({ force: true }); +}; + +export const createNewCase = (newCase: TestCase) => { + cy.get(TITLE_INPUT).type(newCase.name, { force: true }); + newCase.tags.forEach(tag => { + cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); + }); + cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); + + cy.get(INSERT_TIMELINE_BTN).click({ force: true }); + cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`); + cy.get(TIMELINE).should('be.visible'); + cy.get(TIMELINE) + .eq(1) + .click({ force: true }); + + cy.get(SUBMIT_BTN).click({ force: true }); + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); +}; diff --git a/x-pack/plugins/siem/cypress/urls/navigation.ts b/x-pack/plugins/siem/cypress/urls/navigation.ts index 5e65e5aa34c18..263469a4dbaed 100644 --- a/x-pack/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/plugins/siem/cypress/urls/navigation.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CASES = '/app/siem#/case'; export const DETECTIONS = 'app/siem#/detections'; export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { diff --git a/x-pack/plugins/siem/kibana.json b/x-pack/plugins/siem/kibana.json index 1eb1a7dbde876..39e50838c1c97 100644 --- a/x-pack/plugins/siem/kibana.json +++ b/x-pack/plugins/siem/kibana.json @@ -3,8 +3,27 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "siem"], - "requiredPlugins": ["actions", "alerting", "features", "licensing"], - "optionalPlugins": ["encryptedSavedObjects", "ml", "security", "spaces"], + "requiredPlugins": [ + "actions", + "alerting", + "data", + "embeddable", + "features", + "home", + "inspector", + "licensing", + "maps", + "triggers_actions_ui", + "uiActions" + ], + "optionalPlugins": [ + "encryptedSavedObjects", + "ml", + "newsfeed", + "security", + "spaces", + "usageCollection" + ], "server": true, - "ui": false + "ui": true } diff --git a/x-pack/plugins/siem/package.json b/x-pack/plugins/siem/package.json index 1fcef46243628..829332918d3c4 100644 --- a/x-pack/plugins/siem/package.json +++ b/x-pack/plugins/siem/package.json @@ -5,7 +5,7 @@ "private": true, "license": "Elastic-License", "scripts": { - "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js & node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;", @@ -15,6 +15,7 @@ "@types/lodash": "^4.14.110" }, "dependencies": { - "lodash": "^4.17.15" + "lodash": "^4.17.15", + "react-markdown": "^4.0.6" } } diff --git a/x-pack/plugins/siem/public/app/app.tsx b/x-pack/plugins/siem/public/app/app.tsx new file mode 100644 index 0000000000000..6e2a4642f99a4 --- /dev/null +++ b/x-pack/plugins/siem/public/app/app.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createHashHistory, History } from 'history'; +import React, { memo, useMemo, FC } from 'react'; +import { ApolloProvider } from 'react-apollo'; +import { Store } from 'redux'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { ThemeProvider } from 'styled-components'; + +import { EuiErrorBoundary } from '@elastic/eui'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { BehaviorSubject } from 'rxjs'; +import { pluck } from 'rxjs/operators'; + +import { KibanaContextProvider, useKibana, useUiSetting$ } from '../lib/kibana'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; + +import { DEFAULT_DARK_MODE } from '../../common/constants'; +import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; +import { compose } from '../lib/compose/kibana_compose'; +import { AppFrontendLibs, AppApolloClient } from '../lib/lib'; +import { StartServices } from '../plugin'; +import { PageRouter } from '../routes'; +import { createStore, createInitialState } from '../store'; +import { GlobalToaster, ManageGlobalToaster } from '../components/toasters'; +import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabilities_provider'; + +import { ApolloClientContext } from '../utils/apollo_context'; + +interface AppPluginRootComponentProps { + apolloClient: AppApolloClient; + history: History; + store: Store; + theme: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +const AppPluginRootComponent: React.FC<AppPluginRootComponentProps> = ({ + theme, + store, + apolloClient, + history, +}) => ( + <ManageGlobalToaster> + <ReduxStoreProvider store={store}> + <ApolloProvider client={apolloClient}> + <ApolloClientContext.Provider value={apolloClient}> + <ThemeProvider theme={theme}> + <MlCapabilitiesProvider> + <PageRouter history={history} /> + </MlCapabilitiesProvider> + </ThemeProvider> + <ErrorToastDispatcher /> + <GlobalToaster /> + </ApolloClientContext.Provider> + </ApolloProvider> + </ReduxStoreProvider> + </ManageGlobalToaster> +); + +const AppPluginRoot = memo(AppPluginRootComponent); + +const StartAppComponent: FC<AppFrontendLibs> = libs => { + const { i18n } = useKibana().services; + const history = createHashHistory(); + const libs$ = new BehaviorSubject(libs); + const store = createStore(createInitialState(), libs$.pipe(pluck('apolloClient'))); + const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); + const theme = useMemo( + () => ({ + eui: darkMode ? euiDarkVars : euiLightVars, + darkMode, + }), + [darkMode] + ); + + return ( + <EuiErrorBoundary> + <i18n.Context> + <AppPluginRoot + store={store} + apolloClient={libs.apolloClient} + history={history} + theme={theme} + /> + </i18n.Context> + </EuiErrorBoundary> + ); +}; + +const StartApp = memo(StartAppComponent); + +interface SiemAppComponentProps { + services: StartServices; +} + +const SiemAppComponent: React.FC<SiemAppComponentProps> = ({ services }) => ( + <KibanaContextProvider + services={{ + appName: 'siem', + storage: new Storage(localStorage), + ...services, + }} + > + <StartApp {...compose(services)} /> + </KibanaContextProvider> +); + +export const SiemApp = memo(SiemAppComponent); diff --git a/x-pack/plugins/siem/public/app/index.tsx b/x-pack/plugins/siem/public/app/index.tsx new file mode 100644 index 0000000000000..7275a718564ef --- /dev/null +++ b/x-pack/plugins/siem/public/app/index.tsx @@ -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. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { AppMountParameters } from '../../../../../src/core/public'; +import { StartServices } from '../plugin'; +import { SiemApp } from './app'; + +export const renderApp = (services: StartServices, { element }: AppMountParameters) => { + render(<SiemApp services={services} />, element); + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx rename to x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx index dd608babef48f..d545a071c3ea6 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; -import { Filter } from '../../../../../../../src/plugins/data/public'; +import { Filter } from '../../../../../../src/plugins/data/public'; import { StatefulEventsViewer } from '../events_viewer'; import * as i18n from './translations'; import { alertsDefaultModel } from './default_headers'; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.ts b/x-pack/plugins/siem/public/components/alerts_viewer/default_headers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/default_headers.ts rename to x-pack/plugins/siem/public/components/alerts_viewer/default_headers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts b/x-pack/plugins/siem/public/components/alerts_viewer/histogram_configs.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/histogram_configs.ts rename to x-pack/plugins/siem/public/components/alerts_viewer/histogram_configs.ts diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx new file mode 100644 index 0000000000000..957feb6244792 --- /dev/null +++ b/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx @@ -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 React, { useEffect, useCallback, useMemo } from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { AlertsComponentsQueryProps } from './types'; +import { AlertsTable } from './alerts_table'; +import * as i18n from './translations'; +import { useUiSetting$ } from '../../lib/kibana'; +import { MatrixHistogramContainer } from '../matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; +const ID = 'alertsOverTimeQuery'; + +export const AlertsView = ({ + deleteQuery, + endDate, + filterQuery, + pageFilters, + setQuery, + startDate, + type, +}: AlertsComponentsQueryProps) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const getSubtitle = useCallback( + (totalCount: number) => + `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( + totalCount + )}`, + [] + ); + const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + subtitle: getSubtitle, + }), + [getSubtitle] + ); + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + return ( + <> + <MatrixHistogramContainer + endDate={endDate} + filterQuery={filterQuery} + id={ID} + setQuery={setQuery} + sourceId="default" + startDate={startDate} + type={type} + {...alertsHistogramConfigs} + /> + <AlertsTable endDate={endDate} startDate={startDate} pageFilters={pageFilters} /> + </> + ); +}; +AlertsView.displayName = 'AlertsView'; diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts b/x-pack/plugins/siem/public/components/alerts_viewer/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts rename to x-pack/plugins/siem/public/components/alerts_viewer/translations.ts diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/types.ts b/x-pack/plugins/siem/public/components/alerts_viewer/types.ts new file mode 100644 index 0000000000000..321f7214c8fef --- /dev/null +++ b/x-pack/plugins/siem/public/components/alerts_viewer/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter } from '../../../../../../src/plugins/data/public'; +import { HostsComponentsQueryProps } from '../../pages/hosts/navigation/types'; +import { NetworkComponentQueryProps } from '../../pages/network/navigation/types'; +import { MatrixHistogramOption } from '../matrix_histogram/types'; + +type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; +export interface AlertsComponentsQueryProps + extends Pick< + CommonQueryProps, + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' + > { + pageFilters: Filter[]; + stackByOptions?: MatrixHistogramOption[]; + defaultFilters?: Filter[]; + defaultStackByOption?: MatrixHistogramOption; +} diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx rename to x-pack/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx diff --git a/x-pack/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/plugins/siem/public/components/and_or_badge/index.tsx new file mode 100644 index 0000000000000..28355372df146 --- /dev/null +++ b/x-pack/plugins/siem/public/components/and_or_badge/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; + +const RoundedBadge = (styled(EuiBadge)` + align-items: center; + border-radius: 100%; + display: inline-flex; + font-size: 9px; + height: 34px; + justify-content: center; + margin: 0 5px 0 5px; + padding: 7px 6px 4px 6px; + user-select: none; + width: 34px; + + .euiBadge__content { + position: relative; + top: -1px; + } + + .euiBadge__text { + text-overflow: clip; + } +` as unknown) as typeof EuiBadge; + +RoundedBadge.displayName = 'RoundedBadge'; + +export type AndOr = 'and' | 'or'; + +/** Displays AND / OR in a round badge */ +// Ref: https://github.com/elastic/eui/issues/1655 +export const AndOrBadge = React.memo<{ type: AndOr }>(({ type }) => { + return ( + <RoundedBadge data-test-subj="and-or-badge" color="hollow"> + {type === 'and' ? i18n.AND : i18n.OR} + </RoundedBadge> + ); +}); + +AndOrBadge.displayName = 'AndOrBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/translations.ts b/x-pack/plugins/siem/public/components/and_or_badge/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/and_or_badge/translations.ts rename to x-pack/plugins/siem/public/components/and_or_badge/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/helpers.test.ts b/x-pack/plugins/siem/public/components/arrows/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/helpers.test.ts rename to x-pack/plugins/siem/public/components/arrows/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/helpers.ts b/x-pack/plugins/siem/public/components/arrows/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/helpers.ts rename to x-pack/plugins/siem/public/components/arrows/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx b/x-pack/plugins/siem/public/components/arrows/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx rename to x-pack/plugins/siem/public/components/arrows/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx b/x-pack/plugins/siem/public/components/arrows/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/arrows/index.tsx rename to x-pack/plugins/siem/public/components/arrows/index.tsx diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx new file mode 100644 index 0000000000000..dccc156ff6e44 --- /dev/null +++ b/x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { storiesOf } from '@storybook/react'; +import { ThemeProvider } from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { + QuerySuggestion, + QuerySuggestionTypes, +} from '../../../../../../../src/plugins/data/public'; +import { SuggestionItem } from '../suggestion_item'; + +const suggestion: QuerySuggestion = { + description: 'Description...', + end: 3, + start: 1, + text: 'Text...', + type: QuerySuggestionTypes.Value, +}; + +storiesOf('components/SuggestionItem', module).add('example', () => ( + <ThemeProvider + theme={() => ({ + eui: euiLightVars, + darkMode: false, + })} + > + <SuggestionItem suggestion={suggestion} /> + </ThemeProvider> +)); diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/autocomplete_field/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/autocomplete_field/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/autocomplete_field/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/index.test.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/index.test.tsx new file mode 100644 index 0000000000000..72236d799f995 --- /dev/null +++ b/x-pack/plugins/siem/public/components/autocomplete_field/index.test.tsx @@ -0,0 +1,385 @@ +/* + * 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 { EuiFieldSearch } from '@elastic/eui'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, shallow } from 'enzyme'; +import { noop } from 'lodash/fp'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { QuerySuggestion, QuerySuggestionTypes } from '../../../../../../src/plugins/data/public'; + +import { TestProviders } from '../../mock'; + +import { AutocompleteField } from '.'; + +const mockAutoCompleteData: QuerySuggestion[] = [ + { + type: QuerySuggestionTypes.Field, + text: 'agent.ephemeral_id ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.ephemeral_id</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.hostname ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.hostname</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.id ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.id</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.name ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.name</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.type ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.type</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.version ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.version</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test1 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test1</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test2 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test2</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test3 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test3</span></p>', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test4 ', + description: + '<p>Filter results that contain <span class="suggestionItem__callout">agent.test4</span></p>', + start: 0, + end: 1, + }, +]; + +describe('Autocomplete', () => { + describe('rendering', () => { + test('it renders against snapshot', () => { + const placeholder = 'myPlaceholder'; + + const wrapper = shallow( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder={placeholder} + suggestions={[]} + value={''} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it is rendering with placeholder', () => { + const placeholder = 'myPlaceholder'; + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder={placeholder} + suggestions={[]} + value={''} + /> + ); + const input = wrapper.find('input[type="search"]'); + expect(input.find('[placeholder]').props().placeholder).toEqual(placeholder); + }); + + test('Rendering suggested items', () => { + const wrapper = mount( + <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + </ThemeProvider> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + wrapper.update(); + + expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(10); + }); + + test('Should Not render suggested items if loading new suggestions', () => { + const wrapper = mount( + <ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}> + <AutocompleteField + isLoadingSuggestions={true} + isValid={false} + loadSuggestions={noop} + onChange={noop} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + </ThemeProvider> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + wrapper.update(); + + expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(0); + }); + }); + + describe('events', () => { + test('OnChange should have been called', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={[]} + value={''} + /> + ); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('change', { target: { value: 'test' } }); + expect(onChange).toHaveBeenCalled(); + }); + }); + + test('OnSubmit should have been called by keying enter on the search input', () => { + const onSubmit = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={true} + loadSuggestions={noop} + onChange={noop} + onSubmit={onSubmit} + placeholder="" + suggestions={mockAutoCompleteData} + value={'filter: query'} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: null }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); + expect(onSubmit).toHaveBeenCalled(); + }); + + test('OnSubmit should have been called by onSearch event on the input', () => { + const onSubmit = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={true} + loadSuggestions={noop} + onChange={noop} + onSubmit={onSubmit} + placeholder="" + suggestions={mockAutoCompleteData} + value={'filter: query'} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: null }); + const wrapperFixedEuiFieldSearch = wrapper.find(EuiFieldSearch); + // TODO: FixedEuiFieldSearch fails to import + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (wrapperFixedEuiFieldSearch as any).props().onSearch(); + expect(onSubmit).toHaveBeenCalled(); + }); + + test('OnChange should have been called if keying enter on a suggested item selected', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: 1 }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should be called if tab is pressed when a suggested item is selected', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: 1 }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should NOT be called if tab is pressed when more than one item is suggested, and no selection has been made', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={mockAutoCompleteData} + value={''} + /> + ); + + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('OnChange should be called if tab is pressed when only one item is suggested, even though that item is NOT selected', () => { + const onChange = jest.fn((value: string) => value); + const onlyOneSuggestion = [mockAutoCompleteData[0]]; + + const wrapper = mount( + <TestProviders> + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={onlyOneSuggestion} + value={''} + /> + </TestProviders> + ); + + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should NOT be called if tab is pressed when 0 items are suggested', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={noop} + onChange={onChange} + onSubmit={noop} + placeholder="" + suggestions={[]} + value={''} + /> + ); + + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('Load more suggestions when arrowdown on the search bar', () => { + const loadSuggestions = jest.fn(noop); + + const wrapper = mount( + <AutocompleteField + isLoadingSuggestions={false} + isValid={false} + loadSuggestions={loadSuggestions} + onChange={noop} + onSubmit={noop} + placeholder="" + suggestions={[]} + value={''} + /> + ); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'ArrowDown', preventDefault: noop }); + expect(loadSuggestions).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx new file mode 100644 index 0000000000000..9821bb6048b51 --- /dev/null +++ b/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx @@ -0,0 +1,333 @@ +/* + * 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 { + EuiFieldSearch, + EuiFieldSearchProps, + EuiOutsideClickDetector, + EuiPanel, +} from '@elastic/eui'; +import React from 'react'; +import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; + +import euiStyled from '../../../../../legacy/common/eui_styled_components'; + +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + 'data-test-subj'?: string; + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: QuerySuggestion[]; + value: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + isFocused: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.PureComponent< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + isFocused: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { + 'data-test-subj': dataTestSubj, + suggestions, + isLoadingSuggestions, + isValid, + placeholder, + value, + } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + return ( + <EuiOutsideClickDetector onOutsideClick={this.handleBlur}> + <AutocompleteContainer> + <FixedEuiFieldSearch + data-test-subj={dataTestSubj} + fullWidth + inputRef={this.handleChangeInputRef} + isLoading={isLoadingSuggestions} + isInvalid={!isValid} + onChange={this.handleChange} + onFocus={this.handleFocus} + onKeyDown={this.handleKeyDown} + onKeyUp={this.handleKeyUp} + onSearch={this.submit} + placeholder={placeholder} + value={value} + /> + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + <SuggestionsPanel> + {suggestions.map((suggestion, suggestionIndex) => ( + <SuggestionItem + key={suggestion.text} + suggestion={suggestion} + isSelected={suggestionIndex === selectedIndex} + onMouseEnter={this.selectSuggestionAt(suggestionIndex)} + onClick={this.applySuggestionAt(suggestionIndex)} + /> + ))} + </SuggestionsPanel> + ) : null} + </AutocompleteContainer> + </EuiOutsideClickDetector> + ); + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { + const hasNewValue = prevProps.value !== this.props.value; + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewSuggestions && this.state.isFocused) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent<HTMLInputElement>) => { + const { suggestions } = this.props; + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Tab': + evt.preventDefault(); + if (this.state.areSuggestionsVisible && this.props.suggestions.length === 1) { + this.applySuggestionAt(0)(); + } else if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } + break; + case 'Escape': + evt.preventDefault(); + evt.stopPropagation(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent<HTMLInputElement>) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private handleFocus = () => { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = () => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(this.props.value, inputCursorPosition, 10); + }; +} + +type StateUpdater<State, Props = {}> = ( + prevState: Readonly<State>, + prevProps: Readonly<Props> +) => State | null; + +function composeStateUpdaters<State, Props>(...updaters: Array<StateUpdater<State, Props>>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, +}); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + +export const FixedEuiFieldSearch: React.FC<React.InputHTMLAttributes<HTMLInputElement> & + EuiFieldSearchProps & { + inputRef?: (element: HTMLInputElement | null) => void; + onSearch: (value: string) => void; + }> = EuiFieldSearch as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +const AutocompleteContainer = euiStyled.div` + position: relative; +`; + +AutocompleteContainer.displayName = 'AutocompleteContainer'; + +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ + paddingSize: 'none', + hasShadow: true, +}))` + position: absolute; + width: 100%; + margin-top: 2px; + overflow: hidden; + z-index: ${props => props.theme.eui.euiZLevel1}; +`; + +SuggestionsPanel.displayName = 'SuggestionsPanel'; diff --git a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx rename to x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx index f99a545d558f7..be9a9817265b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx @@ -8,8 +8,8 @@ import { EuiIcon } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; import styled from 'styled-components'; -import euiStyled from '../../../../../common/eui_styled_components'; -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; +import euiStyled from '../../../../../legacy/common/eui_styled_components'; +import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; interface SuggestionItemProps { isSelected?: boolean; diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx b/x-pack/plugins/siem/public/components/bytes/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx rename to x-pack/plugins/siem/public/components/bytes/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.tsx b/x-pack/plugins/siem/public/components/bytes/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/bytes/index.tsx rename to x-pack/plugins/siem/public/components/bytes/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/siem/public/components/certificate_fingerprint/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx rename to x-pack/plugins/siem/public/components/certificate_fingerprint/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx b/x-pack/plugins/siem/public/components/certificate_fingerprint/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx rename to x-pack/plugins/siem/public/components/certificate_fingerprint/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/translations.ts b/x-pack/plugins/siem/public/components/certificate_fingerprint/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/translations.ts rename to x-pack/plugins/siem/public/components/certificate_fingerprint/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap b/x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap rename to x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap b/x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap rename to x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/plugins/siem/public/components/charts/areachart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/areachart.test.tsx rename to x-pack/plugins/siem/public/components/charts/areachart.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx b/x-pack/plugins/siem/public/components/charts/areachart.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/areachart.tsx rename to x-pack/plugins/siem/public/components/charts/areachart.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/plugins/siem/public/components/charts/barchart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/barchart.test.tsx rename to x-pack/plugins/siem/public/components/charts/barchart.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/plugins/siem/public/components/charts/barchart.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx rename to x-pack/plugins/siem/public/components/charts/barchart.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx b/x-pack/plugins/siem/public/components/charts/chart_place_holder.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.test.tsx rename to x-pack/plugins/siem/public/components/charts/chart_place_holder.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx b/x-pack/plugins/siem/public/components/charts/chart_place_holder.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/chart_place_holder.tsx rename to x-pack/plugins/siem/public/components/charts/chart_place_holder.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx b/x-pack/plugins/siem/public/components/charts/common.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/common.test.tsx rename to x-pack/plugins/siem/public/components/charts/common.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx b/x-pack/plugins/siem/public/components/charts/common.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/charts/common.tsx rename to x-pack/plugins/siem/public/components/charts/common.tsx index c7b40c50ffde8..74d728e65f018 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/plugins/siem/public/components/charts/common.tsx @@ -19,7 +19,7 @@ import { import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { DEFAULT_DARK_MODE } from '../../../../../../plugins/siem/common/constants'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; import { useUiSetting } from '../../lib/kibana'; export const defaultChartHeight = '100%'; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.test.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend_item.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.test.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend_item.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx b/x-pack/plugins/siem/public/components/charts/draggable_legend_item.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/draggable_legend_item.tsx rename to x-pack/plugins/siem/public/components/charts/draggable_legend_item.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/charts/translation.ts b/x-pack/plugins/siem/public/components/charts/translation.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/charts/translation.ts rename to x-pack/plugins/siem/public/components/charts/translation.ts diff --git a/x-pack/legacy/plugins/siem/public/components/direction/direction.test.tsx b/x-pack/plugins/siem/public/components/direction/direction.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/direction/direction.test.tsx rename to x-pack/plugins/siem/public/components/direction/direction.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx b/x-pack/plugins/siem/public/components/direction/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/direction/index.tsx rename to x-pack/plugins/siem/public/components/direction/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index f8b5eb7209ff4..1d9508fc28f3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -13,7 +13,7 @@ import { wait } from '../../lib/helpers'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public'; import { TimelineContext } from '../timeline/timeline_context'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/siem/public/components/drag_and_drop/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.test.ts rename to x-pack/plugins/siem/public/components/drag_and_drop/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/plugins/siem/public/components/drag_and_drop/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/helpers.ts rename to x-pack/plugins/siem/public/components/drag_and_drop/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/provider_container.tsx b/x-pack/plugins/siem/public/components/drag_and_drop/provider_container.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/provider_container.tsx rename to x-pack/plugins/siem/public/components/drag_and_drop/provider_container.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts b/x-pack/plugins/siem/public/components/drag_and_drop/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/drag_and_drop/translations.ts rename to x-pack/plugins/siem/public/components/drag_and_drop/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/plugins/siem/public/components/draggables/field_badge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx rename to x-pack/plugins/siem/public/components/draggables/field_badge/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/translations.ts b/x-pack/plugins/siem/public/components/draggables/field_badge/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/field_badge/translations.ts rename to x-pack/plugins/siem/public/components/draggables/field_badge/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/plugins/siem/public/components/draggables/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx rename to x-pack/plugins/siem/public/components/draggables/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/draggables/index.tsx b/x-pack/plugins/siem/public/components/draggables/index.tsx new file mode 100644 index 0000000000000..cea900f7bccf9 --- /dev/null +++ b/x-pack/plugins/siem/public/components/draggables/index.tsx @@ -0,0 +1,176 @@ +/* + * 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 { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../drag_and_drop/helpers'; +import { getEmptyStringTag } from '../empty_value'; +import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; +import { Provider } from '../timeline/data_providers/provider'; + +export interface DefaultDraggableType { + id: string; + field: string; + value?: string | null; + name?: string | null; + queryValue?: string | null; + children?: React.ReactNode; + tooltipContent?: React.ReactNode; +} + +/** + * Only returns true if the specified tooltipContent is exactly `null`. + * Example input / output: + * `bob -> false` + * `undefined -> false` + * `<span>thing</span> -> false` + * `null -> true` + */ +export const tooltipContentIsExplicitlyNull = (tooltipContent?: React.ReactNode): boolean => + tooltipContent === null; // an explicit / exact null check + +/** + * Derives the tooltip content from the field name if no tooltip was specified + */ +export const getDefaultWhenTooltipIsUnspecified = ({ + field, + tooltipContent, +}: { + field: string; + tooltipContent?: React.ReactNode; +}): React.ReactNode => (tooltipContent != null ? tooltipContent : field); + +/** + * Renders the content of the draggable, wrapped in a tooltip + */ +const Content = React.memo<{ + children?: React.ReactNode; + field: string; + tooltipContent?: React.ReactNode; + value?: string | null; +}>(({ children, field, tooltipContent, value }) => + !tooltipContentIsExplicitlyNull(tooltipContent) ? ( + <EuiToolTip + data-test-subj={`${field}-tooltip`} + content={getDefaultWhenTooltipIsUnspecified({ tooltipContent, field })} + > + <>{children ? children : value}</> + </EuiToolTip> + ) : ( + <>{children ? children : value}</> + ) +); + +Content.displayName = 'Content'; + +/** + * Draggable text (or an arbitrary visualization specified by `children`) + * that's only displayed when the specified value is non-`null`. + * + * @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}` + * @param field - the name of the field, e.g. `network.transport` + * @param value - value of the field e.g. `tcp` + * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data + * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior + * @param tooltipContent - defaults to displaying `field`, pass `null` to + * prevent a tooltip from being displayed, or pass arbitrary content + * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data + */ +export const DefaultDraggable = React.memo<DefaultDraggableType>( + ({ id, field, value, name, children, tooltipContent, queryValue }) => + value != null ? ( + <DraggableWrapper + dataProvider={{ + and: [], + enabled: true, + id: escapeDataProviderId(id), + name: name ? name : value, + excluded: false, + kqlQuery: '', + queryMatch: { + field, + value: queryValue ? queryValue : value, + operator: IS_OPERATOR, + }, + }} + render={(dataProvider, _, snapshot) => + snapshot.isDragging ? ( + <DragEffects> + <Provider dataProvider={dataProvider} /> + </DragEffects> + ) : ( + <Content field={field} tooltipContent={tooltipContent} value={value}> + {children} + </Content> + ) + } + /> + ) : null +); + +DefaultDraggable.displayName = 'DefaultDraggable'; + +export const Badge = styled(EuiBadge)` + vertical-align: top; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +Badge.displayName = 'Badge'; + +export type BadgeDraggableType = Omit<DefaultDraggableType, 'id'> & { + contextId: string; + eventId: string; + iconType?: IconType; + color?: string; +}; + +/** + * A draggable badge that's only displayed when the specified value is non-`null`. + * + * @param contextId - used as part of the formula to derive a unique draggable id, this describes the context e.g. `event-fields-browser` in which the badge is displayed + * @param eventId - uniquely identifies an event, as specified in the `_id` field of the document + * @param field - the name of the field, e.g. `network.transport` + * @param value - value of the field e.g. `tcp` + * @param iconType -the (optional) type of icon e.g. `snowflake` to display on the badge + * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data + * @param color - defaults to `hollow`, optionally overwrite the color of the badge icon + * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior + * @param tooltipContent - defaults to displaying `field`, pass `null` to + * prevent a tooltip from being displayed, or pass arbitrary content + * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data + */ +export const DraggableBadge = React.memo<BadgeDraggableType>( + ({ + contextId, + eventId, + field, + value, + iconType, + name, + color = 'hollow', + children, + tooltipContent, + queryValue, + }) => + value != null ? ( + <DefaultDraggable + id={`draggable-badge-default-draggable-${contextId}-${eventId}-${field}-${value}`} + field={field} + name={name} + value={value} + tooltipContent={tooltipContent} + queryValue={queryValue} + > + <Badge iconType={iconType} color={color} title=""> + {children ? children : value !== '' ? value : getEmptyStringTag()} + </Badge> + </DefaultDraggable> + ) : null +); + +DraggableBadge.displayName = 'DraggableBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/duration/index.test.tsx b/x-pack/plugins/siem/public/components/duration/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/duration/index.test.tsx rename to x-pack/plugins/siem/public/components/duration/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx b/x-pack/plugins/siem/public/components/duration/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/duration/index.tsx rename to x-pack/plugins/siem/public/components/duration/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.test.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.test.tsx rename to x-pack/plugins/siem/public/components/edit_data_provider/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/edit_data_provider/helpers.tsx rename to x-pack/plugins/siem/public/components/edit_data_provider/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.test.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.test.tsx rename to x-pack/plugins/siem/public/components/edit_data_provider/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/edit_data_provider/index.tsx rename to x-pack/plugins/siem/public/components/edit_data_provider/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts b/x-pack/plugins/siem/public/components/edit_data_provider/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/edit_data_provider/translations.ts rename to x-pack/plugins/siem/public/components/edit_data_provider/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/plugins/siem/public/components/embeddables/__mocks__/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts rename to x-pack/plugins/siem/public/components/embeddables/__mocks__/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx b/x-pack/plugins/siem/public/components/embeddables/embeddable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/embeddable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.tsx b/x-pack/plugins/siem/public/components/embeddables/embeddable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.tsx rename to x-pack/plugins/siem/public/components/embeddables/embeddable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx b/x-pack/plugins/siem/public/components/embeddables/embeddable_header.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/embeddable_header.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx b/x-pack/plugins/siem/public/components/embeddables/embeddable_header.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.tsx rename to x-pack/plugins/siem/public/components/embeddables/embeddable_header.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/siem/public/components/embeddables/embedded_map.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/embedded_map.test.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/plugins/siem/public/components/embeddables/embedded_map.tsx new file mode 100644 index 0000000000000..d2dd3e5429341 --- /dev/null +++ b/x-pack/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -0,0 +1,224 @@ +/* + * 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 { EuiLink, EuiText } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { createPortalNode, InPortal } from 'react-reverse-portal'; +import styled, { css } from 'styled-components'; + +import { EmbeddablePanel, ErrorEmbeddable } from '../../../../../../src/plugins/embeddable/public'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; +import { useIndexPatterns } from '../../hooks/use_index_patterns'; +import { Loader } from '../loader'; +import { displayErrorToast, useStateToaster } from '../toasters'; +import { Embeddable } from './embeddable'; +import { EmbeddableHeader } from './embeddable_header'; +import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; +import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; +import { MapToolTip } from './map_tool_tip/map_tool_tip'; +import * as i18n from './translations'; +import { SetQuery } from './types'; +import { MapEmbeddable } from '../../../../../legacy/plugins/maps/public'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { useKibana, useUiSetting$ } from '../../lib/kibana'; +import { getSavedObjectFinder } from '../../../../../../src/plugins/saved_objects/public'; + +interface EmbeddableMapProps { + maintainRatio?: boolean; +} + +const EmbeddableMap = styled.div.attrs(() => ({ + className: 'siemEmbeddable__map', +}))<EmbeddableMapProps>` + .embPanel { + border: none; + box-shadow: none; + } + + .mapToolbarOverlay__button { + display: none; + } + + ${({ maintainRatio }) => + maintainRatio && + css` + padding-top: calc(3 / 4 * 100%); /* 4:3 (standard) ratio */ + position: relative; + + @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { + padding-top: calc(9 / 32 * 100%); /* 32:9 (ultra widescreen) ratio */ + } + + @media only screen and (min-width: 1441px) and (min-height: 901px) { + padding-top: calc(9 / 21 * 100%); /* 21:9 (ultrawide) ratio */ + } + + .embPanel { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + } + `} +`; +EmbeddableMap.displayName = 'EmbeddableMap'; + +export interface EmbeddedMapProps { + query: Query; + filters: Filter[]; + startDate: number; + endDate: number; + setQuery: SetQuery; +} + +export const EmbeddedMapComponent = ({ + endDate, + filters, + query, + setQuery, + startDate, +}: EmbeddedMapProps) => { + const [embeddable, setEmbeddable] = React.useState<MapEmbeddable | undefined | ErrorEmbeddable>( + undefined + ); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + const [isIndexError, setIsIndexError] = useState(false); + + const [, dispatchToaster] = useStateToaster(); + const [loadingKibanaIndexPatterns, kibanaIndexPatterns] = useIndexPatterns(); + const [siemDefaultIndices] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + + // This portalNode provided by react-reverse-portal allows us re-parent the MapToolTip within our + // own component tree instead of the embeddables (default). This is necessary to have access to + // the Redux store, theme provider, etc, which is required to register and un-register the draggable + // Search InPortal/OutPortal for implementation touch points + const portalNode = React.useMemo(() => createPortalNode(), []); + + const { services } = useKibana(); + + // Initial Load useEffect + useEffect(() => { + let isSubscribed = true; + async function setupEmbeddable() { + // Ensure at least one `siem:defaultIndex` kibana index pattern exists before creating embeddable + const matchingIndexPatterns = findMatchingIndexPatterns({ + kibanaIndexPatterns, + siemDefaultIndices, + }); + + if (matchingIndexPatterns.length === 0 && isSubscribed) { + setIsLoading(false); + setIsIndexError(true); + return; + } + + // Create & set Embeddable + try { + const embeddableObject = await createEmbeddable( + filters, + getIndexPatternTitleIdMapping(matchingIndexPatterns), + query, + startDate, + endDate, + setQuery, + portalNode, + services.embeddable + ); + if (isSubscribed) { + setEmbeddable(embeddableObject); + } + } catch (e) { + if (isSubscribed) { + displayErrorToast(i18n.ERROR_CREATING_EMBEDDABLE, [e.message], dispatchToaster); + setIsError(true); + } + } + if (isSubscribed) { + setIsLoading(false); + } + } + + if (!loadingKibanaIndexPatterns) { + setupEmbeddable(); + } + return () => { + isSubscribed = false; + }; + }, [loadingKibanaIndexPatterns, kibanaIndexPatterns]); + + // queryExpression updated useEffect + useEffect(() => { + if (embeddable != null) { + embeddable.updateInput({ query }); + } + }, [query]); + + useEffect(() => { + if (embeddable != null) { + embeddable.updateInput({ filters }); + } + }, [filters]); + + // DateRange updated useEffect + useEffect(() => { + if (embeddable != null && startDate != null && endDate != null) { + const timeRange = { + from: new Date(startDate).toISOString(), + to: new Date(endDate).toISOString(), + }; + embeddable.updateInput({ timeRange }); + } + }, [startDate, endDate]); + + return isError ? null : ( + <Embeddable> + <EmbeddableHeader title={i18n.EMBEDDABLE_HEADER_TITLE}> + <EuiText size="xs"> + <EuiLink + href={`${services.docLinks.ELASTIC_WEBSITE_URL}guide/en/siem/guide/${services.docLinks.DOC_LINK_VERSION}/conf-map-ui.html`} + target="_blank" + > + {i18n.EMBEDDABLE_HEADER_HELP} + </EuiLink> + </EuiText> + </EmbeddableHeader> + + <InPortal node={portalNode}> + <MapToolTip /> + </InPortal> + + <EmbeddableMap maintainRatio={!isIndexError}> + {embeddable != null ? ( + <EmbeddablePanel + data-test-subj="embeddable-panel" + embeddable={embeddable} + getActions={services.uiActions.getTriggerCompatibleActions} + getEmbeddableFactory={services.embeddable.getEmbeddableFactory} + getAllEmbeddableFactories={services.embeddable.getEmbeddableFactories} + notifications={services.notifications} + overlays={services.overlays} + inspector={services.inspector} + application={services.application} + SavedObjectFinder={getSavedObjectFinder(services.savedObjects, services.uiSettings)} + /> + ) : !isLoading && isIndexError ? ( + <IndexPatternsMissingPrompt data-test-subj="missing-prompt" /> + ) : ( + <Loader data-test-subj="loading-panel" overlay size="xl" /> + )} + </EmbeddableMap> + </Embeddable> + ); +}; + +EmbeddedMapComponent.displayName = 'EmbeddedMapComponent'; + +export const EmbeddedMap = React.memo(EmbeddedMapComponent); + +EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx index f4e6ee5f878a6..aaae43d9684af 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; -import { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers'; import { createPortalNode } from 'react-reverse-portal'; import { mockAPMIndexPattern, @@ -16,10 +16,9 @@ import { mockGlobIndexPattern, } from './__mocks__/mock'; -jest.mock('ui/new_platform'); +const mockEmbeddable = embeddablePluginMock.createStartContract(); -const { npStart } = createUiNewPlatformMock(); -npStart.plugins.embeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ +mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ create: () => ({ reload: jest.fn(), setRenderTooltipContent: jest.fn(), @@ -39,7 +38,7 @@ describe('embedded_map_helpers', () => { 0, setQueryMock, createPortalNode(), - npStart.plugins.embeddable + mockEmbeddable ); expect(setQueryMock).toHaveBeenCalledTimes(1); }); @@ -54,7 +53,7 @@ describe('embedded_map_helpers', () => { 0, setQueryMock, createPortalNode(), - npStart.plugins.embeddable + mockEmbeddable ); expect(setQueryMock.mock.calls[0][0].refetch).not.toBe(embeddable.reload); setQueryMock.mock.results[0].value(); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx rename to x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 0c7a1212ba280..dd7e1cd6ea9ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -10,21 +10,21 @@ import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; import { IndexPatternMapping, SetQuery } from './types'; import { getLayerList } from './map_config'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../plugins/maps/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/public'; import { MapEmbeddable, RenderTooltipContentParams, MapEmbeddableInput, -} from '../../../../maps/public'; +} from '../../../../../legacy/plugins/maps/public'; import * as i18n from './translations'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; import { EmbeddableStart, isErrorEmbeddable, EmbeddableOutput, ViewMode, ErrorEmbeddable, -} from '../../../../../../../src/plugins/embeddable/public'; +} from '../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../hooks/types'; /** @@ -109,7 +109,7 @@ export const createEmbeddable = async ( if (!isErrorEmbeddable(embeddableObject)) { embeddableObject.setRenderTooltipContent(renderTooltipContent); - embeddableObject.setLayerList(getLayerList(indexPatterns)); + await embeddableObject.setLayerList(getLayerList(indexPatterns)); } // Wire up to app refresh action diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx b/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx rename to x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts b/x-pack/plugins/siem/public/components/embeddables/map_config.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_config.test.ts rename to x-pack/plugins/siem/public/components/embeddables/map_config.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/plugins/siem/public/components/embeddables/map_config.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts rename to x-pack/plugins/siem/public/components/embeddables/map_config.ts index 8c96e0b75a136..0d1cd515820c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/plugins/siem/public/components/embeddables/map_config.ts @@ -13,7 +13,7 @@ import { LayerMappingDetails, } from './types'; import * as i18n from './translations'; -import { SOURCE_TYPES } from '../../../../../../plugins/maps/common/constants'; +import { SOURCE_TYPES } from '../../../../maps/common/constants'; const euiVisColorPalette = euiPaletteColorBlind(); // Update field mappings to modify what fields will be returned to map tooltip diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx index eff4769944765..7c2d5e51d813f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx +++ b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx @@ -17,10 +17,10 @@ import { import { FeatureProperty } from '../types'; import * as i18n from '../translations'; -const FlowBadge = styled(EuiBadge)` +const FlowBadge = (styled(EuiBadge)` height: 45px; min-width: 85px; -`; +` as unknown) as typeof EuiBadge; const EuiFlexGroupStyled = styled(EuiFlexGroup)` margin: 0 auto; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.tsx b/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.tsx rename to x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts b/x-pack/plugins/siem/public/components/embeddables/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/embeddables/translations.ts rename to x-pack/plugins/siem/public/components/embeddables/translations.ts diff --git a/x-pack/plugins/siem/public/components/embeddables/types.ts b/x-pack/plugins/siem/public/components/embeddables/types.ts new file mode 100644 index 0000000000000..d8e20c7f47b4e --- /dev/null +++ b/x-pack/plugins/siem/public/components/embeddables/types.ts @@ -0,0 +1,58 @@ +/* + * 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 { RenderTooltipContentParams } from '../../../../../legacy/plugins/maps/public'; +import { inputsModel } from '../../store/inputs'; + +export interface IndexPatternMapping { + title: string; + id: string; +} + +export interface LayerMappingDetails { + metricField: string; + geoField: string; + tooltipProperties: string[]; + label: string; +} + +export interface LayerMapping { + source: LayerMappingDetails; + destination: LayerMappingDetails; +} + +export interface LayerMappingCollection { + [indexPatternTitle: string]: LayerMapping; +} + +export type SetQuery = (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; +}) => void; + +export interface MapFeature { + id: number; + layerId: string; +} + +export interface LoadFeatureProps { + layerId: string; + featureId: number; +} + +export interface FeatureProperty { + _propertyKey: string; + _rawValue: string | string[]; +} + +export interface FeatureGeometry { + coordinates: [number]; + type: string; +} + +export type MapToolTipProps = Partial<RenderTooltipContentParams>; diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx b/x-pack/plugins/siem/public/components/empty_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_page/index.test.tsx rename to x-pack/plugins/siem/public/components/empty_page/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx b/x-pack/plugins/siem/public/components/empty_page/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx rename to x-pack/plugins/siem/public/components/empty_page/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap rename to x-pack/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx b/x-pack/plugins/siem/public/components/empty_value/empty_value.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/empty_value.test.tsx rename to x-pack/plugins/siem/public/components/empty_value/empty_value.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/index.tsx b/x-pack/plugins/siem/public/components/empty_value/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/index.tsx rename to x-pack/plugins/siem/public/components/empty_value/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/empty_value/translations.ts b/x-pack/plugins/siem/public/components/empty_value/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/empty_value/translations.ts rename to x-pack/plugins/siem/public/components/empty_value/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx rename to x-pack/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.tsx b/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.tsx rename to x-pack/plugins/siem/public/components/error_toast_dispatcher/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap rename to x-pack/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/plugins/siem/public/components/event_details/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx rename to x-pack/plugins/siem/public/components/event_details/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/plugins/siem/public/components/event_details/event_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx rename to x-pack/plugins/siem/public/components/event_details/event_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx b/x-pack/plugins/siem/public/components/event_details/event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_details.tsx rename to x-pack/plugins/siem/public/components/event_details/event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx rename to x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx b/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx rename to x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_id.ts b/x-pack/plugins/siem/public/components/event_details/event_id.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/event_id.ts rename to x-pack/plugins/siem/public/components/event_details/event_id.ts diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx b/x-pack/plugins/siem/public/components/event_details/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx rename to x-pack/plugins/siem/public/components/event_details/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/helpers.tsx b/x-pack/plugins/siem/public/components/event_details/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/helpers.tsx rename to x-pack/plugins/siem/public/components/event_details/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx b/x-pack/plugins/siem/public/components/event_details/json_view.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/json_view.test.tsx rename to x-pack/plugins/siem/public/components/event_details/json_view.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/plugins/siem/public/components/event_details/json_view.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx rename to x-pack/plugins/siem/public/components/event_details/json_view.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx b/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/stateful_event_details.tsx rename to x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/translations.ts b/x-pack/plugins/siem/public/components/event_details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/translations.ts rename to x-pack/plugins/siem/public/components/event_details/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/types.ts b/x-pack/plugins/siem/public/components/event_details/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/event_details/types.ts rename to x-pack/plugins/siem/public/components/event_details/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/default_headers.tsx b/x-pack/plugins/siem/public/components/events_viewer/default_headers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/default_headers.tsx rename to x-pack/plugins/siem/public/components/events_viewer/default_headers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/default_model.tsx b/x-pack/plugins/siem/public/components/events_viewer/default_model.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/default_model.tsx rename to x-pack/plugins/siem/public/components/events_viewer/default_model.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx b/x-pack/plugins/siem/public/components/events_viewer/event_details_width_context.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/event_details_width_context.tsx rename to x-pack/plugins/siem/public/components/events_viewer/event_details_width_context.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/siem/public/components/events_viewer/events_viewer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.test.tsx rename to x-pack/plugins/siem/public/components/events_viewer/events_viewer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx rename to x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx index d210c749dae9c..aff66396af39d 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx @@ -27,12 +27,7 @@ import { TimelineRefetch } from '../timeline/refetch_timeline'; import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../src/plugins/data/public'; +import { Filter, esQuery, IIndexPattern, Query } from '../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/plugins/siem/public/components/events_viewer/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx rename to x-pack/plugins/siem/public/components/events_viewer/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/plugins/siem/public/components/events_viewer/index.tsx new file mode 100644 index 0000000000000..bc6a1b3b77bfa --- /dev/null +++ b/x-pack/plugins/siem/public/components/events_viewer/index.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; +import { inputsActions, timelineActions } from '../../store/actions'; +import { + ColumnHeaderOptions, + SubsetTimelineModel, + TimelineModel, +} from '../../store/timeline/model'; +import { OnChangeItemsPerPage } from '../timeline/events'; +import { Filter } from '../../../../../../src/plugins/data/public'; +import { useUiSetting } from '../../lib/kibana'; +import { EventsViewer } from './events_viewer'; +import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; +import { TimelineTypeContextProps } from '../timeline/timeline_context'; +import { InspectButtonContainer } from '../inspect'; +import * as i18n from './translations'; + +export interface OwnProps { + defaultIndices?: string[]; + defaultModel: SubsetTimelineModel; + end: number; + id: string; + start: number; + headerFilterGroup?: React.ReactNode; + pageFilters?: Filter[]; + timelineTypeContext?: TimelineTypeContextProps; + utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; +} + +type Props = OwnProps & PropsFromRedux; + +const defaultTimelineTypeContext = { + loadingText: i18n.LOADING_EVENTS, +}; + +const StatefulEventsViewerComponent: React.FC<Props> = ({ + createTimeline, + columns, + dataProviders, + deletedEventIds, + defaultIndices, + deleteEventQuery, + end, + filters, + headerFilterGroup, + id, + isLive, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + pageFilters, + query, + removeColumn, + start, + showCheckboxes, + showRowRenderers, + sort, + timelineTypeContext = defaultTimelineTypeContext, + updateItemsPerPage, + upsertColumn, + utilityBar, +}) => { + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + defaultIndices ?? useUiSetting<string[]>(DEFAULT_INDEX_KEY) + ); + + useEffect(() => { + if (createTimeline != null) { + createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); + } + return () => { + deleteEventQuery({ id, inputId: 'global' }); + }; + }, []); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), + [id, updateItemsPerPage] + ); + + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + const exists = columns.findIndex(c => c.id === column.id) !== -1; + + if (!exists && upsertColumn != null) { + upsertColumn({ + column, + id, + index: 1, + }); + } + + if (exists && removeColumn != null) { + removeColumn({ + columnId: column.id, + id, + }); + } + }, + [columns, id, upsertColumn, removeColumn] + ); + + const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); + + return ( + <InspectButtonContainer> + <EventsViewer + browserFields={browserFields} + columns={columns} + id={id} + dataProviders={dataProviders!} + deletedEventIds={deletedEventIds} + end={end} + filters={globalFilters} + headerFilterGroup={headerFilterGroup} + indexPattern={indexPatterns} + isLive={isLive} + itemsPerPage={itemsPerPage!} + itemsPerPageOptions={itemsPerPageOptions!} + kqlMode={kqlMode} + onChangeItemsPerPage={onChangeItemsPerPage} + query={query} + start={start} + sort={sort!} + timelineTypeContext={timelineTypeContext} + toggleColumn={toggleColumn} + utilityBar={utilityBar} + /> + </InspectButtonContainer> + ); +}; + +const makeMapStateToProps = () => { + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getEvents = timelineSelectors.getEventsByIdSelector(); + const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { + const input: inputsModel.InputsRange = getInputsTimeline(state); + const events: TimelineModel = getEvents(state, id) ?? defaultModel; + const { + columns, + dataProviders, + deletedEventIds, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + sort, + showCheckboxes, + showRowRenderers, + } = events; + + return { + columns, + dataProviders, + deletedEventIds, + filters: getGlobalFiltersQuerySelector(state), + id, + isLive: input.policy.kind === 'interval', + itemsPerPage, + itemsPerPageOptions, + kqlMode, + query: getGlobalQuerySelector(state), + sort, + showCheckboxes, + showRowRenderers, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = { + createTimeline: timelineActions.createTimeline, + deleteEventQuery: inputsActions.deleteOneQuery, + updateItemsPerPage: timelineActions.updateItemsPerPage, + removeColumn: timelineActions.removeColumn, + upsertColumn: timelineActions.upsertColumn, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulEventsViewer = connector( + React.memo( + StatefulEventsViewerComponent, + (prevProps, nextProps) => + prevProps.id === nextProps.id && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.deletedEventIds === nextProps.deletedEventIds && + prevProps.end === nextProps.end && + deepEqual(prevProps.filters, nextProps.filters) && + prevProps.isLive === nextProps.isLive && + prevProps.itemsPerPage === nextProps.itemsPerPage && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + prevProps.kqlMode === nextProps.kqlMode && + deepEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.sort, nextProps.sort) && + prevProps.start === nextProps.start && + deepEqual(prevProps.pageFilters, nextProps.pageFilters) && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && + prevProps.start === nextProps.start && + deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && + prevProps.utilityBar === nextProps.utilityBar + ) +); diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts b/x-pack/plugins/siem/public/components/events_viewer/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/mock.ts rename to x-pack/plugins/siem/public/components/events_viewer/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/translations.ts b/x-pack/plugins/siem/public/components/events_viewer/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/events_viewer/translations.ts rename to x-pack/plugins/siem/public/components/events_viewer/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx b/x-pack/plugins/siem/public/components/external_link_icon/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/external_link_icon/index.test.tsx rename to x-pack/plugins/siem/public/components/external_link_icon/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx b/x-pack/plugins/siem/public/components/external_link_icon/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/external_link_icon/index.tsx rename to x-pack/plugins/siem/public/components/external_link_icon/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap rename to x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx rename to x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx rename to x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/categories_pane.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/categories_pane.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx b/x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/categories_pane.tsx rename to x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/category.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/plugins/siem/public/components/fields_browser/category.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_columns.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_title.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_title.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx b/x-pack/plugins/siem/public/components/fields_browser/category_title.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/category_title.tsx rename to x-pack/plugins/siem/public/components/fields_browser/category_title.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_browser.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_browser.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_browser.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_items.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_items.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_items.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_items.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_name.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_name.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/plugins/siem/public/components/fields_browser/field_name.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/field_name.tsx rename to x-pack/plugins/siem/public/components/fields_browser/field_name.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/fields_pane.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/fields_pane.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx rename to x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/header.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/header.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/header.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/plugins/siem/public/components/fields_browser/header.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx rename to x-pack/plugins/siem/public/components/fields_browser/header.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.tsx b/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/helpers.tsx rename to x-pack/plugins/siem/public/components/fields_browser/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx rename to x-pack/plugins/siem/public/components/fields_browser/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/plugins/siem/public/components/fields_browser/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/index.tsx rename to x-pack/plugins/siem/public/components/fields_browser/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts b/x-pack/plugins/siem/public/components/fields_browser/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/translations.ts rename to x-pack/plugins/siem/public/components/fields_browser/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts b/x-pack/plugins/siem/public/components/fields_browser/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/fields_browser/types.ts rename to x-pack/plugins/siem/public/components/fields_browser/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/plugins/siem/public/components/filter_popover/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filter_popover/index.tsx rename to x-pack/plugins/siem/public/components/filter_popover/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap rename to x-pack/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx b/x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx rename to x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/plugins/siem/public/components/filters_global/filters_global.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx rename to x-pack/plugins/siem/public/components/filters_global/filters_global.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/index.tsx b/x-pack/plugins/siem/public/components/filters_global/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/filters_global/index.tsx rename to x-pack/plugins/siem/public/components/filters_global/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap rename to x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap b/x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap rename to x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.tsx b/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/flow_target_select.tsx rename to x-pack/plugins/siem/public/components/flow_controls/flow_target_select.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/translations.ts b/x-pack/plugins/siem/public/components/flow_controls/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flow_controls/translations.ts rename to x-pack/plugins/siem/public/components/flow_controls/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx b/x-pack/plugins/siem/public/components/flyout/button/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/button/index.tsx rename to x-pack/plugins/siem/public/components/flyout/button/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/button/translations.ts b/x-pack/plugins/siem/public/components/flyout/button/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/button/translations.ts rename to x-pack/plugins/siem/public/components/flyout/button/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx b/x-pack/plugins/siem/public/components/flyout/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header/index.tsx rename to x-pack/plugins/siem/public/components/flyout/header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/index.tsx rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/header_with_close_button/translations.ts rename to x-pack/plugins/siem/public/components/flyout/header_with_close_button/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx rename to x-pack/plugins/siem/public/components/flyout/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/flyout/index.tsx b/x-pack/plugins/siem/public/components/flyout/index.tsx new file mode 100644 index 0000000000000..404ca4a16e0f1 --- /dev/null +++ b/x-pack/plugins/siem/public/components/flyout/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import styled from 'styled-components'; + +import { State, timelineSelectors } from '../../store'; +import { DataProvider } from '../timeline/data_providers/data_provider'; +import { FlyoutButton } from './button'; +import { Pane } from './pane'; +import { timelineActions } from '../../store/actions'; +import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; +import { StatefulTimeline } from '../timeline'; +import { TimelineById } from '../../store/timeline/types'; + +export const Badge = (styled(EuiBadge)` + position: absolute; + padding-left: 4px; + padding-right: 4px; + right: 0%; + top: 0%; + border-bottom-left-radius: 5px; +` as unknown) as typeof EuiBadge; + +Badge.displayName = 'Badge'; + +const Visible = styled.div<{ show?: boolean }>` + visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; +`; + +Visible.displayName = 'Visible'; + +interface OwnProps { + flyoutHeight: number; + timelineId: string; + usersViewing: string[]; +} + +type Props = OwnProps & ProsFromRedux; + +export const FlyoutComponent = React.memo<Props>( + ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { + const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ + showTimeline, + timelineId, + ]); + const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ + showTimeline, + timelineId, + ]); + + return ( + <> + <Visible show={show}> + <Pane + flyoutHeight={flyoutHeight} + onClose={handleClose} + timelineId={timelineId} + width={width} + > + <StatefulTimeline onClose={handleClose} usersViewing={usersViewing} id={timelineId} /> + </Pane> + </Visible> + <FlyoutButton + dataProviders={dataProviders} + show={!show} + timelineId={timelineId} + onOpen={handleOpen} + /> + </> + ); + } +); + +FlyoutComponent.displayName = 'FlyoutComponent'; + +const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; +const DEFAULT_TIMELINE_BY_ID = {}; + +const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timelineById: TimelineById = + timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; + /* + In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender + of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS + */ + const dataProviders = timelineById[timelineId]?.dataProviders.length + ? timelineById[timelineId]?.dataProviders + : DEFAULT_DATA_PROVIDERS; + const show = timelineById[timelineId]?.show ?? false; + const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; + + return { dataProviders, show, width }; +}; + +const mapDispatchToProps = { + showTimeline: timelineActions.showTimeline, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type ProsFromRedux = ConnectedProps<typeof connector>; + +export const Flyout = connector(FlyoutComponent); + +Flyout.displayName = 'Flyout'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx rename to x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/plugins/siem/public/components/flyout/pane/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx rename to x-pack/plugins/siem/public/components/flyout/pane/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx b/x-pack/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx rename to x-pack/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts b/x-pack/plugins/siem/public/components/flyout/pane/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/flyout/pane/translations.ts rename to x-pack/plugins/siem/public/components/flyout/pane/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx rename to x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx new file mode 100644 index 0000000000000..98a1acf471629 --- /dev/null +++ b/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx @@ -0,0 +1,33 @@ +/* + * 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 numeral from '@elastic/numeral'; + +import { DEFAULT_BYTES_FORMAT } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; + +type Bytes = string | number; + +export const formatBytes = (value: Bytes, format: string) => { + return numeral(value).format(format); +}; + +export const useFormatBytes = () => { + const [bytesFormat] = useUiSetting$<string>(DEFAULT_BYTES_FORMAT); + + return (value: Bytes) => formatBytes(value, bytesFormat); +}; + +export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => ( + <>{useFormatBytes()(value)}</> +); + +PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; + +export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); + +PreferenceFormattedBytes.displayName = 'PreferenceFormattedBytes'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx rename to x-pack/plugins/siem/public/components/formatted_date/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/plugins/siem/public/components/formatted_date/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx rename to x-pack/plugins/siem/public/components/formatted_date/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.test.ts b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.test.ts rename to x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.ts b/x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_date/maybe_date.ts rename to x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.test.ts b/x-pack/plugins/siem/public/components/formatted_duration/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.test.ts rename to x-pack/plugins/siem/public/components/formatted_duration/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.tsx b/x-pack/plugins/siem/public/components/formatted_duration/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/helpers.tsx rename to x-pack/plugins/siem/public/components/formatted_duration/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx b/x-pack/plugins/siem/public/components/formatted_duration/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx rename to x-pack/plugins/siem/public/components/formatted_duration/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx rename to x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/translations.ts b/x-pack/plugins/siem/public/components/formatted_duration/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_duration/translations.ts rename to x-pack/plugins/siem/public/components/formatted_duration/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/plugins/siem/public/components/formatted_ip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx rename to x-pack/plugins/siem/public/components/formatted_ip/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/index.test.tsx b/x-pack/plugins/siem/public/components/generic_downloader/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/index.test.tsx rename to x-pack/plugins/siem/public/components/generic_downloader/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/index.tsx b/x-pack/plugins/siem/public/components/generic_downloader/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/index.tsx rename to x-pack/plugins/siem/public/components/generic_downloader/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/generic_downloader/translations.ts b/x-pack/plugins/siem/public/components/generic_downloader/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/generic_downloader/translations.ts rename to x-pack/plugins/siem/public/components/generic_downloader/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/plugins/siem/public/components/header_global/index.test.tsx new file mode 100644 index 0000000000000..0f6c5c2e139a7 --- /dev/null +++ b/x-pack/plugins/siem/public/components/header_global/index.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; + +import '../../mock/match_media'; +import { HeaderGlobal } from './index'; + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ + pathname: '/app/siem#/hosts/allHosts', + hash: '', + search: '', + state: '', + }), + withRouter: () => jest.fn(), +})); + +// Test will fail because we will to need to mock some core services to make the test work +// For now let's forget about SiemSearchBar +jest.mock('../search_bar', () => ({ + SiemSearchBar: () => null, +})); + +describe('HeaderGlobal', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('it renders', () => { + const wrapper = shallow(<HeaderGlobal />); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/plugins/siem/public/components/header_global/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_global/index.tsx rename to x-pack/plugins/siem/public/components/header_global/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/translations.ts b/x-pack/plugins/siem/public/components/header_global/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_global/translations.ts rename to x-pack/plugins/siem/public/components/header_global/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap b/x-pack/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/siem/public/components/header_page/editable_title.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/editable_title.test.tsx rename to x-pack/plugins/siem/public/components/header_page/editable_title.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx b/x-pack/plugins/siem/public/components/header_page/editable_title.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx rename to x-pack/plugins/siem/public/components/header_page/editable_title.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/plugins/siem/public/components/header_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx rename to x-pack/plugins/siem/public/components/header_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/index.tsx b/x-pack/plugins/siem/public/components/header_page/index.tsx new file mode 100644 index 0000000000000..e1559cf9e0c48 --- /dev/null +++ b/x-pack/plugins/siem/public/components/header_page/index.tsx @@ -0,0 +1,127 @@ +/* + * 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { LinkIcon, LinkIconProps } from '../link_icon'; +import { Subtitle, SubtitleProps } from '../subtitle'; +import { Title } from './title'; +import { DraggableArguments, BadgeOptions, TitleProp } from './types'; + +interface HeaderProps { + border?: boolean; + isLoading?: boolean; +} + +const Header = styled.header.attrs({ + className: 'siemHeaderPage', +})<HeaderProps>` + ${({ border, theme }) => css` + margin-bottom: ${theme.eui.euiSizeL}; + + ${border && + css` + border-bottom: ${theme.eui.euiBorderThin}; + padding-bottom: ${theme.eui.paddingSizes.l}; + .euiProgress { + top: ${theme.eui.paddingSizes.l}; + } + `} + `} +`; +Header.displayName = 'Header'; + +const FlexItem = styled(EuiFlexItem)` + display: block; +`; +FlexItem.displayName = 'FlexItem'; + +const LinkBack = styled.div.attrs({ + className: 'siemHeaderPage__linkBack', +})` + ${({ theme }) => css` + font-size: ${theme.eui.euiFontSizeXS}; + line-height: ${theme.eui.euiLineHeight}; + margin-bottom: ${theme.eui.euiSizeS}; + `} +`; +LinkBack.displayName = 'LinkBack'; + +const Badge = (styled(EuiBadge)` + letter-spacing: 0; +` as unknown) as typeof EuiBadge; +Badge.displayName = 'Badge'; + +interface BackOptions { + href: LinkIconProps['href']; + text: LinkIconProps['children']; + dataTestSubj?: string; +} + +export interface HeaderPageProps extends HeaderProps { + backOptions?: BackOptions; + badgeOptions?: BadgeOptions; + children?: React.ReactNode; + draggableArguments?: DraggableArguments; + subtitle?: SubtitleProps['items']; + subtitle2?: SubtitleProps['items']; + title: TitleProp; + titleNode?: React.ReactElement; +} + +const HeaderPageComponent: React.FC<HeaderPageProps> = ({ + backOptions, + badgeOptions, + border, + children, + draggableArguments, + isLoading, + subtitle, + subtitle2, + title, + titleNode, + ...rest +}) => ( + <Header border={border} {...rest}> + <EuiFlexGroup alignItems="center"> + <FlexItem> + {backOptions && ( + <LinkBack> + <LinkIcon + dataTestSubj={backOptions.dataTestSubj} + href={backOptions.href} + iconType="arrowLeft" + > + {backOptions.text} + </LinkIcon> + </LinkBack> + )} + + {titleNode || ( + <Title + draggableArguments={draggableArguments} + title={title} + badgeOptions={badgeOptions} + /> + )} + + {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} + {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} + {border && isLoading && <EuiProgress size="xs" color="accent" />} + </FlexItem> + + {children && ( + <FlexItem data-test-subj="header-page-supplements" grow={false}> + {children} + </FlexItem> + )} + </EuiFlexGroup> + </Header> +); + +export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/title.test.tsx b/x-pack/plugins/siem/public/components/header_page/title.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/title.test.tsx rename to x-pack/plugins/siem/public/components/header_page/title.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx b/x-pack/plugins/siem/public/components/header_page/title.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/header_page/title.tsx rename to x-pack/plugins/siem/public/components/header_page/title.tsx index 59039ddd6a23b..43b50c24f6b5b 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/title.tsx +++ b/x-pack/plugins/siem/public/components/header_page/title.tsx @@ -17,9 +17,9 @@ const StyledEuiBetaBadge = styled(EuiBetaBadge)` StyledEuiBetaBadge.displayName = 'StyledEuiBetaBadge'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` letter-spacing: 0; -`; +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; interface Props { diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/translations.ts b/x-pack/plugins/siem/public/components/header_page/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/translations.ts rename to x-pack/plugins/siem/public/components/header_page/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/types.ts b/x-pack/plugins/siem/public/components/header_page/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_page/types.ts rename to x-pack/plugins/siem/public/components/header_page/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/plugins/siem/public/components/header_section/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx rename to x-pack/plugins/siem/public/components/header_section/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.tsx b/x-pack/plugins/siem/public/components/header_section/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/header_section/index.tsx rename to x-pack/plugins/siem/public/components/header_section/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx b/x-pack/plugins/siem/public/components/help_menu/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx rename to x-pack/plugins/siem/public/components/help_menu/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/import_data_modal/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/import_data_modal/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/import_data_modal/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/import_data_modal/index.test.tsx b/x-pack/plugins/siem/public/components/import_data_modal/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/import_data_modal/index.test.tsx rename to x-pack/plugins/siem/public/components/import_data_modal/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/import_data_modal/index.tsx b/x-pack/plugins/siem/public/components/import_data_modal/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/import_data_modal/index.tsx rename to x-pack/plugins/siem/public/components/import_data_modal/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/import_data_modal/translations.ts b/x-pack/plugins/siem/public/components/import_data_modal/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/import_data_modal/translations.ts rename to x-pack/plugins/siem/public/components/import_data_modal/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.test.tsx b/x-pack/plugins/siem/public/components/inspect/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/inspect/index.test.tsx rename to x-pack/plugins/siem/public/components/inspect/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/plugins/siem/public/components/inspect/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/inspect/index.tsx rename to x-pack/plugins/siem/public/components/inspect/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/modal.test.tsx b/x-pack/plugins/siem/public/components/inspect/modal.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/inspect/modal.test.tsx rename to x-pack/plugins/siem/public/components/inspect/modal.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/modal.tsx b/x-pack/plugins/siem/public/components/inspect/modal.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/inspect/modal.tsx rename to x-pack/plugins/siem/public/components/inspect/modal.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/translations.ts b/x-pack/plugins/siem/public/components/inspect/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/inspect/translations.ts rename to x-pack/plugins/siem/public/components/inspect/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ip/index.test.tsx b/x-pack/plugins/siem/public/components/ip/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ip/index.test.tsx rename to x-pack/plugins/siem/public/components/ip/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx b/x-pack/plugins/siem/public/components/ip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ip/index.tsx rename to x-pack/plugins/siem/public/components/ip/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/siem/public/components/ja3_fingerprint/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx rename to x-pack/plugins/siem/public/components/ja3_fingerprint/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx b/x-pack/plugins/siem/public/components/ja3_fingerprint/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx rename to x-pack/plugins/siem/public/components/ja3_fingerprint/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/translations.ts b/x-pack/plugins/siem/public/components/ja3_fingerprint/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/translations.ts rename to x-pack/plugins/siem/public/components/ja3_fingerprint/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx b/x-pack/plugins/siem/public/components/last_event_time/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx rename to x-pack/plugins/siem/public/components/last_event_time/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.tsx b/x-pack/plugins/siem/public/components/last_event_time/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/last_event_time/index.tsx rename to x-pack/plugins/siem/public/components/last_event_time/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/lazy_accordion/index.tsx b/x-pack/plugins/siem/public/components/lazy_accordion/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/lazy_accordion/index.tsx rename to x-pack/plugins/siem/public/components/lazy_accordion/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx b/x-pack/plugins/siem/public/components/link_icon/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx rename to x-pack/plugins/siem/public/components/link_icon/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/link_icon/index.tsx b/x-pack/plugins/siem/public/components/link_icon/index.tsx new file mode 100644 index 0000000000000..36f57c46c1628 --- /dev/null +++ b/x-pack/plugins/siem/public/components/link_icon/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon, EuiLink, IconSize, IconType } from '@elastic/eui'; +import { LinkAnchorProps } from '@elastic/eui/src/components/link/link'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +interface LinkProps { + color?: LinkAnchorProps['color']; + disabled?: boolean; + href?: string; + iconSide?: 'left' | 'right'; + onClick?: Function; + ariaLabel?: string; +} + +const Link = styled(({ iconSide, children, ...rest }) => <EuiLink {...rest}>{children}</EuiLink>)< + LinkProps +>` + ${({ iconSide, theme }) => css` + align-items: center; + display: inline-flex; + vertical-align: top; + white-space: nowrap; + + ${iconSide === 'left' && + css` + .euiIcon { + margin-right: ${theme.eui.euiSizeXS}; + } + `} + + ${iconSide === 'right' && + css` + flex-direction: row-reverse; + + .euiIcon { + margin-left: ${theme.eui.euiSizeXS}; + } + `} + `} +`; +Link.displayName = 'Link'; + +export interface LinkIconProps extends LinkProps { + children: string; + iconSize?: IconSize; + iconType: IconType; + dataTestSubj?: string; +} + +export const LinkIcon = React.memo<LinkIconProps>( + ({ + children, + color, + dataTestSubj, + disabled, + href, + iconSide = 'left', + iconSize = 's', + iconType, + onClick, + ariaLabel, + }) => ( + <Link + className="siemLinkIcon" + color={color} + data-test-subj={dataTestSubj} + disabled={disabled} + href={href} + iconSide={iconSide} + onClick={onClick} + aria-label={ariaLabel ?? children} + > + <EuiIcon size={iconSize} type={iconType} /> + <span className="siemLinkIcon__label">{children}</span> + </Link> + ) +); +LinkIcon.displayName = 'LinkIcon'; diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts b/x-pack/plugins/siem/public/components/link_to/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/helpers.test.ts rename to x-pack/plugins/siem/public/components/link_to/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts b/x-pack/plugins/siem/public/components/link_to/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/helpers.ts rename to x-pack/plugins/siem/public/components/link_to/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/index.ts b/x-pack/plugins/siem/public/components/link_to/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/index.ts rename to x-pack/plugins/siem/public/components/link_to/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/plugins/siem/public/components/link_to/link_to.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx rename to x-pack/plugins/siem/public/components/link_to/link_to.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_case.tsx rename to x-pack/plugins/siem/public/components/link_to/redirect_to_case.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx rename to x-pack/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_hosts.tsx rename to x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_network.tsx rename to x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_overview.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_overview.tsx rename to x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/redirect_to_timelines.tsx rename to x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/redirect_wrapper.tsx b/x-pack/plugins/siem/public/components/link_to/redirect_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/link_to/redirect_wrapper.tsx rename to x-pack/plugins/siem/public/components/link_to/redirect_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/links/index.test.tsx b/x-pack/plugins/siem/public/components/links/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/links/index.test.tsx rename to x-pack/plugins/siem/public/components/links/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/links/index.tsx b/x-pack/plugins/siem/public/components/links/index.tsx new file mode 100644 index 0000000000000..6d473f4721710 --- /dev/null +++ b/x-pack/plugins/siem/public/components/links/index.tsx @@ -0,0 +1,312 @@ +/* + * 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 { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { isNil } from 'lodash/fp'; +import styled from 'styled-components'; + +import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants'; +import { + DefaultFieldRendererOverflow, + DEFAULT_MORE_MAX_HEIGHT, +} from '../field_renderers/field_renderers'; +import { encodeIpv6 } from '../../lib/helpers'; +import { + getCaseDetailsUrl, + getHostDetailsUrl, + getIPDetailsUrl, + getCreateCaseUrl, +} from '../link_to'; +import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; +import { useUiSetting$ } from '../../lib/kibana'; +import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; +import { ExternalLinkIcon } from '../external_link_icon'; +import { navTabs } from '../../pages/home/home_navigations'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; + +import * as i18n from './translations'; + +export const DEFAULT_NUMBER_OF_LINK = 5; + +// Internal Links +const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ + children, + hostName, +}) => ( + <EuiLink href={getHostDetailsUrl(encodeURIComponent(hostName))}> + {children ? children : hostName} + </EuiLink> +); + +const whitelistUrlSchemes = ['http://', 'https://']; +export const ExternalLink = React.memo<{ + url: string; + children?: React.ReactNode; + idx?: number; + overflowIndexStart?: number; + allItemsLimit?: number; +}>( + ({ + url, + children, + idx, + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + }) => { + const lastVisibleItemIndex = overflowIndexStart - 1; + const lastItemIndex = allItemsLimit - 1; + const lastIndexToShow = Math.max(0, Math.min(lastVisibleItemIndex, lastItemIndex)); + const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); + return url && inWhitelist && !isUrlInvalid(url) && children ? ( + <EuiToolTip content={url} position="top" data-test-subj="externalLinkTooltip"> + <EuiLink href={url} target="_blank" rel="noopener" data-test-subj="externalLink"> + {children} + <ExternalLinkIcon data-test-subj="externalLinkIcon" /> + {!isNil(idx) && idx < lastIndexToShow && <Comma data-test-subj="externalLinkComma" />} + </EuiLink> + </EuiToolTip> + ) : null; + } +); + +ExternalLink.displayName = 'ExternalLink'; + +export const HostDetailsLink = React.memo(HostDetailsLinkComponent); + +const IPDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + ip: string; + flowTarget?: FlowTarget | FlowTargetSourceDest; +}> = ({ children, ip, flowTarget = FlowTarget.source }) => ( + <EuiLink href={`${getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget)}`}> + {children ? children : ip} + </EuiLink> +); + +export const IPDetailsLink = React.memo(IPDetailsLinkComponent); + +const CaseDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + detailName: string; + title?: string; +}> = ({ children, detailName, title }) => { + const search = useGetUrlSearch(navTabs.case); + + return ( + <EuiLink + href={getCaseDetailsUrl({ id: detailName, search })} + data-test-subj="case-details-link" + aria-label={i18n.CASE_DETAILS_LINK_ARIA(title ?? detailName)} + > + {children ? children : detailName} + </EuiLink> + ); +}; +export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); +CaseDetailsLink.displayName = 'CaseDetailsLink'; + +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { + const search = useGetUrlSearch(navTabs.case); + return <EuiLink href={getCreateCaseUrl(search)}>{children}</EuiLink>; +}); + +CreateCaseLink.displayName = 'CreateCaseLink'; + +// External Links +export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( + ({ children, link }) => ( + <ExternalLink url={`https://www.google.com/search?q=${encodeURIComponent(link)}`}> + {children ? children : link} + </ExternalLink> + ) +); + +GoogleLink.displayName = 'GoogleLink'; + +export const PortOrServiceNameLink = React.memo<{ + children?: React.ReactNode; + portOrServiceName: number | string; +}>(({ children, portOrServiceName }) => ( + <EuiLink + data-test-subj="port-or-service-name-link" + href={`https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=${encodeURIComponent( + String(portOrServiceName) + )}`} + target="_blank" + > + {children ? children : portOrServiceName} + </EuiLink> +)); + +PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; + +export const Ja3FingerprintLink = React.memo<{ + children?: React.ReactNode; + ja3Fingerprint: string; +}>(({ children, ja3Fingerprint }) => ( + <EuiLink + data-test-subj="ja3-fingerprint-link" + href={`https://sslbl.abuse.ch/ja3-fingerprints/${encodeURIComponent(ja3Fingerprint)}`} + target="_blank" + > + {children ? children : ja3Fingerprint} + </EuiLink> +)); + +Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; + +export const CertificateFingerprintLink = React.memo<{ + children?: React.ReactNode; + certificateFingerprint: string; +}>(({ children, certificateFingerprint }) => ( + <EuiLink + data-test-subj="certificate-fingerprint-link" + href={`https://sslbl.abuse.ch/ssl-certificates/sha1/${encodeURIComponent( + certificateFingerprint + )}`} + target="_blank" + > + {children ? children : certificateFingerprint} + </EuiLink> +)); + +CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; + +enum DefaultReputationLink { + 'virustotal.com' = 'virustotal.com', + 'talosIntelligence.com' = 'talosIntelligence.com', +} + +export interface ReputationLinkSetting { + name: string; + url_template: string; +} + +function isDefaultReputationLink(name: string): name is DefaultReputationLink { + return ( + name === DefaultReputationLink['virustotal.com'] || + name === DefaultReputationLink['talosIntelligence.com'] + ); +} +const isReputationLink = ( + rowItem: string | ReputationLinkSetting +): rowItem is ReputationLinkSetting => + (rowItem as ReputationLinkSetting).url_template !== undefined && + (rowItem as ReputationLinkSetting).name !== undefined; + +export const Comma = styled('span')` + margin-right: 5px; + margin-left: 5px; + &::after { + content: ' ,'; + } +`; + +Comma.displayName = 'Comma'; + +const defaultNameMapping: Record<DefaultReputationLink, string> = { + [DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL, + [DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE, +}; + +const ReputationLinkComponent: React.FC<{ + overflowIndexStart?: number; + allItemsLimit?: number; + showDomain?: boolean; + domain: string; + direction?: 'row' | 'column'; +}> = ({ + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + showDomain = false, + domain, + direction = 'row', +}) => { + const [ipReputationLinksSetting] = useUiSetting$<ReputationLinkSetting[]>( + IP_REPUTATION_LINKS_SETTING + ); + + const ipReputationLinks: ReputationLinkSetting[] = useMemo( + () => + ipReputationLinksSetting + ?.slice(0, allItemsLimit) + .filter( + ({ url_template, name }) => + !isNil(url_template) && !isNil(name) && !isUrlInvalid(url_template) + ) + .map(({ name, url_template }: { name: string; url_template: string }) => ({ + name: isDefaultReputationLink(name) ? defaultNameMapping[name] : name, + url_template: url_template.replace(`{{ip}}`, encodeURIComponent(domain)), + })), + [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] + ); + + return ipReputationLinks?.length > 0 ? ( + <section> + <EuiFlexGroup + gutterSize="none" + justifyContent="center" + direction={direction} + alignItems="center" + data-test-subj="reputationLinkGroup" + > + <EuiFlexItem grow={true}> + {ipReputationLinks + ?.slice(0, overflowIndexStart) + .map(({ name, url_template: urlTemplate }: ReputationLinkSetting, id) => ( + <ExternalLink + allItemsLimit={ipReputationLinks.length} + idx={id} + overflowIndexStart={overflowIndexStart} + url={urlTemplate} + data-test-subj="externalLinkComponent" + key={`reputationLink-${id}`} + > + <>{showDomain ? domain : name ?? domain}</> + </ExternalLink> + ))} + </EuiFlexItem> + + <EuiFlexItem grow={false}> + <DefaultFieldRendererOverflow + rowItems={ipReputationLinks} + idPrefix="moreReputationLink" + render={rowItem => { + return ( + isReputationLink(rowItem) && ( + <ExternalLink + url={rowItem.url_template} + overflowIndexStart={overflowIndexStart} + allItemsLimit={allItemsLimit} + > + <>{rowItem.name ?? domain}</> + </ExternalLink> + ) + ); + }} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={overflowIndexStart} + /> + </EuiFlexItem> + </EuiFlexGroup> + </section> + ) : null; +}; + +ReputationLinkComponent.displayName = 'ReputationLinkComponent'; + +export const ReputationLink = React.memo(ReputationLinkComponent); + +export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( + ({ children, domain }) => ( + <ExternalLink url={`https://www.iana.org/whois?q=${encodeURIComponent(domain)}`}> + {children ? children : domain} + </ExternalLink> + ) +); + +WhoIsLink.displayName = 'WhoIsLink'; diff --git a/x-pack/legacy/plugins/siem/public/components/links/translations.ts b/x-pack/plugins/siem/public/components/links/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/links/translations.ts rename to x-pack/plugins/siem/public/components/links/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx b/x-pack/plugins/siem/public/components/loader/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loader/index.test.tsx rename to x-pack/plugins/siem/public/components/loader/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/plugins/siem/public/components/loader/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loader/index.tsx rename to x-pack/plugins/siem/public/components/loader/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/loading/index.tsx b/x-pack/plugins/siem/public/components/loading/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/loading/index.tsx rename to x-pack/plugins/siem/public/components/loading/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx b/x-pack/plugins/siem/public/components/localized_date_tooltip/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.test.tsx rename to x-pack/plugins/siem/public/components/localized_date_tooltip/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx b/x-pack/plugins/siem/public/components/localized_date_tooltip/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/localized_date_tooltip/index.tsx rename to x-pack/plugins/siem/public/components/localized_date_tooltip/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap b/x-pack/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap rename to x-pack/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx b/x-pack/plugins/siem/public/components/markdown/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/index.test.tsx rename to x-pack/plugins/siem/public/components/markdown/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/index.tsx b/x-pack/plugins/siem/public/components/markdown/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/index.tsx rename to x-pack/plugins/siem/public/components/markdown/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/plugins/siem/public/components/markdown/markdown_hint.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx rename to x-pack/plugins/siem/public/components/markdown/markdown_hint.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx b/x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.tsx rename to x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/translations.ts b/x-pack/plugins/siem/public/components/markdown/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown/translations.ts rename to x-pack/plugins/siem/public/components/markdown/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts b/x-pack/plugins/siem/public/components/markdown_editor/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown_editor/constants.ts rename to x-pack/plugins/siem/public/components/markdown_editor/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx b/x-pack/plugins/siem/public/components/markdown_editor/form.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown_editor/form.tsx rename to x-pack/plugins/siem/public/components/markdown_editor/form.tsx diff --git a/x-pack/plugins/siem/public/components/markdown_editor/index.tsx b/x-pack/plugins/siem/public/components/markdown_editor/index.tsx new file mode 100644 index 0000000000000..4fb7086e82b28 --- /dev/null +++ b/x-pack/plugins/siem/public/components/markdown_editor/index.tsx @@ -0,0 +1,165 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiPanel, + EuiTabbedContent, + EuiTextArea, +} from '@elastic/eui'; +import React, { useMemo, useCallback, ChangeEvent } from 'react'; +import styled, { css } from 'styled-components'; + +import { Markdown } from '../markdown'; +import * as i18n from './translations'; +import { MARKDOWN_HELP_LINK } from './constants'; + +const TextArea = styled(EuiTextArea)` + width: 100%; +`; + +const Container = styled(EuiPanel)` + ${({ theme }) => css` + padding: 0; + background: ${theme.eui.euiColorLightestShade}; + position: relative; + .markdown-tabs-header { + position: absolute; + top: ${theme.eui.euiSizeS}; + right: ${theme.eui.euiSizeS}; + z-index: ${theme.eui.euiZContentMenu}; + } + .euiTab { + padding: 10px; + } + .markdown-tabs { + width: 100%; + } + .markdown-tabs-footer { + height: 41px; + padding: 0 ${theme.eui.euiSizeM}; + .euiLink { + font-size: ${theme.eui.euiSizeM}; + } + } + .euiFormRow__labelWrapper { + position: absolute; + top: -${theme.eui.euiSizeL}; + } + .euiFormErrorText { + padding: 0 ${theme.eui.euiSizeM}; + } + `} +`; + +const MarkdownContainer = styled(EuiPanel)` + min-height: 150px; + overflow: auto; +`; + +export interface CursorPosition { + start: number; + end: number; +} + +/** An input for entering a new case description */ +export const MarkdownEditor = React.memo<{ + bottomRightContent?: React.ReactNode; + topRightContent?: React.ReactNode; + content: string; + isDisabled?: boolean; + onChange: (description: string) => void; + onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void; + placeholder?: string; +}>( + ({ + bottomRightContent, + topRightContent, + content, + isDisabled = false, + onChange, + placeholder, + onCursorPositionUpdate, + }) => { + const handleOnChange = useCallback( + (evt: ChangeEvent<HTMLTextAreaElement>) => { + onChange(evt.target.value); + }, + [onChange] + ); + + const setCursorPosition = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + if (onCursorPositionUpdate) { + onCursorPositionUpdate({ + start: e!.target!.selectionStart ?? 0, + end: e!.target!.selectionEnd ?? 0, + }); + } + return false; + }; + + const tabs = useMemo( + () => [ + { + id: 'comment', + name: i18n.MARKDOWN, + content: ( + <TextArea + data-test-subj="textAreaInput" + onChange={handleOnChange} + onBlur={setCursorPosition} + aria-label={`markdown-editor-comment`} + fullWidth={true} + disabled={isDisabled} + placeholder={placeholder ?? ''} + spellCheck={false} + value={content} + /> + ), + }, + { + id: 'preview', + name: i18n.PREVIEW, + content: ( + <MarkdownContainer data-test-subj="markdown-container" paddingSize="s"> + <Markdown raw={content} /> + </MarkdownContainer> + ), + }, + ], + [content, isDisabled, placeholder] + ); + return ( + <Container> + {topRightContent && <div className={`markdown-tabs-header`}>{topRightContent}</div>} + <EuiTabbedContent + className={`markdown-tabs`} + data-test-subj={`markdown-tabs`} + size="s" + tabs={tabs} + initialSelectedTab={tabs[0]} + /> + <EuiFlexGroup + className={`markdown-tabs-footer`} + alignItems="center" + gutterSize="none" + justifyContent="spaceBetween" + > + <EuiFlexItem grow={false}> + <EuiLink href={MARKDOWN_HELP_LINK} external target="_blank"> + {i18n.MARKDOWN_SYNTAX_HELP} + </EuiLink> + </EuiFlexItem> + {bottomRightContent && <EuiFlexItem grow={false}>{bottomRightContent}</EuiFlexItem>} + </EuiFlexGroup> + </Container> + ); + } +); + +MarkdownEditor.displayName = 'MarkdownEditor'; diff --git a/x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts b/x-pack/plugins/siem/public/components/markdown_editor/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/markdown_editor/translations.ts rename to x-pack/plugins/siem/public/components/markdown_editor/translations.ts diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..c4bdff7ea649a --- /dev/null +++ b/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Matrix Histogram Component not initial load it renders no MatrixLoader 1`] = `"<div class=\\"sc-AykKI gltyKM\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKK guzfus\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"barchart\\"></div></div></div>"`; + +exports[`Matrix Histogram Component on initial load it renders MatrixLoader 1`] = `"<div class=\\"sc-AykKI RNnzH\\"><div class=\\"euiPanel euiPanel--paddingMedium sc-AykKC sc-AykKK guzfus\\" data-test-subj=\\"mockIdPanel\\" height=\\"300\\"><div class=\\"headerSection\\"></div><div class=\\"matrixLoader\\"></div></div></div><div class=\\"euiSpacer euiSpacer--l\\" data-test-subj=\\"spacer\\"></div>"`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.test.tsx rename to x-pack/plugins/siem/public/components/matrix_histogram/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx rename to x-pack/plugins/siem/public/components/matrix_histogram/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx rename to x-pack/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/translations.ts b/x-pack/plugins/siem/public/components/matrix_histogram/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/translations.ts rename to x-pack/plugins/siem/public/components/matrix_histogram/translations.ts diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/plugins/siem/public/components/matrix_histogram/types.ts new file mode 100644 index 0000000000000..c59775ad325d0 --- /dev/null +++ b/x-pack/plugins/siem/public/components/matrix_histogram/types.ts @@ -0,0 +1,143 @@ +/* + * 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 { EuiTitleSize } from '@elastic/eui'; +import { ScaleType, Position, TickFormatter } from '@elastic/charts'; +import { ActionCreator } from 'redux'; +import { ESQuery } from '../../../common/typed_json'; +import { SetQuery } from '../../pages/hosts/navigation/types'; +import { InputsModelId } from '../../store/inputs/constants'; +import { HistogramType } from '../../graphql/types'; +import { UpdateDateRange } from '../charts/common'; + +export type MatrixHistogramMappingTypes = Record< + string, + { key: string; value: null; color?: string | undefined } +>; +export interface MatrixHistogramOption { + text: string; + value: string; +} + +export type GetSubTitle = (count: number) => string; +export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; + +export interface MatrixHisrogramConfigs { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; + titleSize?: EuiTitleSize; +} + +interface MatrixHistogramBasicProps { + chartHeight?: number; + defaultIndex: string[]; + defaultStackByOption: MatrixHistogramOption; + dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + endDate: number; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + id: string; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + panelHeight?: number; + setQuery: SetQuery; + startDate: number; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title?: string | GetTitle; + titleSize?: EuiTitleSize; +} + +export interface MatrixHistogramQueryProps { + endDate: number; + errorMessage: string; + filterQuery?: ESQuery | string | undefined; + setAbsoluteRangeDatePicker?: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + stackByField: string; + startDate: number; + indexToAdd?: string[] | null; + isInspected: boolean; + histogramType: HistogramType; +} + +export interface MatrixHistogramProps extends MatrixHistogramBasicProps { + scaleType?: ScaleType; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; + showSpacer?: boolean; + legendPosition?: Position; +} + +export interface HistogramBucket { + key_as_string: string; + key: number; + doc_count: number; +} +export interface GroupBucket { + key: string; + signals: { + buckets: HistogramBucket[]; + }; +} + +export interface HistogramAggregation { + histogramAgg: { + buckets: GroupBucket[]; + }; +} + +export interface BarchartConfigs { + series: { + xScaleType: ScaleType; + yScaleType: ScaleType; + stackAccessors: string[]; + }; + axis: { + xTickFormatter: TickFormatter; + yTickFormatter: TickFormatter; + tickSize: number; + }; + settings: { + legendPosition: Position; + onBrushEnd: UpdateDateRange; + showLegend: boolean; + showLegendExtra: boolean; + theme: { + scales: { + barsPadding: number; + }; + chartMargins: { + left: number; + right: number; + top: number; + bottom: number; + }; + chartPaddings: { + left: number; + right: number; + top: number; + bottom: number; + }; + }; + }; + customHeight: number; +} diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts b/x-pack/plugins/siem/public/components/matrix_histogram/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.test.ts rename to x-pack/plugins/siem/public/components/matrix_histogram/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/plugins/siem/public/components/matrix_histogram/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/matrix_histogram/utils.ts rename to x-pack/plugins/siem/public/components/matrix_histogram/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx rename to x-pack/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts b/x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts b/x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/translations.ts b/x-pack/plugins/siem/public/components/ml/anomaly/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/translations.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts b/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts rename to x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts index cebfc172ee6ff..d64bd3a64e941 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts @@ -6,7 +6,7 @@ import { useState, useEffect } from 'react'; -import { DEFAULT_ANOMALY_SCORE } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/plugins/siem/public/components/ml/api/anomalies_table_data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts rename to x-pack/plugins/siem/public/components/ml/api/anomalies_table_data.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/errors.ts b/x-pack/plugins/siem/public/components/ml/api/errors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/errors.ts rename to x-pack/plugins/siem/public/components/ml/api/errors.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/plugins/siem/public/components/ml/api/get_ml_capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts rename to x-pack/plugins/siem/public/components/ml/api/get_ml_capabilities.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts b/x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts rename to x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts b/x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/throw_if_not_ok.ts rename to x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts b/x-pack/plugins/siem/public/components/ml/api/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/api/translations.ts rename to x-pack/plugins/siem/public/components/ml/api/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx rename to x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx index b5aacdf664c67..b7c544273ae92 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -14,7 +14,7 @@ import { emptyEntity, multipleEntities, getMultipleEntities } from './entity_hel import { SiemPageName } from '../../../pages/home/types'; import { HostsTableType } from '../../../store/hosts/model'; -import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/public'; interface QueryStringType { '?_g': string; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx rename to x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx index e27e9dc084c57..54773e3ab6dda 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx +++ b/x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx @@ -13,7 +13,7 @@ import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, getMultipleEntities, multipleEntities } from './entity_helpers'; import { SiemPageName } from '../../../pages/home/types'; -import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/public'; interface QueryStringType { '?_g': string; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts b/x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts rename to x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts b/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts rename to x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.ts b/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/host_to_criteria.ts rename to x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts b/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts rename to x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.ts b/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/criteria/network_to_criteria.ts rename to x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/empty_ml_capabilities.ts b/x-pack/plugins/siem/public/components/ml/empty_ml_capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/empty_ml_capabilities.ts rename to x-pack/plugins/siem/public/components/ml/empty_ml_capabilities.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx b/x-pack/plugins/siem/public/components/ml/entity_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx rename to x-pack/plugins/siem/public/components/ml/entity_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx b/x-pack/plugins/siem/public/components/ml/entity_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.tsx rename to x-pack/plugins/siem/public/components/ml/entity_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/get_entries.test.ts b/x-pack/plugins/siem/public/components/ml/get_entries.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/get_entries.test.ts rename to x-pack/plugins/siem/public/components/ml/get_entries.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/get_entries.ts b/x-pack/plugins/siem/public/components/ml/get_entries.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/get_entries.ts rename to x-pack/plugins/siem/public/components/ml/get_entries.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx b/x-pack/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx rename to x-pack/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx b/x-pack/plugins/siem/public/components/ml/influencers/create_influencers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/create_influencers.tsx rename to x-pack/plugins/siem/public/components/ml/influencers/create_influencers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/host_to_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts b/x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts rename to x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.ts b/x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/influencers/network_to_influencers.ts rename to x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.test.ts b/x-pack/plugins/siem/public/components/ml/links/create_explorer_link.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.test.ts rename to x-pack/plugins/siem/public/components/ml/links/create_explorer_link.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.ts b/x-pack/plugins/siem/public/components/ml/links/create_explorer_link.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_explorer_link.ts rename to x-pack/plugins/siem/public/components/ml/links/create_explorer_link.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.test.ts b/x-pack/plugins/siem/public/components/ml/links/create_series_link.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.test.ts rename to x-pack/plugins/siem/public/components/ml/links/create_series_link.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.ts b/x-pack/plugins/siem/public/components/ml/links/create_series_link.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/links/create_series_link.ts rename to x-pack/plugins/siem/public/components/ml/links/create_series_link.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/mock.ts b/x-pack/plugins/siem/public/components/ml/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/mock.ts rename to x-pack/plugins/siem/public/components/ml/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts b/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts rename to x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx rename to x-pack/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/permissions/translations.ts b/x-pack/plugins/siem/public/components/ml/permissions/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/permissions/translations.ts rename to x-pack/plugins/siem/public/components/ml/permissions/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap b/x-pack/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_score.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_score.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_score.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_score.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_score.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.tsx b/x-pack/plugins/siem/public/components/ml/score/anomaly_scores.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.tsx rename to x-pack/plugins/siem/public/components/ml/score/anomaly_scores.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx b/x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx rename to x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx index 24f203a3682d5..e7615bf3b89ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/create_description_list.tsx +++ b/x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx @@ -8,7 +8,7 @@ import { EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic import React from 'react'; import styled from 'styled-components'; -import { DescriptionList } from '../../../../../../../plugins/siem/common/utility_types'; +import { DescriptionList } from '../../../../common/utility_types'; import { Anomaly, NarrowDateRange } from '../types'; import { getScoreString } from './score_health'; import { PreferenceFormattedDate } from '../../formatted_date'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx b/x-pack/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts b/x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts rename to x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts b/x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/create_entities_from_score.ts rename to x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx b/x-pack/plugins/siem/public/components/ml/score/draggable_score.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.test.tsx rename to x-pack/plugins/siem/public/components/ml/score/draggable_score.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx b/x-pack/plugins/siem/public/components/ml/score/draggable_score.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/draggable_score.tsx rename to x-pack/plugins/siem/public/components/ml/score/draggable_score.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/get_score_string.test.ts b/x-pack/plugins/siem/public/components/ml/score/get_score_string.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/get_score_string.test.ts rename to x-pack/plugins/siem/public/components/ml/score/get_score_string.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.test.ts b/x-pack/plugins/siem/public/components/ml/score/get_top_severity.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.test.ts rename to x-pack/plugins/siem/public/components/ml/score/get_top_severity.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.ts b/x-pack/plugins/siem/public/components/ml/score/get_top_severity.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/get_top_severity.ts rename to x-pack/plugins/siem/public/components/ml/score/get_top_severity.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/score_health.tsx b/x-pack/plugins/siem/public/components/ml/score/score_health.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/score_health.tsx rename to x-pack/plugins/siem/public/components/ml/score/score_health.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts b/x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts rename to x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts b/x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts rename to x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/translations.ts b/x-pack/plugins/siem/public/components/ml/score/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/score/translations.ts rename to x-pack/plugins/siem/public/components/ml/score/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx rename to x-pack/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx rename to x-pack/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/basic_table.tsx b/x-pack/plugins/siem/public/components/ml/tables/basic_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/basic_table.tsx rename to x-pack/plugins/siem/public/components/ml/tables/basic_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts b/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts rename to x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.test.ts b/x-pack/plugins/siem/public/components/ml/tables/create_compound_key.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/create_compound_key.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.ts b/x-pack/plugins/siem/public/components/ml/tables/create_compound_key.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/create_compound_key.ts rename to x-pack/plugins/siem/public/components/ml/tables/create_compound_key.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx rename to x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.test.ts b/x-pack/plugins/siem/public/components/ml/tables/host_equality.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/host_equality.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.ts b/x-pack/plugins/siem/public/components/ml/tables/host_equality.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/host_equality.ts rename to x-pack/plugins/siem/public/components/ml/tables/host_equality.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.test.ts b/x-pack/plugins/siem/public/components/ml/tables/network_equality.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.test.ts rename to x-pack/plugins/siem/public/components/ml/tables/network_equality.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.ts b/x-pack/plugins/siem/public/components/ml/tables/network_equality.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/network_equality.ts rename to x-pack/plugins/siem/public/components/ml/tables/network_equality.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/translations.ts b/x-pack/plugins/siem/public/components/ml/tables/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/tables/translations.ts rename to x-pack/plugins/siem/public/components/ml/tables/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/types.test.ts b/x-pack/plugins/siem/public/components/ml/types.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/types.test.ts rename to x-pack/plugins/siem/public/components/ml/types.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml/types.ts b/x-pack/plugins/siem/public/components/ml/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml/types.ts rename to x-pack/plugins/siem/public/components/ml/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx b/x-pack/plugins/siem/public/components/ml_popover/__mocks__/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/__mocks__/api.tsx rename to x-pack/plugins/siem/public/components/ml_popover/__mocks__/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/plugins/siem/public/components/ml_popover/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx rename to x-pack/plugins/siem/public/components/ml_popover/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx b/x-pack/plugins/siem/public/components/ml_popover/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/helpers.tsx rename to x-pack/plugins/siem/public/components/ml_popover/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/hooks/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/hooks/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx index bc488ee00988b..7bcbf4afa10cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx +++ b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { checkRecognizer, getJobsSummary, getModules } from '../api'; import { SiemJob } from '../types'; import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions'; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx rename to x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx index a0343608dc67a..e7b14f2e80bf2 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx @@ -11,7 +11,7 @@ import { isJobLoading, isJobFailed, isJobStarted, -} from '../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; +} from '../../../../common/detection_engine/ml_helpers'; import { SiemJob } from '../types'; const StaticSwitch = styled(EuiSwitch)` diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/jobs_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/jobs_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_modules.tsx b/x-pack/plugins/siem/public/components/ml_popover/ml_modules.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/ml_modules.tsx rename to x-pack/plugins/siem/public/components/ml_popover/ml_modules.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx index bd7d696757ca6..3c93e1c195cd7 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx +++ b/x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx @@ -9,7 +9,6 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { MlPopover } from './ml_popover'; -jest.mock('ui/new_platform'); jest.mock('../../lib/kibana'); jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/plugins/siem/public/components/ml_popover/ml_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.tsx rename to x-pack/plugins/siem/public/components/ml_popover/ml_popover.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/popover_description.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/popover_description.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx b/x-pack/plugins/siem/public/components/ml_popover/popover_description.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/popover_description.tsx rename to x-pack/plugins/siem/public/components/ml_popover/popover_description.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts b/x-pack/plugins/siem/public/components/ml_popover/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/translations.ts rename to x-pack/plugins/siem/public/components/ml_popover/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml_popover/types.ts b/x-pack/plugins/siem/public/components/ml_popover/types.ts new file mode 100644 index 0000000000000..58d40c298b329 --- /dev/null +++ b/x-pack/plugins/siem/public/components/ml_popover/types.ts @@ -0,0 +1,203 @@ +/* + * 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 { AuditMessageBase } from '../../../../ml/common/types/audit_message'; +import { MlError } from '../ml/types'; + +export interface Group { + id: string; + jobIds: string[]; + calendarIds: string[]; +} + +export interface CheckRecognizerProps { + indexPatternName: string[]; + signal: AbortSignal; +} + +export interface RecognizerModule { + id: string; + title: string; + query: Record<string, object>; + description: string; + logo: { + icon: string; + }; +} + +export interface GetModulesProps { + moduleId?: string; + signal: AbortSignal; +} + +export interface Module { + id: string; + title: string; + description: string; + type: string; + logoFile: string; + defaultIndexPattern: string; + query: Record<string, object>; + jobs: ModuleJob[]; + datafeeds: ModuleDatafeed[]; + kibana: object; +} + +/** + * Representation of an ML Job as returned from `the ml/modules/get_module` API + */ +export interface ModuleJob { + id: string; + config: { + groups: string[]; + description: string; + analysis_config: { + bucket_span: string; + summary_count_field_name?: string; + detectors: Detector[]; + influencers: string[]; + }; + analysis_limits: { + model_memory_limit: string; + }; + data_description: { + time_field: string; + time_format?: string; + }; + model_plot_config?: { + enabled: boolean; + }; + custom_settings: { + created_by: string; + custom_urls: CustomURL[]; + }; + job_type: string; + }; +} + +// TODO: Speak to ML team about why the get_module API will sometimes return indexes and other times indices +// See mockGetModuleResponse for examples +export interface ModuleDatafeed { + id: string; + config: { + job_id: string; + indexes?: string[]; + indices?: string[]; + query: Record<string, object>; + }; +} + +export interface MlSetupArgs { + configTemplate: string; + indexPatternName: string; + jobIdErrorFilter: string[]; + groups: string[]; + prefix?: string; +} + +/** + * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API + */ +export interface JobSummary { + auditMessage?: AuditMessageBase; + datafeedId: string; + datafeedIndices: string[]; + datafeedState: string; + description: string; + earliestTimestampMs?: number; + latestResultsTimestampMs?: number; + groups: string[]; + hasDatafeed: boolean; + id: string; + isSingleMetricViewerJob: boolean; + jobState: string; + latestTimestampMs?: number; + memory_status: string; + nodeName?: string; + processed_record_count: number; +} + +export interface Detector { + detector_description: string; + function: string; + by_field_name: string; + partition_field_name?: string; +} + +export interface CustomURL { + url_name: string; + url_value: string; +} + +/** + * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary + * that includes necessary metadata like moduleName, defaultIndexPattern, etc. + */ +export interface SiemJob extends JobSummary { + moduleId: string; + defaultIndexPattern: string; + isCompatible: boolean; + isInstalled: boolean; + isElasticJob: boolean; +} + +export interface AugmentedSiemJobFields { + moduleId: string; + defaultIndexPattern: string; + isCompatible: boolean; + isElasticJob: boolean; +} + +export interface SetupMlResponseJob { + id: string; + success: boolean; + error?: MlError; +} + +export interface SetupMlResponseDatafeed { + id: string; + success: boolean; + started: boolean; + error?: MlError; +} + +export interface SetupMlResponse { + jobs: SetupMlResponseJob[]; + datafeeds: SetupMlResponseDatafeed[]; + kibana: {}; +} + +export interface StartDatafeedResponse { + [key: string]: { + started: boolean; + error?: string; + }; +} + +export interface ErrorResponse { + statusCode?: number; + error?: string; + message?: string; +} + +export interface StopDatafeedResponse { + [key: string]: { + stopped: boolean; + }; +} + +export interface CloseJobsResponse { + [key: string]: { + closed: boolean; + }; +} + +export interface JobsFilters { + filterQuery: string; + showCustomJobs: boolean; + showElasticJobs: boolean; + selectedGroups: string[]; +} diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx rename to x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx b/x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/ml_popover/upgrade_contents.tsx rename to x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts rename to x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts new file mode 100644 index 0000000000000..85d77485830a5 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts @@ -0,0 +1,143 @@ +/* + * 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 { getOr, omit } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { APP_NAME } from '../../../../common/constants'; +import { StartServices } from '../../../plugin'; +import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; +import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; +import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; +import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; +import { SiemPageName } from '../../../pages/home/types'; +import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types'; +import { getOverviewUrl } from '../../link_to'; + +import { TabNavigationProps } from '../tab_navigation/types'; +import { getSearch } from '../helpers'; +import { SearchNavTab } from '../types'; + +export const setBreadcrumbs = ( + spyState: RouteSpyState & TabNavigationProps, + chrome: StartServices['chrome'] +) => { + const breadcrumbs = getBreadcrumbsForRoute(spyState); + if (breadcrumbs) { + chrome.setBreadcrumbs(breadcrumbs); + } +}; + +export const siemRootBreadcrumb: ChromeBreadcrumb[] = [ + { + text: APP_NAME, + href: getOverviewUrl(), + }, +]; + +const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.network; + +const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.hosts; + +const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => + spyState != null && spyState.pageName === SiemPageName.case; + +const isDetectionsRoutes = (spyState: RouteSpyState) => + spyState != null && spyState.pageName === SiemPageName.detections; + +export const getBreadcrumbsForRoute = ( + object: RouteSpyState & TabNavigationProps +): ChromeBreadcrumb[] | null => { + const spyState: RouteSpyState = omit('navTabs', object); + if (isHostsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + return [ + ...siemRootBreadcrumb, + ...getHostDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isNetworkRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + return [ + ...siemRootBreadcrumb, + ...getIPDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isDetectionsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getDetectionRulesBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isCaseRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getCaseDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if ( + spyState != null && + object.navTabs && + spyState.pageName && + object.navTabs[spyState.pageName] + ) { + return [ + ...siemRootBreadcrumb, + { + text: object.navTabs[spyState.pageName].name, + href: '', + }, + ]; + } + + return null; +}; diff --git a/x-pack/plugins/siem/public/components/navigation/helpers.ts b/x-pack/plugins/siem/public/components/navigation/helpers.ts new file mode 100644 index 0000000000000..291cb90098f78 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/helpers.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { Location } from 'history'; + +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../store/timeline/model'; +import { CONSTANTS } from '../url_state/constants'; +import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; +import { + replaceQueryStringInLocation, + replaceStateKeyInQueryString, + getQueryStringFromLocation, +} from '../url_state/helpers'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; + +import { SearchNavTab } from './types'; + +export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { + if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { + return URL_STATE_KEYS[tab.urlKey].reduce<Location>( + (myLocation: Location, urlKey: KeyUrlState) => { + let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = ''; + + if (urlKey === CONSTANTS.appQuery && urlState.query != null) { + if (urlState.query.query === '') { + urlStateToReplace = ''; + } else { + urlStateToReplace = urlState.query; + } + } else if (urlKey === CONSTANTS.filters && urlState.filters != null) { + if (isEmpty(urlState.filters)) { + urlStateToReplace = ''; + } else { + urlStateToReplace = urlState.filters; + } + } else if (urlKey === CONSTANTS.timerange) { + urlStateToReplace = urlState[CONSTANTS.timerange]; + } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { + const timeline = urlState[CONSTANTS.timeline]; + if (timeline.id === '') { + urlStateToReplace = ''; + } else { + urlStateToReplace = timeline; + } + } + return replaceQueryStringInLocation( + myLocation, + replaceStateKeyInQueryString( + urlKey, + urlStateToReplace + )(getQueryStringFromLocation(myLocation.search)) + ); + }, + { + pathname: '', + hash: '', + search: '', + state: '', + } + ).search; + } + return ''; +}; diff --git a/x-pack/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/plugins/siem/public/components/navigation/index.test.tsx new file mode 100644 index 0000000000000..d8b62029138c8 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/index.test.tsx @@ -0,0 +1,238 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { CONSTANTS } from '../url_state/constants'; +import { SiemNavigationComponent } from './'; +import { setBreadcrumbs } from './breadcrumbs'; +import { navTabs } from '../../pages/home/home_navigations'; +import { HostsTableType } from '../../store/hosts/model'; +import { RouteSpyState } from '../../utils/route/types'; +import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; + +jest.mock('./breadcrumbs', () => ({ + setBreadcrumbs: jest.fn(), +})); + +describe('SIEM Navigation', () => { + const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, + navTabs, + urlState: { + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }, + }; + const wrapper = mount(<SiemNavigationComponent {...mockProps} />); + test('it calls setBreadcrumbs with correct path on mount', () => { + expect(setBreadcrumbs).toHaveBeenNthCalledWith( + 1, + { + detailName: undefined, + navTabs: { + case: { + disabled: false, + href: '#/link-to/case', + id: 'case', + name: 'Cases', + urlKey: 'case', + }, + detections: { + disabled: false, + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', + }, + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + pageName: 'hosts', + pathName: '/hosts', + search: '', + tabName: 'authentications', + query: { query: '', language: 'kuery' }, + filters: [], + savedQuery: undefined, + timeline: { + id: '', + isOpen: false, + }, + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }, + undefined + ); + }); + test('it calls setBreadcrumbs with correct path on update', () => { + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); + wrapper.update(); + expect(setBreadcrumbs).toHaveBeenNthCalledWith( + 1, + { + detailName: undefined, + filters: [], + navTabs: { + case: { + disabled: false, + href: '#/link-to/case', + id: 'case', + name: 'Cases', + urlKey: 'case', + }, + detections: { + disabled: false, + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', + }, + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + pageName: 'hosts', + pathName: '/hosts', + query: { language: 'kuery', query: '' }, + savedQuery: undefined, + search: '', + state: undefined, + tabName: 'authentications', + timeline: { id: '', isOpen: false }, + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }, + undefined + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.tsx b/x-pack/plugins/siem/public/components/navigation/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/index.tsx rename to x-pack/plugins/siem/public/components/navigation/index.tsx diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx new file mode 100644 index 0000000000000..99ded06cfdcc8 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -0,0 +1,157 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { navTabs } from '../../../pages/home/home_navigations'; +import { SiemPageName } from '../../../pages/home/types'; +import { navTabsHostDetails } from '../../../pages/hosts/details/nav_tabs'; +import { HostsTableType } from '../../../store/hosts/model'; +import { RouteSpyState } from '../../../utils/route/types'; +import { CONSTANTS } from '../../url_state/constants'; +import { TabNavigationComponent } from './'; +import { TabNavigationProps } from './types'; + +describe('Tab Navigation', () => { + const pageName = SiemPageName.hosts; + const hostName = 'siem-window'; + const tabName = HostsTableType.authentications; + const pathName = `/${pageName}/${hostName}/${tabName}`; + + describe('Page Navigation', () => { + const mockProps: TabNavigationProps & RouteSpyState = { + pageName, + pathName, + detailName: undefined, + search: '', + tabName, + navTabs, + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }; + test('it mounts with correct tab highlighted', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const hostsTab = wrapper.find('EuiTab[data-test-subj="navigation-hosts"]'); + expect(hostsTab.prop('isSelected')).toBeTruthy(); + }); + test('it changes active tab when nav changes by props', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const networkTab = () => wrapper.find('EuiTab[data-test-subj="navigation-network"]').first(); + expect(networkTab().prop('isSelected')).toBeFalsy(); + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); + wrapper.update(); + expect(networkTab().prop('isSelected')).toBeTruthy(); + }); + test('it carries the url state in the link', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const firstTab = wrapper.find('EuiTab[data-test-subj="navigation-network"]'); + expect(firstTab.props().href).toBe( + "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" + ); + }); + }); + + describe('Table Navigation', () => { + const mockHasMlUserPermissions = true; + const mockProps: TabNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, + navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions), + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }; + test('it mounts with correct tab highlighted', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const tableNavigationTab = wrapper.find( + `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` + ); + + expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); + }); + test('it changes active tab when nav changes by props', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const tableNavigationTab = () => + wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); + expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); + wrapper.setProps({ + pageName: SiemPageName.hosts, + pathName: `/${SiemPageName.hosts}`, + tabName: HostsTableType.events, + }); + wrapper.update(); + expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); + }); + test('it carries the url state in the link', () => { + const wrapper = mount(<TabNavigationComponent {...mockProps} />); + const firstTab = wrapper.find( + `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` + ); + expect(firstTab.props().href).toBe( + `#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))` + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx rename to x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts b/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts new file mode 100644 index 0000000000000..2e2dea09f8c38 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts @@ -0,0 +1,33 @@ +/* + * 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 { UrlInputsModel } from '../../../store/inputs/model'; +import { CONSTANTS } from '../../url_state/constants'; +import { HostsTableType } from '../../../store/hosts/model'; +import { TimelineUrl } from '../../../store/timeline/model'; +import { Filter, Query } from '../../../../../../../src/plugins/data/public'; + +import { SiemNavigationProps } from '../types'; + +export interface TabNavigationProps extends SiemNavigationProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timeline]: TimelineUrl; +} + +export interface TabNavigationItemProps { + href: string; + hrefWithSearch: string; + id: string; + disabled: boolean; + name: string; + isSelected: boolean; +} diff --git a/x-pack/plugins/siem/public/components/navigation/types.ts b/x-pack/plugins/siem/public/components/navigation/types.ts new file mode 100644 index 0000000000000..e8a2865938062 --- /dev/null +++ b/x-pack/plugins/siem/public/components/navigation/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter, Query } from '../../../../../../src/plugins/data/public'; +import { HostsTableType } from '../../store/hosts/model'; +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../store/timeline/model'; +import { CONSTANTS, UrlStateType } from '../url_state/constants'; + +export interface SiemNavigationProps { + display?: 'default' | 'condensed'; + navTabs: Record<string, NavTab>; +} + +export interface SiemNavigationComponentProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + urlState: { + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timeline]: TimelineUrl; + }; +} + +export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; + +export interface NavTab { + id: string; + name: string; + href: string; + disabled: boolean; + urlKey: UrlStateType; + isDetailPage?: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/use_get_url_search.tsx b/x-pack/plugins/siem/public/components/navigation/use_get_url_search.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/navigation/use_get_url_search.tsx rename to x-pack/plugins/siem/public/components/navigation/use_get_url_search.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx b/x-pack/plugins/siem/public/components/netflow/fingerprints/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/fingerprints/index.tsx rename to x-pack/plugins/siem/public/components/netflow/fingerprints/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx b/x-pack/plugins/siem/public/components/netflow/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx rename to x-pack/plugins/siem/public/components/netflow/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.tsx b/x-pack/plugins/siem/public/components/netflow/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/index.tsx rename to x-pack/plugins/siem/public/components/netflow/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx b/x-pack/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx b/x-pack/plugins/siem/public/components/netflow/netflow_columns/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/index.tsx rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/types.ts b/x-pack/plugins/siem/public/components/netflow/netflow_columns/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/types.ts rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx b/x-pack/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx rename to x-pack/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/types.ts b/x-pack/plugins/siem/public/components/netflow/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/netflow/types.ts rename to x-pack/plugins/siem/public/components/netflow/types.ts diff --git a/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts b/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts new file mode 100644 index 0000000000000..96bd9b08bf8bf --- /dev/null +++ b/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts @@ -0,0 +1,492 @@ +/* + * 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 { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../common/constants'; +import { KibanaServices } from '../../lib/kibana'; +import { rawNewsApiResponse } from '../../mock/news'; +import { rawNewsJSON } from '../../mock/raw_news'; + +import { + fetchNews, + getLocale, + getNewsFeedUrl, + getNewsItemsFromApiResponse, + removeSnapshotFromVersion, + showNewsItem, +} from './helpers'; +import { NewsItem, RawNewsApiResponse } from './types'; + +jest.mock('../../lib/kibana'); + +describe('helpers', () => { + describe('removeSnapshotFromVersion', () => { + test('it should remove an all-caps `-SNAPSHOT`', () => { + const version = '8.0.0-SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should remove a mixed-case `-SnApShoT`', () => { + const version = '8.0.0-SnApShoT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should remove all occurrences of `-SNAPSHOT`, regardless of where they appear in the version', () => { + const version = '-SNAPSHOT8.0.0-SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should NOT transform a version when it does not contain a `-SNAPSHOT`', () => { + const version = '8.0.0'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should NOT transform a version if it omits the dash in `SNAPSHOT`', () => { + const version = '8.0.0SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0SNAPSHOT'); + }); + + test('it should NOT transform a version if has only a partial `-SNAPSHOT`', () => { + const version = '8.0.0-SNAP'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0-SNAP'); + }); + + test('it should NOT transform an undefined version', () => { + const version = undefined; + + expect(removeSnapshotFromVersion(version)).toBeUndefined(); + }); + + test('it should NOT transform an empty version', () => { + const version = ''; + + expect(removeSnapshotFromVersion(version)).toEqual(''); + }); + }); + + describe('getNewsFeedUrl', () => { + const getKibanaVersion = () => '8.0.0'; + + test('it combines the (default) base URL from settings and the Kibana version to return the expected URL', () => { + expect( + getNewsFeedUrl({ newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, getKibanaVersion }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + + test('it combines a URL with extra whitespace and the Kibana version to return the expected URL', () => { + const withExtraWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT} `; + + expect(getNewsFeedUrl({ newsFeedUrlSetting: withExtraWhitespace, getKibanaVersion })).toEqual( + 'https://feeds.elastic.co/security-solution/v8.0.0.json' + ); + }); + + test('it combines a URL with a trailing slash and the Kibana version to return the expected URL', () => { + const withTrailingSlash = `${NEWS_FEED_URL_SETTING_DEFAULT}/`; + + expect(getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlash, getKibanaVersion })).toEqual( + 'https://feeds.elastic.co/security-solution/v8.0.0.json' + ); + }); + + test('it combines a URL with a trailing slash plus whitespace and the Kibana version to return the expected URL', () => { + const withTrailingSlashPlusWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT}/ `; + + expect( + getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlashPlusWhitespace, getKibanaVersion }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + + test('it combines a URL and a Kibana version with a `-SNAPSHOT` to return the expected URL', () => { + const getKibanaVersionWithSnapshot = () => '8.0.0-SNAPSHOT'; + + expect( + getNewsFeedUrl({ + newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, + getKibanaVersion: getKibanaVersionWithSnapshot, + }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + }); + + describe('getLocale', () => { + const fallback = 'wowzers'; + + test('it returns language specified in the document', () => { + const lang = 'ja'; + + document.documentElement.lang = lang; + + expect(getLocale(fallback)).toEqual(lang); + }); + + test('it returns the fallback when the language in the document is an empty string', () => { + document.documentElement.lang = ''; + + expect(getLocale(fallback)).toEqual(fallback); + }); + }); + + describe('getNewsItemsFromApiResponse', () => { + const expectedNewsItems: NewsItem[] = [ + { + description: + "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", + expireOn: expect.any(Date), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: + 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', + linkUrl: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Got SIEM Questions?', + }, + { + description: + 'Elastic Security combines the threat hunting and analytics of Elastic SIEM with the prevention and response provided by Elastic Endpoint Security.', + expireOn: expect.any(Date), + hash: 'edcb2d396ffdd80bfd5a97fbc0dc9f4b73477f9be556863fe0a1caf086679420', + imageUrl: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1caa35177420c61b/5d0d0394d8ff351753cbf2c5/illustrated-screenshot-hero-siem.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/elastic-security-7-5-0-released?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Elastic Security 7.5.0 released', + }, + { + description: + 'At Elastic, we’re bringing endpoint protection and SIEM together into the same experience to streamline how you secure your organization.', + expireOn: expect.any(Date), + hash: 'ec970adc85e9eede83f77e4cc6a6fea00cd7822cbe48a71dc2c5f1df10939196', + imageUrl: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd0eb8689eafe398a/5d970ecc1970e80e85277925/illustration-endpoint-hero.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/webinars/elastic-endpoint-security-overview-security-starts-at-the-endpoint?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Elastic Endpoint Security Overview Webinar', + }, + { + description: + 'For small businesses and homes, having access to effective security analytics can come at a high cost of either time or money. Well, until now!', + expireOn: expect.any(Date), + hash: 'aa243fd5845356a5ccd54a7a11b208ed307e0d88158873b1fcf7d1164b739bac', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt024c26b7636cb24f/5daf4e293a326d6df6c0e025/home-siem-blog-1-map.jpg?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/elastic-siem-for-small-business-and-home-1-getting-started?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Trying Elastic SIEM at Home?', + }, + { + description: + 'Elastic is excited to announce the introduction of Elastic Endpoint Security, based on Elastic’s acquisition of Endgame, a pioneer and industry-recognized leader in endpoint threat prevention, detection, and response.', + expireOn: expect.any(Date), + hash: '3c64576c9749d33ff98726d641cdf2fb2bfde3dd9a6f99ff2573ac8d8c5b2c02', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt1f87637fb7870298/5d9fe27bf8ca980f8717f6f8/screenshot-resolver-trickbot-enrichments-showing-defender-shutdown-endgame-2-optimized.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/introducing-elastic-endpoint-security?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Introducing Elastic Endpoint Security', + }, + { + description: + 'Elastic SIEM is powered by Elastic Common Schema. With ECS, analytics content such as dashboards, rules, and machine learning jobs can be applied more broadly, searches can be crafted more narrowly, and field names are easier to remember.', + expireOn: expect.any(Date), + hash: 'b8a0d3d21e9638bde891ab5eb32594b3d7a3daacc7f0900c6dd506d5d7b42410', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt71256f06dc672546/5c98d595975fd58f4d12646d/ecs-intro-dashboard-1360.jpg?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/introducing-the-elastic-common-schema?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'What is Elastic Common Schema (ECS)?', + }, + ]; + + test('it returns an empty collection of news items when the response is undefined', () => { + expect(getNewsItemsFromApiResponse(undefined)).toEqual([]); + }); + + test('it returns an empty collection of news items when the response is null', () => { + expect(getNewsItemsFromApiResponse(null)).toEqual([]); + }); + + test('it returns an empty collection of news items when the response items are undefined', () => { + expect(getNewsItemsFromApiResponse({ items: undefined })).toEqual([]); + }); + + test('it returns an empty collection of news items when the response items are null', () => { + expect(getNewsItemsFromApiResponse({ items: null })).toEqual([]); + }); + + test('it returns the expected news items when the browser language matches the i18n values in the response', () => { + const lang = 'en'; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when an ALL CAPS the browser language matches the i18n values in the response', () => { + const allCapsLang = 'EN'; + + document.documentElement.lang = allCapsLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when the browser language does NOT match the i18n values in the response', () => { + const nonMatchingLang = 'ja'; + + document.documentElement.lang = nonMatchingLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when the browser language is an empty string', () => { + const emptyLang = ''; + + document.documentElement.lang = emptyLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news item when parsing a raw JSON response', () => { + const lang = 'en'; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(JSON.parse(rawNewsJSON))).toEqual(expectedNewsItems); + }); + + describe('translated items', () => { + const translatedDescription = + 'Elastic SIEMユーザーの素晴らしいコミュニティがそこにあります。 Elastic SIEMアプリの設定、学習、使用、および脅威の検出に関するディスカッションに参加してください!'; + const translatedImageUrl = 'https://aws1.discourse-cdn.com/elastic/translated-image-url'; + const translatedLinkUrl = 'https://discuss.elastic.co/translated-link-url'; + const translatedTitle = 'SIEMに関する質問はありますか?'; + + const withNonDefaultTranslations: RawNewsApiResponse = { + items: [ + { + title: { en: 'Got SIEM Questions?', ja: translatedTitle }, + description: { + en: + "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", + ja: translatedDescription, + }, + link_text: null, + link_url: { + en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + ja: translatedLinkUrl, + }, + languages: null, + badge: { en: '7.6' }, + image_url: { + en: + 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', + ja: translatedImageUrl, + }, + publish_on: new Date('2020-01-01T00:00:00'), + expire_on: new Date('2020-12-31T00:00:00'), + }, + ], + }; + + test('it returns a translated description when the browser language matches additional translated content', () => { + const lang = 'ja'; // an additional translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].description).toEqual( + translatedDescription + ); + }); + + test('it returns a translated imageUrl when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].imageUrl).toEqual( + translatedImageUrl + ); + }); + + test('it returns a translated linkUrl when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].linkUrl).toEqual( + translatedLinkUrl + ); + }); + + test('it returns a translated title when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + translatedTitle + ); + }); + + test('it returns the default translated title when the browser language matches additional translated content', () => { + const lang = 'fr'; // no translation for this language + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + 'Got SIEM Questions?' + ); + }); + + test('it returns the default translated title when the browser language is an empty string', () => { + const lang = ''; // just an empty string + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + 'Got SIEM Questions?' + ); + }); + }); + + test('it generates a news item hash when an item does NOT include it', () => { + const lang = 'en'; + + const itemHasNoHash: RawNewsApiResponse = { + items: [ + { + title: { en: 'Got SIEM Questions?' }, + description: { + en: 'some description', + }, + link_text: null, + link_url: { en: 'https://example.com/link-url' }, + languages: null, + badge: { en: '7.6' }, + image_url: { + en: 'https://example.com/image-url', + }, + publish_on: new Date('2020-01-01T00:00:00'), + expire_on: new Date('2020-12-31T00:00:00'), + }, + ], + }; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(itemHasNoHash)[0].hash.length).toBeGreaterThan(0); + }); + }); + + describe('fetchNews', () => { + const mockKibanaServices = KibanaServices.get as jest.Mock; + const fetchMock = jest.fn(); + mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rawNewsApiResponse); + }); + + test('it returns the raw API response from the news feed', async () => { + const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; + expect(await fetchNews({ newsFeedUrl })).toEqual(rawNewsApiResponse); + }); + }); + + describe('showNewsItem', () => { + const MOCK_DATE_NOW = 1579848101395; // 2020-01-24T06:41:41.395Z + + let dateNowSpy: { mockRestore: () => void }; + + beforeAll(() => { + dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_NOW); + }); + + afterAll(() => { + dateNowSpy.mockRestore(); + }); + + test('it should return true when the article has already been published, and will expire in the future', () => { + const alreadyPublishedAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 1000), + title: 'Show this post', + }; + + expect(showNewsItem(alreadyPublishedAndNotExpired)).toEqual(true); + }); + + test('it should return false when the article was published exactly "now", and will expire in the future', () => { + const publishedJustNowAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(publishedJustNowAndNotExpired)).toEqual(false); + }); + + test('it should return false when the article has not been published yet, and has not expired yet', () => { + const notPublishedAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 5000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW + 1000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(notPublishedAndNotExpired)).toEqual(false); + }); + + test('it should return false when the article was published in the past, and will expire exactly now', () => { + const alreadyPublishedAndExpiredNow: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 1000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(alreadyPublishedAndExpiredNow)).toEqual(false); + }); + + test('it should return false when the article was published in the past, and it already expired', () => { + const articleJustExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW - 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 5000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(articleJustExpired)).toEqual(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts b/x-pack/plugins/siem/public/components/news_feed/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/helpers.ts rename to x-pack/plugins/siem/public/components/news_feed/helpers.ts diff --git a/x-pack/plugins/siem/public/components/news_feed/index.tsx b/x-pack/plugins/siem/public/components/news_feed/index.tsx new file mode 100644 index 0000000000000..ae41737c5bcad --- /dev/null +++ b/x-pack/plugins/siem/public/components/news_feed/index.tsx @@ -0,0 +1,60 @@ +/* + * 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, { useEffect, useState } from 'react'; + +import { fetchNews, getNewsFeedUrl, getNewsItemsFromApiResponse } from './helpers'; +import { useKibana, useUiSetting$, KibanaServices } from '../../lib/kibana'; +import { NewsFeed } from './news_feed'; +import { NewsItem } from './types'; + +export const StatefulNewsFeed = React.memo<{ + enableNewsFeedSetting: string; + newsFeedSetting: string; +}>(({ enableNewsFeedSetting, newsFeedSetting }) => { + const kibanaNewsfeedEnabled = useKibana().services.newsfeed; + const [enableNewsFeed] = useUiSetting$<boolean>(enableNewsFeedSetting); + const [newsFeedUrlSetting] = useUiSetting$<string>(newsFeedSetting); + const [news, setNews] = useState<NewsItem[] | null>(null); + + // respect kibana's global newsfeed.enabled setting + const newsfeedEnabled = kibanaNewsfeedEnabled && enableNewsFeed; + + const newsFeedUrl = getNewsFeedUrl({ + newsFeedUrlSetting, + getKibanaVersion: () => KibanaServices.getKibanaVersion(), + }); + + useEffect(() => { + let canceled = false; + + const fetchData = async () => { + try { + const apiResponse = await fetchNews({ newsFeedUrl }); + + if (!canceled) { + setNews(getNewsItemsFromApiResponse(apiResponse)); + } + } catch { + if (!canceled) { + setNews([]); + } + } + }; + + if (newsfeedEnabled) { + fetchData(); + } + + return () => { + canceled = true; + }; + }, [newsfeedEnabled, newsFeedUrl]); + + return <>{newsfeedEnabled ? <NewsFeed news={news} /> : null}</>; +}); + +StatefulNewsFeed.displayName = 'StatefulNewsFeed'; diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx b/x-pack/plugins/siem/public/components/news_feed/news_feed.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/news_feed.tsx rename to x-pack/plugins/siem/public/components/news_feed/news_feed.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx b/x-pack/plugins/siem/public/components/news_feed/news_link/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/news_link/index.tsx rename to x-pack/plugins/siem/public/components/news_feed/news_link/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx b/x-pack/plugins/siem/public/components/news_feed/no_news/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/no_news/index.tsx rename to x-pack/plugins/siem/public/components/news_feed/no_news/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx b/x-pack/plugins/siem/public/components/news_feed/post/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/post/index.tsx rename to x-pack/plugins/siem/public/components/news_feed/post/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts b/x-pack/plugins/siem/public/components/news_feed/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/translations.ts rename to x-pack/plugins/siem/public/components/news_feed/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/news_feed/types.ts b/x-pack/plugins/siem/public/components/news_feed/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/news_feed/types.ts rename to x-pack/plugins/siem/public/components/news_feed/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap b/x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap rename to x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx b/x-pack/plugins/siem/public/components/notes/add_note/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/index.test.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/index.tsx b/x-pack/plugins/siem/public/components/notes/add_note/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/index.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx b/x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.test.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/add_note/new_note.tsx rename to x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/columns.tsx b/x-pack/plugins/siem/public/components/notes/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/columns.tsx rename to x-pack/plugins/siem/public/components/notes/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx b/x-pack/plugins/siem/public/components/notes/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/helpers.tsx rename to x-pack/plugins/siem/public/components/notes/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/index.tsx b/x-pack/plugins/siem/public/components/notes/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/index.tsx rename to x-pack/plugins/siem/public/components/notes/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap similarity index 89% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap rename to x-pack/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap index 321b9ea9549a5..fb64708cdb897 100644 --- a/x-pack/legacy/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap +++ b/x-pack/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap @@ -53,11 +53,11 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "xs": 0, }, "euiButtonColorDisabled": "#434548", - "euiButtonColorDisabledText": "#757678", + "euiButtonColorDisabledText": "#4c4e51", "euiButtonColorGhostDisabled": "#343741", "euiButtonEmptyTypes": Object { "danger": "#ff6666", - "disabled": "#757678", + "disabled": "#4c4e51", "ghost": "#ffffff", "primary": "#1ba9f5", "text": "#dfe5ef", @@ -66,10 +66,10 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiButtonHeightSmall": "32px", "euiButtonIconTypes": Object { "danger": "#ff6666", - "disabled": "#757678", + "disabled": "#4c4e51", "ghost": "#ffffff", "primary": "#1ba9f5", - "subdued": "#98a2b3", + "subdued": "#81858f", "success": "#7de2d1", "text": "#dfe5ef", "warning": "#ffce7a", @@ -142,12 +142,16 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiCodeBlockTitleColor": "#75a5ff", "euiCodeBlockTypeColor": "#da4939", "euiCodeFontFamily": "'Roboto Mono', 'Consolas', 'Menlo', 'Courier', monospace", + "euiCollapsibleNavGroupDarkBackgroundColor": "#131317", + "euiCollapsibleNavGroupDarkHighContrastColor": "#1ba9f5", + "euiCollapsibleNavGroupLightBackgroundColor": "#1a1b20", + "euiCollapsibleNavWidth": "320px", "euiColorAccent": "#f990c0", "euiColorAccentText": "#f990c0", - "euiColorChartBand": "#2a2c35", + "euiColorChartBand": "#2a2b33", "euiColorChartLines": "#343741", "euiColorDanger": "#ff6666", - "euiColorDangerText": "#ff6666", + "euiColorDangerText": "#ff7575", "euiColorDarkShade": "#98a2b3", "euiColorDarkestShade": "#d4dae5", "euiColorEmptyShade": "#1d1e24", @@ -218,15 +222,17 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` }, "euiExpressionColors": Object { "accent": "#f990c0", - "danger": "#ff6666", + "danger": "#ff7575", "primary": "#1ba9f5", "secondary": "#7de2d1", - "subdued": "#98a2b3", + "subdued": "#81858f", "warning": "#ffce7a", }, "euiFilePickerTallHeight": "128px", "euiFocusBackgroundColor": "#232635", "euiFocusRingAnimStartColor": "rgba(27, 169, 245, 0)", + "euiFocusRingAnimStartSize": "6px", + "euiFocusRingAnimStartSizeLarge": "10px", "euiFocusRingColor": "rgba(27, 169, 245, 0.3)", "euiFocusRingSize": "3px", "euiFocusRingSizeLarge": "4px", @@ -270,13 +276,15 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiHeaderBackgroundColor": "#1d1e24", "euiHeaderBreadcrumbColor": "#d4dae5", "euiHeaderChildSize": "48px", + "euiHeaderHeight": "48px", + "euiHeaderHeightCompensation": "49px", "euiIconColors": Object { "accent": "#f990c0", - "danger": "#ff6666", + "danger": "#ff7575", "ghost": "#ffffff", "primary": "#1ba9f5", "secondary": "#7de2d1", - "subdued": "#98a2b3", + "subdued": "#81858f", "success": "#7de2d1", "text": "#dfe5ef", "warning": "#ffce7a", @@ -294,14 +302,17 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiLineHeight": 1.5, "euiLinkColor": "#1ba9f5", "euiListGroupGutterTypes": Object { - "gutterM": "16px", - "gutterS": "8px", + "gutterMedium": "16px", + "gutterSmall": "8px", }, "euiListGroupItemColorTypes": Object { + "ghost": "#ffffff", "primary": "#1ba9f5", - "subdued": "#98a2b3", + "subdued": "#81858f", "text": "#dfe5ef", }, + "euiListGroupItemHoverBackground": "rgba(52, 55, 65, 0.25)", + "euiListGroupItemHoverBackgroundGhost": "rgba(255, 255, 255, 0.09999999999999998)", "euiListGroupItemSizeTypes": Object { "large": "20px", "medium": "16px", @@ -373,7 +384,7 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "danger": "#ff6666", "primary": "#1ba9f5", "secondary": "#7de2d1", - "subdued": "#535966", + "subdued": "#81858f", "warning": "#ffce7a", }, "euiProgressSizes": Object { @@ -461,9 +472,55 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` "euiTableHoverSelectedColor": "#202230", "euiTableSelectedColor": "#232635", "euiTextColor": "#dfe5ef", + "euiTextColors": Object { + "accent": "#f990c0", + "danger": "#ff6666", + "default": "#dfe5ef", + "ghost": "#ffffff", + "secondary": "#7de2d1", + "subdued": "#81858f", + "warning": "#ffce7a", + }, "euiTextConstrainedMaxWidth": "36em", "euiTextScale": "2.25 1.75 1.25 1.125 1 0.875 0.75", + "euiTextSubduedColor": "#81858f", "euiTitleColor": "#dfe5ef", + "euiTitles": Object { + "l": Object { + "font-size": "36px", + "font-weight": 300, + "letter-spacing": "-0.03em", + "line-height": "3rem", + }, + "m": Object { + "font-size": "28px", + "font-weight": 300, + "letter-spacing": "-0.04em", + "line-height": "2.5rem", + }, + "s": Object { + "font-size": "20px", + "font-weight": 500, + "letter-spacing": "-0.025em", + "line-height": "2rem", + }, + "xs": Object { + "font-size": "16px", + "font-weight": 600, + "letter-spacing": "-0.02em", + "line-height": "1.5rem", + }, + "xxs": Object { + "font-size": "14px", + "font-weight": 700, + "line-height": "1.5rem", + }, + "xxxs": Object { + "font-size": "12px", + "font-weight": 700, + "line-height": "1.5rem", + }, + }, "euiToastTypes": Object { "danger": "#ff6666", "primary": "#1ba9f5", @@ -611,11 +668,12 @@ exports[`NoteCardBody renders correctly against snapshot 1`] = ` }, "textColors": Object { "accent": "#f990c0", - "danger": "#ff6666", - "default": "#dfe5ef", + "danger": "#ff7575", "ghost": "#ffffff", + "primary": "#1ba9f5", "secondary": "#7de2d1", - "subdued": "#98a2b3", + "subdued": "#81858f", + "text": "#dfe5ef", "warning": "#ffce7a", }, "textareaResizing": Object { diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.test.tsx b/x-pack/plugins/siem/public/components/notes/note_card/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/index.test.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/index.tsx b/x-pack/plugins/siem/public/components/notes/note_card/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/index.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx b/x-pack/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx b/x-pack/plugins/siem/public/components/notes/note_card/note_card_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_body.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/note_card_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx b/x-pack/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.tsx b/x-pack/plugins/siem/public/components/notes/note_card/note_card_header.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/note_card_header.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/note_card_header.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.test.tsx b/x-pack/plugins/siem/public/components/notes/note_card/note_created.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.test.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/note_created.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.tsx b/x-pack/plugins/siem/public/components/notes/note_card/note_created.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_card/note_created.tsx rename to x-pack/plugins/siem/public/components/notes/note_card/note_created.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.test.tsx b/x-pack/plugins/siem/public/components/notes/note_cards/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.test.tsx rename to x-pack/plugins/siem/public/components/notes/note_cards/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx b/x-pack/plugins/siem/public/components/notes/note_cards/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/note_cards/index.tsx rename to x-pack/plugins/siem/public/components/notes/note_cards/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/notes/translations.ts b/x-pack/plugins/siem/public/components/notes/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/notes/translations.ts rename to x-pack/plugins/siem/public/components/notes/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/constants.ts b/x-pack/plugins/siem/public/components/open_timeline/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/constants.ts rename to x-pack/plugins/siem/public/components/open_timeline/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx b/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx rename to x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/edit_timeline_actions.tsx b/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/edit_timeline_actions.tsx rename to x-pack/plugins/siem/public/components/open_timeline/edit_timeline_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx b/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx rename to x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.tsx b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.tsx rename to x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx new file mode 100644 index 0000000000000..12cf952bb1ff8 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx @@ -0,0 +1,73 @@ +/* + * 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, useCallback } from 'react'; +import { DeleteTimelines } from '../types'; + +import { TimelineDownloader } from './export_timeline'; +import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; +import { exportSelectedTimeline } from '../../../containers/timeline/api'; + +export interface ExportTimeline { + disableExportTimelineDownloader: () => void; + enableExportTimelineDownloader: () => void; + isEnableDownloader: boolean; +} + +export const useExportTimeline = (): ExportTimeline => { + const [isEnableDownloader, setIsEnableDownloader] = useState(false); + + const enableExportTimelineDownloader = useCallback(() => { + setIsEnableDownloader(true); + }, []); + + const disableExportTimelineDownloader = useCallback(() => { + setIsEnableDownloader(false); + }, []); + + return { + disableExportTimelineDownloader, + enableExportTimelineDownloader, + isEnableDownloader, + }; +}; + +const EditTimelineActionsComponent: React.FC<{ + deleteTimelines: DeleteTimelines | undefined; + ids: string[]; + isEnableDownloader: boolean; + isDeleteTimelineModalOpen: boolean; + onComplete: () => void; + title: string; +}> = ({ + deleteTimelines, + ids, + isEnableDownloader, + isDeleteTimelineModalOpen, + onComplete, + title, +}) => ( + <> + <TimelineDownloader + exportedIds={ids} + getExportedData={exportSelectedTimeline} + isEnableDownloader={isEnableDownloader} + onComplete={onComplete} + /> + {deleteTimelines != null && ( + <DeleteTimelineModalOverlay + deleteTimelines={deleteTimelines} + isModalOpen={isDeleteTimelineModalOpen} + onComplete={onComplete} + savedObjectIds={ids} + title={title} + /> + )} + </> +); + +export const EditTimelineActions = React.memo(EditTimelineActionsComponent); +export const EditOneTimelineAction = React.memo(EditTimelineActionsComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts rename to x-pack/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts rename to x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts rename to x-pack/plugins/siem/public/components/open_timeline/helpers.ts diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx new file mode 100644 index 0000000000000..ea28bc06ef915 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx @@ -0,0 +1,649 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; +import { MockedProvider } from 'react-apollo/test-utils'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { wait } from '../../lib/helpers'; +import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_providers'; +import { mockOpenTimelineQueryResults } from '../../mock/timeline_results'; +import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; + +import { NotePreviews } from './note_previews'; +import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; +import { StatefulOpenTimeline } from '.'; +import { useGetAllTimeline, getAllTimeline } from '../../containers/timeline/all'; +jest.mock('../../lib/kibana'); +jest.mock('../../containers/timeline/all', () => { + const originalModule = jest.requireActual('../../containers/timeline/all'); + return { + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); + +describe('StatefulOpenTimeline', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const title = 'All Timelines / Open Timelines'; + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); + + test('it has the expected initial state', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + const componentProps = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .props(); + + expect(componentProps).toEqual({ + ...componentProps, + itemIdToExpandedNotesRowMap: {}, + onlyFavorites: false, + pageIndex: 0, + pageSize: 10, + query: '', + selectedItems: [], + sortDirection: 'desc', + sortField: 'updated', + }); + }); + + describe('#onQueryChange', () => { + test('it updates the query state with the expected trimmed value when the user enters a query', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + expect( + wrapper + .find('[data-test-subj="search-row"]') + .first() + .prop('query') + ).toEqual('abcd'); + }); + + test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + + expect( + wrapper + .find('[data-test-subj="query-message"]') + .first() + .text() + ).toContain('Showing: 11 timelines with'); + }); + + test('echos (renders) the query when the user enters a query', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + + expect( + wrapper + .find('[data-test-subj="selectable-query-text"]') + .first() + .text() + ).toEqual('with "abcd"'); + }); + }); + + describe('#focusInput', () => { + test('focuses the input when the component mounts', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + expect( + wrapper + .find(`.${OPEN_TIMELINE_CLASS_NAME} input`) + .first() + .getDOMNode().id === document.activeElement!.id + ).toBe(true); + }); + }); + + describe('#onAddTimelinesToFavorites', () => { + // This functionality is hiding for now and waiting to see the light in the near future + test.skip('it invokes addTimelinesToFavorites with the selected timelines when the button is clicked', async () => { + const addTimelinesToFavorites = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper + .find('[data-test-subj="favorite-selected"]') + .first() + .simulate('click'); + + expect(addTimelinesToFavorites).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); + }); + + describe('#onDeleteSelected', () => { + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes deleteTimelines with the selected timelines when the button is clicked', async () => { + const deleteTimelines = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper + .find('[data-test-subj="delete-selected"]') + .first() + .simulate('click'); + + expect(deleteTimelines).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); + }); + + describe('#onSelectionChange', () => { + test('it updates the selection state when timelines are selected', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + const selectedItems: [] = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); + + expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query + }); + }); + + describe('#onTableChange', () => { + test('it updates the sort state when the user clicks on a column to sort it', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('desc'); + + wrapper + .find('thead tr th button') + .at(0) + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('asc'); + }); + }); + + describe('#onToggleOnlyFavorites', () => { + test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(false); + + wrapper + .find('[data-test-subj="only-favorites-toggle"]') + .first() + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(true); + }); + }); + + describe('#onToggleShowNotes', () => { + test('it updates the itemIdToExpandedNotesRowMap state when the user clicks the expand notes button', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({}); + + wrapper + .find('[data-test-subj="expand-notes"]') + .first() + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({ + '10849df0-7b44-11e9-a608-ab3d811609': ( + <NotePreviews + notes={ + mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes != null + ? mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].notes.map( + note => ({ ...note, savedObjectId: note.noteId }) + ) + : [] + } + /> + ), + }); + }); + + test('it renders the expanded notes when the expand button is clicked', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper.update(); + + wrapper + .find('[data-test-subj="expand-notes"]') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); + + expect( + wrapper + .find('[data-test-subj="note-previews-container"]') + .find('[data-test-subj="updated-by"]') + .first() + .text() + ).toEqual('elastic'); + }); + }); + + test('it renders the title', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + expect( + wrapper + .find('[data-test-subj="header-section-title"]') + .first() + .text() + ).toEqual(title); + }); + + describe('#resetSelectionState', () => { + test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + const getSelectedItem = (): [] => + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); + await wait(); + expect(getSelectedItem().length).toEqual(0); + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + expect(getSelectedItem().length).toEqual(13); + }); + }); + + test('it renders the expected count of matching timelines when no query has been entered', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <MockedProvider addTypename={false}> + <TestProviderWithoutDragAndDrop> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </TestProviderWithoutDragAndDrop> + </MockedProvider> + </ThemeProvider> + ); + + await wait(); + + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="query-message"]') + .first() + .text() + ).toContain('Showing: 11 timelines '); + }); + + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes onOpenTimeline with the expected parameters when the hyperlink is clicked', async () => { + const onOpenTimeline = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find( + `[data-test-subj="title-${ + mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId + }"]` + ) + .first() + .simulate('click'); + + expect(onOpenTimeline).toHaveBeenCalledWith({ + duplicate: false, + timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] + .savedObjectId, + }); + }); + + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes onOpenTimeline with the expected params when the button is clicked', async () => { + const onOpenTimeline = jest.fn(); + + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <StatefulOpenTimeline + data-test-subj="stateful-timeline" + apolloClient={apolloClient} + isModal={false} + defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} + title={title} + /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper + .find('[data-test-subj="open-duplicate"]') + .first() + .simulate('click'); + + expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx new file mode 100644 index 0000000000000..d26d02780ffba --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/index.tsx @@ -0,0 +1,343 @@ +/* + * 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 ApolloClient from 'apollo-client'; +import React, { useEffect, useState, useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { Dispatch } from 'redux'; +import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; +import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; +import { useGetAllTimeline } from '../../containers/timeline/all'; +import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types'; +import { State, timelineSelectors } from '../../store'; +import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; +import { timelineDefaults } from '../../store/timeline/defaults'; +import { + createTimeline as dispatchCreateNewTimeline, + updateIsLoading as dispatchUpdateIsLoading, +} from '../../store/timeline/actions'; +import { OpenTimeline } from './open_timeline'; +import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; +import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; +import { + ActionTimelineToShow, + DeleteTimelines, + EuiSearchBarQuery, + OnDeleteSelected, + OnOpenTimeline, + OnQueryChange, + OnSelectionChange, + OnTableChange, + OnTableChangeParams, + OpenTimelineProps, + OnToggleOnlyFavorites, + OpenTimelineResult, + OnToggleShowNotes, + OnDeleteOneTimeline, +} from './types'; +import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; + +interface OwnProps<TCache = object> { + apolloClient: ApolloClient<TCache>; + /** Displays open timeline in modal */ + isModal: boolean; + closeModalTimeline?: () => void; + hideActions?: ActionTimelineToShow[]; + onOpenTimeline?: (timeline: TimelineModel) => void; +} + +export type OpenTimelineOwnProps = OwnProps & + Pick< + OpenTimelineProps, + 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' + > & + PropsFromRedux; + +/** Returns a collection of selected timeline ids */ +export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): string[] => + selectedItems.reduce<string[]>( + (validSelections, timelineResult) => + timelineResult.savedObjectId != null + ? [...validSelections, timelineResult.savedObjectId] + : validSelections, + [] + ); + +/** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ +export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( + ({ + apolloClient, + closeModalTimeline, + createNewTimeline, + defaultPageSize, + hideActions = [], + isModal = false, + importDataModalToggle, + onOpenTimeline, + setImportDataModalToggle, + timeline, + title, + updateTimeline, + updateIsLoading, + }) => { + /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ + const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< + Record<string, JSX.Element> + >({}); + /** Only query for favorite timelines when true */ + const [onlyFavorites, setOnlyFavorites] = useState(false); + /** The requested page of results */ + const [pageIndex, setPageIndex] = useState(0); + /** The requested size of each page of search results */ + const [pageSize, setPageSize] = useState(defaultPageSize); + /** The current search criteria */ + const [search, setSearch] = useState(''); + /** The currently-selected timelines in the table */ + const [selectedItems, setSelectedItems] = useState<OpenTimelineResult[]>([]); + /** The requested sort direction of the query results */ + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); + /** The requested field to sort on */ + const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + + const { fetchAllTimeline, timelines, loading, totalCount, refetch } = useGetAllTimeline(); + + /** Invoked when the user presses enters to submit the text in the search input */ + const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { + setSearch(query.queryText.trim()); + }, []); + + /** Focuses the input that filters the field browser */ + const focusInput = () => { + const elements = document.querySelector<HTMLElement>(`.${OPEN_TIMELINE_CLASS_NAME} input`); + + if (elements != null) { + elements.focus(); + } + }; + + /* This feature will be implemented in the near future, so we are keeping it to know what to do */ + + /** Invoked when the user clicks the action to add the selected timelines to favorites */ + // const onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { + // const { addTimelinesToFavorites } = this.props; + // const { selectedItems } = this.state; + // if (addTimelinesToFavorites != null) { + // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); + // TODO: it's not possible to clear the selection state of the newly-favorited + // items, because we can't pass the selection state as props to the table. + // See: https://github.com/elastic/eui/issues/1077 + // TODO: the query must re-execute to show the results of the mutation + // } + // }; + + const deleteTimelines: DeleteTimelines = useCallback( + async (timelineIds: string[]) => { + if (timelineIds.includes(timeline.savedObjectId || '')) { + createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + } + await apolloClient.mutate< + DeleteTimelineMutation.Mutation, + DeleteTimelineMutation.Variables + >({ + mutation: deleteTimelineMutation, + fetchPolicy: 'no-cache', + variables: { id: timelineIds }, + }); + refetch(); + }, + [apolloClient, createNewTimeline, refetch, timeline] + ); + + const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( + async (timelineIds: string[]) => { + await deleteTimelines(timelineIds); + }, + [deleteTimelines] + ); + + /** Invoked when the user clicks the action to delete the selected timelines */ + const onDeleteSelected: OnDeleteSelected = useCallback(async () => { + await deleteTimelines(getSelectedTimelineIds(selectedItems)); + + // NOTE: we clear the selection state below, but if the server fails to + // delete a timeline, it will remain selected in the table: + resetSelectionState(); + + // TODO: the query must re-execute to show the results of the deletion + }, [selectedItems, deleteTimelines]); + + /** Invoked when the user selects (or de-selects) timelines */ + const onSelectionChange: OnSelectionChange = useCallback( + (newSelectedItems: OpenTimelineResult[]) => { + setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 + }, + [] + ); + + /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ + const onTableChange: OnTableChange = useCallback(({ page, sort }: OnTableChangeParams) => { + const { index, size } = page; + const { field, direction } = sort; + setPageIndex(index); + setPageSize(size); + setSortDirection(direction); + setSortField(field); + }, []); + + /** Invoked when the user toggles the option to only view favorite timelines */ + const onToggleOnlyFavorites: OnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ + const onToggleShowNotes: OnToggleShowNotes = useCallback( + (newItemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => { + setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); + }, + [] + ); + + /** Resets the selection state such that all timelines are unselected */ + const resetSelectionState = useCallback(() => { + setSelectedItems([]); + }, []); + + const openTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + if (isModal && closeModalTimeline != null) { + closeModalTimeline(); + } + + queryTimelineById({ + apolloClient, + duplicate, + onOpenTimeline, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + useEffect(() => { + focusInput(); + }, []); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: pageIndex + 1, + pageSize, + }, + search, + sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, + onlyUserFavorite: onlyFavorites, + timelines, + totalCount, + }); + }, [ + pageIndex, + pageSize, + search, + sortField, + sortDirection, + timelines, + totalCount, + onlyFavorites, + ]); + + return !isModal ? ( + <OpenTimeline + data-test-subj={'open-timeline'} + deleteTimelines={onDeleteOneTimeline} + defaultPageSize={defaultPageSize} + isLoading={loading} + itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} + importDataModalToggle={importDataModalToggle} + onAddTimelinesToFavorites={undefined} + onDeleteSelected={onDeleteSelected} + onlyFavorites={onlyFavorites} + onOpenTimeline={openTimeline} + onQueryChange={onQueryChange} + onSelectionChange={onSelectionChange} + onTableChange={onTableChange} + onToggleOnlyFavorites={onToggleOnlyFavorites} + onToggleShowNotes={onToggleShowNotes} + pageIndex={pageIndex} + pageSize={pageSize} + query={search} + refetch={refetch} + searchResults={timelines} + setImportDataModalToggle={setImportDataModalToggle} + selectedItems={selectedItems} + sortDirection={sortDirection} + sortField={sortField} + title={title} + totalSearchResultsCount={totalCount} + /> + ) : ( + <OpenTimelineModalBody + data-test-subj={'open-timeline-modal'} + deleteTimelines={onDeleteOneTimeline} + defaultPageSize={defaultPageSize} + hideActions={hideActions} + isLoading={loading} + itemIdToExpandedNotesRowMap={itemIdToExpandedNotesRowMap} + onAddTimelinesToFavorites={undefined} + onlyFavorites={onlyFavorites} + onOpenTimeline={openTimeline} + onQueryChange={onQueryChange} + onSelectionChange={onSelectionChange} + onTableChange={onTableChange} + onToggleOnlyFavorites={onToggleOnlyFavorites} + onToggleShowNotes={onToggleShowNotes} + pageIndex={pageIndex} + pageSize={pageSize} + query={search} + searchResults={timelines} + selectedItems={selectedItems} + sortDirection={sortDirection} + sortField={sortField} + title={title} + totalSearchResultsCount={totalCount} + /> + ); + } +); + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const mapStateToProps = (state: State) => { + const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; + return { + timeline, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + createNewTimeline: ({ + id, + columns, + show, + }: { + id: string; + columns: ColumnHeaderOptions[]; + show?: boolean; + }) => dispatch(dispatchCreateNewTimeline({ id, columns, show })), + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(dispatchUpdateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulOpenTimeline = connector(StatefulOpenTimelineComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx rename to x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx index 6b2f953b82de4..26aeab87e3510 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx @@ -14,7 +14,7 @@ import { TimelinesTable } from './timelines_table'; import { TitleRow } from './title_row'; import { ImportDataModal } from '../import_data_modal'; import * as i18n from './translations'; -import { importTimelines } from '../../containers/timeline/all/api'; +import { importTimelines } from '../../containers/timeline/api'; import { UtilityBarGroup, diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx new file mode 100644 index 0000000000000..46a0d46c1e0d1 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { ThemeProvider } from 'styled-components'; + +import { wait } from '../../../lib/helpers'; +import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers'; +import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results'; +import { useGetAllTimeline, getAllTimeline } from '../../../containers/timeline/all'; + +import { OpenTimelineModal } from '.'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../../utils/apollo_context', () => ({ + useApolloClient: () => ({}), +})); +jest.mock('../../../containers/timeline/all', () => { + const originalModule = jest.requireActual('../../../containers/timeline/all'); + return { + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); + +describe('OpenTimelineModal', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); + + test('it renders the expected modal', async () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <TestProviderWithoutDragAndDrop> + <MockedProvider mocks={mockOpenTimelineQueryResults} addTypename={false}> + <OpenTimelineModal onClose={jest.fn()} /> + </MockedProvider> + </TestProviderWithoutDragAndDrop> + </ThemeProvider> + ); + + await wait(); + + wrapper.update(); + + expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx rename to x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/search_row/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/search_row/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/search_row/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/search_row/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/search_row/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts rename to x-pack/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/title_row/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.test.tsx rename to x-pack/plugins/siem/public/components/open_timeline/title_row/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/title_row/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/title_row/index.tsx rename to x-pack/plugins/siem/public/components/open_timeline/title_row/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts b/x-pack/plugins/siem/public/components/open_timeline/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/open_timeline/translations.ts rename to x-pack/plugins/siem/public/components/open_timeline/translations.ts diff --git a/x-pack/plugins/siem/public/components/open_timeline/types.ts b/x-pack/plugins/siem/public/components/open_timeline/types.ts new file mode 100644 index 0000000000000..41999c6249277 --- /dev/null +++ b/x-pack/plugins/siem/public/components/open_timeline/types.ts @@ -0,0 +1,190 @@ +/* + * 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 { SetStateAction, Dispatch } from 'react'; +import { AllTimelinesVariables } from '../../containers/timeline/all'; +import { TimelineModel } from '../../store/timeline/model'; +import { NoteResult } from '../../graphql/types'; +import { Refetch } from '../../store/inputs/model'; +import { TimelineType } from '../../../common/types/timeline'; + +/** The users who added a timeline to favorites */ +export interface FavoriteTimelineResult { + userId?: number | null; + userName?: string | null; + favoriteDate?: number | null; +} + +export interface TimelineResultNote { + savedObjectId?: string | null; + note?: string | null; + noteId?: string | null; + updated?: number | null; + updatedBy?: string | null; +} + +export interface TimelineActionsOverflowColumns { + width: string; + actions: Array<{ + name: string; + icon?: string; + onClick?: (timeline: OpenTimelineResult) => void; + description: string; + render?: (timeline: OpenTimelineResult) => JSX.Element; + } | null>; +} + +/** The results of the query run by the OpenTimeline component */ +export interface OpenTimelineResult { + created?: number | null; + description?: string | null; + eventIdToNoteIds?: Readonly<Record<string, string[]>> | null; + favorite?: FavoriteTimelineResult[] | null; + noteIds?: string[] | null; + notes?: TimelineResultNote[] | null; + pinnedEventIds?: Readonly<Record<string, boolean>> | null; + savedObjectId?: string | null; + title?: string | null; + templateTimelineId?: string | null; + type?: TimelineType.template | TimelineType.default; + updated?: number | null; + updatedBy?: string | null; +} + +/** + * EuiSearchBar returns this object when the user changes the query. At the + * time of this writing, there is no typescript definition for this type, so + * only the properties used by the Open Timeline component are exposed. + */ +export interface EuiSearchBarQuery { + queryText: string; +} + +/** Performs IO to delete the specified timelines */ +export type DeleteTimelines = (timelineIds: string[], variables?: AllTimelinesVariables) => void; + +/** Invoked when the user clicks the action make the selected timelines favorites */ +export type OnAddTimelinesToFavorites = () => void; + +/** Invoked when the user clicks the action to delete the selected timelines */ +export type OnDeleteSelected = () => void; +export type OnDeleteOneTimeline = (timelineIds: string[]) => void; + +/** Invoked when the user clicks on the name of a timeline to open it */ +export type OnOpenTimeline = ({ + duplicate, + timelineId, +}: { + duplicate: boolean; + timelineId: string; +}) => void; + +export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; +export type SetActionTimeline = Dispatch<SetStateAction<OpenTimelineResult | undefined>>; +export type EnableExportTimelineDownloader = (selectedItem: OpenTimelineResult) => void; +/** Invoked when the user presses enters to submit the text in the search input */ +export type OnQueryChange = (query: EuiSearchBarQuery) => void; + +/** Invoked when the user selects (or de-selects) timelines in the table */ +export type OnSelectionChange = (selectedItems: OpenTimelineResult[]) => void; + +/** Invoked when the user toggles the option to only view favorite timelines */ +export type OnToggleOnlyFavorites = () => void; + +/** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ +export type OnToggleShowNotes = (itemIdToExpandedNotesRowMap: Record<string, JSX.Element>) => void; + +/** Parameters to the OnTableChange callback */ +export interface OnTableChangeParams { + page: { + index: number; + size: number; + }; + sort: { + field: string; + direction: 'asc' | 'desc'; + }; +} + +/** Invoked by the EUI table implementation when the user interacts with the table */ +export type OnTableChange = (tableChange: OnTableChangeParams) => void; + +export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable'; + +export interface OpenTimelineProps { + /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ + deleteTimelines?: DeleteTimelines; + /** The default requested size of each page of search results */ + defaultPageSize: number; + /** Displays an indicator that data is loading when true */ + isLoading: boolean; + /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ + itemIdToExpandedNotesRowMap: Record<string, JSX.Element>; + /** Display import timelines modal*/ + importDataModalToggle?: boolean; + /** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */ + onAddTimelinesToFavorites?: OnAddTimelinesToFavorites; + /** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */ + onDeleteSelected?: OnDeleteSelected; + /** Only show favorite timelines when true */ + onlyFavorites: boolean; + /** Invoked when the user presses enter after typing in the search bar */ + onQueryChange: OnQueryChange; + /** Invoked when the user selects (or de-selects) timelines in the table */ + onSelectionChange: OnSelectionChange; + /** Invoked when the user clicks on the name of a timeline to open it */ + onOpenTimeline: OnOpenTimeline; + /** Invoked by the EUI table implementation when the user interacts with the table */ + onTableChange: OnTableChange; + /** Invoked when the user toggles the option to only show favorite timelines */ + onToggleOnlyFavorites: OnToggleOnlyFavorites; + /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ + onToggleShowNotes: OnToggleShowNotes; + /** the requested page of results */ + pageIndex: number; + /** the requested size of each page of search results */ + pageSize: number; + /** The currently applied search criteria */ + query: string; + /** Refetch table */ + refetch?: Refetch; + /** The results of executing a search */ + searchResults: OpenTimelineResult[]; + /** the currently-selected timelines in the table */ + selectedItems: OpenTimelineResult[]; + /** Toggle export timelines modal*/ + setImportDataModalToggle?: React.Dispatch<React.SetStateAction<boolean>>; + /** the requested sort direction of the query results */ + sortDirection: 'asc' | 'desc'; + /** the requested field to sort on */ + sortField: string; + /** The title of the Open Timeline component */ + title: string; + /** The total (server-side) count of the search results */ + totalSearchResultsCount: number; + /** Hide action on timeline if needed it */ + hideActions?: ActionTimelineToShow[]; +} + +export interface UpdateTimeline { + duplicate: boolean; + id: string; + from: number; + notes: NoteResult[] | null | undefined; + timeline: TimelineModel; + to: number; + ruleNote?: string; +} + +export type DispatchUpdateTimeline = ({ + duplicate, + id, + from, + notes, + timeline, + to, + ruleNote, +}: UpdateTimeline) => () => void; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts new file mode 100644 index 0000000000000..6573b6371e9a4 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter } from '../../../../../../../src/plugins/data/public'; + +export const createFilter = ( + key: string, + value: string[] | string | null | undefined, + negate: boolean = false +): Filter => { + const queryValue = value != null ? (Array.isArray(value) ? value[0] : value) : null; + return queryValue != null + ? { + meta: { + alias: null, + negate, + disabled: false, + type: 'phrase', + key, + value: queryValue, + params: { + query: queryValue, + }, + }, + query: { + match: { + [key]: { + query: queryValue, + type: 'phrase', + }, + }, + }, + } + : ({ + exists: { + field: key, + }, + meta: { + alias: null, + disabled: false, + key, + negate: value === undefined, + type: 'exists', + value: 'exists', + }, + } as Filter); +}; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx new file mode 100644 index 0000000000000..7aed36422bd2f --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { WithHoverActions } from '../../with_hover_actions'; +import { useKibana } from '../../../lib/kibana'; + +import * as i18n from './translations'; + +export * from './helpers'; + +interface OwnProps { + children: JSX.Element; + filter: Filter; + onFilterAdded?: () => void; +} + +export const AddFilterToGlobalSearchBar = React.memo<OwnProps>( + ({ children, filter, onFilterAdded }) => { + const { filterManager } = useKibana().services.data.query; + + const filterForValue = useCallback(() => { + filterManager.addFilters(filter); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); + + const filterOutValue = useCallback(() => { + filterManager.addFilters({ + ...filter, + meta: { + ...filter.meta, + negate: true, + }, + }); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); + + return ( + <WithHoverActions + hoverContent={ + <div data-test-subj="hover-actions-container"> + <EuiToolTip content={i18n.FILTER_FOR_VALUE}> + <EuiButtonIcon + aria-label={i18n.FILTER_FOR_VALUE} + color="text" + data-test-subj="add-to-filter" + iconType="magnifyWithPlus" + onClick={filterForValue} + /> + </EuiToolTip> + + <EuiToolTip content={i18n.FILTER_OUT_VALUE}> + <EuiButtonIcon + aria-label={i18n.FILTER_OUT_VALUE} + color="text" + data-test-subj="filter-out-value" + iconType="magnifyWithMinus" + onClick={filterOutValue} + /> + </EuiToolTip> + </div> + } + render={() => children} + /> + ); + } +); + +AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts rename to x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/authentications_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx new file mode 100644 index 0000000000000..4d0e6a737d303 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx @@ -0,0 +1,191 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { getOr } from 'lodash/fp'; +import React from 'react'; + +import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; +import { DescriptionList } from '../../../../../common/utility_types'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { getEmptyTagValue } from '../../../empty_value'; +import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/field_renderers'; +import { InspectButton, InspectButtonContainer } from '../../../inspect'; +import { HostItem } from '../../../../graphql/types'; +import { Loader } from '../../../loader'; +import { IPDetailsLink } from '../../../links'; +import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; +import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; +import { AnomalyScores } from '../../../ml/score/anomaly_scores'; +import { Anomalies, NarrowDateRange } from '../../../ml/types'; +import { DescriptionListStyled, OverviewWrapper } from '../../index'; +import { FirstLastSeenHost, FirstLastSeenHostType } from '../first_last_seen_host'; + +import * as i18n from './translations'; + +interface HostSummaryProps { + data: HostItem; + id: string; + loading: boolean; + isLoadingAnomaliesData: boolean; + anomaliesData: Anomalies | null; + startDate: number; + endDate: number; + narrowDateRange: NarrowDateRange; +} + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( + <EuiFlexItem key={key}> + <DescriptionListStyled listItems={descriptionList} /> + </EuiFlexItem> +); + +export const HostOverview = React.memo<HostSummaryProps>( + ({ + data, + loading, + id, + startDate, + endDate, + isLoadingAnomaliesData, + anomaliesData, + narrowDateRange, + }) => { + const capabilities = useMlCapabilities(); + const userPermissions = hasMlUserPermissions(capabilities); + const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); + + const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( + <DefaultFieldRenderer + rowItems={getOr([], fieldName, fieldData)} + attrName={fieldName} + idPrefix="host-overview" + /> + ); + + const column: DescriptionList[] = [ + { + title: i18n.HOST_ID, + description: data.host + ? hostIdRenderer({ host: data.host, noLink: true }) + : getEmptyTagValue(), + }, + { + title: i18n.FIRST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + <FirstLastSeenHost + hostname={data.host.name[0]} + type={FirstLastSeenHostType.FIRST_SEEN} + /> + ) : ( + getEmptyTagValue() + ), + }, + { + title: i18n.LAST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + <FirstLastSeenHost + hostname={data.host.name[0]} + type={FirstLastSeenHostType.LAST_SEEN} + /> + ) : ( + getEmptyTagValue() + ), + }, + ]; + const firstColumn = userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + <AnomalyScores + anomalies={anomaliesData} + startDate={startDate} + endDate={endDate} + isLoading={isLoadingAnomaliesData} + narrowDateRange={narrowDateRange} + /> + ), + }, + ] + : column; + + const descriptionLists: Readonly<DescriptionList[][]> = [ + firstColumn, + [ + { + title: i18n.IP_ADDRESSES, + description: ( + <DefaultFieldRenderer + rowItems={getOr([], 'host.ip', data)} + attrName={'host.ip'} + idPrefix="host-overview" + render={ip => (ip != null ? <IPDetailsLink ip={ip} /> : getEmptyTagValue())} + /> + ), + }, + { + title: i18n.MAC_ADDRESSES, + description: getDefaultRenderer('host.mac', data), + }, + { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, + ], + [ + { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, + { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, + { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, + { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, + ], + [ + { + title: i18n.CLOUD_PROVIDER, + description: getDefaultRenderer('cloud.provider', data), + }, + { + title: i18n.REGION, + description: getDefaultRenderer('cloud.region', data), + }, + { + title: i18n.INSTANCE_ID, + description: getDefaultRenderer('cloud.instance.id', data), + }, + { + title: i18n.MACHINE_TYPE, + description: getDefaultRenderer('cloud.machine.type', data), + }, + ], + ]; + + return ( + <InspectButtonContainer> + <OverviewWrapper> + <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} + + {loading && ( + <Loader + overlay + overlayBackground={ + darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor + } + size="xl" + /> + )} + </OverviewWrapper> + </InspectButtonContainer> + ); + } +); + +HostOverview.displayName = 'HostOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/host_overview/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/host_overview/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/host_overview/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/host_overview/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/hosts_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts rename to x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts rename to x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/index.tsx b/x-pack/plugins/siem/public/components/page/index.tsx new file mode 100644 index 0000000000000..5feb2ef73c57f --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/index.tsx @@ -0,0 +1,203 @@ +/* + * 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 { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; +import styled, { createGlobalStyle } from 'styled-components'; + +/* + SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly + and `EuiPopover`, `EuiToolTip` global styles +*/ +export const AppGlobalStyle = createGlobalStyle` + /* dirty hack to fix draggables with tooltip on FF */ + body#siem-app { + position: static; + } + /* end of dirty hack to fix draggables with tooltip on FF */ + + div.app-wrapper { + background-color: rgba(0,0,0,0); + } + + div.application { + background-color: rgba(0,0,0,0); + } + + .euiPopover__panel.euiPopover__panel-isOpen { + z-index: 9900 !important; + min-width: 24px; + } + .euiToolTip { + z-index: 9950 !important; + } + + /* + overrides the default styling of euiComboBoxOptionsList because it's implemented + as a popover, so it's not selectable as a child of the styled component + */ + .euiComboBoxOptionsList { + z-index: 9999; + } + + /* overrides default styling in angular code that was not theme-friendly */ + .euiPanel-loading-hide-border { + border: none; + } + + /* hide open popovers when a modal is being displayed to prevent them from covering the modal */ + body.euiBody-hasOverlayMask .euiPopover__panel-isOpen { + visibility: hidden !important; + } + + /* ensure elastic charts tooltips appear above open euiPopovers */ + .echTooltip { + z-index: 9950; + } + +`; + +export const DescriptionListStyled = styled(EuiDescriptionList)` + ${({ theme }) => ` + dt { + font-size: ${theme.eui.euiFontSizeXS} !important; + } + dd { + width: fit-content; + } + dd > div { + width: fit-content; + } + `} +`; + +DescriptionListStyled.displayName = 'DescriptionListStyled'; + +export const PageContainer = styled.div` + display: flex; + flex-direction: column; + align-items: stretch; + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + height: 100%; + padding: 1rem; + overflow: hidden; + margin: 0px; +`; + +PageContainer.displayName = 'PageContainer'; + +export const PageContent = styled.div` + flex: 1 1 auto; + height: 100%; + position: relative; + overflow-y: hidden; + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + margin-top: 62px; +`; + +PageContent.displayName = 'PageContent'; + +export const FlexPage = styled(EuiPage)` + flex: 1 0 0; +`; + +FlexPage.displayName = 'FlexPage'; + +export const PageHeader = styled.div` + background-color: ${props => props.theme.eui.euiColorEmptyShade}; + display: flex; + user-select: none; + padding: 1rem 1rem 0rem 1rem; + width: 100vw; + position: fixed; +`; + +PageHeader.displayName = 'PageHeader'; + +export const FooterContainer = styled.div` + flex: 0; + bottom: 0; + color: #666; + left: 0; + position: fixed; + text-align: left; + user-select: none; + width: 100%; + background-color: #f5f7fa; + padding: 16px; + border-top: 1px solid #d3dae6; +`; + +FooterContainer.displayName = 'FooterContainer'; + +export const PaneScrollContainer = styled.div` + height: 100%; + overflow-y: scroll; + > div:last-child { + margin-bottom: 3rem; + } +`; + +PaneScrollContainer.displayName = 'PaneScrollContainer'; + +export const Pane = styled.div` + height: 100%; + overflow: hidden; + user-select: none; +`; + +Pane.displayName = 'Pane'; + +export const PaneHeader = styled.div` + display: flex; +`; + +PaneHeader.displayName = 'PaneHeader'; + +export const Pane1FlexContent = styled.div` + display: flex; + flex-direction: row; + flex-wrap: wrap; + height: 100%; +`; + +Pane1FlexContent.displayName = 'Pane1FlexContent'; + +export const CountBadge = (styled(EuiBadge)` + margin-left: 5px; +` as unknown) as typeof EuiBadge; + +CountBadge.displayName = 'CountBadge'; + +export const Spacer = styled.span` + margin-left: 5px; +`; + +Spacer.displayName = 'Spacer'; + +export const Badge = (styled(EuiBadge)` + vertical-align: top; +` as unknown) as typeof EuiBadge; + +Badge.displayName = 'Badge'; + +export const MoreRowItems = styled(EuiIcon)` + margin-left: 5px; +`; + +MoreRowItems.displayName = 'MoreRowItems'; + +export const OverviewWrapper = styled(EuiFlexGroup)` + position: relative; + + .euiButtonIcon { + position: absolute; + right: ${props => props.theme.eui.euiSizeM}; + top: 6px; + z-index: 2; + } +`; + +OverviewWrapper.displayName = 'OverviewWrapper'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx b/x-pack/plugins/siem/public/components/page/manage_query.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/manage_query.tsx rename to x-pack/plugins/siem/public/components/page/manage_query.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx b/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx rename to x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/index.tsx b/x-pack/plugins/siem/public/components/page/network/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/index.tsx rename to x-pack/plugins/siem/public/components/page/network/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/ip_overview/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx new file mode 100644 index 0000000000000..56b59ca97156f --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx @@ -0,0 +1,166 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; + +import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; +import { DescriptionList } from '../../../../../common/utility_types'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { FlowTarget, IpOverviewData, Overview } from '../../../../graphql/types'; +import { networkModel } from '../../../../store'; +import { getEmptyTagValue } from '../../../empty_value'; + +import { + autonomousSystemRenderer, + dateRenderer, + hostIdRenderer, + hostNameRenderer, + locationRenderer, + reputationRenderer, + whoisRenderer, +} from '../../../field_renderers/field_renderers'; +import * as i18n from './translations'; +import { DescriptionListStyled, OverviewWrapper } from '../../index'; +import { Loader } from '../../../loader'; +import { Anomalies, NarrowDateRange } from '../../../ml/types'; +import { AnomalyScores } from '../../../ml/score/anomaly_scores'; +import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; +import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; +import { InspectButton, InspectButtonContainer } from '../../../inspect'; + +interface OwnProps { + data: IpOverviewData; + flowTarget: FlowTarget; + id: string; + ip: string; + loading: boolean; + isLoadingAnomaliesData: boolean; + anomaliesData: Anomalies | null; + startDate: number; + endDate: number; + type: networkModel.NetworkType; + narrowDateRange: NarrowDateRange; +} + +export type IpOverviewProps = OwnProps; + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => { + return ( + <EuiFlexItem key={key}> + <DescriptionListStyled listItems={descriptionList} /> + </EuiFlexItem> + ); +}; + +export const IpOverview = React.memo<IpOverviewProps>( + ({ + id, + ip, + data, + loading, + flowTarget, + startDate, + endDate, + isLoadingAnomaliesData, + anomaliesData, + narrowDateRange, + }) => { + const capabilities = useMlCapabilities(); + const userPermissions = hasMlUserPermissions(capabilities); + const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE); + const typeData: Overview = data[flowTarget]!; + const column: DescriptionList[] = [ + { + title: i18n.LOCATION, + description: locationRenderer( + [`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`], + data + ), + }, + { + title: i18n.AUTONOMOUS_SYSTEM, + description: typeData + ? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget) + : getEmptyTagValue(), + }, + ]; + + const firstColumn: DescriptionList[] = userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + <AnomalyScores + anomalies={anomaliesData} + startDate={startDate} + endDate={endDate} + isLoading={isLoadingAnomaliesData} + narrowDateRange={narrowDateRange} + /> + ), + }, + ] + : column; + + const descriptionLists: Readonly<DescriptionList[][]> = [ + firstColumn, + [ + { + title: i18n.FIRST_SEEN, + description: typeData ? dateRenderer(typeData.firstSeen) : getEmptyTagValue(), + }, + { + title: i18n.LAST_SEEN, + description: typeData ? dateRenderer(typeData.lastSeen) : getEmptyTagValue(), + }, + ], + [ + { + title: i18n.HOST_ID, + description: typeData + ? hostIdRenderer({ host: data.host, ipFilter: ip }) + : getEmptyTagValue(), + }, + { + title: i18n.HOST_NAME, + description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), + }, + ], + [ + { title: i18n.WHOIS, description: whoisRenderer(ip) }, + { title: i18n.REPUTATION, description: reputationRenderer(ip) }, + ], + ]; + + return ( + <InspectButtonContainer> + <OverviewWrapper> + <InspectButton queryId={id} title={i18n.INSPECT_TITLE} inspectIndex={0} /> + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} + + {loading && ( + <Loader + overlay + overlayBackground={ + darkMode ? darkTheme.euiPageBackgroundColor : lightTheme.euiPageBackgroundColor + } + size="xl" + /> + )} + </OverviewWrapper> + </InspectButtonContainer> + ); + } +); + +IpOverview.displayName = 'IpOverview'; diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/mock.ts b/x-pack/plugins/siem/public/components/page/network/ip_overview/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/mock.ts rename to x-pack/plugins/siem/public/components/page/network/ip_overview/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/translations.ts b/x-pack/plugins/siem/public/components/page/network/ip_overview/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/ip_overview/translations.ts rename to x-pack/plugins/siem/public/components/page/network/ip_overview/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/kpi_network/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/index.tsx rename to x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts b/x-pack/plugins/siem/public/components/page/network/kpi_network/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/mock.ts rename to x-pack/plugins/siem/public/components/page/network/kpi_network/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/translations.ts b/x-pack/plugins/siem/public/components/page/network/kpi_network/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/kpi_network/translations.ts rename to x-pack/plugins/siem/public/components/page/network/kpi_network/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_dns_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_dns_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_dns_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_http_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_http_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_http_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_http_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_http_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_http_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_http_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/tls_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/tls_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/tls_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/tls_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/tls_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/tls_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/tls_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/tls_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/columns.tsx rename to x-pack/plugins/siem/public/components/page/network/users_table/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx rename to x-pack/plugins/siem/public/components/page/network/users_table/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.tsx rename to x-pack/plugins/siem/public/components/page/network/users_table/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/users_table/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/mock.ts rename to x-pack/plugins/siem/public/components/page/network/users_table/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/translations.ts b/x-pack/plugins/siem/public/components/page/network/users_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/network/users_table/translations.ts rename to x-pack/plugins/siem/public/components/page/network/users_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx b/x-pack/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx rename to x-pack/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_host/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx new file mode 100644 index 0000000000000..52c142ceff480 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx @@ -0,0 +1,129 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useMemo } from 'react'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { ESQuery } from '../../../../../common/typed_json'; +import { + ID as OverviewHostQueryId, + OverviewHostQuery, +} from '../../../../containers/overview/overview_host'; +import { HeaderSection } from '../../../header_section'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { getHostsUrl } from '../../../link_to'; +import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; +import { manageQuery } from '../../../page/manage_query'; +import { inputsModel } from '../../../../store/inputs'; +import { InspectButtonContainer } from '../../../inspect'; +import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; +import { navTabs } from '../../../../pages/home/home_navigations'; + +export interface OwnProps { + startDate: number; + endDate: number; + filterQuery?: ESQuery | string; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; +} + +const OverviewHostStatsManage = manageQuery(OverviewHostStats); +export type OverviewHostProps = OwnProps; + +const OverviewHostComponent: React.FC<OverviewHostProps> = ({ + endDate, + filterQuery, + startDate, + setQuery, +}) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.hosts); + const hostPageButton = useMemo( + () => ( + <EuiButton href={getHostsUrl(urlSearch)}> + <FormattedMessage id="xpack.siem.overview.hostsAction" defaultMessage="View hosts" /> + </EuiButton> + ), + [urlSearch] + ); + return ( + <EuiFlexItem> + <InspectButtonContainer> + <EuiPanel> + <OverviewHostQuery + data-test-subj="overview-host-query" + endDate={endDate} + filterQuery={filterQuery} + sourceId="default" + startDate={startDate} + > + {({ overviewHost, loading, id, inspect, refetch }) => { + const hostEventsCount = getOverviewHostStats(overviewHost).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedHostEventsCount = numeral(hostEventsCount).format(defaultNumberFormat); + + return ( + <> + <HeaderSection + id={OverviewHostQueryId} + subtitle={ + !isEmpty(overviewHost) ? ( + <FormattedMessage + defaultMessage="Showing: {formattedHostEventsCount} {hostEventsCount, plural, one {event} other {events}}" + id="xpack.siem.overview.overviewHost.hostsSubtitle" + values={{ + formattedHostEventsCount, + hostEventsCount, + }} + /> + ) : ( + <>{''}</> + ) + } + title={ + <FormattedMessage + id="xpack.siem.overview.hostsTitle" + defaultMessage="Host events" + /> + } + > + {hostPageButton} + </HeaderSection> + + <OverviewHostStatsManage + loading={loading} + data={overviewHost} + setQuery={setQuery} + id={id} + inspect={inspect} + refetch={refetch} + /> + </> + ); + }} + </OverviewHostQuery> + </EuiPanel> + </InspectButtonContainer> + </EuiFlexItem> + ); +}; + +export const OverviewHost = React.memo(OverviewHostComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts rename to x-pack/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_network/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx new file mode 100644 index 0000000000000..d649a0dd9e923 --- /dev/null +++ b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx @@ -0,0 +1,132 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useMemo } from 'react'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { ESQuery } from '../../../../../common/typed_json'; +import { HeaderSection } from '../../../header_section'; +import { useUiSetting$ } from '../../../../lib/kibana'; +import { manageQuery } from '../../../page/manage_query'; +import { + ID as OverviewNetworkQueryId, + OverviewNetworkQuery, +} from '../../../../containers/overview/overview_network'; +import { inputsModel } from '../../../../store/inputs'; +import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; +import { getNetworkUrl } from '../../../link_to'; +import { InspectButtonContainer } from '../../../inspect'; +import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; +import { navTabs } from '../../../../pages/home/home_navigations'; + +export interface OverviewNetworkProps { + startDate: number; + endDate: number; + filterQuery?: ESQuery | string; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; +} + +const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); + +const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({ + endDate, + filterQuery, + startDate, + setQuery, +}) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.network); + const networkPageButton = useMemo( + () => ( + <EuiButton href={getNetworkUrl(urlSearch)}> + <FormattedMessage id="xpack.siem.overview.networkAction" defaultMessage="View network" /> + </EuiButton> + ), + [urlSearch] + ); + return ( + <EuiFlexItem> + <InspectButtonContainer> + <EuiPanel> + <OverviewNetworkQuery + data-test-subj="overview-network-query" + endDate={endDate} + filterQuery={filterQuery} + sourceId="default" + startDate={startDate} + > + {({ overviewNetwork, loading, id, inspect, refetch }) => { + const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedNetworkEventsCount = numeral(networkEventsCount).format( + defaultNumberFormat + ); + + return ( + <> + <HeaderSection + id={OverviewNetworkQueryId} + subtitle={ + !isEmpty(overviewNetwork) ? ( + <FormattedMessage + defaultMessage="Showing: {formattedNetworkEventsCount} {networkEventsCount, plural, one {event} other {events}}" + id="xpack.siem.overview.overviewNetwork.networkSubtitle" + values={{ + formattedNetworkEventsCount, + networkEventsCount, + }} + /> + ) : ( + <>{''}</> + ) + } + title={ + <FormattedMessage + id="xpack.siem.overview.networkTitle" + defaultMessage="Network events" + /> + } + > + {networkPageButton} + </HeaderSection> + + <OverviewNetworkStatsManage + loading={loading} + data={overviewNetwork} + id={id} + inspect={inspect} + setQuery={setQuery} + refetch={refetch} + /> + </> + ); + }} + </OverviewNetworkQuery> + </EuiPanel> + </InspectButtonContainer> + </EuiFlexItem> + ); +}; + +OverviewNetworkComponent.displayName = 'OverviewNetworkComponent'; + +export const OverviewNetwork = React.memo(OverviewNetworkComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts rename to x-pack/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx b/x-pack/plugins/siem/public/components/page/overview/stat_value.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx rename to x-pack/plugins/siem/public/components/page/overview/stat_value.tsx index cada0a9aff939..7615001eec9da 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/overview/stat_value.tsx +++ b/x-pack/plugins/siem/public/components/page/overview/stat_value.tsx @@ -9,7 +9,7 @@ import numeral from '@elastic/numeral'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { useUiSetting$ } from '../../../lib/kibana'; const ProgressContainer = styled.div` diff --git a/x-pack/legacy/plugins/siem/public/components/page/overview/types.ts b/x-pack/plugins/siem/public/components/page/overview/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/overview/types.ts rename to x-pack/plugins/siem/public/components/page/overview/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page/translations.ts b/x-pack/plugins/siem/public/components/page/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page/translations.ts rename to x-pack/plugins/siem/public/components/page/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/page_route/index.tsx b/x-pack/plugins/siem/public/components/page_route/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page_route/index.tsx rename to x-pack/plugins/siem/public/components/page_route/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page_route/pageroute.test.tsx b/x-pack/plugins/siem/public/components/page_route/pageroute.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page_route/pageroute.test.tsx rename to x-pack/plugins/siem/public/components/page_route/pageroute.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/page_route/pageroute.tsx b/x-pack/plugins/siem/public/components/page_route/pageroute.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/page_route/pageroute.tsx rename to x-pack/plugins/siem/public/components/page_route/pageroute.tsx diff --git a/x-pack/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..f42cd1876f2a2 --- /dev/null +++ b/x-pack/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -0,0 +1,793 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Paginated Table Component rendering it renders the default load more table 1`] = ` +<ContextProvider + value={ + Object { + "darkMode": true, + "eui": Object { + "avatarSizing": Object { + "l": Object { + "font-size": "19.200000000000003px", + "size": "40px", + }, + "m": Object { + "font-size": "14.4px", + "size": "32px", + }, + "s": Object { + "font-size": "12px", + "size": "24px", + }, + "xl": Object { + "font-size": "25.6px", + "size": "64px", + }, + }, + "euiAnimSlightBounce": "cubic-bezier(0.34, 1.61, 0.7, 1)", + "euiAnimSlightResistance": "cubic-bezier(0.694, 0.0482, 0.335, 1)", + "euiAnimSpeedExtraFast": "90ms", + "euiAnimSpeedExtraSlow": "500ms", + "euiAnimSpeedFast": "150ms", + "euiAnimSpeedNormal": "250ms", + "euiAnimSpeedSlow": "350ms", + "euiBadgeGroupGutterTypes": Object { + "gutterExtraSmall": "4px", + "gutterSmall": "8px", + }, + "euiBorderColor": "#343741", + "euiBorderEditable": "2px dotted #343741", + "euiBorderRadius": "4px", + "euiBorderThick": "2px solid #343741", + "euiBorderThin": "1px solid #343741", + "euiBorderWidthThick": "2px", + "euiBorderWidthThin": "1px", + "euiBreadcrumbSpacing": "8px", + "euiBreadcrumbTruncateWidth": "160px", + "euiBreakpointKeys": "'xs', 's', 'm', 'l', 'xl'", + "euiBreakpoints": Object { + "l": "992px", + "m": "768px", + "s": "575px", + "xl": "1200px", + "xs": 0, + }, + "euiButtonColorDisabled": "#434548", + "euiButtonColorDisabledText": "#4c4e51", + "euiButtonColorGhostDisabled": "#343741", + "euiButtonEmptyTypes": Object { + "danger": "#ff6666", + "disabled": "#4c4e51", + "ghost": "#ffffff", + "primary": "#1ba9f5", + "text": "#dfe5ef", + }, + "euiButtonHeight": "40px", + "euiButtonHeightSmall": "32px", + "euiButtonIconTypes": Object { + "danger": "#ff6666", + "disabled": "#4c4e51", + "ghost": "#ffffff", + "primary": "#1ba9f5", + "subdued": "#81858f", + "success": "#7de2d1", + "text": "#dfe5ef", + "warning": "#ffce7a", + }, + "euiButtonMinWidth": "112px", + "euiButtonToggleBorderColor": "#343741", + "euiButtonToggleTypes": Object { + "danger": "#ff6666", + "ghost": "#ffffff", + "primary": "#1ba9f5", + "secondary": "#7de2d1", + "text": "#98a2b3", + "warning": "#ffce7a", + }, + "euiButtonTypes": Object { + "danger": "#ff6666", + "ghost": "#ffffff", + "primary": "#1ba9f5", + "secondary": "#7de2d1", + "text": "#98a2b3", + "warning": "#ffce7a", + }, + "euiCallOutTypes": Object { + "danger": "#ff6666", + "primary": "#1ba9f5", + "success": "#7de2d1", + "warning": "#ffce7a", + }, + "euiCardBottomNodeHeight": "40px", + "euiCardSelectButtonBackgrounds": Object { + "danger": "#4d1f1f", + "ghost": "#98a2b3", + "primary": "#08334a", + "success": "#26443f", + "text": "#25262e", + }, + "euiCardSelectButtonBorders": Object { + "danger": "#ff6666", + "ghost": "#98a2b3", + "primary": "#1ba9f5", + "success": "#7de2d1", + "text": "#7de2d1", + }, + "euiCardSpacing": "16px", + "euiCheckBoxSize": "16px", + "euiCodeBlockAdditionBackgroundColor": "#144212", + "euiCodeBlockAdditionColor": "#e6e1dc", + "euiCodeBlockAttributeColor": "#80cbbf", + "euiCodeBlockBackgroundColor": "#25262e", + "euiCodeBlockBuiltInColor": "#0086b3", + "euiCodeBlockColor": "#dfe5ef", + "euiCodeBlockCommentColor": "#656565", + "euiCodeBlockDeletionBackgroundColor": "#660000", + "euiCodeBlockDeletionColor": "#e6e1dc", + "euiCodeBlockFunctionTitleColor": "#75a5ff", + "euiCodeBlockKeywordColor": "#c792ea", + "euiCodeBlockMetaColor": "#75a5ff", + "euiCodeBlockNameColor": "#e06c75", + "euiCodeBlockNumberColor": "#f77669", + "euiCodeBlockParamsColor": "#eefff7", + "euiCodeBlockRegexpColor": "#009926", + "euiCodeBlockSectionColor": "#ffc66d", + "euiCodeBlockSelectedBackgroundColor": "inherit", + "euiCodeBlockSelectorClassColor": "#ffcb68", + "euiCodeBlockSelectorIdColor": "#f77669", + "euiCodeBlockSelectorTagColor": "#c792ea", + "euiCodeBlockStringColor": "#c3e88d", + "euiCodeBlockSymbolColor": "#c792ea", + "euiCodeBlockTagColor": "#abb2bf", + "euiCodeBlockTitleColor": "#75a5ff", + "euiCodeBlockTypeColor": "#da4939", + "euiCodeFontFamily": "'Roboto Mono', 'Consolas', 'Menlo', 'Courier', monospace", + "euiCollapsibleNavGroupDarkBackgroundColor": "#131317", + "euiCollapsibleNavGroupDarkHighContrastColor": "#1ba9f5", + "euiCollapsibleNavGroupLightBackgroundColor": "#1a1b20", + "euiCollapsibleNavWidth": "320px", + "euiColorAccent": "#f990c0", + "euiColorAccentText": "#f990c0", + "euiColorChartBand": "#2a2b33", + "euiColorChartLines": "#343741", + "euiColorDanger": "#ff6666", + "euiColorDangerText": "#ff7575", + "euiColorDarkShade": "#98a2b3", + "euiColorDarkestShade": "#d4dae5", + "euiColorEmptyShade": "#1d1e24", + "euiColorFullShade": "#ffffff", + "euiColorGhost": "#ffffff", + "euiColorHighlight": "#2e2d25", + "euiColorInk": "#000000", + "euiColorLightShade": "#343741", + "euiColorLightestShade": "#25262e", + "euiColorMediumShade": "#535966", + "euiColorPickerIndicatorSize": "12px", + "euiColorPickerSaturationRange0": "#000000", + "euiColorPickerSaturationRange1": "rgba(0, 0, 0, 0)", + "euiColorPickerValueRange0": "#ffffff", + "euiColorPickerValueRange1": "rgba(255, 255, 255, 0)", + "euiColorPickerWidth": "152px", + "euiColorPrimary": "#1ba9f5", + "euiColorPrimaryText": "#1ba9f5", + "euiColorSecondary": "#7de2d1", + "euiColorSecondaryText": "#7de2d1", + "euiColorSuccess": "#7de2d1", + "euiColorSuccessText": "#7de2d1", + "euiColorVis0": "#54b399", + "euiColorVis0_behindText": "#6dccb1", + "euiColorVis1": "#6092c0", + "euiColorVis1_behindText": "#79aad9", + "euiColorVis2": "#d36086", + "euiColorVis2_behindText": "#ee789d", + "euiColorVis3": "#9170b8", + "euiColorVis3_behindText": "#a987d1", + "euiColorVis4": "#ca8eae", + "euiColorVis4_behindText": "#e4a6c7", + "euiColorVis5": "#d6bf57", + "euiColorVis5_behindText": "#f1d86f", + "euiColorVis6": "#b9a888", + "euiColorVis6_behindText": "#d2c0a0", + "euiColorVis7": "#da8b45", + "euiColorVis7_behindText": "#f5a35c", + "euiColorVis8": "#aa6556", + "euiColorVis8_behindText": "#c47c6c", + "euiColorVis9": "#e7664c", + "euiColorVis9_behindText": "#ff7e62", + "euiColorWarning": "#ffce7a", + "euiColorWarningText": "#ffce7a", + "euiContextMenuWidth": "256px", + "euiControlBarBackground": "#000000", + "euiControlBarBorderColor": "rgba(255, 255, 255, 0.19999999999999996)", + "euiControlBarHeights": Object { + "l": "100vh", + "m": "480px", + "s": "240px", + }, + "euiControlBarInitialHeight": "40px", + "euiControlBarMaxHeight": "calc(100vh - 80px)", + "euiControlBarText": "#a9aaad", + "euiDataGridCellPaddingL": "8px", + "euiDataGridCellPaddingM": "6px", + "euiDataGridCellPaddingS": "4px", + "euiDataGridColumnResizerWidth": "3px", + "euiDataGridPrefix": ".euiDataGrid--", + "euiDataGridStyles": "'bordersAll', 'bordersNone', 'bordersHorizontal', 'paddingSmall', 'paddingMedium', 'paddingLarge', 'stripes', 'rowHoverNone', 'rowHoverHighlight', 'headerShade', 'headerUnderline', 'fontSizeSmall', 'fontSizeLarge', 'noControls'", + "euiDataGridVerticalBorder": "solid 1px #24272e", + "euiDatePickerCalendarWidth": "284px", + "euiDragAndDropSpacing": Object { + "l": "8px", + "m": "4px", + "s": "2px", + }, + "euiExpressionColors": Object { + "accent": "#f990c0", + "danger": "#ff7575", + "primary": "#1ba9f5", + "secondary": "#7de2d1", + "subdued": "#81858f", + "warning": "#ffce7a", + }, + "euiFilePickerTallHeight": "128px", + "euiFocusBackgroundColor": "#232635", + "euiFocusRingAnimStartColor": "rgba(27, 169, 245, 0)", + "euiFocusRingAnimStartSize": "6px", + "euiFocusRingAnimStartSizeLarge": "10px", + "euiFocusRingColor": "rgba(27, 169, 245, 0.3)", + "euiFocusRingSize": "3px", + "euiFocusRingSizeLarge": "4px", + "euiFontFamily": "'Inter UI', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica', 'Arial', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + "euiFontFeatureSettings": "calt 1 kern 1 liga 1", + "euiFontSize": "16px", + "euiFontSizeL": "20px", + "euiFontSizeM": "18px", + "euiFontSizeS": "14px", + "euiFontSizeXL": "28px", + "euiFontSizeXS": "12px", + "euiFontSizeXXL": "36px", + "euiFontWeightBold": 700, + "euiFontWeightLight": 300, + "euiFontWeightMedium": 500, + "euiFontWeightRegular": 400, + "euiFontWeightSemiBold": 600, + "euiFormBackgroundColor": "#16171c", + "euiFormBackgroundDisabledColor": "#202128", + "euiFormBackgroundReadOnlyColor": "rgba(0, 0, 0, 0.050000000000000044)", + "euiFormBorderColor": "rgba(255, 255, 255, 0.09999999999999998)", + "euiFormBorderDisabledColor": "rgba(255, 255, 255, 0.09999999999999998)", + "euiFormBorderOpaqueColor": "#ffffff", + "euiFormControlBoxShadow": "0 1px 1px -1px rgba(0, 0, 0, 0.19999999999999996) 0 3px 2px -2px rgba(0, 0, 0, 0.19999999999999996)", + "euiFormControlCompressedBorderRadius": "2px", + "euiFormControlCompressedHeight": "32px", + "euiFormControlCompressedPadding": "8px", + "euiFormControlDisabledColor": "#535966", + "euiFormControlHeight": "40px", + "euiFormControlLayoutGroupInputCompressedBorderRadius": "1px", + "euiFormControlLayoutGroupInputCompressedHeight": "30px", + "euiFormControlLayoutGroupInputHeight": "38px", + "euiFormControlPadding": "12px", + "euiFormCustomControlBorderColor": "#66676d", + "euiFormCustomControlDisabledIconColor": "#a6aab0", + "euiFormInputGroupBorder": "1px solid #282a30", + "euiFormInputGroupLabelBackground": "#1f2127", + "euiFormMaxWidth": "400px", + "euiGradientMiddle": "#282a31", + "euiGradientStartStop": "#2e3039", + "euiHeaderBackgroundColor": "#1d1e24", + "euiHeaderBreadcrumbColor": "#d4dae5", + "euiHeaderChildSize": "48px", + "euiHeaderHeight": "48px", + "euiHeaderHeightCompensation": "49px", + "euiIconColors": Object { + "accent": "#f990c0", + "danger": "#ff7575", + "ghost": "#ffffff", + "primary": "#1ba9f5", + "secondary": "#7de2d1", + "subdued": "#81858f", + "success": "#7de2d1", + "text": "#dfe5ef", + "warning": "#ffce7a", + }, + "euiIconLoadingOpacity": 0.05, + "euiIconSizes": Object { + "large": "24px", + "medium": "16px", + "small": "12px", + "xLarge": "32px", + "xxLarge": "40px", + }, + "euiKeyPadMenuItemBetaBadgeSize": "20px", + "euiKeyPadMenuSize": "96px", + "euiLineHeight": 1.5, + "euiLinkColor": "#1ba9f5", + "euiListGroupGutterTypes": Object { + "gutterMedium": "16px", + "gutterSmall": "8px", + }, + "euiListGroupItemColorTypes": Object { + "ghost": "#ffffff", + "primary": "#1ba9f5", + "subdued": "#81858f", + "text": "#dfe5ef", + }, + "euiListGroupItemHoverBackground": "rgba(52, 55, 65, 0.25)", + "euiListGroupItemHoverBackgroundGhost": "rgba(255, 255, 255, 0.09999999999999998)", + "euiListGroupItemSizeTypes": Object { + "large": "20px", + "medium": "16px", + "small": "14px", + "xSmall": "12px", + }, + "euiNavDrawerBackgroundColor": "#1d1e24", + "euiNavDrawerContractingDelay": "150ms", + "euiNavDrawerExpandingDelay": "250ms", + "euiNavDrawerExtendedDelay": "1000ms", + "euiNavDrawerMenuAddedDelay": "90ms", + "euiNavDrawerSideShadow": "2px 0 2px -1px rgba(0, 0, 0, 0.3)", + "euiNavDrawerTopPosition": "49px", + "euiNavDrawerWidthCollapsed": "48px", + "euiNavDrawerWidthExpanded": "240px", + "euiPageBackgroundColor": "#1a1b20", + "euiPaletteColorBlind": Object { + "euiColorVis0": Object { + "behindText": "#6dccb1", + "graphic": "#54b399", + }, + "euiColorVis1": Object { + "behindText": "#79aad9", + "graphic": "#6092c0", + }, + "euiColorVis2": Object { + "behindText": "#ee789d", + "graphic": "#d36086", + }, + "euiColorVis3": Object { + "behindText": "#a987d1", + "graphic": "#9170b8", + }, + "euiColorVis4": Object { + "behindText": "#e4a6c7", + "graphic": "#ca8eae", + }, + "euiColorVis5": Object { + "behindText": "#f1d86f", + "graphic": "#d6bf57", + }, + "euiColorVis6": Object { + "behindText": "#d2c0a0", + "graphic": "#b9a888", + }, + "euiColorVis7": Object { + "behindText": "#f5a35c", + "graphic": "#da8b45", + }, + "euiColorVis8": Object { + "behindText": "#c47c6c", + "graphic": "#aa6556", + }, + "euiColorVis9": Object { + "behindText": "#ff7e62", + "graphic": "#e7664c", + }, + }, + "euiPaletteColorBlindKeys": "'euiColorVis0', 'euiColorVis1', 'euiColorVis2', 'euiColorVis3', 'euiColorVis4', 'euiColorVis5', 'euiColorVis6', 'euiColorVis7', 'euiColorVis8', 'euiColorVis9'", + "euiPanelPaddingModifiers": Object { + "paddingLarge": "24px", + "paddingMedium": "16px", + "paddingSmall": "8px", + }, + "euiPopoverArrowSize": "12px", + "euiPopoverTranslateDistance": "8px", + "euiProgressColors": Object { + "accent": "#f990c0", + "danger": "#ff6666", + "primary": "#1ba9f5", + "secondary": "#7de2d1", + "subdued": "#81858f", + "warning": "#ffce7a", + }, + "euiProgressSizes": Object { + "l": "16px", + "m": "8px", + "s": "4px", + "xs": "2px", + }, + "euiRadioSize": "16px", + "euiRangeDisabledOpacity": 0.25, + "euiRangeHighlightHeight": "4px", + "euiRangeLevelColors": Object { + "danger": "#ff6666", + "primary": "#1ba9f5", + "success": "#7de2d1", + "warning": "#ffce7a", + }, + "euiRangeThumbBorderColor": "#98a2b3", + "euiRangeThumbHeight": "16px", + "euiRangeThumbRadius": "50%", + "euiRangeThumbWidth": "16px", + "euiRangeTrackBorderColor": "#98a2b3", + "euiRangeTrackBorderWidth": 0, + "euiRangeTrackColor": "#98a2b3", + "euiRangeTrackHeight": "2px", + "euiRangeTrackRadius": "4px", + "euiRangeTrackWidth": "100%", + "euiScrollBar": "16px", + "euiScrollBarCorner": "6px", + "euiSelectableListItemBorder": "1px solid #202128", + "euiSelectableListItemPadding": "4px 12px", + "euiShadowColor": "#000000", + "euiShadowColorLarge": "#000000", + "euiSize": "16px", + "euiSizeL": "24px", + "euiSizeM": "12px", + "euiSizeS": "8px", + "euiSizeXL": "32px", + "euiSizeXS": "4px", + "euiSizeXXL": "40px", + "euiStepNumberMargin": "16px", + "euiStepNumberSize": "32px", + "euiStepStatusColorsToFade": Object { + "danger": "#ff6666", + "disabled": "#98a2b3", + "incomplete": "#98a2b3", + "warning": "#ffce7a", + }, + "euiSuggestItemColors": Object { + "tint0": "#54b399", + "tint1": "#6092c0", + "tint10": "#98a2b3", + "tint2": "#d36086", + "tint3": "#9170b8", + "tint4": "#ca8eae", + "tint5": "#d6bf57", + "tint6": "#b9a888", + "tint7": "#da8b45", + "tint8": "#aa6556", + "tint9": "#e7664c", + }, + "euiSuperDatePickerButtonWidth": "118px", + "euiSuperDatePickerWidth": "480px", + "euiSwitchHeight": "20px", + "euiSwitchHeightCompressed": "16px", + "euiSwitchHeightMini": "10px", + "euiSwitchIconHeight": "16px", + "euiSwitchOffColor": "rgba(83, 89, 102, 0.7)", + "euiSwitchThumbSize": "20px", + "euiSwitchThumbSizeCompressed": "16px", + "euiSwitchThumbSizeMini": "10px", + "euiSwitchWidth": "44px", + "euiSwitchWidthCompressed": "28px", + "euiSwitchWidthMini": "22px", + "euiTabFontSize": "16px", + "euiTabFontSizeS": "14px", + "euiTableActionsAreaWidth": "40px", + "euiTableActionsBorderColor": "rgba(83, 89, 102, 0.09999999999999998)", + "euiTableCellCheckboxWidth": "32px", + "euiTableCellContentPadding": "8px", + "euiTableCellContentPaddingCompressed": "4px", + "euiTableFocusClickableColor": "rgba(27, 169, 245, 0.09999999999999998)", + "euiTableHoverClickableColor": "rgba(27, 169, 245, 0.050000000000000044)", + "euiTableHoverColor": "#1e1e25", + "euiTableHoverSelectedColor": "#202230", + "euiTableSelectedColor": "#232635", + "euiTextColor": "#dfe5ef", + "euiTextColors": Object { + "accent": "#f990c0", + "danger": "#ff6666", + "default": "#dfe5ef", + "ghost": "#ffffff", + "secondary": "#7de2d1", + "subdued": "#81858f", + "warning": "#ffce7a", + }, + "euiTextConstrainedMaxWidth": "36em", + "euiTextScale": "2.25 1.75 1.25 1.125 1 0.875 0.75", + "euiTextSubduedColor": "#81858f", + "euiTitleColor": "#dfe5ef", + "euiTitles": Object { + "l": Object { + "font-size": "36px", + "font-weight": 300, + "letter-spacing": "-0.03em", + "line-height": "3rem", + }, + "m": Object { + "font-size": "28px", + "font-weight": 300, + "letter-spacing": "-0.04em", + "line-height": "2.5rem", + }, + "s": Object { + "font-size": "20px", + "font-weight": 500, + "letter-spacing": "-0.025em", + "line-height": "2rem", + }, + "xs": Object { + "font-size": "16px", + "font-weight": 600, + "letter-spacing": "-0.02em", + "line-height": "1.5rem", + }, + "xxs": Object { + "font-size": "14px", + "font-weight": 700, + "line-height": "1.5rem", + }, + "xxxs": Object { + "font-size": "12px", + "font-weight": 700, + "line-height": "1.5rem", + }, + }, + "euiToastTypes": Object { + "danger": "#ff6666", + "primary": "#1ba9f5", + "success": "#7de2d1", + "warning": "#ffce7a", + }, + "euiToastWidth": "320px", + "euiTokenGrayColor": "#535966", + "euiTokenTypeKeys": "'euiColorVis0', 'euiColorVis1', 'euiColorVis2', 'euiColorVis3', 'euiColorVis4', 'euiColorVis5', 'euiColorVis6', 'euiColorVis7', 'euiColorVis8', 'euiColorVis9', 'gray'", + "euiTokenTypes": Object { + "euiColorVis0": Object { + "behindText": "#6dccb1", + "graphic": "#54b399", + }, + "euiColorVis1": Object { + "behindText": "#79aad9", + "graphic": "#6092c0", + }, + "euiColorVis2": Object { + "behindText": "#ee789d", + "graphic": "#d36086", + }, + "euiColorVis3": Object { + "behindText": "#a987d1", + "graphic": "#9170b8", + }, + "euiColorVis4": Object { + "behindText": "#e4a6c7", + "graphic": "#ca8eae", + }, + "euiColorVis5": Object { + "behindText": "#f1d86f", + "graphic": "#d6bf57", + }, + "euiColorVis6": Object { + "behindText": "#d2c0a0", + "graphic": "#b9a888", + }, + "euiColorVis7": Object { + "behindText": "#f5a35c", + "graphic": "#da8b45", + }, + "euiColorVis8": Object { + "behindText": "#c47c6c", + "graphic": "#aa6556", + }, + "euiColorVis9": Object { + "behindText": "#ff7e62", + "graphic": "#e7664c", + }, + "gray": Object { + "behindText": "#535966", + "graphic": "#535966", + }, + }, + "euiTooltipAnimations": Object { + "bottom": "euiToolTipLeft", + "left": "euiToolTipBottom", + "right": "euiToolTipRight", + "top": "euiToolTipTop", + }, + "euiTooltipBackgroundColor": "#000000", + "euiZComboBox": 8001, + "euiZContent": 0, + "euiZContentMenu": 2000, + "euiZHeader": 1000, + "euiZLevel0": 0, + "euiZLevel1": 1000, + "euiZLevel2": 2000, + "euiZLevel3": 3000, + "euiZLevel4": 4000, + "euiZLevel5": 5000, + "euiZLevel6": 6000, + "euiZLevel7": 7000, + "euiZLevel8": 8000, + "euiZLevel9": 9000, + "euiZMask": 6000, + "euiZModal": 8000, + "euiZNavigation": 4000, + "euiZToastList": 9000, + "flyoutSizes": Object { + "large": Object { + "max": "992px", + "min": "691px", + "width": "75vw", + }, + "medium": Object { + "max": "768px", + "min": "424px", + "width": "50vw", + }, + "small": Object { + "max": "403px", + "min": "384px", + "width": "25vw", + }, + }, + "fractions": Object { + "fourths": Object { + "count": 4, + "percentage": "25%", + }, + "halves": Object { + "count": 2, + "percentage": "50%", + }, + "single": Object { + "count": 1, + "percentage": "100%", + }, + "thirds": Object { + "count": 3, + "percentage": "33.3%", + }, + }, + "gutterTypes": Object { + "gutterExtraLarge": "40px", + "gutterExtraSmall": "4px", + "gutterLarge": "24px", + "gutterMedium": "16px", + "gutterSmall": "8px", + }, + "paddingSizes": Object { + "l": "24px", + "m": "16px", + "s": "8px", + "xl": "32px", + "xs": "4px", + }, + "ruleMargins": Object { + "marginLarge": "24px", + "marginMedium": "16px", + "marginSmall": "12px", + "marginXLarge": "32px", + "marginXSmall": "8px", + "marginXXLarge": "40px", + }, + "spacerSizes": Object { + "l": "24px", + "m": "16px", + "s": "8px", + "xl": "32px", + "xs": "4px", + "xxl": "40px", + }, + "textColors": Object { + "accent": "#f990c0", + "danger": "#ff7575", + "ghost": "#ffffff", + "primary": "#1ba9f5", + "secondary": "#7de2d1", + "subdued": "#81858f", + "text": "#dfe5ef", + "warning": "#ffce7a", + }, + "textareaResizing": Object { + "both": "resizeBoth", + "horizontal": "resizeHorizontal", + "none": "resizeNone", + "vertical": "resizeVertical", + }, + }, + } + } +> + <PaginatedTableComponent + activePage={0} + columns={ + Array [ + Object { + "field": "node.host.name", + "hideForMobile": false, + "name": "Host", + "render": [Function], + "truncateText": false, + }, + Object { + "field": "node.host.firstSeen", + "hideForMobile": false, + "name": "First seen", + "render": [Function], + "truncateText": false, + }, + Object { + "field": "node.host.os", + "hideForMobile": false, + "name": "OS", + "render": [Function], + "truncateText": false, + }, + Object { + "field": "node.host.version", + "hideForMobile": false, + "name": "Version", + "render": [Function], + "truncateText": false, + }, + ] + } + headerCount={1} + headerSupplement={ + <p> + My test supplement. + </p> + } + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={ + Array [ + Object { + "numberOfRow": 2, + "text": "2 rows", + }, + Object { + "numberOfRow": 5, + "text": "5 rows", + }, + Object { + "numberOfRow": 10, + "text": "10 rows", + }, + Object { + "numberOfRow": 20, + "text": "20 rows", + }, + Object { + "numberOfRow": 50, + "text": "50 rows", + }, + ] + } + limit={1} + loadPage={[MockFunction]} + loading={false} + pageOfItems={ + Array [ + Object { + "cursor": Object { + "value": "98966fa2013c396155c460d35c0902be", + }, + "host": Object { + "_id": "cPsuhGcB0WOhS6qyTKC0", + "firstSeen": "2018-12-06T15:40:53.319Z", + "name": "elrond.elstc.co", + "os": "Ubuntu", + "version": "18.04.1 LTS (Bionic Beaver)", + }, + }, + Object { + "cursor": Object { + "value": "aa7ca589f1b8220002f2fc61c64cfbf1", + }, + "host": Object { + "_id": "KwQDiWcB0WOhS6qyXmrW", + "firstSeen": "2018-12-07T14:12:38.560Z", + "name": "siem-kibana", + "os": "Debian GNU/Linux", + "version": "9 (stretch)", + }, + }, + ] + } + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={[MockFunction]} + updateLimitPagination={[Function]} + /> +</ContextProvider> +`; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.test.ts b/x-pack/plugins/siem/public/components/paginated_table/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.test.ts rename to x-pack/plugins/siem/public/components/paginated_table/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.ts b/x-pack/plugins/siem/public/components/paginated_table/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.ts rename to x-pack/plugins/siem/public/components/paginated_table/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.mock.tsx b/x-pack/plugins/siem/public/components/paginated_table/index.mock.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/paginated_table/index.mock.tsx rename to x-pack/plugins/siem/public/components/paginated_table/index.mock.tsx diff --git a/x-pack/plugins/siem/public/components/paginated_table/index.test.tsx b/x-pack/plugins/siem/public/components/paginated_table/index.test.tsx new file mode 100644 index 0000000000000..94dac6607ce21 --- /dev/null +++ b/x-pack/plugins/siem/public/components/paginated_table/index.test.tsx @@ -0,0 +1,522 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; +import { Direction } from '../../graphql/types'; + +import { BasicTableProps, PaginatedTable } from './index'; +import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; + +jest.mock('react', () => { + const r = jest.requireActual('react'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { ...r, memo: (x: any) => x }; +}); + +describe('Paginated Table Component', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + let loadPage: jest.Mock<number>; + let updateLimitPagination: jest.Mock<number>; + let updateActivePage: jest.Mock<number>; + beforeEach(() => { + loadPage = jest.fn(); + updateLimitPagination = jest.fn(); + updateActivePage = jest.fn(); + }); + + describe('rendering', () => { + test('it renders the default load more table', () => { + const wrapper = shallow( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the loading panel at the beginning ', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={-1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={true} + loadPage={loadPage} + pageOfItems={[]} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect( + wrapper.find('[data-test-subj="initialLoadingPanelPaginatedTable"]').exists() + ).toBeTruthy(); + }); + + test('it renders the over loading panel after data has been in the table ', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={true} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper.find('[data-test-subj="loadingPanelPaginatedTable"]').exists()).toBeTruthy(); + }); + + test('it renders the correct amount of pages and starts at activePage: 0', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + const paginiationProps = wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .props(); + + const expectedPaginationProps = { + 'data-test-subj': 'numberedPagination', + pageCount: 10, + activePage: 0, + }; + expect(JSON.stringify(paginiationProps)).toEqual(JSON.stringify(expectedPaginationProps)); + }); + + test('it render popover to select new limit in table', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy(); + }); + + test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={[]} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); + }); + + test('It should render a sort icon if sorting is defined', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={sortedHosts} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={jest.fn()} + onChange={mockOnChange} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + sorting={{ direction: Direction.asc, field: 'node.host.name' }} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy(); + }); + + test('Should display toast when user reaches end of results max', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls.length).toEqual(0); + }); + + test('Should show items per row if totalCount is greater than items', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={30} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); + }); + + test('Should hide items per row if totalCount is less than items', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={1} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); + }); + }); + + describe('Events', () => { + test('should call updateActivePage with 1 when clicking to the first page', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls[0][0]).toEqual(1); + }); + + test('Should call updateActivePage with 0 when you pick a new limit', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMorePickSizeRow"] button') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls[1][0]).toEqual(0); + }); + + test('should update the page when the activePage is changed from redux', () => { + const ourProps: BasicTableProps<unknown> = { + activePage: 3, + columns: getHostsColumns(), + headerCount: 1, + headerSupplement: <p>{'My test supplement.'}</p>, + headerTitle: 'Hosts', + headerTooltip: 'My test tooltip', + headerUnit: 'Test Unit', + itemsPerRow: rowItems, + limit: 1, + loading: false, + loadPage, + pageOfItems: mockData.Hosts.edges, + showMorePagesIndicator: true, + totalCount: 10, + updateActivePage, + updateLimitPagination: limit => updateLimitPagination({ limit }), + }; + + // enzyme does not allow us to pass props to child of HOC + // so we make a component to pass it the props context + // ComponentWithContext will pass the changed props to Component + // https://github.com/airbnb/enzyme/issues/1853#issuecomment-443475903 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ComponentWithContext = (props: BasicTableProps<any>) => { + return ( + <ThemeProvider theme={theme}> + <PaginatedTable {...props} /> + </ThemeProvider> + ); + }; + + const wrapper = mount(<ComponentWithContext {...ourProps} />); + expect( + wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .prop('activePage') + ).toEqual(3); + wrapper.setProps({ activePage: 0 }); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .prop('activePage') + ).toEqual(0); + }); + + test('Should call updateLimitPagination when you pick a new limit', () => { + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={getHostsColumns()} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMorePickSizeRow"] button') + .first() + .simulate('click'); + expect(updateLimitPagination).toBeCalled(); + }); + + test('Should call onChange when you choose a new sort in the table', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + <ThemeProvider theme={theme}> + <PaginatedTable + activePage={0} + columns={sortedHosts} + headerCount={1} + headerSupplement={<p>{'My test supplement.'}</p>} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={jest.fn()} + onChange={mockOnChange} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + sorting={{ direction: Direction.asc, field: 'node.host.name' }} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> + </ThemeProvider> + ); + + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + expect(mockOnChange).toBeCalled(); + expect(mockOnChange.mock.calls[0]).toEqual([ + { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/plugins/siem/public/components/paginated_table/index.tsx new file mode 100644 index 0000000000000..a815ecd100518 --- /dev/null +++ b/x-pack/plugins/siem/public/components/paginated_table/index.tsx @@ -0,0 +1,350 @@ +/* + * 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 { + EuiBasicTable, + EuiBasicTableProps, + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiGlobalToastListToast as Toast, + EuiLoadingContent, + EuiPagination, + EuiPopover, + Direction, +} from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; +import styled from 'styled-components'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; +import { AuthTableColumns } from '../page/hosts/authentications_table'; +import { HostsTableColumns } from '../page/hosts/hosts_table'; +import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; +import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; +import { + NetworkTopNFlowColumns, + NetworkTopNFlowColumnsIpDetails, +} from '../page/network/network_top_n_flow_table/columns'; +import { + NetworkTopCountriesColumns, + NetworkTopCountriesColumnsIpDetails, +} from '../page/network/network_top_countries_table/columns'; +import { TlsColumns } from '../page/network/tls_table/columns'; +import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table'; +import { UsersColumns } from '../page/network/users_table/columns'; +import { HeaderSection } from '../header_section'; +import { Loader } from '../loader'; +import { useStateToaster } from '../toasters'; + +import * as i18n from './translations'; +import { Panel } from '../panel'; +import { InspectButtonContainer } from '../inspect'; + +const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; + +export interface ItemsPerRow { + text: string; + numberOfRow: number; +} + +export interface SortingBasicTable { + field: string; + direction: Direction; + allowNeutralSort?: boolean; +} + +export interface Criteria { + page?: { index: number; size: number }; + sort?: SortingBasicTable; +} + +declare type HostsTableColumnsTest = [ + Columns<string>, + Columns<string>, + Columns<string>, + Columns<string> +]; + +declare type BasicTableColumns = + | AuthTableColumns + | HostsTableColumns + | HostsTableColumnsTest + | NetworkDnsColumns + | NetworkHttpColumns + | NetworkTopCountriesColumns + | NetworkTopCountriesColumnsIpDetails + | NetworkTopNFlowColumns + | NetworkTopNFlowColumnsIpDetails + | TlsColumns + | UncommonProcessTableColumns + | UsersColumns; + +declare type SiemTables = BasicTableProps<BasicTableColumns>; + +// Using telescoping templates to remove 'any' that was polluting downstream column type checks +export interface BasicTableProps<T> { + activePage: number; + columns: T; + dataTestSubj?: string; + headerCount: number; + headerSupplement?: React.ReactElement; + headerTitle: string | React.ReactElement; + headerTooltip?: string; + headerUnit: string | React.ReactElement; + id?: string; + itemsPerRow?: ItemsPerRow[]; + isInspect?: boolean; + limit: number; + loading: boolean; + loadPage: (activePage: number) => void; + onChange?: (criteria: Criteria) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pageOfItems: any[]; + showMorePagesIndicator: boolean; + sorting?: SortingBasicTable; + totalCount: number; + updateActivePage: (activePage: number) => void; + updateLimitPagination: (limit: number) => void; +} +type Func<T> = (arg: T) => string | number; + +export interface Columns<T, U = T> { + align?: string; + field?: string; + hideForMobile?: boolean; + isMobileHeader?: boolean; + name: string | React.ReactNode; + render?: (item: T, node: U) => React.ReactNode; + sortable?: boolean | Func<T>; + truncateText?: boolean; + width?: string; +} + +const PaginatedTableComponent: FC<SiemTables> = ({ + activePage, + columns, + dataTestSubj = DEFAULT_DATA_TEST_SUBJ, + headerCount, + headerSupplement, + headerTitle, + headerTooltip, + headerUnit, + id, + isInspect, + itemsPerRow, + limit, + loading, + loadPage, + onChange = noop, + pageOfItems, + showMorePagesIndicator, + sorting = null, + totalCount, + updateActivePage, + updateLimitPagination, +}) => { + const [myLoading, setMyLoading] = useState(loading); + const [myActivePage, setActivePage] = useState(activePage); + const [loadingInitial, setLoadingInitial] = useState(headerCount === -1); + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const pageCount = Math.ceil(totalCount / limit); + const dispatchToaster = useStateToaster()[1]; + + useEffect(() => { + setActivePage(activePage); + }, [activePage]); + + useEffect(() => { + if (headerCount >= 0 && loadingInitial) { + setLoadingInitial(false); + } + }, [loadingInitial, headerCount]); + + useEffect(() => { + setMyLoading(loading); + }, [loading]); + + const onButtonClick = () => { + setPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setPopoverOpen(false); + }; + + const goToPage = (newActivePage: number) => { + if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + const toast: Toast = { + id: 'PaginationWarningMsg', + title: headerTitle + i18n.TOAST_TITLE, + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 10000, + text: i18n.TOAST_TEXT, + }; + return dispatchToaster({ + type: 'addToaster', + toast, + }); + } + setActivePage(newActivePage); + loadPage(newActivePage); + updateActivePage(newActivePage); + }; + + const button = ( + <EuiButtonEmpty + size="xs" + color="text" + iconType="arrowDown" + iconSide="right" + onClick={onButtonClick} + > + {`${i18n.ROWS}: ${limit}`} + </EuiButtonEmpty> + ); + + const rowItems = + itemsPerRow && + itemsPerRow.map((item: ItemsPerRow) => ( + <EuiContextMenuItem + key={item.text} + icon={limit === item.numberOfRow ? 'check' : 'empty'} + onClick={() => { + closePopover(); + updateLimitPagination(item.numberOfRow); + updateActivePage(0); // reset results to first page + }} + > + {item.text} + </EuiContextMenuItem> + )); + const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; + + return ( + <InspectButtonContainer show={!loadingInitial}> + <Panel data-test-subj={`${dataTestSubj}-loading-${loading}`} loading={loading}> + <HeaderSection + id={id} + subtitle={ + !loadingInitial && + `${i18n.SHOWING}: ${headerCount >= 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` + } + title={headerTitle} + tooltip={headerTooltip} + > + {!loadingInitial && headerSupplement} + </HeaderSection> + + {loadingInitial ? ( + <EuiLoadingContent data-test-subj="initialLoadingPanelPaginatedTable" lines={10} /> + ) : ( + <> + <BasicTable + columns={columns} + compressed + items={pageOfItems} + onChange={onChange} + sorting={ + sorting + ? { + sort: { + field: sorting.field, + direction: sorting.direction, + }, + } + : undefined + } + /> + <FooterAction> + <EuiFlexItem> + {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( + <EuiPopover + id="customizablePagination" + data-test-subj="loadingMoreSizeRowPopover" + button={button} + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + <EuiContextMenuPanel items={rowItems} data-test-subj="loadingMorePickSizeRow" /> + </EuiPopover> + )} + </EuiFlexItem> + + <PaginationWrapper grow={false}> + <EuiPagination + data-test-subj="numberedPagination" + pageCount={pageCount} + activePage={myActivePage} + onPageClick={goToPage} + /> + </PaginationWrapper> + </FooterAction> + {(isInspect || myLoading) && ( + <Loader data-test-subj="loadingPanelPaginatedTable" overlay size="xl" /> + )} + </> + )} + </Panel> + </InspectButtonContainer> + ); +}; + +export const PaginatedTable = memo(PaginatedTableComponent); + +type BasicTableType = ComponentType<EuiBasicTableProps<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any +const BasicTable = styled(EuiBasicTable as BasicTableType)` + tbody { + th, + td { + vertical-align: top; + } + + .euiTableCellContent { + display: block; + } + } +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +BasicTable.displayName = 'BasicTable'; + +const FooterAction = styled(EuiFlexGroup).attrs(() => ({ + alignItems: 'center', + responsive: false, +}))` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; +`; + +FooterAction.displayName = 'FooterAction'; + +const PaginationEuiFlexItem = styled(EuiFlexItem)` + @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { + .euiButtonIcon:last-child { + margin-left: 28px; + } + + .euiPagination { + position: relative; + } + + .euiPagination::before { + bottom: 0; + color: ${({ theme }) => theme.eui.euiButtonColorDisabled}; + content: '\\2026'; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + padding: 5px ${({ theme }) => theme.eui.euiSizeS}; + position: absolute; + right: ${({ theme }) => theme.eui.euiSizeL}; + } + } +`; + +PaginationEuiFlexItem.displayName = 'PaginationEuiFlexItem'; diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/translations.ts b/x-pack/plugins/siem/public/components/paginated_table/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/paginated_table/translations.ts rename to x-pack/plugins/siem/public/components/paginated_table/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/panel/index.test.tsx b/x-pack/plugins/siem/public/components/panel/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/panel/index.test.tsx rename to x-pack/plugins/siem/public/components/panel/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/panel/index.tsx b/x-pack/plugins/siem/public/components/panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/panel/index.tsx rename to x-pack/plugins/siem/public/components/panel/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/pin/index.test.tsx b/x-pack/plugins/siem/public/components/pin/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/pin/index.test.tsx rename to x-pack/plugins/siem/public/components/pin/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/pin/index.tsx b/x-pack/plugins/siem/public/components/pin/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/pin/index.tsx rename to x-pack/plugins/siem/public/components/pin/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx b/x-pack/plugins/siem/public/components/port/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/port/index.test.tsx rename to x-pack/plugins/siem/public/components/port/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.tsx b/x-pack/plugins/siem/public/components/port/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/port/index.tsx rename to x-pack/plugins/siem/public/components/port/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx b/x-pack/plugins/siem/public/components/progress_inline/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/progress_inline/index.test.tsx rename to x-pack/plugins/siem/public/components/progress_inline/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/progress_inline/index.tsx b/x-pack/plugins/siem/public/components/progress_inline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/progress_inline/index.tsx rename to x-pack/plugins/siem/public/components/progress_inline/index.tsx diff --git a/x-pack/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/plugins/siem/public/components/query_bar/index.test.tsx new file mode 100644 index 0000000000000..e27669b2b15be --- /dev/null +++ b/x-pack/plugins/siem/public/components/query_bar/index.test.tsx @@ -0,0 +1,338 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../common/constants'; +import { TestProviders, mockIndexPattern } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager, SearchBar } from '../../../../../../src/plugins/data/public'; +import { QueryBar, QueryBarComponentProps } from '.'; +import { createKibanaContextProviderMock } from '../../mock/kibana_react'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +describe('QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockOnChangeQuery = jest.fn(); + const mockOnSubmitQuery = jest.fn(); + const mockOnSavedQuery = jest.fn(); + + beforeEach(() => { + mockOnChangeQuery.mockClear(); + mockOnSubmitQuery.mockClear(); + mockOnSavedQuery.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + <TestProviders> + <QueryBar + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + </TestProviders> + ); + const { + customSubmitButton, + timeHistory, + onClearSavedQuery, + onFiltersUpdated, + onQueryChange, + onQuerySubmit, + onSaved, + onSavedQueryUpdated, + ...searchBarProps + } = wrapper.find(SearchBar).props(); + + expect(searchBarProps).toEqual({ + dataTestSubj: undefined, + dateRangeFrom: 'now-24h', + dateRangeTo: 'now', + filters: [], + indexPatterns: [ + { + fields: [ + { + aggregatable: true, + name: '@timestamp', + searchable: true, + type: 'date', + }, + { + aggregatable: true, + name: '@version', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test1', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test2', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test3', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test4', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test5', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test6', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test7', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test8', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'host.name', + searchable: true, + type: 'string', + }, + ], + title: 'filebeat-*,auditbeat-*,packetbeat-*', + }, + ], + isLoading: false, + isRefreshPaused: true, + query: { + language: 'kuery', + query: 'here: query', + }, + refreshInterval: undefined, + showAutoRefreshOnly: false, + showDatePicker: false, + showFilterBar: true, + showQueryBar: true, + showQueryInput: true, + showSaveQuery: true, + }); + }); + + describe('#onQueryChange', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const KibanaWithStorageProvider = createKibanaContextProviderMock(); + + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <KibanaWithStorageProvider services={{ storage: { get: jest.fn() } }}> + <QueryBar {...props} /> + </KibanaWithStorageProvider> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); + queryInput.simulate('change', { target: { value: 'hello: world' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onQuerySubmit', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <QueryBar {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <QueryBar {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSubmitQuery: jest.fn() }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onSavedQueryUpdated', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + <TestProviders> + <QueryBar {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + dateRangeFrom={DEFAULT_FROM} + dateRangeTo={DEFAULT_TO} + hideSavedQuery={false} + indexPattern={mockIndexPattern} + isRefreshPaused={true} + filterQuery={{ query: 'here: query', language: 'kuery' }} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filters={[]} + onChangedQuery={mockOnChangeQuery} + onSubmitQuery={mockOnSubmitQuery} + onSavedQuery={mockOnSavedQuery} + /> + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSavedQuery: jest.fn() }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/query_bar/index.tsx b/x-pack/plugins/siem/public/components/query_bar/index.tsx new file mode 100644 index 0000000000000..1ad7bc16b901e --- /dev/null +++ b/x-pack/plugins/siem/public/components/query_bar/index.tsx @@ -0,0 +1,153 @@ +/* + * 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, { memo, useState, useEffect, useMemo, useCallback } from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { + Filter, + IIndexPattern, + FilterManager, + Query, + TimeHistory, + TimeRange, + SavedQuery, + SearchBar, + SavedQueryTimeFilter, +} from '../../../../../../src/plugins/data/public'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; + +export interface QueryBarComponentProps { + dataTestSubj?: string; + dateRangeFrom?: string; + dateRangeTo?: string; + hideSavedQuery?: boolean; + indexPattern: IIndexPattern; + isLoading?: boolean; + isRefreshPaused?: boolean; + filterQuery: Query; + filterManager: FilterManager; + filters: Filter[]; + onChangedQuery: (query: Query) => void; + onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; + refreshInterval?: number; + savedQuery?: SavedQuery | null; + onSavedQuery: (savedQuery: SavedQuery | null) => void; +} + +export const QueryBar = memo<QueryBarComponentProps>( + ({ + dateRangeFrom, + dateRangeTo, + hideSavedQuery = false, + indexPattern, + isLoading = false, + isRefreshPaused, + filterQuery, + filterManager, + filters, + onChangedQuery, + onSubmitQuery, + refreshInterval, + savedQuery, + onSavedQuery, + dataTestSubj, + }) => { + const [draftQuery, setDraftQuery] = useState(filterQuery); + + useEffect(() => { + setDraftQuery(filterQuery); + }, [filterQuery]); + + const onQuerySubmit = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { + onSubmitQuery(payload.query); + } + }, + [filterQuery, onSubmitQuery] + ); + + const onQueryChange = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !deepEqual(payload.query, draftQuery)) { + setDraftQuery(payload.query); + onChangedQuery(payload.query); + } + }, + [draftQuery, onChangedQuery, setDraftQuery] + ); + + const onSaved = useCallback( + (newSavedQuery: SavedQuery) => { + onSavedQuery(newSavedQuery); + }, + [onSavedQuery] + ); + + const onSavedQueryUpdated = useCallback( + (savedQueryUpdated: SavedQuery) => { + const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; + onSubmitQuery(newQuery, timefilter); + filterManager.setFilters(newFilters || []); + onSavedQuery(savedQueryUpdated); + }, + [filterManager, onSubmitQuery, onSavedQuery] + ); + + const onClearSavedQuery = useCallback(() => { + if (savedQuery != null) { + onSubmitQuery({ + query: '', + language: savedQuery.attributes.query.language, + }); + filterManager.setFilters([]); + onSavedQuery(null); + } + }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); + + const onFiltersUpdated = useCallback( + (newFilters: Filter[]) => { + filterManager.setFilters(newFilters); + }, + [filterManager] + ); + + const CustomButton = <>{null}</>; + const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); + + const searchBarProps = savedQuery != null ? { savedQuery } : {}; + + return ( + <SearchBar + customSubmitButton={CustomButton} + dateRangeFrom={dateRangeFrom} + dateRangeTo={dateRangeTo} + filters={filters} + indexPatterns={indexPatterns} + isLoading={isLoading} + isRefreshPaused={isRefreshPaused} + query={draftQuery} + onClearSavedQuery={onClearSavedQuery} + onFiltersUpdated={onFiltersUpdated} + onQueryChange={onQueryChange} + onQuerySubmit={onQuerySubmit} + onSaved={onSaved} + onSavedQueryUpdated={onSavedQueryUpdated} + refreshInterval={refreshInterval} + showAutoRefreshOnly={false} + showFilterBar={!hideSavedQuery} + showDatePicker={false} + showQueryBar={true} + showQueryInput={true} + showSaveQuery={true} + timeHistory={new TimeHistory(new Storage(localStorage))} + dataTestSubj={dataTestSubj} + {...searchBarProps} + /> + ); + } +); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/filters/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/filters/index.tsx rename to x-pack/plugins/siem/public/components/recent_cases/filters/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/index.tsx rename to x-pack/plugins/siem/public/components/recent_cases/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/no_cases/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/no_cases/index.tsx rename to x-pack/plugins/siem/public/components/recent_cases/no_cases/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/siem/public/components/recent_cases/recent_cases.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/recent_cases.tsx rename to x-pack/plugins/siem/public/components/recent_cases/recent_cases.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts b/x-pack/plugins/siem/public/components/recent_cases/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/translations.ts rename to x-pack/plugins/siem/public/components/recent_cases/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts b/x-pack/plugins/siem/public/components/recent_cases/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_cases/types.ts rename to x-pack/plugins/siem/public/components/recent_cases/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/counts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/counts/index.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/counts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/filters/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/filters/index.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/filters/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/header/index.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/header/index.tsx diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx new file mode 100644 index 0000000000000..b641038f35ba6 --- /dev/null +++ b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx @@ -0,0 +1,114 @@ +/* + * 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 ApolloClient from 'apollo-client'; +import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { useGetAllTimeline } from '../../containers/timeline/all'; +import { SortFieldTimeline, Direction } from '../../graphql/types'; +import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; +import { OnOpenTimeline } from '../open_timeline/types'; +import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; +import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions'; + +import { RecentTimelines } from './recent_timelines'; +import * as i18n from './translations'; +import { FilterMode } from './types'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; +import { navTabs } from '../../pages/home/home_navigations'; +import { getTimelinesUrl } from '../link_to/redirect_to_timelines'; + +interface OwnProps { + apolloClient: ApolloClient<{}>; + filterBy: FilterMode; +} + +export type Props = OwnProps & PropsFromRedux; + +const PAGE_SIZE = 3; + +const StatefulRecentTimelinesComponent = React.memo<Props>( + ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { + const onOpenTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + queryTimelineById({ + apolloClient, + duplicate, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + const noTimelinesMessage = + filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + const urlSearch = useGetUrlSearch(navTabs.timelines); + const linkAllTimelines = useMemo( + () => <EuiLink href={getTimelinesUrl(urlSearch)}>{i18n.VIEW_ALL_TIMELINES}</EuiLink>, + [urlSearch] + ); + const loadingPlaceholders = useMemo( + () => ( + <LoadingPlaceholders lines={2} placeholders={filterBy === 'favorites' ? 1 : PAGE_SIZE} /> + ), + [filterBy] + ); + + const { fetchAllTimeline, timelines, totalCount, loading } = useGetAllTimeline(); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize: PAGE_SIZE, + }, + search: '', + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: filterBy === 'favorites', + timelines, + totalCount, + }); + }, [filterBy, timelines, totalCount]); + + return ( + <> + {loading ? ( + loadingPlaceholders + ) : ( + <RecentTimelines + noTimelinesMessage={noTimelinesMessage} + onOpenTimeline={onOpenTimeline} + timelines={timelines} + /> + )} + <EuiHorizontalRule margin="s" /> + <EuiText size="xs">{linkAllTimelines}</EuiText> + </> + ); + } +); + +StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(dispatchUpdateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulRecentTimelines = connector(StatefulRecentTimelinesComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/siem/public/components/recent_timelines/recent_timelines.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/recent_timelines.tsx rename to x-pack/plugins/siem/public/components/recent_timelines/recent_timelines.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts b/x-pack/plugins/siem/public/components/recent_timelines/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/translations.ts rename to x-pack/plugins/siem/public/components/recent_timelines/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts b/x-pack/plugins/siem/public/components/recent_timelines/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/recent_timelines/types.ts rename to x-pack/plugins/siem/public/components/recent_timelines/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx b/x-pack/plugins/siem/public/components/scroll_to_top/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.test.tsx rename to x-pack/plugins/siem/public/components/scroll_to_top/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx b/x-pack/plugins/siem/public/components/scroll_to_top/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/scroll_to_top/index.tsx rename to x-pack/plugins/siem/public/components/scroll_to_top/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/plugins/siem/public/components/search_bar/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx rename to x-pack/plugins/siem/public/components/search_bar/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts b/x-pack/plugins/siem/public/components/search_bar/selectors.ts similarity index 91% rename from x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts rename to x-pack/plugins/siem/public/components/search_bar/selectors.ts index 793737a1ad754..4e700a46ca0e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts +++ b/x-pack/plugins/siem/public/components/search_bar/selectors.ts @@ -6,7 +6,7 @@ import { createSelector } from 'reselect'; import { InputsRange } from '../../store/inputs/model'; -import { Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; +import { Query, SavedQuery } from '../../../../../../src/plugins/data/public'; export { endSelector, diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx b/x-pack/plugins/siem/public/components/selectable_text/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/selectable_text/index.test.tsx rename to x-pack/plugins/siem/public/components/selectable_text/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/selectable_text/index.tsx b/x-pack/plugins/siem/public/components/selectable_text/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/selectable_text/index.tsx rename to x-pack/plugins/siem/public/components/selectable_text/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx b/x-pack/plugins/siem/public/components/sidebar_header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/sidebar_header/index.tsx rename to x-pack/plugins/siem/public/components/sidebar_header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/plugins/siem/public/components/skeleton_row/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/skeleton_row/index.test.tsx rename to x-pack/plugins/siem/public/components/skeleton_row/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/plugins/siem/public/components/skeleton_row/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/skeleton_row/index.tsx rename to x-pack/plugins/siem/public/components/skeleton_row/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx b/x-pack/plugins/siem/public/components/source_destination/country_flag.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/country_flag.tsx rename to x-pack/plugins/siem/public/components/source_destination/country_flag.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/field_names.ts b/x-pack/plugins/siem/public/components/source_destination/field_names.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/field_names.ts rename to x-pack/plugins/siem/public/components/source_destination/field_names.ts diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx b/x-pack/plugins/siem/public/components/source_destination/geo_fields.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/geo_fields.tsx rename to x-pack/plugins/siem/public/components/source_destination/geo_fields.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx b/x-pack/plugins/siem/public/components/source_destination/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx rename to x-pack/plugins/siem/public/components/source_destination/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx b/x-pack/plugins/siem/public/components/source_destination/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/index.tsx rename to x-pack/plugins/siem/public/components/source_destination/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx b/x-pack/plugins/siem/public/components/source_destination/ip_with_port.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/ip_with_port.tsx rename to x-pack/plugins/siem/public/components/source_destination/ip_with_port.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/label.tsx b/x-pack/plugins/siem/public/components/source_destination/label.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/label.tsx rename to x-pack/plugins/siem/public/components/source_destination/label.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx b/x-pack/plugins/siem/public/components/source_destination/network.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/network.tsx rename to x-pack/plugins/siem/public/components/source_destination/network.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_arrows.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_arrows.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_arrows.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_ip.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx b/x-pack/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx rename to x-pack/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/translations.ts b/x-pack/plugins/siem/public/components/source_destination/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/translations.ts rename to x-pack/plugins/siem/public/components/source_destination/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/types.ts b/x-pack/plugins/siem/public/components/source_destination/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/source_destination/types.ts rename to x-pack/plugins/siem/public/components/source_destination/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/stat_items/index.test.tsx rename to x-pack/plugins/siem/public/components/stat_items/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/stat_items/index.tsx rename to x-pack/plugins/siem/public/components/stat_items/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx b/x-pack/plugins/siem/public/components/subtitle/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/subtitle/index.test.tsx rename to x-pack/plugins/siem/public/components/subtitle/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/subtitle/index.tsx b/x-pack/plugins/siem/public/components/subtitle/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/subtitle/index.tsx rename to x-pack/plugins/siem/public/components/subtitle/index.tsx diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx new file mode 100644 index 0000000000000..b6b515ceeffa6 --- /dev/null +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx @@ -0,0 +1,443 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; +import { apolloClientObservable, mockGlobalState } from '../../mock'; +import { createUseUiSetting$Mock } from '../../mock/kibana_react'; +import { createStore, State } from '../../store'; + +import { SuperDatePicker, makeMapStateToProps } from '.'; +import { cloneDeep } from 'lodash/fp'; + +jest.mock('../../lib/kibana'); +const mockUseUiSetting$ = useUiSetting$ as jest.Mock; +const timepickerRanges = [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, +]; + +describe('SIEM Super Date Picker', () => { + describe('#SuperDatePicker', () => { + const state: State = mockGlobalState; + let store = createStore(state, apolloClientObservable); + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(state, apolloClientObservable); + mockUseUiSetting$.mockImplementation((key, defaultValue) => { + const useUiSetting$Mock = createUseUiSetting$Mock(); + + return key === DEFAULT_TIMEPICKER_QUICK_RANGES + ? [timepickerRanges, jest.fn()] + : useUiSetting$Mock(key, defaultValue); + }); + }); + + describe('Pick Relative Date', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.euiQuickSelect__applyButton') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Make Sure it is relative date', () => { + expect(store.getState().inputs.global.timerange.kind).toBe('relative'); + }); + + test('Make Sure it is last 24 hours date', () => { + expect(store.getState().inputs.global.timerange.fromStr).toBe('now-24h'); + expect(store.getState().inputs.global.timerange.toStr).toBe('now'); + }); + + test('Make Sure it is Today date', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d'); + expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); + }); + + test('Make Sure to (end date) is superior than from (start date)', () => { + expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( + store.getState().inputs.global.timerange.from + ); + }); + }); + + describe('Recently used date ranges', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Today is in Recently used date ranges', () => { + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Today'); + }); + + test('Today and Last 24 hours are in Recently used date ranges', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.euiQuickSelect__applyButton') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Last 24 hoursToday'); + }); + + test('Make sure that it does not add any duplicate if you click again on today', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Today'); + }); + }); + + describe('Refresh Every', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + const wrapperFixedEuiFieldSearch = wrapper.find( + 'input[data-test-subj="superDatePickerRefreshIntervalInput"]' + ); + + wrapperFixedEuiFieldSearch.simulate('change', { target: { value: '2' } }); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerToggleRefreshButton"]') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Make sure the duration get updated to 2 minutes === 120000ms', () => { + expect(store.getState().inputs.global.policy.duration).toEqual(120000); + }); + + test('Make sure the stream live started', () => { + expect(store.getState().inputs.global.policy.kind).toBe('interval'); + }); + + test('Make sure we can stop the stream live', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerToggleRefreshButton"]') + .first() + .simulate('click'); + wrapper.update(); + + expect(store.getState().inputs.global.policy.kind).toBe('manual'); + }); + }); + + describe('Pick Absolute Date', () => { + let wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + beforeEach(() => { + wrapper = mount( + <ReduxStoreProvider store={store}> + <SuperDatePicker id="global" /> + </ReduxStoreProvider> + ); + wrapper + .find('[data-test-subj="superDatePickerShowDatesButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerstartDatePopoverButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerAbsoluteTab"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.react-datepicker__navigation--previous') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('div.react-datepicker__day') + .at(1) + .simulate('click'); + wrapper.update(); + + wrapper + .find('button[data-test-subj="superDatePickerApplyTimeButton"]') + .first() + .simulate('click'); + wrapper.update(); + }); + }); + + describe('#makeMapStateToProps', () => { + test('it should return the same shallow references given the same input twice', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const props2 = mapStateToProps(state, { id: 'global' }); + Object.keys(props1).forEach(key => { + expect((props1 as Record<string, {}>)[key]).toBe((props2 as Record<string, {}>)[key]); + }); + }); + + test('it should not return the same reference if policy kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.kind = 'interval'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.policy).not.toBe(props2.policy); + }); + + test('it should not return the same reference if duration is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.duration = 99999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.duration).not.toBe(props2.duration); + }); + + test('it should not return the same reference if timerange kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.kind = 'absolute'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.kind).not.toBe(props2.kind); + }); + + test('it should not return the same reference if timerange from is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.from = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.start).not.toBe(props2.start); + }); + + test('it should not return the same reference if timerange to is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.to = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.end).not.toBe(props2.end); + }); + + test('it should not return the same reference of toStr if toStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.toStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.toStr).not.toBe(props2.toStr); + }); + + test('it should not return the same reference of fromStr if fromStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.fromStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.fromStr).not.toBe(props2.fromStr); + }); + + test('it should not return the same reference of isLoadingSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.queries = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.isLoading).not.toBe(props2.isLoading); + }); + + test('it should not return the same reference of refetchSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.queries = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.queries).not.toBe(props2.queries); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx new file mode 100644 index 0000000000000..ad38a7d61bcba --- /dev/null +++ b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx @@ -0,0 +1,313 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; +import { + EuiSuperDatePicker, + OnRefreshChangeProps, + EuiSuperDatePickerRecentRange, + OnRefreshProps, + OnTimeChangeProps, +} from '@elastic/eui'; +import { getOr, take, isEmpty } from 'lodash/fp'; +import React, { useState, useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; +import { inputsModel, State } from '../../store'; +import { inputsActions, timelineActions } from '../../store/actions'; +import { InputsModelId } from '../../store/inputs/constants'; +import { + policySelector, + durationSelector, + kindSelector, + startSelector, + endSelector, + fromStrSelector, + toStrSelector, + isLoadingSelector, + queriesSelector, + kqlQuerySelector, +} from './selectors'; +import { InputsRange } from '../../store/inputs/model'; + +const MAX_RECENTLY_USED_RANGES = 9; + +interface Range { + from: string; + to: string; + display: string; +} + +interface UpdateReduxTime extends OnTimeChangeProps { + id: InputsModelId; + kql?: inputsModel.GlobalKqlQuery | undefined; + timelineId?: string; +} + +interface ReturnUpdateReduxTime { + kqlHaveBeenUpdated: boolean; +} + +export type DispatchUpdateReduxTime = ({ + end, + id, + isQuickSelection, + kql, + start, + timelineId, +}: UpdateReduxTime) => ReturnUpdateReduxTime; + +interface OwnProps { + disabled?: boolean; + id: InputsModelId; + timelineId?: string; +} + +export type SuperDatePickerProps = OwnProps & PropsFromRedux; + +export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>( + ({ + duration, + end, + fromStr, + id, + isLoading, + kind, + kqlQuery, + policy, + queries, + setDuration, + start, + startAutoReload, + stopAutoReload, + timelineId, + toStr, + updateReduxTime, + }) => { + const [isQuickSelection, setIsQuickSelection] = useState(true); + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState<EuiSuperDatePickerRecentRange[]>( + [] + ); + const onRefresh = useCallback( + ({ start: newStart, end: newEnd }: OnRefreshProps): void => { + const { kqlHaveBeenUpdated } = updateReduxTime({ + end: newEnd, + id, + isInvalid: false, + isQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const currentStart = formatDate(newStart); + const currentEnd = isQuickSelection + ? formatDate(newEnd, { roundUp: true }) + : formatDate(newEnd); + if ( + !kqlHaveBeenUpdated && + (!isQuickSelection || (start === currentStart && end === currentEnd)) + ) { + refetchQuery(queries); + } + }, + [end, id, isQuickSelection, kqlQuery, start, timelineId] + ); + + const onRefreshChange = useCallback( + ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { + if (duration !== refreshInterval) { + setDuration({ id, duration: refreshInterval }); + } + + if (isPaused && policy === 'interval') { + stopAutoReload({ id }); + } else if (!isPaused && policy === 'manual') { + startAutoReload({ id }); + } + + if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { + refetchQuery(queries); + } + }, + [id, isQuickSelection, duration, policy, toStr] + ); + + const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { + newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const onTimeChange = useCallback( + ({ + start: newStart, + end: newEnd, + isQuickSelection: newIsQuickSelection, + isInvalid, + }: OnTimeChangeProps) => { + if (!isInvalid) { + updateReduxTime({ + end: newEnd, + id, + isInvalid, + isQuickSelection: newIsQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const newRecentlyUsedRanges = [ + { start: newStart, end: newEnd }, + ...take( + MAX_RECENTLY_USED_RANGES, + recentlyUsedRanges.filter( + recentlyUsedRange => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + ), + ]; + + setRecentlyUsedRanges(newRecentlyUsedRanges); + setIsQuickSelection(newIsQuickSelection); + } + }, + [recentlyUsedRanges, kqlQuery] + ); + + const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); + const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + + const [quickRanges] = useUiSetting$<Range[]>(DEFAULT_TIMEPICKER_QUICK_RANGES); + const commonlyUsedRanges = isEmpty(quickRanges) + ? [] + : quickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); + + return ( + <EuiSuperDatePicker + commonlyUsedRanges={commonlyUsedRanges} + end={endDate} + isLoading={isLoading} + isPaused={policy === 'manual'} + onRefresh={onRefresh} + onRefreshChange={onRefreshChange} + onTimeChange={onTimeChange} + recentlyUsedRanges={recentlyUsedRanges} + refreshInterval={duration} + showUpdateButton={true} + start={startDate} + /> + ); + } +); + +export const formatDate = ( + date: string, + options?: { + roundUp?: boolean; + } +) => { + const momentDate = dateMath.parse(date, options); + return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; +}; + +export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ + end, + id, + isQuickSelection, + kql, + start, + timelineId, +}: UpdateReduxTime): ReturnUpdateReduxTime => { + const fromDate = formatDate(start); + let toDate = formatDate(end, { roundUp: true }); + if (isQuickSelection) { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } else { + toDate = formatDate(end); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + id, + from: formatDate(start), + to: formatDate(end), + }) + ); + } + if (timelineId != null) { + dispatch( + timelineActions.updateRange({ + id: timelineId, + start: fromDate, + end: toDate, + }) + ); + } + if (kql) { + return { + kqlHaveBeenUpdated: kql.refetch(dispatch), + }; + } + + return { + kqlHaveBeenUpdated: false, + }; +}; + +export const makeMapStateToProps = () => { + const getDurationSelector = durationSelector(); + const getEndSelector = endSelector(); + const getFromStrSelector = fromStrSelector(); + const getIsLoadingSelector = isLoadingSelector(); + const getKindSelector = kindSelector(); + const getKqlQuerySelector = kqlQuerySelector(); + const getPolicySelector = policySelector(); + const getQueriesSelector = queriesSelector(); + const getStartSelector = startSelector(); + const getToStrSelector = toStrSelector(); + return (state: State, { id }: OwnProps) => { + const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); + return { + duration: getDurationSelector(inputsRange), + end: getEndSelector(inputsRange), + fromStr: getFromStrSelector(inputsRange), + isLoading: getIsLoadingSelector(inputsRange), + kind: getKindSelector(inputsRange), + kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery, + policy: getPolicySelector(inputsRange), + queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[], + start: getStartSelector(inputsRange), + toStr: getToStrSelector(inputsRange), + }; + }; +}; + +SuperDatePickerComponent.displayName = 'SuperDatePickerComponent'; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + startAutoReload: ({ id }: { id: InputsModelId }) => + dispatch(inputsActions.startAutoReload({ id })), + stopAutoReload: ({ id }: { id: InputsModelId }) => dispatch(inputsActions.stopAutoReload({ id })), + setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => + dispatch(inputsActions.setDuration({ id, duration })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const SuperDatePicker = connector(SuperDatePickerComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts b/x-pack/plugins/siem/public/components/super_date_picker/selectors.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.test.ts rename to x-pack/plugins/siem/public/components/super_date_picker/selectors.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts b/x-pack/plugins/siem/public/components/super_date_picker/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/super_date_picker/selectors.ts rename to x-pack/plugins/siem/public/components/super_date_picker/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap b/x-pack/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap rename to x-pack/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/plugins/siem/public/components/tables/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx rename to x-pack/plugins/siem/public/components/tables/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx b/x-pack/plugins/siem/public/components/tables/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/tables/helpers.tsx rename to x-pack/plugins/siem/public/components/tables/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx b/x-pack/plugins/siem/public/components/timeline/auto_save_warning/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx rename to x-pack/plugins/siem/public/components/timeline/auto_save_warning/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/translations.ts b/x-pack/plugins/siem/public/components/timeline/auto_save_warning/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/translations.ts rename to x-pack/plugins/siem/public/components/timeline/auto_save_warning/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/actions/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/actions/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/actions/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_headers/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_id.ts b/x-pack/plugins/siem/public/components/timeline/body/column_id.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/column_id.ts rename to x-pack/plugins/siem/public/components/timeline/body/column_id.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/constants.ts b/x-pack/plugins/siem/public/components/timeline/body/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/constants.ts rename to x-pack/plugins/siem/public/components/timeline/body/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/siem/public/components/timeline/body/events/event_column_view.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/events/event_column_view.tsx rename to x-pack/plugins/siem/public/components/timeline/body/events/event_column_view.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/events/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/events/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/events/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/siem/public/components/timeline/body/events/stateful_event.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/events/stateful_event.tsx rename to x-pack/plugins/siem/public/components/timeline/body/events/stateful_event.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts b/x-pack/plugins/siem/public/components/timeline/body/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/index.tsx rename to x-pack/plugins/siem/public/components/timeline/body/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts b/x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts b/x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts rename to x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/args.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/args.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/constants.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/constants.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/constants.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/index.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parse_value.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index 24c52f3372d62..66c559729cccd 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -28,9 +28,9 @@ const SignatureFlexItem = styled(EuiFlexItem)` SignatureFlexItem.displayName = 'SignatureFlexItem'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` vertical-align: top; -`; +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/system/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx rename to x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx index 39c21c4ffa33b..4cb8140e22cef 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -19,9 +19,9 @@ import { IS_OPERATOR } from '../../../data_providers/data_provider'; import * as i18n from './translations'; -const Badge = styled(EuiBadge)` +const Badge = (styled(EuiBadge)` vertical-align: top; -`; +` as unknown) as typeof EuiBadge; Badge.displayName = 'Badge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/index.ts b/x-pack/plugins/siem/public/components/timeline/body/sort/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/sort/index.ts rename to x-pack/plugins/siem/public/components/timeline/body/sort/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx rename to x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/stateful_body.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.test.tsx rename to x-pack/plugins/siem/public/components/timeline/body/stateful_body.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/plugins/siem/public/components/timeline/body/stateful_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/stateful_body.tsx rename to x-pack/plugins/siem/public/components/timeline/body/stateful_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts b/x-pack/plugins/siem/public/components/timeline/body/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/body/translations.ts rename to x-pack/plugins/siem/public/components/timeline/body/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_provider.ts b/x-pack/plugins/siem/public/components/timeline/data_providers/data_provider.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_provider.ts rename to x-pack/plugins/siem/public/components/timeline/data_providers/data_provider.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/empty.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.test.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/empty.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx index 56639f90c1464..60c868f780ff3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx @@ -21,12 +21,12 @@ const Text = styled(EuiText)` Text.displayName = 'Text'; -const BadgeHighlighted = styled(EuiBadge)` +const BadgeHighlighted = (styled(EuiBadge)` height: 20px; margin: 0 5px 0 5px; maxwidth: 85px; minwidth: 85px; -`; +` as unknown) as typeof EuiBadge; BadgeHighlighted.displayName = 'BadgeHighlighted'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/index.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/mock/mock_data_providers.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/mock/mock_data_providers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/mock/mock_data_providers.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/mock/mock_data_providers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.test.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.test.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/provider.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/provider.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx index 8e4665acc2c26..e04aed17c6d67 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx @@ -18,7 +18,7 @@ import { EXISTS_OPERATOR, QueryOperator } from './data_provider'; import * as i18n from './translations'; -const ProviderBadgeStyled = styled(EuiBadge)` +const ProviderBadgeStyled = (styled(EuiBadge)` .euiToolTipAnchor { &::after { font-style: normal; @@ -44,7 +44,7 @@ const ProviderBadgeStyled = styled(EuiBadge)` margin-right: 0; margin-left: 4px; } -`; +` as unknown) as typeof EuiBadge; ProviderBadgeStyled.displayName = 'ProviderBadgeStyled'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx index 663b3dd501341..3a691d2bbc621 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -54,9 +54,9 @@ const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>` DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; -const NumberProviderAndBadge = styled(EuiBadge)` +const NumberProviderAndBadge = (styled(EuiBadge)` margin: 0px 5px; -`; +` as unknown) as typeof EuiBadge; NumberProviderAndBadge.displayName = 'NumberProviderAndBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/providers.test.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/providers.test.tsx index a4de8ffa3b9c5..0c8a6932adf91 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/siem/public/components/timeline/data_providers/providers.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; import { TestProviders } from '../../../mock/test_providers'; import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; -import { FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { TimelineContext } from '../timeline_context'; import { mockDataProviders } from './mock/mock_data_providers'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/providers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.tsx rename to x-pack/plugins/siem/public/components/timeline/data_providers/providers.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts b/x-pack/plugins/siem/public/components/timeline/data_providers/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/data_providers/translations.ts rename to x-pack/plugins/siem/public/components/timeline/data_providers/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/events.ts b/x-pack/plugins/siem/public/components/timeline/events.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/events.ts rename to x-pack/plugins/siem/public/components/timeline/events.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx b/x-pack/plugins/siem/public/components/timeline/expandable_event/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/index.tsx rename to x-pack/plugins/siem/public/components/timeline/expandable_event/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/translations.tsx b/x-pack/plugins/siem/public/components/timeline/expandable_event/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/expandable_event/translations.tsx rename to x-pack/plugins/siem/public/components/timeline/expandable_event/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx b/x-pack/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx rename to x-pack/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/footer/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/footer/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/plugins/siem/public/components/timeline/footer/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/index.tsx rename to x-pack/plugins/siem/public/components/timeline/footer/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx b/x-pack/plugins/siem/public/components/timeline/footer/last_updated.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/last_updated.tsx rename to x-pack/plugins/siem/public/components/timeline/footer/last_updated.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/mock.ts b/x-pack/plugins/siem/public/components/timeline/footer/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/mock.ts rename to x-pack/plugins/siem/public/components/timeline/footer/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/footer/translations.ts b/x-pack/plugins/siem/public/components/timeline/footer/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/footer/translations.ts rename to x-pack/plugins/siem/public/components/timeline/footer/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx new file mode 100644 index 0000000000000..6f2053488f69b --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx @@ -0,0 +1,97 @@ +/* + * 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 { shallow } from 'enzyme'; +import React from 'react'; + +import { mockIndexPattern } from '../../../mock'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; +import { TestProviders } from '../../../mock/test_providers'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; + +import { TimelineHeader } from '.'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +jest.mock('../../../lib/kibana'); + +describe('Header', () => { + const indexPattern = mockIndexPattern; + const mount = useMountAppended(); + + describe('rendering', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + <TimelineHeader + browserFields={{}} + dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + id="foo" + indexPattern={indexPattern} + onChangeDataProviderKqlQuery={jest.fn()} + onChangeDroppableAndProvider={jest.fn()} + onDataProviderEdited={jest.fn()} + onDataProviderRemoved={jest.fn()} + onToggleDataProviderEnabled={jest.fn()} + onToggleDataProviderExcluded={jest.fn()} + show={true} + showCallOutUnauthorizedMsg={false} + /> + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the data providers', () => { + const wrapper = mount( + <TestProviders> + <TimelineHeader + browserFields={{}} + dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + id="foo" + indexPattern={indexPattern} + onChangeDataProviderKqlQuery={jest.fn()} + onChangeDroppableAndProvider={jest.fn()} + onDataProviderEdited={jest.fn()} + onDataProviderRemoved={jest.fn()} + onToggleDataProviderEnabled={jest.fn()} + onToggleDataProviderExcluded={jest.fn()} + show={true} + showCallOutUnauthorizedMsg={false} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); + }); + + test('it renders the unauthorized call out providers', () => { + const wrapper = mount( + <TestProviders> + <TimelineHeader + browserFields={{}} + dataProviders={mockDataProviders} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + id="foo" + indexPattern={indexPattern} + onChangeDataProviderKqlQuery={jest.fn()} + onChangeDroppableAndProvider={jest.fn()} + onDataProviderEdited={jest.fn()} + onDataProviderRemoved={jest.fn()} + onToggleDataProviderEnabled={jest.fn()} + onToggleDataProviderExcluded={jest.fn()} + show={true} + showCallOutUnauthorizedMsg={true} + /> + </TestProviders> + ); + + expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/plugins/siem/public/components/timeline/header/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx rename to x-pack/plugins/siem/public/components/timeline/header/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/translations.ts b/x-pack/plugins/siem/public/components/timeline/header/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/header/translations.ts rename to x-pack/plugins/siem/public/components/timeline/header/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx new file mode 100644 index 0000000000000..fc5a8ae924f82 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx @@ -0,0 +1,398 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import { mockIndexPattern } from '../../mock'; + +import { mockDataProviders } from './data_providers/mock/mock_data_providers'; +import { buildGlobalQuery, combineQueries } from './helpers'; +import { mockBrowserFields } from '../../containers/source/mock'; +import { EsQueryConfig, Filter, esFilters } from '../../../../../../src/plugins/data/public'; + +const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); +const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); +const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + +describe('Build KQL Query', () => { + test('Build KQL query with one data provider', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); + }); + + test('Build KQL query with one data provider as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Buld KQL query with one data provider as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Buld KQL query with one data provider as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider', () => { + const dataProviders = mockDataProviders.slice(0, 2); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); + }); + + test('Build KQL query with one data provider and one and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = mockDataProviders.slice(1, 2); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); + }); + + test('Build KQL query with one data provider and one and as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider and multiple and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' + ); + }); +}); + +describe('Combined Queries', () => { + const config: EsQueryConfig = { + allowLeadingWildcards: true, + queryStringOptions: {}, + ignoreFilterIfFieldNotInIndex: true, + dateFormatTZ: 'America/New_York', + }; + test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + }) + ).toBeNull(); + }); + + test('No Data Provider & No kqlQuery & isEventViewer is true', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + }); + }); + + test('No Data Provider & No kqlQuery & with Filters', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: 'host.name' }, + } as Filter, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + }); + }); + + test('Only Data Provider', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with a date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only KQL search/filter query', () => { + const { filterQuery } = combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL filter query', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL filter query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); +}); diff --git a/x-pack/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/helpers.tsx new file mode 100644 index 0000000000000..53ab7d81cadc2 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/helpers.tsx @@ -0,0 +1,160 @@ +/* + * 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 { isEmpty, isNumber, get } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; + +import { escapeQueryValue, convertToBuildEsQuery } from '../../lib/keury'; + +import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider'; +import { BrowserFields } from '../../containers/source'; +import { + IIndexPattern, + Query, + EsQueryConfig, + Filter, +} from '../../../../../../src/plugins/data/public'; + +const convertDateFieldToQuery = (field: string, value: string | number) => + `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; + +const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { + const baseFields = get('base', browserFields); + if (baseFields != null && baseFields.fields != null) { + return Object.keys(baseFields.fields); + } + return []; +}); + +const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { + const splitFields = field.split('.'); + const baseFields = getBaseFields(browserFields); + if (baseFields.includes(field)) { + return ['base', 'fields', field]; + } + return [splitFields[0], 'fields', field]; +}; + +const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { + const pathBrowserField = getBrowserFieldPath(field, browserFields); + const browserField = get(pathBrowserField, browserFields); + if (browserField != null && browserField.type === 'date') { + return true; + } + return false; +}; + +const buildQueryMatch = ( + dataProvider: DataProvider | DataProvidersAnd, + browserFields: BrowserFields +) => + `${dataProvider.excluded ? 'NOT ' : ''}${ + dataProvider.queryMatch.operator !== EXISTS_OPERATOR + ? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) + ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) + : `${dataProvider.queryMatch.field} : ${ + isNumber(dataProvider.queryMatch.value) + ? dataProvider.queryMatch.value + : escapeQueryValue(dataProvider.queryMatch.value) + }` + : `${dataProvider.queryMatch.field} ${EXISTS_OPERATOR}` + }`.trim(); + +const buildQueryForAndProvider = ( + dataAndProviders: DataProvidersAnd[], + browserFields: BrowserFields +) => + dataAndProviders + .reduce((andQuery, andDataProvider) => { + const prepend = (q: string) => `${q !== '' ? `${q} and ` : ''}`; + return andDataProvider.enabled + ? `${prepend(andQuery)} ${buildQueryMatch(andDataProvider, browserFields)}` + : andQuery; + }, '') + .trim(); + +export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => + dataProviders + .reduce((query, dataProvider: DataProvider, i) => { + const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; + const openParen = i > 0 ? '(' : ''; + const closeParen = i > 0 ? ')' : ''; + return dataProvider.enabled + ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} + ${ + dataProvider.and.length > 0 + ? ` and ${buildQueryForAndProvider(dataProvider.and, browserFields)}` + : '' + }${closeParen}`.trim() + : query; + }, '') + .trim(); + +export const combineQueries = ({ + config, + dataProviders, + indexPattern, + browserFields, + filters = [], + kqlQuery, + kqlMode, + start, + end, + isEventViewer, +}: { + config: EsQueryConfig; + dataProviders: DataProvider[]; + indexPattern: IIndexPattern; + browserFields: BrowserFields; + filters: Filter[]; + kqlQuery: Query; + kqlMode: string; + start: number; + end: number; + isEventViewer?: boolean; +}): { filterQuery: string } | null => { + const kuery: Query = { query: '', language: kqlQuery.language }; + if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { + return null; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { + kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { + kuery.query = `(${buildGlobalQuery( + dataProviders, + browserFields + )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } + const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; + const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; + kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( + kqlQuery.query as string + )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; +}; + +/** + * The CSS class name of a "stateful event", which appears in both + * the `Timeline` and the `Events Viewer` widget + */ +export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/index.tsx rename to x-pack/plugins/siem/public/components/timeline/index.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx new file mode 100644 index 0000000000000..c5aea833a4b2f --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +/* eslint-disable @kbn/eslint/module_migration */ +import routeData from 'react-router'; +/* eslint-enable @kbn/eslint/module_migration */ +import { InsertTimelinePopoverComponent } from './'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const reactRedux = jest.requireActual('react-redux'); + return { + ...reactRedux, + useDispatch: () => mockDispatch, + }; +}); +const mockLocation = { + pathname: '/apath', + hash: '', + search: '', + state: '', +}; +const mockLocationWithState = { + ...mockLocation, + state: { + insertTimeline: { + timelineId: 'timeline-id', + timelineSavedObjectId: '34578-3497-5893-47589-34759', + timelineTitle: 'Timeline title', + }, + }, +}; + +const onTimelineChange = jest.fn(); +const defaultProps = { + isDisabled: false, + onTimelineChange, +}; + +describe('Insert timeline popover ', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should insert a timeline when passed in the router state', () => { + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocationWithState); + mount(<InsertTimelinePopoverComponent {...defaultProps} />); + expect(mockDispatch).toBeCalledWith({ + payload: { id: 'timeline-id', show: false }, + type: 'x-pack/siem/local/timeline/SHOW_TIMELINE', + }); + expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); + }); + it('should do nothing when router state', () => { + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + mount(<InsertTimelinePopoverComponent {...defaultProps} />); + expect(mockDispatch).toHaveBeenCalledTimes(0); + expect(onTimelineChange).toHaveBeenCalledTimes(0); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx rename to x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx rename to x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx new file mode 100644 index 0000000000000..4c64c8a100b41 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx @@ -0,0 +1,327 @@ +/* + * 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 { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiModal, + EuiOverlayMask, + EuiToolTip, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; +import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; + +import { Note } from '../../../lib/note'; +import { Notes } from '../../notes'; +import { AssociateNote, UpdateNote } from '../../notes/helpers'; +import { NOTES_PANEL_WIDTH } from './notes_size'; +import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; +import * as i18n from './translations'; +import { SiemPageName } from '../../../pages/home/types'; +import { timelineSelectors } from '../../../store/timeline'; +import { State } from '../../../store'; + +export const historyToolTip = 'The chronological history of actions related to this timeline'; +export const streamLiveToolTip = 'Update the Timeline as new data arrives'; +export const newTimelineToolTip = 'Create a new timeline'; + +const NotesCountBadge = (styled(EuiBadge)` + margin-left: 5px; +` as unknown) as typeof EuiBadge; + +NotesCountBadge.displayName = 'NotesCountBadge'; + +type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; +type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; +type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; +type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; + +export const StarIcon = React.memo<{ + isFavorite: boolean; + timelineId: string; + updateIsFavorite: UpdateIsFavorite; +}>(({ isFavorite, timelineId: id, updateIsFavorite }) => ( + // TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener + // TODO: 2 error is: Elements with the 'button' interactive role must be focusable + // TODO: Investigate this error + // eslint-disable-next-line + <div role="button" onClick={() => updateIsFavorite({ id, isFavorite: !isFavorite })}> + {isFavorite ? ( + <EuiToolTip data-test-subj="timeline-favorite-filled-star-tool-tip" content={i18n.FAVORITE}> + <StyledStar data-test-subj="timeline-favorite-filled-star" type="starFilled" size="l" /> + </EuiToolTip> + ) : ( + <EuiToolTip content={i18n.NOT_A_FAVORITE}> + <StyledStar data-test-subj="timeline-favorite-empty-star" type="starEmpty" size="l" /> + </EuiToolTip> + )} + </div> +)); +StarIcon.displayName = 'StarIcon'; + +interface DescriptionProps { + description: string; + timelineId: string; + updateDescription: UpdateDescription; +} + +export const Description = React.memo<DescriptionProps>( + ({ description, timelineId, updateDescription }) => ( + <EuiToolTip data-test-subj="timeline-description-tool-tip" content={i18n.DESCRIPTION_TOOL_TIP}> + <DescriptionContainer data-test-subj="description-container"> + <EuiFieldText + aria-label={i18n.TIMELINE_DESCRIPTION} + data-test-subj="timeline-description" + fullWidth={true} + onChange={e => updateDescription({ id: timelineId, description: e.target.value })} + placeholder={i18n.DESCRIPTION} + spellCheck={true} + value={description} + /> + </DescriptionContainer> + </EuiToolTip> + ) +); +Description.displayName = 'Description'; + +interface NameProps { + timelineId: string; + title: string; + updateTitle: UpdateTitle; +} + +export const Name = React.memo<NameProps>(({ timelineId, title, updateTitle }) => ( + <EuiToolTip data-test-subj="timeline-title-tool-tip" content={i18n.TITLE}> + <NameField + aria-label={i18n.TIMELINE_TITLE} + data-test-subj="timeline-title" + onChange={e => updateTitle({ id: timelineId, title: e.target.value })} + placeholder={i18n.UNTITLED_TIMELINE} + spellCheck={true} + value={title} + /> + </EuiToolTip> +)); +Name.displayName = 'Name'; + +interface NewCaseProps { + onClosePopover: () => void; + timelineId: string; + timelineTitle: string; +} + +export const NewCase = React.memo<NewCaseProps>(({ onClosePopover, timelineId, timelineTitle }) => { + const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); + const handleClick = useCallback(() => { + onClosePopover(); + history.push({ + pathname: `/${SiemPageName.case}/create`, + state: { + insertTimeline: { + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }, + }, + }); + }, [onClosePopover, history, timelineId, timelineTitle]); + + return ( + <EuiButtonEmpty + data-test-subj="attach-timeline-case" + color="text" + iconSide="left" + iconType="paperClip" + onClick={handleClick} + > + {i18n.ATTACH_TIMELINE_TO_NEW_CASE} + </EuiButtonEmpty> + ); +}); +NewCase.displayName = 'NewCase'; + +interface NewTimelineProps { + createTimeline: CreateTimeline; + onClosePopover: () => void; + timelineId: string; +} + +export const NewTimeline = React.memo<NewTimelineProps>( + ({ createTimeline, onClosePopover, timelineId }) => { + const handleClick = useCallback(() => { + createTimeline({ id: timelineId, show: true }); + onClosePopover(); + }, [createTimeline, timelineId, onClosePopover]); + + return ( + <EuiButtonEmpty + data-test-subj="timeline-new" + color="text" + iconSide="left" + iconType="plusInCircle" + onClick={handleClick} + > + {i18n.NEW_TIMELINE} + </EuiButtonEmpty> + ); + } +); +NewTimeline.displayName = 'NewTimeline'; + +interface NotesButtonProps { + animate?: boolean; + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + noteIds: string[]; + size: 's' | 'l'; + showNotes: boolean; + toggleShowNotes: () => void; + text?: string; + toolTip?: string; + updateNote: UpdateNote; +} + +const getNewNoteId = (): string => uuid.v4(); + +interface LargeNotesButtonProps { + noteIds: string[]; + text?: string; + toggleShowNotes: () => void; +} + +const LargeNotesButton = React.memo<LargeNotesButtonProps>(({ noteIds, text, toggleShowNotes }) => ( + <EuiButton + data-test-subj="timeline-notes-button-large" + onClick={() => toggleShowNotes()} + size="m" + > + <EuiFlexGroup alignItems="center" gutterSize="none" justifyContent="center"> + <EuiFlexItem grow={false}> + <EuiIcon color="subdued" size="m" type="editorComment" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + {text && text.length ? <LabelText>{text}</LabelText> : null} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <NotesCountBadge data-test-subj="timeline-notes-count" color="hollow"> + {noteIds.length} + </NotesCountBadge> + </EuiFlexItem> + </EuiFlexGroup> + </EuiButton> +)); +LargeNotesButton.displayName = 'LargeNotesButton'; + +interface SmallNotesButtonProps { + noteIds: string[]; + toggleShowNotes: () => void; +} + +const SmallNotesButton = React.memo<SmallNotesButtonProps>(({ noteIds, toggleShowNotes }) => ( + <EuiButtonIcon + aria-label={i18n.NOTES} + data-test-subj="timeline-notes-button-small" + iconType="editorComment" + onClick={() => toggleShowNotes()} + /> +)); +SmallNotesButton.displayName = 'SmallNotesButton'; + +/** + * The internal implementation of the `NotesButton` + */ +const NotesButtonComponent = React.memo<NotesButtonProps>( + ({ + animate = true, + associateNote, + getNotesByIds, + noteIds, + showNotes, + size, + toggleShowNotes, + text, + updateNote, + }) => ( + <ButtonContainer animate={animate} data-test-subj="timeline-notes-button-container"> + <> + {size === 'l' ? ( + <LargeNotesButton noteIds={noteIds} text={text} toggleShowNotes={toggleShowNotes} /> + ) : ( + <SmallNotesButton noteIds={noteIds} toggleShowNotes={toggleShowNotes} /> + )} + {size === 'l' && showNotes ? ( + <EuiOverlayMask> + <EuiModal maxWidth={NOTES_PANEL_WIDTH} onClose={toggleShowNotes}> + <Notes + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + getNewNoteId={getNewNoteId} + updateNote={updateNote} + /> + </EuiModal> + </EuiOverlayMask> + ) : null} + </> + </ButtonContainer> + ) +); +NotesButtonComponent.displayName = 'NotesButtonComponent'; + +export const NotesButton = React.memo<NotesButtonProps>( + ({ + animate = true, + associateNote, + getNotesByIds, + noteIds, + showNotes, + size, + toggleShowNotes, + toolTip, + text, + updateNote, + }) => + showNotes ? ( + <NotesButtonComponent + animate={animate} + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + showNotes={showNotes} + size={size} + toggleShowNotes={toggleShowNotes} + text={text} + updateNote={updateNote} + /> + ) : ( + <EuiToolTip content={toolTip || ''} data-test-subj="timeline-notes-tool-tip"> + <NotesButtonComponent + animate={animate} + associateNote={associateNote} + getNotesByIds={getNotesByIds} + noteIds={noteIds} + showNotes={showNotes} + size={size} + toggleShowNotes={toggleShowNotes} + text={text} + updateNote={updateNote} + /> + </EuiToolTip> + ) +); +NotesButton.displayName = 'NotesButton'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/properties/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/index.test.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/plugins/siem/public/components/timeline/properties/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/index.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/notes_size.ts b/x-pack/plugins/siem/public/components/timeline/properties/notes_size.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/notes_size.ts rename to x-pack/plugins/siem/public/components/timeline/properties/notes_size.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx b/x-pack/plugins/siem/public/components/timeline/properties/properties_left.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_left.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/properties_left.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx b/x-pack/plugins/siem/public/components/timeline/properties/properties_right.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/properties_right.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/properties_right.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/plugins/siem/public/components/timeline/properties/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/styles.tsx rename to x-pack/plugins/siem/public/components/timeline/properties/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/properties/translations.ts b/x-pack/plugins/siem/public/components/timeline/properties/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/properties/translations.ts rename to x-pack/plugins/siem/public/components/timeline/properties/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx new file mode 100644 index 0000000000000..a78e5b8e1d226 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx @@ -0,0 +1,409 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; +import { mockBrowserFields } from '../../../containers/source/mock'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { mockIndexPattern, TestProviders } from '../../../mock'; +import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; +import { QueryBar } from '../../query_bar'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { buildGlobalQuery } from '../helpers'; + +import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +jest.mock('../../../lib/kibana'); + +describe('Timeline QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/<Provider> does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockApplyKqlFilterQuery = jest.fn(); + const mockSetFilters = jest.fn(); + const mockSetKqlFilterQueryDraft = jest.fn(); + const mockSetSavedQueryId = jest.fn(); + const mockUpdateReduxTime = jest.fn(); + + beforeEach(() => { + mockApplyKqlFilterQuery.mockClear(); + mockSetFilters.mockClear(); + mockSetKqlFilterQueryDraft.mockClear(); + mockSetSavedQueryId.mockClear(); + mockUpdateReduxTime.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + <TestProviders> + <QueryBarTimeline + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + </TestProviders> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + + expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); + expect(queryBarProps.dateRangeTo).toEqual('now'); + expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); + expect(queryBarProps.savedQuery).toEqual(null); + }); + + describe('#onChangeQuery', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSubmitQuery', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ timelineId: 'new-timeline' }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSavedQuery', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + + test('is only reference that changed when savedQueryId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + <TestProviders> + <QueryBarTimeline {...props} /> + </TestProviders> + ); + + const wrapper = mount( + <Proxy + applyKqlFilterQuery={mockApplyKqlFilterQuery} + browserFields={mockBrowserFields} + dataProviders={mockDataProviders} + filters={[]} + filterManager={new FilterManager(mockUiSettingsForFilterManager)} + filterQuery={{ expression: 'here: query', kind: 'kuery' }} + filterQueryDraft={{ expression: 'here: query', kind: 'kuery' }} + from={0} + fromStr={DEFAULT_FROM} + to={1} + toStr={DEFAULT_TO} + kqlMode="search" + indexPattern={mockIndexPattern} + isRefreshPaused={true} + refreshInterval={3000} + savedQueryId={null} + setFilters={mockSetFilters} + setKqlFilterQueryDraft={mockSetKqlFilterQueryDraft} + setSavedQueryId={mockSetSavedQueryId} + timelineId="timeline-real-id" + updateReduxTime={mockUpdateReduxTime} + /> + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ + savedQueryId: 'new', + }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + }); + + describe('#getDataProviderFilter', () => { + test('returns valid data provider filter with a simple bool data provider', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + name: 'Provider 1', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', + }, + }); + }); + + test('returns valid data provider filter with an exists operator', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery( + [ + { + id: `id-exists`, + name, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: '', + operator: ':*', + }, + and: [], + }, + ], + mockBrowserFields + ), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx new file mode 100644 index 0000000000000..7d2b4f71183dd --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx @@ -0,0 +1,319 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useState, useEffect } from 'react'; +import { Subscription } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; + +import { + IIndexPattern, + Query, + Filter, + esFilters, + FilterManager, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../../../../src/plugins/data/public'; + +import { BrowserFields } from '../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; +import { KqlMode } from '../../../store/timeline/model'; +import { useSavedQueryServices } from '../../../utils/saved_query_services'; +import { DispatchUpdateReduxTime } from '../../super_date_picker'; +import { QueryBar } from '../../query_bar'; +import { DataProvider } from '../data_providers/data_provider'; +import { buildGlobalQuery } from '../helpers'; + +export interface QueryBarTimelineComponentProps { + applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filters: Filter[]; + filterManager: FilterManager; + filterQuery: KueryFilterQuery; + filterQueryDraft: KueryFilterQuery; + from: number; + fromStr: string; + kqlMode: KqlMode; + indexPattern: IIndexPattern; + isRefreshPaused: boolean; + refreshInterval: number; + savedQueryId: string | null; + setFilters: (filters: Filter[]) => void; + setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; + setSavedQueryId: (savedQueryId: string | null) => void; + timelineId: string; + to: number; + toStr: string; + updateReduxTime: DispatchUpdateReduxTime; +} + +const timelineFilterDropArea = 'timeline-filter-drop-area'; + +export const QueryBarTimeline = memo<QueryBarTimelineComponentProps>( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterManager, + filterQuery, + filterQueryDraft, + from, + fromStr, + kqlMode, + indexPattern, + isRefreshPaused, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + refreshInterval, + timelineId, + to, + toStr, + updateReduxTime, + }) => { + const [dateRangeFrom, setDateRangeFrom] = useState<string>( + fromStr != null ? fromStr : new Date(from).toISOString() + ); + const [dateRangeTo, setDateRangTo] = useState<string>( + toStr != null ? toStr : new Date(to).toISOString() + ); + + const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); + const [filterQueryConverted, setFilterQueryConverted] = useState<Query>({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + const [queryBarFilters, setQueryBarFilters] = useState<Filter[]>([]); + const [dataProvidersDsl, setDataProvidersDsl] = useState<string>( + convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) + ); + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters(filters); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + setFilters(filterWithoutDropArea); + setQueryBarFilters(filterWithoutDropArea); + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, []); + + useEffect(() => { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + if (!deepEqual(filters, filterWithoutDropArea)) { + filterManager.setFilters(filters); + } + }, [filters]); + + useEffect(() => { + setFilterQueryConverted({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + }, [filterQuery]); + + useEffect(() => { + setDataProvidersDsl( + convertKueryToElasticSearchQuery( + buildGlobalQuery(dataProviders, browserFields), + indexPattern + ) + ); + }, [dataProviders, browserFields, indexPattern]); + + useEffect(() => { + if (fromStr != null && toStr != null) { + setDateRangeFrom(fromStr); + setDateRangTo(toStr); + } else if (from != null && to != null) { + setDateRangeFrom(new Date(from).toISOString()); + setDateRangTo(new Date(to).toISOString()); + } + }, [from, fromStr, to, toStr]); + + useEffect(() => { + let isSubscribed = true; + async function setSavedQueryByServices() { + if (savedQueryId != null && savedQueryServices != null) { + try { + // The getSavedQuery function will throw a promise rejection in + // src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts + // if the savedObjectsClient is undefined. This is happening in a test + // so I wrapped this in a try catch to keep the unhandled promise rejection + // warning from appearing in tests. + const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery({ + ...mySavedQuery, + attributes: { + ...mySavedQuery.attributes, + filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), + }, + }); + } + } catch (exc) { + setSavedQuery(null); + } + } else if (isSubscribed) { + setSavedQuery(null); + } + } + setSavedQueryByServices(); + return () => { + isSubscribed = false; + }; + }, [savedQueryId]); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + if ( + filterQueryDraft == null || + (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || + filterQueryDraft.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + } + }, + [filterQueryDraft] + ); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + if ( + filterQuery == null || + (filterQuery != null && filterQuery.expression !== newQuery.query) || + filterQuery.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); + } + if (timefilter != null) { + const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); + + updateReduxTime({ + id: 'timeline', + end: timefilter.to, + start: timefilter.from, + isInvalid: false, + isQuickSelection, + timelineId, + }); + } + }, + [filterQuery, timelineId] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + if (newSavedQuery.id !== savedQueryId) { + setSavedQueryId(newSavedQuery.id); + } + if (savedQueryServices != null && dataProvidersDsl !== '') { + const dataProviderFilterExists = + newSavedQuery.attributes.filters != null + ? newSavedQuery.attributes.filters.findIndex( + f => f.meta.controlledBy === timelineFilterDropArea + ) + : -1; + savedQueryServices.saveQuery( + { + ...newSavedQuery.attributes, + filters: + newSavedQuery.attributes.filters != null + ? dataProviderFilterExists > -1 + ? [ + ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), + getDataProviderFilter(dataProvidersDsl), + ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), + ] + : [ + ...newSavedQuery.attributes.filters, + getDataProviderFilter(dataProvidersDsl), + ] + : [], + }, + { + overwrite: true, + } + ); + } + } else { + setSavedQueryId(null); + } + }, + [dataProvidersDsl, savedQueryId, savedQueryServices] + ); + + return ( + <QueryBar + dateRangeFrom={dateRangeFrom} + dateRangeTo={dateRangeTo} + hideSavedQuery={kqlMode === 'search'} + indexPattern={indexPattern} + isRefreshPaused={isRefreshPaused} + filterQuery={filterQueryConverted} + filterManager={filterManager} + filters={queryBarFilters} + onChangedQuery={onChangedQuery} + onSubmitQuery={onSubmitQuery} + refreshInterval={refreshInterval} + savedQuery={savedQuery} + onSavedQuery={onSavedQuery} + dataTestSubj={'timelineQueryInput'} + /> + ); + } +); + +export const getDataProviderFilter = (dataProviderDsl: string): Filter => { + const dslObject = JSON.parse(dataProviderDsl); + const key = Object.keys(dslObject); + return { + ...dslObject, + meta: { + alias: timelineFilterDropArea, + controlledBy: timelineFilterDropArea, + negate: false, + disabled: false, + type: 'custom', + key: isEmpty(key) ? 'bool' : key[0], + value: dataProviderDsl, + }, + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx b/x-pack/plugins/siem/public/components/timeline/refetch_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/refetch_timeline.tsx rename to x-pack/plugins/siem/public/components/timeline/refetch_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx new file mode 100644 index 0000000000000..fa92ef9ce5965 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx @@ -0,0 +1,239 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; +import deepEqual from 'fast-deep-equal'; + +import { Filter, FilterManager, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; +import { + KueryFilterQuery, + SerializedFilterQuery, + State, + timelineSelectors, + inputsModel, + inputsSelectors, +} from '../../../store'; +import { timelineActions } from '../../../store/actions'; +import { KqlMode, TimelineModel, EventType } from '../../../store/timeline/model'; +import { timelineDefaults } from '../../../store/timeline/defaults'; +import { dispatchUpdateReduxTime } from '../../super_date_picker'; +import { SearchOrFilter } from './search_or_filter'; + +interface OwnProps { + browserFields: BrowserFields; + filterManager: FilterManager; + indexPattern: IIndexPattern; + timelineId: string; +} + +type Props = OwnProps & PropsFromRedux; + +const StatefulSearchOrFilterComponent = React.memo<Props>( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + eventType, + filters, + filterManager, + filterQuery, + filterQueryDraft, + from, + fromStr, + indexPattern, + isRefreshPaused, + kqlMode, + refreshInterval, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + timelineId, + to, + toStr, + updateEventType, + updateKqlMode, + updateReduxTime, + }) => { + const applyFilterQueryFromKueryExpression = useCallback( + (expression: string, kind) => + applyKqlFilterQuery({ + id: timelineId, + filterQuery: { + kuery: { + kind, + expression, + }, + serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), + }, + }), + [indexPattern, timelineId] + ); + + const setFilterQueryDraftFromKueryExpression = useCallback( + (expression: string, kind) => + setKqlFilterQueryDraft({ + id: timelineId, + filterQueryDraft: { + kind, + expression, + }, + }), + [timelineId] + ); + + const setFiltersInTimeline = useCallback( + (newFilters: Filter[]) => + setFilters({ + id: timelineId, + filters: newFilters, + }), + [timelineId] + ); + + const setSavedQueryInTimeline = useCallback( + (newSavedQueryId: string | null) => + setSavedQueryId({ + id: timelineId, + savedQueryId: newSavedQueryId, + }), + [timelineId] + ); + + const handleUpdateEventType = useCallback( + (newEventType: EventType) => + updateEventType({ + id: timelineId, + eventType: newEventType, + }), + [timelineId] + ); + + return ( + <SearchOrFilter + applyKqlFilterQuery={applyFilterQueryFromKueryExpression} + browserFields={browserFields} + dataProviders={dataProviders} + eventType={eventType} + filters={filters} + filterManager={filterManager} + filterQuery={filterQuery} + filterQueryDraft={filterQueryDraft} + from={from} + fromStr={fromStr} + indexPattern={indexPattern} + isRefreshPaused={isRefreshPaused} + kqlMode={kqlMode!} + refreshInterval={refreshInterval} + savedQueryId={savedQueryId} + setFilters={setFiltersInTimeline} + setKqlFilterQueryDraft={setFilterQueryDraftFromKueryExpression!} + setSavedQueryId={setSavedQueryInTimeline} + timelineId={timelineId} + to={to} + toStr={toStr} + updateEventType={handleUpdateEventType} + updateKqlMode={updateKqlMode!} + updateReduxTime={updateReduxTime} + /> + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.eventType === nextProps.eventType && + prevProps.filterManager === nextProps.filterManager && + prevProps.from === nextProps.from && + prevProps.fromStr === nextProps.fromStr && + prevProps.to === nextProps.to && + prevProps.toStr === nextProps.toStr && + prevProps.isRefreshPaused === nextProps.isRefreshPaused && + prevProps.refreshInterval === nextProps.refreshInterval && + prevProps.timelineId === nextProps.timelineId && + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.filterQuery, nextProps.filterQuery) && + deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.kqlMode, nextProps.kqlMode) && + deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && + deepEqual(prevProps.timelineId, nextProps.timelineId) + ); + } +); +StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); + const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; + const input: inputsModel.InputsRange = getInputsTimeline(state); + const policy: inputsModel.Policy = getInputsPolicy(state); + return { + dataProviders: timeline.dataProviders, + eventType: timeline.eventType ?? 'raw', + filterQuery: getKqlFilterQuery(state, timelineId)!, + filterQueryDraft: getKqlFilterQueryDraft(state, timelineId)!, + filters: timeline.filters!, + from: input.timerange.from, + fromStr: input.timerange.fromStr!, + isRefreshPaused: policy.kind === 'manual', + kqlMode: getOr('filter', 'kqlMode', timeline), + refreshInterval: policy.duration, + savedQueryId: getOr(null, 'savedQueryId', timeline), + to: input.timerange.to, + toStr: input.timerange.toStr!, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => + dispatch( + timelineActions.applyKqlFilterQuery({ + id, + filterQuery, + }) + ), + updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => + dispatch(timelineActions.updateEventType({ id, eventType })), + updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => + dispatch(timelineActions.updateKqlMode({ id, kqlMode })), + setKqlFilterQueryDraft: ({ + id, + filterQueryDraft, + }: { + id: string; + filterQueryDraft: KueryFilterQuery; + }) => + dispatch( + timelineActions.setKqlFilterQueryDraft({ + id, + filterQueryDraft, + }) + ), + setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => + dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), + setFilters: ({ id, filters }: { id: string; filters: Filter[] }) => + dispatch(timelineActions.setFilters({ id, filters })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const StatefulSearchOrFilter = connector(StatefulSearchOrFilterComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx index 02a575db259bb..0b8ed71135744 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx @@ -8,11 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/ import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; -import { - Filter, - FilterManager, - IIndexPattern, -} from '../../../../../../../../src/plugins/data/public'; +import { Filter, FilterManager, IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../containers/source'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; import { KqlMode, EventType } from '../../../store/timeline/model'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/siem/public/components/timeline/search_or_filter/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_or_filter/translations.ts rename to x-pack/plugins/siem/public/components/timeline/search_or_filter/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/plugins/siem/public/components/timeline/search_super_select/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx rename to x-pack/plugins/siem/public/components/timeline/search_super_select/index.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx new file mode 100644 index 0000000000000..4cc89e5bdba73 --- /dev/null +++ b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx @@ -0,0 +1,280 @@ +/* + * 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 { + EuiSelectable, + EuiHighlight, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTextColor, + EuiSelectableOption, + EuiPortal, + EuiFilterGroup, + EuiFilterButton, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; +import { ListProps } from 'react-virtualized'; +import styled from 'styled-components'; + +import { useGetAllTimeline } from '../../../containers/timeline/all'; +import { SortFieldTimeline, Direction } from '../../../graphql/types'; +import { isUntitled } from '../../open_timeline/helpers'; +import * as i18nTimeline from '../../open_timeline/translations'; +import { OpenTimelineResult } from '../../open_timeline/types'; +import { getEmptyTagValue } from '../../empty_value'; + +import * as i18n from '../translations'; + +const MyEuiFlexItem = styled(EuiFlexItem)` + display: inline-block; + max-width: 296px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + padding 0px 4px; +`; + +const EuiSelectableContainer = styled.div<{ isLoading: boolean }>` + .euiSelectable { + .euiFormControlLayout__childrenWrapper { + display: flex; + } + ${({ isLoading }) => `${ + isLoading + ? ` + .euiFormControlLayoutIcons { + display: none; + } + .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { + display: block; + left: 12px; + top: 12px; + }` + : '' + } + `} + } +`; + +const ORIGINAL_PAGE_SIZE = 50; +const POPOVER_HEIGHT = 260; +const TIMELINE_ITEM_HEIGHT = 50; + +export interface GetSelectableOptions { + timelines: OpenTimelineResult[]; + onlyFavorites: boolean; + searchTimelineValue: string; +} + +interface SelectableTimelineProps { + hideUntitled?: boolean; + getSelectableOptions: ({ + timelines, + onlyFavorites, + searchTimelineValue, + }: GetSelectableOptions) => EuiSelectableOption[]; + onClosePopover: () => void; + onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; +} + +const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({ + hideUntitled = false, + getSelectableOptions, + onClosePopover, + onTimelineChange, +}) => { + const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); + const [heightTrigger, setHeightTrigger] = useState(0); + const [searchTimelineValue, setSearchTimelineValue] = useState(''); + const [onlyFavorites, setOnlyFavorites] = useState(false); + const [searchRef, setSearchRef] = useState<HTMLElement | null>(null); + const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline(); + + const onSearchTimeline = useCallback(val => { + setSearchTimelineValue(val); + }, []); + + const handleOnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + const handleOnScroll = useCallback( + ( + totalTimelines: number, + totalCount: number, + { + clientHeight, + scrollHeight, + scrollTop, + }: { + clientHeight: number; + scrollHeight: number; + scrollTop: number; + } + ) => { + if (totalTimelines < totalCount) { + const clientHeightTrigger = clientHeight * 1.2; + if ( + scrollTop > 10 && + scrollHeight - scrollTop < clientHeightTrigger && + scrollHeight > heightTrigger + ) { + setHeightTrigger(scrollHeight); + setPageSize(pageSize + ORIGINAL_PAGE_SIZE); + } + } + }, + [heightTrigger, pageSize] + ); + + const renderTimelineOption = useCallback((option, searchValue) => { + return ( + <EuiFlexGroup + gutterSize="s" + justifyContent="spaceBetween" + alignItems="center" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiIcon type={`${option.checked === 'on' ? 'check' : 'none'}`} color="primary" /> + </EuiFlexItem> + <EuiFlexItem grow={true}> + <EuiFlexGroup gutterSize="none" direction="column"> + <MyEuiFlexItem data-test-subj="timeline" grow={false}> + <EuiHighlight search={searchValue}> + {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} + </EuiHighlight> + </MyEuiFlexItem> + <MyEuiFlexItem grow={false}> + <EuiTextColor color="subdued" component="span"> + <small> + {option.description != null && option.description.trim().length > 0 + ? option.description + : getEmptyTagValue()} + </small> + </EuiTextColor> + </MyEuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiIcon + type={`${ + option.favorite != null && isEmpty(option.favorite) ? 'starEmpty' : 'starFilled' + }`} + /> + </EuiFlexItem> + </EuiFlexGroup> + ); + }, []); + + const handleTimelineChange = useCallback( + options => { + const selectedTimeline = options.filter( + (option: { checked: string }) => option.checked === 'on' + ); + if (selectedTimeline != null && selectedTimeline.length > 0) { + onTimelineChange( + isEmpty(selectedTimeline[0].title) + ? i18nTimeline.UNTITLED_TIMELINE + : selectedTimeline[0].title, + selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id + ); + } + onClosePopover(); + }, + [onClosePopover, onTimelineChange] + ); + + const favoritePortal = useMemo( + () => + searchRef != null ? ( + <EuiPortal insert={{ sibling: searchRef, position: 'after' }}> + <MyEuiFlexGroup gutterSize="xs" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiFilterGroup> + <EuiFilterButton + size="l" + data-test-subj="only-favorites-toggle" + hasActiveFilters={onlyFavorites} + onClick={handleOnToggleOnlyFavorites} + > + {i18nTimeline.ONLY_FAVORITES} + </EuiFilterButton> + </EuiFilterGroup> + </EuiFlexItem> + </MyEuiFlexGroup> + </EuiPortal> + ) : null, + [searchRef, onlyFavorites, handleOnToggleOnlyFavorites] + ); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize, + }, + search: searchTimelineValue, + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: onlyFavorites, + timelines, + totalCount: timelineCount, + }); + }, [onlyFavorites, pageSize, searchTimelineValue, timelines, timelineCount]); + + return ( + <EuiSelectableContainer isLoading={loading}> + <EuiSelectable + height={POPOVER_HEIGHT} + isLoading={loading && timelines.length === 0} + listProps={{ + rowHeight: TIMELINE_ITEM_HEIGHT, + showIcons: false, + virtualizedProps: ({ + onScroll: handleOnScroll.bind( + null, + timelines.filter(t => !hideUntitled || t.title !== '').length, + timelineCount + ), + } as unknown) as ListProps, + }} + renderOption={renderTimelineOption} + onChange={handleTimelineChange} + searchable + searchProps={{ + 'data-test-subj': 'timeline-super-select-search-box', + isLoading: loading, + placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER, + onSearch: onSearchTimeline, + incremental: false, + inputRef: (ref: HTMLElement) => { + setSearchRef(ref); + }, + }} + singleSelection={true} + options={getSelectableOptions({ timelines, onlyFavorites, searchTimelineValue })} + > + {(list, search) => ( + <> + {search} + {favoritePortal} + {list} + </> + )} + </EuiSelectable> + </EuiSelectableContainer> + ); +}; + +export const SelectableTimeline = memo(SelectableTimelineComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx b/x-pack/plugins/siem/public/components/timeline/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/styles.tsx rename to x-pack/plugins/siem/public/components/timeline/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/plugins/siem/public/components/timeline/timeline.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx rename to x-pack/plugins/siem/public/components/timeline/timeline.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/plugins/siem/public/components/timeline/timeline.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx rename to x-pack/plugins/siem/public/components/timeline/timeline.tsx index 222cc0530bddb..10f10b1a86f1e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/plugins/siem/public/components/timeline/timeline.tsx @@ -39,7 +39,7 @@ import { Filter, FilterManager, IIndexPattern, -} from '../../../../../../../src/plugins/data/public'; +} from '../../../../../../src/plugins/data/public'; const TimelineContainer = styled.div` height: 100%; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/plugins/siem/public/components/timeline/timeline_context.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx rename to x-pack/plugins/siem/public/components/timeline/timeline_context.tsx index 7c1eadd8e8bed..25a0078b6066a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/plugins/siem/public/components/timeline/timeline_context.tsx @@ -6,7 +6,7 @@ import React, { createContext, memo, useContext, useEffect, useState } from 'react'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../../src/plugins/data/public'; import { TimelineAction } from './body/actions'; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts b/x-pack/plugins/siem/public/components/timeline/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/timeline/translations.ts rename to x-pack/plugins/siem/public/components/timeline/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap rename to x-pack/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/errors.ts b/x-pack/plugins/siem/public/components/toasters/errors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/errors.ts rename to x-pack/plugins/siem/public/components/toasters/errors.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/index.test.tsx b/x-pack/plugins/siem/public/components/toasters/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/index.test.tsx rename to x-pack/plugins/siem/public/components/toasters/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/index.tsx b/x-pack/plugins/siem/public/components/toasters/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/index.tsx rename to x-pack/plugins/siem/public/components/toasters/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx b/x-pack/plugins/siem/public/components/toasters/modal_all_errors.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.test.tsx rename to x-pack/plugins/siem/public/components/toasters/modal_all_errors.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx b/x-pack/plugins/siem/public/components/toasters/modal_all_errors.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/modal_all_errors.tsx rename to x-pack/plugins/siem/public/components/toasters/modal_all_errors.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/translations.ts b/x-pack/plugins/siem/public/components/toasters/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/translations.ts rename to x-pack/plugins/siem/public/components/toasters/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/utils.test.ts b/x-pack/plugins/siem/public/components/toasters/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/utils.test.ts rename to x-pack/plugins/siem/public/components/toasters/utils.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/utils.ts b/x-pack/plugins/siem/public/components/toasters/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/toasters/utils.ts rename to x-pack/plugins/siem/public/components/toasters/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx b/x-pack/plugins/siem/public/components/top_n/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/helpers.test.tsx rename to x-pack/plugins/siem/public/components/top_n/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts b/x-pack/plugins/siem/public/components/top_n/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/helpers.ts rename to x-pack/plugins/siem/public/components/top_n/helpers.ts diff --git a/x-pack/plugins/siem/public/components/top_n/index.test.tsx b/x-pack/plugins/siem/public/components/top_n/index.test.tsx new file mode 100644 index 0000000000000..9325dcf499b2b --- /dev/null +++ b/x-pack/plugins/siem/public/components/top_n/index.test.tsx @@ -0,0 +1,379 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../containers/source/mock'; +import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager } from '../../../../../../src/plugins/data/public'; +import { createStore, State } from '../../store'; +import { TimelineContext, TimelineTypeContext } from '../timeline/timeline_context'; + +import { Props } from './top_n'; +import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; + +jest.mock('../../lib/kibana'); + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +const field = 'process.name'; +const value = 'nice'; + +const state: State = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + query: { + query: 'host.name : end*', + language: 'kuery', + }, + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { + query: 'Linux', + }, + }, + query: { + match: { + 'host.os.name': { + query: 'Linux', + type: 'phrase', + }, + }, + }, + }, + ], + }, + timeline: { + ...mockGlobalState.inputs.timeline, + timerange: { + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: 1586835969047, + to: 1586922369047, + }, + }, + }, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [ACTIVE_TIMELINE_REDUX_ID]: { + ...mockGlobalState.timeline.timelineById.test, + id: ACTIVE_TIMELINE_REDUX_ID, + dataProviders: [ + { + id: + 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', + name: 'tcp', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'network.transport', + value: 'tcp', + operator: ':', + }, + and: [], + }, + ], + eventType: 'all', + filters: [ + { + meta: { + alias: null, + disabled: false, + key: 'source.port', + negate: false, + params: { + query: '30045', + }, + type: 'phrase', + }, + query: { + match: { + 'source.port': { + query: '30045', + type: 'phrase', + }, + }, + }, + }, + ], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'host.name : *', + }, + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + filterQueryDraft: { + kind: 'kuery', + expression: 'host.name : *', + }, + }, + }, + }, + }, +}; +const store = createStore(state, apolloClientObservable); + +describe('StatefulTopN', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering in a global NON-timeline context', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + <TestProviders store={store}> + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TestProviders> + ); + }); + + test('it has undefined combinedQueries when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toBeUndefined(); + }); + + test(`defaults to the 'Raw events' view when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('raw'); + }); + + test(`provides a 'deleteQuery' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeDefined(); + }); + + test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { query: 'Linux' }, + }, + query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, + }, + ]); + }); + + test(`provides 'from' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(0); + }); + + test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); + }); + + test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); + }); + + test(`provides 'to' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1); + }); + }); + + describe('rendering in a timeline context', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + + wrapper = mount( + <TestProviders store={store}> + <TimelineContext.Provider value={{ filterManager, isLoading: false }}> + <TimelineTypeContext.Provider value={{ id: ACTIVE_TIMELINE_REDUX_ID }}> + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TimelineTypeContext.Provider> + </TimelineContext.Provider> + </TestProviders> + ); + }); + + test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' + ); + }); + + test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('all'); + }); + + test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeUndefined(); + }); + + test(`provides empty filters when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([]); + }); + + test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(1586835969047); + }); + + test('provides an empty query when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: '', language: 'kuery' }); + }); + + test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); + }); + + test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1586922369047); + }); + }); + + test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { + const filterManager = new FilterManager(mockUiSettingsForFilterManager); + const wrapper = mount( + <TestProviders store={store}> + <TimelineContext.Provider value={{ filterManager, isLoading: false }}> + <TimelineTypeContext.Provider + value={{ documentType: 'signals', id: ACTIVE_TIMELINE_REDUX_ID }} + > + <StatefulTopN + browserFields={mockBrowserFields} + field={field} + toggleTopN={jest.fn()} + onFilterAdded={jest.fn()} + value={value} + /> + </TimelineTypeContext.Provider> + </TimelineContext.Provider> + </TestProviders> + ); + + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('signal'); + }); +}); diff --git a/x-pack/plugins/siem/public/components/top_n/index.tsx b/x-pack/plugins/siem/public/components/top_n/index.tsx new file mode 100644 index 0000000000000..9863df42f101d --- /dev/null +++ b/x-pack/plugins/siem/public/components/top_n/index.tsx @@ -0,0 +1,166 @@ +/* + * 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 { connect, ConnectedProps } from 'react-redux'; + +import { GlobalTime } from '../../containers/global_time'; +import { BrowserFields, WithSource } from '../../containers/source'; +import { useKibana } from '../../lib/kibana'; +import { esQuery, Filter, Query } from '../../../../../../src/plugins/data/public'; +import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { timelineDefaults } from '../../store/timeline/defaults'; +import { TimelineModel } from '../../store/timeline/model'; +import { combineQueries } from '../timeline/helpers'; +import { useTimelineTypeContext } from '../timeline/timeline_context'; + +import { getOptions } from './helpers'; +import { TopN } from './top_n'; + +/** The currently active timeline always has this Redux ID */ +export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; + +const EMPTY_FILTERS: Filter[] = []; +const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); + + // The mapped Redux state provided to this component includes the global + // filters that appear at the top of most views in the app, and all the + // filters in the active timeline: + const mapStateToProps = (state: State) => { + const activeTimeline: TimelineModel = + getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; + const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; + const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); + + return { + activeTimelineEventType: activeTimeline.eventType, + activeTimelineFilters, + activeTimelineFrom: activeTimelineInput.timerange.from, + activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), + activeTimelineTo: activeTimelineInput.timerange.to, + dataProviders: activeTimeline.dataProviders, + globalQuery: getGlobalQuerySelector(state), + globalFilters: getGlobalFiltersQuerySelector(state), + kqlMode: activeTimeline.kqlMode, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +interface OwnProps { + browserFields: BrowserFields; + field: string; + toggleTopN: () => void; + onFilterAdded?: () => void; + value?: string[] | string | null; +} +type PropsFromRedux = ConnectedProps<typeof connector>; +type Props = OwnProps & PropsFromRedux; + +const StatefulTopNComponent: React.FC<Props> = ({ + activeTimelineEventType, + activeTimelineFilters, + activeTimelineFrom, + activeTimelineKqlQueryExpression, + activeTimelineTo, + browserFields, + dataProviders, + field, + globalFilters = EMPTY_FILTERS, + globalQuery = EMPTY_QUERY, + kqlMode, + onFilterAdded, + setAbsoluteRangeDatePicker, + toggleTopN, + value, +}) => { + const kibana = useKibana(); + + // Regarding data from useTimelineTypeContext: + // * `documentType` (e.g. 'signals') may only be populated in some views, + // e.g. the `Signals` view on the `Detections` page. + // * `id` (`timelineId`) may only be populated when we are rendered in the + // context of the active timeline. + // * `indexToAdd`, which enables the signals index to be appended to + // the `indexPattern` returned by `WithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the signals index + // to the index pattern. + const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); + + const options = getOptions( + timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined + ); + + return ( + <GlobalTime> + {({ from, deleteQuery, setQuery, to }) => ( + <WithSource sourceId="default" indexToAdd={indexToAdd}> + {({ indexPattern }) => ( + <TopN + combinedQueries={ + timelineId === ACTIVE_TIMELINE_REDUX_ID + ? combineQueries({ + browserFields, + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders, + end: activeTimelineTo, + filters: activeTimelineFilters, + indexPattern, + kqlMode, + kqlQuery: { + language: 'kuery', + query: activeTimelineKqlQueryExpression ?? '', + }, + start: activeTimelineFrom, + })?.filterQuery + : undefined + } + data-test-subj="top-n" + defaultView={ + documentType?.toLocaleLowerCase() === 'signals' ? 'signal' : options[0].value + } + deleteQuery={timelineId === ACTIVE_TIMELINE_REDUX_ID ? undefined : deleteQuery} + field={field} + filters={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_FILTERS : globalFilters} + from={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineFrom : from} + indexPattern={indexPattern} + indexToAdd={indexToAdd} + options={options} + query={timelineId === ACTIVE_TIMELINE_REDUX_ID ? EMPTY_QUERY : globalQuery} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + setAbsoluteRangeDatePickerTarget={ + timelineId === ACTIVE_TIMELINE_REDUX_ID ? 'timeline' : 'global' + } + setQuery={setQuery} + to={timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineTo : to} + toggleTopN={toggleTopN} + onFilterAdded={onFilterAdded} + value={value} + /> + )} + </WithSource> + )} + </GlobalTime> + ); +}; + +StatefulTopNComponent.displayName = 'StatefulTopNComponent'; + +export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx b/x-pack/plugins/siem/public/components/top_n/top_n.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/top_n.test.tsx rename to x-pack/plugins/siem/public/components/top_n/top_n.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx b/x-pack/plugins/siem/public/components/top_n/top_n.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx rename to x-pack/plugins/siem/public/components/top_n/top_n.tsx index 136252617e2a2..d8dc63ef92ec6 100644 --- a/x-pack/legacy/plugins/siem/public/components/top_n/top_n.tsx +++ b/x-pack/plugins/siem/public/components/top_n/top_n.tsx @@ -11,7 +11,7 @@ import { ActionCreator } from 'typescript-fsa'; import { EventsByDataset } from '../../pages/overview/events_by_dataset'; import { SignalsByCategory } from '../../pages/overview/signals_by_category'; -import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { Filter, IIndexPattern, Query } from '../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { EventType } from '../../store/timeline/model'; diff --git a/x-pack/legacy/plugins/siem/public/components/top_n/translations.ts b/x-pack/plugins/siem/public/components/top_n/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/top_n/translations.ts rename to x-pack/plugins/siem/public/components/top_n/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/plugins/siem/public/components/truncatable_text/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx rename to x-pack/plugins/siem/public/components/truncatable_text/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx b/x-pack/plugins/siem/public/components/truncatable_text/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/truncatable_text/index.tsx rename to x-pack/plugins/siem/public/components/truncatable_text/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/constants.ts b/x-pack/plugins/siem/public/components/url_state/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/constants.ts rename to x-pack/plugins/siem/public/components/url_state/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts b/x-pack/plugins/siem/public/components/url_state/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/helpers.test.ts rename to x-pack/plugins/siem/public/components/url_state/helpers.test.ts diff --git a/x-pack/plugins/siem/public/components/url_state/helpers.ts b/x-pack/plugins/siem/public/components/url_state/helpers.ts new file mode 100644 index 0000000000000..62196b90e5e6f --- /dev/null +++ b/x-pack/plugins/siem/public/components/url_state/helpers.ts @@ -0,0 +1,260 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { parse, stringify } from 'query-string'; +import { decode, encode } from 'rison-node'; +import * as H from 'history'; + +import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { url } from '../../../../../../src/plugins/kibana_utils/public'; + +import { SiemPageName } from '../../pages/home/types'; +import { inputsSelectors, State, timelineSelectors } from '../../store'; +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../store/timeline/model'; +import { formatDate } from '../super_date_picker'; +import { NavTab } from '../navigation/types'; +import { CONSTANTS, UrlStateType } from './constants'; +import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; + +export const decodeRisonUrlState = <T>(value: string | undefined): T | null => { + try { + return value ? ((decode(value) as unknown) as T) : null; + } catch (error) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { + return null; + } + throw error; + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const encodeRisonUrlState = (state: any) => encode(state); + +export const getQueryStringFromLocation = (search: string) => search.substring(1); + +export const getParamFromQueryString = (queryString: string, key: string) => { + const parsedQueryString = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + + return Array.isArray(queryParam) ? queryParam[0] : queryParam; +}; + +export const replaceStateKeyInQueryString = <T>(stateKey: string, urlState: T) => ( + queryString: string +): string => { + const previousQueryValues = parse(queryString, { sort: false }); + if (urlState == null || (typeof urlState === 'string' && urlState === '')) { + delete previousQueryValues[stateKey]; + + return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); + } + + // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ + // Remove this if these utilities are promoted to kibana core + const encodedUrlState = + typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); +}; + +export const replaceQueryStringInLocation = ( + location: H.Location, + queryString: string +): H.Location => { + if (queryString === getQueryStringFromLocation(location.search)) { + return location; + } else { + return { + ...location, + search: `?${queryString}`, + }; + } +}; + +export const getUrlType = (pageName: string): UrlStateType => { + if (pageName === SiemPageName.overview) { + return 'overview'; + } else if (pageName === SiemPageName.hosts) { + return 'host'; + } else if (pageName === SiemPageName.network) { + return 'network'; + } else if (pageName === SiemPageName.detections) { + return 'detections'; + } else if (pageName === SiemPageName.timelines) { + return 'timeline'; + } else if (pageName === SiemPageName.case) { + return 'case'; + } + return 'overview'; +}; + +export const getTitle = ( + pageName: string, + detailName: string | undefined, + navTabs: Record<string, NavTab> +): string => { + if (detailName != null) return detailName; + return navTabs[pageName] != null ? navTabs[pageName].name : ''; +}; + +export const makeMapStateToProps = () => { + const getInputsSelector = inputsSelectors.inputsSelector(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); + const getTimelines = timelineSelectors.getTimelines(); + const mapStateToProps = (state: State) => { + const inputState = getInputsSelector(state); + const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; + const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; + + const timeline = Object.entries(getTimelines(state)).reduce( + (obj, [timelineId, timelineObj]) => ({ + id: timelineObj.savedObjectId != null ? timelineObj.savedObjectId : '', + isOpen: timelineObj.show, + }), + { id: '', isOpen: false } + ); + + let searchAttr: { + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + } = { + [CONSTANTS.appQuery]: getGlobalQuerySelector(state), + [CONSTANTS.filters]: getGlobalFiltersQuerySelector(state), + }; + const savedQuery = getGlobalSavedQuerySelector(state); + if (savedQuery != null && savedQuery.id !== '') { + searchAttr = { + [CONSTANTS.savedQuery]: savedQuery.id, + }; + } + + return { + urlState: { + ...searchAttr, + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: globalTimerange, + linkTo: globalLinkTo, + }, + timeline: { + [CONSTANTS.timerange]: timelineTimerange, + linkTo: timelineLinkTo, + }, + }, + [CONSTANTS.timeline]: timeline, + }, + }; + }; + + return mapStateToProps; +}; + +export const updateTimerangeUrl = ( + timeRange: UrlInputsModel, + isInitializing: boolean +): UrlInputsModel => { + if (timeRange.global.timerange.kind === 'relative') { + timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); + timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); + } + if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { + timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); + timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { + roundUp: true, + }); + } + return timeRange; +}; + +export const updateUrlStateString = ({ + isInitializing, + history, + newUrlStateString, + pathName, + search, + updateTimerange, + urlKey, +}: UpdateUrlStateString): string => { + if (urlKey === CONSTANTS.appQuery) { + const queryState = decodeRisonUrlState<Query>(newUrlStateString); + if (queryState != null && queryState.query === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timerange && updateTimerange) { + const queryState = decodeRisonUrlState<UrlInputsModel>(newUrlStateString); + if (queryState != null && queryState.global != null) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.filters) { + const queryState = decodeRisonUrlState<Filter[]>(newUrlStateString); + if (isEmpty(queryState)) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timeline) { + const queryState = decodeRisonUrlState<TimelineUrl>(newUrlStateString); + if (queryState != null && queryState.id === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } + return search; +}; + +export const replaceStateInLocation = <T>({ + history, + urlStateToReplace, + urlStateKey, + pathName, + search, +}: ReplaceStateInLocation<T>) => { + const newLocation = replaceQueryStringInLocation( + { + hash: '', + pathname: pathName, + search, + state: '', + }, + replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) + ); + if (history) { + history.replace(newLocation); + } + return newLocation.search; +}; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/plugins/siem/public/components/url_state/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/index.test.tsx rename to x-pack/plugins/siem/public/components/url_state/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/index.tsx rename to x-pack/plugins/siem/public/components/url_state/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/plugins/siem/public/components/url_state/index_mocked.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/index_mocked.test.tsx rename to x-pack/plugins/siem/public/components/url_state/index_mocked.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx rename to x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx index 4838fb4499b87..54a196d1b8161 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx @@ -7,7 +7,7 @@ import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { Query, Filter } from '../../../../../../src/plugins/data/public'; import { inputsActions } from '../../store/actions'; import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants'; import { diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.test.ts b/x-pack/plugins/siem/public/components/url_state/normalize_time_range.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.test.ts rename to x-pack/plugins/siem/public/components/url_state/normalize_time_range.test.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.ts b/x-pack/plugins/siem/public/components/url_state/normalize_time_range.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/normalize_time_range.ts rename to x-pack/plugins/siem/public/components/url_state/normalize_time_range.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts b/x-pack/plugins/siem/public/components/url_state/test_dependencies.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts rename to x-pack/plugins/siem/public/components/url_state/test_dependencies.ts index dc1b8d428bb20..974bee53bc2ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/siem/public/components/url_state/test_dependencies.ts @@ -15,7 +15,7 @@ import { HostsTableType } from '../../store/hosts/model'; import { CONSTANTS } from './constants'; import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url'; import { UrlStateContainerPropTypes, LocationTypes } from './types'; -import { Query } from '../../../../../../../src/plugins/data/public'; +import { Query } from '../../../../../../src/plugins/data/public'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/plugins/siem/public/components/url_state/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/types.ts rename to x-pack/plugins/siem/public/components/url_state/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/plugins/siem/public/components/url_state/use_url_state.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/url_state/use_url_state.tsx rename to x-pack/plugins/siem/public/components/url_state/use_url_state.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap rename to x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts b/x-pack/plugins/siem/public/components/utility_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/index.ts rename to x-pack/plugins/siem/public/components/utility_bar/index.ts diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx b/x-pack/plugins/siem/public/components/utility_bar/styles.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/styles.tsx rename to x-pack/plugins/siem/public/components/utility_bar/styles.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_action.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_group.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_section.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx b/x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utility_bar/utility_bar_text.tsx rename to x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/utils.ts b/x-pack/plugins/siem/public/components/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/utils.ts rename to x-pack/plugins/siem/public/components/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx b/x-pack/plugins/siem/public/components/with_hover_actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/with_hover_actions/index.tsx rename to x-pack/plugins/siem/public/components/with_hover_actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx b/x-pack/plugins/siem/public/components/wrapper_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/wrapper_page/index.test.tsx rename to x-pack/plugins/siem/public/components/wrapper_page/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/plugins/siem/public/components/wrapper_page/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/components/wrapper_page/index.tsx rename to x-pack/plugins/siem/public/components/wrapper_page/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts rename to x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx new file mode 100644 index 0000000000000..2bbb4cde92b15 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; + +import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; +import { AnomaliesQueryTabBodyProps } from './types'; +import { getAnomaliesFilterQuery } from './utils'; +import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useUiSetting$ } from '../../../lib/kibana'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +const ID = 'anomaliesOverTimeQuery'; + +export const AnomaliesQueryTabBody = ({ + deleteQuery, + endDate, + setQuery, + skip, + startDate, + type, + narrowDateRange, + filterQuery, + anomaliesFilterQuery, + AnomaliesTableComponent, + flowTarget, + ip, +}: AnomaliesQueryTabBodyProps) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + + const [, siemJobs] = useSiemJobs(true); + const [anomalyScore] = useUiSetting$<number>(DEFAULT_ANOMALY_SCORE); + + const mergedFilterQuery = getAnomaliesFilterQuery( + filterQuery, + anomaliesFilterQuery, + siemJobs, + anomalyScore, + flowTarget, + ip + ); + + return ( + <> + <MatrixHistogramContainer + endDate={endDate} + filterQuery={mergedFilterQuery} + id={ID} + setQuery={setQuery} + sourceId="default" + startDate={startDate} + type={type} + {...histogramConfigs} + /> + <AnomaliesTableComponent + startDate={startDate} + endDate={endDate} + skip={skip} + type={type as never} + narrowDateRange={narrowDateRange} + flowTarget={flowTarget} + ip={ip} + /> + </> + ); +}; + +AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts rename to x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts new file mode 100644 index 0000000000000..f6cae81e3c6c4 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts @@ -0,0 +1,35 @@ +/* + * 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 { ESTermQuery } from '../../../../common/typed_json'; +import { NarrowDateRange } from '../../../components/ml/types'; +import { UpdateDateRange } from '../../../components/charts/common'; +import { SetQuery } from '../../../pages/hosts/navigation/types'; +import { FlowTarget } from '../../../graphql/types'; +import { HostsType } from '../../../store/hosts/model'; +import { NetworkType } from '../../../store/network/model'; +import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; + +interface QueryTabBodyProps { + type: HostsType | NetworkType; + filterQuery?: string | ESTermQuery; +} + +export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { + anomaliesFilterQuery?: object; + AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; + deleteQuery?: ({ id }: { id: string }) => void; + endDate: number; + flowTarget?: FlowTarget; + narrowDateRange: NarrowDateRange; + setQuery: SetQuery; + startDate: number; + skip: boolean; + updateDateRange?: UpdateDateRange; + hideHistogramIfEmpty?: boolean; + ip?: string; +}; diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts new file mode 100644 index 0000000000000..790a797b2fead --- /dev/null +++ b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.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 deepmerge from 'deepmerge'; + +import { ESTermQuery } from '../../../../common/typed_json'; +import { createFilter } from '../../helpers'; +import { SiemJob } from '../../../components/ml_popover/types'; +import { FlowTarget } from '../../../graphql/types'; + +export const getAnomaliesFilterQuery = ( + filterQuery: string | ESTermQuery | undefined, + anomaliesFilterQuery: object = {}, + siemJobs: SiemJob[] = [], + anomalyScore: number, + flowTarget?: FlowTarget, + ip?: string +): string => { + const siemJobIds = siemJobs + .filter(job => job.isInstalled) + .map(job => job.id) + .map(jobId => ({ + match_phrase: { + job_id: jobId, + }, + })); + + const filterQueryString = createFilter(filterQuery); + const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; + const mergedFilterQuery = deepmerge.all([ + filterQueryObject, + anomaliesFilterQuery, + { + bool: { + filter: [ + { + bool: { + should: siemJobIds, + minimum_should_match: 1, + }, + }, + { + match_phrase: { + result_type: 'record', + }, + }, + flowTarget && + ip && { + match_phrase: { + [`${flowTarget}.ip`]: ip, + }, + }, + { + range: { + record_score: { + gte: anomalyScore, + }, + }, + }, + ], + }, + }, + ]); + + return JSON.stringify(mergedFilterQuery); +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts b/x-pack/plugins/siem/public/containers/authentications/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/authentications/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/authentications/index.tsx b/x-pack/plugins/siem/public/containers/authentications/index.tsx new file mode 100644 index 0000000000000..6d4a88c45a768 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/authentications/index.tsx @@ -0,0 +1,150 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + AuthenticationsEdges, + GetAuthenticationsQuery, + PageInfoPaginated, +} from '../../graphql/types'; +import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; + +import { authenticationsQuery } from './index.gql_query'; + +const ID = 'authenticationQuery'; + +export interface AuthenticationArgs { + authentications: AuthenticationsEdges[]; + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: AuthenticationArgs) => React.ReactNode; + type: hostsModel.HostsType; +} + +export interface AuthenticationsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; +} + +type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; + +class AuthenticationsComponentQuery extends QueryTemplatePaginated< + AuthenticationsProps, + GetAuthenticationsQuery.Query, + GetAuthenticationsQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetAuthenticationsQuery.Variables = { + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + pagination: generateTablePaginationOptions(activePage, limit), + filterQuery: createFilter(filterQuery), + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }; + return ( + <Query<GetAuthenticationsQuery.Query, GetAuthenticationsQuery.Variables> + query={authenticationsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const authentications = getOr([], 'source.Authentications.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Authentications: { + ...fetchMoreResult.source.Authentications, + edges: [...fetchMoreResult.source.Authentications.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + authentications, + id, + inspect: getOr(null, 'source.Authentications.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Authentications.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.Authentications.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getAuthenticationsSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +export const AuthenticationsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(AuthenticationsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/case/__mocks__/api.ts b/x-pack/plugins/siem/public/containers/case/__mocks__/api.ts new file mode 100644 index 0000000000000..dcc31401564b1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/__mocks__/api.ts @@ -0,0 +1,122 @@ +/* + * 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 { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + CaseUserActions, + FetchCasesProps, + SortFieldCase, +} from '../types'; +import { + actionLicenses, + allCases, + basicCase, + basicCaseCommentPatch, + basicCasePost, + casesStatus, + caseUserActions, + pushedCase, + respReporters, + serviceConnector, + tags, +} from '../mock'; +import { + CaseExternalServiceRequest, + CasePatchRequest, + CasePostRequest, + CommentRequest, + ServiceConnectorCaseParams, + ServiceConnectorCaseResponse, + User, +} from '../../../../../case/common/api'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise<Case> => { + return Promise.resolve(basicCase); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => + Promise.resolve(casesStatus); + +export const getTags = async (signal: AbortSignal): Promise<string[]> => Promise.resolve(tags); + +export const getReporters = async (signal: AbortSignal): Promise<User[]> => + Promise.resolve(respReporters); + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise<CaseUserActions[]> => Promise.resolve(caseUserActions); + +export const getCases = async ({ + filterOptions = { + search: '', + reporters: [], + status: 'open', + tags: [], + }, + queryParams = { + page: 1, + perPage: 5, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise<AllCases> => Promise.resolve(allCases); + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => + Promise.resolve(basicCasePost); + +export const patchCase = async ( + caseId: string, + updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, + version: string, + signal: AbortSignal +): Promise<Case[]> => Promise.resolve([basicCase]); + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise<Case[]> => Promise.resolve(allCases.cases); + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCase); + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal +): Promise<Case> => Promise.resolve(basicCaseCommentPatch); + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<boolean> => + Promise.resolve(true); + +export const pushCase = async ( + caseId: string, + push: CaseExternalServiceRequest, + signal: AbortSignal +): Promise<Case> => Promise.resolve(pushedCase); + +export const pushToService = async ( + connectorId: string, + casePushParams: ServiceConnectorCaseParams, + signal: AbortSignal +): Promise<ServiceConnectorCaseResponse> => Promise.resolve(serviceConnector); + +export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => + Promise.resolve(actionLicenses); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx b/x-pack/plugins/siem/public/containers/case/api.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx rename to x-pack/plugins/siem/public/containers/case/api.test.tsx index 4f5655cc9f221..174738098fa10 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/api.test.tsx @@ -5,6 +5,9 @@ */ import { KibanaServices } from '../../lib/kibana'; + +import { CASES_URL } from '../../../../case/common/constants'; + import { deleteCases, getActionLicense, @@ -22,6 +25,7 @@ import { pushCase, pushToService, } from './api'; + import { actionLicenses, allCases, @@ -44,7 +48,7 @@ import { caseUserActionsSnake, casesStatusSnake, } from './mock'; -import { CASES_URL } from './constants'; + import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; import * as i18n from './translations'; @@ -414,7 +418,9 @@ describe('Case Configuration API', () => { await pushToService(connectorId, casePushParams, abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/action/${connectorId}/_execute`, { method: 'POST', - body: JSON.stringify({ params: casePushParams }), + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), signal: abortCtrl.signal, }); }); diff --git a/x-pack/plugins/siem/public/containers/case/api.ts b/x-pack/plugins/siem/public/containers/case/api.ts new file mode 100644 index 0000000000000..438eae9d88a44 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/api.ts @@ -0,0 +1,268 @@ +/* + * 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 { + CaseResponse, + CasesResponse, + CasesFindResponse, + CasePatchRequest, + CasePostRequest, + CasesStatusResponse, + CommentRequest, + User, + CaseUserActionsResponse, + CaseExternalServiceRequest, + ServiceConnectorCaseParams, + ServiceConnectorCaseResponse, + ActionTypeExecutorResult, +} from '../../../../case/common/api'; + +import { + CASE_STATUS_URL, + CASES_URL, + CASE_TAGS_URL, + CASE_REPORTERS_URL, + ACTION_TYPES_URL, + ACTION_URL, +} from '../../../../case/common/constants'; + +import { + getCaseDetailsUrl, + getCaseUserActionUrl, + getCaseCommentsUrl, +} from '../../../../case/common/api/helpers'; + +import { KibanaServices } from '../../lib/kibana'; + +import { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + FetchCasesProps, + SortFieldCase, + CaseUserActions, +} from './types'; + +import { + convertToCamelCase, + convertAllCasesToCamel, + convertArrayToCamelCase, + decodeCaseResponse, + decodeCasesResponse, + decodeCasesFindResponse, + decodeCasesStatusResponse, + decodeCaseUserActionsResponse, + decodeServiceConnectorCaseResponse, +} from './utils'; + +import * as i18n from './translations'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>(getCaseDetailsUrl(caseId), { + method: 'GET', + query: { + includeComments, + }, + signal, + }); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise<CasesStatus> => { + const response = await KibanaServices.get().http.fetch<CasesStatusResponse>(CASE_STATUS_URL, { + method: 'GET', + signal, + }); + return convertToCamelCase<CasesStatusResponse, CasesStatus>(decodeCasesStatusResponse(response)); +}; + +export const getTags = async (signal: AbortSignal): Promise<string[]> => { + const response = await KibanaServices.get().http.fetch<string[]>(CASE_TAGS_URL, { + method: 'GET', + signal, + }); + return response ?? []; +}; + +export const getReporters = async (signal: AbortSignal): Promise<User[]> => { + const response = await KibanaServices.get().http.fetch<User[]>(CASE_REPORTERS_URL, { + method: 'GET', + signal, + }); + return response ?? []; +}; + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise<CaseUserActions[]> => { + const response = await KibanaServices.get().http.fetch<CaseUserActionsResponse>( + getCaseUserActionUrl(caseId), + { + method: 'GET', + signal, + } + ); + return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; +}; + +export const getCases = async ({ + filterOptions = { + search: '', + reporters: [], + status: 'open', + tags: [], + }, + queryParams = { + page: 1, + perPage: 20, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise<AllCases> => { + const query = { + reporters: filterOptions.reporters.map(r => r.username ?? '').filter(r => r !== ''), + tags: filterOptions.tags, + ...(filterOptions.status !== '' ? { status: filterOptions.status } : {}), + ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), + ...queryParams, + }; + const response = await KibanaServices.get().http.fetch<CasesFindResponse>(`${CASES_URL}/_find`, { + method: 'GET', + query, + signal, + }); + return convertAllCasesToCamel(decodeCasesFindResponse(response)); +}; + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>(CASES_URL, { + method: 'POST', + body: JSON.stringify(newCase), + signal, + }); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const patchCase = async ( + caseId: string, + updatedCase: Pick<CasePatchRequest, 'description' | 'status' | 'tags' | 'title'>, + version: string, + signal: AbortSignal +): Promise<Case[]> => { + const response = await KibanaServices.get().http.fetch<CasesResponse>(CASES_URL, { + method: 'PATCH', + body: JSON.stringify({ cases: [{ ...updatedCase, id: caseId, version }] }), + signal, + }); + return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response)); +}; + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise<Case[]> => { + const response = await KibanaServices.get().http.fetch<CasesResponse>(CASES_URL, { + method: 'PATCH', + body: JSON.stringify({ cases }), + signal, + }); + return convertToCamelCase<CasesResponse, Case[]>(decodeCasesResponse(response)); +}; + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>( + `${CASES_URL}/${caseId}/comments`, + { + method: 'POST', + body: JSON.stringify(newComment), + signal, + } + ); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>(getCaseCommentsUrl(caseId), { + method: 'PATCH', + body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), + signal, + }); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise<string> => { + const response = await KibanaServices.get().http.fetch<string>(CASES_URL, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + signal, + }); + return response; +}; + +export const pushCase = async ( + caseId: string, + push: CaseExternalServiceRequest, + signal: AbortSignal +): Promise<Case> => { + const response = await KibanaServices.get().http.fetch<CaseResponse>( + `${getCaseDetailsUrl(caseId)}/_push`, + { + method: 'POST', + body: JSON.stringify(push), + signal, + } + ); + return convertToCamelCase<CaseResponse, Case>(decodeCaseResponse(response)); +}; + +export const pushToService = async ( + connectorId: string, + casePushParams: ServiceConnectorCaseParams, + signal: AbortSignal +): Promise<ServiceConnectorCaseResponse> => { + const response = await KibanaServices.get().http.fetch<ActionTypeExecutorResult>( + `${ACTION_URL}/${connectorId}/_execute`, + { + method: 'POST', + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), + signal, + } + ); + + if (response.status === 'error') { + throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); + } + + return decodeServiceConnectorCaseResponse(response.data); +}; + +export const getActionLicense = async (signal: AbortSignal): Promise<ActionLicense[]> => { + const response = await KibanaServices.get().http.fetch<ActionLicense[]>(ACTION_TYPES_URL, { + method: 'GET', + signal, + }); + return response; +}; diff --git a/x-pack/plugins/siem/public/containers/case/configure/__mocks__/api.ts b/x-pack/plugins/siem/public/containers/case/configure/__mocks__/api.ts new file mode 100644 index 0000000000000..c3611f490708a --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/__mocks__/api.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CasesConfigurePatch, + CasesConfigureRequest, + Connector, +} from '../../../../../../case/common/api'; + +import { ApiProps } from '../../types'; +import { CaseConfigure } from '../types'; +import { connectorsMock, caseConfigurationCamelCaseResponseMock } from '../mock'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => + Promise.resolve(connectorsMock); + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure> => + Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise<CaseConfigure> => Promise.resolve(caseConfigurationCamelCaseResponseMock); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts b/x-pack/plugins/siem/public/containers/case/configure/api.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/api.test.ts rename to x-pack/plugins/siem/public/containers/case/configure/api.test.ts diff --git a/x-pack/plugins/siem/public/containers/case/configure/api.ts b/x-pack/plugins/siem/public/containers/case/configure/api.ts new file mode 100644 index 0000000000000..4f516764e46f3 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/api.ts @@ -0,0 +1,82 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { + Connector, + CasesConfigurePatch, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../case/common/api'; +import { KibanaServices } from '../../../lib/kibana'; + +import { + CASE_CONFIGURE_CONNECTORS_URL, + CASE_CONFIGURE_URL, +} from '../../../../../case/common/constants'; + +import { ApiProps } from '../types'; +import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; +import { CaseConfigure } from './types'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise<Connector[]> => { + const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { + method: 'GET', + signal, + }); + + return response; +}; + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise<CaseConfigure | null> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'GET', + signal, + } + ); + + return !isEmpty(response) + ? convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ) + : null; +}; + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise<CaseConfigure> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'POST', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ); +}; + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise<CaseConfigure> => { + const response = await KibanaServices.get().http.fetch<CasesConfigureResponse>( + CASE_CONFIGURE_URL, + { + method: 'PATCH', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase<CasesConfigureResponse, CaseConfigure>( + decodeCaseConfigureResponse(response) + ); +}; diff --git a/x-pack/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/plugins/siem/public/containers/case/configure/mock.ts new file mode 100644 index 0000000000000..c6824bd50edb5 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/mock.ts @@ -0,0 +1,100 @@ +/* + * 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 { + Connector, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../case/common/api'; +import { CaseConfigure, CasesConfigurationMapping } from './types'; + +export const mapping: CasesConfigurationMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; +export const connectorsMock: Connector[] = [ + { + id: '123', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + casesConfiguration: { + mapping, + }, + }, + isPreconfigured: false, + }, + { + id: '456', + actionTypeId: '.servicenow', + name: 'My Connector 2', + config: { + apiUrl: 'https://instance2.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + isPreconfigured: false, + }, +]; + +export const caseConfigurationResposeMock: CasesConfigureResponse = { + created_at: '2020-04-06T13:03:18.657Z', + created_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-pushing', + updated_at: '2020-04-06T14:03:18.657Z', + updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; + +export const caseConfigurationMock: CasesConfigureRequest = { + connector_id: '123', + connector_name: 'My Connector', + closure_type: 'close-by-user', +}; + +export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { + createdAt: '2020-04-06T13:03:18.657Z', + createdBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + connectorId: '123', + connectorName: 'My Connector', + closureType: 'close-by-pushing', + updatedAt: '2020-04-06T14:03:18.657Z', + updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, + version: 'WzHJ12', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts b/x-pack/plugins/siem/public/containers/case/configure/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts rename to x-pack/plugins/siem/public/containers/case/configure/translations.ts diff --git a/x-pack/plugins/siem/public/containers/case/configure/types.ts b/x-pack/plugins/siem/public/containers/case/configure/types.ts new file mode 100644 index 0000000000000..ed95315c066dc --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/types.ts @@ -0,0 +1,38 @@ +/* + * 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 { ElasticUser } from '../types'; +import { + ActionType, + CasesConfigurationMaps, + CaseField, + ClosureType, + Connector, + ThirdPartyField, +} from '../../../../../case/common/api'; + +export { ActionType, CasesConfigurationMaps, CaseField, ClosureType, Connector, ThirdPartyField }; + +export interface CasesConfigurationMapping { + source: CaseField; + target: ThirdPartyField; + actionType: ActionType; +} + +export interface CaseConfigure { + createdAt: string; + createdBy: ElasticUser; + connectorId: string; + connectorName: string; + closureType: ClosureType; + updatedAt: string; + updatedBy: ElasticUser; + version: string; +} + +export interface CCMapsCombinedActionAttributes extends CasesConfigurationMaps { + actionType?: ActionType; +} diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx new file mode 100644 index 0000000000000..2826e9a2c2e55 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx @@ -0,0 +1,260 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { + initialState, + useCaseConfigure, + ReturnUseCaseConfigure, + ConnectorConfiguration, +} from './use_configure'; +import { mapping, caseConfigurationCamelCaseResponseMock } from './mock'; +import * as api from './api'; + +jest.mock('./api'); + +const configuration: ConnectorConfiguration = { + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-pushing', +}; + +describe('useConfigure', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialState, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); + + test('fetch case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + ...initialState, + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + currentConfiguration: { + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + }, + version: caseConfigurationCamelCaseResponseMock.version, + firstLoad: true, + loading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); + + test('refetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + expect(spyOnGetCaseConfigure).toHaveBeenCalledTimes(2); + }); + }); + + test('correctly sets mappings', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current.mapping).toEqual(null); + result.current.setMapping(mapping); + expect(result.current.mapping).toEqual(mapping); + }); + }); + + test('set isLoading to true when fetching case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.refetchCaseConfigure(); + + expect(result.current.loading).toBe(true); + }); + }); + + test('persist case configuration', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + result.current.persistCaseConfigure(configuration); + expect(result.current.persistLoading).toBeTruthy(); + }); + }); + + test('save case configuration - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current.connectorId).toEqual('123'); + await waitForNextUpdate(); + expect(result.current.connectorId).toEqual('456'); + }); + }); + + test('save case configuration - patchCaseConfigure', async () => { + const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); + spyOnPatchCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current.connectorId).toEqual('123'); + await waitForNextUpdate(); + expect(result.current.connectorId).toEqual('456'); + }); + }); + + test('unhappy path - fetch case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + ...initialState, + loading: false, + persistLoading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); + + test('unhappy path - persist case configuration', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnUseCaseConfigure>(() => + useCaseConfigure() + ); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + result.current.persistCaseConfigure(configuration); + + expect(result.current).toEqual({ + ...initialState, + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + currentConfiguration: { + closureType: caseConfigurationCamelCaseResponseMock.closureType, + connectorId: caseConfigurationCamelCaseResponseMock.connectorId, + connectorName: caseConfigurationCamelCaseResponseMock.connectorName, + }, + firstLoad: true, + loading: false, + refetchCaseConfigure: result.current.refetchCaseConfigure, + persistCaseConfigure: result.current.persistCaseConfigure, + setCurrentConfiguration: result.current.setCurrentConfiguration, + setConnector: result.current.setConnector, + setClosureType: result.current.setClosureType, + setMapping: result.current.setMapping, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx new file mode 100644 index 0000000000000..a185d435f7165 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -0,0 +1,326 @@ +/* + * 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 { useEffect, useCallback, useReducer } from 'react'; +import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; + +import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; +import * as i18n from './translations'; +import { CasesConfigurationMapping, ClosureType } from './types'; + +interface Connector { + connectorId: string; + connectorName: string; +} +export interface ConnectorConfiguration extends Connector { + closureType: ClosureType; +} + +export interface State extends ConnectorConfiguration { + currentConfiguration: ConnectorConfiguration; + firstLoad: boolean; + loading: boolean; + mapping: CasesConfigurationMapping[] | null; + persistLoading: boolean; + version: string; +} +export type Action = + | { + type: 'setCurrentConfiguration'; + currentConfiguration: ConnectorConfiguration; + } + | { + type: 'setConnector'; + connector: Connector; + } + | { + type: 'setLoading'; + payload: boolean; + } + | { + type: 'setFirstLoad'; + payload: boolean; + } + | { + type: 'setPersistLoading'; + payload: boolean; + } + | { + type: 'setVersion'; + payload: string; + } + | { + type: 'setClosureType'; + closureType: ClosureType; + } + | { + type: 'setMapping'; + mapping: CasesConfigurationMapping[]; + }; + +export const configureCasesReducer = (state: State, action: Action) => { + switch (action.type) { + case 'setLoading': + return { + ...state, + loading: action.payload, + }; + case 'setFirstLoad': + return { + ...state, + firstLoad: action.payload, + }; + case 'setPersistLoading': + return { + ...state, + persistLoading: action.payload, + }; + case 'setVersion': + return { + ...state, + version: action.payload, + }; + case 'setCurrentConfiguration': { + return { + ...state, + currentConfiguration: { ...action.currentConfiguration }, + }; + } + case 'setConnector': { + return { + ...state, + ...action.connector, + }; + } + case 'setClosureType': { + return { + ...state, + closureType: action.closureType, + }; + } + case 'setMapping': { + return { + ...state, + mapping: action.mapping, + }; + } + default: + return state; + } +}; + +export interface ReturnUseCaseConfigure extends State { + persistCaseConfigure: ({ + connectorId, + connectorName, + closureType, + }: ConnectorConfiguration) => unknown; + refetchCaseConfigure: () => void; + setClosureType: (closureType: ClosureType) => void; + setConnector: (connectorId: string, connectorName?: string) => void; + setCurrentConfiguration: (configuration: ConnectorConfiguration) => void; + setMapping: (newMapping: CasesConfigurationMapping[]) => void; +} + +export const initialState: State = { + closureType: 'close-by-user', + connectorId: 'none', + connectorName: 'none', + currentConfiguration: { + closureType: 'close-by-user', + connectorId: 'none', + connectorName: 'none', + }, + firstLoad: false, + loading: true, + mapping: null, + persistLoading: false, + version: '', +}; + +export const useCaseConfigure = (): ReturnUseCaseConfigure => { + const [state, dispatch] = useReducer(configureCasesReducer, initialState); + + const setCurrentConfiguration = useCallback((configuration: ConnectorConfiguration) => { + dispatch({ + currentConfiguration: configuration, + type: 'setCurrentConfiguration', + }); + }, []); + + const setConnector = useCallback((connectorId: string, connectorName?: string) => { + dispatch({ + connector: { connectorId, connectorName: connectorName ?? '' }, + type: 'setConnector', + }); + }, []); + + const setClosureType = useCallback((closureType: ClosureType) => { + dispatch({ + closureType, + type: 'setClosureType', + }); + }, []); + + const setMapping = useCallback((newMapping: CasesConfigurationMapping[]) => { + dispatch({ + mapping: newMapping, + type: 'setMapping', + }); + }, []); + + const setLoading = useCallback((isLoading: boolean) => { + dispatch({ + payload: isLoading, + type: 'setLoading', + }); + }, []); + + const setFirstLoad = useCallback((isFirstLoad: boolean) => { + dispatch({ + payload: isFirstLoad, + type: 'setFirstLoad', + }); + }, []); + + const setPersistLoading = useCallback((isPersistLoading: boolean) => { + dispatch({ + payload: isPersistLoading, + type: 'setPersistLoading', + }); + }, []); + + const setVersion = useCallback((version: string) => { + dispatch({ + payload: version, + type: 'setVersion', + }); + }, []); + + const [, dispatchToaster] = useStateToaster(); + + const refetchCaseConfigure = useCallback(() => { + let didCancel = false; + const abortCtrl = new AbortController(); + + const fetchCaseConfiguration = async () => { + try { + setLoading(true); + const res = await getCaseConfigure({ signal: abortCtrl.signal }); + if (!didCancel) { + if (res != null) { + setConnector(res.connectorId, res.connectorName); + if (setClosureType != null) { + setClosureType(res.closureType); + } + setVersion(res.version); + + if (!state.firstLoad) { + setFirstLoad(true); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + closureType: res.closureType, + connectorId: res.connectorId, + connectorName: res.connectorName, + }); + } + } + } + setLoading(false); + } + } catch (error) { + if (!didCancel) { + setLoading(false); + errorToToaster({ + dispatchToaster, + error: error.body && error.body.message ? new Error(error.body.message) : error, + title: i18n.ERROR_TITLE, + }); + } + } + }; + + fetchCaseConfiguration(); + + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, [state.firstLoad]); + + const persistCaseConfigure = useCallback( + async ({ connectorId, connectorName, closureType }: ConnectorConfiguration) => { + let didCancel = false; + const abortCtrl = new AbortController(); + const saveCaseConfiguration = async () => { + try { + setPersistLoading(true); + const connectorObj = { + connector_id: connectorId, + connector_name: connectorName, + closure_type: closureType, + }; + const res = + state.version.length === 0 + ? await postCaseConfigure(connectorObj, abortCtrl.signal) + : await patchCaseConfigure( + { + ...connectorObj, + version: state.version, + }, + abortCtrl.signal + ); + if (!didCancel) { + setConnector(res.connectorId, res.connectorName); + if (setClosureType) { + setClosureType(res.closureType); + } + setVersion(res.version); + if (setCurrentConfiguration != null) { + setCurrentConfiguration({ + connectorId: res.connectorId, + closureType: res.closureType, + connectorName: res.connectorName, + }); + } + + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); + setPersistLoading(false); + } + } catch (error) { + if (!didCancel) { + setPersistLoading(false); + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } + } + }; + saveCaseConfiguration(); + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, + [state.version] + ); + + useEffect(() => { + refetchCaseConfigure(); + }, []); + + return { + ...state, + refetchCaseConfigure, + persistCaseConfigure, + setCurrentConfiguration, + setConnector, + setClosureType, + setMapping, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_connectors.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.test.tsx rename to x-pack/plugins/siem/public/containers/case/configure/use_connectors.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx b/x-pack/plugins/siem/public/containers/case/configure/use_connectors.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/configure/use_connectors.tsx rename to x-pack/plugins/siem/public/containers/case/configure/use_connectors.tsx diff --git a/x-pack/plugins/siem/public/containers/case/constants.ts b/x-pack/plugins/siem/public/containers/case/constants.ts new file mode 100644 index 0000000000000..d8bb499ed7922 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/constants.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 5; diff --git a/x-pack/plugins/siem/public/containers/case/mock.ts b/x-pack/plugins/siem/public/containers/case/mock.ts new file mode 100644 index 0000000000000..a3a8db2c40950 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/mock.ts @@ -0,0 +1,308 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; + +import { + CommentResponse, + ServiceConnectorCaseResponse, + Status, + UserAction, + UserActionField, + CaseResponse, + CasesStatusResponse, + CaseUserActionsResponse, + CasesResponse, + CasesFindResponse, +} from '../../../../case/common/api/cases'; +import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; + +export const basicCaseId = 'basic-case-id'; +const basicCommentId = 'basic-comment-id'; +const basicCreatedAt = '2020-02-19T23:06:33.798Z'; +const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; +const laterTime = '2020-02-28T15:02:57.995Z'; +export const elasticUser = { + fullName: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; + +export const tags: string[] = ['coke', 'pepsi']; + +export const basicComment: Comment = { + comment: 'Solve this fast!', + id: basicCommentId, + createdAt: basicCreatedAt, + createdBy: elasticUser, + pushedAt: null, + pushedBy: null, + updatedAt: null, + updatedBy: null, + version: 'WzQ3LDFc', +}; + +export const basicCase: Case = { + closedAt: null, + closedBy: null, + id: basicCaseId, + comments: [basicComment], + createdAt: basicCreatedAt, + createdBy: elasticUser, + description: 'Security banana Issue', + externalService: null, + status: 'open', + tags, + title: 'Another horrible breach!!', + totalComment: 1, + updatedAt: basicUpdatedAt, + updatedBy: elasticUser, + version: 'WzQ3LDFd', +}; + +export const basicCasePost: Case = { + ...basicCase, + updatedAt: null, + updatedBy: null, +}; + +export const basicCommentPatch: Comment = { + ...basicComment, + updatedAt: basicUpdatedAt, + updatedBy: { + username: 'elastic', + }, +}; + +export const basicCaseCommentPatch = { + ...basicCase, + comments: [basicCommentPatch], +}; + +export const casesStatus: CasesStatus = { + countClosedCases: 130, + countOpenCases: 20, +}; + +const basicPush = { + connectorId: 'connector_id', + connectorName: 'connector name', + externalId: 'external_id', + externalTitle: 'external title', + externalUrl: 'basicPush.com', + pushedAt: basicUpdatedAt, + pushedBy: elasticUser, +}; + +export const pushedCase: Case = { + ...basicCase, + externalService: basicPush, +}; + +export const serviceConnector: ServiceConnectorCaseResponse = { + title: '123', + id: '444', + pushedDate: basicUpdatedAt, + url: 'connector.com', + comments: [ + { + commentId: basicCommentId, + pushedDate: basicUpdatedAt, + }, + ], +}; + +const basicAction = { + actionAt: basicCreatedAt, + actionBy: elasticUser, + oldValue: null, + newValue: 'what a cool value', + caseId: basicCaseId, + commentId: null, +}; + +export const casePushParams = { + actionBy: elasticUser, + caseId: basicCaseId, + createdAt: basicCreatedAt, + createdBy: elasticUser, + externalId: null, + title: 'what a cool value', + commentId: null, + updatedAt: basicCreatedAt, + updatedBy: elasticUser, + description: 'nice', + comments: null, +}; +export const actionTypeExecutorResult = { + actionId: 'string', + status: 'ok', + data: serviceConnector, +}; + +export const cases: Case[] = [ + basicCase, + { ...pushedCase, id: '1', totalComment: 0, comments: [] }, + { ...pushedCase, updatedAt: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCase, id: '3', totalComment: 0, comments: [] }, + { ...basicCase, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCases: AllCases = { + cases, + page: 1, + perPage: 5, + total: 10, + ...casesStatus, +}; +export const actionLicenses: ActionLicense[] = [ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }, +]; + +// Snake case for mock api responses +export const elasticUserSnake = { + full_name: 'Leslie Knope', + username: 'lknope', + email: 'leslie.knope@elastic.co', +}; +export const basicCommentSnake: CommentResponse = { + ...basicComment, + comment: 'Solve this fast!', + id: basicCommentId, + created_at: basicCreatedAt, + created_by: elasticUserSnake, + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, +}; + +export const basicCaseSnake: CaseResponse = { + ...basicCase, + status: 'open' as Status, + closed_at: null, + closed_by: null, + comments: [basicCommentSnake], + created_at: basicCreatedAt, + created_by: elasticUserSnake, + external_service: null, + updated_at: basicUpdatedAt, + updated_by: elasticUserSnake, +}; + +export const casesStatusSnake: CasesStatusResponse = { + count_closed_cases: 130, + count_open_cases: 20, +}; + +export const pushSnake = { + connector_id: 'connector_id', + connector_name: 'connector name', + external_id: 'external_id', + external_title: 'external title', + external_url: 'basicPush.com', +}; +const basicPushSnake = { + ...pushSnake, + pushed_at: basicUpdatedAt, + pushed_by: elasticUserSnake, +}; +export const pushedCaseSnake = { + ...basicCaseSnake, + external_service: basicPushSnake, +}; + +export const reporters: string[] = ['alexis', 'kim', 'maria', 'steph']; +export const respReporters = [ + { username: 'alexis', full_name: null, email: null }, + { username: 'kim', full_name: null, email: null }, + { username: 'maria', full_name: null, email: null }, + { username: 'steph', full_name: null, email: null }, +]; +export const casesSnake: CasesResponse = [ + basicCaseSnake, + { ...pushedCaseSnake, id: '1', totalComment: 0, comments: [] }, + { ...pushedCaseSnake, updated_at: laterTime, id: '2', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '3', totalComment: 0, comments: [] }, + { ...basicCaseSnake, id: '4', totalComment: 0, comments: [] }, +]; + +export const allCasesSnake: CasesFindResponse = { + cases: casesSnake, + page: 1, + per_page: 5, + total: 10, + ...casesStatusSnake, +}; + +const basicActionSnake = { + action_at: basicCreatedAt, + action_by: elasticUserSnake, + old_value: null, + new_value: 'what a cool value', + case_id: basicCaseId, + comment_id: null, +}; +export const getUserActionSnake = (af: UserActionField, a: UserAction) => ({ + ...basicActionSnake, + action_id: `${af[0]}-${a}`, + action_field: af, + action: a, + comment_id: af[0] === 'comment' ? basicCommentId : null, + new_value: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const caseUserActionsSnake: CaseUserActionsResponse = [ + getUserActionSnake(['description'], 'create'), + getUserActionSnake(['comment'], 'create'), + getUserActionSnake(['description'], 'update'), +]; + +// user actions + +export const getUserAction = (af: UserActionField, a: UserAction) => ({ + ...basicAction, + actionId: `${af[0]}-${a}`, + actionField: af, + action: a, + commentId: af[0] === 'comment' ? basicCommentId : null, + newValue: + a === 'push-to-service' && af[0] === 'pushed' + ? JSON.stringify(basicPushSnake) + : basicAction.newValue, +}); + +export const caseUserActions: CaseUserActions[] = [ + getUserAction(['description'], 'create'), + getUserAction(['comment'], 'create'), + getUserAction(['description'], 'update'), +]; + +// components tests +export const useGetCasesMockState: UseGetCasesState = { + data: allCases, + loading: [], + selectedCases: [], + isError: false, + queryParams: DEFAULT_QUERY_PARAMS, + filterOptions: DEFAULT_FILTER_OPTIONS, +}; + +export const basicCaseClosed: Case = { + ...basicCase, + closedAt: '2020-02-25T23:06:33.798Z', + closedBy: elasticUser, + status: 'closed', +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/plugins/siem/public/containers/case/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/translations.ts rename to x-pack/plugins/siem/public/containers/case/translations.ts diff --git a/x-pack/plugins/siem/public/containers/case/types.ts b/x-pack/plugins/siem/public/containers/case/types.ts new file mode 100644 index 0000000000000..dde13dc38aca8 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/types.ts @@ -0,0 +1,121 @@ +/* + * 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 { User, UserActionField, UserAction } from '../../../../case/common/api'; + +export interface Comment { + id: string; + createdAt: string; + createdBy: ElasticUser; + comment: string; + pushedAt: string | null; + pushedBy: string | null; + updatedAt: string | null; + updatedBy: ElasticUser | null; + version: string; +} +export interface CaseUserActions { + actionId: string; + actionField: UserActionField; + action: UserAction; + actionAt: string; + actionBy: ElasticUser; + caseId: string; + commentId: string | null; + newValue: string | null; + oldValue: string | null; +} + +export interface CaseExternalService { + pushedAt: string; + pushedBy: ElasticUser; + connectorId: string; + connectorName: string; + externalId: string; + externalTitle: string; + externalUrl: string; +} +export interface Case { + id: string; + closedAt: string | null; + closedBy: ElasticUser | null; + comments: Comment[]; + createdAt: string; + createdBy: ElasticUser; + description: string; + externalService: CaseExternalService | null; + status: string; + tags: string[]; + title: string; + totalComment: number; + updatedAt: string | null; + updatedBy: ElasticUser | null; + version: string; +} + +export interface QueryParams { + page: number; + perPage: number; + sortField: SortFieldCase; + sortOrder: 'asc' | 'desc'; +} + +export interface FilterOptions { + search: string; + status: string; + tags: string[]; + reporters: User[]; +} + +export interface CasesStatus { + countClosedCases: number | null; + countOpenCases: number | null; +} + +export interface AllCases extends CasesStatus { + cases: Case[]; + page: number; + perPage: number; + total: number; +} + +export enum SortFieldCase { + createdAt = 'createdAt', + closedAt = 'closedAt', +} + +export interface ElasticUser { + readonly email?: string | null; + readonly fullName?: string | null; + readonly username?: string | null; +} + +export interface FetchCasesProps extends ApiProps { + queryParams?: QueryParams; + filterOptions?: FilterOptions; +} + +export interface ApiProps { + signal: AbortSignal; +} + +export interface BulkUpdateStatus { + status: string; + id: string; + version: string; +} +export interface ActionLicense { + id: string; + name: string; + enabled: boolean; + enabledInConfig: boolean; + enabledInLicense: boolean; +} + +export interface DeleteCase { + id: string; + title?: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx b/x-pack/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/plugins/siem/public/containers/case/use_bulk_update_case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx rename to x-pack/plugins/siem/public/containers/case/use_bulk_update_case.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx b/x-pack/plugins/siem/public/containers/case/use_delete_cases.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_delete_cases.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/plugins/siem/public/containers/case/use_delete_cases.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx rename to x-pack/plugins/siem/public/containers/case/use_delete_cases.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_action_license.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_action_license.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx b/x-pack/plugins/siem/public/containers/case/use_get_action_license.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_action_license.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_action_license.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_case_user_actions.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases_status.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases_status.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx b/x-pack/plugins/siem/public/containers/case/use_get_cases_status.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_cases_status.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_cases_status.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_reporters.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_reporters.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx b/x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx index 2fc9b8294c8e0..01679ae4ccd82 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from 'react'; import { isEmpty } from 'lodash/fp'; -import { User } from '../../../../../../plugins/case/common/api'; +import { User } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getReporters } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx b/x-pack/plugins/siem/public/containers/case/use_get_tags.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_tags.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/plugins/siem/public/containers/case/use_get_tags.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx rename to x-pack/plugins/siem/public/containers/case/use_get_tags.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_case.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_case.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_case.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/plugins/siem/public/containers/case/use_post_case.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_case.tsx index aeb50fc098eee..b33269f26e97d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_case.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { CasePostRequest } from '../../../../../../plugins/case/common/api'; +import { CasePostRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postCase } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_comment.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_comment.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/plugins/siem/public/containers/case/use_post_comment.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_comment.tsx index c6d34b5449977..c7d3b4125aada 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_comment.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { CommentRequest } from '../../../../../../plugins/case/common/api'; +import { CommentRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postComment } from './api'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx index b07a346a8da46..b9698c3e864e3 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx @@ -55,8 +55,8 @@ describe('usePostPushToService', () => { { connector_id: samplePush.connectorId, connector_name: samplePush.connectorName, - external_id: serviceConnector.incidentId, - external_title: serviceConnector.number, + external_id: serviceConnector.id, + external_title: serviceConnector.title, external_url: serviceConnector.url, }, abortCtrl.signal diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx rename to x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx index 89e7e18cf0688..c9d1b963f411a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -9,7 +9,7 @@ import { useReducer, useCallback } from 'react'; import { ServiceConnectorCaseResponse, ServiceConnectorCaseParams, -} from '../../../../../../plugins/case/common/api'; +} from '../../../../case/common/api'; import { errorToToaster, useStateToaster, displaySuccessToast } from '../../components/toasters'; import { getCase, pushToService, pushCase } from './api'; @@ -98,8 +98,8 @@ export const usePostPushToService = (): UsePostPushToService => { { connector_id: connectorId, connector_name: connectorName, - external_id: responseService.incidentId, - external_title: responseService.number, + external_id: responseService.id, + external_title: responseService.title, external_url: responseService.url, }, abortCtrl.signal @@ -180,7 +180,7 @@ export const formatServiceRequestData = (myCase: Case): ServiceConnectorCasePara : null, })), description, - incidentId: externalService?.externalId ?? null, + externalId: externalService?.externalId ?? null, title, updatedAt, updatedBy: diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx b/x-pack/plugins/siem/public/containers/case/use_update_case.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_update_case.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_update_case.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/plugins/siem/public/containers/case/use_update_case.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx rename to x-pack/plugins/siem/public/containers/case/use_update_case.tsx index 7ebbbba076c12..2f2fe18321246 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/plugins/siem/public/containers/case/use_update_case.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; -import { CasePatchRequest } from '../../../../../../plugins/case/common/api'; +import { CasePatchRequest } from '../../../../case/common/api'; import { patchCase } from './api'; import * as i18n from './translations'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx b/x-pack/plugins/siem/public/containers/case/use_update_comment.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.test.tsx rename to x-pack/plugins/siem/public/containers/case/use_update_comment.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/plugins/siem/public/containers/case/use_update_comment.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx rename to x-pack/plugins/siem/public/containers/case/use_update_comment.tsx diff --git a/x-pack/plugins/siem/public/containers/case/utils.ts b/x-pack/plugins/siem/public/containers/case/utils.ts new file mode 100644 index 0000000000000..aaa5ff4ab44c1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/case/utils.ts @@ -0,0 +1,99 @@ +/* + * 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 { camelCase, isArray, isObject, set } from 'lodash'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { + CasesFindResponse, + CasesFindResponseRt, + CaseResponse, + CaseResponseRt, + CasesResponse, + CasesResponseRt, + CasesStatusResponseRt, + CasesStatusResponse, + throwErrors, + CasesConfigureResponse, + CaseConfigureResponseRt, + CaseUserActionsResponse, + CaseUserActionsResponseRt, + ServiceConnectorCaseResponseRt, + ServiceConnectorCaseResponse, +} from '../../../../case/common/api'; +import { ToasterError } from '../../components/toasters'; +import { AllCases, Case } from './types'; + +export const getTypedPayload = <T>(a: unknown): T => a as T; + +export const convertArrayToCamelCase = (arrayOfSnakes: unknown[]): unknown[] => + arrayOfSnakes.reduce((acc: unknown[], value) => { + if (isArray(value)) { + return [...acc, convertArrayToCamelCase(value)]; + } else if (isObject(value)) { + return [...acc, convertToCamelCase(value)]; + } else { + return [...acc, value]; + } + }, []); + +export const convertToCamelCase = <T, U extends {}>(snakeCase: T): U => + Object.entries(snakeCase).reduce((acc, [key, value]) => { + if (isArray(value)) { + set(acc, camelCase(key), convertArrayToCamelCase(value)); + } else if (isObject(value)) { + set(acc, camelCase(key), convertToCamelCase(value)); + } else { + set(acc, camelCase(key), value); + } + return acc; + }, {} as U); + +export const convertAllCasesToCamel = (snakeCases: CasesFindResponse): AllCases => ({ + cases: snakeCases.cases.map(snakeCase => convertToCamelCase<CaseResponse, Case>(snakeCase)), + countClosedCases: snakeCases.count_closed_cases, + countOpenCases: snakeCases.count_open_cases, + page: snakeCases.page, + perPage: snakeCases.per_page, + total: snakeCases.total, +}); + +export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => + pipe( + CasesStatusResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const createToasterPlainError = (message: string) => new ToasterError([message]); + +export const decodeCaseResponse = (respCase?: CaseResponse) => + pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesResponse = (respCase?: CasesResponse) => + pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => + pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => + pipe( + CaseConfigureResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) => + pipe( + CaseUserActionsResponseRt.decode(respUserActions), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => + pipe( + ServiceConnectorCaseResponseRt.decode(respPushCase), + fold(throwErrors(createToasterPlainError), identity) + ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.test.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/api.test.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts new file mode 100644 index 0000000000000..c1fadf289ef4d --- /dev/null +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -0,0 +1,331 @@ +/* + * 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 { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_PREPACKAGED_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, + DETECTION_ENGINE_TAGS_URL, +} from '../../../../common/constants'; +import { + AddRulesProps, + DeleteRulesProps, + DuplicateRulesProps, + EnableRulesProps, + FetchRulesProps, + FetchRulesResponse, + NewRule, + Rule, + FetchRuleProps, + BasicFetchProps, + ImportDataProps, + ExportDocumentsProps, + RuleStatusResponse, + ImportDataResponse, + PrePackagedRulesStatusResponse, + BulkRuleResponse, +} from './types'; +import { KibanaServices } from '../../../lib/kibana'; +import * as i18n from '../../../pages/detection_engine/rules/translations'; + +/** + * Add provided Rule + * + * @param rule to add + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const addRule = async ({ rule, signal }: AddRulesProps): Promise<NewRule> => + KibanaServices.get().http.fetch<NewRule>(DETECTION_ENGINE_RULES_URL, { + method: rule.id != null ? 'PUT' : 'POST', + body: JSON.stringify(rule), + signal, + }); + +/** + * Fetches all rules from the Detection Engine API + * + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param pagination desired pagination options (e.g. page/perPage) + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + signal, +}: FetchRulesProps): Promise<FetchRulesResponse> => { + const filters = [ + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), + ...(filterOptions.showCustomRules + ? [`alert.attributes.tags: "__internal_immutable:false"`] + : []), + ...(filterOptions.showElasticRules + ? [`alert.attributes.tags: "__internal_immutable:true"`] + : []), + ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), + ]; + + const query = { + page: pagination.page, + per_page: pagination.perPage, + sort_field: filterOptions.sortField, + sort_order: filterOptions.sortOrder, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; + + return KibanaServices.get().http.fetch<FetchRulesResponse>( + `${DETECTION_ENGINE_RULES_URL}/_find`, + { + method: 'GET', + query, + signal, + } + ); +}; + +/** + * Fetch a Rule by providing a Rule ID + * + * @param id Rule ID's (not rule_id) + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise<Rule> => + KibanaServices.get().http.fetch<Rule>(DETECTION_ENGINE_RULES_URL, { + method: 'GET', + query: { id }, + signal, + }); + +/** + * Enables/Disables provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to enable/disable + * @param enabled to enable or disable + * + * @throws An error if response is not OK + */ +export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise<BulkRuleResponse> => + KibanaServices.get().http.fetch<BulkRuleResponse>(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { + method: 'PATCH', + body: JSON.stringify(ids.map(id => ({ id, enabled }))), + }); + +/** + * Deletes provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to delete + * + * @throws An error if response is not OK + */ +export const deleteRules = async ({ ids }: DeleteRulesProps): Promise<BulkRuleResponse> => + KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { + method: 'DELETE', + body: JSON.stringify(ids.map(id => ({ id }))), + }); + +/** + * Duplicates provided Rules + * + * @param rules to duplicate + * + * @throws An error if response is not OK + */ +export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise<BulkRuleResponse> => + KibanaServices.get().http.fetch<Rule[]>(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { + method: 'POST', + body: JSON.stringify( + rules.map(rule => ({ + ...rule, + name: `${rule.name} [${i18n.DUPLICATE}]`, + created_at: undefined, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_at: undefined, + updated_by: undefined, + enabled: rule.enabled, + immutable: undefined, + last_success_at: undefined, + last_success_message: undefined, + last_failure_at: undefined, + last_failure_message: undefined, + status: undefined, + status_date: undefined, + })) + ), + }); + +/** + * Create Prepackaged Rules + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise<boolean> => { + await KibanaServices.get().http.fetch<unknown>(DETECTION_ENGINE_PREPACKAGED_URL, { + method: 'PUT', + signal, + }); + + return true; +}; + +/** + * Imports rules in the same format as exported via the _export API + * + * @param fileToImport File to upload containing rules to import + * @param overwrite whether or not to overwrite rules with the same ruleId + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const importRules = async ({ + fileToImport, + overwrite = false, + signal, +}: ImportDataProps): Promise<ImportDataResponse> => { + const formData = new FormData(); + formData.append('file', fileToImport); + + return KibanaServices.get().http.fetch<ImportDataResponse>( + `${DETECTION_ENGINE_RULES_URL}/_import`, + { + method: 'POST', + headers: { 'Content-Type': undefined }, + query: { overwrite }, + body: formData, + signal, + } + ); +}; + +/** + * Export rules from the server as a file download + * + * @param excludeExportDetails whether or not to exclude additional details at bottom of exported file (defaults to false) + * @param filename of exported rules. Be sure to include `.ndjson` extension! (defaults to localized `rules_export.ndjson`) + * @param ruleIds array of rule_id's (not id!) to export (empty array exports _all_ rules) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const exportRules = async ({ + excludeExportDetails = false, + filename = `${i18n.EXPORT_FILENAME}.ndjson`, + ids = [], + signal, +}: ExportDocumentsProps): Promise<Blob> => { + const body = + ids.length > 0 ? JSON.stringify({ objects: ids.map(rule => ({ rule_id: rule })) }) : undefined; + + return KibanaServices.get().http.fetch<Blob>(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + }); +}; + +/** + * Get Rule Status provided Rule ID + * + * @param id string of Rule ID's (not rule_id) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRuleStatusById = async ({ + id, + signal, +}: { + id: string; + signal: AbortSignal; +}): Promise<RuleStatusResponse> => + KibanaServices.get().http.fetch<RuleStatusResponse>(DETECTION_ENGINE_RULES_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ ids: [id] }), + signal, + }); + +/** + * Return rule statuses given list of alert ids + * + * @param ids array of string of Rule ID's (not rule_id) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRulesStatusByIds = async ({ + ids, + signal, +}: { + ids: string[]; + signal: AbortSignal; +}): Promise<RuleStatusResponse> => { + const res = await KibanaServices.get().http.fetch<RuleStatusResponse>( + DETECTION_ENGINE_RULES_STATUS_URL, + { + method: 'POST', + body: JSON.stringify({ ids }), + signal, + } + ); + return res; +}; + +/** + * Fetch all unique Tags used by Rules + * + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise<string[]> => + KibanaServices.get().http.fetch<string[]>(DETECTION_ENGINE_TAGS_URL, { + method: 'GET', + signal, + }); + +/** + * Get pre packaged rules Status + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getPrePackagedRulesStatus = async ({ + signal, +}: { + signal: AbortSignal; +}): Promise<PrePackagedRulesStatusResponse> => + KibanaServices.get().http.fetch<PrePackagedRulesStatusResponse>( + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, + { + method: 'GET', + signal, + } + ); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx index 83b8a3581a4be..8c688fe5615f0 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -6,7 +6,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { DEFAULT_INDEX_PATTERN } from '../../../../../../../plugins/siem/common/constants'; +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { useApolloClient } from '../../../utils/apollo_context'; import { mocksSource } from '../../source/mock'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx index c5aefac15f48e..7e222045a1a3b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -8,7 +8,7 @@ import { isEmpty, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; import deepEqual from 'fast-deep-equal'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; import { BrowserFields, getBrowserFields, diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/index.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/mock.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/translations.ts rename to x-pack/plugins/siem/public/containers/detection_engine/rules/translations.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..f89d21ef1aeb1 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -0,0 +1,246 @@ +/* + * 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 * as t from 'io-ts'; + +import { RuleTypeSchema } from '../../../../common/detection_engine/types'; + +/** + * Params is an "record", since it is a type of AlertActionParams which is action templates. + * @see x-pack/plugins/alerting/common/alert.ts + */ +export const action = t.exact( + t.type({ + group: t.string, + id: t.string, + action_type_id: t.string, + params: t.record(t.string, t.any), + }) +); + +export const NewRuleSchema = t.intersection([ + t.type({ + description: t.string, + enabled: t.boolean, + interval: t.string, + name: t.string, + risk_score: t.number, + severity: t.string, + type: RuleTypeSchema, + }), + t.partial({ + actions: t.array(action), + anomaly_threshold: t.number, + created_by: t.string, + false_positives: t.array(t.string), + filters: t.array(t.unknown), + from: t.string, + id: t.string, + index: t.array(t.string), + language: t.string, + machine_learning_job_id: t.string, + max_signals: t.number, + query: t.string, + references: t.array(t.string), + rule_id: t.string, + saved_id: t.string, + tags: t.array(t.string), + threat: t.array(t.unknown), + throttle: t.union([t.string, t.null]), + to: t.string, + updated_by: t.string, + note: t.string, + }), +]); + +export const NewRulesSchema = t.array(NewRuleSchema); +export type NewRule = t.TypeOf<typeof NewRuleSchema>; + +export interface AddRulesProps { + rule: NewRule; + signal: AbortSignal; +} + +const MetaRule = t.intersection([ + t.type({ + from: t.string, + }), + t.partial({ + throttle: t.string, + kibana_siem_app_url: t.string, + }), +]); + +export const RuleSchema = t.intersection([ + t.type({ + created_at: t.string, + created_by: t.string, + description: t.string, + enabled: t.boolean, + false_positives: t.array(t.string), + from: t.string, + id: t.string, + interval: t.string, + immutable: t.boolean, + name: t.string, + max_signals: t.number, + references: t.array(t.string), + risk_score: t.number, + rule_id: t.string, + severity: t.string, + tags: t.array(t.string), + type: RuleTypeSchema, + to: t.string, + threat: t.array(t.unknown), + updated_at: t.string, + updated_by: t.string, + actions: t.array(action), + throttle: t.union([t.string, t.null]), + }), + t.partial({ + anomaly_threshold: t.number, + filters: t.array(t.unknown), + index: t.array(t.string), + language: t.string, + last_failure_at: t.string, + last_failure_message: t.string, + meta: MetaRule, + machine_learning_job_id: t.string, + output_index: t.string, + query: t.string, + saved_id: t.string, + status: t.string, + status_date: t.string, + timeline_id: t.string, + timeline_title: t.string, + note: t.string, + version: t.number, + }), +]); + +export const RulesSchema = t.array(RuleSchema); + +export type Rule = t.TypeOf<typeof RuleSchema>; +export type Rules = t.TypeOf<typeof RulesSchema>; + +export interface RuleError { + id?: string; + rule_id?: string; + error: { status_code: number; message: string }; +} + +export type BulkRuleResponse = Array<Rule | RuleError>; + +export interface RuleResponseBuckets { + rules: Rule[]; + errors: RuleError[]; +} + +export interface PaginationOptions { + page: number; + perPage: number; + total: number; +} + +export interface FetchRulesProps { + pagination?: PaginationOptions; + filterOptions?: FilterOptions; + signal: AbortSignal; +} + +export interface FilterOptions { + filter: string; + sortField: string; + sortOrder: 'asc' | 'desc'; + showCustomRules?: boolean; + showElasticRules?: boolean; + tags?: string[]; +} + +export interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: Rule[]; +} + +export interface FetchRuleProps { + id: string; + signal: AbortSignal; +} + +export interface EnableRulesProps { + ids: string[]; + enabled: boolean; +} + +export interface DeleteRulesProps { + ids: string[]; +} + +export interface DuplicateRulesProps { + rules: Rule[]; +} + +export interface BasicFetchProps { + signal: AbortSignal; +} + +export interface ImportDataProps { + fileToImport: File; + overwrite?: boolean; + signal: AbortSignal; +} + +export interface ImportRulesResponseError { + rule_id: string; + error: { + status_code: number; + message: string; + }; +} + +export interface ImportDataResponse { + success: boolean; + success_count: number; + errors: ImportRulesResponseError[]; +} + +export interface ExportDocumentsProps { + ids: string[]; + filename?: string; + excludeExportDetails?: boolean; + signal: AbortSignal; +} + +export interface RuleStatus { + current_status: RuleInfoStatus; + failures: RuleInfoStatus[]; +} + +export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; +export interface RuleInfoStatus { + alert_id: string; + status_date: string; + status: RuleStatusType | null; + last_failure_at: string | null; + last_success_at: string | null; + last_failure_message: string | null; + last_success_message: string | null; + last_look_back_date: string | null | undefined; + gap: string | null | undefined; + bulk_create_time_durations: string[] | null | undefined; + search_after_time_durations: string[] | null | undefined; +} + +export type RuleStatusResponse = Record<string, RuleStatus>; + +export interface PrePackagedRulesStatusResponse { + rules_custom_installed: number; + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx new file mode 100644 index 0000000000000..f74c2bad1019e --- /dev/null +++ b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx @@ -0,0 +1,180 @@ +/* + * 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 { renderHook, act, cleanup } from '@testing-library/react-hooks'; +import { + useRuleStatus, + ReturnRuleStatus, + useRulesStatuses, + ReturnRulesStatuses, +} from './use_rule_status'; +import * as api from './api'; +import { Rule } from '../rules/types'; + +jest.mock('./api'); + +const testRule: Rule = { + actions: [ + { + group: 'fake group', + id: 'fake id', + action_type_id: 'fake action_type_id', + params: { + someKey: 'someVal', + }, + }, + ], + created_at: 'mm/dd/yyyyTHH:MM:sssz', + created_by: 'mockUser', + description: 'some desc', + enabled: true, + false_positives: [], + filters: [], + from: 'now-360s', + id: '12345678987654321', + immutable: false, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '5m', + language: 'kuery', + name: 'Test rule', + max_signals: 100, + query: "user.email: 'root@elastic.co'", + references: [], + risk_score: 75, + rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', + severity: 'high', + tags: ['APM'], + threat: [], + throttle: null, + to: 'now', + type: 'query', + updated_at: 'mm/dd/yyyyTHH:MM:sssz', + updated_by: 'mockUser', +}; + +describe('useRuleStatus', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + afterEach(async () => { + cleanup(); + }); + + describe('useRuleStatus', () => { + test('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + expect(result.current).toEqual([true, null, null]); + }); + }); + + test('fetch rule status', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual([ + false, + { + current_status: { + alert_id: 'alertId', + last_failure_at: null, + last_failure_message: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_success_message: 'it is a success', + status: 'succeeded', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + gap: null, + bulk_create_time_durations: ['2235.01'], + search_after_time_durations: ['616.97'], + last_look_back_date: '2020-03-19T00:32:07.996Z', + }, + failures: [], + }, + result.current[2], + ]); + }); + }); + + test('re-fetch rule status', async () => { + const spyOngetRuleStatusById = jest.spyOn(api, 'getRuleStatusById'); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnRuleStatus>(() => + useRuleStatus('myOwnRuleID') + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + if (result.current[2]) { + result.current[2]('myOwnRuleID'); + } + await waitForNextUpdate(); + expect(spyOngetRuleStatusById).toHaveBeenCalledTimes(2); + }); + }); + }); + + describe('useRulesStatuses', () => { + test('init rules statuses', async () => { + const payload = [testRule]; + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnRulesStatuses>(() => + useRulesStatuses(payload) + ); + await waitForNextUpdate(); + expect(result.current).toEqual({ loading: false, rulesStatuses: [] }); + }); + }); + + test('fetch rules statuses', async () => { + const payload = [testRule]; + await act(async () => { + const { result, waitForNextUpdate } = renderHook<string, ReturnRulesStatuses>(() => + useRulesStatuses(payload) + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + loading: false, + rulesStatuses: [ + { + current_status: { + alert_id: 'alertId', + bulk_create_time_durations: ['2235.01'], + gap: null, + last_failure_at: null, + last_failure_message: null, + last_look_back_date: '2020-03-19T00:32:07.996Z', + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_success_message: 'it is a success', + search_after_time_durations: ['616.97'], + status: 'succeeded', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + }, + failures: [], + id: '12345678987654321', + activate: true, + name: 'Test rule', + }, + ], + }); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.test.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/api.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.test.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/api.test.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/api.ts new file mode 100644 index 0000000000000..1397e4a8696be --- /dev/null +++ b/x-pack/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -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 { + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_INDEX_URL, + DETECTION_ENGINE_PRIVILEGES_URL, +} from '../../../../common/constants'; +import { KibanaServices } from '../../../lib/kibana'; +import { + BasicSignals, + Privilege, + QuerySignals, + SignalSearchResponse, + SignalsIndex, + UpdateSignalStatusProps, +} from './types'; + +/** + * Fetch Signals by providing a query + * + * @param query String to match a dsl + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchQuerySignals = async <Hit, Aggregations>({ + query, + signal, +}: QuerySignals): Promise<SignalSearchResponse<Hit, Aggregations>> => + KibanaServices.get().http.fetch<SignalSearchResponse<Hit, Aggregations>>( + DETECTION_ENGINE_QUERY_SIGNALS_URL, + { + method: 'POST', + body: JSON.stringify(query), + signal, + } + ); + +/** + * Update signal status by query + * + * @param query of signals to update + * @param status to update to('open' / 'closed') + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const updateSignalStatus = async ({ + query, + status, + signal, +}: UpdateSignalStatusProps): Promise<unknown> => + KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ status, ...query }), + signal, + }); + +/** + * Fetch Signal Index + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => + KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); + +/** + * Get User Privileges + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise<Privilege> => + KibanaServices.get().http.fetch<Privilege>(DETECTION_ENGINE_PRIVILEGES_URL, { + method: 'GET', + signal, + }); + +/** + * Create Signal Index if needed it + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const createSignalIndex = async ({ signal }: BasicSignals): Promise<SignalsIndex> => + KibanaServices.get().http.fetch<SignalsIndex>(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/mock.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/mock.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/mock.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/translations.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts rename to x-pack/plugins/siem/public/containers/detection_engine/signals/types.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_query.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx rename to x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/errors/index.test.tsx b/x-pack/plugins/siem/public/containers/errors/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/errors/index.test.tsx rename to x-pack/plugins/siem/public/containers/errors/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/errors/index.tsx b/x-pack/plugins/siem/public/containers/errors/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/errors/index.tsx rename to x-pack/plugins/siem/public/containers/errors/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/errors/translations.ts b/x-pack/plugins/siem/public/containers/errors/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/errors/translations.ts rename to x-pack/plugins/siem/public/containers/errors/translations.ts diff --git a/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts new file mode 100644 index 0000000000000..9cae503d30940 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash/fp'; +import React, { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetLastEventTimeQuery, LastEventIndexKey, LastTimeDetails } from '../../../graphql/types'; +import { inputsModel } from '../../../store'; +import { QueryTemplateProps } from '../../query_template'; +import { useUiSetting$ } from '../../../lib/kibana'; + +import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; +import { useApolloClient } from '../../../utils/apollo_context'; + +export interface LastEventTimeArgs { + id: string; + errorMessage: string; + lastSeen: Date; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: LastEventTimeArgs) => React.ReactNode; + indexKey: LastEventIndexKey; +} + +export function useLastEventTimeQuery<TCache = object>( + indexKey: LastEventIndexKey, + details: LastTimeDetails, + sourceId: string +) { + const [loading, updateLoading] = useState(false); + const [lastSeen, updateLastSeen] = useState<number | null>(null); + const [errorMessage, updateErrorMessage] = useState<string | null>(null); + const [currentIndexKey, updateCurrentIndexKey] = useState<LastEventIndexKey | null>(null); + const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const apolloClient = useApolloClient(); + async function fetchLastEventTime(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query<GetLastEventTimeQuery.Query, GetLastEventTimeQuery.Variables>({ + query: LastEventTimeGqlQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + indexKey, + details, + defaultIndex, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateLastSeen(get('data.source.LastEventTime.lastSeen', result)); + updateErrorMessage(null); + updateCurrentIndexKey(currentIndexKey); + }, + error => { + updateLoading(false); + updateLastSeen(null); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchLastEventTime(signal); + return () => abortCtrl.abort(); + }, [apolloClient, indexKey, details.hostName, details.ip]); + + return { lastSeen, loading, errorMessage }; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts rename to x-pack/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts new file mode 100644 index 0000000000000..43f55dfcf2777 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { GetLastEventTimeQuery, LastEventIndexKey } from '../../../graphql/types'; + +import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; + +interface MockLastEventTimeQuery { + request: { + query: GetLastEventTimeQuery.Query; + variables: GetLastEventTimeQuery.Variables; + }; + result: { + data?: { + source: { + id: string; + LastEventTime: { + lastSeen: string | null; + errorMessage: string | null; + }; + }; + }; + errors?: [{ message: string }]; + }; +} + +const getTimeTwelveMinutesAgo = () => { + const d = new Date(); + const ts = d.getTime(); + const twelveMinutes = ts - 12 * 60 * 1000; + return new Date(twelveMinutes).toISOString(); +}; + +export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ + { + request: { + query: LastEventTimeGqlQuery, + variables: { + sourceId: 'default', + indexKey: LastEventIndexKey.hosts, + details: {}, + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + LastEventTime: { + lastSeen: getTimeTwelveMinutesAgo(), + errorMessage: null, + }, + }, + }, + }, + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx b/x-pack/plugins/siem/public/containers/global_time/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/global_time/index.tsx rename to x-pack/plugins/siem/public/containers/global_time/index.tsx diff --git a/x-pack/plugins/siem/public/containers/helpers.test.ts b/x-pack/plugins/siem/public/containers/helpers.test.ts new file mode 100644 index 0000000000000..5d378d79acc7a --- /dev/null +++ b/x-pack/plugins/siem/public/containers/helpers.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { ESQuery } from '../../common/typed_json'; + +import { createFilter } from './helpers'; + +describe('Helpers', () => { + describe('#createFilter', () => { + test('if it is a string it returns untouched', () => { + const filter = createFilter('even invalid strings return the same'); + expect(filter).toBe('even invalid strings return the same'); + }); + + test('if it is an ESQuery object it will be returned as a string', () => { + const query: ESQuery = { term: { 'host.id': 'host-value' } }; + const filter = createFilter(query); + expect(filter).toBe(JSON.stringify(query)); + }); + + test('if it is undefined, then undefined is returned', () => { + const filter = createFilter(undefined); + expect(filter).toBe(undefined); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/containers/helpers.ts b/x-pack/plugins/siem/public/containers/helpers.ts new file mode 100644 index 0000000000000..5f66e3f4b88d4 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/helpers.ts @@ -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 { FetchPolicy } from 'apollo-client'; +import { isString } from 'lodash/fp'; + +import { ESQuery } from '../../common/typed_json'; + +export const createFilter = (filterQuery: ESQuery | string | undefined) => + isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); + +export const getDefaultFetchPolicy = (): FetchPolicy => 'cache-and-network'; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts rename to x-pack/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts new file mode 100644 index 0000000000000..a460fa8999b57 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ApolloClient from 'apollo-client'; +import { get } from 'lodash/fp'; +import React, { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { useUiSetting$ } from '../../../lib/kibana'; +import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; +import { inputsModel } from '../../../store'; +import { QueryTemplateProps } from '../../query_template'; + +import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; + +export interface FirstLastSeenHostArgs { + id: string; + errorMessage: string; + firstSeen: Date; + lastSeen: Date; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: FirstLastSeenHostArgs) => React.ReactNode; + hostName: string; +} + +export function useFirstLastSeenHostQuery<TCache = object>( + hostName: string, + sourceId: string, + apolloClient: ApolloClient<TCache> +) { + const [loading, updateLoading] = useState(false); + const [firstSeen, updateFirstSeen] = useState<Date | null>(null); + const [lastSeen, updateLastSeen] = useState<Date | null>(null); + const [errorMessage, updateErrorMessage] = useState<string | null>(null); + const [defaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + + async function fetchFirstLastSeenHost(signal: AbortSignal) { + updateLoading(true); + return apolloClient + .query<GetHostFirstLastSeenQuery.Query, GetHostFirstLastSeenQuery.Variables>({ + query: HostFirstLastSeenGqlQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + hostName, + defaultIndex, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); + updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); + updateErrorMessage(null); + }, + error => { + updateLoading(false); + updateFirstSeen(null); + updateLastSeen(null); + updateErrorMessage(error.message); + } + ); + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchFirstLastSeenHost(signal); + return () => abortCtrl.abort(); + }, []); + + return { firstSeen, lastSeen, loading, errorMessage }; +} diff --git a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts new file mode 100644 index 0000000000000..f59df84dacc1b --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; + +import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; + +interface MockedProvidedQuery { + request: { + query: GetHostFirstLastSeenQuery.Query; + variables: GetHostFirstLastSeenQuery.Variables; + }; + result: { + data?: { + source: { + id: string; + HostFirstLastSeen: { + firstSeen: string | null; + lastSeen: string | null; + }; + }; + }; + errors?: [{ message: string }]; + }; +} +export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ + { + request: { + query: HostFirstLastSeenGqlQuery, + variables: { + sourceId: 'default', + hostName: 'kibana-siem', + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + HostFirstLastSeen: { + firstSeen: '2019-04-08T16:09:40.692Z', + lastSeen: '2019-04-08T18:35:45.064Z', + }, + }, + }, + }, + }, +]; diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts b/x-pack/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts rename to x-pack/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/hosts/index.tsx b/x-pack/plugins/siem/public/containers/hosts/index.tsx new file mode 100644 index 0000000000000..733c2224d840a --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/index.tsx @@ -0,0 +1,183 @@ +/* + * 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 { get, getOr } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + Direction, + GetHostsTableQuery, + HostsEdges, + HostsFields, + PageInfoPaginated, +} from '../../graphql/types'; +import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; + +import { HostsTableQuery } from './hosts_table.gql_query'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; + +const ID = 'hostsQuery'; + +export interface HostsArgs { + endDate: number; + hosts: HostsEdges[]; + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: number; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: HostsArgs) => React.ReactNode; + type: hostsModel.HostsType; + startDate: number; + endDate: number; +} + +export interface HostsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sortField: HostsFields; + direction: Direction; +} + +type HostsProps = OwnProps & HostsComponentReduxProps & WithKibanaProps; + +class HostsComponentQuery extends QueryTemplatePaginated< + HostsProps, + GetHostsTableQuery.Query, + GetHostsTableQuery.Variables +> { + private memoizedHosts: ( + variables: string, + data: GetHostsTableQuery.Source | undefined + ) => HostsEdges[]; + + constructor(props: HostsProps) { + super(props); + this.memoizedHosts = memoizeOne(this.getHosts); + } + + public render() { + const { + activePage, + id = ID, + isInspected, + children, + direction, + filterQuery, + endDate, + kibana, + limit, + startDate, + skip, + sourceId, + sortField, + } = this.props; + const defaultIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); + + const variables: GetHostsTableQuery.Variables = { + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: { + direction, + field: sortField, + }, + pagination: generateTablePaginationOptions(activePage, limit), + filterQuery: createFilter(filterQuery), + defaultIndex, + inspect: isInspected, + }; + return ( + <Query<GetHostsTableQuery.Query, GetHostsTableQuery.Variables> + query={HostsTableQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={variables} + skip={skip} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Hosts: { + ...fetchMoreResult.source.Hosts, + edges: [...fetchMoreResult.source.Hosts.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + endDate, + hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)), + id, + inspect: getOr(null, 'source.Hosts.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Hosts.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + startDate, + totalCount: getOr(-1, 'source.Hosts.totalCount', data), + }); + }} + </Query> + ); + } + + private getHosts = ( + variables: string, + source: GetHostsTableQuery.Source | undefined + ): HostsEdges[] => getOr([], 'Hosts.edges', source); +} + +const makeMapStateToProps = () => { + const getHostsSelector = hostsSelectors.hostsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getHostsSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +export const HostsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(HostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts b/x-pack/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts rename to x-pack/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx b/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx new file mode 100644 index 0000000000000..5057e872b5313 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel, inputsSelectors, State } from '../../../store'; +import { getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplate, QueryTemplateProps } from '../../query_template'; +import { withKibana, WithKibanaProps } from '../../../lib/kibana'; + +import { HostOverviewQuery } from './host_overview.gql_query'; +import { GetHostOverviewQuery, HostItem } from '../../../graphql/types'; + +const ID = 'hostOverviewQuery'; + +export interface HostOverviewArgs { + id: string; + inspect: inputsModel.InspectQuery; + hostOverview: HostItem; + loading: boolean; + refetch: inputsModel.Refetch; + startDate: number; + endDate: number; +} + +export interface HostOverviewReduxProps { + isInspected: boolean; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: HostOverviewArgs) => React.ReactNode; + hostName: string; + startDate: number; + endDate: number; +} + +type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; + +class HostOverviewByNameComponentQuery extends QueryTemplate< + HostsOverViewProps, + GetHostOverviewQuery.Query, + GetHostOverviewQuery.Variables +> { + public render() { + const { + id = ID, + isInspected, + children, + hostName, + kibana, + skip, + sourceId, + startDate, + endDate, + } = this.props; + return ( + <Query<GetHostOverviewQuery.Query, GetHostOverviewQuery.Variables> + query={HostOverviewQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + hostName, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const hostOverview = getOr([], 'source.HostOverview', data); + return children({ + id, + inspect: getOr(null, 'source.HostOverview.inspect', data), + refetch, + loading, + hostOverview, + startDate, + endDate, + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const HostOverviewByNameQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(HostOverviewByNameComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/ip_overview/index.gql_query.ts b/x-pack/plugins/siem/public/containers/ip_overview/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/ip_overview/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/ip_overview/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/ip_overview/index.tsx b/x-pack/plugins/siem/public/containers/ip_overview/index.tsx new file mode 100644 index 0000000000000..ade94c430c6ef --- /dev/null +++ b/x-pack/plugins/siem/public/containers/ip_overview/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetIpOverviewQuery, IpOverviewData } from '../../graphql/types'; +import { networkModel, inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { ipOverviewQuery } from './index.gql_query'; + +const ID = 'ipOverviewQuery'; + +export interface IpOverviewArgs { + id: string; + inspect: inputsModel.InspectQuery; + ipOverviewData: IpOverviewData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface IpOverviewProps extends QueryTemplateProps { + children: (args: IpOverviewArgs) => React.ReactNode; + type: networkModel.NetworkType; + ip: string; +} + +const IpOverviewComponentQuery = React.memo<IpOverviewProps & PropsFromRedux>( + ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( + <Query<GetIpOverviewQuery.Query, GetIpOverviewQuery.Variables> + query={ipOverviewQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + filterQuery: createFilter(filterQuery), + ip, + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const init: IpOverviewData = { host: {} }; + const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data); + return children({ + id, + inspect: getOr(null, 'source.IpOverview.inspect', data), + ipOverviewData, + loading, + refetch, + }); + }} + </Query> + ) +); + +IpOverviewComponentQuery.displayName = 'IpOverviewComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: IpOverviewProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const IpOverviewQuery = connector(IpOverviewComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx b/x-pack/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx rename to x-pack/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx diff --git a/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx b/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx new file mode 100644 index 0000000000000..de9d54b1a185c --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { kpiHostDetailsQuery } from './index.gql_query'; + +const ID = 'kpiHostDetailsQuery'; + +export interface KpiHostDetailsArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiHostDetails: KpiHostDetailsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface QueryKpiHostDetailsProps extends QueryTemplateProps { + children: (args: KpiHostDetailsArgs) => React.ReactNode; +} + +const KpiHostDetailsComponentQuery = React.memo<QueryKpiHostDetailsProps & PropsFromRedux>( + ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( + <Query<GetKpiHostDetailsQuery.Query, GetKpiHostDetailsQuery.Variables> + query={kpiHostDetailsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiHostDetails = getOr({}, `source.KpiHostDetails`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiHostDetails.inspect', data), + kpiHostDetails, + loading, + refetch, + }); + }} + </Query> + ) +); + +KpiHostDetailsComponentQuery.displayName = 'KpiHostDetailsComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: QueryKpiHostDetailsProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const KpiHostDetailsQuery = connector(KpiHostDetailsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts b/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx new file mode 100644 index 0000000000000..5be2423e8a162 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { kpiHostsQuery } from './index.gql_query'; + +const ID = 'kpiHostsQuery'; + +export interface KpiHostsArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiHosts: KpiHostsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiHostsProps extends QueryTemplateProps { + children: (args: KpiHostsArgs) => React.ReactNode; +} + +const KpiHostsComponentQuery = React.memo<KpiHostsProps & PropsFromRedux>( + ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( + <Query<GetKpiHostsQuery.Query, GetKpiHostsQuery.Variables> + query={kpiHostsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiHosts = getOr({}, `source.KpiHosts`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiHosts.inspect', data), + kpiHosts, + loading, + refetch, + }); + }} + </Query> + ) +); + +KpiHostsComponentQuery.displayName = 'KpiHostsComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: KpiHostsProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const KpiHostsQuery = connector(KpiHostsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/kpi_network/index.gql_query.ts b/x-pack/plugins/siem/public/containers/kpi_network/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/kpi_network/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/kpi_network/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/kpi_network/index.tsx b/x-pack/plugins/siem/public/containers/kpi_network/index.tsx new file mode 100644 index 0000000000000..338cdc39b178c --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kpi_network/index.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetKpiNetworkQuery, KpiNetworkData } from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { useUiSetting } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplateProps } from '../query_template'; + +import { kpiNetworkQuery } from './index.gql_query'; + +const ID = 'kpiNetworkQuery'; + +export interface KpiNetworkArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiNetwork: KpiNetworkData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiNetworkProps extends QueryTemplateProps { + children: (args: KpiNetworkArgs) => React.ReactNode; +} + +const KpiNetworkComponentQuery = React.memo<KpiNetworkProps & PropsFromRedux>( + ({ id = ID, children, filterQuery, isInspected, skip, sourceId, startDate, endDate }) => ( + <Query<GetKpiNetworkQuery.Query, GetKpiNetworkQuery.Variables> + query={kpiNetworkQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiNetwork = getOr({}, `source.KpiNetwork`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiNetwork.inspect', data), + kpiNetwork, + loading, + refetch, + }); + }} + </Query> + ) +); + +KpiNetworkComponentQuery.displayName = 'KpiNetworkComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: KpiNetworkProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const KpiNetworkQuery = connector(KpiNetworkComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx b/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx new file mode 100644 index 0000000000000..6120538a01e78 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { QuerySuggestion, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { useKibana } from '../../lib/kibana'; + +type RendererResult = React.ReactElement<JSX.Element> | null; +type RendererFunction<RenderArgs, Result = RendererResult> = (args: RenderArgs) => Result; + +interface KueryAutocompletionLifecycleProps { + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: QuerySuggestion[]; + }>; + indexPattern: IIndexPattern; +} + +interface KueryAutocompletionCurrentRequest { + expression: string; + cursorPosition: number; +} + +export const KueryAutocompletion = React.memo<KueryAutocompletionLifecycleProps>( + ({ children, indexPattern }) => { + const [currentRequest, setCurrentRequest] = useState<KueryAutocompletionCurrentRequest | null>( + null + ); + const [suggestions, setSuggestions] = useState<QuerySuggestion[]>([]); + const kibana = useKibana(); + const loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number + ) => { + const language = 'kuery'; + + if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { + return; + } + + const futureRequest = { + expression, + cursorPosition, + }; + setCurrentRequest({ + expression, + cursorPosition, + }); + setSuggestions([]); + + if ( + futureRequest && + futureRequest.expression !== (currentRequest && currentRequest.expression) && + futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) + ) { + const newSuggestions = + (await kibana.services.data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: [], + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + })) || []; + + setCurrentRequest(null); + setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); + } + }; + + return children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions, + suggestions, + }); + } +); + +KueryAutocompletion.displayName = 'KueryAutocompletion'; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts b/x-pack/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/containers/matrix_histogram/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/matrix_histogram/index.test.tsx rename to x-pack/plugins/siem/public/containers/matrix_histogram/index.test.tsx diff --git a/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts new file mode 100644 index 0000000000000..18bb611191bbc --- /dev/null +++ b/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { useEffect, useMemo, useState, useRef } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { useUiSetting$ } from '../../lib/kibana'; +import { createFilter } from '../helpers'; +import { useApolloClient } from '../../utils/apollo_context'; +import { inputsModel } from '../../store'; +import { MatrixHistogramGqlQuery } from './index.gql_query'; +import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../graphql/types'; + +export const useQuery = <Hit, Aggs, TCache = object>({ + endDate, + errorMessage, + filterQuery, + histogramType, + indexToAdd, + isInspected, + stackByField, + startDate, +}: MatrixHistogramQueryProps) => { + const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo<string[]>(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + + const [, dispatchToaster] = useStateToaster(); + const refetch = useRef<inputsModel.Refetch>(); + const [loading, setLoading] = useState<boolean>(false); + const [data, setData] = useState<MatrixOverTimeHistogramData[] | null>(null); + const [inspect, setInspect] = useState<inputsModel.InspectQuery | null>(null); + const [totalCount, setTotalCount] = useState<number>(-1); + const apolloClient = useApolloClient(); + + useEffect(() => { + const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { + filterQuery: createFilter(filterQuery), + sourceId: 'default', + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex, + inspect: isInspected, + stackByField, + histogramType, + }; + let isSubscribed = true; + const abortCtrl = new AbortController(); + const abortSignal = abortCtrl.signal; + + async function fetchData() { + if (!apolloClient) return null; + setLoading(true); + return apolloClient + .query<GetMatrixHistogramQuery.Query, GetMatrixHistogramQuery.Variables>({ + query: MatrixHistogramGqlQuery, + fetchPolicy: 'network-only', + variables: matrixHistogramVariables, + context: { + fetchOptions: { + abortSignal, + }, + }, + }) + .then( + result => { + if (isSubscribed) { + const source = result?.data?.source?.MatrixHistogram ?? {}; + setData(source?.matrixHistogramData ?? []); + setTotalCount(source?.totalCount ?? -1); + setInspect(source?.inspect ?? null); + setLoading(false); + } + }, + error => { + if (isSubscribed) { + setData(null); + setTotalCount(-1); + setInspect(null); + setLoading(false); + errorToToaster({ title: errorMessage, error, dispatchToaster }); + } + } + ); + } + refetch.current = fetchData; + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ + defaultIndex, + errorMessage, + filterQuery, + histogramType, + indexToAdd, + isInspected, + stackByField, + startDate, + endDate, + data, + ]); + + return { data, loading, inspect, totalCount, refetch: refetch.current }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_dns/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_dns/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_dns/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/plugins/siem/public/containers/network_dns/index.tsx new file mode 100644 index 0000000000000..04c8783c30a0f --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_dns/index.tsx @@ -0,0 +1,209 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DocumentNode } from 'graphql'; +import { ScaleType } from '@elastic/charts'; +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + GetNetworkDnsQuery, + NetworkDnsEdges, + NetworkDnsSortField, + PageInfoPaginated, + MatrixOverOrdinalHistogramData, +} from '../../graphql/types'; +import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkDnsQuery } from './index.gql_query'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; +import { MatrixHistogram } from '../../components/matrix_histogram'; +import { MatrixHistogramOption, GetSubTitle } from '../../components/matrix_histogram/types'; +import { UpdateDateRange } from '../../components/charts/common'; +import { SetQuery } from '../../pages/hosts/navigation/types'; + +const ID = 'networkDnsQuery'; +export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; +export interface NetworkDnsArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkDns: NetworkDnsEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + stackByField?: string; + totalCount: number; + histogram: MatrixOverOrdinalHistogramData[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkDnsArgs) => React.ReactNode; + type: networkModel.NetworkType; +} + +interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { + dataKey: string | string[]; + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + isDnsHistogram?: boolean; + query: DocumentNode; + scaleType: ScaleType; + setQuery: SetQuery; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string; + type: networkModel.NetworkType; + updateDateRange: UpdateDateRange; + yTickFormatter?: (value: number) => string; +} + +export interface NetworkDnsComponentReduxProps { + activePage: number; + sort: NetworkDnsSortField; + isInspected: boolean; + isPtrIncluded: boolean; + limit: number; +} + +type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; + +export class NetworkDnsComponentQuery extends QueryTemplatePaginated< + NetworkDnsProps, + GetNetworkDnsQuery.Query, + GetNetworkDnsQuery.Variables +> { + public render() { + const { + activePage, + children, + sort, + endDate, + filterQuery, + id = ID, + isInspected, + isPtrIncluded, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetNetworkDnsQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + isPtrIncluded, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + + return ( + <Query<GetNetworkDnsQuery.Query, GetNetworkDnsQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkDnsQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkDns = getOr([], `source.NetworkDns.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkDns: { + ...fetchMoreResult.source.NetworkDns, + edges: [...fetchMoreResult.source.NetworkDns.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkDns.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkDns, + pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), + histogram: getOr(null, 'source.NetworkDns.histogram', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +const makeMapHistogramStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +export const NetworkDnsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkDnsComponentQuery); + +export const NetworkDnsHistogramQuery = compose<React.ComponentClass<DnsHistogramOwnProps>>( + connect(makeMapHistogramStateToProps), + withKibana +)(MatrixHistogram); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_http/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_http/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_http/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_http/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_http/index.tsx b/x-pack/plugins/siem/public/containers/network_http/index.tsx new file mode 100644 index 0000000000000..bf4e64f63d559 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_http/index.tsx @@ -0,0 +1,156 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + GetNetworkHttpQuery, + NetworkHttpEdges, + NetworkHttpSortField, + PageInfoPaginated, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkHttpQuery } from './index.gql_query'; + +const ID = 'networkHttpQuery'; + +export interface NetworkHttpArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkHttp: NetworkHttpEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkHttpArgs) => React.ReactNode; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkHttpComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkHttpSortField; +} + +type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; + +class NetworkHttpComponentQuery extends QueryTemplatePaginated< + NetworkHttpProps, + GetNetworkHttpQuery.Query, + GetNetworkHttpQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + sort, + startDate, + } = this.props; + const variables: GetNetworkHttpQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetNetworkHttpQuery.Query, GetNetworkHttpQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkHttpQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkHttp = getOr([], `source.NetworkHttp.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkHttp: { + ...fetchMoreResult.source.NetworkHttp, + edges: [...fetchMoreResult.source.NetworkHttp.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkHttp.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkHttp, + pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getHttpSelector = networkSelectors.httpSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { id = ID, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getHttpSelector(state, type), + isInspected, + }; + }; +}; + +export const NetworkHttpQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkHttpComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_top_countries/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_top_countries/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_top_countries/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx b/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx new file mode 100644 index 0000000000000..bd1e1a002bbcd --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx @@ -0,0 +1,160 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + FlowTargetSourceDest, + GetNetworkTopCountriesQuery, + NetworkTopCountriesEdges, + NetworkTopTablesSortField, + PageInfoPaginated, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkTopCountriesQuery } from './index.gql_query'; + +const ID = 'networkTopCountriesQuery'; + +export interface NetworkTopCountriesArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkTopCountries: NetworkTopCountriesEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkTopCountriesArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkTopCountriesComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkTopTablesSortField; +} + +type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; + +class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< + NetworkTopCountriesProps, + GetNetworkTopCountriesQuery.Query, + GetNetworkTopCountriesQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + flowTarget, + filterQuery, + kibana, + id = `${ID}-${flowTarget}`, + ip, + isInspected, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetNetworkTopCountriesQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetNetworkTopCountriesQuery.Query, GetNetworkTopCountriesQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkTopCountriesQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkTopCountries: { + ...fetchMoreResult.source.NetworkTopCountries, + edges: [...fetchMoreResult.source.NetworkTopCountries.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkTopCountries, + pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTopCountriesSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const NetworkTopCountriesQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkTopCountriesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts b/x-pack/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx new file mode 100644 index 0000000000000..f0f1f8257f29f --- /dev/null +++ b/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx @@ -0,0 +1,160 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + FlowTargetSourceDest, + GetNetworkTopNFlowQuery, + NetworkTopNFlowEdges, + NetworkTopTablesSortField, + PageInfoPaginated, +} from '../../graphql/types'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { networkTopNFlowQuery } from './index.gql_query'; + +const ID = 'networkTopNFlowQuery'; + +export interface NetworkTopNFlowArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkTopNFlow: NetworkTopNFlowEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkTopNFlowArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkTopNFlowComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkTopTablesSortField; +} + +type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps & WithKibanaProps; + +class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated< + NetworkTopNFlowProps, + GetNetworkTopNFlowQuery.Query, + GetNetworkTopNFlowQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + flowTarget, + filterQuery, + kibana, + id = `${ID}-${flowTarget}`, + ip, + isInspected, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetNetworkTopNFlowQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetNetworkTopNFlowQuery.Query, GetNetworkTopNFlowQuery.Variables> + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkTopNFlowQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkTopNFlow = getOr([], `source.NetworkTopNFlow.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkTopNFlow: { + ...fetchMoreResult.source.NetworkTopNFlow, + edges: [...fetchMoreResult.source.NetworkTopNFlow.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkTopNFlow.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkTopNFlow, + pageInfo: getOr({}, 'source.NetworkTopNFlow.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkTopNFlow.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getTopNFlowSelector = networkSelectors.topNFlowSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTopNFlowSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const NetworkTopNFlowQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(NetworkTopNFlowComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts b/x-pack/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx b/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx new file mode 100644 index 0000000000000..2dd9ccf24d802 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx @@ -0,0 +1,89 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; +import { useUiSetting } from '../../../lib/kibana'; +import { inputsModel, inputsSelectors } from '../../../store/inputs'; +import { State } from '../../../store'; +import { createFilter, getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplateProps } from '../../query_template'; + +import { overviewHostQuery } from './index.gql_query'; + +export const ID = 'overviewHostQuery'; + +export interface OverviewHostArgs { + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + overviewHost: OverviewHostData; + refetch: inputsModel.Refetch; +} + +export interface OverviewHostProps extends QueryTemplateProps { + children: (args: OverviewHostArgs) => React.ReactNode; + sourceId: string; + endDate: number; + startDate: number; +} + +const OverviewHostComponentQuery = React.memo<OverviewHostProps & PropsFromRedux>( + ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => { + return ( + <Query<GetOverviewHostQuery.Query, GetOverviewHostQuery.Variables> + query={overviewHostQuery} + fetchPolicy={getDefaultFetchPolicy()} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const overviewHost = getOr({}, `source.OverviewHost`, data); + return children({ + id, + inspect: getOr(null, 'source.OverviewHost.inspect', data), + overviewHost, + loading, + refetch, + }); + }} + </Query> + ); + } +); + +OverviewHostComponentQuery.displayName = 'OverviewHostComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OverviewHostProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const OverviewHostQuery = connector(OverviewHostComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts b/x-pack/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx b/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx new file mode 100644 index 0000000000000..d0acd41c224a5 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetOverviewNetworkQuery, OverviewNetworkData } from '../../../graphql/types'; +import { useUiSetting } from '../../../lib/kibana'; +import { State } from '../../../store'; +import { inputsModel, inputsSelectors } from '../../../store/inputs'; +import { createFilter, getDefaultFetchPolicy } from '../../helpers'; +import { QueryTemplateProps } from '../../query_template'; + +import { overviewNetworkQuery } from './index.gql_query'; + +export const ID = 'overviewNetworkQuery'; + +export interface OverviewNetworkArgs { + id: string; + inspect: inputsModel.InspectQuery; + overviewNetwork: OverviewNetworkData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OverviewNetworkProps extends QueryTemplateProps { + children: (args: OverviewNetworkArgs) => React.ReactNode; + sourceId: string; + endDate: number; + startDate: number; +} + +export const OverviewNetworkComponentQuery = React.memo<OverviewNetworkProps & PropsFromRedux>( + ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( + <Query<GetOverviewNetworkQuery.Query, GetOverviewNetworkQuery.Variables> + query={overviewNetworkQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); + return children({ + id, + inspect: getOr(null, 'source.OverviewNetwork.inspect', data), + overviewNetwork, + loading, + refetch, + }); + }} + </Query> + ) +); + +OverviewNetworkComponentQuery.displayName = 'OverviewNetworkComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OverviewNetworkProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const OverviewNetworkQuery = connector(OverviewNetworkComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template.tsx b/x-pack/plugins/siem/public/containers/query_template.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/query_template.tsx rename to x-pack/plugins/siem/public/containers/query_template.tsx index c33f5fd89a79b..dfb452c24b86e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template.tsx +++ b/x-pack/plugins/siem/public/containers/query_template.tsx @@ -8,7 +8,7 @@ import { ApolloQueryResult } from 'apollo-client'; import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; -import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; +import { ESQuery } from '../../common/typed_json'; export interface QueryTemplateProps { id?: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx b/x-pack/plugins/siem/public/containers/query_template_paginated.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx rename to x-pack/plugins/siem/public/containers/query_template_paginated.tsx index 45041a6447611..db618f216d83e 100644 --- a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx +++ b/x-pack/plugins/siem/public/containers/query_template_paginated.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; import deepEqual from 'fast-deep-equal'; -import { ESQuery } from '../../../../../plugins/siem/common/typed_json'; +import { ESQuery } from '../../common/typed_json'; import { inputsModel } from '../store/model'; import { generateTablePaginationOptions } from '../components/paginated_table/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.gql_query.ts b/x-pack/plugins/siem/public/containers/source/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/source/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/source/index.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/source/index.test.tsx b/x-pack/plugins/siem/public/containers/source/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/source/index.test.tsx rename to x-pack/plugins/siem/public/containers/source/index.test.tsx diff --git a/x-pack/plugins/siem/public/containers/source/index.tsx b/x-pack/plugins/siem/public/containers/source/index.tsx new file mode 100644 index 0000000000000..e9359fdb19587 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/source/index.tsx @@ -0,0 +1,177 @@ +/* + * 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 { isUndefined } from 'lodash'; +import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; +import { Query } from 'react-apollo'; +import React, { useEffect, useMemo, useState } from 'react'; +import memoizeOne from 'memoize-one'; +import { IIndexPattern } from 'src/plugins/data/public'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; + +import { IndexField, SourceQuery } from '../../graphql/types'; + +import { sourceQuery } from './index.gql_query'; +import { useApolloClient } from '../../utils/apollo_context'; + +export { sourceQuery }; + +export interface BrowserField { + aggregatable: boolean; + category: string; + description: string | null; + example: string | number | null; + fields: Readonly<Record<string, Partial<BrowserField>>>; + format: string; + indexes: string[]; + name: string; + searchable: boolean; + type: string; +} + +export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>; + +export const getAllBrowserFields = (browserFields: BrowserFields): Array<Partial<BrowserField>> => + Object.values(browserFields).reduce<Array<Partial<BrowserField>>>( + (acc, namespace) => [ + ...acc, + ...Object.values(namespace.fields != null ? namespace.fields : {}), + ], + [] + ); + +export const getAllFieldsByName = ( + browserFields: BrowserFields +): { [fieldName: string]: Partial<BrowserField> } => + keyBy('name', getAllBrowserFields(browserFields)); + +interface WithSourceArgs { + indicesExist: boolean; + browserFields: BrowserFields; + indexPattern: IIndexPattern; +} + +interface WithSourceProps { + children: (args: WithSourceArgs) => React.ReactNode; + indexToAdd?: string[] | null; + sourceId: string; +} + +export const getIndexFields = memoizeOne( + (title: string, fields: IndexField[]): IIndexPattern => + fields && fields.length > 0 + ? { + fields: fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field)), + title, + } + : { fields: [], title } +); + +export const getBrowserFields = memoizeOne( + (title: string, fields: IndexField[]): BrowserFields => + fields && fields.length > 0 + ? fields.reduce<BrowserFields>( + (accumulator: BrowserFields, field: IndexField) => + set([field.category, 'fields', field.name], field, accumulator), + {} + ) + : {} +); + +export const WithSource = React.memo<WithSourceProps>(({ children, indexToAdd, sourceId }) => { + const [configIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo<string[]>(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + + return ( + <Query<SourceQuery.Query, SourceQuery.Variables> + query={sourceQuery} + fetchPolicy="cache-first" + notifyOnNetworkStatusChange + variables={{ + sourceId, + defaultIndex, + }} + > + {({ data }) => + children({ + indicesExist: get('source.status.indicesExist', data), + browserFields: getBrowserFields( + defaultIndex.join(), + get('source.status.indexFields', data) + ), + indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), + }) + } + </Query> + ); +}); + +WithSource.displayName = 'WithSource'; + +export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => + indicesExist || isUndefined(indicesExist); + +export const useWithSource = (sourceId: string, indices: string[]) => { + const [loading, updateLoading] = useState(false); + const [indicesExist, setIndicesExist] = useState<boolean | undefined | null>(undefined); + const [browserFields, setBrowserFields] = useState<BrowserFields | null>(null); + const [indexPattern, setIndexPattern] = useState<IIndexPattern | null>(null); + const [errorMessage, updateErrorMessage] = useState<string | null>(null); + + const apolloClient = useApolloClient(); + async function fetchSource(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query<SourceQuery.Query, SourceQuery.Variables>({ + query: sourceQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + defaultIndex: indices, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateErrorMessage(null); + setIndicesExist(get('data.source.status.indicesExist', result)); + setBrowserFields( + getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) + ); + setIndexPattern( + getIndexFields(indices.join(), get('data.source.status.indexFields', result)) + ); + }, + error => { + updateLoading(false); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchSource(signal); + return () => abortCtrl.abort(); + }, [apolloClient, sourceId, indices]); + + return { indicesExist, browserFields, indexPattern, loading, errorMessage }; +}; diff --git a/x-pack/plugins/siem/public/containers/source/mock.ts b/x-pack/plugins/siem/public/containers/source/mock.ts new file mode 100644 index 0000000000000..092aad9e7400c --- /dev/null +++ b/x-pack/plugins/siem/public/containers/source/mock.ts @@ -0,0 +1,699 @@ +/* + * 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 { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; + +import { BrowserFields } from '.'; +import { sourceQuery } from './index.gql_query'; + +export const mocksSource = [ + { + request: { + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + configuration: {}, + status: { + indicesExist: true, + winlogbeatIndices: [ + 'winlogbeat-7.0.0-2019.02.17', + 'winlogbeat-7.0.0-2019.02.18', + 'winlogbeat-7.0.0-2019.02.19', + 'winlogbeat-7.0.0-2019.02.20', + 'winlogbeat-7.0.0-2019.02.21', + 'winlogbeat-7.0.0-2019.02.21-000001', + 'winlogbeat-7.0.0-2019.02.22', + 'winlogbeat-8.0.0-2019.02.19-000001', + ], + auditbeatIndices: [ + 'auditbeat-7.0.0-2019.02.17', + 'auditbeat-7.0.0-2019.02.18', + 'auditbeat-7.0.0-2019.02.19', + 'auditbeat-7.0.0-2019.02.20', + 'auditbeat-7.0.0-2019.02.21', + 'auditbeat-7.0.0-2019.02.21-000001', + 'auditbeat-7.0.0-2019.02.22', + 'auditbeat-8.0.0-2019.02.19-000001', + ], + filebeatIndices: [ + 'filebeat-7.0.0-iot-2019.06', + 'filebeat-7.0.0-iot-2019.07', + 'filebeat-7.0.0-iot-2019.08', + 'filebeat-7.0.0-iot-2019.09', + 'filebeat-7.0.0-iot-2019.10', + 'filebeat-8.0.0-2019.02.19-000001', + ], + indexFields: [ + { + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'source', + description: + 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + }, + ], + }, + }, + }, + }, + }, +]; + +export const mockIndexFields = [ + { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, + { aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a0', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a1', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a2', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'client.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.geo.country_iso_code', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.account.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.availability_zone', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.tag', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'destination.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'destination.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'source.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'source.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'event.end', searchable: true, type: 'date' }, +]; + +export const mockBrowserFields: BrowserFields = { + agent: { + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + 'agent.id': { + aggregatable: true, + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + }, + 'agent.name': { + aggregatable: true, + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + }, + }, + }, + auditd: { + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + 'auditd.data.a1': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + }, + 'auditd.data.a2': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + }, + }, + }, + base: { + fields: { + '@timestamp': { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + }, + }, + }, + client: { + fields: { + 'client.address': { + aggregatable: true, + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + }, + 'client.bytes': { + aggregatable: true, + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + 'client.geo.country_iso_code': { + aggregatable: true, + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + aggregatable: true, + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + }, + 'cloud.availability_zone': { + aggregatable: true, + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + }, + }, + }, + container: { + fields: { + 'container.id': { + aggregatable: true, + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + }, + 'container.image.name': { + aggregatable: true, + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + }, + 'container.image.tag': { + aggregatable: true, + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + }, + }, + }, + destination: { + fields: { + 'destination.address': { + aggregatable: true, + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + }, + 'destination.bytes': { + aggregatable: true, + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + }, + 'destination.domain': { + aggregatable: true, + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + }, + 'destination.ip': { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + 'destination.port': { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + }, + }, + event: { + fields: { + 'event.end': { + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + aggregatable: true, + }, + }, + }, + source: { + fields: { + 'source.ip': { + aggregatable: true, + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + 'source.port': { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts similarity index 94% rename from x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts index e380e46e77070..7d30b6c22a110 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/all/index.gql_query.ts +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts @@ -55,6 +55,9 @@ export const allTimelinesQuery = gql` noteIds pinnedEventIds title + timelineType + templateTimelineId + templateTimelineVersion created createdBy updated diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx new file mode 100644 index 0000000000000..62c8d21a2e944 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx @@ -0,0 +1,193 @@ +/* + * 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 { getOr, noop } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import { useCallback, useState, useRef, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { OpenTimelineResult } from '../../../components/open_timeline/types'; +import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { + GetAllTimeline, + PageInfoTimeline, + SortTimeline, + TimelineResult, +} from '../../../graphql/types'; +import { inputsModel, inputsActions } from '../../../store/inputs'; +import { useApolloClient } from '../../../utils/apollo_context'; + +import { allTimelinesQuery } from './index.gql_query'; +import * as i18n from '../../../pages/timelines/translations'; + +export interface AllTimelinesArgs { + fetchAllTimeline: ({ onlyUserFavorite, pageInfo, search, sort }: AllTimelinesVariables) => void; + timelines: OpenTimelineResult[]; + loading: boolean; + totalCount: number; + refetch: () => void; +} + +export interface AllTimelinesVariables { + onlyUserFavorite: boolean; + pageInfo: PageInfoTimeline; + search: string; + sort: SortTimeline; + timelines: OpenTimelineResult[]; + totalCount: number; +} + +export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; + +export const getAllTimeline = memoizeOne( + (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => + timelines.map(timeline => ({ + created: timeline.created, + description: timeline.description, + eventIdToNoteIds: + timeline.eventIdToNoteIds != null + ? timeline.eventIdToNoteIds.reduce((acc, note) => { + if (note.eventId != null) { + const notes = getOr([], note.eventId, acc); + return { ...acc, [note.eventId]: [...notes, note.noteId] }; + } + return acc; + }, {}) + : null, + favorite: timeline.favorite, + noteIds: timeline.noteIds, + notes: + timeline.notes != null + ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) + : null, + pinnedEventIds: + timeline.pinnedEventIds != null + ? timeline.pinnedEventIds.reduce( + (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), + {} + ) + : null, + savedObjectId: timeline.savedObjectId, + title: timeline.title, + updated: timeline.updated, + updatedBy: timeline.updatedBy, + })) +); + +export const useGetAllTimeline = (): AllTimelinesArgs => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + const refetch = useRef<inputsModel.Refetch>(); + const [, dispatchToaster] = useStateToaster(); + const [allTimelines, setAllTimelines] = useState<AllTimelinesArgs>({ + fetchAllTimeline: noop, + loading: false, + refetch: refetch.current ?? noop, + totalCount: 0, + timelines: [], + }); + + const fetchAllTimeline = useCallback( + async ({ + onlyUserFavorite, + pageInfo, + search, + sort, + timelines, + totalCount, + }: AllTimelinesVariables) => { + let didCancel = false; + const abortCtrl = new AbortController(); + + const fetchData = async () => { + try { + if (apolloClient != null) { + setAllTimelines({ + ...allTimelines, + timelines: timelines ?? allTimelines.timelines, + totalCount: totalCount ?? allTimelines.totalCount, + loading: true, + }); + const variables: GetAllTimeline.Variables = { + onlyUserFavorite, + pageInfo, + search, + sort, + }; + const response = await apolloClient.query< + GetAllTimeline.Query, + GetAllTimeline.Variables + >({ + query: allTimelinesQuery, + fetchPolicy: 'network-only', + variables, + context: { + fetchOptions: { + abortSignal: abortCtrl.signal, + }, + }, + }); + if (!didCancel) { + dispatch( + inputsActions.setQuery({ + inputId: 'global', + id: ALL_TIMELINE_QUERY_ID, + loading: false, + refetch: refetch.current ?? noop, + inspect: null, + }) + ); + setAllTimelines({ + fetchAllTimeline, + loading: false, + refetch: refetch.current ?? noop, + totalCount: getOr(0, 'getAllTimeline.totalCount', response.data), + timelines: getAllTimeline( + JSON.stringify(variables), + getOr([], 'getAllTimeline.timeline', response.data) + ), + }); + } + } + } catch (error) { + if (!didCancel) { + errorToToaster({ + title: i18n.ERROR_FETCHING_TIMELINES_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + setAllTimelines({ + fetchAllTimeline, + loading: false, + refetch: noop, + totalCount: 0, + timelines: [], + }); + } + } + }; + refetch.current = fetchData; + fetchData(); + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, + [apolloClient, allTimelines] + ); + + useEffect(() => { + return () => { + dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: ALL_TIMELINE_QUERY_ID })); + }; + }, [dispatch]); + + return { + ...allTimelines, + fetchAllTimeline, + refetch: refetch.current ?? noop, + }; +}; diff --git a/x-pack/plugins/siem/public/containers/timeline/api.ts b/x-pack/plugins/siem/public/containers/timeline/api.ts new file mode 100644 index 0000000000000..023e2e6af9f88 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/api.ts @@ -0,0 +1,115 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { throwErrors } from '../../../../case/common/api'; +import { + SavedTimeline, + TimelineResponse, + TimelineResponseType, +} from '../../../common/types/timeline'; +import { TIMELINE_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../common/constants'; + +import { KibanaServices } from '../../lib/kibana'; +import { ExportSelectedData } from '../../components/generic_downloader'; + +import { createToasterPlainError } from '../case/utils'; +import { ImportDataProps, ImportDataResponse } from '../detection_engine/rules'; + +interface RequestPostTimeline { + timeline: SavedTimeline; + signal?: AbortSignal; +} + +interface RequestPatchTimeline<T = string> extends RequestPostTimeline { + timelineId: T; + version: T; +} + +type RequestPersistTimeline = RequestPostTimeline & Partial<RequestPatchTimeline<null | string>>; + +const decodeTimelineResponse = (respTimeline?: TimelineResponse) => + pipe( + TimelineResponseType.decode(respTimeline), + fold(throwErrors(createToasterPlainError), identity) + ); + +const postTimeline = async ({ timeline }: RequestPostTimeline): Promise<TimelineResponse> => { + const response = await KibanaServices.get().http.post<TimelineResponse>(TIMELINE_URL, { + method: 'POST', + body: JSON.stringify({ timeline }), + }); + + return decodeTimelineResponse(response); +}; + +const patchTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPatchTimeline): Promise<TimelineResponse> => { + const response = await KibanaServices.get().http.patch<TimelineResponse>(TIMELINE_URL, { + method: 'PATCH', + body: JSON.stringify({ timeline, timelineId, version }), + }); + + return decodeTimelineResponse(response); +}; + +export const persistTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPersistTimeline): Promise<TimelineResponse> => { + if (timelineId == null) { + return postTimeline({ timeline }); + } + return patchTimeline({ + timelineId, + timeline, + version: version ?? '', + }); +}; + +export const importTimelines = async ({ + fileToImport, + overwrite = false, + signal, +}: ImportDataProps): Promise<ImportDataResponse> => { + const formData = new FormData(); + formData.append('file', fileToImport); + + return KibanaServices.get().http.fetch<ImportDataResponse>(`${TIMELINE_IMPORT_URL}`, { + method: 'POST', + headers: { 'Content-Type': undefined }, + query: { overwrite }, + body: formData, + signal, + }); +}; + +export const exportSelectedTimeline: ExportSelectedData = async ({ + excludeExportDetails = false, + filename = `timelines_export.ndjson`, + ids = [], + signal, +}): Promise<Blob> => { + const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; + const response = await KibanaServices.get().http.fetch<Blob>(`${TIMELINE_EXPORT_URL}`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); + + return response.body!; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/details/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/details/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/plugins/siem/public/containers/timeline/details/index.tsx new file mode 100644 index 0000000000000..cf1b8954307e7 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/details/index.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; +import { useUiSetting } from '../../../lib/kibana'; + +import { timelineDetailsQuery } from './index.gql_query'; + +export interface EventsArgs { + detailsData: DetailItem[] | null; + loading: boolean; +} + +export interface TimelineDetailsProps { + children?: (args: EventsArgs) => React.ReactElement; + indexName: string; + eventId: string; + executeQuery: boolean; + sourceId: string; +} + +const getDetailsEvent = memoizeOne( + (variables: string, detail: DetailItem[]): DetailItem[] => detail +); + +const TimelineDetailsQueryComponent: React.FC<TimelineDetailsProps> = ({ + children, + indexName, + eventId, + executeQuery, + sourceId, +}) => { + const variables: GetTimelineDetailsQuery.Variables = { + sourceId, + indexName, + eventId, + defaultIndex: useUiSetting<string[]>(DEFAULT_INDEX_KEY), + }; + return executeQuery ? ( + <Query<GetTimelineDetailsQuery.Query, GetTimelineDetailsQuery.Variables> + query={timelineDetailsQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading, refetch }) => + children!({ + loading, + detailsData: getDetailsEvent( + JSON.stringify(variables), + getOr([], 'source.TimelineDetails.data', data) + ), + }) + } + </Query> + ) : ( + children!({ loading: false, detailsData: null }) + ); +}; + +export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/index.tsx b/x-pack/plugins/siem/public/containers/timeline/index.tsx new file mode 100644 index 0000000000000..6e09e124696b6 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/timeline/index.tsx @@ -0,0 +1,199 @@ +/* + * 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 { getOr, uniqBy } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { compose, Dispatch } from 'redux'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; +import { + GetTimelineQuery, + PageInfo, + SortField, + TimelineEdges, + TimelineItem, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { createFilter } from '../helpers'; +import { QueryTemplate, QueryTemplateProps } from '../query_template'; +import { EventType } from '../../store/timeline/model'; +import { timelineQuery } from './index.gql_query'; +import { timelineActions } from '../../store/timeline'; +import { SIGNALS_PAGE_TIMELINE_ID } from '../../pages/detection_engine/components/signals'; + +export interface TimelineArgs { + events: TimelineItem[]; + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + loadMore: (cursor: string, tieBreaker: string) => void; + pageInfo: PageInfo; + refetch: inputsModel.Refetch; + totalCount: number; + getUpdatedAt: () => number; +} + +export interface CustomReduxProps { + clearSignalsState: ({ id }: { id?: string }) => void; +} + +export interface OwnProps extends QueryTemplateProps { + children?: (args: TimelineArgs) => React.ReactNode; + eventType?: EventType; + id: string; + indexPattern?: IIndexPattern; + indexToAdd?: string[]; + limit: number; + sortField: SortField; + fields: string[]; +} + +type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; + +class TimelineQueryComponent extends QueryTemplate< + TimelineQueryProps, + GetTimelineQuery.Query, + GetTimelineQuery.Variables +> { + private updatedDate: number = Date.now(); + private memoizedTimelineEvents: (variables: string, events: TimelineEdges[]) => TimelineItem[]; + + constructor(props: TimelineQueryProps) { + super(props); + this.memoizedTimelineEvents = memoizeOne(this.getTimelineEvents); + } + + public render() { + const { + children, + clearSignalsState, + eventType = 'raw', + id, + indexPattern, + indexToAdd = [], + isInspected, + kibana, + limit, + fields, + filterQuery, + sourceId, + sortField, + } = this.props; + const defaultKibanaIndex = kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY); + const defaultIndex = + indexPattern == null || (indexPattern != null && indexPattern.title === '') + ? [ + ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), + ...(['all', 'signal'].includes(eventType) ? indexToAdd : []), + ] + : indexPattern?.title.split(',') ?? []; + const variables: GetTimelineQuery.Variables = { + fieldRequested: fields, + filterQuery: createFilter(filterQuery), + sourceId, + pagination: { limit, cursor: null, tiebreaker: null }, + sortField, + defaultIndex, + inspect: isInspected, + }; + + return ( + <Query<GetTimelineQuery.Query, GetTimelineQuery.Variables> + query={timelineQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading, fetchMore, refetch }) => { + this.setRefetch(refetch); + this.setExecuteBeforeRefetch(clearSignalsState); + this.setExecuteBeforeFetchMore(clearSignalsState); + + const timelineEdges = getOr([], 'source.Timeline.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newCursor: string, tiebreaker?: string) => ({ + variables: { + pagination: { + cursor: newCursor, + tiebreaker, + limit, + }, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Timeline: { + ...fetchMoreResult.source.Timeline, + edges: uniqBy('node._id', [ + ...prev.source.Timeline.edges, + ...fetchMoreResult.source.Timeline.edges, + ]), + }, + }, + }; + }, + })); + this.updatedDate = Date.now(); + return children!({ + id, + inspect: getOr(null, 'source.Timeline.inspect', data), + refetch: this.wrappedRefetch, + loading, + totalCount: getOr(0, 'source.Timeline.totalCount', data), + pageInfo: getOr({}, 'source.Timeline.pageInfo', data), + events: this.memoizedTimelineEvents(JSON.stringify(variables), timelineEdges), + loadMore: this.wrappedLoadMore, + getUpdatedAt: this.getUpdatedAt, + }); + }} + </Query> + ); + } + + private getUpdatedAt = () => this.updatedDate; + + private getTimelineEvents = (variables: string, timelineEdges: TimelineEdges[]): TimelineItem[] => + timelineEdges.map((e: TimelineEdges) => e.node); +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { id }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSignalsState: ({ id }: { id?: string }) => { + if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) { + dispatch(timelineActions.clearEventsLoading({ id })); + dispatch(timelineActions.clearEventsDeleted({ id })); + } + }, +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const TimelineQuery = compose<React.ComponentClass<OwnProps>>( + connector, + withKibana +)(TimelineQueryComponent); diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/one/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts b/x-pack/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts rename to x-pack/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts b/x-pack/plugins/siem/public/containers/tls/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/tls/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/tls/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/tls/index.tsx b/x-pack/plugins/siem/public/containers/tls/index.tsx new file mode 100644 index 0000000000000..3738355c8846e --- /dev/null +++ b/x-pack/plugins/siem/public/containers/tls/index.tsx @@ -0,0 +1,159 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + PageInfoPaginated, + TlsEdges, + TlsSortField, + GetTlsQuery, + FlowTargetSourceDest, +} from '../../graphql/types'; +import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; +import { tlsQuery } from './index.gql_query'; + +const ID = 'tlsQuery'; + +export interface TlsArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + tls: TlsEdges[]; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: TlsArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip: string; + type: networkModel.NetworkType; +} + +export interface TlsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: TlsSortField; +} + +type TlsProps = OwnProps & TlsComponentReduxProps & WithKibanaProps; + +class TlsComponentQuery extends QueryTemplatePaginated< + TlsProps, + GetTlsQuery.Query, + GetTlsQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetTlsQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate ? startDate : 0, + to: endDate ? endDate : Date.now(), + }, + }; + return ( + <Query<GetTlsQuery.Query, GetTlsQuery.Variables> + query={tlsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const tls = getOr([], 'source.Tls.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Tls: { + ...fetchMoreResult.source.Tls, + edges: [...fetchMoreResult.source.Tls.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.Tls.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Tls.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + tls, + totalCount: getOr(-1, 'source.Tls.totalCount', data), + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getTlsSelector = networkSelectors.tlsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = ID, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTlsSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const TlsQuery = compose<React.ComponentClass<OwnProps>>( + connect(makeMapStateToProps), + withKibana +)(TlsComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts b/x-pack/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx b/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx new file mode 100644 index 0000000000000..0a2ce67d9be80 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { + GetUncommonProcessesQuery, + PageInfoPaginated, + UncommonProcessesEdges, +} from '../../graphql/types'; +import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; + +import { uncommonProcessesQuery } from './index.gql_query'; + +const ID = 'uncommonProcessesQuery'; + +export interface UncommonProcessesArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; + uncommonProcesses: UncommonProcessesEdges[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: UncommonProcessesArgs) => React.ReactNode; + type: hostsModel.HostsType; +} + +type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; + +class UncommonProcessesComponentQuery extends QueryTemplatePaginated< + UncommonProcessesProps, + GetUncommonProcessesQuery.Query, + GetUncommonProcessesQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetUncommonProcessesQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + pagination: generateTablePaginationOptions(activePage, limit), + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetUncommonProcessesQuery.Query, GetUncommonProcessesQuery.Variables> + query={uncommonProcessesQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + UncommonProcesses: { + ...fetchMoreResult.source.UncommonProcesses, + edges: [...fetchMoreResult.source.UncommonProcesses.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.UncommonProcesses.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), + uncommonProcesses, + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getUncommonProcessesSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const UncommonProcessesQuery = compose<React.ComponentClass<OwnProps>>( + connector, + withKibana +)(UncommonProcessesComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/containers/users/index.gql_query.ts b/x-pack/plugins/siem/public/containers/users/index.gql_query.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/containers/users/index.gql_query.ts rename to x-pack/plugins/siem/public/containers/users/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/users/index.tsx b/x-pack/plugins/siem/public/containers/users/index.tsx new file mode 100644 index 0000000000000..5f71449c52460 --- /dev/null +++ b/x-pack/plugins/siem/public/containers/users/index.tsx @@ -0,0 +1,153 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../graphql/types'; +import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; +import { withKibana, WithKibanaProps } from '../../lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../helpers'; +import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; +import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; + +import { usersQuery } from './index.gql_query'; + +const ID = 'usersQuery'; + +export interface UsersArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; + users: UsersEdges[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: UsersArgs) => React.ReactNode; + flowTarget: FlowTarget; + ip: string; + type: networkModel.NetworkType; +} + +type UsersProps = OwnProps & PropsFromRedux & WithKibanaProps; + +class UsersComponentQuery extends QueryTemplatePaginated< + UsersProps, + GetUsersQuery.Query, + GetUsersQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetUsersQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get<string[]>(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + <Query<GetUsersQuery.Query, GetUsersQuery.Variables> + query={usersQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const users = getOr([], `source.Users.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Users: { + ...fetchMoreResult.source.Users, + edges: [...fetchMoreResult.source.Users.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.Users.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Users.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.Users.totalCount', data), + users, + }); + }} + </Query> + ); + } +} + +const makeMapStateToProps = () => { + const getUsersSelector = networkSelectors.usersSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getUsersSelector(state), + isInspected, + }; + }; + + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const UsersQuery = compose<React.ComponentClass<OwnProps>>( + connector, + withKibana +)(UsersComponentQuery); diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/plugins/siem/public/graphql/introspection.json similarity index 99% rename from x-pack/legacy/plugins/siem/public/graphql/introspection.json rename to x-pack/plugins/siem/public/graphql/introspection.json index 2a9dd8f2aacfe..4026a043c7778 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/plugins/siem/public/graphql/introspection.json @@ -9728,6 +9728,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "templateTimelineId", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "templateTimelineVersion", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "timelineType", + "description": "", + "args": [], + "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updated", "description": "", @@ -10323,6 +10347,39 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. ", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TimelineType", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "default", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "template", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "PageInfoTimeline", @@ -10863,6 +10920,24 @@ "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "defaultValue": null }, + { + "name": "templateTimelineId", + "description": "", + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null + }, + { + "name": "templateTimelineVersion", + "description": "", + "type": { "kind": "SCALAR", "name": "Int", "ofType": null }, + "defaultValue": null + }, + { + "name": "timelineType", + "description": "", + "type": { "kind": "ENUM", "name": "TimelineType", "ofType": null }, + "defaultValue": null + }, { "name": "dateRange", "description": "", diff --git a/x-pack/plugins/siem/public/graphql/types.ts b/x-pack/plugins/siem/public/graphql/types.ts new file mode 100644 index 0000000000000..8c39d5e58b99e --- /dev/null +++ b/x-pack/plugins/siem/public/graphql/types.ts @@ -0,0 +1,5966 @@ +/* tslint:disable */ +/* eslint-disable */ +/* + * 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 Maybe<T> = T | null; + +export interface PageInfoNote { + pageIndex: number; + + pageSize: number; +} + +export interface SortNote { + sortField: SortFieldNote; + + sortOrder: Direction; +} + +export interface TimerangeInput { + /** The interval string to use for last bucket. The format is '{value}{unit}'. For example '5m' would return the metrics for the last 5 minutes of the timespan. */ + interval: string; + /** The end of the timerange */ + to: number; + /** The beginning of the timerange */ + from: number; +} + +export interface PaginationInputPaginated { + /** The activePage parameter defines the page of results you want to fetch */ + activePage: number; + /** The cursorStart parameter defines the start of the results to be displayed */ + cursorStart: number; + /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */ + fakePossibleCount: number; + /** The querySize parameter is the number of items to be returned */ + querySize: number; +} + +export interface PaginationInput { + /** The limit parameter allows you to configure the maximum amount of items to be returned */ + limit: number; + /** The cursor parameter defines the next result you want to fetch */ + cursor?: Maybe<string>; + /** The tiebreaker parameter allow to be more precise to fetch the next item */ + tiebreaker?: Maybe<string>; +} + +export interface SortField { + sortFieldId: string; + + direction: Direction; +} + +export interface LastTimeDetails { + hostName?: Maybe<string>; + + ip?: Maybe<string>; +} + +export interface HostsSortField { + field: HostsFields; + + direction: Direction; +} + +export interface UsersSortField { + field: UsersFields; + + direction: Direction; +} + +export interface NetworkTopTablesSortField { + field: NetworkTopTablesFields; + + direction: Direction; +} + +export interface NetworkDnsSortField { + field: NetworkDnsFields; + + direction: Direction; +} + +export interface NetworkHttpSortField { + direction: Direction; +} + +export interface TlsSortField { + field: TlsFields; + + direction: Direction; +} + +export interface PageInfoTimeline { + pageIndex: number; + + pageSize: number; +} + +export interface SortTimeline { + sortField: SortFieldTimeline; + + sortOrder: Direction; +} + +export interface NoteInput { + eventId?: Maybe<string>; + + note?: Maybe<string>; + + timelineId?: Maybe<string>; +} + +export interface TimelineInput { + columns?: Maybe<ColumnHeaderInput[]>; + + dataProviders?: Maybe<DataProviderInput[]>; + + description?: Maybe<string>; + + eventType?: Maybe<string>; + + filters?: Maybe<FilterTimelineInput[]>; + + kqlMode?: Maybe<string>; + + kqlQuery?: Maybe<SerializedFilterQueryInput>; + + title?: Maybe<string>; + + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + + dateRange?: Maybe<DateRangePickerInput>; + + savedQueryId?: Maybe<string>; + + sort?: Maybe<SortTimelineInput>; +} + +export interface ColumnHeaderInput { + aggregatable?: Maybe<boolean>; + + category?: Maybe<string>; + + columnHeaderType?: Maybe<string>; + + description?: Maybe<string>; + + example?: Maybe<string>; + + indexes?: Maybe<string[]>; + + id?: Maybe<string>; + + name?: Maybe<string>; + + placeholder?: Maybe<string>; + + searchable?: Maybe<boolean>; + + type?: Maybe<string>; +} + +export interface DataProviderInput { + id?: Maybe<string>; + + name?: Maybe<string>; + + enabled?: Maybe<boolean>; + + excluded?: Maybe<boolean>; + + kqlQuery?: Maybe<string>; + + queryMatch?: Maybe<QueryMatchInput>; + + and?: Maybe<DataProviderInput[]>; +} + +export interface QueryMatchInput { + field?: Maybe<string>; + + displayField?: Maybe<string>; + + value?: Maybe<string>; + + displayValue?: Maybe<string>; + + operator?: Maybe<string>; +} + +export interface FilterTimelineInput { + exists?: Maybe<string>; + + meta?: Maybe<FilterMetaTimelineInput>; + + match_all?: Maybe<string>; + + missing?: Maybe<string>; + + query?: Maybe<string>; + + range?: Maybe<string>; + + script?: Maybe<string>; +} + +export interface FilterMetaTimelineInput { + alias?: Maybe<string>; + + controlledBy?: Maybe<string>; + + disabled?: Maybe<boolean>; + + field?: Maybe<string>; + + formattedValue?: Maybe<string>; + + index?: Maybe<string>; + + key?: Maybe<string>; + + negate?: Maybe<boolean>; + + params?: Maybe<string>; + + type?: Maybe<string>; + + value?: Maybe<string>; +} + +export interface SerializedFilterQueryInput { + filterQuery?: Maybe<SerializedKueryQueryInput>; +} + +export interface SerializedKueryQueryInput { + kuery?: Maybe<KueryFilterQueryInput>; + + serializedQuery?: Maybe<string>; +} + +export interface KueryFilterQueryInput { + kind?: Maybe<string>; + + expression?: Maybe<string>; +} + +export interface DateRangePickerInput { + start?: Maybe<number>; + + end?: Maybe<number>; +} + +export interface SortTimelineInput { + columnId?: Maybe<string>; + + sortDirection?: Maybe<string>; +} + +export interface FavoriteTimelineInput { + fullName?: Maybe<string>; + + userName?: Maybe<string>; + + favoriteDate?: Maybe<number>; +} + +export enum SortFieldNote { + updatedBy = 'updatedBy', + updated = 'updated', +} + +export enum Direction { + asc = 'asc', + desc = 'desc', +} + +export enum LastEventIndexKey { + hostDetails = 'hostDetails', + hosts = 'hosts', + ipDetails = 'ipDetails', + network = 'network', +} + +export enum HostsFields { + hostName = 'hostName', + lastSeen = 'lastSeen', +} + +export enum UsersFields { + name = 'name', + count = 'count', +} + +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + +export enum HistogramType { + authentications = 'authentications', + anomalies = 'anomalies', + events = 'events', + alerts = 'alerts', + dns = 'dns', +} + +export enum FlowTargetSourceDest { + destination = 'destination', + source = 'source', +} + +export enum NetworkTopTablesFields { + bytes_in = 'bytes_in', + bytes_out = 'bytes_out', + flows = 'flows', + destination_ips = 'destination_ips', + source_ips = 'source_ips', +} + +export enum NetworkDnsFields { + dnsName = 'dnsName', + queryCount = 'queryCount', + uniqueDomains = 'uniqueDomains', + dnsBytesIn = 'dnsBytesIn', + dnsBytesOut = 'dnsBytesOut', +} + +export enum TlsFields { + _id = '_id', +} + +export enum TimelineType { + default = 'default', + template = 'template', +} + +export enum SortFieldTimeline { + title = 'title', + description = 'description', + updated = 'updated', + created = 'created', +} + +export enum NetworkDirectionEcs { + inbound = 'inbound', + outbound = 'outbound', + internal = 'internal', + external = 'external', + incoming = 'incoming', + outgoing = 'outgoing', + listening = 'listening', + unknown = 'unknown', +} + +export enum NetworkHttpFields { + domains = 'domains', + lastHost = 'lastHost', + lastSourceIp = 'lastSourceIp', + methods = 'methods', + path = 'path', + requestCount = 'requestCount', + statuses = 'statuses', +} + +export enum FlowDirection { + uniDirectional = 'uniDirectional', + biDirectional = 'biDirectional', +} + +export type ToStringArray = string[]; + +export type Date = string; + +export type ToNumberArray = number[]; + +export type ToDateArray = string[]; + +export type ToBooleanArray = boolean[]; + +export type ToAny = any; + +export type EsValue = any; + +// ==================================================== +// Scalars +// ==================================================== + +// ==================================================== +// Types +// ==================================================== + +export interface Query { + getNote: NoteResult; + + getNotesByTimelineId: NoteResult[]; + + getNotesByEventId: NoteResult[]; + + getAllNotes: ResponseNotes; + + getAllPinnedEventsByTimelineId: PinnedEvent[]; + /** Get a security data source by id */ + source: Source; + /** Get a list of all security data sources */ + allSources: Source[]; + + getOneTimeline: TimelineResult; + + getAllTimeline: ResponseTimelines; +} + +export interface NoteResult { + eventId?: Maybe<string>; + + note?: Maybe<string>; + + timelineId?: Maybe<string>; + + noteId: string; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + timelineVersion?: Maybe<string>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version?: Maybe<string>; +} + +export interface ResponseNotes { + notes: NoteResult[]; + + totalCount?: Maybe<number>; +} + +export interface PinnedEvent { + code?: Maybe<number>; + + message?: Maybe<string>; + + pinnedEventId: string; + + eventId?: Maybe<string>; + + timelineId?: Maybe<string>; + + timelineVersion?: Maybe<string>; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version?: Maybe<string>; +} + +export interface Source { + /** The id of the source */ + id: string; + /** The raw configuration of the source */ + configuration: SourceConfiguration; + /** The status of the source */ + status: SourceStatus; + /** Gets Authentication success and failures based on a timerange */ + Authentications: AuthenticationsData; + + Timeline: TimelineData; + + TimelineDetails: TimelineDetailsData; + + LastEventTime: LastEventTimeData; + /** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */ + Hosts: HostsData; + + HostOverview: HostItem; + + HostFirstLastSeen: FirstLastSeenHost; + + IpOverview?: Maybe<IpOverviewData>; + + Users: UsersData; + + KpiNetwork?: Maybe<KpiNetworkData>; + + KpiHosts: KpiHostsData; + + KpiHostDetails: KpiHostDetailsData; + + MatrixHistogram: MatrixHistogramOverTimeData; + + NetworkTopCountries: NetworkTopCountriesData; + + NetworkTopNFlow: NetworkTopNFlowData; + + NetworkDns: NetworkDnsData; + + NetworkDnsHistogram: NetworkDsOverTimeData; + + NetworkHttp: NetworkHttpData; + + OverviewNetwork?: Maybe<OverviewNetworkData>; + + OverviewHost?: Maybe<OverviewHostData>; + + Tls: TlsData; + /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ + UncommonProcesses: UncommonProcessesData; + /** Just a simple example to get the app name */ + whoAmI?: Maybe<SayMyName>; +} + +/** A set of configuration options for a security data source */ +export interface SourceConfiguration { + /** The field mapping to use for this source */ + fields: SourceFields; +} + +/** A mapping of semantic fields to their document counterparts */ +export interface SourceFields { + /** The field to identify a container by */ + container: string; + /** The fields to identify a host by */ + host: string; + /** The fields that may contain the log event message. The first field found win. */ + message: string[]; + /** The field to identify a pod by */ + pod: string; + /** The field to use as a tiebreaker for log events that have identical timestamps */ + tiebreaker: string; + /** The field to use as a timestamp for metrics and logs */ + timestamp: string; +} + +/** The status of an infrastructure data source */ +export interface SourceStatus { + /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */ + indicesExist: boolean; + /** The list of fields defined in the index mappings */ + indexFields: IndexField[]; +} + +/** A descriptor of a field in an index */ +export interface IndexField { + /** Where the field belong */ + category: string; + /** Example of field's value */ + example?: Maybe<string>; + /** whether the field's belong to an alias index */ + indexes: (Maybe<string>)[]; + /** The name of the field */ + name: string; + /** The type of the field's values as recognized by Kibana */ + type: string; + /** Whether the field's values can be efficiently searched for */ + searchable: boolean; + /** Whether the field's values can be aggregated */ + aggregatable: boolean; + /** Description of the field */ + description?: Maybe<string>; + + format?: Maybe<string>; +} + +export interface AuthenticationsData { + edges: AuthenticationsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface AuthenticationsEdges { + node: AuthenticationItem; + + cursor: CursorType; +} + +export interface AuthenticationItem { + _id: string; + + failures: number; + + successes: number; + + user: UserEcsFields; + + lastSuccess?: Maybe<LastSourceHost>; + + lastFailure?: Maybe<LastSourceHost>; +} + +export interface UserEcsFields { + domain?: Maybe<string[]>; + + id?: Maybe<string[]>; + + name?: Maybe<string[]>; + + full_name?: Maybe<string[]>; + + email?: Maybe<string[]>; + + hash?: Maybe<string[]>; + + group?: Maybe<string[]>; +} + +export interface LastSourceHost { + timestamp?: Maybe<string>; + + source?: Maybe<SourceEcsFields>; + + host?: Maybe<HostEcsFields>; +} + +export interface SourceEcsFields { + bytes?: Maybe<number[]>; + + ip?: Maybe<string[]>; + + port?: Maybe<number[]>; + + domain?: Maybe<string[]>; + + geo?: Maybe<GeoEcsFields>; + + packets?: Maybe<number[]>; +} + +export interface GeoEcsFields { + city_name?: Maybe<string[]>; + + continent_name?: Maybe<string[]>; + + country_iso_code?: Maybe<string[]>; + + country_name?: Maybe<string[]>; + + location?: Maybe<Location>; + + region_iso_code?: Maybe<string[]>; + + region_name?: Maybe<string[]>; +} + +export interface Location { + lon?: Maybe<number[]>; + + lat?: Maybe<number[]>; +} + +export interface HostEcsFields { + architecture?: Maybe<string[]>; + + id?: Maybe<string[]>; + + ip?: Maybe<string[]>; + + mac?: Maybe<string[]>; + + name?: Maybe<string[]>; + + os?: Maybe<OsEcsFields>; + + type?: Maybe<string[]>; +} + +export interface OsEcsFields { + platform?: Maybe<string[]>; + + name?: Maybe<string[]>; + + full?: Maybe<string[]>; + + family?: Maybe<string[]>; + + version?: Maybe<string[]>; + + kernel?: Maybe<string[]>; +} + +export interface CursorType { + value?: Maybe<string>; + + tiebreaker?: Maybe<string>; +} + +export interface PageInfoPaginated { + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; +} + +export interface Inspect { + dsl: string[]; + + response: string[]; +} + +export interface TimelineData { + edges: TimelineEdges[]; + + totalCount: number; + + pageInfo: PageInfo; + + inspect?: Maybe<Inspect>; +} + +export interface TimelineEdges { + node: TimelineItem; + + cursor: CursorType; +} + +export interface TimelineItem { + _id: string; + + _index?: Maybe<string>; + + data: TimelineNonEcsData[]; + + ecs: Ecs; +} + +export interface TimelineNonEcsData { + field: string; + + value?: Maybe<string[]>; +} + +export interface Ecs { + _id: string; + + _index?: Maybe<string>; + + auditd?: Maybe<AuditdEcsFields>; + + destination?: Maybe<DestinationEcsFields>; + + dns?: Maybe<DnsEcsFields>; + + endgame?: Maybe<EndgameEcsFields>; + + event?: Maybe<EventEcsFields>; + + geo?: Maybe<GeoEcsFields>; + + host?: Maybe<HostEcsFields>; + + network?: Maybe<NetworkEcsField>; + + rule?: Maybe<RuleEcsField>; + + signal?: Maybe<SignalField>; + + source?: Maybe<SourceEcsFields>; + + suricata?: Maybe<SuricataEcsFields>; + + tls?: Maybe<TlsEcsFields>; + + zeek?: Maybe<ZeekEcsFields>; + + http?: Maybe<HttpEcsFields>; + + url?: Maybe<UrlEcsFields>; + + timestamp?: Maybe<string>; + + message?: Maybe<string[]>; + + user?: Maybe<UserEcsFields>; + + winlog?: Maybe<WinlogEcsFields>; + + process?: Maybe<ProcessEcsFields>; + + file?: Maybe<FileFields>; + + system?: Maybe<SystemEcsField>; +} + +export interface AuditdEcsFields { + result?: Maybe<string[]>; + + session?: Maybe<string[]>; + + data?: Maybe<AuditdData>; + + summary?: Maybe<Summary>; + + sequence?: Maybe<string[]>; +} + +export interface AuditdData { + acct?: Maybe<string[]>; + + terminal?: Maybe<string[]>; + + op?: Maybe<string[]>; +} + +export interface Summary { + actor?: Maybe<PrimarySecondary>; + + object?: Maybe<PrimarySecondary>; + + how?: Maybe<string[]>; + + message_type?: Maybe<string[]>; + + sequence?: Maybe<string[]>; +} + +export interface PrimarySecondary { + primary?: Maybe<string[]>; + + secondary?: Maybe<string[]>; + + type?: Maybe<string[]>; +} + +export interface DestinationEcsFields { + bytes?: Maybe<number[]>; + + ip?: Maybe<string[]>; + + port?: Maybe<number[]>; + + domain?: Maybe<string[]>; + + geo?: Maybe<GeoEcsFields>; + + packets?: Maybe<number[]>; +} + +export interface DnsEcsFields { + question?: Maybe<DnsQuestionData>; + + resolved_ip?: Maybe<string[]>; + + response_code?: Maybe<string[]>; +} + +export interface DnsQuestionData { + name?: Maybe<string[]>; + + type?: Maybe<string[]>; +} + +export interface EndgameEcsFields { + exit_code?: Maybe<number[]>; + + file_name?: Maybe<string[]>; + + file_path?: Maybe<string[]>; + + logon_type?: Maybe<number[]>; + + parent_process_name?: Maybe<string[]>; + + pid?: Maybe<number[]>; + + process_name?: Maybe<string[]>; + + subject_domain_name?: Maybe<string[]>; + + subject_logon_id?: Maybe<string[]>; + + subject_user_name?: Maybe<string[]>; + + target_domain_name?: Maybe<string[]>; + + target_logon_id?: Maybe<string[]>; + + target_user_name?: Maybe<string[]>; +} + +export interface EventEcsFields { + action?: Maybe<string[]>; + + category?: Maybe<string[]>; + + code?: Maybe<string[]>; + + created?: Maybe<string[]>; + + dataset?: Maybe<string[]>; + + duration?: Maybe<number[]>; + + end?: Maybe<string[]>; + + hash?: Maybe<string[]>; + + id?: Maybe<string[]>; + + kind?: Maybe<string[]>; + + module?: Maybe<string[]>; + + original?: Maybe<string[]>; + + outcome?: Maybe<string[]>; + + risk_score?: Maybe<number[]>; + + risk_score_norm?: Maybe<number[]>; + + severity?: Maybe<number[]>; + + start?: Maybe<string[]>; + + timezone?: Maybe<string[]>; + + type?: Maybe<string[]>; +} + +export interface NetworkEcsField { + bytes?: Maybe<number[]>; + + community_id?: Maybe<string[]>; + + direction?: Maybe<string[]>; + + packets?: Maybe<number[]>; + + protocol?: Maybe<string[]>; + + transport?: Maybe<string[]>; +} + +export interface RuleEcsField { + reference?: Maybe<string[]>; +} + +export interface SignalField { + rule?: Maybe<RuleField>; + + original_time?: Maybe<string[]>; +} + +export interface RuleField { + id?: Maybe<string[]>; + + rule_id?: Maybe<string[]>; + + false_positives: string[]; + + saved_id?: Maybe<string[]>; + + timeline_id?: Maybe<string[]>; + + timeline_title?: Maybe<string[]>; + + max_signals?: Maybe<number[]>; + + risk_score?: Maybe<string[]>; + + output_index?: Maybe<string[]>; + + description?: Maybe<string[]>; + + from?: Maybe<string[]>; + + immutable?: Maybe<boolean[]>; + + index?: Maybe<string[]>; + + interval?: Maybe<string[]>; + + language?: Maybe<string[]>; + + query?: Maybe<string[]>; + + references?: Maybe<string[]>; + + severity?: Maybe<string[]>; + + tags?: Maybe<string[]>; + + threat?: Maybe<ToAny>; + + type?: Maybe<string[]>; + + size?: Maybe<string[]>; + + to?: Maybe<string[]>; + + enabled?: Maybe<boolean[]>; + + filters?: Maybe<ToAny>; + + created_at?: Maybe<string[]>; + + updated_at?: Maybe<string[]>; + + created_by?: Maybe<string[]>; + + updated_by?: Maybe<string[]>; + + version?: Maybe<string[]>; + + note?: Maybe<string[]>; +} + +export interface SuricataEcsFields { + eve?: Maybe<SuricataEveData>; +} + +export interface SuricataEveData { + alert?: Maybe<SuricataAlertData>; + + flow_id?: Maybe<number[]>; + + proto?: Maybe<string[]>; +} + +export interface SuricataAlertData { + signature?: Maybe<string[]>; + + signature_id?: Maybe<number[]>; +} + +export interface TlsEcsFields { + client_certificate?: Maybe<TlsClientCertificateData>; + + fingerprints?: Maybe<TlsFingerprintsData>; + + server_certificate?: Maybe<TlsServerCertificateData>; +} + +export interface TlsClientCertificateData { + fingerprint?: Maybe<FingerprintData>; +} + +export interface FingerprintData { + sha1?: Maybe<string[]>; +} + +export interface TlsFingerprintsData { + ja3?: Maybe<TlsJa3Data>; +} + +export interface TlsJa3Data { + hash?: Maybe<string[]>; +} + +export interface TlsServerCertificateData { + fingerprint?: Maybe<FingerprintData>; +} + +export interface ZeekEcsFields { + session_id?: Maybe<string[]>; + + connection?: Maybe<ZeekConnectionData>; + + notice?: Maybe<ZeekNoticeData>; + + dns?: Maybe<ZeekDnsData>; + + http?: Maybe<ZeekHttpData>; + + files?: Maybe<ZeekFileData>; + + ssl?: Maybe<ZeekSslData>; +} + +export interface ZeekConnectionData { + local_resp?: Maybe<boolean[]>; + + local_orig?: Maybe<boolean[]>; + + missed_bytes?: Maybe<number[]>; + + state?: Maybe<string[]>; + + history?: Maybe<string[]>; +} + +export interface ZeekNoticeData { + suppress_for?: Maybe<number[]>; + + msg?: Maybe<string[]>; + + note?: Maybe<string[]>; + + sub?: Maybe<string[]>; + + dst?: Maybe<string[]>; + + dropped?: Maybe<boolean[]>; + + peer_descr?: Maybe<string[]>; +} + +export interface ZeekDnsData { + AA?: Maybe<boolean[]>; + + qclass_name?: Maybe<string[]>; + + RD?: Maybe<boolean[]>; + + qtype_name?: Maybe<string[]>; + + rejected?: Maybe<boolean[]>; + + qtype?: Maybe<string[]>; + + query?: Maybe<string[]>; + + trans_id?: Maybe<number[]>; + + qclass?: Maybe<string[]>; + + RA?: Maybe<boolean[]>; + + TC?: Maybe<boolean[]>; +} + +export interface ZeekHttpData { + resp_mime_types?: Maybe<string[]>; + + trans_depth?: Maybe<string[]>; + + status_msg?: Maybe<string[]>; + + resp_fuids?: Maybe<string[]>; + + tags?: Maybe<string[]>; +} + +export interface ZeekFileData { + session_ids?: Maybe<string[]>; + + timedout?: Maybe<boolean[]>; + + local_orig?: Maybe<boolean[]>; + + tx_host?: Maybe<string[]>; + + source?: Maybe<string[]>; + + is_orig?: Maybe<boolean[]>; + + overflow_bytes?: Maybe<number[]>; + + sha1?: Maybe<string[]>; + + duration?: Maybe<number[]>; + + depth?: Maybe<number[]>; + + analyzers?: Maybe<string[]>; + + mime_type?: Maybe<string[]>; + + rx_host?: Maybe<string[]>; + + total_bytes?: Maybe<number[]>; + + fuid?: Maybe<string[]>; + + seen_bytes?: Maybe<number[]>; + + missing_bytes?: Maybe<number[]>; + + md5?: Maybe<string[]>; +} + +export interface ZeekSslData { + cipher?: Maybe<string[]>; + + established?: Maybe<boolean[]>; + + resumed?: Maybe<boolean[]>; + + version?: Maybe<string[]>; +} + +export interface HttpEcsFields { + version?: Maybe<string[]>; + + request?: Maybe<HttpRequestData>; + + response?: Maybe<HttpResponseData>; +} + +export interface HttpRequestData { + method?: Maybe<string[]>; + + body?: Maybe<HttpBodyData>; + + referrer?: Maybe<string[]>; + + bytes?: Maybe<number[]>; +} + +export interface HttpBodyData { + content?: Maybe<string[]>; + + bytes?: Maybe<number[]>; +} + +export interface HttpResponseData { + status_code?: Maybe<number[]>; + + body?: Maybe<HttpBodyData>; + + bytes?: Maybe<number[]>; +} + +export interface UrlEcsFields { + domain?: Maybe<string[]>; + + original?: Maybe<string[]>; + + username?: Maybe<string[]>; + + password?: Maybe<string[]>; +} + +export interface WinlogEcsFields { + event_id?: Maybe<number[]>; +} + +export interface ProcessEcsFields { + hash?: Maybe<ProcessHashData>; + + pid?: Maybe<number[]>; + + name?: Maybe<string[]>; + + ppid?: Maybe<number[]>; + + args?: Maybe<string[]>; + + executable?: Maybe<string[]>; + + title?: Maybe<string[]>; + + thread?: Maybe<Thread>; + + working_directory?: Maybe<string[]>; +} + +export interface ProcessHashData { + md5?: Maybe<string[]>; + + sha1?: Maybe<string[]>; + + sha256?: Maybe<string[]>; +} + +export interface Thread { + id?: Maybe<number[]>; + + start?: Maybe<string[]>; +} + +export interface FileFields { + name?: Maybe<string[]>; + + path?: Maybe<string[]>; + + target_path?: Maybe<string[]>; + + extension?: Maybe<string[]>; + + type?: Maybe<string[]>; + + device?: Maybe<string[]>; + + inode?: Maybe<string[]>; + + uid?: Maybe<string[]>; + + owner?: Maybe<string[]>; + + gid?: Maybe<string[]>; + + group?: Maybe<string[]>; + + mode?: Maybe<string[]>; + + size?: Maybe<number[]>; + + mtime?: Maybe<string[]>; + + ctime?: Maybe<string[]>; +} + +export interface SystemEcsField { + audit?: Maybe<AuditEcsFields>; + + auth?: Maybe<AuthEcsFields>; +} + +export interface AuditEcsFields { + package?: Maybe<PackageEcsFields>; +} + +export interface PackageEcsFields { + arch?: Maybe<string[]>; + + entity_id?: Maybe<string[]>; + + name?: Maybe<string[]>; + + size?: Maybe<number[]>; + + summary?: Maybe<string[]>; + + version?: Maybe<string[]>; +} + +export interface AuthEcsFields { + ssh?: Maybe<SshEcsFields>; +} + +export interface SshEcsFields { + method?: Maybe<string[]>; + + signature?: Maybe<string[]>; +} + +export interface PageInfo { + endCursor?: Maybe<CursorType>; + + hasNextPage?: Maybe<boolean>; +} + +export interface TimelineDetailsData { + data?: Maybe<DetailItem[]>; + + inspect?: Maybe<Inspect>; +} + +export interface DetailItem { + field: string; + + values?: Maybe<string[]>; + + originalValue?: Maybe<EsValue>; +} + +export interface LastEventTimeData { + lastSeen?: Maybe<string>; + + inspect?: Maybe<Inspect>; +} + +export interface HostsData { + edges: HostsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface HostsEdges { + node: HostItem; + + cursor: CursorType; +} + +export interface HostItem { + _id?: Maybe<string>; + + lastSeen?: Maybe<string>; + + host?: Maybe<HostEcsFields>; + + cloud?: Maybe<CloudFields>; + + inspect?: Maybe<Inspect>; +} + +export interface CloudFields { + instance?: Maybe<CloudInstance>; + + machine?: Maybe<CloudMachine>; + + provider?: Maybe<(Maybe<string>)[]>; + + region?: Maybe<(Maybe<string>)[]>; +} + +export interface CloudInstance { + id?: Maybe<(Maybe<string>)[]>; +} + +export interface CloudMachine { + type?: Maybe<(Maybe<string>)[]>; +} + +export interface FirstLastSeenHost { + inspect?: Maybe<Inspect>; + + firstSeen?: Maybe<string>; + + lastSeen?: Maybe<string>; +} + +export interface IpOverviewData { + client?: Maybe<Overview>; + + destination?: Maybe<Overview>; + + host: HostEcsFields; + + server?: Maybe<Overview>; + + source?: Maybe<Overview>; + + inspect?: Maybe<Inspect>; +} + +export interface Overview { + firstSeen?: Maybe<string>; + + lastSeen?: Maybe<string>; + + autonomousSystem: AutonomousSystem; + + geo: GeoEcsFields; +} + +export interface AutonomousSystem { + number?: Maybe<number>; + + organization?: Maybe<AutonomousSystemOrganization>; +} + +export interface AutonomousSystemOrganization { + name?: Maybe<string>; +} + +export interface UsersData { + edges: UsersEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface UsersEdges { + node: UsersNode; + + cursor: CursorType; +} + +export interface UsersNode { + _id?: Maybe<string>; + + timestamp?: Maybe<string>; + + user?: Maybe<UsersItem>; +} + +export interface UsersItem { + name?: Maybe<string>; + + id?: Maybe<string[]>; + + groupId?: Maybe<string[]>; + + groupName?: Maybe<string[]>; + + count?: Maybe<number>; +} + +export interface KpiNetworkData { + networkEvents?: Maybe<number>; + + uniqueFlowId?: Maybe<number>; + + uniqueSourcePrivateIps?: Maybe<number>; + + uniqueSourcePrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; + + uniqueDestinationPrivateIps?: Maybe<number>; + + uniqueDestinationPrivateIpsHistogram?: Maybe<KpiNetworkHistogramData[]>; + + dnsQueries?: Maybe<number>; + + tlsHandshakes?: Maybe<number>; + + inspect?: Maybe<Inspect>; +} + +export interface KpiNetworkHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; +} + +export interface KpiHostsData { + hosts?: Maybe<number>; + + hostsHistogram?: Maybe<KpiHostHistogramData[]>; + + authSuccess?: Maybe<number>; + + authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; + + authFailure?: Maybe<number>; + + authFailureHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueSourceIps?: Maybe<number>; + + uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueDestinationIps?: Maybe<number>; + + uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + inspect?: Maybe<Inspect>; +} + +export interface KpiHostHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; +} + +export interface KpiHostDetailsData { + authSuccess?: Maybe<number>; + + authSuccessHistogram?: Maybe<KpiHostHistogramData[]>; + + authFailure?: Maybe<number>; + + authFailureHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueSourceIps?: Maybe<number>; + + uniqueSourceIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + uniqueDestinationIps?: Maybe<number>; + + uniqueDestinationIpsHistogram?: Maybe<KpiHostHistogramData[]>; + + inspect?: Maybe<Inspect>; +} + +export interface MatrixHistogramOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface MatrixOverTimeHistogramData { + x?: Maybe<number>; + + y?: Maybe<number>; + + g?: Maybe<string>; +} + +export interface NetworkTopCountriesData { + edges: NetworkTopCountriesEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface NetworkTopCountriesEdges { + node: NetworkTopCountriesItem; + + cursor: CursorType; +} + +export interface NetworkTopCountriesItem { + _id?: Maybe<string>; + + source?: Maybe<TopCountriesItemSource>; + + destination?: Maybe<TopCountriesItemDestination>; + + network?: Maybe<TopNetworkTablesEcsField>; +} + +export interface TopCountriesItemSource { + country?: Maybe<string>; + + destination_ips?: Maybe<number>; + + flows?: Maybe<number>; + + location?: Maybe<GeoItem>; + + source_ips?: Maybe<number>; +} + +export interface GeoItem { + geo?: Maybe<GeoEcsFields>; + + flowTarget?: Maybe<FlowTargetSourceDest>; +} + +export interface TopCountriesItemDestination { + country?: Maybe<string>; + + destination_ips?: Maybe<number>; + + flows?: Maybe<number>; + + location?: Maybe<GeoItem>; + + source_ips?: Maybe<number>; +} + +export interface TopNetworkTablesEcsField { + bytes_in?: Maybe<number>; + + bytes_out?: Maybe<number>; +} + +export interface NetworkTopNFlowData { + edges: NetworkTopNFlowEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface NetworkTopNFlowEdges { + node: NetworkTopNFlowItem; + + cursor: CursorType; +} + +export interface NetworkTopNFlowItem { + _id?: Maybe<string>; + + source?: Maybe<TopNFlowItemSource>; + + destination?: Maybe<TopNFlowItemDestination>; + + network?: Maybe<TopNetworkTablesEcsField>; +} + +export interface TopNFlowItemSource { + autonomous_system?: Maybe<AutonomousSystemItem>; + + domain?: Maybe<string[]>; + + ip?: Maybe<string>; + + location?: Maybe<GeoItem>; + + flows?: Maybe<number>; + + destination_ips?: Maybe<number>; +} + +export interface AutonomousSystemItem { + name?: Maybe<string>; + + number?: Maybe<number>; +} + +export interface TopNFlowItemDestination { + autonomous_system?: Maybe<AutonomousSystemItem>; + + domain?: Maybe<string[]>; + + ip?: Maybe<string>; + + location?: Maybe<GeoItem>; + + flows?: Maybe<number>; + + source_ips?: Maybe<number>; +} + +export interface NetworkDnsData { + edges: NetworkDnsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; + + histogram?: Maybe<MatrixOverOrdinalHistogramData[]>; +} + +export interface NetworkDnsEdges { + node: NetworkDnsItem; + + cursor: CursorType; +} + +export interface NetworkDnsItem { + _id?: Maybe<string>; + + dnsBytesIn?: Maybe<number>; + + dnsBytesOut?: Maybe<number>; + + dnsName?: Maybe<string>; + + queryCount?: Maybe<number>; + + uniqueDomains?: Maybe<number>; +} + +export interface MatrixOverOrdinalHistogramData { + x: string; + + y: number; + + g: string; +} + +export interface NetworkDsOverTimeData { + inspect?: Maybe<Inspect>; + + matrixHistogramData: MatrixOverTimeHistogramData[]; + + totalCount: number; +} + +export interface NetworkHttpData { + edges: NetworkHttpEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface NetworkHttpEdges { + node: NetworkHttpItem; + + cursor: CursorType; +} + +export interface NetworkHttpItem { + _id?: Maybe<string>; + + domains: string[]; + + lastHost?: Maybe<string>; + + lastSourceIp?: Maybe<string>; + + methods: string[]; + + path?: Maybe<string>; + + requestCount?: Maybe<number>; + + statuses: string[]; +} + +export interface OverviewNetworkData { + auditbeatSocket?: Maybe<number>; + + filebeatCisco?: Maybe<number>; + + filebeatNetflow?: Maybe<number>; + + filebeatPanw?: Maybe<number>; + + filebeatSuricata?: Maybe<number>; + + filebeatZeek?: Maybe<number>; + + packetbeatDNS?: Maybe<number>; + + packetbeatFlow?: Maybe<number>; + + packetbeatTLS?: Maybe<number>; + + inspect?: Maybe<Inspect>; +} + +export interface OverviewHostData { + auditbeatAuditd?: Maybe<number>; + + auditbeatFIM?: Maybe<number>; + + auditbeatLogin?: Maybe<number>; + + auditbeatPackage?: Maybe<number>; + + auditbeatProcess?: Maybe<number>; + + auditbeatUser?: Maybe<number>; + + endgameDns?: Maybe<number>; + + endgameFile?: Maybe<number>; + + endgameImageLoad?: Maybe<number>; + + endgameNetwork?: Maybe<number>; + + endgameProcess?: Maybe<number>; + + endgameRegistry?: Maybe<number>; + + endgameSecurity?: Maybe<number>; + + filebeatSystemModule?: Maybe<number>; + + winlogbeatSecurity?: Maybe<number>; + + winlogbeatMWSysmonOperational?: Maybe<number>; + + inspect?: Maybe<Inspect>; +} + +export interface TlsData { + edges: TlsEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface TlsEdges { + node: TlsNode; + + cursor: CursorType; +} + +export interface TlsNode { + _id?: Maybe<string>; + + timestamp?: Maybe<string>; + + notAfter?: Maybe<string[]>; + + subjects?: Maybe<string[]>; + + ja3?: Maybe<string[]>; + + issuers?: Maybe<string[]>; +} + +export interface UncommonProcessesData { + edges: UncommonProcessesEdges[]; + + totalCount: number; + + pageInfo: PageInfoPaginated; + + inspect?: Maybe<Inspect>; +} + +export interface UncommonProcessesEdges { + node: UncommonProcessItem; + + cursor: CursorType; +} + +export interface UncommonProcessItem { + _id: string; + + instances: number; + + process: ProcessEcsFields; + + hosts: HostEcsFields[]; + + user?: Maybe<UserEcsFields>; +} + +export interface SayMyName { + /** The id of the source */ + appName: string; +} + +export interface TimelineResult { + columns?: Maybe<ColumnHeaderResult[]>; + + created?: Maybe<number>; + + createdBy?: Maybe<string>; + + dataProviders?: Maybe<DataProviderResult[]>; + + dateRange?: Maybe<DateRangePickerResult>; + + description?: Maybe<string>; + + eventIdToNoteIds?: Maybe<NoteResult[]>; + + eventType?: Maybe<string>; + + favorite?: Maybe<FavoriteTimelineResult[]>; + + filters?: Maybe<FilterTimelineResult[]>; + + kqlMode?: Maybe<string>; + + kqlQuery?: Maybe<SerializedFilterQueryResult>; + + notes?: Maybe<NoteResult[]>; + + noteIds?: Maybe<string[]>; + + pinnedEventIds?: Maybe<string[]>; + + pinnedEventsSaveObject?: Maybe<PinnedEvent[]>; + + savedQueryId?: Maybe<string>; + + savedObjectId: string; + + sort?: Maybe<SortTimelineResult>; + + title?: Maybe<string>; + + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + + updated?: Maybe<number>; + + updatedBy?: Maybe<string>; + + version: string; +} + +export interface ColumnHeaderResult { + aggregatable?: Maybe<boolean>; + + category?: Maybe<string>; + + columnHeaderType?: Maybe<string>; + + description?: Maybe<string>; + + example?: Maybe<string>; + + indexes?: Maybe<string[]>; + + id?: Maybe<string>; + + name?: Maybe<string>; + + placeholder?: Maybe<string>; + + searchable?: Maybe<boolean>; + + type?: Maybe<string>; +} + +export interface DataProviderResult { + id?: Maybe<string>; + + name?: Maybe<string>; + + enabled?: Maybe<boolean>; + + excluded?: Maybe<boolean>; + + kqlQuery?: Maybe<string>; + + queryMatch?: Maybe<QueryMatchResult>; + + and?: Maybe<DataProviderResult[]>; +} + +export interface QueryMatchResult { + field?: Maybe<string>; + + displayField?: Maybe<string>; + + value?: Maybe<string>; + + displayValue?: Maybe<string>; + + operator?: Maybe<string>; +} + +export interface DateRangePickerResult { + start?: Maybe<number>; + + end?: Maybe<number>; +} + +export interface FavoriteTimelineResult { + fullName?: Maybe<string>; + + userName?: Maybe<string>; + + favoriteDate?: Maybe<number>; +} + +export interface FilterTimelineResult { + exists?: Maybe<string>; + + meta?: Maybe<FilterMetaTimelineResult>; + + match_all?: Maybe<string>; + + missing?: Maybe<string>; + + query?: Maybe<string>; + + range?: Maybe<string>; + + script?: Maybe<string>; +} + +export interface FilterMetaTimelineResult { + alias?: Maybe<string>; + + controlledBy?: Maybe<string>; + + disabled?: Maybe<boolean>; + + field?: Maybe<string>; + + formattedValue?: Maybe<string>; + + index?: Maybe<string>; + + key?: Maybe<string>; + + negate?: Maybe<boolean>; + + params?: Maybe<string>; + + type?: Maybe<string>; + + value?: Maybe<string>; +} + +export interface SerializedFilterQueryResult { + filterQuery?: Maybe<SerializedKueryQueryResult>; +} + +export interface SerializedKueryQueryResult { + kuery?: Maybe<KueryFilterQueryResult>; + + serializedQuery?: Maybe<string>; +} + +export interface KueryFilterQueryResult { + kind?: Maybe<string>; + + expression?: Maybe<string>; +} + +export interface SortTimelineResult { + columnId?: Maybe<string>; + + sortDirection?: Maybe<string>; +} + +export interface ResponseTimelines { + timeline: (Maybe<TimelineResult>)[]; + + totalCount?: Maybe<number>; +} + +export interface Mutation { + /** Persists a note */ + persistNote: ResponseNote; + + deleteNote?: Maybe<boolean>; + + deleteNoteByTimelineId?: Maybe<boolean>; + /** Persists a pinned event in a timeline */ + persistPinnedEventOnTimeline?: Maybe<PinnedEvent>; + /** Remove a pinned events in a timeline */ + deletePinnedEventOnTimeline: boolean; + /** Remove all pinned events in a timeline */ + deleteAllPinnedEventsOnTimeline: boolean; + /** Persists a timeline */ + persistTimeline: ResponseTimeline; + + persistFavorite: ResponseFavoriteTimeline; + + deleteTimeline: boolean; +} + +export interface ResponseNote { + code?: Maybe<number>; + + message?: Maybe<string>; + + note: NoteResult; +} + +export interface ResponseTimeline { + code?: Maybe<number>; + + message?: Maybe<string>; + + timeline: TimelineResult; +} + +export interface ResponseFavoriteTimeline { + code?: Maybe<number>; + + message?: Maybe<string>; + + savedObjectId: string; + + version: string; + + favorite?: Maybe<FavoriteTimelineResult[]>; +} + +export interface EcsEdges { + node: Ecs; + + cursor: CursorType; +} + +export interface EventsTimelineData { + edges: EcsEdges[]; + + totalCount: number; + + pageInfo: PageInfo; + + inspect?: Maybe<Inspect>; +} + +export interface OsFields { + platform?: Maybe<string>; + + name?: Maybe<string>; + + full?: Maybe<string>; + + family?: Maybe<string>; + + version?: Maybe<string>; + + kernel?: Maybe<string>; +} + +export interface HostFields { + architecture?: Maybe<string>; + + id?: Maybe<string>; + + ip?: Maybe<(Maybe<string>)[]>; + + mac?: Maybe<(Maybe<string>)[]>; + + name?: Maybe<string>; + + os?: Maybe<OsFields>; + + type?: Maybe<string>; +} + +// ==================================================== +// Arguments +// ==================================================== + +export interface GetNoteQueryArgs { + id: string; +} +export interface GetNotesByTimelineIdQueryArgs { + timelineId: string; +} +export interface GetNotesByEventIdQueryArgs { + eventId: string; +} +export interface GetAllNotesQueryArgs { + pageInfo?: Maybe<PageInfoNote>; + + search?: Maybe<string>; + + sort?: Maybe<SortNote>; +} +export interface GetAllPinnedEventsByTimelineIdQueryArgs { + timelineId: string; +} +export interface SourceQueryArgs { + /** The id of the source */ + id: string; +} +export interface GetOneTimelineQueryArgs { + id: string; +} +export interface GetAllTimelineQueryArgs { + pageInfo?: Maybe<PageInfoTimeline>; + + search?: Maybe<string>; + + sort?: Maybe<SortTimeline>; + + onlyUserFavorite?: Maybe<boolean>; +} +export interface AuthenticationsSourceArgs { + timerange: TimerangeInput; + + pagination: PaginationInputPaginated; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface TimelineSourceArgs { + pagination: PaginationInput; + + sortField: SortField; + + fieldRequested: string[]; + + timerange?: Maybe<TimerangeInput>; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface TimelineDetailsSourceArgs { + eventId: string; + + indexName: string; + + defaultIndex: string[]; +} +export interface LastEventTimeSourceArgs { + id?: Maybe<string>; + + indexKey: LastEventIndexKey; + + details: LastTimeDetails; + + defaultIndex: string[]; +} +export interface HostsSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + pagination: PaginationInputPaginated; + + sort: HostsSortField; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface HostOverviewSourceArgs { + id?: Maybe<string>; + + hostName: string; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface HostFirstLastSeenSourceArgs { + id?: Maybe<string>; + + hostName: string; + + defaultIndex: string[]; +} +export interface IpOverviewSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip: string; + + defaultIndex: string[]; +} +export interface UsersSourceArgs { + filterQuery?: Maybe<string>; + + id?: Maybe<string>; + + ip: string; + + pagination: PaginationInputPaginated; + + sort: UsersSortField; + + flowTarget: FlowTarget; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface KpiNetworkSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface KpiHostsSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface KpiHostDetailsSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface MatrixHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField: string; + + histogramType: HistogramType; +} +export interface NetworkTopCountriesSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip?: Maybe<string>; + + flowTarget: FlowTargetSourceDest; + + pagination: PaginationInputPaginated; + + sort: NetworkTopTablesSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface NetworkTopNFlowSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip?: Maybe<string>; + + flowTarget: FlowTargetSourceDest; + + pagination: PaginationInputPaginated; + + sort: NetworkTopTablesSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface NetworkDnsSourceArgs { + filterQuery?: Maybe<string>; + + id?: Maybe<string>; + + isPtrIncluded: boolean; + + pagination: PaginationInputPaginated; + + sort: NetworkDnsSortField; + + stackByField?: Maybe<string>; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface NetworkDnsHistogramSourceArgs { + filterQuery?: Maybe<string>; + + defaultIndex: string[]; + + timerange: TimerangeInput; + + stackByField?: Maybe<string>; +} +export interface NetworkHttpSourceArgs { + id?: Maybe<string>; + + filterQuery?: Maybe<string>; + + ip?: Maybe<string>; + + pagination: PaginationInputPaginated; + + sort: NetworkHttpSortField; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface OverviewNetworkSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface OverviewHostSourceArgs { + id?: Maybe<string>; + + timerange: TimerangeInput; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface TlsSourceArgs { + filterQuery?: Maybe<string>; + + id?: Maybe<string>; + + ip: string; + + pagination: PaginationInputPaginated; + + sort: TlsSortField; + + flowTarget: FlowTargetSourceDest; + + timerange: TimerangeInput; + + defaultIndex: string[]; +} +export interface UncommonProcessesSourceArgs { + timerange: TimerangeInput; + + pagination: PaginationInputPaginated; + + filterQuery?: Maybe<string>; + + defaultIndex: string[]; +} +export interface IndicesExistSourceStatusArgs { + defaultIndex: string[]; +} +export interface IndexFieldsSourceStatusArgs { + defaultIndex: string[]; +} +export interface PersistNoteMutationArgs { + noteId?: Maybe<string>; + + version?: Maybe<string>; + + note: NoteInput; +} +export interface DeleteNoteMutationArgs { + id: string[]; +} +export interface DeleteNoteByTimelineIdMutationArgs { + timelineId: string; + + version?: Maybe<string>; +} +export interface PersistPinnedEventOnTimelineMutationArgs { + pinnedEventId?: Maybe<string>; + + eventId: string; + + timelineId?: Maybe<string>; +} +export interface DeletePinnedEventOnTimelineMutationArgs { + id: string[]; +} +export interface DeleteAllPinnedEventsOnTimelineMutationArgs { + timelineId: string; +} +export interface PersistTimelineMutationArgs { + id?: Maybe<string>; + + version?: Maybe<string>; + + timeline: TimelineInput; +} +export interface PersistFavoriteMutationArgs { + timelineId?: Maybe<string>; +} +export interface DeleteTimelineMutationArgs { + id: string[]; +} + +// ==================================================== +// Documents +// ==================================================== + +export namespace GetAuthenticationsQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + pagination: PaginationInputPaginated; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Authentications: Authentications; + }; + + export type Authentications = { + __typename?: 'AuthenticationsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'AuthenticationsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'AuthenticationItem'; + + _id: string; + + failures: number; + + successes: number; + + user: User; + + lastSuccess: Maybe<LastSuccess>; + + lastFailure: Maybe<LastFailure>; + }; + + export type User = { + __typename?: 'UserEcsFields'; + + name: Maybe<string[]>; + }; + + export type LastSuccess = { + __typename?: 'LastSourceHost'; + + timestamp: Maybe<string>; + + source: Maybe<_Source>; + + host: Maybe<Host>; + }; + + export type _Source = { + __typename?: 'SourceEcsFields'; + + ip: Maybe<string[]>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type LastFailure = { + __typename?: 'LastSourceHost'; + + timestamp: Maybe<string>; + + source: Maybe<__Source>; + + host: Maybe<_Host>; + }; + + export type __Source = { + __typename?: 'SourceEcsFields'; + + ip: Maybe<string[]>; + }; + + export type _Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetLastEventTimeQuery { + export type Variables = { + sourceId: string; + indexKey: LastEventIndexKey; + details: LastTimeDetails; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + LastEventTime: LastEventTime; + }; + + export type LastEventTime = { + __typename?: 'LastEventTimeData'; + + lastSeen: Maybe<string>; + }; +} + +export namespace GetHostFirstLastSeenQuery { + export type Variables = { + sourceId: string; + hostName: string; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + HostFirstLastSeen: HostFirstLastSeen; + }; + + export type HostFirstLastSeen = { + __typename?: 'FirstLastSeenHost'; + + firstSeen: Maybe<string>; + + lastSeen: Maybe<string>; + }; +} + +export namespace GetHostsTableQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + pagination: PaginationInputPaginated; + sort: HostsSortField; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Hosts: Hosts; + }; + + export type Hosts = { + __typename?: 'HostsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'HostsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'HostItem'; + + _id: Maybe<string>; + + lastSeen: Maybe<string>; + + host: Maybe<Host>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + + os: Maybe<Os>; + }; + + export type Os = { + __typename?: 'OsEcsFields'; + + name: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetHostOverviewQuery { + export type Variables = { + sourceId: string; + hostName: string; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + HostOverview: HostOverview; + }; + + export type HostOverview = { + __typename?: 'HostItem'; + + _id: Maybe<string>; + + host: Maybe<Host>; + + cloud: Maybe<Cloud>; + + inspect: Maybe<Inspect>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + architecture: Maybe<string[]>; + + id: Maybe<string[]>; + + ip: Maybe<string[]>; + + mac: Maybe<string[]>; + + name: Maybe<string[]>; + + os: Maybe<Os>; + + type: Maybe<string[]>; + }; + + export type Os = { + __typename?: 'OsEcsFields'; + + family: Maybe<string[]>; + + name: Maybe<string[]>; + + platform: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Cloud = { + __typename?: 'CloudFields'; + + instance: Maybe<Instance>; + + machine: Maybe<Machine>; + + provider: Maybe<(Maybe<string>)[]>; + + region: Maybe<(Maybe<string>)[]>; + }; + + export type Instance = { + __typename?: 'CloudInstance'; + + id: Maybe<(Maybe<string>)[]>; + }; + + export type Machine = { + __typename?: 'CloudMachine'; + + type: Maybe<(Maybe<string>)[]>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetIpOverviewQuery { + export type Variables = { + sourceId: string; + filterQuery?: Maybe<string>; + ip: string; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + IpOverview: Maybe<IpOverview>; + }; + + export type IpOverview = { + __typename?: 'IpOverviewData'; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + host: Host; + + inspect: Maybe<Inspect>; + }; + + export type _Source = { + __typename?: 'Overview'; + + firstSeen: Maybe<string>; + + lastSeen: Maybe<string>; + + autonomousSystem: AutonomousSystem; + + geo: Geo; + }; + + export type AutonomousSystem = { + __typename?: 'AutonomousSystem'; + + number: Maybe<number>; + + organization: Maybe<Organization>; + }; + + export type Organization = { + __typename?: 'AutonomousSystemOrganization'; + + name: Maybe<string>; + }; + + export type Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + city_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + country_name: Maybe<string[]>; + + location: Maybe<Location>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Location = { + __typename?: 'Location'; + + lat: Maybe<number[]>; + + lon: Maybe<number[]>; + }; + + export type Destination = { + __typename?: 'Overview'; + + firstSeen: Maybe<string>; + + lastSeen: Maybe<string>; + + autonomousSystem: _AutonomousSystem; + + geo: _Geo; + }; + + export type _AutonomousSystem = { + __typename?: 'AutonomousSystem'; + + number: Maybe<number>; + + organization: Maybe<_Organization>; + }; + + export type _Organization = { + __typename?: 'AutonomousSystemOrganization'; + + name: Maybe<string>; + }; + + export type _Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + city_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + country_name: Maybe<string[]>; + + location: Maybe<_Location>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type _Location = { + __typename?: 'Location'; + + lat: Maybe<number[]>; + + lon: Maybe<number[]>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + architecture: Maybe<string[]>; + + id: Maybe<string[]>; + + ip: Maybe<string[]>; + + mac: Maybe<string[]>; + + name: Maybe<string[]>; + + os: Maybe<Os>; + + type: Maybe<string[]>; + }; + + export type Os = { + __typename?: 'OsEcsFields'; + + family: Maybe<string[]>; + + name: Maybe<string[]>; + + platform: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetKpiHostDetailsQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + KpiHostDetails: KpiHostDetails; + }; + + export type KpiHostDetails = { + __typename?: 'KpiHostDetailsData'; + + authSuccess: Maybe<number>; + + authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; + + authFailure: Maybe<number>; + + authFailureHistogram: Maybe<AuthFailureHistogram[]>; + + uniqueSourceIps: Maybe<number>; + + uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; + + uniqueDestinationIps: Maybe<number>; + + uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; + + inspect: Maybe<Inspect>; + }; + + export type AuthSuccessHistogram = KpiHostDetailsChartFields.Fragment; + + export type AuthFailureHistogram = KpiHostDetailsChartFields.Fragment; + + export type UniqueSourceIpsHistogram = KpiHostDetailsChartFields.Fragment; + + export type UniqueDestinationIpsHistogram = KpiHostDetailsChartFields.Fragment; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetKpiHostsQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + KpiHosts: KpiHosts; + }; + + export type KpiHosts = { + __typename?: 'KpiHostsData'; + + hosts: Maybe<number>; + + hostsHistogram: Maybe<HostsHistogram[]>; + + authSuccess: Maybe<number>; + + authSuccessHistogram: Maybe<AuthSuccessHistogram[]>; + + authFailure: Maybe<number>; + + authFailureHistogram: Maybe<AuthFailureHistogram[]>; + + uniqueSourceIps: Maybe<number>; + + uniqueSourceIpsHistogram: Maybe<UniqueSourceIpsHistogram[]>; + + uniqueDestinationIps: Maybe<number>; + + uniqueDestinationIpsHistogram: Maybe<UniqueDestinationIpsHistogram[]>; + + inspect: Maybe<Inspect>; + }; + + export type HostsHistogram = KpiHostChartFields.Fragment; + + export type AuthSuccessHistogram = KpiHostChartFields.Fragment; + + export type AuthFailureHistogram = KpiHostChartFields.Fragment; + + export type UniqueSourceIpsHistogram = KpiHostChartFields.Fragment; + + export type UniqueDestinationIpsHistogram = KpiHostChartFields.Fragment; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetKpiNetworkQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + KpiNetwork: Maybe<KpiNetwork>; + }; + + export type KpiNetwork = { + __typename?: 'KpiNetworkData'; + + networkEvents: Maybe<number>; + + uniqueFlowId: Maybe<number>; + + uniqueSourcePrivateIps: Maybe<number>; + + uniqueSourcePrivateIpsHistogram: Maybe<UniqueSourcePrivateIpsHistogram[]>; + + uniqueDestinationPrivateIps: Maybe<number>; + + uniqueDestinationPrivateIpsHistogram: Maybe<UniqueDestinationPrivateIpsHistogram[]>; + + dnsQueries: Maybe<number>; + + tlsHandshakes: Maybe<number>; + + inspect: Maybe<Inspect>; + }; + + export type UniqueSourcePrivateIpsHistogram = KpiNetworkChartFields.Fragment; + + export type UniqueDestinationPrivateIpsHistogram = KpiNetworkChartFields.Fragment; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetMatrixHistogramQuery { + export type Variables = { + defaultIndex: string[]; + filterQuery?: Maybe<string>; + histogramType: HistogramType; + inspect: boolean; + sourceId: string; + stackByField: string; + timerange: TimerangeInput; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + MatrixHistogram: MatrixHistogram; + }; + + export type MatrixHistogram = { + __typename?: 'MatrixHistogramOverTimeData'; + + matrixHistogramData: MatrixHistogramData[]; + + totalCount: number; + + inspect: Maybe<Inspect>; + }; + + export type MatrixHistogramData = { + __typename?: 'MatrixOverTimeHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + + g: Maybe<string>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkDnsQuery { + export type Variables = { + defaultIndex: string[]; + filterQuery?: Maybe<string>; + inspect: boolean; + isPtrIncluded: boolean; + pagination: PaginationInputPaginated; + sort: NetworkDnsSortField; + sourceId: string; + stackByField?: Maybe<string>; + timerange: TimerangeInput; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkDns: NetworkDns; + }; + + export type NetworkDns = { + __typename?: 'NetworkDnsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkDnsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkDnsItem'; + + _id: Maybe<string>; + + dnsBytesIn: Maybe<number>; + + dnsBytesOut: Maybe<number>; + + dnsName: Maybe<string>; + + queryCount: Maybe<number>; + + uniqueDomains: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkHttpQuery { + export type Variables = { + sourceId: string; + ip?: Maybe<string>; + filterQuery?: Maybe<string>; + pagination: PaginationInputPaginated; + sort: NetworkHttpSortField; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkHttp: NetworkHttp; + }; + + export type NetworkHttp = { + __typename?: 'NetworkHttpData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkHttpEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkHttpItem'; + + domains: string[]; + + lastHost: Maybe<string>; + + lastSourceIp: Maybe<string>; + + methods: string[]; + + path: Maybe<string>; + + requestCount: Maybe<number>; + + statuses: string[]; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkTopCountriesQuery { + export type Variables = { + sourceId: string; + ip?: Maybe<string>; + filterQuery?: Maybe<string>; + pagination: PaginationInputPaginated; + sort: NetworkTopTablesSortField; + flowTarget: FlowTargetSourceDest; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkTopCountries: NetworkTopCountries; + }; + + export type NetworkTopCountries = { + __typename?: 'NetworkTopCountriesData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkTopCountriesEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkTopCountriesItem'; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + network: Maybe<Network>; + }; + + export type _Source = { + __typename?: 'TopCountriesItemSource'; + + country: Maybe<string>; + + destination_ips: Maybe<number>; + + flows: Maybe<number>; + + source_ips: Maybe<number>; + }; + + export type Destination = { + __typename?: 'TopCountriesItemDestination'; + + country: Maybe<string>; + + destination_ips: Maybe<number>; + + flows: Maybe<number>; + + source_ips: Maybe<number>; + }; + + export type Network = { + __typename?: 'TopNetworkTablesEcsField'; + + bytes_in: Maybe<number>; + + bytes_out: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetNetworkTopNFlowQuery { + export type Variables = { + sourceId: string; + ip?: Maybe<string>; + filterQuery?: Maybe<string>; + pagination: PaginationInputPaginated; + sort: NetworkTopTablesSortField; + flowTarget: FlowTargetSourceDest; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + NetworkTopNFlow: NetworkTopNFlow; + }; + + export type NetworkTopNFlow = { + __typename?: 'NetworkTopNFlowData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'NetworkTopNFlowEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'NetworkTopNFlowItem'; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + network: Maybe<Network>; + }; + + export type _Source = { + __typename?: 'TopNFlowItemSource'; + + autonomous_system: Maybe<AutonomousSystem>; + + domain: Maybe<string[]>; + + ip: Maybe<string>; + + location: Maybe<Location>; + + flows: Maybe<number>; + + destination_ips: Maybe<number>; + }; + + export type AutonomousSystem = { + __typename?: 'AutonomousSystemItem'; + + name: Maybe<string>; + + number: Maybe<number>; + }; + + export type Location = { + __typename?: 'GeoItem'; + + geo: Maybe<Geo>; + + flowTarget: Maybe<FlowTargetSourceDest>; + }; + + export type Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Destination = { + __typename?: 'TopNFlowItemDestination'; + + autonomous_system: Maybe<_AutonomousSystem>; + + domain: Maybe<string[]>; + + ip: Maybe<string>; + + location: Maybe<_Location>; + + flows: Maybe<number>; + + source_ips: Maybe<number>; + }; + + export type _AutonomousSystem = { + __typename?: 'AutonomousSystemItem'; + + name: Maybe<string>; + + number: Maybe<number>; + }; + + export type _Location = { + __typename?: 'GeoItem'; + + geo: Maybe<_Geo>; + + flowTarget: Maybe<FlowTargetSourceDest>; + }; + + export type _Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Network = { + __typename?: 'TopNetworkTablesEcsField'; + + bytes_in: Maybe<number>; + + bytes_out: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetOverviewHostQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + OverviewHost: Maybe<OverviewHost>; + }; + + export type OverviewHost = { + __typename?: 'OverviewHostData'; + + auditbeatAuditd: Maybe<number>; + + auditbeatFIM: Maybe<number>; + + auditbeatLogin: Maybe<number>; + + auditbeatPackage: Maybe<number>; + + auditbeatProcess: Maybe<number>; + + auditbeatUser: Maybe<number>; + + endgameDns: Maybe<number>; + + endgameFile: Maybe<number>; + + endgameImageLoad: Maybe<number>; + + endgameNetwork: Maybe<number>; + + endgameProcess: Maybe<number>; + + endgameRegistry: Maybe<number>; + + endgameSecurity: Maybe<number>; + + filebeatSystemModule: Maybe<number>; + + winlogbeatSecurity: Maybe<number>; + + winlogbeatMWSysmonOperational: Maybe<number>; + + inspect: Maybe<Inspect>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetOverviewNetworkQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + OverviewNetwork: Maybe<OverviewNetwork>; + }; + + export type OverviewNetwork = { + __typename?: 'OverviewNetworkData'; + + auditbeatSocket: Maybe<number>; + + filebeatCisco: Maybe<number>; + + filebeatNetflow: Maybe<number>; + + filebeatPanw: Maybe<number>; + + filebeatSuricata: Maybe<number>; + + filebeatZeek: Maybe<number>; + + packetbeatDNS: Maybe<number>; + + packetbeatFlow: Maybe<number>; + + packetbeatTLS: Maybe<number>; + + inspect: Maybe<Inspect>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace SourceQuery { + export type Variables = { + sourceId?: Maybe<string>; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + status: Status; + }; + + export type Status = { + __typename?: 'SourceStatus'; + + indicesExist: boolean; + + indexFields: IndexFields[]; + }; + + export type IndexFields = { + __typename?: 'IndexField'; + + category: string; + + description: Maybe<string>; + + example: Maybe<string>; + + indexes: (Maybe<string>)[]; + + name: string; + + searchable: boolean; + + type: string; + + aggregatable: boolean; + + format: Maybe<string>; + }; +} + +export namespace GetAllTimeline { + export type Variables = { + pageInfo: PageInfoTimeline; + search?: Maybe<string>; + sort?: Maybe<SortTimeline>; + onlyUserFavorite?: Maybe<boolean>; + }; + + export type Query = { + __typename?: 'Query'; + + getAllTimeline: GetAllTimeline; + }; + + export type GetAllTimeline = { + __typename?: 'ResponseTimelines'; + + totalCount: Maybe<number>; + + timeline: (Maybe<Timeline>)[]; + }; + + export type Timeline = { + __typename?: 'TimelineResult'; + + savedObjectId: string; + + description: Maybe<string>; + + favorite: Maybe<Favorite[]>; + + eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; + + notes: Maybe<Notes[]>; + + noteIds: Maybe<string[]>; + + pinnedEventIds: Maybe<string[]>; + + title: Maybe<string>; + + timelineType: Maybe<TimelineType>; + + templateTimelineId: Maybe<string>; + + templateTimelineVersion: Maybe<number>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: string; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; + + export type EventIdToNoteIds = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + timelineVersion: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type Notes = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; +} + +export namespace DeleteTimelineMutation { + export type Variables = { + id: string[]; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + deleteTimeline: boolean; + }; +} + +export namespace GetTimelineDetailsQuery { + export type Variables = { + sourceId: string; + eventId: string; + indexName: string; + defaultIndex: string[]; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + TimelineDetails: TimelineDetails; + }; + + export type TimelineDetails = { + __typename?: 'TimelineDetailsData'; + + data: Maybe<Data[]>; + }; + + export type Data = { + __typename?: 'DetailItem'; + + field: string; + + values: Maybe<string[]>; + + originalValue: Maybe<EsValue>; + }; +} + +export namespace PersistTimelineFavoriteMutation { + export type Variables = { + timelineId?: Maybe<string>; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistFavorite: PersistFavorite; + }; + + export type PersistFavorite = { + __typename?: 'ResponseFavoriteTimeline'; + + savedObjectId: string; + + version: string; + + favorite: Maybe<Favorite[]>; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; +} + +export namespace GetTimelineQuery { + export type Variables = { + sourceId: string; + fieldRequested: string[]; + pagination: PaginationInput; + sortField: SortField; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Timeline: Timeline; + }; + + export type Timeline = { + __typename?: 'TimelineData'; + + totalCount: number; + + inspect: Maybe<Inspect>; + + pageInfo: PageInfo; + + edges: Edges[]; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; + + export type PageInfo = { + __typename?: 'PageInfo'; + + endCursor: Maybe<EndCursor>; + + hasNextPage: Maybe<boolean>; + }; + + export type EndCursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + + tiebreaker: Maybe<string>; + }; + + export type Edges = { + __typename?: 'TimelineEdges'; + + node: Node; + }; + + export type Node = { + __typename?: 'TimelineItem'; + + _id: string; + + _index: Maybe<string>; + + data: Data[]; + + ecs: Ecs; + }; + + export type Data = { + __typename?: 'TimelineNonEcsData'; + + field: string; + + value: Maybe<string[]>; + }; + + export type Ecs = { + __typename?: 'ECS'; + + _id: string; + + _index: Maybe<string>; + + timestamp: Maybe<string>; + + message: Maybe<string[]>; + + system: Maybe<System>; + + event: Maybe<Event>; + + auditd: Maybe<Auditd>; + + file: Maybe<File>; + + host: Maybe<Host>; + + rule: Maybe<Rule>; + + source: Maybe<_Source>; + + destination: Maybe<Destination>; + + dns: Maybe<Dns>; + + endgame: Maybe<Endgame>; + + geo: Maybe<__Geo>; + + signal: Maybe<Signal>; + + suricata: Maybe<Suricata>; + + network: Maybe<Network>; + + http: Maybe<Http>; + + tls: Maybe<Tls>; + + url: Maybe<Url>; + + user: Maybe<User>; + + winlog: Maybe<Winlog>; + + process: Maybe<Process>; + + zeek: Maybe<Zeek>; + }; + + export type System = { + __typename?: 'SystemEcsField'; + + auth: Maybe<Auth>; + + audit: Maybe<Audit>; + }; + + export type Auth = { + __typename?: 'AuthEcsFields'; + + ssh: Maybe<Ssh>; + }; + + export type Ssh = { + __typename?: 'SshEcsFields'; + + signature: Maybe<string[]>; + + method: Maybe<string[]>; + }; + + export type Audit = { + __typename?: 'AuditEcsFields'; + + package: Maybe<Package>; + }; + + export type Package = { + __typename?: 'PackageEcsFields'; + + arch: Maybe<string[]>; + + entity_id: Maybe<string[]>; + + name: Maybe<string[]>; + + size: Maybe<number[]>; + + summary: Maybe<string[]>; + + version: Maybe<string[]>; + }; + + export type Event = { + __typename?: 'EventEcsFields'; + + action: Maybe<string[]>; + + category: Maybe<string[]>; + + code: Maybe<string[]>; + + created: Maybe<string[]>; + + dataset: Maybe<string[]>; + + duration: Maybe<number[]>; + + end: Maybe<string[]>; + + hash: Maybe<string[]>; + + id: Maybe<string[]>; + + kind: Maybe<string[]>; + + module: Maybe<string[]>; + + original: Maybe<string[]>; + + outcome: Maybe<string[]>; + + risk_score: Maybe<number[]>; + + risk_score_norm: Maybe<number[]>; + + severity: Maybe<number[]>; + + start: Maybe<string[]>; + + timezone: Maybe<string[]>; + + type: Maybe<string[]>; + }; + + export type Auditd = { + __typename?: 'AuditdEcsFields'; + + result: Maybe<string[]>; + + session: Maybe<string[]>; + + data: Maybe<_Data>; + + summary: Maybe<Summary>; + }; + + export type _Data = { + __typename?: 'AuditdData'; + + acct: Maybe<string[]>; + + terminal: Maybe<string[]>; + + op: Maybe<string[]>; + }; + + export type Summary = { + __typename?: 'Summary'; + + actor: Maybe<Actor>; + + object: Maybe<Object>; + + how: Maybe<string[]>; + + message_type: Maybe<string[]>; + + sequence: Maybe<string[]>; + }; + + export type Actor = { + __typename?: 'PrimarySecondary'; + + primary: Maybe<string[]>; + + secondary: Maybe<string[]>; + }; + + export type Object = { + __typename?: 'PrimarySecondary'; + + primary: Maybe<string[]>; + + secondary: Maybe<string[]>; + + type: Maybe<string[]>; + }; + + export type File = { + __typename?: 'FileFields'; + + name: Maybe<string[]>; + + path: Maybe<string[]>; + + target_path: Maybe<string[]>; + + extension: Maybe<string[]>; + + type: Maybe<string[]>; + + device: Maybe<string[]>; + + inode: Maybe<string[]>; + + uid: Maybe<string[]>; + + owner: Maybe<string[]>; + + gid: Maybe<string[]>; + + group: Maybe<string[]>; + + mode: Maybe<string[]>; + + size: Maybe<number[]>; + + mtime: Maybe<string[]>; + + ctime: Maybe<string[]>; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + + ip: Maybe<string[]>; + }; + + export type Rule = { + __typename?: 'RuleEcsField'; + + reference: Maybe<string[]>; + }; + + export type _Source = { + __typename?: 'SourceEcsFields'; + + bytes: Maybe<number[]>; + + ip: Maybe<string[]>; + + packets: Maybe<number[]>; + + port: Maybe<number[]>; + + geo: Maybe<Geo>; + }; + + export type Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Destination = { + __typename?: 'DestinationEcsFields'; + + bytes: Maybe<number[]>; + + ip: Maybe<string[]>; + + packets: Maybe<number[]>; + + port: Maybe<number[]>; + + geo: Maybe<_Geo>; + }; + + export type _Geo = { + __typename?: 'GeoEcsFields'; + + continent_name: Maybe<string[]>; + + country_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + + city_name: Maybe<string[]>; + + region_iso_code: Maybe<string[]>; + + region_name: Maybe<string[]>; + }; + + export type Dns = { + __typename?: 'DnsEcsFields'; + + question: Maybe<Question>; + + resolved_ip: Maybe<string[]>; + + response_code: Maybe<string[]>; + }; + + export type Question = { + __typename?: 'DnsQuestionData'; + + name: Maybe<string[]>; + + type: Maybe<string[]>; + }; + + export type Endgame = { + __typename?: 'EndgameEcsFields'; + + exit_code: Maybe<number[]>; + + file_name: Maybe<string[]>; + + file_path: Maybe<string[]>; + + logon_type: Maybe<number[]>; + + parent_process_name: Maybe<string[]>; + + pid: Maybe<number[]>; + + process_name: Maybe<string[]>; + + subject_domain_name: Maybe<string[]>; + + subject_logon_id: Maybe<string[]>; + + subject_user_name: Maybe<string[]>; + + target_domain_name: Maybe<string[]>; + + target_logon_id: Maybe<string[]>; + + target_user_name: Maybe<string[]>; + }; + + export type __Geo = { + __typename?: 'GeoEcsFields'; + + region_name: Maybe<string[]>; + + country_iso_code: Maybe<string[]>; + }; + + export type Signal = { + __typename?: 'SignalField'; + + original_time: Maybe<string[]>; + + rule: Maybe<_Rule>; + }; + + export type _Rule = { + __typename?: 'RuleField'; + + id: Maybe<string[]>; + + saved_id: Maybe<string[]>; + + timeline_id: Maybe<string[]>; + + timeline_title: Maybe<string[]>; + + output_index: Maybe<string[]>; + + from: Maybe<string[]>; + + index: Maybe<string[]>; + + language: Maybe<string[]>; + + query: Maybe<string[]>; + + to: Maybe<string[]>; + + filters: Maybe<ToAny>; + + note: Maybe<string[]>; + }; + + export type Suricata = { + __typename?: 'SuricataEcsFields'; + + eve: Maybe<Eve>; + }; + + export type Eve = { + __typename?: 'SuricataEveData'; + + proto: Maybe<string[]>; + + flow_id: Maybe<number[]>; + + alert: Maybe<Alert>; + }; + + export type Alert = { + __typename?: 'SuricataAlertData'; + + signature: Maybe<string[]>; + + signature_id: Maybe<number[]>; + }; + + export type Network = { + __typename?: 'NetworkEcsField'; + + bytes: Maybe<number[]>; + + community_id: Maybe<string[]>; + + direction: Maybe<string[]>; + + packets: Maybe<number[]>; + + protocol: Maybe<string[]>; + + transport: Maybe<string[]>; + }; + + export type Http = { + __typename?: 'HttpEcsFields'; + + version: Maybe<string[]>; + + request: Maybe<Request>; + + response: Maybe<Response>; + }; + + export type Request = { + __typename?: 'HttpRequestData'; + + method: Maybe<string[]>; + + body: Maybe<Body>; + + referrer: Maybe<string[]>; + }; + + export type Body = { + __typename?: 'HttpBodyData'; + + bytes: Maybe<number[]>; + + content: Maybe<string[]>; + }; + + export type Response = { + __typename?: 'HttpResponseData'; + + status_code: Maybe<number[]>; + + body: Maybe<_Body>; + }; + + export type _Body = { + __typename?: 'HttpBodyData'; + + bytes: Maybe<number[]>; + + content: Maybe<string[]>; + }; + + export type Tls = { + __typename?: 'TlsEcsFields'; + + client_certificate: Maybe<ClientCertificate>; + + fingerprints: Maybe<Fingerprints>; + + server_certificate: Maybe<ServerCertificate>; + }; + + export type ClientCertificate = { + __typename?: 'TlsClientCertificateData'; + + fingerprint: Maybe<Fingerprint>; + }; + + export type Fingerprint = { + __typename?: 'FingerprintData'; + + sha1: Maybe<string[]>; + }; + + export type Fingerprints = { + __typename?: 'TlsFingerprintsData'; + + ja3: Maybe<Ja3>; + }; + + export type Ja3 = { + __typename?: 'TlsJa3Data'; + + hash: Maybe<string[]>; + }; + + export type ServerCertificate = { + __typename?: 'TlsServerCertificateData'; + + fingerprint: Maybe<_Fingerprint>; + }; + + export type _Fingerprint = { + __typename?: 'FingerprintData'; + + sha1: Maybe<string[]>; + }; + + export type Url = { + __typename?: 'UrlEcsFields'; + + original: Maybe<string[]>; + + domain: Maybe<string[]>; + + username: Maybe<string[]>; + + password: Maybe<string[]>; + }; + + export type User = { + __typename?: 'UserEcsFields'; + + domain: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type Winlog = { + __typename?: 'WinlogEcsFields'; + + event_id: Maybe<number[]>; + }; + + export type Process = { + __typename?: 'ProcessEcsFields'; + + hash: Maybe<Hash>; + + pid: Maybe<number[]>; + + name: Maybe<string[]>; + + ppid: Maybe<number[]>; + + args: Maybe<string[]>; + + executable: Maybe<string[]>; + + title: Maybe<string[]>; + + working_directory: Maybe<string[]>; + }; + + export type Hash = { + __typename?: 'ProcessHashData'; + + md5: Maybe<string[]>; + + sha1: Maybe<string[]>; + + sha256: Maybe<string[]>; + }; + + export type Zeek = { + __typename?: 'ZeekEcsFields'; + + session_id: Maybe<string[]>; + + connection: Maybe<Connection>; + + notice: Maybe<Notice>; + + dns: Maybe<_Dns>; + + http: Maybe<_Http>; + + files: Maybe<Files>; + + ssl: Maybe<Ssl>; + }; + + export type Connection = { + __typename?: 'ZeekConnectionData'; + + local_resp: Maybe<boolean[]>; + + local_orig: Maybe<boolean[]>; + + missed_bytes: Maybe<number[]>; + + state: Maybe<string[]>; + + history: Maybe<string[]>; + }; + + export type Notice = { + __typename?: 'ZeekNoticeData'; + + suppress_for: Maybe<number[]>; + + msg: Maybe<string[]>; + + note: Maybe<string[]>; + + sub: Maybe<string[]>; + + dst: Maybe<string[]>; + + dropped: Maybe<boolean[]>; + + peer_descr: Maybe<string[]>; + }; + + export type _Dns = { + __typename?: 'ZeekDnsData'; + + AA: Maybe<boolean[]>; + + qclass_name: Maybe<string[]>; + + RD: Maybe<boolean[]>; + + qtype_name: Maybe<string[]>; + + rejected: Maybe<boolean[]>; + + qtype: Maybe<string[]>; + + query: Maybe<string[]>; + + trans_id: Maybe<number[]>; + + qclass: Maybe<string[]>; + + RA: Maybe<boolean[]>; + + TC: Maybe<boolean[]>; + }; + + export type _Http = { + __typename?: 'ZeekHttpData'; + + resp_mime_types: Maybe<string[]>; + + trans_depth: Maybe<string[]>; + + status_msg: Maybe<string[]>; + + resp_fuids: Maybe<string[]>; + + tags: Maybe<string[]>; + }; + + export type Files = { + __typename?: 'ZeekFileData'; + + session_ids: Maybe<string[]>; + + timedout: Maybe<boolean[]>; + + local_orig: Maybe<boolean[]>; + + tx_host: Maybe<string[]>; + + source: Maybe<string[]>; + + is_orig: Maybe<boolean[]>; + + overflow_bytes: Maybe<number[]>; + + sha1: Maybe<string[]>; + + duration: Maybe<number[]>; + + depth: Maybe<number[]>; + + analyzers: Maybe<string[]>; + + mime_type: Maybe<string[]>; + + rx_host: Maybe<string[]>; + + total_bytes: Maybe<number[]>; + + fuid: Maybe<string[]>; + + seen_bytes: Maybe<number[]>; + + missing_bytes: Maybe<number[]>; + + md5: Maybe<string[]>; + }; + + export type Ssl = { + __typename?: 'ZeekSslData'; + + cipher: Maybe<string[]>; + + established: Maybe<boolean[]>; + + resumed: Maybe<boolean[]>; + + version: Maybe<string[]>; + }; +} + +export namespace PersistTimelineNoteMutation { + export type Variables = { + noteId?: Maybe<string>; + version?: Maybe<string>; + note: NoteInput; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistNote: PersistNote; + }; + + export type PersistNote = { + __typename?: 'ResponseNote'; + + code: Maybe<number>; + + message: Maybe<string>; + + note: Note; + }; + + export type Note = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; +} + +export namespace GetOneTimeline { + export type Variables = { + id: string; + }; + + export type Query = { + __typename?: 'Query'; + + getOneTimeline: GetOneTimeline; + }; + + export type GetOneTimeline = { + __typename?: 'TimelineResult'; + + savedObjectId: string; + + columns: Maybe<Columns[]>; + + dataProviders: Maybe<DataProviders[]>; + + dateRange: Maybe<DateRange>; + + description: Maybe<string>; + + eventType: Maybe<string>; + + eventIdToNoteIds: Maybe<EventIdToNoteIds[]>; + + favorite: Maybe<Favorite[]>; + + filters: Maybe<Filters[]>; + + kqlMode: Maybe<string>; + + kqlQuery: Maybe<KqlQuery>; + + notes: Maybe<Notes[]>; + + noteIds: Maybe<string[]>; + + pinnedEventIds: Maybe<string[]>; + + pinnedEventsSaveObject: Maybe<PinnedEventsSaveObject[]>; + + title: Maybe<string>; + + savedQueryId: Maybe<string>; + + sort: Maybe<Sort>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: string; + }; + + export type Columns = { + __typename?: 'ColumnHeaderResult'; + + aggregatable: Maybe<boolean>; + + category: Maybe<string>; + + columnHeaderType: Maybe<string>; + + description: Maybe<string>; + + example: Maybe<string>; + + indexes: Maybe<string[]>; + + id: Maybe<string>; + + name: Maybe<string>; + + searchable: Maybe<boolean>; + + type: Maybe<string>; + }; + + export type DataProviders = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<QueryMatch>; + + and: Maybe<And[]>; + }; + + export type QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type And = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<_QueryMatch>; + }; + + export type _QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type DateRange = { + __typename?: 'DateRangePickerResult'; + + start: Maybe<number>; + + end: Maybe<number>; + }; + + export type EventIdToNoteIds = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + timelineVersion: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; + + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe<Meta>; + + query: Maybe<string>; + + exists: Maybe<string>; + + match_all: Maybe<string>; + + missing: Maybe<string>; + + range: Maybe<string>; + + script: Maybe<string>; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe<string>; + + controlledBy: Maybe<string>; + + disabled: Maybe<boolean>; + + field: Maybe<string>; + + formattedValue: Maybe<string>; + + index: Maybe<string>; + + key: Maybe<string>; + + negate: Maybe<boolean>; + + params: Maybe<string>; + + type: Maybe<string>; + + value: Maybe<string>; + }; + + export type KqlQuery = { + __typename?: 'SerializedFilterQueryResult'; + + filterQuery: Maybe<FilterQuery>; + }; + + export type FilterQuery = { + __typename?: 'SerializedKueryQueryResult'; + + kuery: Maybe<Kuery>; + + serializedQuery: Maybe<string>; + }; + + export type Kuery = { + __typename?: 'KueryFilterQueryResult'; + + kind: Maybe<string>; + + expression: Maybe<string>; + }; + + export type Notes = { + __typename?: 'NoteResult'; + + eventId: Maybe<string>; + + note: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + noteId: string; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type PinnedEventsSaveObject = { + __typename?: 'PinnedEvent'; + + pinnedEventId: string; + + eventId: Maybe<string>; + + timelineId: Maybe<string>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; + + export type Sort = { + __typename?: 'SortTimelineResult'; + + columnId: Maybe<string>; + + sortDirection: Maybe<string>; + }; +} + +export namespace PersistTimelineMutation { + export type Variables = { + timelineId?: Maybe<string>; + version?: Maybe<string>; + timeline: TimelineInput; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistTimeline: PersistTimeline; + }; + + export type PersistTimeline = { + __typename?: 'ResponseTimeline'; + + code: Maybe<number>; + + message: Maybe<string>; + + timeline: Timeline; + }; + + export type Timeline = { + __typename?: 'TimelineResult'; + + savedObjectId: string; + + version: string; + + columns: Maybe<Columns[]>; + + dataProviders: Maybe<DataProviders[]>; + + description: Maybe<string>; + + eventType: Maybe<string>; + + favorite: Maybe<Favorite[]>; + + filters: Maybe<Filters[]>; + + kqlMode: Maybe<string>; + + kqlQuery: Maybe<KqlQuery>; + + title: Maybe<string>; + + dateRange: Maybe<DateRange>; + + savedQueryId: Maybe<string>; + + sort: Maybe<Sort>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + }; + + export type Columns = { + __typename?: 'ColumnHeaderResult'; + + aggregatable: Maybe<boolean>; + + category: Maybe<string>; + + columnHeaderType: Maybe<string>; + + description: Maybe<string>; + + example: Maybe<string>; + + indexes: Maybe<string[]>; + + id: Maybe<string>; + + name: Maybe<string>; + + searchable: Maybe<boolean>; + + type: Maybe<string>; + }; + + export type DataProviders = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<QueryMatch>; + + and: Maybe<And[]>; + }; + + export type QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type And = { + __typename?: 'DataProviderResult'; + + id: Maybe<string>; + + name: Maybe<string>; + + enabled: Maybe<boolean>; + + excluded: Maybe<boolean>; + + kqlQuery: Maybe<string>; + + queryMatch: Maybe<_QueryMatch>; + }; + + export type _QueryMatch = { + __typename?: 'QueryMatchResult'; + + field: Maybe<string>; + + displayField: Maybe<string>; + + value: Maybe<string>; + + displayValue: Maybe<string>; + + operator: Maybe<string>; + }; + + export type Favorite = { + __typename?: 'FavoriteTimelineResult'; + + fullName: Maybe<string>; + + userName: Maybe<string>; + + favoriteDate: Maybe<number>; + }; + + export type Filters = { + __typename?: 'FilterTimelineResult'; + + meta: Maybe<Meta>; + + query: Maybe<string>; + + exists: Maybe<string>; + + match_all: Maybe<string>; + + missing: Maybe<string>; + + range: Maybe<string>; + + script: Maybe<string>; + }; + + export type Meta = { + __typename?: 'FilterMetaTimelineResult'; + + alias: Maybe<string>; + + controlledBy: Maybe<string>; + + disabled: Maybe<boolean>; + + field: Maybe<string>; + + formattedValue: Maybe<string>; + + index: Maybe<string>; + + key: Maybe<string>; + + negate: Maybe<boolean>; + + params: Maybe<string>; + + type: Maybe<string>; + + value: Maybe<string>; + }; + + export type KqlQuery = { + __typename?: 'SerializedFilterQueryResult'; + + filterQuery: Maybe<FilterQuery>; + }; + + export type FilterQuery = { + __typename?: 'SerializedKueryQueryResult'; + + kuery: Maybe<Kuery>; + + serializedQuery: Maybe<string>; + }; + + export type Kuery = { + __typename?: 'KueryFilterQueryResult'; + + kind: Maybe<string>; + + expression: Maybe<string>; + }; + + export type DateRange = { + __typename?: 'DateRangePickerResult'; + + start: Maybe<number>; + + end: Maybe<number>; + }; + + export type Sort = { + __typename?: 'SortTimelineResult'; + + columnId: Maybe<string>; + + sortDirection: Maybe<string>; + }; +} + +export namespace PersistTimelinePinnedEventMutation { + export type Variables = { + pinnedEventId?: Maybe<string>; + eventId: string; + timelineId?: Maybe<string>; + }; + + export type Mutation = { + __typename?: 'Mutation'; + + persistPinnedEventOnTimeline: Maybe<PersistPinnedEventOnTimeline>; + }; + + export type PersistPinnedEventOnTimeline = { + __typename?: 'PinnedEvent'; + + pinnedEventId: string; + + eventId: Maybe<string>; + + timelineId: Maybe<string>; + + timelineVersion: Maybe<string>; + + created: Maybe<number>; + + createdBy: Maybe<string>; + + updated: Maybe<number>; + + updatedBy: Maybe<string>; + + version: Maybe<string>; + }; +} + +export namespace GetTlsQuery { + export type Variables = { + sourceId: string; + filterQuery?: Maybe<string>; + flowTarget: FlowTargetSourceDest; + ip: string; + pagination: PaginationInputPaginated; + sort: TlsSortField; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Tls: Tls; + }; + + export type Tls = { + __typename?: 'TlsData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'TlsEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'TlsNode'; + + _id: Maybe<string>; + + subjects: Maybe<string[]>; + + ja3: Maybe<string[]>; + + issuers: Maybe<string[]>; + + notAfter: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetUncommonProcessesQuery { + export type Variables = { + sourceId: string; + timerange: TimerangeInput; + pagination: PaginationInputPaginated; + filterQuery?: Maybe<string>; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + UncommonProcesses: UncommonProcesses; + }; + + export type UncommonProcesses = { + __typename?: 'UncommonProcessesData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'UncommonProcessesEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'UncommonProcessItem'; + + _id: string; + + instances: number; + + process: Process; + + user: Maybe<User>; + + hosts: Hosts[]; + }; + + export type Process = { + __typename?: 'ProcessEcsFields'; + + args: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type User = { + __typename?: 'UserEcsFields'; + + id: Maybe<string[]>; + + name: Maybe<string[]>; + }; + + export type Hosts = { + __typename?: 'HostEcsFields'; + + name: Maybe<string[]>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace GetUsersQuery { + export type Variables = { + sourceId: string; + filterQuery?: Maybe<string>; + flowTarget: FlowTarget; + ip: string; + pagination: PaginationInputPaginated; + sort: UsersSortField; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + Users: Users; + }; + + export type Users = { + __typename?: 'UsersData'; + + totalCount: number; + + edges: Edges[]; + + pageInfo: PageInfo; + + inspect: Maybe<Inspect>; + }; + + export type Edges = { + __typename?: 'UsersEdges'; + + node: Node; + + cursor: Cursor; + }; + + export type Node = { + __typename?: 'UsersNode'; + + user: Maybe<User>; + }; + + export type User = { + __typename?: 'UsersItem'; + + name: Maybe<string>; + + id: Maybe<string[]>; + + groupId: Maybe<string[]>; + + groupName: Maybe<string[]>; + + count: Maybe<number>; + }; + + export type Cursor = { + __typename?: 'CursorType'; + + value: Maybe<string>; + }; + + export type PageInfo = { + __typename?: 'PageInfoPaginated'; + + activePage: number; + + fakeTotalCount: number; + + showMorePagesIndicator: boolean; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; +} + +export namespace KpiHostDetailsChartFields { + export type Fragment = { + __typename?: 'KpiHostHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + }; +} + +export namespace KpiHostChartFields { + export type Fragment = { + __typename?: 'KpiHostHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + }; +} + +export namespace KpiNetworkChartFields { + export type Fragment = { + __typename?: 'KpiNetworkHistogramData'; + + x: Maybe<number>; + + y: Maybe<number>; + }; +} diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/plugins/siem/public/hooks/api/__mock__/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx rename to x-pack/plugins/siem/public/hooks/api/__mock__/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/plugins/siem/public/hooks/api/api.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/api.tsx rename to x-pack/plugins/siem/public/hooks/api/api.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/helpers.test.tsx b/x-pack/plugins/siem/public/hooks/api/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/helpers.test.tsx rename to x-pack/plugins/siem/public/hooks/api/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/helpers.tsx b/x-pack/plugins/siem/public/hooks/api/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/api/helpers.tsx rename to x-pack/plugins/siem/public/hooks/api/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/hooks/translations.ts b/x-pack/plugins/siem/public/hooks/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/translations.ts rename to x-pack/plugins/siem/public/hooks/translations.ts diff --git a/x-pack/plugins/siem/public/hooks/types.ts b/x-pack/plugins/siem/public/hooks/types.ts new file mode 100644 index 0000000000000..6527904964d00 --- /dev/null +++ b/x-pack/plugins/siem/public/hooks/types.ts @@ -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 { SimpleSavedObject } from '../../../../../src/core/public'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IndexPatternSavedObjectAttributes = { title: string }; + +export type IndexPatternSavedObject = Pick< + SimpleSavedObject<IndexPatternSavedObjectAttributes>, + 'type' | 'id' | 'attributes' | '_version' +>; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/plugins/siem/public/hooks/use_index_patterns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx rename to x-pack/plugins/siem/public/hooks/use_index_patterns.tsx diff --git a/x-pack/plugins/siem/public/index.ts b/x-pack/plugins/siem/public/index.ts new file mode 100644 index 0000000000000..46f72c1fa17c8 --- /dev/null +++ b/x-pack/plugins/siem/public/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from '../../../../src/core/public'; +import { Plugin, PluginSetup, PluginStart } from './plugin'; + +export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(context); + +export { Plugin, PluginSetup, PluginStart }; diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx b/x-pack/plugins/siem/public/lib/clipboard/clipboard.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/clipboard/clipboard.tsx rename to x-pack/plugins/siem/public/lib/clipboard/clipboard.tsx diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/translations.ts b/x-pack/plugins/siem/public/lib/clipboard/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/clipboard/translations.ts rename to x-pack/plugins/siem/public/lib/clipboard/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx rename to x-pack/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx diff --git a/x-pack/legacy/plugins/siem/public/lib/compose/helpers.test.ts b/x-pack/plugins/siem/public/lib/compose/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/compose/helpers.test.ts rename to x-pack/plugins/siem/public/lib/compose/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts b/x-pack/plugins/siem/public/lib/compose/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/compose/helpers.ts rename to x-pack/plugins/siem/public/lib/compose/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/compose/kibana_compose.tsx b/x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx similarity index 94% rename from x-pack/legacy/plugins/siem/public/lib/compose/kibana_compose.tsx rename to x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx index c742ced4c504c..fb30c9a5411ed 100644 --- a/x-pack/legacy/plugins/siem/public/lib/compose/kibana_compose.tsx +++ b/x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx @@ -8,8 +8,8 @@ import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemo import ApolloClient from 'apollo-client'; import { ApolloLink } from 'apollo-link'; +import { CoreStart } from '../../../../../../src/core/public'; import introspectionQueryResultData from '../../graphql/introspection.json'; -import { CoreStart } from '../../plugin'; import { AppFrontendLibs } from '../lib'; import { getLinks } from './helpers'; diff --git a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx new file mode 100644 index 0000000000000..c5a35da56284d --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useEffect } from 'react'; +import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; + +import { isEmpty, get } from 'lodash/fp'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorFieldsProps } from '../../../../../../triggers_actions_ui/public/types'; +import { FieldMapping } from '../../../../pages/case/components/configure_cases/field_mapping'; + +import { defaultMapping } from '../../config'; +import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; + +import * as i18n from '../../translations'; +import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; + +export const withConnectorFlyout = <T extends ActionConnector>({ + ConnectorFormComponent, + secretKeys = [], + configKeys = [], +}: ConnectorFlyoutHOCProps<T>) => { + const ConnectorFlyout: React.FC<ActionConnectorFieldsProps<T>> = ({ + action, + editActionConfig, + editActionSecrets, + errors, + }) => { + /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. + * If we do, errors will be shown the first time the flyout is open even though the user did not + * interact with the form. Also, we would like to show errors for empty fields provided by the user. + /*/ + const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; + const configKeysWithDefault = [...configKeys, 'apiUrl']; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + /** + * We need to distinguish between the add flyout and the edit flyout. + * useEffect will run only once on component mount. + * This guarantees that the function below will run only once. + * On the first render of the component the apiUrl can be either undefined or filled. + * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. + */ + + useEffect(() => { + if (!isEmpty(apiUrl)) { + secretKeys.forEach((key: string) => editActionSecrets(key, '')); + } + }, []); + + if (isEmpty(mapping)) { + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: defaultMapping, + }); + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + [] + ); + + const handleOnBlurActionConfig = useCallback( + (key: string) => { + if (configKeysWithDefault.includes(key) && get(key, action.config) == null) { + editActionConfig(key, ''); + } + }, + [action.config] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + [] + ); + + const handleOnBlurSecretConfig = useCallback( + (key: string) => { + if (secretKeys.includes(key) && get(key, action.secrets) == null) { + editActionSecrets(key, ''); + } + }, + [action.secrets] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: newMapping, + }), + [action.config] + ); + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + id="apiUrl" + fullWidth + error={errors.apiUrl} + isInvalid={isApiUrlInvalid} + label={i18n.API_URL_LABEL} + > + <EuiFieldText + fullWidth + isInvalid={isApiUrlInvalid} + name="apiUrl" + value={apiUrl || ''} // Needed to prevent uncontrolled input error when value is undefined + data-test-subj="apiUrlFromInput" + placeholder="https://<site-url>" + onChange={evt => handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="m" /> + <ConnectorFormComponent + errors={errors} + action={action} + onChangeSecret={handleOnChangeSecretConfig} + onBlurSecret={handleOnBlurSecretConfig} + onChangeConfig={handleOnChangeActionConfig} + onBlurConfig={handleOnBlurActionConfig} + /> + <EuiSpacer size="l" /> + <EuiFlexGroup> + <EuiFlexItem> + <FieldMapping + disabled={true} + mapping={mapping as CasesConfigurationMapping[]} + onChangeMapping={handleOnChangeMappingConfig} + /> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); + }; + + return ConnectorFlyout; +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/lib/connectors/config.ts new file mode 100644 index 0000000000000..98473e49622a9 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/config.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasesConfigurationMapping } from '../../containers/case/configure/types'; + +import { Connector } from './types'; +import { connector as serviceNowConnectorConfig } from './servicenow/config'; +import { connector as jiraConnectorConfig } from './jira/config'; + +export const connectorsConfiguration: Record<string, Connector> = { + '.servicenow': serviceNowConnectorConfig, + '.jira': jiraConnectorConfig, +}; + +export const defaultMapping: CasesConfigurationMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; diff --git a/x-pack/plugins/siem/public/lib/connectors/index.ts b/x-pack/plugins/siem/public/lib/connectors/index.ts new file mode 100644 index 0000000000000..2ce61bef49c5e --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getActionType as serviceNowActionType } from './servicenow'; +export { getActionType as jiraActionType } from './jira'; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/config.ts b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts new file mode 100644 index 0000000000000..42bd1b9cdc191 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Connector } from '../types'; + +import { JIRA_TITLE } from './translations'; +import logo from './logo.svg'; + +export const connector: Connector = { + id: '.jira', + name: JIRA_TITLE, + logo, + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx new file mode 100644 index 0000000000000..482808fca53b1 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx @@ -0,0 +1,110 @@ +/* + * 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 { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import * as i18n from './translations'; +import { ConnectorFlyoutFormProps } from '../types'; +import { JiraActionConnector } from './types'; +import { withConnectorFlyout } from '../components/connector_flyout'; + +const JiraConnectorForm: React.FC<ConnectorFlyoutFormProps<JiraActionConnector>> = ({ + errors, + action, + onChangeSecret, + onBlurSecret, + onChangeConfig, + onBlurConfig, +}) => { + const { projectKey } = action.config; + const { email, apiToken } = action.secrets; + const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null; + const isEmailInvalid: boolean = errors.email.length > 0 && email != null; + const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + id="connector-jira-project-key" + fullWidth + error={errors.projectKey} + isInvalid={isProjectKeyInvalid} + label={i18n.JIRA_PROJECT_KEY_LABEL} + > + <EuiFieldText + fullWidth + isInvalid={isProjectKeyInvalid} + name="connector-jira-project-key" + value={projectKey || ''} // Needed to prevent uncontrolled input error when value is undefined + data-test-subj="connector-jira-project-key-form-input" + onChange={evt => onChangeConfig('projectKey', evt.target.value)} + onBlur={() => onBlurConfig('projectKey')} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="m" /> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + id="connector-jira-email" + fullWidth + error={errors.email} + isInvalid={isEmailInvalid} + label={i18n.EMAIL_LABEL} + > + <EuiFieldText + fullWidth + isInvalid={isEmailInvalid} + name="connector-jira-email" + value={email || ''} // Needed to prevent uncontrolled input error when value is undefined + data-test-subj="connector-jira-email-form-input" + onChange={evt => onChangeSecret('email', evt.target.value)} + onBlur={() => onBlurSecret('email')} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="m" /> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + id="connector-jira-apiToken" + fullWidth + error={errors.apiToken} + isInvalid={isApiTokenInvalid} + label={i18n.API_TOKEN_LABEL} + > + <EuiFieldPassword + fullWidth + isInvalid={isApiTokenInvalid} + name="connector-jira-apiToken" + value={apiToken || ''} // Needed to prevent uncontrolled input error when value is undefined + data-test-subj="connector-jira-apiToken-form-input" + onChange={evt => onChangeSecret('apiToken', evt.target.value)} + onBlur={() => onBlurSecret('apiToken')} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +export const JiraConnectorFlyout = withConnectorFlyout<JiraActionConnector>({ + ConnectorFormComponent: JiraConnectorForm, + secretKeys: ['email', 'apiToken'], + configKeys: ['projectKey'], +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx new file mode 100644 index 0000000000000..ada9608e37c98 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx @@ -0,0 +1,54 @@ +/* + * 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 { + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/types'; + +import { connector } from './config'; +import { createActionType } from '../utils'; +import logo from './logo.svg'; +import { JiraActionConnector } from './types'; +import { JiraConnectorFlyout } from './flyout'; +import * as i18n from './translations'; + +interface Errors { + projectKey: string[]; + email: string[]; + apiToken: string[]; +} + +const validateConnector = (action: JiraActionConnector): ValidationResult => { + const errors: Errors = { + projectKey: [], + email: [], + apiToken: [], + }; + + if (!action.config.projectKey) { + errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; + } + + if (!action.secrets.email) { + errors.email = [...errors.email, i18n.EMAIL_REQUIRED]; + } + + if (!action.secrets.apiToken) { + errors.apiToken = [...errors.apiToken, i18n.API_TOKEN_REQUIRED]; + } + + return { errors }; +}; + +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.JIRA_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: JiraConnectorFlyout, +}); diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/logos/servicenow.svg b/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg old mode 100755 new mode 100644 similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/connectors/logos/servicenow.svg rename to x-pack/plugins/siem/public/lib/connectors/jira/logo.svg diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts new file mode 100644 index 0000000000000..751aaecdad964 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts @@ -0,0 +1,28 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const JIRA_DESC = i18n.translate('xpack.siem.case.connectors.jira.selectMessageText', { + defaultMessage: 'Push or update SIEM case data to a new issue in Jira', +}); + +export const JIRA_TITLE = i18n.translate('xpack.siem.case.connectors.jira.actionTypeTitle', { + defaultMessage: 'Jira', +}); + +export const JIRA_PROJECT_KEY_LABEL = i18n.translate('xpack.siem.case.connectors.jira.projectKey', { + defaultMessage: 'Project key', +}); + +export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.jira.requiredProjectKeyTextField', + { + defaultMessage: 'Project key is required', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/types.ts b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts new file mode 100644 index 0000000000000..13e4e8f6a289e --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, +} from '../../../../../actions/server/builtin_action_types/jira/types'; + +export interface JiraActionConnector { + config: JiraPublicConfigurationType; + secrets: JiraSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts new file mode 100644 index 0000000000000..7bc1b117b3422 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Connector } from '../types'; + +import { SERVICENOW_TITLE } from './translations'; +import logo from './logo.svg'; + +export const connector: Connector = { + id: '.servicenow', + name: SERVICENOW_TITLE, + logo, + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx new file mode 100644 index 0000000000000..bcde802e7bd1e --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx @@ -0,0 +1,83 @@ +/* + * 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 { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import * as i18n from './translations'; +import { ConnectorFlyoutFormProps } from '../types'; +import { ServiceNowActionConnector } from './types'; +import { withConnectorFlyout } from '../components/connector_flyout'; + +const ServiceNowConnectorForm: React.FC<ConnectorFlyoutFormProps<ServiceNowActionConnector>> = ({ + errors, + action, + onChangeSecret, + onBlurSecret, +}) => { + const { username, password } = action.secrets; + const isUsernameInvalid: boolean = errors.username.length > 0 && username != null; + const isPasswordInvalid: boolean = errors.password.length > 0 && password != null; + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + id="connector-servicenow-username" + fullWidth + error={errors.username} + isInvalid={isUsernameInvalid} + label={i18n.USERNAME_LABEL} + > + <EuiFieldText + fullWidth + isInvalid={isUsernameInvalid} + name="connector-servicenow-username" + value={username || ''} // Needed to prevent uncontrolled input error when value is undefined + data-test-subj="connector-servicenow-username-form-input" + onChange={evt => onChangeSecret('username', evt.target.value)} + onBlur={() => onBlurSecret('username')} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="m" /> + <EuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + id="connector-servicenow-password" + fullWidth + error={errors.password} + isInvalid={isPasswordInvalid} + label={i18n.PASSWORD_LABEL} + > + <EuiFieldPassword + fullWidth + isInvalid={isPasswordInvalid} + name="connector-servicenow-password" + value={password || ''} // Needed to prevent uncontrolled input error when value is undefined + data-test-subj="connector-servicenow-password-form-input" + onChange={evt => onChangeSecret('password', evt.target.value)} + onBlur={() => onBlurSecret('password')} + /> + </EuiFormRow> + </EuiFlexItem> + </EuiFlexGroup> + </> + ); +}; + +export const ServiceNowConnectorFlyout = withConnectorFlyout<ServiceNowActionConnector>({ + ConnectorFormComponent: ServiceNowConnectorForm, + secretKeys: ['username', 'password'], +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx new file mode 100644 index 0000000000000..1f8e61b6d3ea7 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/types'; + +import { connector } from './config'; +import { createActionType } from '../utils'; +import logo from './logo.svg'; +import { ServiceNowActionConnector } from './types'; +import { ServiceNowConnectorFlyout } from './flyout'; +import * as i18n from './translations'; + +interface Errors { + username: string[]; + password: string[]; +} + +const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { + const errors: Errors = { + username: [], + password: [], + }; + + if (!action.secrets.username) { + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; + } + + if (!action.secrets.password) { + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; + } + + return { errors }; +}; + +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.SERVICENOW_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: ServiceNowConnectorFlyout, +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg b/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg new file mode 100644 index 0000000000000..dcd022a8dca18 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg @@ -0,0 +1,5 @@ +<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M0 20.566v-8.418h2.166v.689C2.806 12.295 3.594 12 4.53 12a3.74 3.74 0 012.905 1.379c.541.64.886 1.526.886 2.953v4.283H6.055v-4.48c0-.837-.197-1.28-.492-1.575-.295-.295-.738-.492-1.28-.492-.935 0-1.723.59-2.018 1.034v5.464H0z" fill="#293F41" /> + <path fill-rule="evenodd" d="M14.08 12c-2.659 0-4.923 2.166-4.923 4.874 0 1.428.59 2.707 1.526 3.643.345.345.886.345 1.28.05.542-.444 1.28-.69 2.117-.69s1.526.246 2.117.69a.976.976 0 001.28-.1 4.936 4.936 0 001.526-3.593C18.953 14.215 16.788 12 14.08 12zm-.05 7.385c-1.427 0-2.46-1.084-2.46-2.462 0-1.378.984-2.461 2.46-2.461 1.478 0 2.462 1.132 2.462 2.461 0 1.33-.984 2.462-2.461 2.462z" fill="#82B6A1" /> + <path fill-rule="evenodd" d="M23.385 20.566H21.71l-3.348-8.418h2.265l1.821 4.824 1.822-4.824h1.87l1.773 4.824 1.821-4.824H32l-3.348 8.418h-1.674l-1.772-4.775-1.821 4.775z" fill="#293F41" /> +</svg> diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts new file mode 100644 index 0000000000000..5dac9eddd1536 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts @@ -0,0 +1,23 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const SERVICENOW_DESC = i18n.translate( + 'xpack.siem.case.connectors.servicenow.selectMessageText', + { + defaultMessage: 'Push or update SIEM case data to a new incident in ServiceNow', + } +); + +export const SERVICENOW_TITLE = i18n.translate( + 'xpack.siem.case.connectors.servicenow.actionTypeTitle', + { + defaultMessage: 'ServiceNow', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts new file mode 100644 index 0000000000000..b7f0e79eb37e3 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, +} from '../../../../../actions/server/builtin_action_types/servicenow/types'; + +export interface ServiceNowActionConnector { + config: ServiceNowPublicConfigurationType; + secrets: ServiceNowSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/translations.ts b/x-pack/plugins/siem/public/lib/connectors/translations.ts new file mode 100644 index 0000000000000..b9c1d0fa2a17f --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/translations.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const API_URL_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiUrlTextField', + { + defaultMessage: 'URL is required', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.siem.case.connectors.common.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid', + } +); + +export const USERNAME_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.usernameTextFieldLabel', + { + defaultMessage: 'Username', + } +); + +export const USERNAME_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredUsernameTextField', + { + defaultMessage: 'Username is required', + } +); + +export const PASSWORD_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.passwordTextFieldLabel', + { + defaultMessage: 'Password', + } +); + +export const PASSWORD_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredPasswordTextField', + { + defaultMessage: 'Password is required', + } +); + +export const API_TOKEN_LABEL = i18n.translate( + 'xpack.siem.case.connectors.common.apiTokenTextFieldLabel', + { + defaultMessage: 'Api token', + } +); + +export const API_TOKEN_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredApiTokenTextField', + { + defaultMessage: 'Api token is required', + } +); + +export const EMAIL_LABEL = i18n.translate('xpack.siem.case.connectors.common.emailTextFieldLabel', { + defaultMessage: 'Email', +}); + +export const EMAIL_REQUIRED = i18n.translate( + 'xpack.siem.case.connectors.common.requiredEmailTextField', + { + defaultMessage: 'Email is required', + } +); diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts new file mode 100644 index 0000000000000..9af60f4995e54 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { ActionType } from '../../../../triggers_actions_ui/public'; +import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types'; + +export interface Connector extends ActionType { + logo: string; +} + +export interface ActionConnector { + config: ExternalIncidentServiceConfiguration; + secrets: {}; +} + +export interface ActionConnectorParams { + message: string; +} + +export interface ActionConnectorValidationErrors { + apiUrl: string[]; +} + +export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>; + +export interface ConnectorFlyoutFormProps<T> { + errors: { [key: string]: string[] }; + action: T; + onChangeSecret: (key: string, value: string) => void; + onBlurSecret: (key: string) => void; + onChangeConfig: (key: string, value: string) => void; + onBlurConfig: (key: string) => void; +} + +export interface ConnectorFlyoutHOCProps<T> { + ConnectorFormComponent: React.FC<ConnectorFlyoutFormProps<T>>; + configKeys?: string[]; + secretKeys?: string[]; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts new file mode 100644 index 0000000000000..5b5270ade5a65 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/connectors/utils.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ActionTypeModel, + ValidationResult, + ActionParamsProps, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../triggers_actions_ui/public/types'; + +import { + ActionConnector, + ActionConnectorParams, + ActionConnectorValidationErrors, + Optional, +} from './types'; +import { isUrlInvalid } from './validators'; + +import * as i18n from './translations'; + +export const createActionType = ({ + id, + actionTypeTitle, + selectMessage, + iconClass, + validateConnector, + validateParams = connectorParamsValidator, + actionConnectorFields, + actionParamsFields = ConnectorParamsFields, +}: Optional<ActionTypeModel, 'validateParams' | 'actionParamsFields'>) => (): ActionTypeModel => { + return { + id, + iconClass, + selectMessage, + actionTypeTitle, + validateConnector: (action: ActionConnector): ValidationResult => { + const errors: ActionConnectorValidationErrors = { + apiUrl: [], + }; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (isUrlInvalid(action.config.apiUrl)) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + return { errors: { ...errors, ...validateConnector(action).errors } }; + }, + validateParams, + actionConnectorFields, + actionParamsFields, + }; +}; + +const ConnectorParamsFields: React.FunctionComponent<ActionParamsProps<ActionConnectorParams>> = ({ + actionParams, + editAction, + index, + errors, +}) => { + return null; +}; + +const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { + return { errors: {} }; +}; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/validators.ts b/x-pack/plugins/siem/public/lib/connectors/validators.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/connectors/validators.ts rename to x-pack/plugins/siem/public/lib/connectors/validators.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/helpers/index.test.tsx b/x-pack/plugins/siem/public/lib/helpers/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/helpers/index.test.tsx rename to x-pack/plugins/siem/public/lib/helpers/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/lib/helpers/index.tsx b/x-pack/plugins/siem/public/lib/helpers/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/helpers/index.tsx rename to x-pack/plugins/siem/public/lib/helpers/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/lib/helpers/scheduler.ts b/x-pack/plugins/siem/public/lib/helpers/scheduler.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/helpers/scheduler.ts rename to x-pack/plugins/siem/public/lib/helpers/scheduler.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/history/index.ts b/x-pack/plugins/siem/public/lib/history/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/history/index.ts rename to x-pack/plugins/siem/public/lib/history/index.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/keury/index.test.ts b/x-pack/plugins/siem/public/lib/keury/index.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/keury/index.test.ts rename to x-pack/plugins/siem/public/lib/keury/index.test.ts diff --git a/x-pack/plugins/siem/public/lib/keury/index.ts b/x-pack/plugins/siem/public/lib/keury/index.ts new file mode 100644 index 0000000000000..810baa89cd60d --- /dev/null +++ b/x-pack/plugins/siem/public/lib/keury/index.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty, isString, flow } from 'lodash/fp'; +import { + EsQueryConfig, + Query, + Filter, + esQuery, + esKuery, + IIndexPattern, +} from '../../../../../../src/plugins/data/public'; + +import { JsonObject } from '../../../../../../src/plugins/kibana_utils/public'; + +import { KueryFilterQuery } from '../../store'; + +export const convertKueryToElasticSearchQuery = ( + kueryExpression: string, + indexPattern?: IIndexPattern +) => { + try { + return kueryExpression + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) + : ''; + } catch (err) { + return ''; + } +}; + +export const convertKueryToDslFilter = ( + kueryExpression: string, + indexPattern: IIndexPattern +): JsonObject => { + try { + return kueryExpression + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + : {}; + } catch (err) { + return {}; + } +}; + +export const escapeQueryValue = (val: number | string = ''): string | number => { + if (isString(val)) { + if (isEmpty(val)) { + return '""'; + } + return `"${escapeKuery(val)}"`; + } + + return val; +}; + +export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { + if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { + try { + esKuery.fromKueryExpression(kqlFilterQuery.expression); + } catch (err) { + return false; + } + } + return true; +}; + +const escapeWhitespace = (val: string) => + val + .replace(/\t/g, '\\t') + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n'); + +// See the SpecialCharacter rule in kuery.peg +const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string + +// See the Keyword rule in kuery.peg +const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); + +const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); + +export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); + +export const convertToBuildEsQuery = ({ + config, + indexPattern, + queries, + filters, +}: { + config: EsQueryConfig; + indexPattern: IIndexPattern; + queries: Query[]; + filters: Filter[]; +}) => { + try { + return JSON.stringify( + esQuery.buildEsQuery( + indexPattern, + queries, + filters.filter(f => f.meta.disabled === false), + { + ...config, + dateFormatTZ: undefined, + } + ) + ); + } catch (exp) { + return ''; + } +}; diff --git a/x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts b/x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts new file mode 100644 index 0000000000000..c3e1f35f37356 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createKibanaContextProviderMock, + createUseUiSettingMock, + createUseUiSetting$Mock, + createUseKibanaMock, + createWithKibanaMock, +} from '../../../mock/kibana_react'; + +export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; +export const useKibana = jest.fn(createUseKibanaMock()); +export const useUiSetting = jest.fn(createUseUiSettingMock()); +export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); +export const useTimeZone = jest.fn(); +export const useDateFormat = jest.fn(); +export const useBasePath = jest.fn(() => '/test/base/path'); +export const useCurrentUser = jest.fn(); +export const withKibana = jest.fn(createWithKibanaMock()); +export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock()); diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts b/x-pack/plugins/siem/public/lib/kibana/hooks.ts similarity index 94% rename from x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts rename to x-pack/plugins/siem/public/lib/kibana/hooks.ts index e1d0a445bf2fb..d62701fe5944a 100644 --- a/x-pack/legacy/plugins/siem/public/lib/kibana/hooks.ts +++ b/x-pack/plugins/siem/public/lib/kibana/hooks.ts @@ -8,13 +8,10 @@ import moment from 'moment-timezone'; import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, -} from '../../../../../../plugins/siem/common/constants'; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; import { useUiSetting, useKibana } from './kibana_react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { AuthenticatedUser } from '../../../../../../plugins/security/common/model'; +import { AuthenticatedUser } from '../../../../security/common/model'; import { convertToCamelCase } from '../../containers/case/utils'; export const useDateFormat = (): string => useUiSetting<string>(DEFAULT_DATE_FORMAT); diff --git a/x-pack/legacy/plugins/siem/public/lib/kibana/index.ts b/x-pack/plugins/siem/public/lib/kibana/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/kibana/index.ts rename to x-pack/plugins/siem/public/lib/kibana/index.ts diff --git a/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts b/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts new file mode 100644 index 0000000000000..88be8d25e5840 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + KibanaContextProvider, + KibanaReactContextValue, + useKibana, + useUiSetting, + useUiSetting$, + withKibana, +} from '../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../plugin'; + +export type KibanaContext = KibanaReactContextValue<StartServices>; +export interface WithKibanaProps { + kibana: KibanaContext; +} + +// eslint-disable-next-line react-hooks/rules-of-hooks +const typedUseKibana = () => useKibana<StartServices>(); + +export { + KibanaContextProvider, + typedUseKibana as useKibana, + useUiSetting, + useUiSetting$, + withKibana, +}; diff --git a/x-pack/plugins/siem/public/lib/kibana/services.ts b/x-pack/plugins/siem/public/lib/kibana/services.ts new file mode 100644 index 0000000000000..4ab3e102f56ab --- /dev/null +++ b/x-pack/plugins/siem/public/lib/kibana/services.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from '../../../../../../src/core/public'; + +type GlobalServices = Pick<CoreStart, 'http' | 'uiSettings'>; + +export class KibanaServices { + private static kibanaVersion?: string; + private static services?: GlobalServices; + + public static init({ + http, + kibanaVersion, + uiSettings, + }: GlobalServices & { kibanaVersion: string }) { + this.services = { http, uiSettings }; + this.kibanaVersion = kibanaVersion; + } + + public static get(): GlobalServices { + if (!this.services) { + this.throwUninitializedError(); + } + + return this.services; + } + + public static getKibanaVersion(): string { + if (!this.kibanaVersion) { + this.throwUninitializedError(); + } + + return this.kibanaVersion; + } + + private static throwUninitializedError(): never { + throw new Error( + 'Kibana services not initialized - are you trying to import this module from outside of the SIEM app?' + ); + } +} diff --git a/x-pack/legacy/plugins/siem/public/lib/lib.ts b/x-pack/plugins/siem/public/lib/lib.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/lib.ts rename to x-pack/plugins/siem/public/lib/lib.ts diff --git a/x-pack/legacy/plugins/siem/public/lib/note/index.ts b/x-pack/plugins/siem/public/lib/note/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/lib/note/index.ts rename to x-pack/plugins/siem/public/lib/note/index.ts diff --git a/x-pack/plugins/siem/public/lib/telemetry/index.ts b/x-pack/plugins/siem/public/lib/telemetry/index.ts new file mode 100644 index 0000000000000..37d181e9b8ad7 --- /dev/null +++ b/x-pack/plugins/siem/public/lib/telemetry/index.ts @@ -0,0 +1,54 @@ +/* + * 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 { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; + +import { SetupPlugins } from '../../plugin'; +export { telemetryMiddleware } from './middleware'; + +export { METRIC_TYPE }; + +type TrackFn = (type: UiStatsMetricType, event: string | string[], count?: number) => void; + +const noop = () => {}; + +let _track: TrackFn; + +export const track: TrackFn = (type, event, count) => { + try { + _track(type, event, count); + } catch (error) { + // ignore failed tracking call + } +}; + +export const initTelemetry = (usageCollection: SetupPlugins['usageCollection'], appId: string) => { + _track = usageCollection?.reportUiStats?.bind(null, appId) ?? noop; +}; + +export enum TELEMETRY_EVENT { + // Detections + SIEM_RULE_ENABLED = 'siem_rule_enabled', + SIEM_RULE_DISABLED = 'siem_rule_disabled', + CUSTOM_RULE_ENABLED = 'custom_rule_enabled', + CUSTOM_RULE_DISABLED = 'custom_rule_disabled', + + // ML + SIEM_JOB_ENABLED = 'siem_job_enabled', + SIEM_JOB_DISABLED = 'siem_job_disabled', + CUSTOM_JOB_ENABLED = 'custom_job_enabled', + CUSTOM_JOB_DISABLED = 'custom_job_disabled', + JOB_ENABLE_FAILURE = 'job_enable_failure', + JOB_DISABLE_FAILURE = 'job_disable_failure', + + // Timeline + TIMELINE_OPENED = 'open_timeline', + TIMELINE_SAVED = 'timeline_saved', + TIMELINE_NAMED = 'timeline_named', + + // UI Interactions + TAB_CLICKED = 'tab_', +} diff --git a/x-pack/legacy/plugins/siem/public/lib/telemetry/middleware.ts b/x-pack/plugins/siem/public/lib/telemetry/middleware.ts similarity index 93% rename from x-pack/legacy/plugins/siem/public/lib/telemetry/middleware.ts rename to x-pack/plugins/siem/public/lib/telemetry/middleware.ts index 59c6cb3566907..ca889e20e695f 100644 --- a/x-pack/legacy/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/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx b/x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx similarity index 86% rename from x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx rename to x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx index b72c34d3b59a7..1696001203bc8 100644 --- a/x-pack/legacy/plugins/siem/public/lib/theme/use_eui_theme.tsx +++ b/x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx @@ -7,7 +7,7 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { DEFAULT_DARK_MODE } from '../../../../../../plugins/siem/common/constants'; +import { DEFAULT_DARK_MODE } from '../../../common/constants'; import { useUiSetting$ } from '../kibana'; export const useEuiTheme = () => { diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/plugins/siem/public/mock/global_state.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/mock/global_state.ts rename to x-pack/plugins/siem/public/mock/global_state.ts index 266c3aadea8af..6678c3043a3da 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/plugins/siem/public/mock/global_state.ts @@ -22,7 +22,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../../../../plugins/siem/common/constants'; +} from '../../common/constants'; export const mockGlobalState: State = { app: { diff --git a/x-pack/legacy/plugins/siem/public/mock/header.ts b/x-pack/plugins/siem/public/mock/header.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/header.ts rename to x-pack/plugins/siem/public/mock/header.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx b/x-pack/plugins/siem/public/mock/hook_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/hook_wrapper.tsx rename to x-pack/plugins/siem/public/mock/hook_wrapper.tsx diff --git a/x-pack/legacy/plugins/siem/public/mock/index.ts b/x-pack/plugins/siem/public/mock/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/index.ts rename to x-pack/plugins/siem/public/mock/index.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/index_pattern.ts b/x-pack/plugins/siem/public/mock/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/index_pattern.ts rename to x-pack/plugins/siem/public/mock/index_pattern.ts diff --git a/x-pack/plugins/siem/public/mock/kibana_core.ts b/x-pack/plugins/siem/public/mock/kibana_core.ts new file mode 100644 index 0000000000000..b175ddbf5106d --- /dev/null +++ b/x-pack/plugins/siem/public/mock/kibana_core.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { coreMock } from '../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; + +export const createKibanaCoreStartMock = () => coreMock.createStart(); +export const createKibanaPluginsStartMock = () => ({ + data: dataPluginMock.createStartContract(), +}); diff --git a/x-pack/plugins/siem/public/mock/kibana_react.ts b/x-pack/plugins/siem/public/mock/kibana_react.ts new file mode 100644 index 0000000000000..cebba3e237f98 --- /dev/null +++ b/x-pack/plugins/siem/public/mock/kibana_react.ts @@ -0,0 +1,108 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; + +import { + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT, + DEFAULT_DATE_FORMAT_TZ, + DEFAULT_DARK_MODE, + DEFAULT_TIME_RANGE, + DEFAULT_REFRESH_RATE_INTERVAL, + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_BYTES_FORMAT, + DEFAULT_INDEX_PATTERN, +} from '../../common/constants'; +import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const mockUiSettings: Record<string, any> = { + [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, + [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, + [DEFAULT_SIEM_TIME_RANGE]: { + from: DEFAULT_FROM, + to: DEFAULT_TO, + }, + [DEFAULT_SIEM_REFRESH_INTERVAL]: { + pause: DEFAULT_INTERVAL_PAUSE, + value: DEFAULT_INTERVAL_VALUE, + }, + [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, + [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', + [DEFAULT_DATE_FORMAT_TZ]: 'UTC', + [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', + [DEFAULT_DARK_MODE]: false, +}; + +export const createUseUiSettingMock = () => <T extends unknown = string>( + key: string, + defaultValue?: T +): T => { + const result = mockUiSettings[key]; + + if (typeof result != null) return result; + + if (defaultValue != null) { + return defaultValue; + } + + throw new Error(`Unexpected config key: ${key}`); +}; + +export const createUseUiSetting$Mock = () => { + const useUiSettingMock = createUseUiSettingMock(); + + return <T extends unknown = string>( + key: string, + defaultValue?: T + ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; +}; + +export const createUseKibanaMock = () => { + const core = createKibanaCoreStartMock(); + const plugins = createKibanaPluginsStartMock(); + const useUiSetting = createUseUiSettingMock(); + + const services = { + ...core, + ...plugins, + uiSettings: { + ...core.uiSettings, + get: useUiSetting, + }, + }; + + return () => ({ services }); +}; + +export const createWithKibanaMock = () => { + const kibana = createUseKibanaMock()(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (Component: any) => (props: any) => { + return React.createElement(Component, { ...props, kibana }); + }; +}; + +export const createKibanaContextProviderMock = () => { + const kibana = createUseKibanaMock()(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ({ services, ...rest }: any) => + React.createElement(KibanaContextProvider, { + ...rest, + services: { ...kibana.services, ...services }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/public/mock/match_media.ts b/x-pack/plugins/siem/public/mock/match_media.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/match_media.ts rename to x-pack/plugins/siem/public/mock/match_media.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts b/x-pack/plugins/siem/public/mock/mock_detail_item.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts rename to x-pack/plugins/siem/public/mock/mock_detail_item.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts b/x-pack/plugins/siem/public/mock/mock_ecs.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts rename to x-pack/plugins/siem/public/mock/mock_ecs.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts b/x-pack/plugins/siem/public/mock/mock_endgame_ecs_data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_endgame_ecs_data.ts rename to x-pack/plugins/siem/public/mock/mock_endgame_ecs_data.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts b/x-pack/plugins/siem/public/mock/mock_timeline_data.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/mock_timeline_data.ts rename to x-pack/plugins/siem/public/mock/mock_timeline_data.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/netflow.ts b/x-pack/plugins/siem/public/mock/netflow.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/netflow.ts rename to x-pack/plugins/siem/public/mock/netflow.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/news.ts b/x-pack/plugins/siem/public/mock/news.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/news.ts rename to x-pack/plugins/siem/public/mock/news.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/raw_news.ts b/x-pack/plugins/siem/public/mock/raw_news.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/raw_news.ts rename to x-pack/plugins/siem/public/mock/raw_news.ts diff --git a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx b/x-pack/plugins/siem/public/mock/test_providers.tsx similarity index 99% rename from x-pack/legacy/plugins/siem/public/mock/test_providers.tsx rename to x-pack/plugins/siem/public/mock/test_providers.tsx index 952f7f51b63f2..59e3874c6d0a1 100644 --- a/x-pack/legacy/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/plugins/siem/public/mock/test_providers.tsx @@ -22,8 +22,6 @@ import { mockGlobalState } from './global_state'; import { createKibanaContextProviderMock } from './kibana_react'; import { FieldHook, useForm } from '../shared_imports'; -jest.mock('ui/new_platform'); - const state: State = mockGlobalState; interface Props { diff --git a/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts b/x-pack/plugins/siem/public/mock/timeline_results.ts similarity index 98% rename from x-pack/legacy/plugins/siem/public/mock/timeline_results.ts rename to x-pack/plugins/siem/public/mock/timeline_results.ts index 363281e563317..edd1c73771829 100644 --- a/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/mock/timeline_results.ts @@ -10,7 +10,7 @@ import { allTimelinesQuery } from '../containers/timeline/all/index.gql_query'; import { CreateTimelineProps } from '../pages/detection_engine/components/signals/types'; import { TimelineModel } from '../store/timeline/model'; import { timelineDefaults } from '../store/timeline/defaults'; -import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; +import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter'; export interface MockedProvidedQuery { request: { query: GetAllTimeline.Query; @@ -168,6 +168,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 1', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -294,6 +297,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -420,6 +426,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 2', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -546,6 +555,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 3', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -672,6 +684,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 4', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -798,6 +813,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 5', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -924,6 +942,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 6', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1050,6 +1071,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1176,6 +1200,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1302,6 +1329,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1428,6 +1458,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1554,6 +1587,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, @@ -1680,6 +1716,9 @@ export const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ 'ZF0W12oB9v5HJNSHwY6L', ], title: 'test 7', + timelineType: null, + templateTimelineId: null, + templateTimelineVersion: null, created: 1558386787614, createdBy: 'elastic', updated: 1558390951234, diff --git a/x-pack/legacy/plugins/siem/public/mock/utils.ts b/x-pack/plugins/siem/public/mock/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/mock/utils.ts rename to x-pack/plugins/siem/public/mock/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/404.tsx b/x-pack/plugins/siem/public/pages/404.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/404.tsx rename to x-pack/plugins/siem/public/pages/404.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/plugins/siem/public/pages/case/case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/case.tsx rename to x-pack/plugins/siem/public/pages/case/case.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/plugins/siem/public/pages/case/case_details.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx rename to x-pack/plugins/siem/public/pages/case/case_details.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts similarity index 80% rename from x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts rename to x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts index 9d2ac29bc47d7..12946c3af06bd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts +++ b/x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts @@ -3,6 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); export const mockFormHook = { isSubmitted: false, isSubmitting: false, @@ -35,3 +39,5 @@ export const getFormMock = (sampleData: any) => ({ }), getFormData: () => sampleData, }); + +export const useFormMock = useForm as jest.Mock; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts b/x-pack/plugins/siem/public/pages/case/components/__mock__/router.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/router.ts rename to x-pack/plugins/siem/public/pages/case/components/__mock__/router.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.test.tsx new file mode 100644 index 0000000000000..7ba8ec9666253 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.test.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { AddComment } from './'; +import { TestProviders } from '../../../../mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostComment } from '../../../../containers/case/use_post_comment'; +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { wait } from '../../../../lib/helpers'; +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../../../containers/case/use_post_comment'); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCommentMock = usePostComment as jest.Mock; + +const onCommentSaving = jest.fn(); +const onCommentPosted = jest.fn(); +const postComment = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const addCommentProps = { + caseId: '1234', + disabled: false, + insertQuote: null, + onCommentSaving, + onCommentPosted, + showLoading: false, +}; + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; + +const defaultPostCommment = { + isLoading: false, + isError: false, + postComment, +}; +const sampleData = { + comment: 'what a cool comment', +}; +describe('AddComment ', () => { + const formHookMock = getFormMock(sampleData); + + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCommentMock.mockImplementation(() => defaultPostCommment); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('should post comment on submit click', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...addCommentProps} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); + + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .simulate('click'); + await wait(); + expect(onCommentSaving).toBeCalled(); + expect(postComment).toBeCalledWith(sampleData, onCommentPosted); + expect(formHookMock.reset).toBeCalled(); + }); + + it('should render spinner and disable submit when loading', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, showLoading: true }} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should disable submit button when disabled prop passed', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, disabled: true }} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should insert a quote if one is available', () => { + const sampleQuote = 'what a cool quote'; + mount( + <TestProviders> + <Router history={mockHistory}> + <AddComment {...{ ...addCommentProps, insertQuote: sampleQuote }} /> + </Router> + </TestProviders> + ); + + expect(formHookMock.setFieldValue).toBeCalledWith( + 'comment', + `${sampleData.comment}\n\n${sampleQuote}` + ); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx new file mode 100644 index 0000000000000..aa987b277da06 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -0,0 +1,114 @@ +/* + * 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 { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; +import styled from 'styled-components'; + +import { CommentRequest } from '../../../../../../case/common/api'; +import { usePostComment } from '../../../../containers/case/use_post_comment'; +import { Case } from '../../../../containers/case/types'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; +import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { Form, useForm, UseField } from '../../../../shared_imports'; + +import * as i18n from '../../translations'; +import { schema } from './schema'; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +const initialCommentValue: CommentRequest = { + comment: '', +}; + +interface AddCommentProps { + caseId: string; + disabled?: boolean; + insertQuote: string | null; + onCommentSaving?: () => void; + onCommentPosted: (newCase: Case) => void; + showLoading?: boolean; +} + +export const AddComment = React.memo<AddCommentProps>( + ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { + const { isLoading, postComment } = usePostComment(caseId); + const { form } = useForm<CommentRequest>({ + defaultValue: initialCommentValue, + options: { stripEmptyFields: false }, + schema, + }); + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CommentRequest>( + form, + 'comment' + ); + + useEffect(() => { + if (insertQuote !== null) { + const { comment } = form.getFormData(); + form.setFieldValue( + 'comment', + `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` + ); + } + }, [insertQuote]); + + const onSubmit = useCallback(async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + if (onCommentSaving != null) { + onCommentSaving(); + } + await postComment(data, onCommentPosted); + form.reset(); + } + }, [form, onCommentPosted, onCommentSaving]); + return ( + <span id="add-comment-permLink"> + {isLoading && showLoading && <MySpinner data-test-subj="loading-spinner" size="xl" />} + <Form form={form}> + <UseField + path="comment" + component={MarkdownEditorForm} + componentProps={{ + idAria: 'caseComment', + isDisabled: isLoading, + dataTestSubj: 'add-comment', + placeholder: i18n.ADD_COMMENT_HELP_TEXT, + onCursorPositionUpdate: handleCursorChange, + bottomRightContent: ( + <EuiButton + data-test-subj="submit-comment" + iconType="plusInCircle" + isDisabled={isLoading || disabled} + isLoading={isLoading} + onClick={onSubmit} + size="s" + > + {i18n.ADD_COMMENT} + </EuiButton> + ), + topRightContent: ( + <InsertTimelinePopover + hideUntitled={true} + isDisabled={isLoading} + onTimelineChange={handleOnTimelineChange} + /> + ), + }} + /> + </Form> + </span> + ); + } +); + +AddComment.displayName = 'AddComment'; diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx new file mode 100644 index 0000000000000..ad73fd71b8e11 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CommentRequest } from '../../../../../../case/common/api'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; +import * as i18n from '../../translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema<CommentRequest> = { + comment: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.COMMENT_REQUIRED), + }, + ], + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx new file mode 100644 index 0000000000000..9c2a7fc07f2d3 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -0,0 +1,203 @@ +/* + * 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, { useCallback } from 'react'; +import { + EuiBadge, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiTableActionsColumnType, + EuiAvatar, + EuiLink, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { Case } from '../../../../containers/case/types'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { CaseDetailsLink } from '../../../../components/links'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import * as i18n from './translations'; + +export type CasesColumns = + | EuiTableFieldDataColumnType<Case> + | EuiTableComputedColumnType<Case> + | EuiTableActionsColumnType<Case>; + +const MediumShadeText = styled.p` + color: ${({ theme }) => theme.eui.euiColorMediumShade}; +`; + +const Spacer = styled.span` + margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const renderStringField = (field: string, dataTestSubj: string) => + field != null ? <span data-test-subj={dataTestSubj}>{field}</span> : getEmptyTagValue(); + +export const getCasesColumns = ( + actions: Array<DefaultItemIconButtonAction<Case>>, + filterStatus: string +): CasesColumns[] => [ + { + name: i18n.NAME, + render: (theCase: Case) => { + if (theCase.id != null && theCase.title != null) { + const caseDetailsLinkComponent = ( + <CaseDetailsLink detailName={theCase.id} title={theCase.title}> + {theCase.title} + </CaseDetailsLink> + ); + return theCase.status === 'open' ? ( + caseDetailsLinkComponent + ) : ( + <> + <MediumShadeText> + {caseDetailsLinkComponent} + <Spacer>{i18n.CLOSED}</Spacer> + </MediumShadeText> + </> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'createdBy', + name: i18n.REPORTER, + render: (createdBy: Case['createdBy']) => { + if (createdBy != null) { + return ( + <> + <EuiAvatar + className="userAction__circle" + name={createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} + size="s" + /> + <Spacer data-test-subj="case-table-column-createdBy"> + {createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} + </Spacer> + </> + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'tags', + name: i18n.TAGS, + render: (tags: Case['tags']) => { + if (tags != null && tags.length > 0) { + return ( + <TruncatableText> + {tags.map((tag: string, i: number) => ( + <EuiBadge + color="hollow" + key={`${tag}-${i}`} + data-test-subj={`case-table-column-tags-${i}`} + > + {tag} + </EuiBadge> + ))} + </TruncatableText> + ); + } + return getEmptyTagValue(); + }, + truncateText: true, + }, + { + align: 'right', + field: 'totalComment', + name: i18n.COMMENTS, + sortable: true, + render: (totalComment: Case['totalComment']) => + totalComment != null + ? renderStringField(`${totalComment}`, `case-table-column-commentCount`) + : getEmptyTagValue(), + }, + filterStatus === 'open' + ? { + field: 'createdAt', + name: i18n.OPENED_ON, + sortable: true, + render: (createdAt: Case['createdAt']) => { + if (createdAt != null) { + return ( + <span data-test-subj={`case-table-column-createdAt`}> + <FormattedRelativePreferenceDate value={createdAt} /> + </span> + ); + } + return getEmptyTagValue(); + }, + } + : { + field: 'closedAt', + name: i18n.CLOSED_ON, + sortable: true, + render: (closedAt: Case['closedAt']) => { + if (closedAt != null) { + return ( + <span data-test-subj={`case-table-column-closedAt`}> + <FormattedRelativePreferenceDate value={closedAt} /> + </span> + ); + } + return getEmptyTagValue(); + }, + }, + { + name: i18n.SERVICENOW_INCIDENT, + render: (theCase: Case) => { + if (theCase.id != null) { + return <ServiceNowColumn theCase={theCase} />; + } + return getEmptyTagValue(); + }, + }, + { + name: i18n.ACTIONS, + actions, + }, +]; + +interface Props { + theCase: Case; +} + +export const ServiceNowColumn: React.FC<Props> = ({ theCase }) => { + const handleRenderDataToPush = useCallback(() => { + const lastCaseUpdate = theCase.updatedAt != null ? new Date(theCase.updatedAt) : null; + const lastCasePush = + theCase.externalService?.pushedAt != null + ? new Date(theCase.externalService?.pushedAt) + : null; + const hasDataToPush = + lastCasePush === null || + (lastCasePush != null && + lastCaseUpdate != null && + lastCasePush.getTime() < lastCaseUpdate?.getTime()); + return ( + <p> + <EuiLink + data-test-subj={`case-table-column-external`} + href={theCase.externalService?.externalUrl} + target="_blank" + aria-label={i18n.SERVICENOW_LINK_ARIA} + > + {theCase.externalService?.externalTitle} + </EuiLink> + {hasDataToPush + ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) + : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} + </p> + ); + }, [theCase]); + if (theCase.externalService !== null) { + return handleRenderDataToPush(); + } + return renderStringField(i18n.NOT_PUSHED, `case-table-column-external-notPushed`); +}; diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/index.test.tsx new file mode 100644 index 0000000000000..eb5bca6cc57ff --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -0,0 +1,347 @@ +/* + * 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 { mount } from 'enzyme'; +import moment from 'moment-timezone'; +import { AllCases } from './'; +import { TestProviders } from '../../../../mock'; +import { useGetCasesMockState } from '../../../../containers/case/mock'; +import * as i18n from './translations'; + +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; +import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; +import { getCasesColumns } from './columns'; +jest.mock('../../../../containers/case/use_bulk_update_case'); +jest.mock('../../../../containers/case/use_delete_cases'); +jest.mock('../../../../containers/case/use_get_cases'); +jest.mock('../../../../containers/case/use_get_cases_status'); +const useDeleteCasesMock = useDeleteCases as jest.Mock; +const useGetCasesMock = useGetCases as jest.Mock; +const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; +const useUpdateCasesMock = useUpdateCases as jest.Mock; + +describe('AllCases', () => { + const dispatchResetIsDeleted = jest.fn(); + const dispatchResetIsUpdated = jest.fn(); + const dispatchUpdateCaseProperty = jest.fn(); + const handleOnDeleteConfirm = jest.fn(); + const handleToggleModal = jest.fn(); + const refetchCases = jest.fn(); + const setFilters = jest.fn(); + const setQueryParams = jest.fn(); + const setSelectedCases = jest.fn(); + const updateBulkStatus = jest.fn(); + const fetchCasesStatus = jest.fn(); + const emptyTag = getEmptyTagValue().props.children; + + const defaultGetCases = { + ...useGetCasesMockState, + dispatchUpdateCaseProperty, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + }; + const defaultDeleteCases = { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isDeleted: false, + isDisplayConfirmDeleteModal: false, + isLoading: false, + }; + const defaultCasesStatus = { + countClosedCases: 0, + countOpenCases: 5, + fetchCasesStatus, + isError: false, + isLoading: true, + }; + const defaultUpdateCases = { + isUpdated: false, + isLoading: false, + isError: false, + dispatchResetIsUpdated, + updateBulkStatus, + }; + /* eslint-disable no-console */ + // Silence until enzyme fixed to use ReactTestUtils.act() + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + beforeEach(() => { + jest.resetAllMocks(); + useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); + useGetCasesMock.mockImplementation(() => defaultGetCases); + useDeleteCasesMock.mockImplementation(() => defaultDeleteCases); + useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); + moment.tz.setDefault('UTC'); + }); + it('should render AllCases', () => { + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + expect( + wrapper + .find(`a[data-test-subj="case-details-link"]`) + .first() + .prop('href') + ).toEqual( + `#/link-to/case/${useGetCasesMockState.data.cases[0].id}?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))` + ); + expect( + wrapper + .find(`a[data-test-subj="case-details-link"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].title); + expect( + wrapper + .find(`span[data-test-subj="case-table-column-tags-0"]`) + .first() + .prop('title') + ).toEqual(useGetCasesMockState.data.cases[0].tags[0]); + expect( + wrapper + .find(`[data-test-subj="case-table-column-createdBy"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].createdBy.fullName); + expect( + wrapper + .find(`[data-test-subj="case-table-column-createdAt"]`) + .first() + .childAt(0) + .prop('value') + ).toBe(useGetCasesMockState.data.cases[0].createdAt); + expect( + wrapper + .find(`[data-test-subj="case-table-case-count"]`) + .first() + .text() + ).toEqual('Showing 10 cases'); + }); + it('should render empty fields', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + data: { + ...defaultGetCases.data, + cases: [ + { + ...defaultGetCases.data.cases[0], + id: null, + createdAt: null, + createdBy: null, + tags: null, + title: null, + totalComment: null, + }, + ], + }, + })); + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + const checkIt = (columnName: string, key: number) => { + const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key); + if (columnName === i18n.ACTIONS) { + return; + } + expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName); + expect(column.find('span').text()).toEqual(emptyTag); + }; + getCasesColumns([], 'open').map((i, key) => i.name != null && checkIt(`${i.name}`, key)); + }); + it('should tableHeaderSortButton AllCases', () => { + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper + .find('[data-test-subj="tableHeaderSortButton"]') + .first() + .simulate('click'); + expect(setQueryParams).toBeCalledWith({ + page: 1, + perPage: 5, + sortField: 'createdAt', + sortOrder: 'asc', + }); + }); + it('closes case when row action icon clicked', () => { + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper + .find('[data-test-subj="action-close"]') + .first() + .simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'closed', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); + it('opens case when row action icon clicked', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' }, + })); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper + .find('[data-test-subj="action-open"]') + .first() + .simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'open', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); + it('Bulk delete', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + })); + useDeleteCasesMock + .mockReturnValueOnce({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: false, + }) + .mockReturnValue({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: true, + }); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-delete-button"]') + .first() + .simulate('click'); + expect(handleToggleModal).toBeCalled(); + + wrapper + .find( + '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' + ) + .last() + .simulate('click'); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( + useGetCasesMockState.data.cases.map(({ id }) => ({ id })) + ); + }); + it('Bulk close status update', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + })); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-close-button"]') + .first() + .simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); + }); + it('Bulk open status update', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + filterOptions: { + ...defaultGetCases.filterOptions, + status: 'closed', + }, + })); + + const wrapper = mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-open-button"]') + .first() + .simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); + }); + it('isDeleted is true, refetch', () => { + useDeleteCasesMock.mockImplementation(() => ({ + ...defaultDeleteCases, + isDeleted: true, + })); + + mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsDeleted).toBeCalled(); + }); + it('isUpdated is true, refetch', () => { + useUpdateCasesMock.mockImplementation(() => ({ + ...defaultUpdateCases, + isUpdated: true, + })); + + mount( + <TestProviders> + <AllCases userCanCrud={true} /> + </TestProviders> + ); + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsUpdated).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx new file mode 100644 index 0000000000000..9dd90074a2e7b --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -0,0 +1,439 @@ +/* + * 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, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + EuiBasicTable, + EuiButton, + EuiContextMenuPanel, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiProgress, + EuiTableSortingType, +} from '@elastic/eui'; +import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; +import { isEmpty } from 'lodash/fp'; +import styled, { css } from 'styled-components'; +import * as i18n from './translations'; + +import { getCasesColumns } from './columns'; +import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; +import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases'; +import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; +import { Panel } from '../../../../components/panel'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/utility_bar'; +import { getCreateCaseUrl } from '../../../../components/link_to'; +import { getBulkItems } from '../bulk_actions'; +import { CaseHeaderPage } from '../case_header_page'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; +import { OpenClosedStats } from '../open_closed_stats'; +import { navTabs } from '../../../home/home_navigations'; + +import { getActions } from './actions'; +import { CasesTableFilters } from './table_filters'; +import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { getActionLicenseError } from '../use_push_to_service/helpers'; +import { CaseCallOut } from '../callout'; +import { ConfigureCaseButton } from '../configure_cases/button'; +import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; + +const Div = styled.div` + margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; +`; +const FlexItemDivider = styled(EuiFlexItem)` + ${({ theme }) => css` + .euiFlexGroup--gutterMedium > &.euiFlexItem { + border-right: ${theme.eui.euiBorderThin}; + padding-right: ${theme.eui.euiSize}; + margin-right: ${theme.eui.euiSize}; + } + `} +`; + +const ProgressLoader = styled(EuiProgress)` + ${({ theme }) => css` + top: 2px; + border-radius: ${theme.eui.euiBorderRadius}; + z-index: ${theme.eui.euiZHeader}; + `} +`; + +const getSortField = (field: string): SortFieldCase => { + if (field === SortFieldCase.createdAt) { + return SortFieldCase.createdAt; + } else if (field === SortFieldCase.closedAt) { + return SortFieldCase.closedAt; + } + return SortFieldCase.createdAt; +}; + +interface AllCasesProps { + userCanCrud: boolean; +} +export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => { + const urlSearch = useGetUrlSearch(navTabs.case); + const { actionLicense } = useGetActionLicense(); + const { + countClosedCases, + countOpenCases, + isLoading: isCasesStatusLoading, + fetchCasesStatus, + } = useGetCasesStatus(); + const { + data, + dispatchUpdateCaseProperty, + filterOptions, + loading, + queryParams, + selectedCases, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + } = useGetCases(); + + // Delete case + const { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isLoading: isDeleting, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + + // Update case + const { + dispatchResetIsUpdated, + isLoading: isUpdating, + isUpdated, + updateBulkStatus, + } = useUpdateCases(); + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState<DeleteCase[]>([]); + const filterRefetch = useRef<() => void>(); + const setFilterRefetch = useCallback( + (refetchFilter: () => void) => { + filterRefetch.current = refetchFilter; + }, + [filterRefetch.current] + ); + const refreshCases = useCallback( + (dataRefresh = true) => { + if (dataRefresh) refetchCases(); + fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); + if (filterRefetch.current != null) { + filterRefetch.current(); + } + }, + [filterOptions, queryParams, filterRefetch.current] + ); + + useEffect(() => { + if (isDeleted) { + refreshCases(); + dispatchResetIsDeleted(); + } + if (isUpdated) { + refreshCases(); + dispatchResetIsUpdated(); + } + }, [isDeleted, isUpdated]); + const confirmDeleteModal = useMemo( + () => ( + <ConfirmDeleteCaseModal + caseTitle={deleteThisCase.title} + isModalVisible={isDisplayConfirmDeleteModal} + isPlural={deleteBulk.length > 0} + onCancel={handleToggleModal} + onConfirm={handleOnDeleteConfirm.bind( + null, + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] + )} + /> + ), + [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] + ); + + const toggleDeleteModal = useCallback((deleteCase: Case) => { + handleToggleModal(); + setDeleteThisCase(deleteCase); + }, []); + + const toggleBulkDeleteModal = useCallback( + (caseIds: string[]) => { + handleToggleModal(); + if (caseIds.length === 1) { + const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); + if (singleCase) { + return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + } + } + const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); + setDeleteBulk(convertToDeleteCases); + }, + [selectedCases] + ); + + const handleUpdateCaseStatus = useCallback( + (status: string) => { + updateBulkStatus(selectedCases, status); + }, + [selectedCases] + ); + + const selectedCaseIds = useMemo( + (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), + [selectedCases] + ); + + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + <EuiContextMenuPanel + data-test-subj="cases-bulk-actions" + items={getBulkItems({ + caseStatus: filterOptions.status, + closePopover, + deleteCasesAction: toggleBulkDeleteModal, + selectedCaseIds, + updateCaseStatus: handleUpdateCaseStatus, + })} + /> + ), + [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal] + ); + const handleDispatchUpdate = useCallback( + (args: Omit<UpdateCase, 'refetchCasesStatus'>) => { + dispatchUpdateCaseProperty({ ...args, refetchCasesStatus: fetchCasesStatus }); + }, + [dispatchUpdateCaseProperty, fetchCasesStatus] + ); + + const actions = useMemo( + () => + getActions({ + caseStatus: filterOptions.status, + deleteCaseOnClick: toggleDeleteModal, + dispatchUpdate: handleDispatchUpdate, + }), + [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] + ); + + const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + let newQueryParams = queryParams; + if (sort) { + newQueryParams = { + ...newQueryParams, + sortField: getSortField(sort.field), + sortOrder: sort.direction, + }; + } + if (page) { + newQueryParams = { + ...newQueryParams, + page: page.index + 1, + perPage: page.size, + }; + } + setQueryParams(newQueryParams); + refreshCases(false); + }, + [queryParams] + ); + + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial<FilterOptions>) => { + if (newFilterOptions.status && newFilterOptions.status === 'closed') { + setQueryParams({ sortField: SortFieldCase.closedAt }); + } else if (newFilterOptions.status && newFilterOptions.status === 'open') { + setQueryParams({ sortField: SortFieldCase.createdAt }); + } + setFilters(newFilterOptions); + refreshCases(false); + }, + [filterOptions, queryParams] + ); + + const memoizedGetCasesColumns = useMemo( + () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status), + [actions, filterOptions.status, userCanCrud] + ); + const memoizedPagination = useMemo( + () => ({ + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, + totalItemCount: data.total, + pageSizeOptions: [5, 10, 15, 20, 25], + }), + [data, queryParams] + ); + + const sorting: EuiTableSortingType<Case> = { + sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, + }; + const euiBasicTableSelectionProps = useMemo<EuiTableSelectionType<Case>>( + () => ({ onSelectionChange: setSelectedCases }), + [selectedCases] + ); + const isCasesLoading = useMemo( + () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, + [loading] + ); + const isDataEmpty = useMemo(() => data.total === 0, [data]); + + return ( + <> + {!isEmpty(actionsErrors) && ( + <CaseCallOut title={ERROR_PUSH_SERVICE_CALLOUT_TITLE} messages={actionsErrors} /> + )} + <CaseHeaderPage title={i18n.PAGE_TITLE}> + <EuiFlexGroup alignItems="center" gutterSize="m" responsive={false} wrap={true}> + <EuiFlexItem grow={false}> + <OpenClosedStats + dataTestSubj="openStatsHeader" + caseCount={countOpenCases} + caseStatus={'open'} + isLoading={isCasesStatusLoading} + /> + </EuiFlexItem> + <FlexItemDivider grow={false}> + <OpenClosedStats + dataTestSubj="closedStatsHeader" + caseCount={countClosedCases} + caseStatus={'closed'} + isLoading={isCasesStatusLoading} + /> + </FlexItemDivider> + <EuiFlexItem grow={false}> + <ConfigureCaseButton + label={i18n.CONFIGURE_CASES_BUTTON} + isDisabled={!isEmpty(actionsErrors) || !userCanCrud} + showToolTip={!isEmpty(actionsErrors)} + msgTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].description : <></>} + titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} + urlSearch={urlSearch} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + isDisabled={!userCanCrud} + fill + href={getCreateCaseUrl(urlSearch)} + iconType="plusInCircle" + data-test-subj="createNewCaseBtn" + > + {i18n.CREATE_TITLE} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </CaseHeaderPage> + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( + <ProgressLoader size="xs" color="accent" className="essentialAnimation" /> + )} + <Panel loading={isCasesLoading}> + <CasesTableFilters + countClosedCases={data.countClosedCases} + countOpenCases={data.countOpenCases} + onFilterChanged={onFilterChangedCallback} + initial={{ + search: filterOptions.search, + reporters: filterOptions.reporters, + tags: filterOptions.tags, + status: filterOptions.status, + }} + setFilterRefetch={setFilterRefetch} + /> + {isCasesLoading && isDataEmpty ? ( + <Div> + <EuiLoadingContent data-test-subj="initialLoadingPanelAllCases" lines={10} /> + </Div> + ) : ( + <Div> + <UtilityBar border> + <UtilityBarSection> + <UtilityBarGroup> + <UtilityBarText data-test-subj="case-table-case-count"> + {i18n.SHOWING_CASES(data.total ?? 0)} + </UtilityBarText> + </UtilityBarGroup> + <UtilityBarGroup> + <UtilityBarText data-test-subj="case-table-selected-case-count"> + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} + </UtilityBarText> + {userCanCrud && ( + <UtilityBarAction + data-test-subj="case-table-bulk-actions" + iconSide="right" + iconType="arrowDown" + popoverContent={getBulkItemsPopoverContent} + > + {i18n.BULK_ACTIONS} + </UtilityBarAction> + )} + <UtilityBarAction iconSide="left" iconType="refresh" onClick={refreshCases}> + {i18n.REFRESH} + </UtilityBarAction> + </UtilityBarGroup> + </UtilityBarSection> + </UtilityBar> + <EuiBasicTable + columns={memoizedGetCasesColumns} + data-test-subj="cases-table" + isSelectable={userCanCrud} + itemId="id" + items={data.cases} + noItemsMessage={ + <EuiEmptyPrompt + title={<h3>{i18n.NO_CASES}</h3>} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + <EuiButton + isDisabled={!userCanCrud} + fill + size="s" + href={getCreateCaseUrl(urlSearch)} + iconType="plusInCircle" + > + {i18n.ADD_NEW_CASE} + </EuiButton> + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + selection={userCanCrud ? euiBasicTableSelectionProps : {}} + sorting={sorting} + /> + </Div> + )} + </Panel> + {confirmDeleteModal} + </> + ); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx rename to x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/plugins/siem/public/pages/case/components/all_cases/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/all_cases/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/plugins/siem/public/pages/case/components/bulk_actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/bulk_actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts b/x-pack/plugins/siem/public/pages/case/components/bulk_actions/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/bulk_actions/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx b/x-pack/plugins/siem/public/pages/case/components/callout/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx rename to x-pack/plugins/siem/public/pages/case/components/callout/helpers.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/callout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/callout/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx b/x-pack/plugins/siem/public/pages/case/components/callout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/callout/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/translations.ts b/x-pack/plugins/siem/public/pages/case/components/callout/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/callout/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_header_page/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/case_header_page/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts b/x-pack/plugins/siem/public/pages/case/components/case_header_page/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_header_page/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/case_header_page/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx rename to x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx new file mode 100644 index 0000000000000..01b9bc42f8e91 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -0,0 +1,342 @@ +/* + * 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 { + EuiButtonToggle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiLoadingSpinner, + EuiHorizontalRule, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; +import { Case } from '../../../../containers/case/types'; +import { getCaseUrl } from '../../../../components/link_to'; +import { HeaderPage } from '../../../../components/header_page'; +import { EditableTitle } from '../../../../components/header_page/editable_title'; +import { TagList } from '../tag_list'; +import { useGetCase } from '../../../../containers/case/use_get_case'; +import { UserActionTree } from '../user_action_tree'; +import { UserList } from '../user_list'; +import { useUpdateCase } from '../../../../containers/case/use_update_case'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; +import { WrapperPage } from '../../../../components/wrapper_page'; +import { getTypedPayload } from '../../../../containers/case/utils'; +import { WhitePageWrapper } from '../wrappers'; +import { useBasePath } from '../../../../lib/kibana'; +import { CaseStatus } from '../case_status'; +import { navTabs } from '../../../home/home_navigations'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; +import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; +import { usePushToService } from '../use_push_to_service'; + +interface Props { + caseId: string; + userCanCrud: boolean; +} + +const MyWrapper = styled(WrapperPage)` + padding-bottom: 0; +`; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const MyEuiHorizontalRule = styled(EuiHorizontalRule)` + margin-left: 48px; + &.euiHorizontalRule--full { + width: calc(100% - 48px); + } +`; + +export interface CaseProps extends Props { + fetchCase: () => void; + caseData: Case; + updateCase: (newCase: Case) => void; +} + +export const CaseComponent = React.memo<CaseProps>( + ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { + const basePath = window.location.origin + useBasePath(); + const caseLink = `${basePath}/app/siem#/case/${caseId}`; + const search = useGetUrlSearch(navTabs.case); + + const [initLoadingData, setInitLoadingData] = useState(true); + const { + caseUserActions, + fetchCaseUserActions, + firstIndexPushToService, + hasDataToPush, + isLoading: isLoadingUserActions, + lastIndexPushToService, + participants, + } = useGetCaseUserActions(caseId); + const { isLoading, updateKey, updateCaseProperty } = useUpdateCase({ + caseId, + }); + + // Update Fields + const onUpdateField = useCallback( + (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { + const handleUpdateNewCase = (newCase: Case) => + updateCase({ ...newCase, comments: caseData.comments }); + switch (newUpdateKey) { + case 'title': + const titleUpdate = getTypedPayload<string>(updateValue); + if (titleUpdate.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'title', + updateValue: titleUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'description': + const descriptionUpdate = getTypedPayload<string>(updateValue); + if (descriptionUpdate.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'description', + updateValue: descriptionUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'tags': + const tagsUpdate = getTypedPayload<string[]>(updateValue); + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'tags', + updateValue: tagsUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + break; + case 'status': + const statusUpdate = getTypedPayload<string>(updateValue); + if (caseData.status !== updateValue) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'status', + updateValue: statusUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + default: + return null; + } + }, + [fetchCaseUserActions, updateCaseProperty, updateCase, caseData] + ); + const handleUpdateCase = useCallback( + (newCase: Case) => { + updateCase(newCase); + fetchCaseUserActions(newCase.id); + }, + [updateCase, fetchCaseUserActions] + ); + + const { pushButton, pushCallouts } = usePushToService({ + caseId: caseData.id, + caseStatus: caseData.status, + isNew: caseUserActions.filter(cua => cua.action === 'push-to-service').length === 0, + updateCase: handleUpdateCase, + userCanCrud, + }); + + const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [ + onUpdateField, + ]); + const toggleStatusCase = useCallback( + e => onUpdateField('status', e.target.checked ? 'closed' : 'open'), + [onUpdateField] + ); + const handleRefresh = useCallback(() => { + fetchCaseUserActions(caseData.id); + fetchCase(); + }, [caseData.id, fetchCase, fetchCaseUserActions]); + + const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); + + const caseStatusData = useMemo( + () => + caseData.status === 'open' + ? { + 'data-test-subj': 'case-view-createdAt', + value: caseData.createdAt, + title: i18n.CASE_OPENED, + buttonLabel: i18n.CLOSE_CASE, + status: caseData.status, + icon: 'folderCheck', + badgeColor: 'secondary', + isSelected: false, + } + : { + 'data-test-subj': 'case-view-closedAt', + value: caseData.closedAt ?? '', + title: i18n.CASE_CLOSED, + buttonLabel: i18n.REOPEN_CASE, + status: caseData.status, + icon: 'folderExclamation', + badgeColor: 'danger', + isSelected: true, + }, + [caseData.closedAt, caseData.createdAt, caseData.status] + ); + const emailContent = useMemo( + () => ({ + subject: i18n.EMAIL_SUBJECT(caseData.title), + body: i18n.EMAIL_BODY(caseLink), + }), + [caseLink, caseData.title] + ); + + useEffect(() => { + if (initLoadingData && !isLoadingUserActions) { + setInitLoadingData(false); + } + }, [initLoadingData, isLoadingUserActions]); + + return ( + <> + <MyWrapper> + <HeaderPage + backOptions={{ + href: getCaseUrl(search), + text: i18n.BACK_TO_ALL, + dataTestSubj: 'backToCases', + }} + data-test-subj="case-view-title" + titleNode={ + <EditableTitle + disabled={!userCanCrud} + isLoading={isLoading && updateKey === 'title'} + title={caseData.title} + onSubmit={onSubmitTitle} + /> + } + title={caseData.title} + > + <CaseStatus + caseData={caseData} + disabled={!userCanCrud} + isLoading={isLoading && updateKey === 'status'} + onRefresh={handleRefresh} + toggleStatusCase={toggleStatusCase} + {...caseStatusData} + /> + </HeaderPage> + </MyWrapper> + <WhitePageWrapper> + <MyWrapper> + {pushCallouts != null && pushCallouts} + <EuiFlexGroup> + <EuiFlexItem grow={6}> + {initLoadingData && <EuiLoadingContent lines={8} />} + {!initLoadingData && ( + <> + <UserActionTree + caseUserActions={caseUserActions} + data={caseData} + fetchUserActions={fetchCaseUserActions.bind(null, caseData.id)} + firstIndexPushToService={firstIndexPushToService} + isLoadingDescription={isLoading && updateKey === 'description'} + isLoadingUserActions={isLoadingUserActions} + lastIndexPushToService={lastIndexPushToService} + onUpdateField={onUpdateField} + updateCase={updateCase} + userCanCrud={userCanCrud} + /> + <MyEuiHorizontalRule margin="s" /> + <EuiFlexGroup alignItems="center" gutterSize="s" justifyContent="flexEnd"> + <EuiFlexItem grow={false}> + <EuiButtonToggle + data-test-subj={caseStatusData['data-test-subj']} + iconType={caseStatusData.icon} + isDisabled={!userCanCrud} + isSelected={caseStatusData.isSelected} + isLoading={isLoading && updateKey === 'status'} + label={caseStatusData.buttonLabel} + onChange={toggleStatusCase} + /> + </EuiFlexItem> + {hasDataToPush && ( + <EuiFlexItem data-test-subj="has-data-to-push-button" grow={false}> + {pushButton} + </EuiFlexItem> + )} + </EuiFlexGroup> + </> + )} + </EuiFlexItem> + <EuiFlexItem grow={2}> + <UserList + data-test-subj="case-view-user-list-reporter" + email={emailContent} + headline={i18n.REPORTER} + users={[caseData.createdBy]} + /> + <UserList + data-test-subj="case-view-user-list-participants" + email={emailContent} + headline={i18n.PARTICIPANTS} + loading={isLoadingUserActions} + users={participants} + /> + <TagList + data-test-subj="case-view-tag-list" + disabled={!userCanCrud} + tags={caseData.tags} + onSubmit={onSubmitTags} + isLoading={isLoading && updateKey === 'tags'} + /> + </EuiFlexItem> + </EuiFlexGroup> + </MyWrapper> + </WhitePageWrapper> + <SpyRoute state={spyState} /> + </> + ); + } +); + +export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { + const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId); + if (isError) { + return null; + } + if (isLoading) { + return ( + <MyEuiFlexGroup justifyContent="center" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiLoadingSpinner data-test-subj="case-view-loading" size="xl" /> + </EuiFlexItem> + </MyEuiFlexGroup> + ); + } + + return ( + <CaseComponent + caseId={caseId} + fetchCase={fetchCase} + caseData={data} + updateCase={updateCase} + userCanCrud={userCanCrud} + /> + ); +}); + +CaseComponent.displayName = 'CaseComponent'; +CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/plugins/siem/public/pages/case/components/case_view/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/case_view/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx new file mode 100644 index 0000000000000..0eccd8980ccd2 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx @@ -0,0 +1,54 @@ +/* + * 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 { Connector } from '../../../../../containers/case/configure/types'; +import { ReturnConnectors } from '../../../../../containers/case/configure/use_connectors'; +import { connectorsMock } from '../../../../../containers/case/configure/mock'; +import { ReturnUseCaseConfigure } from '../../../../../containers/case/configure/use_configure'; +import { createUseKibanaMock } from '../../../../../mock/kibana_react'; +export { mapping } from '../../../../../containers/case/configure/mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { actionTypeRegistryMock } from '../../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; + +export const connectors: Connector[] = connectorsMock; + +export const searchURL = + '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; + +export const useCaseConfigureResponse: ReturnUseCaseConfigure = { + closureType: 'close-by-user', + connectorId: 'none', + connectorName: 'none', + currentConfiguration: { + connectorId: 'none', + closureType: 'close-by-user', + connectorName: 'none', + }, + firstLoad: false, + loading: false, + mapping: null, + persistCaseConfigure: jest.fn(), + persistLoading: false, + refetchCaseConfigure: jest.fn(), + setClosureType: jest.fn(), + setConnector: jest.fn(), + setCurrentConfiguration: jest.fn(), + setMapping: jest.fn(), + version: '', +}; + +export const useConnectorsResponse: ReturnConnectors = { + loading: false, + connectors, + refetchConnectors: jest.fn(), +}; + +export const kibanaMockImplementationArgs = { + services: { + ...createUseKibanaMock()().services, + triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/button.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx similarity index 92% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx index 15066e73eee82..d5575f3bac4c8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx @@ -9,7 +9,7 @@ import { EuiIcon, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; import { Connector } from '../../../../containers/case/configure/types'; -import { connectors as connectorsDefinition } from '../../../../lib/connectors/config'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; import * as i18n from './translations'; export interface Props { @@ -54,7 +54,7 @@ const ConnectorsDropdownComponent: React.FC<Props> = ({ inputDisplay: ( <> <EuiIconExtended - type={connectorsDefinition[connector.actionTypeId].logo} + type={connectorsConfiguration[connector.actionTypeId]?.logo ?? ''} size={ICON_SIZE} /> <span>{connector.name}</span> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx new file mode 100644 index 0000000000000..fde179f3d25fc --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -0,0 +1,792 @@ +/* + * 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 { ReactWrapper, mount } from 'enzyme'; + +import { ConfigureCases } from './'; +import { TestProviders } from '../../../../mock'; +import { Connectors } from './connectors'; +import { ClosureOptions } from './closure_options'; +import { Mapping } from './mapping'; +import { + ActionsConnectorsContextProvider, + ConnectorAddFlyout, + ConnectorEditFlyout, +} from '../../../../../../triggers_actions_ui/public'; +import { EuiBottomBar } from '@elastic/eui'; + +import { useKibana } from '../../../../lib/kibana'; +import { useConnectors } from '../../../../containers/case/configure/use_connectors'; +import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; + +import { + connectors, + searchURL, + useCaseConfigureResponse, + useConnectorsResponse, + kibanaMockImplementationArgs, + mapping, +} from './__mock__'; + +jest.mock('../../../../lib/kibana'); +jest.mock('../../../../containers/case/configure/use_connectors'); +jest.mock('../../../../containers/case/configure/use_configure'); +jest.mock('../../../../components/navigation/use_get_url_search'); + +const useKibanaMock = useKibana as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; +describe('ConfigureCases', () => { + describe('rendering', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + }); + + test('it renders the Connectors', () => { + expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').exists()).toBeTruthy(); + }); + + test('it renders the ClosureType', () => { + expect( + wrapper.find('[data-test-subj="case-closure-options-form-group"]').exists() + ).toBeTruthy(); + }); + + test('it renders the Mapping', () => { + expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').exists()).toBeTruthy(); + }); + + test('it renders the ActionsConnectorsContextProvider', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy(); + }); + + test('it renders the ConnectorAddFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy(); + }); + + test('it does NOT render the ConnectorEditFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); + }); + + test('it does NOT render the EuiCallOut', () => { + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeFalsy(); + }); + + test('it does NOT render the EuiBottomBar', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it disables correctly ClosureOptions when the connector is set to none', () => { + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + }); + + describe('Unhappy path', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + closureType: 'close-by-user', + connectorId: 'not-id', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'not-id', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + }); + + test('it shows the warning callout when configuration is invalid', () => { + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeTruthy(); + }); + + test('it disables the update connector button when the connectorId is invalid', () => { + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + }); + + describe('Happy path', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '123', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '123', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + }); + + test('it renders the ConnectorEditFlyout', () => { + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy(); + }); + + test('it renders with correct props', () => { + // Connector + expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); + expect(wrapper.find(Connectors).prop('disabled')).toBe(false); + expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); + expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('123'); + + // ClosureOptions + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); + expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); + + // Mapping + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false); + expect(wrapper.find(Mapping).prop('mapping')).toEqual( + connectors[0].config.casesConfiguration.mapping + ); + + // Flyouts + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + logo: 'test-file-stub', + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + }, + { + id: '.jira', + name: 'Jira', + logo: 'test-file-stub', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + }, + ]); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); + }); + + test('it does not shows the action bar when there is no change', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it disables the mapping permanently', () => { + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it sets the mapping of a connector correctly', () => { + expect(wrapper.find(Mapping).prop('mapping')).toEqual( + connectors[0].config.casesConfiguration.mapping + ); + }); + + // TODO: When mapping is enabled the test.todo should be implemented. + test.todo('the mapping is changed successfully when changing the third party'); + test.todo('the mapping is changed successfully when changing the action type'); + test.todo('it disables the update connector button when loading the configuration'); + + test('it disables correctly when the user cannot crud', () => { + const newWrapper = mount(<ConfigureCases userCanCrud={false} />, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + expect(newWrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(true); + }); + }); + + describe('loading connectors', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + }); + + test('it disables correctly Connector when loading connectors', () => { + expect(wrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it pass the correct value to isLoading attribute on Connector', () => { + expect(wrapper.find(Connectors).prop('isLoading')).toBe(true); + }); + + test('it disables correctly ClosureOptions when loading connectors', () => { + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when loading the connectors', () => { + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + test('it disables the buttons of action bar when loading connectors', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '123', + closureType: 'close-by-user', + }, + })); + const newWrapper = mount(<ConfigureCases userCanCrud />, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + }); + + describe('saving configuration', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + persistLoading: true, + })); + + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + }); + + test('it disables correctly Connector when saving configuration', () => { + expect(wrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it disables correctly ClosureOptions when saving configuration', () => { + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when saving the configuration', () => { + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the buttons of action bar when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '123', + closureType: 'close-by-user', + }, + persistLoading: true, + })); + + const newWrapper = mount(<ConfigureCases userCanCrud />, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it shows the loading spinner when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '123', + closureType: 'close-by-user', + }, + persistLoading: true, + })); + + const newWrapper = mount(<ConfigureCases userCanCrud />, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isLoading') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isLoading') + ).toBe(true); + }); + }); + + describe('update connector', () => { + let wrapper: ReactWrapper; + const persistCaseConfigure = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '123', + closureType: 'close-by-user', + }, + persistCaseConfigure, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + }); + + test('it submits the configuration correctly', () => { + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-user', + }); + }); + + test('it has the correct url on cancel button', () => { + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('href') + ).toBe(`#/link-to/case${searchURL}`); + }); + test('it disables the buttons of action bar when loading configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '123', + closureType: 'close-by-user', + }, + loading: true, + })); + const newWrapper = mount(<ConfigureCases userCanCrud />, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + }); + + describe('user interactions', () => { + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '456', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + }); + + test('it show the add flyout when pressing the add connector button', () => { + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + wrapper + .find('button[data-test-subj="case-configure-add-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + }); + + test('it show the edit flyout when pressing the update connector button', () => { + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + wrapper + .find('button[data-test-subj="case-mapping-update-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + }); + + test('it tracks the changes successfully', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: '123', + closureType: 'close-by-pushing', + }, + })); + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('2 unsaved changes'); + }); + test('it tracks the changes successfully when name changes', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + connectorName: 'nameChange', + currentConfiguration: { + connectorId: '123', + closureType: 'close-by-pushing', + connectorName: 'before', + }, + })); + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('2 unsaved changes'); + }); + + test('it tracks and reverts the changes successfully ', () => { + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + // change settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // revert back to initial settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-123"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-user"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it close and restores the action bar when the add connector button is pressed', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-pushing', + connectorId: '456', + currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + })); + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + // Press add connector button + wrapper + .find('button[data-test-subj="case-configure-add-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + + // Close the add flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it close and restores the action bar when the update connector button is pressed', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-pushing', + connectorId: '456', + currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + })); + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // Press update connector button + wrapper + .find('button[data-test-subj="case-mapping-update-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + + // Close the edit flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it shows the action bar when the connector is changed', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '123', + currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, + })); + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it closes the action bar when pressing save', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-user', + connectorId: '456', + currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping, + closureType: 'close-by-pushing', + connectorId: '456', + currentConfiguration: { connectorId: '456', closureType: 'close-by-user' }, + })); + const wrapper = mount(<ConfigureCases userCanCrud />, { wrappingComponent: TestProviders }); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx new file mode 100644 index 0000000000000..66eef9e3ec7bf --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -0,0 +1,316 @@ +/* + * 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, { useCallback, useEffect, useState, Dispatch, SetStateAction } from 'react'; +import styled, { css } from 'styled-components'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiCallOut, + EuiBottomBar, + EuiButtonEmpty, + EuiText, +} from '@elastic/eui'; +import { isEmpty, difference } from 'lodash/fp'; +import { useKibana } from '../../../../lib/kibana'; +import { useConnectors } from '../../../../containers/case/configure/use_connectors'; +import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; +import { + ActionsConnectorsContextProvider, + ActionType, + ConnectorAddFlyout, + ConnectorEditFlyout, +} from '../../../../../../triggers_actions_ui/public'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorTableItem } from '../../../../../../triggers_actions_ui/public/types'; +import { getCaseUrl } from '../../../../components/link_to'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; +import { CCMapsCombinedActionAttributes } from '../../../../containers/case/configure/types'; +import { connectorsConfiguration } from '../../../../lib/connectors/config'; + +import { Connectors } from '../configure_cases/connectors'; +import { ClosureOptions } from '../configure_cases/closure_options'; +import { Mapping } from '../configure_cases/mapping'; +import { SectionWrapper } from '../wrappers'; +import { navTabs } from '../../../../pages/home/home_navigations'; +import * as i18n from './translations'; + +const FormWrapper = styled.div` + ${({ theme }) => css` + & > * { + margin-top 40px; + } + + & > :first-child { + margin-top: 0; + } + + padding-top: ${theme.eui.paddingSizes.xl}; + padding-bottom: ${theme.eui.paddingSizes.xl}; + `} +`; + +const actionTypes: ActionType[] = Object.values(connectorsConfiguration); + +interface ConfigureCasesComponentProps { + userCanCrud: boolean; +} + +const ConfigureCasesComponent: React.FC<ConfigureCasesComponentProps> = ({ userCanCrud }) => { + const search = useGetUrlSearch(navTabs.case); + const { http, triggers_actions_ui, notifications, application } = useKibana().services; + + const [connectorIsValid, setConnectorIsValid] = useState(true); + const [addFlyoutVisible, setAddFlyoutVisibility] = useState<boolean>(false); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false); + const [editedConnectorItem, setEditedConnectorItem] = useState<ActionConnectorTableItem | null>( + null + ); + + const [actionBarVisible, setActionBarVisible] = useState(false); + const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0); + + const { + connectorId, + closureType, + mapping, + currentConfiguration, + loading: loadingCaseConfigure, + persistLoading, + persistCaseConfigure, + setConnector, + setClosureType, + setMapping, + } = useCaseConfigure(); + + const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); + + // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise<void>. + // TODO: Fix it if reloadConnectors type change. + const reloadConnectors = useCallback(async () => refetchConnectors(), []); + const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; + const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none'; + + const handleSubmit = useCallback( + // TO DO give a warning/error to user when field are not mapped so they have chance to do it + () => { + setActionBarVisible(false); + persistCaseConfigure({ + connectorId, + connectorName: connectors.find(c => c.id === connectorId)?.name ?? '', + closureType, + }); + }, + [connectorId, connectors, closureType, mapping] + ); + + const onClickAddConnector = useCallback(() => { + setActionBarVisible(false); + setAddFlyoutVisibility(true); + }, []); + + const onClickUpdateConnector = useCallback(() => { + setActionBarVisible(false); + setEditFlyoutVisibility(true); + }, []); + + const handleActionBar = useCallback(() => { + const currentConfigurationMinusName = { + connectorId: currentConfiguration.connectorId, + closureType: currentConfiguration.closureType, + }; + const unsavedChanges = difference(Object.values(currentConfigurationMinusName), [ + connectorId, + closureType, + ]).length; + setActionBarVisible(!(unsavedChanges === 0)); + setTotalConfigurationChanges(unsavedChanges); + }, [currentConfiguration, connectorId, closureType]); + + const handleSetAddFlyoutVisibility = useCallback( + (isVisible: boolean) => { + handleActionBar(); + setAddFlyoutVisibility(isVisible); + }, + [currentConfiguration, connectorId, closureType] + ); + + const handleSetEditFlyoutVisibility = useCallback( + (isVisible: boolean) => { + handleActionBar(); + setEditFlyoutVisibility(isVisible); + }, + [currentConfiguration, connectorId, closureType] + ); + + useEffect(() => { + if ( + !isEmpty(connectors) && + connectorId !== 'none' && + connectors.some(c => c.id === connectorId) + ) { + const myConnector = connectors.find(c => c.id === connectorId); + const myMapping = myConnector?.config?.casesConfiguration?.mapping ?? []; + setMapping( + myMapping.map((m: CCMapsCombinedActionAttributes) => ({ + source: m.source, + target: m.target, + actionType: m.action_type ?? m.actionType, + })) + ); + } + }, [connectors, connectorId]); + + useEffect(() => { + if ( + !isLoadingConnectors && + connectorId !== 'none' && + !connectors.some(c => c.id === connectorId) + ) { + setConnectorIsValid(false); + } else if ( + !isLoadingConnectors && + (connectorId === 'none' || connectors.some(c => c.id === connectorId)) + ) { + setConnectorIsValid(true); + } + }, [connectors, connectorId]); + + useEffect(() => { + if (!isLoadingConnectors && connectorId !== 'none') { + setEditedConnectorItem( + connectors.find(c => c.id === connectorId) as ActionConnectorTableItem + ); + } + }, [connectors, connectorId]); + + useEffect(() => { + handleActionBar(); + }, [ + connectors, + connectorId, + closureType, + currentConfiguration.connectorId, + currentConfiguration.closureType, + ]); + + return ( + <FormWrapper> + {!connectorIsValid && ( + <SectionWrapper style={{ marginTop: 0 }}> + <EuiCallOut + title={i18n.WARNING_NO_CONNECTOR_TITLE} + color="warning" + iconType="help" + data-test-subj="configure-cases-warning-callout" + > + {i18n.WARNING_NO_CONNECTOR_MESSAGE} + </EuiCallOut> + </SectionWrapper> + )} + <SectionWrapper> + <Connectors + connectors={connectors ?? []} + disabled={persistLoading || isLoadingConnectors || !userCanCrud} + isLoading={isLoadingConnectors} + onChangeConnector={setConnector} + handleShowAddFlyout={onClickAddConnector} + selectedConnector={connectorId} + /> + </SectionWrapper> + <SectionWrapper> + <ClosureOptions + closureTypeSelected={closureType} + disabled={persistLoading || isLoadingConnectors || connectorId === 'none' || !userCanCrud} + onChangeClosureType={setClosureType} + /> + </SectionWrapper> + <SectionWrapper> + <Mapping + disabled + updateConnectorDisabled={updateConnectorDisabled || !userCanCrud} + mapping={mapping} + onChangeMapping={setMapping} + setEditFlyoutVisibility={onClickUpdateConnector} + /> + </SectionWrapper> + {actionBarVisible && ( + <EuiBottomBar data-test-subj="case-configure-action-bottom-bar"> + <EuiFlexGroup justifyContent="spaceBetween" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="s"> + <EuiText data-test-subj="case-configure-action-bottom-bar-total-changes"> + {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} + </EuiText> + </EuiFlexGroup> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + color="ghost" + iconType="cross" + isDisabled={isLoadingAny} + isLoading={persistLoading} + aria-label={i18n.CANCEL} + href={getCaseUrl(search)} + data-test-subj="case-configure-action-bottom-bar-cancel-button" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + fill + color="secondary" + iconType="save" + aria-label={i18n.SAVE_CHANGES} + isDisabled={isLoadingAny} + isLoading={persistLoading} + onClick={handleSubmit} + data-test-subj="case-configure-action-bottom-bar-save-button" + > + {i18n.SAVE_CHANGES} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + </EuiBottomBar> + )} + <ActionsConnectorsContextProvider + value={{ + http, + actionTypeRegistry: triggers_actions_ui.actionTypeRegistry, + toastNotifications: notifications.toasts, + capabilities: application.capabilities, + reloadConnectors, + }} + > + <ConnectorAddFlyout + addFlyoutVisible={addFlyoutVisible} + setAddFlyoutVisibility={handleSetAddFlyoutVisibility as Dispatch<SetStateAction<boolean>>} + actionTypes={actionTypes} + /> + {editedConnectorItem && ( + <ConnectorEditFlyout + key={editedConnectorItem.id} + initialConnector={editedConnectorItem} + editFlyoutVisible={editFlyoutVisible} + setEditFlyoutVisibility={ + handleSetEditFlyoutVisibility as Dispatch<SetStateAction<boolean>> + } + /> + )} + </ActionsConnectorsContextProvider> + </FormWrapper> + ); +}; + +export const ConfigureCases = React.memo(ConfigureCasesComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts b/x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts rename to x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx b/x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts b/x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/create/index.test.tsx new file mode 100644 index 0000000000000..4c2e15ddfa98a --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/create/index.test.tsx @@ -0,0 +1,162 @@ +/* + * 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 { mount } from 'enzyme'; + +import { Create } from './'; +import { TestProviders } from '../../../../mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + +jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../../../containers/case/use_post_case'); +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +import { wait } from '../../../../lib/helpers'; +import { SiemPageName } from '../../../home/types'; +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCaseMock = usePostCase as jest.Mock; + +const postCase = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; + +const sampleTags = ['coke', 'pepsi']; +const sampleData = { + description: 'what a great description', + tags: sampleTags, + title: 'what a cool title', +}; +const defaultPostCase = { + isLoading: false, + isError: false, + caseData: null, + postCase, +}; +describe('Create case', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const fetchTags = jest.fn(); + const formHookMock = getFormMock(sampleData); + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCaseMock.mockImplementation(() => defaultPostCase); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + + it('should post case on submit click', async () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="create-case-submit"]`) + .first() + .simulate('click'); + await wait(); + expect(postCase).toBeCalledWith(sampleData); + }); + + it('should redirect to all cases on cancel click', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="create-case-cancel"]`) + .first() + .simulate('click'); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`); + }); + it('should redirect to new case when caseData is there', () => { + const sampleId = '777777'; + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } })); + mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual( + `/${SiemPageName.case}/${sampleId}` + ); + }); + + it('should render spinner when loading', () => { + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); + }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <Create /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/plugins/siem/public/pages/case/components/create/index.tsx new file mode 100644 index 0000000000000..6731b88572cdd --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/create/index.tsx @@ -0,0 +1,215 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { Redirect } from 'react-router-dom'; + +import { isEqual } from 'lodash/fp'; +import { CasePostRequest } from '../../../../../../case/common/api'; +import { + Field, + Form, + getUseField, + useForm, + UseField, + FormDataProvider, +} from '../../../../shared_imports'; +import { usePostCase } from '../../../../containers/case/use_post_case'; +import { schema } from './schema'; +import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; +import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import * as i18n from '../../translations'; +import { SiemPageName } from '../../../home/types'; +import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + +export const CommonUseField = getUseField({ component: Field }); + +const ContainerBig = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeXL}; + `} +`; + +const Container = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSize}; + `} +`; +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; + z-index: 99; +`; + +const initialCaseValue: CasePostRequest = { + description: '', + tags: [], + title: '', +}; + +export const Create = React.memo(() => { + const { caseData, isLoading, postCase } = usePostCase(); + const [isCancel, setIsCancel] = useState(false); + const { form } = useForm<CasePostRequest>({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + }); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline<CasePostRequest>( + form, + 'description' + ); + + const onSubmit = useCallback(async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + await postCase(data); + } + }, [form]); + + const handleSetIsCancel = useCallback(() => { + setIsCancel(true); + }, []); + + if (caseData != null && caseData.id) { + return <Redirect to={`/${SiemPageName.case}/${caseData.id}`} />; + } + + if (isCancel) { + return <Redirect to={`/${SiemPageName.case}`} />; + } + + return ( + <EuiPanel> + {isLoading && <MySpinner data-test-subj="create-case-loading-spinner" size="xl" />} + <Form form={form}> + <CommonUseField + path="title" + componentProps={{ + idAria: 'caseTitle', + 'data-test-subj': 'caseTitle', + euiFieldProps: { + fullWidth: false, + disabled: isLoading, + }, + }} + /> + <Container> + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + disabled: isLoading, + options, + noSuggestions: false, + }, + }} + /> + </Container> + <ContainerBig> + <UseField + path="description" + component={MarkdownEditorForm} + componentProps={{ + dataTestSubj: 'caseDescription', + idAria: 'caseDescription', + isDisabled: isLoading, + onCursorPositionUpdate: handleCursorChange, + topRightContent: ( + <InsertTimelinePopover + hideUntitled={true} + isDisabled={isLoading} + onTimelineChange={handleOnTimelineChange} + /> + ), + }} + /> + </ContainerBig> + <FormDataProvider pathsToWatch="tags"> + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} + </FormDataProvider> + </Form> + <Container> + <EuiFlexGroup + alignItems="center" + justifyContent="flexEnd" + gutterSize="xs" + responsive={false} + > + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="create-case-cancel" + size="s" + onClick={handleSetIsCancel} + iconType="cross" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="create-case-submit" + fill + iconType="plusInCircle" + isDisabled={isLoading} + isLoading={isLoading} + onClick={onSubmit} + > + {i18n.CREATE_CASE} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </Container> + </EuiPanel> + ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx b/x-pack/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx new file mode 100644 index 0000000000000..a4e0bb6916531 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasePostRequest } from '../../../../../../case/common/api'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; +import * as i18n from '../../translations'; + +import { OptionalFieldLabel } from './optional_field_label'; +const { emptyField } = fieldValidators; + +export const schemaTags = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, + labelAppend: OptionalFieldLabel, +}; + +export const schema: FormSchema<CasePostRequest> = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.NAME, + validations: [ + { + validator: emptyField(i18n.TITLE_REQUIRED), + }, + ], + }, + description: { + label: i18n.DESCRIPTION, + validations: [ + { + validator: emptyField(i18n.DESCRIPTION_REQUIRED), + }, + ], + }, + tags: schemaTags, +}; diff --git a/x-pack/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx b/x-pack/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx new file mode 100644 index 0000000000000..b9dab13090aca --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx @@ -0,0 +1,39 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; +import * as i18n from '../all_cases/translations'; + +export interface Props { + caseCount: number | null; + caseStatus: 'open' | 'closed'; + isLoading: boolean; + dataTestSubj?: string; +} + +export const OpenClosedStats = React.memo<Props>( + ({ caseCount, caseStatus, isLoading, dataTestSubj }) => { + const openClosedStats = useMemo( + () => [ + { + title: caseStatus === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, + description: isLoading ? <EuiLoadingSpinner /> : caseCount ?? 'N/A', + }, + ], + [caseCount, caseStatus, isLoading, dataTestSubj] + ); + return ( + <EuiDescriptionList + data-test-subj={dataTestSubj} + textStyle="reverse" + listItems={openClosedStats} + /> + ); + } +); + +OpenClosedStats.displayName = 'OpenClosedStats'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts b/x-pack/plugins/siem/public/pages/case/components/property_actions/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/constants.ts rename to x-pack/plugins/siem/public/pages/case/components/property_actions/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/plugins/siem/public/pages/case/components/property_actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/property_actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/translations.ts b/x-pack/plugins/siem/public/pages/case/components/property_actions/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/property_actions/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/property_actions/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/tag_list/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.test.tsx new file mode 100644 index 0000000000000..9ddb96a4ed295 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.test.tsx @@ -0,0 +1,180 @@ +/* + * 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 { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import { TagList } from './'; +import { getFormMock } from '../__mock__/form'; +import { TestProviders } from '../../../../mock'; +import { wait } from '../../../../lib/helpers'; +import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); +const onSubmit = jest.fn(); +const defaultProps = { + disabled: false, + isLoading: false, + onSubmit, + tags: [], +}; + +describe('TagList ', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const sampleTags = ['coke', 'pepsi']; + const fetchTags = jest.fn(); + const formHookMock = getFormMock({ tags: sampleTags }); + beforeEach(() => { + jest.resetAllMocks(); + (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); + + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + it('Renders no tags, and then edit', () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeFalsy(); + expect( + wrapper + .find(`[data-test-subj="edit-tags"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Edit tag on submit', async () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + wrapper + .find(`[data-test-subj="edit-tags-submit"]`) + .last() + .simulate('click'); + await wait(); + expect(onSubmit).toBeCalledWith(sampleTags); + }); + }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + <TestProviders> + <TagList {...defaultProps} /> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); + it('Cancels on cancel', async () => { + const props = { + ...defaultProps, + tags: ['pepsi'], + }; + const wrapper = mount( + <TestProviders> + <TagList {...props} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeFalsy(); + wrapper + .find(`[data-test-subj="edit-tags-cancel"]`) + .last() + .simulate('click'); + await wait(); + wrapper.update(); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + }); + }); + it('Renders disabled button', () => { + const props = { ...defaultProps, disabled: true }; + const wrapper = mount( + <TestProviders> + <TagList {...props} /> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .prop('disabled') + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx new file mode 100644 index 0000000000000..c61feab0bab98 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -0,0 +1,179 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import { + EuiText, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiLoadingSpinner, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { isEqual } from 'lodash/fp'; +import * as i18n from './translations'; +import { Form, FormDataProvider, useForm } from '../../../../shared_imports'; +import { schema } from './schema'; +import { CommonUseField } from '../create'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + +interface TagListProps { + disabled?: boolean; + isLoading: boolean; + onSubmit: (a: string[]) => void; + tags: string[]; +} + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + p { + font-size: ${theme.eui.euiSizeM}; + } + `} +`; + +export const TagList = React.memo( + ({ disabled = false, isLoading, onSubmit, tags }: TagListProps) => { + const { form } = useForm({ + defaultValue: { tags }, + options: { stripEmptyFields: false }, + schema, + }); + const [isEditTags, setIsEditTags] = useState(false); + + const onSubmitTags = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && newData.tags) { + onSubmit(newData.tags); + setIsEditTags(false); + } + }, [form, onSubmit]); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); + + return ( + <EuiText> + <EuiFlexGroup alignItems="center" gutterSize="xs" justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <h4>{i18n.TAGS}</h4> + </EuiFlexItem> + {isLoading && <EuiLoadingSpinner data-test-subj="tag-list-loading" />} + {!isLoading && ( + <EuiFlexItem data-test-subj="tag-list-edit" grow={false}> + <EuiButtonIcon + data-test-subj="tag-list-edit-button" + isDisabled={disabled} + aria-label={i18n.EDIT_TAGS_ARIA} + iconType={'pencil'} + onClick={setIsEditTags.bind(null, true)} + /> + </EuiFlexItem> + )} + </EuiFlexGroup> + <EuiHorizontalRule margin="xs" /> + <MyFlexGroup gutterSize="xs" data-test-subj="case-tags"> + {tags.length === 0 && !isEditTags && <p data-test-subj="no-tags">{i18n.NO_TAGS}</p>} + {tags.length > 0 && + !isEditTags && + tags.map((tag, key) => ( + <EuiFlexItem grow={false} key={`${tag}${key}`}> + <EuiBadge data-test-subj="case-tag" color="hollow"> + {tag} + </EuiBadge> + </EuiFlexItem> + ))} + {isEditTags && ( + <EuiFlexGroup data-test-subj="edit-tags" direction="column"> + <EuiFlexItem> + <Form form={form}> + <CommonUseField + path="tags" + componentProps={{ + idAria: 'caseTags', + 'data-test-subj': 'caseTags', + euiFieldProps: { + fullWidth: true, + placeholder: '', + options, + noSuggestions: false, + }, + }} + /> + <FormDataProvider pathsToWatch="tags"> + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} + </FormDataProvider> + </Form> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGroup gutterSize="s" alignItems="center"> + <EuiFlexItem grow={false}> + <EuiButton + color="secondary" + data-test-subj="edit-tags-submit" + fill + iconType="save" + onClick={onSubmitTags} + size="s" + > + {i18n.SAVE} + </EuiButton> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="edit-tags-cancel" + iconType="cross" + onClick={setIsEditTags.bind(null, false)} + size="s" + > + {i18n.CANCEL} + </EuiButtonEmpty> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + )} + </MyFlexGroup> + </EuiText> + ); + } +); + +TagList.displayName = 'TagList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/tag_list/schema.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/schema.tsx rename to x-pack/plugins/siem/public/pages/case/components/tag_list/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/translations.ts b/x-pack/plugins/siem/public/pages/case/components/tag_list/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/tag_list/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx rename to x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx new file mode 100644 index 0000000000000..d428d9988ae39 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx @@ -0,0 +1,192 @@ +/* + * 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. + */ +/* eslint-disable react/display-name */ +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { usePushToService, ReturnUsePushToService, UsePushToService } from './'; +import { TestProviders } from '../../../../mock'; +import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; +import { ClosureType } from '../../../../../../case/common/api/cases'; +import * as i18n from './translations'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { getKibanaConfigError, getLicenseError } from './helpers'; +import * as api from '../../../../containers/case/configure/api'; +jest.mock('../../../../containers/case/use_get_action_license'); +jest.mock('../../../../containers/case/use_post_push_to_service'); +jest.mock('../../../../containers/case/configure/api'); + +describe('usePushToService', () => { + const caseId = '12345'; + const updateCase = jest.fn(); + const postPushToService = jest.fn(); + const mockPostPush = { + isLoading: false, + postPushToService, + }; + const closureType: ClosureType = 'close-by-user'; + const mockConnector = { + connectorId: 'c00l', + connectorName: 'name', + }; + const mockCaseConfigure = { + ...mockConnector, + createdAt: 'string', + createdBy: {}, + closureType, + updatedAt: 'string', + updatedBy: {}, + version: 'string', + }; + const getConfigureMock = jest.spyOn(api, 'getCaseConfigure'); + const actionLicense = { + id: '.servicenow', + name: 'ServiceNow', + minimumLicenseRequired: 'platinum', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }; + beforeEach(() => { + jest.resetAllMocks(); + (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense, + })); + getConfigureMock.mockImplementation(() => Promise.resolve(mockCaseConfigure)); + }); + it('push case button posts the push with correct args', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(getConfigureMock).toBeCalled(); + result.current.pushButton.props.children.props.onClick(); + expect(postPushToService).toBeCalledWith({ ...mockConnector, caseId, updateCase }); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + it('Displays message when user does not have premium license', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInLicense: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getLicenseError().title); + }); + }); + it('Displays message when user does not have case enabled in config', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInConfig: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); + }); + }); + it('Displays message when user does not have a connector configured', async () => { + getConfigureMock.mockImplementation(() => + Promise.resolve({ + ...mockCaseConfigure, + connectorId: 'none', + }) + ); + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'open', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); + }); + }); + it('Displays message when case is closed', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook<UsePushToService, ReturnUsePushToService>( + () => + usePushToService({ + caseId, + caseStatus: 'closed', + isNew: false, + updateCase, + userCanCrud: true, + }), + { + wrapper: ({ children }) => <TestProviders> {children}</TestProviders>, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx new file mode 100644 index 0000000000000..6109fd05096b9 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx @@ -0,0 +1,159 @@ +/* + * 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 { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; + +import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; +import { Case } from '../../../../containers/case/types'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; +import { getConfigureCasesUrl } from '../../../../components/link_to'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../../home/home_navigations'; +import { CaseCallOut } from '../callout'; +import { getLicenseError, getKibanaConfigError } from './helpers'; +import * as i18n from './translations'; + +export interface UsePushToService { + caseId: string; + caseStatus: string; + isNew: boolean; + updateCase: (newCase: Case) => void; + userCanCrud: boolean; +} + +export interface ReturnUsePushToService { + pushButton: JSX.Element; + pushCallouts: JSX.Element | null; +} + +export const usePushToService = ({ + caseId, + caseStatus, + isNew, + updateCase, + userCanCrud, +}: UsePushToService): ReturnUsePushToService => { + const urlSearch = useGetUrlSearch(navTabs.case); + + const { isLoading, postPushToService } = usePostPushToService(); + + const { connectorId, connectorName, loading: loadingCaseConfigure } = useCaseConfigure(); + + const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); + + const handlePushToService = useCallback(() => { + if (connectorId != null) { + postPushToService({ + caseId, + connectorId, + connectorName, + updateCase, + }); + } + }, [caseId, connectorId, connectorName, postPushToService, updateCase]); + + const errorsMsg = useMemo(() => { + let errors: Array<{ title: string; description: JSX.Element }> = []; + if (actionLicense != null && !actionLicense.enabledInLicense) { + errors = [...errors, getLicenseError()]; + } + if (connectorId === 'none' && !loadingCaseConfigure && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, + description: ( + <FormattedMessage + defaultMessage="To open and update cases in external systems, you must configure a {link}." + id="xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription" + values={{ + link: ( + <EuiLink href={getConfigureCasesUrl(urlSearch)} target="_blank"> + {i18n.LINK_CONNECTOR_CONFIGURE} + </EuiLink> + ), + }} + /> + ), + }, + ]; + } + if (caseStatus === 'closed') { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE, + description: ( + <FormattedMessage + defaultMessage="Closed cases cannot be sent to external systems. Reopen the case if you want to open or update it in an external system." + id="xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedDescription" + /> + ), + }, + ]; + } + if (actionLicense != null && !actionLicense.enabledInConfig) { + errors = [...errors, getKibanaConfigError()]; + } + return errors; + }, [actionLicense, caseStatus, connectorId, loadingCaseConfigure, loadingLicense, urlSearch]); + + const pushToServiceButton = useMemo( + () => ( + <EuiButton + data-test-subj="push-to-service-now" + fill + iconType="importAction" + onClick={handlePushToService} + disabled={ + isLoading || + loadingLicense || + loadingCaseConfigure || + errorsMsg.length > 0 || + !userCanCrud + } + isLoading={isLoading} + > + {isNew ? i18n.PUSH_SERVICENOW : i18n.UPDATE_PUSH_SERVICENOW} + </EuiButton> + ), + [ + isNew, + handlePushToService, + isLoading, + loadingLicense, + loadingCaseConfigure, + errorsMsg, + userCanCrud, + ] + ); + + const objToReturn = useMemo( + () => ({ + pushButton: + errorsMsg.length > 0 ? ( + <EuiToolTip + position="top" + title={errorsMsg[0].title} + content={<p>{errorsMsg[0].description}</p>} + > + {pushToServiceButton} + </EuiToolTip> + ) : ( + <>{pushToServiceButton}</> + ), + pushCallouts: + errorsMsg.length > 0 ? ( + <CaseCallOut title={i18n.ERROR_PUSH_SERVICE_CALLOUT_TITLE} messages={errorsMsg} /> + ) : null, + }), + [errorsMsg, pushToServiceButton] + ); + return objToReturn; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx new file mode 100644 index 0000000000000..96f87c9082945 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui'; +import React from 'react'; + +import { CaseFullExternalService } from '../../../../../../case/common/api'; +import { CaseUserActions } from '../../../../containers/case/types'; +import * as i18n from '../case_view/translations'; + +interface LabelTitle { + action: CaseUserActions; + field: string; + firstIndexPushToService: number; + index: number; +} + +export const getLabelTitle = ({ action, field, firstIndexPushToService, index }: LabelTitle) => { + if (field === 'tags') { + return getTagsLabelTitle(action); + } else if (field === 'title' && action.action === 'update') { + return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ + action.newValue + }"`; + } else if (field === 'description' && action.action === 'update') { + return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; + } else if (field === 'status' && action.action === 'update') { + return `${ + action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase() + } ${i18n.CASE}`; + } else if (field === 'comment' && action.action === 'update') { + return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; + } else if (field === 'pushed' && action.action === 'push-to-service' && action.newValue != null) { + return getPushedServiceLabelTitle(action, firstIndexPushToService, index); + } + return ''; +}; + +const getTagsLabelTitle = (action: CaseUserActions) => ( + <EuiFlexGroup alignItems="baseline" gutterSize="xs" component="span"> + <EuiFlexItem data-test-subj="ua-tags-label"> + {action.action === 'add' && i18n.ADDED_FIELD} + {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} + </EuiFlexItem> + {action.newValue != null && + action.newValue.split(',').map(tag => ( + <EuiFlexItem grow={false} key={tag}> + <EuiBadge data-test-subj={`ua-tag`} color="default"> + {tag} + </EuiBadge> + </EuiFlexItem> + ))} + </EuiFlexGroup> +); + +const getPushedServiceLabelTitle = ( + action: CaseUserActions, + firstIndexPushToService: number, + index: number +) => { + const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; + return ( + <EuiFlexGroup alignItems="baseline" gutterSize="xs" data-test-subj="pushed-service-label-title"> + <EuiFlexItem data-test-subj="pushed-label"> + {firstIndexPushToService === index ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiLink data-test-subj="pushed-value" href={pushedVal?.external_url} target="_blank"> + {pushedVal?.connector_name} {pushedVal?.external_title} + </EuiLink> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx new file mode 100644 index 0000000000000..ff402e8ea1c8b --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx @@ -0,0 +1,327 @@ +/* + * 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 { mount } from 'enzyme'; + +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { getFormMock, useFormMock } from '../__mock__/form'; +import { useUpdateComment } from '../../../../containers/case/use_update_comment'; +import { basicCase, getUserAction } from '../../../../containers/case/mock'; +import { UserActionTree } from './'; +import { TestProviders } from '../../../../mock'; +import { wait } from '../../../../lib/helpers'; +import { act } from 'react-dom/test-utils'; + +const fetchUserActions = jest.fn(); +const onUpdateField = jest.fn(); +const updateCase = jest.fn(); +const defaultProps = { + data: basicCase, + caseUserActions: [], + firstIndexPushToService: -1, + isLoadingDescription: false, + isLoadingUserActions: false, + lastIndexPushToService: -1, + userCanCrud: true, + fetchUserActions, + onUpdateField, + updateCase, +}; +const useUpdateCommentMock = useUpdateComment as jest.Mock; +jest.mock('../../../../containers/case/use_update_comment'); + +const patchComment = jest.fn(); +describe('UserActionTree ', () => { + const sampleData = { + content: 'what a great comment update', + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + useUpdateCommentMock.mockImplementation(() => ({ + isLoadingIds: [], + patchComment, + })); + const formHookMock = getFormMock(sampleData); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('Loading spinner when user actions loading and displays fullName/username', () => { + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...{ ...defaultProps, isLoadingUserActions: true }} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="user-actions-loading"]`).exists()).toBeTruthy(); + + expect( + wrapper + .find(`[data-test-subj="user-action-avatar"]`) + .first() + .prop('name') + ).toEqual(defaultProps.data.createdBy.fullName); + expect( + wrapper + .find(`[data-test-subj="user-action-title"] strong`) + .first() + .text() + ).toEqual(defaultProps.data.createdBy.username); + }); + it('Renders service now update line with top and bottom when push is required', () => { + const ourActions = [ + getUserAction(['comment'], 'push-to-service'), + getUserAction(['comment'], 'update'), + ]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + lastIndexPushToService: 0, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy(); + }); + it('Renders service now update line with top only when push is up to date', () => { + const ourActions = [getUserAction(['comment'], 'push-to-service')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + lastIndexPushToService: 0, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy(); + }); + + it('Outlines comment when update move to link is clicked', () => { + const ourActions = [getUserAction(['comment'], 'create'), getUserAction(['comment'], 'update')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(''); + wrapper + .find(`[data-test-subj="comment-update-action"] [data-test-subj="move-to-link"]`) + .first() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(ourActions[0].commentId); + }); + + it('Switches to markdown when edit is clicked and back to panel when canceled', () => { + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(true); + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]` + ) + .first() + .simulate('click'); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + }); + + it('calls update comment when comment markdown is saved', async () => { + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + await act(async () => { + await wait(); + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: sampleData.content, + caseId: props.data.id, + commentId: props.data.comments[0].id, + fetchUserActions, + updateCase, + version: props.data.comments[0].version, + }); + }); + }); + + it('calls update description when description markdown is saved', async () => { + const props = defaultProps; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + wrapper + .find( + `[data-test-subj="user-action-description"] [data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + await act(async () => { + await wait(); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(onUpdateField).toBeCalledWith('description', sampleData.content); + }); + }); + + it('quotes', async () => { + const commentData = { + comment: '', + }; + const formHookMock = getFormMock(commentData); + const setFieldValue = jest.fn(); + useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } })); + const props = defaultProps; + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) + .first() + .simulate('click'); + expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); + }); + it('Outlines comment when url param is provided', () => { + const commentId = 'neat-comment-id'; + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId }); + const wrapper = mount( + <TestProviders> + <Router history={mockHistory}> + <UserActionTree {...props} /> + </Router> + </TestProviders> + ); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(commentId); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/schema.ts b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/schema.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/schema.ts rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/schema.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_list/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_list/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/plugins/siem/public/pages/case/components/user_list/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/user_list/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/translations.ts b/x-pack/plugins/siem/public/pages/case/components/user_list/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/user_list/translations.ts rename to x-pack/plugins/siem/public/pages/case/components/user_list/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx b/x-pack/plugins/siem/public/pages/case/components/wrappers/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/components/wrappers/index.tsx rename to x-pack/plugins/siem/public/pages/case/components/wrappers/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx b/x-pack/plugins/siem/public/pages/case/configure_cases.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx rename to x-pack/plugins/siem/public/pages/case/configure_cases.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/plugins/siem/public/pages/case/create_case.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx rename to x-pack/plugins/siem/public/pages/case/create_case.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/index.tsx b/x-pack/plugins/siem/public/pages/case/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/index.tsx rename to x-pack/plugins/siem/public/pages/case/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx b/x-pack/plugins/siem/public/pages/case/saved_object_no_permissions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx rename to x-pack/plugins/siem/public/pages/case/saved_object_no_permissions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/plugins/siem/public/pages/case/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/case/translations.ts rename to x-pack/plugins/siem/public/pages/case/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/utils.ts b/x-pack/plugins/siem/public/pages/case/utils.ts new file mode 100644 index 0000000000000..f1aea747485e4 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/case/utils.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from 'src/core/public'; + +import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; +import { RouteSpyState } from '../../utils/route/types'; +import * as i18n from './translations'; + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { + const queryParameters = !isEmpty(search[0]) ? search[0] : null; + + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getCaseUrl(queryParameters), + }, + ]; + if (params.detailName === 'create') { + breadcrumb = [ + ...breadcrumb, + { + text: i18n.CREATE_BC_TITLE, + href: getCreateCaseUrl(queryParameters), + }, + ]; + } else if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state?.caseTitle ?? '', + href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/plugins/siem/public/pages/common/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/common/translations.ts rename to x-pack/plugins/siem/public/pages/common/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx index 6212cad7e1845..5428b9932fbde 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { Filter } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; import { TimelineAction } from '../../../../components/timeline/body/actions'; import { buildSignalsRuleIdFilter, getSignalsActions } from './default_config'; import { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index fd3b9a6f68e82..81b643b7894df 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -10,7 +10,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import ApolloClient from 'apollo-client'; import React from 'react'; -import { Filter } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; import { TimelineAction, TimelineActionProps } from '../../../../components/timeline/body/actions'; import { defaultColumnHeaderType } from '../../../../components/timeline/body/column_headers/default_headers'; import { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts new file mode 100644 index 0000000000000..a948d2b940b0c --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts @@ -0,0 +1,273 @@ +/* + * 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 { + getStringArray, + replaceTemplateFieldFromQuery, + replaceTemplateFieldFromMatchFilters, + reformatDataProviderWithNewValue, +} from './helpers'; +import { mockEcsData } from '../../../../mock/mock_ecs'; +import { Filter } from '../../../../../../../../src/plugins/data/public'; +import { DataProvider } from '../../../../components/timeline/data_providers/data_provider'; +import { mockDataProviders } from '../../../../components/timeline/data_providers/mock/mock_data_providers'; +import { cloneDeep } from 'lodash/fp'; + +describe('helpers', () => { + let mockEcsDataClone = cloneDeep(mockEcsData); + beforeEach(() => { + mockEcsDataClone = cloneDeep(mockEcsData); + }); + describe('getStringOrStringArray', () => { + test('it should correctly return a string array', () => { + const value = getStringArray('x', { + x: 'The nickname of the developer we all :heart:', + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with a single element', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:'], + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with two elements of strings', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + }); + expect(value).toEqual([ + 'The nickname of the developer we all :heart:', + 'We are all made of stars', + ]); + }); + + test('it should correctly return a string array with deep elements', () => { + const value = getStringArray('x.y.z', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual(['zed']); + }); + + test('it should correctly return a string array with a non-existent value', () => { + const value = getStringArray('non.existent', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual([]); + }); + + test('it should trace an error if the value is not a string', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: 5 }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + 5, + 'when trying to access field:', + 'a', + 'from data object of:', + { a: 5 } + ); + }); + + test('it should trace an error if the value is an array of mixed values', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + ['hi', 5], + 'when trying to access field:', + 'a', + 'from data object of:', + { a: ['hi', 5] } + ); + }); + }); + + describe('replaceTemplateFieldFromQuery', () => { + test('given an empty query string this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('given a query string with spaces this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('it should replace a query with a template value such as apache from a mock template', () => { + const replacement = replaceTemplateFieldFromQuery( + 'host.name: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('host.name: apache'); + }); + + test('it should replace a template field with an ECS value that is not an array', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); + expect(replacement).toEqual('host.name: *'); + }); + + test('it should NOT replace a query with a template value that is not part of the template fields array', () => { + const replacement = replaceTemplateFieldFromQuery( + 'user.id: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('user.id: placeholdertext'); + }); + }); + + describe('replaceTemplateFieldFromMatchFilters', () => { + test('given an empty query filter this will return an empty filter', () => { + const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); + expect(replacement).toEqual([]); + }); + + test('given a query filter this will return that filter with the placeholder replaced', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Braden' }, + }, + query: { match_phrase: { 'host.name': 'Braden' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'apache' }, + }, + query: { match_phrase: { 'host.name': 'apache' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + + test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + }); + + describe('reformatDataProviderWithNewValue', () => { + test('it should replace a query with a template value such as apache from a mock data provider', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'user.id'; + mockDataProvider.id = 'my-id'; + mockDataProvider.name = 'Rebecca'; + mockDataProvider.queryMatch.value = 'Rebecca'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'my-id', + name: 'Rebecca', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.id', + value: 'Rebecca', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts new file mode 100644 index 0000000000000..3fa2da37046b0 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts @@ -0,0 +1,165 @@ +/* + * 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 { get, isEmpty } from 'lodash/fp'; +import { Filter, esKuery, KueryNode } from '../../../../../../../../src/plugins/data/public'; +import { + DataProvider, + DataProvidersAnd, +} from '../../../../components/timeline/data_providers/data_provider'; +import { Ecs } from '../../../../graphql/types'; + +interface FindValueToChangeInQuery { + field: string; + valueToChange: string; +} + +/** + * Fields that will be replaced with the template strings from a a saved timeline template. + * This is used for the signals detection engine feature when you save a timeline template + * and are the fields you can replace when creating a template. + */ +const templateFields = [ + 'host.name', + 'host.hostname', + 'host.domain', + 'host.id', + 'host.ip', + 'client.ip', + 'destination.ip', + 'server.ip', + 'source.ip', + 'network.community_id', + 'user.name', + 'process.name', +]; + +/** + * This will return an unknown as a string array if it exists from an unknown data type and a string + * that represents the path within the data object the same as lodash's "get". If the value is non-existent + * we will return an empty array. If it is a non string value then this will log a trace to the console + * that it encountered an error and return an empty array. + * @param field string of the field to access + * @param data The unknown data that is typically a ECS value to get the value + * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console + */ +export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { + const value: unknown | undefined = get(field, data); + if (value == null) { + return []; + } else if (typeof value === 'string') { + return [value]; + } else if (Array.isArray(value) && value.every(element => typeof element === 'string')) { + return value; + } else { + localConsole.trace( + 'Data type that is not a string or string array detected:', + value, + 'when trying to access field:', + field, + 'from data object of:', + data + ); + return []; + } +}; + +export const findValueToChangeInQuery = ( + kueryNode: KueryNode, + valueToChange: FindValueToChangeInQuery[] = [] +): FindValueToChangeInQuery[] => { + let localValueToChange = valueToChange; + if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { + localValueToChange = [ + ...localValueToChange, + { + field: kueryNode.arguments[0].value, + valueToChange: kueryNode.arguments[1].value, + }, + ]; + } + return kueryNode.arguments.reduce( + (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { + if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { + return [ + ...addValueToChange, + { + field: ast.arguments[0].value, + valueToChange: ast.arguments[1].value, + }, + ]; + } + if (ast.arguments) { + return findValueToChangeInQuery(ast, addValueToChange); + } + return addValueToChange; + }, + localValueToChange + ); +}; + +export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { + if (query.trim() !== '') { + const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); + return valueToChange.reduce((newQuery, vtc) => { + const newValue = getStringArray(vtc.field, ecsData); + if (newValue.length) { + return newQuery.replace(vtc.valueToChange, newValue[0]); + } else { + return newQuery; + } + }, query); + } else { + return ''; + } +}; + +export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => + filters.map(filter => { + if ( + filter.meta.type === 'phrase' && + filter.meta.key != null && + templateFields.includes(filter.meta.key) + ) { + const newValue = getStringArray(filter.meta.key, ecsData); + if (newValue.length) { + filter.meta.params = { query: newValue[0] }; + filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; + } + } + return filter; + }); + +export const reformatDataProviderWithNewValue = <T extends DataProvider | DataProvidersAnd>( + dataProvider: T, + ecsData: Ecs +): T => { + if (templateFields.includes(dataProvider.queryMatch.field)) { + const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); + if (newValue.length) { + dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); + dataProvider.name = newValue[0]; + dataProvider.queryMatch.value = newValue[0]; + dataProvider.queryMatch.displayField = undefined; + dataProvider.queryMatch.displayValue = undefined; + } + } + return dataProvider; +}; + +export const replaceTemplateFieldFromDataProviders = ( + dataProviders: DataProvider[], + ecsData: Ecs +): DataProvider[] => + dataProviders.map(dataProvider => { + const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); + if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { + newDataProvider.and = newDataProvider.and.map(andDataProvider => + reformatDataProviderWithNewValue(andDataProvider, ecsData) + ); + } + return newDataProvider; + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx new file mode 100644 index 0000000000000..5442c8c19b5a7 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -0,0 +1,369 @@ +/* + * 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 { EuiPanel, EuiLoadingContent } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { Filter, esQuery } from '../../../../../../../../src/plugins/data/public'; +import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; +import { StatefulEventsViewer } from '../../../../components/events_viewer'; +import { HeaderSection } from '../../../../components/header_section'; +import { combineQueries } from '../../../../components/timeline/helpers'; +import { useKibana } from '../../../../lib/kibana'; +import { inputsSelectors, State, inputsModel } from '../../../../store'; +import { timelineActions, timelineSelectors } from '../../../../store/timeline'; +import { TimelineModel } from '../../../../store/timeline/model'; +import { timelineDefaults } from '../../../../store/timeline/defaults'; +import { useApolloClient } from '../../../../utils/apollo_context'; + +import { updateSignalStatusAction } from './actions'; +import { + getSignalsActions, + requiredFieldsForActions, + signalsClosedFilters, + signalsDefaultModel, + signalsOpenFilters, +} from './default_config'; +import { + FILTER_CLOSED, + FILTER_OPEN, + SignalFilterOption, + SignalsTableFilterGroup, +} from './signals_filter_group'; +import { SignalsUtilityBar } from './signals_utility_bar'; +import * as i18n from './translations'; +import { + CreateTimelineProps, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateSignalsStatusCallback, + UpdateSignalsStatusProps, +} from './types'; +import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers'; + +export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; + +interface OwnProps { + canUserCRUD: boolean; + defaultFilters?: Filter[]; + hasIndexWrite: boolean; + from: number; + loading: boolean; + signalsIndex: string; + to: number; +} + +type SignalsTableComponentProps = OwnProps & PropsFromRedux; + +export const SignalsTableComponent: React.FC<SignalsTableComponentProps> = ({ + canUserCRUD, + clearEventsDeleted, + clearEventsLoading, + clearSelected, + defaultFilters, + from, + globalFilters, + globalQuery, + hasIndexWrite, + isSelectAllChecked, + loading, + loadingEventIds, + selectedEventIds, + setEventsDeleted, + setEventsLoading, + signalsIndex, + to, + updateTimeline, + updateTimelineIsLoading, +}) => { + const [selectAll, setSelectAll] = useState(false); + const apolloClient = useApolloClient(); + + const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); + const [filterGroup, setFilterGroup] = useState<SignalFilterOption>(FILTER_OPEN); + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + signalsIndex !== '' ? [signalsIndex] : [] + ); + const kibana = useKibana(); + + const getGlobalQuery = useCallback(() => { + if (browserFields != null && indexPatterns != null) { + return combineQueries({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders: [], + indexPattern: indexPatterns, + browserFields, + filters: isEmpty(defaultFilters) + ? globalFilters + : [...(defaultFilters ?? []), ...globalFilters], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + start: from, + end: to, + isEventViewer: true, + }); + } + return null; + }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); + + // Callback for creating a new timeline -- utilized by row/batch actions + const createTimelineCallback = useCallback( + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimeline({ + duplicate: true, + from: fromTimeline, + id: 'timeline-1', + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [updateTimeline, updateTimelineIsLoading] + ); + + const setEventsLoadingCallback = useCallback( + ({ eventIds, isLoading }: SetEventsLoadingProps) => { + setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading }); + }, + [setEventsLoading, SIGNALS_PAGE_TIMELINE_ID] + ); + + const setEventsDeletedCallback = useCallback( + ({ eventIds, isDeleted }: SetEventsDeletedProps) => { + setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted }); + }, + [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] + ); + + // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar + useEffect(() => { + if (!isSelectAllChecked) { + setShowClearSelectionAction(false); + } else { + setSelectAll(false); + } + }, [isSelectAllChecked]); + + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: SignalFilterOption) => { + clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID }); + clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID }); + clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + + // Callback for clearing entire selection from utility bar + const clearSelectionCallback = useCallback(() => { + clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + setSelectAll(false); + setShowClearSelectionAction(false); + }, [clearSelected, setSelectAll, setShowClearSelectionAction]); + + // Callback for selecting all events on all pages from utility bar + // Dispatches to stateful_body's selectAll via TimelineTypeContext props + // as scope of response data required to actually set selectedEvents + const selectAllCallback = useCallback(() => { + setSelectAll(true); + setShowClearSelectionAction(true); + }, [setSelectAll, setShowClearSelectionAction]); + + const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback( + async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => { + await updateSignalStatusAction({ + query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, + signalIds: Object.keys(selectedEventIds), + status, + setEventsDeleted: setEventsDeletedCallback, + setEventsLoading: setEventsLoadingCallback, + }); + refetchQuery(); + }, + [ + getGlobalQuery, + selectedEventIds, + setEventsDeletedCallback, + setEventsLoadingCallback, + showClearSelectionAction, + ] + ); + + // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component + const utilityBarCallback = useCallback( + (refetchQuery: inputsModel.Refetch, totalCount: number) => { + return ( + <SignalsUtilityBar + canUserCRUD={canUserCRUD} + areEventsLoading={loadingEventIds.length > 0} + clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} + isFilteredToOpen={filterGroup === FILTER_OPEN} + selectAll={selectAllCallback} + selectedEventIds={selectedEventIds} + showClearSelection={showClearSelectionAction} + totalCount={totalCount} + updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} + /> + ); + }, + [ + canUserCRUD, + hasIndexWrite, + clearSelectionCallback, + filterGroup, + loadingEventIds.length, + selectAllCallback, + selectedEventIds, + showClearSelectionAction, + updateSignalsStatusCallback, + ] + ); + + // Send to Timeline / Update Signal Status Actions for each table row + const additionalActions = useMemo( + () => + getSignalsActions({ + apolloClient, + canUserCRUD, + hasIndexWrite, + createTimeline: createTimelineCallback, + setEventsLoading: setEventsLoadingCallback, + setEventsDeleted: setEventsDeletedCallback, + status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, + updateTimelineIsLoading, + }), + [ + apolloClient, + canUserCRUD, + createTimelineCallback, + hasIndexWrite, + filterGroup, + setEventsLoadingCallback, + setEventsDeletedCallback, + updateTimelineIsLoading, + ] + ); + + const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); + const defaultFiltersMemo = useMemo(() => { + if (isEmpty(defaultFilters)) { + return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters; + } else if (defaultFilters != null && !isEmpty(defaultFilters)) { + return [ + ...defaultFilters, + ...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters), + ]; + } + }, [defaultFilters, filterGroup]); + + const timelineTypeContext = useMemo( + () => ({ + documentType: i18n.SIGNALS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_SIGNALS, + loadingText: i18n.LOADING_SIGNALS, + queryFields: requiredFieldsForActions, + timelineActions: additionalActions, + title: i18n.SIGNALS_TABLE_TITLE, + selectAll: canUserCRUD ? selectAll : false, + }), + [additionalActions, canUserCRUD, selectAll] + ); + + const headerFilterGroup = useMemo( + () => <SignalsTableFilterGroup onFilterGroupChanged={onFilterGroupChangedCallback} />, + [onFilterGroupChangedCallback] + ); + + if (loading || isEmpty(signalsIndex)) { + return ( + <EuiPanel> + <HeaderSection title={i18n.SIGNALS_TABLE_TITLE} /> + <EuiLoadingContent data-test-subj="loading-signals-panel" /> + </EuiPanel> + ); + } + + return ( + <StatefulEventsViewer + defaultIndices={defaultIndices} + pageFilters={defaultFiltersMemo} + defaultModel={signalsDefaultModel} + end={to} + headerFilterGroup={headerFilterGroup} + id={SIGNALS_PAGE_TIMELINE_ID} + start={from} + timelineTypeContext={timelineTypeContext} + utilityBar={utilityBarCallback} + /> + ); +}; + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getGlobalInputs = inputsSelectors.globalSelector(); + const mapStateToProps = (state: State) => { + const timeline: TimelineModel = + getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults; + const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; + + const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + return { + globalQuery: query, + globalFilters: filters, + deletedEventIds, + isSelectAllChecked, + loadingEventIds, + selectedEventIds, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + setEventsLoading: ({ + id, + eventIds, + isLoading, + }: { + id: string; + eventIds: string[]; + isLoading: boolean; + }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + setEventsDeleted: ({ + id, + eventIds, + isDeleted, + }: { + id: string; + eventIds: string[]; + isDeleted: boolean; + }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(timelineActions.updateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const SignalsTable = connector(React.memo(SignalsTableComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx new file mode 100644 index 0000000000000..b9268716f85f0 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx @@ -0,0 +1,125 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../components/utility_bar'; +import * as i18n from './translations'; +import { useUiSetting$ } from '../../../../../lib/kibana'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { UpdateSignalsStatus } from '../types'; +import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; + +interface SignalsUtilityBarProps { + canUserCRUD: boolean; + hasIndexWrite: boolean; + areEventsLoading: boolean; + clearSelection: () => void; + isFilteredToOpen: boolean; + selectAll: () => void; + selectedEventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + showClearSelection: boolean; + totalCount: number; + updateSignalsStatus: UpdateSignalsStatus; +} + +const SignalsUtilityBarComponent: React.FC<SignalsUtilityBarProps> = ({ + canUserCRUD, + hasIndexWrite, + areEventsLoading, + clearSelection, + totalCount, + selectedEventIds, + isFilteredToOpen, + selectAll, + showClearSelection, + updateSignalsStatus, +}) => { + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + + const handleUpdateStatus = useCallback(async () => { + await updateSignalsStatus({ + signalIds: Object.keys(selectedEventIds), + status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, + }); + }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); + + const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); + const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( + defaultNumberFormat + ); + + return ( + <> + <UtilityBar> + <UtilityBarSection> + <UtilityBarGroup> + <UtilityBarText dataTestSubj="showingSignals"> + {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} + </UtilityBarText> + </UtilityBarGroup> + + <UtilityBarGroup> + {canUserCRUD && hasIndexWrite && ( + <> + <UtilityBarText dataTestSubj="selectedSignals"> + {i18n.SELECTED_SIGNALS( + showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, + showClearSelection ? totalCount : Object.keys(selectedEventIds).length + )} + </UtilityBarText> + + <UtilityBarAction + dataTestSubj="openCloseSignal" + disabled={areEventsLoading || isEmpty(selectedEventIds)} + iconType={isFilteredToOpen ? 'securitySignalResolved' : 'securitySignalDetected'} + onClick={handleUpdateStatus} + > + {isFilteredToOpen + ? i18n.BATCH_ACTION_CLOSE_SELECTED + : i18n.BATCH_ACTION_OPEN_SELECTED} + </UtilityBarAction> + + <UtilityBarAction + iconType={showClearSelection ? 'cross' : 'pagesSelect'} + onClick={() => { + if (!showClearSelection) { + selectAll(); + } else { + clearSelection(); + } + }} + > + {showClearSelection + ? i18n.CLEAR_SELECTION + : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} + </UtilityBarAction> + </> + )} + </UtilityBarGroup> + </UtilityBarSection> + </UtilityBar> + </> + ); +}; + +export const SignalsUtilityBar = React.memo( + SignalsUtilityBarComponent, + (prevProps, nextProps) => + prevProps.areEventsLoading === nextProps.areEventsLoading && + prevProps.selectedEventIds === nextProps.selectedEventIds && + prevProps.totalCount === nextProps.totalCount && + prevProps.showClearSelection === nextProps.showClearSelection +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx new file mode 100644 index 0000000000000..24b12cae62d85 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.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 { showAllOthersBucket } from '../../../../../common/constants'; +import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; +import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types'; +import * as i18n from './translations'; + +export const formatSignalsData = ( + signalsData: SignalSearchResponse<{}, SignalsAggregation> | null +) => { + const groupBuckets: SignalsGroupBucket[] = + signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; + return groupBuckets.reduce<HistogramData[]>((acc, { key: group, signals }) => { + const signalsBucket: SignalsBucket[] = signals.buckets ?? []; + + return [ + ...acc, + ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ + x: key, + y: doc_count, + g: group, + })), + ]; + }, []); +}; + +export const getSignalsHistogramQuery = ( + stackByField: string, + from: number, + to: number, + additionalFilters: Array<{ + bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; + }> +) => { + const missing = showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + } + : {}; + + return { + aggs: { + signalsByGrouping: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + signals: { + date_histogram: { + field: '@timestamp', + fixed_interval: `${Math.floor((to - from) / 32)}ms`, + min_doc_count: 0, + extended_bounds: { + min: from, + max: to, + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + }, + }, + }; +}; + +/** + * Returns `true` when the signals histogram initial loading spinner should be shown + * + * @param isInitialLoading The loading spinner will only be displayed if this value is `true`, because after initial load, a different, non-spinner loading indicator is displayed + * @param isLoadingSignals When `true`, IO is being performed to request signals (for rendering in the histogram) + */ +export const showInitialLoadingSpinner = ({ + isInitialLoading, + isLoadingSignals, +}: { + isInitialLoading: boolean; + isLoadingSignals: boolean; +}): boolean => isInitialLoading && isLoadingSignals; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx new file mode 100644 index 0000000000000..cf6730ea3ec3d --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx @@ -0,0 +1,283 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash/fp'; +import uuid from 'uuid'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { LegendItem } from '../../../../components/charts/draggable_legend_item'; +import { escapeDataProviderId } from '../../../../components/drag_and_drop/helpers'; +import { HeaderSection } from '../../../../components/header_section'; +import { Filter, esQuery, Query } from '../../../../../../../../src/plugins/data/public'; +import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; +import { getDetectionEngineUrl } from '../../../../components/link_to'; +import { defaultLegendColors } from '../../../../components/matrix_histogram/utils'; +import { InspectButtonContainer } from '../../../../components/inspect'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; +import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; +import { MatrixHistogramOption } from '../../../../components/matrix_histogram/types'; +import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; +import { navTabs } from '../../../home/home_navigations'; +import { signalsHistogramOptions } from './config'; +import { formatSignalsData, getSignalsHistogramQuery, showInitialLoadingSpinner } from './helpers'; +import { SignalsHistogram } from './signals_histogram'; +import * as i18n from './translations'; +import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; + +const DEFAULT_PANEL_HEIGHT = 300; + +const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` + display: flex; + flex-direction: column; + ${({ height }) => (height != null ? `height: ${height}px;` : '')} + position: relative; +`; + +const defaultTotalSignalsObj: SignalsTotal = { + value: 0, + relation: 'eq', +}; + +export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; + +const ViewSignalsFlexItem = styled(EuiFlexItem)` + margin-left: 24px; +`; + +interface SignalsHistogramPanelProps { + chartHeight?: number; + defaultStackByOption?: SignalsHistogramOption; + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + /** Override all defaults, and only display this field */ + onlyField?: string; + query?: Query; + legendPosition?: Position; + panelHeight?: number; + signalIndexName: string | null; + setQuery: (params: RegisterQuery) => void; + showLinkToSignals?: boolean; + showTotalSignalsCount?: boolean; + stackByOptions?: SignalsHistogramOption[]; + title?: string; + to: number; + updateDateRange: (min: number, max: number) => void; +} + +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const NO_LEGEND_DATA: LegendItem[] = []; + +export const SignalsHistogramPanel = memo<SignalsHistogramPanelProps>( + ({ + chartHeight, + defaultStackByOption = signalsHistogramOptions[0], + deleteQuery, + filters, + headerChildren, + onlyField, + query, + from, + legendPosition = 'right', + panelHeight = DEFAULT_PANEL_HEIGHT, + setQuery, + signalIndexName, + showLinkToSignals = false, + showTotalSignalsCount = false, + stackByOptions, + to, + title = i18n.HISTOGRAM_HEADER, + updateDateRange, + }) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const [totalSignalsObj, setTotalSignalsObj] = useState<SignalsTotal>(defaultTotalSignalsObj); + const [selectedStackByOption, setSelectedStackByOption] = useState<SignalsHistogramOption>( + onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) + ); + const { + loading: isLoadingSignals, + data: signalsData, + setQuery: setSignalsQuery, + response, + request, + refetch, + } = useQuerySignals<{}, SignalsAggregation>( + getSignalsHistogramQuery(selectedStackByOption.value, from, to, []), + signalIndexName + ); + const kibana = useKibana(); + const urlSearch = useGetUrlSearch(navTabs.detections); + + const totalSignals = useMemo( + () => + i18n.SHOWING_SIGNALS( + numeral(totalSignalsObj.value).format(defaultNumberFormat), + totalSignalsObj.value, + totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' + ), + [totalSignalsObj] + ); + + const setSelectedOptionCallback = useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { + setSelectedStackByOption( + stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption + ); + }, []); + + const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); + + const legendItems: LegendItem[] = useMemo( + () => + signalsData?.aggregations?.signalsByGrouping?.buckets != null + ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ + color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + dataProviderId: escapeDataProviderId( + `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` + ), + field: selectedStackByOption.value, + value: bucket.key, + })) + : NO_LEGEND_DATA, + [signalsData, selectedStackByOption.value] + ); + + useEffect(() => { + let canceled = false; + + if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingSignals })) { + setIsInitialLoading(false); + } + + return () => { + canceled = true; // prevent long running data fetches from updating state after unmounting + }; + }, [isInitialLoading, isLoadingSignals, setIsInitialLoading]); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, []); + + useEffect(() => { + if (refetch != null && setQuery != null) { + setQuery({ + id: uniqueQueryId, + inspect: { + dsl: [request], + response: [response], + }, + loading: isLoadingSignals, + refetch, + }); + } + }, [setQuery, isLoadingSignals, signalsData, response, request, refetch]); + + useEffect(() => { + setTotalSignalsObj( + signalsData?.hits.total ?? { + value: 0, + relation: 'eq', + } + ); + }, [signalsData]); + + useEffect(() => { + const converted = esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter(f => f.meta.disabled === false) ?? [], + { + ...esQuery.getEsQueryConfig(kibana.services.uiSettings), + dateFormatTZ: undefined, + } + ); + + setSignalsQuery( + getSignalsHistogramQuery( + selectedStackByOption.value, + from, + to, + !isEmpty(converted) ? [converted] : [] + ) + ); + }, [selectedStackByOption.value, from, to, query, filters]); + + const linkButton = useMemo(() => { + if (showLinkToSignals) { + return ( + <ViewSignalsFlexItem grow={false}> + <EuiButton href={getDetectionEngineUrl(urlSearch)}>{i18n.VIEW_SIGNALS}</EuiButton> + </ViewSignalsFlexItem> + ); + } + }, [showLinkToSignals, urlSearch]); + + const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ + onlyField, + title, + ]); + + return ( + <InspectButtonContainer data-test-subj="signals-histogram-panel" show={!isInitialLoading}> + <StyledEuiPanel height={panelHeight}> + <HeaderSection + id={uniqueQueryId} + title={titleText} + titleSize={onlyField == null ? 'm' : 's'} + subtitle={!isInitialLoading && showTotalSignalsCount && totalSignals} + > + <EuiFlexGroup alignItems="center" gutterSize="none"> + <EuiFlexItem grow={false}> + {stackByOptions && ( + <EuiSelect + onChange={setSelectedOptionCallback} + options={stackByOptions} + prepend={i18n.STACK_BY_LABEL} + value={selectedStackByOption.value} + /> + )} + {headerChildren != null && headerChildren} + </EuiFlexItem> + {linkButton} + </EuiFlexGroup> + </HeaderSection> + + {isInitialLoading ? ( + <MatrixLoader /> + ) : ( + <SignalsHistogram + chartHeight={chartHeight} + data={formattedSignalsData} + from={from} + legendItems={legendItems} + legendPosition={legendPosition} + loading={isLoadingSignals} + to={to} + updateDateRange={updateDateRange} + /> + )} + </StyledEuiPanel> + </InspectButtonContainer> + ); + } +); + +SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts rename to x-pack/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/mitre/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/mitre/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/mitre/types.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts new file mode 100644 index 0000000000000..66964fae70f94 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { esFilters } from '../../../../../../../../../src/plugins/data/public'; +import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; +import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; +import { FieldValueQueryBar } from '../../components/query_bar'; + +export const mockQueryBar: FieldValueQueryBar = { + query: { + query: 'test query', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +export const mockRule = (id: string): Rule => ({ + actions: [], + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: [], + filters: [], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Home Grown!', + query: '', + references: [], + saved_id: "Garrett's IP", + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Untitled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'saved_query', + threat: [], + throttle: 'no_actions', + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockRuleWithEverything = (id: string): Rule => ({ + actions: [], + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: ['test'], + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Query with rule-id', + query: 'user.name: root or user.name: admin', + references: ['www.test.co'], + saved_id: 'test123', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: ['tag1', 'tag2'], + to: 'now', + type: 'saved_query', + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + throttle: 'no_actions', + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ + isNew, + name: 'Query with rule-id', + description: '24/7', + severity: 'low', + riskScore: 21, + references: ['www.test.co'], + falsePositives: ['test'], + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + note: '# this is some markdown documentation', +}); + +export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({ + isNew, + actions: [], + kibanaSiemAppUrl: 'http://localhost:5601/app/siem', + enabled, + throttle: 'no_actions', +}); + +export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ + isNew, + ruleType: 'query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['filebeat-'], + queryBar: mockQueryBar, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, +}); + +export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ + isNew, + interval: '5m', + from: '6m', + to: 'now', +}); + +export const mockRuleError = (id: string): RuleError => ({ + rule_id: id, + error: { status_code: 404, message: `id: "${id}" not found` }, +}); + +export const mockRules: Rule[] = [ + mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), + mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), +]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx new file mode 100644 index 0000000000000..d383b5cd464ce --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -0,0 +1,332 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import { + EuiBadge, + EuiLink, + EuiBasicTableColumn, + EuiTableActionsColumnType, + EuiText, + EuiHealth, + EuiToolTip, +} from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n/react'; +import * as H from 'history'; +import React, { Dispatch } from 'react'; + +import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; +import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules'; +import { getEmptyTagValue } from '../../../../components/empty_value'; +import { FormattedDate } from '../../../../components/formatted_date'; +import { getRuleDetailsUrl } from '../../../../components/link_to/redirect_to_detection_engine'; +import { ActionToaster } from '../../../../components/toasters'; +import { TruncatableText } from '../../../../components/truncatable_text'; +import { getStatusColor } from '../components/rule_status/helpers'; +import { RuleSwitch } from '../components/rule_switch'; +import { SeverityBadge } from '../components/severity_badge'; +import * as i18n from '../translations'; +import { + deleteRulesAction, + duplicateRulesAction, + editRuleAction, + exportRulesAction, +} from './actions'; +import { Action } from './reducer'; +import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip'; +import * as detectionI18n from '../../translations'; + +export const getActions = ( + dispatch: React.Dispatch<Action>, + dispatchToaster: Dispatch<ActionToaster>, + history: H.History, + reFetchRules: (refreshPrePackagedRule?: boolean) => void +) => [ + { + description: i18n.EDIT_RULE_SETTINGS, + icon: 'controlsHorizontal', + name: i18n.EDIT_RULE_SETTINGS, + onClick: (rowItem: Rule) => editRuleAction(rowItem, history), + }, + { + description: i18n.DUPLICATE_RULE, + icon: 'copy', + name: i18n.DUPLICATE_RULE, + onClick: async (rowItem: Rule) => { + await duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster); + await reFetchRules(true); + }, + }, + { + description: i18n.EXPORT_RULE, + icon: 'exportAction', + name: i18n.EXPORT_RULE, + onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch), + enabled: (rowItem: Rule) => !rowItem.immutable, + }, + { + 'data-test-subj': 'deleteRuleAction', + description: i18n.DELETE_RULE, + icon: 'trash', + name: i18n.DELETE_RULE, + onClick: async (rowItem: Rule) => { + await deleteRulesAction([rowItem.id], dispatch, dispatchToaster); + await reFetchRules(true); + }, + }, +]; + +export type RuleStatusRowItemType = RuleStatus & { + name: string; + id: string; +}; +export type RulesColumns = EuiBasicTableColumn<Rule> | EuiTableActionsColumnType<Rule>; +export type RulesStatusesColumns = EuiBasicTableColumn<RuleStatusRowItemType>; + +interface GetColumns { + dispatch: React.Dispatch<Action>; + dispatchToaster: Dispatch<ActionToaster>; + history: H.History; + hasMlPermissions: boolean; + hasNoPermissions: boolean; + loadingRuleIds: string[]; + reFetchRules: (refreshPrePackagedRule?: boolean) => void; +} + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const getColumns = ({ + dispatch, + dispatchToaster, + history, + hasMlPermissions, + hasNoPermissions, + loadingRuleIds, + reFetchRules, +}: GetColumns): RulesColumns[] => { + const cols: RulesColumns[] = [ + { + field: 'name', + name: i18n.COLUMN_RULE, + render: (value: Rule['name'], item: Rule) => ( + <EuiLink data-test-subj="ruleName" href={getRuleDetailsUrl(item.id)}> + {value} + </EuiLink> + ), + truncateText: true, + width: '24%', + }, + { + field: 'risk_score', + name: i18n.COLUMN_RISK_SCORE, + render: (value: Rule['risk_score']) => ( + <EuiText data-test-subj="riskScore" size="s"> + {value} + </EuiText> + ), + truncateText: true, + width: '14%', + }, + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: Rule['severity']) => <SeverityBadge value={value} />, + truncateText: true, + width: '16%', + }, + { + field: 'status_date', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: Rule['status_date']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <LocalizedDateTooltip fieldName={i18n.COLUMN_LAST_COMPLETE_RUN} date={new Date(value)}> + <FormattedRelative value={value} /> + </LocalizedDateTooltip> + ); + }, + truncateText: true, + width: '20%', + }, + { + field: 'status', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: Rule['status']) => { + return ( + <> + <EuiHealth color={getStatusColor(value ?? null)}> + {value ?? getEmptyTagValue()} + </EuiHealth> + </> + ); + }, + width: '16%', + truncateText: true, + }, + { + field: 'tags', + name: i18n.COLUMN_TAGS, + render: (value: Rule['tags']) => ( + <TruncatableText data-test-subj="tags"> + {value.map((tag, i) => ( + <EuiBadge color="hollow" key={`${tag}-${i}`}> + {tag} + </EuiBadge> + ))} + </TruncatableText> + ), + truncateText: true, + width: '20%', + }, + { + align: 'center', + field: 'enabled', + name: i18n.COLUMN_ACTIVATE, + render: (value: Rule['enabled'], item: Rule) => ( + <EuiToolTip + position="top" + content={ + isMlRule(item.type) && !hasMlPermissions + ? detectionI18n.ML_RULES_DISABLED_MESSAGE + : undefined + } + > + <RuleSwitch + data-test-subj="enabled" + dispatch={dispatch} + id={item.id} + enabled={item.enabled} + isDisabled={ + hasNoPermissions || (isMlRule(item.type) && !hasMlPermissions && !item.enabled) + } + isLoading={loadingRuleIds.includes(item.id)} + /> + </EuiToolTip> + ), + sortable: true, + width: '95px', + }, + ]; + const actions: RulesColumns[] = [ + { + actions: getActions(dispatch, dispatchToaster, history, reFetchRules), + width: '40px', + } as EuiTableActionsColumnType<Rule>, + ]; + + return hasNoPermissions ? cols : [...cols, ...actions]; +}; + +export const getMonitoringColumns = (): RulesStatusesColumns[] => { + const cols: RulesStatusesColumns[] = [ + { + field: 'name', + name: i18n.COLUMN_RULE, + render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => { + return ( + <EuiLink data-test-subj="ruleName" href={getRuleDetailsUrl(item.id)}> + {value} + </EuiLink> + ); + }, + truncateText: true, + width: '24%', + }, + { + field: 'current_status.bulk_create_time_durations', + name: i18n.COLUMN_INDEXING_TIMES, + render: (value: RuleStatus['current_status']['bulk_create_time_durations']) => ( + <EuiText data-test-subj="bulk_create_time_durations" size="s"> + {value != null && value.length > 0 + ? Math.max(...value?.map(item => Number.parseFloat(item))) + : getEmptyTagValue()} + </EuiText> + ), + truncateText: true, + width: '14%', + }, + { + field: 'current_status.search_after_time_durations', + name: i18n.COLUMN_QUERY_TIMES, + render: (value: RuleStatus['current_status']['search_after_time_durations']) => ( + <EuiText data-test-subj="search_after_time_durations" size="s"> + {value != null && value.length > 0 + ? Math.max(...value?.map(item => Number.parseFloat(item))) + : getEmptyTagValue()} + </EuiText> + ), + truncateText: true, + width: '14%', + }, + { + field: 'current_status.gap', + name: i18n.COLUMN_GAP, + render: (value: RuleStatus['current_status']['gap']) => ( + <EuiText data-test-subj="gap" size="s"> + {value ?? getEmptyTagValue()} + </EuiText> + ), + truncateText: true, + width: '14%', + }, + { + field: 'current_status.last_look_back_date', + name: i18n.COLUMN_LAST_LOOKBACK_DATE, + render: (value: RuleStatus['current_status']['last_look_back_date']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <FormattedDate value={value} fieldName={'last look back date'} /> + ); + }, + truncateText: true, + width: '16%', + }, + { + field: 'current_status.status_date', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: RuleStatus['current_status']['status_date']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + <LocalizedDateTooltip fieldName={i18n.COLUMN_LAST_COMPLETE_RUN} date={new Date(value)}> + <FormattedRelative value={value} /> + </LocalizedDateTooltip> + ); + }, + truncateText: true, + width: '20%', + }, + { + field: 'current_status.status', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: RuleStatus['current_status']['status']) => { + return ( + <> + <EuiHealth color={getStatusColor(value ?? null)}> + {value ?? getEmptyTagValue()} + </EuiHealth> + </> + ); + }, + width: '16%', + truncateText: true, + }, + { + field: 'activate', + name: i18n.COLUMN_ACTIVATE, + render: (value: Rule['enabled']) => ( + <EuiText data-test-subj="search_after_time_durations" size="s"> + {value ? i18n.ACTIVE : i18n.INACTIVE} + </EuiText> + ), + width: '95px', + }, + ]; + + return cols; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx new file mode 100644 index 0000000000000..59b3b02ff3587 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx @@ -0,0 +1,230 @@ +/* + * 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 { shallow, mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import { createKibanaContextProviderMock } from '../../../../mock/kibana_react'; +import { TestProviders } from '../../../../mock'; +import { wait } from '../../../../lib/helpers'; +import { AllRules } from './index'; + +jest.mock('./reducer', () => { + return { + allRulesReducer: jest.fn().mockReturnValue(() => ({ + exportRuleIds: [], + filterOptions: { + filter: 'some filter', + sortField: 'some sort field', + sortOrder: 'desc', + }, + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + page: 1, + perPage: 20, + total: 1, + }, + rules: [ + { + actions: [], + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: 'jibber jabber', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: 'rule-id-1', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: 'host.name:*', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + throttle: null, + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + ], + selectedRuleIds: [], + })), + }; +}); + +jest.mock('../../../../containers/detection_engine/rules', () => { + return { + useRules: jest.fn().mockReturnValue([ + false, + { + page: 1, + perPage: 20, + total: 1, + data: [ + { + actions: [], + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: 'jibber jabber', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: 'rule-id-1', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: 'host.name:*', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + throttle: null, + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + ], + }, + ]), + useRulesStatuses: jest.fn().mockReturnValue({ + loading: false, + rulesStatuses: [ + { + current_status: { + alert_id: 'alertId', + bulk_create_time_durations: ['2235.01'], + gap: null, + last_failure_at: null, + last_failure_message: null, + last_look_back_date: new Date().toISOString(), + last_success_at: new Date().toISOString(), + last_success_message: 'it is a success', + search_after_time_durations: ['616.97'], + status: 'succeeded', + status_date: new Date().toISOString(), + }, + failures: [], + id: '12345678987654321', + activate: true, + name: 'Test rule', + }, + ], + }), + }; +}); + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); + +describe('AllRules', () => { + it('renders correctly', () => { + const wrapper = shallow( + <AllRules + createPrePackagedRules={jest.fn()} + hasNoPermissions={false} + loading={false} + loadingCreatePrePackagedRules={false} + refetchPrePackagedRulesStatus={jest.fn()} + rulesCustomInstalled={0} + rulesInstalled={0} + rulesNotInstalled={0} + rulesNotUpdated={0} + setRefreshRulesData={jest.fn()} + /> + ); + + expect(wrapper.find('[title="All rules"]')).toHaveLength(1); + }); + + it('renders rules tab', async () => { + const KibanaContext = createKibanaContextProviderMock(); + const wrapper = mount( + <TestProviders> + <KibanaContext services={{ storage: { get: jest.fn() } }}> + <AllRules + createPrePackagedRules={jest.fn()} + hasNoPermissions={false} + loading={false} + loadingCreatePrePackagedRules={false} + refetchPrePackagedRulesStatus={jest.fn()} + rulesCustomInstalled={1} + rulesInstalled={0} + rulesNotInstalled={0} + rulesNotUpdated={0} + setRefreshRulesData={jest.fn()} + /> + </KibanaContext> + </TestProviders> + ); + + await act(async () => { + await wait(); + + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); + }); + }); + + it('renders monitoring tab when monitoring tab clicked', async () => { + const KibanaContext = createKibanaContextProviderMock(); + + const wrapper = mount( + <TestProviders> + <KibanaContext services={{ storage: { get: jest.fn() } }}> + <AllRules + createPrePackagedRules={jest.fn()} + hasNoPermissions={false} + loading={false} + loadingCreatePrePackagedRules={false} + refetchPrePackagedRulesStatus={jest.fn()} + rulesCustomInstalled={1} + rulesInstalled={0} + rulesNotInstalled={0} + rulesNotUpdated={0} + setRefreshRulesData={jest.fn()} + /> + </KibanaContext> + </TestProviders> + ); + const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button'); + monitoringTab.simulate('click'); + + await act(async () => { + wrapper.update(); + await wait(); + + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx new file mode 100644 index 0000000000000..18ca4d42bd018 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -0,0 +1,423 @@ +/* + * 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 { + EuiBasicTable, + EuiContextMenuPanel, + EuiLoadingContent, + EuiSpacer, + EuiTab, + EuiTabs, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import uuid from 'uuid'; + +import { + useRules, + useRulesStatuses, + CreatePreBuiltRules, + FilterOptions, + Rule, + PaginationOptions, + exportRules, +} from '../../../../containers/detection_engine/rules'; +import { HeaderSection } from '../../../../components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../components/utility_bar'; +import { useStateToaster } from '../../../../components/toasters'; +import { Loader } from '../../../../components/loader'; +import { Panel } from '../../../../components/panel'; +import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_empty_prompt'; +import { GenericDownloader } from '../../../../components/generic_downloader'; +import { AllRulesTables, SortingType } from '../components/all_rules_tables'; +import { getPrePackagedRuleStatus } from '../helpers'; +import * as i18n from '../translations'; +import { EuiBasicTableOnChange } from '../types'; +import { getBatchItems } from './batch_actions'; +import { getColumns, getMonitoringColumns } from './columns'; +import { showRulesTable } from './helpers'; +import { allRulesReducer, State } from './reducer'; +import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; +import { useMlCapabilities } from '../../../../components/ml_popover/hooks/use_ml_capabilities'; +import { hasMlAdminPermissions } from '../../../../components/ml/permissions/has_ml_admin_permissions'; + +const SORT_FIELD = 'enabled'; +const initialState: State = { + exportRuleIds: [], + filterOptions: { + filter: '', + sortField: SORT_FIELD, + sortOrder: 'desc', + }, + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + rules: [], + selectedRuleIds: [], +}; + +interface AllRulesProps { + createPrePackagedRules: CreatePreBuiltRules | null; + hasNoPermissions: boolean; + loading: boolean; + loadingCreatePrePackagedRules: boolean; + refetchPrePackagedRulesStatus: () => void; + rulesCustomInstalled: number | null; + rulesInstalled: number | null; + rulesNotInstalled: number | null; + rulesNotUpdated: number | null; + setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; +} + +export enum AllRulesTabs { + rules = 'rules', + monitoring = 'monitoring', +} + +const allRulesTabs = [ + { + id: AllRulesTabs.rules, + name: i18n.RULES_TAB, + disabled: false, + }, + { + id: AllRulesTabs.monitoring, + name: i18n.MONITORING_TAB, + disabled: false, + }, +]; + +/** + * Table Component for displaying all Rules for a given cluster. Provides the ability to filter + * by name, sort by enabled, and perform the following actions: + * * Enable/Disable + * * Duplicate + * * Delete + * * Import/Export + */ +export const AllRules = React.memo<AllRulesProps>( + ({ + createPrePackagedRules, + hasNoPermissions, + loading, + loadingCreatePrePackagedRules, + refetchPrePackagedRulesStatus, + rulesCustomInstalled, + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated, + setRefreshRulesData, + }) => { + const [initLoading, setInitLoading] = useState(true); + const tableRef = useRef<EuiBasicTable>(); + const [ + { + exportRuleIds, + filterOptions, + loadingRuleIds, + loadingRulesAction, + pagination, + rules, + selectedRuleIds, + }, + dispatch, + ] = useReducer(allRulesReducer(tableRef), initialState); + const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); + const history = useHistory(); + const [, dispatchToaster] = useStateToaster(); + const mlCapabilities = useMlCapabilities(); + const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); + + // TODO: Refactor license check + hasMlAdminPermissions to common check + const hasMlPermissions = + mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + + const setRules = useCallback((newRules: Rule[], newPagination: Partial<PaginationOptions>) => { + dispatch({ + type: 'setRules', + rules: newRules, + pagination: newPagination, + }); + }, []); + + const [isLoadingRules, , reFetchRulesData] = useRules({ + pagination, + filterOptions, + refetchPrePackagedRulesStatus, + dispatchRulesInReducer: setRules, + }); + + const sorting = useMemo( + (): SortingType => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }), + [filterOptions.sortOrder] + ); + + const prePackagedRuleStatus = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + <EuiContextMenuPanel + items={getBatchItems({ + closePopover, + dispatch, + dispatchToaster, + hasMlPermissions, + loadingRuleIds, + selectedRuleIds, + reFetchRules: reFetchRulesData, + rules, + })} + /> + ), + [ + dispatch, + dispatchToaster, + hasMlPermissions, + loadingRuleIds, + reFetchRulesData, + rules, + selectedRuleIds, + ] + ); + + const paginationMemo = useMemo( + () => ({ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + }), + [pagination] + ); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + sortField: SORT_FIELD, // Only enabled is supported for sorting currently + sortOrder: sort?.direction ?? 'desc', + }, + pagination: { page: page.index + 1, perPage: page.size }, + }); + }, + [dispatch] + ); + + const rulesColumns = useMemo(() => { + return getColumns({ + dispatch, + dispatchToaster, + history, + hasMlPermissions, + hasNoPermissions, + loadingRuleIds: + loadingRulesAction != null && + (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') + ? loadingRuleIds + : [], + reFetchRules: reFetchRulesData, + }); + }, [ + dispatch, + dispatchToaster, + hasMlPermissions, + history, + loadingRuleIds, + loadingRulesAction, + reFetchRulesData, + ]); + + const monitoringColumns = useMemo(() => getMonitoringColumns(), []); + + useEffect(() => { + if (reFetchRulesData != null) { + setRefreshRulesData(reFetchRulesData); + } + }, [reFetchRulesData, setRefreshRulesData]); + + useEffect(() => { + if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) { + setInitLoading(false); + } + }, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]); + + const handleCreatePrePackagedRules = useCallback(async () => { + if (createPrePackagedRules != null && reFetchRulesData != null) { + await createPrePackagedRules(); + reFetchRulesData(true); + } + }, [createPrePackagedRules, reFetchRulesData]); + + const euiBasicTableSelectionProps = useMemo( + () => ({ + selectable: (item: Rule) => !loadingRuleIds.includes(item.id), + onSelectionChange: (selected: Rule[]) => + dispatch({ type: 'selectedRuleIds', ids: selected.map(r => r.id) }), + }), + [loadingRuleIds] + ); + + const onFilterChangedCallback = useCallback((newFilterOptions: Partial<FilterOptions>) => { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...newFilterOptions, + }, + pagination: { page: 1 }, + }); + }, []); + + const isLoadingAnActionOnRule = useMemo(() => { + if ( + loadingRuleIds.length > 0 && + (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') + ) { + return false; + } else if (loadingRuleIds.length > 0) { + return true; + } + return false; + }, [loadingRuleIds, loadingRulesAction]); + + const tabs = useMemo( + () => ( + <EuiTabs> + {allRulesTabs.map(tab => ( + <EuiTab + data-test-subj={`allRulesTableTab-${tab.id}`} + onClick={() => setAllRulesTab(tab.id)} + isSelected={tab.id === allRulesTab} + disabled={tab.disabled} + key={tab.id} + > + {tab.name} + </EuiTab> + ))} + </EuiTabs> + ), + [allRulesTabs, allRulesTab, setAllRulesTab] + ); + + return ( + <> + <GenericDownloader + filename={`${i18n.EXPORT_FILENAME}.ndjson`} + ids={exportRuleIds} + onExportSuccess={exportCount => { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + exportSelectedData={exportRules} + /> + <EuiSpacer /> + {tabs} + <EuiSpacer /> + + <Panel loading={loading || isLoadingRules || isLoadingRulesStatuses}> + <> + <HeaderSection split title={i18n.ALL_RULES}> + <RulesTableFilters + onFilterChanged={onFilterChangedCallback} + rulesCustomInstalled={rulesCustomInstalled} + rulesInstalled={rulesInstalled} + /> + </HeaderSection> + + {(loading || isLoadingRules || isLoadingAnActionOnRule || isLoadingRulesStatuses) && + !initLoading && ( + <Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" /> + )} + {rulesCustomInstalled != null && + rulesCustomInstalled === 0 && + prePackagedRuleStatus === 'ruleNotInstalled' && + !initLoading && ( + <PrePackagedRulesPrompt + createPrePackagedRules={handleCreatePrePackagedRules} + loading={loadingCreatePrePackagedRules} + userHasNoPermissions={hasNoPermissions} + /> + )} + {initLoading && ( + <EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} /> + )} + {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && ( + <> + <UtilityBar> + <UtilityBarSection> + <UtilityBarGroup> + <UtilityBarText dataTestSubj="showingRules"> + {i18n.SHOWING_RULES(pagination.total ?? 0)} + </UtilityBarText> + </UtilityBarGroup> + + <UtilityBarGroup> + <UtilityBarText>{i18n.SELECTED_RULES(selectedRuleIds.length)}</UtilityBarText> + {!hasNoPermissions && ( + <UtilityBarAction + dataTestSubj="bulkActions" + iconSide="right" + iconType="arrowDown" + popoverContent={getBatchItemsPopoverContent} + > + {i18n.BATCH_ACTIONS} + </UtilityBarAction> + )} + <UtilityBarAction + iconSide="left" + iconType="refresh" + onClick={() => reFetchRulesData(true)} + > + {i18n.REFRESH} + </UtilityBarAction> + </UtilityBarGroup> + </UtilityBarSection> + </UtilityBar> + <AllRulesTables + selectedTab={allRulesTab} + euiBasicTableSelectionProps={euiBasicTableSelectionProps} + hasNoPermissions={hasNoPermissions} + monitoringColumns={monitoringColumns} + pagination={paginationMemo} + rules={rules} + rulesColumns={rulesColumns} + rulesStatuses={rulesStatuses} + sorting={sorting} + tableOnChangeCallback={tableOnChangeCallback} + tableRef={tableRef} + /> + </> + )} + </> + </Panel> + </> + ); + } +); + +AllRules.displayName = 'AllRules'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx new file mode 100644 index 0000000000000..8afb8db0c8d5b --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx @@ -0,0 +1,120 @@ +/* + * 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, { useRef } from 'react'; +import { shallow } from 'enzyme'; + +import { AllRulesTables } from './index'; +import { AllRulesTabs } from '../../all'; + +describe('AllRulesTables', () => { + it('renders correctly', () => { + const Component = () => { + const ref = useRef(); + + return ( + <AllRulesTables + selectedTab={AllRulesTabs.rules} + euiBasicTableSelectionProps={{}} + hasNoPermissions={false} + monitoringColumns={[]} + rules={[]} + rulesColumns={[]} + rulesStatuses={[]} + tableOnChangeCallback={jest.fn()} + tableRef={ref} + pagination={{ + pageIndex: 0, + pageSize: 0, + totalItemCount: 0, + pageSizeOptions: [0], + }} + sorting={{ + sort: { + field: 'enabled', + direction: 'asc', + }, + }} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); + }); + + it('renders rules tab when "selectedTab" is "rules"', () => { + const Component = () => { + const ref = useRef(); + + return ( + <AllRulesTables + selectedTab={AllRulesTabs.rules} + euiBasicTableSelectionProps={{}} + hasNoPermissions={false} + monitoringColumns={[]} + rules={[]} + rulesColumns={[]} + rulesStatuses={[]} + tableOnChangeCallback={jest.fn()} + tableRef={ref} + pagination={{ + pageIndex: 0, + pageSize: 0, + totalItemCount: 0, + pageSizeOptions: [0], + }} + sorting={{ + sort: { + field: 'enabled', + direction: 'asc', + }, + }} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); + expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(0); + }); + + it('renders monitoring tab when "selectedTab" is "monitoring"', () => { + const Component = () => { + const ref = useRef(); + + return ( + <AllRulesTables + selectedTab={AllRulesTabs.monitoring} + euiBasicTableSelectionProps={{}} + hasNoPermissions={false} + monitoringColumns={[]} + rules={[]} + rulesColumns={[]} + rulesStatuses={[]} + tableOnChangeCallback={jest.fn()} + tableRef={ref} + pagination={{ + pageIndex: 0, + pageSize: 0, + totalItemCount: 0, + pageSizeOptions: [0], + }} + sorting={{ + sort: { + field: 'enabled', + direction: 'asc', + }, + }} + /> + ); + }; + const wrapper = shallow(<Component />); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(0); + expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx new file mode 100644 index 0000000000000..8ea5606d0082c --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx @@ -0,0 +1,115 @@ +/* + * 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 { + EuiBasicTable, + EuiBasicTableColumn, + EuiEmptyPrompt, + Direction, + EuiTableSelectionType, +} from '@elastic/eui'; +import React, { useMemo, memo } from 'react'; +import styled from 'styled-components'; + +import { EuiBasicTableOnChange } from '../../types'; +import * as i18n from '../../translations'; +import { + RulesColumns, + RuleStatusRowItemType, +} from '../../../../../pages/detection_engine/rules/all/columns'; +import { Rule, Rules } from '../../../../../containers/detection_engine/rules'; +import { AllRulesTabs } from '../../all'; + +// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way +// after few hours of fight with typescript !!!! I lost :( +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; + +export interface SortingType { + sort: { + field: 'enabled'; + direction: Direction; + }; +} + +interface AllRulesTablesProps { + euiBasicTableSelectionProps: EuiTableSelectionType<Rule>; + hasNoPermissions: boolean; + monitoringColumns: Array<EuiBasicTableColumn<RuleStatusRowItemType>>; + pagination: { + pageIndex: number; + pageSize: number; + totalItemCount: number; + pageSizeOptions: number[]; + }; + rules: Rules; + rulesColumns: RulesColumns[]; + rulesStatuses: RuleStatusRowItemType[]; + sorting: { + sort: { + field: 'enabled'; + direction: Direction; + }; + }; + tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; + tableRef?: React.MutableRefObject<EuiBasicTable | undefined>; + selectedTab: AllRulesTabs; +} + +export const AllRulesTablesComponent: React.FC<AllRulesTablesProps> = ({ + euiBasicTableSelectionProps, + hasNoPermissions, + monitoringColumns, + pagination, + rules, + rulesColumns, + rulesStatuses, + sorting, + tableOnChangeCallback, + tableRef, + selectedTab, +}) => { + const emptyPrompt = useMemo(() => { + return ( + <EuiEmptyPrompt title={<h3>{i18n.NO_RULES}</h3>} titleSize="xs" body={i18n.NO_RULES_BODY} /> + ); + }, []); + + return ( + <> + {selectedTab === AllRulesTabs.rules && ( + <MyEuiBasicTable + data-test-subj="rules-table" + columns={rulesColumns} + isSelectable={!hasNoPermissions ?? false} + itemId="id" + items={rules ?? []} + noItemsMessage={emptyPrompt} + onChange={tableOnChangeCallback} + pagination={pagination} + ref={tableRef} + sorting={sorting} + selection={hasNoPermissions ? undefined : euiBasicTableSelectionProps} + /> + )} + {selectedTab === AllRulesTabs.monitoring && ( + <MyEuiBasicTable + data-test-subj="monitoring-table" + columns={monitoringColumns} + isSelectable={!hasNoPermissions ?? false} + itemId="id" + items={rulesStatuses} + noItemsMessage={emptyPrompt} + onChange={tableOnChangeCallback} + pagination={pagination} + sorting={sorting} + /> + )} + </> + ); +}; + +export const AllRulesTables = memo(AllRulesTablesComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx new file mode 100644 index 0000000000000..186aeae42246d --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx @@ -0,0 +1,415 @@ +/* + * 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 { shallow } from 'enzyme'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { coreMock } from '../../../../../../../../../src/core/public/mocks'; +import { esFilters, FilterManager } from '../../../../../../../../../src/plugins/data/public'; +import { SeverityBadge } from '../severity_badge'; + +import * as i18n from './translations'; +import { + isNotEmptyArray, + buildQueryBarDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildStringArrayDescription, + buildSeverityDescription, + buildUrlsDescription, + buildNoteDescription, + buildRuleTypeDescription, +} from './helpers'; +import { ListItems } from './types'; + +const setupMock = coreMock.createSetup(); +const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } +}; +setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); +const mockFilterManager = new FilterManager(setupMock.uiSettings); + +const mockQueryBar = { + query: 'test query', + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +describe('helpers', () => { + describe('isNotEmptyArray', () => { + test('returns false if empty array', () => { + const result = isNotEmptyArray([]); + expect(result).toBeFalsy(); + }); + + test('returns false if array of empty strings', () => { + const result = isNotEmptyArray(['', '']); + expect(result).toBeFalsy(); + }); + + test('returns true if array of string with space', () => { + const result = isNotEmptyArray([' ']); + expect(result).toBeTruthy(); + }); + + test('returns true if array with at least one non-empty string', () => { + const result = isNotEmptyArray(['', 'abc']); + expect(result).toBeTruthy(); + }); + }); + + describe('buildQueryBarDescription', () => { + test('returns empty array if no filters, query or savedId exist', () => { + const emptyMockQueryBar = { + query: '', + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: emptyMockQueryBar.filters, + filterManager: mockFilterManager, + query: emptyMockQueryBar.query, + savedId: emptyMockQueryBar.saved_id, + }); + expect(result).toEqual([]); + }); + + test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + }); + + test('returns expected array of ListItems when filters AND indexPatterns exist', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} </>); + expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); + expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); + }); + + test('returns expected array of ListItems when "query.query" exists', () => { + const mockQueryBarWithQuery = { + ...mockQueryBar, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithQuery.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithQuery.query, + savedId: mockQueryBarWithQuery.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} </>); + }); + + test('returns expected array of ListItems when "savedId" exists', () => { + const mockQueryBarWithSavedId = { + ...mockQueryBar, + query: '', + filters: [], + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithSavedId.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithSavedId.query, + savedId: mockQueryBarWithSavedId.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} </>); + expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} </>); + }); + }); + + describe('buildThreatDescription', () => { + test('returns empty array if no threats', () => { + const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); + expect(result).toHaveLength(0); + }); + + test('returns empty tactic link if no corresponding tactic id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns empty technique link if no corresponding technique id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); + }); + + test('returns with corresponding tactic and technique link text', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns corresponding number of tactic and technique links', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, + { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, + ], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, + ], + tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, + }, + ], + }); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); + }); + }); + + describe('buildUnorderedListArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + [] + ); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + ['', 'falsePositive1', 'falsePositive2'] + ); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); + }); + }); + + describe('buildStringArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ + '', + 'tag1', + 'tag2', + ]); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .first() + .text() + ).toEqual('tag1'); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .at(1) + .text() + ).toEqual('tag2'); + }); + }); + + describe('buildSeverityDescription', () => { + test('returns ListItem with passed in label and SeverityBadge component', () => { + const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + + expect(result[0].title).toEqual('Test label'); + expect(result[0].description).toEqual(<SeverityBadge value="Test description value" />); + }); + }); + + describe('buildUrlsDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUrlsDescription('Test label', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUrlsDescription('Test label', [ + 'www.test.com', + 'www.test2.com', + ]); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .first() + .text() + ).toEqual('www.test.com'); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .at(1) + .text() + ).toEqual('www.test2.com'); + }); + }); + + describe('buildNoteDescription', () => { + test('returns ListItem with passed in label and note content', () => { + const noteSample = + 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; + const result: ListItems[] = buildNoteDescription('Test label', noteSample); + const wrapper = shallow<React.ReactElement>(result[0].description as React.ReactElement); + const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); + + expect(result[0].title).toEqual('Test label'); + expect(noteElement.exists()).toBeTruthy(); + expect(noteElement.text()).toEqual(noteSample); + }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildNoteDescription('Test label', ''); + + expect(result).toHaveLength(0); + }); + }); + + describe('buildRuleTypeDescription', () => { + it('returns the label for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.description).toEqual('Machine Learning'); + }); + + it('returns the label for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.description).toEqual('Query'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx new file mode 100644 index 0000000000000..1ac371a3f6829 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -0,0 +1,294 @@ +/* + * 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 { + EuiBadge, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiLink, + EuiText, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import { RuleType } from '../../../../../../common/detection_engine/types'; +import { esFilters } from '../../../../../../../../../src/plugins/data/public'; + +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; + +import * as i18n from './translations'; +import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; +import { SeverityBadge } from '../severity_badge'; +import ListTreeIcon from './assets/list_tree_icon.svg'; +import { assertUnreachable } from '../../../../../lib/helpers'; + +const NoteDescriptionContainer = styled(EuiFlexItem)` + height: 105px; + overflow-y: hidden; +`; + +export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); + +const EuiBadgeWrap = (styled(EuiBadge)` + .euiBadge__text { + white-space: pre-wrap !important; + } +` as unknown) as typeof EuiBadge; + +export const buildQueryBarDescription = ({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, +}: BuildQueryBarDescription): ListItems[] => { + let items: ListItems[] = []; + if (!isEmpty(filters)) { + filterManager.setFilters(filters); + items = [ + ...items, + { + title: <>{i18n.FILTERS_LABEL} </>, + description: ( + <EuiFlexGroup wrap responsive={false} gutterSize="xs"> + {filterManager.getFilters().map((filter, index) => ( + <EuiFlexItem grow={false} key={`${field}-filter-${index}`}> + <EuiBadgeWrap color="hollow"> + {indexPatterns != null ? ( + <esFilters.FilterLabel + filter={filter} + valueLabel={esFilters.getDisplayValueFromFilter(filter, [indexPatterns])} + /> + ) : ( + <EuiLoadingSpinner size="m" /> + )} + </EuiBadgeWrap> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ), + }, + ]; + } + if (!isEmpty(query)) { + items = [ + ...items, + { + title: <>{i18n.QUERY_LABEL} </>, + description: <>{query} </>, + }, + ]; + } + if (!isEmpty(savedId)) { + items = [ + ...items, + { + title: <>{i18n.SAVED_ID_LABEL} </>, + description: <>{savedId} </>, + }, + ]; + } + return items; +}; + +const ThreatEuiFlexGroup = styled(EuiFlexGroup)` + .euiFlexItem { + margin-bottom: 0px; + } +`; + +const TechniqueLinkItem = styled(EuiButtonEmpty)` + .euiIcon { + width: 8px; + height: 8px; + } +`; + +export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { + if (threat.length > 0) { + return [ + { + title: label, + description: ( + <ThreatEuiFlexGroup direction="column"> + {threat.map((singleThreat, index) => { + const tactic = tacticsOptions.find(t => t.id === singleThreat.tactic.id); + return ( + <EuiFlexItem key={`${singleThreat.tactic.name}-${index}`}> + <EuiLink + data-test-subj="threatTacticLink" + href={singleThreat.tactic.reference} + target="_blank" + > + {tactic != null ? tactic.text : ''} + </EuiLink> + <EuiFlexGroup gutterSize="none" alignItems="flexStart" direction="column"> + {singleThreat.technique.map(technique => { + const myTechnique = techniquesOptions.find(t => t.id === technique.id); + return ( + <EuiFlexItem> + <TechniqueLinkItem + data-test-subj="threatTechniqueLink" + href={technique.reference} + target="_blank" + iconType={ListTreeIcon} + size="xs" + flush="left" + > + {myTechnique != null ? myTechnique.label : ''} + </TechniqueLinkItem> + </EuiFlexItem> + ); + })} + </EuiFlexGroup> + </EuiFlexItem> + ); + })} + <EuiSpacer /> + </ThreatEuiFlexGroup> + ), + }, + ]; + } + return []; +}; + +export const buildUnorderedListArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + <EuiText size="s"> + <ul> + {values.map(val => + isEmpty(val) ? null : ( + <li data-test-subj="unorderedListArrayDescriptionItem" key={`${field}-${val}`}> + {val} + </li> + ) + )} + </ul> + </EuiText> + ), + }, + ]; + } + return []; +}; + +export const buildStringArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + <EuiFlexGroup responsive={false} gutterSize="xs" wrap> + {values.map((val: string) => + isEmpty(val) ? null : ( + <EuiFlexItem grow={false} key={`${field}-${val}`}> + <EuiBadgeWrap data-test-subj="stringArrayDescriptionBadgeItem" color="hollow"> + {val} + </EuiBadgeWrap> + </EuiFlexItem> + ) + )} + </EuiFlexGroup> + ), + }, + ]; + } + return []; +}; + +export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ + { + title: label, + description: <SeverityBadge value={value} />, + }, +]; + +export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + <EuiText size="s"> + <ul> + {values + .filter(v => !isEmpty(v)) + .map((val, index) => ( + <li data-test-subj="urlsDescriptionReferenceLinkItem" key={`${index}-${val}`}> + <EuiLink href={val} external target="_blank"> + {val} + </EuiLink> + </li> + ))} + </ul> + </EuiText> + ), + }, + ]; + } + return []; +}; + +export const buildNoteDescription = (label: string, note: string): ListItems[] => { + if (note.trim() !== '') { + return [ + { + title: label, + description: ( + <NoteDescriptionContainer> + <div data-test-subj="noteDescriptionItem" className="eui-yScrollWithShadows"> + {note} + </div> + </NoteDescriptionContainer> + ), + }, + ]; + } + return []; +}; + +export const buildRuleTypeDescription = (label: string, ruleType: RuleType): ListItems[] => { + switch (ruleType) { + case 'machine_learning': { + return [ + { + title: label, + description: i18n.ML_TYPE_DESCRIPTION, + }, + ]; + } + case 'query': + case 'saved_query': { + return [ + { + title: label, + description: i18n.QUERY_TYPE_DESCRIPTION, + }, + ]; + } + default: + return assertUnreachable(ruleType); + } +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx new file mode 100644 index 0000000000000..fdfcfd0fd85fe --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -0,0 +1,474 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { + StepRuleDescriptionComponent, + addFilterStateIfNotThere, + buildListItems, + getDescriptionItem, +} from './'; + +import { + esFilters, + Filter, + FilterManager, +} from '../../../../../../../../../src/plugins/data/public'; +import { mockAboutStepRule, mockDefineStepRule } from '../../all/__mocks__/mock'; +import { coreMock } from '../../../../../../../../../src/core/public/mocks'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import * as i18n from './translations'; + +import { schema } from '../step_about_rule/schema'; +import { ListItems } from './types'; +import { AboutStepRule } from '../../types'; + +jest.mock('../../../../../lib/kibana'); + +describe('description_step', () => { + const setupMock = coreMock.createSetup(); + const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } + }; + let mockFilterManager: FilterManager; + let mockAboutStep: AboutStepRule; + + beforeEach(() => { + setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); + mockFilterManager = new FilterManager(setupMock.uiSettings); + mockAboutStep = mockAboutStepRule(); + }); + + describe('StepRuleDescriptionComponent', () => { + test('renders correctly against snapshot when columns is "multi"', () => { + const wrapper = shallow( + <StepRuleDescriptionComponent columns="multi" data={mockAboutStep} schema={schema} /> + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); + }); + + test('renders correctly against snapshot when columns is "single"', () => { + const wrapper = shallow( + <StepRuleDescriptionComponent columns="single" data={mockAboutStep} schema={schema} /> + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + }); + + test('renders correctly against snapshot when columns is "singleSplit', () => { + const wrapper = shallow( + <StepRuleDescriptionComponent columns="singleSplit" data={mockAboutStep} schema={schema} /> + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + expect( + wrapper + .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') + .at(0) + .prop('type') + ).toEqual('column'); + }); + }); + + describe('addFilterStateIfNotThere', () => { + test('it does not change the state if it is global', () => { + const filters: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + + test('it adds the state if it does not exist as local', () => { + const filters: Filter[] = [ + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + }); + + describe('buildListItems', () => { + test('returns expected ListItems array when given valid inputs', () => { + const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); + + expect(result.length).toEqual(9); + }); + }); + + describe('getDescriptionItem', () => { + test('returns ListItem with all values enumerated when value[field] is an array', () => { + const result: ListItems[] = getDescriptionItem( + 'tags', + 'Tags label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Tags label'); + expect(typeof result[0].description).toEqual('object'); + }); + + test('returns ListItem with description of value[field] when value[field] is a string', () => { + const result: ListItems[] = getDescriptionItem( + 'description', + 'Description label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Description label'); + expect(result[0].description).toEqual('24/7'); + }); + + test('returns empty array when "value" is a non-existant property in "field"', () => { + const result: ListItems[] = getDescriptionItem( + 'jibberjabber', + 'JibberJabber label', + mockAboutStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + + describe('queryBar', () => { + test('returns array of ListItems when queryBar exist', () => { + const mockQueryBar = { + isNew: false, + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: null, + saved_id: null, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'queryBar', + 'Query bar label', + mockQueryBar, + mockFilterManager + ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} </>); + expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} </>); + }); + }); + + describe('threat', () => { + test('returns array of ListItems when threat exist', () => { + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threat label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + + test('filters out threats with tactic.name of "none"', () => { + const mockStep = { + ...mockAboutStep, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + }); + + describe('references', () => { + test('returns array of ListItems when references exist', () => { + const result: ListItems[] = getDescriptionItem( + 'references', + 'Reference label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Reference label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('falsePositives', () => { + test('returns array of ListItems when falsePositives exist', () => { + const result: ListItems[] = getDescriptionItem( + 'falsePositives', + 'False positives label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('False positives label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('severity', () => { + test('returns array of ListItems when severity exist', () => { + const result: ListItems[] = getDescriptionItem( + 'severity', + 'Severity label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Severity label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('riskScore', () => { + test('returns array of ListItems when riskScore exist', () => { + const result: ListItems[] = getDescriptionItem( + 'riskScore', + 'Risk score label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Risk score label'); + expect(result[0].description).toEqual(21); + }); + }); + + describe('timeline', () => { + test('returns timeline title if one exists', () => { + const mockDefineStep = mockDefineStepRule(); + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockDefineStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual('Titled timeline'); + }); + + test('returns default timeline title if none exists', () => { + const mockStep = { + ...mockDefineStepRule(), + timeline: { + id: '12345', + }, + }; + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); + }); + }); + + describe('note', () => { + test('returns default "note" description', () => { + const result: ListItems[] = getDescriptionItem( + 'note', + 'Investigation guide', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Investigation guide'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx new file mode 100644 index 0000000000000..108f213811412 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -0,0 +1,205 @@ +/* + * 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 { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; +import React, { memo, useState } from 'react'; +import styled from 'styled-components'; + +import { RuleType } from '../../../../../../common/detection_engine/types'; +import { + IIndexPattern, + Filter, + esFilters, + FilterManager, +} from '../../../../../../../../../src/plugins/data/public'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import { useKibana } from '../../../../../lib/kibana'; +import { IMitreEnterpriseAttack } from '../../types'; +import { FieldValueTimeline } from '../pick_timeline'; +import { FormSchema } from '../../../../../shared_imports'; +import { ListItems } from './types'; +import { + buildQueryBarDescription, + buildSeverityDescription, + buildStringArrayDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildUrlsDescription, + buildNoteDescription, + buildRuleTypeDescription, +} from './helpers'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; +import { buildMlJobDescription } from './ml_job_description'; + +const DescriptionListContainer = styled(EuiDescriptionList)` + &.euiDescriptionList--column .euiDescriptionList__title { + width: 30%; + } + &.euiDescriptionList--column .euiDescriptionList__description { + width: 70%; + } +`; + +interface StepRuleDescriptionProps { + columns?: 'multi' | 'single' | 'singleSplit'; + data: unknown; + indexPatterns?: IIndexPattern; + schema: FormSchema; +} + +export const StepRuleDescriptionComponent: React.FC<StepRuleDescriptionProps> = ({ + data, + columns = 'multi', + indexPatterns, + schema, +}) => { + const kibana = useKibana(); + const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); + const [, siemJobs] = useSiemJobs(true); + + const keys = Object.keys(schema); + const listItems = keys.reduce((acc: ListItems[], key: string) => { + if (key === 'machineLearningJobId') { + return [ + ...acc, + buildMlJobDescription( + get(key, data) as string, + (get(key, schema) as { label: string }).label, + siemJobs + ), + ]; + } + return [...acc, ...buildListItems(data, pick(key, schema), filterManager, indexPatterns)]; + }, []); + + if (columns === 'multi') { + return ( + <EuiFlexGroup> + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( + <EuiFlexItem + data-test-subj="listItemColumnStepRuleDescription" + key={`description-step-rule-${index}`} + > + <EuiDescriptionList listItems={chunkListItems} /> + </EuiFlexItem> + ))} + </EuiFlexGroup> + ); + } + + return ( + <EuiFlexGroup> + <EuiFlexItem data-test-subj="listItemColumnStepRuleDescription"> + {columns === 'single' ? ( + <EuiDescriptionList listItems={listItems} /> + ) : ( + <DescriptionListContainer + data-test-subj="singleSplitStepRuleDescriptionList" + type="column" + listItems={listItems} + /> + )} + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +export const StepRuleDescription = memo(StepRuleDescriptionComponent); + +export const buildListItems = ( + data: unknown, + schema: FormSchema, + filterManager: FilterManager, + indexPatterns?: IIndexPattern +): ListItems[] => + Object.keys(schema).reduce<ListItems[]>( + (acc, field) => [ + ...acc, + ...getDescriptionItem( + field, + get([field, 'label'], schema), + data, + filterManager, + indexPatterns + ), + ], + [] + ); + +export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { + return filters.map(filter => { + if (filter.$state == null) { + return { $state: { store: esFilters.FilterStateStore.APP_STATE }, ...filter }; + } else { + return filter; + } + }); +}; + +export const getDescriptionItem = ( + field: string, + label: string, + data: unknown, + filterManager: FilterManager, + indexPatterns?: IIndexPattern +): ListItems[] => { + if (field === 'queryBar') { + const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); + const query = get('queryBar.query.query', data); + const savedId = get('queryBar.saved_id', data); + return buildQueryBarDescription({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, + }); + } else if (field === 'threat') { + const threat: IMitreEnterpriseAttack[] = get(field, data).filter( + (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' + ); + return buildThreatDescription({ label, threat }); + } else if (field === 'references') { + const urls: string[] = get(field, data); + return buildUrlsDescription(label, urls); + } else if (field === 'falsePositives') { + const values: string[] = get(field, data); + return buildUnorderedListArrayDescription(label, field, values); + } else if (Array.isArray(get(field, data))) { + const values: string[] = get(field, data); + return buildStringArrayDescription(label, field, values); + } else if (field === 'severity') { + const val: string = get(field, data); + return buildSeverityDescription(label, val); + } else if (field === 'timeline') { + const timeline = get(field, data) as FieldValueTimeline; + return [ + { + title: label, + description: timeline.title ?? DEFAULT_TIMELINE_TITLE, + }, + ]; + } else if (field === 'note') { + const val: string = get(field, data); + return buildNoteDescription(label, val); + } else if (field === 'ruleType') { + const ruleType: RuleType = get(field, data); + return buildRuleTypeDescription(label, ruleType); + } + + const description: string = get(field, data); + if (isNumber(description) || !isEmpty(description)) { + return [ + { + title: label, + description, + }, + ]; + } + return []; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx similarity index 96% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx index 5e8681a90d428..79993c37e549c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx @@ -8,7 +8,7 @@ import React from 'react'; import styled from 'styled-components'; import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; -import { isJobStarted } from '../../../../../../../../../plugins/siem/common/detection_engine/ml_helpers'; +import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; import { useKibana } from '../../../../../lib/kibana'; import { SiemJob } from '../../../../../components/ml_popover/types'; import { ListItems } from './types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts new file mode 100644 index 0000000000000..564a3c5dc2c01 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ReactNode } from 'react'; + +import { + IIndexPattern, + Filter, + FilterManager, +} from '../../../../../../../../../src/plugins/data/public'; +import { IMitreEnterpriseAttack } from '../../types'; + +export interface ListItems { + title: NonNullable<ReactNode>; + description: NonNullable<ReactNode>; +} + +export interface BuildQueryBarDescription { + field: string; + filters: Filter[]; + filterManager: FilterManager; + query: string; + savedId: string; + indexPatterns?: IIndexPattern; +} + +export interface BuildThreatDescription { + label: string; + threat: IMitreEnterpriseAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx new file mode 100644 index 0000000000000..4fb9faaea711c --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx @@ -0,0 +1,140 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; + +import styled from 'styled-components'; +import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; +import { useKibana } from '../../../../../lib/kibana'; +import { + ML_JOB_SELECT_PLACEHOLDER_TEXT, + ENABLE_ML_JOB_WARNING, +} from '../step_define_rule/translations'; + +const HelpTextWarningContainer = styled.div` + margin-top: 10px; +`; + +const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` + margin-bottom: 5px; +`; + +const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ + href, + showEnableWarning = false, +}) => ( + <> + <FormattedMessage + id="xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdHelpText" + defaultMessage="We've provided a few common jobs to get you started. To add your own custom jobs, assign a group of “siem” to those jobs in the {machineLearning} application to make them appear here." + values={{ + machineLearning: ( + <EuiLink href={href} target="_blank"> + <FormattedMessage + id="xpack.siem.components.mlJobSelect.machineLearningLink" + defaultMessage="Machine Learning" + /> + </EuiLink> + ), + }} + /> + {showEnableWarning && ( + <HelpTextWarningContainer> + <EuiText size="xs" color="warning"> + <EuiIcon type="alert" /> + <span>{ENABLE_ML_JOB_WARNING}</span> + </EuiText> + </HelpTextWarningContainer> + )} + </> +); + +const JobDisplay: React.FC<{ title: string; description: string }> = ({ title, description }) => ( + <> + <strong>{title}</strong> + <EuiText size="xs" color="subdued"> + <p>{description}</p> + </EuiText> + </> +); + +interface MlJobSelectProps { + describedByIds: string[]; + field: FieldHook; +} + +export const MlJobSelect: React.FC<MlJobSelectProps> = ({ describedByIds = [], field }) => { + const jobId = field.value as string; + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [isLoading, siemJobs] = useSiemJobs(false); + const mlUrl = useKibana().services.application.getUrlForApp('ml'); + const handleJobChange = useCallback( + (machineLearningJobId: string) => { + field.setValue(machineLearningJobId); + }, + [field] + ); + const placeholderOption = { + value: 'placeholder', + inputDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + dropdownDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + disabled: true, + }; + + const jobOptions = siemJobs.map(job => ({ + value: job.id, + inputDisplay: job.id, + dropdownDisplay: <JobDisplay title={job.id} description={job.description} />, + })); + + const options = [placeholderOption, ...jobOptions]; + + const isJobRunning = useMemo(() => { + // If the selected job is not found in the list, it means the placeholder is selected + // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' + const job = siemJobs.find(j => j.id === jobId); + return job == null || isJobStarted(job.jobState, job.datafeedState); + }, [siemJobs, jobId]); + + return ( + <MlJobSelectEuiFlexGroup> + <EuiFlexItem> + <EuiFormRow + label={field.label} + helpText={<HelpText href={mlUrl} showEnableWarning={!isJobRunning} />} + isInvalid={isInvalid} + error={errorMessage} + data-test-subj="mlJobSelect" + describedByIds={describedByIds} + > + <EuiFlexGroup> + <EuiFlexItem> + <EuiSuperSelect + hasDividers + isLoading={isLoading} + onChange={handleJobChange} + options={options} + valueOfSelected={jobId || 'placeholder'} + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + </EuiFlexItem> + </MlJobSelectEuiFlexGroup> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx new file mode 100644 index 0000000000000..b92d98a4afb13 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -0,0 +1,285 @@ +/* + * 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 { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Subscription } from 'rxjs'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { + Filter, + IIndexPattern, + Query, + FilterManager, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../../../../../../src/plugins/data/public'; + +import { BrowserFields } from '../../../../../containers/source'; +import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal'; +import { ActionTimelineToShow } from '../../../../../components/open_timeline/types'; +import { QueryBar } from '../../../../../components/query_bar'; +import { buildGlobalQuery } from '../../../../../components/timeline/helpers'; +import { getDataProviderFilter } from '../../../../../components/timeline/query_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; +import { useKibana } from '../../../../../lib/kibana'; +import { TimelineModel } from '../../../../../store/timeline/model'; +import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; +import * as i18n from './translations'; + +export interface FieldValueQueryBar { + filters: Filter[]; + query: Query; + saved_id?: string; +} +interface QueryBarDefineRuleProps { + browserFields: BrowserFields; + dataTestSubj: string; + field: FieldHook; + idAria: string; + isLoading: boolean; + indexPattern: IIndexPattern; + onCloseTimelineSearch: () => void; + openTimelineSearch: boolean; + resizeParentContainer?: (height: number) => void; +} + +const StyledEuiFormRow = styled(EuiFormRow)` + .kbnTypeahead__items { + max-height: 45vh !important; + } + .globalQueryBar { + padding: 4px 0px 0px 0px; + .kbnQueryBar { + & > div:first-child { + margin: 0px 0px 0px 4px; + } + } + } +`; + +// TODO need to add disabled in the SearchBar + +export const QueryBarDefineRule = ({ + browserFields, + dataTestSubj, + field, + idAria, + indexPattern, + isLoading = false, + onCloseTimelineSearch, + openTimelineSearch = false, + resizeParentContainer, +}: QueryBarDefineRuleProps) => { + const [originalHeight, setOriginalHeight] = useState(-1); + const [loadingTimeline, setLoadingTimeline] = useState(false); + const [savedQuery, setSavedQuery] = useState<SavedQuery | null>(null); + const [queryDraft, setQueryDraft] = useState<Query>({ query: '', language: 'kuery' }); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const kibana = useKibana(); + const [filterManager] = useState<FilterManager>(new FilterManager(kibana.services.uiSettings)); + + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters([]); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const newFilters = filterManager.getFilters(); + const { filters } = field.value as FieldValueQueryBar; + + if (!deepEqual(filters, newFilters)) { + field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); + } + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, [field.value]); + + useEffect(() => { + let isSubscribed = true; + async function updateFilterQueryFromValue() { + const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; + if (!deepEqual(query, queryDraft)) { + setQueryDraft(query); + } + if (!deepEqual(filters, filterManager.getFilters())) { + filterManager.setFilters(filters); + } + if ( + (savedId != null && savedQuery != null && savedId !== savedQuery.id) || + (savedId != null && savedQuery == null) + ) { + try { + const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery(mySavedQuery); + } + } catch { + setSavedQuery(null); + } + } else if (savedId == null && savedQuery != null) { + setSavedQuery(null); + } + } + updateFilterQueryFromValue(); + return () => { + isSubscribed = false; + }; + }, [field.value]); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + const { query } = field.value as FieldValueQueryBar; + if (!deepEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + const { query } = field.value as FieldValueQueryBar; + if (!deepEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + const { saved_id: savedId } = field.value as FieldValueQueryBar; + if (newSavedQuery.id !== savedId) { + setSavedQuery(newSavedQuery); + field.setValue({ + filters: newSavedQuery.attributes.filters, + query: newSavedQuery.attributes.query, + saved_id: newSavedQuery.id, + }); + } + } + }, + [field.value] + ); + + const onCloseTimelineModal = useCallback(() => { + setLoadingTimeline(true); + onCloseTimelineSearch(); + }, [onCloseTimelineSearch]); + + const onOpenTimeline = useCallback( + (timeline: TimelineModel) => { + setLoadingTimeline(false); + const newQuery = { + query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', + language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', + }; + const dataProvidersDsl = + timeline.dataProviders != null && timeline.dataProviders.length > 0 + ? convertKueryToElasticSearchQuery( + buildGlobalQuery(timeline.dataProviders, browserFields), + indexPattern + ) + : ''; + const newFilters = timeline.filters ?? []; + field.setValue({ + filters: + dataProvidersDsl !== '' + ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] + : newFilters, + query: newQuery, + saved_id: '', + }); + }, + [browserFields, field, indexPattern] + ); + + const onMutation = (event: unknown, observer: unknown) => { + if (resizeParentContainer != null) { + const suggestionContainer = document.getElementById('kbnTypeahead__items'); + if (suggestionContainer != null) { + const box = suggestionContainer.getBoundingClientRect(); + const accordionContainer = document.getElementById('define-rule'); + if (accordionContainer != null) { + const accordionBox = accordionContainer.getBoundingClientRect(); + if (originalHeight === -1 || accordionBox.height < originalHeight + box.height) { + resizeParentContainer(originalHeight + box.height - 100); + } + if (originalHeight === -1) { + setOriginalHeight(accordionBox.height); + } + } + } else { + resizeParentContainer(-1); + } + } + }; + + const actionTimelineToHide = useMemo<ActionTimelineToShow[]>(() => ['duplicate'], []); + + return ( + <> + <StyledEuiFormRow + label={field.label} + labelAppend={field.labelAppend} + helpText={field.helpText} + error={errorMessage} + isInvalid={isInvalid} + fullWidth + data-test-subj={dataTestSubj} + describedByIds={idAria ? [idAria] : undefined} + > + <EuiMutationObserver + observerOptions={{ subtree: true, attributes: true, childList: true }} + onMutation={onMutation} + > + {mutationRef => ( + <div ref={mutationRef}> + <QueryBar + indexPattern={indexPattern} + isLoading={isLoading || loadingTimeline} + isRefreshPaused={false} + filterQuery={queryDraft} + filterManager={filterManager} + filters={filterManager.getFilters() || []} + onChangedQuery={onChangedQuery} + onSubmitQuery={onSubmitQuery} + savedQuery={savedQuery} + onSavedQuery={onSavedQuery} + hideSavedQuery={false} + /> + </div> + )} + </EuiMutationObserver> + </StyledEuiFormRow> + {openTimelineSearch ? ( + <OpenTimelineModal + hideActions={actionTimelineToHide} + modalTitle={i18n.IMPORT_TIMELINE_MODAL} + onClose={onCloseTimelineModal} + onOpen={onOpenTimeline} + /> + ) : null} + </> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx new file mode 100644 index 0000000000000..b743888b67815 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx @@ -0,0 +1,83 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import deepMerge from 'deepmerge'; + +import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { loadActionTypes } from '../../../../../../../triggers_actions_ui/public/application/lib/action_connector_api'; +import { SelectField } from '../../../../../shared_imports'; +import { ActionForm, ActionType } from '../../../../../../../triggers_actions_ui/public'; +import { AlertAction } from '../../../../../../../alerting/common'; +import { useKibana } from '../../../../../lib/kibana'; + +type ThrottleSelectField = typeof SelectField; + +const DEFAULT_ACTION_GROUP_ID = 'default'; +const DEFAULT_ACTION_MESSAGE = + 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; + +export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { + const [supportedActionTypes, setSupportedActionTypes] = useState<ActionType[] | undefined>(); + const { + http, + triggers_actions_ui: { actionTypeRegistry }, + notifications, + } = useKibana().services; + + const setActionIdByIndex = useCallback( + (id: string, index: number) => { + const updatedActions = [...(field.value as Array<Partial<AlertAction>>)]; + updatedActions[index] = deepMerge(updatedActions[index], { id }); + field.setValue(updatedActions); + }, + [field] + ); + + const setAlertProperty = useCallback( + (updatedActions: AlertAction[]) => field.setValue(updatedActions), + [field] + ); + + const setActionParamsProperty = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (key: string, value: any, index: number) => { + const updatedActions = [...(field.value as AlertAction[])]; + updatedActions[index].params[key] = value; + field.setValue(updatedActions); + }, + [field] + ); + + useEffect(() => { + (async function() { + const actionTypes = await loadActionTypes({ http }); + const supportedTypes = actionTypes.filter(actionType => + NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) + ); + setSupportedActionTypes(supportedTypes); + })(); + }, []); + + if (!supportedActionTypes) return <></>; + + return ( + <ActionForm + actions={field.value as AlertAction[]} + messageVariables={messageVariables} + defaultActionGroupId={DEFAULT_ACTION_GROUP_ID} + setActionIdByIndex={setActionIdByIndex} + setAlertProperty={setAlertProperty} + setActionParamsProperty={setActionParamsProperty} + http={http} + actionTypeRegistry={actionTypeRegistry} + actionTypes={supportedActionTypes} + defaultActionMessage={DEFAULT_ACTION_MESSAGE} + toastNotifications={notifications.toasts} + /> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx new file mode 100644 index 0000000000000..6f3d299da8d45 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx @@ -0,0 +1,123 @@ +/* + * 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, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCard, + EuiFlexGrid, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiText, +} from '@elastic/eui'; + +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { RuleType } from '../../../../../../common/detection_engine/types'; +import { FieldHook } from '../../../../../shared_imports'; +import { useKibana } from '../../../../../lib/kibana'; +import * as i18n from './translations'; + +const MlCardDescription = ({ + subscriptionUrl, + hasValidLicense = false, +}: { + subscriptionUrl: string; + hasValidLicense?: boolean; +}) => ( + <EuiText size="s"> + {hasValidLicense ? ( + i18n.ML_TYPE_DESCRIPTION + ) : ( + <FormattedMessage + id="xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription" + defaultMessage="Access to ML requires a {subscriptionsLink}." + values={{ + subscriptionsLink: ( + <EuiLink href={subscriptionUrl} target="_blank"> + <FormattedMessage + id="xpack.siem.components.stepDefineRule.ruleTypeField.subscriptionsLink" + defaultMessage="Platinum subscription" + /> + </EuiLink> + ), + }} + /> + )} + </EuiText> +); + +interface SelectRuleTypeProps { + describedByIds?: string[]; + field: FieldHook; + hasValidLicense?: boolean; + isMlAdmin?: boolean; + isReadOnly?: boolean; +} + +export const SelectRuleType: React.FC<SelectRuleTypeProps> = ({ + describedByIds = [], + field, + isReadOnly = false, + hasValidLicense = false, + isMlAdmin = false, +}) => { + const ruleType = field.value as RuleType; + const setType = useCallback( + (type: RuleType) => { + field.setValue(type); + }, + [field] + ); + const setMl = useCallback(() => setType('machine_learning'), [setType]); + const setQuery = useCallback(() => setType('query'), [setType]); + const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; + const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { + path: '#/management/elasticsearch/license_management', + }); + + return ( + <EuiFormRow + fullWidth + data-test-subj="selectRuleType" + describedByIds={describedByIds} + label={field.label} + > + <EuiFlexGrid columns={4}> + <EuiFlexItem> + <EuiCard + data-test-subj="customRuleType" + title={i18n.QUERY_TYPE_TITLE} + description={i18n.QUERY_TYPE_DESCRIPTION} + icon={<EuiIcon size="l" type="search" />} + selectable={{ + isDisabled: isReadOnly, + onClick: setQuery, + isSelected: !isMlRule(ruleType), + }} + /> + </EuiFlexItem> + <EuiFlexItem> + <EuiCard + data-test-subj="machineLearningRuleType" + title={i18n.ML_TYPE_TITLE} + description={ + <MlCardDescription subscriptionUrl={licensingUrl} hasValidLicense={hasValidLicense} /> + } + icon={<EuiIcon size="l" type="machineLearningApp" />} + isDisabled={mlCardDisabled} + selectable={{ + isDisabled: mlCardDisabled, + onClick: setMl, + isSelected: isMlRule(ruleType), + }} + /> + </EuiFlexItem> + </EuiFlexGrid> + </EuiFormRow> + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx new file mode 100644 index 0000000000000..b6887badc56be --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -0,0 +1,276 @@ +/* + * 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 { EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import React, { FC, memo, useCallback, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; +import { useMlCapabilities } from '../../../../../components/ml_popover/hooks/use_ml_capabilities'; +import { useUiSetting$ } from '../../../../../lib/kibana'; +import { setFieldValue } from '../../helpers'; +import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; +import { StepRuleDescription } from '../description_step'; +import { QueryBarDefineRule } from '../query_bar'; +import { SelectRuleType } from '../select_rule_type'; +import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; +import { MlJobSelect } from '../ml_job_select'; +import { PickTimeline } from '../pick_timeline'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { NextStep } from '../next_step'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, + FormSchema, +} from '../../../../../shared_imports'; +import { schema } from './schema'; +import * as i18n from './translations'; +import { filterRuleFieldsForType, RuleFields } from '../../create/helpers'; +import { hasMlAdminPermissions } from '../../../../../components/ml/permissions/has_ml_admin_permissions'; + +const CommonUseField = getUseField({ component: Field }); + +interface StepDefineRuleProps extends RuleStepProps { + defaultValues?: DefineStepRule | null; +} + +const stepDefineDefaultValue: DefineStepRule = { + anomalyThreshold: 50, + index: [], + isNew: true, + machineLearningJobId: '', + ruleType: 'query', + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: undefined, + }, + timeline: { + id: null, + title: DEFAULT_TIMELINE_TITLE, + }, +}; + +const MyLabelButton = styled(EuiButtonEmpty)` + height: 18px; + font-size: 12px; + + .euiIcon { + width: 14px; + height: 14px; + } +`; + +MyLabelButton.defaultProps = { + flush: 'right', +}; + +const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ + addPadding = false, + defaultValues, + descriptionColumns = 'singleSplit', + isReadOnlyView, + isLoading, + isUpdateView = false, + setForm, + setStepData, +}) => { + const mlCapabilities = useMlCapabilities(); + const [openTimelineSearch, setOpenTimelineSearch] = useState(false); + const [indexModified, setIndexModified] = useState(false); + const [localIsMlRule, setIsMlRule] = useState(false); + const [indicesConfig] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY); + const [myStepData, setMyStepData] = useState<DefineStepRule>({ + ...stepDefineDefaultValue, + index: indicesConfig ?? [], + }); + const [ + { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + ] = useFetchIndexPatterns(myStepData.index); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); + + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.defineRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid && setStepData) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); + } + } + }, [form]); + + useEffect(() => { + const { isNew, ...values } = myStepData; + if (defaultValues != null && !deepEqual(values, defaultValues)) { + const newValues = { ...values, ...defaultValues, isNew: false }; + setMyStepData(newValues); + setFieldValue(form, schema, newValues); + } + }, [defaultValues, setMyStepData, setFieldValue]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.defineRule, form); + } + }, [form]); + + const handleResetIndices = useCallback(() => { + const indexField = form.getFields().index; + indexField.setValue(indicesConfig); + }, [form, indicesConfig]); + + const handleOpenTimelineSearch = useCallback(() => { + setOpenTimelineSearch(true); + }, []); + + const handleCloseTimelineSearch = useCallback(() => { + setOpenTimelineSearch(false); + }, []); + + return isReadOnlyView ? ( + <StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}> + <StepRuleDescription + columns={descriptionColumns} + indexPatterns={indexPatternQueryBar as IIndexPattern} + schema={filterRuleFieldsForType(schema as FormSchema & RuleFields, myStepData.ruleType)} + data={filterRuleFieldsForType(myStepData, myStepData.ruleType)} + /> + </StepContentWrapper> + ) : ( + <> + <StepContentWrapper addPadding={!isUpdateView}> + <Form form={form} data-test-subj="stepDefineRule"> + <UseField + path="ruleType" + component={SelectRuleType} + componentProps={{ + describedByIds: ['detectionEngineStepDefineRuleType'], + isReadOnly: isUpdateView, + hasValidLicense: mlCapabilities.isPlatinumOrTrialLicense, + isMlAdmin: hasMlAdminPermissions(mlCapabilities), + }} + /> + <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'none' : 'flex' }}> + <> + <CommonUseField + path="index" + config={{ + ...schema.index, + labelAppend: indexModified ? ( + <MyLabelButton onClick={handleResetIndices} iconType="refresh"> + {i18n.RESET_DEFAULT_INDEX} + </MyLabelButton> + ) : null, + }} + componentProps={{ + idAria: 'detectionEngineStepDefineRuleIndices', + 'data-test-subj': 'detectionEngineStepDefineRuleIndices', + euiFieldProps: { + fullWidth: true, + isDisabled: isLoading, + placeholder: '', + }, + }} + /> + <UseField + path="queryBar" + config={{ + ...schema.queryBar, + labelAppend: ( + <MyLabelButton onClick={handleOpenTimelineSearch}> + {i18n.IMPORT_TIMELINE_QUERY} + </MyLabelButton> + ), + }} + component={QueryBarDefineRule} + componentProps={{ + browserFields, + idAria: 'detectionEngineStepDefineRuleQueryBar', + indexPattern: indexPatternQueryBar, + isDisabled: isLoading, + isLoading: indexPatternLoadingQueryBar, + dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + openTimelineSearch, + onCloseTimelineSearch: handleCloseTimelineSearch, + }} + /> + </> + </EuiFormRow> + <EuiFormRow fullWidth style={{ display: localIsMlRule ? 'flex' : 'none' }}> + <> + <UseField + path="machineLearningJobId" + component={MlJobSelect} + componentProps={{ + describedByIds: ['detectionEngineStepDefineRulemachineLearningJobId'], + }} + /> + <UseField + path="anomalyThreshold" + component={AnomalyThresholdSlider} + componentProps={{ + describedByIds: ['detectionEngineStepDefineRuleAnomalyThreshold'], + }} + /> + </> + </EuiFormRow> + <UseField + path="timeline" + component={PickTimeline} + componentProps={{ + idAria: 'detectionEngineStepDefineRuleTimeline', + isDisabled: isLoading, + dataTestSubj: 'detectionEngineStepDefineRuleTimeline', + }} + /> + <FormDataProvider pathsToWatch={['index', 'ruleType']}> + {({ index, ruleType }) => { + if (index != null) { + if (deepEqual(index, indicesConfig) && indexModified) { + setIndexModified(false); + } else if (!deepEqual(index, indicesConfig) && !indexModified) { + setIndexModified(true); + } + } + + if (isMlRule(ruleType) && !localIsMlRule) { + setIsMlRule(true); + clearErrors(); + } else if (!isMlRule(ruleType) && localIsMlRule) { + setIsMlRule(false); + clearErrors(); + } + + return null; + }} + </FormDataProvider> + </Form> + </StepContentWrapper> + + {!isUpdateView && ( + <NextStep dataTestSubj="define-continue" onClick={onSubmit} isDisabled={isLoading} /> + )} + </> + ); +}; + +export const StepDefineRule = memo(StepDefineRuleComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx new file mode 100644 index 0000000000000..8915c5f0a224f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -0,0 +1,176 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; + +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { esKuery } from '../../../../../../../../../src/plugins/data/public'; +import { FieldValueQueryBar } from '../query_bar'; +import { + ERROR_CODE, + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, +} from '../../../../../shared_imports'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; + +export const schema: FormSchema = { + index: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', + { + defaultMessage: 'Index patterns', + } + ), + helpText: <EuiText size="xs">{INDEX_HELPER_TEXT}</EuiText>, + validations: [ + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ formData }] = args; + const needsValidation = !isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'A minimum of one index pattern is required.', + } + ) + )(...args); + }, + }, + ], + }, + queryBar: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', + { + defaultMessage: 'Custom query', + } + ), + validations: [ + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ value, path, formData }] = args; + const { query, filters } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + return isEmpty(query.query as string) && isEmpty(filters) + ? { + code: 'ERR_FIELD_MISSING', + path, + message: CUSTOM_QUERY_REQUIRED, + } + : undefined; + }, + }, + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ value, path, formData }] = args; + const { query } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + if (!isEmpty(query.query as string) && query.language === 'kuery') { + try { + esKuery.fromKueryExpression(query.query); + } catch (err) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: INVALID_CUSTOM_QUERY, + }; + } + } + }, + }, + ], + }, + ruleType: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', + { + defaultMessage: 'Rule type', + } + ), + validations: [], + }, + anomalyThreshold: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', + { + defaultMessage: 'Anomaly score threshold', + } + ), + validations: [], + }, + machineLearningJobId: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', + { + defaultMessage: 'Machine Learning job', + } + ), + validations: [ + { + validator: ( + ...args: Parameters<ValidationFunc> + ): ReturnType<ValidationFunc<{}, ERROR_CODE>> | undefined => { + const [{ formData }] = args; + const needsValidation = isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', + { + defaultMessage: 'A Machine Learning job is required.', + } + ) + )(...args); + }, + }, + ], + }, + timeline: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', + { + defaultMessage: 'Timeline template', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', + { + defaultMessage: + 'Select an existing timeline to use as a template when investigating generated signals.', + } + ), + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx new file mode 100644 index 0000000000000..0cf15c41a0f91 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx @@ -0,0 +1,36 @@ +/* + * 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, { useCallback } from 'react'; + +import { + NOTIFICATION_THROTTLE_RULE, + NOTIFICATION_THROTTLE_NO_ACTIONS, +} from '../../../../../../common/constants'; +import { SelectField } from '../../../../../shared_imports'; + +export const THROTTLE_OPTIONS = [ + { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, + { value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' }, + { value: '1h', text: 'Hourly' }, + { value: '1d', text: 'Daily' }, + { value: '7d', text: 'Weekly' }, +]; + +type ThrottleSelectField = typeof SelectField; + +export const ThrottleSelectField: ThrottleSelectField = props => { + const onChange = useCallback( + e => { + const throttle = e.target.value; + props.field.setValue(throttle); + props.handleChange(throttle); + }, + [props.field.setValue, props.handleChange] + ); + const newEuiFieldProps = { ...props.euiFieldProps, onChange }; + return <SelectField {...props} euiFieldProps={newEuiFieldProps} />; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts new file mode 100644 index 0000000000000..7ad116c313361 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -0,0 +1,174 @@ +/* + * 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 { has, isEmpty } from 'lodash/fp'; +import moment from 'moment'; +import deepmerge from 'deepmerge'; + +import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; +import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; +import { RuleType } from '../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; +import { NewRule } from '../../../../containers/detection_engine/rules'; + +import { + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + ActionsStepRule, + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + ActionsStepRuleJson, +} from '../types'; + +export const getTimeTypeValue = (time: string): { unit: string; value: number } => { + const timeObj = { + unit: '', + value: 0, + }; + const filterTimeVal = (time as string).match(/\d+/g); + const filterTimeType = (time as string).match(/[a-zA-Z]+/g); + if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { + timeObj.value = Number(filterTimeVal[0]); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) + ) { + timeObj.unit = filterTimeType[0]; + } + return timeObj; +}; + +export interface RuleFields { + anomalyThreshold: unknown; + machineLearningJobId: unknown; + queryBar: unknown; + index: unknown; + ruleType: unknown; +} +type QueryRuleFields<T> = Omit<T, 'anomalyThreshold' | 'machineLearningJobId'>; +type MlRuleFields<T> = Omit<T, 'queryBar' | 'index'>; + +const isMlFields = <T>(fields: QueryRuleFields<T> | MlRuleFields<T>): fields is MlRuleFields<T> => + has('anomalyThreshold', fields); + +export const filterRuleFieldsForType = <T extends RuleFields>(fields: T, type: RuleType) => { + if (isMlRule(type)) { + const { index, queryBar, ...mlRuleFields } = fields; + return mlRuleFields; + } else { + const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; + return queryRuleFields; + } +}; + +export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { + const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); + const { ruleType, timeline } = ruleFields; + const baseFields = { + type: ruleType, + ...(timeline.id != null && + timeline.title != null && { + timeline_id: timeline.id, + timeline_title: timeline.title, + }), + }; + + const typeFields = isMlFields(ruleFields) + ? { + anomaly_threshold: ruleFields.anomalyThreshold, + machine_learning_job_id: ruleFields.machineLearningJobId, + } + : { + index: ruleFields.index, + filters: ruleFields.queryBar?.filters, + language: ruleFields.queryBar?.query?.language, + query: ruleFields.queryBar?.query?.query as string, + saved_id: ruleFields.queryBar?.saved_id, + ...(ruleType === 'query' && + ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), + }; + + return { + ...baseFields, + ...typeFields, + }; +}; + +export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { + const { isNew, ...formatScheduleData } = scheduleData; + if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { + const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( + formatScheduleData.interval + ); + const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); + const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); + duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); + formatScheduleData.from = `now-${duration.asSeconds()}s`; + formatScheduleData.to = 'now'; + } + return { + ...formatScheduleData, + meta: { + from: scheduleData.from, + }, + }; +}; + +export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { + const { falsePositives, references, riskScore, threat, isNew, note, ...rest } = aboutStepData; + return { + false_positives: falsePositives.filter(item => !isEmpty(item)), + references: references.filter(item => !isEmpty(item)), + risk_score: riskScore, + threat: threat + .filter(singleThreat => singleThreat.tactic.name !== 'none') + .map(singleThreat => ({ + ...singleThreat, + framework: 'MITRE ATT&CK', + technique: singleThreat.technique.map(technique => { + const { id, name, reference } = technique; + return { id, name, reference }; + }), + })), + ...(!isEmpty(note) ? { note } : {}), + ...rest, + }; +}; + +export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { + const { + actions = [], + enabled, + kibanaSiemAppUrl, + throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, + } = actionsStepData; + + return { + actions: actions.map(transformAlertToRuleAction), + enabled, + throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, + meta: { + kibana_siem_app_url: kibanaSiemAppUrl, + }, + }; +}; + +export const formatRule = ( + defineStepData: DefineStepRule, + aboutStepData: AboutStepRule, + scheduleData: ScheduleStepRule, + actionsData: ActionsStepRule +): NewRule => + deepmerge.all([ + formatDefineStepData(defineStepData), + formatAboutStepData(aboutStepData), + formatScheduleStepData(scheduleData), + formatActionsStepData(actionsData), + ]) as NewRule; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/create/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/details/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx new file mode 100644 index 0000000000000..f2a04a87ced27 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -0,0 +1,378 @@ +/* + * 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 { + GetStepsData, + getDefineStepsData, + getScheduleStepsData, + getStepsData, + getAboutStepsData, + getActionsStepsData, + getHumanizedDuration, + getModifiedAboutDetailsData, + determineDetailsValue, + userHasNoPermissions, +} from './helpers'; +import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + ScheduleStepRule, + ActionsStepRule, +} from './types'; + +describe('rule helpers', () => { + describe('getStepsData', () => { + test('returns object with about, define, schedule and actions step properties formatted', () => { + const { + defineRuleData, + modifiedAboutRuleDetailsData, + aboutRuleData, + scheduleRuleData, + ruleActionsData, + }: GetStepsData = getStepsData({ + rule: mockRuleWithEverything('test-id'), + }); + const defineRuleStepData = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + index: ['auditbeat-*'], + machineLearningJobId: '', + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + }; + const aboutRuleStepData = { + description: '24/7', + falsePositives: ['test'], + isNew: false, + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + riskScore: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; + const ruleActionsStepData = { + enabled: true, + throttle: 'no_actions', + isNew: false, + actions: [], + }; + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(defineRuleData).toEqual(defineRuleStepData); + expect(aboutRuleData).toEqual(aboutRuleStepData); + expect(scheduleRuleData).toEqual(scheduleRuleStepData); + expect(ruleActionsData).toEqual(ruleActionsStepData); + expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); + }); + }); + + describe('getAboutStepsData', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); + + expect(result.name).toEqual(''); + expect(result.description).toEqual(''); + expect(result.note).toEqual(''); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); + + expect(result.note).toEqual(''); + }); + }); + + describe('determineDetailsValue', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( + mockRuleWithEverything('test-id'), + true + ); + const expected = { name: '', description: '', note: '' }; + + expect(result).toEqual(expected); + }); + + test('returns name, description, and note values if detailsView is false', () => { + const mockedRule = mockRuleWithEverything('test-id'); + const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( + mockedRule, + false + ); + const expected = { + name: mockedRule.name, + description: mockedRule.description, + note: mockedRule.note, + }; + + expect(result).toEqual(expected); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: Pick<Rule, 'name' | 'description' | 'note'> = determineDetailsValue( + mockedRule, + false + ); + const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; + + expect(result).toEqual(expected); + }); + }); + + describe('getDefineStepsData', () => { + test('returns with saved_id if value exists on rule', () => { + const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); + const expected = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: "Garrett's IP", + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Untitled timeline', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns with saved_id of undefined if value does not exist on rule', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + delete mockedRule.saved_id; + const result: DefineStepRule = getDefineStepsData(mockedRule); + const expected = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: undefined, + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Untitled timeline', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns timeline id and title of null if they do not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.timeline_id; + delete mockedRule.timeline_title; + const result: DefineStepRule = getDefineStepsData(mockedRule); + + expect(result.timeline.id).toBeNull(); + expect(result.timeline.title).toBeNull(); + }); + }); + + describe('getHumanizedDuration', () => { + test('returns from as seconds if from duration is less than a minute', () => { + const result = getHumanizedDuration('now-62s', '1m'); + + expect(result).toEqual('2s'); + }); + + test('returns from as minutes if from duration is less than an hour', () => { + const result = getHumanizedDuration('now-660s', '5m'); + + expect(result).toEqual('6m'); + }); + + test('returns from as hours if from duration is more than 60 minutes', () => { + const result = getHumanizedDuration('now-7400s', '5m'); + + expect(result).toEqual('1h'); + }); + + test('returns from as if from is not parsable as dateMath', () => { + const result = getHumanizedDuration('randomstring', '5m'); + + expect(result).toEqual('NaNh'); + }); + + test('returns from as 5m if interval is not parsable as dateMath', () => { + const result = getHumanizedDuration('now-300s', 'randomstring'); + + expect(result).toEqual('5m'); + }); + }); + + describe('getScheduleStepsData', () => { + test('returns expected ScheduleStep rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + const result: ScheduleStepRule = getScheduleStepsData(mockedRule); + const expected = { + isNew: false, + interval: mockedRule.interval, + from: '0s', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getActionsStepsData', () => { + test('returns expected ActionsStepRule rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + actions: [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + }, + ], + }; + const result: ActionsStepRule = getActionsStepsData(mockedRule); + const expected = { + actions: [ + { + id: 'id', + group: 'group', + params: {}, + actionTypeId: 'action_type_id', + }, + ], + enabled: mockedRule.enabled, + isNew: false, + throttle: 'no_actions', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getModifiedAboutDetailsData', () => { + test('returns object with "note" and "description" being those of passed in rule', () => { + const result: AboutStepRuleDetails = getModifiedAboutDetailsData( + mockRuleWithEverything('test-id') + ); + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(result).toEqual(aboutRuleDataDetailsData); + }); + + test('returns "note" with empty string if "note" does not exist', () => { + const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; + const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); + + const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; + + expect(result).toEqual(aboutRuleDetailsData); + }); + }); + + describe('userHasNoPermissions', () => { + test("returns false when user's CRUD operations are null", () => { + const result: boolean = userHasNoPermissions(null); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns true when user cannot CRUD', () => { + const result: boolean = userHasNoPermissions(false); + const userHasNoPermissionsExpectedResult = true; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns false when user can CRUD', () => { + const result: boolean = userHasNoPermissions(true); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx new file mode 100644 index 0000000000000..2ccbffd864070 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -0,0 +1,273 @@ +/* + * 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 dateMath from '@elastic/datemath'; +import { get } from 'lodash/fp'; +import moment from 'moment'; +import memoizeOne from 'memoize-one'; +import { useLocation } from 'react-router-dom'; + +import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../containers/detection_engine/rules'; +import { FormData, FormHook, FormSchema } from '../../../shared_imports'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + IMitreEnterpriseAttack, + ScheduleStepRule, + ActionsStepRule, +} from './types'; + +export interface GetStepsData { + aboutRuleData: AboutStepRule; + modifiedAboutRuleDetailsData: AboutStepRuleDetails; + defineRuleData: DefineStepRule; + scheduleRuleData: ScheduleStepRule; + ruleActionsData: ActionsStepRule; +} + +export const getStepsData = ({ + rule, + detailsView = false, +}: { + rule: Rule; + detailsView?: boolean; +}): GetStepsData => { + const defineRuleData: DefineStepRule = getDefineStepsData(rule); + const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); + const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); + const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); + const ruleActionsData: ActionsStepRule = getActionsStepsData(rule); + + return { + aboutRuleData, + modifiedAboutRuleDetailsData, + defineRuleData, + scheduleRuleData, + ruleActionsData, + }; +}; + +export const getActionsStepsData = ( + rule: Omit<Rule, 'actions'> & { actions: RuleAlertAction[] } +): ActionsStepRule => { + const { enabled, throttle, meta, actions = [] } = rule; + + return { + actions: actions?.map(transformRuleToAlertAction), + isNew: false, + throttle, + kibanaSiemAppUrl: meta?.kibana_siem_app_url, + enabled, + }; +}; + +export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ + isNew: false, + ruleType: rule.type, + anomalyThreshold: rule.anomaly_threshold ?? 50, + machineLearningJobId: rule.machine_learning_job_id ?? '', + index: rule.index ?? [], + queryBar: { + query: { query: rule.query ?? '', language: rule.language ?? '' }, + filters: (rule.filters ?? []) as Filter[], + saved_id: rule.saved_id, + }, + timeline: { + id: rule.timeline_id ?? null, + title: rule.timeline_title ?? null, + }, +}); + +export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { + const { interval, from } = rule; + const fromHumanizedValue = getHumanizedDuration(from, interval); + + return { + isNew: false, + interval, + from: fromHumanizedValue, + }; +}; + +export const getHumanizedDuration = (from: string, interval: string): string => { + const fromValue = dateMath.parse(from) ?? moment(); + const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); + + const fromDuration = moment.duration(intervalValue.diff(fromValue)); + const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; + + if (fromDuration.asSeconds() < 60) { + return `${Math.floor(fromDuration.asSeconds())}s`; + } else if (fromDuration.asMinutes() < 60) { + return `${Math.floor(fromDuration.asMinutes())}m`; + } + + return fromHumanize; +}; + +export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { + const { name, description, note } = determineDetailsValue(rule, detailsView); + const { + references, + severity, + false_positives: falsePositives, + risk_score: riskScore, + tags, + threat, + } = rule; + + return { + isNew: false, + name, + description, + note: note!, + references, + severity, + tags, + riskScore, + falsePositives, + threat: threat as IMitreEnterpriseAttack[], + }; +}; + +export const determineDetailsValue = ( + rule: Rule, + detailsView: boolean +): Pick<Rule, 'name' | 'description' | 'note'> => { + const { name, description, note } = rule; + if (detailsView) { + return { name: '', description: '', note: '' }; + } + + return { name, description, note: note ?? '' }; +}; + +export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ + note: rule.note ?? '', + description: rule.description, +}); + +export const useQuery = () => new URLSearchParams(useLocation().search); + +export type PrePackagedRuleStatus = + | 'ruleInstalled' + | 'ruleNotInstalled' + | 'ruleNeedUpdate' + | 'someRuleUninstall' + | 'unknown'; + +export const getPrePackagedRuleStatus = ( + rulesInstalled: number | null, + rulesNotInstalled: number | null, + rulesNotUpdated: number | null +): PrePackagedRuleStatus => { + if ( + rulesNotInstalled != null && + rulesInstalled === 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'ruleNotInstalled'; + } else if ( + rulesInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled === 0 && + rulesNotUpdated === 0 + ) { + return 'ruleInstalled'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'someRuleUninstall'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesNotUpdated != null && + rulesInstalled > 0 && + rulesNotInstalled >= 0 && + rulesNotUpdated > 0 + ) { + return 'ruleNeedUpdate'; + } + return 'unknown'; +}; +export const setFieldValue = ( + form: FormHook<FormData>, + schema: FormSchema<FormData>, + defaultValues: unknown +) => + Object.keys(schema).forEach(key => { + const val = get(key, defaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); + +export const redirectToDetections = ( + isSignalIndexExists: boolean | null, + isAuthenticated: boolean | null, + hasEncryptionKey: boolean | null +) => + isSignalIndexExists != null && + isAuthenticated != null && + hasEncryptionKey != null && + (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); + +export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { + const commonRuleParamsKeys = [ + 'id', + 'name', + 'description', + 'false_positives', + 'rule_id', + 'max_signals', + 'risk_score', + 'output_index', + 'references', + 'severity', + 'timeline_id', + 'timeline_title', + 'threat', + 'type', + 'version', + // 'lists', + ]; + + const ruleParamsKeys = [ + ...commonRuleParamsKeys, + ...(isMlRule(ruleType) + ? ['anomaly_threshold', 'machine_learning_job_id'] + : ['index', 'filters', 'language', 'query', 'saved_id']), + ].sort(); + + return ruleParamsKeys; +}; + +export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => { + if (!ruleType) { + return []; + } + const actionMessageRuleParams = getActionMessageRuleParams(ruleType); + + return [ + 'state.signals_count', + '{context.results_link}', + ...actionMessageRuleParams.map(param => `context.rule.${param}`), + ]; +}); + +// typed as null not undefined as the initial state for this value is null. +export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => + canUserCRUD != null ? !canUserCRUD : false; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.test.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/index.tsx rename to x-pack/plugins/siem/public/pages/detection_engine/rules/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts new file mode 100644 index 0000000000000..dcb5397d28f7c --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -0,0 +1,141 @@ +/* + * 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 { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; +import { AlertAction } from '../../../../../alerting/common'; +import { Filter } from '../../../../../../../src/plugins/data/common'; +import { FieldValueQueryBar } from './components/query_bar'; +import { FormData, FormHook } from '../../../shared_imports'; +import { FieldValueTimeline } from './components/pick_timeline'; + +export interface EuiBasicTableSortTypes { + field: string; + direction: 'asc' | 'desc'; +} + +export interface EuiBasicTableOnChange { + page: { + index: number; + size: number; + }; + sort?: EuiBasicTableSortTypes; +} + +export enum RuleStep { + defineRule = 'define-rule', + aboutRule = 'about-rule', + scheduleRule = 'schedule-rule', + ruleActions = 'rule-actions', +} +export type RuleStatusType = 'passive' | 'active' | 'valid'; + +export interface RuleStepData { + data: unknown; + isValid: boolean; +} + +export interface RuleStepProps { + addPadding?: boolean; + descriptionColumns?: 'multi' | 'single' | 'singleSplit'; + setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; + isReadOnlyView: boolean; + isUpdateView?: boolean; + isLoading: boolean; + resizeParentContainer?: (height: number) => void; + setForm?: (step: RuleStep, form: FormHook<FormData>) => void; +} + +interface StepRuleData { + isNew: boolean; +} +export interface AboutStepRule extends StepRuleData { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; + threat: IMitreEnterpriseAttack[]; + note: string; +} + +export interface AboutStepRuleDetails { + note: string; + description: string; +} + +export interface DefineStepRule extends StepRuleData { + anomalyThreshold: number; + index: string[]; + machineLearningJobId: string; + queryBar: FieldValueQueryBar; + ruleType: RuleType; + timeline: FieldValueTimeline; +} + +export interface ScheduleStepRule extends StepRuleData { + interval: string; + from: string; + to?: string; +} + +export interface ActionsStepRule extends StepRuleData { + actions: AlertAction[]; + enabled: boolean; + kibanaSiemAppUrl?: string; + throttle?: string | null; +} + +export interface DefineStepRuleJson { + anomaly_threshold?: number; + index?: string[]; + filters?: Filter[]; + machine_learning_job_id?: string; + saved_id?: string; + query?: string; + language?: string; + timeline_id?: string; + timeline_title?: string; + type: RuleType; +} + +export interface AboutStepRuleJson { + name: string; + description: string; + severity: string; + risk_score: number; + references: string[]; + false_positives: string[]; + tags: string[]; + threat: IMitreEnterpriseAttack[]; + note?: string; +} + +export interface ScheduleStepRuleJson { + interval: string; + from: string; + to?: string; + meta?: unknown; +} + +export interface ActionsStepRuleJson { + actions: RuleAlertAction[]; + enabled: boolean; + throttle?: string | null; + meta?: unknown; +} + +export interface IMitreAttack { + id: string; + name: string; + reference: string; +} +export interface IMitreEnterpriseAttack { + framework: string; + tactic: IMitreAttack; + technique: IMitreAttack[]; +} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/utils.test.ts rename to x-pack/plugins/siem/public/pages/detection_engine/rules/utils.test.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts new file mode 100644 index 0000000000000..f93ad94dd462b --- /dev/null +++ b/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts @@ -0,0 +1,98 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { + getDetectionEngineUrl, + getDetectionEngineTabUrl, + getRulesUrl, + getRuleDetailsUrl, + getCreateRuleUrl, + getEditRuleUrl, +} from '../../../components/link_to/redirect_to_detection_engine'; +import * as i18nDetections from '../translations'; +import * as i18nRules from './translations'; +import { RouteSpyState } from '../../../utils/route/types'; + +const getTabBreadcrumb = (pathname: string, search: string[]) => { + const tabPath = pathname.split('/')[2]; + + if (tabPath === 'alerts') { + return { + text: i18nDetections.ALERT, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'signals') { + return { + text: i18nDetections.SIGNAL, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'rules') { + return { + text: i18nRules.PAGE_TITLE, + href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } +}; + +const isRuleCreatePage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/create'); + +const isRuleEditPage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/edit'); + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18nDetections.PAGE_TITLE, + href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); + + if (tabBreadcrumb) { + breadcrumb = [...breadcrumb, tabBreadcrumb]; + } + + if (params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state.ruleName, + href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleCreatePage(params.pathName)) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.ADD_PAGE_TITLE, + href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.EDIT_PAGE_TITLE, + href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/plugins/siem/public/pages/detection_engine/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts rename to x-pack/plugins/siem/public/pages/detection_engine/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts rename to x-pack/plugins/siem/public/pages/detection_engine/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/plugins/siem/public/pages/home/home_navigations.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx rename to x-pack/plugins/siem/public/pages/home/home_navigations.tsx diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx new file mode 100644 index 0000000000000..a9e0962f16e6e --- /dev/null +++ b/x-pack/plugins/siem/public/pages/home/index.tsx @@ -0,0 +1,143 @@ +/* + * 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, { useMemo } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import styled from 'styled-components'; + +import { useThrottledResizeObserver } from '../../components/utils'; +import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; +import { Flyout } from '../../components/flyout'; +import { HeaderGlobal } from '../../components/header_global'; +import { HelpMenu } from '../../components/help_menu'; +import { LinkToPage } from '../../components/link_to'; +import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container'; +import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; +import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning'; +import { UseUrlState } from '../../components/url_state'; +import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; +import { SpyRoute } from '../../utils/route/spy_routes'; +import { useShowTimeline } from '../../utils/timeline/use_show_timeline'; +import { NotFoundPage } from '../404'; +import { DetectionEngineContainer } from '../detection_engine'; +import { HostsContainer } from '../hosts'; +import { NetworkContainer } from '../network'; +import { Overview } from '../overview'; +import { Case } from '../case'; +import { Timelines } from '../timelines'; +import { navTabs } from './home_navigations'; +import { SiemPageName } from './types'; + +const WrappedByAutoSizer = styled.div` + height: 100%; +`; +WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; + +const Main = styled.main` + height: 100%; +`; +Main.displayName = 'Main'; + +const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) + +/** the global Kibana navigation at the top of every page */ +const globalHeaderHeightPx = 48; + +const calculateFlyoutHeight = ({ + globalHeaderSize, + windowHeight, +}: { + globalHeaderSize: number; + windowHeight: number; +}): number => Math.max(0, windowHeight - globalHeaderSize); + +export const HomePage: React.FC = () => { + const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); + const flyoutHeight = useMemo( + () => + calculateFlyoutHeight({ + globalHeaderSize: globalHeaderHeightPx, + windowHeight, + }), + [windowHeight] + ); + + const [showTimeline] = useShowTimeline(); + + return ( + <WrappedByAutoSizer data-test-subj="wrapped-by-auto-sizer" ref={measureRef}> + <HeaderGlobal /> + + <Main data-test-subj="pageContainer"> + <WithSource sourceId="default"> + {({ browserFields, indexPattern, indicesExist }) => ( + <DragDropContextWrapper browserFields={browserFields}> + <UseUrlState indexPattern={indexPattern} navTabs={navTabs} /> + {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( + <> + <AutoSaveWarningMsg /> + <Flyout + flyoutHeight={flyoutHeight} + timelineId="timeline-1" + usersViewing={usersViewing} + /> + </> + )} + + <Switch> + <Redirect exact from="/" to={`/${SiemPageName.overview}`} /> + <Route path={`/:pageName(${SiemPageName.overview})`} render={() => <Overview />} /> + <Route + path={`/:pageName(${SiemPageName.hosts})`} + render={({ match }) => <HostsContainer url={match.url} />} + /> + <Route + path={`/:pageName(${SiemPageName.network})`} + render={({ location, match }) => ( + <NetworkContainer location={location} url={match.url} /> + )} + /> + <Route + path={`/:pageName(${SiemPageName.detections})`} + render={({ location, match }) => ( + <DetectionEngineContainer location={location} url={match.url} /> + )} + /> + <Route + path={`/:pageName(${SiemPageName.timelines})`} + render={() => <Timelines />} + /> + <Route path="/link-to" render={props => <LinkToPage {...props} />} /> + <Route + path="/ml-hosts" + render={({ location, match }) => ( + <MlHostConditionalContainer location={location} url={match.url} /> + )} + /> + <Route + path="/ml-network" + render={({ location, match }) => ( + <MlNetworkConditionalContainer location={location} url={match.url} /> + )} + /> + <Route path={`/:pageName(${SiemPageName.case})`}> + <Case /> + </Route> + <Route render={() => <NotFoundPage />} /> + </Switch> + </DragDropContextWrapper> + )} + </WithSource> + </Main> + + <HelpMenu /> + + <SpyRoute /> + </WrappedByAutoSizer> + ); +}; + +HomePage.displayName = 'HomePage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/plugins/siem/public/pages/home/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/home/translations.ts rename to x-pack/plugins/siem/public/pages/home/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/plugins/siem/public/pages/home/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/home/types.ts rename to x-pack/plugins/siem/public/pages/home/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/details_tabs.tsx diff --git a/x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts b/x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts new file mode 100644 index 0000000000000..d989d709cc4af --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getHostDetailsEventsKqlQueryExpression, getHostDetailsPageFilters } from './helpers'; +import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; + +describe('hosts page helpers', () => { + describe('getHostDetailsEventsKqlQueryExpression', () => { + const filterQueryExpression = 'user.name: "root"'; + const hostName = 'foo'; + + it('combines the filterQueryExpression and hostname when both are NOT empty', () => { + expect(getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName })).toEqual( + 'user.name: "root" and host.name: "foo"' + ); + }); + + it('returns just the filterQueryExpression when it is NOT empty, but hostname is empty', () => { + expect( + getHostDetailsEventsKqlQueryExpression({ filterQueryExpression, hostName: '' }) + ).toEqual('user.name: "root"'); + }); + + it('returns just the hostname when filterQueryExpression is empty, but hostname is NOT empty', () => { + expect( + getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName }) + ).toEqual('host.name: "foo"'); + }); + + it('returns an empty string when both the filterQueryExpression and hostname are empty', () => { + expect( + getHostDetailsEventsKqlQueryExpression({ filterQueryExpression: '', hostName: '' }) + ).toEqual(''); + }); + }); + + describe('getHostDetailsPageFilters', () => { + it('correctly constructs pageFilters for the given hostName', () => { + const expected: Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: 'host-1', + params: { + query: 'host-1', + }, + }, + query: { + match: { + 'host.name': { + query: 'host-1', + type: 'phrase', + }, + }, + }, + }, + ]; + expect(getHostDetailsPageFilters('host-1')).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts b/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts new file mode 100644 index 0000000000000..6da76f2fb5cac --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { escapeQueryValue } from '../../../lib/keury'; +import { Filter } from '../../../../../../../src/plugins/data/public'; + +/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ +export const getHostDetailsEventsKqlQueryExpression = ({ + filterQueryExpression, + hostName, +}: { + filterQueryExpression: string; + hostName: string; +}): string => { + if (filterQueryExpression.length) { + return `${filterQueryExpression}${ + hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' + }`; + } else { + return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; + } +}; + +export const getHostDetailsPageFilters = (hostName: string): Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: hostName, + params: { + query: hostName, + }, + }, + query: { + match: { + 'host.name': { + query: hostName, + type: 'phrase', + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/plugins/siem/public/pages/hosts/details/index.tsx new file mode 100644 index 0000000000000..ef04288aa1b6f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/index.tsx @@ -0,0 +1,229 @@ +/* + * 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 { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import React, { useEffect, useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../components/filters_global'; +import { HeaderPage } from '../../../components/header_page'; +import { LastEventTime } from '../../../components/last_event_time'; +import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; +import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria'; +import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions'; +import { useMlCapabilities } from '../../../components/ml_popover/hooks/use_ml_capabilities'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { SiemNavigation } from '../../../components/navigation'; +import { KpiHostsComponent } from '../../../components/page/hosts'; +import { HostOverview } from '../../../components/page/hosts/host_overview'; +import { manageQuery } from '../../../components/page/manage_query'; +import { SiemSearchBar } from '../../../components/search_bar'; +import { WrapperPage } from '../../../components/wrapper_page'; +import { HostOverviewByNameQuery } from '../../../containers/hosts/overview'; +import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { LastEventIndexKey } from '../../../graphql/types'; +import { useKibana } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { inputsSelectors, State } from '../../../store'; +import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { SpyRoute } from '../../../utils/route/spy_routes'; +import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; + +import { HostsEmptyPage } from '../hosts_empty_page'; +import { HostDetailsTabs } from './details_tabs'; +import { navTabsHostDetails } from './nav_tabs'; +import { HostDetailsProps } from './types'; +import { type } from './utils'; +import { getHostDetailsPageFilters } from './helpers'; + +const HostOverviewManage = manageQuery(HostOverview); +const KpiHostDetailsManage = manageQuery(KpiHostsComponent); + +const HostDetailsComponent = React.memo<HostDetailsProps & PropsFromRedux>( + ({ + filters, + from, + isInitializing, + query, + setAbsoluteRangeDatePicker, + setHostDetailsTablesActivePageToZero, + setQuery, + to, + detailName, + deleteQuery, + hostDetailsPagePath, + }) => { + useEffect(() => { + setHostDetailsTablesActivePageToZero(); + }, [setHostDetailsTablesActivePageToZero, detailName]); + const capabilities = useMlCapabilities(); + const kibana = useKibana(); + const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ + detailName, + ]); + const getFilters = () => [...hostDetailsPageFilters, ...filters]; + const narrowDateRange = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + return ( + <> + <WithSource sourceId="default"> + {({ indicesExist, indexPattern }) => { + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: getFilters(), + }); + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + <StickyContainer> + <FiltersGlobal> + <SiemSearchBar indexPattern={indexPattern} id="global" /> + </FiltersGlobal> + + <WrapperPage> + <HeaderPage + border + subtitle={ + <LastEventTime + indexKey={LastEventIndexKey.hostDetails} + hostName={detailName} + /> + } + title={detailName} + /> + + <HostOverviewByNameQuery + sourceId="default" + hostName={detailName} + skip={isInitializing} + startDate={from} + endDate={to} + > + {({ hostOverview, loading, id, inspect, refetch }) => ( + <AnomalyTableProvider + criteriaFields={hostToCriteria(hostOverview)} + startDate={from} + endDate={to} + skip={isInitializing} + > + {({ isLoadingAnomaliesData, anomaliesData }) => ( + <HostOverviewManage + id={id} + inspect={inspect} + refetch={refetch} + setQuery={setQuery} + data={hostOverview} + anomaliesData={anomaliesData} + isLoadingAnomaliesData={isLoadingAnomaliesData} + loading={loading} + startDate={from} + endDate={to} + narrowDateRange={(score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + </AnomalyTableProvider> + )} + </HostOverviewByNameQuery> + + <EuiHorizontalRule /> + + <KpiHostDetailsQuery + sourceId="default" + filterQuery={filterQuery} + skip={isInitializing} + startDate={from} + endDate={to} + > + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + <KpiHostDetailsManage + data={kpiHostDetails} + from={from} + id={id} + inspect={inspect} + loading={loading} + refetch={refetch} + setQuery={setQuery} + to={to} + narrowDateRange={narrowDateRange} + /> + )} + </KpiHostDetailsQuery> + + <EuiSpacer /> + + <SiemNavigation + navTabs={navTabsHostDetails(detailName, hasMlUserPermissions(capabilities))} + /> + + <EuiSpacer /> + + <HostDetailsTabs + isInitializing={isInitializing} + deleteQuery={deleteQuery} + pageFilters={hostDetailsPageFilters} + to={to} + from={from} + detailName={detailName} + type={type} + setQuery={setQuery} + filterQuery={filterQuery} + hostDetailsPagePath={hostDetailsPagePath} + indexPattern={indexPattern} + setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} + /> + </WrapperPage> + </StickyContainer> + ) : ( + <WrapperPage> + <HeaderPage border title={detailName} /> + + <HostsEmptyPage /> + </WrapperPage> + ); + }} + </WithSource> + + <SpyRoute /> + </> + ); + } +); +HostDetailsComponent.displayName = 'HostDetailsComponent'; + +export const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + return (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + setHostDetailsTablesActivePageToZero: dispatchHostDetailsTablesActivePageToZero, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const HostDetails = connector(HostDetailsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/nav_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts b/x-pack/plugins/siem/public/pages/hosts/details/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/details/types.ts rename to x-pack/plugins/siem/public/pages/hosts/details/types.ts diff --git a/x-pack/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/plugins/siem/public/pages/hosts/details/utils.ts new file mode 100644 index 0000000000000..af4ba8eb091e2 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/details/utils.ts @@ -0,0 +1,58 @@ +/* + * 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 { get, isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { hostsModel } from '../../../store'; +import { HostsTableType } from '../../../store/hosts/model'; +import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; + +import * as i18n from '../translations'; +import { HostRouteSpyState } from '../../../utils/route/types'; + +export const type = hostsModel.HostsType.details; + +const TabNameMappedToI18nKey: Record<HostsTableType, string> = { + [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, + [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, + [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, + [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, + [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, +}; + +export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + if (params.tabName != null) { + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx index 99cf767c65e08..6134c1dd6911a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; -import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import '../../mock/match_media'; import { mocksSource } from '../../containers/source/mock'; import { wait } from '../../lib/helpers'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts.tsx index d574925a91600..028c804f4d48f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/plugins/siem/public/pages/hosts/hosts.tsx @@ -28,7 +28,7 @@ import { inputsSelectors, State, hostsModel } from '../../store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { esQuery } from '../../../../../../src/plugins/data/public'; import { useMlCapabilities } from '../../components/ml_popover/hooks/use_ml_capabilities'; import { HostsEmptyPage } from './hosts_empty_page'; import { HostsTabs } from './hosts_tabs'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts_empty_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts_empty_page.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts_empty_page.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/hosts_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/hosts_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/hosts_tabs.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx b/x-pack/plugins/siem/public/pages/hosts/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/index.tsx rename to x-pack/plugins/siem/public/pages/hosts/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/nav_tabs.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.test.tsx rename to x-pack/plugins/siem/public/pages/hosts/nav_tabs.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/nav_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/nav_tabs.tsx rename to x-pack/plugins/siem/public/pages/hosts/nav_tabs.tsx diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx new file mode 100644 index 0000000000000..ec33834b1bf73 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx @@ -0,0 +1,54 @@ +/* + * 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, { useMemo } from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { AlertsView } from '../../../components/alerts_viewer'; +import { AlertsComponentQueryProps } from './types'; + +export const filterHostData: Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "host.name"}}],"minimum_should_match": 1}}]}}}', + }, + }, +]; +export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { + const { pageFilters, ...rest } = alertsProps; + const hostPageFilters = useMemo( + () => (pageFilters != null ? [...filterHostData, ...pageFilters] : filterHostData), + [pageFilters] + ); + + return <AlertsView {...rest} pageFilters={hostPageFilters} />; +}); + +HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts b/x-pack/plugins/siem/public/pages/hosts/navigation/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/index.ts rename to x-pack/plugins/siem/public/pages/hosts/navigation/index.ts diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts new file mode 100644 index 0000000000000..20d4d4e463a7f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESTermQuery } from '../../../../common/typed_json'; +import { Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { NarrowDateRange } from '../../../components/ml/types'; +import { InspectQuery, Refetch } from '../../../store/inputs/model'; + +import { HostsTableType, HostsType } from '../../../store/hosts/model'; +import { NavTab } from '../../../components/navigation/types'; +import { UpdateDateRange } from '../../../components/charts/common'; + +export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & + HostsTableType.authentications & + HostsTableType.uncommonProcesses & + HostsTableType.events; + +type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies; + +type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission; + +export type HostsNavTab = Record<KeyHostsNavTab, NavTab>; + +export type SetQuery = ({ + id, + inspect, + loading, + refetch, +}: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; +}) => void; + +export interface QueryTabBodyProps { + type: HostsType; + startDate: number; + endDate: number; + filterQuery?: string | ESTermQuery; +} + +export type HostsComponentsQueryProps = QueryTabBodyProps & { + deleteQuery?: ({ id }: { id: string }) => void; + indexPattern: IIndexPattern; + pageFilters?: Filter[]; + skip: boolean; + setQuery: SetQuery; + updateDateRange?: UpdateDateRange; + narrowDateRange?: NarrowDateRange; +}; + +export type AlertsComponentQueryProps = HostsComponentsQueryProps & { + filterQuery: string; + pageFilters?: Filter[]; +}; + +export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts b/x-pack/plugins/siem/public/pages/hosts/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/translations.ts rename to x-pack/plugins/siem/public/pages/hosts/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/types.ts b/x-pack/plugins/siem/public/pages/hosts/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/hosts/types.ts rename to x-pack/plugins/siem/public/pages/hosts/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/index.tsx b/x-pack/plugins/siem/public/pages/network/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/index.tsx rename to x-pack/plugins/siem/public/pages/network/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx new file mode 100644 index 0000000000000..350d6e34c1c0f --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx @@ -0,0 +1,298 @@ +/* + * 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 { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../components/filters_global'; +import { HeaderPage } from '../../../components/header_page'; +import { LastEventTime } from '../../../components/last_event_time'; +import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; +import { networkToCriteria } from '../../../components/ml/criteria/network_to_criteria'; +import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; +import { manageQuery } from '../../../components/page/manage_query'; +import { FlowTargetSelectConnected } from '../../../components/page/network/flow_target_select_connected'; +import { IpOverview } from '../../../components/page/network/ip_overview'; +import { SiemSearchBar } from '../../../components/search_bar'; +import { WrapperPage } from '../../../components/wrapper_page'; +import { IpOverviewQuery } from '../../../containers/ip_overview'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; +import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; +import { useKibana } from '../../../lib/kibana'; +import { decodeIpv6 } from '../../../lib/helpers'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { ConditionalFlexGroup } from '../../../pages/network/navigation/conditional_flex_group'; +import { networkModel, State, inputsSelectors } from '../../../store'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; +import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../../store/network/actions'; +import { SpyRoute } from '../../../utils/route/spy_routes'; +import { NetworkEmptyPage } from '../network_empty_page'; +import { NetworkHttpQueryTable } from './network_http_query_table'; +import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table'; +import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; +import { TlsQueryTable } from './tls_query_table'; +import { IPDetailsComponentProps } from './types'; +import { UsersQueryTable } from './users_query_table'; +import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; +import { esQuery } from '../../../../../../../src/plugins/data/public'; + +export { getBreadcrumbs } from './utils'; + +const IpOverviewManage = manageQuery(IpOverview); + +export const IPDetailsComponent: React.FC<IPDetailsComponentProps & PropsFromRedux> = ({ + detailName, + filters, + flowTarget, + from, + isInitializing, + query, + setAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero, + setQuery, + to, +}) => { + const type = networkModel.NetworkType.details; + const narrowDateRange = useCallback( + (score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + const kibana = useKibana(); + + useEffect(() => { + setIpDetailsTablesActivePageToZero(); + }, [detailName, setIpDetailsTablesActivePageToZero]); + + return ( + <> + <WithSource sourceId="default" data-test-subj="ip-details-page"> + {({ indicesExist, indexPattern }) => { + const ip = decodeIpv6(detailName); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + <StickyContainer> + <FiltersGlobal> + <SiemSearchBar indexPattern={indexPattern} id="global" /> + </FiltersGlobal> + + <WrapperPage> + <HeaderPage + border + data-test-subj="ip-details-headline" + draggableArguments={{ field: `${flowTarget}.ip`, value: ip }} + subtitle={<LastEventTime indexKey={LastEventIndexKey.ipDetails} ip={ip} />} + title={ip} + > + <FlowTargetSelectConnected flowTarget={flowTarget} /> + </HeaderPage> + + <IpOverviewQuery + skip={isInitializing} + sourceId="default" + filterQuery={filterQuery} + type={type} + ip={ip} + > + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + <AnomalyTableProvider + criteriaFields={networkToCriteria(detailName, flowTarget)} + startDate={from} + endDate={to} + skip={isInitializing} + > + {({ isLoadingAnomaliesData, anomaliesData }) => ( + <IpOverviewManage + id={id} + inspect={inspect} + ip={ip} + data={ipOverviewData} + anomaliesData={anomaliesData} + loading={loading} + isLoadingAnomaliesData={isLoadingAnomaliesData} + type={type} + flowTarget={flowTarget} + refetch={refetch} + setQuery={setQuery} + startDate={from} + endDate={to} + narrowDateRange={narrowDateRange} + /> + )} + </AnomalyTableProvider> + )} + </IpOverviewQuery> + + <EuiHorizontalRule /> + + <ConditionalFlexGroup direction="column"> + <EuiFlexItem> + <NetworkTopNFlowQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={FlowTargetSourceDest.source} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + + <EuiFlexItem> + <NetworkTopNFlowQueryTable + endDate={to} + flowTarget={FlowTargetSourceDest.destination} + filterQuery={filterQuery} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + </ConditionalFlexGroup> + + <EuiSpacer /> + + <ConditionalFlexGroup direction="column"> + <EuiFlexItem> + <NetworkTopCountriesQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={FlowTargetSourceDest.source} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + + <EuiFlexItem> + <NetworkTopCountriesQueryTable + endDate={to} + flowTarget={FlowTargetSourceDest.destination} + filterQuery={filterQuery} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + indexPattern={indexPattern} + /> + </EuiFlexItem> + </ConditionalFlexGroup> + + <EuiSpacer /> + + <UsersQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={flowTarget} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + /> + + <EuiSpacer /> + + <NetworkHttpQueryTable + endDate={to} + filterQuery={filterQuery} + ip={ip} + skip={isInitializing} + startDate={from} + type={type} + setQuery={setQuery} + /> + + <EuiSpacer /> + + <TlsQueryTable + endDate={to} + filterQuery={filterQuery} + flowTarget={(flowTarget as unknown) as FlowTargetSourceDest} + ip={ip} + setQuery={setQuery} + skip={isInitializing} + startDate={from} + type={type} + /> + + <EuiSpacer /> + + <AnomaliesQueryTabBody + filterQuery={filterQuery} + setQuery={setQuery} + startDate={from} + endDate={to} + skip={isInitializing} + ip={ip} + type={type} + flowTarget={flowTarget} + narrowDateRange={narrowDateRange} + hideHistogramIfEmpty={true} + AnomaliesTableComponent={AnomaliesNetworkTable} + /> + </WrapperPage> + </StickyContainer> + ) : ( + <WrapperPage> + <HeaderPage border title={ip} /> + + <NetworkEmptyPage /> + </WrapperPage> + ); + }} + </WithSource> + + <SpyRoute /> + </> + ); +}; +IPDetailsComponent.displayName = 'IPDetailsComponent'; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + + return (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, +}; + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps<typeof connector>; + +export const IPDetails = connector(React.memo(IPDetailsComponent)); diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/types.ts b/x-pack/plugins/siem/public/pages/network/ip_details/types.ts new file mode 100644 index 0000000000000..11c41fc74515e --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/ip_details/types.ts @@ -0,0 +1,53 @@ +/* + * 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 { IIndexPattern } from 'src/plugins/data/public'; + +import { ESTermQuery } from '../../../../common/typed_json'; +import { NetworkType } from '../../../store/network/model'; +import { InspectQuery, Refetch } from '../../../store/inputs/model'; +import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { GlobalTimeArgs } from '../../../containers/global_time'; + +export const type = NetworkType.details; + +export type IPDetailsComponentProps = GlobalTimeArgs & { + detailName: string; + flowTarget: FlowTarget; +}; + +export interface OwnProps { + type: NetworkType; + startDate: number; + endDate: number; + filterQuery: string | ESTermQuery; + ip: string; + skip: boolean; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; + }) => void; +} + +export type NetworkComponentsQueryProps = OwnProps & { + flowTarget: FlowTarget; +}; + +export type TlsQueryTableComponentProps = OwnProps & { + flowTarget: FlowTargetSourceDest; +}; + +export type NetworkWithIndexComponentsQueryTableProps = OwnProps & { + flowTarget: FlowTargetSourceDest; + indexPattern: IIndexPattern; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/users_query_table.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/users_query_table.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/ip_details/users_query_table.tsx rename to x-pack/plugins/siem/public/pages/network/ip_details/users_query_table.tsx diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts b/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts new file mode 100644 index 0000000000000..9d15d7ee250c9 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts @@ -0,0 +1,60 @@ +/* + * 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 { get, isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { decodeIpv6 } from '../../../lib/helpers'; +import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network'; +import { networkModel } from '../../../store/network'; +import * as i18n from '../translations'; +import { NetworkRouteType } from '../navigation/types'; +import { NetworkRouteSpyState } from '../../../utils/route/types'; + +export const type = networkModel.NetworkType.details; +const TabNameMappedToI18nKey: Record<NetworkRouteType, string> = { + [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, + [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, + [NetworkRouteType.http]: i18n.NAVIGATION_HTTP_TITLE, + [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, +}; + +export const getBreadcrumbs = ( + params: NetworkRouteSpyState, + search: string[] +): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: decodeIpv6(params.detailName), + href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ + !isEmpty(search[1]) ? search[1] : '' + }`, + }, + ]; + } + + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + return breadcrumb; +}; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx new file mode 100644 index 0000000000000..4c4f6c06ce1e1 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { AlertsView } from '../../../components/alerts_viewer'; +import { NetworkComponentQueryProps } from './types'; + +export const filterNetworkData: Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + exists: { + field: 'destination.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field": "source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field": "destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}', + }, + }, +]; + +export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( + <AlertsView {...alertsProps} pageFilters={filterNetworkData} /> +)); + +NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx b/x-pack/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts b/x-pack/plugins/siem/public/pages/network/navigation/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/index.ts rename to x-pack/plugins/siem/public/pages/network/navigation/index.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/network/navigation/nav_tabs.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/nav_tabs.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/nav_tabs.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/plugins/siem/public/pages/network/navigation/network_routes.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/network_routes.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx b/x-pack/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx rename to x-pack/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx diff --git a/x-pack/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/plugins/siem/public/pages/network/navigation/types.ts new file mode 100644 index 0000000000000..ee03bff99b967 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/network/navigation/types.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ESTermQuery } from '../../../../common/typed_json'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +import { NavTab } from '../../../components/navigation/types'; +import { FlowTargetSourceDest } from '../../../graphql/types'; +import { networkModel } from '../../../store'; +import { GlobalTimeArgs } from '../../../containers/global_time'; + +import { SetAbsoluteRangeDatePicker } from '../types'; +import { NarrowDateRange } from '../../../components/ml/types'; + +interface QueryTabBodyProps extends Pick<GlobalTimeArgs, 'setQuery' | 'deleteQuery'> { + skip: boolean; + type: networkModel.NetworkType; + startDate: number; + endDate: number; + filterQuery?: string | ESTermQuery; + narrowDateRange?: NarrowDateRange; +} + +export type NetworkComponentQueryProps = QueryTabBodyProps; + +export type IPsQueryTabBodyProps = QueryTabBodyProps & { + indexPattern: IIndexPattern; + flowTarget: FlowTargetSourceDest; +}; + +export type TlsQueryTabBodyProps = QueryTabBodyProps & { + flowTarget: FlowTargetSourceDest; + ip?: string; +}; + +export type HttpQueryTabBodyProps = QueryTabBodyProps & { + ip?: string; +}; + +export type NetworkRoutesProps = GlobalTimeArgs & { + networkPagePath: string; + type: networkModel.NetworkType; + filterQuery?: string | ESTermQuery; + indexPattern: IIndexPattern; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; +}; + +export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & + NetworkRouteType.flows & + NetworkRouteType.http & + NetworkRouteType.tls & + NetworkRouteType.alerts; + +type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & + NetworkRouteType.anomalies; + +type KeyNetworkNavTab = KeyNetworkNavTabWithoutMlPermission | KeyNetworkNavTabWithMlPermission; + +export type NetworkNavTab = Record<KeyNetworkNavTab, NavTab>; + +export enum NetworkRouteType { + flows = 'flows', + dns = 'dns', + anomalies = 'anomalies', + tls = 'tls', + http = 'http', + alerts = 'alerts', +} + +export type GetNetworkRoutePath = ( + pagePath: string, + capabilitiesFetched: boolean, + hasMlUserPermission: boolean +) => string; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts b/x-pack/plugins/siem/public/pages/network/navigation/utils.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/navigation/utils.ts rename to x-pack/plugins/siem/public/pages/network/navigation/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx b/x-pack/plugins/siem/public/pages/network/network.test.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx rename to x-pack/plugins/siem/public/pages/network/network.test.tsx index 797fef1586518..300cb83c4ce75 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/plugins/siem/public/pages/network/network.test.tsx @@ -11,7 +11,7 @@ import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import '../../mock/match_media'; -import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import { mocksSource } from '../../containers/source/mock'; import { TestProviders, mockGlobalState, apolloClientObservable } from '../../mock'; import { State, createStore } from '../../store'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx b/x-pack/plugins/siem/public/pages/network/network.tsx similarity index 98% rename from x-pack/legacy/plugins/siem/public/pages/network/network.tsx rename to x-pack/plugins/siem/public/pages/network/network.tsx index 9b1ee76e1d376..65b3142c8c074 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/network.tsx +++ b/x-pack/plugins/siem/public/pages/network/network.tsx @@ -10,7 +10,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { esQuery } from '../../../../../../src/plugins/data/public'; import { EmbeddedMap } from '../../components/embeddables/embedded_map'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; diff --git a/x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx b/x-pack/plugins/siem/public/pages/network/network_empty_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/network_empty_page.tsx rename to x-pack/plugins/siem/public/pages/network/network_empty_page.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/network/translations.ts b/x-pack/plugins/siem/public/pages/network/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/translations.ts rename to x-pack/plugins/siem/public/pages/network/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/network/types.ts b/x-pack/plugins/siem/public/pages/network/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/network/types.ts rename to x-pack/plugins/siem/public/pages/network/types.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx rename to x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx new file mode 100644 index 0000000000000..a1936cf9221f8 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx @@ -0,0 +1,123 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { useEffect, useMemo } from 'react'; +import { Position } from '@elastic/charts'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; +import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { useKibana, useUiSetting$ } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; +import { HostsType } from '../../../store/hosts/model'; + +import * as i18n from '../translations'; +import { + alertsStackByOptions, + histogramConfigs, +} from '../../../components/alerts_viewer/histogram_configs'; +import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; + +const ID = 'alertsByCategoryOverview'; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'event.module'; + +interface Props { + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + hideHeaderChildren?: boolean; + indexPattern: IIndexPattern; + query?: Query; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const AlertsByCategoryComponent: React.FC<Props> = ({ + deleteQuery, + filters = NO_FILTERS, + from, + hideHeaderChildren = false, + indexPattern, + query = DEFAULT_QUERY, + setQuery, + to, +}) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + + const kibana = useKibana(); + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.detections); + + const alertsCountViewAlertsButton = useMemo( + () => ( + <EuiButton data-test-subj="view-alerts" href={getDetectionEngineAlertUrl(urlSearch)}> + {i18n.VIEW_ALERTS} + </EuiButton> + ), + [urlSearch] + ); + + const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + legendPosition: Position.Right, + }), + [] + ); + + return ( + <MatrixHistogramContainer + endDate={to} + filterQuery={convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + })} + headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} + id={ID} + setQuery={setQuery} + sourceId="default" + startDate={from} + type={HostsType.page} + {...alertsByCategoryHistogramConfigs} + /> + ); +}; + +AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent'; + +export const AlertsByCategory = React.memo(AlertsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.test.tsx b/x-pack/plugins/siem/public/pages/overview/event_counts/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/event_counts/index.test.tsx rename to x-pack/plugins/siem/public/pages/overview/event_counts/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx b/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx new file mode 100644 index 0000000000000..f242b0d84d7c1 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { OverviewHost } from '../../../components/page/overview/overview_host'; +import { OverviewNetwork } from '../../../components/page/overview/overview_network'; +import { filterHostData } from '../../hosts/navigation/alerts_query_tab_body'; +import { useKibana } from '../../../lib/kibana'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { filterNetworkData } from '../../network/navigation/alerts_query_tab_body'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; + +const HorizontalSpacer = styled(EuiFlexItem)` + width: 24px; +`; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +interface Props { + filters?: Filter[]; + from: number; + indexPattern: IIndexPattern; + query?: Query; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const EventCountsComponent: React.FC<Props> = ({ + filters = NO_FILTERS, + from, + indexPattern, + query = DEFAULT_QUERY, + setQuery, + to, +}) => { + const kibana = useKibana(); + + return ( + <EuiFlexGroup gutterSize="none" justifyContent="spaceBetween"> + <EuiFlexItem grow={true}> + <OverviewHost + endDate={to} + filterQuery={convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: [...filters, ...filterHostData], + })} + startDate={from} + setQuery={setQuery} + /> + </EuiFlexItem> + + <HorizontalSpacer grow={false} /> + + <EuiFlexItem grow={true}> + <OverviewNetwork + endDate={to} + filterQuery={convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: [...filters, ...filterNetworkData], + })} + startDate={from} + setQuery={setQuery} + /> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; + +export const EventCounts = React.memo(EventCountsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx b/x-pack/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx rename to x-pack/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx diff --git a/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx new file mode 100644 index 0000000000000..77d6da7a7efc4 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -0,0 +1,174 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { EuiButton } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { useEffect, useMemo } from 'react'; +import uuid from 'uuid'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; +import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { + MatrixHisrogramConfigs, + MatrixHistogramOption, +} from '../../../components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; +import { navTabs } from '../../home/home_navigations'; +import { eventsStackByOptions } from '../../hosts/navigation'; +import { convertToBuildEsQuery } from '../../../lib/keury'; +import { useKibana, useUiSetting$ } from '../../../lib/kibana'; +import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; +import { HostsTableType, HostsType } from '../../../store/hosts/model'; +import { InputsModelId } from '../../../store/inputs/constants'; + +import * as i18n from '../translations'; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'event.dataset'; + +const ID = 'eventsByDatasetOverview'; + +interface Props { + combinedQueries?: string; + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + indexPattern: IIndexPattern; + indexToAdd?: string[] | null; + onlyField?: string; + query?: Query; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + showSpacer?: boolean; + to: number; +} + +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const EventsByDatasetComponent: React.FC<Props> = ({ + combinedQueries, + deleteQuery, + filters = NO_FILTERS, + from, + headerChildren, + indexPattern, + indexToAdd, + onlyField, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePickerTarget, + setQuery, + showSpacer = true, + to, +}) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, [deleteQuery, uniqueQueryId]); + + const kibana = useKibana(); + const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.hosts); + + const eventsCountViewEventsButton = useMemo( + () => ( + <EuiButton href={getTabsOnHostsUrl(HostsTableType.events, urlSearch)}> + {i18n.VIEW_EVENTS} + </EuiButton> + ), + [urlSearch] + ); + + const filterQuery = useMemo( + () => + combinedQueries == null + ? convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }) + : combinedQueries, + [combinedQueries, kibana, indexPattern, query, filters] + ); + + const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + stackByOptions: + onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, + defaultStackByOption: + onlyField != null + ? getHistogramOption(onlyField) + : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + legendPosition: Position.Right, + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + titleSize: onlyField == null ? 'm' : 's', + }), + [onlyField, defaultNumberFormat] + ); + + const headerContent = useMemo(() => { + if (onlyField == null || headerChildren != null) { + return ( + <> + {headerChildren} + {onlyField == null && eventsCountViewEventsButton} + </> + ); + } else { + return null; + } + }, [onlyField, headerChildren, eventsCountViewEventsButton]); + + return ( + <MatrixHistogramContainer + endDate={to} + filterQuery={filterQuery} + headerChildren={headerContent} + id={uniqueQueryId} + indexToAdd={indexToAdd} + setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} + setQuery={setQuery} + showSpacer={showSpacer} + sourceId="default" + startDate={from} + type={HostsType.page} + {...eventsByDatasetHistogramConfigs} + title={onlyField != null ? i18n.TOP(onlyField) : eventsByDatasetHistogramConfigs.title} + /> + ); +}; + +EventsByDatasetComponent.displayName = 'EventsByDatasetComponent'; + +export const EventsByDataset = React.memo(EventsByDatasetComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/index.tsx b/x-pack/plugins/siem/public/pages/overview/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/index.tsx rename to x-pack/plugins/siem/public/pages/overview/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx b/x-pack/plugins/siem/public/pages/overview/overview.test.tsx similarity index 95% rename from x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx rename to x-pack/plugins/siem/public/pages/overview/overview.test.tsx index b20cd84295566..c129258fa2e87 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/overview.test.tsx +++ b/x-pack/plugins/siem/public/pages/overview/overview.test.tsx @@ -15,14 +15,7 @@ import { TestProviders } from '../../mock'; import { mocksSource } from '../../containers/source/mock'; import { Overview } from './index'; -jest.mock('ui/chrome', () => ({ - getKibanaVersion: () => { - return 'v8.0.0'; - }, - breadcrumbs: { - set: jest.fn(), - }, -})); +jest.mock('../../lib/kibana'); // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx b/x-pack/plugins/siem/public/pages/overview/overview.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/overview.tsx rename to x-pack/plugins/siem/public/pages/overview/overview.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx b/x-pack/plugins/siem/public/pages/overview/overview_empty/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/overview_empty/index.tsx rename to x-pack/plugins/siem/public/pages/overview/overview_empty/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx b/x-pack/plugins/siem/public/pages/overview/sidebar/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/sidebar/index.tsx rename to x-pack/plugins/siem/public/pages/overview/sidebar/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx b/x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx similarity index 97% rename from x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx rename to x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx index 4d4d96803cd65..c972fd83cc88f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/sidebar/sidebar.tsx +++ b/x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx @@ -8,10 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { - ENABLE_NEWS_FEED_SETTING, - NEWS_FEED_URL_SETTING, -} from '../../../../../../../plugins/siem/common/constants'; +import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; import { Filters as RecentCasesFilters } from '../../../components/recent_cases/filters'; import { Filters as RecentTimelinesFilters } from '../../../components/recent_timelines/filters'; import { StatefulRecentCases } from '../../../components/recent_cases'; diff --git a/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx new file mode 100644 index 0000000000000..6357d93d7ba71 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx @@ -0,0 +1,89 @@ +/* + * 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, { useCallback } from 'react'; + +import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel'; +import { signalsHistogramOptions } from '../../detection_engine/components/signals_histogram_panel/config'; +import { useSignalIndex } from '../../../containers/detection_engine/signals/use_signal_index'; +import { SetAbsoluteRangeDatePicker } from '../../network/types'; +import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../store'; +import { InputsModelId } from '../../../store/inputs/constants'; +import * as i18n from '../translations'; + +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; +const NO_FILTERS: Filter[] = []; + +interface Props { + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + indexPattern: IIndexPattern; + /** Override all defaults, and only display this field */ + onlyField?: string; + query?: Query; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const SignalsByCategoryComponent: React.FC<Props> = ({ + deleteQuery, + filters = NO_FILTERS, + from, + headerChildren, + onlyField, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget = 'global', + setQuery, + to, +}) => { + const { signalIndexName } = useSignalIndex(); + const updateDateRangeCallback = useCallback( + (min: number, max: number) => { + setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + const defaultStackByOption = + signalsHistogramOptions.find(o => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0]; + + return ( + <SignalsHistogramPanel + deleteQuery={deleteQuery} + defaultStackByOption={defaultStackByOption} + filters={filters} + from={from} + headerChildren={headerChildren} + onlyField={onlyField} + query={query} + signalIndexName={signalIndexName} + setQuery={setQuery} + showTotalSignalsCount={true} + showLinkToSignals={onlyField == null ? true : false} + stackByOptions={onlyField == null ? signalsHistogramOptions : undefined} + legendPosition={'right'} + to={to} + title={i18n.SIGNAL_COUNT} + updateDateRange={updateDateRangeCallback} + /> + ); +}; + +SignalsByCategoryComponent.displayName = 'SignalsByCategoryComponent'; + +export const SignalsByCategory = React.memo(SignalsByCategoryComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx b/x-pack/plugins/siem/public/pages/overview/summary.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/summary.tsx rename to x-pack/plugins/siem/public/pages/overview/summary.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/translations.ts b/x-pack/plugins/siem/public/pages/overview/translations.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/overview/translations.ts rename to x-pack/plugins/siem/public/pages/overview/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx b/x-pack/plugins/siem/public/pages/timelines/index.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/timelines/index.tsx rename to x-pack/plugins/siem/public/pages/timelines/index.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/plugins/siem/public/pages/timelines/timelines_page.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx rename to x-pack/plugins/siem/public/pages/timelines/timelines_page.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/plugins/siem/public/pages/timelines/timelines_page.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx rename to x-pack/plugins/siem/public/pages/timelines/timelines_page.tsx diff --git a/x-pack/plugins/siem/public/pages/timelines/translations.ts b/x-pack/plugins/siem/public/pages/timelines/translations.ts new file mode 100644 index 0000000000000..304474bbff2c5 --- /dev/null +++ b/x-pack/plugins/siem/public/pages/timelines/translations.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PAGE_TITLE = i18n.translate('xpack.siem.timelines.pageTitle', { + defaultMessage: 'Timelines', +}); + +export const ALL_TIMELINES_PANEL_TITLE = i18n.translate( + 'xpack.siem.timelines.allTimelines.panelTitle', + { + defaultMessage: 'All timelines', + } +); + +export const ALL_TIMELINES_IMPORT_TIMELINE_TITLE = i18n.translate( + 'xpack.siem.timelines.allTimelines.importTimelineTitle', + { + defaultMessage: 'Import Timeline', + } +); + +export const ERROR_FETCHING_TIMELINES_TITLE = i18n.translate( + 'xpack.siem.timelines.allTimelines.errorFetchingTimelinesTitle', + { + defaultMessage: 'Failed to query all timelines data', + } +); diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx new file mode 100644 index 0000000000000..2f2bd70569dcd --- /dev/null +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { + AppMountParameters, + CoreSetup, + CoreStart, + PluginInitializerContext, + Plugin as IPlugin, +} from '../../../../src/core/public'; +import { + HomePublicPluginSetup, + FeatureCatalogueCategory, +} from '../../../../src/plugins/home/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { Start as NewsfeedStart } from '../../../../src/plugins/newsfeed/public'; +import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; +import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { + TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, + TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, +} from '../../triggers_actions_ui/public'; +import { SecurityPluginSetup } from '../../security/public'; +import { APP_ID, APP_NAME, APP_PATH, APP_ICON } from '../common/constants'; +import { initTelemetry } from './lib/telemetry'; +import { KibanaServices } from './lib/kibana/services'; +import { serviceNowActionType, jiraActionType } from './lib/connectors'; + +export interface SetupPlugins { + home: HomePublicPluginSetup; + security: SecurityPluginSetup; + triggers_actions_ui: TriggersActionsSetup; + usageCollection?: UsageCollectionSetup; +} + +export interface StartPlugins { + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + inspector: InspectorStart; + newsfeed?: NewsfeedStart; + triggers_actions_ui: TriggersActionsStart; + uiActions: UiActionsStart; +} + +export type StartServices = CoreStart & + StartPlugins & { + security: SecurityPluginSetup; + }; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginStart {} + +export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { + private kibanaVersion: string; + + constructor(initializerContext: PluginInitializerContext) { + this.kibanaVersion = initializerContext.env.packageInfo.version; + } + + public setup(core: CoreSetup<StartPlugins, PluginStart>, plugins: SetupPlugins) { + initTelemetry(plugins.usageCollection, APP_ID); + + plugins.home.featureCatalogue.register({ + id: APP_ID, + title: i18n.translate('xpack.siem.featureCatalogue.title', { + defaultMessage: 'SIEM', + }), + description: i18n.translate('xpack.siem.featureCatalogue.description', { + defaultMessage: 'Explore security metrics and logs for events and alerts', + }), + icon: APP_ICON, + path: APP_PATH, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + + plugins.triggers_actions_ui.actionTypeRegistry.register(serviceNowActionType()); + plugins.triggers_actions_ui.actionTypeRegistry.register(jiraActionType()); + + core.application.register({ + id: APP_ID, + title: APP_NAME, + order: 9000, + euiIconType: APP_ICON, + async mount(params: AppMountParameters) { + const [coreStart, startPlugins] = await core.getStartServices(); + const { renderApp } = await import('./app'); + const services = { + ...coreStart, + ...startPlugins, + security: plugins.security, + } as StartServices; + + return renderApp(services, params); + }, + }); + + return {}; + } + + public start(core: CoreStart, plugins: StartPlugins) { + KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion }); + + return {}; + } + + public stop() { + return {}; + } +} diff --git a/x-pack/legacy/plugins/siem/public/routes.tsx b/x-pack/plugins/siem/public/routes.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/routes.tsx rename to x-pack/plugins/siem/public/routes.tsx diff --git a/x-pack/plugins/siem/public/shared_imports.ts b/x-pack/plugins/siem/public/shared_imports.ts new file mode 100644 index 0000000000000..472006a9e55b1 --- /dev/null +++ b/x-pack/plugins/siem/public/shared_imports.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + getUseField, + getFieldValidityAndErrorMessage, + FieldHook, + FieldValidateResponse, + FIELD_TYPES, + Form, + FormData, + FormDataProvider, + FormHook, + FormSchema, + UseField, + useForm, + ValidationFunc, + VALIDATION_TYPES, +} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +export { Field, SelectField } from '../../../../src/plugins/es_ui_shared/static/forms/components'; +export { fieldValidators } from '../../../../src/plugins/es_ui_shared/static/forms/helpers'; +export { ERROR_CODE } from '../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types'; diff --git a/x-pack/legacy/plugins/siem/public/store/actions.ts b/x-pack/plugins/siem/public/store/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/actions.ts rename to x-pack/plugins/siem/public/store/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/actions.ts b/x-pack/plugins/siem/public/store/app/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/actions.ts rename to x-pack/plugins/siem/public/store/app/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/index.ts b/x-pack/plugins/siem/public/store/app/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/index.ts rename to x-pack/plugins/siem/public/store/app/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/model.ts b/x-pack/plugins/siem/public/store/app/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/model.ts rename to x-pack/plugins/siem/public/store/app/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/reducer.ts b/x-pack/plugins/siem/public/store/app/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/reducer.ts rename to x-pack/plugins/siem/public/store/app/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/app/selectors.ts b/x-pack/plugins/siem/public/store/app/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/app/selectors.ts rename to x-pack/plugins/siem/public/store/app/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/constants.ts b/x-pack/plugins/siem/public/store/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/constants.ts rename to x-pack/plugins/siem/public/store/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/actions.ts b/x-pack/plugins/siem/public/store/drag_and_drop/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/actions.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/index.ts b/x-pack/plugins/siem/public/store/drag_and_drop/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/index.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/model.ts b/x-pack/plugins/siem/public/store/drag_and_drop/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/model.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.test.ts b/x-pack/plugins/siem/public/store/drag_and_drop/reducer.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.test.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/reducer.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.ts b/x-pack/plugins/siem/public/store/drag_and_drop/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/reducer.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/drag_and_drop/selectors.ts b/x-pack/plugins/siem/public/store/drag_and_drop/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/drag_and_drop/selectors.ts rename to x-pack/plugins/siem/public/store/drag_and_drop/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/epic.ts b/x-pack/plugins/siem/public/store/epic.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/epic.ts rename to x-pack/plugins/siem/public/store/epic.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts b/x-pack/plugins/siem/public/store/hosts/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/actions.ts rename to x-pack/plugins/siem/public/store/hosts/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts b/x-pack/plugins/siem/public/store/hosts/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/helpers.test.ts rename to x-pack/plugins/siem/public/store/hosts/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts b/x-pack/plugins/siem/public/store/hosts/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/helpers.ts rename to x-pack/plugins/siem/public/store/hosts/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/index.ts b/x-pack/plugins/siem/public/store/hosts/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/index.ts rename to x-pack/plugins/siem/public/store/hosts/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts b/x-pack/plugins/siem/public/store/hosts/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/model.ts rename to x-pack/plugins/siem/public/store/hosts/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts b/x-pack/plugins/siem/public/store/hosts/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts rename to x-pack/plugins/siem/public/store/hosts/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts b/x-pack/plugins/siem/public/store/hosts/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/hosts/selectors.ts rename to x-pack/plugins/siem/public/store/hosts/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/index.ts b/x-pack/plugins/siem/public/store/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/index.ts rename to x-pack/plugins/siem/public/store/index.ts diff --git a/x-pack/plugins/siem/public/store/inputs/actions.ts b/x-pack/plugins/siem/public/store/inputs/actions.ts new file mode 100644 index 0000000000000..04cdf5246de2c --- /dev/null +++ b/x-pack/plugins/siem/public/store/inputs/actions.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; + +import { InspectQuery, Refetch, RefetchKql } from './model'; +import { InputsModelId } from './constants'; +import { Filter, SavedQuery } from '../../../../../../src/plugins/data/public'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); + +export const setAbsoluteRangeDatePicker = actionCreator<{ + id: InputsModelId; + from: number; + to: number; +}>('SET_ABSOLUTE_RANGE_DATE_PICKER'); + +export const setTimelineRangeDatePicker = actionCreator<{ + from: number; + to: number; +}>('SET_TIMELINE_RANGE_DATE_PICKER'); + +export const setRelativeRangeDatePicker = actionCreator<{ + id: InputsModelId; + fromStr: string; + toStr: string; + from: number; + to: number; +}>('SET_RELATIVE_RANGE_DATE_PICKER'); + +export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); + +export const startAutoReload = actionCreator<{ id: InputsModelId }>('START_KQL_AUTO_RELOAD'); + +export const stopAutoReload = actionCreator<{ id: InputsModelId }>('STOP_KQL_AUTO_RELOAD'); + +export const setQuery = actionCreator<{ + inputId: InputsModelId; + id: string; + loading: boolean; + refetch: Refetch | RefetchKql; + inspect: InspectQuery | null; +}>('SET_QUERY'); + +export const deleteOneQuery = actionCreator<{ + inputId: InputsModelId; + id: string; +}>('DELETE_QUERY'); + +export const setInspectionParameter = actionCreator<{ + id: string; + inputId: InputsModelId; + isInspected: boolean; + selectedInspectIndex: number; +}>('SET_INSPECTION_PARAMETER'); + +export const deleteAllQuery = actionCreator<{ id: InputsModelId }>('DELETE_ALL_QUERY'); + +export const toggleTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>( + 'TOGGLE_TIMELINE_LINK_TO' +); + +export const removeTimelineLinkTo = actionCreator('REMOVE_TIMELINE_LINK_TO'); +export const addTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_TIMELINE_LINK_TO'); + +export const removeGlobalLinkTo = actionCreator('REMOVE_GLOBAL_LINK_TO'); +export const addGlobalLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_GLOBAL_LINK_TO'); + +export const setFilterQuery = actionCreator<{ + id: InputsModelId; + query: string | { [key: string]: unknown }; + language: string; +}>('SET_FILTER_QUERY'); + +export const setSavedQuery = actionCreator<{ + id: InputsModelId; + savedQuery: SavedQuery | undefined; +}>('SET_SAVED_QUERY'); + +export const setSearchBarFilter = actionCreator<{ + id: InputsModelId; + filters: Filter[]; +}>('SET_SEARCH_BAR_FILTER'); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/constants.ts b/x-pack/plugins/siem/public/store/inputs/constants.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/constants.ts rename to x-pack/plugins/siem/public/store/inputs/constants.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/helpers.test.ts b/x-pack/plugins/siem/public/store/inputs/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/helpers.test.ts rename to x-pack/plugins/siem/public/store/inputs/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/helpers.ts b/x-pack/plugins/siem/public/store/inputs/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/helpers.ts rename to x-pack/plugins/siem/public/store/inputs/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/index.ts b/x-pack/plugins/siem/public/store/inputs/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/index.ts rename to x-pack/plugins/siem/public/store/inputs/index.ts diff --git a/x-pack/plugins/siem/public/store/inputs/model.ts b/x-pack/plugins/siem/public/store/inputs/model.ts new file mode 100644 index 0000000000000..3e6be6ce859e5 --- /dev/null +++ b/x-pack/plugins/siem/public/store/inputs/model.ts @@ -0,0 +1,103 @@ +/* + * 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 { Dispatch } from 'redux'; +import { InputsModelId } from './constants'; +import { CONSTANTS } from '../../components/url_state/constants'; +import { Query, Filter, SavedQuery } from '../../../../../../src/plugins/data/public'; + +export interface AbsoluteTimeRange { + kind: 'absolute'; + fromStr: undefined; + toStr: undefined; + from: number; + to: number; +} + +export interface RelativeTimeRange { + kind: 'relative'; + fromStr: string; + toStr: string; + from: number; + to: number; +} + +export const isRelativeTimeRange = ( + timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange +): timeRange is RelativeTimeRange => timeRange.kind === 'relative'; + +export const isAbsoluteTimeRange = ( + timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange +): timeRange is AbsoluteTimeRange => timeRange.kind === 'absolute'; + +export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; + +export type URLTimeRange = Omit<TimeRange, 'from' | 'to'> & { + from: string | TimeRange['from']; + to: string | TimeRange['to']; +}; + +export interface Policy { + kind: 'manual' | 'interval'; + duration: number; // in ms +} + +interface InspectVariables { + inspect: boolean; +} +export type RefetchWithParams = ({ inspect }: InspectVariables) => void; +export type RefetchKql = (dispatch: Dispatch) => boolean; +export type Refetch = () => void; + +export interface InspectQuery { + dsl: string[]; + response: string[]; +} + +export interface GlobalGenericQuery { + inspect: InspectQuery | null; + isInspected: boolean; + loading: boolean; + selectedInspectIndex: number; +} + +export interface GlobalGraphqlQuery extends GlobalGenericQuery { + id: string; + refetch: null | Refetch | RefetchWithParams; +} +export interface GlobalKqlQuery extends GlobalGenericQuery { + id: 'kql'; + refetch: RefetchKql; +} + +export type GlobalQuery = GlobalGraphqlQuery | GlobalKqlQuery; + +export interface InputsRange { + timerange: TimeRange; + policy: Policy; + queries: GlobalQuery[]; + linkTo: InputsModelId[]; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; +} + +export interface LinkTo { + linkTo: InputsModelId[]; +} + +export interface InputsModel { + global: InputsRange; + timeline: InputsRange; +} +export interface UrlInputsModelInputs { + linkTo: InputsModelId[]; + [CONSTANTS.timerange]: TimeRange; +} +export interface UrlInputsModel { + global: UrlInputsModelInputs; + timeline: UrlInputsModelInputs; +} diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts b/x-pack/plugins/siem/public/store/inputs/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/reducer.ts rename to x-pack/plugins/siem/public/store/inputs/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts b/x-pack/plugins/siem/public/store/inputs/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/inputs/selectors.ts rename to x-pack/plugins/siem/public/store/inputs/selectors.ts diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts new file mode 100644 index 0000000000000..686dc096e61b0 --- /dev/null +++ b/x-pack/plugins/siem/public/store/model.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { appModel } from './app'; +export { dragAndDropModel } from './drag_and_drop'; +export { hostsModel } from './hosts'; +export { inputsModel } from './inputs'; +export { networkModel } from './network'; +export * from './types'; diff --git a/x-pack/legacy/plugins/siem/public/store/network/actions.ts b/x-pack/plugins/siem/public/store/network/actions.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/actions.ts rename to x-pack/plugins/siem/public/store/network/actions.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts b/x-pack/plugins/siem/public/store/network/helpers.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/helpers.test.ts rename to x-pack/plugins/siem/public/store/network/helpers.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/helpers.ts b/x-pack/plugins/siem/public/store/network/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/helpers.ts rename to x-pack/plugins/siem/public/store/network/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/index.ts b/x-pack/plugins/siem/public/store/network/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/index.ts rename to x-pack/plugins/siem/public/store/network/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/model.ts b/x-pack/plugins/siem/public/store/network/model.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/model.ts rename to x-pack/plugins/siem/public/store/network/model.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/reducer.ts b/x-pack/plugins/siem/public/store/network/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/reducer.ts rename to x-pack/plugins/siem/public/store/network/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/network/selectors.ts b/x-pack/plugins/siem/public/store/network/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/network/selectors.ts rename to x-pack/plugins/siem/public/store/network/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/reducer.ts b/x-pack/plugins/siem/public/store/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/reducer.ts rename to x-pack/plugins/siem/public/store/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/selectors.ts b/x-pack/plugins/siem/public/store/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/selectors.ts rename to x-pack/plugins/siem/public/store/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/store.ts b/x-pack/plugins/siem/public/store/store.ts similarity index 96% rename from x-pack/legacy/plugins/siem/public/store/store.ts rename to x-pack/plugins/siem/public/store/store.ts index d3559e7a7adde..2af0f87b4494d 100644 --- a/x-pack/legacy/plugins/siem/public/store/store.ts +++ b/x-pack/plugins/siem/public/store/store.ts @@ -32,6 +32,7 @@ export const createStore = ( const middlewareDependencies = { apolloClient$: apolloClient, + selectAllTimelineQuery: inputsSelectors.globalQueryByIdSelector, selectNotesByIdSelector: appSelectors.selectNotesByIdSelector, timelineByIdSelector: timelineSelectors.timelineByIdSelector, timelineTimeRangeSelector: inputsSelectors.timelineTimeRangeSelector, diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts new file mode 100644 index 0000000000000..12155decf40d4 --- /dev/null +++ b/x-pack/plugins/siem/public/store/timeline/actions.ts @@ -0,0 +1,249 @@ +/* + * 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 actionCreatorFactory from 'typescript-fsa'; + +import { Filter } from '../../../../../../src/plugins/data/public'; +import { Sort } from '../../components/timeline/body/sort'; +import { + DataProvider, + QueryOperator, +} from '../../components/timeline/data_providers/data_provider'; +import { KueryFilterQuery, SerializedFilterQuery } from '../types'; + +import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; +import { TimelineNonEcsData } from '../../graphql/types'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline'); + +export const addHistory = actionCreator<{ id: string; historyId: string }>('ADD_HISTORY'); + +export const addNote = actionCreator<{ id: string; noteId: string }>('ADD_NOTE'); + +export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventId: string }>( + 'ADD_NOTE_TO_EVENT' +); + +export const upsertColumn = actionCreator<{ + column: ColumnHeaderOptions; + id: string; + index: number; +}>('UPSERT_COLUMN'); + +export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); + +export const applyDeltaToWidth = actionCreator<{ + id: string; + delta: number; + bodyClientWidthPixels: number; + minWidthPixels: number; + maxWidthPercent: number; +}>('APPLY_DELTA_TO_WIDTH'); + +export const applyDeltaToColumnWidth = actionCreator<{ + id: string; + columnId: string; + delta: number; +}>('APPLY_DELTA_TO_COLUMN_WIDTH'); + +export const createTimeline = actionCreator<{ + id: string; + dataProviders?: DataProvider[]; + dateRange?: { + start: number; + end: number; + }; + filters?: Filter[]; + columns: ColumnHeaderOptions[]; + itemsPerPage?: number; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + show?: boolean; + sort?: Sort; + showCheckboxes?: boolean; + showRowRenderers?: boolean; +}>('CREATE_TIMELINE'); + +export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); + +export const removeColumn = actionCreator<{ + id: string; + columnId: string; +}>('REMOVE_COLUMN'); + +export const removeProvider = actionCreator<{ + id: string; + providerId: string; + andProviderId?: string; +}>('REMOVE_PROVIDER'); + +export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE'); + +export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT'); + +export const updateTimeline = actionCreator<{ + id: string; + timeline: TimelineModel; +}>('UPDATE_TIMELINE'); + +export const addTimeline = actionCreator<{ + id: string; + timeline: TimelineModel; +}>('ADD_TIMELINE'); + +export const startTimelineSaving = actionCreator<{ + id: string; +}>('START_TIMELINE_SAVING'); + +export const endTimelineSaving = actionCreator<{ + id: string; +}>('END_TIMELINE_SAVING'); + +export const updateIsLoading = actionCreator<{ + id: string; + isLoading: boolean; +}>('UPDATE_LOADING'); + +export const updateColumns = actionCreator<{ + id: string; + columns: ColumnHeaderOptions[]; +}>('UPDATE_COLUMNS'); + +export const updateDataProviderEnabled = actionCreator<{ + id: string; + enabled: boolean; + providerId: string; + andProviderId?: string; +}>('TOGGLE_PROVIDER_ENABLED'); + +export const updateDataProviderExcluded = actionCreator<{ + id: string; + excluded: boolean; + providerId: string; + andProviderId?: string; +}>('TOGGLE_PROVIDER_EXCLUDED'); + +export const dataProviderEdited = actionCreator<{ + andProviderId?: string; + excluded: boolean; + field: string; + id: string; + operator: QueryOperator; + providerId: string; + value: string | number; +}>('DATA_PROVIDER_EDITED'); + +export const updateDataProviderKqlQuery = actionCreator<{ + id: string; + kqlQuery: string; + providerId: string; +}>('PROVIDER_EDIT_KQL_QUERY'); + +export const updateHighlightedDropAndProviderId = actionCreator<{ + id: string; + providerId: string; +}>('UPDATE_DROP_AND_PROVIDER'); + +export const updateDescription = actionCreator<{ id: string; description: string }>( + 'UPDATE_DESCRIPTION' +); + +export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); + +export const setKqlFilterQueryDraft = actionCreator<{ + id: string; + filterQueryDraft: KueryFilterQuery; +}>('SET_KQL_FILTER_QUERY_DRAFT'); + +export const applyKqlFilterQuery = actionCreator<{ + id: string; + filterQuery: SerializedFilterQuery; +}>('APPLY_KQL_FILTER_QUERY'); + +export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean }>( + 'UPDATE_IS_FAVORITE' +); + +export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE'); + +export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( + 'UPDATE_ITEMS_PER_PAGE' +); + +export const updateItemsPerPageOptions = actionCreator<{ + id: string; + itemsPerPageOptions: number[]; +}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); + +export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); + +export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( + 'UPDATE_PAGE_INDEX' +); + +export const updateProviders = actionCreator<{ id: string; providers: DataProvider[] }>( + 'UPDATE_PROVIDERS' +); + +export const updateRange = actionCreator<{ id: string; start: number; end: number }>( + 'UPDATE_RANGE' +); + +export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); + +export const updateAutoSaveMsg = actionCreator<{ + timelineId: string | null; + newTimelineModel: TimelineModel | null; +}>('UPDATE_AUTO_SAVE'); + +export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); + +export const setSavedQueryId = actionCreator<{ + id: string; + savedQueryId: string | null; +}>('SET_TIMELINE_SAVED_QUERY'); + +export const setFilters = actionCreator<{ + id: string; + filters: Filter[]; +}>('SET_TIMELINE_FILTERS'); + +export const setSelected = actionCreator<{ + id: string; + eventIds: Readonly<Record<string, TimelineNonEcsData[]>>; + isSelected: boolean; + isSelectAllChecked: boolean; +}>('SET_TIMELINE_SELECTED'); + +export const clearSelected = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_SELECTED'); + +export const setEventsLoading = actionCreator<{ + id: string; + eventIds: string[]; + isLoading: boolean; +}>('SET_TIMELINE_EVENTS_LOADING'); + +export const clearEventsLoading = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_LOADING'); + +export const setEventsDeleted = actionCreator<{ + id: string; + eventIds: string[]; + isDeleted: boolean; +}>('SET_TIMELINE_EVENTS_DELETED'); + +export const clearEventsDeleted = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_DELETED'); + +export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( + 'UPDATE_EVENT_TYPE' +); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts b/x-pack/plugins/siem/public/store/timeline/defaults.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/defaults.ts rename to x-pack/plugins/siem/public/store/timeline/defaults.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts b/x-pack/plugins/siem/public/store/timeline/epic.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts rename to x-pack/plugins/siem/public/store/timeline/epic.test.ts index 13d30825a169c..bb6babcba41ae 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.test.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic.test.ts @@ -8,7 +8,7 @@ import { TimelineModel } from './model'; import { Direction } from '../../graphql/types'; import { convertTimelineAsInput } from './epic'; -import { Filter, esFilters } from '../../../../../../../src/plugins/data/public'; +import { Filter, esFilters } from '../../../../../../src/plugins/data/public'; describe('Epic Timeline', () => { describe('#convertTimelineAsInput ', () => { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/plugins/siem/public/store/timeline/epic.ts similarity index 85% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic.ts rename to x-pack/plugins/siem/public/store/timeline/epic.ts index e6acff8736492..6812d8d8aa672 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic.ts @@ -28,18 +28,12 @@ import { takeUntil, } from 'rxjs/operators'; -import { esFilters, Filter, MatchAllFilter } from '../../../../../../../src/plugins/data/public'; -import { persistTimelineMutation } from '../../containers/timeline/persist.gql_query'; -import { - PersistTimelineMutation, - TimelineInput, - ResponseTimeline, - TimelineResult, -} from '../../graphql/types'; +import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public'; +import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types'; import { AppApolloClient } from '../../lib/lib'; import { addError } from '../app/actions'; import { NotesById } from '../app/model'; -import { TimeRange } from '../inputs/model'; +import { inputsModel } from '../inputs'; import { applyKqlFilterQuery, @@ -75,13 +69,15 @@ import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_p import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite'; import { isNotNull } from './helpers'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; -import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { ActionTimeline, TimelineById } from './types'; +import { persistTimeline } from '../../containers/timeline/api'; +import { ALL_TIMELINE_QUERY_ID } from '../../containers/timeline/all'; interface TimelineEpicDependencies<State> { timelineByIdSelector: (state: State) => TimelineById; - timelineTimeRangeSelector: (state: State) => TimeRange; + timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange; + selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; selectNotesByIdSelector: (state: State) => NotesById; apolloClient$: Observable<AppApolloClient>; } @@ -119,10 +115,24 @@ export const createTimelineEpic = <State>(): Epic< > => ( action$, state$, - { selectNotesByIdSelector, timelineByIdSelector, timelineTimeRangeSelector, apolloClient$ } + { + selectAllTimelineQuery, + selectNotesByIdSelector, + timelineByIdSelector, + timelineTimeRangeSelector, + apolloClient$, + } ) => { const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull)); + const allTimelineQuery$ = state$.pipe( + map(state => { + const getQuery = selectAllTimelineQuery(); + return getQuery(state, ALL_TIMELINE_QUERY_ID); + }), + filter(isNotNull) + ); + const notes$ = state$.pipe(map(selectNotesByIdSelector), filter(isNotNull)); const timelineTimeRange$ = state$.pipe(map(timelineTimeRangeSelector), filter(isNotNull)); @@ -168,33 +178,52 @@ export const createTimelineEpic = <State>(): Epic< const version = myEpicTimelineId.getTimelineVersion(); if (timelineNoteActionsType.includes(action.type)) { - return epicPersistNote(apolloClient, action, timeline, notes, action$, timeline$, notes$); + return epicPersistNote( + apolloClient, + action, + timeline, + notes, + action$, + timeline$, + notes$, + allTimelineQuery$ + ); } else if (timelinePinnedEventActionsType.includes(action.type)) { - return epicPersistPinnedEvent(apolloClient, action, timeline, action$, timeline$); + return epicPersistPinnedEvent( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); } else if (timelineFavoriteActionsType.includes(action.type)) { - return epicPersistTimelineFavorite(apolloClient, action, timeline, action$, timeline$); + return epicPersistTimelineFavorite( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); } else if (timelineActionsType.includes(action.type)) { return from( - apolloClient.mutate< - PersistTimelineMutation.Mutation, - PersistTimelineMutation.Variables - >({ - mutation: persistTimelineMutation, - fetchPolicy: 'no-cache', - variables: { - timelineId, - version, - timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), - }, - refetchQueries, + persistTimeline({ + timelineId, + version, + timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimeline]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimeline, allTimelineQuery]) => { const savedTimeline = recentTimeline[action.payload.id]; const response: ResponseTimeline = get('data.persistTimeline', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ response.code === 409 ? updateAutoSaveMsg({ @@ -261,7 +290,7 @@ const timelineInput: TimelineInput = { export const convertTimelineAsInput = ( timeline: TimelineModel, - timelineTimeRange: TimeRange + timelineTimeRange: inputsModel.TimeRange ): TimelineInput => Object.keys(timelineInput).reduce<TimelineInput>((acc, key) => { if (has(key, timeline)) { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts b/x-pack/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts rename to x-pack/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts b/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts similarity index 91% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts rename to x-pack/plugins/siem/public/store/timeline/epic_favorite.ts index 4d1b73aa70a6e..6a1dadb8a59f5 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts @@ -26,6 +26,7 @@ import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persi import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { ActionTimeline, TimelineById } from './types'; +import { inputsModel } from '../inputs'; export const timelineFavoriteActionsType = [updateIsFavorite.type]; @@ -34,7 +35,8 @@ export const epicPersistTimelineFavorite = ( action: ActionTimeline, timeline: TimelineById, action$: Observable<Action>, - timeline$: Observable<TimelineById> + timeline$: Observable<TimelineById>, + allTimelineQuery$: Observable<inputsModel.GlobalQuery> // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable<any> => from( @@ -50,12 +52,16 @@ export const epicPersistTimelineFavorite = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimelines]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimelines, allTimelineQuery]) => { const savedTimeline = recentTimelines[action.payload.id]; const response: ResponseFavoriteTimeline = get('data.persistFavorite', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ ...callOutMsg, updateTimeline({ diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts b/x-pack/plugins/siem/public/store/timeline/epic_note.ts similarity index 92% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts rename to x-pack/plugins/siem/public/store/timeline/epic_note.ts index e5a712fe2c666..3722a6ad8036c 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_note.ts @@ -16,6 +16,7 @@ import { persistTimelineNoteMutation } from '../../containers/timeline/notes/per import { PersistTimelineNoteMutation, ResponseNote } from '../../graphql/types'; import { updateNote, addError } from '../app/actions'; import { NotesById } from '../app/model'; +import { inputsModel } from '../inputs'; import { addNote, @@ -39,7 +40,8 @@ export const epicPersistNote = ( notes: NotesById, action$: Observable<Action>, timeline$: Observable<TimelineById>, - notes$: Observable<NotesById> + notes$: Observable<NotesById>, + allTimelineQuery$: Observable<inputsModel.GlobalQuery> // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable<any> => from( @@ -61,12 +63,16 @@ export const epicPersistNote = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$, notes$), - mergeMap(([result, recentTimeline, recentNotes]) => { + withLatestFrom(timeline$, notes$, allTimelineQuery$), + mergeMap(([result, recentTimeline, recentNotes, allTimelineQuery]) => { const noteIdRedux = action.payload.noteId; const response: ResponseNote = get('data.persistNote', result); const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ ...callOutMsg, recentTimeline[action.payload.id].savedObjectId == null diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts similarity index 93% rename from x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts rename to x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts index 2260999a91e7b..a1281250ba72a 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts @@ -15,6 +15,8 @@ import { filter, mergeMap, startWith, withLatestFrom, takeUntil } from 'rxjs/ope import { persistTimelinePinnedEventMutation } from '../../containers/timeline/pinned_event/persist.gql_query'; import { PersistTimelinePinnedEventMutation, PinnedEvent } from '../../graphql/types'; import { addError } from '../app/actions'; +import { inputsModel } from '../inputs'; + import { pinEvent, endTimelineSaving, @@ -35,7 +37,8 @@ export const epicPersistPinnedEvent = ( action: ActionTimeline, timeline: TimelineById, action$: Observable<Action>, - timeline$: Observable<TimelineById> + timeline$: Observable<TimelineById>, + allTimelineQuery$: Observable<inputsModel.GlobalQuery> // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Observable<any> => from( @@ -57,12 +60,16 @@ export const epicPersistPinnedEvent = ( refetchQueries, }) ).pipe( - withLatestFrom(timeline$), - mergeMap(([result, recentTimeline]) => { + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimeline, allTimelineQuery]) => { const savedTimeline = recentTimeline[action.payload.id]; const response: PinnedEvent = get('data.persistPinnedEventOnTimeline', result); const callOutMsg = response && response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + return [ response != null ? updateTimeline({ diff --git a/x-pack/plugins/siem/public/store/timeline/helpers.ts b/x-pack/plugins/siem/public/store/timeline/helpers.ts new file mode 100644 index 0000000000000..19de49918d100 --- /dev/null +++ b/x-pack/plugins/siem/public/store/timeline/helpers.ts @@ -0,0 +1,1324 @@ +/* + * 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 { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; + +import { Filter } from '../../../../../../src/plugins/data/public'; +import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers'; +import { Sort } from '../../components/timeline/body/sort'; +import { + DataProvider, + QueryOperator, + QueryMatch, +} from '../../components/timeline/data_providers/data_provider'; +import { KueryFilterQuery, SerializedFilterQuery } from '../model'; + +import { timelineDefaults } from './defaults'; +import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; +import { TimelineById, TimelineState } from './types'; +import { TimelineNonEcsData } from '../../graphql/types'; + +const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference + +export const isNotNull = <T>(value: T | null): value is T => value !== null; + +export const initialTimelineState: TimelineState = { + timelineById: EMPTY_TIMELINE_BY_ID, + autoSavedWarningMsg: { + timelineId: null, + newTimelineModel: null, + }, + showCallOutUnauthorizedMsg: false, +}; + +interface AddTimelineHistoryParams { + id: string; + historyId: string; + timelineById: TimelineById; +} + +export const addTimelineHistory = ({ + id, + historyId, + timelineById, +}: AddTimelineHistoryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + historyIds: uniq([...timeline.historyIds, historyId]), + }, + }; +}; + +interface AddTimelineNoteParams { + id: string; + noteId: string; + timelineById: TimelineById; +} + +export const addTimelineNote = ({ + id, + noteId, + timelineById, +}: AddTimelineNoteParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + noteIds: [...timeline.noteIds, noteId], + }, + }; +}; + +interface AddTimelineNoteToEventParams { + id: string; + noteId: string; + eventId: string; + timelineById: TimelineById; +} + +export const addTimelineNoteToEvent = ({ + id, + noteId, + eventId, + timelineById, +}: AddTimelineNoteToEventParams): TimelineById => { + const timeline = timelineById[id]; + const existingNoteIds = getOr([], `eventIdToNoteIds.${eventId}`, timeline); + + return { + ...timelineById, + [id]: { + ...timeline, + eventIdToNoteIds: { + ...timeline.eventIdToNoteIds, + ...{ [eventId]: uniq([...existingNoteIds, noteId]) }, + }, + }, + }; +}; + +interface AddTimelineParams { + id: string; + timeline: TimelineModel; + timelineById: TimelineById; +} + +/** + * Add a saved object timeline to the store + * and default the value to what need to be if values are null + */ +export const addTimelineToStore = ({ + id, + timeline, + timelineById, +}: AddTimelineParams): TimelineById => ({ + ...timelineById, + [id]: { + ...timeline, + isLoading: timelineById[id].isLoading, + }, +}); + +interface AddNewTimelineParams { + columns: ColumnHeaderOptions[]; + dataProviders?: DataProvider[]; + dateRange?: { + start: number; + end: number; + }; + filters?: Filter[]; + id: string; + itemsPerPage?: number; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + show?: boolean; + sort?: Sort; + showCheckboxes?: boolean; + showRowRenderers?: boolean; + timelineById: TimelineById; +} + +/** Adds a new `Timeline` to the provided collection of `TimelineById` */ +export const addNewTimeline = ({ + columns, + dataProviders = [], + dateRange = { start: 0, end: 0 }, + filters = timelineDefaults.filters, + id, + itemsPerPage = timelineDefaults.itemsPerPage, + kqlQuery = { filterQuery: null, filterQueryDraft: null }, + sort = timelineDefaults.sort, + show = false, + showCheckboxes = false, + showRowRenderers = true, + timelineById, +}: AddNewTimelineParams): TimelineById => ({ + ...timelineById, + [id]: { + id, + ...timelineDefaults, + columns, + dataProviders, + dateRange, + filters, + itemsPerPage, + kqlQuery, + sort, + show, + savedObjectId: null, + version: null, + isSaving: false, + isLoading: false, + showCheckboxes, + showRowRenderers, + }, +}); + +interface PinTimelineEventParams { + id: string; + eventId: string; + timelineById: TimelineById; +} + +export const pinTimelineEvent = ({ + id, + eventId, + timelineById, +}: PinTimelineEventParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + pinnedEventIds: { + ...timeline.pinnedEventIds, + ...{ [eventId]: true }, + }, + }, + }; +}; + +interface UpdateShowTimelineProps { + id: string; + show: boolean; + timelineById: TimelineById; +} + +export const updateTimelineShowTimeline = ({ + id, + show, + timelineById, +}: UpdateShowTimelineProps): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + show, + }, + }; +}; + +interface ApplyDeltaToCurrentWidthParams { + id: string; + delta: number; + bodyClientWidthPixels: number; + minWidthPixels: number; + maxWidthPercent: number; + timelineById: TimelineById; +} + +export const applyDeltaToCurrentWidth = ({ + id, + delta, + bodyClientWidthPixels, + minWidthPixels, + maxWidthPercent, + timelineById, +}: ApplyDeltaToCurrentWidthParams): TimelineById => { + const timeline = timelineById[id]; + + const requestedWidth = timeline.width + delta * -1; // raw change in width + const maxWidthPixels = (maxWidthPercent / 100) * bodyClientWidthPixels; + const clampedWidth = Math.min(requestedWidth, maxWidthPixels); + const width = Math.max(minWidthPixels, clampedWidth); // if the clamped width is smaller than the min, use the min + + return { + ...timelineById, + [id]: { + ...timeline, + width, + }, + }; +}; + +const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { + if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { + return true; + } + return false; +}; + +const addAndToProviderInTimeline = ( + id: string, + provider: DataProvider, + timeline: TimelineModel, + timelineById: TimelineById +): TimelineById => { + const alreadyExistsProviderIndex = timeline.dataProviders.findIndex( + p => p.id === timeline.highlightedDropAndProviderId + ); + const newProvider = timeline.dataProviders[alreadyExistsProviderIndex]; + const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id); + const { and, ...andProvider } = provider; + + if ( + isEqualWith(queryMatchCustomizer, newProvider.queryMatch, andProvider.queryMatch) || + (alreadyExistsAndProviderIndex === -1 && + newProvider.and.filter(itemAndProvider => + isEqualWith(queryMatchCustomizer, itemAndProvider.queryMatch, andProvider.queryMatch) + ).length > 0) + ) { + return timelineById; + } + + const dataProviders = [ + ...timeline.dataProviders.slice(0, alreadyExistsProviderIndex), + { + ...timeline.dataProviders[alreadyExistsProviderIndex], + and: + alreadyExistsAndProviderIndex > -1 + ? [ + ...newProvider.and.slice(0, alreadyExistsAndProviderIndex), + andProvider, + ...newProvider.and.slice(alreadyExistsAndProviderIndex + 1), + ] + : [...newProvider.and, andProvider], + }, + ...timeline.dataProviders.slice(alreadyExistsProviderIndex + 1), + ]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; + +const addProviderToTimeline = ( + id: string, + provider: DataProvider, + timeline: TimelineModel, + timelineById: TimelineById +): TimelineById => { + const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id); + + if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { + provider.id = `${provider.id}-${ + timeline.dataProviders.filter(p => p.id === provider.id).length + }`; + } + + const dataProviders = + alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) + ? [ + ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), + provider, + ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), + ] + : [...timeline.dataProviders, provider]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; + +interface AddTimelineColumnParams { + column: ColumnHeaderOptions; + id: string; + index: number; + timelineById: TimelineById; +} + +/** + * Adds or updates a column. When updating a column, it will be moved to the + * new index + */ +export const upsertTimelineColumn = ({ + column, + id, + index, + timelineById, +}: AddTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + const alreadyExistsAtIndex = timeline.columns.findIndex(c => c.id === column.id); + + if (alreadyExistsAtIndex !== -1) { + // remove the existing entry and add the new one at the specified index + const reordered = timeline.columns.filter(c => c.id !== column.id); + reordered.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns: reordered, + }, + }; + } + + // add the new entry at the specified index + const columns = [...timeline.columns]; + columns.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface RemoveTimelineColumnParams { + id: string; + columnId: string; + timelineById: TimelineById; +} + +export const removeTimelineColumn = ({ + id, + columnId, + timelineById, +}: RemoveTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + + const columns = timeline.columns.filter(c => c.id !== columnId); + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface ApplyDeltaToTimelineColumnWidth { + id: string; + columnId: string; + delta: number; + timelineById: TimelineById; +} + +export const applyDeltaToTimelineColumnWidth = ({ + id, + columnId, + delta, + timelineById, +}: ApplyDeltaToTimelineColumnWidth): TimelineById => { + const timeline = timelineById[id]; + + const columnIndex = timeline.columns.findIndex(c => c.id === columnId); + if (columnIndex === -1) { + // the column was not found + return { + ...timelineById, + [id]: { + ...timeline, + }, + }; + } + const minWidthPixels = getColumnWidthFromType(timeline.columns[columnIndex].type!); + const requestedWidth = timeline.columns[columnIndex].width + delta; // raw change in width + const width = Math.max(minWidthPixels, requestedWidth); // if the requested width is smaller than the min, use the min + + const columnWithNewWidth = { + ...timeline.columns[columnIndex], + width, + }; + + const columns = [ + ...timeline.columns.slice(0, columnIndex), + columnWithNewWidth, + ...timeline.columns.slice(columnIndex + 1), + ]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface AddTimelineProviderParams { + id: string; + provider: DataProvider; + timelineById: TimelineById; +} + +export const addTimelineProvider = ({ + id, + provider, + timelineById, +}: AddTimelineProviderParams): TimelineById => { + const timeline = timelineById[id]; + + if (timeline.highlightedDropAndProviderId !== '') { + return addAndToProviderInTimeline(id, provider, timeline, timelineById); + } else { + return addProviderToTimeline(id, provider, timeline, timelineById); + } +}; + +interface ApplyKqlFilterQueryDraftParams { + id: string; + filterQuery: SerializedFilterQuery; + timelineById: TimelineById; +} + +export const applyKqlFilterQueryDraft = ({ + id, + filterQuery, + timelineById, +}: ApplyKqlFilterQueryDraftParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlQuery: { + ...timeline.kqlQuery, + filterQuery, + }, + }, + }; +}; + +interface UpdateTimelineKqlModeParams { + id: string; + kqlMode: KqlMode; + timelineById: TimelineById; +} + +export const updateTimelineKqlMode = ({ + id, + kqlMode, + timelineById, +}: UpdateTimelineKqlModeParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlMode, + }, + }; +}; + +interface UpdateKqlFilterQueryDraftParams { + id: string; + filterQueryDraft: KueryFilterQuery; + timelineById: TimelineById; +} + +export const updateKqlFilterQueryDraft = ({ + id, + filterQueryDraft, + timelineById, +}: UpdateKqlFilterQueryDraftParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlQuery: { + ...timeline.kqlQuery, + filterQueryDraft, + }, + }, + }; +}; + +interface UpdateTimelineColumnsParams { + id: string; + columns: ColumnHeaderOptions[]; + timelineById: TimelineById; +} + +export const updateTimelineColumns = ({ + id, + columns, + timelineById, +}: UpdateTimelineColumnsParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface UpdateTimelineDescriptionParams { + id: string; + description: string; + timelineById: TimelineById; +} + +export const updateTimelineDescription = ({ + id, + description, + timelineById, +}: UpdateTimelineDescriptionParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + description: description.endsWith(' ') ? `${description.trim()} ` : description.trim(), + }, + }; +}; + +interface UpdateTimelineTitleParams { + id: string; + title: string; + timelineById: TimelineById; +} + +export const updateTimelineTitle = ({ + id, + title, + timelineById, +}: UpdateTimelineTitleParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + title: title.endsWith(' ') ? `${title.trim()} ` : title.trim(), + }, + }; +}; + +interface UpdateTimelineEventTypeParams { + id: string; + eventType: EventType; + timelineById: TimelineById; +} + +export const updateTimelineEventType = ({ + id, + eventType, + timelineById, +}: UpdateTimelineEventTypeParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + eventType, + }, + }; +}; + +interface UpdateTimelineIsFavoriteParams { + id: string; + isFavorite: boolean; + timelineById: TimelineById; +} + +export const updateTimelineIsFavorite = ({ + id, + isFavorite, + timelineById, +}: UpdateTimelineIsFavoriteParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + isFavorite, + }, + }; +}; + +interface UpdateTimelineIsLiveParams { + id: string; + isLive: boolean; + timelineById: TimelineById; +} + +export const updateTimelineIsLive = ({ + id, + isLive, + timelineById, +}: UpdateTimelineIsLiveParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + isLive, + }, + }; +}; + +interface UpdateTimelineProvidersParams { + id: string; + providers: DataProvider[]; + timelineById: TimelineById; +} + +export const updateTimelineProviders = ({ + id, + providers, + timelineById, +}: UpdateTimelineProvidersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: providers, + }, + }; +}; + +interface UpdateTimelineRangeParams { + id: string; + start: number; + end: number; + timelineById: TimelineById; +} + +export const updateTimelineRange = ({ + id, + start, + end, + timelineById, +}: UpdateTimelineRangeParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dateRange: { + start, + end, + }, + }, + }; +}; + +interface UpdateTimelineSortParams { + id: string; + sort: Sort; + timelineById: TimelineById; +} + +export const updateTimelineSort = ({ + id, + sort, + timelineById, +}: UpdateTimelineSortParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + sort, + }, + }; +}; + +const updateEnabledAndProvider = ( + andProviderId: string, + enabled: boolean, + providerId: string, + timeline: TimelineModel +) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId ? { ...andProvider, enabled } : andProvider + ), + } + : provider + ); + +const updateEnabledProvider = (enabled: boolean, providerId: string, timeline: TimelineModel) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + enabled, + } + : provider + ); + +interface UpdateTimelineProviderEnabledParams { + id: string; + providerId: string; + enabled: boolean; + timelineById: TimelineById; + andProviderId?: string; +} + +export const updateTimelineProviderEnabled = ({ + id, + providerId, + enabled, + timelineById, + andProviderId, +}: UpdateTimelineProviderEnabledParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateEnabledAndProvider(andProviderId, enabled, providerId, timeline) + : updateEnabledProvider(enabled, providerId, timeline), + }, + }; +}; + +const updateExcludedAndProvider = ( + andProviderId: string, + excluded: boolean, + providerId: string, + timeline: TimelineModel +) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId ? { ...andProvider, excluded } : andProvider + ), + } + : provider + ); + +const updateExcludedProvider = (excluded: boolean, providerId: string, timeline: TimelineModel) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + excluded, + } + : provider + ); + +interface UpdateTimelineProviderExcludedParams { + id: string; + providerId: string; + excluded: boolean; + timelineById: TimelineById; + andProviderId?: string; +} + +export const updateTimelineProviderExcluded = ({ + id, + providerId, + excluded, + timelineById, + andProviderId, +}: UpdateTimelineProviderExcludedParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateExcludedAndProvider(andProviderId, excluded, providerId, timeline) + : updateExcludedProvider(excluded, providerId, timeline), + }, + }; +}; + +const updateProviderProperties = ({ + excluded, + field, + operator, + providerId, + timeline, + value, +}: { + excluded: boolean; + field: string; + operator: QueryOperator; + providerId: string; + timeline: TimelineModel; + value: string | number; +}) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + excluded, + queryMatch: { + ...provider.queryMatch, + field, + displayField: field, + value, + displayValue: value, + operator, + }, + } + : provider + ); + +const updateAndProviderProperties = ({ + andProviderId, + excluded, + field, + operator, + providerId, + timeline, + value, +}: { + andProviderId: string; + excluded: boolean; + field: string; + operator: QueryOperator; + providerId: string; + timeline: TimelineModel; + value: string | number; +}) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId + ? { + ...andProvider, + excluded, + queryMatch: { + ...andProvider.queryMatch, + field, + displayField: field, + value, + displayValue: value, + operator, + }, + } + : andProvider + ), + } + : provider + ); + +interface UpdateTimelineProviderEditPropertiesParams { + andProviderId?: string; + excluded: boolean; + field: string; + id: string; + operator: QueryOperator; + providerId: string; + timelineById: TimelineById; + value: string | number; +} + +export const updateTimelineProviderProperties = ({ + andProviderId, + excluded, + field, + id, + operator, + providerId, + timelineById, + value, +}: UpdateTimelineProviderEditPropertiesParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateAndProviderProperties({ + andProviderId, + excluded, + field, + operator, + providerId, + timeline, + value, + }) + : updateProviderProperties({ + excluded, + field, + operator, + providerId, + timeline, + value, + }), + }, + }; +}; + +interface UpdateTimelineProviderKqlQueryParams { + id: string; + providerId: string; + kqlQuery: string; + timelineById: TimelineById; +} + +export const updateTimelineProviderKqlQuery = ({ + id, + providerId, + kqlQuery, + timelineById, +}: UpdateTimelineProviderKqlQueryParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: timeline.dataProviders.map(provider => + provider.id === providerId ? { ...provider, ...{ kqlQuery } } : provider + ), + }, + }; +}; + +interface UpdateTimelineItemsPerPageParams { + id: string; + itemsPerPage: number; + timelineById: TimelineById; +} + +export const updateTimelineItemsPerPage = ({ + id, + itemsPerPage, + timelineById, +}: UpdateTimelineItemsPerPageParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPage, + }, + }; +}; + +interface UpdateTimelinePageIndexParams { + id: string; + activePage: number; + timelineById: TimelineById; +} + +export const updateTimelinePageIndex = ({ + id, + activePage, + timelineById, +}: UpdateTimelinePageIndexParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + activePage, + }, + }; +}; + +interface UpdateTimelinePerPageOptionsParams { + id: string; + itemsPerPageOptions: number[]; + timelineById: TimelineById; +} + +export const updateTimelinePerPageOptions = ({ + id, + itemsPerPageOptions, + timelineById, +}: UpdateTimelinePerPageOptionsParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPageOptions, + }, + }; +}; + +const removeAndProvider = (andProviderId: string, providerId: string, timeline: TimelineModel) => { + const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); + const providerAndIndex = timeline.dataProviders[providerIndex].and.findIndex( + p => p.id === andProviderId + ); + return [ + ...timeline.dataProviders.slice(0, providerIndex), + { + ...timeline.dataProviders[providerIndex], + and: [ + ...timeline.dataProviders[providerIndex].and.slice(0, providerAndIndex), + ...timeline.dataProviders[providerIndex].and.slice(providerAndIndex + 1), + ], + }, + ...timeline.dataProviders.slice(providerIndex + 1), + ]; +}; + +const removeProvider = (providerId: string, timeline: TimelineModel) => { + const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); + return [ + ...timeline.dataProviders.slice(0, providerIndex), + ...(timeline.dataProviders[providerIndex].and.length + ? [ + { + ...timeline.dataProviders[providerIndex].and.slice(0, 1)[0], + and: [...timeline.dataProviders[providerIndex].and.slice(1)], + }, + ] + : []), + ...timeline.dataProviders.slice(providerIndex + 1), + ]; +}; + +interface RemoveTimelineProviderParams { + id: string; + providerId: string; + timelineById: TimelineById; + andProviderId?: string; +} + +export const removeTimelineProvider = ({ + id, + providerId, + timelineById, + andProviderId, +}: RemoveTimelineProviderParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? removeAndProvider(andProviderId, providerId, timeline) + : removeProvider(providerId, timeline), + }, + }; +}; + +interface SetDeletedTimelineEventsParams { + id: string; + eventIds: string[]; + isDeleted: boolean; + timelineById: TimelineById; +} + +export const setDeletedTimelineEvents = ({ + id, + eventIds, + isDeleted, + timelineById, +}: SetDeletedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const deletedEventIds = isDeleted + ? union(timeline.deletedEventIds, eventIds) + : timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); + + const selectedEventIds = Object.fromEntries( + Object.entries(timeline.selectedEventIds).filter( + ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) + ) + ); + + const isSelectAllChecked = + Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false; + + return { + ...timelineById, + [id]: { + ...timeline, + deletedEventIds, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +interface SetLoadingTimelineEventsParams { + id: string; + eventIds: string[]; + isLoading: boolean; + timelineById: TimelineById; +} + +export const setLoadingTimelineEvents = ({ + id, + eventIds, + isLoading, + timelineById, +}: SetLoadingTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const loadingEventIds = isLoading + ? union(timeline.loadingEventIds, eventIds) + : timeline.loadingEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); + + return { + ...timelineById, + [id]: { + ...timeline, + loadingEventIds, + }, + }; +}; + +interface SetSelectedTimelineEventsParams { + id: string; + eventIds: Record<string, TimelineNonEcsData[]>; + isSelectAllChecked: boolean; + isSelected: boolean; + timelineById: TimelineById; +} + +export const setSelectedTimelineEvents = ({ + id, + eventIds, + isSelectAllChecked = false, + isSelected, + timelineById, +}: SetSelectedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const selectedEventIds = isSelected + ? { ...timeline.selectedEventIds, ...eventIds } + : omit(Object.keys(eventIds), timeline.selectedEventIds); + + return { + ...timelineById, + [id]: { + ...timeline, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +interface UnPinTimelineEventParams { + id: string; + eventId: string; + timelineById: TimelineById; +} + +export const unPinTimelineEvent = ({ + id, + eventId, + timelineById, +}: UnPinTimelineEventParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + pinnedEventIds: omit(eventId, timeline.pinnedEventIds), + }, + }; +}; + +interface UpdateHighlightedDropAndProviderIdParams { + id: string; + providerId: string; + timelineById: TimelineById; +} + +export const updateHighlightedDropAndProvider = ({ + id, + providerId, + timelineById, +}: UpdateHighlightedDropAndProviderIdParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + highlightedDropAndProviderId: providerId, + }, + }; +}; + +interface UpdateSavedQueryParams { + id: string; + savedQueryId: string | null; + timelineById: TimelineById; +} + +export const updateSavedQuery = ({ + id, + savedQueryId, + timelineById, +}: UpdateSavedQueryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + savedQueryId, + }, + }; +}; + +interface UpdateFiltersParams { + id: string; + filters: Filter[]; + timelineById: TimelineById; +} + +export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + filters, + }, + }; +}; diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/index.ts b/x-pack/plugins/siem/public/store/timeline/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/index.ts rename to x-pack/plugins/siem/public/store/timeline/index.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/manage_timeline_id.tsx b/x-pack/plugins/siem/public/store/timeline/manage_timeline_id.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/manage_timeline_id.tsx rename to x-pack/plugins/siem/public/store/timeline/manage_timeline_id.tsx diff --git a/x-pack/plugins/siem/public/store/timeline/model.ts b/x-pack/plugins/siem/public/store/timeline/model.ts new file mode 100644 index 0000000000000..15bd2980e4aeb --- /dev/null +++ b/x-pack/plugins/siem/public/store/timeline/model.ts @@ -0,0 +1,149 @@ +/* + * 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 { Filter } from '../../../../../../src/plugins/data/public'; +import { DataProvider } from '../../components/timeline/data_providers/data_provider'; +import { Sort } from '../../components/timeline/body/sort'; +import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types'; +import { KueryFilterQuery, SerializedFilterQuery } from '../model'; + +export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages +export type KqlMode = 'filter' | 'search'; +export type EventType = 'all' | 'raw' | 'signal'; + +export type ColumnHeaderType = 'not-filtered' | 'text-filter'; + +/** Uniquely identifies a column */ +export type ColumnId = string; + +/** The specification of a column header */ +export interface ColumnHeaderOptions { + aggregatable?: boolean; + category?: string; + columnHeaderType: ColumnHeaderType; + description?: string; + example?: string; + format?: string; + id: ColumnId; + label?: string; + linkField?: string; + placeholder?: string; + type?: string; + width: number; +} + +export interface TimelineModel { + /** The columns displayed in the timeline */ + columns: ColumnHeaderOptions[]; + /** The sources of the event data shown in the timeline */ + dataProviders: DataProvider[]; + /** Events to not be rendered **/ + deletedEventIds: string[]; + /** A summary of the events and notes in this timeline */ + description: string; + /** Typoe of event you want to see in this timeline */ + eventType?: EventType; + /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ + eventIdToNoteIds: Record<string, string[]>; + filters?: Filter[]; + /** The chronological history of actions related to this timeline */ + historyIds: string[]; + /** The chronological history of actions related to this timeline */ + highlightedDropAndProviderId: string; + /** Uniquely identifies the timeline */ + id: string; + /** If selectAll checkbox in header is checked **/ + isSelectAllChecked: boolean; + /** Events to be rendered as loading **/ + loadingEventIds: string[]; + savedObjectId: string | null; + /** When true, this timeline was marked as "favorite" by the user */ + isFavorite: boolean; + /** When true, the timeline will update as new data arrives */ + isLive: boolean; + /** The number of items to show in a single page of results */ + itemsPerPage: number; + /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ + itemsPerPageOptions: number[]; + /** determines the behavior of the KQL bar */ + kqlMode: KqlMode; + /** the KQL query in the KQL bar */ + kqlQuery: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + /** Title */ + title: string; + /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ + noteIds: string[]; + /** Events pinned to this timeline */ + pinnedEventIds: Record<string, boolean>; + pinnedEventsSaveObject: Record<string, PinnedEvent>; + /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ + dateRange: { + start: number; + end: number; + }; + savedQueryId?: string | null; + /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ + selectedEventIds: Record<string, TimelineNonEcsData[]>; + /** When true, show the timeline flyover */ + show: boolean; + /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ + showCheckboxes: boolean; + /** When true, shows additional rowRenderers below the PlainRowRenderer **/ + showRowRenderers: boolean; + /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ + sort: Sort; + /** Persists the UI state (width) of the timeline flyover */ + width: number; + /** timeline is saving */ + isSaving: boolean; + isLoading: boolean; + version: string | null; +} + +export type SubsetTimelineModel = Readonly< + Pick< + TimelineModel, + | 'columns' + | 'dataProviders' + | 'deletedEventIds' + | 'description' + | 'eventType' + | 'eventIdToNoteIds' + | 'highlightedDropAndProviderId' + | 'historyIds' + | 'isFavorite' + | 'isLive' + | 'isSelectAllChecked' + | 'itemsPerPage' + | 'itemsPerPageOptions' + | 'kqlMode' + | 'kqlQuery' + | 'title' + | 'loadingEventIds' + | 'noteIds' + | 'pinnedEventIds' + | 'pinnedEventsSaveObject' + | 'dateRange' + | 'selectedEventIds' + | 'show' + | 'showCheckboxes' + | 'showRowRenderers' + | 'sort' + | 'width' + | 'isSaving' + | 'isLoading' + | 'savedObjectId' + | 'version' + > +>; + +export interface TimelineUrl { + id: string; + isOpen: boolean; +} diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/my_epic_timeline_id.ts b/x-pack/plugins/siem/public/store/timeline/my_epic_timeline_id.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/my_epic_timeline_id.ts rename to x-pack/plugins/siem/public/store/timeline/my_epic_timeline_id.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/reducer.test.ts b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/reducer.test.ts rename to x-pack/plugins/siem/public/store/timeline/reducer.test.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts b/x-pack/plugins/siem/public/store/timeline/reducer.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts rename to x-pack/plugins/siem/public/store/timeline/reducer.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/refetch_queries.ts b/x-pack/plugins/siem/public/store/timeline/refetch_queries.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/refetch_queries.ts rename to x-pack/plugins/siem/public/store/timeline/refetch_queries.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts b/x-pack/plugins/siem/public/store/timeline/selectors.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts rename to x-pack/plugins/siem/public/store/timeline/selectors.ts diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/types.ts b/x-pack/plugins/siem/public/store/timeline/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/store/timeline/types.ts rename to x-pack/plugins/siem/public/store/timeline/types.ts 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 0000000000000..2c679ba41116e --- /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; +} diff --git a/x-pack/legacy/plugins/siem/public/utils/api/index.ts b/x-pack/plugins/siem/public/utils/api/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/api/index.ts rename to x-pack/plugins/siem/public/utils/api/index.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/apollo_context.ts b/x-pack/plugins/siem/public/utils/apollo_context.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/apollo_context.ts rename to x-pack/plugins/siem/public/utils/apollo_context.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts b/x-pack/plugins/siem/public/utils/default_date_settings.test.ts similarity index 99% rename from x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts rename to x-pack/plugins/siem/public/utils/default_date_settings.test.ts index bb66067512d1f..9dc179ba7a6e2 100644 --- a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.test.ts +++ b/x-pack/plugins/siem/public/utils/default_date_settings.test.ts @@ -21,7 +21,7 @@ import { DEFAULT_INTERVAL_PAUSE, DEFAULT_INTERVAL_VALUE, DEFAULT_INTERVAL_TYPE, -} from '../../../../../plugins/siem/common/constants'; +} from '../../common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; @@ -30,7 +30,7 @@ import { Policy } from '../store/inputs/model'; // we have to repeat ourselves once const DEFAULT_FROM_DATE = '1983-05-31T13:03:54.234Z'; const DEFAULT_TO_DATE = '1990-05-31T13:03:54.234Z'; -jest.mock('../../../../../plugins/siem/common/constants', () => ({ +jest.mock('../../common/constants', () => ({ DEFAULT_FROM: '1983-05-31T13:03:54.234Z', DEFAULT_TO: '1990-05-31T13:03:54.234Z', DEFAULT_INTERVAL_PAUSE: true, diff --git a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts b/x-pack/plugins/siem/public/utils/default_date_settings.ts similarity index 98% rename from x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts rename to x-pack/plugins/siem/public/utils/default_date_settings.ts index 89f7d34d85131..c4869a4851ae5 100644 --- a/x-pack/legacy/plugins/siem/public/utils/default_date_settings.ts +++ b/x-pack/plugins/siem/public/utils/default_date_settings.ts @@ -15,7 +15,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../../../../plugins/siem/common/constants'; +} from '../../common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx b/x-pack/plugins/siem/public/utils/kql/use_update_kql.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.test.tsx rename to x-pack/plugins/siem/public/utils/kql/use_update_kql.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/plugins/siem/public/utils/kql/use_update_kql.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/kql/use_update_kql.tsx rename to x-pack/plugins/siem/public/utils/kql/use_update_kql.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/logo_endpoint/64_color.svg b/x-pack/plugins/siem/public/utils/logo_endpoint/64_color.svg similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/logo_endpoint/64_color.svg rename to x-pack/plugins/siem/public/utils/logo_endpoint/64_color.svg diff --git a/x-pack/legacy/plugins/siem/public/utils/route/helpers.ts b/x-pack/plugins/siem/public/utils/route/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/helpers.ts rename to x-pack/plugins/siem/public/utils/route/helpers.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx b/x-pack/plugins/siem/public/utils/route/index.test.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/index.test.tsx rename to x-pack/plugins/siem/public/utils/route/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx b/x-pack/plugins/siem/public/utils/route/manage_spy_routes.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/manage_spy_routes.tsx rename to x-pack/plugins/siem/public/utils/route/manage_spy_routes.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/plugins/siem/public/utils/route/spy_routes.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/spy_routes.tsx rename to x-pack/plugins/siem/public/utils/route/spy_routes.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/route/types.ts b/x-pack/plugins/siem/public/utils/route/types.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/types.ts rename to x-pack/plugins/siem/public/utils/route/types.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx b/x-pack/plugins/siem/public/utils/route/use_route_spy.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/route/use_route_spy.tsx rename to x-pack/plugins/siem/public/utils/route/use_route_spy.tsx diff --git a/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx new file mode 100644 index 0000000000000..335398177f0f4 --- /dev/null +++ b/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; +import { + SavedQueryService, + createSavedQueryService, +} from '../../../../../../src/plugins/data/public'; + +import { useKibana } from '../../lib/kibana'; + +export const useSavedQueryServices = () => { + const kibana = useKibana(); + const client = kibana.services.savedObjects.client; + + const [savedQueryService, setSavedQueryService] = useState<SavedQueryService>( + createSavedQueryService(client) + ); + + useEffect(() => { + setSavedQueryService(createSavedQueryService(client)); + }, [client]); + return savedQueryService; +}; diff --git a/x-pack/legacy/plugins/siem/public/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/siem/public/utils/timeline/use_show_timeline.tsx similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/timeline/use_show_timeline.tsx rename to x-pack/plugins/siem/public/utils/timeline/use_show_timeline.tsx diff --git a/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts b/x-pack/plugins/siem/public/utils/use_mount_appended.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts rename to x-pack/plugins/siem/public/utils/use_mount_appended.ts diff --git a/x-pack/legacy/plugins/siem/public/utils/validators/index.ts b/x-pack/plugins/siem/public/utils/validators/index.ts similarity index 100% rename from x-pack/legacy/plugins/siem/public/utils/validators/index.ts rename to x-pack/plugins/siem/public/utils/validators/index.ts diff --git a/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js b/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js index 0b5e5d6cf13b5..9b4a57f09066d 100644 --- a/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js +++ b/x-pack/plugins/siem/scripts/check_circular_deps/run_check_circular_deps_cli.js @@ -11,24 +11,23 @@ import madge from 'madge'; /* eslint-disable-next-line import/no-extraneous-dependencies */ import { run, createFailError } from '@kbn/dev-utils'; -const legacyPluginPath = '../../../../legacy/plugins/siem'; -const pluginPath = '../..'; - run( async ({ log }) => { const result = await madge( - [resolve(__dirname, legacyPluginPath, 'public'), resolve(__dirname, pluginPath, 'common')], + [resolve(__dirname, '../../public'), resolve(__dirname, '../../common')], { fileExtensions: ['ts', 'js', 'tsx'], excludeRegExp: [ 'test.ts$', 'test.tsx$', 'containers/detection_engine/rules/types.ts$', - 'core/public/chrome/chrome_service.tsx$', 'src/core/server/types.ts$', 'src/core/server/saved_objects/types.ts$', + 'src/core/public/chrome/chrome_service.tsx$', 'src/core/public/overlays/banners/banners_service.tsx$', 'src/core/public/saved_objects/saved_objects_client.ts$', + 'src/plugins/data/public', + 'src/plugins/ui_actions/public', ], } ); diff --git a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js index 478463b1a8064..c8642e8b42c8f 100644 --- a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js @@ -12,13 +12,7 @@ const fetch = require('node-fetch'); const { camelCase } = require('lodash'); const { resolve } = require('path'); -const OUTPUT_DIRECTORY = resolve( - '../../legacy/plugins/siem', - 'public', - 'pages', - 'detection_engine', - 'mitre' -); +const OUTPUT_DIRECTORY = resolve('public', 'pages', 'detection_engine', 'mitre'); const MITRE_ENTREPRISE_ATTACK_URL = 'https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json'; @@ -123,7 +117,7 @@ async function main() { .replace(/}"/g, '}') .replace(/"{/g, '{')}; - export const techniques = ${JSON.stringify(techniques, null, 2)}; + export const technique = ${JSON.stringify(techniques, null, 2)}; export const techniquesOptions: MitreTechniquesOptions[] = ${JSON.stringify(getTechniquesOptions(techniques), null, 2) diff --git a/x-pack/plugins/siem/scripts/generate_types_from_graphql.js b/x-pack/plugins/siem/scripts/generate_types_from_graphql.js index bded8832aba5a..e6b063dfd2c07 100644 --- a/x-pack/plugins/siem/scripts/generate_types_from_graphql.js +++ b/x-pack/plugins/siem/scripts/generate_types_from_graphql.js @@ -10,19 +10,12 @@ const { join, resolve } = require('path'); // eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved const { generate } = require('graphql-code-generator'); -const legacyPluginPath = '../../legacy/plugins/siem'; - const GRAPHQL_GLOBS = [ - join(legacyPluginPath, 'public', 'containers', '**', '*.gql_query.ts{,x}'), + join('public', 'containers', '**', '*.gql_query.ts{,x}'), join('common', 'graphql', '**', '*.gql_query.ts{,x}'), ]; -const OUTPUT_INTROSPECTION_PATH = resolve( - legacyPluginPath, - 'public', - 'graphql', - 'introspection.json' -); -const OUTPUT_CLIENT_TYPES_PATH = resolve(legacyPluginPath, 'public', 'graphql', 'types.ts'); +const OUTPUT_INTROSPECTION_PATH = resolve('public', 'graphql', 'introspection.json'); +const OUTPUT_CLIENT_TYPES_PATH = resolve('public', 'graphql', 'types.ts'); const OUTPUT_SERVER_TYPES_PATH = resolve('server', 'graphql', 'types.ts'); const SCHEMA_PATH = resolve(__dirname, 'combined_schema.ts'); diff --git a/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md b/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md index 2b402367c1db3..fbcd3329312aa 100644 --- a/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md +++ b/x-pack/plugins/siem/scripts/optimize_tsconfig/README.md @@ -1,5 +1,5 @@ Hard forked from here: -x-pack/legacy/plugins/apm/scripts/optimize-tsconfig.js +x-pack/plugins/apm/scripts/optimize-tsconfig.js #### Optimizing TypeScript diff --git a/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json b/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json index 42d26c4c27ed6..fb89c0e0fc3e2 100644 --- a/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json +++ b/x-pack/plugins/siem/scripts/optimize_tsconfig/tsconfig.json @@ -2,7 +2,6 @@ "include": [ "typings/**/*", "plugins/siem/**/*", - "legacy/plugins/siem/**/*", "plugins/apm/typings/numeral.d.ts", "legacy/plugins/canvas/types/webpack.d.ts", "plugins/triggers_actions_ui/**/*" diff --git a/x-pack/plugins/siem/server/client/client.test.ts b/x-pack/plugins/siem/server/client/client.test.ts index 94ff2149b8c64..c0ae15cb73f4e 100644 --- a/x-pack/plugins/siem/server/client/client.test.ts +++ b/x-pack/plugins/siem/server/client/client.test.ts @@ -9,7 +9,7 @@ import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { SiemClient } from './client'; describe('SiemClient', () => { - describe('#signalsIndex', () => { + describe('#getSignalsIndex', () => { it('returns the index scoped to the specified spaceId', () => { const mockConfig = { ...createMockConfig(), @@ -18,7 +18,7 @@ describe('SiemClient', () => { const spaceId = 'fooSpace'; const client = new SiemClient(spaceId, mockConfig); - expect(client.signalsIndex).toEqual('mockSignalsIndex-fooSpace'); + expect(client.getSignalsIndex()).toEqual('mockSignalsIndex-fooSpace'); }); }); }); diff --git a/x-pack/plugins/siem/server/client/client.ts b/x-pack/plugins/siem/server/client/client.ts index 6cb0d4cfade77..5780bb4173f79 100644 --- a/x-pack/plugins/siem/server/client/client.ts +++ b/x-pack/plugins/siem/server/client/client.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConfigType } from '..'; +import { ConfigType } from '../config'; export class SiemClient { - public readonly signalsIndex: string; + private readonly signalsIndex: string; constructor(private spaceId: string, private config: ConfigType) { const configuredSignalsIndex = this.config.signalsIndex; this.signalsIndex = `${configuredSignalsIndex}-${this.spaceId}`; } + + public getSignalsIndex = (): string => this.signalsIndex; } diff --git a/x-pack/plugins/siem/server/client/factory.ts b/x-pack/plugins/siem/server/client/factory.ts index d3d6b84e5b090..69db4d7eed98f 100644 --- a/x-pack/plugins/siem/server/client/factory.ts +++ b/x-pack/plugins/siem/server/client/factory.ts @@ -6,7 +6,7 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SiemClient } from './client'; -import { ConfigType } from '..'; +import { ConfigType } from '../config'; interface SetupDependencies { getSpaceId?: (request: KibanaRequest) => string | undefined; diff --git a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts index 9dd04247b7f47..bc2b3a53d85f3 100644 --- a/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -125,6 +125,11 @@ export const timelineSchema = gql` script: String } + enum TimelineType { + default + template + } + input TimelineInput { columns: [ColumnHeaderInput!] dataProviders: [DataProviderInput!] @@ -134,6 +139,9 @@ export const timelineSchema = gql` kqlMode: String kqlQuery: SerializedFilterQueryInput title: String + templateTimelineId: String + templateTimelineVersion: Int + timelineType: TimelineType dateRange: DateRangePickerInput savedQueryId: String sort: SortTimelineInput @@ -237,6 +245,9 @@ export const timelineSchema = gql` savedObjectId: String! sort: SortTimelineResult title: String + templateTimelineId: String + templateTimelineVersion: Int + timelineType: TimelineType updated: Float updatedBy: String version: String! diff --git a/x-pack/plugins/siem/server/graphql/types.ts b/x-pack/plugins/siem/server/graphql/types.ts index d272b7ff59b79..6a35ba08f8e43 100644 --- a/x-pack/plugins/siem/server/graphql/types.ts +++ b/x-pack/plugins/siem/server/graphql/types.ts @@ -134,6 +134,12 @@ export interface TimelineInput { title?: Maybe<string>; + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + dateRange?: Maybe<DateRangePickerInput>; savedQueryId?: Maybe<string>; @@ -336,6 +342,11 @@ export enum TlsFields { _id = '_id', } +export enum TimelineType { + default = 'default', + template = 'template', +} + export enum SortFieldTimeline { title = 'title', description = 'description', @@ -1946,6 +1957,12 @@ export interface TimelineResult { title?: Maybe<string>; + templateTimelineId?: Maybe<string>; + + templateTimelineVersion?: Maybe<number>; + + timelineType?: Maybe<TimelineType>; + updated?: Maybe<number>; updatedBy?: Maybe<string>; @@ -8023,6 +8040,12 @@ export namespace TimelineResultResolvers { title?: TitleResolver<Maybe<string>, TypeParent, TContext>; + templateTimelineId?: TemplateTimelineIdResolver<Maybe<string>, TypeParent, TContext>; + + templateTimelineVersion?: TemplateTimelineVersionResolver<Maybe<number>, TypeParent, TContext>; + + timelineType?: TimelineTypeResolver<Maybe<TimelineType>, TypeParent, TContext>; + updated?: UpdatedResolver<Maybe<number>, TypeParent, TContext>; updatedBy?: UpdatedByResolver<Maybe<string>, TypeParent, TContext>; @@ -8130,6 +8153,21 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver<R, Parent, TContext>; + export type TemplateTimelineIdResolver< + R = Maybe<string>, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type TemplateTimelineVersionResolver< + R = Maybe<number>, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; + export type TimelineTypeResolver< + R = Maybe<TimelineType>, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver<R, Parent, TContext>; export type UpdatedResolver< R = Maybe<number>, Parent = TimelineResult, diff --git a/x-pack/plugins/siem/server/index.ts b/x-pack/plugins/siem/server/index.ts index 83e2f900a3b90..e9cd78589fac9 100644 --- a/x-pack/plugins/siem/server/index.ts +++ b/x-pack/plugins/siem/server/index.ts @@ -5,7 +5,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { Plugin, PluginSetup, PluginStart } from './plugin'; import { configSchema, ConfigType } from './config'; export const plugin = (context: PluginInitializerContext) => { @@ -14,4 +14,4 @@ export const plugin = (context: PluginInitializerContext) => { export const config = { schema: configSchema }; -export { ConfigType }; +export { ConfigType, Plugin, PluginSetup, PluginStart }; diff --git a/x-pack/plugins/siem/server/lib/compose/kibana.ts b/x-pack/plugins/siem/server/lib/compose/kibana.ts index 9c46f3320e37e..8bc90bed25168 100644 --- a/x-pack/plugins/siem/server/lib/compose/kibana.ts +++ b/x-pack/plugins/siem/server/lib/compose/kibana.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, SetupPlugins } from '../../plugin'; +import { CoreSetup } from '../../../../../../src/core/server'; +import { SetupPlugins } from '../../plugin'; import { Authentications } from '../authentications'; import { ElasticsearchAuthenticationAdapter } from '../authentications/elasticsearch_adapter'; @@ -27,9 +28,9 @@ import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes'; -import { Note } from '../note/saved_object'; -import { PinnedEvent } from '../pinned_event/saved_object'; -import { Timeline } from '../timeline/saved_object'; +import * as note from '../note/saved_object'; +import * as pinnedEvent from '../pinned_event/saved_object'; +import * as timeline from '../timeline/saved_object'; import { ElasticsearchMatrixHistogramAdapter, MatrixHistogram } from '../matrix_histogram'; export function compose( @@ -41,10 +42,6 @@ export function compose( const sources = new Sources(new ConfigurationSourcesAdapter()); const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework)); - const timeline = new Timeline(); - const note = new Note(); - const pinnedEvent = new PinnedEvent(); - const domainLibs: AppDomainLibs = { authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index 6244a4cc64e68..e8d778bddadc2 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult } from '../routes/__mocks__/request_responses'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; import { buildSignalsSearchQuery } from './build_signals_query'; @@ -15,12 +15,12 @@ jest.mock('./build_signals_query'); describe('rules_notification_alert_type', () => { let payload: NotificationExecutorOptions; let alert: ReturnType<typeof rulesNotificationAlertType>; - let logger: ReturnType<typeof loggerMock.create>; + let logger: ReturnType<typeof loggingServiceMock.createLogger>; let alertServices: AlertServicesMock; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); payload = { alertId: '1111', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts index 4fce037b483d5..0c9ccf069b3b6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getNotificationResult, getResult } from '../routes/__mocks__/request_responses'; import { isAlertTypes, isNotificationAlertExecutor } from './types'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; @@ -20,7 +20,9 @@ describe('types', () => { it('isNotificationAlertExecutor should return true it passed object is NotificationAlertTypeDefinition type', () => { expect( - isNotificationAlertExecutor(rulesNotificationAlertType({ logger: loggerMock.create() })) + isNotificationAlertExecutor( + rulesNotificationAlertType({ logger: loggingServiceMock.createLogger() }) + ) ).toEqual(true); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts index 10efdb518f7b7..f3b4068f6dd2d 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -13,6 +13,7 @@ import { import { alertsClientMock } from '../../../../../../alerting/server/mocks'; import { actionsClientMock } from '../../../../../../actions/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; +import { siemMock } from '../../../../mocks'; const createMockClients = () => ({ actionsClient: actionsClientMock.create(), @@ -20,7 +21,7 @@ const createMockClients = () => ({ clusterClient: elasticsearchServiceMock.createScopedClusterClient(), licensing: { license: licensingMock.createLicenseMock() }, savedObjectsClient: savedObjectsClientMock.create(), - siemClient: { signalsIndex: 'mockSignalsIndex' }, + siemClient: siemMock.createClient(), }); const createRequestContextMock = ( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index cb48e35228858..20b8ad29d2715 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -37,7 +37,7 @@ export const createIndexRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(callCluster, index); if (indexExists) { return siemResponse.error({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index 5eff38b778492..79cf4851f9ab8 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -45,7 +45,7 @@ export const deleteIndexRoute = (router: IRouter) => { } const callCluster = clusterClient.callAsCurrentUser; - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(callCluster, index); if (!indexExists) { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 8ff8d7461ecd1..2b418892f0f39 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -29,7 +29,7 @@ export const readIndexRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); if (indexExists) { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 7dbbe837e656d..e3c41c555f297 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -36,7 +36,7 @@ export const readPrivilegesRoute = ( return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); const privileges = merge(clusterPrivileges, { is_authenticated: security?.authc.isAuthenticated(request) ?? false, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index bfc8c9c54b2c0..3c6adce45f959 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -49,7 +49,7 @@ export const addPrepackedRulesRoute = (router: IRouter) => { const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const { signalsIndex } = siemClient; + const signalsIndex = siemClient.getSignalsIndex(); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { const signalsIndexExists = await getIndexExists( clusterClient.callAsCurrentUser, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 2d7ddb79e5af5..133c98a6af7b3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -92,7 +92,7 @@ export const createRulesBulkRoute = (router: IRouter) => { try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 1f0896686aca0..9f1cddb2051c9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -9,10 +9,8 @@ import uuid from 'uuid'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; import { RuleAlertParamsRest } from '../../types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { transformValidate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { createRulesSchema } from '../schemas/create_rules_schema'; @@ -23,6 +21,7 @@ import { validateLicenseForRuleType, } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const createRulesRoute = (router: IRouter): void => { router.post( @@ -82,7 +81,7 @@ export const createRulesRoute = (router: IRouter): void => { return siemResponse.error({ statusCode: 404 }); } - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return siemResponse.error({ @@ -145,10 +144,7 @@ export const createRulesRoute = (router: IRouter): void => { name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 38748e287ab45..b35ba27ef3561 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -11,14 +11,11 @@ import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; -import { - IRuleSavedAttributesSavedObjectAttributes, - DeleteRulesRequestParams, -} from '../../rules/types'; +import { DeleteRulesRequestParams } from '../../rules/types'; import { deleteRules } from '../../rules/delete_rules'; import { deleteNotifications } from '../../notifications/delete_notifications'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; type Config = RouteConfig<unknown, unknown, DeleteRulesRequestParams, 'delete' | 'post'>; type Handler = RequestHandler<unknown, unknown, DeleteRulesRequestParams, 'delete' | 'post'>; @@ -44,6 +41,8 @@ export const deleteRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const rules = await Promise.all( request.body.map(async payloadRule => { const { id, rule_id: ruleId } = payloadRule; @@ -61,17 +60,12 @@ export const deleteRulesBulkRoute = (router: IRouter) => { ruleAlertId: rule.id, savedObjectsClient, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 6, search: rule.id, searchFields: ['alertId'], }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); + ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id)); return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 098d556741fed..2288633ee8d2e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -11,13 +11,10 @@ import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; -import { - DeleteRuleRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { DeleteRuleRequestParams } from '../../rules/types'; import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const deleteRulesRoute = (router: IRouter) => { router.delete( @@ -44,6 +41,7 @@ export const deleteRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await deleteRules({ actionsClient, alertsClient, @@ -56,17 +54,12 @@ export const deleteRulesRoute = (router: IRouter) => { ruleAlertId: rule.id, savedObjectsClient, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 6, search: rule.id, searchFields: ['alertId'], }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); + ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id)); const [validated, errors] = transformValidate( rule, undefined, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index 8433b74adf310..bc4568dd0a40b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -6,7 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ConfigType } from '../../../..'; +import { ConfigType } from '../../../../config'; import { ExportRulesRequestParams } from '../../rules/types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 9661fac81497c..f293b9e64a316 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -7,15 +7,12 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; -import { - FindRulesRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { FindRulesRequestParams } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; import { transformValidateFindAlerts } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const findRulesRoute = (router: IRouter) => { router.get( @@ -40,6 +37,7 @@ export const findRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await findRules({ alertsClient, perPage: query.per_page, @@ -50,10 +48,7 @@ export const findRulesRoute = (router: IRouter) => { }); const ruleStatuses = await Promise.all( rules.data.map(async rule => { - const results = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const results = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 6b54a25a1b1c4..8e35fecf6a652 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -9,17 +9,16 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequestParams, - IRuleSavedAttributesSavedObjectAttributes, RuleStatusResponse, IRuleStatusAttributes, } from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { buildRouteValidation, transformError, convertToSnakeCase, buildSiemResponse, } from '../utils'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const findRulesStatusesRoute = (router: IRouter) => { router.post( @@ -50,12 +49,10 @@ export const findRulesStatusesRoute = (router: IRouter) => { } */ try { + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const statuses = await body.ids.reduce<Promise<RuleStatusResponse | {}>>( async (acc, id) => { - const lastFiveErrorsForId = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const lastFiveErrorsForId = await ruleStatusClient.find({ perPage: 6, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 8c052cfdf4024..1233e01a67762 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -126,6 +126,7 @@ describe('import_rules_route', () => { }); test('returns an error if the index does not exist', async () => { + clients.siemClient.getSignalsIndex.mockReturnValue('mockSignalsIndex'); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); const response = await server.inject(request, context); expect(response.status).toEqual(200); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 527fab786910f..202252da293ee 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -10,7 +10,7 @@ import { extname } from 'path'; import { IRouter } from '../../../../../../../../src/core/server'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ConfigType } from '../../../..'; +import { ConfigType } from '../../../../config'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequestParams } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; @@ -147,7 +147,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { ruleType: type, }); - const signalsIndex = siemClient.signalsIndex; + const signalsIndex = siemClient.getSignalsIndex(); const indexExists = await getIndexExists( clusterClient.callAsCurrentUser, signalsIndex diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index e4236f4632dcd..534253db65d78 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - IRuleSavedAttributesSavedObjectAttributes, - PatchRuleAlertParamsRest, -} from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { transformBulkError, buildRouteValidation, @@ -21,8 +18,8 @@ import { transformValidateBulkError, validate } from './validate'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const patchRulesBulkRoute = (router: IRouter) => { router.patch( @@ -46,6 +43,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { const { @@ -131,10 +129,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { throttle, name: rule.name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 23469144e11f8..f7932cb016ba7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -7,10 +7,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRules } from '../../rules/patch_rules'; -import { - PatchRuleAlertParamsRest, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { patchRulesSchema } from '../schemas/patch_rules_schema'; import { buildRouteValidation, @@ -20,8 +17,8 @@ import { } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const patchRulesRoute = (router: IRouter) => { router.patch( @@ -83,6 +80,7 @@ export const patchRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await patchRules({ actionsClient, alertsClient, @@ -127,10 +125,7 @@ export const patchRulesRoute = (router: IRouter) => { throttle, name: rule.name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index 4d23e0217f2e8..cedd7ccd1a411 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -11,12 +11,9 @@ import { transformValidate } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; import { readRules } from '../../rules/read_rules'; import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { - ReadRuleRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { ReadRuleRequestParams } from '../../rules/types'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const readRulesRoute = (router: IRouter) => { router.get( @@ -41,6 +38,7 @@ export const readRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await readRules({ alertsClient, id, @@ -51,10 +49,7 @@ export const readRulesRoute = (router: IRouter) => { savedObjectsClient, ruleAlertId: rule.id, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 6db91d74294fc..f929f2fb3f649 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - IRuleSavedAttributesSavedObjectAttributes, - UpdateRuleAlertParamsRest, -} from '../../rules/types'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { @@ -19,10 +16,10 @@ import { validateLicenseForRuleType, } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const updateRulesBulkRoute = (router: IRouter) => { router.put( @@ -47,6 +44,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { const { @@ -83,7 +81,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { version, exceptions_list, } = payloadRule; - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); @@ -134,10 +132,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { throttle, name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 7dbbe5a22ab46..dedc2c914410a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - UpdateRuleAlertParamsRest, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; import { buildRouteValidation, @@ -19,9 +16,9 @@ import { } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const updateRulesRoute = (router: IRouter) => { router.put( @@ -78,12 +75,13 @@ export const updateRulesRoute = (router: IRouter) => { const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.siem?.getSiemClient(); + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const rule = await updateRules({ alertsClient, actionsClient, @@ -131,10 +129,7 @@ export const updateRulesRoute = (router: IRouter) => { throttle, name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts index c207d075331b6..cfba40fc225a2 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -9,8 +9,9 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as t from 'io-ts'; +import { formatErrors } from '../../../../utils/build_validation/format_errors'; +import { exactCheck } from '../../../../utils/build_validation/exact_check'; import { PartialAlert, FindResult } from '../../../../../../alerting/server'; -import { formatErrors } from '../schemas/response/utils'; import { isAlertType, IRuleSavedAttributesSavedObjectAttributes, @@ -19,7 +20,6 @@ import { import { OutputRuleAlertRest } from '../../types'; import { createBulkErrorObject, BulkError } from '../utils'; import { rulesSchema, RulesSchema } from '../schemas/response/rules_schema'; -import { exactCheck } from '../schemas/response/exact_check'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; import { findRulesSchema } from '../schemas/response/find_rules_schema'; import { RuleActions } from '../../rule_actions/types'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index 21f18f9db55fb..fef6bcf42e49f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -4,32 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as t from 'io-ts'; -import { fold } from 'fp-ts/lib/Either'; import { RulesSchema } from '../rules_schema'; import { RulesBulkSchema } from '../rules_bulk_schema'; import { ErrorSchema } from '../error_schema'; import { FindRulesSchema } from '../find_rules_schema'; -import { formatErrors } from '../utils'; -import { pipe } from 'fp-ts/lib/pipeable'; - -interface Message<T> { - errors: t.Errors; - schema: T | {}; -} - -const onLeft = <T>(errors: t.Errors): Message<T> => { - return { schema: {}, errors }; -}; - -const onRight = <T>(schema: T): Message<T> => { - return { - schema, - errors: [], - }; -}; - -export const foldLeftRight = fold(onLeft, onRight); export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; @@ -127,18 +105,3 @@ export const getFindResponseSingle = (): FindRulesSchema => ({ total: 1, data: [getBaseResponsePayload()], }); - -/** - * Convenience utility to keep the error message handling within tests to be - * very concise. - * @param validation The validation to get the errors from - */ -export const getPaths = <A>(validation: t.Validation<A>): string[] => { - return pipe( - validation, - fold( - errors => formatErrors(errors), - () => ['no errors'] - ) - ); -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts index 0eda2a7a13d96..fbd2382e2826d 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts @@ -15,17 +15,13 @@ import { addQueryFields, addMlFields, } from './check_type_dependents'; -import { - foldLeftRight, - getBaseResponsePayload, - getPaths, - getMlRuleResponsePayload, -} from './__mocks__/utils'; +import { getBaseResponsePayload, getMlRuleResponsePayload } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; -import { exactCheck } from './exact_check'; import { RulesSchema } from './rules_schema'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; describe('check_type_dependents', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts index 11d8b85f25920..6e159a792edb6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts @@ -7,10 +7,11 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; -import { foldLeftRight, getErrorPayload, getPaths } from './__mocks__/utils'; +import { getErrorPayload } from './__mocks__/utils'; import { errorSchema, ErrorSchema } from './error_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('error_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts deleted file mode 100644 index 6fa0472950189..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.ts +++ /dev/null @@ -1,84 +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 * as t from 'io-ts'; -import { left, Either, fold, right } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { isObject, get } from 'lodash/fp'; - -/** - * Given an original object and a decoded object this will return an error - * if and only if the original object has additional keys that the decoded - * object does not have. If the original decoded already has an error, then - * this will return the error as is and not continue. - * - * NOTE: You MUST use t.exact(...) for this to operate correctly as your schema - * needs to remove additional keys before the compare - * - * You might not need this in the future if the below issue is solved: - * https://github.com/gcanti/io-ts/issues/322 - * - * @param original The original to check if it has additional keys - * @param decoded The decoded either which has either an existing error or the - * decoded object which could have additional keys stripped from it. - */ -export const exactCheck = <T>( - original: object, - decoded: Either<t.Errors, T> -): Either<t.Errors, T> => { - const onLeft = (errors: t.Errors): Either<t.Errors, T> => left(errors); - const onRight = (decodedValue: T): Either<t.Errors, T> => { - const differences = findDifferencesRecursive(original, decodedValue); - if (differences.length !== 0) { - const validationError: t.ValidationError = { - value: differences, - context: [], - message: `invalid keys "${differences.join(',')}"`, - }; - const error: t.Errors = [validationError]; - return left(error); - } else { - return right(decodedValue); - } - }; - return pipe(decoded, fold(onLeft, onRight)); -}; - -export const findDifferencesRecursive = <T>(original: object, decodedValue: T): string[] => { - if (decodedValue == null) { - try { - // It is null and painful when the original contains an object or an array - // the the decoded value does not have. - return [JSON.stringify(original)]; - } catch (err) { - return ['circular reference']; - } - } - const decodedKeys = Object.keys(decodedValue); - const differences = Object.keys(original).flatMap(originalKey => { - const foundKey = decodedKeys.some(key => key === originalKey); - const topLevelKey = foundKey ? [] : [originalKey]; - // I use lodash to cheat and get an any (not going to lie ;-)) - const valueObjectOrArrayOriginal = get(originalKey, original); - const valueObjectOrArrayDecoded = get(originalKey, decodedValue); - if (isObject(valueObjectOrArrayOriginal)) { - return [ - ...topLevelKey, - ...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), - ]; - } else if (Array.isArray(valueObjectOrArrayOriginal)) { - return [ - ...topLevelKey, - ...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => - findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) - ), - ]; - } else { - return topLevelKey; - } - }); - return differences; -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts index f5c1970ee8c55..68b67db595d76 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts @@ -5,17 +5,13 @@ */ import { findRulesSchema, FindRulesSchema } from './find_rules_schema'; -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { - foldLeftRight, - getFindResponseSingle, - getBaseResponsePayload, - getPaths, -} from './__mocks__/utils'; +import { getFindResponseSingle, getBaseResponsePayload } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { RulesSchema } from './rules_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { getPaths, foldLeftRight } from '../../../../../utils/build_validation/__mocks__/utils'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; describe('find_rules_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts index ce4bbf420a634..b0b863ebbbc0b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; -import { left } from 'fp-ts/lib/Either'; +import { left, Either } from 'fp-ts/lib/Either'; import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; import { ErrorSchema } from './error_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { getPaths, foldLeftRight } from '../../../../../utils/build_validation/__mocks__/utils'; +import { Errors } from 'io-ts'; describe('import_rules_schema', () => { beforeAll(() => { @@ -79,13 +80,31 @@ describe('import_rules_schema', () => { }); test('it should NOT validate a success that is not a boolean', () => { + type UnsafeCastForTest = Either< + Errors, + { + success: string; + success_count: number; + errors: Array< + { + id?: string | undefined; + rule_id?: string | undefined; + } & { + error: { + status_code: number; + message: string; + }; + } + >; + } + >; const payload: Omit<ImportRulesSchema, 'success'> & { success: string } = { success: 'hello', success_count: 0, errors: [], }; const decoded = importRulesSchema.decode(payload); - const checked = exactCheck(payload, decoded); + const checked = exactCheck(payload, decoded as UnsafeCastForTest); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "hello" supplied to "success"']); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts index 46667826416e1..827167c63fd58 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rules_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts index 1c270ff402f75..a864667583c0a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exactCheck } from './exact_check'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesStatusSchema, prePackagedRulesStatusSchema, } from './prepackaged_rules_status_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rules_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts index 8dc97d727c4d1..9a7cf5e2c2871 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts @@ -7,17 +7,13 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; -import { - foldLeftRight, - getBaseResponsePayload, - getErrorPayload, - getPaths, -} from './__mocks__/utils'; +import { getBaseResponsePayload, getErrorPayload } from './__mocks__/utils'; import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rule_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts index 4bfc51c1a66aa..82a6682e6461f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts @@ -7,10 +7,11 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; import { rulesSchema, RulesSchema, removeList } from './rules_schema'; -import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; +import { getBaseResponsePayload } from './__mocks__/utils'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts index 68a3c8b303823..85fb124464487 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts @@ -7,10 +7,10 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { exactCheck } from './exact_check'; -import { foldLeftRight, getPaths } from './__mocks__/utils'; import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; +import { exactCheck } from '../../../../../utils/build_validation/exact_check'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('prepackaged_rule_schema', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts deleted file mode 100644 index c1eb32be4895c..0000000000000 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts +++ /dev/null @@ -1,139 +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 * as t from 'io-ts'; -import { formatErrors } from './utils'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; - -describe('utils', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - - test('returns an empty error message string if there are no errors', () => { - const errors: t.Errors = []; - const output = formatErrors(errors); - expect(output).toEqual([]); - }); - - test('returns a single error message if given one', () => { - const validationError: t.ValidationError = { - value: 'Some existing error', - context: [], - message: 'some error', - }; - const errors: t.Errors = [validationError]; - const output = formatErrors(errors); - expect(output).toEqual(['some error']); - }); - - test('returns a two error messages if given two', () => { - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context: [], - message: 'some error 1', - }; - const validationError2: t.ValidationError = { - value: 'Some existing error 2', - context: [], - message: 'some error 2', - }; - const errors: t.Errors = [validationError1, validationError2]; - const output = formatErrors(errors); - expect(output).toEqual(['some error 1', 'some error 2']); - }); - - test('will use message before context if it is set', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - message: 'I should be used first', - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['I should be used first']); - }); - - test('will use context entry of a single string', () => { - const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); - }); - - test('will use two context entries of two strings', () => { - const context: t.Context = ([ - { key: 'some string key 1' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', - ]); - }); - - test('will filter out and not use any strings of numbers', () => { - const context: t.Context = ([ - { key: '5' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use null', () => { - const context: t.Context = ([ - { key: null }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); - - test('will filter out and not use empty strings', () => { - const context: t.Context = ([ - { key: '' }, - { key: 'some string key 2' }, - ] as unknown) as t.Context; - const validationError1: t.ValidationError = { - value: 'Some existing error 1', - context, - }; - const errors: t.Errors = [validationError1]; - const output = formatErrors(errors); - expect(output).toEqual([ - 'Invalid value "Some existing error 1" supplied to "some string key 2"', - ]); - }); -}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts index fbafaf7f52ecb..ff62ea4443f3f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/iso_date_string.test.ts @@ -6,8 +6,8 @@ import { IsoDateString } from './iso_date_string'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('ios_date_string', () => { test('it should validate a iso string', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts index e8a9c7b0886a1..2a97c8a4a143e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts @@ -6,8 +6,8 @@ import { ListsDefaultArray } from './lists_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('lists_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts index bc17303f24203..d6f21681df88f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/positive_integer_greater_than_zero.test.ts @@ -6,8 +6,8 @@ import { PositiveIntegerGreaterThanZero } from './positive_integer_greater_than_zero'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts index cee451279663a..26441745a7f29 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/postive_integer.test.ts @@ -6,8 +6,8 @@ import { PositiveInteger } from './positive_integer'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('positive_integer_greater_than_zero', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts index 3ae8415b4f170..76f722274ce23 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/references_default_array.test.ts @@ -6,8 +6,8 @@ import { ReferencesDefaultArray } from './references_default_array'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('references_default_array', () => { test('it should validate an empty array', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts index ab3f80944489f..76e3445358dd9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/risk_score.test.ts @@ -6,8 +6,8 @@ import { RiskScore } from './risk_score'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('risk_score', () => { test('it should validate a positive number', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts index 342e6f2db2e16..7b68dbcef2d7e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/types/uuid.test.ts @@ -6,8 +6,8 @@ import { UUID } from './uuid'; import { pipe } from 'fp-ts/lib/pipeable'; -import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; +import { foldLeftRight, getPaths } from '../../../../../utils/build_validation/__mocks__/utils'; describe('uuid', () => { test('it should validate a uuid', () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index c71761fcc39db..bcb70b6b4f0dd 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -44,7 +44,7 @@ export const setSignalsStatusRoute = (router: IRouter) => { } try { const result = await clusterClient.callAsCurrentUser('updateByQuery', { - index: siemClient.signalsIndex, + index: siemClient.getSignalsIndex(), body: { script: { source: `ctx._source.signal.status = '${status}'`, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index fd02b3371ed38..41896c725b903 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -29,7 +29,7 @@ export const querySignalsRoute = (router: IRouter) => { try { const result = await clusterClient.callAsCurrentUser('search', { - index: siemClient.signalsIndex, + index: siemClient.getSignalsIndex(), body: { query, aggs, _source, track_total_hits, size }, ignoreUnavailable: true, }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts index f54f43c41ef6e..e50f82bb482a7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts @@ -4,37 +4,44 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsType } from '../../../../../../../src/core/server'; + export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; export const ruleActionsSavedObjectMappings = { - [ruleActionsSavedObjectType]: { - properties: { - alertThrottle: { - type: 'keyword', - }, - ruleAlertId: { - type: 'keyword', - }, - ruleThrottle: { - type: 'keyword', - }, - actions: { - properties: { - group: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - action_type_id: { - type: 'keyword', - }, - params: { - dynamic: true, - properties: {}, - }, + properties: { + alertThrottle: { + type: 'keyword', + }, + ruleAlertId: { + type: 'keyword', + }, + ruleThrottle: { + type: 'keyword', + }, + actions: { + properties: { + group: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + action_type_id: { + type: 'keyword', + }, + params: { + type: 'object', + enabled: false, }, }, }, }, }; + +export const type: SavedObjectsType = { + name: ruleActionsSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: ruleActionsSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index c23f539b58160..85b13ed9cf4ed 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -7,10 +7,10 @@ import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; -import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; +import { PatchRuleParams } from './types'; import { addTags } from './add_tags'; -import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion, calculateName, calculateInterval } from './utils'; +import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; export const patchRules = async ({ alertsClient, @@ -134,22 +134,22 @@ export const patchRules = async ({ await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); - const ruleCurrentStatus = savedObjectsClient - ? await savedObjectsClient.find<IRuleSavedAttributesSavedObjectAttributes>({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }) - : null; + + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleCurrentStatus = await ruleStatusClient.find({ + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + // set current status for this rule to be 'going to run' if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - currentStatusToDisable.attributes.status = 'going to run'; - await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + await ruleStatusClient.update(currentStatusToDisable.id, { ...currentStatusToDisable.attributes, + status: 'going to run', }); } } else { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json index cfc322788d4be..c83c0e01d7fa0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_adversary_behavior_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json index 0647fe9c9ce10..18472abbd70d7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], @@ -17,4 +17,4 @@ ], "type": "query", "version": 2 -} \ No newline at end of file +} diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json index 036c88688d9bd..03024ad15396e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_dumping_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json index 0fe610d551152..e5a128029f585 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Credential Manipulation. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json index a317c77bcd90a..1c05743fae62f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_cred_manipulation_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Credential Manipulation. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json index 97640c0cea9b2..3396a8563ba1c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected an Exploit. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json index 069687a5af00f..2f70c539414c6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_exploit_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented an Exploit. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json index a7d3371190ced..cbf6c286a439f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Malware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json index dd7bf72c34f90..49c7c160e5daf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_malware_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Malware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json index a8e102cc4619d..e836bd037ddc5 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Permission Theft. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json index c97330f2349eb..e9ac8d7ba6686 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_permission_theft_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Permission Theft. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json index e644c0e8d66eb..8e25832b0e89a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Process Injection. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json index 61cbe267f9a46..a59428275ca22 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_process_injection_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Process Injection. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json index 0e88b26cb2c75..22091d8c9b68f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_detected.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint detected Ransomware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json index ba341f059f26d..947bfcbba39a0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security_ransomware_prevented.json @@ -1,6 +1,6 @@ { "description": "Elastic Endpoint prevented Ransomware. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.", - "from": "now-660s", + "from": "now-15m", "index": [ "endgame-*" ], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts index 1d91def5fa6cc..2dcc90240ad40 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts @@ -4,44 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsType } from '../../../../../../../src/core/server'; + export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status'; export const ruleStatusSavedObjectMappings = { - [ruleStatusSavedObjectType]: { - properties: { - alertId: { - type: 'keyword', - }, - status: { - type: 'keyword', - }, - statusDate: { - type: 'date', - }, - lastFailureAt: { - type: 'date', - }, - lastSuccessAt: { - type: 'date', - }, - lastFailureMessage: { - type: 'text', - }, - lastSuccessMessage: { - type: 'text', - }, - lastLookBackDate: { - type: 'date', - }, - gap: { - type: 'text', - }, - bulkCreateTimeDurations: { - type: 'float', - }, - searchAfterTimeDurations: { - type: 'float', - }, + properties: { + alertId: { + type: 'keyword', + }, + status: { + type: 'keyword', + }, + statusDate: { + type: 'date', + }, + lastFailureAt: { + type: 'date', + }, + lastSuccessAt: { + type: 'date', + }, + lastFailureMessage: { + type: 'text', + }, + lastSuccessMessage: { + type: 'text', + }, + lastLookBackDate: { + type: 'date', + }, + gap: { + type: 'text', + }, + bulkCreateTimeDurations: { + type: 'float', + }, + searchAfterTimeDurations: { + type: 'float', }, }, }; + +export const type: SavedObjectsType = { + name: ruleStatusSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: ruleStatusSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 7ddbbd76b0661..29c2cfdf91076 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -7,11 +7,11 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; -import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; +import { UpdateRuleParams } from './types'; import { addTags } from './add_tags'; -import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion } from './utils'; import { hasListsFeature } from '../feature_flags'; +import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; export const updateRules = async ({ alertsClient, @@ -129,22 +129,22 @@ export const updateRules = async ({ await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); - const ruleCurrentStatus = savedObjectsClient - ? await savedObjectsClient.find<IRuleSavedAttributesSavedObjectAttributes>({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }) - : null; + + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleCurrentStatus = await ruleStatusClient.find({ + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + // set current status for this rule to be 'going to run' if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - currentStatusToDisable.attributes.status = 'going to run'; - await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + await ruleStatusClient.update(currentStatusToDisable.id, { ...currentStatusToDisable.attributes, + status: 'going to run', }); } } diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh index 750c5574f4a72..2028216e6770f 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_instances.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./get_action_instances.sh -# https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md#get-apiaction_find-find-actions +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md#get-apiaction_find-find-actions curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/action/_getAll \ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh index 8d8cbdd70a803..c587e9a204182 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/get_action_types.sh @@ -10,7 +10,7 @@ set -e ./check_env_variables.sh # Example: ./get_action_types.sh -# https://github.com/elastic/kibana/blob/master/x-pack/legacy/plugins/actions/README.md +# https://github.com/elastic/kibana/blob/master/x-pack/plugins/actions/README.md curl -s -k \ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ -X GET ${KIBANA_URL}${SPACE_URL}/api/action/types \ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh b/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh index a56d788d69c16..22b602f935187 100755 --- a/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh +++ b/x-pack/plugins/siem/server/lib/detection_engine/scripts/hard_reset.sh @@ -9,9 +9,15 @@ set -e ./check_env_variables.sh +# Clean up and remove all actions and alerts from SIEM +# within saved objects ./delete_all_actions.sh ./delete_all_alerts.sh ./delete_all_alert_tasks.sh + +# delete all the statuses from the signal index ./delete_all_statuses.sh + +# re-create the signal index ./delete_signal_index.sh ./post_signal_index.sh diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 8a5da8e859721..251a1e6d118ff 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -13,7 +13,7 @@ import { import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; import { RuleTypeParams, OutputRuleAlertRest } from '../../types'; import { IRuleStatusAttributes } from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../../../saved_objects'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; export const sampleRuleAlertParams = ( maxSignals?: number | undefined, @@ -86,7 +86,7 @@ export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSource _id: someUuid, _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', }, }); @@ -97,7 +97,7 @@ export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): Sig _id: someUuid, _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', }, }); @@ -109,7 +109,7 @@ export const sampleDocWithSortId = (someUuid: string = sampleIdGuid): SignalSour _id: someUuid, _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', }, sort: ['1234567891111'], }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index bbd01cfaafc62..df9d282b71e5e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -59,7 +59,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], @@ -185,7 +185,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], @@ -309,7 +309,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], @@ -426,7 +426,7 @@ describe('buildBulkBody', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { actions: [], diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts index 0a50c33fbbfe4..f3f4ab60e4db6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/build_signal.test.ts @@ -41,7 +41,7 @@ describe('buildSignal', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', status: 'open', rule: { created_by: 'elastic', @@ -101,7 +101,7 @@ describe('buildSignal', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', original_event: { action: 'socket_opened', dataset: 'socket', @@ -173,7 +173,7 @@ describe('buildSignal', () => { depth: 1, }, ], - original_time: 'someTimeStamp', + original_time: '2020-04-20T21:27:45+0000', original_event: { action: 'socket_opened', dataset: 'socket', 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 d298f1cc7cbc6..a8cc6dc680410 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/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index cec011ae8c445..2cb23b05f6a9b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -30,7 +30,7 @@ describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { const sampleParams = sampleRuleAlertParams(); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults(), ruleParams: sampleParams, services: mockService, @@ -55,6 +55,7 @@ describe('searchAfterAndBulkCreate', () => { expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(0); + expect(lastLookBackDate).toBeNull(); }); test('if successful iteration of while loop with maxDocs', async () => { @@ -105,7 +106,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), ruleParams: sampleParams, services: mockService, @@ -130,13 +131,14 @@ describe('searchAfterAndBulkCreate', () => { expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(3); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if unsuccessful first bulk create', async () => { const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); const sampleParams = sampleRuleAlertParams(10); mockService.callCluster.mockResolvedValue(sampleBulkCreateDuplicateResult); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -161,6 +163,7 @@ describe('searchAfterAndBulkCreate', () => { expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { @@ -179,7 +182,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, @@ -204,6 +207,7 @@ describe('searchAfterAndBulkCreate', () => { expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { @@ -222,7 +226,7 @@ describe('searchAfterAndBulkCreate', () => { }, ], }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortIdNoHits(), ruleParams: sampleParams, services: mockService, @@ -246,6 +250,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { @@ -267,7 +272,7 @@ describe('searchAfterAndBulkCreate', () => { ], }) .mockResolvedValueOnce(sampleDocSearchResultsNoSortId()); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -291,6 +296,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { @@ -312,7 +318,7 @@ describe('searchAfterAndBulkCreate', () => { ], }) .mockResolvedValueOnce(sampleEmptyDocSearchResults()); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -336,6 +342,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(true); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); test('if returns false when singleSearchAfter throws an exception', async () => { @@ -359,7 +366,7 @@ describe('searchAfterAndBulkCreate', () => { .mockImplementation(() => { throw Error('Fake Error'); }); - const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -383,5 +390,6 @@ describe('searchAfterAndBulkCreate', () => { }); expect(success).toEqual(false); expect(createdSignalsCount).toEqual(1); + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index e287e33295c89..acf3e9bfb055c 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -98,13 +98,15 @@ export const searchAfterAndBulkCreate = async ({ tags, throttle, }); - toReturn.lastLookBackDate = - someResult.hits.hits.length > 0 - ? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp']) - : null; - if (createdItemsCount) { + + if (createdItemsCount > 0) { toReturn.createdSignalsCount = createdItemsCount; + toReturn.lastLookBackDate = + someResult.hits.hits.length > 0 + ? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp']) + : null; } + if (bulkCreateDuration) { toReturn.bulkCreateTimes.push(bulkCreateDuration); } diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 7eecc5cb9bad0..0c7f0839f8daf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; @@ -70,13 +70,13 @@ describe('rules_notification_alert_type', () => { }; let payload: jest.Mocked<RuleExecutorOptions>; let alert: ReturnType<typeof signalRulesAlertType>; - let logger: ReturnType<typeof loggerMock.create>; + let logger: ReturnType<typeof loggingServiceMock.createLogger>; let alertServices: AlertServicesMock; let ruleStatusService: Record<string, jest.Mock>; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); ruleStatusService = { success: jest.fn(), find: jest.fn(), diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 51cc0f449b17a..6f3cc6e708fce 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -300,7 +300,7 @@ describe('singleBulkCreate', () => { _id: 'e1e08ddc-5e37-49ff-a258-5393aa44435a', _source: { someKey: 'someValue', - '@timestamp': 'someTimeStamp', + '@timestamp': '2020-04-20T21:27:45+0000', signal: { parent: { rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', @@ -334,7 +334,7 @@ describe('singleBulkCreate', () => { test('filter duplicate rules will return back search responses if they do not have a signal and will NOT filter the source out', () => { const ancestors = sampleDocWithAncestors(); - ancestors.hits.hits[0]._source = { '@timestamp': 'some timestamp' }; + ancestors.hits.hits[0]._source = { '@timestamp': '2020-04-20T21:27:45+0000' }; const filtered = filterDuplicateRules('04128c15-0d1b-4716-a4c5-46997ac7f3bd', ancestors); expect(filtered).toEqual([ { @@ -343,7 +343,7 @@ describe('singleBulkCreate', () => { _score: 100, _version: 1, _id: 'e1e08ddc-5e37-49ff-a258-5393aa44435a', - _source: { '@timestamp': 'some timestamp' }, + _source: { '@timestamp': '2020-04-20T21:27:45+0000' }, }, ]); }); diff --git a/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts b/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts index 762416149c0fb..1e99c40ef5727 100644 --- a/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/siem/server/lib/framework/kibana_framework_adapter.ts @@ -9,6 +9,7 @@ import { GraphQLSchema } from 'graphql'; import { runHttpQuery } from 'apollo-server-core'; import { schema as configSchema } from '@kbn/config-schema'; import { + CoreSetup, IRouter, KibanaResponseFactory, RequestHandlerContext, @@ -16,7 +17,7 @@ import { } from '../../../../../../src/core/server'; import { IndexPatternsFetcher } from '../../../../../../src/plugins/data/server'; import { AuthenticatedUser } from '../../../../security/common/model'; -import { CoreSetup, SetupPlugins } from '../../plugin'; +import { SetupPlugins } from '../../plugin'; import { FrameworkAdapter, diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts index 2b94fd4516786..219465f551457 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object.ts @@ -15,6 +15,11 @@ import { identity } from 'fp-ts/lib/function'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { + SavedNote, + NoteSavedObjectRuntimeType, + NoteSavedObject, +} from '../../../common/types/timeline/note'; import { PageInfoNote, ResponseNote, @@ -23,178 +28,198 @@ import { NoteResult, } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; -import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; import { noteSavedObjectType } from './saved_object_mappings'; -import { timelineSavedObjectType } from '../../saved_objects'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; +import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; -export class Note { - public async deleteNote(request: FrameworkRequest, noteIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId)) - ); - } - - public async deleteNoteByTimelineId(request: FrameworkRequest, timelineId: string) { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const notesToBeDeleted = await this.getAllSavedNote(request, options); - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - notesToBeDeleted.notes.map(note => - savedObjectsClient.delete(noteSavedObjectType, note.noteId) - ) - ); - } - - public async getNote(request: FrameworkRequest, noteId: string): Promise<NoteSavedObject> { - return this.getSavedNote(request, noteId); - } - - public async getNotesByEventId( - request: FrameworkRequest, - eventId: string - ): Promise<NoteSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: eventId, - searchFields: ['eventId'], - }; - const notesByEventId = await this.getAllSavedNote(request, options); - return notesByEventId.notes; - } - - public async getNotesByTimelineId( - request: FrameworkRequest, - timelineId: string - ): Promise<NoteSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const notesByTimelineId = await this.getAllSavedNote(request, options); - return notesByTimelineId.notes; - } - - public async getAllNotes( +export interface Note { + deleteNote: (request: FrameworkRequest, noteIds: string[]) => Promise<void>; + deleteNoteByTimelineId: (request: FrameworkRequest, noteIds: string) => Promise<void>; + getNote: (request: FrameworkRequest, noteId: string) => Promise<NoteSavedObject>; + getNotesByEventId: (request: FrameworkRequest, noteId: string) => Promise<NoteSavedObject[]>; + getNotesByTimelineId: (request: FrameworkRequest, noteId: string) => Promise<NoteSavedObject[]>; + getAllNotes: ( request: FrameworkRequest, pageInfo: PageInfoNote | null, search: string | null, sort: SortNote | null - ): Promise<ResponseNotes> { - const options: SavedObjectsFindOptions = { - type: noteSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['note'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - return this.getAllSavedNote(request, options); - } - - public async persistNote( + ) => Promise<ResponseNotes>; + persistNote: ( request: FrameworkRequest, noteId: string | null, version: string | null, note: SavedNote - ): Promise<ResponseNote> { - try { - const savedObjectsClient = request.context.core.savedObjects.client; - - if (noteId == null) { - const timelineVersionSavedObject = - note.timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user) - ) - ); - note.timelineId = timelineResult.savedObjectId; - return timelineResult.version; - })() - : null; - - // Create new note - return { - code: 200, - message: 'success', - note: convertSavedObjectToSavedNote( - await savedObjectsClient.create( - noteSavedObjectType, - pickSavedNote(noteId, note, request.user) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ), - }; - } + ) => Promise<ResponseNote>; + convertSavedObjectToSavedNote: ( + savedObject: unknown, + timelineVersion?: string | undefined | null + ) => NoteSavedObject; +} + +export const deleteNote = async (request: FrameworkRequest, noteIds: string[]) => { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all(noteIds.map(noteId => savedObjectsClient.delete(noteSavedObjectType, noteId))); +}; + +export const deleteNoteByTimelineId = async (request: FrameworkRequest, timelineId: string) => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const notesToBeDeleted = await getAllSavedNote(request, options); + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + notesToBeDeleted.notes.map(note => savedObjectsClient.delete(noteSavedObjectType, note.noteId)) + ); +}; + +export const getNote = async ( + request: FrameworkRequest, + noteId: string +): Promise<NoteSavedObject> => { + return getSavedNote(request, noteId); +}; + +export const getNotesByEventId = async ( + request: FrameworkRequest, + eventId: string +): Promise<NoteSavedObject[]> => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: eventId, + searchFields: ['eventId'], + }; + const notesByEventId = await getAllSavedNote(request, options); + return notesByEventId.notes; +}; + +export const getNotesByTimelineId = async ( + request: FrameworkRequest, + timelineId: string +): Promise<NoteSavedObject[]> => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const notesByTimelineId = await getAllSavedNote(request, options); + return notesByTimelineId.notes; +}; + +export const getAllNotes = async ( + request: FrameworkRequest, + pageInfo: PageInfoNote | null, + search: string | null, + sort: SortNote | null +): Promise<ResponseNotes> => { + const options: SavedObjectsFindOptions = { + type: noteSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: ['note'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return getAllSavedNote(request, options); +}; + +export const persistNote = async ( + request: FrameworkRequest, + noteId: string | null, + version: string | null, + note: SavedNote +): Promise<ResponseNote> => { + try { + const savedObjectsClient = request.context.core.savedObjects.client; - // Update new note + if (noteId == null) { + const timelineVersionSavedObject = + note.timelineId == null + ? await (async () => { + const timelineResult = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(null, {}, request.user) + ) + ); + note.timelineId = timelineResult.savedObjectId; + return timelineResult.version; + })() + : null; - const existingNote = await this.getSavedNote(request, noteId); + // Create new note return { code: 200, message: 'success', note: convertSavedObjectToSavedNote( - await savedObjectsClient.update( + await savedObjectsClient.create( noteSavedObjectType, - noteId, - pickSavedNote(noteId, note, request.user), - { - version: existingNote.version || undefined, - } - ) + pickSavedNote(noteId, note, request.user) + ), + timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined ), }; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 403) { - const noteToReturn: NoteResult = { - ...note, - noteId: uuid.v1(), - version: '', - timelineId: '', - timelineVersion: '', - }; - return { - code: 403, - message: err.message, - note: noteToReturn, - }; - } - throw err; } - } - private async getSavedNote(request: FrameworkRequest, NoteId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId); - - return convertSavedObjectToSavedNote(savedObject); - } - - private async getAllSavedNote(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); + // Update new note + const existingNote = await getSavedNote(request, noteId); return { - totalCount: savedObjects.total, - notes: savedObjects.saved_objects.map(savedObject => - convertSavedObjectToSavedNote(savedObject) + code: 200, + message: 'success', + note: convertSavedObjectToSavedNote( + await savedObjectsClient.update( + noteSavedObjectType, + noteId, + pickSavedNote(noteId, note, request.user), + { + version: existingNote.version || undefined, + } + ) ), }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { + const noteToReturn: NoteResult = { + ...note, + noteId: uuid.v1(), + version: '', + timelineId: '', + timelineVersion: '', + }; + return { + code: 403, + message: err.message, + note: noteToReturn, + }; + } + throw err; } -} +}; + +const getSavedNote = async (request: FrameworkRequest, NoteId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(noteSavedObjectType, NoteId); + + return convertSavedObjectToSavedNote(savedObject); +}; + +const getAllSavedNote = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObjects = await savedObjectsClient.find(options); + + return { + totalCount: savedObjects.total, + notes: savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedNote(savedObject) + ), + }; +}; export const convertSavedObjectToSavedNote = ( savedObject: unknown, diff --git a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts index b001e30e52336..0f079571b868b 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts @@ -4,37 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedNote } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const noteSavedObjectType = 'siem-ui-timeline-note'; -export const noteSavedObjectMappings: { - [noteSavedObjectType]: ElasticsearchMappingOf<SavedNote>; -} = { - [noteSavedObjectType]: { - properties: { - timelineId: { - type: 'keyword', - }, - eventId: { - type: 'keyword', - }, - note: { - type: 'text', - }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', - }, +export const noteSavedObjectMappings = { + properties: { + timelineId: { + type: 'keyword', + }, + eventId: { + type: 'keyword', + }, + note: { + type: 'text', + }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', }, }, }; + +export const type: SavedObjectsType = { + name: noteSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: noteSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/note/types.ts b/x-pack/plugins/siem/server/lib/note/types.ts deleted file mode 100644 index f7a10317bd84d..0000000000000 --- a/x-pack/plugins/siem/server/lib/note/types.ts +++ /dev/null @@ -1,68 +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. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; - -import { unionWithNullType } from '../framework'; - -/* - * Note Types - */ -export const SavedNoteRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - timelineId: unionWithNullType(runtimeTypes.string), - }), - runtimeTypes.partial({ - eventId: unionWithNullType(runtimeTypes.string), - note: unionWithNullType(runtimeTypes.string), - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), - }), -]); - -export interface SavedNote extends runtimeTypes.TypeOf<typeof SavedNoteRuntimeType> {} - -/** - * Note Saved object type with metadata - */ - -export const NoteSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedNoteRuntimeType, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - noteId: runtimeTypes.string, - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export const NoteSavedObjectToReturnRuntimeType = runtimeTypes.intersection([ - SavedNoteRuntimeType, - runtimeTypes.type({ - noteId: runtimeTypes.string, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export interface NoteSavedObject - extends runtimeTypes.TypeOf<typeof NoteSavedObjectToReturnRuntimeType> {} diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts index 7fc23d86d8218..c653f23d5c149 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -13,173 +13,224 @@ import { identity } from 'fp-ts/lib/function'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { FrameworkRequest } from '../framework'; import { PinnedEventSavedObject, PinnedEventSavedObjectRuntimeType, SavedPinnedEvent, -} from './types'; +} from '../../../common/types/timeline/pinned_event'; +import { FrameworkRequest } from '../framework'; + import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; -import { pinnedEventSavedObjectType, timelineSavedObjectType } from '../../saved_objects'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; +import { pinnedEventSavedObjectType } from './saved_object_mappings'; +import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; -export class PinnedEvent { - public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - pinnedEventIds.map(pinnedEventId => - savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId) - ) - ); - } +export interface PinnedEvent { + deletePinnedEventOnTimeline: ( + request: FrameworkRequest, + pinnedEventIds: string[] + ) => Promise<void>; - public async deleteAllPinnedEventsOnTimeline(request: FrameworkRequest, timelineId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - const pinnedEventToBeDeleted = await this.getAllSavedPinnedEvents(request, options); - await Promise.all( - pinnedEventToBeDeleted.map(pinnedEvent => - savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId) - ) - ); - } + deleteAllPinnedEventsOnTimeline: (request: FrameworkRequest, timelineId: string) => Promise<void>; - public async getPinnedEvent( + getPinnedEvent: ( request: FrameworkRequest, pinnedEventId: string - ): Promise<PinnedEventSavedObject> { - return this.getSavedPinnedEvent(request, pinnedEventId); - } + ) => Promise<PinnedEventSavedObject>; - public async getAllPinnedEventsByTimelineId( + getAllPinnedEventsByTimelineId: ( request: FrameworkRequest, timelineId: string - ): Promise<PinnedEventSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - search: timelineId, - searchFields: ['timelineId'], - }; - return this.getAllSavedPinnedEvents(request, options); - } + ) => Promise<PinnedEventSavedObject[]>; - public async getAllPinnedEvents( + getAllPinnedEvents: ( request: FrameworkRequest, pageInfo: PageInfoNote | null, search: string | null, sort: SortNote | null - ): Promise<PinnedEventSavedObject[]> { - const options: SavedObjectsFindOptions = { - type: pinnedEventSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: ['timelineId', 'eventId'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - return this.getAllSavedPinnedEvents(request, options); - } + ) => Promise<PinnedEventSavedObject[]>; - public async persistPinnedEventOnTimeline( + persistPinnedEventOnTimeline: ( request: FrameworkRequest, pinnedEventId: string | null, // pinned event saved object id eventId: string, timelineId: string | null - ): Promise<PinnedEventResponse | null> { - const savedObjectsClient = request.context.core.savedObjects.client; - - try { - if (pinnedEventId == null) { - const timelineVersionSavedObject = - timelineId == null - ? await (async () => { - const timelineResult = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(null, {}, request.user || null) - ) - ); - timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign - return timelineResult.version; - })() - : null; - - if (timelineId != null) { - const allPinnedEventId = await this.getAllPinnedEventsByTimelineId(request, timelineId); - const isPinnedAlreadyExisting = allPinnedEventId.filter( - pinnedEvent => pinnedEvent.eventId === eventId - ); + ) => Promise<PinnedEventResponse | null>; - if (isPinnedAlreadyExisting.length === 0) { - const savedPinnedEvent: SavedPinnedEvent = { - eventId, - timelineId, - }; - // create Pinned Event on Timeline - return convertSavedObjectToSavedPinnedEvent( - await savedObjectsClient.create( - pinnedEventSavedObjectType, - pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) - ), - timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined - ); - } - return isPinnedAlreadyExisting[0]; + convertSavedObjectToSavedPinnedEvent: ( + savedObject: unknown, + timelineVersion?: string | undefined | null + ) => PinnedEventSavedObject; + + pickSavedPinnedEvent: ( + pinnedEventId: string | null, + savedPinnedEvent: SavedPinnedEvent, + userInfo: AuthenticatedUser | null + ) => // eslint-disable-next-line @typescript-eslint/no-explicit-any + any; +} + +export const deletePinnedEventOnTimeline = async ( + request: FrameworkRequest, + pinnedEventIds: string[] +) => { + const savedObjectsClient = request.context.core.savedObjects.client; + + await Promise.all( + pinnedEventIds.map(pinnedEventId => + savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEventId) + ) + ); +}; + +export const deleteAllPinnedEventsOnTimeline = async ( + request: FrameworkRequest, + timelineId: string +) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + const pinnedEventToBeDeleted = await getAllSavedPinnedEvents(request, options); + await Promise.all( + pinnedEventToBeDeleted.map(pinnedEvent => + savedObjectsClient.delete(pinnedEventSavedObjectType, pinnedEvent.pinnedEventId) + ) + ); +}; + +export const getPinnedEvent = async ( + request: FrameworkRequest, + pinnedEventId: string +): Promise<PinnedEventSavedObject> => { + return getSavedPinnedEvent(request, pinnedEventId); +}; + +export const getAllPinnedEventsByTimelineId = async ( + request: FrameworkRequest, + timelineId: string +): Promise<PinnedEventSavedObject[]> => { + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + search: timelineId, + searchFields: ['timelineId'], + }; + return getAllSavedPinnedEvents(request, options); +}; + +export const getAllPinnedEvents = async ( + request: FrameworkRequest, + pageInfo: PageInfoNote | null, + search: string | null, + sort: SortNote | null +): Promise<PinnedEventSavedObject[]> => { + const options: SavedObjectsFindOptions = { + type: pinnedEventSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: ['timelineId', 'eventId'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return getAllSavedPinnedEvents(request, options); +}; + +export const persistPinnedEventOnTimeline = async ( + request: FrameworkRequest, + pinnedEventId: string | null, // pinned event saved object id + eventId: string, + timelineId: string | null +): Promise<PinnedEventResponse | null> => { + const savedObjectsClient = request.context.core.savedObjects.client; + + try { + if (pinnedEventId == null) { + const timelineVersionSavedObject = + timelineId == null + ? await (async () => { + const timelineResult = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(null, {}, request.user || null) + ) + ); + timelineId = timelineResult.savedObjectId; // eslint-disable-line no-param-reassign + return timelineResult.version; + })() + : null; + + if (timelineId != null) { + const allPinnedEventId = await getAllPinnedEventsByTimelineId(request, timelineId); + const isPinnedAlreadyExisting = allPinnedEventId.filter( + pinnedEvent => pinnedEvent.eventId === eventId + ); + + if (isPinnedAlreadyExisting.length === 0) { + const savedPinnedEvent: SavedPinnedEvent = { + eventId, + timelineId, + }; + // create Pinned Event on Timeline + return convertSavedObjectToSavedPinnedEvent( + await savedObjectsClient.create( + pinnedEventSavedObjectType, + pickSavedPinnedEvent(pinnedEventId, savedPinnedEvent, request.user || null) + ), + timelineVersionSavedObject != null ? timelineVersionSavedObject : undefined + ); } - throw new Error('You can NOT pinned event without a timelineID'); + return isPinnedAlreadyExisting[0]; } - // Delete Pinned Event on Timeline - await this.deletePinnedEventOnTimeline(request, [pinnedEventId]); + throw new Error('You can NOT pinned event without a timelineID'); + } + // Delete Pinned Event on Timeline + await deletePinnedEventOnTimeline(request, [pinnedEventId]); + return null; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 404) { + /* + * Why we are doing that, because if it is not found for sure that it will be unpinned + * There is no need to bring back this error since we can assume that it is unpinned + */ return null; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 404) { - /* - * Why we are doing that, because if it is not found for sure that it will be unpinned - * There is no need to bring back this error since we can assume that it is unpinned - */ - return null; - } - if (getOr(null, 'output.statusCode', err) === 403) { - return pinnedEventId != null - ? { - code: 403, - message: err.message, - pinnedEventId: eventId, - timelineId: '', - timelineVersion: '', - } - : null; - } - throw err; } + if (getOr(null, 'output.statusCode', err) === 403) { + return pinnedEventId != null + ? { + code: 403, + message: err.message, + pinnedEventId: eventId, + timelineId: '', + timelineVersion: '', + } + : null; + } + throw err; } +}; - private async getSavedPinnedEvent(request: FrameworkRequest, pinnedEventId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); +const getSavedPinnedEvent = async (request: FrameworkRequest, pinnedEventId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(pinnedEventSavedObjectType, pinnedEventId); - return convertSavedObjectToSavedPinnedEvent(savedObject); - } + return convertSavedObjectToSavedPinnedEvent(savedObject); +}; - private async getAllSavedPinnedEvents( - request: FrameworkRequest, - options: SavedObjectsFindOptions - ) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObjects = await savedObjectsClient.find(options); - - return savedObjects.saved_objects.map(savedObject => - convertSavedObjectToSavedPinnedEvent(savedObject) - ); - } -} +const getAllSavedPinnedEvents = async ( + request: FrameworkRequest, + options: SavedObjectsFindOptions +) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObjects = await savedObjectsClient.find(options); + + return savedObjects.saved_objects.map(savedObject => + convertSavedObjectToSavedPinnedEvent(savedObject) + ); +}; export const convertSavedObjectToSavedPinnedEvent = ( savedObject: unknown, diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts index 322f585ae8ff2..1a4cd3fce575d 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts @@ -4,34 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedPinnedEvent } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event'; -export const pinnedEventSavedObjectMappings: { - [pinnedEventSavedObjectType]: ElasticsearchMappingOf<SavedPinnedEvent>; -} = { - [pinnedEventSavedObjectType]: { - properties: { - timelineId: { - type: 'keyword', - }, - eventId: { - type: 'keyword', - }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', - }, +export const pinnedEventSavedObjectMappings = { + properties: { + timelineId: { + type: 'keyword', + }, + eventId: { + type: 'keyword', + }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', }, }, }; + +export const type: SavedObjectsType = { + name: pinnedEventSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: pinnedEventSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/pinned_event/types.ts b/x-pack/plugins/siem/server/lib/pinned_event/types.ts deleted file mode 100644 index e598f03935047..0000000000000 --- a/x-pack/plugins/siem/server/lib/pinned_event/types.ts +++ /dev/null @@ -1,67 +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. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; - -import { unionWithNullType } from '../framework'; - -/* - * Note Types - */ -export const SavedPinnedEventRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - timelineId: runtimeTypes.string, - eventId: runtimeTypes.string, - }), - runtimeTypes.partial({ - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), - }), -]); - -export interface SavedPinnedEvent extends runtimeTypes.TypeOf<typeof SavedPinnedEventRuntimeType> {} - -/** - * Note Saved object type with metadata - */ - -export const PinnedEventSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedPinnedEventRuntimeType, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - pinnedEventId: unionWithNullType(runtimeTypes.string), - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export const PinnedEventToReturnSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - pinnedEventId: runtimeTypes.string, - version: runtimeTypes.string, - }), - SavedPinnedEventRuntimeType, - runtimeTypes.partial({ - timelineVersion: runtimeTypes.union([ - runtimeTypes.string, - runtimeTypes.null, - runtimeTypes.undefined, - ]), - }), -]); - -export interface PinnedEventSavedObject - extends runtimeTypes.TypeOf<typeof PinnedEventToReturnSavedObjectRuntimeType> {} diff --git a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts index ea5db565483c8..bde24a338ec84 100644 --- a/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/convert_saved_object_to_savedtimeline.ts @@ -8,16 +8,21 @@ import { failure } from 'io-ts/lib/PathReporter'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { TimelineSavedObjectRuntimeType, TimelineSavedObject } from './types'; +import { + TimelineSavedObjectRuntimeType, + TimelineSavedObject, +} from '../../../common/types/timeline'; export const convertSavedObjectToSavedTimeline = (savedObject: unknown): TimelineSavedObject => { const timeline = pipe( TimelineSavedObjectRuntimeType.decode(savedObject), - map(savedTimeline => ({ - savedObjectId: savedTimeline.id, - version: savedTimeline.version, - ...savedTimeline.attributes, - })), + map(savedTimeline => { + return { + savedObjectId: savedTimeline.id, + version: savedTimeline.version, + ...savedTimeline.attributes, + }; + }), fold(errors => { throw new Error(failure(errors).join('\n')); }, identity) diff --git a/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts index abe8de9bf5b94..6b4017b5e4d5c 100644 --- a/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts +++ b/x-pack/plugins/siem/server/lib/timeline/create_timelines_stream_from_ndjson.ts @@ -22,6 +22,7 @@ import { import { ImportTimelineResponse } from './routes/utils/import_timelines'; import { ImportTimelinesSchemaRt } from './routes/schemas/import_timelines_schema'; +import { BadRequestError } from '../detection_engine/errors/bad_request_error'; type ErrorFactory = (message: string) => Error; @@ -38,8 +39,11 @@ export const decodeOrThrow = <A, O, I>( pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); export const validateTimelines = (): Transform => - createMapStream((obj: ImportTimelineResponse) => decodeOrThrow(ImportTimelinesSchemaRt)(obj)); - + createMapStream((obj: ImportTimelineResponse) => + obj instanceof Error + ? new BadRequestError(obj.message) + : decodeOrThrow(ImportTimelinesSchemaRt)(obj) + ); export const createTimelinesStreamFromNdJson = (ruleLimit: number) => { return [ createSplitStream('\n'), diff --git a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts index 19adb7ac1045a..eeded1cc2532d 100644 --- a/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts +++ b/x-pack/plugins/siem/server/lib/timeline/pick_saved_timeline.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import { AuthenticatedUser } from '../../../../security/common/model'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; -import { SavedTimeline } from './types'; +import { SavedTimeline, TimelineType } from '../../../common/types/timeline'; export const pickSavedTimeline = ( timelineId: string | null, @@ -24,5 +25,21 @@ export const pickSavedTimeline = ( savedTimeline.updated = dateNow; savedTimeline.updatedBy = userInfo?.username ?? UNAUTHENTICATED_USER; } + + if (savedTimeline.timelineType === TimelineType.template) { + savedTimeline.timelineType = TimelineType.template; + if (savedTimeline.templateTimelineId == null) { + savedTimeline.templateTimelineId = uuid.v4(); + } + + if (savedTimeline.templateTimelineVersion == null) { + savedTimeline.templateTimelineVersion = 1; + } + } else { + savedTimeline.timelineType = TimelineType.default; + savedTimeline.templateTimelineId = null; + savedTimeline.templateTimelineVersion = null; + } + return savedTimeline; }; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/README.md b/x-pack/plugins/siem/server/lib/timeline/routes/README.md new file mode 100644 index 0000000000000..2c5547e39fc4e --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/README.md @@ -0,0 +1,326 @@ +**Timeline apis** + + 1. Create timeline api + 2. Update timeline api + 3. Create template timeline api + 4. Update template timeline api + + +## Create timeline api +#### POST /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + } + }, + "timelineId":null, // Leave this as null + "version":null // Leave this as null +} +``` + + +## Update timeline api +#### PATCH /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "created": 1587468588922, + "createdBy": "casetester", + "updated": 1587468588922, + "updatedBy": "casetester", + "timelineType": "default" + }, + "timelineId":"68ea5330-83c3-11ea-bff9-ab01dd7cb6cc", // Have to match the existing timeline savedObject id + "version":"WzYwLDFd" // Have to match the existing timeline version +} +``` + +## Create template timeline api +#### POST /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [ + + ], + "description": "", + "eventType": "all", + "filters": [ + + ], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "timelineType": "template" // This is the difference between create timeline + }, + "timelineId":null, // Leave this as null + "version":null // Leave this as null +} +``` + + +## Update template timeline api +#### PATCH /api/timeline +##### Authorization +Type: Basic Auth +username: Your Kibana username +password: Your Kibana password + + +##### Request header +``` +Content-Type: application/json +kbn-version: 8.0.0 +``` +##### Request body +```json +{ + "timeline": { + "columns": [ + { + "columnHeaderType": "not-filtered", + "id": "@timestamp" + }, + { + "columnHeaderType": "not-filtered", + "id": "message" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.category" + }, + { + "columnHeaderType": "not-filtered", + "id": "event.action" + }, + { + "columnHeaderType": "not-filtered", + "id": "host.name" + }, + { + "columnHeaderType": "not-filtered", + "id": "source.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "destination.ip" + }, + { + "columnHeaderType": "not-filtered", + "id": "user.name" + } + ], + "dataProviders": [], + "description": "", + "eventType": "all", + "filters": [], + "kqlMode": "filter", + "kqlQuery": { + "filterQuery": null + }, + "title": "abd", + "dateRange": { + "start": 1587370079200, + "end": 1587456479201 + }, + "savedQueryId": null, + "sort": { + "columnId": "@timestamp", + "sortDirection": "desc" + }, + "timelineType": "template", + "created": 1587473119992, + "createdBy": "casetester", + "updated": 1587473119992, + "updatedBy": "casetester", + "templateTimelineId": "745d0316-6af7-43bf-afd6-9747119754fb", // Please provide the existing template timeline version + "templateTimelineVersion": 2 // Please provide a template timeline version grater than existing one + }, + "timelineId":"f5a4bd10-83cd-11ea-bf78-0547a65f1281", // This is a must as well + "version":"Wzg2LDFd" // Please provide the existing timeline version +} +``` \ No newline at end of file diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts index 686f2b491cf88..a832c818d48b0 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/import_timelines.ts @@ -5,6 +5,7 @@ */ import { omit } from 'lodash/fp'; +import { TimelineType } from '../../../../../common/types/timeline'; export const mockDuplicateIdErrors = []; @@ -148,6 +149,13 @@ export const mockGetTimelineValue = { pinnedEventIds: ['k-gi8nABm-sIqJ_scOoS'], }; +export const mockGetTemplateTimelineValue = { + ...mockGetTimelineValue, + timelineType: TimelineType.template, + templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineVersion: 1, +}; + export const mockParsedTimelineObject = omit( [ 'globalNotes', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts index a83c443773302..304ca309775ff 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/__mocks__/request_responses.ts @@ -3,10 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { TIMELINE_EXPORT_URL, TIMELINE_IMPORT_URL } from '../../../../../common/constants'; -import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import * as rt from 'io-ts'; +import { + TIMELINE_EXPORT_URL, + TIMELINE_IMPORT_URL, + TIMELINE_URL, +} from '../../../../../common/constants'; import stream from 'stream'; +import { requestMock } from '../../../detection_engine/routes/__mocks__'; +import { SavedTimeline, TimelineType } from '../../../../../common/types/timeline'; +import { updateTimelineSchema } from '../schemas/update_timelines_schema'; +import { createTimelineSchema } from '../schemas/create_timelines_schema'; + const readable = new stream.Readable(); export const getExportTimelinesRequest = () => requestMock.create({ @@ -31,6 +39,96 @@ export const getImportTimelinesRequest = (filename?: string) => }, }); +export const inputTimeline: SavedTimeline = { + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp' }, + { columnHeaderType: 'not-filtered', id: 'message' }, + { columnHeaderType: 'not-filtered', id: 'event.category' }, + { columnHeaderType: 'not-filtered', id: 'event.action' }, + { columnHeaderType: 'not-filtered', id: 'host.name' }, + { columnHeaderType: 'not-filtered', id: 'source.ip' }, + { columnHeaderType: 'not-filtered', id: 'destination.ip' }, + { columnHeaderType: 'not-filtered', id: 'user.name' }, + ], + dataProviders: [], + description: '', + eventType: 'all', + filters: [], + kqlMode: 'filter', + kqlQuery: { filterQuery: null }, + title: 't', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + dateRange: { start: 1585227005527, end: 1585313405527 }, + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, +}; + +export const inputTemplateTimeline = { + ...inputTimeline, + timelineType: TimelineType.template, + templateTimelineId: null, + templateTimelineVersion: null, +}; + +export const createTimelineWithoutTimelineId = { + templateTimelineId: null, + timeline: inputTimeline, + timelineId: null, + version: null, + timelineType: TimelineType.default, +}; + +export const createTemplateTimelineWithoutTimelineId = { + templateTimelineId: null, + timeline: inputTemplateTimeline, + timelineId: null, + version: null, + timelineType: TimelineType.template, +}; + +export const createTimelineWithTimelineId = { + ...createTimelineWithoutTimelineId, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', +}; + +export const createTemplateTimelineWithTimelineId = { + ...createTemplateTimelineWithoutTimelineId, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineId: 'existing template timeline id', +}; + +export const updateTimelineWithTimelineId = { + timeline: inputTimeline, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + version: 'WzEyMjUsMV0=', +}; + +export const updateTemplateTimelineWithTimelineId = { + timeline: { + ...inputTemplateTimeline, + templateTimelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + templateTimelineVersion: 2, + }, + timelineId: '79deb4c0-6bc1-11ea-a90b-f5341fb7a189', + version: 'WzEyMjUsMV0=', +}; + +export const getCreateTimelinesRequest = (mockBody: rt.TypeOf<typeof createTimelineSchema>) => + requestMock.create({ + method: 'post', + path: TIMELINE_URL, + body: mockBody, + }); + +export const getUpdateTimelinesRequest = (mockBody: rt.TypeOf<typeof updateTimelineSchema>) => + requestMock.create({ + method: 'patch', + path: TIMELINE_URL, + body: mockBody, + }); + export const getImportTimelinesRequestEnableOverwrite = (filename?: string) => requestMock.create({ method: 'post', diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts new file mode 100644 index 0000000000000..70ee1532395a5 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.test.ts @@ -0,0 +1,272 @@ +/* + * 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 { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { + mockGetCurrentUser, + mockGetTimelineValue, + mockGetTemplateTimelineValue, +} from './__mocks__/import_timelines'; +import { + getCreateTimelinesRequest, + inputTimeline, + createTimelineWithoutTimelineId, + createTimelineWithTimelineId, + createTemplateTimelineWithoutTimelineId, + createTemplateTimelineWithTimelineId, +} from './__mocks__/request_responses'; +import { + CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_TIMELINE_ERROR_MESSAGE, +} from './utils/create_timelines'; + +describe('create timelines', () => { + let server: ReturnType<typeof serverMock.create>; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + let mockGetTimeline: jest.Mock; + let mockPersistTimeline: jest.Mock; + let mockPersistPinnedEventOnTimeline: jest.Mock; + let mockPersistNote: jest.Mock; + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + mockGetTimeline = jest.fn(); + mockPersistTimeline = jest.fn(); + mockPersistPinnedEventOnTimeline = jest.fn(); + mockPersistNote = jest.fn(); + }); + + describe('Manipulate timeline', () => { + describe('Create a new timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: createTimelineWithTimelineId, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getCreateTimelinesRequest(createTimelineWithoutTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Create a new timeline savedObject', async () => { + expect(mockPersistTimeline).toHaveBeenCalled(); + }); + + test('should Create a new timeline savedObject without timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); + }); + + test('should Create a new timeline savedObject without timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); + }); + + test('should Create a new timeline savedObject witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline); + }); + + test('should NOT Create new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Create notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithoutTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe('Import a timeline already exist', () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: CREATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); + + describe('Manipulate template timeline', () => { + describe('Create a new template timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: createTemplateTimelineWithTimelineId, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getCreateTimelinesRequest(createTemplateTimelineWithoutTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Create a new template timeline savedObject', async () => { + expect(mockPersistTimeline).toHaveBeenCalled(); + }); + + test('should Create a new template timeline savedObject without timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toBeNull(); + }); + + test('should Create a new template timeline savedObject without template timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toBeNull(); + }); + + test('should Create a new template timeline savedObject witn given template timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual( + createTemplateTimelineWithTimelineId.timeline + ); + }); + + test('should NOT Create new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Create notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTimelineWithoutTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe('Import a template timeline already exist', () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const createTimelinesRoute = jest.requireActual('./create_timelines_route') + .createTimelinesRoute; + createTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getCreateTimelinesRequest(createTemplateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts new file mode 100644 index 0000000000000..c456ae31fb7da --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/create_timelines_route.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_URL } from '../../../../common/constants'; +import { TimelineType } from '../../../../common/types/timeline'; + +import { ConfigType } from '../../..'; +import { SetupPlugins } from '../../../plugin'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; + +import { createTimelineSchema } from './schemas/create_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; +import { + createTimelines, + getTimeline, + getTemplateTimeline, + CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + CREATE_TIMELINE_ERROR_MESSAGE, +} from './utils/create_timelines'; + +export const createTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.post( + { + path: TIMELINE_URL, + validate: { + body: buildRouteValidation(createTimelineSchema), + }, + options: { + tags: ['access:siem'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + + const { timelineId, timeline, version } = request.body; + const { templateTimelineId, timelineType } = timeline; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; + + const existTimeline = + timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; + const existTemplateTimeline = + templateTimelineId != null + ? await getTemplateTimeline(frameworkRequest, templateTimelineId) + : null; + + if ( + (!isHandlingTemplateTimeline && existTimeline != null) || + (isHandlingTemplateTimeline && (existTemplateTimeline != null || existTimeline != null)) + ) { + return siemResponse.error({ + body: isHandlingTemplateTimeline + ? CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE + : CREATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }); + } + + // Create timeline + const newTimeline = await createTimelines(frameworkRequest, timeline, null, version); + return response.ok({ + body: { + data: { + persistTimeline: newTimeline, + }, + }, + }); + } catch (err) { + const error = transformError(err); + + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts index 47ca25e16bd50..2bccb7c393837 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.test.ts @@ -85,7 +85,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[0][0]).toEqual( - 'Invalid value undefined supplied to : { ids: Array<string> }/ids: Array<string>' + 'Invalid value "undefined" supplied to "ids"' ); }); @@ -98,11 +98,7 @@ describe('export timelines', () => { const result = server.validate(request); expect(result.badRequest.mock.calls[1][0]).toEqual( - [ - 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/file_name: string', - 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/0: "true"', - 'Invalid value undefined supplied to : { file_name: string, exclude_export_details: ("true" | "false") }/exclude_export_details: ("true" | "false")/1: "false"', - ].join('\n') + 'Invalid value "undefined" supplied to "file_name",Invalid value "undefined" supplied to "exclude_export_details",Invalid value "undefined" supplied to "exclude_export_details"' ); }); }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts index c59f6eb6ce3da..e0eefbf811a56 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts @@ -8,7 +8,7 @@ import { set as _set } from 'lodash/fp'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; import { IRouter } from '../../../../../../../src/core/server'; -import { ConfigType } from '../../..'; +import { ConfigType } from '../../../config'; import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; import { getExportTimelineByObjectIds } from './utils/export_timelines'; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts index 3931bf0e5bea5..56c152d02ae98 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.test.ts @@ -6,13 +6,13 @@ import { getImportTimelinesRequest } from './__mocks__/request_responses'; import { - createMockConfig, serverMock, requestContextMock, requestMock, + createMockConfig, } from '../../detection_engine/routes/__mocks__'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; -import { SecurityPluginSetup } from '../../../../../security/server'; +import { SecurityPluginSetup } from '../../../../../../plugins/security/server'; import { mockUniqueParsedObjects, @@ -24,7 +24,6 @@ import { } from './__mocks__/import_timelines'; describe('import timelines', () => { - let config: ReturnType<typeof createMockConfig>; let server: ReturnType<typeof serverMock.create>; let request: ReturnType<typeof requestMock.create>; let securitySetup: SecurityPluginSetup; @@ -43,7 +42,6 @@ describe('import timelines', () => { server = serverMock.create(); context = requestContextMock.createTools().context; - config = createMockConfig(); securitySetup = ({ authc: { @@ -84,40 +82,28 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(null), - persistTimeline: mockPersistTimeline.mockReturnValue({ - timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion }, - }), - }; + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: { savedObjectId: newTimelineSavedObjectId, version: newTimelineVersion }, }), }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); test('should use given timelineId to check if the timeline savedObject already exist', async () => { @@ -230,38 +216,26 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), - persistTimeline: mockPersistTimeline, - }; - }), + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline, }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); }); test('returns error message', async () => { @@ -286,36 +260,24 @@ describe('import timelines', () => { beforeEach(() => { jest.doMock('../saved_object', () => { return { - Timeline: jest.fn().mockImplementation(() => { - return { - getTimeline: mockGetTimeline.mockReturnValue(null), - persistTimeline: mockPersistTimeline.mockReturnValue({ - timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, - }), - }; + getTimeline: mockGetTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: { savedObjectId: '79deb4c0-6bc1-11ea-9999-f5341fb7a189' }, }), }; }); jest.doMock('../../pinned_event/saved_object', () => { return { - PinnedEvent: jest.fn().mockImplementation(() => { - return { - persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( - new Error('Test error') - ), - }; - }), + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline.mockReturnValue( + new Error('Test error') + ), }; }); jest.doMock('../../note/saved_object', () => { return { - Note: jest.fn().mockImplementation(() => { - return { - persistNote: mockPersistNote, - }; - }), + persistNote: mockPersistNote, }; }); }); @@ -328,14 +290,14 @@ describe('import timelines', () => { const importTimelinesRoute = jest.requireActual('./import_timelines_route') .importTimelinesRoute; - importTimelinesRoute(server.router, config, securitySetup); + importTimelinesRoute(server.router, createMockConfig(), securitySetup); const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( [ - 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/0: ReadableRt', - 'Invalid value undefined supplied to : { file: (ReadableRt & { hapi: { filename: string } }) }/file: (ReadableRt & { hapi: { filename: string } })/1: { hapi: { filename: string } }', - ].join('\n') + 'Invalid value "undefined" supplied to "file"', + 'Invalid value "undefined" supplied to "file"', + ].join(',') ); }); }); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 258ef9faf671b..bff89bdf9b5b2 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -5,9 +5,19 @@ */ import { extname } from 'path'; -import { chunk, omit, set } from 'lodash/fp'; +import { chunk, omit } from 'lodash/fp'; + +import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; +import { IRouter } from '../../../../../../../src/core/server'; import { TIMELINE_IMPORT_URL } from '../../../../common/constants'; + +import { SetupPlugins } from '../../../plugin'; +import { ConfigType } from '../../../config'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; + +import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; +import { validate } from '../../detection_engine/routes/rules/validate'; import { buildSiemResponse, createBulkErrorObject, @@ -16,32 +26,22 @@ import { } from '../../detection_engine/routes/utils'; import { createTimelinesStreamFromNdJson } from '../create_timelines_stream_from_ndjson'; -import { createPromiseFromStreams } from '../../../../../../../src/legacy/utils'; +import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; import { - createTimelines, getTupleDuplicateErrorsAndUniqueTimeline, isBulkError, isImportRegular, ImportTimelineResponse, ImportTimelinesSchema, PromiseFromStreams, + timelineSavedObjectOmittedFields, } from './utils/import_timelines'; +import { createTimelines, getTimeline } from './utils/create_timelines'; -import { IRouter } from '../../../../../../../src/core/server'; -import { SetupPlugins } from '../../../plugin'; -import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; -import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; -import { ConfigType } from '../../..'; - -import { Timeline } from '../saved_object'; -import { validate } from '../../detection_engine/routes/rules/validate'; -import { FrameworkRequest } from '../../framework'; -import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; const CHUNK_PARSED_OBJECT_SIZE = 10; -const timelineLib = new Timeline(); - export const importTimelinesRoute = ( router: IRouter, config: ConfigType, @@ -95,9 +95,7 @@ export const importTimelinesRoute = ( const chunkParseObjects = chunk(CHUNK_PARSED_OBJECT_SIZE, uniqueParsedObjects); let importTimelineResponse: ImportTimelineResponse[] = []; - const user = await security?.authc.getCurrentUser(request); - let frameworkRequest = set('context.core.savedObjects.client', savedObjectsClient, request); - frameworkRequest = set('user', user, frameworkRequest); + const frameworkRequest = await buildFrameworkRequest(context, security, request); while (chunkParseObjects.length) { const batchParseObjects = chunkParseObjects.shift() ?? []; @@ -125,32 +123,16 @@ export const importTimelinesRoute = ( eventNotes, } = parsedTimeline; const parsedTimelineObject = omit( - [ - 'globalNotes', - 'eventNotes', - 'pinnedEventIds', - 'version', - 'savedObjectId', - 'created', - 'createdBy', - 'updated', - 'updatedBy', - ], + timelineSavedObjectOmittedFields, parsedTimeline ); + let newTimeline = null; try { - let timeline = null; - try { - timeline = await timelineLib.getTimeline( - (frameworkRequest as unknown) as FrameworkRequest, - savedObjectId - ); - // eslint-disable-next-line no-empty - } catch (e) {} + const timeline = await getTimeline(frameworkRequest, savedObjectId); if (timeline == null) { - const newSavedObjectId = await createTimelines( - (frameworkRequest as unknown) as FrameworkRequest, + newTimeline = await createTimelines( + frameworkRequest, parsedTimelineObject, null, // timelineSavedObjectId null, // timelineVersion @@ -159,7 +141,10 @@ export const importTimelinesRoute = ( [] // existing note ids ); - resolve({ timeline_id: newSavedObjectId, status_code: 200 }); + resolve({ + timeline_id: newTimeline.timeline.savedObjectId, + status_code: 200, + }); } else { resolve( createBulkErrorObject({ diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/create_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/create_timelines_schema.ts new file mode 100644 index 0000000000000..241d266a14c78 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/create_timelines_schema.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import * as rt from 'io-ts'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const createTimelineSchema = rt.intersection([ + rt.type({ + timeline: SavedTimelineRuntimeType, + }), + rt.partial({ + timelineId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), + }), +]); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts index 056fdaf0d2515..3b340b1c15359 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/import_timelines_schema.ts @@ -7,14 +7,17 @@ import * as rt from 'io-ts'; import { Readable } from 'stream'; import { either } from 'fp-ts/lib/Either'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; + import { eventNotes, globalNotes, pinnedEventIds } from './schemas'; -import { SavedTimelineRuntimeType } from '../../types'; +import { unionWithNullType } from '../../../../../common/utility_types'; export const ImportTimelinesSchemaRt = rt.intersection([ SavedTimelineRuntimeType, rt.type({ - savedObjectId: rt.string, - version: rt.string, + savedObjectId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), }), rt.type({ globalNotes, diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index 71627363ef0f8..1fd3a3554dc15 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import * as runtimeTypes from 'io-ts'; -import { unionWithNullType } from '../../../framework'; -import { SavedNoteRuntimeType } from '../../../note/types'; +import { unionWithNullType } from '../../../../../common/utility_types'; +import { SavedNoteRuntimeType } from '../../../../../common/types/timeline/note'; -export const eventNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); -export const globalNotes = runtimeTypes.array(unionWithNullType(SavedNoteRuntimeType)); -export const pinnedEventIds = runtimeTypes.array(unionWithNullType(runtimeTypes.string)); +export const eventNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); +export const globalNotes = unionWithNullType(runtimeTypes.array(SavedNoteRuntimeType)); +export const pinnedEventIds = unionWithNullType(runtimeTypes.array(runtimeTypes.string)); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts new file mode 100644 index 0000000000000..43f4208947aa5 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/schemas/update_timelines_schema.ts @@ -0,0 +1,16 @@ +/* + * 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 * as rt from 'io-ts'; + +import { SavedTimelineRuntimeType } from '../../../../../common/types/timeline'; +import { unionWithNullType } from '../../../../../common/utility_types'; + +export const updateTimelineSchema = rt.type({ + timeline: SavedTimelineRuntimeType, + timelineId: unionWithNullType(rt.string), + version: unionWithNullType(rt.string), +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts new file mode 100644 index 0000000000000..9c47488d47159 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.test.ts @@ -0,0 +1,288 @@ +/* + * 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 { SecurityPluginSetup } from '../../../../../../plugins/security/server'; + +import { + serverMock, + requestContextMock, + createMockConfig, +} from '../../detection_engine/routes/__mocks__'; + +import { + getUpdateTimelinesRequest, + inputTimeline, + updateTimelineWithTimelineId, + updateTemplateTimelineWithTimelineId, +} from './__mocks__/request_responses'; +import { + mockGetCurrentUser, + mockGetTimelineValue, + mockGetTemplateTimelineValue, +} from './__mocks__/import_timelines'; +import { + UPDATE_TIMELINE_ERROR_MESSAGE, + UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, +} from './utils/update_timelines'; + +describe('update timelines', () => { + let server: ReturnType<typeof serverMock.create>; + let securitySetup: SecurityPluginSetup; + let { context } = requestContextMock.createTools(); + let mockGetTimeline: jest.Mock; + let mockGetTemplateTimeline: jest.Mock; + let mockPersistTimeline: jest.Mock; + let mockPersistPinnedEventOnTimeline: jest.Mock; + let mockPersistNote: jest.Mock; + + beforeEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + server = serverMock.create(); + context = requestContextMock.createTools().context; + + securitySetup = ({ + authc: { + getCurrentUser: jest.fn().mockReturnValue(mockGetCurrentUser), + }, + authz: {}, + } as unknown) as SecurityPluginSetup; + + mockGetTimeline = jest.fn(); + mockGetTemplateTimeline = jest.fn(); + mockPersistTimeline = jest.fn(); + mockPersistPinnedEventOnTimeline = jest.fn(); + mockPersistNote = jest.fn(); + }); + + describe('Manipulate timeline', () => { + describe('Update an existing timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTimelineValue), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: updateTimelineWithTimelineId.timeline, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getUpdateTimelinesRequest(updateTimelineWithTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Check a if given timeline id exist', async () => { + expect(mockGetTimeline.mock.calls[0][1]).toEqual(updateTimelineWithTimelineId.timelineId); + }); + + test('should Update existing timeline savedObject with timelineId', async () => { + expect(mockPersistTimeline.mock.calls[0][1]).toEqual( + updateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing timeline savedObject with timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toEqual(updateTimelineWithTimelineId.version); + }); + + test('should Update existing timeline savedObject witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual(inputTimeline); + }); + + test('should NOT Update new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Update notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create timeline successfully', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTimelineWithTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe("Update a timeline that doesn't exist", () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue(null), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: UPDATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); + + describe('Manipulate template timeline', () => { + describe('Update an existing template timeline', () => { + beforeEach(async () => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(mockGetTemplateTimelineValue), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + timeline: [mockGetTemplateTimelineValue], + }), + persistTimeline: mockPersistTimeline.mockReturnValue({ + timeline: updateTimelineWithTimelineId.timeline, + }), + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + + const mockRequest = getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId); + await server.inject(mockRequest, context); + }); + + test('should Check if given timeline id exist', async () => { + expect(mockGetTimeline.mock.calls[0][1]).toEqual( + updateTemplateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing template timeline with template timelineId', async () => { + expect(mockGetTemplateTimeline.mock.calls[0][1]).toEqual( + updateTemplateTimelineWithTimelineId.timelineId + ); + }); + + test('should Update existing template timeline with timeline version', async () => { + expect(mockPersistTimeline.mock.calls[0][2]).toEqual( + updateTemplateTimelineWithTimelineId.version + ); + }); + + test('should Update existing template timeline witn given timeline', async () => { + expect(mockPersistTimeline.mock.calls[0][3]).toEqual( + updateTemplateTimelineWithTimelineId.timeline + ); + }); + + test('should NOT Update new pinned events', async () => { + expect(mockPersistPinnedEventOnTimeline).not.toBeCalled(); + }); + + test('should NOT Update notes', async () => { + expect(mockPersistNote).not.toBeCalled(); + }); + + test('returns 200 when create template timeline successfully', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId), + context + ); + expect(response.status).toEqual(200); + }); + }); + + describe("Update a template timeline that doesn't exist", () => { + beforeEach(() => { + jest.doMock('../saved_object', () => { + return { + getTimeline: mockGetTimeline.mockReturnValue(null), + getTimelineByTemplateTimelineId: mockGetTemplateTimeline.mockReturnValue({ + timeline: [], + }), + persistTimeline: mockPersistTimeline, + }; + }); + + jest.doMock('../../pinned_event/saved_object', () => { + return { + persistPinnedEventOnTimeline: mockPersistPinnedEventOnTimeline, + }; + }); + + jest.doMock('../../note/saved_object', () => { + return { + persistNote: mockPersistNote, + }; + }); + + const updateTimelinesRoute = jest.requireActual('./update_timelines_route') + .updateTimelinesRoute; + updateTimelinesRoute(server.router, createMockConfig(), securitySetup); + }); + + test('returns error message', async () => { + const response = await server.inject( + getUpdateTimelinesRequest(updateTemplateTimelineWithTimelineId), + context + ); + expect(response.body).toEqual({ + message: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + status_code: 405, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts new file mode 100644 index 0000000000000..a0f3d11a1533d --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/update_timelines_route.ts @@ -0,0 +1,87 @@ +/* + * 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 { IRouter } from '../../../../../../../src/core/server'; + +import { TIMELINE_URL } from '../../../../common/constants'; +import { TimelineType } from '../../../../common/types/timeline'; + +import { SetupPlugins } from '../../../plugin'; +import { buildRouteValidation } from '../../../utils/build_validation/route_validation'; +import { ConfigType } from '../../..'; + +import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; +import { FrameworkRequest } from '../../framework'; + +import { updateTimelineSchema } from './schemas/update_timelines_schema'; +import { buildFrameworkRequest } from './utils/common'; +import { createTimelines, getTimeline, getTemplateTimeline } from './utils/create_timelines'; +import { checkIsFailureCases } from './utils/update_timelines'; + +export const updateTimelinesRoute = ( + router: IRouter, + config: ConfigType, + security: SetupPlugins['security'] +) => { + router.patch( + { + path: TIMELINE_URL, + validate: { + body: buildRouteValidation(updateTimelineSchema), + }, + options: { + tags: ['access:siem'], + }, + }, + // eslint-disable-next-line complexity + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const frameworkRequest = await buildFrameworkRequest(context, security, request); + const { timelineId, timeline, version } = request.body; + const { templateTimelineId, templateTimelineVersion, timelineType } = timeline; + const isHandlingTemplateTimeline = timelineType === TimelineType.template; + const existTimeline = + timelineId != null ? await getTimeline(frameworkRequest, timelineId) : null; + + const existTemplateTimeline = + templateTimelineId != null + ? await getTemplateTimeline(frameworkRequest, templateTimelineId) + : null; + const errorObj = checkIsFailureCases( + isHandlingTemplateTimeline, + version, + templateTimelineVersion ?? null, + existTimeline, + existTemplateTimeline + ); + if (errorObj != null) { + return siemResponse.error(errorObj); + } + const updatedTimeline = await createTimelines( + (frameworkRequest as unknown) as FrameworkRequest, + timeline, + timelineId, + version + ); + return response.ok({ + body: { + data: { + persistTimeline: updatedTimeline, + }, + }, + }); + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts new file mode 100644 index 0000000000000..1036a74b74a03 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { set } from 'lodash/fp'; + +import { SetupPlugins } from '../../../../plugin'; +import { KibanaRequest } from '../../../../../../../../src/core/server'; +import { RequestHandlerContext } from '../../../../../../../../target/types/core/server'; +import { FrameworkRequest } from '../../../framework'; + +export const buildFrameworkRequest = async ( + context: RequestHandlerContext, + security: SetupPlugins['security'], + request: KibanaRequest +): Promise<FrameworkRequest> => { + const savedObjectsClient = context.core.savedObjects.client; + const user = await security?.authc.getCurrentUser(request); + + return set<FrameworkRequest>( + 'user', + user, + set<KibanaRequest & { context: RequestHandlerContext }>( + 'context.core.savedObjects.client', + savedObjectsClient, + request + ) + ); +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts new file mode 100644 index 0000000000000..2c67a514cdf97 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/create_timelines.ts @@ -0,0 +1,150 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import * as timelineLib from '../../saved_object'; +import * as pinnedEventLib from '../../../pinned_event/saved_object'; +import * as noteLib from '../../../note/saved_object'; +import { FrameworkRequest } from '../../../framework'; +import { SavedTimeline, TimelineSavedObject } from '../../../../../common/types/timeline'; +import { SavedNote } from '../../../../../common/types/timeline/note'; +import { NoteResult, ResponseTimeline } from '../../../../graphql/types'; +export const CREATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE timeline with POST is not allowed, please use PATCH instead'; +export const CREATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = + 'UPDATE template timeline with POST is not allowed, please use PATCH instead'; + +export const saveTimelines = ( + frameworkRequest: FrameworkRequest, + timeline: SavedTimeline, + timelineSavedObjectId?: string | null, + timelineVersion?: string | null +): Promise<ResponseTimeline> => { + return timelineLib.persistTimeline( + frameworkRequest, + timelineSavedObjectId ?? null, + timelineVersion ?? null, + timeline + ); +}; + +export const savePinnedEvents = ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + pinnedEventIds: string[] +) => + Promise.all( + pinnedEventIds.map(eventId => + pinnedEventLib.persistPinnedEventOnTimeline( + frameworkRequest, + null, // pinnedEventSavedObjectId + eventId, + timelineSavedObjectId + ) + ) + ); + +export const saveNotes = ( + frameworkRequest: FrameworkRequest, + timelineSavedObjectId: string, + timelineVersion?: string | null, + existingNoteIds?: string[], + newNotes?: NoteResult[] +) => { + return Promise.all( + newNotes?.map(note => { + const newNote: SavedNote = { + eventId: note.eventId, + note: note.note, + timelineId: timelineSavedObjectId, + }; + + return noteLib.persistNote( + frameworkRequest, + existingNoteIds?.find(nId => nId === note.noteId) ?? null, + timelineVersion ?? null, + newNote + ); + }) ?? [] + ); +}; + +export const createTimelines = async ( + frameworkRequest: FrameworkRequest, + timeline: SavedTimeline, + timelineSavedObjectId?: string | null, + timelineVersion?: string | null, + pinnedEventIds?: string[] | null, + notes?: NoteResult[], + existingNoteIds?: string[] +): Promise<ResponseTimeline> => { + const responseTimeline = await saveTimelines( + frameworkRequest, + timeline, + timelineSavedObjectId, + timelineVersion + ); + const newTimelineSavedObjectId = responseTimeline.timeline.savedObjectId; + const newTimelineVersion = responseTimeline.timeline.version; + + let myPromises: unknown[] = []; + if (pinnedEventIds != null && !isEmpty(pinnedEventIds)) { + myPromises = [ + ...myPromises, + savePinnedEvents( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + pinnedEventIds + ), + ]; + } + if (!isEmpty(notes)) { + myPromises = [ + ...myPromises, + saveNotes( + frameworkRequest, + timelineSavedObjectId ?? newTimelineSavedObjectId, + newTimelineVersion, + existingNoteIds, + notes + ), + ]; + } + + if (myPromises.length > 0) { + await Promise.all(myPromises); + } + + return responseTimeline; +}; + +export const getTimeline = async ( + frameworkRequest: FrameworkRequest, + savedObjectId: string +): Promise<TimelineSavedObject | null> => { + let timeline = null; + try { + timeline = await timelineLib.getTimeline(frameworkRequest, savedObjectId); + // eslint-disable-next-line no-empty + } catch (e) {} + return timeline; +}; + +export const getTemplateTimeline = async ( + frameworkRequest: FrameworkRequest, + templateTimelineId: string +): Promise<TimelineSavedObject | null> => { + let templateTimeline = null; + try { + templateTimeline = await timelineLib.getTimelineByTemplateTimelineId( + frameworkRequest, + templateTimelineId + ); + } catch (e) { + return null; + } + return templateTimeline.timeline[0]; +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index edd4abe0d76b5..ea9a5fab66805 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -4,19 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set as _set } from 'lodash/fp'; -import { - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, -} from '../../../../saved_objects'; -import { NoteSavedObject } from '../../../note/types'; -import { PinnedEventSavedObject } from '../../../pinned_event/types'; -import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline'; - -import { convertSavedObjectToSavedPinnedEvent } from '../../../pinned_event/saved_object'; -import { convertSavedObjectToSavedNote } from '../../../note/saved_object'; - import { SavedObjectsClient, SavedObjectsFindOptions, @@ -28,9 +15,21 @@ import { ExportTimelineSavedObjectsClient, ExportedNotes, TimelineSavedObject, -} from '../../types'; + ExportTimelineNotFoundError, +} from '../../../../../common/types/timeline'; +import { NoteSavedObject } from '../../../../../common/types/timeline/note'; +import { PinnedEventSavedObject } from '../../../../../common/types/timeline/pinned_event'; + import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; +import { convertSavedObjectToSavedPinnedEvent } from '../../../pinned_event/saved_object'; +import { convertSavedObjectToSavedNote } from '../../../note/saved_object'; +import { pinnedEventSavedObjectType } from '../../../pinned_event/saved_object_mappings'; +import { noteSavedObjectType } from '../../../note/saved_object_mappings'; + +import { timelineSavedObjectType } from '../../saved_object_mappings'; +import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline'; + export type TimelineSavedObjectsClient = Pick< SavedObjectsClient, | 'get' @@ -129,12 +128,23 @@ const getTimelines = async ( ) ); - const timelineObjects: TimelineSavedObject[] | undefined = - savedObjects != null - ? savedObjects.saved_objects.map((savedObject: unknown) => { - return convertSavedObjectToSavedTimeline(savedObject); - }) - : []; + const timelineObjects: { + timelines: TimelineSavedObject[]; + errors: ExportTimelineNotFoundError[]; + } = savedObjects.saved_objects.reduce( + (acc, savedObject) => { + return savedObject.error == null + ? { + errors: acc.errors, + timelines: [...acc.timelines, convertSavedObjectToSavedTimeline(savedObject)], + } + : { errors: [...acc.errors, savedObject.error], timelines: acc.timelines }; + }, + { + timelines: [] as TimelineSavedObject[], + errors: [] as ExportTimelineNotFoundError[], + } + ); return timelineObjects; }; @@ -142,12 +152,8 @@ const getTimelines = async ( const getTimelinesFromObjects = async ( savedObjectsClient: ExportTimelineSavedObjectsClient, ids: string[] -): Promise<ExportedTimelines[]> => { - const timelines: TimelineSavedObject[] = await getTimelines(savedObjectsClient, ids); - // To Do for feature freeze - // if (timelines.length !== request.body.ids.length) { - // //figure out which is missing to tell user - // } +): Promise<Array<ExportedTimelines | ExportTimelineNotFoundError>> => { + const { timelines, errors } = await getTimelines(savedObjectsClient, ids); const [notes, pinnedEventIds] = await Promise.all([ Promise.all(ids.map(timelineId => getNotesByTimelineId(savedObjectsClient, timelineId))), @@ -181,7 +187,7 @@ const getTimelinesFromObjects = async ( return acc; }, []); - return myResponse ?? []; + return [...myResponse, ...errors] ?? []; }; export const getExportTimelineByObjectIds = async ({ diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts index f69a715f9b2c9..9e120cdc023dc 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/import_timelines.ts @@ -7,20 +7,10 @@ import uuid from 'uuid'; import { has } from 'lodash/fp'; import { createBulkErrorObject, BulkError } from '../../../detection_engine/routes/utils'; -import { PinnedEvent } from '../../../pinned_event/saved_object'; -import { Note } from '../../../note/saved_object'; - -import { Timeline } from '../../saved_object'; -import { SavedTimeline } from '../../types'; -import { FrameworkRequest } from '../../../framework'; -import { SavedNote } from '../../../note/types'; +import { SavedTimeline } from '../../../../../common/types/timeline'; import { NoteResult } from '../../../../graphql/types'; import { HapiReadableStream } from '../../../detection_engine/rules/types'; -const pinnedEventLib = new PinnedEvent(); -const timelineLib = new Timeline(); -const noteLib = new Note(); - export interface ImportTimelinesSchema { success: boolean; success_count: number; @@ -84,100 +74,6 @@ export const getTupleDuplicateErrorsAndUniqueTimeline = ( return [Array.from(errors.values()), Array.from(timelinesAcc.values())]; }; -export const saveTimelines = async ( - frameworkRequest: FrameworkRequest, - timeline: SavedTimeline, - timelineSavedObjectId?: string | null, - timelineVersion?: string | null -) => { - const newTimelineRes = await timelineLib.persistTimeline( - frameworkRequest, - timelineSavedObjectId ?? null, - timelineVersion ?? null, - timeline - ); - - return { - newTimelineSavedObjectId: newTimelineRes?.timeline?.savedObjectId ?? null, - newTimelineVersion: newTimelineRes?.timeline?.version ?? null, - }; -}; - -export const savePinnedEvents = ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - pinnedEventIds?: string[] | null -) => { - return ( - pinnedEventIds?.map(eventId => { - return pinnedEventLib.persistPinnedEventOnTimeline( - frameworkRequest, - null, // pinnedEventSavedObjectId - eventId, - timelineSavedObjectId - ); - }) ?? [] - ); -}; - -export const saveNotes = ( - frameworkRequest: FrameworkRequest, - timelineSavedObjectId: string, - timelineVersion?: string | null, - existingNoteIds?: string[], - newNotes?: NoteResult[] -) => { - return Promise.all( - newNotes?.map(note => { - const newNote: SavedNote = { - eventId: note.eventId, - note: note.note, - timelineId: timelineSavedObjectId, - }; - - return noteLib.persistNote( - frameworkRequest, - existingNoteIds?.find(nId => nId === note.noteId) ?? null, - timelineVersion ?? null, - newNote - ); - }) ?? [] - ); -}; - -export const createTimelines = async ( - frameworkRequest: FrameworkRequest, - timeline: SavedTimeline, - timelineSavedObjectId?: string | null, - timelineVersion?: string | null, - pinnedEventIds?: string[] | null, - notes?: NoteResult[], - existingNoteIds?: string[] -) => { - const { newTimelineSavedObjectId, newTimelineVersion } = await saveTimelines( - frameworkRequest, - timeline, - timelineSavedObjectId, - timelineVersion - ); - await Promise.all([ - savePinnedEvents( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - pinnedEventIds - ), - saveNotes( - frameworkRequest, - timelineSavedObjectId ?? newTimelineSavedObjectId, - newTimelineVersion, - existingNoteIds, - notes - ), - ]); - - return newTimelineSavedObjectId; -}; - export const isImportRegular = ( importTimelineResponse: ImportTimelineResponse ): importTimelineResponse is ImportRegular => { @@ -189,3 +85,15 @@ export const isBulkError = ( ): importRuleResponse is BulkError => { return has('error', importRuleResponse); }; + +export const timelineSavedObjectOmittedFields = [ + 'globalNotes', + 'eventNotes', + 'pinnedEventIds', + 'version', + 'savedObjectId', + 'created', + 'createdBy', + 'updated', + 'updatedBy', +]; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts new file mode 100644 index 0000000000000..6a25d8def9116 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/update_timelines.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TimelineSavedObject } from '../../../../../common/types/timeline'; + +export const UPDATE_TIMELINE_ERROR_MESSAGE = + 'CREATE timeline with PATCH is not allowed, please use POST instead'; +export const UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE = + 'CREATE template timeline with PATCH is not allowed, please use POST instead'; +export const NO_MATCH_VERSION_ERROR_MESSAGE = + 'TimelineVersion conflict: The given version doesn not match with existing timeline'; +export const NO_MATCH_ID_ERROR_MESSAGE = + "Timeline id doesn't match with existing template timeline"; +export const OLDER_VERSION_ERROR_MESSAGE = + 'Template timelineVersion conflict: The given version is older then existing version'; + +export const checkIsFailureCases = ( + isHandlingTemplateTimeline: boolean, + version: string | null, + templateTimelineVersion: number | null, + existTimeline: TimelineSavedObject | null, + existTemplateTimeline: TimelineSavedObject | null +) => { + if (!isHandlingTemplateTimeline && existTimeline == null) { + return { + body: UPDATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else if (isHandlingTemplateTimeline && existTemplateTimeline == null) { + // Throw error to create template timeline in patch + return { + body: UPDATE_TEMPLATE_TIMELINE_ERROR_MESSAGE, + statusCode: 405, + }; + } else if ( + isHandlingTemplateTimeline && + existTimeline != null && + existTemplateTimeline != null && + existTimeline.savedObjectId !== existTemplateTimeline.savedObjectId + ) { + // Throw error you can not have a no matching between your timeline and your template timeline during an update + return { + body: NO_MATCH_ID_ERROR_MESSAGE, + statusCode: 409, + }; + } else if (!isHandlingTemplateTimeline && existTimeline?.version !== version) { + // throw error 409 conflict timeline + return { + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else if ( + isHandlingTemplateTimeline && + existTemplateTimeline != null && + existTemplateTimeline.templateTimelineVersion == null && + existTemplateTimeline.version !== version + ) { + // throw error 409 conflict timeline + return { + body: NO_MATCH_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else if ( + isHandlingTemplateTimeline && + templateTimelineVersion != null && + existTemplateTimeline != null && + existTemplateTimeline.templateTimelineVersion != null && + existTemplateTimeline.templateTimelineVersion >= templateTimelineVersion + ) { + // Throw error you can not update a template timeline version with an old version + return { + body: OLDER_VERSION_ERROR_MESSAGE, + statusCode: 409, + }; + } else { + return null; + } +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts index e8cd27947589f..d2df7589f3c4a 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object.ts @@ -8,260 +8,308 @@ import { getOr } from 'lodash/fp'; import { SavedObjectsFindOptions } from '../../../../../../src/core/server'; import { UNAUTHENTICATED_USER } from '../../../common/constants'; +import { NoteSavedObject } from '../../../common/types/timeline/note'; +import { PinnedEventSavedObject } from '../../../common/types/timeline/pinned_event'; +import { SavedTimeline, TimelineSavedObject, TimelineType } from '../../../common/types/timeline'; import { ResponseTimeline, PageInfoTimeline, SortTimeline, ResponseFavoriteTimeline, TimelineResult, + Maybe, } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; -import { Note } from '../note/saved_object'; -import { NoteSavedObject } from '../note/types'; -import { PinnedEventSavedObject } from '../pinned_event/types'; -import { PinnedEvent } from '../pinned_event/saved_object'; +import * as note from '../note/saved_object'; +import * as pinnedEvent from '../pinned_event/saved_object'; import { convertSavedObjectToSavedTimeline } from './convert_saved_object_to_savedtimeline'; import { pickSavedTimeline } from './pick_saved_timeline'; import { timelineSavedObjectType } from './saved_object_mappings'; -import { SavedTimeline, TimelineSavedObject } from './types'; interface ResponseTimelines { timeline: TimelineSavedObject[]; totalCount: number; } -export class Timeline { - private readonly note = new Note(); - private readonly pinnedEvent = new PinnedEvent(); +export interface ResponseTemplateTimeline { + code?: Maybe<number>; - public async getTimeline( - request: FrameworkRequest, - timelineId: string - ): Promise<TimelineSavedObject> { - return this.getSavedTimeline(request, timelineId); - } + message?: Maybe<string>; + + templateTimeline: TimelineResult; +} - public async getAllTimeline( +export interface Timeline { + getTimeline: (request: FrameworkRequest, timelineId: string) => Promise<TimelineSavedObject>; + + getAllTimeline: ( request: FrameworkRequest, onlyUserFavorite: boolean | null, pageInfo: PageInfoTimeline | null, search: string | null, sort: SortTimeline | null - ): Promise<ResponseTimelines> { - const options: SavedObjectsFindOptions = { - type: timelineSavedObjectType, - perPage: pageInfo != null ? pageInfo.pageSize : undefined, - page: pageInfo != null ? pageInfo.pageIndex : undefined, - search: search != null ? search : undefined, - searchFields: onlyUserFavorite - ? ['title', 'description', 'favorite.keySearch'] - : ['title', 'description'], - sortField: sort != null ? sort.sortField : undefined, - sortOrder: sort != null ? sort.sortOrder : undefined, - }; - - return this.getAllSavedTimeline(request, options); - } + ) => Promise<ResponseTimelines>; - public async persistFavorite( + persistFavorite: ( request: FrameworkRequest, timelineId: string | null - ): Promise<ResponseFavoriteTimeline> { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const fullName = request.user?.full_name ?? ''; - try { - let timeline: SavedTimeline = {}; - if (timelineId != null) { - const { - eventIdToNoteIds, - notes, - noteIds, - pinnedEventIds, - pinnedEventsSaveObject, - savedObjectId, - version, - ...savedTimeline - } = await this.getBasicSavedTimeline(request, timelineId); - timelineId = savedObjectId; // eslint-disable-line no-param-reassign - timeline = savedTimeline; - } + ) => Promise<ResponseFavoriteTimeline>; - const userFavoriteTimeline = { - keySearch: userName != null ? convertStringToBase64(userName) : null, - favoriteDate: new Date().valueOf(), - fullName, - userName, - }; - if (timeline.favorite != null) { - const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( - user => user.userName === userName - ); - - timeline.favorite = - alreadyExistsTimelineFavoriteByUser > -1 - ? [ - ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), - ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), - ] - : [...timeline.favorite, userFavoriteTimeline]; - } else if (timeline.favorite == null) { - timeline.favorite = [userFavoriteTimeline]; - } + persistTimeline: ( + request: FrameworkRequest, + timelineId: string | null, + version: string | null, + timeline: SavedTimeline, + timelineType?: TimelineType | null + ) => Promise<ResponseTimeline>; - const persistResponse = await this.persistTimeline(request, timelineId, null, timeline); + deleteTimeline: (request: FrameworkRequest, timelineIds: string[]) => Promise<void>; + convertStringToBase64: (text: string) => string; + timelineWithReduxProperties: ( + notes: NoteSavedObject[], + pinnedEvents: PinnedEventSavedObject[], + timeline: TimelineSavedObject, + userName: string + ) => TimelineSavedObject; +} + +export const getTimeline = async ( + request: FrameworkRequest, + timelineId: string +): Promise<TimelineSavedObject> => { + return getSavedTimeline(request, timelineId); +}; + +export const getTimelineByTemplateTimelineId = async ( + request: FrameworkRequest, + templateTimelineId: string +): Promise<{ + totalCount: number; + timeline: TimelineSavedObject[]; +}> => { + const options: SavedObjectsFindOptions = { + type: timelineSavedObjectType, + filter: `siem-ui-timeline.attributes.templateTimelineId: ${templateTimelineId}`, + }; + return getAllSavedTimeline(request, options); +}; + +export const getAllTimeline = async ( + request: FrameworkRequest, + onlyUserFavorite: boolean | null, + pageInfo: PageInfoTimeline | null, + search: string | null, + sort: SortTimeline | null +): Promise<ResponseTimelines> => { + const options: SavedObjectsFindOptions = { + type: timelineSavedObjectType, + perPage: pageInfo != null ? pageInfo.pageSize : undefined, + page: pageInfo != null ? pageInfo.pageIndex : undefined, + search: search != null ? search : undefined, + searchFields: onlyUserFavorite + ? ['title', 'description', 'favorite.keySearch'] + : ['title', 'description'], + sortField: sort != null ? sort.sortField : undefined, + sortOrder: sort != null ? sort.sortOrder : undefined, + }; + return getAllSavedTimeline(request, options); +}; + +export const persistFavorite = async ( + request: FrameworkRequest, + timelineId: string | null +): Promise<ResponseFavoriteTimeline> => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const fullName = request.user?.full_name ?? ''; + try { + let timeline: SavedTimeline = {}; + if (timelineId != null) { + const { + eventIdToNoteIds, + notes, + noteIds, + pinnedEventIds, + pinnedEventsSaveObject, + savedObjectId, + version, + ...savedTimeline + } = await getBasicSavedTimeline(request, timelineId); + timelineId = savedObjectId; // eslint-disable-line no-param-reassign + timeline = savedTimeline; + } + + const userFavoriteTimeline = { + keySearch: userName != null ? convertStringToBase64(userName) : null, + favoriteDate: new Date().valueOf(), + fullName, + userName, + }; + if (timeline.favorite != null) { + const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( + user => user.userName === userName + ); + + timeline.favorite = + alreadyExistsTimelineFavoriteByUser > -1 + ? [ + ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), + ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), + ] + : [...timeline.favorite, userFavoriteTimeline]; + } else if (timeline.favorite == null) { + timeline.favorite = [userFavoriteTimeline]; + } + + const persistResponse = await persistTimeline(request, timelineId, null, timeline); + return { + savedObjectId: persistResponse.timeline.savedObjectId, + version: persistResponse.timeline.version, + favorite: + persistResponse.timeline.favorite != null + ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) + : [], + }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { return { - savedObjectId: persistResponse.timeline.savedObjectId, - version: persistResponse.timeline.version, - favorite: - persistResponse.timeline.favorite != null - ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) - : [], + savedObjectId: '', + version: '', + favorite: [], + code: 403, + message: err.message, }; - } catch (err) { - if (getOr(null, 'output.statusCode', err) === 403) { - return { - savedObjectId: '', - version: '', - favorite: [], - code: 403, - message: err.message, - }; - } - throw err; } + throw err; } +}; - public async persistTimeline( - request: FrameworkRequest, - timelineId: string | null, - version: string | null, - timeline: SavedTimeline - ): Promise<ResponseTimeline> { - const savedObjectsClient = request.context.core.savedObjects.client; - try { - if (timelineId == null) { - // Create new timeline - const newTimeline = convertSavedObjectToSavedTimeline( - await savedObjectsClient.create( - timelineSavedObjectType, - pickSavedTimeline(timelineId, timeline, request.user) - ) - ); - return { - code: 200, - message: 'success', - timeline: newTimeline, - }; - } - // Update Timeline - await savedObjectsClient.update( - timelineSavedObjectType, - timelineId, - pickSavedTimeline(timelineId, timeline, request.user), - { - version: version || undefined, - } +export const persistTimeline = async ( + request: FrameworkRequest, + timelineId: string | null, + version: string | null, + timeline: SavedTimeline +): Promise<ResponseTimeline> => { + const savedObjectsClient = request.context.core.savedObjects.client; + try { + if (timelineId == null) { + // Create new timeline + const newTimeline = convertSavedObjectToSavedTimeline( + await savedObjectsClient.create( + timelineSavedObjectType, + pickSavedTimeline(timelineId, timeline, request.user) + ) ); - return { code: 200, message: 'success', - timeline: await this.getSavedTimeline(request, timelineId), + timeline: newTimeline, }; - } catch (err) { - if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { - return { - code: 409, - message: err.message, - timeline: await this.getSavedTimeline(request, timelineId), - }; - } else if (getOr(null, 'output.statusCode', err) === 403) { - const timelineToReturn: TimelineResult = { - ...timeline, - savedObjectId: '', - version: '', - }; - return { - code: 403, - message: err.message, - timeline: timelineToReturn, - }; + } + // Update Timeline + await savedObjectsClient.update( + timelineSavedObjectType, + timelineId, + pickSavedTimeline(timelineId, timeline, request.user), + { + version: version || undefined, } - throw err; + ); + + return { + code: 200, + message: 'success', + timeline: await getSavedTimeline(request, timelineId), + }; + } catch (err) { + if (timelineId != null && savedObjectsClient.errors.isConflictError(err)) { + return { + code: 409, + message: err.message, + timeline: await getSavedTimeline(request, timelineId), + }; + } else if (getOr(null, 'output.statusCode', err) === 403) { + const timelineToReturn: TimelineResult = { + ...timeline, + savedObjectId: '', + version: '', + }; + return { + code: 403, + message: err.message, + timeline: timelineToReturn, + }; } + throw err; } +}; - public async deleteTimeline(request: FrameworkRequest, timelineIds: string[]) { - const savedObjectsClient = request.context.core.savedObjects.client; - - await Promise.all( - timelineIds.map(timelineId => - Promise.all([ - savedObjectsClient.delete(timelineSavedObjectType, timelineId), - this.note.deleteNoteByTimelineId(request, timelineId), - this.pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), - ]) - ) - ); - } +export const deleteTimeline = async (request: FrameworkRequest, timelineIds: string[]) => { + const savedObjectsClient = request.context.core.savedObjects.client; - private async getBasicSavedTimeline(request: FrameworkRequest, timelineId: string) { - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + await Promise.all( + timelineIds.map(timelineId => + Promise.all([ + savedObjectsClient.delete(timelineSavedObjectType, timelineId), + note.deleteNoteByTimelineId(request, timelineId), + pinnedEvent.deleteAllPinnedEventsOnTimeline(request, timelineId), + ]) + ) + ); +}; - return convertSavedObjectToSavedTimeline(savedObject); - } +const getBasicSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + + return convertSavedObjectToSavedTimeline(savedObject); +}; - private async getSavedTimeline(request: FrameworkRequest, timelineId: string) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; +const getSavedTimeline = async (request: FrameworkRequest, timelineId: string) => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const savedObjectsClient = request.context.core.savedObjects.client; - const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - const timelineWithNotesAndPinnedEvents = await Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), - Promise.resolve(timelineSaveObject), - ]); + const savedObjectsClient = request.context.core.savedObjects.client; + const savedObject = await savedObjectsClient.get(timelineSavedObjectType, timelineId); + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + const timelineWithNotesAndPinnedEvents = await Promise.all([ + note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), + Promise.resolve(timelineSaveObject), + ]); - const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; + const [notes, pinnedEvents, timeline] = timelineWithNotesAndPinnedEvents; - return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); + return timelineWithReduxProperties(notes, pinnedEvents, timeline, userName); +}; + +const getAllSavedTimeline = async (request: FrameworkRequest, options: SavedObjectsFindOptions) => { + const userName = request.user?.username ?? UNAUTHENTICATED_USER; + const savedObjectsClient = request.context.core.savedObjects.client; + if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { + options.search = `${options.search != null ? options.search : ''} ${ + userName != null ? convertStringToBase64(userName) : null + }`; } - private async getAllSavedTimeline(request: FrameworkRequest, options: SavedObjectsFindOptions) { - const userName = request.user?.username ?? UNAUTHENTICATED_USER; - const savedObjectsClient = request.context.core.savedObjects.client; - if (options.searchFields != null && options.searchFields.includes('favorite.keySearch')) { - options.search = `${options.search != null ? options.search : ''} ${ - userName != null ? convertStringToBase64(userName) : null - }`; - } + const savedObjects = await savedObjectsClient.find(options); - const savedObjects = await savedObjectsClient.find(options); - - const timelinesWithNotesAndPinnedEvents = await Promise.all( - savedObjects.saved_objects.map(async savedObject => { - const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); - return Promise.all([ - this.note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), - this.pinnedEvent.getAllPinnedEventsByTimelineId( - request, - timelineSaveObject.savedObjectId - ), - Promise.resolve(timelineSaveObject), - ]); - }) - ); + const timelinesWithNotesAndPinnedEvents = await Promise.all( + savedObjects.saved_objects.map(async savedObject => { + const timelineSaveObject = convertSavedObjectToSavedTimeline(savedObject); + return Promise.all([ + note.getNotesByTimelineId(request, timelineSaveObject.savedObjectId), + pinnedEvent.getAllPinnedEventsByTimelineId(request, timelineSaveObject.savedObjectId), + Promise.resolve(timelineSaveObject), + ]); + }) + ); - return { - totalCount: savedObjects.total, - timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => - timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) - ), - }; - } -} + return { + totalCount: savedObjects.total, + timeline: timelinesWithNotesAndPinnedEvents.map(([notes, pinnedEvents, timeline]) => + timelineWithReduxProperties(notes, pinnedEvents, timeline, userName) + ), + }; +}; export const convertStringToBase64 = (text: string): string => Buffer.from(text).toString('base64'); @@ -283,11 +331,9 @@ export const timelineWithReduxProperties = ( timeline.favorite != null && userName != null ? timeline.favorite.filter(fav => fav.userName === userName) : [], - eventIdToNoteIds: notes.filter(note => note.eventId != null), - noteIds: notes - .filter(note => note.eventId == null && note.noteId != null) - .map(note => note.noteId), + eventIdToNoteIds: notes.filter(n => n.eventId != null), + noteIds: notes.filter(n => n.eventId == null && n.noteId != null).map(n => n.noteId), notes, - pinnedEventIds: pinnedEvents.map(pinnedEvent => pinnedEvent.eventId), + pinnedEventIds: pinnedEvents.map(e => e.eventId), pinnedEventsSaveObject: pinnedEvents, }); diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts index 8fc12fd56a8f6..1cab24d0879ff 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -4,272 +4,283 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedTimeline } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const timelineSavedObjectType = 'siem-ui-timeline'; -export const timelineSavedObjectMappings: { - [timelineSavedObjectType]: ElasticsearchMappingOf<SavedTimeline>; -} = { - [timelineSavedObjectType]: { - properties: { - columns: { - properties: { - aggregatable: { - type: 'boolean', - }, - category: { - type: 'keyword', - }, - columnHeaderType: { - type: 'keyword', - }, - description: { - type: 'text', - }, - example: { - type: 'text', - }, - indexes: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - placeholder: { - type: 'text', - }, - searchable: { - type: 'boolean', - }, - type: { - type: 'keyword', - }, +export const timelineSavedObjectMappings = { + properties: { + columns: { + properties: { + aggregatable: { + type: 'boolean', + }, + category: { + type: 'keyword', + }, + columnHeaderType: { + type: 'keyword', + }, + description: { + type: 'text', + }, + example: { + type: 'text', + }, + indexes: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + placeholder: { + type: 'text', + }, + searchable: { + type: 'boolean', + }, + type: { + type: 'keyword', }, }, - dataProviders: { - properties: { - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - enabled: { - type: 'boolean', - }, - excluded: { - type: 'boolean', - }, - kqlQuery: { - type: 'text', - }, - queryMatch: { - properties: { - field: { - type: 'text', - }, - displayField: { - type: 'text', - }, - value: { - type: 'text', - }, - displayValue: { - type: 'text', - }, - operator: { - type: 'text', - }, + }, + dataProviders: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + enabled: { + type: 'boolean', + }, + excluded: { + type: 'boolean', + }, + kqlQuery: { + type: 'text', + }, + queryMatch: { + properties: { + field: { + type: 'text', + }, + displayField: { + type: 'text', + }, + value: { + type: 'text', + }, + displayValue: { + type: 'text', + }, + operator: { + type: 'text', }, }, - and: { - properties: { - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - enabled: { - type: 'boolean', - }, - excluded: { - type: 'boolean', - }, - kqlQuery: { - type: 'text', - }, - queryMatch: { - properties: { - field: { - type: 'text', - }, - displayField: { - type: 'text', - }, - value: { - type: 'text', - }, - displayValue: { - type: 'text', - }, - operator: { - type: 'text', - }, + }, + and: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + enabled: { + type: 'boolean', + }, + excluded: { + type: 'boolean', + }, + kqlQuery: { + type: 'text', + }, + queryMatch: { + properties: { + field: { + type: 'text', + }, + displayField: { + type: 'text', + }, + value: { + type: 'text', + }, + displayValue: { + type: 'text', + }, + operator: { + type: 'text', }, }, }, }, }, }, - description: { - type: 'text', - }, - eventType: { - type: 'keyword', - }, - favorite: { - properties: { - keySearch: { - type: 'text', - }, - fullName: { - type: 'text', - }, - userName: { - type: 'text', - }, - favoriteDate: { - type: 'date', - }, + }, + description: { + type: 'text', + }, + eventType: { + type: 'keyword', + }, + favorite: { + properties: { + keySearch: { + type: 'text', + }, + fullName: { + type: 'text', + }, + userName: { + type: 'text', + }, + favoriteDate: { + type: 'date', }, }, - filters: { - properties: { - meta: { - properties: { - alias: { - type: 'text', - }, - controlledBy: { - type: 'text', - }, - disabled: { - type: 'boolean', - }, - field: { - type: 'text', - }, - formattedValue: { - type: 'text', - }, - index: { - type: 'keyword', - }, - key: { - type: 'keyword', - }, - negate: { - type: 'boolean', - }, - params: { - type: 'text', - }, - type: { - type: 'keyword', - }, - value: { - type: 'text', - }, + }, + filters: { + properties: { + meta: { + properties: { + alias: { + type: 'text', + }, + controlledBy: { + type: 'text', + }, + disabled: { + type: 'boolean', + }, + field: { + type: 'text', + }, + formattedValue: { + type: 'text', + }, + index: { + type: 'keyword', + }, + key: { + type: 'keyword', + }, + negate: { + type: 'boolean', + }, + params: { + type: 'text', + }, + type: { + type: 'keyword', + }, + value: { + type: 'text', }, - }, - exists: { - type: 'text', - }, - match_all: { - type: 'text', - }, - missing: { - type: 'text', - }, - query: { - type: 'text', - }, - range: { - type: 'text', - }, - script: { - type: 'text', }, }, + exists: { + type: 'text', + }, + match_all: { + type: 'text', + }, + missing: { + type: 'text', + }, + query: { + type: 'text', + }, + range: { + type: 'text', + }, + script: { + type: 'text', + }, }, - kqlMode: { - type: 'keyword', - }, - kqlQuery: { - properties: { - filterQuery: { - properties: { - kuery: { - properties: { - kind: { - type: 'keyword', - }, - expression: { - type: 'text', - }, + }, + kqlMode: { + type: 'keyword', + }, + kqlQuery: { + properties: { + filterQuery: { + properties: { + kuery: { + properties: { + kind: { + type: 'keyword', + }, + expression: { + type: 'text', }, - }, - serializedQuery: { - type: 'text', }, }, + serializedQuery: { + type: 'text', + }, }, }, }, - title: { - type: 'text', - }, - dateRange: { - properties: { - start: { - type: 'date', - }, - end: { - type: 'date', - }, + }, + title: { + type: 'text', + }, + templateTimelineId: { + type: 'text', + }, + templateTimelineVersion: { + type: 'integer', + }, + timelineType: { + type: 'keyword', + }, + dateRange: { + properties: { + start: { + type: 'date', }, - }, - savedQueryId: { - type: 'keyword', - }, - sort: { - properties: { - columnId: { - type: 'keyword', - }, - sortDirection: { - type: 'keyword', - }, + end: { + type: 'date', }, }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', + }, + savedQueryId: { + type: 'keyword', + }, + sort: { + properties: { + columnId: { + type: 'keyword', + }, + sortDirection: { + type: 'keyword', + }, }, }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', + }, }, }; + +export const type: SavedObjectsType = { + name: timelineSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: timelineSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/types.ts b/x-pack/plugins/siem/server/lib/timeline/types.ts deleted file mode 100644 index 0bce3300591c2..0000000000000 --- a/x-pack/plugins/siem/server/lib/timeline/types.ts +++ /dev/null @@ -1,245 +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. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as runtimeTypes from 'io-ts'; - -import { unionWithNullType } from '../framework'; -import { NoteSavedObjectToReturnRuntimeType, NoteSavedObject } from '../note/types'; -import { - PinnedEventToReturnSavedObjectRuntimeType, - PinnedEventSavedObject, -} from '../pinned_event/types'; -import { SavedObjectsClient } from '../../../../../../src/core/server'; - -/* - * ColumnHeader Types - */ -const SavedColumnHeaderRuntimeType = runtimeTypes.partial({ - aggregatable: unionWithNullType(runtimeTypes.boolean), - category: unionWithNullType(runtimeTypes.string), - columnHeaderType: unionWithNullType(runtimeTypes.string), - description: unionWithNullType(runtimeTypes.string), - example: unionWithNullType(runtimeTypes.string), - indexes: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - placeholder: unionWithNullType(runtimeTypes.string), - searchable: unionWithNullType(runtimeTypes.boolean), - type: unionWithNullType(runtimeTypes.string), -}); - -/* - * DataProvider Types - */ -const SavedDataProviderQueryMatchBasicRuntimeType = runtimeTypes.partial({ - field: unionWithNullType(runtimeTypes.string), - displayField: unionWithNullType(runtimeTypes.string), - value: unionWithNullType(runtimeTypes.string), - displayValue: unionWithNullType(runtimeTypes.string), - operator: unionWithNullType(runtimeTypes.string), -}); - -const SavedDataProviderQueryMatchRuntimeType = runtimeTypes.partial({ - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - enabled: unionWithNullType(runtimeTypes.boolean), - excluded: unionWithNullType(runtimeTypes.boolean), - kqlQuery: unionWithNullType(runtimeTypes.string), - queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), -}); - -const SavedDataProviderRuntimeType = runtimeTypes.partial({ - id: unionWithNullType(runtimeTypes.string), - name: unionWithNullType(runtimeTypes.string), - enabled: unionWithNullType(runtimeTypes.boolean), - excluded: unionWithNullType(runtimeTypes.boolean), - kqlQuery: unionWithNullType(runtimeTypes.string), - queryMatch: unionWithNullType(SavedDataProviderQueryMatchBasicRuntimeType), - and: unionWithNullType(runtimeTypes.array(SavedDataProviderQueryMatchRuntimeType)), -}); - -/* - * Filters Types - */ -const SavedFilterMetaRuntimeType = runtimeTypes.partial({ - alias: unionWithNullType(runtimeTypes.string), - controlledBy: unionWithNullType(runtimeTypes.string), - disabled: unionWithNullType(runtimeTypes.boolean), - field: unionWithNullType(runtimeTypes.string), - formattedValue: unionWithNullType(runtimeTypes.string), - index: unionWithNullType(runtimeTypes.string), - key: unionWithNullType(runtimeTypes.string), - negate: unionWithNullType(runtimeTypes.boolean), - params: unionWithNullType(runtimeTypes.string), - type: unionWithNullType(runtimeTypes.string), - value: unionWithNullType(runtimeTypes.string), -}); - -const SavedFilterRuntimeType = runtimeTypes.partial({ - exists: unionWithNullType(runtimeTypes.string), - meta: unionWithNullType(SavedFilterMetaRuntimeType), - match_all: unionWithNullType(runtimeTypes.string), - missing: unionWithNullType(runtimeTypes.string), - query: unionWithNullType(runtimeTypes.string), - range: unionWithNullType(runtimeTypes.string), - script: unionWithNullType(runtimeTypes.string), -}); - -/* - * kqlQuery -> filterQuery Types - */ -const SavedKueryFilterQueryRuntimeType = runtimeTypes.partial({ - kind: unionWithNullType(runtimeTypes.string), - expression: unionWithNullType(runtimeTypes.string), -}); - -const SavedSerializedFilterQueryQueryRuntimeType = runtimeTypes.partial({ - kuery: unionWithNullType(SavedKueryFilterQueryRuntimeType), - serializedQuery: unionWithNullType(runtimeTypes.string), -}); - -const SavedFilterQueryQueryRuntimeType = runtimeTypes.partial({ - filterQuery: unionWithNullType(SavedSerializedFilterQueryQueryRuntimeType), -}); - -/* - * DatePicker Range Types - */ -const SavedDateRangePickerRuntimeType = runtimeTypes.partial({ - start: unionWithNullType(runtimeTypes.number), - end: unionWithNullType(runtimeTypes.number), -}); - -/* - * Favorite Types - */ -const SavedFavoriteRuntimeType = runtimeTypes.partial({ - keySearch: unionWithNullType(runtimeTypes.string), - favoriteDate: unionWithNullType(runtimeTypes.number), - fullName: unionWithNullType(runtimeTypes.string), - userName: unionWithNullType(runtimeTypes.string), -}); - -/* - * Sort Types - */ -const SavedSortRuntimeType = runtimeTypes.partial({ - columnId: unionWithNullType(runtimeTypes.string), - sortDirection: unionWithNullType(runtimeTypes.string), -}); - -/* - * Timeline Types - */ -export const SavedTimelineRuntimeType = runtimeTypes.partial({ - columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), - dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), - description: unionWithNullType(runtimeTypes.string), - eventType: unionWithNullType(runtimeTypes.string), - favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), - filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), - kqlMode: unionWithNullType(runtimeTypes.string), - kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), - title: unionWithNullType(runtimeTypes.string), - dateRange: unionWithNullType(SavedDateRangePickerRuntimeType), - savedQueryId: unionWithNullType(runtimeTypes.string), - sort: unionWithNullType(SavedSortRuntimeType), - created: unionWithNullType(runtimeTypes.number), - createdBy: unionWithNullType(runtimeTypes.string), - updated: unionWithNullType(runtimeTypes.number), - updatedBy: unionWithNullType(runtimeTypes.string), -}); - -export interface SavedTimeline extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} - -export interface SavedTimelineNote extends runtimeTypes.TypeOf<typeof SavedTimelineRuntimeType> {} - -/** - * Timeline Saved object type with metadata - */ - -export const TimelineSavedObjectRuntimeType = runtimeTypes.intersection([ - runtimeTypes.type({ - id: runtimeTypes.string, - attributes: SavedTimelineRuntimeType, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - savedObjectId: runtimeTypes.string, - }), -]); - -export const TimelineSavedToReturnObjectRuntimeType = runtimeTypes.intersection([ - SavedTimelineRuntimeType, - runtimeTypes.type({ - savedObjectId: runtimeTypes.string, - version: runtimeTypes.string, - }), - runtimeTypes.partial({ - eventIdToNoteIds: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), - noteIds: runtimeTypes.array(runtimeTypes.string), - notes: runtimeTypes.array(NoteSavedObjectToReturnRuntimeType), - pinnedEventIds: runtimeTypes.array(runtimeTypes.string), - pinnedEventsSaveObject: runtimeTypes.array(PinnedEventToReturnSavedObjectRuntimeType), - }), -]); - -export interface TimelineSavedObject - extends runtimeTypes.TypeOf<typeof TimelineSavedToReturnObjectRuntimeType> {} - -/** - * All Timeline Saved object type with metadata - */ - -export const AllTimelineSavedObjectRuntimeType = runtimeTypes.type({ - total: runtimeTypes.number, - data: TimelineSavedToReturnObjectRuntimeType, -}); - -export interface AllTimelineSavedObject - extends runtimeTypes.TypeOf<typeof AllTimelineSavedObjectRuntimeType> {} - -/** - * Import/export timelines - */ - -export type ExportTimelineSavedObjectsClient = Pick< - SavedObjectsClient, - | 'get' - | 'errors' - | 'create' - | 'bulkCreate' - | 'delete' - | 'find' - | 'bulkGet' - | 'update' - | 'bulkUpdate' ->; - -export type ExportedGlobalNotes = Array<Exclude<NoteSavedObject, 'eventId'>>; -export type ExportedEventNotes = NoteSavedObject[]; - -export interface ExportedNotes { - eventNotes: ExportedEventNotes; - globalNotes: ExportedGlobalNotes; -} - -export type ExportedTimelines = TimelineSavedObject & - ExportedNotes & { - pinnedEventIds: string[]; - }; - -export interface BulkGetInput { - type: string; - id: string; -} - -export type NotesAndPinnedEventsByTimelineId = Record< - string, - { notes: NoteSavedObject[]; pinnedEvents: PinnedEventSavedObject[] } ->; diff --git a/x-pack/plugins/siem/server/lib/types.ts b/x-pack/plugins/siem/server/lib/types.ts index a74fe8f778ba9..2a897806dc628 100644 --- a/x-pack/plugins/siem/server/lib/types.ts +++ b/x-pack/plugins/siem/server/lib/types.ts @@ -6,7 +6,7 @@ import { AuthenticatedUser } from '../../../security/public'; import { RequestHandlerContext } from '../../../../../src/core/server'; -export { ConfigType as Configuration } from '../'; +export { ConfigType as Configuration } from '../config'; import { Authentications } from './authentications'; import { Events } from './events'; diff --git a/x-pack/plugins/siem/server/mocks.ts b/x-pack/plugins/siem/server/mocks.ts new file mode 100644 index 0000000000000..44c41be86b6ff --- /dev/null +++ b/x-pack/plugins/siem/server/mocks.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. + */ + +import { SiemClient } from './types'; + +type SiemClientMock = jest.Mocked<SiemClient>; +const createSiemClientMock = (): SiemClientMock => + (({ + getSignalsIndex: jest.fn(), + } as unknown) as SiemClientMock); + +export const siemMock = { + createClient: createSiemClientMock, +}; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index b9ec1c2e92438..3ef4b39bd0979 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -11,19 +11,16 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, + Plugin as IPlugin, PluginInitializerContext, Logger, } from '../../../../src/core/server'; -import { - PluginStartContract as AlertingStart, - PluginSetupContract as AlertingSetup, -} from '../../alerting/server'; +import { PluginSetupContract as AlertingSetup } from '../../alerting/server'; import { SecurityPluginSetup as SecuritySetup } from '../../security/server'; import { PluginSetupContract as FeaturesSetup } from '../../features/server'; import { MlPluginSetup as MlSetup } from '../../ml/server'; import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; -import { PluginStartContract as ActionsStart } from '../../actions/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -33,17 +30,11 @@ import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type'; import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types'; import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; -import { - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, -} from './saved_objects'; +import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { SiemClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; - -export { CoreSetup, CoreStart }; +import { initUiSettings } from './ui_settings'; +import { APP_ID, APP_ICON } from '../common/constants'; export interface SetupPlugins { alerting: AlertingSetup; @@ -55,13 +46,15 @@ export interface SetupPlugins { ml?: MlSetup; } -export interface StartPlugins { - actions: ActionsStart; - alerting: AlertingStart; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StartPlugins {} -export class Plugin { - readonly name = 'siem'; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginStart {} + +export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { private readonly logger: Logger; private readonly config$: Observable<ConfigType>; private context: PluginInitializerContext; @@ -69,14 +62,14 @@ export class Plugin { constructor(context: PluginInitializerContext) { this.context = context; - this.logger = context.logger.get('plugins', this.name); + this.logger = context.logger.get('plugins', APP_ID); this.config$ = createConfig$(context); this.siemClientFactory = new SiemClientFactory(); this.logger.debug('plugin initialized'); } - public async setup(core: CoreSetup, plugins: SetupPlugins) { + public async setup(core: CoreSetup<StartPlugins, PluginStart>, plugins: SetupPlugins) { this.logger.debug('plugin setup'); if (hasListsFeature()) { @@ -86,8 +79,11 @@ export class Plugin { ); } + initSavedObjects(core.savedObjects); + initUiSettings(core.uiSettings); + const router = core.http.createRouter(); - core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({ + core.http.registerRouteHandlerContext(APP_ID, (context, request, response) => ({ getSiemClient: () => this.siemClientFactory.create(request), })); @@ -106,12 +102,12 @@ export class Plugin { ); plugins.features.registerFeature({ - id: this.name, + id: APP_ID, name: i18n.translate('xpack.siem.featureRegistry.linkSiemTitle', { defaultMessage: 'SIEM', }), order: 1100, - icon: 'securityAnalyticsApp', + icon: APP_ICON, navLinkId: 'siem', app: ['siem', 'kibana'], catalogue: ['siem'], @@ -125,15 +121,11 @@ export class Plugin { 'alert', 'action', 'action_task_params', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, 'cases', 'cases-comments', 'cases-configure', 'cases-user-actions', + ...savedObjectTypes, ], read: ['config'], }, @@ -156,15 +148,11 @@ export class Plugin { all: ['alert', 'action', 'action_task_params'], read: [ 'config', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, 'cases', 'cases-comments', 'cases-configure', 'cases-user-actions', + ...savedObjectTypes, ], }, ui: [ @@ -201,7 +189,11 @@ export class Plugin { const libs = compose(core, plugins, this.context.env.mode.prod); initServer(libs); + + return {}; } - public start(core: CoreStart, plugins: StartPlugins) {} + public start(core: CoreStart, plugins: StartPlugins) { + return {}; + } } diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts index 64b232a2686b8..ffad86a09cee7 100644 --- a/x-pack/plugins/siem/server/routes/index.ts +++ b/x-pack/plugins/siem/server/routes/index.ts @@ -30,8 +30,10 @@ import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/fin import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route'; import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route'; +import { createTimelinesRoute } from '../lib/timeline/routes/create_timelines_route'; +import { updateTimelinesRoute } from '../lib/timeline/routes/update_timelines_route'; import { SetupPlugins } from '../plugin'; -import { ConfigType } from '..'; +import { ConfigType } from '../config'; export const initRoutes = ( router: IRouter, @@ -55,6 +57,8 @@ export const initRoutes = ( patchRulesBulkRoute(router); deleteRulesBulkRoute(router); + createTimelinesRoute(router, config, security); + updateTimelinesRoute(router, config, security); importRulesRoute(router, config); exportRulesRoute(router, config); diff --git a/x-pack/plugins/siem/server/saved_objects.ts b/x-pack/plugins/siem/server/saved_objects.ts index 7b097eefedb46..66a470099d649 100644 --- a/x-pack/plugins/siem/server/saved_objects.ts +++ b/x-pack/plugins/siem/server/saved_objects.ts @@ -4,35 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noteSavedObjectType, noteSavedObjectMappings } from './lib/note/saved_object_mappings'; -import { - pinnedEventSavedObjectType, - pinnedEventSavedObjectMappings, -} from './lib/pinned_event/saved_object_mappings'; -import { - timelineSavedObjectType, - timelineSavedObjectMappings, -} from './lib/timeline/saved_object_mappings'; -import { - ruleStatusSavedObjectMappings, - ruleStatusSavedObjectType, -} from './lib/detection_engine/rules/saved_object_mappings'; -import { - ruleActionsSavedObjectMappings, - ruleActionsSavedObjectType, -} from './lib/detection_engine/rule_actions/saved_object_mappings'; +import { CoreSetup } from '../../../../src/core/server'; -export { - noteSavedObjectType, - pinnedEventSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, - timelineSavedObjectType, -}; -export const savedObjectMappings = { - ...timelineSavedObjectMappings, - ...noteSavedObjectMappings, - ...pinnedEventSavedObjectMappings, - ...ruleStatusSavedObjectMappings, - ...ruleActionsSavedObjectMappings, +import { type as noteType } from './lib/note/saved_object_mappings'; +import { type as pinnedEventType } from './lib/pinned_event/saved_object_mappings'; +import { type as timelineType } from './lib/timeline/saved_object_mappings'; +import { type as ruleStatusType } from './lib/detection_engine/rules/saved_object_mappings'; +import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings'; + +const types = [noteType, pinnedEventType, ruleActionsType, ruleStatusType, timelineType]; + +export const savedObjectTypes = types.map(type => type.name); + +export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => { + types.forEach(type => savedObjects.registerType(type)); }; diff --git a/x-pack/plugins/siem/server/ui_settings.ts b/x-pack/plugins/siem/server/ui_settings.ts new file mode 100644 index 0000000000000..26b7fd72571af --- /dev/null +++ b/x-pack/plugins/siem/server/ui_settings.ts @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { CoreSetup } from '../../../../src/core/server'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_INDEX_PATTERN, + DEFAULT_ANOMALY_SCORE, + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_FROM, + DEFAULT_TO, + ENABLE_NEWS_FEED_SETTING, + NEWS_FEED_URL_SETTING, + NEWS_FEED_URL_SETTING_DEFAULT, + IP_REPUTATION_LINKS_SETTING, + IP_REPUTATION_LINKS_SETTING_DEFAULT, +} from '../common/constants'; + +export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { + uiSettings.register({ + [DEFAULT_SIEM_REFRESH_INTERVAL]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', { + defaultMessage: 'Time filter refresh interval', + }), + value: `{ + "pause": ${DEFAULT_INTERVAL_PAUSE}, + "value": ${DEFAULT_INTERVAL_VALUE} +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', { + defaultMessage: + '<p>Default refresh interval for the SIEM time filter, in milliseconds.</p>', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.object({ + value: schema.number(), + pause: schema.boolean(), + }), + }, + [DEFAULT_SIEM_TIME_RANGE]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', { + defaultMessage: 'Time filter period', + }), + value: `{ + "from": "${DEFAULT_FROM}", + "to": "${DEFAULT_TO}" +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', { + defaultMessage: '<p>Default period of time in the SIEM time filter.</p>', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + }, + [DEFAULT_INDEX_KEY]: { + name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { + defaultMessage: 'Elasticsearch indices', + }), + value: DEFAULT_INDEX_PATTERN, + description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', { + defaultMessage: + '<p>Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.</p>', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.arrayOf(schema.string()), + }, + [DEFAULT_ANOMALY_SCORE]: { + name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', { + defaultMessage: 'Anomaly threshold', + }), + value: 50, + type: 'number', + description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', { + defaultMessage: + '<p>Value above which Machine Learning job anomalies are displayed in the SIEM app.</p><p>Valid values: 0 to 100.</p>', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.number(), + }, + [ENABLE_NEWS_FEED_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', { + defaultMessage: 'News feed', + }), + value: true, + description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', { + defaultMessage: '<p>Enables the News feed</p>', + }), + type: 'boolean', + category: ['siem'], + requiresPageReload: true, + schema: schema.boolean(), + }, + [NEWS_FEED_URL_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', { + defaultMessage: 'News feed URL', + }), + value: NEWS_FEED_URL_SETTING_DEFAULT, + description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', { + defaultMessage: '<p>News feed content will be retrieved from this URL</p>', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.string(), + }, + [IP_REPUTATION_LINKS_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', { + defaultMessage: 'IP Reputation Links', + }), + value: IP_REPUTATION_LINKS_SETTING_DEFAULT, + type: 'json', + description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', { + defaultMessage: + 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.arrayOf( + schema.object({ + name: schema.string(), + url_template: schema.string(), + }) + ), + }, + }); +}; diff --git a/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts b/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts index 5b667f461fc60..78aadf75e54c3 100644 --- a/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts +++ b/x-pack/plugins/siem/server/utils/build_query/calculate_timeseries_interval.ts @@ -5,7 +5,7 @@ */ /* ** Applying the same logic as: - ** x-pack/legacy/plugins/apm/server/lib/helpers/get_bucket_size/calculate_auto.js + ** x-pack/plugins/apm/server/lib/helpers/get_bucket_size/calculate_auto.js */ import moment from 'moment'; import { get } from 'lodash/fp'; diff --git a/x-pack/plugins/siem/server/utils/build_validation/__mocks__/utils.ts b/x-pack/plugins/siem/server/utils/build_validation/__mocks__/utils.ts new file mode 100644 index 0000000000000..578972dda5aef --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/__mocks__/utils.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { formatErrors } from '../format_errors'; + +interface Message<T> { + errors: t.Errors; + schema: T | {}; +} + +const onLeft = <T>(errors: t.Errors): Message<T> => { + return { schema: {}, errors }; +}; + +const onRight = <T>(schema: T): Message<T> => { + return { + schema, + errors: [], + }; +}; + +export const foldLeftRight = fold(onLeft, onRight); + +/** + * Convenience utility to keep the error message handling within tests to be + * very concise. + * @param validation The validation to get the errors from + */ +export const getPaths = <A>(validation: t.Validation<A>): string[] => { + return pipe( + validation, + fold( + errors => formatErrors(errors), + () => ['no errors'] + ) + ); +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts b/x-pack/plugins/siem/server/utils/build_validation/exact_check.test.ts similarity index 94% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts rename to x-pack/plugins/siem/server/utils/build_validation/exact_check.test.ts index cae4365d06856..1e70deaeed438 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/exact_check.test.ts @@ -5,22 +5,13 @@ */ import * as t from 'io-ts'; -import { left, right } from 'fp-ts/lib/Either'; +import { left, right, Either } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { exactCheck, findDifferencesRecursive } from './exact_check'; -import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('exact_check', () => { - beforeAll(() => { - setFeatureFlagsForTestsOnly(); - }); - - afterAll(() => { - unSetFeatureFlagsForTestsOnly(); - }); - test('it returns an error if given extra object properties', () => { const someType = t.exact( t.type({ @@ -36,14 +27,22 @@ describe('exact_check', () => { }); test('it returns an error if the data type is not as expected', () => { + type UnsafeCastForTest = Either< + t.Errors, + { + a: number; + } + >; + const someType = t.exact( t.type({ a: t.string, }) ); + const payload = { a: 1 }; const decoded = someType.decode(payload); - const checked = exactCheck(payload, decoded); + const checked = exactCheck(payload, decoded as UnsafeCastForTest); const message = pipe(checked, foldLeftRight); expect(getPaths(left(message.errors))).toEqual(['Invalid value "1" supplied to "a"']); expect(message.schema).toEqual({}); diff --git a/x-pack/plugins/siem/server/utils/build_validation/exact_check.ts b/x-pack/plugins/siem/server/utils/build_validation/exact_check.ts new file mode 100644 index 0000000000000..9484765f9973d --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/exact_check.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { left, Either, fold, right } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { isObject, get } from 'lodash/fp'; + +/** + * Given an original object and a decoded object this will return an error + * if and only if the original object has additional keys that the decoded + * object does not have. If the original decoded already has an error, then + * this will return the error as is and not continue. + * + * NOTE: You MUST use t.exact(...) for this to operate correctly as your schema + * needs to remove additional keys before the compare + * + * You might not need this in the future if the below issue is solved: + * https://github.com/gcanti/io-ts/issues/322 + * + * @param original The original to check if it has additional keys + * @param decoded The decoded either which has either an existing error or the + * decoded object which could have additional keys stripped from it. + */ +export const exactCheck = <T>(original: T, decoded: Either<t.Errors, T>): Either<t.Errors, T> => { + const onLeft = (errors: t.Errors): Either<t.Errors, T> => left(errors); + const onRight = (decodedValue: T): Either<t.Errors, T> => { + const differences = findDifferencesRecursive(original, decodedValue); + if (differences.length !== 0) { + const validationError: t.ValidationError = { + value: differences, + context: [], + message: `invalid keys "${differences.join(',')}"`, + }; + const error: t.Errors = [validationError]; + return left(error); + } else { + return right(decodedValue); + } + }; + return pipe(decoded, fold(onLeft, onRight)); +}; + +export const findDifferencesRecursive = <T>(original: T, decodedValue: T): string[] => { + if (decodedValue == null) { + try { + // It is null and painful when the original contains an object or an array + // the the decoded value does not have. + return [JSON.stringify(original)]; + } catch (err) { + return ['circular reference']; + } + } else if (typeof original !== 'object' || original == null) { + // We are not an object or null so do not report differences + return []; + } else { + const decodedKeys = Object.keys(decodedValue); + const differences = Object.keys(original).flatMap(originalKey => { + const foundKey = decodedKeys.some(key => key === originalKey); + const topLevelKey = foundKey ? [] : [originalKey]; + // I use lodash to cheat and get an any (not going to lie ;-)) + const valueObjectOrArrayOriginal = get(originalKey, original); + const valueObjectOrArrayDecoded = get(originalKey, decodedValue); + if (isObject(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...findDifferencesRecursive(valueObjectOrArrayOriginal, valueObjectOrArrayDecoded), + ]; + } else if (Array.isArray(valueObjectOrArrayOriginal)) { + return [ + ...topLevelKey, + ...valueObjectOrArrayOriginal.flatMap((arrayElement, index) => + findDifferencesRecursive(arrayElement, get(index, valueObjectOrArrayDecoded)) + ), + ]; + } else { + return topLevelKey; + } + }); + return differences; + } +}; diff --git a/x-pack/plugins/siem/server/utils/build_validation/format_errors.test.ts b/x-pack/plugins/siem/server/utils/build_validation/format_errors.test.ts new file mode 100644 index 0000000000000..f9dd9e76a1d9c --- /dev/null +++ b/x-pack/plugins/siem/server/utils/build_validation/format_errors.test.ts @@ -0,0 +1,130 @@ +/* + * 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 * as t from 'io-ts'; +import { formatErrors } from './format_errors'; + +describe('utils', () => { + test('returns an empty error message string if there are no errors', () => { + const errors: t.Errors = []; + const output = formatErrors(errors); + expect(output).toEqual([]); + }); + + test('returns a single error message if given one', () => { + const validationError: t.ValidationError = { + value: 'Some existing error', + context: [], + message: 'some error', + }; + const errors: t.Errors = [validationError]; + const output = formatErrors(errors); + expect(output).toEqual(['some error']); + }); + + test('returns a two error messages if given two', () => { + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context: [], + message: 'some error 1', + }; + const validationError2: t.ValidationError = { + value: 'Some existing error 2', + context: [], + message: 'some error 2', + }; + const errors: t.Errors = [validationError1, validationError2]; + const output = formatErrors(errors); + expect(output).toEqual(['some error 1', 'some error 2']); + }); + + test('will use message before context if it is set', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + message: 'I should be used first', + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['I should be used first']); + }); + + test('will use context entry of a single string', () => { + const context: t.Context = ([{ key: 'some string key' }] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual(['Invalid value "Some existing error 1" supplied to "some string key"']); + }); + + test('will use two context entries of two strings', () => { + const context: t.Context = ([ + { key: 'some string key 1' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 1,some string key 2"', + ]); + }); + + test('will filter out and not use any strings of numbers', () => { + const context: t.Context = ([ + { key: '5' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use null', () => { + const context: t.Context = ([ + { key: null }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); + + test('will filter out and not use empty strings', () => { + const context: t.Context = ([ + { key: '' }, + { key: 'some string key 2' }, + ] as unknown) as t.Context; + const validationError1: t.ValidationError = { + value: 'Some existing error 1', + context, + }; + const errors: t.Errors = [validationError1]; + const output = formatErrors(errors); + expect(output).toEqual([ + 'Invalid value "Some existing error 1" supplied to "some string key 2"', + ]); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts b/x-pack/plugins/siem/server/utils/build_validation/format_errors.ts similarity index 100% rename from x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.ts rename to x-pack/plugins/siem/server/utils/build_validation/format_errors.ts diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts index d17a8457ff81b..8a1e4271303a6 100644 --- a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts @@ -6,12 +6,36 @@ import { buildRouteValidation } from './route_validation'; import * as rt from 'io-ts'; -import { RouteValidationResultFactory } from '../../../../../../src/core/server/http'; +import { RouteValidationResultFactory } from 'src/core/server'; describe('buildRouteValidation', () => { - const schema = rt.type({ - ids: rt.array(rt.string), - }); + const schema = rt.exact( + rt.type({ + ids: rt.array(rt.string), + }) + ); + type Schema = rt.TypeOf<typeof schema>; + + /** + * If your schema is using exact all the way down then the validation will + * catch any additional keys that should not be present within the validation + * when the route_validation uses the exact check. + */ + const deepSchema = rt.exact( + rt.type({ + topLevel: rt.exact( + rt.type({ + secondLevel: rt.exact( + rt.type({ + thirdLevel: rt.string, + }) + ), + }) + ), + }) + ); + type DeepSchema = rt.TypeOf<typeof deepSchema>; + const validationResult: RouteValidationResultFactory = { ok: jest.fn().mockImplementation(validatedInput => validatedInput), badRequest: jest.fn().mockImplementation(e => e), @@ -22,18 +46,41 @@ describe('buildRouteValidation', () => { }); test('return validation error', () => { - const input = { id: 'someId' }; + const input: Omit<Schema, 'ids'> & { id: string } = { id: 'someId' }; const result = buildRouteValidation(schema)(input, validationResult); - expect(result).toEqual( - 'Invalid value undefined supplied to : { ids: Array<string> }/ids: Array<string>' - ); + expect(result).toEqual('Invalid value "undefined" supplied to "ids"'); }); test('return validated input', () => { - const input = { ids: ['someId'] }; + const input: Schema = { ids: ['someId'] }; const result = buildRouteValidation(schema)(input, validationResult); expect(result).toEqual(input); }); + + test('returns validation error if given extra keys on input for an array', () => { + const input: Schema & { somethingExtra: string } = { + ids: ['someId'], + somethingExtra: 'hello', + }; + const result = buildRouteValidation(schema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingExtra"'); + }); + + test('return validation input for a deep 3rd level object', () => { + const input: DeepSchema = { topLevel: { secondLevel: { thirdLevel: 'hello' } } }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual(input); + }); + + test('return validation error for a deep 3rd level object that has an extra key value of "somethingElse"', () => { + const input: DeepSchema & { + topLevel: { secondLevel: { thirdLevel: string; somethingElse: string } }; + } = { + topLevel: { secondLevel: { thirdLevel: 'hello', somethingElse: 'extraKey' } }, + }; + const result = buildRouteValidation(deepSchema)(input, validationResult); + expect(result).toEqual('invalid keys "somethingElse"'); + }); }); diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts index bfcd0998fe690..30b95dcfa94ee 100644 --- a/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.ts @@ -7,12 +7,13 @@ import { fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import * as rt from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; import { RouteValidationFunction, RouteValidationResultFactory, RouteValidationError, } from '../../../../../../src/core/server'; +import { exactCheck } from './exact_check'; +import { formatErrors } from './format_errors'; type RequestValidationResult<T> = | { @@ -32,8 +33,9 @@ export const buildRouteValidation = <T extends rt.Mixed, A = rt.TypeOf<T>>( ) => pipe( schema.decode(inputValue), + decoded => exactCheck(inputValue, decoded), fold<rt.Errors, A, RequestValidationResult<A>>( - (errors: rt.Errors) => validationResult.badRequest(failure(errors).join('\n')), + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), (validatedInput: A) => validationResult.ok(validatedInput) ) ); diff --git a/x-pack/plugins/siem/server/utils/typed_resolvers.ts b/x-pack/plugins/siem/server/utils/typed_resolvers.ts index da38e8a1e1bf2..4f19bd54b01f0 100644 --- a/x-pack/plugins/siem/server/utils/typed_resolvers.ts +++ b/x-pack/plugins/siem/server/utils/typed_resolvers.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as runtimeTypes from 'io-ts'; import { GraphQLResolveInfo } from 'graphql'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -106,6 +105,3 @@ export type ChildResolverOf<Resolver_, ParentResolver> = ResolverWithParent< Resolver_, ResultOf<ParentResolver> >; - -export const unionWithNullType = <T extends runtimeTypes.Mixed>(type: T) => - runtimeTypes.union([type, runtimeTypes.null]); diff --git a/x-pack/plugins/snapshot_restore/common/types/index.ts b/x-pack/plugins/snapshot_restore/common/types/index.ts index 5cb3839fa9e01..d52584ca737a2 100644 --- a/x-pack/plugins/snapshot_restore/common/types/index.ts +++ b/x-pack/plugins/snapshot_restore/common/types/index.ts @@ -8,4 +8,3 @@ export * from './repository'; export * from './snapshot'; export * from './restore'; export * from './policy'; -export * from './privileges'; diff --git a/x-pack/plugins/snapshot_restore/common/types/privileges.ts b/x-pack/plugins/snapshot_restore/common/types/privileges.ts deleted file mode 100644 index bf710b8225599..0000000000000 --- a/x-pack/plugins/snapshot_restore/common/types/privileges.ts +++ /dev/null @@ -1,14 +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. - */ - -export interface MissingPrivileges { - [key: string]: string[] | undefined; -} - -export interface Privileges { - hasAllPrivileges: boolean; - missingPrivileges: MissingPrivileges; -} diff --git a/x-pack/plugins/snapshot_restore/public/application/app.tsx b/x-pack/plugins/snapshot_restore/public/application/app.tsx index 77ef697814b2c..350d8aec711ed 100644 --- a/x-pack/plugins/snapshot_restore/public/application/app.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/app.tsx @@ -4,13 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common/constants'; -import { SectionLoading, SectionError } from './components'; +import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common'; +import { + useAuthorizationContext, + SectionError, + WithPrivileges, + NotAuthorizedSection, +} from '../shared_imports'; +import { SectionLoading } from './components'; import { BASE_PATH, DEFAULT_SECTION, Section } from './constants'; import { RepositoryAdd, @@ -21,11 +27,10 @@ import { PolicyEdit, } from './sections'; import { useConfig } from './app_context'; -import { AuthorizationContext, WithPrivileges, NotAuthorizedSection } from './lib/authorization'; export const App: React.FunctionComponent = () => { const { slm_ui: slmUi } = useConfig(); - const { apiError } = useContext(AuthorizationContext); + const { apiError } = useAuthorizationContext(); const sections: Section[] = ['repositories', 'snapshots', 'restore_status']; diff --git a/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx b/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx index e2732c0051337..3ca25b7d32dba 100644 --- a/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/app_providers.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { API_BASE_PATH } from '../../common/constants'; -import { AuthorizationProvider } from './lib/authorization'; +import { API_BASE_PATH } from '../../common'; +import { AuthorizationProvider } from '../shared_imports'; import { AppContextProvider, AppDependencies } from './app_context'; interface Props { @@ -18,10 +18,11 @@ export const AppProviders = ({ appDependencies, children }: Props) => { const { core } = appDependencies; const { i18n: { Context: I18nContext }, + http, } = core; return ( - <AuthorizationProvider privilegesEndpoint={`${API_BASE_PATH}privileges`}> + <AuthorizationProvider httpClient={http} privilegesEndpoint={`${API_BASE_PATH}privileges`}> <I18nContext> <AppContextProvider value={appDependencies}>{children}</AppContextProvider> </I18nContext> diff --git a/x-pack/plugins/snapshot_restore/public/application/components/index.ts b/x-pack/plugins/snapshot_restore/public/application/components/index.ts index a7038ebd71578..f5bb892389870 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/components/index.ts @@ -10,7 +10,6 @@ export { RepositoryDeleteProvider } from './repository_delete_provider'; export { RepositoryForm } from './repository_form'; export { RepositoryVerificationBadge } from './repository_verification_badge'; export { RepositoryTypeLogo } from './repository_type_logo'; -export { SectionError, Error } from './section_error'; export { SectionLoading } from './section_loading'; export { SnapshotDeleteProvider } from './snapshot_delete_provider'; export { RestoreSnapshotForm } from './restore_snapshot_form'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index f2d4e2bd74598..105f0601e3dfb 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -21,13 +21,13 @@ import { } from '@elastic/eui'; import { Repository } from '../../../../../common/types'; -import { CronEditor } from '../../../../shared_imports'; +import { CronEditor, SectionError } from '../../../../shared_imports'; import { useServices } from '../../../app_context'; import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants'; import { useLoadRepositories } from '../../../services/http'; import { linkToAddRepository } from '../../../services/navigation'; import { documentationLinksService } from '../../../services/documentation'; -import { SectionLoading, SectionError } from '../../'; +import { SectionLoading } from '../../'; import { StepProps } from './'; export const PolicyStepLogistics: React.FunctionComponent<StepProps> = ({ diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx index 3b4c9d595b9f2..34bc06343a780 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/step_one.tsx @@ -23,13 +23,14 @@ import { } from '@elastic/eui'; import { Repository, RepositoryType, EmptyRepository } from '../../../../common/types'; -import { REPOSITORY_TYPES } from '../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../common'; +import { SectionError, Error } from '../../../shared_imports'; import { documentationLinksService } from '../../services/documentation'; import { useLoadRepositoryTypes } from '../../services/http'; import { textService } from '../../services/text'; import { RepositoryValidation } from '../../services/validation'; -import { SectionError, SectionLoading, RepositoryTypeLogo, Error } from '../'; +import { SectionLoading, RepositoryTypeLogo } from '../'; interface Props { repository: Repository | EmptyRepository; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx index 75295a1205cef..1e54868397a6d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx @@ -6,11 +6,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { REPOSITORY_TYPES } from '../../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../../common'; import { Repository, RepositoryType, EmptyRepository } from '../../../../../common/types'; +import { SectionError } from '../../../../shared_imports'; import { useServices } from '../../../app_context'; import { RepositorySettingsValidation } from '../../../services/validation'; -import { SectionError } from '../../index'; import { AzureSettings } from './azure_settings'; import { FSSettings } from './fs_settings'; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/section_error.tsx b/x-pack/plugins/snapshot_restore/public/application/components/section_error.tsx deleted file mode 100644 index bd9e48796779e..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/components/section_error.tsx +++ /dev/null @@ -1,50 +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 { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { Fragment } from 'react'; - -export interface Error { - error: string; - cause?: string[]; - message?: string; -} - -interface Props { - title: React.ReactNode; - error: Error; - actions?: JSX.Element; -} - -export const SectionError: React.FunctionComponent<Props> = ({ - title, - error, - actions, - ...rest -}) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error; - - return ( - <EuiCallOut title={title} color="danger" iconType="alert" {...rest}> - {cause ? message || errorString : <p>{message || errorString}</p>} - {cause && ( - <Fragment> - <EuiSpacer size="s" /> - <ul> - {cause.map((causeMsg, i) => ( - <li key={i}>{causeMsg}</li> - ))} - </ul> - </Fragment> - )} - {actions ? actions : null} - </EuiCallOut> - ); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/authorization_provider.tsx deleted file mode 100644 index d32fe29cc1dfa..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/authorization_provider.tsx +++ /dev/null @@ -1,47 +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 React, { createContext } from 'react'; -import { useRequest } from '../../../services/http/use_request'; -import { Privileges } from '../../../../../common/types'; -import { Error } from '../../../components/section_error'; - -interface Authorization { - isLoading: boolean; - apiError: Error | null; - privileges: Privileges; -} - -const initialValue: Authorization = { - isLoading: true, - apiError: null, - privileges: { - hasAllPrivileges: true, - missingPrivileges: {}, - }, -}; - -export const AuthorizationContext = createContext<Authorization>(initialValue); - -interface Props { - privilegesEndpoint: string; - children: React.ReactNode; -} - -export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) => { - const { isLoading, error, data: privilegesData } = useRequest({ - path: privilegesEndpoint, - method: 'get', - }); - - const value = { - isLoading, - privileges: isLoading ? { hasAllPrivileges: true, missingPrivileges: {} } : privilegesData, - apiError: error ? error : null, - } as Authorization; - - return <AuthorizationContext.Provider value={value}>{children}</AuthorizationContext.Provider>; -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/index.ts b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/index.ts deleted file mode 100644 index ac77aa5268660..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/index.ts +++ /dev/null @@ -1,11 +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. - */ - -export { AuthorizationProvider, AuthorizationContext } from './authorization_provider'; - -export { WithPrivileges } from './with_privileges'; - -export { NotAuthorizedSection } from './not_authorized_section'; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/not_authorized_section.tsx b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/not_authorized_section.tsx deleted file mode 100644 index 3fc13245708e8..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/not_authorized_section.tsx +++ /dev/null @@ -1,17 +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 React from 'react'; -import { EuiEmptyPrompt } from '@elastic/eui'; - -interface Props { - title: React.ReactNode; - message: React.ReactNode | string; -} - -export const NotAuthorizedSection = ({ title, message }: Props) => ( - <EuiEmptyPrompt iconType="securityApp" title={<h2>{title}</h2>} body={<p>{message}</p>} /> -); diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/with_privileges.tsx deleted file mode 100644 index 223a2882c3cab..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/components/with_privileges.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useContext } from 'react'; - -import { MissingPrivileges } from '../../../../../common/types'; -import { AuthorizationContext } from './authorization_provider'; - -interface Props { - /** - * Each required privilege must have the format "section.privilege". - * To indicate that *all* privileges from a section are required, we can use the asterix - * e.g. "index.*" - */ - privileges: string | string[]; - children: (childrenProps: { - isLoading: boolean; - hasPrivileges: boolean; - privilegesMissing: MissingPrivileges; - }) => JSX.Element; -} - -type Privilege = [string, string]; - -const toArray = (value: string | string[]): string[] => - Array.isArray(value) ? (value as string[]) : ([value] as string[]); - -export const WithPrivileges = ({ privileges: requiredPrivileges, children }: Props) => { - const { isLoading, privileges } = useContext(AuthorizationContext); - - const privilegesToArray: Privilege[] = toArray(requiredPrivileges).map(p => { - const [section, privilege] = p.split('.'); - if (!privilege) { - // Oh! we forgot to use the dot "." notation. - throw new Error('Required privilege must have the format "section.privilege"'); - } - return [section, privilege]; - }); - - const hasPrivileges = isLoading - ? false - : privilegesToArray.every(privilege => { - const [section, requiredPrivilege] = privilege; - if (!privileges.missingPrivileges[section]) { - // if the section does not exist in our missingPriviledges, everything is OK - return true; - } - if (privileges.missingPrivileges[section]!.length === 0) { - return true; - } - if (requiredPrivilege === '*') { - // If length > 0 and we require them all... KO - return false; - } - // If we require _some_ privilege, we make sure that the one - // we require is *not* in the missingPrivilege array - return !privileges.missingPrivileges[section]!.includes(requiredPrivilege); - }); - - const privilegesMissing = privilegesToArray.reduce((acc, [section, privilege]) => { - if (privilege === '*') { - acc[section] = privileges.missingPrivileges[section] || []; - } else if ( - privileges.missingPrivileges[section] && - privileges.missingPrivileges[section]!.includes(privilege) - ) { - const missing: string[] = acc[section] || []; - acc[section] = [...missing, privilege]; - } - - return acc; - }, {} as MissingPrivileges); - - return children({ isLoading, hasPrivileges, privilegesMissing }); -}; diff --git a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/index.ts b/x-pack/plugins/snapshot_restore/public/application/lib/authorization/index.ts deleted file mode 100644 index 73bbde465146c..0000000000000 --- a/x-pack/plugins/snapshot_restore/public/application/lib/authorization/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './components'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx index f3110199ee17c..03d381c9f3aa3 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx @@ -26,6 +26,7 @@ import { import { SlmPolicy } from '../../../../../../common/types'; import { useServices } from '../../../../app_context'; +import { SectionError, Error } from '../../../../../shared_imports'; import { UIM_POLICY_DETAIL_PANEL_SUMMARY_TAB, UIM_POLICY_DETAIL_PANEL_HISTORY_TAB, @@ -34,11 +35,9 @@ import { useLoadPolicy } from '../../../../services/http'; import { linkToEditPolicy, linkToSnapshot } from '../../../../services/navigation'; import { - SectionError, SectionLoading, PolicyExecuteProvider, PolicyDeleteProvider, - Error, } from '../../../../components'; import { TabSummary, TabHistory } from './tabs'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx index 0122e25e5e165..51297038b0f3f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx @@ -9,13 +9,19 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiEmptyPrompt, EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { + SectionError, + Error, + WithPrivileges, + NotAuthorizedSection, +} from '../../../../shared_imports'; + import { SlmPolicy } from '../../../../../common/types'; -import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common'; +import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants'; import { useLoadPolicies, useLoadRetentionSettings } from '../../../services/http'; import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation'; -import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization'; import { useServices } from '../../../app_context'; import { PolicyDetails } from './policy_details'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx index 7f9c5c5af7705..ba28bcddf5347 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; +import { Error } from '../../../../../shared_imports'; import { UIM_POLICY_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useServices } from '../../../../app_context'; import { @@ -28,7 +29,6 @@ import { PolicyExecuteProvider, PolicyDeleteProvider, } from '../../../../components'; -import { Error } from '../../../../components/section_error'; import { linkToAddPolicy, linkToEditPolicy } from '../../../../services/navigation'; import { SendRequestResponse } from '../../../../../shared_imports'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx index d293f194f647a..9932f14664076 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_details/repository_details.tsx @@ -25,6 +25,8 @@ import { import 'brace/theme/textmate'; +import { SectionError, Error } from '../../../../../shared_imports'; + import { useServices } from '../../../../app_context'; import { documentationLinksService } from '../../../../services/documentation'; import { @@ -35,7 +37,8 @@ import { import { textService } from '../../../../services/text'; import { linkToSnapshots, linkToEditRepository } from '../../../../services/navigation'; -import { REPOSITORY_TYPES } from '../../../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../../../common'; + import { Repository, RepositoryVerification, @@ -43,10 +46,8 @@ import { } from '../../../../../../common/types'; import { RepositoryDeleteProvider, - SectionError, SectionLoading, RepositoryVerificationBadge, - Error, } from '../../../../components'; import { TypeDetails } from './type_details'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx index 6fa12537e9d6f..2256fa5991dec 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx @@ -10,7 +10,8 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { Repository } from '../../../../../common/types'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { SectionError, Error } from '../../../../shared_imports'; +import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_REPOSITORY_LIST_LOAD } from '../../../constants'; import { useServices } from '../../../app_context'; import { useLoadRepositories } from '../../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx index 7c0438f6b837f..bf2643d78bca1 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_table/repository_table.tsx @@ -15,15 +15,14 @@ import { EuiIconTip, } from '@elastic/eui'; -import { REPOSITORY_TYPES } from '../../../../../../common/constants'; +import { REPOSITORY_TYPES } from '../../../../../../common'; import { Repository, RepositoryType } from '../../../../../../common/types'; -import { Error } from '../../../../components/section_error'; +import { Error, SendRequestResponse } from '../../../../../shared_imports'; import { RepositoryDeleteProvider } from '../../../../components'; import { UIM_REPOSITORY_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useServices } from '../../../../app_context'; import { textService } from '../../../../services/text'; import { linkToEditRepository, linkToAddRepository } from '../../../../services/navigation'; -import { SendRequestResponse } from '../../../../../shared_imports'; interface Props { repositories: Repository[]; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx index da9ce3b124a11..0e3d9363d0535 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx @@ -18,14 +18,19 @@ import { EuiLoadingSpinner, EuiLink, } from '@elastic/eui'; -import { APP_RESTORE_INDEX_PRIVILEGES } from '../../../../../common/constants'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { APP_RESTORE_INDEX_PRIVILEGES } from '../../../../../common'; +import { + WithPrivileges, + NotAuthorizedSection, + SectionError, + Error, +} from '../../../../shared_imports'; +import { SectionLoading } from '../../../components'; import { UIM_RESTORE_LIST_LOAD } from '../../../constants'; import { useLoadRestores } from '../../../services/http'; import { linkToSnapshots } from '../../../services/navigation'; import { useServices } from '../../../app_context'; import { RestoreTable } from './restore_table'; -import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization'; const ONE_SECOND_MS = 1000; const TEN_SECONDS_MS = 10 * 1000; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx index d16545debe1ec..1943762a3c36e 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx @@ -23,12 +23,8 @@ import React, { Fragment, useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { SnapshotDetails as ISnapshotDetails } from '../../../../../../common/types'; -import { - SectionError, - SectionLoading, - SnapshotDeleteProvider, - Error, -} from '../../../../components'; +import { SectionError, Error } from '../../../../../shared_imports'; +import { SectionLoading, SnapshotDeleteProvider } from '../../../../components'; import { useServices } from '../../../../app_context'; import { UIM_SNAPSHOT_DETAIL_PANEL_SUMMARY_TAB, diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx index fe99ccb6f596c..30e4c771644bc 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx @@ -10,10 +10,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiButton, EuiCallOut, EuiLink, EuiEmptyPrompt, EuiSpacer, EuiIcon } from '@elastic/eui'; -import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants'; -import { SectionError, SectionLoading, Error } from '../../../components'; +import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common'; +import { WithPrivileges, SectionError, Error } from '../../../../shared_imports'; +import { SectionLoading } from '../../../components'; import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants'; -import { WithPrivileges } from '../../../lib/authorization'; import { documentationLinksService } from '../../../services/documentation'; import { useLoadSnapshots } from '../../../services/http'; import { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx index ad64dcc7adcfe..427c241970007 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx @@ -17,10 +17,10 @@ import { } from '@elastic/eui'; import { SnapshotDetails } from '../../../../../../common/types'; +import { Error } from '../../../../../shared_imports'; import { SNAPSHOT_STATE, UIM_SNAPSHOT_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useServices } from '../../../../app_context'; import { linkToRepository, linkToRestoreSnapshot } from '../../../../services/navigation'; -import { Error } from '../../../../components/section_error'; import { DataPlaceholder, FormattedDateTime, SnapshotDeleteProvider } from '../../../../components'; import { SendRequestResponse } from '../../../../../shared_imports'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx index 4eb0f54978d09..6d1a432be7f9f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_add/policy_add.tsx @@ -9,9 +9,11 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../common/types'; -import { TIME_UNITS } from '../../../../common/constants'; +import { TIME_UNITS } from '../../../../common'; -import { PolicyForm, SectionError, SectionLoading, Error } from '../../components'; +import { SectionError, Error } from '../../../shared_imports'; + +import { PolicyForm, SectionLoading } from '../../components'; import { BASE_PATH, DEFAULT_POLICY_SCHEDULE } from '../../constants'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { addPolicy, useLoadIndices } from '../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx index 9ca7eba5c4eeb..0f1473fc05492 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx @@ -9,8 +9,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../common/types'; +import { SectionError, Error } from '../../../shared_imports'; import { TIME_UNITS } from '../../../../common/constants'; -import { SectionError, SectionLoading, PolicyForm, Error } from '../../components'; +import { SectionLoading, PolicyForm } from '../../components'; import { BASE_PATH } from '../../constants'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx index 126e04bc7dc1d..08bfde833c368 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_add/repository_add.tsx @@ -12,7 +12,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { Repository, EmptyRepository } from '../../../../common/types'; -import { RepositoryForm, SectionError } from '../../components'; +import { SectionError } from '../../../shared_imports'; + +import { RepositoryForm } from '../../components'; import { BASE_PATH, Section } from '../../constants'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { addRepository } from '../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx index aa29b8b9f0551..95f8b9b8bde7d 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/repository_edit/repository_edit.tsx @@ -10,7 +10,8 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiCallOut, EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { Repository, EmptyRepository } from '../../../../common/types'; -import { RepositoryForm, SectionError, SectionLoading, Error } from '../../components'; +import { SectionError, Error } from '../../../shared_imports'; +import { RepositoryForm, SectionLoading } from '../../components'; import { BASE_PATH, Section } from '../../constants'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx index 252fd07a85f80..9eabed8341ee0 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/restore_snapshot/restore_snapshot.tsx @@ -9,8 +9,9 @@ import { RouteComponentProps } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; import { SnapshotDetails, RestoreSettings } from '../../../../common/types'; +import { SectionError, Error } from '../../../shared_imports'; import { BASE_PATH } from '../../constants'; -import { SectionError, SectionLoading, RestoreSnapshotForm, Error } from '../../components'; +import { SectionLoading, RestoreSnapshotForm } from '../../components'; import { useServices } from '../../app_context'; import { breadcrumbService, docTitleService } from '../../services/navigation'; import { useLoadSnapshot, executeRestore } from '../../services/http'; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts index 200d601fd2ce9..27a565ccb74bc 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/http/use_request.ts @@ -9,11 +9,10 @@ import { UseRequestConfig, sendRequest as _sendRequest, useRequest as _useRequest, + Error as CustomError, } from '../../../shared_imports'; -import { Error as CustomError } from '../../components/section_error'; - -import { httpService } from './index'; +import { httpService } from '.'; export const sendRequest = (config: SendRequestConfig) => { return _sendRequest<any, CustomError>(httpService.httpClient, config); diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts index 7e7ef09d0c09d..e0024ea8e0c12 100644 --- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts @@ -12,4 +12,10 @@ export { useRequest, CronEditor, DAY, + SectionError, + Error, + WithPrivileges, + useAuthorizationContext, + NotAuthorizedSection, + AuthorizationProvider, } from '../../../../src/plugins/es_ui_shared/public'; diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts index 5d334fddc144b..bda64fdb66571 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts @@ -3,12 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Privileges } from '../../../common/types'; +import { Privileges } from '../../../../../../src/plugins/es_ui_shared/public'; + import { APP_REQUIRED_CLUSTER_PRIVILEGES, APP_RESTORE_INDEX_PRIVILEGES, APP_SLM_CLUSTER_PRIVILEGES, -} from '../../../common/constants'; +} from '../../../common'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; 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 b063404f68e4f..65a810ff94a1f 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<any, any> = doc => { if (!doc.attributes.hasOwnProperty('disabledFeatures')) { doc.attributes.disabledFeatures = []; } diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 90187b7853185..3df2b015aa034 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -8,7 +8,7 @@ import { CallAPIOptions } from 'src/core/server'; import { take } from 'rxjs/operators'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Observable } from 'rxjs'; -import { KIBANA_STATS_TYPE_MONITORING } from '../../../../legacy/plugins/monitoring/common/constants'; +import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; import { KIBANA_SPACES_STATS_TYPE } from '../../common/constants'; import { PluginsSetup } from '../plugin'; diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index f7962f7011f34..8e877f696a2fc 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -7,7 +7,7 @@ import { configSchema } from './config'; describe('config validation', () => { test('task manager defaults', () => { - const config: Record<string, any> = {}; + const config: Record<string, unknown> = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "enabled": true, @@ -21,7 +21,7 @@ describe('config validation', () => { }); test('the ElastiSearch Tasks index cannot be used for task manager', () => { - const config: Record<string, any> = { + const config: Record<string, unknown> = { index: '.tasks', }; expect(() => { diff --git a/x-pack/plugins/task_manager/server/create_task_manager.ts b/x-pack/plugins/task_manager/server/create_task_manager.ts index 9ff97bbcc17e6..7ab6acba7976d 100644 --- a/x-pack/plugins/task_manager/server/create_task_manager.ts +++ b/x-pack/plugins/task_manager/server/create_task_manager.ts @@ -12,9 +12,10 @@ import { } from '../../../../src/core/server'; import { TaskManager } from './task_manager'; import { Logger } from './types'; +import { TaskManagerConfig } from './config'; export interface LegacyDeps { - config: any; + config: unknown; elasticsearch: Pick<IClusterClient, 'callAsInternalUser'>; savedObjectsRepository: ISavedObjectsRepository; savedObjectsSerializer: SavedObjectsSerializer; @@ -33,7 +34,7 @@ export function createTaskManager( ) { return new TaskManager({ taskManagerId: core.uuid.getInstanceUuid(), - config, + config: config as TaskManagerConfig, savedObjectsRepository, serializer: savedObjectsSerializer, callAsInternalUser, diff --git a/x-pack/plugins/task_manager/server/lib/middleware.test.ts b/x-pack/plugins/task_manager/server/lib/middleware.test.ts index 3aa39eb3db513..abf69e726262f 100644 --- a/x-pack/plugins/task_manager/server/lib/middleware.test.ts +++ b/x-pack/plugins/task_manager/server/lib/middleware.test.ts @@ -28,9 +28,9 @@ const getMockConcreteTaskInstance = () => { scheduledAt: Date; startedAt: Date | null; retryAt: Date | null; - state: any; + state: unknown; taskType: string; - params: any; + params: unknown; ownerId: string | null; } = { id: 'hy8o99o83', @@ -47,7 +47,7 @@ const getMockConcreteTaskInstance = () => { params: { abc: 'def' }, ownerId: null, }; - return concrete; + return (concrete as unknown) as ConcreteTaskInstance; }; const getMockRunContext = (runTask: ConcreteTaskInstance) => ({ taskInstance: runTask, @@ -95,7 +95,7 @@ describe('addMiddlewareToChain', () => { await middlewareChain .beforeSave({ taskInstance: getMockTaskInstance() }) - .then((saveOpts: any) => { + .then((saveOpts: unknown) => { expect(saveOpts).toMatchInlineSnapshot(` Object { "taskInstance": Object { diff --git a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts b/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts index da64befe28673..650eb36347c86 100644 --- a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts +++ b/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { RunContext } from '../task'; +import { RunContext, TaskDictionary, TaskDefinition } from '../task'; import { sanitizeTaskDefinitions } from './sanitize_task_definitions'; interface Opts { @@ -14,7 +14,7 @@ interface Opts { const getMockTaskDefinitions = (opts: Opts) => { const { numTasks } = opts; - const tasks: any = {}; + const tasks: Record<string, unknown> = {}; for (let i = 0; i < numTasks; i++) { const type = `test_task_type_${i}`; @@ -35,7 +35,7 @@ const getMockTaskDefinitions = (opts: Opts) => { }, }; } - return tasks; + return (tasks as unknown) as TaskDictionary<TaskDefinition>; }; describe('sanitizeTaskDefinitions', () => { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index e837fcd9c0dec..0f6e3fc31d96d 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -12,11 +12,10 @@ import { TaskManager } from './task_manager'; import { createTaskManager } from './create_task_manager'; import { TaskManagerConfig } from './config'; import { Middleware } from './lib/middleware'; +import { setupSavedObjects } from './saved_objects'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface PluginLegacyDependencies {} export type TaskManagerSetupContract = { - registerLegacyAPI: (legacyDependencies: PluginLegacyDependencies) => Promise<TaskManager>; + registerLegacyAPI: () => Promise<TaskManager>; } & Pick<TaskManager, 'addMiddleware' | 'registerTaskDefinitions'>; export type TaskManagerStartContract = Pick< @@ -35,12 +34,18 @@ export class TaskManagerPlugin this.currentConfig = {} as TaskManagerConfig; } - public setup(core: CoreSetup, plugins: any): TaskManagerSetupContract { + public async setup(core: CoreSetup, plugins: unknown): Promise<TaskManagerSetupContract> { const logger = this.initContext.logger.get('taskManager'); - const config$ = this.initContext.config.create<TaskManagerConfig>(); + const config = await this.initContext.config + .create<TaskManagerConfig>() + .pipe(first()) + .toPromise(); + + setupSavedObjects(core.savedObjects, config); + return { - registerLegacyAPI: once((__LEGACY: PluginLegacyDependencies) => { - config$.subscribe(async config => { + registerLegacyAPI: once(() => { + (async () => { const [{ savedObjects, elasticsearch }] = await core.getStartServices(); const savedObjectsRepository = savedObjects.createInternalRepository(['task']); this.legacyTaskManager$.next( @@ -53,7 +58,7 @@ export class TaskManagerPlugin }) ); this.legacyTaskManager$.complete(); - }); + })(); return this.taskManager; }), addMiddleware: (middleware: Middleware) => { diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 8f7cc47f936b2..4fd4da3d83a36 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -55,6 +55,7 @@ export const IdleTaskWithExpiredRunAt: MustCondition<TermFilter | RangeFilter> = }; // TODO: Fix query clauses to support this +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const InactiveTasks: BoolClauseWithAnyCondition<any> = { bool: { must_not: [ diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts new file mode 100644 index 0000000000000..0ad9021cd7f39 --- /dev/null +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { SavedObjectsServiceSetup } from 'kibana/server'; +import mappings from './mappings.json'; +import { TaskManagerConfig } from '../config.js'; + +export function setupSavedObjects( + savedObjects: SavedObjectsServiceSetup, + config: TaskManagerConfig +) { + savedObjects.registerType({ + name: 'task', + namespaceType: 'agnostic', + hidden: true, + convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + mappings: mappings.task, + indexPattern: config.index, + }); +} diff --git a/x-pack/legacy/plugins/task_manager/server/mappings.json b/x-pack/plugins/task_manager/server/saved_objects/mappings.json similarity index 100% rename from x-pack/legacy/plugins/task_manager/server/mappings.json rename to x-pack/plugins/task_manager/server/saved_objects/mappings.json diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..1c2cf73d0fe13 --- /dev/null +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts @@ -0,0 +1,35 @@ +/* + * 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 { SavedObject } from '../../../../../src/core/server'; + +export const migrations = { + task: { + '7.4.0': (doc: SavedObject<Record<string, unknown>>) => ({ + ...doc, + updated_at: new Date().toISOString(), + }), + '7.6.0': moveIntervalIntoSchedule, + }, +}; + +function moveIntervalIntoSchedule({ + attributes: { interval, ...attributes }, + ...doc +}: SavedObject<Record<string, unknown>>) { + return { + ...doc, + attributes: { + ...attributes, + ...(interval + ? { + schedule: { + interval, + }, + } + : {}), + }, + }; +} diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index 48e87582ce3fe..e806a866ff0b7 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -28,7 +28,7 @@ type Require<T extends object, P extends keyof T> = Omit<T, P> & Required<Pick<T * A loosely typed definition of the elasticjs wrapper. It's beyond the scope * of this work to try to make a comprehensive type definition of this. */ -export type ElasticJs = (action: string, args: any) => Promise<any>; +export type ElasticJs = (action: string, args: unknown) => Promise<unknown>; /** * The run context is passed into a task's run function as its sole argument. @@ -61,12 +61,12 @@ export interface RunResult { * The state which will be passed to the next run of this task (if this is a * recurring task). See the RunContext type definition for more details. */ - state: Record<string, any>; + state: Record<string, unknown>; } export interface SuccessfulRunResult { runAt?: Date; - state?: Record<string, any>; + state?: Record<string, unknown>; } export interface FailedRunResult extends SuccessfulRunResult { @@ -237,6 +237,9 @@ export interface TaskInstance { * A task-specific set of parameters, used by the task's run function to tailor * its work. This is generally user-input, such as { sms: '333-444-2222' }. */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record<string, any>; /** @@ -244,6 +247,9 @@ export interface TaskInstance { * run. If there was no previous run, or if the previous run did not return * any state, this will be the empy object: {} */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record<string, any>; /** @@ -336,6 +342,9 @@ export interface ConcreteTaskInstance extends TaskInstance { * run. If there was no previous run, or if the previous run did not return * any state, this will be the empy object: {} */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record<string, any>; /** @@ -343,3 +352,15 @@ export interface ConcreteTaskInstance extends TaskInstance { */ ownerId: string | null; } + +export type SerializedConcreteTaskInstance = Omit< + ConcreteTaskInstance, + 'state' | 'params' | 'scheduledAt' | 'startedAt' | 'retryAt' | 'runAt' +> & { + state: string; + params: string; + scheduledAt: string; + startedAt: string | null; + retryAt: string | null; + runAt: string; +}; diff --git a/x-pack/plugins/task_manager/server/task_events.ts b/x-pack/plugins/task_manager/server/task_events.ts index 063ac2499471f..b17a3636c1730 100644 --- a/x-pack/plugins/task_manager/server/task_events.ts +++ b/x-pack/plugins/task_manager/server/task_events.ts @@ -68,16 +68,18 @@ export function asTaskRunRequestEvent( } export function isTaskMarkRunningEvent( - taskEvent: TaskEvent<any, any> + taskEvent: TaskEvent<unknown, unknown> ): taskEvent is TaskMarkRunning { return taskEvent.type === TaskEventType.TASK_MARK_RUNNING; } -export function isTaskRunEvent(taskEvent: TaskEvent<any, any>): taskEvent is TaskRun { +export function isTaskRunEvent(taskEvent: TaskEvent<unknown, unknown>): taskEvent is TaskRun { return taskEvent.type === TaskEventType.TASK_RUN; } -export function isTaskClaimEvent(taskEvent: TaskEvent<any, any>): taskEvent is TaskClaim { +export function isTaskClaimEvent(taskEvent: TaskEvent<unknown, unknown>): taskEvent is TaskClaim { return taskEvent.type === TaskEventType.TASK_CLAIM; } -export function isTaskRunRequestEvent(taskEvent: TaskEvent<any, any>): taskEvent is TaskRunRequest { +export function isTaskRunRequestEvent( + taskEvent: TaskEvent<unknown, unknown> +): taskEvent is TaskRunRequest { return taskEvent.type === TaskEventType.TASK_RUN_REQUEST; } diff --git a/x-pack/plugins/task_manager/server/task_manager.test.ts b/x-pack/plugins/task_manager/server/task_manager.test.ts index 3d48ce18c9d6a..3f3b14a791f24 100644 --- a/x-pack/plugins/task_manager/server/task_manager.test.ts +++ b/x-pack/plugins/task_manager/server/task_manager.test.ts @@ -25,6 +25,7 @@ import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src import { mockLogger } from './test_utils'; import { asErr, asOk } from './lib/result_type'; import { ConcreteTaskInstance, TaskLifecycleResult, TaskStatus } from './task'; +import { Middleware } from './lib/middleware'; const savedObjectsClient = savedObjectsRepositoryMock.create(); const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); @@ -247,20 +248,20 @@ describe('TaskManager', () => { test('allows middleware registration before starting', () => { const client = new TaskManager(taskManagerOpts); - const middleware = { - beforeSave: async (saveOpts: any) => saveOpts, - beforeRun: async (runOpts: any) => runOpts, - beforeMarkRunning: async (runOpts: any) => runOpts, + const middleware: Middleware = { + beforeSave: jest.fn(async saveOpts => saveOpts), + beforeRun: jest.fn(async runOpts => runOpts), + beforeMarkRunning: jest.fn(async runOpts => runOpts), }; expect(() => client.addMiddleware(middleware)).not.toThrow(); }); test('disallows middleware registration after starting', async () => { const client = new TaskManager(taskManagerOpts); - const middleware = { - beforeSave: async (saveOpts: any) => saveOpts, - beforeRun: async (runOpts: any) => runOpts, - beforeMarkRunning: async (runOpts: any) => runOpts, + const middleware: Middleware = { + beforeSave: jest.fn(async saveOpts => saveOpts), + beforeRun: jest.fn(async runOpts => runOpts), + beforeMarkRunning: jest.fn(async runOpts => runOpts), }; client.start(); diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index a7c67d190e72e..2a45a599120dd 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -43,6 +43,7 @@ import { TaskLifecycle, TaskLifecycleResult, TaskStatus, + ElasticJs, } from './task'; import { createTaskPoller, PollingError, PollingErrorType } from './task_poller'; import { TaskPool } from './task_pool'; @@ -129,7 +130,7 @@ export class TaskManager { this.store = new TaskStore({ serializer: opts.serializer, savedObjectsRepository: opts.savedObjectsRepository, - callCluster: opts.callAsInternalUser, + callCluster: (opts.callAsInternalUser as unknown) as ElasticJs, index: opts.config.index, maxAttempts: opts.config.max_attempts, definitions: this.definitions, @@ -239,7 +240,7 @@ export class TaskManager { * @param taskDefinitions - The Kibana task definitions dictionary */ public registerTaskDefinitions(taskDefinitions: TaskDictionary<TaskDefinition>) { - this.assertUninitialized('register task definitions'); + this.assertUninitialized('register task definitions', Object.keys(taskDefinitions).join(', ')); const duplicate = Object.keys(taskDefinitions).find(k => !!this.definitions[k]); if (duplicate) { throw new Error(`Task ${duplicate} is already defined!`); @@ -273,7 +274,7 @@ export class TaskManager { */ public async schedule( taskInstance: TaskInstanceWithDeprecatedFields, - options?: any + options?: object ): Promise<ConcreteTaskInstance> { await this.waitUntilStarted(); const { taskInstance: modifiedTask } = await this.middleware.beforeSave({ @@ -308,7 +309,7 @@ export class TaskManager { */ public async ensureScheduled( taskInstance: TaskInstanceWithId, - options?: any + options?: object ): Promise<TaskInstanceWithId> { try { return await this.schedule(taskInstance, options); @@ -359,9 +360,11 @@ export class TaskManager { * @param {string} message shown if task manager is already initialized * @returns void */ - private assertUninitialized(message: string) { + private assertUninitialized(message: string, context?: string) { if (this.isStarted) { - throw new Error(`Cannot ${message} after the task manager is initialized!`); + throw new Error( + `${context ? `[${context}] ` : ''}Cannot ${message} after the task manager is initialized` + ); } } } diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index fb87b6290a3da..dee4d80b97917 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -9,6 +9,7 @@ import { TaskPool, TaskPoolRunResult } from './task_pool'; import { mockLogger, resolvable, sleep } from './test_utils'; import { asOk } from './lib/result_type'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import moment from 'moment'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { @@ -190,14 +191,16 @@ describe('TaskPool', () => { }); test('run cancels expired tasks prior to running new tasks', async () => { + const logger = mockLogger(); const pool = new TaskPool({ maxWorkers: 2, - logger: mockLogger(), + logger, }); const expired = resolvable(); const shouldRun = sinon.spy(() => Promise.resolve()); const shouldNotRun = sinon.spy(() => Promise.resolve()); + const now = new Date(); const result = await pool.run([ { ...mockTask(), @@ -207,6 +210,16 @@ describe('TaskPool', () => { await sleep(10); return asOk({ state: {} }); }, + get expiration() { + return now; + }, + get startedAt() { + // 5 and a half minutes + return moment(now) + .subtract(5, 'm') + .subtract(30, 's') + .toDate(); + }, cancel: shouldRun, }, { @@ -231,6 +244,10 @@ describe('TaskPool', () => { expect(pool.occupiedWorkers).toEqual(2); expect(pool.availableWorkers).toEqual(0); + + expect(logger.warn).toHaveBeenCalledWith( + `Cancelling task TaskType "shooooo" as it expired at ${now.toISOString()} after running for 05m 30s (with timeout set at 5m).` + ); }); test('logs if cancellation errors', async () => { @@ -285,6 +302,20 @@ describe('TaskPool', () => { markTaskAsRunning: jest.fn(async () => true), run: mockRun(), toString: () => `TaskType "shooooo"`, + get expiration() { + return new Date(); + }, + get startedAt() { + return new Date(); + }, + get definition() { + return { + type: '', + title: '', + timeout: '5m', + createTaskRunner: jest.fn(), + }; + }, }; } }); diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 8999fb48680ce..bd0de86551aaa 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -8,7 +8,9 @@ * This module contains the logic that ensures we don't run too many * tasks at once in a given Kibana instance. */ +import moment, { Duration } from 'moment'; import { performance } from 'perf_hooks'; +import { padLeft } from 'lodash'; import { Logger } from './types'; import { TaskRunner } from './task_runner'; import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; @@ -148,7 +150,19 @@ export class TaskPool { private cancelExpiredTasks() { for (const task of this.running) { if (task.isExpired) { - this.logger.debug(`Cancelling expired task ${task.toString()}.`); + this.logger.warn( + `Cancelling task ${task.toString()} as it expired at ${task.expiration.toISOString()}${ + task.startedAt + ? ` after running for ${durationAsString( + moment.duration( + moment(new Date()) + .utc() + .diff(task.startedAt) + ) + )}` + : `` + }${task.definition.timeout ? ` (with timeout set at ${task.definition.timeout})` : ``}.` + ); this.cancelTask(task); } } @@ -169,3 +183,8 @@ function partitionListByCount<T>(list: T[], count: number): [T[], T[]] { const listInCount = list.splice(0, count); return [listInCount, list]; } + +function durationAsString(duration: Duration): string { + const [m, s] = [duration.minutes(), duration.seconds()].map(value => padLeft(`${value}`, 2, '0')); + return `${m}m ${s}s`; +} diff --git a/x-pack/plugins/task_manager/server/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_runner.test.ts index 3f0132105347e..07247dcb1da47 100644 --- a/x-pack/plugins/task_manager/server/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_runner.test.ts @@ -9,10 +9,11 @@ import sinon from 'sinon'; import { minutesFromNow } from './lib/intervals'; import { asOk, asErr } from './lib/result_type'; import { TaskEvent, asTaskRunEvent, asTaskMarkRunningEvent } from './task_events'; -import { ConcreteTaskInstance, TaskStatus } from './task'; +import { ConcreteTaskInstance, TaskStatus, TaskDictionary, TaskDefinition } from './task'; import { TaskManagerRunner } from './task_runner'; import { mockLogger } from './test_utils'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import moment from 'moment'; let fakeTimer: sinon.SinonFakeTimers; @@ -113,6 +114,60 @@ describe('TaskManagerRunner', () => { expect(instance.runAt.getTime()).toBeLessThanOrEqual(minutesFromNow(10).getTime()); }); + test('expiration returns time after which timeout will have elapsed from start', async () => { + const now = moment(); + const { runner } = testOpts({ + instance: { + schedule: { interval: '10m' }, + status: TaskStatus.Running, + startedAt: now.toDate(), + }, + definitions: { + bar: { + timeout: `1m`, + createTaskRunner: () => ({ + async run() { + return; + }, + }), + }, + }, + }); + + await runner.run(); + + expect(runner.isExpired).toBe(false); + expect(runner.expiration).toEqual(now.add(1, 'm').toDate()); + }); + + test('runDuration returns duration which has elapsed since start', async () => { + const now = moment() + .subtract(30, 's') + .toDate(); + const { runner } = testOpts({ + instance: { + schedule: { interval: '10m' }, + status: TaskStatus.Running, + startedAt: now, + }, + definitions: { + bar: { + timeout: `1m`, + createTaskRunner: () => ({ + async run() { + return; + }, + }), + }, + }, + }); + + await runner.run(); + + expect(runner.isExpired).toBe(false); + expect(runner.startedAt).toEqual(now); + }); + test('reschedules tasks that return a runAt', async () => { const runAt = minutesFromNow(_.random(1, 10)); const { runner, store } = testOpts({ @@ -208,7 +263,7 @@ describe('TaskManagerRunner', () => { expect(logger.warn).not.toHaveBeenCalled(); }); - test('warns if cancel is called on a non-cancellable task', async () => { + test('debug logs if cancel is called on a non-cancellable task', async () => { const { runner, logger } = testOpts({ definitions: { bar: { @@ -223,10 +278,7 @@ describe('TaskManagerRunner', () => { await runner.cancel(); await promise; - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"The task bar \\"foo\\" is not cancellable."` - ); + expect(logger.debug).toHaveBeenCalledWith(`The task bar "foo" is not cancellable.`); }); test('sets startedAt, status, attempts and retryAt when claiming a task', async () => { @@ -854,8 +906,8 @@ describe('TaskManagerRunner', () => { interface TestOpts { instance?: Partial<ConcreteTaskInstance>; - definitions?: any; - onTaskEvent?: (event: TaskEvent<any, any>) => void; + definitions?: unknown; + onTaskEvent?: (event: TaskEvent<unknown, unknown>) => void; } function testOpts(opts: TestOpts) { @@ -904,7 +956,7 @@ describe('TaskManagerRunner', () => { title: 'Bar!', createTaskRunner, }, - }), + }) as TaskDictionary<TaskDefinition>, onTaskEvent: opts.onTaskEvent, }); @@ -918,7 +970,7 @@ describe('TaskManagerRunner', () => { }; } - async function testReturn(result: any, shouldBeValid: boolean) { + async function testReturn(result: unknown, shouldBeValid: boolean) { const { runner, logger } = testOpts({ definitions: { bar: { @@ -939,11 +991,11 @@ describe('TaskManagerRunner', () => { } } - function allowsReturnType(result: any) { + function allowsReturnType(result: unknown) { return testReturn(result, true); } - function disallowsReturnType(result: any) { + function disallowsReturnType(result: unknown) { return testReturn(result, false); } }); diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts index 682885aaa0b1c..7a9fa0c45e15f 100644 --- a/x-pack/plugins/task_manager/server/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_runner.ts @@ -39,6 +39,9 @@ const EMPTY_RUN_RESULT: SuccessfulRunResult = {}; export interface TaskRunner { isExpired: boolean; + expiration: Date; + startedAt: Date | null; + definition: TaskDefinition; cancel: CancelFunction; markTaskAsRunning: () => Promise<boolean>; run: () => Promise<Result<SuccessfulRunResult, FailedRunResult>>; @@ -129,11 +132,25 @@ export class TaskManagerRunner implements TaskRunner { return this.definitions[this.taskType]; } + /** + * Gets the time at which this task will expire. + */ + public get expiration() { + return intervalFromDate(this.instance.startedAt!, this.definition.timeout)!; + } + + /** + * Gets the duration of the current task run + */ + public get startedAt() { + return this.instance.startedAt; + } + /** * Gets whether or not this task has run longer than its expiration setting allows. */ public get isExpired() { - return intervalFromDate(this.instance.startedAt!, this.definition.timeout)! < new Date(); + return this.expiration < new Date(); } /** @@ -261,12 +278,12 @@ export class TaskManagerRunner implements TaskRunner { */ public async cancel() { const { task } = this; - if (task && task.cancel) { + if (task?.cancel) { this.task = undefined; return task.cancel(); } - this.logger.warn(`The task ${this} is not cancellable.`); + this.logger.debug(`The task ${this} is not cancellable.`); } private validateResult(result?: RunResult | void): Result<SuccessfulRunResult, FailedRunResult> { @@ -392,7 +409,7 @@ export class TaskManagerRunner implements TaskRunner { attempts, addDuration, }: { - error: any; + error: Error; attempts: number; addDuration?: string; }): Date | null { diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 4ecefcb7984eb..6524ea212e7c5 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -15,11 +15,17 @@ import { TaskInstance, TaskStatus, TaskLifecycleResult, + SerializedConcreteTaskInstance, + ConcreteTaskInstance, } from './task'; import { StoreOpts, OwnershipClaimingOpts, TaskStore, SearchOpts } from './task_store'; -import { savedObjectsRepositoryMock } from '../../../../src/core/server/mocks'; -import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src/core/server'; -import { SavedObjectsErrorHelpers } from '../../../../src/core/server/saved_objects/service/lib/errors'; +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { + SavedObjectsSerializer, + SavedObjectTypeRegistry, + SavedObjectAttributes, + SavedObjectsErrorHelpers, +} from 'src/core/server'; import { asTaskClaimEvent, TaskEvent } from './task_events'; import { asOk, asErr } from './lib/result_type'; @@ -47,6 +53,7 @@ const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); beforeEach(() => jest.resetAllMocks()); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date { constructor() { return mockedDate; @@ -58,9 +65,9 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); describe('TaskStore', () => { describe('schedule', () => { - async function testSchedule(task: TaskInstance) { + async function testSchedule(task: unknown) { const callCluster = jest.fn(); - savedObjectsClient.create.mockImplementation(async (type: string, attributes: any) => ({ + savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ id: 'testid', type, attributes, @@ -76,7 +83,7 @@ describe('TaskStore', () => { definitions: taskDefinitions, savedObjectsRepository: savedObjectsClient, }); - const result = await store.schedule(task); + const result = await store.schedule(task as TaskInstance); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -149,14 +156,16 @@ describe('TaskStore', () => { test('sets runAt to now if not specified', async () => { await testSchedule({ taskType: 'dernstraight', params: {}, state: {} }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes: any = savedObjectsClient.create.mock.calls[0][1]; + const attributes = savedObjectsClient.create.mock + .calls[0][1] as SerializedConcreteTaskInstance; expect(new Date(attributes.runAt as string).getTime()).toEqual(mockedDate.getTime()); }); test('ensures params and state are not null', async () => { - await testSchedule({ taskType: 'yawn' } as any); + await testSchedule({ taskType: 'yawn' }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes: any = savedObjectsClient.create.mock.calls[0][1]; + const attributes = savedObjectsClient.create.mock + .calls[0][1] as SerializedConcreteTaskInstance; expect(attributes.params).toEqual('{}'); expect(attributes.state).toEqual('{}'); }); @@ -169,8 +178,8 @@ describe('TaskStore', () => { }); describe('fetch', () => { - async function testFetch(opts?: SearchOpts, hits: any[] = []) { - const callCluster = sinon.spy(async (name: string, params?: any) => ({ hits: { hits } })); + async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { + const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ hits: { hits } })); const store = new TaskStore({ index: 'tasky', taskManagerId: '', @@ -229,11 +238,11 @@ describe('TaskStore', () => { claimingOpts, }: { opts: Partial<StoreOpts>; - hits?: any[]; + hits?: unknown[]; claimingOpts: OwnershipClaimingOpts; }) { const versionConflicts = 2; - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: hits.length + versionConflicts, @@ -266,7 +275,7 @@ describe('TaskStore', () => { } test('it returns normally with no tasks when the index does not exist.', async () => { - const callCluster = sinon.spy(async (name: string, params?: any) => ({ + const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ total: 0, updated: 0, })); @@ -745,7 +754,7 @@ if (doc['task.runAt'].size()!=0) { }; savedObjectsClient.update.mockImplementation( - async (type: string, id: string, attributes: any) => { + async (type: string, id: string, attributes: SavedObjectAttributes) => { return { id, type, @@ -1017,7 +1026,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when a task is succesfully claimed by id', async done => { const { taskManagerId, runAt, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1036,9 +1045,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent<any, any>) => event.id === 'aaa')) + .pipe(filter((event: TaskEvent<ConcreteTaskInstance, Error>) => event.id === 'aaa')) .subscribe({ - next: (event: TaskEvent<any, any>) => { + next: (event: TaskEvent<ConcreteTaskInstance, Error>) => { expect(event).toMatchObject( asTaskClaimEvent( 'aaa', @@ -1074,7 +1083,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when a task is succesfully by scheduling', async done => { const { taskManagerId, runAt, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1093,9 +1102,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent<any, any>) => event.id === 'bbb')) + .pipe(filter((event: TaskEvent<ConcreteTaskInstance, Error>) => event.id === 'bbb')) .subscribe({ - next: (event: TaskEvent<any, any>) => { + next: (event: TaskEvent<ConcreteTaskInstance, Error>) => { expect(event).toMatchObject( asTaskClaimEvent( 'bbb', @@ -1131,7 +1140,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when the store fails to claim a required task by id', async done => { const { taskManagerId, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1150,9 +1159,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent<any, any>) => event.id === 'ccc')) + .pipe(filter((event: TaskEvent<ConcreteTaskInstance, Error>) => event.id === 'ccc')) .subscribe({ - next: (event: TaskEvent<any, any>) => { + next: (event: TaskEvent<ConcreteTaskInstance, Error>) => { expect(event).toMatchObject( asTaskClaimEvent('ccc', asErr(new Error(`failed to claim task 'ccc'`))) ); diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 0e487386eb04d..01299615c7d49 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -9,11 +9,11 @@ */ import apm from 'elastic-apm-node'; import { Subject, Observable } from 'rxjs'; -import { omit, difference } from 'lodash'; +import { omit, difference, defaults } from 'lodash'; +import { SearchResponse, UpdateDocumentByQueryResponse } from 'elasticsearch'; import { SavedObject, - SavedObjectAttributes, SavedObjectsSerializer, SavedObjectsRawDoc, ISavedObjectsRepository, @@ -29,6 +29,7 @@ import { TaskInstance, TaskLifecycle, TaskLifecycleResult, + SerializedConcreteTaskInstance, } from './task'; import { TaskClaim, asTaskClaimEvent } from './task_events'; @@ -71,7 +72,7 @@ export interface SearchOpts { query?: object; size?: number; seq_no_primary_term?: boolean; - search_after?: any[]; + search_after?: unknown[]; } export interface UpdateByQuerySearchOpts extends SearchOpts { @@ -166,7 +167,7 @@ export class TaskStore { ); } - const savedObject = await this.savedObjectsRepository.create( + const savedObject = await this.savedObjectsRepository.create<SerializedConcreteTaskInstance>( 'task', taskInstanceToAttributes(taskInstance), { id: taskInstance.id, refresh: false } @@ -314,17 +315,21 @@ export class TaskStore { * @returns {Promise<TaskDoc>} */ public async update(doc: ConcreteTaskInstance): Promise<ConcreteTaskInstance> { - const updatedSavedObject = await this.savedObjectsRepository.update( - 'task', - doc.id, - taskInstanceToAttributes(doc), - { - refresh: false, - version: doc.version, - } - ); + const attributes = taskInstanceToAttributes(doc); + const updatedSavedObject = await this.savedObjectsRepository.update< + SerializedConcreteTaskInstance + >('task', doc.id, attributes, { + refresh: false, + version: doc.version, + }); - return savedObjectToConcreteTaskInstance(updatedSavedObject); + return savedObjectToConcreteTaskInstance( + // The SavedObjects update api forces a Partial on the `attributes` on the response, + // but actually returns the whole object that is passed to it, so as we know we're + // passing in the whole object, this is safe to do. + // This is far from ideal, but unless we change the SavedObjectsClient this is the best we can do + { ...updatedSavedObject, attributes: defaults(updatedSavedObject.attributes, attributes) } + ); } /** @@ -377,12 +382,12 @@ export class TaskStore { }, }); - const rawDocs = result.hits.hits; + const rawDocs = (result as SearchResponse<unknown>).hits.hits; return { docs: (rawDocs as SavedObjectsRawDoc[]) .map(doc => this.serializer.rawToSavedObject(doc)) - .map(doc => omit(doc, 'namespace') as SavedObject) + .map(doc => omit(doc, 'namespace') as SavedObject<SerializedConcreteTaskInstance>) .map(savedObjectToConcreteTaskInstance), }; } @@ -404,7 +409,7 @@ export class TaskStore { }, }); - const { total, updated, version_conflicts } = result; + const { total, updated, version_conflicts } = result as UpdateDocumentByQueryResponse; return { total, updated, @@ -413,7 +418,7 @@ export class TaskStore { } } -function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes { +function taskInstanceToAttributes(doc: TaskInstance): SerializedConcreteTaskInstance { return { ...omit(doc, 'id', 'version'), params: JSON.stringify(doc.params || {}), @@ -428,8 +433,7 @@ function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes { } export function savedObjectToConcreteTaskInstance( - // TODO: define saved object type - savedObject: Omit<SavedObject<any>, 'references'> + savedObject: Omit<SavedObject<SerializedConcreteTaskInstance>, 'references'> ): ConcreteTaskInstance { return { ...savedObject.attributes, @@ -437,8 +441,8 @@ export function savedObjectToConcreteTaskInstance( version: savedObject.version, scheduledAt: new Date(savedObject.attributes.scheduledAt), runAt: new Date(savedObject.attributes.runAt), - startedAt: savedObject.attributes.startedAt && new Date(savedObject.attributes.startedAt), - retryAt: savedObject.attributes.retryAt && new Date(savedObject.attributes.retryAt), + startedAt: savedObject.attributes.startedAt ? new Date(savedObject.attributes.startedAt) : null, + retryAt: savedObject.attributes.retryAt ? new Date(savedObject.attributes.retryAt) : null, state: parseJSONField(savedObject.attributes.state, 'state', savedObject.id), params: parseJSONField(savedObject.attributes.params, 'params', savedObject.id), }; diff --git a/x-pack/plugins/task_manager/server/test_utils/index.ts b/x-pack/plugins/task_manager/server/test_utils/index.ts index 719ccadbe33dd..3dfc53672b46f 100644 --- a/x-pack/plugins/task_manager/server/test_utils/index.ts +++ b/x-pack/plugins/task_manager/server/test_utils/index.ts @@ -33,11 +33,11 @@ interface Resolvable { */ export function resolvable(): PromiseLike<void> & Resolvable { let resolve: () => void; - const result = new Promise<void>(r => (resolve = r)) as any; - - result.resolve = () => nativeTimeout(resolve, 0); - - return result; + return Object.assign(new Promise<void>(r => (resolve = r)), { + resolve() { + return nativeTimeout(resolve, 0); + }, + }); } /** diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts index f2a9995098e59..5dfe3d3e99a7f 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_stats_with_xpack.test.ts @@ -31,7 +31,6 @@ const kibana = { const getContext = () => ({ version: '8675309-snapshot', - isDev: true, logger: coreMock.createPluginInitializerContext().logger.get('test'), }); diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 0db11ffa061c0..9d8106a1366d6 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// actual mocks export const expandLiteralStrings = jest.fn(); export const XJsonMode = jest.fn(); export const useRequest = jest.fn(() => ({ @@ -11,5 +12,20 @@ export const useRequest = jest.fn(() => ({ error: null, data: undefined, })); -export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; -export const SORT_DIRECTION = { ASC: 'asc' }; + +// just passing through the reimports +export { getErrorMessage } from '../../../ml/common/util/errors'; +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts new file mode 100644 index 0000000000000..0e5ecb5d3b214 --- /dev/null +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getPreviewRequestBody, + PivotAggsConfig, + PivotGroupByConfig, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, + SimpleQuery, +} from '../common'; + +import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; + +describe('Transform: Data Grid', () => { + test('getPivotPreviewDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const groupBy: PivotGroupByConfig = { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-group-by-field', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', + }; + const agg: PivotAggsConfig = { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-agg-field', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', + }; + const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); + const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); + + expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview +{ + "source": { + "index": [ + "the-index-pattern-title" + ] + }, + "pivot": { + "group_by": { + "the-group-by-agg-name": { + "terms": { + "field": "the-group-by-field" + } + } + }, + "aggregations": { + "the-agg-agg-name": { + "avg": { + "field": "the-agg-field" + } + } + } + } +} +`); + }); +}); + +describe('Transform: Index Preview Common', () => { + test('getIndexDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const indexPreviewDevConsoleStatement = getIndexDevConsoleStatement( + query, + 'the-index-pattern-title' + ); + + expect(indexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search +{ + "query": { + "query_string": { + "query": "*", + "default_operator": "AND" + } + } +} +`); + }); +}); diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index 0e9cceefb3156..cf9ba5d6f5853 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridStyle } from '@elastic/eui'; +import { PivotQuery } from './request'; +import { PreviewRequestBody } from './transform'; export const INIT_MAX_COLUMNS = 20; -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'highlight', - header: 'shade', +export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { + return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, +export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { + return `GET ${indexPatternTitle}/_search\n${JSON.stringify( + { + query, + }, + null, + 2 + )}\n`; }; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index daeddaa801828..009c8c7a2a9f5 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -5,7 +5,11 @@ */ export { AggName, isAggName } from './aggregations'; -export { euiDataGridStyle, euiDataGridToolbarSettings, INIT_MAX_COLUMNS } from './data_grid'; +export { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, + INIT_MAX_COLUMNS, +} from './data_grid'; export { getDefaultSelectableFields, getFlattenedFields, diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts deleted file mode 100644 index 172256ddb5cee..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts +++ /dev/null @@ -1,117 +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 { EuiDataGridSorting } from '@elastic/eui'; - -import { - getPreviewRequestBody, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, - SimpleQuery, -} from '../../common'; - -import { multiColumnSortFactory, getPivotPreviewDevConsoleStatement } from './common'; - -describe('Transform: Define Pivot Common', () => { - test('multiColumnSortFactory()', () => { - const data = [ - { s: 'a', n: 1 }, - { s: 'a', n: 2 }, - { s: 'b', n: 3 }, - { s: 'b', n: 4 }, - ]; - - const sortingColumns1: EuiDataGridSorting['columns'] = [{ id: 's', direction: 'desc' }]; - const multiColumnSort1 = multiColumnSortFactory(sortingColumns1); - data.sort(multiColumnSort1); - - expect(data).toStrictEqual([ - { s: 'b', n: 3 }, - { s: 'b', n: 4 }, - { s: 'a', n: 1 }, - { s: 'a', n: 2 }, - ]); - - const sortingColumns2: EuiDataGridSorting['columns'] = [ - { id: 's', direction: 'asc' }, - { id: 'n', direction: 'desc' }, - ]; - const multiColumnSort2 = multiColumnSortFactory(sortingColumns2); - data.sort(multiColumnSort2); - - expect(data).toStrictEqual([ - { s: 'a', n: 2 }, - { s: 'a', n: 1 }, - { s: 'b', n: 4 }, - { s: 'b', n: 3 }, - ]); - - const sortingColumns3: EuiDataGridSorting['columns'] = [ - { id: 'n', direction: 'desc' }, - { id: 's', direction: 'desc' }, - ]; - const multiColumnSort3 = multiColumnSortFactory(sortingColumns3); - data.sort(multiColumnSort3); - - expect(data).toStrictEqual([ - { s: 'b', n: 4 }, - { s: 'b', n: 3 }, - { s: 'a', n: 2 }, - { s: 'a', n: 1 }, - ]); - }); - - test('getPivotPreviewDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }; - const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); - const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); - - expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview -{ - "source": { - "index": [ - "the-index-pattern-title" - ] - }, - "pivot": { - "group_by": { - "the-group-by-agg-name": { - "terms": { - "field": "the-group-by-field" - } - } - }, - "aggregations": { - "the-agg-agg-name": { - "avg": { - "field": "the-agg-field" - } - } - } - } -} -`); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts deleted file mode 100644 index 498c3a3ac60af..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts +++ /dev/null @@ -1,60 +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 { EuiDataGridSorting } from '@elastic/eui'; - -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { PreviewRequestBody } from '../../common'; - -/** - * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. - * `sortFn()` is recursive to support sorting on multiple columns. - * - * @param sortingColumns - The EUI data grid sorting configuration - * @returns The sorting function which can be used with an array's sort() function. - */ -export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { - const isString = (arg: any): arg is string => { - return typeof arg === 'string'; - }; - - const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { - const sort = sortingColumns[sortingColumnIndex]; - const aValue = getNestedProperty(a, sort.id, null); - const bValue = getNestedProperty(b, sort.id, null); - - if (typeof aValue === 'number' && typeof bValue === 'number') { - if (aValue < bValue) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue > bValue) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (isString(aValue) && isString(bValue)) { - if (aValue.localeCompare(bValue) === -1) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue.localeCompare(bValue) === 1) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (sortingColumnIndex + 1 < sortingColumns.length) { - return sortFn(a, b, sortingColumnIndex + 1); - } - - return 0; - }; - - return sortFn; -}; - -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { - return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts deleted file mode 100644 index 049e73d6309fc..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { PivotPreview } from './pivot_preview'; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx deleted file mode 100644 index 5ed50eaab46ba..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx +++ /dev/null @@ -1,55 +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 React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { - getPivotQuery, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../common'; - -import { PivotPreview } from './pivot_preview'; - -jest.mock('../../../shared_imports'); -jest.mock('../../../app/app_dependencies'); - -describe('Transform: <PivotPreview />', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const props = { - aggs: { 'the-agg-name': agg }, - groupBy: { 'the-group-by-name': groupBy }, - indexPatternTitle: 'the-index-pattern-title', - query: getPivotQuery('the-query'), - }; - - const { getByText } = render(<PivotPreview {...props} />); - - // Act - // Assert - expect(getByText('Transform pivot preview')).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx deleted file mode 100644 index c50df0366d698..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx +++ /dev/null @@ -1,345 +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 moment from 'moment-timezone'; -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiDataGridSorting, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiTitle, -} from '@elastic/eui'; - -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfig, - PivotGroupByConfigDict, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; - -import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common'; -import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; - -function sortColumns(groupByArr: PivotGroupByConfig[]) { - return (a: string, b: string) => { - // make sure groupBy fields are always most left columns - if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { - return a.localeCompare(b); - } - if (groupByArr.some(d => d.aggName === a)) { - return -1; - } - if (groupByArr.some(d => d.aggName === b)) { - return 1; - } - return a.localeCompare(b); - }; -} - -interface PreviewTitleProps { - previewRequest: PreviewRequestBody; -} - -const PreviewTitle: FC<PreviewTitleProps> = ({ previewRequest }) => { - const euiCopyText = i18n.translate('xpack.transform.pivotPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', - }); - - return ( - <EuiFlexGroup> - <EuiFlexItem> - <EuiTitle size="xs"> - <span> - {i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { - defaultMessage: 'Transform pivot preview', - })} - </span> - </EuiTitle> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiCopy - beforeMessage={euiCopyText} - textToCopy={getPivotPreviewDevConsoleStatement(previewRequest)} - > - {(copy: () => void) => ( - <EuiButtonIcon onClick={copy} iconType="copyClipboard" aria-label={euiCopyText} /> - )} - </EuiCopy> - </EuiFlexItem> - </EuiFlexGroup> - ); -}; - -interface ErrorMessageProps { - message: string; -} - -const ErrorMessage: FC<ErrorMessageProps> = ({ message }) => ( - <EuiCodeBlock language="json" fontSize="s" paddingSize="s" isCopyable> - {message} - </EuiCodeBlock> -); - -interface PivotPreviewProps { - aggs: PivotAggsConfigDict; - groupBy: PivotGroupByConfigDict; - indexPatternTitle: SearchItems['indexPattern']['title']; - query: PivotQuery; - showHeader?: boolean; -} - -const defaultPagination = { pageIndex: 0, pageSize: 5 }; - -export const PivotPreview: FC<PivotPreviewProps> = React.memo( - ({ aggs, groupBy, indexPatternTitle, query, showHeader = true }) => { - const { - previewData: data, - previewMappings, - errorMessage, - previewRequest, - status, - } = usePivotPreviewData(indexPatternTitle, query, aggs, groupBy); - const groupByArr = dictionaryToArray(groupBy); - - // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - key => previewMappings.properties[key].type !== 'object' - ); - columnKeys.sort(sortColumns(groupByArr)); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState<EsFieldName[]>([]); - - useEffect(() => { - setVisibleColumns(columnKeys.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columnKeys.join()]); - - const [pagination, setPagination] = useState(defaultPagination); - - // Reset pagination if data changes. This is to avoid ending up with an empty table - // when for example the user selected a page that is not available with the updated data. - useEffect(() => { - setPagination(defaultPagination); - }, [data.length]); - - // EuiDataGrid State - const dataGridColumns = columnKeys.map(id => { - const field = previewMappings.properties[id]; - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case ES_FIELD_TYPES.GEO_POINT: - case ES_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case ES_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - case ES_FIELD_TYPES.DATE_NANOS: - schema = 'datetime'; - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - schema = 'numeric'; - break; - // keep schema undefined for text based columns - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - break; - } - - return { id, schema }; - }); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - // Sorting config - const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]); - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - if (sortingColumns.length > 0) { - data.sort(multiColumnSortFactory(sortingColumns)); - } - - const pageData = data.slice( - pagination.pageIndex * pagination.pageSize, - (pagination.pageIndex + 1) * pagination.pageSize - ); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = pageData.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - if ( - [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( - previewMappings.properties[columnId].type - ) - ) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); - - if (status === PIVOT_PREVIEW_STATUS.ERROR) { - return ( - <div data-test-subj="transformPivotPreview error"> - <PreviewTitle previewRequest={previewRequest} /> - <EuiCallOut - title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewError', { - defaultMessage: 'An error occurred loading the pivot preview.', - })} - color="danger" - iconType="cross" - > - <ErrorMessage message={errorMessage} /> - </EuiCallOut> - </div> - ); - } - - if (data.length === 0) { - let noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', - { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - } - ); - - const aggsArr = dictionaryToArray(aggs); - if (aggsArr.length === 0 || groupByArr.length === 0) { - noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', - { - defaultMessage: 'Please choose at least one group-by field and aggregation.', - } - ); - } - - return ( - <div data-test-subj="transformPivotPreview empty"> - <PreviewTitle previewRequest={previewRequest} /> - <EuiCallOut - title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle', { - defaultMessage: 'Pivot preview not available', - })} - color="primary" - > - <p>{noDataMessage}</p> - </EuiCallOut> - </div> - ); - } - - if (columnKeys.length === 0) { - return null; - } - - return ( - <div data-test-subj="transformPivotPreview loaded"> - {showHeader && ( - <> - <PreviewTitle previewRequest={previewRequest} /> - <div className="transform__progress"> - {status === PIVOT_PREVIEW_STATUS.LOADING && <EuiProgress size="xs" color="accent" />} - {status !== PIVOT_PREVIEW_STATUS.LOADING && ( - <EuiProgress size="xs" color="accent" max={1} value={0} /> - )} - </div> - </> - )} - {dataGridColumns.length > 0 && data.length > 0 && ( - <EuiDataGrid - aria-label="Source index preview" - columns={dataGridColumns} - columnVisibility={{ visibleColumns, setVisibleColumns }} - gridStyle={euiDataGridStyle} - rowCount={data.length} - renderCellValue={renderCellValue} - sorting={{ columns: sortingColumns, onSort }} - toolbarVisibility={euiDataGridToolbarSettings} - pagination={{ - ...pagination, - pageSizeOptions: [5, 10, 25], - onChangeItemsPerPage, - onChangePage, - }} - /> - )} - </div> - ); - } -); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx deleted file mode 100644 index 8d09d06b1c731..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx +++ /dev/null @@ -1,69 +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 React, { FC } from 'react'; -import ReactDOM from 'react-dom'; - -import { SimpleQuery } from '../../common'; -import { - PIVOT_PREVIEW_STATUS, - usePivotPreviewData, - UsePivotPreviewDataReturnType, -} from './use_pivot_preview_data'; - -jest.mock('../../hooks/use_api'); - -type Callback = () => void; -interface TestHookProps { - callback: Callback; -} - -const TestHook: FC<TestHookProps> = ({ callback }) => { - callback(); - return null; -}; - -const testHook = (callback: Callback) => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(<TestHook callback={callback} />, container); -}; - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -let pivotPreviewObj: UsePivotPreviewDataReturnType; - -describe('usePivotPreviewData', () => { - test('indexPattern not defined', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - test('indexPattern set triggers loading', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - // ideally this should be LOADING instead of UNUSED but jest/enzyme/hooks doesn't - // trigger that state upate yet. - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest/enzyme's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts deleted file mode 100644 index 83fa7ba189ff0..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts +++ /dev/null @@ -1,91 +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 { useEffect, useState } from 'react'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { useApi } from '../../hooks/use_api'; - -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; - -import { - getPreviewRequestBody, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfigDict, - PivotQuery, - PreviewData, - PreviewMappings, -} from '../../common'; - -export enum PIVOT_PREVIEW_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -export interface UsePivotPreviewDataReturnType { - errorMessage: string; - status: PIVOT_PREVIEW_STATUS; - previewData: PreviewData; - previewMappings: PreviewMappings; - previewRequest: PreviewRequestBody; -} - -export const usePivotPreviewData = ( - indexPatternTitle: IndexPattern['title'], - query: PivotQuery, - aggs: PivotAggsConfigDict, - groupBy: PivotGroupByConfigDict -): UsePivotPreviewDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(PIVOT_PREVIEW_STATUS.UNUSED); - const [previewData, setPreviewData] = useState<PreviewData>([]); - const [previewMappings, setPreviewMappings] = useState<PreviewMappings>({ properties: {} }); - const api = useApi(); - - const aggsArr = dictionaryToArray(aggs); - const groupByArr = dictionaryToArray(groupBy); - - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - - const getPreviewData = async () => { - if (aggsArr.length === 0 || groupByArr.length === 0) { - setPreviewData([]); - return; - } - - setErrorMessage(''); - setStatus(PIVOT_PREVIEW_STATUS.LOADING); - - try { - const resp = await api.getTransformsPreview(previewRequest); - setPreviewData(resp.preview); - setPreviewMappings(resp.generated_dest_index.mappings); - setStatus(PIVOT_PREVIEW_STATUS.LOADED); - } catch (e) { - setErrorMessage(JSON.stringify(e, null, 2)); - setPreviewData([]); - setPreviewMappings({ properties: {} }); - setStatus(PIVOT_PREVIEW_STATUS.ERROR); - } - }; - - useEffect(() => { - getPreviewData(); - // custom comparison - /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - indexPatternTitle, - JSON.stringify(aggsArr), - JSON.stringify(groupByArr), - JSON.stringify(query), - /* eslint-enable react-hooks/exhaustive-deps */ - ]); - - return { errorMessage, status, previewData, previewMappings, previewRequest }; -}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx new file mode 100644 index 0000000000000..4ca536e3c115d --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { render, wait } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import '@testing-library/jest-dom/extend-expect'; + +import { CoreSetup } from 'src/core/public'; + +import { DataGrid, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; + +import { SimpleQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useIndexData } from './use_index_data'; + +jest.mock('../../shared_imports'); +jest.mock('../app_dependencies'); +jest.mock('./use_api'); + +const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, +}; + +describe('Transform: useIndexData()', () => { + test('indexPattern set triggers loading', async done => { + const { result, waitForNextUpdate } = renderHook(() => + useIndexData( + ({ + id: 'the-id', + title: 'the-title', + fields: [], + } as unknown) as SearchItems['indexPattern'], + query + ) + ); + const IndexObj: UseIndexDataReturnType = result.current; + + await waitForNextUpdate(); + + expect(IndexObj.errorMessage).toBe(''); + expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); + expect(IndexObj.tableItems).toEqual([]); + done(); + }); +}); + +describe('Transform: <DataGrid /> with useIndexData()', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { + // Arrange + const indexPattern = { + title: 'the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const Wrapper = () => { + const props = { + ...useIndexData(indexPattern, { match_all: {} }), + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], + }; + + return <DataGrid {...props} />; + }; + const { getByText } = render(<Wrapper />); + + // Act + // Assert + expect(getByText('the-index-preview-title')).toBeInTheDocument(); + await wait(); + done(); + }); +}); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts new file mode 100644 index 0000000000000..ec5a4d244c152 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; + +import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + getErrorMessage, + useDataGrid, + useRenderCellValue, + EsSorting, + SearchResponse7, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +type IndexSearchResponse = SearchResponse7; + +export const useIndexData = ( + indexPattern: SearchItems['indexPattern'], + query: PivotQuery +): UseIndexDataReturnType => { + const api = useApi(); + + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + + // EuiDataGrid State + const columns = [ + ...indexPatternFields.map(id => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { id, schema }; + }), + ]; + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + const getIndexData = async function() { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const esSearchRequest = { + index: indexPattern.title, + body: { + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + query: isDefaultQuery(query) ? matchAllQuery : query, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map(d => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts new file mode 100644 index 0000000000000..ff7ca5d42b5f7 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -0,0 +1,240 @@ +/* + * 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 moment from 'moment-timezone'; +import { useEffect, useMemo, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; + +import { dictionaryToArray } from '../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../common/utils/object_utils'; + +import { + getErrorMessage, + multiColumnSortFactory, + useDataGrid, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { + getPreviewRequestBody, + PivotAggsConfigDict, + PivotGroupByConfigDict, + PivotGroupByConfig, + PivotQuery, + PreviewMappings, +} from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +function sortColumns(groupByArr: PivotGroupByConfig[]) { + return (a: string, b: string) => { + // make sure groupBy fields are always most left columns + if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { + return a.localeCompare(b); + } + if (groupByArr.some(d => d.aggName === a)) { + return -1; + } + if (groupByArr.some(d => d.aggName === b)) { + return 1; + } + return a.localeCompare(b); + }; +} + +export const usePivotData = ( + indexPatternTitle: SearchItems['indexPattern']['title'], + query: PivotQuery, + aggs: PivotAggsConfigDict, + groupBy: PivotGroupByConfigDict +): UseIndexDataReturnType => { + const [previewMappings, setPreviewMappings] = useState<PreviewMappings>({ properties: {} }); + const api = useApi(); + + const aggsArr = dictionaryToArray(aggs); + const groupByArr = dictionaryToArray(groupBy); + + // Filters mapping properties of type `object`, which get returned for nested field parents. + const columnKeys = Object.keys(previewMappings.properties).filter( + key => previewMappings.properties[key].type !== 'object' + ); + columnKeys.sort(sortColumns(groupByArr)); + + // EuiDataGrid State + const columns = columnKeys.map(id => { + const field = previewMappings.properties[id]; + + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case ES_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: + schema = 'datetime'; + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + schema = 'numeric'; + break; + // keep schema undefined for text based columns + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + break; + } + + return { id, schema }; + }); + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setNoDataMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); + + const getPreviewData = async () => { + if (aggsArr.length === 0 || groupByArr.length === 0) { + setTableItems([]); + setRowCount(0); + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', { + defaultMessage: 'Please choose at least one group-by field and aggregation.', + }) + ); + return; + } + + setErrorMessage(''); + setNoDataMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resp = await api.getTransformsPreview(previewRequest); + setTableItems(resp.preview); + setRowCount(resp.preview.length); + setPreviewMappings(resp.generated_dest_index.mappings); + setStatus(INDEX_STATUS.LOADED); + + if (resp.preview.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); + } + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setTableItems([]); + setRowCount(0); + setPreviewMappings({ properties: {} }); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + useEffect(() => { + getPreviewData(); + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + indexPatternTitle, + JSON.stringify(aggsArr), + JSON.stringify(groupByArr), + JSON.stringify(query), + /* eslint-enable react-hooks/exhaustive-deps */ + ]); + + if (sortingColumns.length > 0) { + tableItems.sort(multiColumnSortFactory(sortingColumns)); + } + + const pageData = tableItems.slice( + pagination.pageIndex * pagination.pageSize, + (pagination.pageIndex + 1) * pagination.pageSize + ); + + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = pageData.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if ( + [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( + previewMappings.properties[columnId].type + ) + ) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { + return cellValue ? 'true' : 'false'; + } + + return cellValue; + }; + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap deleted file mode 100644 index b668c7d8e4a69..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: <ExpandedRow /> Test against strings, objects and arrays. 1`] = ` -<EuiText> - <span - key="name" - > - <EuiBadge> - name - : - </EuiBadge> - <small> - - the-name -    - </small> - </span> - <span - key="nested.inner1" - > - <EuiBadge> - nested.inner1 - : - </EuiBadge> - <small> - - the-inner-1 -    - </small> - </span> - <span - key="nested.inner2" - > - <EuiBadge> - nested.inner2 - : - </EuiBadge> - <small> - - the-inner-2 -    - </small> - </span> - <span - key="arrayString" - > - <EuiBadge> - arrayString - : - </EuiBadge> - <small> - - ["the-array-string-1","the-array-string-2"] -    - </small> - </span> - <span - key="arrayObject" - > - <EuiBadge> - arrayObject - : - </EuiBadge> - <small> - - [{"object1":"the-object-1"},{"object2":"the-objects-2"}] -    - </small> - </span> -</EuiText> -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts deleted file mode 100644 index d3bf81bba2e56..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts +++ /dev/null @@ -1,35 +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 { SimpleQuery } from '../../../../common'; - -import { getSourceIndexDevConsoleStatement } from './common'; - -describe('Transform: Source Index Preview Common', () => { - test('getSourceIndexDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const sourceIndexPreviewDevConsoleStatement = getSourceIndexDevConsoleStatement( - query, - 'the-index-pattern-title' - ); - - expect(sourceIndexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search -{ - "query": { - "query_string": { - "query": "*", - "default_operator": "AND" - } - } -} -`); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts deleted file mode 100644 index c34675463bf8b..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts +++ /dev/null @@ -1,17 +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 { PivotQuery } from '../../../../common'; - -export const getSourceIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { - return `GET ${indexPatternTitle}/_search\n${JSON.stringify( - { - query, - }, - null, - 2 - )}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx deleted file mode 100644 index ddd1a1482fd35..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx +++ /dev/null @@ -1,46 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; -import { getFlattenedFields } from '../../../../common'; - -import { ExpandedRow } from './expanded_row'; - -describe('Transform: <ExpandedRow />', () => { - test('Test against strings, objects and arrays.', () => { - const source = { - name: 'the-name', - nested: { - inner1: 'the-inner-1', - inner2: 'the-inner-2', - }, - arrayString: ['the-array-string-1', 'the-array-string-2'], - arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], - } as Record<string, any>; - - const flattenedSource = getFlattenedFields(source).reduce((p, c) => { - p[c] = getNestedProperty(source, c); - if (p[c] === undefined) { - p[c] = source[`"${c}"`]; - } - return p; - }, {} as Record<string, any>); - - const props = { - item: { - _id: 'the-id', - _source: flattenedSource, - }, - }; - - const wrapper = shallow(<ExpandedRow {...props} />); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx deleted file mode 100644 index 9b83a3e5da8a8..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx +++ /dev/null @@ -1,22 +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 React from 'react'; - -import { EuiBadge, EuiText } from '@elastic/eui'; - -import { EsDoc } from '../../../../common'; - -export const ExpandedRow: React.FC<{ item: EsDoc }> = ({ item }) => ( - <EuiText> - {Object.entries(item._source).map(([k, value]) => ( - <span key={k}> - <EuiBadge>{k}:</EuiBadge> - <small> {typeof value === 'string' ? value : JSON.stringify(value)}  </small> - </span> - ))} - </EuiText> -); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts deleted file mode 100644 index a13e678813a00..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { SourceIndexPreview } from './source_index_preview'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx deleted file mode 100644 index 32f6ff9490a0f..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx +++ /dev/null @@ -1,38 +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 React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { getPivotQuery } from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; - -import { SourceIndexPreview } from './source_index_preview'; - -jest.mock('../../../../../shared_imports'); -jest.mock('../../../../../app/app_dependencies'); - -describe('Transform: <SourceIndexPreview />', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const props = { - indexPattern: { - title: 'the-index-pattern-title', - fields: [] as any[], - } as SearchItems['indexPattern'], - query: getPivotQuery('the-query'), - }; - const { getByText } = render(<SourceIndexPreview {...props} />); - - // Act - // Assert - expect(getByText(`Source index ${props.indexPattern.title}`)).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx deleted file mode 100644 index bcdeb7ddb0d36..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ /dev/null @@ -1,293 +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 moment from 'moment-timezone'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; -import { useToastNotifications } from '../../../../app_dependencies'; - -import { getSourceIndexDevConsoleStatement } from './common'; -import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data'; - -interface SourceIndexPreviewTitle { - indexPatternTitle: string; -} -const SourceIndexPreviewTitle: React.FC<SourceIndexPreviewTitle> = ({ indexPatternTitle }) => ( - <EuiTitle size="xs"> - <span> - {i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternTitle', { - defaultMessage: 'Source index {indexPatternTitle}', - values: { indexPatternTitle }, - })} - </span> - </EuiTitle> -); - -interface Props { - indexPattern: SearchItems['indexPattern']; - query: PivotQuery; -} - -export const SourceIndexPreview: React.FC<Props> = React.memo(({ indexPattern, query }) => { - const toastNotifications = useToastNotifications(); - const allFields = indexPattern.fields.map(f => f.name); - const indexPatternFields: string[] = allFields.filter(f => { - if (indexPattern.metaFields.includes(f)) { - return false; - } - - const fieldParts = f.split('.'); - const lastPart = fieldParts.pop(); - if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { - return false; - } - - return true; - }); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState<EsFieldName[]>([]); - - useEffect(() => { - setVisibleColumns(indexPatternFields.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPatternFields.join()]); - - const { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems: data, - } = useSourceIndexData(indexPattern, query); - - // EuiDataGrid State - const dataGridColumns = [ - ...indexPatternFields.map(id => { - const field = indexPattern.fields.getByName(id); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case KBN_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case KBN_FIELD_TYPES.DATE: - schema = 'datetime'; - break; - case KBN_FIELD_TYPES.GEO_POINT: - case KBN_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case KBN_FIELD_TYPES.NUMBER: - schema = 'numeric'; - break; - } - - return { id, schema }; - }), - ]; - - const onSort = useCallback( - (sc: Array<{ id: string; direction: 'asc' | 'desc' }>) => { - // Check if an unsupported column type for sorting was selected. - const invalidSortingColumnns = sc.reduce<string[]>((arr, current) => { - const columnType = dataGridColumns.find(dgc => dgc.id === current.id); - if (columnType?.schema === 'json') { - arr.push(current.id); - } - return arr; - }, []); - if (invalidSortingColumnns.length === 0) { - setSortingColumns(sc); - } else { - invalidSortingColumnns.forEach(columnId => { - toastNotifications.addDanger( - i18n.translate('xpack.transform.sourceIndexPreview.invalidSortingColumnError', { - defaultMessage: `The column '{columnId}' cannot be used for sorting.`, - values: { columnId }, - }) - ); - }); - } - }, - [dataGridColumns, setSortingColumns, toastNotifications] - ); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = data.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(data[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - const field = indexPattern.fields.getByName(columnId); - if (field?.type === KBN_FIELD_TYPES.DATE) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (field?.type === KBN_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [data, indexPattern.fields, pagination.pageIndex, pagination.pageSize]); - - if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) { - return ( - <div data-test-subj="transformSourceIndexPreview empty"> - <SourceIndexPreviewTitle indexPatternTitle={indexPattern.title} /> - <EuiCallOut - title={i18n.translate( - 'xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle', - { - defaultMessage: 'Empty source index query result.', - } - )} - color="primary" - > - <p> - {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', { - defaultMessage: - 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', - })} - </p> - </EuiCallOut> - </div> - ); - } - - const euiCopyText = i18n.translate('xpack.transform.sourceIndexPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', - }); - - return ( - <div - data-test-subj={`transformSourceIndexPreview ${ - status === SOURCE_INDEX_STATUS.ERROR ? 'error' : 'loaded' - }`} - > - <EuiFlexGroup alignItems="center" justifyContent="spaceBetween"> - <EuiFlexItem> - <SourceIndexPreviewTitle indexPatternTitle={indexPattern.title} /> - </EuiFlexItem> - <EuiFlexItem grow={false}> - <EuiCopy - beforeMessage={euiCopyText} - textToCopy={getSourceIndexDevConsoleStatement(query, indexPattern.title)} - > - {(copy: () => void) => ( - <EuiButtonIcon onClick={copy} iconType="copyClipboard" aria-label={euiCopyText} /> - )} - </EuiCopy> - </EuiFlexItem> - </EuiFlexGroup> - <div className="transform__progress"> - {status === SOURCE_INDEX_STATUS.LOADING && <EuiProgress size="xs" color="accent" />} - {status !== SOURCE_INDEX_STATUS.LOADING && ( - <EuiProgress size="xs" color="accent" max={1} value={0} /> - )} - </div> - {status === SOURCE_INDEX_STATUS.ERROR && ( - <div data-test-subj="transformSourceIndexPreview error"> - <EuiCallOut - title={i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternError', { - defaultMessage: 'An error occurred loading the source index data.', - })} - color="danger" - iconType="cross" - > - <EuiCodeBlock language="json" fontSize="s" paddingSize="s" isCopyable> - {errorMessage} - </EuiCodeBlock> - </EuiCallOut> - <EuiSpacer size="m" /> - </div> - )} - <EuiDataGrid - aria-label="Source index preview" - columns={dataGridColumns} - columnVisibility={{ visibleColumns, setVisibleColumns }} - gridStyle={euiDataGridStyle} - rowCount={rowCount} - renderCellValue={renderCellValue} - sorting={{ columns: sortingColumns, onSort }} - toolbarVisibility={euiDataGridToolbarSettings} - pagination={{ - ...pagination, - pageSizeOptions: [5, 10, 25], - onChangeItemsPerPage, - onChangePage, - }} - /> - </div> - ); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx deleted file mode 100644 index 5a1d8a8db5b42..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ /dev/null @@ -1,43 +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 { renderHook } from '@testing-library/react-hooks'; -import '@testing-library/jest-dom/extend-expect'; - -import { SimpleQuery } from '../../../../common'; -import { - SOURCE_INDEX_STATUS, - useSourceIndexData, - UseSourceIndexDataReturnType, -} from './use_source_index_data'; - -jest.mock('../../../../hooks/use_api'); - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -describe('useSourceIndexData', () => { - test('indexPattern set triggers loading', async done => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query) - ); - const sourceIndexObj: UseSourceIndexDataReturnType = result.current; - - await waitForNextUpdate(); - - expect(sourceIndexObj.errorMessage).toBe(''); - expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.LOADING); - expect(sourceIndexObj.tableItems).toEqual([]); - done(); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts deleted file mode 100644 index 5301a3c168a51..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ /dev/null @@ -1,143 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; - -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IIndexPattern } from 'src/plugins/data/public'; - -import { Dictionary } from '../../../../../../common/types/common'; - -import { isDefaultQuery, matchAllQuery, EsDocSource, PivotQuery } from '../../../../common'; -import { useApi } from '../../../../hooks/use_api'; - -export enum SOURCE_INDEX_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -interface ErrorResponse { - request: Dictionary<any>; - response: Dictionary<any>; - body: { - statusCode: number; - error: string; - message: string; - }; - name: string; - req: Dictionary<any>; - res: Dictionary<any>; -} - -const isErrorResponse = (arg: any): arg is ErrorResponse => { - return arg?.body?.error !== undefined && arg?.body?.message !== undefined; -}; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse<any> { - hits: SearchResponse<any>['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -type SourceIndexSearchResponse = SearchResponse7; - -type SourceIndexPagination = Pick<EuiDataGridPaginationProps, 'pageIndex' | 'pageSize'>; -const defaultPagination: SourceIndexPagination = { pageIndex: 0, pageSize: 5 }; - -export interface UseSourceIndexDataReturnType { - errorMessage: string; - pagination: SourceIndexPagination; - setPagination: Dispatch<SetStateAction<SourceIndexPagination>>; - setSortingColumns: Dispatch<SetStateAction<EuiDataGridSorting['columns']>>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: SOURCE_INDEX_STATUS; - tableItems: EsDocSource[]; -} - -export const useSourceIndexData = ( - indexPattern: IIndexPattern, - query: PivotQuery -): UseSourceIndexDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED); - const [pagination, setPagination] = useState(defaultPagination); - const [sortingColumns, setSortingColumns] = useState<EuiDataGridSorting['columns']>([]); - const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState<EsDocSource[]>([]); - const api = useApi(); - - useEffect(() => { - setPagination(defaultPagination); - }, [query]); - - const getSourceIndexData = async function() { - setErrorMessage(''); - setStatus(SOURCE_INDEX_STATUS.LOADING); - - const sort: EsSorting = sortingColumns.reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const esSearchRequest = { - index: indexPattern.title, - body: { - // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - query: isDefaultQuery(query) ? matchAllQuery : query, - from: pagination.pageIndex * pagination.pageSize, - size: pagination.pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }; - - try { - const resp: SourceIndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map(d => d._source); - - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(SOURCE_INDEX_STATUS.LOADED); - } catch (e) { - if (isErrorResponse(e)) { - setErrorMessage(`${e.body.error}: ${e.body.message}`); - } else { - setErrorMessage(JSON.stringify(e, null, 2)); - } - setStatus(SOURCE_INDEX_STATUS.ERROR); - } - }; - - useEffect(() => { - getSourceIndexData(); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); - return { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems, - }; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 320e405b5d437..0e6e2c1a38d0e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,19 +35,19 @@ import { import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { DataGrid } from '../../../../../shared_imports'; + +import { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, +} from '../../../../common/data_grid'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; +import { useIndexData } from '../../../../hooks/use_index_data'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { useToastNotifications } from '../../../../app_dependencies'; -import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; -import { DropDown } from '../aggregation_dropdown'; -import { AggListForm } from '../aggregation_list'; -import { GroupByListForm } from '../group_by_list'; -import { SourceIndexPreview } from '../source_index_preview'; -import { SwitchModal } from './switch_modal'; - import { getPivotQuery, getPreviewRequestBody, @@ -61,11 +61,17 @@ import { PivotGroupByConfig, PivotGroupByConfigDict, PivotSupportedGroupByAggs, + TransformPivotConfig, PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; +import { DropDown } from '../aggregation_dropdown'; +import { AggListForm } from '../aggregation_list'; +import { GroupByListForm } from '../group_by_list'; + import { getPivotDropdownOptions } from './common'; +import { SwitchModal } from './switch_modal'; export interface StepDefineExposedState { aggList: PivotAggsConfigDict; @@ -296,7 +302,6 @@ export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange, return; } } catch (e) { - console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console setErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -593,6 +598,9 @@ export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange, /* eslint-enable react-hooks/exhaustive-deps */ ]); + const indexPreviewProps = useIndexData(indexPattern, pivotQuery); + const pivotPreviewProps = usePivotData(indexPattern.title, pivotQuery, aggList, groupByList); + // TODO This should use the actual value of `indices.query.bool.max_clause_count` const maxIndexFields = 1024; const numIndexFields = indexPattern.fields.length; @@ -973,13 +981,37 @@ export const StepDefineForm: FC<Props> = React.memo(({ overrides = {}, onChange, </EuiFlexItem> <EuiFlexItem grow={false} style={{ maxWidth: 'calc(100% - 468px)' }}> - <SourceIndexPreview indexPattern={searchItems.indexPattern} query={pivotQuery} /> + <DataGrid + {...indexPreviewProps} + copyToClipboard={getIndexDevConsoleStatement(pivotQuery, indexPattern.title)} + copyToClipboardDescription={i18n.translate( + 'xpack.transform.indexPreview.copyClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the index preview to the clipboard.', + } + )} + dataTestSubj="transformIndexPreview" + title={i18n.translate('xpack.transform.indexPreview.indexPatternTitle', { + defaultMessage: 'Index {indexPatternTitle}', + values: { indexPatternTitle: indexPattern.title }, + })} + toastNotifications={toastNotifications} + /> <EuiHorizontalRule /> - <PivotPreview - aggs={aggList} - groupBy={groupByList} - indexPatternTitle={searchItems.indexPattern.title} - query={pivotQuery} + <DataGrid + {...pivotPreviewProps} + copyToClipboard={getPivotPreviewDevConsoleStatement(previewRequest)} + copyToClipboardDescription={i18n.translate( + 'xpack.transform.pivotPreview.copyClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', + } + )} + dataTestSubj="transformPivotPreview" + title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { + defaultMessage: 'Transform pivot preview', + })} + toastNotifications={toastNotifications} /> </EuiFlexItem> </EuiFlexGroup> diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index f31514e67003b..b9021f4ee5b11 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -17,8 +17,19 @@ import { EuiText, } from '@elastic/eui'; -import { getPivotQuery, isDefaultQuery, isMatchAllQuery } from '../../../../common'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { dictionaryToArray } from '../../../../../../common/types/common'; + +import { DataGrid } from '../../../../../shared_imports'; + +import { useToastNotifications } from '../../../../app_dependencies'; +import { + getPivotQuery, + getPivotPreviewDevConsoleStatement, + getPreviewRequestBody, + isDefaultQuery, + isMatchAllQuery, +} from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -35,8 +46,25 @@ export const StepDefineSummary: FC<Props> = ({ formState: { searchString, searchQuery, groupByList, aggList }, searchItems, }) => { + const toastNotifications = useToastNotifications(); + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); + const previewRequest = getPreviewRequestBody( + searchItems.indexPattern.title, + pivotQuery, + pivotGroupByArr, + pivotAggsArr + ); + + const pivotPreviewProps = usePivotData( + searchItems.indexPattern.title, + pivotQuery, + aggList, + groupByList + ); + return ( <EuiFlexGroup> <EuiFlexItem grow={false} style={{ minWidth: '420px' }}> @@ -117,11 +145,20 @@ export const StepDefineSummary: FC<Props> = ({ <EuiFlexItem> <EuiText> - <PivotPreview - aggs={aggList} - groupBy={groupByList} - indexPatternTitle={searchItems.indexPattern.title} - query={pivotQuery} + <DataGrid + {...pivotPreviewProps} + copyToClipboard={getPivotPreviewDevConsoleStatement(previewRequest)} + copyToClipboardDescription={i18n.translate( + 'xpack.transform.pivotPreview.copyClipboardTooltip', + { + defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', + } + )} + dataTestSubj="transformPivotPreview" + title={i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { + defaultMessage: 'Transform pivot preview', + })} + toastNotifications={toastNotifications} /> </EuiText> </EuiFlexItem> diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 9a39616fb0989..23f482b5bc76a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -240,7 +240,6 @@ export const StepDetailsForm: FC<Props> = React.memo( ]} > <EuiFieldText - placeholder="transform ID" value={transformId} onChange={e => setTransformId(e.target.value)} aria-label={i18n.translate( @@ -257,15 +256,12 @@ export const StepDetailsForm: FC<Props> = React.memo( label={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionLabel', { defaultMessage: 'Transform description', })} - helpText={i18n.translate( - 'xpack.transform.stepDetailsForm.transformDescriptionHelpText', - { - defaultMessage: 'Optional descriptive text.', - } - )} > <EuiFieldText - placeholder="transform description" + placeholder={i18n.translate( + 'xpack.transform.stepDetailsForm.transformDescriptionPlaceholderText', + { defaultMessage: 'Description (optional)' } + )} value={transformDescription} onChange={e => setTransformDescription(e.target.value)} aria-label={i18n.translate( @@ -310,7 +306,6 @@ export const StepDetailsForm: FC<Props> = React.memo( } > <EuiFieldText - placeholder="destination index" value={destinationIndex} onChange={e => setDestinationIndex(e.target.value)} aria-label={i18n.translate( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index eaaedc2eb77ce..e183712b390cf 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,37 +6,39 @@ import React, { FC } from 'react'; -import { SearchItems } from '../../../../hooks/use_search_items'; +import { DataGrid } from '../../../../../shared_imports'; +import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, TransformPivotConfig } from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { SearchItems } from '../../../../hooks/use_search_items'; import { applyTransformConfigToDefineState, getDefaultStepDefineState, } from '../../../create_transform/components/step_define/'; -import { PivotPreview } from '../../../../components/pivot_preview'; -interface Props { +interface ExpandedRowPreviewPaneProps { transformConfig: TransformPivotConfig; } -export const ExpandedRowPreviewPane: FC<Props> = ({ transformConfig }) => { - const previewConfig = applyTransformConfigToDefineState( +export const ExpandedRowPreviewPane: FC<ExpandedRowPreviewPaneProps> = ({ transformConfig }) => { + const toastNotifications = useToastNotifications(); + const { aggList, groupByList, searchQuery } = applyTransformConfigToDefineState( getDefaultStepDefineState({} as SearchItems), transformConfig ); - + const pivotQuery = getPivotQuery(searchQuery); const indexPatternTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; + const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); return ( - <PivotPreview - aggs={previewConfig.aggList} - groupBy={previewConfig.groupByList} - indexPatternTitle={indexPatternTitle} - query={getPivotQuery(previewConfig.searchQuery)} - showHeader={false} + <DataGrid + {...pivotPreviewProps} + dataTestSubj="transformPivotPreview" + toastNotifications={toastNotifications} /> ); }; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 494b6db6aafe0..bcd8e53e3d191 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -17,3 +17,18 @@ export { } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { getErrorMessage } from '../../ml/common/util/errors'; + +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4e1217ac9e7b5..d1270ea92c51e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -138,17 +138,6 @@ "charts.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", "charts.controls.vislibBasicOptions.legendPositionLabel": "凡例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", - "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー", - "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。", - "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。", - "common.ui.errorAutoCreateIndex.errorTitle": "おっと!", - "common.ui.errorAutoCreateIndex.howToFixError.goBackText": "ブラウザの戻るボタンで前の画面に戻ります。", - "common.ui.errorAutoCreateIndex.howToFixError.removeConfigText": "Elasticsearch 構成ファイルから {autoCreateIndexActionConfig} を削除します。", - "common.ui.errorAutoCreateIndex.howToFixError.restartText": "Elasticsearch を再起動します。", - "common.ui.errorAutoCreateIndex.howToFixErrorTitle": "どうすれば良いのでしょう?", - "common.ui.errorAutoCreateIndex.noteImageAriaLabel": "情報", - "common.ui.errorAutoCreateIndex.noteMessage": "{autoCreateIndexActionConfig} は、機能を有効にするパターンのホワイトリストを定義することもできます。Kibana と同じ理由でこの機能を使用する他のプラグイン/操作をすべて把握する必要があるため、この設定のこのような使い方はここでは説明しません。", - "common.ui.errorAutoCreateIndex.noteTitle": "注:", "common.ui.errorUrlOverflow.breadcrumbs.errorText": "エラー", "common.ui.errorUrlOverflow.errorDescription": "とても長い URL ですね。残念なお知らせがあります。ご使用のブラウザは Kibana の超巨大 URL に対応していません。問題を避けるため、Kibana はご使用のブラウザでの URL を {urlCharacterLimit} 文字に制限します。", "common.ui.errorUrlOverflow.errorTitle": "おっと!", @@ -548,6 +537,8 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", "dashboard.factory.displayName": "ダッシュボード", "dashboard.panel.removePanel.replacePanel": "パネルの交換", + "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", + "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", "data.common.kql.errors.fieldNameText": "フィールド名", @@ -669,6 +660,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "このリクエストは Elasticsearch にクエリし、ビジュアライゼーション用のデータを取得します。", "data.functions.esaggs.inspector.dataRequest.title": "データ", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", @@ -2002,8 +1994,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "にアクセスして有効な別のドキュメントを選択してください。", "kbn.context.unableToLoadAnchorDocumentDescription": "別のドキュメントが読み込めません", "kbn.context.unableToLoadDocumentDescription": "ドキュメントが読み込めません", - "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", - "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "kbn.dashboardTitle": "ダッシュボード", "kbn.devToolsTitle": "開発ツール", "kbn.discover.backToTopLinkText": "最上部へ戻る。", @@ -2186,10 +2176,8 @@ "kbn.management.createIndexPatternHeader": "{indexPatternName} の作成", "kbn.management.createIndexPatternLabel": "Kibana は、可視化などを目的に Elasticsearch インデックスからデータを取得するために、インデックスパターンを使用します。", "kbn.management.editIndexPattern.deleteButton": "削除", - "kbn.management.editIndexPattern.deleteFieldButton": "削除", "kbn.management.editIndexPattern.deleteHeader": "インデックスパターンを削除しますか?", "kbn.management.editIndexPattern.detailsAria": "インデックスパターンの詳細", - "kbn.management.editIndexPattern.editFieldButton": "編集", "kbn.management.editIndexPattern.fields.allLangsDropDown": "すべての言語", "kbn.management.editIndexPattern.fields.allTypesDropDown": "すべてのフィールドタイプ", "kbn.management.editIndexPattern.fields.filterAria": "フィルター", @@ -2215,7 +2203,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "タイプ", "kbn.management.editIndexPattern.mappingConflictHeader": "マッピングの矛盾", "kbn.management.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {フィールドが} other {# フィールドが}}このパターンと一致するインデックスの間で異なるタイプ (文字列、整数など) に定義されています。これらの矛盾したフィールドは Kibana の一部で使用できますが、Kibana がタイプを把握しなければならない機能には使用できません。この問題を修正するにはデータのレンダリングが必要です。", - "kbn.management.editIndexPattern.notDateErrorMessage": "このフィールドは日付ではなく {fieldType} です。", "kbn.management.editIndexPattern.refreshAria": "フィールドリストを再度読み込みます", "kbn.management.editIndexPattern.refreshButton": "更新", "kbn.management.editIndexPattern.refreshHeader": "フィールドリストを更新しますか?", @@ -2244,7 +2231,6 @@ "kbn.management.editIndexPattern.scripted.table.nameHeader": "名前", "kbn.management.editIndexPattern.scripted.table.scriptDescription": "フィールドのスクリプトです", "kbn.management.editIndexPattern.scripted.table.scriptHeader": "スクリプト", - "kbn.management.editIndexPattern.scripted.unknownModeErrorMessage": "不明なフィールド設定モード {mode}", "kbn.management.editIndexPattern.scriptedHeader": "スクリプトフィールド", "kbn.management.editIndexPattern.scriptedLabel": "ビジュアライゼーションにスクリプトフィールドを使用し、ドキュメントに表示させることができます。但し、スクリプトフィールドは検索できません。", "kbn.management.editIndexPattern.setDefaultAria": "デフォルトのインデックスに設定", @@ -2296,6 +2282,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可視化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", @@ -2417,8 +2405,6 @@ "savedObjectsManagement.breadcrumb.index": "保存されたオブジェクト", "savedObjectsManagement.field.offLabel": "オフ", "savedObjectsManagement.field.onLabel": "オン", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可視化", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "kibana_legacy.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", @@ -2432,7 +2418,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "すべて", "kibana_utils.defaultFeedbackMessage": "フィードバックがありますか?{link} で問題を報告してください。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", - "kibana_utils.indexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", @@ -2594,7 +2579,6 @@ "telemetry.welcomeBanner.enableButtonLabel": "有効にする", "telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遠隔測定に関するプライバシーステートメント", "telemetry.welcomeBanner.title": "Elastic Stack の改善にご協力ください", - "tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子は data-update に対応できるようこのメソドを導入する必要があります", "tileMap.function.help": "タイルマップのビジュアライゼーションです", "tileMap.geohashLayer.mapTitle": "{mapType} マップタイプが認識されません", "tileMap.tooltipFormatter.latitudeLabel": "緯度", @@ -2616,25 +2600,6 @@ "tileMap.visParams.desaturateTilesLabel": "タイルを不飽和化", "tileMap.visParams.mapTypeLabel": "マップタイプ", "tileMap.visParams.reduceVibrancyOfTileColorsTip": "色の鮮明度を下げます。この機能は Internet Explorer ではバージョンにかかわらず利用できません。", - "tileMap.wmsOptions.attributionStringTip": "右下角の属性文字列", - "tileMap.wmsOptions.baseLayerSettingsTitle": "ベースレイヤー設定", - "tileMap.wmsOptions.imageFormatToUseTip": "通常画像/png または画像/jpeg です。サーバーが透明レイヤーを返す場合は png を使用します。", - "tileMap.wmsOptions.layersLabel": "レイヤー", - "tileMap.wmsOptions.listOfLayersToUseTip": "使用するレイヤーのコンマ区切りのリストです。", - "tileMap.wmsOptions.mapLoadFailDescription": "このパラメーターが正しくないと、マップが正常に読み込まれません。", - "tileMap.wmsOptions.urlOfWMSWebServiceTip": "WMS web サービスの URL です。", - "tileMap.wmsOptions.useWMSCompliantMapTileServerTip": "WMS 対応のマップタイルサーバーを使用します。上級者向けです。", - "tileMap.wmsOptions.versionOfWMSserverSupportsTip": "サーバーがサポートしている WMS のバージョンです。", - "tileMap.wmsOptions.wmsAttributionLabel": "WMS 属性", - "tileMap.wmsOptions.wmsDescription": "WMS は、マップイメージサービスの {wmsLink} です。", - "tileMap.wmsOptions.wmsFormatLabel": "WMS フォーマット", - "tileMap.wmsOptions.wmsLayersLabel": "WMS レイヤー", - "tileMap.wmsOptions.wmsLinkText": "OGC スタンダード", - "tileMap.wmsOptions.wmsMapServerLabel": "WMS マップサーバー", - "tileMap.wmsOptions.wmsServerSupportedStylesListTip": "WMS サーバーがサポートしている使用スタイルのコンマ区切りのリストです。大抵は空白のままです。", - "tileMap.wmsOptions.wmsStylesLabel": "WMS スタイル", - "tileMap.wmsOptions.wmsUrlLabel": "WMS URL", - "tileMap.wmsOptions.wmsVersionLabel": "WMS バージョン", "timelion.badge.readOnly.text": "読み込み専用", "timelion.badge.readOnly.tooltip": "Timelion シートを保存できません", "timelion.breadcrumbs.create": "作成", @@ -4033,17 +3998,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "メッセージのロギングエラー", "xpack.actions.builtin.serverLogTitle": "サーバーログ", - "xpack.actions.builtin.servicenow.emptyMapping": "[casesConfiguration.mapping]: 空以外の値が必要ですが空でした", - "xpack.actions.builtin.servicenow.informationAdded": "({date} に {user} が追加)", - "xpack.actions.builtin.servicenow.informationCreated": "({date} に {user} が作成)", - "xpack.actions.builtin.servicenow.informationDefault": "({date} に {user} が作成)", - "xpack.actions.builtin.servicenow.informationUpdated": "({date} に {user} が更新)", - "xpack.actions.builtin.servicenow.postingErrorMessage": "servicenow イベントの送信エラー", - "xpack.actions.builtin.servicenow.postingRetryErrorMessage": "servicenow イベントの送信エラー: http status {status}、後で再試行", - "xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage": "servicenow イベントの送信エラー: 予期しないステータス {status}", - "xpack.actions.builtin.servicenow.servicenowApiNullError": "ServiceNow [apiUrl] が必要です", - "xpack.actions.builtin.servicenow.servicenowApiWhitelistError": "servicenow アクションの構成エラー: {message}", - "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "slack メッセージの投稿エラー", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "slack メッセージの投稿エラー、 {retryString} に再試行", "xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage": "slack メッセージの投稿エラー、後ほど再試行", @@ -4219,7 +4173,6 @@ "xpack.apm.alertTypes.errorRate": "エラー率", "xpack.apm.alertTypes.transactionDuration": "トランザクション期間", "xpack.apm.apmDescription": "アプリケーション内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", - "xpack.apm.apmForESDescription": "Elastic Stack 用の APM", "xpack.apm.applyFilter": "{title} フィルターを適用", "xpack.apm.applyOptions": "オプションを適用", "xpack.apm.breadcrumb.errorsTitle": "エラー", @@ -5022,8 +4975,6 @@ "xpack.canvas.elements.bubbleChartHelpText": "カスタマイズ可能なバブルチャートです", "xpack.canvas.elements.debugDisplayName": "デバッグ", "xpack.canvas.elements.debugHelpText": "エレメントの構成をダンプします", - "xpack.canvas.elements.donutChartDisplayName": "ドーナッツチャート", - "xpack.canvas.elements.donutChartHelpText": "カスタマイズ可能なドーナッツチャートです", "xpack.canvas.elements.dropdownFilterDisplayName": "ドロップダウンフィルター", "xpack.canvas.elements.dropdownFilterHelpText": "「exactly」フィルターの値を選択できるドロップダウンです", "xpack.canvas.elements.horizontalBarChartDisplayName": "水平棒グラフ", @@ -5058,8 +5009,6 @@ "xpack.canvas.elements.shapeHelpText": "カスタマイズ可能な図形です", "xpack.canvas.elements.tableDisplayName": "データテーブル", "xpack.canvas.elements.tableHelpText": "データをチューブ形式で表示する、スクロール可能なグリッドです", - "xpack.canvas.elements.tiltedPieDisplayName": "傾き円グラフ", - "xpack.canvas.elements.tiltedPieHelpText": "カスタマイズ可能な傾き円グラフです", "xpack.canvas.elements.timeFilterDisplayName": "時間フィルター", "xpack.canvas.elements.timeFilterHelpText": "期間を設定します", "xpack.canvas.elements.verticalBarChartDisplayName": "垂直棒グラフ", @@ -5070,16 +5019,16 @@ "xpack.canvas.elements.verticalProgressPillHelpText": "進捗状況を垂直のピルで表示します", "xpack.canvas.elementSettings.dataTabLabel": "データ", "xpack.canvas.elementSettings.displayTabLabel": "表示", - "xpack.canvas.elementTypes.addNewElementDescription": "ワークパッドのエレメントをグループ化して保存し、新規エレメントを作成します", - "xpack.canvas.elementTypes.addNewElementTitle": "新規エレメントの作成", - "xpack.canvas.elementTypes.cancelButtonLabel": "キャンセル", - "xpack.canvas.elementTypes.deleteButtonLabel": "削除", - "xpack.canvas.elementTypes.deleteElementDescription": "このエレメントを削除してよろしいですか?", - "xpack.canvas.elementTypes.deleteElementTitle": "エレメント「{elementName}」を削除しますか?", - "xpack.canvas.elementTypes.editElementTitle": "エレメントを編集", - "xpack.canvas.elementTypes.elementsTitle": "エレメント", - "xpack.canvas.elementTypes.findElementPlaceholder": "エレメントを検索", - "xpack.canvas.elementTypes.myElementsTitle": "マイエレメント", + "xpack.canvas.savedElementsModal.addNewElementDescription": "ワークパッドのエレメントをグループ化して保存し、新規エレメントを作成します", + "xpack.canvas.savedElementsModal.addNewElementTitle": "新規エレメントの作成", + "xpack.canvas.savedElementsModal.cancelButtonLabel": "キャンセル", + "xpack.canvas.savedElementsModal.deleteButtonLabel": "削除", + "xpack.canvas.savedElementsModal.deleteElementDescription": "このエレメントを削除してよろしいですか?", + "xpack.canvas.savedElementsModal.deleteElementTitle": "エレメント「{elementName}」を削除しますか?", + "xpack.canvas.savedElementsModal.editElementTitle": "エレメントを編集", + "xpack.canvas.savedElementsModal.elementsTitle": "エレメント", + "xpack.canvas.savedElementsModal.findElementPlaceholder": "エレメントを検索", + "xpack.canvas.savedElementsModal.myElementsTitle": "マイエレメント", "xpack.canvas.embedObject.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", "xpack.canvas.embedObject.titleText": "オブジェクトの埋め込み", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "無効な引数インデックス: {index}", @@ -5572,33 +5521,12 @@ "xpack.canvas.sidebarContent.groupedElementSidebarTitle": "グループ化されたエレメント", "xpack.canvas.sidebarContent.multiElementSidebarTitle": "複数エレメント", "xpack.canvas.sidebarContent.singleElementSidebarTitle": "選択されたエレメント", - "xpack.canvas.sidebarHeader.alignmentMenuItemLabel": "アラインメント", - "xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel": "一番下", "xpack.canvas.sidebarHeader.bringForwardArialLabel": "エレメントを 1 つ上のレイヤーに移動", "xpack.canvas.sidebarHeader.bringToFrontArialLabel": "エレメントを一番上のレイヤーに移動", - "xpack.canvas.sidebarHeader.centerAlignMenuItemLabel": "中央", - "xpack.canvas.sidebarHeader.contextMenuAriaLabel": "エレメントオプション", - "xpack.canvas.sidebarHeader.createElementModalTitle": "新規エレメントの作成", - "xpack.canvas.sidebarHeader.distributionMenutItemLabel": "分布", - "xpack.canvas.sidebarHeader.groupMenuItemLabel": "グループ", - "xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel": "横", - "xpack.canvas.sidebarHeader.leftAlignMenuItemLabel": "左", - "xpack.canvas.sidebarHeader.middleAlignMenuItemLabel": "真ん中", - "xpack.canvas.sidebarHeader.orderMenuItemLabel": "順序", - "xpack.canvas.sidebarHeader.rightAlignMenuItemLabel": "右", - "xpack.canvas.sidebarHeader.savedElementMenuItemLabel": "新規エレメントとして保存", "xpack.canvas.sidebarHeader.sendBackwardArialLabel": "エレメントを 1 つ下のレイヤーに移動", "xpack.canvas.sidebarHeader.sendToBackArialLabel": "エレメントを一番下のレイヤーに移動", - "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "一番上", - "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "グループ解除", - "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "縦", - "xpack.canvas.tags.chartTag": "チャート", - "xpack.canvas.tags.filterTag": "フィルター", - "xpack.canvas.tags.graphicTag": "グラフィック", "xpack.canvas.tags.presentationTag": "プレゼンテーション", - "xpack.canvas.tags.proportionTag": "比率", "xpack.canvas.tags.reportTag": "レポート", - "xpack.canvas.tags.textTag": "テキスト", "xpack.canvas.templates.darkHelp": "ダークカラーテーマのプレゼンテーションデッキです", "xpack.canvas.templates.darkName": "ダーク", "xpack.canvas.templates.lightHelp": "ライトカラーテーマのプレゼンテーションデッキです", @@ -5898,7 +5826,6 @@ "xpack.canvas.workpadHeader.cycleIntervalHoursText": "{hours} {hours, plural, one {時間} other {時間}}ごと", "xpack.canvas.workpadHeader.cycleIntervalMinutesText": "{minutes} {minutes, plural, one {分} other {分}}ごと", "xpack.canvas.workpadHeader.cycleIntervalSecondsText": "{seconds} {seconds, plural, one {秒} other {秒}}ごと", - "xpack.canvas.workpadHeader.embedObjectButtonLabel": "オブジェクトを埋め込む", "xpack.canvas.workpadHeader.fullscreenButtonAriaLabel": "全画面表示", "xpack.canvas.workpadHeader.fullscreenTooltip": "全画面モードを開始します", "xpack.canvas.workpadHeader.hideEditControlTooltip": "編集コントロールを非表示にします", @@ -5908,41 +5835,52 @@ "xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel": "自動更新間隔を変更します", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText": "手動で", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle": "エレメントを更新", - "xpack.canvas.workpadHeaderControlSettings.settingsTooltip": "設定をコントロールします", "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "設定", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "{secondsExample}、{minutesExample}、{hoursExample} のような短い表記を使用します", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "カスタム間隔を設定", + "xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel": "アラインメント", + "xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel": "一番下", + "xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel": "中央", + "xpack.canvas.workpadHeaderEditMenu.createElementModalTitle": "新規エレメントの作成", + "xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel": "分布", + "xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel": "グループ", + "xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel": "横", + "xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel": "左", + "xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel": "真ん中", + "xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel": "順序", + "xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel": "右", + "xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel": "新規エレメントとして保存", "xpack.canvas.workpadHeaderKioskControl.controlTitle": "全画面ページのサイクル", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "サイクル間隔を変更", "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "スライドを自動的にサイクル", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "エレメントを更新", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "データを更新", - "xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage": "{PDF} 生成 {URL} がクリップボードにコピーされました。", - "xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage": "レポート構成がクリップボードにコピーされました", - "xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage": "「{workpadName}」の {PDF} の作成に失敗しました", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage": "{PDF} をエクスポート中です。管理で進捗を確認できます。", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle": "ワークパッド「{workpadName}」の {PDF} エクスポート", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel": "この {URL} を使用してスクリプトから、または Watcher で {PDF} を生成することもできます。{URL} をクリップボードにコピーするにはエンターキーを押してください。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel": "{POST} {URL} をコピー", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription": "{POST} {URL} をコピーして {KIBANA} 外または ウォッチャー から生成することもできます。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel": "{PDF} を生成", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription": "ワークパッドのサイズによって、{PDF} の生成には数分かかる場合があります。", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle": "{JSON} をダウンロード", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle": "{PDF} レポート", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle": "「{workpadName}」の {ZIP} ファイルの作成に失敗しました。ワークパッドが大きすぎる可能性があります。ファイルを別々にダウンロードする必要があります。", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle": "Web サイトで共有", - "xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage": "このワークパッドを共有", - "xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage": "未知のエクスポートタイプ: {type}", - "xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning": "このワークパッドには {CANVAS} シェアラブルワークパッドランタイムがサポートしていないレンダリング関数が含まれています。これらのエレメントはレンダリングされません:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel": "ズームコントロール", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip": "ズームコントロール", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText": "ウィンドウに合わせる", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomInText": "ズームイン", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText": "ズームアウト", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle": "ズーム:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue": "リセット", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText": "{scalePercentage}%", + "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} がクリップボードにコピーされました。", + "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "レポート構成がクリップボードにコピーされました", + "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", + "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "「{workpadName}」の {PDF} の作成に失敗しました", + "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "{PDF} をエクスポート中です。管理で進捗を確認できます。", + "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "ワークパッド「{workpadName}」の {PDF} エクスポート", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "この {URL} を使用してスクリプトから、または Watcher で {PDF} を生成することもできます。{URL} をクリップボードにコピーするにはエンターキーを押してください。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "{POST} {URL} をコピー", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "{POST} {URL} をコピーして {KIBANA} 外または ウォッチャー から生成することもできます。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "{PDF} を生成", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "ワークパッドのサイズによって、{PDF} の生成には数分かかる場合があります。", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "{JSON} をダウンロード", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} レポート", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle": "「{workpadName}」の {ZIP} ファイルの作成に失敗しました。ワークパッドが大きすぎる可能性があります。ファイルを別々にダウンロードする必要があります。", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle": "Web サイトで共有", + "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "このワークパッドを共有", + "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "未知のエクスポートタイプ: {type}", + "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "このワークパッドには {CANVAS} シェアラブルワークパッドランタイムがサポートしていないレンダリング関数が含まれています。これらのエレメントはレンダリングされません:", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "ズームコントロール", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "ズームコントロール", + "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "ウィンドウに合わせる", + "xpack.canvas.workpadHeaderViewMenu.zoomInText": "ズームイン", + "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "ズームアウト", + "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム:", + "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット", + "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} のコピー", "xpack.canvas.workpadLoader.cloneTooltip": "ワークパッドのクローンを作成します", "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "ワークパッドを作成中...", @@ -6099,9 +6037,6 @@ "xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "一時停止中", "xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "ステータス", "xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "フォロワーインデックスの接尾辞", - "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません", - "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", - "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "キャンセル", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "削除", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "{count} 個の自動フォローパターンを削除しますか?", @@ -6399,7 +6334,6 @@ "xpack.endpoint.host.list.os": "オペレーティングシステム", "xpack.endpoint.host.list.policy": "ポリシー", "xpack.endpoint.host.list.policyStatus": "ポリシーステータス", - "xpack.endpoint.host.list.sensorVersion": "サーバーバージョン", "xpack.endpoint.host.list.totalCount": "表示中: {totalItemCount, plural, one {# ホスト} other {# ホスト}}", "xpack.endpoint.notFound": "ページが見つかりません", "xpack.endpoint.pluginTitle": "エンドポイント", @@ -8180,7 +8114,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "マップビュー", "xpack.infra.viewSwitcher.tableViewLabel": "表ビュー", "xpack.infra.waffle.accountAllTitle": "すべて", - "xpack.infra.waffle.accountLabel": "アカウント: {selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "{field} の平均", "xpack.infra.waffle.aggregationNames.max": "{field} の最大値", "xpack.infra.waffle.aggregationNames.min": "{field} の最小値", @@ -8216,11 +8149,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "カスタムメトリックの変更を保存", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "すべて", - "xpack.infra.waffle.groupByButtonLabel": "グループ分けの条件: ", - "xpack.infra.waffle.inventoryButtonLabel": "ビュー: {selectedText}", "xpack.infra.waffle.loadingDataText": "データを読み込み中", "xpack.infra.waffle.maxGroupByTooltip": "一度に選択できるグループは 2 つのみです", - "xpack.infra.waffle.metricButtonLabel": "メトリック: {selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "カウント", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用状況", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "ディスク読み取り", @@ -8247,7 +8177,6 @@ "xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。", "xpack.infra.waffle.noDataTitle": "表示するデータがありません。", "xpack.infra.waffle.region": "すべて", - "xpack.infra.waffle.regionLabel": "地域: {selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "ビューを保存", "xpack.infra.waffle.savedViews.cancel": "キャンセル", "xpack.infra.waffle.savedViews.cancelButton": "キャンセル", @@ -8280,11 +8209,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": "名前", @@ -8305,7 +8231,6 @@ "xpack.ingestManager.agentDetails.statusLabel": "ステータス", "xpack.ingestManager.agentDetails.typeLabel": "タイプ", "xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "この構成は利用できなくなりました", - "xpack.ingestManager.agentDetails.unenrollButtonText": "登録解除", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "エージェントを読み込む間にエラーが発生しました", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "ユーザー提供メタデータ", "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "ご希望のエージェント構成とプラットフォームをすばやく選択できます。次いで、以下の手順に従ってエージェントをセットアップして登録します。", @@ -8337,8 +8262,6 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "アクション", "xpack.ingestManager.agentList.actionsMenuText": "開く", "xpack.ingestManager.agentList.addButton": "新しいエージェントをインストール", - "xpack.ingestManager.agentList.agentsOnPageSelectedMessage": "このページで {count, plural, one {# エージェント} other {# エージェント}}が選択されます。{selectAllLink}", - "xpack.ingestManager.agentList.allAgentsSelectedMessage": "{count} エージェントすべてが選択されます。{clearSelectionLink}", "xpack.ingestManager.agentList.clearFiltersLinkText": "フィルターを消去", "xpack.ingestManager.agentList.configColumnTitle": "構成", "xpack.ingestManager.agentList.configFilterText": "構成", @@ -8350,15 +8273,12 @@ "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "エージェントが見つかりません。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "最新ではありません", "xpack.ingestManager.agentList.revisionNumber": "rev. {revNumber}", - "xpack.ingestManager.agentList.selectAllAgentsLinkText": "{count} エージェントすべてを選択", - "xpack.ingestManager.agentList.selectPageAgentsLinkText": "このページのみを選択", "xpack.ingestManager.agentList.showInactiveSwitchLabel": "非アクティブエージェントを表示", "xpack.ingestManager.agentList.statusColumnTitle": "ステータス", "xpack.ingestManager.agentList.statusErrorFilterText": "エラー", "xpack.ingestManager.agentList.statusFilterText": "ステータス", "xpack.ingestManager.agentList.statusOfflineFilterText": "オフライン", "xpack.ingestManager.agentList.statusOnlineFilterText": "オンライン", - "xpack.ingestManager.agentList.unenrollButton": "{count, plural, one {# エージェント} other {# エージェント}} の登録を解除", "xpack.ingestManager.agentList.unenrollOneButton": "登録解除", "xpack.ingestManager.agentList.versionTitle": "バージョン", "xpack.ingestManager.agentList.viewActionText": "エージェントの表示", @@ -8366,13 +8286,6 @@ "xpack.ingestManager.agentListStatus.offlineLabel": "オフライン", "xpack.ingestManager.agentListStatus.onlineLabel": "オンライン", "xpack.ingestManager.agentListStatus.totalLabel": "エージェント", - "xpack.ingestManager.apiKeysForm.configLabel": "構成", - "xpack.ingestManager.apiKeysForm.nameLabel": "キー名", - "xpack.ingestManager.apiKeysForm.saveButton": "保存", - "xpack.ingestManager.apiKeysList.apiKeyColumnTitle": "API キー", - "xpack.ingestManager.apiKeysList.configColumnTitle": "構成", - "xpack.ingestManager.apiKeysList.emptyEnrollmentKeysMessage": "API キーがありません", - "xpack.ingestManager.apiKeysList.nameColumnTitle": "名前", "xpack.ingestManager.appNavigation.configurationsLinkText": "構成", "xpack.ingestManager.appNavigation.fleetLinkText": "フリート", "xpack.ingestManager.appNavigation.overviewLinkText": "概要", @@ -8381,7 +8294,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": "データソースを編集", @@ -8389,10 +8301,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 ファイル", "xpack.ingestManager.configDetails.summary.datasources": "データソース", "xpack.ingestManager.configDetails.summary.lastUpdated": "最終更新日:", "xpack.ingestManager.configDetails.summary.revision": "リビジョン", @@ -8402,10 +8310,6 @@ "xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "データソースを作成", "xpack.ingestManager.configDetailsDatasources.createFirstMessage": "この構成にはデータソースはまだありません。", "xpack.ingestManager.configDetailsDatasources.createFirstTitle": "初めてのデーソースを作成する", - "xpack.ingestManager.configForm.descriptionFieldLabel": "説明", - "xpack.ingestManager.configForm.nameFieldLabel": "名前", - "xpack.ingestManager.configForm.nameRequiredErrorMessage": "構成名が必要です", - "xpack.ingestManager.configForm.namespaceFieldLabel": "名前空間", "xpack.ingestManager.createAgentConfig.cancelButtonLabel": "キャンセル", "xpack.ingestManager.createAgentConfig.errorNotificationTitle": "エージェント構成を作成できません", "xpack.ingestManager.createAgentConfig.flyoutTitle": "エージェント構成を作成", @@ -8437,22 +8341,6 @@ "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "パッケージの読み込みエラー", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "選択したパッケージの読み込みエラー", "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "パッケージの検索", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# エージェントを} other {# エージェントを}}{agentConfigsCount, plural, one {このエージェント構成に} other {これらのエージェント構成に}}割り当てました。 {agentsCount, plural, one {このエージェント} other {これらのエージェント}}の登録が解除されます。", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "{agentConfigsCount, plural, one {エージェント構成} other {エージェント構成}} and unenroll {agentsCount, plural, one {エージェント} other {エージェント}} を削除", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel": "{agentConfigsCount, plural, one {エージェント構成} other {エージェント構成}}を削除", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle": "{count, plural, one {this agent config} other {# agent configs}} を削除しますか?", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage": "影響があるエージェントの数を確認中...", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage": "{agentConfigsCount, plural, one {this agent config} other {these agentConfigs}}に割り当てられたエージェントはありません。", - "xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle": "{count} 件のエージェント構成の削除エラー", - "xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle": "エージェント構成「{id}」の削除エラー", - "xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle": "エージェント構成の削除エラー", - "xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "{count} 件のエージェント構成を削除しました", - "xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "エージェント構成「{id}」を削除しました", - "xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.deleteApiKeys.confirmModal.confirmButtonLabel": "削除", - "xpack.ingestManager.deleteApiKeys.confirmModal.title": "API キーを削除: {apiKeyId}", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "{agentConfigName} が一部のエージェントで既に使用されていることをフリートが検出しました。", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "このアクションは {agentsCount} {agentsCount, plural, one {# エージェント} other {# エージェント}}に影響します", "xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "キャンセル", @@ -8468,16 +8356,9 @@ "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "データソース「{id}」を削除しました", "xpack.ingestManager.disabledSecurityDescription": "Elastic Fleet を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", "xpack.ingestManager.disabledSecurityTitle": "セキュリティが有効ではありません", - "xpack.ingestManager.editConfig.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.editConfig.errorNotificationTitle": "エージェント構成を作成できません", - "xpack.ingestManager.editConfig.flyoutTitle": "構成を編集", - "xpack.ingestManager.editConfig.submitButtonLabel": "更新", - "xpack.ingestManager.editConfig.successNotificationTitle": "エージェント構成「{name}」を更新しました", "xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "名前を選択", "xpack.ingestManager.enrollmentApiKeyList.createNewButton": "新規キーを作成", - "xpack.ingestManager.enrollmentApiKeyList.hideTableButton": "を非表示", "xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "既存のキーを使用", - "xpack.ingestManager.enrollmentApiKeyList.viewTableButton": "表示", "xpack.ingestManager.epm.addDatasourceButtonText": "データソースを作成", "xpack.ingestManager.epm.pageSubtitle": "人気のアプリやサービスのパッケージを参照する", "xpack.ingestManager.epm.pageTitle": "Elastic Package Manager", @@ -8506,10 +8387,7 @@ "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "エージェント「{id}」の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle": "{count} 件のエージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle": "エージェント「{id}」の登録解除エラー", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "エージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle": "{count} 件のエージェントの登録を解除しました", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェント「{id}」の登録を解除しました", "xpack.ingestManager.yamlConfig.instructionDescription": "この構成でエージェントを登録するには、ホストで次のコマンドをコピーして実行します。", "xpack.ingestManager.yamlConfig.instructionTittle": "フリートに登録", @@ -8788,8 +8666,6 @@ "xpack.logstash.idFormatErrorMessage": "パイプライン ID は文字またはアンダーラインで始まる必要があり、文字、アンダーライン、ハイフン、数字のみ使用できます", "xpack.logstash.insufficientUserPermissionsDescription": "Logstash パイプラインの管理に必要なユーザーパーミッションがありません", "xpack.logstash.kibanaManagementPipelinesTitle": "Kibana の管理で作成されたパイプラインだけがここに表示されます", - "xpack.logstash.managementSection.createPipelineTitle": "パイプラインの作成", - "xpack.logstash.managementSection.editPipelineTitle": "パイプラインの編集", "xpack.logstash.managementSection.enableSecurityDescription": "Logstash パイプライン管理機能を使用するには、セキュリティを有効にする必要があります。elasticsearch.yml で xpack.security.enabled: true に設定してください。", "xpack.logstash.managementSection.licenseDoesNotSupportDescription": "ご使用の {licenseType} ライセンスは Logstash パイプライン管理をサポートしていません。ライセンスをアップグレードしてください。", "xpack.logstash.managementSection.notPossibleToManagePipelinesMessage": "現在ライセンス情報が利用できないため Logstash パイプラインを使用できません。", @@ -8864,10 +8740,8 @@ "xpack.logstash.workersTooltip": "パイプラインのフィルターとアウトプットステージを同時に実行するワーカーの数です。イベントが詰まってしまう場合や CPU が飽和状態ではない場合は、マシンの処理能力をより有効に活用するため、この数字を上げてみてください。\n\nデフォルト値:ホストの CPU コア数です", "xpack.maps.addLayerPanel.addLayer": "レイヤーを追加", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "データソースを変更", - "xpack.maps.addLayerPanel.chooseDataSourceTitle": "データソースの選択", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル", "xpack.maps.addLayerPanel.importFile": "ファイルのインポート", - "xpack.maps.addLayerPanel.selectSource": "ソースを選択", "xpack.maps.aggs.defaultCountLabel": "カウント", "xpack.maps.appDescription": "マップアプリケーション", "xpack.maps.appTitle": "マップ", @@ -9474,16 +9348,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分類混同行列", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "予測されたラベル", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "マルチクラス混同行列には、分析が実際のクラスで正しくデータポイントを分類した発生数と、別のクラスで誤分類した発生数が含まれます。", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分類ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent": "配列", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent": "この配列ベースの列の完全なコンテンツは表示できません。", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "結果が見つかりませんでした。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回帰評価ドキュメント ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "アクションを表示", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "すべての列を表示", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分類ジョブID {jobId}のデスティネーションインデックス", @@ -9558,38 +9424,19 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "データフレーム分析 {jobId} の開始リクエストが受け付けられました。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "トレーニングパーセンテージ", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "機能影響スコア", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "外れ値検出結果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "実験的", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analytics.exploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "外れ値検出ジョブID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", "xpack.ml.dataframe.analytics.exploration.title": "分析の探索", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "予測があるドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回帰ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "{docFieldsCount, number} 件中 showing {selectedFieldsLength, number} 件の{docFieldsCount, plural, one {フィールド} other {フィールド}}", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "テスト", - "xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "トレーニング", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "回帰分析モデルの実行の効果を測定します。真値と予測値の間の差異の二乗平均合計。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "インデックスのクエリが結果を返しませんでした。デスティネーションインデックスが存在し、ドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "クエリ構文が無効であり、結果を返しませんでした。クエリ構文を確認し、再試行してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "クエリをパースできません。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R の二乗", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "適合度を表します。モデルによる観察された結果の複製の効果を測定します。", - "xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder": "例: 平均>0.5", - "xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel": "列を選択", - "xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle": "フィールドを選択", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回帰ジョブID {jobId}のデスティネーションインデックス", "xpack.ml.dataframe.analytics.regressionExploration.trainingDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "トレーニングエラー", @@ -10611,10 +10458,9 @@ "xpack.ml.overview.feedbackSectionTitle": "フィードバック", "xpack.ml.overview.gettingStartedSectionCreateJob": "新規ジョブを作成中", "xpack.ml.overview.gettingStartedSectionDocs": "ドキュメンテーション", - "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}や{createJob}をご参照ください。Elastic Stackの機械学習の詳細については、{whatIsMachineLearning}をご覧ください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。", + "xpack.ml.overview.gettingStartedSectionText": "機械学習へようこそ。はじめに{docs}や{createJob}をご参照ください。{transforms}を使用して、分析ジョブの機能インデックスを作成することをお勧めします。", "xpack.ml.overview.gettingStartedSectionTitle": "はじめて使う", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearchの変換", - "xpack.ml.overview.gettingStartedSectionWhatIsMachineLearning": "こちら", "xpack.ml.overview.overviewLabel": "概要", "xpack.ml.overview.statsBar.failedAnalyticsLabel": "失敗", "xpack.ml.overview.statsBar.runningAnalyticsLabel": "実行中", @@ -10932,7 +10778,6 @@ "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "デフォルト", "xpack.monitoring.alerts.licenseExpiration.newSubject": "NEW X-Pack 監視:ライセンス期限", "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "RESOLVED X-Pack 監視:ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは、#relative で #absolute に期限が切れます。", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "このクラスターのライセンスはアクティブです。", "xpack.monitoring.alerts.lowSeverityName": "低", "xpack.monitoring.alerts.mediumSeverityName": "中", @@ -12060,7 +11905,6 @@ "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesDescription": "過去 5 分間の平均負荷です。", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesLabel": "5m", "xpack.monitoring.monitoringDescription": "Elastic Stack のリアルタイムのヘルスとパフォーマンスをトラッキングします。", - "xpack.monitoring.monitoringTitle": "Monitoring", "xpack.monitoring.noData.blurbs.changesNeededDescription": "監視を実行するには、次の手順に従います", "xpack.monitoring.noData.blurbs.changesNeededTitle": "調整が必要です", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "次の場所に戻ってください: ", @@ -12140,7 +11984,6 @@ "xpack.monitoring.summaryStatus.statusDescription": "ステータス", "xpack.monitoring.summaryStatus.statusIconLabel": "ステータス: {status}", "xpack.monitoring.summaryStatus.statusIconTitle": "ステータス: {statusIcon}", - "xpack.monitoring.uiExportsDescription": "Elastic Stack の監視です", "xpack.painlessLab.apiReferenceButtonLabel": "API リファレンス", "xpack.painlessLab.context.defaultLabel": "スクリプト結果は文字列に変換されます", "xpack.painlessLab.context.filterLabel": "フィルターのスクリプトクエリのコンテキストを使用する", @@ -12428,7 +12271,6 @@ "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG レポート", - "xpack.rollupJobs.appName": "ロールアップジョブ", "xpack.rollupJobs.appTitle": "ロールアップジョブ", "xpack.rollupJobs.breadcrumbsTitle": "ロールアップジョブ", "xpack.rollupJobs.create.backButton.label": "戻る", @@ -12728,7 +12570,6 @@ "xpack.security.loginPage.esUnavailableTitle": "Elasticsearch クラスターに接続できません", "xpack.security.loginPage.loginProviderDescription": "{providerType}/{providerName} でログイン", "xpack.security.loginPage.loginSelectorErrorMessage": "ログインを実行できませんでした。", - "xpack.security.loginPage.loginSelectorOR": "OR", "xpack.security.loginPage.noLoginMethodsAvailableMessage": "システム管理者にお問い合わせください。", "xpack.security.loginPage.noLoginMethodsAvailableTitle": "ログインが無効です。", "xpack.security.loginPage.requiresSecureConnectionMessage": "システム管理者にお問い合わせください。", @@ -13411,14 +13252,7 @@ "xpack.siem.case.confirmDeleteCase.deleteTitle": "「{caseTitle}」を削除", "xpack.siem.case.confirmDeleteCase.selectedCases": "選択したケースを削除", "xpack.siem.case.connectors.servicenow.actionTypeTitle": "ServiceNow", - "xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel": "URL", - "xpack.siem.case.connectors.servicenow.invalidApiUrlTextField": "URL が無効です", - "xpack.siem.case.connectors.servicenow.passwordTextFieldLabel": "パスワード", - "xpack.siem.case.connectors.servicenow.requiredApiUrlTextField": "URL が必要です", - "xpack.siem.case.connectors.servicenow.requiredPasswordTextField": "パスワードが必要です", - "xpack.siem.case.connectors.servicenow.requiredUsernameTextField": "ユーザー名が必要です", "xpack.siem.case.connectors.servicenow.selectMessageText": "ServiceNow で SIEM ケースデータをb\\更新するか、または新しいインシデントにプッシュする", - "xpack.siem.case.connectors.servicenow.usernameTextFieldLabel": "ユーザー名", "xpack.siem.case.createCase.descriptionFieldRequiredError": "説明が必要です。", "xpack.siem.case.createCase.fieldTagsHelpText": "このケースの 1 つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.siem.case.createCase.titleFieldRequiredError": "タイトルが必要です。", @@ -14211,7 +14045,6 @@ "xpack.siem.kpiNetwork.uniquePrivateIps.sourceUnitLabel": "ソース", "xpack.siem.kpiNetwork.uniquePrivateIps.title": "固有のプライベート IP", "xpack.siem.licensing.unsupportedMachineLearningMessage": "ご使用のライセンスは機械翻訳をサポートしていません。ライセンスをアップグレードしてください。", - "xpack.siem.linkSecurityDescription": "SIEM アプリを閲覧します", "xpack.siem.markdown.hint.boldLabel": "**太字**", "xpack.siem.markdown.hint.bulletLabel": "* ビュレット", "xpack.siem.markdown.hint.codeLabel": "「コード」", @@ -14456,7 +14289,6 @@ "xpack.siem.recentTimelines.pinnedEventsTooltip": "ピン付けされたイベント", "xpack.siem.recentTimelines.untitledTimelineLabel": "無題のタイムライン", "xpack.siem.recentTimelines.viewAllTimelinesLink": "すべてのタイムラインを表示", - "xpack.siem.securityDescription": "SIEM アプリを閲覧します", "xpack.siem.source.destination.packetsLabel": "パケット", "xpack.siem.system.acceptedAConnectionViaDescription": "次の手段で接続を受け付けました:", "xpack.siem.system.acceptedDescription": "以下を経由してユーザーを受け入れました:", @@ -15606,19 +15438,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "インデックスパターン", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "保存検索", "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.pivotPreview.PivotPreviewError": "ピボットプレビューの読み込み中にエラーが発生しました。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "ピボットプレビューを利用できません", "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", "xpack.transform.progress": "進捗", "xpack.transform.sourceIndex": "ソースインデックス", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "ソースインデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列「{columnId}」は並べ替えに使用できません。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "ソースインデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "ソースインデックスクエリの結果がありません", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "ソースインデックスデータの読み込み中にエラーが発生しました。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "ソースインデックス {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "一斉", "xpack.transform.statsBar.continuousTransformsLabel": "連続", "xpack.transform.statsBar.failedTransformsLabel": "失敗", @@ -15707,7 +15531,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "オプションの説明テキストです。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "変換の説明", "xpack.transform.stepDetailsForm.transformIdExistsError": "この ID の変換が既に存在します。", @@ -16015,7 +15838,6 @@ "xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "アクションタイプを読み込み中...", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知間隔", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "アラートがアクティブな間にアクションを繰り返す頻度を定義します。", - "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "アクション:アクションタイプを選択してください", "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "トリガータイプを選択してください", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", @@ -16026,7 +15848,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": "タグ", @@ -16233,7 +16054,6 @@ "xpack.uptime.emptyStateError.notAuthorized": "アップタイムデータの表示が承認されていません。システム管理者にお問い合わせください。", "xpack.uptime.emptyStateError.notFoundPage": "ページが見つかりません", "xpack.uptime.emptyStateError.title": "エラー", - "xpack.uptime.featureCatalogueDescription": "エンドポイントヘルスチェックとアップタイム監視を行います。", "xpack.uptime.featureRegistry.uptimeFeatureName": "アップタイム", "xpack.uptime.filterBar.ariaLabel": "概要ページのインプットフィルター基準", "xpack.uptime.filterBar.filterDownLabel": "ダウン", @@ -16748,4 +16568,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bfdcc8b865313..32c91a6ef2931 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -138,17 +138,6 @@ "charts.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", "charts.controls.vislibBasicOptions.legendPositionLabel": "图例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", - "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误", - "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。", - "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。", - "common.ui.errorAutoCreateIndex.errorTitle": "糟糕!", - "common.ui.errorAutoCreateIndex.howToFixError.goBackText": "使用浏览器的后退按钮返回您之前正做的工作。", - "common.ui.errorAutoCreateIndex.howToFixError.removeConfigText": "从 Elasticsearch 配置文件中删除 {autoCreateIndexActionConfig}", - "common.ui.errorAutoCreateIndex.howToFixError.restartText": "重新启动 Elasticsearch。", - "common.ui.errorAutoCreateIndex.howToFixErrorTitle": "那么,我如何解决此问题?", - "common.ui.errorAutoCreateIndex.noteImageAriaLabel": "信息", - "common.ui.errorAutoCreateIndex.noteMessage": "{autoCreateIndexActionConfig} 还可以定义应启用此功能的模式白名单。我们在这里不讨论如何以那种方式使用该设置,因为这和 Kibana 一样需要您了解依赖该功能的所有其他插件/交互。", - "common.ui.errorAutoCreateIndex.noteTitle": "注意:", "common.ui.errorUrlOverflow.breadcrumbs.errorText": "错误", "common.ui.errorUrlOverflow.errorDescription": "您的 URL 真不小。我有一些不幸的消息:您的浏览器与 Kibana 的超长 URL 不太兼容。为了避免您遇到问题,Kibana 在您的浏览器中将 URL 长度限制在 {urlCharacterLimit} 个字符。", "common.ui.errorUrlOverflow.errorTitle": "喔哦!", @@ -548,6 +537,8 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", "dashboard.factory.displayName": "仪表板", "dashboard.panel.removePanel.replacePanel": "替换面板", + "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", + "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", "data.common.kql.errors.fieldNameText": "字段名称", @@ -669,6 +660,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "此请求将查询 Elasticsearch 以获取用于可视化的数据。", "data.functions.esaggs.inspector.dataRequest.title": "数据", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", @@ -2003,8 +1995,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "以选择有效地定位点文档。", "kbn.context.unableToLoadAnchorDocumentDescription": "无法加载该定位点文档", "kbn.context.unableToLoadDocumentDescription": "无法加载文档", - "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", - "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "kbn.dashboardTitle": "仪表板", "kbn.devToolsTitle": "开发工具", "kbn.discover.backToTopLinkText": "返至顶部。", @@ -2187,10 +2177,8 @@ "kbn.management.createIndexPatternHeader": "创建 {indexPatternName}", "kbn.management.createIndexPatternLabel": "Kibana 使用索引模式从 Elasticsearch 索引中检索数据,以实现诸如可视化等功能。", "kbn.management.editIndexPattern.deleteButton": "删除", - "kbn.management.editIndexPattern.deleteFieldButton": "删除", "kbn.management.editIndexPattern.deleteHeader": "删除索引模式?", "kbn.management.editIndexPattern.detailsAria": "索引模式详细信息", - "kbn.management.editIndexPattern.editFieldButton": "编辑", "kbn.management.editIndexPattern.fields.allLangsDropDown": "所有语言", "kbn.management.editIndexPattern.fields.allTypesDropDown": "所有字段类型", "kbn.management.editIndexPattern.fields.filterAria": "筛选", @@ -2216,7 +2204,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "类型", "kbn.management.editIndexPattern.mappingConflictHeader": "映射冲突", "kbn.management.editIndexPattern.mappingConflictLabel": "匹配此模式的各个索引中{conflictFieldsLength, plural, one {一个字段已} other {# 个字段已}}定义为若干类型(字符串、整数等)。您仍能够在 Kibana 的各个部分中使用这些冲突类型,但它们将无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。", - "kbn.management.editIndexPattern.notDateErrorMessage": "该字段是{fieldType},不是日期。", "kbn.management.editIndexPattern.refreshAria": "重新加载字段列表", "kbn.management.editIndexPattern.refreshButton": "刷新", "kbn.management.editIndexPattern.refreshHeader": "刷新字段列表?", @@ -2245,7 +2232,6 @@ "kbn.management.editIndexPattern.scripted.table.nameHeader": "名称", "kbn.management.editIndexPattern.scripted.table.scriptDescription": "字段的脚本", "kbn.management.editIndexPattern.scripted.table.scriptHeader": "脚本", - "kbn.management.editIndexPattern.scripted.unknownModeErrorMessage": "未知 fieldSettings 模式 {mode}", "kbn.management.editIndexPattern.scriptedHeader": "脚本字段", "kbn.management.editIndexPattern.scriptedLabel": "可以在可视化中使用脚本字段,并在您的文档中显示它们。但是,您不能搜索脚本字段。", "kbn.management.editIndexPattern.setDefaultAria": "设置为默认索引", @@ -2297,6 +2283,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "应用的完整列表位于左侧菜单中。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可视化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", @@ -2418,8 +2406,6 @@ "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", "savedObjectsManagement.breadcrumb.edit": "编辑 {savedObjectType}", "savedObjectsManagement.breadcrumb.index": "已保存对象", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可视化", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "kibana_legacy.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", @@ -2433,7 +2419,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "全部", "kibana_utils.defaultFeedbackMessage": "想反馈?请在 {link} 中创建问题。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "已保存对象缺失", - "kibana_utils.indexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完全还原 URL,请确保使用共享功能。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,另外,似乎没有任何可安全删除的项目。\n\n通常,这可以通过移到全新的选项卡来解决,但这种情况可能是由更大的问题造成。如果您定期看到这个消息,请在 {gitHubIssuesUrl} 报告问题。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", @@ -2595,7 +2580,6 @@ "telemetry.welcomeBanner.enableButtonLabel": "启用", "telemetry.welcomeBanner.telemetryConfigDetailsDescription.telemetryPrivacyStatementLinkText": "遥测隐私声明", "telemetry.welcomeBanner.title": "帮助我们改进 Elastic Stack", - "tileMap.baseMapsVisualization.childShouldImplementMethodErrorMessage": "子函数应实现此方法以响应数据更新", "tileMap.function.help": "磁贴地图可视化", "tileMap.geohashLayer.mapTitle": "{mapType} 地图类型无法识别", "tileMap.tooltipFormatter.latitudeLabel": "纬度", @@ -2617,25 +2601,6 @@ "tileMap.visParams.desaturateTilesLabel": "降低平铺地图饱和度", "tileMap.visParams.mapTypeLabel": "地图类型", "tileMap.visParams.reduceVibrancyOfTileColorsTip": "降低平铺地图颜色的亮度。此设置在任何版本的 IE 浏览器中均不起作用。", - "tileMap.wmsOptions.attributionStringTip": "右下角的属性字符串。", - "tileMap.wmsOptions.baseLayerSettingsTitle": "基础图层设置", - "tileMap.wmsOptions.imageFormatToUseTip": "通常为 image/png 或 image/jpeg。如果服务器返回透明图层,则使用 png。", - "tileMap.wmsOptions.layersLabel": "图层", - "tileMap.wmsOptions.listOfLayersToUseTip": "要使用的图层逗号分隔列表。", - "tileMap.wmsOptions.mapLoadFailDescription": "如果此参数不正确,将无法加载地图。", - "tileMap.wmsOptions.urlOfWMSWebServiceTip": "WMS Web 服务的 URL。", - "tileMap.wmsOptions.useWMSCompliantMapTileServerTip": "使用符合 WMS 规范的平铺地图服务器。仅适用于高级用户。", - "tileMap.wmsOptions.versionOfWMSserverSupportsTip": "服务器支持的 WMS 版本。", - "tileMap.wmsOptions.wmsAttributionLabel": "WMS 属性", - "tileMap.wmsOptions.wmsDescription": "WMS 是用于地图图像服务的 {wmsLink}。", - "tileMap.wmsOptions.wmsFormatLabel": "WMS 格式", - "tileMap.wmsOptions.wmsLayersLabel": "WMS 图层", - "tileMap.wmsOptions.wmsLinkText": "OGC 标准", - "tileMap.wmsOptions.wmsMapServerLabel": "WMS 地图服务器", - "tileMap.wmsOptions.wmsServerSupportedStylesListTip": "要使用的以逗号分隔的 WMS 服务器支持的样式列表。在大部分情况下为空。", - "tileMap.wmsOptions.wmsStylesLabel": "WMS 样式", - "tileMap.wmsOptions.wmsUrlLabel": "WMS url", - "tileMap.wmsOptions.wmsVersionLabel": "WMS 版本", "timelion.badge.readOnly.text": "只读", "timelion.badge.readOnly.tooltip": "无法保存 Timelion 工作表", "timelion.breadcrumbs.create": "创建", @@ -4034,17 +3999,6 @@ "xpack.actions.builtin.pagerdutyTitle": "PagerDuty", "xpack.actions.builtin.serverLog.errorLoggingErrorMessage": "记录消息时出错", "xpack.actions.builtin.serverLogTitle": "服务器日志", - "xpack.actions.builtin.servicenow.emptyMapping": "[casesConfiguration.mapping]:应为非空,但却为空", - "xpack.actions.builtin.servicenow.informationAdded": "(由 {user} 于 {date}添加)", - "xpack.actions.builtin.servicenow.informationCreated": "(由 {user} 于 {date}创建)", - "xpack.actions.builtin.servicenow.informationDefault": "(由 {user} 于 {date}创建)", - "xpack.actions.builtin.servicenow.informationUpdated": "(由 {user} 于 {date}更新)", - "xpack.actions.builtin.servicenow.postingErrorMessage": "发布 servicenow 事件时出错", - "xpack.actions.builtin.servicenow.postingRetryErrorMessage": "发布 servicenow 事件时出错:http 状态 {status},请稍后重试", - "xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage": "发布 servicenow 事件时出错:非预期状态 {status}", - "xpack.actions.builtin.servicenow.servicenowApiNullError": "必须指定 ServiceNow [apiUrl]", - "xpack.actions.builtin.servicenow.servicenowApiWhitelistError": "配置 servicenow 操作时出错:{message}", - "xpack.actions.builtin.servicenowTitle": "ServiceNow", "xpack.actions.builtin.slack.errorPostingErrorMessage": "发布 slack 消息时出错", "xpack.actions.builtin.slack.errorPostingRetryDateErrorMessage": "发布 slack 消息时出错,在 {retryString} 重试", "xpack.actions.builtin.slack.errorPostingRetryLaterErrorMessage": "发布 slack 消息时出错,稍后重试", @@ -4220,7 +4174,6 @@ "xpack.apm.alertTypes.errorRate": "错误率", "xpack.apm.alertTypes.transactionDuration": "事务持续时间", "xpack.apm.apmDescription": "自动从您的应用程序内收集深入全面的性能指标和错误。", - "xpack.apm.apmForESDescription": "Elastic Stack 的 APM", "xpack.apm.applyFilter": "应用 {title} 筛选", "xpack.apm.applyOptions": "应用选项", "xpack.apm.breadcrumb.errorsTitle": "错误", @@ -5023,8 +4976,6 @@ "xpack.canvas.elements.bubbleChartHelpText": "可定制的气泡图", "xpack.canvas.elements.debugDisplayName": "“Debug”(故障排查)", "xpack.canvas.elements.debugHelpText": "只需丢弃元素的配置", - "xpack.canvas.elements.donutChartDisplayName": "圆环图", - "xpack.canvas.elements.donutChartHelpText": "可定制的圆环图", "xpack.canvas.elements.dropdownFilterDisplayName": "下拉列表筛选", "xpack.canvas.elements.dropdownFilterHelpText": "可以从其中为“完全”筛选选择值的下拉列表", "xpack.canvas.elements.horizontalBarChartDisplayName": "水平条形图", @@ -5059,8 +5010,6 @@ "xpack.canvas.elements.shapeHelpText": "可定制的形状", "xpack.canvas.elements.tableDisplayName": "数据表", "xpack.canvas.elements.tableHelpText": "用于以表格形式显示数据的可滚动网格", - "xpack.canvas.elements.tiltedPieDisplayName": "斜饼图", - "xpack.canvas.elements.tiltedPieHelpText": "可定制的斜饼图", "xpack.canvas.elements.timeFilterDisplayName": "时间筛选", "xpack.canvas.elements.timeFilterHelpText": "设置时间窗口", "xpack.canvas.elements.verticalBarChartDisplayName": "垂直条形图", @@ -5071,16 +5020,16 @@ "xpack.canvas.elements.verticalProgressPillHelpText": "将进度显示为垂直胶囊的一部分", "xpack.canvas.elementSettings.dataTabLabel": "数据", "xpack.canvas.elementSettings.displayTabLabel": "显示", - "xpack.canvas.elementTypes.addNewElementDescription": "分组并保存 Workpad 元素以创建新元素", - "xpack.canvas.elementTypes.addNewElementTitle": "添加新元素", - "xpack.canvas.elementTypes.cancelButtonLabel": "取消", - "xpack.canvas.elementTypes.deleteButtonLabel": "删除", - "xpack.canvas.elementTypes.deleteElementDescription": "确定要删除此元素?", - "xpack.canvas.elementTypes.deleteElementTitle": "删除元素“{elementName}”?", - "xpack.canvas.elementTypes.editElementTitle": "编辑元素", - "xpack.canvas.elementTypes.elementsTitle": "元素", - "xpack.canvas.elementTypes.findElementPlaceholder": "查找元素", - "xpack.canvas.elementTypes.myElementsTitle": "我的元素", + "xpack.canvas.savedElementsModal.addNewElementDescription": "分组并保存 Workpad 元素以创建新元素", + "xpack.canvas.savedElementsModal.addNewElementTitle": "添加新元素", + "xpack.canvas.savedElementsModal.cancelButtonLabel": "取消", + "xpack.canvas.savedElementsModal.deleteButtonLabel": "删除", + "xpack.canvas.savedElementsModal.deleteElementDescription": "确定要删除此元素?", + "xpack.canvas.savedElementsModal.deleteElementTitle": "删除元素“{elementName}”?", + "xpack.canvas.savedElementsModal.editElementTitle": "编辑元素", + "xpack.canvas.savedElementsModal.elementsTitle": "元素", + "xpack.canvas.savedElementsModal.findElementPlaceholder": "查找元素", + "xpack.canvas.savedElementsModal.myElementsTitle": "我的元素", "xpack.canvas.embedObject.noMatchingObjectsMessage": "未找到任何匹配对象。", "xpack.canvas.embedObject.titleText": "嵌入对象", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "无效的参数索引:{index}", @@ -5573,33 +5522,12 @@ "xpack.canvas.sidebarContent.groupedElementSidebarTitle": "已分组元素", "xpack.canvas.sidebarContent.multiElementSidebarTitle": "多个元素", "xpack.canvas.sidebarContent.singleElementSidebarTitle": "选定元素", - "xpack.canvas.sidebarHeader.alignmentMenuItemLabel": "对齐方式", - "xpack.canvas.sidebarHeader.bottomAlignMenuItemLabel": "下", "xpack.canvas.sidebarHeader.bringForwardArialLabel": "将元素上移一层", "xpack.canvas.sidebarHeader.bringToFrontArialLabel": "将元素移到顶层", - "xpack.canvas.sidebarHeader.centerAlignMenuItemLabel": "中", - "xpack.canvas.sidebarHeader.contextMenuAriaLabel": "元素选项", - "xpack.canvas.sidebarHeader.createElementModalTitle": "创建新元素", - "xpack.canvas.sidebarHeader.distributionMenutItemLabel": "分布", - "xpack.canvas.sidebarHeader.groupMenuItemLabel": "分组", - "xpack.canvas.sidebarHeader.horizontalDistributionMenutItemLabel": "水平", - "xpack.canvas.sidebarHeader.leftAlignMenuItemLabel": "左", - "xpack.canvas.sidebarHeader.middleAlignMenuItemLabel": "中", - "xpack.canvas.sidebarHeader.orderMenuItemLabel": "顺序", - "xpack.canvas.sidebarHeader.rightAlignMenuItemLabel": "右", - "xpack.canvas.sidebarHeader.savedElementMenuItemLabel": "另存为新元素", "xpack.canvas.sidebarHeader.sendBackwardArialLabel": "将元素下移一层", "xpack.canvas.sidebarHeader.sendToBackArialLabel": "将元素移到底层", - "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "上", - "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "取消分组", - "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "垂直", - "xpack.canvas.tags.chartTag": "图表", - "xpack.canvas.tags.filterTag": "筛选", - "xpack.canvas.tags.graphicTag": "图形", "xpack.canvas.tags.presentationTag": "演示", - "xpack.canvas.tags.proportionTag": "比例", "xpack.canvas.tags.reportTag": "报告", - "xpack.canvas.tags.textTag": "文本", "xpack.canvas.templates.darkHelp": "深色主题的演示幻灯片", "xpack.canvas.templates.darkName": "深色", "xpack.canvas.templates.lightHelp": "浅色主题的演示幻灯片", @@ -5900,7 +5828,6 @@ "xpack.canvas.workpadHeader.cycleIntervalHoursText": "每 {hours} {hours, plural, one {小时} other {小时}}", "xpack.canvas.workpadHeader.cycleIntervalMinutesText": "每 {minutes} {minutes, plural, one {分钟} other {分钟}}", "xpack.canvas.workpadHeader.cycleIntervalSecondsText": "每 {seconds} {seconds, plural, one {秒} other {秒}}", - "xpack.canvas.workpadHeader.embedObjectButtonLabel": "嵌入对象", "xpack.canvas.workpadHeader.fullscreenButtonAriaLabel": "全屏查看", "xpack.canvas.workpadHeader.fullscreenTooltip": "进入全屏模式", "xpack.canvas.workpadHeader.hideEditControlTooltip": "隐藏编辑控件", @@ -5910,41 +5837,55 @@ "xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel": "更改自动刷新时间间隔", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText": "手动", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle": "刷新元素", - "xpack.canvas.workpadHeaderControlSettings.settingsTooltip": "控制设置", "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "设置", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "使用速记表示法,如 {secondsExample}、{minutesExample} 或 {hoursExample}", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "设置定制时间间隔", + "xpack.canvas.workpadHeaderEditMenu.alignmentMenuItemLabel": "对齐方式", + "xpack.canvas.workpadHeaderEditMenu.bottomAlignMenuItemLabel": "下", + "xpack.canvas.workpadHeaderEditMenu.centerAlignMenuItemLabel": "中", + "xpack.canvas.workpadHeaderEditMenu.createElementModalTitle": "创建新元素", + "xpack.canvas.workpadHeaderEditMenu.distributionMenutItemLabel": "分布", + "xpack.canvas.workpadHeaderEditMenu.groupMenuItemLabel": "分组", + "xpack.canvas.workpadHeaderEditMenu.horizontalDistributionMenutItemLabel": "水平", + "xpack.canvas.workpadHeaderEditMenu.leftAlignMenuItemLabel": "左", + "xpack.canvas.workpadHeaderEditMenu.middleAlignMenuItemLabel": "中", + "xpack.canvas.workpadHeaderEditMenu.orderMenuItemLabel": "顺序", + "xpack.canvas.workpadHeaderEditMenu.rightAlignMenuItemLabel": "右", + "xpack.canvas.workpadHeaderEditMenu.savedElementMenuItemLabel": "另存为新元素", + "xpack.canvas.workpadHeaderEditMenu.topAlignMenuItemLabel": "上", + "xpack.canvas.workpadHeaderEditMenu.ungroupMenuItemLabel": "取消分组", + "xpack.canvas.workpadHeaderEditMenu.verticalDistributionMenutItemLabel": "垂直", "xpack.canvas.workpadHeaderKioskControl.controlTitle": "循环播放全屏页面", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "更改循环播放时间间隔", "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "自动循环播放幻灯片", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "刷新元素", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "刷新数据", - "xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage": "{PDF} 生成 {URL} 已复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage": "已将报告配置复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage": "已将共享标记复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage": "无法为“{workpadName}”创建 {PDF}", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage": "正在导出 {PDF}。可以在“管理”中跟踪进度。", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle": "Workpad“{workpadName}”的 {PDF} 导出", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel": "或者,也可以从脚本或使用 {URL} 通过 Watcher 生成 {PDF}。按 Enter 键可将 {URL} 复制到剪贴板。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel": "复制 {POST} {URL}", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription": "或者,复制此 {POST} {URL} 以从 {KIBANA} 外部或从 Watcher 调用生成。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel": "生成 {PDF}", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription": "{PDF} 可能会花费 1 或 2 分钟生成,取决于 Workpad 的大小。", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle": "下载为 {JSON}", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle": "{PDF} 报告", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle": "无法为“{workpadName}”创建 {ZIP} 文件。Workpad 可能过大。您将需要分别下载文件。", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle": "在网站上共享", - "xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage": "共享此 Workpad", - "xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage": "未知导出类型:{type}", - "xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning": "此 Workpad 包含 {CANVAS} Shareable Workpad Runtime 不支持的呈现函数。将不会呈现以下元素:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel": "缩放控制", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip": "缩放控制", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText": "适应窗口大小", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomInText": "放大", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText": "缩小", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle": "缩放", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue": "重置", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText": "{scalePercentage}%", + "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} 已复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "已将报告配置复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "已将共享标记复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "无法为“{workpadName}”创建 {PDF}", + "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "正在导出 {PDF}。可以在“管理”中跟踪进度。", + "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "Workpad“{workpadName}”的 {PDF} 导出", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "或者,也可以从脚本或使用 {URL} 通过 Watcher 生成 {PDF}。按 Enter 键可将 {URL} 复制到剪贴板。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "复制 {POST} {URL}", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "或者,复制此 {POST} {URL} 以从 {KIBANA} 外部或从 Watcher 调用生成。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "生成 {PDF}", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "{PDF} 可能会花费 1 或 2 分钟生成,取决于 Workpad 的大小。", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "下载为 {JSON}", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} 报告", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle": "无法为“{workpadName}”创建 {ZIP} 文件。Workpad 可能过大。您将需要分别下载文件。", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle": "在网站上共享", + "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "共享此 Workpad", + "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "未知导出类型:{type}", + "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "此 Workpad 包含 {CANVAS} Shareable Workpad Runtime 不支持的呈现函数。将不会呈现以下元素:", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "缩放控制", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "缩放控制", + "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "适应窗口大小", + "xpack.canvas.workpadHeaderViewMenu.zoomInText": "放大", + "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "缩小", + "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放", + "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置", + "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} 的副本", "xpack.canvas.workpadLoader.cloneTooltip": "克隆 Workpad", "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "正在创建 Workpad......", @@ -6101,9 +6042,6 @@ "xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "已暂停", "xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "状态", "xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "Follower 索引后缀", - "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期", - "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", - "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "取消", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "删除", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "是否删除 {count} 个自动跟随模式?", @@ -6401,7 +6339,6 @@ "xpack.endpoint.host.list.os": "操作系统", "xpack.endpoint.host.list.policy": "政策", "xpack.endpoint.host.list.policyStatus": "策略状态", - "xpack.endpoint.host.list.sensorVersion": "感应器版本", "xpack.endpoint.host.list.totalCount": "正在显示:{totalItemCount, plural, one {# 个主机} other {# 个主机}}", "xpack.endpoint.notFound": "未找到页面", "xpack.endpoint.pluginTitle": "终端", @@ -8183,7 +8120,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "地图视图", "xpack.infra.viewSwitcher.tableViewLabel": "表视图", "xpack.infra.waffle.accountAllTitle": "全部", - "xpack.infra.waffle.accountLabel": "帐户:{selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "“{field}”的平均值", "xpack.infra.waffle.aggregationNames.max": "“{field}”的最大值", "xpack.infra.waffle.aggregationNames.min": "“{field}”的最小值", @@ -8219,11 +8155,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "保存定制指标的更改", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "全部", - "xpack.infra.waffle.groupByButtonLabel": "分组依据: ", - "xpack.infra.waffle.inventoryButtonLabel": "视图:{selectedText}", "xpack.infra.waffle.loadingDataText": "正在加载数据", "xpack.infra.waffle.maxGroupByTooltip": "一次只能选择两个分组", - "xpack.infra.waffle.metricButtonLabel": "指标:{selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "计数", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取", @@ -8250,7 +8183,6 @@ "xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。", "xpack.infra.waffle.noDataTitle": "没有可显示的数据。", "xpack.infra.waffle.region": "全部", - "xpack.infra.waffle.regionLabel": "地区:{selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "保存视图", "xpack.infra.waffle.savedViews.cancel": "取消", "xpack.infra.waffle.savedViews.cancelButton": "取消", @@ -8283,11 +8215,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": "名称", @@ -8308,7 +8237,6 @@ "xpack.ingestManager.agentDetails.statusLabel": "状态", "xpack.ingestManager.agentDetails.typeLabel": "类型", "xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "此配置不再可用", - "xpack.ingestManager.agentDetails.unenrollButtonText": "取消注册", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "加载代理时发生错误", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "用户提供的元数据", "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "快速选择所需的代理配置和平台。然后,根据下面的说明设置和注册代理。", @@ -8340,8 +8268,6 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "操作", "xpack.ingestManager.agentList.actionsMenuText": "打开", "xpack.ingestManager.agentList.addButton": "安装新代理", - "xpack.ingestManager.agentList.agentsOnPageSelectedMessage": "已选择此页面上的 {count, plural, one {# 个代理} other {# 个代理}}。{selectAllLink}", - "xpack.ingestManager.agentList.allAgentsSelectedMessage": "已选择所有 {count} 个代理。{clearSelectionLink}", "xpack.ingestManager.agentList.clearFiltersLinkText": "清除筛选", "xpack.ingestManager.agentList.configColumnTitle": "配置", "xpack.ingestManager.agentList.configFilterText": "配置", @@ -8353,15 +8279,12 @@ "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "未找到任何代理。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "过时", "xpack.ingestManager.agentList.revisionNumber": "修订 {revNumber}", - "xpack.ingestManager.agentList.selectAllAgentsLinkText": "选择所有 {count} 个代理", - "xpack.ingestManager.agentList.selectPageAgentsLinkText": "仅选择此页面", "xpack.ingestManager.agentList.showInactiveSwitchLabel": "显示非活动代理", "xpack.ingestManager.agentList.statusColumnTitle": "状态", "xpack.ingestManager.agentList.statusErrorFilterText": "错误", "xpack.ingestManager.agentList.statusFilterText": "状态", "xpack.ingestManager.agentList.statusOfflineFilterText": "脱机", "xpack.ingestManager.agentList.statusOnlineFilterText": "联机", - "xpack.ingestManager.agentList.unenrollButton": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}", "xpack.ingestManager.agentList.unenrollOneButton": "取消注册", "xpack.ingestManager.agentList.versionTitle": "版本", "xpack.ingestManager.agentList.viewActionText": "查看代理", @@ -8369,13 +8292,6 @@ "xpack.ingestManager.agentListStatus.offlineLabel": "脱机", "xpack.ingestManager.agentListStatus.onlineLabel": "联机", "xpack.ingestManager.agentListStatus.totalLabel": "代理", - "xpack.ingestManager.apiKeysForm.configLabel": "配置", - "xpack.ingestManager.apiKeysForm.nameLabel": "密钥名称", - "xpack.ingestManager.apiKeysForm.saveButton": "保存", - "xpack.ingestManager.apiKeysList.apiKeyColumnTitle": "API 密钥", - "xpack.ingestManager.apiKeysList.configColumnTitle": "配置", - "xpack.ingestManager.apiKeysList.emptyEnrollmentKeysMessage": "无 API 密钥", - "xpack.ingestManager.apiKeysList.nameColumnTitle": "名称", "xpack.ingestManager.appNavigation.configurationsLinkText": "配置", "xpack.ingestManager.appNavigation.fleetLinkText": "Fleet", "xpack.ingestManager.appNavigation.overviewLinkText": "概览", @@ -8384,7 +8300,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": "编辑数据源", @@ -8392,10 +8307,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 文件", "xpack.ingestManager.configDetails.summary.datasources": "数据源", "xpack.ingestManager.configDetails.summary.lastUpdated": "最后更新时间", "xpack.ingestManager.configDetails.summary.revision": "修订", @@ -8405,10 +8316,6 @@ "xpack.ingestManager.configDetailsDatasources.createFirstButtonText": "创建数据源", "xpack.ingestManager.configDetailsDatasources.createFirstMessage": "此配置尚未有任何数据源。", "xpack.ingestManager.configDetailsDatasources.createFirstTitle": "创建您的首个数据源", - "xpack.ingestManager.configForm.descriptionFieldLabel": "描述", - "xpack.ingestManager.configForm.nameFieldLabel": "名称", - "xpack.ingestManager.configForm.nameRequiredErrorMessage": "配置名称必填", - "xpack.ingestManager.configForm.namespaceFieldLabel": "命名空间", "xpack.ingestManager.createAgentConfig.cancelButtonLabel": "取消", "xpack.ingestManager.createAgentConfig.errorNotificationTitle": "无法创建代理配置", "xpack.ingestManager.createAgentConfig.flyoutTitle": "创建代理配置", @@ -8440,22 +8347,6 @@ "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingPackagesTitle": "加载软件包时出错", "xpack.ingestManager.createDatasource.stepSelectPackage.errorLoadingSelectedPackageTitle": "加载选定软件包时出错", "xpack.ingestManager.createDatasource.stepSelectPackage.filterPackagesInputPlaceholder": "搜索软件包", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.affectedAgentsMessage": "{agentsCount, plural, one {# 个代理} other {# 个代理}}已分配{agentConfigsCount, plural, one {给此代理配置} other {给这些代理配置}}。将取消注册{agentsCount, plural, one {此代理} other {这些代理}}。", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmAndReassignButtonLabel": "删除{agentConfigsCount, plural, one {代理配置} other {代理配置}}并取消注册{agentsCount, plural, one {代理} other {代理}}", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.confirmButtonLabel": "删除{agentConfigsCount, plural, one {代理配置} other {代理配置}}", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.deleteMultipleTitle": "删除{count, plural, one {此代理配置} other {# 个代理配置}}?", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingAgentsCountMessage": "正在检查受影响代理数量……", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.deleteAgentConfigs.confirmModal.noAffectedAgentsMessage": "没有代理分配给{agentConfigsCount, plural, one {此代理配置} other {这些代理配置}}。", - "xpack.ingestManager.deleteAgentConfigs.failureMultipleNotificationTitle": "删除 {count} 个代理配置时出错", - "xpack.ingestManager.deleteAgentConfigs.failureSingleNotificationTitle": "删除代理配置“{id}”时出错", - "xpack.ingestManager.deleteAgentConfigs.fatalErrorNotificationTitle": "删除代理配置时出错", - "xpack.ingestManager.deleteAgentConfigs.successMultipleNotificationTitle": "已删除 {count} 个代理配置", - "xpack.ingestManager.deleteAgentConfigs.successSingleNotificationTitle": "已删除代理配置“{id}”", - "xpack.ingestManager.deleteApiKeys.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.deleteApiKeys.confirmModal.confirmButtonLabel": "删除", - "xpack.ingestManager.deleteApiKeys.confirmModal.title": "删除 api 密钥:{apiKeyId}", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsMessage": "Fleet 已检测到 {agentConfigName} 已由您的部分代理使用。", "xpack.ingestManager.deleteDatasource.confirmModal.affectedAgentsTitle": "此操作将影响 {agentsCount} 个 {agentsCount, plural, one {代理} other {代理}}。", "xpack.ingestManager.deleteDatasource.confirmModal.cancelButtonLabel": "取消", @@ -8471,16 +8362,9 @@ "xpack.ingestManager.deleteDatasource.successSingleNotificationTitle": "已删除数据源“{id}”", "xpack.ingestManager.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Elastic Fleet。", "xpack.ingestManager.disabledSecurityTitle": "安全性未启用", - "xpack.ingestManager.editConfig.cancelButtonLabel": "取消", - "xpack.ingestManager.editConfig.errorNotificationTitle": "无法更新代理配置", - "xpack.ingestManager.editConfig.flyoutTitle": "编辑配置", - "xpack.ingestManager.editConfig.submitButtonLabel": "更新", - "xpack.ingestManager.editConfig.successNotificationTitle": "代理配置“{name}”已更新", "xpack.ingestManager.enrollmentApiKeyForm.namePlaceholder": "选择名称", "xpack.ingestManager.enrollmentApiKeyList.createNewButton": "创建新密钥", - "xpack.ingestManager.enrollmentApiKeyList.hideTableButton": "隐藏", "xpack.ingestManager.enrollmentApiKeyList.useExistingsButton": "使用现有密钥", - "xpack.ingestManager.enrollmentApiKeyList.viewTableButton": "查看", "xpack.ingestManager.epm.addDatasourceButtonText": "创建数据源", "xpack.ingestManager.epm.pageSubtitle": "浏览热门应用和服务的软件。", "xpack.ingestManager.epm.pageTitle": "Elastic Package Manager", @@ -8509,10 +8393,7 @@ "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "取消注册“{id}”?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle": "取消注册 {count} 个代理时出错", - "xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle": "取消注册代理“{id}”时出错", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册代理时出错", - "xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle": "已取消注册 {count} 个代理", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "已取消注册代理“{id}”", "xpack.ingestManager.yamlConfig.instructionDescription": "要将代理注册到此配置,请在您的主机上复制并运行以下命令。", "xpack.ingestManager.yamlConfig.instructionTittle": "注册到 fleet", @@ -8791,8 +8672,6 @@ "xpack.logstash.idFormatErrorMessage": "管道 ID 必须以字母或下划线开头,并只能包含字母、下划线、短划线和数字", "xpack.logstash.insufficientUserPermissionsDescription": "管理 Logstash 管道的用户权限不足", "xpack.logstash.kibanaManagementPipelinesTitle": "仅在 Kibana“管理”中创建的管道显示在此处", - "xpack.logstash.managementSection.createPipelineTitle": "创建管道", - "xpack.logstash.managementSection.editPipelineTitle": "编辑管道", "xpack.logstash.managementSection.enableSecurityDescription": "必须启用 Security,才能使用 Logstash 管道管理功能。请在 elasticsearch.yml 中设置 xpack.security.enabled: true。", "xpack.logstash.managementSection.licenseDoesNotSupportDescription": "您的{licenseType}许可不支持 Logstash 管道管理功能。请升级您的许可。", "xpack.logstash.managementSection.notPossibleToManagePipelinesMessage": "您不能管理 Logstash 管道,因为许可信息当前不可用。", @@ -8867,10 +8746,8 @@ "xpack.logstash.workersTooltip": "并行执行管道的筛选和输出阶段的工作线程数目。如果您发现事件出现积压或 CPU 未饱和,请考虑增大此数值,以更好地利用机器处理能力。\n\n默认值:主机的 CPU 核心数", "xpack.maps.addLayerPanel.addLayer": "添加图层", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改数据源", - "xpack.maps.addLayerPanel.chooseDataSourceTitle": "选择数据源", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "鍙栨秷", "xpack.maps.addLayerPanel.importFile": "导入文件", - "xpack.maps.addLayerPanel.selectSource": "选择源", "xpack.maps.aggs.defaultCountLabel": "计数", "xpack.maps.appDescription": "地图应用程序", "xpack.maps.appTitle": "Maps", @@ -9477,16 +9354,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分类混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "预测标签", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "多类混淆矩阵包含分析使用数据点的实际类正确分类数据点的次数以及分析使用其他类错误分类这些数据点的次数", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分类作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent": "数组", - "xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent": "此基于数组的列的完整内容无法显示。", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "未找到结果。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回归评估文档 ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "显示操作", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "显示所有列", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分类作业 ID {jobId} 的目标索引", @@ -9561,38 +9430,19 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "数据帧分析 {jobId} 启动请求已确认。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "功能影响分数", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "离群值检测结果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "实验性", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analytics.exploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "离群值检测作业 ID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "该索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回归作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "已选择 {docFieldsCount, number} 个{docFieldsCount, plural, one {字段} other {字段}}中的 {selectedFieldsLength, number} 个", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", - "xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel": "测试", - "xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel": "培训", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "度量回归分析模型的表现。真实值与预测值之差的平均平方和。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "该索引的查询未返回结果。请确保目标索引存在且包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "查询语法无效,未返回任何结果。请检查查询语法并重试。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "无法解析查询。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R 平方", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "表示拟合优度。度量模型复制被观察结果的优良性。", - "xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder": "例如 avg>0.5", - "xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel": "选择列", - "xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle": "选择字段", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回归作业 ID {jobId} 的目标索引", "xpack.ml.dataframe.analytics.regressionExploration.trainingDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.trainingErrorTitle": "训练误差", @@ -10614,10 +10464,9 @@ "xpack.ml.overview.feedbackSectionTitle": "反馈", "xpack.ml.overview.gettingStartedSectionCreateJob": "创建新作业", "xpack.ml.overview.gettingStartedSectionDocs": "文档", - "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先阅读我们的{docs}或{createJob}。有关 Elastic Stack 中的机器学习的详情,请参阅{whatIsMachineLearning}。建议使用 {transforms}为分析作业创建功能索引。", + "xpack.ml.overview.gettingStartedSectionText": "欢迎使用 Machine Learning。首先阅读我们的{docs}或{createJob}。建议使用 {transforms}为分析作业创建功能索引。", "xpack.ml.overview.gettingStartedSectionTitle": "入门", "xpack.ml.overview.gettingStartedSectionTransforms": "Elasticsearch 的转换", - "xpack.ml.overview.gettingStartedSectionWhatIsMachineLearning": "此处", "xpack.ml.overview.overviewLabel": "概览", "xpack.ml.overview.statsBar.failedAnalyticsLabel": "失败", "xpack.ml.overview.statsBar.runningAnalyticsLabel": "正在运行", @@ -10936,7 +10785,6 @@ "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "默认值", "xpack.monitoring.alerts.licenseExpiration.newSubject": "新 X-Pack Monitoring:许可证到期", "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "已解决 X-Pack Monitoring:许可证到期", - "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将在 #relative 后,即 #absolute到期", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "此集群的许可证处于活动状态。", "xpack.monitoring.alerts.lowSeverityName": "低", "xpack.monitoring.alerts.mediumSeverityName": "中", @@ -12064,7 +11912,6 @@ "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesDescription": "过去 5 分钟的负载平均值。", "xpack.monitoring.metrics.logstashInstance.systemLoad.last5MinutesLabel": "5 分钟", "xpack.monitoring.monitoringDescription": "跟踪 Elastic Stack 的实时运行状况和性能。", - "xpack.monitoring.monitoringTitle": "Monitoring", "xpack.monitoring.noData.blurbs.changesNeededDescription": "要运行 Monitoring,请执行以下步骤", "xpack.monitoring.noData.blurbs.changesNeededTitle": "您需要做些调整", "xpack.monitoring.noData.blurbs.cloudDeploymentDescription": "请返回到您的 ", @@ -12144,7 +11991,6 @@ "xpack.monitoring.summaryStatus.statusDescription": "状态", "xpack.monitoring.summaryStatus.statusIconLabel": "状态:{status}", "xpack.monitoring.summaryStatus.statusIconTitle": "状态:{statusIcon}", - "xpack.monitoring.uiExportsDescription": "Elastic Stack 的 Monitoring 组件", "xpack.painlessLab.apiReferenceButtonLabel": "API 参考", "xpack.painlessLab.context.defaultLabel": "脚本结果将转换成字符串", "xpack.painlessLab.context.filterLabel": "使用筛选脚本查询的上下文", @@ -12432,7 +12278,6 @@ "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG 报告", - "xpack.rollupJobs.appName": "汇总/打包作业", "xpack.rollupJobs.appTitle": "汇总/打包作业", "xpack.rollupJobs.breadcrumbsTitle": "汇总/打包作业", "xpack.rollupJobs.create.backButton.label": "上一步", @@ -12732,7 +12577,6 @@ "xpack.security.loginPage.esUnavailableTitle": "无法连接到 Elasticsearch 集群", "xpack.security.loginPage.loginProviderDescription": "使用 {providerType}/{providerName} 登录", "xpack.security.loginPage.loginSelectorErrorMessage": "无法执行登录。", - "xpack.security.loginPage.loginSelectorOR": "或", "xpack.security.loginPage.noLoginMethodsAvailableMessage": "请联系您的管理员。", "xpack.security.loginPage.noLoginMethodsAvailableTitle": "登录已禁用。", "xpack.security.loginPage.requiresSecureConnectionMessage": "请联系您的管理员。", @@ -13415,14 +13259,7 @@ "xpack.siem.case.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”", "xpack.siem.case.confirmDeleteCase.selectedCases": "删除选定案例", "xpack.siem.case.connectors.servicenow.actionTypeTitle": "ServiceNow", - "xpack.siem.case.connectors.servicenow.apiUrlTextFieldLabel": "URL", - "xpack.siem.case.connectors.servicenow.invalidApiUrlTextField": "URL 无效", - "xpack.siem.case.connectors.servicenow.passwordTextFieldLabel": "密码", - "xpack.siem.case.connectors.servicenow.requiredApiUrlTextField": "“URL”必填", - "xpack.siem.case.connectors.servicenow.requiredPasswordTextField": "“密码”必填", - "xpack.siem.case.connectors.servicenow.requiredUsernameTextField": "“用户名”必填", "xpack.siem.case.connectors.servicenow.selectMessageText": "将 SIEM 案例数据推送或更新到 ServiceNow 中的新事件", - "xpack.siem.case.connectors.servicenow.usernameTextFieldLabel": "用户名", "xpack.siem.case.createCase.descriptionFieldRequiredError": "描述必填。", "xpack.siem.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。", "xpack.siem.case.createCase.titleFieldRequiredError": "标题必填。", @@ -14215,7 +14052,6 @@ "xpack.siem.kpiNetwork.uniquePrivateIps.sourceUnitLabel": "源", "xpack.siem.kpiNetwork.uniquePrivateIps.title": "唯一专用 IP", "xpack.siem.licensing.unsupportedMachineLearningMessage": "您的许可证不支持 Machine Learning。请升级您的许可证。", - "xpack.siem.linkSecurityDescription": "浏览您的 SIEM 应用", "xpack.siem.markdown.hint.boldLabel": "**粗体**", "xpack.siem.markdown.hint.bulletLabel": "* 项目符号", "xpack.siem.markdown.hint.codeLabel": "`code`", @@ -14460,7 +14296,6 @@ "xpack.siem.recentTimelines.pinnedEventsTooltip": "置顶事件", "xpack.siem.recentTimelines.untitledTimelineLabel": "未命名时间线", "xpack.siem.recentTimelines.viewAllTimelinesLink": "查看所有时间线", - "xpack.siem.securityDescription": "浏览您的 SIEM 应用", "xpack.siem.source.destination.packetsLabel": "pkts", "xpack.siem.system.acceptedAConnectionViaDescription": "已接受连接,通过", "xpack.siem.system.acceptedDescription": "已接受该用户 - 通过", @@ -15610,19 +15445,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "索引模式", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "已保存搜索", "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", - "xpack.transform.pivotPreview.PivotPreviewError": "加载数据透视表预览时出错。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "数据透视表预览不可用", "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", "xpack.transform.progress": "进度", "xpack.transform.sourceIndex": "源索引", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "将源索引预览的开发控制台语句复制到剪贴板。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列“{columnId}”无法用于排序。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "源索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "源索引查询结果为空。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "加载源索引数据时出错。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "源索引 {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "批量", "xpack.transform.statsBar.continuousTransformsLabel": "连续", "xpack.transform.statsBar.failedTransformsLabel": "失败", @@ -15711,7 +15538,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "(可选)描述性文本。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "转换描述", "xpack.transform.stepDetailsForm.transformIdExistsError": "已存在具有此 ID 的转换。", @@ -16020,7 +15846,6 @@ "xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "正在加载操作类型……", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知频率", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义告警处于活动状态时重复操作的频率。", - "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "操作:选择操作类型", "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "选择触发器类型", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", @@ -16031,7 +15856,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": "标记", @@ -16238,7 +16062,6 @@ "xpack.uptime.emptyStateError.notAuthorized": "您无权查看 Uptime 数据,请联系系统管理员。", "xpack.uptime.emptyStateError.notFoundPage": "未找到页面", "xpack.uptime.emptyStateError.title": "错误", - "xpack.uptime.featureCatalogueDescription": "执行终端节点运行状况检查和运行时间监测。", "xpack.uptime.featureRegistry.uptimeFeatureName": "运行时间", "xpack.uptime.filterBar.ariaLabel": "概览页面的输入筛选条件", "xpack.uptime.filterBar.filterDownLabel": "关闭", @@ -16753,4 +16576,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx index 658a0e869548f..fdfaf70648694 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx @@ -4,12 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { FunctionComponent } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from 'react-dom/test-utils'; import { TypeRegistry } from '../../type_registry'; import { registerBuiltInActionTypes } from './index'; import { ActionTypeModel, ActionParamsProps } from '../../../types'; import { IndexActionParams, EsIndexActionConnector } from './types'; import { coreMock } from '../../../../../../../src/core/public/mocks'; +jest.mock('../../../common/index_controls', () => ({ + firstFieldOption: jest.fn(), + getFields: jest.fn(), + getIndexOptions: jest.fn(), + getIndexPatterns: jest.fn(), +})); const ACTION_TYPE_ID = '.index'; let actionTypeModel: ActionTypeModel; @@ -91,13 +98,40 @@ describe('action params validation', () => { }); describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { + test('all connector fields is rendered', async () => { const mocks = coreMock.createSetup(); expect(actionTypeModel.actionConnectorFields).not.toBeNull(); if (!actionTypeModel.actionConnectorFields) { return; } + + const { getIndexPatterns } = jest.requireMock('../../../common/index_controls'); + getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, + ]); + const { getFields } = jest.requireMock('../../../common/index_controls'); + getFields.mockResolvedValueOnce([ + { + type: 'date', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); const ConnectorFields = actionTypeModel.actionConnectorFields; const actionConnector = { secrets: {}, @@ -119,8 +153,38 @@ describe('IndexActionConnectorFields renders', () => { http={mocks.http} /> ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + + const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); + expect(indexSearchBoxValue.first().props().value).toEqual(''); + + const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); + indexComboBox.first().simulate('click'); + const event = { target: { value: 'indexPattern1' } }; + indexComboBox + .find('input') + .first() + .simulate('change', event); + + const indexSearchBoxValueBeforeEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + + const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); + indexComboBoxClear.first().simulate('click'); + + const indexSearchBoxValueAfterEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 15f68e6a9f441..861d6ad7284c2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -86,7 +86,9 @@ const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP const [indexPatterns, setIndexPatterns] = useState([]); const [indexOptions, setIndexOptions] = useState<EuiComboBoxOptionOption[]>([]); - const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); + const [timeFieldOptions, setTimeFieldOptions] = useState<Array<{ value: string; text: string }>>([ + firstFieldOption, + ]); const [isIndiciesLoading, setIsIndiciesLoading] = useState<boolean>(false); useEffect(() => { @@ -151,7 +153,7 @@ const IndexActionConnectorFields: React.FunctionComponent<ActionConnectorFieldsP : [] } onChange={async (selected: EuiComboBoxOptionOption[]) => { - editActionConfig('index', selected[0].value); + editActionConfig('index', selected.length > 0 ? selected[0].value : ''); const indices = selected.map(s => s.value as string); // reset time field and expression fields if indices are deleted diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx index 7da97b9fe3436..1c9e87310107f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.test.tsx @@ -90,7 +90,7 @@ describe('pagerduty action params validation', () => { summary: '2323', source: 'source', severity: 'critical', - timestamp: '234654564654', + timestamp: new Date().toISOString(), component: 'test', group: 'group', class: 'test class', @@ -99,6 +99,7 @@ describe('pagerduty action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { summary: [], + timestamp: [], }, }); }); @@ -156,7 +157,7 @@ describe('PagerDutyParamsFields renders', () => { summary: '2323', source: 'source', severity: SeverityActionOptions.CRITICAL, - timestamp: '234654564654', + timestamp: new Date().toISOString(), component: 'test', group: 'group', class: 'test class', @@ -164,7 +165,7 @@ describe('PagerDutyParamsFields renders', () => { const wrapper = mountWithIntl( <ParamsFields actionParams={actionParams} - errors={{ summary: [] }} + errors={{ summary: [], timestamp: [] }} editAction={() => {}} index={0} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx index d99362c618356..15f91ae1d4609 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty.tsx @@ -14,6 +14,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; import { ActionTypeModel, ActionConnectorFieldsProps, @@ -23,6 +24,7 @@ import { import { PagerDutyActionParams, PagerDutyActionConnector } from './types'; import pagerDutySvg from './pagerduty.svg'; import { AddMessageVariables } from '../add_message_variables'; +import { hasMustacheTokens } from '../../lib/has_mustache_tokens'; export function getActionType(): ActionTypeModel { return { @@ -62,6 +64,7 @@ export function getActionType(): ActionTypeModel { const validationResult = { errors: {} }; const errors = { summary: new Array<string>(), + timestamp: new Array<string>(), }; validationResult.errors = errors; if (!actionParams.summary?.length) { @@ -74,6 +77,24 @@ export function getActionType(): ActionTypeModel { ) ); } + if (actionParams.timestamp && !hasMustacheTokens(actionParams.timestamp)) { + if (isNaN(Date.parse(actionParams.timestamp))) { + const { nowShortFormat, nowLongFormat } = getValidTimestampExamples(); + errors.timestamp.push( + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.error.invalidTimestamp', + { + defaultMessage: + 'Timestamp must be a valid date, such as {nowShortFormat} or {nowLongFormat}.', + values: { + nowShortFormat, + nowLongFormat, + }, + } + ) + ); + } + } return validationResult; }, actionConnectorFields: PagerDutyActionConnectorFields, @@ -334,6 +355,8 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty <EuiFlexItem> <EuiFormRow fullWidth + error={errors.timestamp} + isInvalid={errors.timestamp.length > 0 && timestamp !== undefined} label={i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.timestampTextFieldLabel', { @@ -355,11 +378,14 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty name="timestamp" data-test-subj="timestampInput" value={timestamp || ''} + isInvalid={errors.timestamp.length > 0 && timestamp !== undefined} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { editAction('timestamp', e.target.value, index); }} onBlur={() => { - if (!timestamp) { + if (timestamp?.trim()) { + editAction('timestamp', timestamp.trim(), index); + } else { editAction('timestamp', '', index); } }} @@ -534,3 +560,11 @@ const PagerDutyParamsFields: React.FunctionComponent<ActionParamsProps<PagerDuty </Fragment> ); }; + +function getValidTimestampExamples() { + const now = moment(); + return { + nowShortFormat: now.format('YYYY-MM-DD'), + nowLongFormat: now.format('YYYY-MM-DD h:mm:ss'), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts index 9ce50cf47560a..0a2ec3f203a9a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts @@ -33,11 +33,20 @@ test('should sort enabled action types first', async () => { enabledInConfig: true, enabledInLicense: true, }, + { + id: '4', + minimumLicenseRequired: 'basic', + name: 'x-fourth', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }, ]; const result = [...actionTypes].sort(actionTypeCompare); expect(result[0]).toEqual(actionTypes[0]); expect(result[1]).toEqual(actionTypes[2]); - expect(result[2]).toEqual(actionTypes[1]); + expect(result[2]).toEqual(actionTypes[3]); + expect(result[3]).toEqual(actionTypes[1]); }); test('should sort by name when all enabled', async () => { @@ -66,9 +75,18 @@ test('should sort by name when all enabled', async () => { enabledInConfig: true, enabledInLicense: true, }, + { + id: '4', + minimumLicenseRequired: 'basic', + name: 'x-fourth', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }, ]; const result = [...actionTypes].sort(actionTypeCompare); expect(result[0]).toEqual(actionTypes[1]); expect(result[1]).toEqual(actionTypes[2]); expect(result[2]).toEqual(actionTypes[0]); + expect(result[3]).toEqual(actionTypes[3]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts index d18cb21b3a0fe..8078ef4938e50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts @@ -4,14 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../types'; +import { ActionType, ActionConnector } from '../../types'; -export function actionTypeCompare(a: ActionType, b: ActionType) { - if (a.enabled === true && b.enabled === false) { +export function actionTypeCompare( + a: ActionType, + b: ActionType, + preconfiguredConnectors?: ActionConnector[] +) { + const aEnabled = getIsEnabledValue(a, preconfiguredConnectors); + const bEnabled = getIsEnabledValue(b, preconfiguredConnectors); + + if (aEnabled === true && bEnabled === false) { return -1; } - if (a.enabled === false && b.enabled === true) { + if (aEnabled === false && bEnabled === true) { return 1; } return a.name.localeCompare(b.name); } + +const getIsEnabledValue = (actionType: ActionType, preconfiguredConnectors?: ActionConnector[]) => { + let isEnabled = actionType.enabled; + if ( + !actionType.enabledInConfig && + preconfiguredConnectors && + preconfiguredConnectors.length > 0 + ) { + isEnabled = + preconfiguredConnectors.find(connector => connector.actionTypeId === actionType.id) !== + undefined; + } + return isEnabled; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx index 566ed7935e013..9c017aa6fd31f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx @@ -4,43 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../types'; -import { checkActionTypeEnabled } from './check_action_type_enabled'; +import { ActionType, ActionConnector } from '../../types'; +import { + checkActionTypeEnabled, + checkActionFormActionTypeEnabled, +} from './check_action_type_enabled'; -test(`returns isEnabled:true when action type isn't provided`, async () => { - expect(checkActionTypeEnabled()).toMatchInlineSnapshot(` +describe('checkActionTypeEnabled', () => { + test(`returns isEnabled:true when action type isn't provided`, async () => { + expect(checkActionTypeEnabled()).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); -}); + }); -test('returns isEnabled:true when action type is enabled', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + test('returns isEnabled:true when action type is enabled', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); -}); + }); -test('returns isEnabled:false when action type is disabled by license', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: false, - enabledInConfig: true, - enabledInLicense: false, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + test('returns isEnabled:false when action type is disabled by license', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { "isEnabled": false, "message": "This connector requires a Basic license.", @@ -63,18 +67,82 @@ test('returns isEnabled:false when action type is disabled by license', async () </EuiCard>, } `); + }); + + test('returns isEnabled:false when action type is disabled by config', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + Object { + "isEnabled": false, + "message": "This connector is disabled by the Kibana configuration.", + "messageCard": <EuiCard + className="actCheckActionTypeEnabled__disabledActionWarningCard" + description="" + title="This feature is disabled by the Kibana configuration." + />, + } + `); + }); }); -test('returns isEnabled:false when action type is disabled by config', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: false, - enabledInConfig: false, - enabledInLicense: true, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` +describe('checkActionFormActionTypeEnabled', () => { + const preconfiguredConnectors: ActionConnector[] = [ + { + actionTypeId: '1', + config: {}, + id: 'test1', + isPreconfigured: true, + name: 'test', + secrets: {}, + referencedByCount: 0, + }, + { + actionTypeId: '2', + config: {}, + id: 'test2', + isPreconfigured: true, + name: 'test', + secrets: {}, + referencedByCount: 0, + }, + ]; + + test('returns isEnabled:true when action type is preconfigured', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }; + + expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) + .toMatchInlineSnapshot(` + Object { + "isEnabled": true, + } + `); + }); + + test('returns isEnabled:false when action type is disabled by config and not preconfigured', async () => { + const actionType: ActionType = { + id: 'disabled-by-config', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }; + expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) + .toMatchInlineSnapshot(` Object { "isEnabled": false, "message": "This connector is disabled by the Kibana configuration.", @@ -85,4 +153,5 @@ test('returns isEnabled:false when action type is disabled by config', async () />, } `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx index 263502a82ec79..971d6dbbb57bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx @@ -9,7 +9,7 @@ import { capitalize } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCard, EuiLink } from '@elastic/eui'; -import { ActionType } from '../../types'; +import { ActionType, ActionConnector } from '../../types'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants'; import './check_action_type_enabled.scss'; @@ -22,71 +22,98 @@ export interface IsDisabledResult { messageCard: JSX.Element; } +const getLicenseCheckResult = (actionType: ActionType) => { + return { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage', + { + defaultMessage: 'This connector requires a {minimumLicenseRequired} license.', + values: { + minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), + }, + } + ), + messageCard: ( + <EuiCard + titleSize="xs" + title={i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageTitle', + { + defaultMessage: 'This feature requires a {minimumLicenseRequired} license.', + values: { + minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), + }, + } + )} + // The "re-enable" terminology is used here because this message is used when an alert + // action was previously enabled and needs action to be re-enabled. + description={i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageDescription', + { defaultMessage: 'To re-enable this action, please upgrade your license.' } + )} + className="actCheckActionTypeEnabled__disabledActionWarningCard" + children={ + <EuiLink href={VIEW_LICENSE_OPTIONS_LINK} target="_blank"> + <FormattedMessage + defaultMessage="View license options" + id="xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseLinkTitle" + /> + </EuiLink> + } + /> + ), + }; +}; + +const configurationCheckResult = { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage', + { defaultMessage: 'This connector is disabled by the Kibana configuration.' } + ), + messageCard: ( + <EuiCard + title={i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByConfigMessageTitle', + { defaultMessage: 'This feature is disabled by the Kibana configuration.' } + )} + description="" + className="actCheckActionTypeEnabled__disabledActionWarningCard" + /> + ), +}; + export function checkActionTypeEnabled( actionType?: ActionType ): IsEnabledResult | IsDisabledResult { if (actionType?.enabledInLicense === false) { - return { - isEnabled: false, - message: i18n.translate( - 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage', - { - defaultMessage: 'This connector requires a {minimumLicenseRequired} license.', - values: { - minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), - }, - } - ), - messageCard: ( - <EuiCard - titleSize="xs" - title={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageTitle', - { - defaultMessage: 'This feature requires a {minimumLicenseRequired} license.', - values: { - minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), - }, - } - )} - // The "re-enable" terminology is used here because this message is used when an alert - // action was previously enabled and needs action to be re-enabled. - description={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageDescription', - { defaultMessage: 'To re-enable this action, please upgrade your license.' } - )} - className="actCheckActionTypeEnabled__disabledActionWarningCard" - children={ - <EuiLink href={VIEW_LICENSE_OPTIONS_LINK} target="_blank"> - <FormattedMessage - defaultMessage="View license options" - id="xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseLinkTitle" - /> - </EuiLink> - } - /> - ), - }; + return getLicenseCheckResult(actionType); } if (actionType?.enabledInConfig === false) { - return { - isEnabled: false, - message: i18n.translate( - 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage', - { defaultMessage: 'This connector is disabled by the Kibana configuration.' } - ), - messageCard: ( - <EuiCard - title={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByConfigMessageTitle', - { defaultMessage: 'This feature is disabled by the Kibana configuration.' } - )} - description="" - className="actCheckActionTypeEnabled__disabledActionWarningCard" - /> - ), - }; + return configurationCheckResult; + } + + return { isEnabled: true }; +} + +export function checkActionFormActionTypeEnabled( + actionType: ActionType, + preconfiguredConnectors: ActionConnector[] +): IsEnabledResult | IsDisabledResult { + if (actionType?.enabledInLicense === false) { + return getLicenseCheckResult(actionType); + } + + if ( + actionType?.enabledInConfig === false && + // do not disable action type if it contains preconfigured connectors (is preconfigured) + !preconfiguredConnectors.find( + preconfiguredConnector => preconfiguredConnector.actionTypeId === actionType.id + ) + ) { + return configurationCheckResult; } return { isEnabled: true }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.test.ts new file mode 100644 index 0000000000000..db4f9fa799170 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.test.ts @@ -0,0 +1,23 @@ +/* + * 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 uuid from 'uuid'; + +import { hasMustacheTokens } from './has_mustache_tokens'; + +describe('hasMustacheTokens', () => { + test('returns false for empty string', () => { + expect(hasMustacheTokens('')).toBe(false); + }); + + test('returns false for string without tokens', () => { + expect(hasMustacheTokens(`some random string ${uuid.v4()}`)).toBe(false); + }); + + test('returns true when a template token is present', () => { + expect(hasMustacheTokens('{{context.timestamp}}')).toBe(true); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts new file mode 100644 index 0000000000000..4dcd8113d51fc --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/has_mustache_tokens.ts @@ -0,0 +1,9 @@ +/* + * 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 function hasMustacheTokens(str: string): boolean { + return null !== str.match(/{{.*}}/); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index d4def86b07b1f..aed7d18bd9f3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -73,6 +73,21 @@ describe('action_form', () => { actionParamsFields: null, }; + const preconfiguredOnly = { + id: 'preconfigured', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + describe('action_form in alert', () => { let wrapper: ReactWrapper<any>; @@ -95,6 +110,22 @@ describe('action_form', () => { config: {}, isPreconfigured: true, }, + { + secrets: {}, + id: 'test3', + actionTypeId: preconfiguredOnly.id, + name: 'Preconfigured Only', + config: {}, + isPreconfigured: true, + }, + { + secrets: {}, + id: 'test4', + actionTypeId: preconfiguredOnly.id, + name: 'Regular connector', + config: {}, + isPreconfigured: false, + }, ]); const mockes = coreMock.createSetup(); deps = { @@ -106,6 +137,7 @@ describe('action_form', () => { actionType, disabledByConfigActionType, disabledByLicenseActionType, + preconfiguredOnly, ]); actionTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.get.mockReturnValue(actionType); @@ -166,6 +198,14 @@ describe('action_form', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', }, + { + id: 'preconfigured', + name: 'Preconfigured only', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, { id: 'disabled-by-config', name: 'Disabled by config', @@ -207,21 +247,27 @@ describe('action_form', () => { ).toBeFalsy(); }); - it(`doesn't render action types disabled by config`, async () => { + it('does not render action types disabled by config', async () => { await setup(); const actionOption = wrapper.find( - `[data-test-subj="disabled-by-config-ActionTypeSelectOption"]` + '[data-test-subj="disabled-by-config-ActionTypeSelectOption"]' ); expect(actionOption.exists()).toBeFalsy(); }); - it(`renders available connectors for the selected action type`, async () => { + it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + expect(actionOption.exists()).toBeTruthy(); + }); + + it('renders available connectors for the selected action type', async () => { await setup(); const actionOption = wrapper.find( `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` ); actionOption.first().simulate('click'); - const combobox = wrapper.find(`[data-test-subj="selectActionConnector"]`); + const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` Array [ Object { @@ -238,10 +284,37 @@ describe('action_form', () => { `); }); + it('renders only preconfigured connectors for the selected preconfigured action type', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + actionOption.first().simulate('click'); + const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]'); + expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test3", + "key": "test3", + "label": "Preconfigured Only (preconfigured)", + }, + ] + `); + }); + + it('does not render "Add new" button for preconfigured only action type', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + actionOption.first().simulate('click'); + const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]'); + const addNewConnectorButton = preconfigPannel.find( + '[data-test-subj="addNewActionConnectorButton-preconfigured"]' + ); + expect(addNewConnectorButton.exists()).toBeFalsy(); + }); + it('renders action types disabled by license', async () => { await setup(); const actionOption = wrapper.find( - `[data-test-subj="disabled-by-license-ActionTypeSelectOption"]` + '[data-test-subj="disabled-by-license-ActionTypeSelectOption"]' ); expect(actionOption.exists()).toBeTruthy(); expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 4199cfb7b4b7f..531e9e1926ff4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -29,7 +29,7 @@ import { EuiText, } from '@elastic/eui'; import { HttpSetup, ToastsApi } from 'kibana/public'; -import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; +import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api'; import { IErrorObject, ActionTypeModel, @@ -42,7 +42,7 @@ import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; import { TypeRegistry } from '../../type_registry'; import { actionTypeCompare } from '../../lib/action_type_compare'; -import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; +import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; interface ActionAccordionFormProps { @@ -106,19 +106,13 @@ export const ActionForm = ({ index[actionTypeItem.id] = actionTypeItem; } setActionTypesIndex(index); - const hasActionsDisabled = actions.some(action => !index[action.actionTypeId].enabled); - if (setHasActionsDisabled) { - setHasActionsDisabled(hasActionsDisabled); - } } catch (e) { - if (toastNotifications) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', - { defaultMessage: 'Unable to load action types' } - ), - }); - } + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', + { defaultMessage: 'Unable to load action types' } + ), + }); } finally { setIsLoadingActionTypes(false); } @@ -126,41 +120,72 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // load connectors useEffect(() => { - loadConnectors(); + (async () => { + try { + setIsLoadingConnectors(true); + const loadedConnectors = await loadConnectors({ http }); + setConnectors(loadedConnectors); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', + { + defaultMessage: 'Unable to load connectors', + } + ), + }); + } finally { + setIsLoadingConnectors(false); + } + })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - async function loadConnectors() { - try { - setIsLoadingConnectors(true); - const actionsResponse = await loadAllActions({ http }); - setConnectors(actionsResponse); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', - { - defaultMessage: 'Unable to load connectors', - } - ), - }); - } finally { - setIsLoadingConnectors(false); + useEffect(() => { + const setActionTypesAvalilability = () => { + const preconfiguredConnectors = connectors.filter(connector => connector.isPreconfigured); + const hasActionsDisabled = actions.some( + action => + !actionTypesIndex![action.actionTypeId].enabled && + !checkActionFormActionTypeEnabled( + actionTypesIndex![action.actionTypeId], + preconfiguredConnectors + ).isEnabled + ); + if (setHasActionsDisabled) { + setHasActionsDisabled(hasActionsDisabled); + } + }; + if (connectors.length > 0 && actionTypesIndex) { + setActionTypesAvalilability(); } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connectors, actionTypesIndex]); + const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', { defaultMessage: '(preconfigured)', } ); + const getSelectedOptions = (actionItemId: string) => { - const val = connectors.find(connector => connector.id === actionItemId); - if (!val) { + const selectedConnector = connectors.find(connector => connector.id === actionItemId); + if ( + !selectedConnector || + // if selected connector is not preconfigured and action type is for preconfiguration only, + // do not show regular connectors of this type + (actionTypesIndex && + !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig && + !selectedConnector.isPreconfigured) + ) { return []; } - const optionTitle = `${val.name} ${val.isPreconfigured ? preconfiguredMessage : ''}`; + const optionTitle = `${selectedConnector.name} ${ + selectedConnector.isPreconfigured ? preconfiguredMessage : '' + }`; return [ { label: optionTitle, @@ -179,8 +204,15 @@ export const ActionForm = ({ }, index: number ) => { + const actionType = actionTypesIndex![actionItem.actionTypeId]; + const optionsList = connectors - .filter(connectorItem => connectorItem.actionTypeId === actionItem.actionTypeId) + .filter( + connectorItem => + connectorItem.actionTypeId === actionItem.actionTypeId && + // include only enabled by config connectors or preconfigured + (actionType.enabledInConfig || connectorItem.isPreconfigured) + ) .map(({ name, id, isPreconfigured }) => ({ label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`, key: id, @@ -189,8 +221,9 @@ export const ActionForm = ({ const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields; - const checkEnabledResult = checkActionTypeEnabled( - actionTypesIndex && actionTypesIndex[actionConnector.actionTypeId] + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionTypesIndex![actionConnector.actionTypeId], + connectors.filter(connector => connector.isPreconfigured) ); const accordionContent = checkEnabledResult.isEnabled ? ( @@ -211,19 +244,21 @@ export const ActionForm = ({ /> } labelAppend={ - <EuiButtonEmpty - size="xs" - data-test-subj="createActionConnectorButton" - onClick={() => { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); - setAddModalVisibility(true); - }} - > - <FormattedMessage - defaultMessage="Add new" - id="xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton" - /> - </EuiButtonEmpty> + actionTypesIndex![actionConnector.actionTypeId].enabledInConfig ? ( + <EuiButtonEmpty + size="xs" + data-test-subj={`addNewActionConnectorButton-${actionItem.actionTypeId}`} + onClick={() => { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); + setAddModalVisibility(true); + }} + > + <FormattedMessage + defaultMessage="Add new" + id="xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton" + /> + </EuiButtonEmpty> + ) : null } > <EuiComboBox @@ -231,7 +266,7 @@ export const ActionForm = ({ singleSelection={{ asPlainText: true }} options={optionsList} id={`selectActionConnector-${actionItem.id}`} - data-test-subj="selectActionConnector" + data-test-subj={`selectActionConnector-${actionItem.actionTypeId}`} selectedOptions={getSelectedOptions(actionItem.id)} onChange={selectedOptions => { setActionIdByIndex(selectedOptions[0].id ?? '', index); @@ -258,10 +293,9 @@ export const ActionForm = ({ ); return ( - <Fragment> + <Fragment key={index}> <EuiAccordion initialIsOpen={true} - key={index} id={index.toString()} className="actAccordionActionForm" buttonContentClassName="actAccordionActionForm__button" @@ -273,12 +307,12 @@ export const ActionForm = ({ </EuiFlexItem> <EuiFlexItem> <EuiText> - <p> + <div> <EuiFlexGroup gutterSize="s"> <EuiFlexItem grow={false}> <FormattedMessage defaultMessage="{actionConnectorName}" - id="xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle" + id="xpack.triggersActionsUI.sections.alertForm.existingAlertActionTypeEditTitle" values={{ actionConnectorName: `${actionConnector.name} ${ actionConnector.isPreconfigured ? preconfiguredMessage : '' @@ -304,7 +338,7 @@ export const ActionForm = ({ )} </EuiFlexItem> </EuiFlexGroup> - </p> + </div> </EuiText> </EuiFlexItem> </EuiFlexGroup> @@ -349,10 +383,9 @@ export const ActionForm = ({ const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; return ( - <Fragment> + <Fragment key={index}> <EuiAccordion initialIsOpen={true} - key={index} id={index.toString()} className="actAccordionActionForm" buttonContentClassName="actAccordionActionForm__button" @@ -364,15 +397,15 @@ export const ActionForm = ({ </EuiFlexItem> <EuiFlexItem> <EuiText> - <p> + <div> <FormattedMessage defaultMessage="{actionConnectorName}" - id="xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle" + id="xpack.triggersActionsUI.sections.alertForm.newAlertActionTypeEditTitle" values={{ actionConnectorName: actionTypeRegistered.actionTypeTitle, }} /> - </p> + </div> </EuiText> </EuiFlexItem> </EuiFlexGroup> @@ -486,18 +519,26 @@ export const ActionForm = ({ } } - let actionTypeNodes: JSX.Element[] | null = null; + let actionTypeNodes: Array<JSX.Element | null> | null = null; let hasDisabledByLicenseActionTypes = false; if (actionTypesIndex) { + const preconfiguredConnectors = connectors.filter(connector => connector.isPreconfigured); actionTypeNodes = actionTypeRegistry .list() - .filter( - item => actionTypesIndex[item.id] && actionTypesIndex[item.id].enabledInConfig === true + .filter(item => actionTypesIndex[item.id]) + .sort((a, b) => + actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id], preconfiguredConnectors) ) - .sort((a, b) => actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id])) .map(function(item, index) { const actionType = actionTypesIndex[item.id]; - const checkEnabledResult = checkActionTypeEnabled(actionTypesIndex[item.id]); + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionTypesIndex[item.id], + preconfiguredConnectors + ); + // if action type is not enabled in config and not preconfigured, it shouldn't be displayed + if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) { + return null; + } if (!actionType.enabledInLicense) { hasDisabledByLicenseActionTypes = true; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 9da4f059f8967..230b896eeca7d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -7,24 +7,55 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; import { AlertDetails } from './alert_details'; -import { Alert, ActionType } from '../../../../types'; -import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiBetaBadge } from '@elastic/eui'; +import { Alert, ActionType, AlertTypeRegistryContract } from '../../../../types'; +import { + EuiTitle, + EuiBadge, + EuiFlexItem, + EuiSwitch, + EuiBetaBadge, + EuiButtonEmpty, +} from '@elastic/eui'; import { times, random } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ViewInApp } from './view_in_app'; import { PLUGIN } from '../../../constants/plugin'; +import { coreMock } from 'src/core/public/mocks'; +const mockes = coreMock.createSetup(); jest.mock('../../../app_context', () => ({ useAppDependencies: jest.fn(() => ({ http: jest.fn(), - legacy: { - capabilities: { - get: jest.fn(() => ({})), - }, + capabilities: { + get: jest.fn(() => ({})), }, + actionTypeRegistry: jest.fn(), + alertTypeRegistry: jest.fn(() => { + const mocked: jest.Mocked<AlertTypeRegistryContract> = { + has: jest.fn(), + register: jest.fn(), + get: jest.fn(), + list: jest.fn(), + }; + return mocked; + }), + toastNotifications: mockes.notifications.toasts, + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, + uiSettings: mockes.uiSettings, + dataPlugin: jest.fn(), + charts: jest.fn(), })), })); +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), + useLocation: () => ({ + pathname: '/triggersActions/alerts/', + }), +})); + jest.mock('../../../lib/capabilities', () => ({ hasSaveAlertsCapability: jest.fn(() => true), })); @@ -232,6 +263,28 @@ describe('alert_details', () => { ).containsMatchingElement(<ViewInApp alert={alert} />) ).toBeTruthy(); }); + + it('links to the Edit flyout', () => { + const alert = mockAlert(); + + const alertType = { + id: '.noop', + name: 'No Op', + actionGroups: [{ id: 'default', name: 'Default' }], + actionVariables: { context: [], state: [] }, + defaultActionGroupId: 'default', + }; + + expect( + shallow( + <AlertDetails alert={alert} alertType={alertType} actionTypes={[]} {...mockAlertApis} /> + ) + .find(EuiButtonEmpty) + .find('[data-test-subj="openEditAlertFlyoutButton"]') + .first() + .exists() + ).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 5bfcf9fd2d4e6..318dd28d92da1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import { indexBy } from 'lodash'; +import { useHistory } from 'react-router-dom'; import { EuiPageBody, EuiPageContent, @@ -21,6 +22,7 @@ import { EuiCallOut, EuiSpacer, EuiBetaBadge, + EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -34,6 +36,9 @@ import { import { AlertInstancesRouteWithApi } from './alert_instances_route'; import { ViewInApp } from './view_in_app'; import { PLUGIN } from '../../../constants/plugin'; +import { AlertEdit } from '../../alert_form'; +import { AlertsContextProvider } from '../../../context/alerts_context'; +import { routeToAlertDetails } from '../../../constants'; type AlertDetailsProps = { alert: Alert; @@ -52,7 +57,18 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ muteAlert, requestRefresh, }) => { - const { capabilities } = useAppDependencies(); + const history = useHistory(); + const { + http, + toastNotifications, + capabilities, + alertTypeRegistry, + actionTypeRegistry, + uiSettings, + docLinks, + charts, + dataPlugin, + } = useAppDependencies(); const canSave = hasSaveAlertsCapability(capabilities); @@ -61,6 +77,11 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ const [isEnabled, setIsEnabled] = useState<boolean>(alert.enabled); const [isMuted, setIsMuted] = useState<boolean>(alert.muteAll); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false); + + const setAlert = async () => { + history.push(routeToAlertDetails.replace(`:alertId`, alert.id)); + }; return ( <EuiPage> @@ -90,6 +111,42 @@ export const AlertDetails: React.FunctionComponent<AlertDetailsProps> = ({ </EuiPageContentHeaderSection> <EuiPageContentHeaderSection> <EuiFlexGroup responsive={false} gutterSize="xs"> + {canSave ? ( + <EuiFlexItem grow={false}> + <Fragment> + {' '} + <EuiButtonEmpty + data-test-subj="openEditAlertFlyoutButton" + iconType="pencil" + onClick={() => setEditFlyoutVisibility(true)} + > + <FormattedMessage + id="xpack.triggersActionsUI.sections.alertDetails.editAlertButtonLabel" + defaultMessage="Edit" + /> + </EuiButtonEmpty> + <AlertsContextProvider + value={{ + http, + actionTypeRegistry, + alertTypeRegistry, + toastNotifications, + uiSettings, + docLinks, + charts, + dataFieldsFormats: dataPlugin.fieldFormats, + reloadAlerts: setAlert, + }} + > + <AlertEdit + initialAlert={alert} + editFlyoutVisible={editFlyoutVisible} + setEditFlyoutVisibility={setEditFlyoutVisibility} + /> + </AlertsContextProvider> + </Fragment> + </EuiFlexItem> + ) : null} <EuiFlexItem grow={false}> <ViewInApp alert={alert} /> </EuiFlexItem> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx index 498ecffe9b947..a02b44523e26c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances_route.tsx @@ -26,13 +26,14 @@ export const AlertInstancesRoute: React.FunctionComponent<WithAlertStateProps> = requestRefresh, loadAlertState, }) => { - const { http, toastNotifications } = useAppDependencies(); + const { toastNotifications } = useAppDependencies(); const [alertState, setAlertState] = useState<AlertTaskState | null>(null); useEffect(() => { getAlertState(alert.id, loadAlertState, setAlertState, toastNotifications); - }, [alert, http, loadAlertState, toastNotifications]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [alert]); return alertState ? ( <AlertInstances requestRefresh={requestRefresh} alert={alert} alertState={alertState} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 0620ced6365a9..651f2cdba34af 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -3,7 +3,7 @@ * 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, { useCallback, useReducer, useState } from 'react'; +import React, { useCallback, useReducer, useState, useEffect } from 'react'; import { isObject } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -60,6 +60,9 @@ export const AlertAdd = ({ const setAlert = (value: any) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); }; + const setAlertProperty = (key: string, value: any) => { + dispatch({ command: { type: 'setProperty' }, payload: { key, value } }); + }; const { reloadAlerts, @@ -70,6 +73,10 @@ export const AlertAdd = ({ docLinks, } = useAlertsContext(); + useEffect(() => { + setAlertProperty('alertTypeId', alertTypeId); + }, [alertTypeId]); + const closeFlyout = useCallback(() => { setAddFlyoutVisibility(false); setAlert(initialAlert); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 4255eca83be47..00bc9874face1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -43,6 +43,9 @@ export const AlertEdit = ({ const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState<boolean>(false); const [hasActionsDisabled, setHasActionsDisabled] = useState<boolean>(false); + const setAlert = (key: string, value: any) => { + dispatch({ command: { type: 'setAlert' }, payload: { key, value } }); + }; const { reloadAlerts, @@ -55,6 +58,8 @@ export const AlertEdit = ({ const closeFlyout = useCallback(() => { setEditFlyoutVisibility(false); + setAlert('alert', initialAlert); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setEditFlyoutVisibility]); if (!editFlyoutVisible) { @@ -145,6 +150,7 @@ export const AlertEdit = ({ size="s" color="danger" iconType="alert" + data-test-subj="hasActionsDisabled" title={i18n.translate( 'xpack.triggersActionsUI.sections.alertEdit.disabledActionsWarningTitle', { defaultMessage: 'This alert has actions that are disabled' } 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 72c22f46f217e..8406987e4ed9d 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 66aa02e1930a3..a51ebc3126785 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 1103d7c3921a7..2d9cfcdbda89f 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<AlertTableItem | undefined>(undefined); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false); const [alertsToDelete, setAlertsToDelete] = useState<string[]>([]); 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 ( - <EuiLink - data-test-subj="alertsTableCell-editLink" - color="primary" - onClick={() => editItem(item)} - > - <FormattedMessage - defaultMessage="Edit" - id="xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.editLinkTitle" - /> - </EuiLink> - ); - }, - }, { name: '', width: '40px', @@ -453,14 +425,6 @@ export const AlertsList: React.FunctionComponent = () => { addFlyoutVisible={alertFlyoutVisible} setAddFlyoutVisibility={setAlertFlyoutVisibility} /> - {editFlyoutVisible && editedAlertItem ? ( - <AlertEdit - key={editedAlertItem.id} - initialAlert={editedAlertItem} - editFlyoutVisible={editFlyoutVisible} - setEditFlyoutVisibility={setEditFlyoutVisibility} - /> - ) : null} </AlertsContextProvider> </section> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx index 2f3c0000ef96b..b01104a8d5cf7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.test.tsx @@ -45,6 +45,7 @@ describe('of expression', () => { "asPlainText": true, } } + sortMatchesBy="none" /> `); }); @@ -105,6 +106,7 @@ describe('of expression', () => { "asPlainText": true, } } + sortMatchesBy="none" /> `); }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts index 4228426d62159..5301f6364529d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts @@ -6,7 +6,7 @@ import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; -import { httpServiceMock } from 'src/core/public/http/http_service.mock'; +import { httpServiceMock } from 'src/core/public/mocks'; const mockClient = httpServiceMock.createSetupContract(); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 2af1a9ac38e44..24347b7799871 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -51,6 +51,7 @@ describe('Upgrade Assistant Usage Collector', () => { 'ui_reindex.open': 4, 'ui_reindex.start': 2, 'ui_reindex.stop': 1, + 'ui_reindex.not_defined': 1, }, }; }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 9c2946db7f084..0c2e3a1e43f4a 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { get } from 'lodash'; import { APICaller, ElasticsearchServiceStart, @@ -84,16 +84,19 @@ export async function fetchUpgradeAssistantMetrics( return defaultTelemetrySavedObject; } - const upgradeAssistantTelemetrySOAttrsKeys = Object.keys( - upgradeAssistantTelemetrySavedObjectAttrs - ); - const telemetryObj = defaultTelemetrySavedObject; - - upgradeAssistantTelemetrySOAttrsKeys.forEach((key: string) => { - set(telemetryObj, key, upgradeAssistantTelemetrySavedObjectAttrs[key]); - }); - - return telemetryObj as UpgradeAssistantTelemetrySavedObject; + return { + ui_open: { + overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0), + cluster: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.cluster', 0), + indices: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.indices', 0), + }, + ui_reindex: { + close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0), + open: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.open', 0), + start: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.start', 0), + stop: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.stop', 0), + }, + } as UpgradeAssistantTelemetrySavedObject; }; return { diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index bdca506cc7338..0cdf1ca05feac 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -25,6 +25,8 @@ import { registerClusterCheckupRoutes } from './routes/cluster_checkup'; import { registerDeprecationLoggingRoutes } from './routes/deprecation_logging'; import { registerReindexIndicesRoutes, createReindexWorker } from './routes/reindex_indices'; import { registerTelemetryRoutes } from './routes/telemetry'; +import { telemetrySavedObjectType, reindexOperationSavedObjectType } from './saved_object_types'; + import { RouteDependencies } from './types'; interface PluginsSetup { @@ -57,11 +59,14 @@ export class UpgradeAssistantServerPlugin implements Plugin { } setup( - { http, getStartServices, capabilities }: CoreSetup, + { http, getStartServices, capabilities, savedObjects }: CoreSetup, { usageCollection, cloud, licensing }: PluginsSetup ) { this.licensing = licensing; + savedObjects.registerType(reindexOperationSavedObjectType); + savedObjects.registerType(telemetrySavedObjectType); + const router = http.createRouter(); const dependencies: RouteDependencies = { @@ -85,8 +90,12 @@ export class UpgradeAssistantServerPlugin implements Plugin { registerTelemetryRoutes(dependencies); if (usageCollection) { - getStartServices().then(([{ savedObjects, elasticsearch }]) => { - registerUpgradeAssistantUsageCollector({ elasticsearch, usageCollection, savedObjects }); + getStartServices().then(([{ savedObjects: savedObjectsService, elasticsearch }]) => { + registerUpgradeAssistantUsageCollector({ + elasticsearch, + usageCollection, + savedObjects: savedObjectsService, + }); }); } } diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index b2b8ccf1ca57a..78b03275e0ef9 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -5,7 +5,7 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; +import { savedObjectsServiceMock } from 'src/core/server/mocks'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts new file mode 100644 index 0000000000000..dee0a74d8994b --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { reindexOperationSavedObjectType } from './reindex_operation_saved_object_type'; +export { telemetrySavedObjectType } from './telemetry_saved_object_type'; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts new file mode 100644 index 0000000000000..ba661fbeceb26 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/reindex_operation_saved_object_type.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsType } from 'src/core/server'; + +import { REINDEX_OP_TYPE } from '../../common/types'; + +export const reindexOperationSavedObjectType: SavedObjectsType = { + name: REINDEX_OP_TYPE, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + reindexTaskId: { + type: 'keyword', + }, + indexName: { + type: 'keyword', + }, + newIndexName: { + type: 'keyword', + }, + status: { + type: 'integer', + }, + locked: { + type: 'date', + }, + lastCompletedStep: { + type: 'integer', + }, + errorMessage: { + type: 'keyword', + }, + reindexTaskPercComplete: { + type: 'float', + }, + runningReindexCount: { + type: 'integer', + }, + reindexOptions: { + properties: { + openAndClose: { + type: 'boolean', + }, + queueSettings: { + properties: { + queuedAt: { + type: 'long', + }, + startedAt: { + type: 'long', + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.ts new file mode 100644 index 0000000000000..b1321e634c0f1 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/saved_object_types/telemetry_saved_object_type.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 { SavedObjectsType } from 'src/core/server'; + +import { UPGRADE_ASSISTANT_TYPE } from '../../common/types'; + +export const telemetrySavedObjectType: SavedObjectsType = { + name: UPGRADE_ASSISTANT_TYPE, + hidden: false, + namespaceType: 'agnostic', + mappings: { + properties: { + ui_open: { + properties: { + overview: { + type: 'long', + null_value: 0, + }, + cluster: { + type: 'long', + null_value: 0, + }, + indices: { + type: 'long', + null_value: 0, + }, + }, + }, + ui_reindex: { + properties: { + close: { + type: 'long', + null_value: 0, + }, + open: { + type: 'long', + null_value: 0, + }, + start: { + type: 'long', + null_value: 0, + }, + stop: { + type: 'long', + null_value: 0, + }, + }, + }, + features: { + properties: { + deprecation_logging: { + properties: { + enabled: { + type: 'boolean', + null_value: true, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/uptime/README.md b/x-pack/plugins/uptime/README.md similarity index 100% rename from x-pack/legacy/plugins/uptime/README.md rename to x-pack/plugins/uptime/README.md diff --git a/x-pack/legacy/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/alerts.ts rename to x-pack/plugins/uptime/common/constants/alerts.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/capabilities.ts b/x-pack/plugins/uptime/common/constants/capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/capabilities.ts rename to x-pack/plugins/uptime/common/constants/capabilities.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts b/x-pack/plugins/uptime/common/constants/chart_format_limits.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/chart_format_limits.ts rename to x-pack/plugins/uptime/common/constants/chart_format_limits.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts b/x-pack/plugins/uptime/common/constants/client_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/client_defaults.ts rename to x-pack/plugins/uptime/common/constants/client_defaults.ts diff --git a/x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts b/x-pack/plugins/uptime/common/constants/context_defaults.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/context_defaults.ts rename to x-pack/plugins/uptime/common/constants/context_defaults.ts diff --git a/x-pack/plugins/uptime/common/constants/index.ts b/x-pack/plugins/uptime/common/constants/index.ts new file mode 100644 index 0000000000000..00baa39044a55 --- /dev/null +++ b/x-pack/plugins/uptime/common/constants/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { ACTION_GROUP_DEFINITIONS } from './alerts'; +export { CHART_FORMAT_LIMITS } from './chart_format_limits'; +export { CLIENT_DEFAULTS } from './client_defaults'; +export { CONTEXT_DEFAULTS } from './context_defaults'; +export * from './capabilities'; +export * from './settings_defaults'; +export { PLUGIN } from './plugin'; +export { QUERY } from './query'; +export * from './ui'; +export * from './rest_api'; diff --git a/x-pack/plugins/uptime/common/constants/plugin.ts b/x-pack/plugins/uptime/common/constants/plugin.ts new file mode 100644 index 0000000000000..6064524872a0a --- /dev/null +++ b/x-pack/plugins/uptime/common/constants/plugin.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const PLUGIN = { + APP_ROOT_ID: 'react-uptime-root', + DESCRIPTION: i18n.translate('xpack.uptime.pluginDescription', { + defaultMessage: 'Uptime monitoring', + description: 'The description text that will appear in the feature catalogue.', + }), + ID: 'uptime', + LOCAL_STORAGE_KEY: 'xpack.uptime', + NAME: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { + defaultMessage: 'Uptime', + }), + ROUTER_BASE_NAME: '/app/uptime#', + TITLE: i18n.translate('xpack.uptime.uptimeFeatureCatalogueTitle', { + defaultMessage: 'Uptime', + }), +}; diff --git a/x-pack/legacy/plugins/uptime/common/constants/query.ts b/x-pack/plugins/uptime/common/constants/query.ts similarity index 78% rename from x-pack/legacy/plugins/uptime/common/constants/query.ts rename to x-pack/plugins/uptime/common/constants/query.ts index d728f114aae76..21574f1d8b27e 100644 --- a/x-pack/legacy/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/legacy/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/constants/rest_api.ts rename to x-pack/plugins/uptime/common/constants/rest_api.ts diff --git a/x-pack/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/plugins/uptime/common/constants/settings_defaults.ts new file mode 100644 index 0000000000000..b7986679a09ca --- /dev/null +++ b/x-pack/plugins/uptime/common/constants/settings_defaults.ts @@ -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 { DynamicSettings } from '../runtime_types'; + +export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = { + heartbeatIndices: 'heartbeat-8*', + certThresholds: { + expiration: 30, + age: 365, + }, +}; diff --git a/x-pack/legacy/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts similarity index 84% rename from x-pack/legacy/plugins/uptime/common/constants/ui.ts rename to x-pack/plugins/uptime/common/constants/ui.ts index 29e8dabf53f92..3bf3e3cc0a2cc 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -10,6 +10,8 @@ export const OVERVIEW_ROUTE = '/'; export const SETTINGS_ROUTE = '/settings'; +export const CERTIFICATES_ROUTE = '/certificates'; + export enum STATUS { UP = 'up', DOWN = 'down', @@ -41,3 +43,10 @@ export const SHORT_TIMESPAN_LOCALE = { yy: '%d Yr', }, }; + +export enum CERT_STATUS { + OK = 'OK', + EXPIRING_SOON = 'EXPIRING_SOON', + EXPIRED = 'EXPIRED', + TOO_OLD = 'TOO_OLD', +} diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/alerts/index.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/alerts/index.ts rename to x-pack/plugins/uptime/common/runtime_types/alerts/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/alerts/status_check.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/alerts/status_check.ts rename to x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts diff --git a/x-pack/plugins/uptime/common/runtime_types/certs.ts b/x-pack/plugins/uptime/common/runtime_types/certs.ts new file mode 100644 index 0000000000000..e9071e76b6d75 --- /dev/null +++ b/x-pack/plugins/uptime/common/runtime_types/certs.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const GetCertsParamsType = t.intersection([ + t.type({ + index: t.number, + size: t.number, + sortBy: t.string, + direction: t.string, + }), + t.partial({ + search: t.string, + from: t.string, + to: t.string, + }), +]); + +export type GetCertsParams = t.TypeOf<typeof GetCertsParamsType>; + +export const CertMonitorType = t.partial({ + name: t.string, + id: t.string, + url: t.string, +}); + +export const CertType = t.intersection([ + t.type({ + monitors: t.array(CertMonitorType), + sha256: t.string, + }), + t.partial({ + not_after: t.string, + not_before: t.string, + common_name: t.string, + issuer: t.string, + sha1: t.string, + }), +]); + +export const CertResultType = t.type({ + certs: t.array(CertType), + total: t.number, +}); + +export type Cert = t.TypeOf<typeof CertType>; +export type CertMonitor = t.TypeOf<typeof CertMonitorType>; +export type CertResult = t.TypeOf<typeof CertResultType>; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts b/x-pack/plugins/uptime/common/runtime_types/common.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/common.ts rename to x-pack/plugins/uptime/common/runtime_types/common.ts diff --git a/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts new file mode 100644 index 0000000000000..da887cc5055c1 --- /dev/null +++ b/x-pack/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const CertStateThresholdsType = t.type({ + age: t.number, + expiration: t.number, +}); + +export const DynamicSettingsType = t.type({ + heartbeatIndices: t.string, + certThresholds: CertStateThresholdsType, +}); + +export const DynamicSettingsSaveType = t.intersection([ + t.type({ + success: t.boolean, + }), + t.partial({ + error: t.string, + }), +]); + +export type DynamicSettings = t.TypeOf<typeof DynamicSettingsType>; +export type DynamicSettingsSaveResponse = t.TypeOf<typeof DynamicSettingsSaveType>; +export type CertStateThresholds = t.TypeOf<typeof CertStateThresholdsType>; diff --git a/x-pack/plugins/uptime/common/runtime_types/index.ts b/x-pack/plugins/uptime/common/runtime_types/index.ts new file mode 100644 index 0000000000000..e80471bf8b56f --- /dev/null +++ b/x-pack/plugins/uptime/common/runtime_types/index.ts @@ -0,0 +1,14 @@ +/* + * 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 * from './alerts'; +export * from './certs'; +export * from './common'; +export * from './dynamic_settings'; +export * from './monitor'; +export * from './overview_filters'; +export * from './ping'; +export * from './snapshot'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/details.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/details.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/details.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/index.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/locations.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/locations.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/monitor/state.ts rename to x-pack/plugins/uptime/common/runtime_types/monitor/state.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/index.ts rename to x-pack/plugins/uptime/common/runtime_types/overview_filters/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts rename to x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts b/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/histogram.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts b/x-pack/plugins/uptime/common/runtime_types/ping/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/index.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts similarity index 97% rename from x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts rename to x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index ee14b298f3810..d8dc7fc89d94b 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -17,8 +17,8 @@ export const HttpResponseBodyType = t.partial({ export type HttpResponseBody = t.TypeOf<typeof HttpResponseBodyType>; export const TlsType = t.partial({ - certificate_not_valid_after: t.string, - certificate_not_valid_before: t.string, + not_after: t.string, + not_before: t.string, }); export type Tls = t.TypeOf<typeof TlsType>; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts b/x-pack/plugins/uptime/common/runtime_types/snapshot/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/index.ts rename to x-pack/plugins/uptime/common/runtime_types/snapshot/index.ts diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts b/x-pack/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts rename to x-pack/plugins/uptime/common/runtime_types/snapshot/snapshot_count.ts diff --git a/x-pack/plugins/uptime/common/types/index.ts b/x-pack/plugins/uptime/common/types/index.ts new file mode 100644 index 0000000000000..71ccd54dd3cdc --- /dev/null +++ b/x-pack/plugins/uptime/common/types/index.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +/** Represents the average monitor duration ms at a point in time. */ +export interface MonitorDurationAveragePoint { + /** The timeseries value for this point. */ + x: number; + /** The average duration ms for the monitor. */ + y?: number | null; +} + +export interface LocationDurationLine { + name: string; + + line: MonitorDurationAveragePoint[]; +} + +/** The data used to populate the monitor charts. */ +export interface MonitorDurationResult { + /** The average values for the monitor duration. */ + locationDurationLines: LocationDurationLine[]; +} + +export interface MonitorIdParam { + monitorId: string; +} diff --git a/x-pack/plugins/uptime/kibana.json b/x-pack/plugins/uptime/kibana.json index 6ec8a755ebea0..ce8b64ce07254 100644 --- a/x-pack/plugins/uptime/kibana.json +++ b/x-pack/plugins/uptime/kibana.json @@ -2,8 +2,16 @@ "configPath": ["xpack", "uptime"], "id": "uptime", "kibanaVersion": "kibana", - "requiredPlugins": ["alerting", "features", "licensing", "usageCollection"], + "optionalPlugins": ["capabilities", "data", "home"], + "requiredPlugins": [ + "alerting", + "embeddable", + "features", + "licensing", + "triggers_actions_ui", + "usageCollection" + ], "server": true, - "ui": false, + "ui": true, "version": "8.0.0" } diff --git a/x-pack/legacy/plugins/uptime/public/app.ts b/x-pack/plugins/uptime/public/app.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/app.ts rename to x-pack/plugins/uptime/public/app.ts diff --git a/x-pack/plugins/uptime/public/apps/index.ts b/x-pack/plugins/uptime/public/apps/index.ts new file mode 100644 index 0000000000000..65b80d08d4f20 --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { UptimePlugin } from './plugin'; diff --git a/x-pack/plugins/uptime/public/apps/plugin.ts b/x-pack/plugins/uptime/public/apps/plugin.ts new file mode 100644 index 0000000000000..719dac022dada --- /dev/null +++ b/x-pack/plugins/uptime/public/apps/plugin.ts @@ -0,0 +1,72 @@ +/* + * 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 { LegacyCoreStart, AppMountParameters } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; +import { UMFrontendLibs } from '../lib/lib'; +import { PLUGIN } from '../../common/constants'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; +import { getKibanaFrameworkAdapter } from '../lib/adapters/framework/new_platform_adapter'; +import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; +import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; + +export interface StartObject { + core: LegacyCoreStart; + plugins: any; +} + +export interface ClientPluginsSetup { + data: DataPublicPluginSetup; + home: HomePublicPluginSetup; + triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup; +} + +export interface ClientPluginsStart { + embeddable: EmbeddableStart; +} + +export class UptimePlugin implements Plugin<void, void, ClientPluginsSetup, ClientPluginsStart> { + constructor(_context: PluginInitializerContext) {} + + public async setup( + core: CoreSetup<ClientPluginsStart, unknown>, + plugins: ClientPluginsSetup + ): Promise<void> { + if (plugins.home) { + plugins.home.featureCatalogue.register({ + id: PLUGIN.ID, + title: PLUGIN.TITLE, + description: PLUGIN.DESCRIPTION, + icon: 'uptimeApp', + path: '/app/uptime#/', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + + core.application.register({ + appRoute: '/app/uptime#/', + id: PLUGIN.ID, + euiIconType: 'uptimeApp', + order: 8900, + title: PLUGIN.TITLE, + async mount(params: AppMountParameters) { + const [coreStart, corePlugins] = await core.getStartServices(); + const { element } = params; + const libs: UMFrontendLibs = { + framework: getKibanaFrameworkAdapter(coreStart, plugins, corePlugins), + }; + libs.framework.render(element); + return () => {}; + }, + }); + } + + public start(_start: CoreStart, _plugins: {}): void {} + + public stop(): void {} +} diff --git a/x-pack/legacy/plugins/uptime/public/apps/template.html b/x-pack/plugins/uptime/public/apps/template.html similarity index 100% rename from x-pack/legacy/plugins/uptime/public/apps/template.html rename to x-pack/plugins/uptime/public/apps/template.html diff --git a/x-pack/legacy/plugins/uptime/public/badge.ts b/x-pack/plugins/uptime/public/badge.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/badge.ts rename to x-pack/plugins/uptime/public/badge.ts diff --git a/x-pack/legacy/plugins/uptime/public/breadcrumbs.ts b/x-pack/plugins/uptime/public/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/breadcrumbs.ts rename to x-pack/plugins/uptime/public/breadcrumbs.ts diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap new file mode 100644 index 0000000000000..a79fb0f0d3deb --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_monitors.test.tsx.snap @@ -0,0 +1,134 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertMonitors renders expected elements for valid props 1`] = ` +<span> + <span> + <span + class="euiToolTipAnchor" + > + <button + class="euiLink euiLink--primary" + type="button" + > + <a + data-test-subj="monitor-page-link-bad-ssl-dashboard" + href="/monitor/YmFkLXNzbC1kYXNoYm9hcmQ=" + > + bad-ssl-dashboard + </a> + </button> + </span> + </span> + <span> + , + <span + class="euiToolTipAnchor" + > + <button + class="euiLink euiLink--primary" + type="button" + > + <a + data-test-subj="monitor-page-link-elastic-co" + href="/monitor/ZWxhc3RpYy1jbw==" + > + elastic + </a> + </button> + </span> + </span> + <span> + , + <span + class="euiToolTipAnchor" + > + <button + class="euiLink euiLink--primary" + type="button" + > + <a + data-test-subj="monitor-page-link-extended-validation" + href="/monitor/ZXh0ZW5kZWQtdmFsaWRhdGlvbg==" + > + extended-validation + </a> + </button> + </span> + </span> +</span> +`; + +exports[`CertMonitors shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <CertMonitors + monitors={ + Array [ + Object { + "id": "bad-ssl-dashboard", + "name": "", + "url": "https://badssl.com/dashboard/", + }, + Object { + "id": "elastic-co", + "name": "elastic", + "url": "https://www.elastic.co/", + }, + Object { + "id": "extended-validation", + "name": "", + "url": "https://extended-validation.badssl.com/", + }, + ] + } + /> +</ContextProvider> +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_search.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_search.test.tsx.snap new file mode 100644 index 0000000000000..0706198a099a5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_search.test.tsx.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificatesSearch renders expected elements for valid props 1`] = ` +.c0 { + min-width: 700px; +} + +<div + class="euiFormControlLayout" +> + <div + class="euiFormControlLayout__childrenWrapper" + > + <input + aria-label="Search certificates" + class="euiFieldSearch c0" + data-test-subj="uptimeCertSearch" + placeholder="Search certificates" + type="search" + /> + <div + class="euiFormControlLayoutIcons" + > + <span + class="euiFormControlLayoutCustomIcon" + > + <div + aria-hidden="true" + class="euiFormControlLayoutCustomIcon__icon" + data-euiicon-type="search" + /> + </span> + </div> + </div> +</div> +`; + +exports[`CertificatesSearch shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <CertificateSearch + setSearch={[MockFunction]} + /> +</ContextProvider> +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap new file mode 100644 index 0000000000000..089d272a075c6 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/cert_status.test.tsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertStatus renders expected elements for valid props 1`] = ` +<div + class="euiHealth" +> + <div + class="euiFlexGroup euiFlexGroup--gutterExtraSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + color="success" + data-euiicon-type="dot" + /> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <span> + OK + </span> + </div> + </div> +</div> +`; + +exports[`CertStatus shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <CertStatus + cert={ + Object { + "common_name": "github.com", + "issuer": "DigiCert SHA2 Extended Validation Server CA", + "monitors": Array [ + Object { + "id": "github", + "name": "", + "url": "https://github.com/", + }, + ], + "not_after": "2020-05-08T00:00:00.000Z", + "not_before": "2018-05-08T00:00:00.000Z", + "sha1": "ca06f56b258b7a0d4f2b05470939478651151984", + "sha256": "3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074", + } + } + /> +</ContextProvider> +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/certificates_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/certificates_list.test.tsx.snap new file mode 100644 index 0000000000000..fd90db793b26e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/certificates_list.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificateList shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <CertificateList + onChange={[MockFunction]} + page={ + Object { + "index": 0, + "size": 10, + } + } + sort={ + Object { + "direction": "asc", + "field": "not_after", + } + } + /> +</ContextProvider> +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap new file mode 100644 index 0000000000000..c9b17db5532f4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/__snapshots__/fingerprint_col.test.tsx.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FingerprintCol renders expected elements for valid props 1`] = ` +Array [ + .c1 .euiButtonEmpty__content { + padding-right: 0px; +} + +.c0 { + margin-right: 8px; +} + +<span + class="c0" + data-test-subj="CA06F56B258B7A0D4F2B05470939478651151984" + > + <span + class="euiToolTipAnchor" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary c1" + type="button" + > + <span + class="euiButtonEmpty__content" + > + <span + class="euiButtonEmpty__text" + > + SHA 1 + </span> + </span> + </button> + </span> + <span + class="euiToolTipAnchor" + > + <button + class="euiButtonIcon euiButtonIcon--primary" + title="Click to copy fingerprint value" + type="button" + > + <div + aria-hidden="true" + class="euiButtonIcon__icon" + data-euiicon-type="copy" + /> + </button> + </span> + </span>, + .c1 .euiButtonEmpty__content { + padding-right: 0px; +} + +.c0 { + margin-right: 8px; +} + +<span + class="c0" + data-test-subj="3111500C4A66012CDAE333EC3FCA1C9DDE45C954440E7EE413716BFF3663C074" + > + <span + class="euiToolTipAnchor" + > + <button + class="euiButtonEmpty euiButtonEmpty--primary c1" + type="button" + > + <span + class="euiButtonEmpty__content" + > + <span + class="euiButtonEmpty__text" + > + SHA 256 + </span> + </span> + </button> + </span> + <span + class="euiToolTipAnchor" + > + <button + class="euiButtonIcon euiButtonIcon--primary" + title="Click to copy fingerprint value" + type="button" + > + <div + aria-hidden="true" + class="euiButtonIcon__icon" + data-euiicon-type="copy" + /> + </button> + </span> + </span>, +] +`; + +exports[`FingerprintCol shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <FingerprintCol + cert={ + Object { + "common_name": "github.com", + "issuer": "DigiCert SHA2 Extended Validation Server CA", + "monitors": Array [ + Object { + "id": "github", + "name": "", + "url": "https://github.com/", + }, + ], + "not_after": "2020-05-08T00:00:00.000Z", + "not_before": "2018-05-08T00:00:00.000Z", + "sha1": "ca06f56b258b7a0d4f2b05470939478651151984", + "sha256": "3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074", + } + } + /> +</ContextProvider> +`; diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_monitors.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_monitors.test.tsx new file mode 100644 index 0000000000000..bc4c770d5cd24 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_monitors.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { CertMonitors } from '../cert_monitors'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; + +describe('CertMonitors', () => { + const certMons = [ + { name: '', id: 'bad-ssl-dashboard', url: 'https://badssl.com/dashboard/' }, + { name: 'elastic', id: 'elastic-co', url: 'https://www.elastic.co/' }, + { name: '', id: 'extended-validation', url: 'https://extended-validation.badssl.com/' }, + ]; + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter(<CertMonitors monitors={certMons} />)).toMatchSnapshot(); + }); + + it('renders expected elements for valid props', () => { + expect(renderWithRouter(<CertMonitors monitors={certMons} />)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_search.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_search.test.tsx new file mode 100644 index 0000000000000..27d3bb18f17c2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_search.test.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; +import { CertificateSearch } from '../cert_search'; + +describe('CertificatesSearch', () => { + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter(<CertificateSearch setSearch={jest.fn()} />)).toMatchSnapshot(); + }); + it('renders expected elements for valid props', () => { + expect(renderWithRouter(<CertificateSearch setSearch={jest.fn()} />)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_status.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_status.test.tsx new file mode 100644 index 0000000000000..6f91994fb89c4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/cert_status.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; +import { CertStatus } from '../cert_status'; +import * as redux from 'react-redux'; +import moment from 'moment'; + +describe('CertStatus', () => { + beforeEach(() => { + const spy = jest.spyOn(redux, 'useDispatch'); + spy.mockReturnValue(jest.fn()); + + const spy1 = jest.spyOn(redux, 'useSelector'); + spy1.mockReturnValue(true); + }); + + const cert = { + monitors: [{ name: '', id: 'github', url: 'https://github.com/' }], + not_after: '2020-05-08T00:00:00.000Z', + not_before: '2018-05-08T00:00:00.000Z', + issuer: 'DigiCert SHA2 Extended Validation Server CA', + sha1: 'ca06f56b258b7a0d4f2b05470939478651151984', + sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074', + common_name: 'github.com', + }; + + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter(<CertStatus cert={cert} />)).toMatchSnapshot(); + }); + + it('renders expected elements for valid props', () => { + cert.not_after = moment() + .add('4', 'months') + .toISOString(); + expect(renderWithRouter(<CertStatus cert={cert} />)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/certificates_list.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/certificates_list.test.tsx new file mode 100644 index 0000000000000..a8b60900ec65c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/certificates_list.test.tsx @@ -0,0 +1,26 @@ +/* + * 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 { shallowWithRouter } from '../../../lib'; +import { CertificateList, CertSort } from '../certificates_list'; + +describe('CertificateList', () => { + it('shallow renders expected elements for valid props', () => { + const page = { + index: 0, + size: 10, + }; + const sort: CertSort = { + field: 'not_after', + direction: 'asc', + }; + + expect( + shallowWithRouter(<CertificateList page={page} sort={sort} onChange={jest.fn()} />) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/__tests__/fingerprint_col.test.tsx b/x-pack/plugins/uptime/public/components/certificates/__tests__/fingerprint_col.test.tsx new file mode 100644 index 0000000000000..609b876e24849 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/__tests__/fingerprint_col.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { renderWithRouter, shallowWithRouter } from '../../../lib'; +import { FingerprintCol } from '../fingerprint_col'; +import moment from 'moment'; + +describe('FingerprintCol', () => { + const cert = { + monitors: [{ name: '', id: 'github', url: 'https://github.com/' }], + not_after: '2020-05-08T00:00:00.000Z', + not_before: '2018-05-08T00:00:00.000Z', + issuer: 'DigiCert SHA2 Extended Validation Server CA', + sha1: 'ca06f56b258b7a0d4f2b05470939478651151984', + sha256: '3111500c4a66012cdae333ec3fca1c9dde45c954440e7ee413716bff3663c074', + common_name: 'github.com', + }; + + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter(<FingerprintCol cert={cert} />)).toMatchSnapshot(); + }); + + it('renders expected elements for valid props', () => { + cert.not_after = moment() + .add('4', 'months') + .toISOString(); + + expect(renderWithRouter(<FingerprintCol cert={cert} />)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_monitors.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.tsx new file mode 100644 index 0000000000000..bfd309e59d013 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/cert_monitors.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; +import { CertMonitor } from '../../../common/runtime_types'; +import { MonitorPageLink } from '../common/monitor_page_link'; + +interface Props { + monitors: CertMonitor[]; +} + +export const CertMonitors: React.FC<Props> = ({ monitors }) => { + return ( + <span> + {monitors.map((mon: CertMonitor, ind: number) => ( + <span key={mon.id}> + {ind > 0 && ', '} + <EuiToolTip content={mon.url}> + <MonitorPageLink monitorId={mon.id!} linkParameters={''}> + {mon.name || mon.id} + </MonitorPageLink> + </EuiToolTip> + </span> + ))} + </span> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_search.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_search.tsx new file mode 100644 index 0000000000000..282b623f0f662 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/cert_search.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ChangeEvent } from 'react'; +import { EuiFieldSearch } from '@elastic/eui'; +import styled from 'styled-components'; +import * as labels from './translations'; + +const WrapFieldSearch = styled(EuiFieldSearch)` + min-width: 700px; +`; + +interface Props { + setSearch: (val: string) => void; +} + +export const CertificateSearch: React.FC<Props> = ({ setSearch }) => { + const onChange = (e: ChangeEvent<HTMLInputElement>) => { + setSearch(e.target.value); + }; + + return ( + <WrapFieldSearch + data-test-subj="uptimeCertSearch" + placeholder={labels.SEARCH_CERTS} + onChange={onChange} + isClearable={true} + aria-label={labels.SEARCH_CERTS} + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/cert_status.tsx b/x-pack/plugins/uptime/public/components/certificates/cert_status.tsx new file mode 100644 index 0000000000000..e7a86ce98fa3c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/cert_status.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiHealth } from '@elastic/eui'; +import { Cert } from '../../../common/runtime_types'; +import { useCertStatus } from '../../hooks'; +import * as labels from './translations'; +import { CERT_STATUS } from '../../../common/constants'; + +interface Props { + cert: Cert; +} + +export const CertStatus: React.FC<Props> = ({ cert }) => { + const certStatus = useCertStatus(cert?.not_after, cert?.not_before); + + if (certStatus === CERT_STATUS.EXPIRING_SOON) { + return ( + <EuiHealth color="warning"> + <span>{labels.EXPIRES_SOON}</span> + </EuiHealth> + ); + } + if (certStatus === CERT_STATUS.EXPIRED) { + return ( + <EuiHealth color="danger"> + <span>{labels.EXPIRED}</span> + </EuiHealth> + ); + } + + if (certStatus === CERT_STATUS.TOO_OLD) { + return ( + <EuiHealth color="danger"> + <span>{labels.TOO_OLD}</span> + </EuiHealth> + ); + } + + return ( + <EuiHealth color="success"> + <span>{labels.OK}</span> + </EuiHealth> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx new file mode 100644 index 0000000000000..595aa03c99c73 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/certificates_list.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import moment from 'moment'; +import { useSelector } from 'react-redux'; +import { Direction, EuiBasicTable } from '@elastic/eui'; +import { certificatesSelector } from '../../state/certificates/certificates'; +import { CertStatus } from './cert_status'; +import { CertMonitors } from './cert_monitors'; +import * as labels from './translations'; +import { Cert, CertMonitor } from '../../../common/runtime_types'; +import { FingerprintCol } from './fingerprint_col'; + +interface Page { + index: number; + size: number; +} + +export type CertFields = + | 'sha256' + | 'sha1' + | 'issuer' + | 'common_name' + | 'monitors' + | 'not_after' + | 'not_before'; + +export interface CertSort { + field: CertFields; + direction: Direction; +} + +interface Props { + page: Page; + sort: CertSort; + onChange: (page: Page, sort: CertSort) => void; +} + +export const CertificateList: React.FC<Props> = ({ page, sort, onChange }) => { + const certificates = useSelector(certificatesSelector); + + const onTableChange = (newVal: Partial<Props>) => { + onChange(newVal.page as Page, newVal.sort as CertSort); + }; + + const pagination = { + pageIndex: page.index, + pageSize: page.size, + totalItemCount: certificates?.total ?? 0, + pageSizeOptions: [10, 25, 50, 100], + hidePerPageOptions: false, + }; + + const columns = [ + { + field: 'not_after', + name: labels.STATUS_COL, + sortable: true, + render: (val: string, item: Cert) => <CertStatus cert={item} />, + }, + { + name: labels.COMMON_NAME_COL, + field: 'common_name', + sortable: true, + }, + { + name: labels.MONITORS_COL, + field: 'monitors', + render: (monitors: CertMonitor[]) => <CertMonitors monitors={monitors} />, + }, + { + name: labels.ISSUED_BY_COL, + field: 'issuer', + sortable: true, + }, + { + name: labels.VALID_UNTIL_COL, + field: 'not_after', + sortable: true, + render: (value: string) => moment(value).format('L LT'), + }, + { + name: labels.AGE_COL, + field: 'not_before', + sortable: true, + render: (value: string) => moment().diff(moment(value), 'days') + ' ' + labels.DAYS, + }, + { + name: labels.FINGERPRINTS_COL, + field: 'sha256', + render: (val: string, item: Cert) => <FingerprintCol cert={item} />, + }, + ]; + + return ( + <EuiBasicTable + columns={columns} + items={certificates?.certs ?? []} + pagination={pagination} + onChange={onTableChange} + sorting={{ + sort: { + field: sort.field, + direction: sort.direction, + }, + }} + /> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx new file mode 100644 index 0000000000000..4101573907924 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/fingerprint_col.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiButtonIcon, EuiCopy, EuiToolTip } from '@elastic/eui'; +import styled from 'styled-components'; +import { Cert } from '../../../common/runtime_types'; +import { COPY_FINGERPRINT } from './translations'; + +const EmptyButton = styled(EuiButtonEmpty)` + .euiButtonEmpty__content { + padding-right: 0px; + } +`; + +const Span = styled.span` + margin-right: 8px; +`; + +interface Props { + cert: Cert; +} + +export const FingerprintCol: React.FC<Props> = ({ cert }) => { + const ShaComponent = ({ text, val }: { text: string; val: string }) => { + return ( + <Span data-test-subj={val}> + <EuiToolTip content={val}> + <EmptyButton>{text} </EmptyButton> + </EuiToolTip> + <EuiCopy textToCopy={val ?? ''}> + {copy => <EuiButtonIcon onClick={copy} iconType="copy" title={COPY_FINGERPRINT} />} + </EuiCopy> + </Span> + ); + }; + return ( + <> + <ShaComponent text="SHA 1" val={cert?.sha1?.toUpperCase() ?? ''} /> + <ShaComponent text="SHA 256" val={cert?.sha256?.toUpperCase() ?? ''} /> + </> + ); +}; diff --git a/x-pack/plugins/uptime/public/components/certificates/index.ts b/x-pack/plugins/uptime/public/components/certificates/index.ts new file mode 100644 index 0000000000000..82f3f7ab67c91 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './cert_monitors'; +export * from './cert_search'; +export * from './cert_status'; +export * from './certificates_list'; +export * from './fingerprint_col'; diff --git a/x-pack/plugins/uptime/public/components/certificates/translations.ts b/x-pack/plugins/uptime/public/components/certificates/translations.ts new file mode 100644 index 0000000000000..518eddf1211a4 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/certificates/translations.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const OK = i18n.translate('xpack.uptime.certs.ok', { + defaultMessage: 'OK', +}); + +export const EXPIRED = i18n.translate('xpack.uptime.certs.expired', { + defaultMessage: 'Expired', +}); + +export const EXPIRES_SOON = i18n.translate('xpack.uptime.certs.expireSoon', { + defaultMessage: 'Expires soon', +}); + +export const SEARCH_CERTS = i18n.translate('xpack.uptime.certs.searchCerts', { + defaultMessage: 'Search certificates', +}); + +export const STATUS_COL = i18n.translate('xpack.uptime.certs.list.status', { + defaultMessage: 'Status', +}); + +export const TOO_OLD = i18n.translate('xpack.uptime.certs.list.status.old', { + defaultMessage: 'Too old', +}); + +export const COMMON_NAME_COL = i18n.translate('xpack.uptime.certs.list.commonName', { + defaultMessage: 'Common name', +}); + +export const MONITORS_COL = i18n.translate('xpack.uptime.certs.list.monitors', { + defaultMessage: 'Monitors', +}); + +export const ISSUED_BY_COL = i18n.translate('xpack.uptime.certs.list.issuedBy', { + defaultMessage: 'Issued by', +}); + +export const VALID_UNTIL_COL = i18n.translate('xpack.uptime.certs.list.validUntil', { + defaultMessage: 'Valid until', +}); + +export const AGE_COL = i18n.translate('xpack.uptime.certs.list.ageCol', { + defaultMessage: 'Age', +}); + +export const DAYS = i18n.translate('xpack.uptime.certs.list.days', { + defaultMessage: 'days', +}); + +export const FINGERPRINTS_COL = i18n.translate('xpack.uptime.certs.list.expirationDate', { + defaultMessage: 'Fingerprints', +}); + +export const COPY_FINGERPRINT = i18n.translate('xpack.uptime.certs.list.copyFingerprint', { + defaultMessage: 'Click to copy fingerprint value', +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/location_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/monitor_page_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/monitor_page_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/__tests__/__snapshots__/uptime_date_picker.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/location_link.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/location_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/location_link.test.tsx rename to x-pack/plugins/uptime/public/components/common/__tests__/location_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/monitor_page_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx rename to x-pack/plugins/uptime/public/components/common/__tests__/monitor_page_link.test.tsx index dd6e9c66d395b..36ebeb6615648 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_page_link.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/__tests__/monitor_page_link.test.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorPageLink } from '../monitor_page_link'; describe('MonitorPageLink component', () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx rename to x-pack/plugins/uptime/public/components/common/__tests__/uptime_date_picker.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_empty_state.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/chart_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/donut_chart_legend_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/duration_charts.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/__snapshots__/ping_histogram.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_empty_state.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/chart_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/donut_chart_legend_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/duration_charts.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts b/x-pack/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/get_tick_format.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/monitor_bar_series.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx b/x-pack/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx rename to x-pack/plugins/uptime/public/components/common/charts/__tests__/ping_histogram.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx b/x-pack/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx rename to x-pack/plugins/uptime/public/components/common/charts/annotation_tooltip.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_empty_state.tsx b/x-pack/plugins/uptime/public/components/common/charts/chart_empty_state.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/chart_empty_state.tsx rename to x-pack/plugins/uptime/public/components/common/charts/chart_empty_state.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx rename to x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/chart_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts b/x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts rename to x-pack/plugins/uptime/public/components/common/charts/chart_wrapper/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart.tsx rename to x-pack/plugins/uptime/public/components/common/charts/donut_chart.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx rename to x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx b/x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx rename to x-pack/plugins/uptime/public/components/common/charts/donut_chart_legend_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_chart.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/duration_chart.tsx rename to x-pack/plugins/uptime/public/components/common/charts/duration_chart.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx similarity index 94% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx rename to x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx index a31a143b71fd2..ceb1e700f293e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_bar_list.tsx @@ -9,11 +9,11 @@ import moment from 'moment'; import { AnnotationTooltipFormatter, RectAnnotation } from '@elastic/charts'; import { RectAnnotationDatum } from '@elastic/charts/dist/chart_types/xy_chart/utils/specs'; import { AnnotationTooltip } from './annotation_tooltip'; -import { ANOMALY_SEVERITY } from '../../../../../../../plugins/ml/common/constants/anomalies'; +import { ANOMALY_SEVERITY } from '../../../../../../plugins/ml/common/constants/anomalies'; import { getSeverityColor, getSeverityType, -} from '../../../../../../../plugins/ml/common/util/anomaly_utils'; +} from '../../../../../../plugins/ml/common/util/anomaly_utils'; interface Props { anomalies: any; diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx rename to x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts b/x-pack/plugins/uptime/public/components/common/charts/get_tick_format.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/get_tick_format.ts rename to x-pack/plugins/uptime/public/components/common/charts/get_tick_format.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts b/x-pack/plugins/uptime/public/components/common/charts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/index.ts rename to x-pack/plugins/uptime/public/components/common/charts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx b/x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx rename to x-pack/plugins/uptime/public/components/common/charts/monitor_bar_series.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/charts/ping_histogram.tsx rename to x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap rename to x-pack/plugins/uptime/public/components/common/higher_order/__tests__/__snapshots__/responsive_wrapper.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx rename to x-pack/plugins/uptime/public/components/common/higher_order/__tests__/responsive_wrapper.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts b/x-pack/plugins/uptime/public/components/common/higher_order/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/index.ts rename to x-pack/plugins/uptime/public/components/common/higher_order/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx b/x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx rename to x-pack/plugins/uptime/public/components/common/higher_order/responsive_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx b/x-pack/plugins/uptime/public/components/common/location_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/location_link.tsx rename to x-pack/plugins/uptime/public/components/common/location_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx b/x-pack/plugins/uptime/public/components/common/monitor_page_link.tsx similarity index 89% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx rename to x-pack/plugins/uptime/public/components/common/monitor_page_link.tsx index 803b399810508..77faa8edfc5c8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_page_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/monitor_page_link.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { FC } from 'react'; import { EuiLink } from '@elastic/eui'; import { Link } from 'react-router-dom'; -import React, { FunctionComponent } from 'react'; interface DetailPageLinkProps { /** @@ -19,7 +19,7 @@ interface DetailPageLinkProps { linkParameters: string | undefined; } -export const MonitorPageLink: FunctionComponent<DetailPageLinkProps> = ({ +export const MonitorPageLink: FC<DetailPageLinkProps> = ({ children, monitorId, linkParameters, diff --git a/x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/common/uptime_date_picker.tsx rename to x-pack/plugins/uptime/public/components/common/uptime_date_picker.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/__tests__/__snapshots__/monitor_charts.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/__tests__/monitor_charts.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/index.ts b/x-pack/plugins/uptime/public/components/monitor/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/index.ts rename to x-pack/plugins/uptime/public/components/monitor/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_map.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_missing.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/__snapshots__/location_status_tags.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_map.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_missing.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/__tests__/location_status_tags.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/__mocks__/mock.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/__tests__/map_config.test.ts diff --git a/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx new file mode 100644 index 0000000000000..06cdb07bd8bcd --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/embedded_map.tsx @@ -0,0 +1,129 @@ +/* + * 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, { useEffect, useState, useContext, useRef } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; +import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../../legacy/plugins/maps/public'; +import * as i18n from './translations'; +import { Location } from '../../../../../common/runtime_types'; +import { getLayerList } from './map_config'; +import { UptimeThemeContext, UptimeStartupPluginsContext } from '../../../../contexts'; +import { + isErrorEmbeddable, + ViewMode, + ErrorEmbeddable, +} from '../../../../../../../../src/plugins/embeddable/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/public'; + +export interface EmbeddedMapProps { + upPoints: LocationPoint[]; + downPoints: LocationPoint[]; +} + +export type LocationPoint = Required<Location>; + +const EmbeddedPanel = styled.div` + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + &&& .mapboxgl-canvas { + animation: none !important; + } +`; + +export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProps) => { + const { colors } = useContext(UptimeThemeContext); + const [embeddable, setEmbeddable] = useState<MapEmbeddable | ErrorEmbeddable | undefined>(); + const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null); + const { embeddable: embeddablePlugin } = useContext(UptimeStartupPluginsContext); + if (!embeddablePlugin) { + throw new Error('Embeddable start plugin not found'); + } + const factory: any = embeddablePlugin.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + + const input: MapEmbeddableInput = { + id: uuid.v4(), + filters: [], + hidePanelTitles: true, + refreshConfig: { + value: 0, + pause: false, + }, + viewMode: ViewMode.VIEW, + isLayerTOCOpen: false, + hideFilterActions: true, + // Zoom Lat/Lon values are set to make sure map is in center in the panel + // It wil also omit Greenland/Antarctica etc + mapCenter: { + lon: 11, + lat: 20, + zoom: 0, + }, + disableInteractive: true, + disableTooltipControl: true, + hideToolbarOverlay: true, + hideLayerControl: true, + hideViewControl: true, + }; + + useEffect(() => { + async function setupEmbeddable() { + if (!factory) { + throw new Error('Map embeddable not found.'); + } + const embeddableObject: any = await factory.create({ + ...input, + title: i18n.MAP_TITLE, + }); + + if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { + embeddableObject.setLayerList(getLayerList(upPoints, downPoints, colors)); + } + + setEmbeddable(embeddableObject); + } + setupEmbeddable(); + + // we want this effect to execute exactly once after the component mounts + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // update map layers based on points + useEffect(() => { + if (embeddable && !isErrorEmbeddable(embeddable)) { + embeddable.setLayerList(getLayerList(upPoints, downPoints, colors)); + } + }, [upPoints, downPoints, embeddable, colors]); + + // We can only render after embeddable has already initialized + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable, embeddableRoot]); + + return ( + <EmbeddedPanel> + <div + data-test-subj="xpack.uptime.locationMap.embeddedPanel" + className="embPanel__content" + ref={embeddableRoot} + /> + </EmbeddedPanel> + ); +}); + +EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/low_poly_layer.json diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/map_config.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts b/x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/location_map/embeddables/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/index.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_map.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_map.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_missing.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_missing.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx b/x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx rename to x-pack/plugins/uptime/public/components/monitor/location_map/location_status_tags.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/confirm_delete.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/license_info.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_integerations.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_job_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/__snapshots__/ml_manage_job.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/confirm_delete.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/license_info.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx index c0b02181dcce1..31cdcfac9feef 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_flyout.test.tsx @@ -9,7 +9,7 @@ import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MLFlyoutView } from '../ml_flyout'; import { UptimeSettingsContext } from '../../../../contexts'; import { CLIENT_DEFAULTS } from '../../../../../common/constants'; -import { License } from '../../../../../../../../plugins/licensing/common/license'; +import { License } from '../../../../../../../plugins/licensing/common/license'; const expiredLicense = new License({ signature: 'test signature', diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_integerations.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_job_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/__tests__/ml_manage_job.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/confirm_delete.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts b/x-pack/plugins/uptime/public/components/monitor/ml/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/index.ts rename to x-pack/plugins/uptime/public/components/monitor/ml/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/license_info.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/license_info.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/license_info.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index c3e8579ca4837..6eec30d405f76 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -19,7 +19,7 @@ import * as labels from './translations'; import { useKibana, KibanaReactNotifications, -} from '../../../../../../../../src/plugins/kibana_react/public'; +} from '../../../../../../../src/plugins/kibana_react/public'; import { MLFlyoutView } from './ml_flyout'; import { ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx index 4963a901f0ecc..7f19885c15406 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_integeration.tsx @@ -18,9 +18,9 @@ import { ConfirmJobDeletion } from './confirm_delete'; import { UptimeRefreshContext } from '../../../contexts'; import { getMLJobId } from '../../../state/api/ml_anomaly'; import * as labels from './translations'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ManageMLJobComponent } from './manage_ml_job'; -import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../../plugins/ml/common/types/data_recognizer'; import { useMonitorId } from '../../../hooks'; export const MLIntegrationComponent = () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/ml_job_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ml/translations.tsx rename to x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_charts.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_charts.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/index.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 7e39b977f1271..52d4f620f84b3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -20,7 +20,7 @@ import { } from '../../../state/selectors'; import { UptimeRefreshContext } from '../../../contexts'; import { getMLJobId } from '../../../state/api/ml_anomaly'; -import { JobStat } from '../../../../../../../plugins/ml/common/types/data_recognizer'; +import { JobStat } from '../../../../../ml/common/types/data_recognizer'; import { MonitorDurationComponent } from './monitor_duration'; import { MonitorIdParam } from '../../../../common/types'; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap new file mode 100644 index 0000000000000..2f4473ba54cf9 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SSL Certificate component renders 1`] = ` +Array [ + <div + class="euiText euiText--medium" + > + Certificate + </div>, + <div + class="euiSpacer euiSpacer--s" + />, + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" + > + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <div + aria-label="Expires in 2 months" + class="euiText euiText--small eui-displayInline euiText--constrainedWidth" + > + Expires + <span + class="euiBadge euiBadge--iconLeft" + style="background-color:#d3dae6;color:#000" + > + <span + class="euiBadge__content" + > + <span + class="euiBadge__text" + > + in 2 months + </span> + </span> + </span> + </div> + </div> + <div + class="euiFlexItem" + > + <a + class="eui-displayInline" + href="/certificates" + > + <div + class="euiText euiText--medium" + > + Certificate overview + </div> + </a> + </div> + </div>, +] +`; + +exports[`SSL Certificate component renders null if invalid date 1`] = `null`; + +exports[`SSL Certificate component shallow renders 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <MonitorSSLCertificate + tls={ + Object { + "not_after": "2020-04-24T11:41:38.200Z", + } + } + /> +</ContextProvider> +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/__snapshots__/status_by_location.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx similarity index 88% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx index 5fd32c808da42..b39a1cb537583 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/monitor_status.bar.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorStatusBarComponent } from '../monitor_status_bar'; import { Ping } from '../../../../../common/runtime_types'; +import * as redux from 'react-redux'; describe('MonitorStatusBar component', () => { let monitorStatus: Ping; @@ -46,6 +47,12 @@ describe('MonitorStatusBar component', () => { }, ], }; + + const spy = jest.spyOn(redux, 'useDispatch'); + spy.mockReturnValue(jest.fn()); + + const spy1 = jest.spyOn(redux, 'useSelector'); + spy1.mockReturnValue(true); }); it('renders duration in ms, not us', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx new file mode 100644 index 0000000000000..70a161a2394ec --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/ssl_certificate.test.tsx @@ -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 React from 'react'; +import moment from 'moment'; +import { EuiBadge } from '@elastic/eui'; +import { Tls } from '../../../../../common/runtime_types'; +import { MonitorSSLCertificate } from '../monitor_status_bar'; +import * as redux from 'react-redux'; +import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../../lib'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants'; + +describe('SSL Certificate component', () => { + let monitorTls: Tls; + + beforeEach(() => { + const dateInTwoMonths = moment() + .add(2, 'month') + .toString(); + + monitorTls = { + not_after: dateInTwoMonths, + }; + + const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); + useDispatchSpy.mockReturnValue(jest.fn()); + + const useSelectorSpy = jest.spyOn(redux, 'useSelector'); + useSelectorSpy.mockReturnValue({ settings: DYNAMIC_SETTINGS_DEFAULTS }); + }); + + it('shallow renders', () => { + const monitorTls1 = { + not_after: '2020-04-24T11:41:38.200Z', + }; + const component = shallowWithRouter(<MonitorSSLCertificate tls={monitorTls1} />); + expect(component).toMatchSnapshot(); + }); + + it('renders', () => { + const component = renderWithRouter(<MonitorSSLCertificate tls={monitorTls} />); + expect(component).toMatchSnapshot(); + }); + + it('renders null if invalid date', () => { + monitorTls = { + not_after: 'i am so invalid date', + }; + const component = renderWithRouter(<MonitorSSLCertificate tls={monitorTls} />); + expect(component).toMatchSnapshot(); + }); + + it('renders expiration date with a warning state if ssl expiry date is less than 5 days', () => { + const dateIn5Days = moment() + .add(5, 'day') + .toString(); + monitorTls = { + not_after: dateIn5Days, + }; + const component = mountWithRouter(<MonitorSSLCertificate tls={monitorTls} />); + + const badgeComponent = component.find(EuiBadge); + + expect(badgeComponent.props().color).toBe('warning'); + + const badgeComponentText = component.find('.euiBadge__text'); + expect(badgeComponentText.text()).toBe(moment(dateIn5Days).fromNow()); + + expect(badgeComponent.find('span.euiBadge--warning')).toBeTruthy(); + }); + + it('does not render the expiration date with a warning state if expiry date is greater than a month', () => { + const dateIn40Days = moment() + .add(40, 'day') + .toString(); + monitorTls = { + not_after: dateIn40Days, + }; + const component = mountWithRouter(<MonitorSSLCertificate tls={monitorTls} />); + + const badgeComponent = component.find(EuiBadge); + expect(badgeComponent.props().color).toBe('default'); + + const badgeComponentText = component.find('.euiBadge__text'); + expect(badgeComponentText.text()).toBe(moment(dateIn40Days).fromNow()); + + expect(badgeComponent.find('span.euiBadge--warning')).toHaveLength(0); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/__test__/status_by_location.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/index.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/index.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/index.ts diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx new file mode 100644 index 0000000000000..734a68f00f7de --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/ssl_certificate.tsx @@ -0,0 +1,100 @@ +/* + * 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 moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; +import { EuiSpacer, EuiText, EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Tls } from '../../../../../common/runtime_types'; +import { useCertStatus } from '../../../../hooks'; +import { CERT_STATUS, CERTIFICATES_ROUTE } from '../../../../../common/constants'; + +interface Props { + /** + * TLS information coming from monitor in ES heartbeat index + */ + tls: Tls | null | undefined; +} + +export const MonitorSSLCertificate = ({ tls }: Props) => { + const certStatus = useCertStatus(tls?.not_after); + + const isExpiringSoon = certStatus === CERT_STATUS.EXPIRING_SOON; + + const isExpired = certStatus === CERT_STATUS.EXPIRED; + + const relativeDate = moment(tls?.not_after).fromNow(); + + return certStatus ? ( + <> + <EuiText> + {i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.title', { + defaultMessage: 'Certificate', + })} + </EuiText> + <EuiSpacer size="s" /> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiText + className="eui-displayInline" + grow={false} + size="s" + aria-label={ + isExpired + ? i18n.translate( + 'xpack.uptime.monitorStatusBar.sslCertificateExpired.label.ariaLabel', + { + defaultMessage: 'Expired {validityDate}', + values: { validityDate: relativeDate }, + } + ) + : i18n.translate( + 'xpack.uptime.monitorStatusBar.sslCertificateExpiry.label.ariaLabel', + { + defaultMessage: 'Expires {validityDate}', + values: { validityDate: relativeDate }, + } + ) + } + > + {isExpired ? ( + <FormattedMessage + id="xpack.uptime.monitorStatusBar.sslCertificateExpired.badgeContent" + defaultMessage="Expired {emphasizedText}" + values={{ + emphasizedText: <EuiBadge color={'danger'}>{relativeDate}</EuiBadge>, + }} + /> + ) : ( + <FormattedMessage + id="xpack.uptime.monitorStatusBar.sslCertificateExpiry.badgeContent" + defaultMessage="Expires {emphasizedText}" + values={{ + emphasizedText: ( + <EuiBadge color={isExpiringSoon ? 'warning' : 'default'}> + {relativeDate} + </EuiBadge> + ), + }} + /> + )} + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <Link to={CERTIFICATES_ROUTE} className="eui-displayInline"> + <EuiText> + {i18n.translate('xpack.uptime.monitorStatusBar.sslCertificate.overview', { + defaultMessage: 'Certificate overview', + })} + </EuiText> + </Link> + </EuiFlexItem> + </EuiFlexGroup> + </> + ) : null; +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_bar_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/status_by_location.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/monitor_status_bar/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/status_details_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts b/x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts rename to x-pack/plugins/uptime/public/components/monitor/monitor_status_details/translations.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/index.ts b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/index.ts rename to x-pack/plugins/uptime/public/components/monitor/ping_histogram/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/doc_link_body.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/expanded_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/doc_link_body.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/expanded_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/__tests__/ping_list.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/doc_link_body.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/expanded_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/index.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/index.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/index.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/location_name.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/location_name.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/location_name.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx rename to x-pack/plugins/uptime/public/components/monitor/ping_list/ping_list_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/parsing_error_callout.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/__tests__/__snapshots__/snapshot_heading.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx b/x-pack/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx rename to x-pack/plugins/uptime/public/components/overview/__tests__/parsing_error_callout.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx b/x-pack/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx rename to x-pack/plugins/uptime/public/components/overview/__tests__/snapshot.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx b/x-pack/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx rename to x-pack/plugins/uptime/public/components/overview/__tests__/snapshot_heading.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/__tests__/alert_monitor_status.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alert_monitor_status.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/uptime_alerts_flyout_wrapper.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/index.ts b/x-pack/plugins/uptime/public/components/overview/alerts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/index.ts rename to x-pack/plugins/uptime/public/components/overview/alerts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index 04dfe4b3e3509..92fc015a276d3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } f import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { setAlertFlyoutVisible: (value: boolean) => void; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx similarity index 83% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx index 09e6dc72b7f98..262e1552e3660 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_context_provider.tsx @@ -5,8 +5,8 @@ */ import React from 'react'; -import { AlertsContextProvider } from '../../../../../../../plugins/triggers_actions_ui/public'; -import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { AlertsContextProvider } from '../../../../../../plugins/triggers_actions_ui/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const UptimeAlertsContextProvider: React.FC = ({ children }) => { const { diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx similarity index 90% rename from x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx rename to x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx index 13705e7d19293..9b1d3a73dc661 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/uptime_alerts_flyout_wrapper.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { AlertAdd } from '../../../../../../../plugins/triggers_actions_ui/public'; +import { AlertAdd } from '../../../../../../plugins/triggers_actions_ui/public'; interface Props { alertFlyoutVisible: boolean; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/data_or_index_missing.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/__snapshots__/empty_state.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/data_or_index_missing.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx similarity index 93% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx index acfe2ada5b68d..6328789d03f29 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { EmptyStateComponent } from '../empty_state'; import { StatesIndexStatus } from '../../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http'; -import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError, IHttpFetchError } from 'src/core/public'; import { mountWithRouter, shallowWithRouter } from '../../../../lib'; describe('EmptyState component', () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/data_or_index_missing.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx index 651103a34bf21..d38f203739cea 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx @@ -10,7 +10,7 @@ import { EmptyStateError } from './empty_state_error'; import { EmptyStateLoading } from './empty_state_loading'; import { DataOrIndexMissing } from './data_or_index_missing'; import { DynamicSettings, StatesIndexStatus } from '../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; interface EmptyStateProps { children: JSX.Element[] | JSX.Element; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx index 1135b969018a1..aa4040e319e0f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx @@ -7,7 +7,7 @@ import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import { IHttpFetchError } from '../../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; interface EmptyStateErrorProps { errors: IHttpFetchError[]; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx rename to x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_loading.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts b/x-pack/plugins/uptime/public/components/overview/empty_state/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/empty_state/index.ts rename to x-pack/plugins/uptime/public/components/overview/empty_state/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_status_button.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/parse_filter_map.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_popover.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/filter_status_button.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/parse_filter_map.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/toggle_selected_item.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_group_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/filter_status_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/index.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/parse_filter_map.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts b/x-pack/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts rename to x-pack/plugins/uptime/public/components/overview/filter_group/toggle_selected_item.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx rename to x-pack/plugins/uptime/public/components/overview/filter_group/uptime_filter_button.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/index.ts b/x-pack/plugins/uptime/public/components/overview/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/index.ts rename to x-pack/plugins/uptime/public/components/overview/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/index.ts rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/index.ts diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx new file mode 100644 index 0000000000000..792fff4c7cdca --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx @@ -0,0 +1,156 @@ +/* + * 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 { uniqueId, startsWith } from 'lodash'; +import { EuiCallOut } from '@elastic/eui'; +import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Typeahead } from './typeahead'; +import { useUrlParams } from '../../../hooks'; +import { + esKuery, + IIndexPattern, + QuerySuggestion, + DataPublicPluginSetup, +} from '../../../../../../../src/plugins/data/public'; + +const Container = styled.div` + margin-bottom: 10px; +`; + +interface State { + suggestions: QuerySuggestion[]; + isLoadingIndexPattern: boolean; +} + +function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) { + const ast = esKuery.fromKueryExpression(kuery); + return esKuery.toElasticsearchQuery(ast, indexPattern); +} + +interface Props { + 'aria-label': string; + autocomplete: DataPublicPluginSetup['autocomplete']; + 'data-test-subj': string; + loadIndexPattern: () => void; + indexPattern: IIndexPattern | null; + loading: boolean; +} + +export function KueryBarComponent({ + 'aria-label': ariaLabel, + autocomplete: autocompleteService, + 'data-test-subj': dataTestSubj, + loadIndexPattern, + indexPattern, + loading, +}: Props) { + useEffect(() => { + if (!indexPattern) { + loadIndexPattern(); + } + }, [indexPattern, loadIndexPattern]); + + const [state, setState] = useState<State>({ + suggestions: [], + isLoadingIndexPattern: true, + }); + const [isLoadingSuggestions, setIsLoadingSuggestions] = useState<boolean>(false); + let currentRequestCheck: string; + + const [getUrlParams, updateUrlParams] = useUrlParams(); + const { search: kuery } = getUrlParams(); + + const indexPatternMissing = loading && !indexPattern; + + async function onChange(inputValue: string, selectionStart: number) { + if (!indexPattern) { + return; + } + + setIsLoadingSuggestions(true); + setState({ ...state, suggestions: [] }); + + const currentRequest = uniqueId(); + currentRequestCheck = currentRequest; + + try { + const suggestions = ( + (await autocompleteService.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + query: inputValue, + selectionStart, + selectionEnd: selectionStart, + })) || [] + ) + .filter(suggestion => !startsWith(suggestion.text, 'span.')) + .slice(0, 15); + + if (currentRequest !== currentRequestCheck) { + return; + } + + setIsLoadingSuggestions(false); + setState({ ...state, suggestions }); + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error while fetching suggestions', e); + } + } + + function onSubmit(inputValue: string) { + if (indexPattern === null) { + return; + } + + try { + const res = convertKueryToEsQuery(inputValue, indexPattern); + if (!res) { + return; + } + + updateUrlParams({ search: inputValue.trim() }); + } catch (e) { + console.log('Invalid kuery syntax'); // eslint-disable-line no-console + } + } + + return ( + <Container> + <Typeahead + aria-label={ariaLabel} + data-test-subj={dataTestSubj} + disabled={indexPatternMissing} + isLoading={isLoadingSuggestions || loading} + initialValue={kuery} + onChange={onChange} + onSubmit={onSubmit} + suggestions={state.suggestions} + queryExample="" + /> + + {indexPatternMissing && ( + <EuiCallOut + style={{ display: 'inline-block', marginTop: '10px' }} + title={ + <div> + <FormattedMessage + id="xpack.uptime.kueryBar.indexPatternMissingWarningMessage" + // TODO: we need to determine the best instruction to provide if the index pattern is missing + defaultMessage="There was an error retrieving the index pattern." + /> + </div> + } + color="warning" + iconType="alert" + size="s" + /> + )} + </Container> + ); +} diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/click_outside.js diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.d.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/index.js diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js index 2b5ad9b59e39f..282cf1311a7a9 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestion.js @@ -14,6 +14,7 @@ import { units, fontSizes, unit, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../apm/public/style/variables'; import { tint } from 'polished'; import theme from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js similarity index 97% rename from x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js rename to x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js index 7fbabe71bcdb5..0fdf8c3400562 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/suggestions.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; import styled from 'styled-components'; import { isEmpty } from 'lodash'; import Suggestion from './suggestion'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { units, px, unit } from '../../../../../../apm/public/style/variables'; import { tint } from 'polished'; import theme from '@elastic/eui/dist/eui_theme_light.json'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap similarity index 93% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index ed5602323d254..0d6638e7070d6 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -556,13 +556,35 @@ exports[`MonitorList component renders the monitor list 1`] = ` <div class="euiPanel euiPanel--paddingMedium" > - <h5 - class="euiTitle euiTitle--xsmall" + <div + class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive" > - Monitor status - </h5> + <div + class="euiFlexItem" + > + <h5 + class="euiTitle euiTitle--xsmall" + > + Monitor status + </h5> + </div> + <div + class="euiFlexItem euiFlexItem--flexGrowZero" + > + <h5 + class="euiTitle euiTitle--xsmall" + > + <a + data-test-subj="uptimeCertificatesLink" + href="/certificates" + > + View certificates status + </a> + </h5> + </div> + </div> <div - class="euiSpacer euiSpacer--s" + class="euiSpacer euiSpacer--m" /> <div aria-label="Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying 2 items." @@ -639,9 +661,25 @@ exports[`MonitorList component renders the monitor list 1`] = ` </span> </div> </th> + <th + class="euiTableHeaderCell" + data-test-subj="tableHeaderCell_state.tls_3" + role="columnheader" + scope="col" + > + <div + class="euiTableCellContent euiTableCellContent--alignCenter" + > + <span + class="euiTableCellContent__text" + > + TLS Certificate + </span> + </div> + </th> <th class="euiTableHeaderCell euiTableHeaderCell--hideForMobile" - data-test-subj="tableHeaderCell_histogram.points_3" + data-test-subj="tableHeaderCell_histogram.points_4" role="columnheader" scope="col" > @@ -657,7 +695,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` </th> <td class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_monitor_id_4" + data-test-subj="tableHeaderCell_monitor_id_5" role="columnheader" scope="col" style="width:24px" @@ -791,6 +829,22 @@ exports[`MonitorList component renders the monitor list 1`] = ` </button> </div> </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + TLS Certificate + </div> + <div + class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" + > + <span> + - + </span> + </div> + </td> <td class="euiTableRowCell euiTableRowCell--hideForMobile" > @@ -951,6 +1005,22 @@ exports[`MonitorList component renders the monitor list 1`] = ` </button> </div> </td> + <td + class="euiTableRowCell" + > + <div + class="euiTableRowCell__mobileHeader euiTableRowCell--hideForDesktop" + > + TLS Certificate + </div> + <div + class="euiTableCellContent euiTableCellContent--alignCenter euiTableCellContent--overflowingContent" + > + <span> + - + </span> + </div> + </td> <td class="euiTableRowCell euiTableRowCell--hideForMobile" > diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list_status_column.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx index 9b1d799a23e37..9dd44f5176664 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx @@ -12,12 +12,19 @@ import { } from '../../../../../common/runtime_types'; import { MonitorListComponent } from '../monitor_list'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; +import * as redux from 'react-redux'; describe('MonitorList component', () => { let result: MonitorSummaryResult; let localStorageMock: any; beforeEach(() => { + const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); + useDispatchSpy.mockReturnValue(jest.fn()); + + const useSelectorSpy = jest.spyOn(redux, 'useSelector'); + useSelectorSpy.mockReturnValue(true); + localStorageMock = { getItem: jest.fn().mockImplementation(() => '25'), setItem: jest.fn(), diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_page_size_select.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/parse_timestamp.test.ts diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx new file mode 100644 index 0000000000000..d9380476eaf45 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import moment from 'moment'; +import styled from 'styled-components'; +import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import { Cert } from '../../../../common/runtime_types'; +import { useCertStatus } from '../../../hooks'; +import { EXPIRED, EXPIRES_SOON } from '../../certificates/translations'; +import { CERT_STATUS } from '../../../../common/constants'; + +interface Props { + cert: Cert; +} + +const Span = styled.span` + margin-left: 5px; + vertical-align: middle; +`; + +export const CertStatusColumn: React.FC<Props> = ({ cert }) => { + const certStatus = useCertStatus(cert?.not_after); + + const relativeDate = moment(cert?.not_after).fromNow(); + + const CertStatus = ({ color, text }: { color: string; text: string }) => { + return ( + <EuiToolTip content={moment(cert?.not_after).format('L LT')}> + <EuiText size="s"> + <EuiIcon color={color} type="lock" size="s" /> + <Span> + {text} {relativeDate} + </Span> + </EuiText> + </EuiToolTip> + ); + }; + + if (certStatus === CERT_STATUS.EXPIRING_SOON) { + return <CertStatus color="warning" text={EXPIRES_SOON} />; + } + if (certStatus === CERT_STATUS.EXPIRED) { + return <CertStatus color="danger" text={EXPIRED} />; + } + + return certStatus ? <CertStatus color="success" text={'Expires'} /> : <span>-</span>; +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/index.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/index.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx similarity index 85% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index 18e2e2437e147..616d8fbd76043 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -18,12 +18,13 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; +import { Link } from 'react-router-dom'; import { HistogramPoint, FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types'; import { MonitorSummary } from '../../../../common/runtime_types'; import { MonitorListStatusColumn } from './monitor_list_status_column'; import { ExpandedRowMap } from './types'; import { MonitorBarSeries } from '../../common/charts'; -import { MonitorPageLink } from './monitor_page_link'; +import { MonitorPageLink } from '../../common/monitor_page_link'; import { OverviewPageLink } from './overview_page_link'; import * as labels from './translations'; import { MonitorListPageSizeSelect } from './monitor_list_page_size_select'; @@ -31,6 +32,8 @@ import { MonitorListDrawer } from './monitor_list_drawer/list_drawer_container'; import { MonitorListProps } from './monitor_list_container'; import { MonitorList } from '../../../state/reducers/monitor_list'; import { useUrlParams } from '../../../hooks'; +import { CERTIFICATES_ROUTE } from '../../../../common/constants'; +import { CertStatusColumn } from './cert_status_column'; interface Props extends MonitorListProps { lastRefresh: number; @@ -143,6 +146,12 @@ export const MonitorListComponent: React.FC<Props> = ({ </TruncatedEuiLink> ), }, + { + align: 'center' as const, + field: 'state.tls', + name: labels.TLS_COLUMN_LABEL, + render: (tls: any) => <CertStatusColumn cert={tls?.[0]} />, + }, { align: 'center' as const, field: 'histogram.points', @@ -181,15 +190,32 @@ export const MonitorListComponent: React.FC<Props> = ({ return ( <EuiPanel> - <EuiTitle size="xs"> - <h5> - <FormattedMessage - id="xpack.uptime.monitorList.monitoringStatusTitle" - defaultMessage="Monitor status" - /> - </h5> - </EuiTitle> - <EuiSpacer size="s" /> + <EuiFlexGroup> + <EuiFlexItem> + <EuiTitle size="xs"> + <h5> + <FormattedMessage + id="xpack.uptime.monitorList.monitoringStatusTitle" + defaultMessage="Monitor status" + /> + </h5> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiTitle size="xs"> + <h5> + <Link to={CERTIFICATES_ROUTE} data-test-subj="uptimeCertificatesLink"> + <FormattedMessage + id="xpack.uptime.monitorList.viewCertificateTitle" + defaultMessage="View certificates status" + /> + </Link> + </h5> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="m" /> <EuiBasicTable aria-label={labels.getDescriptionLabel(items.length)} error={error?.message} @@ -202,13 +228,7 @@ export const MonitorListComponent: React.FC<Props> = ({ itemId="monitor_id" itemIdToExpandedRowMap={getExpandedRowMap()} items={items} - // TODO: not needed without sorting and pagination - // onChange={onChange} noItemsMessage={!!filters ? labels.NO_MONITOR_ITEM_SELECTED : labels.NO_DATA_MESSAGE} - // TODO: reintegrate pagination in future release - // pagination={pagination} - // TODO: reintegrate sorting in future release - // sorting={sorting} columns={columns} /> <EuiSpacer size="m" /> diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx index 5bfe6ff0c5b4f..6fb880e28c734 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -9,7 +9,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { getMonitorList } from '../../../state/actions'; import { FetchMonitorStatesQueryArgs } from '../../../../common/runtime_types'; import { monitorListSelector } from '../../../state/selectors'; -import { MonitorListComponent } from './index'; +import { MonitorListComponent } from './monitor_list'; export interface MonitorListProps { filters?: string; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_group.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/integration_link.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_list.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_status_row.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/most_recent_error.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_link.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_row.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/most_recent_error.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/list_drawer_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_list.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_status_row.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx index 1963a9c852b11..62f2f811aad98 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiText, EuiSpacer } from '@elastic/eui'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { MonitorPageLink } from '../monitor_page_link'; +import { MonitorPageLink } from '../../../common/monitor_page_link'; import { useGetUrlParams } from '../../../../hooks'; import { stringifyUrlParams } from '../../../../lib/helper/stringify_url_params'; import { MonitorError } from '../../../../../common/runtime_types'; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_page_size_select.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx rename to x-pack/plugins/uptime/public/components/overview/monitor_list/overview_page_link.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/parse_timestamp.ts diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts new file mode 100644 index 0000000000000..90dc854cc6904 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/translations.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STATUS_COLUMN_LABEL = i18n.translate('xpack.uptime.monitorList.statusColumnLabel', { + defaultMessage: 'Status', +}); + +export const NAME_COLUMN_LABEL = i18n.translate('xpack.uptime.monitorList.nameColumnLabel', { + defaultMessage: 'Name', +}); + +export const HISTORY_COLUMN_LABEL = i18n.translate( + 'xpack.uptime.monitorList.monitorHistoryColumnLabel', + { + defaultMessage: 'Downtime history', + } +); + +export const TLS_COLUMN_LABEL = i18n.translate('xpack.uptime.monitorList.tlsColumnLabel', { + defaultMessage: 'TLS Certificate', +}); + +export const getExpandDrawerLabel = (id: string) => { + return i18n.translate('xpack.uptime.monitorList.expandDrawerButton.ariaLabel', { + defaultMessage: 'Expand row for monitor with ID {id}', + description: 'The user can click a button on this table and expand further details.', + values: { + id, + }, + }); +}; + +export const getDescriptionLabel = (itemsLength: number) => { + return i18n.translate('xpack.uptime.monitorList.table.description', { + defaultMessage: + 'Monitor Status table with columns for Status, Name, URL, IP, Downtime History and Integrations. The table is currently displaying {length} items.', + values: { length: itemsLength }, + }); +}; + +export const NO_MONITOR_ITEM_SELECTED = i18n.translate( + 'xpack.uptime.monitorList.noItemForSelectedFiltersMessage', + { + defaultMessage: 'No monitors found for selected filter criteria', + description: + 'This message is show if there are no monitors in the table and some filter or search criteria exists', + } +); + +export const NO_DATA_MESSAGE = i18n.translate('xpack.uptime.monitorList.noItemMessage', { + defaultMessage: 'No uptime monitors found', + description: 'This message is shown if the monitors table is rendered but has no items.', +}); + +export const URL = i18n.translate('xpack.uptime.monitorList.table.url.name', { + defaultMessage: 'Url', +}); + +export const UP = i18n.translate('xpack.uptime.monitorList.statusColumn.upLabel', { + defaultMessage: 'Up', +}); + +export const DOWN = i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', { + defaultMessage: 'Down', +}); + +export const RESPONSE_ANOMALY_SCORE = i18n.translate( + 'xpack.uptime.monitorList.anomalyColumn.label', + { + defaultMessage: 'Response Anomaly Score', + } +); diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/types.ts b/x-pack/plugins/uptime/public/components/overview/monitor_list/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/monitor_list/types.ts rename to x-pack/plugins/uptime/public/components/overview/monitor_list/types.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/overview_container.tsx b/x-pack/plugins/uptime/public/components/overview/overview_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/overview_container.tsx rename to x-pack/plugins/uptime/public/components/overview/overview_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/parsing_error_callout.tsx b/x-pack/plugins/uptime/public/components/overview/parsing_error_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/parsing_error_callout.tsx rename to x-pack/plugins/uptime/public/components/overview/parsing_error_callout.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/index.ts b/x-pack/plugins/uptime/public/components/overview/snapshot/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/index.ts rename to x-pack/plugins/uptime/public/components/overview/snapshot/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot.tsx rename to x-pack/plugins/uptime/public/components/overview/snapshot/snapshot.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx rename to x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx rename to x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_heading.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/status_panel.tsx b/x-pack/plugins/uptime/public/components/overview/status_panel.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/overview/status_panel.tsx rename to x-pack/plugins/uptime/public/components/overview/status_panel.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap b/x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap similarity index 92% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap rename to x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap index 36bc9bb860211..96d472c91680d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap @@ -52,17 +52,18 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] = } > <CertificateExpirationForm - fieldErrors={Object {}} + fieldErrors={null} formFields={ Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 36, + "certThresholds": Object { + "age": 36, + "expiration": 7, }, "heartbeatIndices": "heartbeat-8*", } } isDisabled={false} + loading={false} onChange={[MockFunction]} /> </ContextProvider> diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap b/x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap similarity index 92% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap rename to x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap index 93151198c0f49..3b0c6d99fd9f8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap @@ -52,17 +52,18 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] = } > <IndicesForm - fieldErrors={Object {}} + fieldErrors={null} formFields={ Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 36, + "certThresholds": Object { + "age": 36, + "expiration": 7, }, "heartbeatIndices": "heartbeat-8*", } } isDisabled={false} + loading={false} onChange={[MockFunction]} /> </ContextProvider> diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx similarity index 87% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx rename to x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx index a3158f3d72445..3d4bd58aabe0f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx +++ b/x-pack/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx @@ -13,12 +13,13 @@ describe('CertificateForm', () => { expect( shallowWithRouter( <CertificateExpirationForm + loading={false} onChange={jest.fn()} formFields={{ heartbeatIndices: 'heartbeat-8*', - certificatesThresholds: { errorState: 7, warningState: 36 }, + certThresholds: { expiration: 7, age: 36 }, }} - fieldErrors={{}} + fieldErrors={null} isDisabled={false} /> ) diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx similarity index 86% rename from x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx rename to x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx index 654d51019d4e5..07a3bf81e39d8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx +++ b/x-pack/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx @@ -13,12 +13,13 @@ describe('CertificateForm', () => { expect( shallowWithRouter( <IndicesForm + loading={false} onChange={jest.fn()} formFields={{ heartbeatIndices: 'heartbeat-8*', - certificatesThresholds: { errorState: 7, warningState: 36 }, + certThresholds: { expiration: 7, age: 36 }, }} - fieldErrors={{}} + fieldErrors={null} isDisabled={false} /> ) diff --git a/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx new file mode 100644 index 0000000000000..209e38785e165 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx @@ -0,0 +1,162 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldNumber, + EuiText, + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { CertStateThresholds } from '../../../common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { SettingsFormProps } from '../../pages/settings'; + +interface ChangedValues { + heartbeatIndices?: string; + certThresholds?: Partial<CertStateThresholds>; +} + +export type OnFieldChangeType = (changedValues: ChangedValues) => void; + +export const CertificateExpirationForm: React.FC<SettingsFormProps> = ({ + loading, + onChange, + formFields, + fieldErrors, + isDisabled, +}) => ( + <> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.certificationSectionTitle" + defaultMessage="Certificate Expiration" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.expirationThreshold" + defaultMessage="Expiration/Age Thresholds" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.certificateThresholdDescription" + defaultMessage="Change the threshold for displaying and alerting on certificate errors. Note: this will affect any configured alerts." + /> + } + > + <EuiFormRow + describedByIds={['errorState']} + error={fieldErrors?.certificatesThresholds?.expirationThresholdError} + fullWidth + helpText={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.expirationThresholdDefaultValue" + defaultMessage="The default value is {defaultValue}" + values={{ + defaultValue: ( + <EuiCode>{DYNAMIC_SETTINGS_DEFAULTS.certThresholds.expiration}</EuiCode> + ), + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.expirationThresholdError} + label={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.errorStateLabel" + defaultMessage="Expiration threshold" + /> + } + > + <EuiFlexGroup> + <EuiFlexItem grow={2}> + <EuiFieldNumber + data-test-subj={`expiration-threshold-input-${loading ? 'loading' : 'loaded'}`} + fullWidth + disabled={isDisabled} + isLoading={loading} + value={formFields?.certThresholds?.expiration || ''} + onChange={e => + onChange({ + certThresholds: { + expiration: Number(e.target.value), + }, + }) + } + /> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiText> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.ageLimit.units.days" + defaultMessage="Days" + /> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + <EuiFormRow + describedByIds={['warningState']} + error={fieldErrors?.certificatesThresholds?.ageThresholdError} + fullWidth + helpText={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.ageThresholdDefaultValue" + defaultMessage="The default value is {defaultValue}" + values={{ + defaultValue: <EuiCode>{DYNAMIC_SETTINGS_DEFAULTS.certThresholds.age}</EuiCode>, + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.ageThresholdError} + label={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.warningStateLabel" + defaultMessage="Age limit" + /> + } + > + <EuiFlexGroup> + <EuiFlexItem grow={2}> + <EuiFieldNumber + data-test-subj={`age-threshold-input-${loading ? 'loading' : 'loaded'}`} + fullWidth + disabled={isDisabled} + isLoading={loading} + value={formFields?.certThresholds?.age || ''} + onChange={e => + onChange({ + certThresholds: { age: Number(e.currentTarget.value) }, + }) + } + /> + </EuiFlexItem> + <EuiFlexItem grow={1}> + <EuiText> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.ageLimit.units.days" + defaultMessage="Days" + /> + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFormRow> + </EuiDescribedFormGroup> + </> +); diff --git a/x-pack/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/plugins/uptime/public/components/settings/indices_form.tsx new file mode 100644 index 0000000000000..b9a5ca0e730de --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/indices_form.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDescribedFormGroup, + EuiFormRow, + EuiCode, + EuiFieldText, + EuiTitle, + EuiSpacer, +} from '@elastic/eui'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { SettingsFormProps } from '../../pages/settings'; + +export const IndicesForm: React.FC<SettingsFormProps> = ({ + onChange, + loading, + formFields, + fieldErrors, + isDisabled, +}) => ( + <> + <EuiTitle size="s"> + <h3> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.indicesSectionTitle" + defaultMessage="Indices" + /> + </h3> + </EuiTitle> + <EuiSpacer size="m" /> + <EuiDescribedFormGroup + title={ + <h4> + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesTitle" + defaultMessage="Uptime indices" + /> + </h4> + } + description={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesDescription" + defaultMessage="Index pattern for matching indices that contain Heartbeat data" + /> + } + > + <EuiFormRow + describedByIds={['heartbeatIndices']} + error={fieldErrors?.heartbeatIndices} + fullWidth + helpText={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesDefaultValue" + defaultMessage="The default value is {defaultValue}" + values={{ + defaultValue: <EuiCode>{DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices}</EuiCode>, + }} + /> + } + isInvalid={!!fieldErrors?.heartbeatIndices} + label={ + <FormattedMessage + id="xpack.uptime.sourceConfiguration.heartbeatIndicesLabel" + defaultMessage="Heartbeat indices" + /> + } + > + <EuiFieldText + data-test-subj={`heartbeat-indices-input-${loading ? 'loading' : 'loaded'}`} + fullWidth + disabled={isDisabled} + isLoading={loading} + value={formFields?.heartbeatIndices || ''} + onChange={(event: any) => onChange({ heartbeatIndices: event.currentTarget.value })} + /> + </EuiFormRow> + </EuiDescribedFormGroup> + </> +); diff --git a/x-pack/plugins/uptime/public/contexts/index.ts b/x-pack/plugins/uptime/public/contexts/index.ts new file mode 100644 index 0000000000000..243a25c26901a --- /dev/null +++ b/x-pack/plugins/uptime/public/contexts/index.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 { UptimeRefreshContext, UptimeRefreshContextProvider } from './uptime_refresh_context'; +export { + UptimeSettingsContextValues, + UptimeSettingsContext, + UptimeSettingsContextProvider, +} from './uptime_settings_context'; +export { UptimeThemeContextProvider, UptimeThemeContext } from './uptime_theme_context'; +export { + UptimeStartupPluginsContext, + UptimeStartupPluginsContextProvider, +} from './uptime_startup_plugins_context'; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_refresh_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_refresh_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_refresh_context.tsx diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx index 137846de103b4..4fabf3f2ed497 100644 --- a/x-pack/legacy/plugins/uptime/public/contexts/uptime_settings_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_settings_context.tsx @@ -9,7 +9,7 @@ import { UptimeAppProps } from '../uptime_app'; import { CLIENT_DEFAULTS, CONTEXT_DEFAULTS } from '../../common/constants'; import { CommonlyUsedRange } from '../components/common/uptime_date_picker'; import { useGetUrlParams } from '../hooks'; -import { ILicense } from '../../../../../plugins/licensing/common/types'; +import { ILicense } from '../../../../plugins/licensing/common/types'; export interface UptimeSettingsContextValues { basePath: string; diff --git a/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.tsx new file mode 100644 index 0000000000000..e516ff44aa12a --- /dev/null +++ b/x-pack/plugins/uptime/public/contexts/uptime_startup_plugins_context.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 React, { createContext } from 'react'; +import { ClientPluginsStart } from '../apps/plugin'; + +export const UptimeStartupPluginsContext = createContext<Partial<ClientPluginsStart>>({}); + +export const UptimeStartupPluginsContextProvider: React.FC<Partial<ClientPluginsStart>> = ({ + children, + ...props +}) => <UptimeStartupPluginsContext.Provider value={{ ...props }} children={children} />; diff --git a/x-pack/legacy/plugins/uptime/public/contexts/uptime_theme_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/contexts/uptime_theme_context.tsx rename to x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap b/x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap rename to x-pack/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx b/x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx similarity index 95% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx rename to x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx index 1ce00fe7ce3af..306919015fcb1 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/uptime/public/hooks/__tests__/use_breadcrumbs.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Route } from 'react-router-dom'; import { mountWithRouter } from '../../lib'; import { OVERVIEW_ROUTE } from '../../../common/constants'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { UptimeUrlParams, getSupportedUrlParams } from '../../lib/helper'; import { makeBaseBreadcrumb, useBreadcrumbs } from '../use_breadcrumbs'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx b/x-pack/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx rename to x-pack/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx diff --git a/x-pack/plugins/uptime/public/hooks/index.ts b/x-pack/plugins/uptime/public/hooks/index.ts new file mode 100644 index 0000000000000..b92d2d4cf7df5 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './use_monitor'; +export * from './use_url_params'; +export * from './use_telemetry'; +export * from './update_kuery_string'; +export * from './use_cert_status'; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts rename to x-pack/plugins/uptime/public/hooks/update_kuery_string.ts index ab4d6f75849e8..492d2bab5bb80 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/plugins/uptime/public/hooks/update_kuery_string.ts @@ -5,7 +5,7 @@ */ import { combineFiltersAndUserSearch, stringifyKueries } from '../lib/helper'; -import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; const getKueryString = (urlFilters: string): string => { let kueryString = ''; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts rename to x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts index d1cc8e1897386..182c6b0114128 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { useEffect } from 'react'; import { UptimeUrlParams } from '../lib/helper'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { useUrlParams } from '.'; export const makeBaseBreadcrumb = (params?: UptimeUrlParams): ChromeBreadcrumb => { diff --git a/x-pack/plugins/uptime/public/hooks/use_cert_status.ts b/x-pack/plugins/uptime/public/hooks/use_cert_status.ts new file mode 100644 index 0000000000000..cb54b05af9dd1 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_cert_status.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { useSelector } from 'react-redux'; +import { selectDynamicSettings } from '../state/selectors'; +import { CERT_STATUS } from '../../common/constants'; + +export const useCertStatus = (expiryDate?: string, issueDate?: string) => { + const dss = useSelector(selectDynamicSettings); + + const expiryThreshold = dss.settings?.certThresholds?.expiration; + + const ageThreshold = dss.settings?.certThresholds?.age; + + const certValidityDate = new Date(expiryDate ?? ''); + + const isValidDate = !isNaN(certValidityDate.valueOf()); + + if (!isValidDate) { + return false; + } + + const isExpiringSoon = moment(certValidityDate).diff(moment(), 'days') < expiryThreshold!; + + const isTooOld = moment().diff(moment(issueDate), 'days') > ageThreshold!; + + const isExpired = moment(certValidityDate) < moment(); + + if (isExpired) { + return CERT_STATUS.EXPIRED; + } + + return isExpiringSoon + ? CERT_STATUS.EXPIRING_SOON + : isTooOld + ? CERT_STATUS.TOO_OLD + : CERT_STATUS.OK; +}; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_monitor.ts b/x-pack/plugins/uptime/public/hooks/use_monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/use_monitor.ts rename to x-pack/plugins/uptime/public/hooks/use_monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts similarity index 97% rename from x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts rename to x-pack/plugins/uptime/public/hooks/use_telemetry.ts index a2012b8ac5636..9b4a441fe5ade 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_telemetry.ts +++ b/x-pack/plugins/uptime/public/hooks/use_telemetry.ts @@ -13,6 +13,7 @@ export enum UptimePage { Overview = 'Overview', Monitor = 'Monitor', Settings = 'Settings', + Certificates = 'Certificates', NotFound = '__not-found__', } diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/plugins/uptime/public/hooks/use_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts rename to x-pack/plugins/uptime/public/hooks/use_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/icons/heartbeat_white.svg b/x-pack/plugins/uptime/public/icons/heartbeat_white.svg similarity index 100% rename from x-pack/legacy/plugins/uptime/public/icons/heartbeat_white.svg rename to x-pack/plugins/uptime/public/icons/heartbeat_white.svg diff --git a/x-pack/plugins/uptime/public/index.ts b/x-pack/plugins/uptime/public/index.ts new file mode 100644 index 0000000000000..48cf2c90ad07b --- /dev/null +++ b/x-pack/plugins/uptime/public/index.ts @@ -0,0 +1,11 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/public'; +import { UptimePlugin } from './apps'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new UptimePlugin(initializerContext); diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts b/x-pack/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts rename to x-pack/plugins/uptime/public/lib/adapters/framework/capabilities_adapter.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx similarity index 86% rename from x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx rename to x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx index 71c73bf5ba5d4..f7f9e056f41af 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx +++ b/x-pack/plugins/uptime/public/lib/adapters/framework/new_platform_adapter.tsx @@ -9,7 +9,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { get } from 'lodash'; import { i18n as i18nFormatter } from '@kbn/i18n'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; import { alertTypeInitializers } from '../../alert_types'; import { UptimeApp, UptimeAppProps } from '../../../uptime_app'; import { getIntegratedAppAvailability } from './capabilities_adapter'; @@ -20,10 +19,12 @@ import { DEFAULT_TIMEPICKER_QUICK_RANGES, } from '../../../../common/constants'; import { UMFrameworkAdapter } from '../../lib'; +import { ClientPluginsStart, ClientPluginsSetup } from '../../../apps/plugin'; export const getKibanaFrameworkAdapter = ( core: CoreStart, - plugins: PluginsSetup + plugins: ClientPluginsSetup, + startPlugins: ClientPluginsStart ): UMFrameworkAdapter => { const { application: { capabilities }, @@ -35,14 +36,15 @@ export const getKibanaFrameworkAdapter = ( const { data: { autocomplete }, - // TODO: after NP migration we can likely fix this typing problem - // @ts-ignore we don't control this type triggers_actions_ui, } = plugins; - alertTypeInitializers.forEach(init => - triggers_actions_ui.alertTypeRegistry.register(init({ autocomplete })) - ); + alertTypeInitializers.forEach(init => { + const alertInitializer = init({ autocomplete }); + if (!triggers_actions_ui.alertTypeRegistry.has(alertInitializer.id)) { + triggers_actions_ui.alertTypeRegistry.register(init({ autocomplete })); + } + }); let breadcrumbs: ChromeBreadcrumb[] = []; core.chrome.getBreadcrumbs$().subscribe((nextBreadcrumbs?: ChromeBreadcrumb[]) => { @@ -68,6 +70,7 @@ export const getKibanaFrameworkAdapter = ( isLogsAvailable: logs, kibanaBreadcrumbs: breadcrumbs, plugins, + startPlugins, renderGlobalHelpControls: () => setHelpExtension({ appName: i18nFormatter.translate('xpack.uptime.header.appName', { diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts rename to x-pack/plugins/uptime/public/lib/alert_types/__tests__/monitor_status.test.ts diff --git a/x-pack/plugins/uptime/public/lib/alert_types/index.ts b/x-pack/plugins/uptime/public/lib/alert_types/index.ts new file mode 100644 index 0000000000000..f7ab254ffe675 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/alert_types/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { initMonitorStatusAlertType } from './monitor_status'; + +export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel; + +export const alertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType]; diff --git a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx similarity index 88% rename from x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx rename to x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx index 0624d20b197c0..e7695fb1cbb56 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.tsx @@ -8,17 +8,12 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import React from 'react'; import DateMath from '@elastic/datemath'; import { isRight } from 'fp-ts/lib/Either'; -import { - AlertTypeModel, - ValidationResult, - // TODO: this typing issue should be resolved after NP migration - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../plugins/triggers_actions_ui/public/types'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; import { AlertTypeInitializer } from '.'; import { StatusCheckExecutorParamsType } from '../../../common/runtime_types'; import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers'; -export const validate = (alertParams: any): ValidationResult => { +export const validate = (alertParams: any) => { const errors: Record<string, any> = {}; const decoded = StatusCheckExecutorParamsType.decode(alertParams); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_kueries.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/__tests__/__snapshots__/stringify_url_params.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/combine_filters_and_user_search.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/convert_measurements.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/series_has_down_values.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_kueries.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts b/x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts rename to x-pack/plugins/uptime/public/lib/helper/__tests__/stringify_url_params.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts b/x-pack/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/__tests__/get_label_format.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts b/x-pack/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/__tests__/is_within_current_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts b/x-pack/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/get_chart_date_label.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts b/x-pack/plugins/uptime/public/lib/helper/charts/get_label_format.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/get_label_format.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/get_label_format.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/index.ts b/x-pack/plugins/uptime/public/lib/helper/charts/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/index.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts b/x-pack/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts rename to x-pack/plugins/uptime/public/lib/helper/charts/is_within_current_date.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts b/x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts rename to x-pack/plugins/uptime/public/lib/helper/combine_filters_and_user_search.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts b/x-pack/plugins/uptime/public/lib/helper/convert_measurements.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/convert_measurements.ts rename to x-pack/plugins/uptime/public/lib/helper/convert_measurements.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts b/x-pack/plugins/uptime/public/lib/helper/get_title.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/get_title.ts rename to x-pack/plugins/uptime/public/lib/helper/get_title.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/helper_with_router.tsx b/x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/helper_with_router.tsx rename to x-pack/plugins/uptime/public/lib/helper/helper_with_router.tsx diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/index.ts b/x-pack/plugins/uptime/public/lib/helper/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/index.ts rename to x-pack/plugins/uptime/public/lib/helper/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/add_base_path.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/build_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_apm_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/index.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/observability_integration/index.ts rename to x-pack/plugins/uptime/public/lib/helper/observability_integration/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts b/x-pack/plugins/uptime/public/lib/helper/series_has_down_values.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/series_has_down_values.ts rename to x-pack/plugins/uptime/public/lib/helper/series_has_down_values.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_kueries.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/stringify_kueries.ts rename to x-pack/plugins/uptime/public/lib/helper/stringify_kueries.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/stringify_url_params.ts rename to x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_absolute_date.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_autorefresh_interval.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_is_paused.test.ts diff --git a/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts new file mode 100644 index 0000000000000..a5c2168378089 --- /dev/null +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/__tests__/parse_url_int.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseUrlInt } from '../parse_url_int'; + +describe('parseUrlInt', () => { + it('parses a number', () => { + const result = parseUrlInt('23', 50); + expect(result).toBe(23); + }); + + it('returns default value for empty string', () => { + const result = parseUrlInt('', 50); + expect(result).toBe(50); + }); + + it('returns default value for non-numeric string', () => { + const result = parseUrlInt('abc', 50); + expect(result).toBe(50); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/index.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/index.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_absolute_date.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_is_paused.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts similarity index 87% rename from x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts rename to x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts index b1a4d0a2aba0d..0e5363527b516 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/parse_url_int.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// TODO: add a comment explaining the purpose of this function export const parseUrlInt = (value: string | undefined, defaultValue: number): number => { const parsed = parseInt(value || '', 10); return isNaN(parsed) ? defaultValue : parsed; diff --git a/x-pack/legacy/plugins/uptime/public/lib/index.ts b/x-pack/plugins/uptime/public/lib/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/index.ts rename to x-pack/plugins/uptime/public/lib/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/lib/lib.ts b/x-pack/plugins/uptime/public/lib/lib.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/lib/lib.ts rename to x-pack/plugins/uptime/public/lib/lib.ts diff --git a/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap new file mode 100644 index 0000000000000..53b2ea27864bc --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/certificates.test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CertificatesPage shallow renders expected elements for valid props 1`] = ` +<ContextProvider + value={ + Object { + "history": Object { + "action": "POP", + "block": [Function], + "canGo": [Function], + "createHref": [Function], + "entries": Array [ + Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + ], + "go": [Function], + "goBack": [Function], + "goForward": [Function], + "index": 0, + "length": 1, + "listen": [Function], + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "push": [Function], + "replace": [Function], + }, + "location": Object { + "hash": "", + "key": "TestKeyForTesting", + "pathname": "/", + "search": "", + "state": undefined, + }, + "match": Object { + "isExact": true, + "params": Object {}, + "path": "/", + "url": "/", + }, + "staticContext": undefined, + } + } +> + <CertificatesPage /> +</ContextProvider> +`; diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/monitor.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/not_found.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/overview.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap b/x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap rename to x-pack/plugins/uptime/public/pages/__tests__/__snapshots__/page_header.test.tsx.snap diff --git a/x-pack/plugins/uptime/public/pages/__tests__/certificates.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/certificates.test.tsx new file mode 100644 index 0000000000000..8dfb6fba3d6be --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/__tests__/certificates.test.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 React from 'react'; +import { shallowWithRouter } from '../../lib'; +import { CertificatesPage } from '../certificates'; + +describe('CertificatesPage', () => { + it('shallow renders expected elements for valid props', () => { + expect(shallowWithRouter(<CertificatesPage />)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/monitor.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/monitor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/monitor.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/monitor.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/not_found.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/not_found.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/not_found.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/not_found.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/overview.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/overview.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/overview.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx b/x-pack/plugins/uptime/public/pages/__tests__/page_header.test.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/__tests__/page_header.test.tsx rename to x-pack/plugins/uptime/public/pages/__tests__/page_header.test.tsx diff --git a/x-pack/plugins/uptime/public/pages/certificates.tsx b/x-pack/plugins/uptime/public/pages/certificates.tsx new file mode 100644 index 0000000000000..d6c1b8e2b4568 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/certificates.tsx @@ -0,0 +1,136 @@ +/* + * 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 { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; +import React, { useContext, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useTrackPageview } from '../../../observability/public'; +import { PageHeader } from './page_header'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../common/constants'; +import { getDynamicSettings } from '../state/actions/dynamic_settings'; +import { UptimeRefreshContext } from '../contexts'; +import * as labels from './translations'; +import { UptimePage, useUptimeTelemetry } from '../hooks'; +import { certificatesSelector, getCertificatesAction } from '../state/certificates/certificates'; +import { CertificateList, CertificateSearch, CertSort } from '../components/certificates'; + +const DEFAULT_PAGE_SIZE = 10; +const LOCAL_STORAGE_KEY = 'xpack.uptime.certList.pageSize'; +const getPageSizeValue = () => { + const value = parseInt(localStorage.getItem(LOCAL_STORAGE_KEY) ?? '', 10); + if (isNaN(value)) { + return DEFAULT_PAGE_SIZE; + } + return value; +}; + +export const CertificatesPage: React.FC = () => { + useUptimeTelemetry(UptimePage.Certificates); + + useTrackPageview({ app: 'uptime', path: 'certificates' }); + useTrackPageview({ app: 'uptime', path: 'certificates', delay: 15000 }); + + useBreadcrumbs([{ text: 'Certificates' }]); + + const [page, setPage] = useState({ index: 0, size: getPageSizeValue() }); + const [sort, setSort] = useState<CertSort>({ + field: 'not_after', + direction: 'asc', + }); + const [search, setSearch] = useState(''); + + const dispatch = useDispatch(); + + const { lastRefresh, refreshApp } = useContext(UptimeRefreshContext); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + + useEffect(() => { + dispatch( + getCertificatesAction.get({ + search, + ...page, + sortBy: sort.field, + direction: sort.direction, + }) + ); + }, [dispatch, page, search, sort.direction, sort.field, lastRefresh]); + + const certificates = useSelector(certificatesSelector); + + return ( + <> + <EuiFlexGroup> + <EuiFlexItem grow={false} style={{ marginRight: 'auto', alignSelf: 'center' }}> + <Link to={OVERVIEW_ROUTE} data-test-subj="uptimeCertificatesToOverviewLink"> + <EuiButtonEmpty size="s" color="primary" iconType="arrowLeft"> + {labels.RETURN_TO_OVERVIEW} + </EuiButtonEmpty> + </Link> + </EuiFlexItem> + <EuiFlexItem grow={false} style={{ alignSelf: 'center' }}> + <Link to={SETTINGS_ROUTE} data-test-subj="uptimeCertificatesToOverviewLink"> + <EuiButtonEmpty size="s" color="primary" iconType="gear"> + {labels.SETTINGS_ON_CERT} + </EuiButtonEmpty> + </Link> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + fill + iconType="refresh" + onClick={() => { + refreshApp(); + }} + data-test-subj="superDatePickerApplyTimeButton" + > + {labels.REFRESH_CERT} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + + <EuiSpacer size="m" /> + <EuiPanel> + <PageHeader + headingText={ + <FormattedMessage + id="xpack.uptime.certificates.heading" + defaultMessage="TLS Certificates ({total})" + values={{ + total: <span data-test-subj="uptimeCertTotal">{certificates?.total ?? 0}</span>, + }} + /> + } + datePicker={false} + /> + <EuiSpacer size="m" /> + <CertificateSearch setSearch={setSearch} /> + <EuiSpacer size="m" /> + <CertificateList + page={page} + onChange={(pageVal, sortVal) => { + setPage(pageVal); + setSort(sortVal); + localStorage.setItem(LOCAL_STORAGE_KEY, pageVal.size.toString()); + }} + sort={sort} + /> + </EuiPanel> + </> + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/pages/index.ts b/x-pack/plugins/uptime/public/pages/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/index.ts rename to x-pack/plugins/uptime/public/pages/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor.tsx similarity index 76% rename from x-pack/legacy/plugins/uptime/public/pages/monitor.tsx rename to x-pack/plugins/uptime/public/pages/monitor.tsx index 4495be9b24dc1..fc796e679a2f6 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor.tsx @@ -5,18 +5,24 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; -import { useSelector } from 'react-redux'; -import { useTrackPageview } from '../../../../../plugins/observability/public'; +import React, { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import { monitorStatusSelector } from '../state/selectors'; import { PageHeader } from './page_header'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useTrackPageview } from '../../../observability/public'; import { useMonitorId, useUptimeTelemetry, UptimePage } from '../hooks'; import { MonitorCharts } from '../components/monitor'; -import { MonitorStatusDetails } from '../components/monitor'; -import { PingList } from '../components/monitor'; +import { MonitorStatusDetails, PingList } from '../components/monitor'; +import { getDynamicSettings } from '../state/actions/dynamic_settings'; export const MonitorPage: React.FC = () => { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + const monitorId = useMonitorId(); const selectedMonitor = useSelector(monitorStatusSelector); diff --git a/x-pack/legacy/plugins/uptime/public/pages/not_found.tsx b/x-pack/plugins/uptime/public/pages/not_found.tsx similarity index 100% rename from x-pack/legacy/plugins/uptime/public/pages/not_found.tsx rename to x-pack/plugins/uptime/public/pages/not_found.tsx diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/plugins/uptime/public/pages/overview.tsx similarity index 96% rename from x-pack/legacy/plugins/uptime/public/pages/overview.tsx rename to x-pack/plugins/uptime/public/pages/overview.tsx index adc36efa6f7db..fefd804cbfabf 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/plugins/uptime/public/pages/overview.tsx @@ -10,11 +10,11 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { useUptimeTelemetry, UptimePage, useGetUrlParams } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; -import { useTrackPageview } from '../../../../../plugins/observability/public'; -import { DataPublicPluginSetup, IIndexPattern } from '../../../../../../src/plugins/data/public'; -import { useUpdateKueryString } from '../hooks'; import { PageHeader } from './page_header'; +import { DataPublicPluginSetup, IIndexPattern } from '../../../../../src/plugins/data/public'; +import { useUpdateKueryString } from '../hooks'; import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { useTrackPageview } from '../../../observability/public'; import { MonitorList } from '../components/overview/monitor_list/monitor_list_container'; import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview'; import { StatusPanel } from '../components/overview/status_panel'; diff --git a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx b/x-pack/plugins/uptime/public/pages/page_header.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/pages/page_header.tsx rename to x-pack/plugins/uptime/public/pages/page_header.tsx index b10bc6ba44f8a..b6791e6a93445 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/page_header.tsx +++ b/x-pack/plugins/uptime/public/pages/page_header.tsx @@ -13,7 +13,7 @@ import { SETTINGS_ROUTE } from '../../common/constants'; import { ToggleAlertFlyoutButton } from '../components/overview/alerts/alerts_containers'; interface PageHeaderProps { - headingText: string; + headingText: string | JSX.Element; extraLinks?: boolean; datePicker?: boolean; } diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx new file mode 100644 index 0000000000000..52096a49435d7 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -0,0 +1,203 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useDispatch, useSelector } from 'react-redux'; +import { isEqual } from 'lodash'; +import { Link } from 'react-router-dom'; +import { selectDynamicSettings } from '../state/selectors'; +import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; +import { DynamicSettings } from '../../common/runtime_types'; +import { useBreadcrumbs } from '../hooks/use_breadcrumbs'; +import { OVERVIEW_ROUTE } from '../../common/constants'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { UptimePage, useUptimeTelemetry } from '../hooks'; +import { IndicesForm } from '../components/settings/indices_form'; +import { + CertificateExpirationForm, + OnFieldChangeType, +} from '../components/settings/certificate_form'; +import * as Translations from './translations'; + +interface SettingsPageFieldErrors { + heartbeatIndices: 'May not be blank' | ''; + certificatesThresholds: { + expirationThresholdError: string | null; + ageThresholdError: string | null; + } | null; +} + +export interface SettingsFormProps { + loading: boolean; + onChange: OnFieldChangeType; + formFields: DynamicSettings | null; + fieldErrors: SettingsPageFieldErrors | null; + isDisabled: boolean; +} + +const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldErrors | null => { + if (formFields) { + const blankStr = 'May not be blank'; + const { certThresholds: certificatesThresholds, heartbeatIndices } = formFields; + const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; + const expirationThresholdError = certificatesThresholds?.expiration ? null : blankStr; + const ageThresholdError = certificatesThresholds?.age ? null : blankStr; + return { + heartbeatIndices: heartbeatIndErr, + certificatesThresholds: + expirationThresholdError || ageThresholdError + ? { + expirationThresholdError, + ageThresholdError, + } + : null, + }; + } + return null; +}; + +export const SettingsPage = () => { + const dss = useSelector(selectDynamicSettings); + + useBreadcrumbs([{ text: Translations.settings.breadcrumbText }]); + + useUptimeTelemetry(UptimePage.Settings); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(getDynamicSettings()); + }, [dispatch]); + + const [formFields, setFormFields] = useState<DynamicSettings | null>( + dss.settings ? { ...dss.settings } : null + ); + + if (!dss.loadError && formFields === null && dss.settings) { + setFormFields(Object.assign({}, { ...dss.settings })); + } + + const fieldErrors = getFieldErrors(formFields); + + const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); + + const onChangeFormField: OnFieldChangeType = changedField => { + if (formFields) { + setFormFields({ + heartbeatIndices: changedField.heartbeatIndices ?? formFields.heartbeatIndices, + certThresholds: Object.assign( + {}, + formFields.certThresholds, + changedField?.certThresholds ?? null + ), + }); + } + }; + + const onApply = (event: React.FormEvent) => { + event.preventDefault(); + if (formFields) { + dispatch(setDynamicSettings(formFields)); + } + }; + + const resetForm = () => setFormFields(dss.settings ? { ...dss.settings } : null); + + const isFormDirty = !isEqual(dss.settings, formFields); + const canEdit: boolean = + !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; + const isFormDisabled = dss.loading || !canEdit; + + const cannotEditNotice = canEdit ? null : ( + <> + <EuiCallOut title={Translations.settings.editNoticeTitle}> + {Translations.settings.editNoticeText} + </EuiCallOut> + <EuiSpacer size="s" /> + </> + ); + + return ( + <> + <Link to={OVERVIEW_ROUTE} data-test-subj="uptimeSettingsToOverviewLink"> + <EuiButtonEmpty size="s" color="primary" iconType="arrowLeft"> + {Translations.settings.returnToOverviewLinkLabel} + </EuiButtonEmpty> + </Link> + <EuiSpacer size="s" /> + <EuiPanel> + <EuiFlexGroup> + <EuiFlexItem grow={false}>{cannotEditNotice}</EuiFlexItem> + </EuiFlexGroup> + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <form onSubmit={onApply}> + <EuiForm> + <IndicesForm + loading={dss.loading} + onChange={onChangeFormField} + formFields={formFields} + fieldErrors={fieldErrors} + isDisabled={isFormDisabled} + /> + <CertificateExpirationForm + loading={dss.loading} + onChange={onChangeFormField} + formFields={formFields} + fieldErrors={fieldErrors} + isDisabled={isFormDisabled} + /> + + <EuiSpacer size="m" /> + <EuiFlexGroup justifyContent="flexEnd" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + data-test-subj="discardSettingsButton" + isDisabled={!isFormDirty || isFormDisabled} + onClick={() => { + resetForm(); + }} + > + <FormattedMessage + id="xpack.uptime.sourceConfiguration.discardSettingsButtonLabel" + defaultMessage="Cancel" + /> + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + data-test-subj="apply-settings-button" + type="submit" + color="primary" + isDisabled={!isFormDirty || !isFormValid || isFormDisabled} + fill + > + <FormattedMessage + id="xpack.uptime.sourceConfiguration.applySettingsButtonLabel" + defaultMessage="Apply changes" + /> + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiForm> + </form> + </EuiFlexItem> + </EuiFlexGroup> + </EuiPanel> + </> + ); +}; diff --git a/x-pack/plugins/uptime/public/pages/translations.ts b/x-pack/plugins/uptime/public/pages/translations.ts new file mode 100644 index 0000000000000..74fb2eeb1416b --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/translations.ts @@ -0,0 +1,38 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const SETTINGS_ON_CERT = i18n.translate('xpack.uptime.certificates.settingsLinkLabel', { + defaultMessage: 'Settings', +}); + +export const RETURN_TO_OVERVIEW = i18n.translate( + 'xpack.uptime.certificates.returnToOverviewLinkLabel', + { + defaultMessage: 'Return to overview', + } +); + +export const REFRESH_CERT = i18n.translate('xpack.uptime.certificates.refresh', { + defaultMessage: 'Refresh', +}); + +export const settings = { + breadcrumbText: i18n.translate('xpack.uptime.settingsBreadcrumbText', { + defaultMessage: 'Settings', + }), + editNoticeTitle: i18n.translate('xpack.uptime.settings.cannotEditTitle', { + defaultMessage: 'You do not have permission to edit settings.', + }), + editNoticeText: i18n.translate('xpack.uptime.settings.cannotEditText', { + defaultMessage: + "Your user currently has 'Read' permissions for the Uptime app. Enable a permissions-level of 'All' to edit these settings.", + }), + returnToOverviewLinkLabel: i18n.translate('xpack.uptime.settings.returnToOverviewLinkLabel', { + defaultMessage: 'Return to overview', + }), +}; diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx new file mode 100644 index 0000000000000..ca97858998df7 --- /dev/null +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { Route, Switch } from 'react-router-dom'; +import { DataPublicPluginSetup } from '../../../../src/plugins/data/public'; +import { OverviewPage } from './components/overview/overview_container'; +import { + CERTIFICATES_ROUTE, + MONITOR_ROUTE, + OVERVIEW_ROUTE, + SETTINGS_ROUTE, +} from '../common/constants'; +import { MonitorPage, NotFoundPage, SettingsPage } from './pages'; +import { CertificatesPage } from './pages/certificates'; + +interface RouterProps { + autocomplete: DataPublicPluginSetup['autocomplete']; +} + +export const PageRouter: FC<RouterProps> = ({ autocomplete }) => ( + <Switch> + <Route path={MONITOR_ROUTE}> + <div data-test-subj="uptimeMonitorPage"> + <MonitorPage /> + </div> + </Route> + <Route path={SETTINGS_ROUTE}> + <div data-test-subj="uptimeSettingsPage"> + <SettingsPage /> + </div> + </Route> + <Route path={CERTIFICATES_ROUTE}> + <div data-test-subj="uptimeCertificatesPage"> + <CertificatesPage /> + </div> + </Route> + <Route path={OVERVIEW_ROUTE}> + <div data-test-subj="uptimeOverviewPage"> + <OverviewPage autocomplete={autocomplete} /> + </div> + </Route> + <Route component={NotFoundPage} /> + </Switch> +); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap b/x-pack/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap rename to x-pack/plugins/uptime/public/state/actions/__tests__/__snapshots__/overview_filters.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts b/x-pack/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts rename to x-pack/plugins/uptime/public/state/actions/__tests__/overview_filters.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/actions/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/actions/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index.ts b/x-pack/plugins/uptime/public/state/actions/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/index.ts rename to x-pack/plugins/uptime/public/state/actions/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index_patternts.ts b/x-pack/plugins/uptime/public/state/actions/index_patternts.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/index_patternts.ts rename to x-pack/plugins/uptime/public/state/actions/index_patternts.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/index_status.ts b/x-pack/plugins/uptime/public/state/actions/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/index_status.ts rename to x-pack/plugins/uptime/public/state/actions/index_status.ts diff --git a/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts new file mode 100644 index 0000000000000..441a3cefdf204 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/ml_anomaly.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { createAsyncAction } from './utils'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { AnomaliesTableRecord } from '../../../../../plugins/ml/common/types/anomalies'; +import { + CreateMLJobSuccess, + DeleteJobResults, + MonitorIdParam, + HeartbeatIndicesParam, +} from './types'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; + +export const resetMLState = createAction('RESET_ML_STATE'); + +export const getExistingMLJobAction = createAsyncAction<MonitorIdParam, JobExistResult>( + 'GET_EXISTING_ML_JOB' +); + +export const createMLJobAction = createAsyncAction< + MonitorIdParam & HeartbeatIndicesParam, + CreateMLJobSuccess | null +>('CREATE_ML_JOB'); + +export const getMLCapabilitiesAction = createAsyncAction<any, MlCapabilitiesResponse>( + 'GET_ML_CAPABILITIES' +); + +export const deleteMLJobAction = createAsyncAction<MonitorIdParam, DeleteJobResults>( + 'DELETE_ML_JOB' +); + +export interface AnomalyRecordsParams { + dateStart: number; + dateEnd: number; + listOfMonitorIds: string[]; + anomalyThreshold?: number; +} + +export interface AnomalyRecords { + anomalies: AnomaliesTableRecord[]; + interval: string; +} + +export const getAnomalyRecordsAction = createAsyncAction<AnomalyRecordsParams, AnomalyRecords>( + 'GET_ANOMALY_RECORDS' +); diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts b/x-pack/plugins/uptime/public/state/actions/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor.ts rename to x-pack/plugins/uptime/public/state/actions/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts similarity index 90% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_duration.ts index 9a2db5be60b12..524044f873687 100644 --- a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_duration.ts +++ b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts @@ -7,7 +7,7 @@ import { createAction } from 'redux-actions'; import { QueryParams } from './types'; import { MonitorDurationResult } from '../../../common/types'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; type MonitorQueryParams = QueryParams & { monitorId: string }; diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts b/x-pack/plugins/uptime/public/state/actions/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_list.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts b/x-pack/plugins/uptime/public/state/actions/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/monitor_status.ts rename to x-pack/plugins/uptime/public/state/actions/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts b/x-pack/plugins/uptime/public/state/actions/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/overview_filters.ts rename to x-pack/plugins/uptime/public/state/actions/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ping.ts b/x-pack/plugins/uptime/public/state/actions/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/ping.ts rename to x-pack/plugins/uptime/public/state/actions/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/plugins/uptime/public/state/actions/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/snapshot.ts rename to x-pack/plugins/uptime/public/state/actions/snapshot.ts diff --git a/x-pack/plugins/uptime/public/state/actions/types.ts b/x-pack/plugins/uptime/public/state/actions/types.ts new file mode 100644 index 0000000000000..dee2df77707d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/types.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux-actions'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; + +export interface AsyncAction<Payload, SuccessPayload> { + get: (payload: Payload) => Action<Payload>; + success: (payload: SuccessPayload) => Action<SuccessPayload>; + fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; +} +export interface AsyncAction1<Payload, SuccessPayload> { + get: (payload?: Payload) => Action<Payload>; + success: (payload: SuccessPayload) => Action<SuccessPayload>; + fail: (payload: IHttpFetchError) => Action<IHttpFetchError>; +} + +export interface MonitorIdParam { + monitorId: string; +} + +export interface HeartbeatIndicesParam { + heartbeatIndices: string; +} + +export interface QueryParams { + monitorId: string; + dateStart: string; + dateEnd: string; + filters?: string; + statusFilter?: string; + location?: string; +} + +export interface MonitorDetailsActionPayload { + monitorId: string; + dateStart: string; + dateEnd: string; + location?: string; +} + +export interface CreateMLJobSuccess { + count: number; + jobId: string; +} + +export interface DeleteJobResults { + [id: string]: { + [status: string]: boolean; + error?: any; + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/actions/ui.ts b/x-pack/plugins/uptime/public/state/actions/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/actions/ui.ts rename to x-pack/plugins/uptime/public/state/actions/ui.ts diff --git a/x-pack/plugins/uptime/public/state/actions/utils.ts b/x-pack/plugins/uptime/public/state/actions/utils.ts new file mode 100644 index 0000000000000..8ce4cf011406b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/actions/utils.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAction } from 'redux-actions'; +import { AsyncAction, AsyncAction1 } from './types'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; + +export function createAsyncAction<Payload, SuccessPayload>( + actionStr: string +): AsyncAction1<Payload, SuccessPayload>; +export function createAsyncAction<Payload, SuccessPayload>( + actionStr: string +): AsyncAction<Payload, SuccessPayload> { + return { + get: createAction<Payload>(actionStr), + success: createAction<SuccessPayload>(`${actionStr}_SUCCESS`), + fail: createAction<IHttpFetchError>(`${actionStr}_FAIL`), + }; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap rename to x-pack/plugins/uptime/public/state/api/__tests__/__snapshots__/snapshot.test.ts.snap diff --git a/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts b/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts new file mode 100644 index 0000000000000..838e5b8246b4b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/__tests__/ml_anomaly.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getMLJobId } from '../ml_anomaly'; + +describe('ML Anomaly API', () => { + it('it generates a lowercase job id', async () => { + const monitorId = 'ABC1334haa'; + + const jobId = getMLJobId(monitorId); + + expect(jobId).toEqual(jobId.toLowerCase()); + }); + + it('should truncate long monitor IDs', () => { + const longAndWeirdMonitorId = + 'https://auto-mmmmxhhhhhccclongAndWeirdMonitorId123yyyyyrereauto-xcmpa-1345555454646'; + + expect(getMLJobId(longAndWeirdMonitorId)).toHaveLength(64); + }); + + it('should remove special characters and replace them with underscore', () => { + const monIdSpecialChars = '/ ? , " < > | * a'; + + const jobId = getMLJobId(monIdSpecialChars); + + const format = /[/?,"<>|*]+/; + + expect(format.test(jobId)).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/plugins/uptime/public/state/api/__tests__/snapshot.test.ts similarity index 95% rename from x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts rename to x-pack/plugins/uptime/public/state/api/__tests__/snapshot.test.ts index 66b376c3ac36f..ff9fcd0573257 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts +++ b/x-pack/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -6,7 +6,7 @@ import { fetchSnapshotCount } from '../snapshot'; import { apiService } from '../utils'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; describe('snapshot API', () => { let fetchMock: jest.SpyInstance<Partial<unknown>>; diff --git a/x-pack/plugins/uptime/public/state/api/certificates.ts b/x-pack/plugins/uptime/public/state/api/certificates.ts new file mode 100644 index 0000000000000..78267e659d233 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/certificates.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { API_URLS } from '../../../common/constants'; +import { apiService } from './utils'; +import { CertResultType, GetCertsParams } from '../../../common/runtime_types'; + +export const fetchCertificates = async (params: GetCertsParams) => { + return await apiService.get(API_URLS.CERTS, params, CertResultType); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/api/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/api/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index.ts b/x-pack/plugins/uptime/public/state/api/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/index.ts rename to x-pack/plugins/uptime/public/state/api/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts b/x-pack/plugins/uptime/public/state/api/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/index_pattern.ts rename to x-pack/plugins/uptime/public/state/api/index_pattern.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/index_status.ts b/x-pack/plugins/uptime/public/state/api/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/index_status.ts rename to x-pack/plugins/uptime/public/state/api/index_status.ts diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts new file mode 100644 index 0000000000000..c4ecb769abefc --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts @@ -0,0 +1,115 @@ +/* + * 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 moment from 'moment'; +import { apiService } from './utils'; +import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; +import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { + CreateMLJobSuccess, + DeleteJobResults, + MonitorIdParam, + HeartbeatIndicesParam, +} from '../actions/types'; +import { DataRecognizerConfigResponse } from '../../../../../plugins/ml/common/types/modules'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; + +const getJobPrefix = (monitorId: string) => { + // ML App doesn't support upper case characters in job name + // Also Spaces and the characters / ? , " < > | * are not allowed + // so we will replace all special chars with _ + + const prefix = monitorId.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); + + // ML Job ID can't be greater than 64 length, so will be substring it, and hope + // At such big length, there is minimum chance of having duplicate monitor id + // Subtracting ML_JOB_ID constant as well + const postfix = '_' + ML_JOB_ID; + + if ((prefix + postfix).length > 64) { + return prefix.substring(0, 64 - postfix.length) + '_'; + } + return prefix + '_'; +}; + +export const getMLJobId = (monitorId: string) => `${getJobPrefix(monitorId)}${ML_JOB_ID}`; + +export const getMLCapabilities = async (): Promise<MlCapabilitiesResponse> => { + return await apiService.get(API_URLS.ML_CAPABILITIES); +}; + +export const getExistingJobs = async (): Promise<JobExistResult> => { + return await apiService.get(API_URLS.ML_MODULE_JOBS + ML_MODULE_ID); +}; + +export const createMLJob = async ({ + monitorId, + heartbeatIndices, +}: MonitorIdParam & HeartbeatIndicesParam): Promise<CreateMLJobSuccess | null> => { + const url = API_URLS.ML_SETUP_MODULE + ML_MODULE_ID; + + const data = { + prefix: `${getJobPrefix(monitorId)}`, + useDedicatedIndex: false, + startDatafeed: true, + start: moment() + .subtract(24, 'h') + .valueOf(), + indexPatternName: heartbeatIndices, + query: { + bool: { + filter: [ + { term: { 'monitor.id': monitorId } }, + { range: { 'monitor.duration.us': { gt: 0 } } }, + ], + }, + }, + }; + + const response: DataRecognizerConfigResponse = await apiService.post(url, data); + if (response?.jobs?.[0]?.id === getMLJobId(monitorId)) { + const jobResponse = response.jobs[0]; + if (jobResponse.success) { + return { + count: 1, + jobId: jobResponse.id, + }; + } else { + const { error } = jobResponse; + throw new Error(error?.msg); + } + } else { + return null; + } +}; + +export const deleteMLJob = async ({ monitorId }: MonitorIdParam): Promise<DeleteJobResults> => { + const data = { jobIds: [getMLJobId(monitorId)] }; + + return await apiService.post(API_URLS.ML_DELETE_JOB, data); +}; + +export const fetchAnomalyRecords = async ({ + dateStart, + dateEnd, + listOfMonitorIds, + anomalyThreshold, +}: AnomalyRecordsParams): Promise<AnomalyRecords> => { + const data = { + jobIds: listOfMonitorIds.map((monitorId: string) => getMLJobId(monitorId)), + criteriaFields: [], + influencers: [], + aggregationInterval: 'auto', + threshold: anomalyThreshold ?? 25, + earliestMs: dateStart, + latestMs: dateEnd, + dateFormatTz: Intl.DateTimeFormat().resolvedOptions().timeZone, + maxRecords: 500, + maxExamples: 10, + }; + return apiService.post(API_URLS.ML_ANOMALIES_RESULT, data); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/plugins/uptime/public/state/api/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor.ts rename to x-pack/plugins/uptime/public/state/api/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts b/x-pack/plugins/uptime/public/state/api/monitor_duration.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/api/monitor_duration.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts b/x-pack/plugins/uptime/public/state/api/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_list.ts rename to x-pack/plugins/uptime/public/state/api/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts b/x-pack/plugins/uptime/public/state/api/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/monitor_status.ts rename to x-pack/plugins/uptime/public/state/api/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts b/x-pack/plugins/uptime/public/state/api/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/overview_filters.ts rename to x-pack/plugins/uptime/public/state/api/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ping.ts b/x-pack/plugins/uptime/public/state/api/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/ping.ts rename to x-pack/plugins/uptime/public/state/api/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts b/x-pack/plugins/uptime/public/state/api/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/snapshot.ts rename to x-pack/plugins/uptime/public/state/api/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/api/types.ts b/x-pack/plugins/uptime/public/state/api/types.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/api/types.ts rename to x-pack/plugins/uptime/public/state/api/types.ts diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts new file mode 100644 index 0000000000000..acd9bec5a74bc --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PathReporter } from 'io-ts/lib/PathReporter'; +import { isRight } from 'fp-ts/lib/Either'; +import { HttpFetchQuery, HttpSetup } from '../../../../../../target/types/core/public'; + +class ApiService { + private static instance: ApiService; + private _http!: HttpSetup; + + public get http() { + return this._http; + } + + public set http(httpSetup: HttpSetup) { + this._http = httpSetup; + } + + private constructor() {} + + static getInstance(): ApiService { + if (!ApiService.instance) { + ApiService.instance = new ApiService(); + } + + return ApiService.instance; + } + + public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any) { + const response = await this._http!.get(apiUrl, { query: params }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right; + } else { + // eslint-disable-next-line no-console + console.error( + `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` + ); + } + } + + return response; + } + + public async post(apiUrl: string, data?: any, decodeType?: any) { + const response = await this._http!.post(apiUrl, { + method: 'POST', + body: JSON.stringify(data), + }); + + if (decodeType) { + const decoded = decodeType.decode(response); + if (isRight(decoded)) { + return decoded.right; + } else { + // eslint-disable-next-line no-console + console.warn( + `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` + ); + } + } + return response; + } + + public async delete(apiUrl: string) { + const response = await this._http!.delete(apiUrl); + if (response instanceof Error) { + throw response; + } + return response; + } +} + +export const apiService = ApiService.getInstance(); diff --git a/x-pack/plugins/uptime/public/state/certificates/certificates.ts b/x-pack/plugins/uptime/public/state/certificates/certificates.ts new file mode 100644 index 0000000000000..18cbcf6bcb614 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/certificates/certificates.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions } from 'redux-actions'; +import { takeLatest } from 'redux-saga/effects'; +import { createAsyncAction } from '../actions/utils'; +import { getAsyncInitialState, handleAsyncAction } from '../reducers/utils'; +import { CertResult, GetCertsParams } from '../../../common/runtime_types'; +import { AppState } from '../index'; +import { AsyncInitialState } from '../reducers/types'; +import { fetchEffectFactory } from '../effects/fetch_effect'; +import { fetchCertificates } from '../api/certificates'; + +export const getCertificatesAction = createAsyncAction<GetCertsParams, CertResult>( + 'GET_CERTIFICATES' +); + +interface CertificatesState { + certs: AsyncInitialState<CertResult>; +} + +const initialState = { + certs: getAsyncInitialState(), +}; + +export const certificatesReducer = handleActions<CertificatesState>( + { + ...handleAsyncAction<CertificatesState>('certs', getCertificatesAction), + }, + initialState +); + +export function* fetchCertificatesEffect() { + yield takeLatest( + getCertificatesAction.get, + fetchEffectFactory(fetchCertificates, getCertificatesAction.success, getCertificatesAction.fail) + ); +} + +export const certificatesSelector = ({ certificates }: AppState) => certificates.certs.data; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts b/x-pack/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts similarity index 96% rename from x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts rename to x-pack/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts index 0354cfeac7b07..4ec35d8cd6c6f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts +++ b/x-pack/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts @@ -7,7 +7,7 @@ import { call, put } from 'redux-saga/effects'; import { fetchEffectFactory } from '../fetch_effect'; import { indexStatusAction } from '../../actions'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; import { StatesIndexStatus } from '../../../../common/runtime_types'; import { fetchIndexStatus } from '../../api'; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/dynamic_settings.ts rename to x-pack/plugins/uptime/public/state/effects/dynamic_settings.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts similarity index 94% rename from x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts rename to x-pack/plugins/uptime/public/state/effects/fetch_effect.ts index b0734cb5ccabb..0aa85609fe4f0 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/fetch_effect.ts +++ b/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts @@ -6,7 +6,7 @@ import { call, put } from 'redux-saga/effects'; import { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; /** * Factory function for a fetch effect. It expects three action creators, diff --git a/x-pack/plugins/uptime/public/state/effects/index.ts b/x-pack/plugins/uptime/public/state/effects/index.ts new file mode 100644 index 0000000000000..211067c840d54 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/effects/index.ts @@ -0,0 +1,36 @@ +/* + * 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 { fork } from 'redux-saga/effects'; +import { fetchMonitorDetailsEffect } from './monitor'; +import { fetchOverviewFiltersEffect } from './overview_filters'; +import { fetchSnapshotCountEffect } from './snapshot'; +import { fetchMonitorListEffect } from './monitor_list'; +import { fetchMonitorStatusEffect } from './monitor_status'; +import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_settings'; +import { fetchIndexPatternEffect } from './index_pattern'; +import { fetchPingsEffect, fetchPingHistogramEffect } from './ping'; +import { fetchMonitorDurationEffect } from './monitor_duration'; +import { fetchMLJobEffect } from './ml_anomaly'; +import { fetchIndexStatusEffect } from './index_status'; +import { fetchCertificatesEffect } from '../certificates/certificates'; + +export function* rootEffect() { + yield fork(fetchMonitorDetailsEffect); + yield fork(fetchSnapshotCountEffect); + yield fork(fetchOverviewFiltersEffect); + yield fork(fetchMonitorListEffect); + yield fork(fetchMonitorStatusEffect); + yield fork(fetchDynamicSettingsEffect); + yield fork(setDynamicSettingsEffect); + yield fork(fetchIndexPatternEffect); + yield fork(fetchPingsEffect); + yield fork(fetchPingHistogramEffect); + yield fork(fetchMLJobEffect); + yield fork(fetchMonitorDurationEffect); + yield fork(fetchIndexStatusEffect); + yield fork(fetchCertificatesEffect); +} diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index_pattern.ts b/x-pack/plugins/uptime/public/state/effects/index_pattern.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/index_pattern.ts rename to x-pack/plugins/uptime/public/state/effects/index_pattern.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts b/x-pack/plugins/uptime/public/state/effects/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/index_status.ts rename to x-pack/plugins/uptime/public/state/effects/index_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/ml_anomaly.ts rename to x-pack/plugins/uptime/public/state/effects/ml_anomaly.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts b/x-pack/plugins/uptime/public/state/effects/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts rename to x-pack/plugins/uptime/public/state/effects/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_duration.ts b/x-pack/plugins/uptime/public/state/effects/monitor_duration.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/effects/monitor_duration.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_list.ts b/x-pack/plugins/uptime/public/state/effects/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor_list.ts rename to x-pack/plugins/uptime/public/state/effects/monitor_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts b/x-pack/plugins/uptime/public/state/effects/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/monitor_status.ts rename to x-pack/plugins/uptime/public/state/effects/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/overview_filters.ts b/x-pack/plugins/uptime/public/state/effects/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/overview_filters.ts rename to x-pack/plugins/uptime/public/state/effects/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/ping.ts b/x-pack/plugins/uptime/public/state/effects/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/ping.ts rename to x-pack/plugins/uptime/public/state/effects/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/plugins/uptime/public/state/effects/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/effects/snapshot.ts rename to x-pack/plugins/uptime/public/state/effects/snapshot.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/index.ts b/x-pack/plugins/uptime/public/state/index.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/index.ts rename to x-pack/plugins/uptime/public/state/index.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/kibana_service.ts b/x-pack/plugins/uptime/public/state/kibana_service.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/kibana_service.ts rename to x-pack/plugins/uptime/public/state/kibana_service.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap rename to x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/snapshot.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap b/x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap rename to x-pack/plugins/uptime/public/state/reducers/__tests__/__snapshots__/ui.test.ts.snap diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts b/x-pack/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts rename to x-pack/plugins/uptime/public/state/reducers/__tests__/snapshot.test.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts b/x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/__tests__/ui.test.ts rename to x-pack/plugins/uptime/public/state/reducers/__tests__/ui.test.ts diff --git a/x-pack/plugins/uptime/public/state/reducers/dynamic_settings.ts b/x-pack/plugins/uptime/public/state/reducers/dynamic_settings.ts new file mode 100644 index 0000000000000..a9ad58e64e552 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/dynamic_settings.ts @@ -0,0 +1,59 @@ +/* + * 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 { handleActions, Action } from 'redux-actions'; +import { + getDynamicSettings, + getDynamicSettingsSuccess, + getDynamicSettingsFail, + setDynamicSettings, + setDynamicSettingsSuccess, + setDynamicSettingsFail, +} from '../actions/dynamic_settings'; +import { DynamicSettings } from '../../../common/runtime_types'; + +export interface DynamicSettingsState { + settings?: DynamicSettings; + loadError?: Error; + saveError?: Error; + loading: boolean; +} + +const initialState: DynamicSettingsState = { + loading: true, +}; + +export const dynamicSettingsReducer = handleActions<DynamicSettingsState, any>( + { + [String(getDynamicSettings)]: state => ({ + ...state, + loading: true, + }), + [String(getDynamicSettingsSuccess)]: (_state, action: Action<DynamicSettings>) => ({ + loading: false, + settings: action.payload, + }), + [String(getDynamicSettingsFail)]: (_state, action: Action<Error>) => ({ + loading: false, + loadError: action.payload, + }), + [String(setDynamicSettings)]: state => ({ + ...state, + loading: true, + }), + [String(setDynamicSettingsSuccess)]: (_state, action: Action<DynamicSettings>) => ({ + settings: action.payload, + saveSucceded: true, + loading: false, + }), + [String(setDynamicSettingsFail)]: (state, action: Action<Error>) => ({ + ...state, + loading: false, + saveSucceeded: false, + saveError: action.payload, + }), + }, + initialState +); diff --git a/x-pack/plugins/uptime/public/state/reducers/index.ts b/x-pack/plugins/uptime/public/state/reducers/index.ts new file mode 100644 index 0000000000000..ead7f5b46431b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/index.ts @@ -0,0 +1,38 @@ +/* + * 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 { combineReducers } from 'redux'; +import { monitorReducer } from './monitor'; +import { overviewFiltersReducer } from './overview_filters'; +import { snapshotReducer } from './snapshot'; +import { uiReducer } from './ui'; +import { monitorStatusReducer } from './monitor_status'; +import { monitorListReducer } from './monitor_list'; +import { dynamicSettingsReducer } from './dynamic_settings'; +import { indexPatternReducer } from './index_pattern'; +import { pingReducer } from './ping'; +import { pingListReducer } from './ping_list'; +import { monitorDurationReducer } from './monitor_duration'; +import { indexStatusReducer } from './index_status'; +import { mlJobsReducer } from './ml_anomaly'; +import { certificatesReducer } from '../certificates/certificates'; + +export const rootReducer = combineReducers({ + monitor: monitorReducer, + overviewFilters: overviewFiltersReducer, + snapshot: snapshotReducer, + ui: uiReducer, + monitorList: monitorListReducer, + monitorStatus: monitorStatusReducer, + dynamicSettings: dynamicSettingsReducer, + indexPattern: indexPatternReducer, + ping: pingReducer, + pingList: pingListReducer, + ml: mlJobsReducer, + monitorDuration: monitorDurationReducer, + indexStatus: indexStatusReducer, + certificates: certificatesReducer, +}); diff --git a/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts new file mode 100644 index 0000000000000..b357f5a904ea6 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/index_pattern.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { handleActions, Action } from 'redux-actions'; +import { getIndexPattern, getIndexPatternSuccess, getIndexPatternFail } from '../actions'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; + +export interface IndexPatternState { + index_pattern: IIndexPattern | null; + errors: any[]; + loading: boolean; +} + +const initialState: IndexPatternState = { + index_pattern: null, + loading: false, + errors: [], +}; + +export const indexPatternReducer = handleActions<IndexPatternState>( + { + [String(getIndexPattern)]: state => ({ + ...state, + loading: true, + }), + + [String(getIndexPatternSuccess)]: (state, action: Action<any>) => ({ + ...state, + loading: false, + index_pattern: { ...action.payload }, + }), + + [String(getIndexPatternFail)]: (state, action: Action<any>) => ({ + ...state, + errors: [...state.errors, action.payload], + loading: false, + }), + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts b/x-pack/plugins/uptime/public/state/reducers/index_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/index_status.ts rename to x-pack/plugins/uptime/public/state/reducers/index_status.ts diff --git a/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts new file mode 100644 index 0000000000000..9a4a949ac4ede --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/ml_anomaly.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { handleActions } from 'redux-actions'; +import { + getExistingMLJobAction, + createMLJobAction, + getAnomalyRecordsAction, + deleteMLJobAction, + resetMLState, + AnomalyRecords, + getMLCapabilitiesAction, +} from '../actions'; +import { getAsyncInitialState, handleAsyncAction } from './utils'; +import { AsyncInitialState } from './types'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; +import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; +import { JobExistResult } from '../../../../../plugins/ml/common/types/data_recognizer'; + +export interface MLJobState { + mlJob: AsyncInitialState<JobExistResult>; + createJob: AsyncInitialState<CreateMLJobSuccess>; + deleteJob: AsyncInitialState<DeleteJobResults>; + anomalies: AsyncInitialState<AnomalyRecords>; + mlCapabilities: AsyncInitialState<MlCapabilitiesResponse>; +} + +const initialState: MLJobState = { + mlJob: getAsyncInitialState(), + createJob: getAsyncInitialState(), + deleteJob: getAsyncInitialState(), + anomalies: getAsyncInitialState(), + mlCapabilities: getAsyncInitialState(), +}; + +export const mlJobsReducer = handleActions<MLJobState>( + { + ...handleAsyncAction<MLJobState>('mlJob', getExistingMLJobAction), + ...handleAsyncAction<MLJobState>('mlCapabilities', getMLCapabilitiesAction), + ...handleAsyncAction<MLJobState>('createJob', createMLJobAction), + ...handleAsyncAction<MLJobState>('deleteJob', deleteMLJobAction), + ...handleAsyncAction<MLJobState>('anomalies', getAnomalyRecordsAction), + ...{ + [String(resetMLState)]: state => ({ + ...state, + mlJob: { + loading: false, + data: null, + error: null, + }, + createJob: { + data: null, + error: null, + loading: false, + }, + deleteJob: { + data: null, + error: null, + loading: false, + }, + }), + }, + }, + initialState +); diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts b/x-pack/plugins/uptime/public/state/reducers/monitor.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_duration.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor_duration.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor_duration.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor_list.ts index cf895aebeb755..59a794a549d57 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_list.ts +++ b/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts @@ -9,9 +9,9 @@ import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '.. import { MonitorSummaryResult } from '../../../common/runtime_types'; export interface MonitorList { - list: MonitorSummaryResult; error?: Error; loading: boolean; + list: MonitorSummaryResult; } export const initialState: MonitorList = { diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_status.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/monitor_status.ts rename to x-pack/plugins/uptime/public/state/reducers/monitor_status.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts b/x-pack/plugins/uptime/public/state/reducers/overview_filters.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/overview_filters.ts rename to x-pack/plugins/uptime/public/state/reducers/overview_filters.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts b/x-pack/plugins/uptime/public/state/reducers/ping.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ping.ts rename to x-pack/plugins/uptime/public/state/reducers/ping.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ping_list.ts b/x-pack/plugins/uptime/public/state/reducers/ping_list.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ping_list.ts rename to x-pack/plugins/uptime/public/state/reducers/ping_list.ts diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/plugins/uptime/public/state/reducers/snapshot.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/snapshot.ts rename to x-pack/plugins/uptime/public/state/reducers/snapshot.ts diff --git a/x-pack/plugins/uptime/public/state/reducers/types.ts b/x-pack/plugins/uptime/public/state/reducers/types.ts new file mode 100644 index 0000000000000..c81ee6875f305 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; + +export interface AsyncInitialState<ReduceStateType> { + data: ReduceStateType | null; + loading: boolean; + error?: IHttpFetchError | null; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts b/x-pack/plugins/uptime/public/state/reducers/ui.ts similarity index 100% rename from x-pack/legacy/plugins/uptime/public/state/reducers/ui.ts rename to x-pack/plugins/uptime/public/state/reducers/ui.ts diff --git a/x-pack/plugins/uptime/public/state/reducers/utils.ts b/x-pack/plugins/uptime/public/state/reducers/utils.ts new file mode 100644 index 0000000000000..15e49e7f6de8b --- /dev/null +++ b/x-pack/plugins/uptime/public/state/reducers/utils.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Action } from 'redux-actions'; +import { AsyncAction } from '../actions/types'; + +export function handleAsyncAction<ReducerState>( + storeKey: string, + asyncAction: AsyncAction<any, any> +) { + return { + [String(asyncAction.get)]: (state: ReducerState) => ({ + ...state, + [storeKey]: { + ...(state as any)[storeKey], + loading: true, + }, + }), + + [String(asyncAction.success)]: (state: ReducerState, action: Action<any>) => ({ + ...state, + [storeKey]: { + ...(state as any)[storeKey], + data: action.payload, + loading: false, + }, + }), + + [String(asyncAction.fail)]: (state: ReducerState, action: Action<any>) => ({ + ...state, + [storeKey]: { + ...(state as any)[storeKey], + data: null, + error: action.payload, + loading: false, + }, + }), + }; +} + +export function getAsyncInitialState(initialData = null) { + return { + data: initialData, + loading: false, + error: null, + }; +} diff --git a/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts new file mode 100644 index 0000000000000..1c4c12f5f52d2 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { getBasePath, isIntegrationsPopupOpen } from '../index'; +import { AppState } from '../../../state'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; + +describe('state selectors', () => { + const state: AppState = { + overviewFilters: { + filters: { + locations: [], + ports: [], + schemes: [], + tags: [], + }, + errors: [], + loading: false, + }, + dynamicSettings: { + settings: DYNAMIC_SETTINGS_DEFAULTS, + loading: false, + }, + monitor: { + monitorDetailsList: [], + monitorLocationsList: new Map(), + loading: false, + errors: [], + }, + snapshot: { + count: { + up: 2, + down: 0, + total: 2, + }, + errors: [], + loading: false, + }, + ui: { + alertFlyoutVisible: false, + basePath: 'yyz', + esKuery: '', + integrationsPopoverOpen: null, + lastRefresh: 125, + }, + monitorStatus: { + status: null, + loading: false, + }, + indexPattern: { + index_pattern: null, + loading: false, + errors: [], + }, + ping: { + pingHistogram: null, + loading: false, + errors: [], + }, + pingList: { + loading: false, + pingList: { + total: 0, + locations: [], + pings: [], + }, + }, + monitorDuration: { + durationLines: null, + loading: false, + errors: [], + }, + monitorList: { + list: { + prevPagePagination: null, + nextPagePagination: null, + summaries: [], + totalSummaryCount: 0, + }, + loading: false, + }, + ml: { + mlJob: { + data: null, + loading: false, + }, + createJob: { data: null, loading: false }, + deleteJob: { data: null, loading: false }, + mlCapabilities: { data: null, loading: false }, + anomalies: { + data: null, + loading: false, + }, + }, + indexStatus: { + indexStatus: { + data: null, + loading: false, + }, + }, + certificates: { + certs: { + data: null, + loading: false, + }, + }, + }; + + it('selects base path from state', () => { + expect(getBasePath(state)).toBe('yyz'); + }); + + it('gets integrations popup state', () => { + const integrationsPopupOpen = { + id: 'popup-id', + open: true, + }; + state.ui.integrationsPopoverOpen = integrationsPopupOpen; + expect(isIntegrationsPopupOpen(state)).toBe(integrationsPopupOpen); + }); +}); diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts new file mode 100644 index 0000000000000..15fc8b8a7b173 --- /dev/null +++ b/x-pack/plugins/uptime/public/state/selectors/index.ts @@ -0,0 +1,107 @@ +/* + * 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 { createSelector } from 'reselect'; +import { AppState } from '../../state'; + +// UI Selectors +export const getBasePath = ({ ui: { basePath } }: AppState) => basePath; + +export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: AppState) => + integrationsPopoverOpen; + +// Monitor Selectors +export const monitorDetailsSelector = (state: AppState, summary: any) => { + return state.monitor.monitorDetailsList[summary.monitor_id]; +}; + +export const monitorLocationsSelector = (state: AppState, monitorId: string) => { + return state.monitor.monitorLocationsList?.get(monitorId); +}; + +export const monitorStatusSelector = (state: AppState) => state.monitorStatus.status; + +export const selectDynamicSettings = (state: AppState) => state.dynamicSettings; + +export const selectIndexPattern = ({ indexPattern }: AppState) => { + return { indexPattern: indexPattern.index_pattern, loading: indexPattern.loading }; +}; + +export const selectPingHistogram = ({ ping, ui }: AppState) => { + return { + data: ping.pingHistogram, + loading: ping.loading, + lastRefresh: ui.lastRefresh, + esKuery: ui.esKuery, + }; +}; + +export const selectPingList = ({ pingList, ui: { lastRefresh } }: AppState) => ({ + pings: pingList, + lastRefresh, +}); + +export const snapshotDataSelector = ({ + snapshot: { count, loading }, + ui: { lastRefresh, esKuery }, +}: AppState) => ({ + count, + lastRefresh, + loading, + esKuery, +}); + +const mlCapabilitiesSelector = (state: AppState) => state.ml.mlCapabilities.data; + +export const hasMLFeatureAvailable = createSelector( + mlCapabilitiesSelector, + mlCapabilities => + mlCapabilities?.isPlatinumOrTrialLicense && mlCapabilities?.mlFeatureEnabledInSpace +); + +export const canCreateMLJobSelector = createSelector( + mlCapabilitiesSelector, + mlCapabilities => mlCapabilities?.capabilities.canCreateJob +); + +export const canDeleteMLJobSelector = createSelector( + mlCapabilitiesSelector, + mlCapabilities => mlCapabilities?.capabilities.canDeleteJob +); + +export const hasMLJobSelector = ({ ml }: AppState) => ml.mlJob; + +export const hasNewMLJobSelector = ({ ml }: AppState) => ml.createJob; + +export const isMLJobCreatingSelector = ({ ml }: AppState) => ml.createJob.loading; + +export const isMLJobDeletingSelector = ({ ml }: AppState) => ml.deleteJob.loading; + +export const isMLJobDeletedSelector = ({ ml }: AppState) => ml.deleteJob; + +export const anomaliesSelector = ({ ml }: AppState) => ml.anomalies.data; + +export const selectDurationLines = ({ monitorDuration }: AppState) => { + return monitorDuration; +}; + +export const selectAlertFlyoutVisibility = ({ ui: { alertFlyoutVisible } }: AppState) => + alertFlyoutVisible; + +export const selectMonitorStatusAlert = ({ indexPattern, overviewFilters, ui }: AppState) => ({ + filters: ui.esKuery, + indexPattern: indexPattern.index_pattern, + locations: overviewFilters.filters.locations, +}); + +export const indexStatusSelector = ({ indexStatus }: AppState) => { + return indexStatus.indexStatus; +}; + +export const monitorListSelector = ({ monitorList, ui: { lastRefresh } }: AppState) => ({ + monitorList, + lastRefresh, +}); diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/plugins/uptime/public/uptime_app.tsx similarity index 75% rename from x-pack/legacy/plugins/uptime/public/uptime_app.tsx rename to x-pack/plugins/uptime/public/uptime_app.tsx index 92775a2663863..0d18f959230d1 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/uptime_app.tsx @@ -10,13 +10,14 @@ import React, { useEffect } from 'react'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter as Router } from 'react-router-dom'; import { I18nStart, ChromeBreadcrumb, CoreStart } from 'src/core/public'; -import { PluginsSetup } from 'ui/new_platform/new_platform'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +import { ClientPluginsSetup, ClientPluginsStart } from './apps/plugin'; import { UMUpdateBadge } from './lib/lib'; import { UptimeRefreshContextProvider, UptimeSettingsContextProvider, UptimeThemeContextProvider, + UptimeStartupPluginsContextProvider, } from './contexts'; import { CommonlyUsedRange } from './components/common/uptime_date_picker'; import { store } from './state'; @@ -47,7 +48,8 @@ export interface UptimeAppProps { isInfraAvailable: boolean; isLogsAvailable: boolean; kibanaBreadcrumbs: ChromeBreadcrumb[]; - plugins: PluginsSetup; + plugins: ClientPluginsSetup; + startPlugins: ClientPluginsStart; routerBasename: string; setBadge: UMUpdateBadge; renderGlobalHelpControls(): void; @@ -66,6 +68,7 @@ const Application = (props: UptimeAppProps) => { renderGlobalHelpControls, routerBasename, setBadge, + startPlugins, } = props; useEffect(() => { @@ -87,7 +90,6 @@ const Application = (props: UptimeAppProps) => { kibanaService.core = core; - // @ts-ignore store.dispatch(setBasePath(basePath)); return ( @@ -99,17 +101,19 @@ const Application = (props: UptimeAppProps) => { <UptimeRefreshContextProvider> <UptimeSettingsContextProvider {...props}> <UptimeThemeContextProvider darkMode={darkMode}> - <UptimeAlertsContextProvider> - <EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp"> - <main> - <UptimeAlertsFlyoutWrapper - alertTypeId="xpack.uptime.alerts.monitorStatus" - canChangeTrigger={false} - /> - <PageRouter autocomplete={plugins.data.autocomplete} /> - </main> - </EuiPage> - </UptimeAlertsContextProvider> + <UptimeStartupPluginsContextProvider {...startPlugins}> + <UptimeAlertsContextProvider> + <EuiPage className="app-wrapper-panel " data-test-subj="uptimeApp"> + <main> + <UptimeAlertsFlyoutWrapper + alertTypeId="xpack.uptime.alerts.monitorStatus" + canChangeTrigger={false} + /> + <PageRouter autocomplete={plugins.data.autocomplete} /> + </main> + </EuiPage> + </UptimeAlertsContextProvider> + </UptimeStartupPluginsContextProvider> </UptimeThemeContextProvider> </UptimeSettingsContextProvider> </UptimeRefreshContextProvider> diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 725b53aeca02d..d68bbabe82b86 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -5,7 +5,7 @@ */ import { Request, Server } from 'hapi'; -import { PLUGIN } from '../../../legacy/plugins/uptime/common/constants'; +import { PLUGIN } from '../common/constants'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework'; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index 98c6be5aa3c8e..f4d1c72770494 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -13,7 +13,7 @@ import { } from 'src/core/server'; import { UMKibanaRoute } from '../../../rest_api'; import { PluginSetupContract } from '../../../../../features/server'; -import { DynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../common/runtime_types'; export type APICaller = ( endpoint: string, @@ -31,7 +31,7 @@ export type UMSavedObjectsQueryFn<T = any, P = undefined> = ( ) => Promise<T> | T; export interface UptimeCoreSetup { - route: IRouter; + router: IRouter; } export interface UptimeCorePlugins { diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index 0176471aec1be..46f46720d4c04 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -21,10 +21,10 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte }; switch (method) { case 'GET': - this.server.route.get(routeDefinition, handler); + this.server.router.get(routeDefinition, handler); break; case 'POST': - this.server.route.post(routeDefinition, handler); + this.server.router.post(routeDefinition, handler); break; default: throw new Error(`Handler for method ${method} is not defined`); diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 2cc6f23ebaae5..24da3f3fa4d06 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -16,7 +16,7 @@ import { AlertType } from '../../../../../alerting/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; /** @@ -27,10 +27,10 @@ import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mo * so we don't have to mock them all for each test. */ const bootstrapDependencies = (customRequests?: any) => { - const route: IRouter = {} as IRouter; + const router: IRouter = {} as IRouter; // these server/libs parameters don't have any functionality, which is fine // because we aren't testing them here - const server: UptimeCoreSetup = { route }; + const server: UptimeCoreSetup = { router }; const libs: UMServerLibs = { requests: {} } as UMServerLibs; libs.requests = { ...libs.requests, ...customRequests }; return { server, libs }; @@ -52,7 +52,7 @@ const mockOptions = ( id: '', type: '', references: [], - attributes: defaultDynamicSettings, + attributes: DYNAMIC_SETTINGS_DEFAULTS, }); return { params, @@ -88,9 +88,9 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 30, + "certThresholds": Object { + "age": 365, + "expiration": 30, }, "heartbeatIndices": "heartbeat-8*", }, @@ -135,9 +135,9 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 30, + "certThresholds": Object { + "age": 365, + "expiration": 30, }, "heartbeatIndices": "heartbeat-8*", }, diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 829e6f92d3702..f9df559a3977b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -9,14 +9,14 @@ import { isRight } from 'fp-ts/lib/Either'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { i18n } from '@kbn/i18n'; import { AlertExecutorOptions } from '../../../../alerting/server'; -import { ACTION_GROUP_DEFINITIONS } from '../../../../../legacy/plugins/uptime/common/constants'; import { UptimeAlertTypeFactory } from './types'; import { GetMonitorStatusResult } from '../requests'; import { StatusCheckExecutorParamsType, StatusCheckAlertStateType, StatusCheckAlertState, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../common/runtime_types'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants'; import { savedObjectsAdapter } from '../saved_objects'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; diff --git a/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts index fb44f5727aab3..26515fb4b4c63 100644 --- a/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts +++ b/x-pack/plugins/uptime/server/lib/helper/get_histogram_interval.ts @@ -5,7 +5,7 @@ */ import DateMath from '@elastic/datemath'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; +import { QUERY } from '../../../common/constants'; export const parseRelativeDate = (dateStr: string, options = {}) => { // We need this this parsing because if user selects This week or this date diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index 539344dfca791..4aec376ceadf0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -5,6 +5,7 @@ */ import { getCerts } from '../get_certs'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getCerts', () => { let mockHits: any; @@ -18,9 +19,10 @@ describe('getCerts', () => { _score: 0, _source: { tls: { - certificate_not_valid_before: '2019-08-16T01:40:25.000Z', server: { x509: { + not_before: '2019-08-16T01:40:25.000Z', + not_after: '2020-07-16T03:15:39.000Z', subject: { common_name: 'r2.shared.global.fastly.net', }, @@ -33,12 +35,14 @@ describe('getCerts', () => { sha256: '12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d', }, }, - certificate_not_valid_after: '2020-07-16T03:15:39.000Z', }, monitor: { name: 'Real World Test', id: 'real-world-test', }, + url: { + full: 'https://fullurl.com', + }, }, fields: { 'tls.server.hash.sha256': [ @@ -86,30 +90,39 @@ describe('getCerts', () => { it('parses query result and returns expected values', async () => { const result = await getCerts({ callES: mockCallES, - dynamicSettings: { heartbeatIndices: 'heartbeat*' }, + dynamicSettings: { + heartbeatIndices: 'heartbeat*', + certThresholds: DYNAMIC_SETTINGS_DEFAULTS.certThresholds, + }, index: 1, from: 'now-2d', to: 'now+1h', search: 'my_common_name', size: 30, + sortBy: 'not_after', + direction: 'desc', }); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "certificate_not_valid_after": "2020-07-16T03:15:39.000Z", - "certificate_not_valid_before": "2019-08-16T01:40:25.000Z", - "common_name": "r2.shared.global.fastly.net", - "issuer": "GlobalSign CloudSSL CA - SHA256 - G3", - "monitors": Array [ - Object { - "id": "real-world-test", - "name": "Real World Test", - }, - ], - "sha1": "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1", - "sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d", - }, - ] + Object { + "certs": Array [ + Object { + "common_name": "r2.shared.global.fastly.net", + "issuer": "GlobalSign CloudSSL CA - SHA256 - G3", + "monitors": Array [ + Object { + "id": "real-world-test", + "name": "Real World Test", + "url": undefined, + }, + ], + "not_after": "2020-07-16T03:15:39.000Z", + "not_before": "2019-08-16T01:40:25.000Z", + "sha1": "b7b4b89ef0d0caf39d223736f0fdbb03c7b426f1", + "sha256": "12b00d04db0db8caa302bfde043e88f95baceb91e86ac143e93830b4bbec726d", + }, + ], + "total": 0, + } `); expect(mockCallES.mock.calls).toMatchInlineSnapshot(` Array [ @@ -124,9 +137,16 @@ describe('getCerts', () => { "tls.server.x509.subject.common_name", "tls.server.hash.sha1", "tls.server.hash.sha256", - "tls.certificate_not_valid_before", - "tls.certificate_not_valid_after", + "tls.server.x509.not_after", + "tls.server.x509.not_before", ], + "aggs": Object { + "total": Object { + "cardinality": Object { + "field": "tls.server.hash.sha256", + }, + }, + }, "collapse": Object { "field": "tls.server.hash.sha256", "inner_hits": Object { @@ -134,6 +154,7 @@ describe('getCerts', () => { "includes": Array [ "monitor.id", "monitor.name", + "url.full", ], }, "collapse": Object { @@ -147,13 +168,13 @@ describe('getCerts', () => { ], }, }, - "from": 1, + "from": 30, "query": Object { "bool": Object { "filter": Array [ Object { "exists": Object { - "field": "tls", + "field": "tls.server", }, }, Object { @@ -165,39 +186,32 @@ describe('getCerts', () => { }, }, ], + "minimum_should_match": 1, "should": Array [ Object { - "wildcard": Object { - "tls.server.issuer": Object { - "value": "*my_common_name*", - }, - }, - }, - Object { - "wildcard": Object { - "tls.common_name": Object { - "value": "*my_common_name*", - }, - }, - }, - Object { - "wildcard": Object { - "monitor.id": Object { - "value": "*my_common_name*", - }, - }, - }, - Object { - "wildcard": Object { - "monitor.name": Object { - "value": "*my_common_name*", - }, + "multi_match": Object { + "fields": Array [ + "monitor.id.text", + "monitor.name.text", + "url.full.text", + "tls.server.x509.subject.common_name.text", + "tls.server.x509.issuer.common_name.text", + ], + "query": "my_common_name", + "type": "phrase_prefix", }, }, ], }, }, "size": 30, + "sort": Array [ + Object { + "tls.server.x509.not_after": Object { + "order": "desc", + }, + }, + ], }, "index": "heartbeat*", }, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index cf8414a3b0a68..f8a335c387f2e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -5,14 +5,14 @@ */ import { getLatestMonitor } from '../get_latest_monitor'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getLatestMonitor', () => { let expectedGetLatestSearchParams: any; let mockEsSearchResult: any; beforeEach(() => { expectedGetLatestSearchParams = { - index: defaultDynamicSettings.heartbeatIndices, + index: DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices, body: { query: { bool: { @@ -32,7 +32,14 @@ describe('getLatestMonitor', () => { }, }, size: 1, - _source: ['url', 'monitor', 'observer', 'tls', '@timestamp'], + _source: [ + 'url', + 'monitor', + 'observer', + '@timestamp', + 'tls.server.x509.not_after', + 'tls.server.x509.not_before', + ], sort: { '@timestamp': { order: 'desc' }, }, @@ -64,7 +71,7 @@ describe('getLatestMonitor', () => { const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult); const result = await getLatestMonitor({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateStart: 'now-1h', dateEnd: 'now', monitorId: 'testMonitor', @@ -83,6 +90,10 @@ describe('getLatestMonitor', () => { "type": "http", }, "timestamp": "123456", + "tls": Object { + "not_after": undefined, + "not_before": undefined, + }, } `); expect(result.timestamp).toBe('123456'); diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index c740581734fdd..45be1df3e8d3b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -7,7 +7,7 @@ import { set } from 'lodash'; import mockChartsData from './monitor_charts_mock.json'; import { getMonitorDurationChart } from '../get_monitor_duration'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will provide expected filters', async () => { @@ -16,7 +16,7 @@ describe('ElasticsearchMonitorsAdapter', () => { const search = searchMock.bind({}); await getMonitorDurationChart({ callES: search, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, monitorId: 'fooID', dateStart: 'now-15m', dateEnd: 'now', @@ -39,7 +39,7 @@ describe('ElasticsearchMonitorsAdapter', () => { expect( await getMonitorDurationChart({ callES: search, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, monitorId: 'id', dateStart: 'now-15m', dateEnd: 'now', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index e429de9ae0d68..82e624221c301 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -6,8 +6,8 @@ import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; -import { ScopedClusterClient } from 'src/core/server/elasticsearch'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { ScopedClusterClient } from 'src/core/server'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; interface BucketItemCriteria { monitor_id: string; @@ -103,7 +103,7 @@ describe('getMonitorStatus', () => { }`; await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, filters: exampleFilter, locations: [], numTimes: 5, @@ -206,7 +206,7 @@ describe('getMonitorStatus', () => { const [callES, esMock] = setupMock([]); await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, locations: ['fairbanks', 'harrisburg'], numTimes: 1, timerange: { @@ -329,7 +329,7 @@ describe('getMonitorStatus', () => { }; const result = await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, ...clientParameters, }); expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); @@ -494,7 +494,7 @@ describe('getMonitorStatus', () => { const [callES] = setupMock(criteria); const result = await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, locations: [], numTimes: 5, timerange: { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index faeb291bb533b..e456670a5e68d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -5,7 +5,7 @@ */ import { getPingHistogram } from '../get_ping_histogram'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getPingHistogram', () => { const standardMockResponse: any = { @@ -59,7 +59,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters: null, @@ -78,7 +78,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters: null, @@ -140,7 +140,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: JSON.stringify(searchFilter), @@ -196,7 +196,7 @@ describe('getPingHistogram', () => { const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`; const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters, @@ -213,7 +213,7 @@ describe('getPingHistogram', () => { mockEsClient.mockReturnValue(standardMockResponse); const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: '', @@ -234,7 +234,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: '', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index fcf773db23de6..fd890a30cf742 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -6,7 +6,7 @@ import { getPings } from '../get_pings'; import { set } from 'lodash'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getAll', () => { let mockEsSearchResult: any; @@ -62,7 +62,7 @@ describe('getAll', () => { }, }; expectedGetAllParams = { - index: defaultDynamicSettings.heartbeatIndices, + index: DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices, body: { query: { bool: { @@ -88,7 +88,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); const result = await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -110,7 +110,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -166,7 +166,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, size: 12, }); @@ -220,7 +220,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'desc', }); @@ -274,7 +274,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, monitorId: 'testmonitorid', }); @@ -333,7 +333,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, status: 'down', }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts index 4f99fbe94d54c..6820cd69376d1 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -5,9 +5,16 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Cert, GetCertsParams } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CertResult, GetCertsParams } from '../../../common/runtime_types'; -export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({ +enum SortFields { + 'issuer' = 'tls.server.x509.issuer.common_name', + 'not_after' = 'tls.server.x509.not_after', + 'not_before' = 'tls.server.x509.not_before', + 'common_name' = 'tls.server.x509.subject.common_name', +} + +export const getCerts: UMElasticsearchQueryFn<GetCertsParams, CertResult> = async ({ callES, dynamicSettings, index, @@ -15,19 +22,29 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({ to, search, size, + sortBy, + direction, }) => { - const searchWrapper = `*${search}*`; + const sort = SortFields[sortBy as keyof typeof SortFields]; + const params: any = { index: dynamicSettings.heartbeatIndices, body: { - from: index, + from: index * size, size, + sort: [ + { + [sort]: { + order: direction, + }, + }, + ], query: { bool: { filter: [ { exists: { - field: 'tls', + field: 'tls.server', }, }, { @@ -48,14 +65,14 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({ 'tls.server.x509.subject.common_name', 'tls.server.hash.sha1', 'tls.server.hash.sha256', - 'tls.certificate_not_valid_before', - 'tls.certificate_not_valid_after', + 'tls.server.x509.not_after', + 'tls.server.x509.not_before', ], collapse: { field: 'tls.server.hash.sha256', inner_hits: { _source: { - includes: ['monitor.id', 'monitor.name'], + includes: ['monitor.id', 'monitor.name', 'url.full'], }, collapse: { field: 'monitor.id', @@ -64,72 +81,67 @@ export const getCerts: UMElasticsearchQueryFn<GetCertsParams, Cert[]> = async ({ sort: [{ 'monitor.id': 'asc' }], }, }, + aggs: { + total: { + cardinality: { + field: 'tls.server.hash.sha256', + }, + }, + }, }, }; if (search) { + params.body.query.bool.minimum_should_match = 1; params.body.query.bool.should = [ { - wildcard: { - 'tls.server.issuer': { - value: searchWrapper, - }, - }, - }, - { - wildcard: { - 'tls.common_name': { - value: searchWrapper, - }, - }, - }, - { - wildcard: { - 'monitor.id': { - value: searchWrapper, - }, - }, - }, - { - wildcard: { - 'monitor.name': { - value: searchWrapper, - }, + multi_match: { + query: escape(search), + type: 'phrase_prefix', + fields: [ + 'monitor.id.text', + 'monitor.name.text', + 'url.full.text', + 'tls.server.x509.subject.common_name.text', + 'tls.server.x509.issuer.common_name.text', + ], }, }, ]; } const result = await callES('search', params); - const formatted = (result?.hits?.hits ?? []).map((hit: any) => { + + const certs = (result?.hits?.hits ?? []).map((hit: any) => { const { _source: { - tls: { - server: { - x509: { - issuer: { common_name: issuer }, - subject: { common_name }, - }, - hash: { sha1, sha256 }, - }, - certificate_not_valid_after, - certificate_not_valid_before, - }, + tls: { server }, }, } = hit; + + const notAfter = server?.x509?.not_after; + const notBefore = server?.x509?.not_before; + const issuer = server?.x509?.issuer?.common_name; + const commonName = server?.x509?.subject?.common_name; + const sha1 = server?.hash?.sha1; + const sha256 = server?.hash?.sha256; + const monitors = hit.inner_hits.monitors.hits.hits.map((monitor: any) => ({ name: monitor._source?.monitor.name, id: monitor._source?.monitor.id, + url: monitor._source?.url?.full, })); + return { monitors, - certificate_not_valid_after, - certificate_not_valid_before, issuer, sha1, sha256, - common_name, + not_after: notAfter, + not_before: notBefore, + common_name: commonName, }; }); - return formatted; + const total = result?.aggregations?.total?.value ?? 0; + return { certs, total }; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts index 95d23ddcbf466..dbe71cf689214 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_filter_bar.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { OverviewFilters } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { OverviewFilters } from '../../../common/runtime_types'; import { generateFilterAggs } from './generate_filter_aggs'; export interface GetFilterBarParams { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts index 6f7854d35b308..7688f04f1acd9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_status.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { StatesIndexStatus } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { StatesIndexStatus } from '../../../common/runtime_types'; export const getIndexStatus: UMElasticsearchQueryFn<{}, StatesIndexStatus> = async ({ callES, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index a8e9ccb875a08..db34de5159213 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -5,7 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Ping } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { Ping } from '../../../common/runtime_types'; export interface GetLatestMonitorParams { /** @member dateRangeStart timestamp bounds */ @@ -45,7 +45,14 @@ export const getLatestMonitor: UMElasticsearchQueryFn<GetLatestMonitorParams, Pi }, }, size: 1, - _source: ['url', 'monitor', 'observer', 'tls', '@timestamp'], + _source: [ + 'url', + 'monitor', + 'observer', + '@timestamp', + 'tls.server.x509.not_after', + 'tls.server.x509.not_before', + ], sort: { '@timestamp': { order: 'desc' }, }, @@ -54,8 +61,13 @@ export const getLatestMonitor: UMElasticsearchQueryFn<GetLatestMonitorParams, Pi const result = await callES('search', params); const doc = result.hits?.hits?.[0]; - const source = doc?._source ?? {}; const docId = doc?._id ?? ''; + const { tls, ...ping } = doc?._source ?? {}; - return { ...source, docId, timestamp: source['@timestamp'] }; + return { + ...ping, + docId, + timestamp: ping['@timestamp'], + tls: { not_after: tls?.server?.x509?.not_after, not_before: tls?.server?.x509?.not_before }, + }; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts index 4ce7176b57b19..cf4ffa339ddfc 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_details.ts @@ -5,10 +5,7 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { - MonitorDetails, - MonitorError, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorDetails, MonitorError } from '../../../common/runtime_types'; export interface GetMonitorDetailsParams { monitorId: string; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts index e9c745b0a8713..ea2a7e790652b 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_duration.ts @@ -5,11 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; -import { - LocationDurationLine, - MonitorDurationResult, -} from '../../../../../legacy/plugins/uptime/common/types'; +import { LocationDurationLine, MonitorDurationResult } from '../../../common/types'; +import { QUERY } from '../../../common/constants'; export interface GetMonitorChartsParams { /** @member monitorId ID value for the selected monitor */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts index f49e404ffb084..c8d3ca043edc5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_locations.ts @@ -5,11 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { UNNAMED_LOCATION } from '../../../../../legacy/plugins/uptime/common/constants'; -import { - MonitorLocations, - MonitorLocation, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorLocations, MonitorLocation } from '../../../common/runtime_types'; +import { UNNAMED_LOCATION } from '../../../common/constants'; /** * Fetch data for the monitor page title. diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 4b40943a85705..b1791dd04861c 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -4,15 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { CONTEXT_DEFAULTS } from '../../../common/constants'; import { fetchPage } from './search'; import { UMElasticsearchQueryFn } from '../adapters'; -import { - SortOrder, - CursorDirection, - MonitorSummary, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; - +import { MonitorSummary, SortOrder, CursorDirection } from '../../../common/runtime_types'; import { QueryContext } from './search'; export interface CursorPagination { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 5a8927764ea5c..299913c8dff08 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -5,12 +5,9 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { QUERY } from '../../../../../legacy/plugins/uptime/common/constants'; import { getFilterClause } from '../helper'; -import { - HistogramResult, - HistogramQueryResult, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { HistogramResult, HistogramQueryResult } from '../../../common/runtime_types'; +import { QUERY } from '../../../common/constants'; export interface GetPingHistogramParams { /** @member dateRangeStart timestamp bounds */ diff --git a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts index 6eccfdb13cef7..a6a0e3c3d6542 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_pings.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_pings.ts @@ -10,7 +10,7 @@ import { HttpResponseBody, PingsResponse, Ping, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../common/runtime_types'; const DEFAULT_PAGE_SIZE = 25; diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 01f2ad88161cf..b57bc87d45418 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -5,8 +5,8 @@ */ import { UMElasticsearchQueryFn } from '../adapters'; -import { Snapshot } from '../../../../../legacy/plugins/uptime/common/runtime_types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { CONTEXT_DEFAULTS } from '../../../common/constants'; +import { Snapshot } from '../../../common/runtime_types'; import { QueryContext } from './search'; export interface GetSnapshotCountParams { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts index 2a8f681ab3453..d4ad80c85ec3d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts @@ -12,7 +12,7 @@ import { MonitorGroupsPage, } from '../fetch_page'; import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummary } from '../../../../../common/runtime_types'; import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; const simpleFixture: MonitorGroups[] = [ diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts index 84774cdeed856..e53fff429dd8d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/query_context.test.ts @@ -6,10 +6,7 @@ import { QueryContext } from '../query_context'; import { CursorPagination } from '../types'; -import { - CursorDirection, - SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../../common/runtime_types'; describe(QueryContext, () => { // 10 minute range diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts index 47034c2130116..40775bde1c7f5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/test_helpers.ts @@ -5,10 +5,7 @@ */ import { CursorPagination } from '../types'; -import { - CursorDirection, - SortOrder, -} from '../../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../../common/runtime_types'; import { QueryContext } from '../query_context'; export const prevPagination = (key: any): CursorPagination => { 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 4739c804d24e7..ee833e4c5abf6 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,14 +6,14 @@ import { get, sortBy } from 'lodash'; import { QueryContext } from './query_context'; -import { QUERY, STATES } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { QUERY } from '../../../../common/constants'; import { Check, Histogram, MonitorSummary, CursorDirection, SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +} from '../../../../common/runtime_types'; import { MonitorEnricher } from './fetch_page'; export const enrichMonitorGroups: MonitorEnricher = async ( @@ -137,11 +137,11 @@ export const enrichMonitorGroups: MonitorEnricher = async ( if (curCheck.tls == null) { curCheck.tls = new HashMap(); } - if (!doc["tls.certificate_not_valid_after"].isEmpty()) { - curCheck.tls.certificate_not_valid_after = doc["tls.certificate_not_valid_after"][0]; + if (!doc["tls.server.x509.not_after"].isEmpty()) { + curCheck.tls.not_after = doc["tls.server.x509.not_after"][0]; } - if (!doc["tls.certificate_not_valid_before"].isEmpty()) { - curCheck.tls.certificate_not_valid_before = doc["tls.certificate_not_valid_before"][0]; + if (!doc["tls.server.x509.not_before"].isEmpty()) { + curCheck.tls.not_before = doc["tls.server.x509.not_before"][0]; } state.checksByAgentIdIP[agentIdIP] = curCheck; @@ -314,7 +314,7 @@ const getHistogramForMonitors = async ( by_id: { terms: { field: 'monitor.id', - size: STATES.LEGACY_STATES_QUERY_SIZE, + size: queryContext.size, }, aggs: { histogram: { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts index 84167840d5d9b..bef8106ad1896 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts @@ -7,12 +7,8 @@ import { flatten } from 'lodash'; import { CursorPagination } from './types'; import { QueryContext } from './query_context'; -import { QUERY } from '../../../../../../legacy/plugins/uptime/common/constants'; -import { - CursorDirection, - MonitorSummary, - SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { QUERY } from '../../../../common/constants'; +import { CursorDirection, MonitorSummary, SortOrder } from '../../../../common/runtime_types'; import { enrichMonitorGroups } from './enrich_monitor_groups'; import { MonitorGroupIterator } from './monitor_group_iterator'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 3449febfa5b05..e60c52660915a 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { QueryContext } from './query_context'; // This is the first phase of the query. In it, we find the most recent check groups that matched the given query. diff --git a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts index 31d9166eb1e73..2fb9562028258 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts @@ -6,7 +6,7 @@ import { QueryContext } from './query_context'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { MonitorGroups } from './fetch_page'; import { CursorPagination } from './types'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 43fc54fb25808..977c32ad1f984 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -5,7 +5,7 @@ */ import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection } from '../../../../common/runtime_types'; import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; /** diff --git a/x-pack/plugins/uptime/server/lib/requests/search/types.ts b/x-pack/plugins/uptime/server/lib/requests/search/types.ts index 2ec52d400b597..35e9647196454 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/types.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/types.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CursorDirection, - SortOrder, -} from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { CursorDirection, SortOrder } from '../../../../common/runtime_types'; export interface CursorPagination { cursorKey?: any; diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 84154429b9188..367db924cf1c6 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -6,12 +6,20 @@ import { UMElasticsearchQueryFn } from '../adapters'; import { + OverviewFilters, + MonitorDetails, + MonitorLocations, + Snapshot, + StatesIndexStatus, HistogramResult, Ping, - PingsResponse as PingResults, + PingsResponse, GetCertsParams, GetPingsParams, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; + CertResult, +} from '../../../common/runtime_types'; +import { MonitorDurationResult } from '../../../common/types'; + import { GetFilterBarParams, GetLatestMonitorParams, @@ -23,22 +31,13 @@ import { GetMonitorStatusParams, GetMonitorStatusResult, } from '.'; -import { - OverviewFilters, - MonitorDetails, - MonitorLocations, - Snapshot, - StatesIndexStatus, - Cert, -} from '../../../../../legacy/plugins/uptime/common/runtime_types'; import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; -import { MonitorDurationResult } from '../../../../../legacy/plugins/uptime/common/types'; type ESQ<P, R> = UMElasticsearchQueryFn<P, R>; export interface UptimeRequests { - getCerts: ESQ<GetCertsParams, Cert[]>; + getCerts: ESQ<GetCertsParams, CertResult>; getFilterBar: ESQ<GetFilterBarParams, OverviewFilters>; getIndexPattern: ESQ<{}, {}>; getLatestMonitor: ESQ<GetLatestMonitorParams, Ping>; @@ -47,7 +46,7 @@ export interface UptimeRequests { getMonitorLocations: ESQ<GetMonitorLocationsParams, MonitorLocations>; getMonitorStates: ESQ<GetMonitorStatesParams, GetMonitorStatesResult>; getMonitorStatus: ESQ<GetMonitorStatusParams, GetMonitorStatusResult[]>; - getPings: ESQ<GetPingsParams, PingResults>; + getPings: ESQ<GetPingsParams, PingsResponse>; getPingHistogram: ESQ<GetPingHistogramParams, HistogramResult>; getSnapshotCount: ESQ<GetSnapshotCountParams, Snapshot>; getIndexStatus: ESQ<{}, StatesIndexStatus>; diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 3ccfd498c44bf..28b9eaad2cf6f 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - DynamicSettings, - defaultDynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../common/constants'; +import { DynamicSettings } from '../../common/runtime_types'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; @@ -28,12 +26,12 @@ export const umDynamicSettings: SavedObjectsType = { heartbeatIndices: { type: 'keyword', }, - certificatesThresholds: { + certThresholds: { properties: { - errorState: { + expiration: { type: 'long', }, - warningState: { + age: { type: 'long', }, }, @@ -49,7 +47,7 @@ export const savedObjectsAdapter: UMSavedObjectsAdapter = { return obj.attributes; } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { - return defaultDynamicSettings; + return DYNAMIC_SETTINGS_DEFAULTS; } throw getErr; } diff --git a/x-pack/plugins/uptime/server/plugin.ts b/x-pack/plugins/uptime/server/plugin.ts index 7cc591a6b2db1..13d1ae216f204 100644 --- a/x-pack/plugins/uptime/server/plugin.ts +++ b/x-pack/plugins/uptime/server/plugin.ts @@ -8,19 +8,20 @@ import { PluginInitializerContext, CoreStart, CoreSetup, + Plugin as PluginType, ISavedObjectsRepository, } from '../../../../src/core/server'; import { initServerWithKibana } from './kibana.index'; import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters'; import { umDynamicSettings } from './lib/saved_objects'; -export class Plugin { +export class Plugin implements PluginType { private savedObjectsClient?: ISavedObjectsRepository; constructor(_initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, plugins: UptimeCorePlugins) { - initServerWithKibana({ route: core.http.createRouter() }, plugins); + initServerWithKibana({ router: core.http.createRouter() }, plugins); core.savedObjects.registerType(umDynamicSettings); KibanaTelemetryAdapter.registerUsageCollector( plugins.usageCollection, @@ -28,7 +29,9 @@ export class Plugin { ); } - public start(_core: CoreStart, _plugins: any) { - this.savedObjectsClient = _core.savedObjects.createInternalRepository(); + public start(core: CoreStart, _plugins: any) { + this.savedObjectsClient = core.savedObjects.createInternalRepository(); } + + public stop() {} } diff --git a/x-pack/plugins/uptime/server/rest_api/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs.ts deleted file mode 100644 index 31fb3f4ab96a7..0000000000000 --- a/x-pack/plugins/uptime/server/rest_api/certs.ts +++ /dev/null @@ -1,54 +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 { schema } from '@kbn/config-schema'; -import { UMServerLibs } from '../lib/lib'; -import { UMRestApiRouteFactory } from '.'; -import { API_URLS } from '../../../../legacy/plugins/uptime/common/constants/rest_api'; - -const DEFAULT_INDEX = 0; -const DEFAULT_SIZE = 25; -const DEFAULT_FROM = 'now-1d'; -const DEFAULT_TO = 'now'; - -export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ - method: 'GET', - path: API_URLS.CERTS, - validate: { - query: schema.object({ - from: schema.maybe(schema.string()), - to: schema.maybe(schema.string()), - search: schema.maybe(schema.string()), - index: schema.maybe(schema.number()), - size: schema.maybe(schema.number()), - }), - }, - writeAccess: false, - options: { - tags: ['access:uptime-read'], - }, - handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { - const index = request.query?.index ?? DEFAULT_INDEX; - const size = request.query?.size ?? DEFAULT_SIZE; - const from = request.query?.from ?? DEFAULT_FROM; - const to = request.query?.to ?? DEFAULT_TO; - const { search } = request.query; - - return response.ok({ - body: { - certs: await libs.requests.getCerts({ - callES, - dynamicSettings, - index, - search, - size, - from, - to, - }), - }, - }); - }, -}); diff --git a/x-pack/plugins/uptime/server/rest_api/certs/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts new file mode 100644 index 0000000000000..a5ca6e264d299 --- /dev/null +++ b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts @@ -0,0 +1,59 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { API_URLS } from '../../../common/constants'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteFactory } from '../types'; + +const DEFAULT_INDEX = 0; +const DEFAULT_SIZE = 25; +const DEFAULT_FROM = 'now-1d'; +const DEFAULT_TO = 'now'; +const DEFAULT_SORT = 'not_after'; +const DEFAULT_DIRECTION = 'asc'; + +export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: API_URLS.CERTS, + validate: { + query: schema.object({ + from: schema.maybe(schema.string()), + to: schema.maybe(schema.string()), + search: schema.maybe(schema.string()), + index: schema.maybe(schema.number()), + size: schema.maybe(schema.number()), + sortBy: schema.maybe(schema.string()), + direction: schema.maybe(schema.string()), + }), + }, + handler: async ({ callES, dynamicSettings }, _context, request, response): Promise<any> => { + const index = request.query?.index ?? DEFAULT_INDEX; + const size = request.query?.size ?? DEFAULT_SIZE; + const from = request.query?.from ?? DEFAULT_FROM; + const to = request.query?.to ?? DEFAULT_TO; + const sortBy = request.query?.sortBy ?? DEFAULT_SORT; + const direction = request.query?.direction ?? DEFAULT_DIRECTION; + const { search } = request.query; + const result = await libs.requests.getCerts({ + callES, + dynamicSettings, + index, + search, + size, + from, + to, + sortBy, + direction, + }); + return response.ok({ + body: { + certs: result.certs, + total: result.total, + }, + }); + }, +}); diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts index 3f4e2fc345182..31833a25ee8ac 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -8,10 +8,7 @@ import { schema } from '@kbn/config-schema'; import { isRight } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../lib/lib'; -import { - DynamicSettings, - DynamicSettingsType, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings, DynamicSettingsType } from '../../common/runtime_types'; import { UMRestApiRouteFactory } from '.'; import { savedObjectsAdapter } from '../lib/saved_objects'; diff --git a/x-pack/plugins/uptime/server/rest_api/index.ts b/x-pack/plugins/uptime/server/rest_api/index.ts index a7a63342d11d4..2b598be284e1c 100644 --- a/x-pack/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/plugins/uptime/server/rest_api/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createGetCertsRoute } from './certs'; +import { createGetCertsRoute } from './certs/certs'; import { createGetOverviewFilters } from './overview_filters'; import { createGetPingHistogramRoute, createGetPingsRoute } from './pings'; import { createGetDynamicSettingsRoute, createPostDynamicSettingsRoute } from './dynamic_settings'; @@ -19,6 +19,7 @@ import { } from './monitors'; import { createGetMonitorDurationRoute } from './monitors/monitors_durations'; import { createGetIndexPatternRoute, createGetIndexStatusRoute } from './index_state'; + export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; export { uptimeRouteWrapper } from './uptime_route_wrapper'; diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 689a75c5903a6..26715f0ff37b6 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -6,7 +6,7 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index 8ed73d90b2389..9a4280efa98f9 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -6,7 +6,7 @@ import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index 5cb4e8a6241b7..60b3eafaa765e 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -6,8 +6,7 @@ import { schema } from '@kbn/config-schema'; import { UMRestApiRouteFactory } from '../types'; -import { CONTEXT_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS, CONTEXT_DEFAULTS } from '../../../common/constants'; export const createMonitorListRoute: UMRestApiRouteFactory = libs => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index 66ce9871506d4..a110209043a7e 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts index 9cf1340fb9409..bb002f8a8c286 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 1cc010781457e..69e719efb0719 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index 9743ced13350a..34313211061b0 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index deac05f36c8dc..00cbaf0d16723 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { objectValuesToArrays } from '../../lib/helper'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; const arrayOrStringType = schema.maybe( schema.oneOf([schema.string(), schema.arrayOf(schema.string())]) diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index dceef5ecb7848..41078f735920b 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; +import { API_URLS } from '../../../common/constants'; export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index 80a887a7f64a9..d97195a7fe2b1 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -9,8 +9,8 @@ import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants/rest_api'; -import { GetPingsParamsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../common/constants'; +import { GetPingsParamsType } from '../../../common/runtime_types'; export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index d870f49280117..7809e102a499f 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index 4b2db71037071..d8387e79e9089 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { KibanaTelemetryAdapter } from '../../lib/adapters/telemetry'; import { UMRestApiRouteFactory } from '../types'; import { PageViewParams } from '../../lib/adapters/telemetry/types'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../common/constants'; export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ method: 'POST', diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index e05e7a4d7faf1..8720b9dc60b12 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -15,8 +15,8 @@ import { KibanaRequest, KibanaResponseFactory, IKibanaResponse, -} from 'src/core/server'; -import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +} from 'kibana/server'; +import { DynamicSettings } from '../../common/runtime_types'; import { UMServerLibs } from '../lib/lib'; /** diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 974f3eb6db60f..efe1f85905970 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -14,7 +14,6 @@ const alwaysImportedTests = [ ]; const onlyNotInCoverageTests = [ require.resolve('../test/reporting/configs/chromium_api.js'), - require.resolve('../test/reporting/configs/chromium_functional.js'), require.resolve('../test/reporting/configs/generate_api.js'), require.resolve('../test/api_integration/config_security_basic.js'), require.resolve('../test/api_integration/config.js'), diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts index a7551ad7e2fad..1244657ed9988 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/servicenow.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts index 46258e41d5d69..4151deab45213 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/slack.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts index 338610e9243a4..bae6dada48fb7 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/webhook.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 457b7621e84bd..72a2774e672f1 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -8,7 +8,7 @@ import path from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; -import { getAllExternalServiceSimulatorPaths } from './fixtures/plugins/actions'; +import { getAllExternalServiceSimulatorPaths } from './fixtures/plugins/actions_simulators'; interface CreateTestConfigOptions { license: string; @@ -23,6 +23,7 @@ const enabledActionTypes = [ '.pagerduty', '.server-log', '.servicenow', + '.jira', '.slack', '.webhook', 'test.authorization', @@ -75,7 +76,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'some.non.existent.com', ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify([ { @@ -124,7 +124,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, - `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions_simulators')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'task_manager')}`, `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'aad')}`, `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts index 05139213b76b9..400aec7e11c8d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -21,7 +21,7 @@ interface CheckAADRequest extends Hapi.Request { // eslint-disable-next-line import/no-default-export export default function(kibana: any) { return new kibana.Plugin({ - require: ['actions', 'alerting', 'encryptedSavedObjects'], + require: ['encryptedSavedObjects'], name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts deleted file mode 100644 index 019b15cc1862a..0000000000000 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts +++ /dev/null @@ -1,86 +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 Hapi from 'hapi'; -import { PluginSetupContract as ActionsPluginSetupContract } from '../../../../../../plugins/actions/server/plugin'; -import { ActionType } from '../../../../../../plugins/actions/server'; - -import { initPlugin as initPagerduty } from './pagerduty_simulation'; -import { initPlugin as initServiceNow } from './servicenow_simulation'; -import { initPlugin as initSlack } from './slack_simulation'; -import { initPlugin as initWebhook } from './webhook_simulation'; - -const NAME = 'actions-FTS-external-service-simulators'; - -export enum ExternalServiceSimulator { - PAGERDUTY = 'pagerduty', - SERVICENOW = 'servicenow', - SLACK = 'slack', - WEBHOOK = 'webhook', -} - -export function getExternalServiceSimulatorPath(service: ExternalServiceSimulator): string { - return `/api/_${NAME}/${service}`; -} - -export function getAllExternalServiceSimulatorPaths(): string[] { - const allPaths = Object.values(ExternalServiceSimulator).map(service => - getExternalServiceSimulatorPath(service) - ); - allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); - return allPaths; -} - -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - require: ['xpack_main', 'actions'], - name: NAME, - init: (server: Hapi.Server) => { - // this action is specifically NOT enabled in ../../config.ts - const notEnabledActionType: ActionType = { - id: 'test.not-enabled', - name: 'Test: Not Enabled', - minimumLicenseRequired: 'gold', - async executor() { - return { status: 'ok', actionId: '' }; - }, - }; - (server.newPlatform.setup.plugins.actions as ActionsPluginSetupContract).registerType( - notEnabledActionType - ); - server.plugins.xpack_main.registerFeature({ - id: 'actions', - name: 'Actions', - app: ['actions', 'kibana'], - privileges: { - all: { - app: ['actions', 'kibana'], - savedObject: { - all: ['action', 'action_task_params'], - read: [], - }, - ui: [], - api: ['actions-read', 'actions-all'], - }, - read: { - app: ['actions', 'kibana'], - savedObject: { - all: ['action_task_params'], - read: ['action'], - }, - ui: [], - api: ['actions-read'], - }, - }, - }); - - initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY)); - initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)); - initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); - initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); - }, - }); -} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/README.md b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/README.md similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/README.md rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/README.md diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts new file mode 100644 index 0000000000000..6e7e9e3793778 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/index.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Hapi from 'hapi'; +import { PluginSetupContract as ActionsPluginSetupContract } from '../../../../../../plugins/actions/server/plugin'; +import { ActionType } from '../../../../../../plugins/actions/server'; + +import { initPlugin as initPagerduty } from './pagerduty_simulation'; +import { initPlugin as initSlack } from './slack_simulation'; +import { initPlugin as initWebhook } from './webhook_simulation'; +import { initPlugin as initServiceNow } from './servicenow_simulation'; +import { initPlugin as initJira } from './jira_simulation'; + +const NAME = 'actions-FTS-external-service-simulators'; + +export enum ExternalServiceSimulator { + PAGERDUTY = 'pagerduty', + SERVICENOW = 'servicenow', + JIRA = 'jira', + SLACK = 'slack', + WEBHOOK = 'webhook', +} + +export function getExternalServiceSimulatorPath(service: ExternalServiceSimulator): string { + return `/api/_${NAME}/${service}`; +} + +export function getAllExternalServiceSimulatorPaths(): string[] { + const allPaths = Object.values(ExternalServiceSimulator).map(service => + getExternalServiceSimulatorPath(service) + ); + + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); + return allPaths; +} + +// eslint-disable-next-line import/no-default-export +export default function(kibana: any) { + return new kibana.Plugin({ + require: ['xpack_main'], + name: NAME, + init: (server: Hapi.Server) => { + // this action is specifically NOT enabled in ../../config.ts + const notEnabledActionType: ActionType = { + id: 'test.not-enabled', + name: 'Test: Not Enabled', + minimumLicenseRequired: 'gold', + async executor() { + return { status: 'ok', actionId: '' }; + }, + }; + (server.newPlatform.setup.plugins.actions as ActionsPluginSetupContract).registerType( + notEnabledActionType + ); + server.plugins.xpack_main.registerFeature({ + id: 'actions', + name: 'Actions', + app: ['actions', 'kibana'], + privileges: { + all: { + app: ['actions', 'kibana'], + savedObject: { + all: ['action', 'action_task_params'], + read: [], + }, + ui: [], + api: ['actions-read', 'actions-all'], + }, + read: { + app: ['actions', 'kibana'], + savedObject: { + all: ['action_task_params'], + read: ['action'], + }, + ui: [], + api: ['actions-read'], + }, + }, + }); + + initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY)); + initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); + initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); + initServiceNow(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)); + initJira(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA)); + }, + }); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts new file mode 100644 index 0000000000000..629d0197b2292 --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/jira_simulation.ts @@ -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 Hapi from 'hapi'; + +interface JiraRequest extends Hapi.Request { + payload: { + summary: string; + description?: string; + comments?: string; + }; +} +export function initPlugin(server: Hapi.Server, path: string) { + server.route({ + method: 'POST', + path: `${path}/rest/api/2/issue`, + options: { + auth: false, + }, + handler: createHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'PUT', + path: `${path}/rest/api/2/issue/{id}`, + options: { + auth: false, + }, + handler: updateHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'GET', + path: `${path}/rest/api/2/issue/{id}`, + options: { + auth: false, + }, + handler: getHandler as Hapi.Lifecycle.Method, + }); + + server.route({ + method: 'POST', + path: `${path}/rest/api/2/issue/{id}/comment`, + options: { + auth: false, + }, + handler: createCommentHanlder as Hapi.Lifecycle.Method, + }); +} + +// ServiceNow simulator: create a servicenow action pointing here, and you can get +// different responses based on the message posted. See the README.md for +// more info. +function createHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + }); +} + +function updateHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', + }); +} + +function getHandler(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + key: 'CK-1', + created: '2020-04-27T14:17:45.490Z', + updated: '2020-04-27T14:17:45.490Z', + summary: 'title', + description: 'description', + }); +} + +function createCommentHanlder(request: JiraRequest, h: any) { + return jsonResponse(h, 200, { + id: '123', + created: '2020-04-27T14:17:45.490Z', + }); +} + +function jsonResponse(h: any, code: number, object?: any) { + if (object == null) { + return h.response('').code(code); + } + + return h + .response(JSON.stringify(object)) + .type('application/json') + .code(code); +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/package.json b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/package.json similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/package.json rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/package.json diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/pagerduty_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/pagerduty_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/pagerduty_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/pagerduty_simulation.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts similarity index 92% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts index a58738e387aeb..cc9521369a47d 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/servicenow_simulation.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; import Hapi from 'hapi'; interface ServiceNowRequest extends Hapi.Request { @@ -29,18 +28,13 @@ export function initPlugin(server: Hapi.Server, path: string) { path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, - validate: { - params: Joi.object({ - id: Joi.string(), - }), - }, }, handler: updateHandler as Hapi.Lifecycle.Method, }); server.route({ method: 'GET', - path: `${path}/api/now/v2/table/incident`, + path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, }, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/slack_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/slack_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/slack_simulation.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/webhook_simulation.ts similarity index 100% rename from x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/webhook_simulation.ts rename to x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/webhook_simulation.ts diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index fe0f630830a56..1a47addf36ab3 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -11,9 +11,10 @@ import { ActionTypeExecutorOptions, ActionType } from '../../../../../../plugins // eslint-disable-next-line import/no-default-export export default function(kibana: any) { return new kibana.Plugin({ - require: ['xpack_main', 'actions', 'alerting', 'elasticsearch'], - name: 'alerts', + require: ['xpack_main', 'elasticsearch'], + name: 'alerts-fixture', init(server: any) { + const clusterClient = server.newPlatform.start.core.elasticsearch.legacy.client; server.plugins.xpack_main.registerFeature({ id: 'alerting', name: 'Alerting', @@ -165,6 +166,22 @@ export default function(kibana: any) { } catch (e) { callClusterError = e; } + // Call scoped cluster + const callScopedCluster = services.getScopedCallCluster(clusterClient); + let callScopedClusterSuccess = false; + let callScopedClusterError; + try { + await callScopedCluster('index', { + index: params.callClusterAuthorizationIndex, + refresh: 'wait_for', + body: { + param1: 'test', + }, + }); + callScopedClusterSuccess = true; + } catch (e) { + callScopedClusterError = e; + } // Saved objects client let savedObjectsClientSuccess = false; let savedObjectsClientError; @@ -185,6 +202,8 @@ export default function(kibana: any) { state: { callClusterSuccess, callClusterError, + callScopedClusterSuccess, + callScopedClusterError, savedObjectsClientSuccess, savedObjectsClientError, }, @@ -376,6 +395,22 @@ export default function(kibana: any) { } catch (e) { callClusterError = e; } + // Call scoped cluster + const callScopedCluster = services.getScopedCallCluster(clusterClient); + let callScopedClusterSuccess = false; + let callScopedClusterError; + try { + await callScopedCluster('index', { + index: params.callClusterAuthorizationIndex, + refresh: 'wait_for', + body: { + param1: 'test', + }, + }); + callScopedClusterSuccess = true; + } catch (e) { + callScopedClusterError = e; + } // Saved objects client let savedObjectsClientSuccess = false; let savedObjectsClientError; @@ -396,6 +431,8 @@ export default function(kibana: any) { state: { callClusterSuccess, callClusterError, + callScopedClusterSuccess, + callScopedClusterError, savedObjectsClientSuccess, savedObjectsClientError, }, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts index 29708f86b0a9b..ac32f05805e4a 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/task_manager/index.ts @@ -31,7 +31,7 @@ const taskByIdQuery = (id: string) => ({ export default function(kibana: any) { return new kibana.Plugin({ name: 'taskManagerHelpers', - require: ['elasticsearch', 'task_manager'], + require: ['elasticsearch'], config(Joi: any) { return Joi.object({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts new file mode 100644 index 0000000000000..ed63d25d86aca --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -0,0 +1,549 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions_simulators'; + +const mapping = [ + { + source: 'title', + target: 'summary', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; + +// eslint-disable-next-line import/no-default-export +export default function jiraTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + const mockJira = { + config: { + apiUrl: 'www.jiraisinkibanaactions.com', + projectKey: 'CK', + casesConfiguration: { mapping }, + }, + secrets: { + apiToken: 'elastic', + email: 'elastic@elastic.co', + }, + params: { + subAction: 'pushToService', + subActionParams: { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], + }, + }, + }; + + let jiraSimulatorURL: string = '<could not determine kibana url>'; + + describe('Jira', () => { + before(() => { + jiraSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) + ); + }); + + after(() => esArchiver.unload('empty_kibana')); + + describe('Jira - Action Creation', () => { + it('should return 200 when creating a jira action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + ...mockJira.config, + apiUrl: jiraSimulatorURL, + }, + secrets: mockJira.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/action/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with no apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { projectKey: 'CK' }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with no projectKey', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { apiUrl: jiraSimulatorURL }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [projectKey]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with a non whitelisted apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: 'http://jira.mynonexistent.com', + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + secrets: mockJira.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring connector action: target url "http://jira.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action without secrets', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [email]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action without casesConfiguration', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + }, + secrets: mockJira.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with empty mapping', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: { mapping: [] }, + }, + secrets: mockJira.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a jira action with wrong actionType', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'description', + actionType: 'non-supported', + }, + ], + }, + }, + secrets: mockJira.secrets, + }) + .expect(400); + }); + }); + + describe('Jira - Executor', () => { + let simulatedActionId: string; + before(async () => { + const { body } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira simulator', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: mockJira.config.projectKey, + casesConfiguration: mockJira.config.casesConfiguration, + }, + secrets: mockJira.secrets, + }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: `error validating action params: Cannot read property 'Symbol(Symbol.iterator)' of undefined`, + }); + }); + }); + + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + }); + }); + }); + + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without caseId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService', subActionParams: {} }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + caseId: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + caseId: 'success', + title: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success', comment: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident without comments', async () => { + const { body } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + ...mockJira.params.subActionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(body).to.eql({ + status: 'ok', + actionId: simulatedActionId, + data: { + id: '123', + title: 'CK-1', + pushedDate: '2020-04-27T14:17:45.490Z', + url: `${jiraSimulatorURL}/browse/CK-1`, + }, + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index eeb0818b5fbab..4c76ebfb93b0b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function pagerdutyTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 054f8f6141817..04cd06999f432 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -11,9 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; - -// node ../scripts/functional_test_runner.js --grep "servicenow" --config=test/alerting_api_integration/security_and_spaces/config.ts +} from '../../../../common/fixtures/plugins/actions_simulators'; const mapping = [ { @@ -24,7 +22,7 @@ const mapping = [ { source: 'description', target: 'description', - actionType: 'append', + actionType: 'overwrite', }, { source: 'comments', @@ -42,40 +40,41 @@ export default function servicenowTest({ getService }: FtrProviderContext) { const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', - casesConfiguration: { mapping: [...mapping] }, + casesConfiguration: { mapping }, }, secrets: { password: 'elastic', username: 'changeme', }, params: { - caseId: '123', - title: 'a title', - description: 'a description', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - incidentId: null, - comments: [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'first comment', - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, - }, - ], + subAction: 'pushToService', + subActionParams: { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + externalId: null, + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: 'elastic' }, + updatedAt: null, + updatedBy: null, + }, + ], + }, }, }; - describe('servicenow', () => { - let simulatedActionId = ''; - let servicenowSimulatorURL: string = '<could not determine kibana url>'; + let servicenowSimulatorURL: string = '<could not determine kibana url>'; - // need to wait for kibanaServer to settle ... + describe('ServiceNow', () => { before(() => { servicenowSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) @@ -84,351 +83,438 @@ export default function servicenowTest({ getService }: FtrProviderContext) { after(() => esArchiver.unload('empty_kibana')); - it('should return 200 when creating a servicenow action successfully', async () => { - const { body: createdAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ + describe('ServiceNow - Action Creation', () => { + it('should return 200 when creating a servicenow action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(200); - - expect(createdAction).to.eql({ - id: createdAction.id, - isPreconfigured: false, - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - }); - - expect(typeof createdAction.id).to.be('string'); - - const { body: fetchedAction } = await supertest - .get(`/api/action/${createdAction.id}`) - .expect(200); - - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - isPreconfigured: false, - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - }); - }); - - it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: {}, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', - }); }); - }); - it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: 'http://servicenow.mynonexistent.com', - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: error configuring servicenow action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', - }); - }); - }); + const { body: fetchedAction } = await supertest + .get(`/api/action/${createdAction.id}`) + .expect(200); - it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, name: 'A servicenow action', actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type secrets: [password]: expected value of type [string] but got [undefined]', - }); }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', + it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); }); - }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { mapping: [] }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: 'http://servicenow.mynonexistent.com', + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not whitelisted in the Kibana config xpack.actions.whitelistedHosts', + }); }); - }); - }); + }); - it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { - await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow action', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'description', - actionType: 'non-supported', - }, - ], + it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(400); - }); - - it('should create our servicenow simulator action successfully', async () => { - const { body: createdSimulatedAction } = await supertest - .post('/api/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A servicenow simulator', - actionTypeId: '.servicenow', - config: { - apiUrl: servicenowSimulatorURL, - casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, - }, - secrets: { ...mockServiceNow.secrets }, - }) - .expect(200); + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [password]: expected value of type [string] but got [undefined]', + }); + }); + }); - simulatedActionId = createdSimulatedAction.id; - }); + it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', + }); + }); + }); - it('should handle executing with a simulated success', async () => { - const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { ...mockServiceNow.params, title: 'success', comments: [] }, - }) - .expect(200); + it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { mapping: [] }, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + }); + }); + }); - expect(result).to.eql({ - status: 'ok', - actionId: simulatedActionId, - data: { - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, - }, + it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { + await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'description', + actionType: 'non-supported', + }, + ], + }, + }, + secrets: mockServiceNow.secrets, + }) + .expect(400); }); }); - it('should handle failing with a simulated success without caseId', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: {}, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [caseId]: expected value of type [string] but got [undefined]', + describe('ServiceNow - Executor', () => { + let simulatedActionId: string; + before(async () => { + const { body } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow simulator', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: mockServiceNow.config.casesConfiguration, + }, + secrets: mockServiceNow.secrets, }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: `error validating action params: Cannot read property 'Symbol(Symbol.iterator)' of undefined`, + }); + }); }); - }); - it('should handle failing with a simulated success without title', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { caseId: 'success' }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [title]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + }); + }); }); - }); - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { caseId: 'success', title: 'success' }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [createdAt]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without commentId', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{}], - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [comments.0.commentId]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without caseId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService', subActionParams: {} }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without comment message', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success' }], - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [comments.0.comment]: expected value of type [string] but got [undefined]', - }); + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + caseId: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', + }); + }); }); - }); - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - caseId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + caseId: 'success', + title: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success', comment: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident without comments', async () => { + const { body: result } = await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + ...mockServiceNow.params.subActionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(result).to.eql({ + status: 'ok', actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: [comments.0.createdAt]: expected value of type [string] but got [undefined]', + data: { + id: '123', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }, }); }); + }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index e00589b7e85b7..386254e49c19c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index fd996ea4507ba..9b66326fa6157 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; const defaultValues: Record<string, any> = { headers: null, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts index a58e14dd563ef..af8af72d458fd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/execute.ts @@ -436,11 +436,16 @@ export default function({ getService }: FtrProviderContext) { indexedRecord = searchResult.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: false, + callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { ...indexedRecord._source.state.callClusterError, statusCode: 403, }, + callScopedClusterError: { + ...indexedRecord._source.state.callScopedClusterError, + statusCode: 403, + }, savedObjectsClientError: { ...indexedRecord._source.state.savedObjectsClientError, output: { @@ -457,6 +462,7 @@ export default function({ getService }: FtrProviderContext) { indexedRecord = searchResult.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...indexedRecord._source.state.savedObjectsClientError, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index c84b089d48c85..e43f9dba4b2dc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -141,9 +141,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { actionTypeId: '.slack', name: 'Slack#xyz', isPreconfigured: true, - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 0b637326d4667..95b564e63d715 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -73,11 +73,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -85,9 +80,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -95,11 +87,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -107,9 +94,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -194,11 +178,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -206,9 +185,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 1, }, { @@ -216,11 +192,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -228,9 +199,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -281,11 +249,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -293,9 +256,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -303,11 +263,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -315,9 +270,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index 8e002bcc8d3da..18b1714582d13 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -15,6 +15,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); + loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/slack')); loadTestFile(require.resolve('./builtin_action_types/webhook')); loadTestFile(require.resolve('./create')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index d8e4f808f5cd2..59cf22b52920c 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -469,11 +469,16 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: false, + callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { ...searchResult.hits.hits[0]._source.state.callClusterError, statusCode: 403, }, + callScopedClusterError: { + ...searchResult.hits.hits[0]._source.state.callScopedClusterError, + statusCode: 403, + }, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, output: { @@ -497,6 +502,7 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, @@ -577,11 +583,16 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: false, + callScopedClusterSuccess: false, savedObjectsClientSuccess: false, callClusterError: { ...searchResult.hits.hits[0]._source.state.callClusterError, statusCode: 403, }, + callScopedClusterError: { + ...searchResult.hits.hits[0]._source.state.callScopedClusterError, + statusCode: 403, + }, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, output: { @@ -605,6 +616,7 @@ instanceStateValue: true expect(searchResult.hits.total.value).to.eql(1); expect(searchResult.hits.hits[0]._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...searchResult.hits.hits[0]._source.state.savedObjectsClientError, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts index 5122a74d53b72..112149a32649a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/builtin_action_types/webhook.ts @@ -10,7 +10,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions'; +} from '../../../../common/fixtures/plugins/actions_simulators'; // eslint-disable-next-line import/no-default-export export default function webhookTest({ getService }: FtrProviderContext) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts index 3faa54ee0b219..715573ef1237e 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/execute.ts @@ -186,6 +186,7 @@ export default function({ getService }: FtrProviderContext) { const indexedRecord = searchResult.hits.hits[0]; expect(indexedRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...indexedRecord._source.state.savedObjectsClientError, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index a4a13441fb766..4eb8c16f4fb3a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -79,9 +79,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index ec59e56b08308..62abdddc6a1bf 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -50,11 +50,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -62,9 +57,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -72,11 +64,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -84,9 +71,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -115,11 +99,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -127,9 +106,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -137,11 +113,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -149,9 +120,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts index 0d1596a95bfbb..95ccfb897cf54 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/alerts_base.ts @@ -277,6 +277,7 @@ instanceStateValue: true )[0]; expect(alertTestRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...alertTestRecord._source.state.savedObjectsClientError, @@ -332,6 +333,7 @@ instanceStateValue: true )[0]; expect(actionTestRecord._source.state).to.eql({ callClusterSuccess: true, + callScopedClusterSuccess: true, savedObjectsClientSuccess: false, savedObjectsClientError: { ...actionTestRecord._source.state.savedObjectsClientError, diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts index 41d78995711f2..8af648e062cf4 100644 --- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -182,15 +182,21 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'development' }, settings: { transaction_sample_rate: '0.9' }, }; + const configProduction = { + service: { name: 'myservice', environment: 'production' }, + settings: { transaction_sample_rate: '0.9' }, + }; let etag: string; before(async () => { log.debug('creating agent configuration'); await createConfiguration(config); + await createConfiguration(configProduction); }); after(async () => { await deleteConfiguration(config); + await deleteConfiguration(configProduction); }); it(`should have 'applied_by_agent=false' before supplying etag`, async () => { @@ -210,17 +216,45 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); it(`should have 'applied_by_agent=true' after supplying etag`, async () => { - async function getAppliedByAgent() { + await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag, + }); + + async function hasBeenAppliedByAgent() { const { body } = await searchConfigurations({ service: { name: 'myservice', environment: 'development' }, - etag, }); return body._source.applied_by_agent; } // wait until `applied_by_agent` has been updated in elasticsearch - expect(await waitFor(getAppliedByAgent)).to.be(true); + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); + }); + it(`should have 'applied_by_agent=false' before marking as applied`, async () => { + const res1 = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + expect(res1.body._source.applied_by_agent).to.be(false); + }); + it(`should have 'applied_by_agent=true' when 'mark_as_applied_by_agent' attribute is true`, async () => { + await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + mark_as_applied_by_agent: true, + }); + + async function hasBeenAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); }); }); }); diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts index e8d336e875b99..73fe435764b74 100644 --- a/x-pack/test/api_integration/apis/endpoint/resolver.ts +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -27,72 +27,65 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('should return details for the root node', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); expect(body.events.length).to.eql(1); - expect(body.pagination.next).to.eql(cursor); - expect(body.pagination.total).to.eql(1); - // default limit - expect(body.pagination.limit).to.eql(100); + expect(body.pagination.nextEvent).to.eql(null); }); it('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( - `/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}&after=${cursor}` + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` ) .set(commonHeaders) .expect(200); expect(body.events).be.empty(); - expect(body.pagination.next).to.eql(null); - expect(body.pagination.total).to.eql(1); + expect(body.pagination.nextEvent).to.eql(null); }); it('should return the first page of information when the cursor is invalid', async () => { const { body } = await supertest .get( - `/api/endpoint/resolver/${entityID}/related?legacyEndpointID=${endpointID}&after=blah` + `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` ) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.not.eql(null); + expect(body.pagination.nextEvent).to.eql(null); }); it('should error on invalid pagination values', async () => { await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=0`) + .get(`/api/endpoint/resolver/${entityID}/events?events=0`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=2000`) + .get(`/api/endpoint/resolver/${entityID}/events?events=2000`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/related?limit=-1`) + .get(`/api/endpoint/resolver/${entityID}/events?events=-1`) .set(commonHeaders) .expect(400); }); it('should not find any events', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/5555/related`) + .get(`/api/endpoint/resolver/5555/events`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); it('should return no results for an invalid endpoint ID', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}/related?legacyEndpointID=foo`) + .get(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextEvent).to.eql(null); expect(body.events).to.be.empty(); }); }); @@ -103,48 +96,46 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC it('should return details for the root node', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}&ancestors=5`) + .get( + `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=5` + ) .set(commonHeaders) .expect(200); expect(body.lifecycle.length).to.eql(2); - expect(body.ancestors.length).to.eql(1); - expect(body.pagination.next).to.eql(null); - // 5 is default parameter - expect(body.pagination.ancestors).to.eql(5); + expect(body.parent).not.to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); it('should have a populated next parameter', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - expect(body.pagination.next).to.eql('94041'); + expect(body.pagination.nextAncestor).to.eql('94041'); }); it('should handle an ancestors param request', async () => { let { body } = await supertest - .get(`/api/endpoint/resolver/${entityID}?legacyEndpointID=${endpointID}`) + .get(`/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - const next = body.pagination.next; + const next = body.pagination.nextAncestor; ({ body } = await supertest - .get(`/api/endpoint/resolver/${next}?legacyEndpointID=${endpointID}&ancestors=1`) + .get(`/api/endpoint/resolver/${next}/ancestry?legacyEndpointID=${endpointID}&ancestors=1`) .set(commonHeaders) .expect(200)); expect(body.lifecycle.length).to.eql(1); - expect(body.ancestors.length).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); it('should handle an invalid id', async () => { const { body } = await supertest - .get(`/api/endpoint/resolver/alskdjflasj`) + .get(`/api/endpoint/resolver/alskdjflasj/ancestry`) .set(commonHeaders) .expect(200); expect(body.lifecycle.length).to.eql(0); - expect(body.ancestors.length).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextAncestor).to.eql(null); }); }); @@ -158,51 +149,58 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.eql(cursor); - // default limit - expect(body.pagination.limit).to.eql(10); - expect(body.children.length).to.eql(1); expect(body.children[0].lifecycle.length).to.eql(2); expect(body.children[0].lifecycle[0].endgame.unique_pid).to.eql(94042); }); + it('returns multiple levels of child process lifecycle events', async () => { + const { body } = await supertest + .get(`/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&generations=3`) + .set(commonHeaders) + .expect(200); + expect(body.pagination.nextChild).to.be(null); + expect(body.children[0].pagination.nextChild).to.be(null); + + expect(body.children.length).to.eql(8); + expect(body.children[0].children[0].lifecycle.length).to.eql(2); + expect(body.children[0].lifecycle[0].endgame.unique_pid).to.eql(93932); + }); + it('returns no values when there is no more data', async () => { const { body } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .get( - `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&after=${cursor}` + `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=${cursor}` ) .set(commonHeaders) .expect(200); expect(body.children).be.empty(); - expect(body.pagination.next).to.eql(null); - expect(body.pagination.total).to.eql(1); + expect(body.pagination.nextChild).to.eql(null); }); it('returns the first page of information when the cursor is invalid', async () => { const { body } = await supertest .get( - `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&after=blah` + `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` ) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(1); - expect(body.pagination.next).to.not.eql(null); + expect(body.children.length).to.eql(1); + expect(body.pagination.nextChild).to.be(null); }); it('errors on invalid pagination values', async () => { await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=0`) + .get(`/api/endpoint/resolver/${entityID}/children?children=0`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=2000`) + .get(`/api/endpoint/resolver/${entityID}/children?children=2000`) .set(commonHeaders) .expect(400); await supertest - .get(`/api/endpoint/resolver/${entityID}/children?limit=-1`) + .get(`/api/endpoint/resolver/${entityID}/children?children=-1`) .set(commonHeaders) .expect(400); }); @@ -212,8 +210,7 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/5555/children`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextChild).to.eql(null); expect(body.children).to.be.empty(); }); @@ -222,10 +219,26 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=foo`) .set(commonHeaders) .expect(200); - expect(body.pagination.total).to.eql(0); - expect(body.pagination.next).to.eql(null); + expect(body.pagination.nextChild).to.eql(null); expect(body.children).to.be.empty(); }); }); + + describe('tree endpoint', () => { + const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; + + it('returns ancestors, events, children, and current process lifecycle', async () => { + const { body } = await supertest + .get(`/api/endpoint/resolver/93933?legacyEndpointID=${endpointID}`) + .set(commonHeaders) + .expect(200); + expect(body.pagination.nextAncestor).to.equal(null); + expect(body.pagination.nextEvent).to.equal(null); + expect(body.pagination.nextChild).to.equal(null); + expect(body.children.length).to.equal(0); + expect(body.events.length).to.equal(0); + expect(body.lifecycle.length).to.equal(2); + }); + }); }); } diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts index f08ce33d8b60f..adde6dd184b81 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -32,12 +32,12 @@ export default function(providerContext: FtrProviderContext) { body: { _source: agentDoc }, } = await esClient.get({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', }); - agentDoc.agents.access_api_key_id = apiKey.id; + agentDoc['fleet-agents'].access_api_key_id = apiKey.id; await esClient.update({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', refresh: 'true', body: { doc: agentDoc, diff --git a/x-pack/test/api_integration/apis/fleet/agents/actions.ts b/x-pack/test/api_integration/apis/fleet/agents/actions.ts index cf0641acf9e1c..577299e652610 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/actions.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/actions.ts @@ -67,7 +67,7 @@ export default function(providerContext: FtrProviderContext) { }, }) .expect(404); - expect(apiResponse.message).to.eql('Saved object [agents/agent100] not found'); + expect(apiResponse.message).to.eql('Saved object [fleet-agents/agent100] not found'); }); }); } diff --git a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts index ca51676126e73..b405b5065bc0e 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts @@ -32,12 +32,12 @@ export default function(providerContext: FtrProviderContext) { body: { _source: agentDoc }, } = await esClient.get({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', }); - agentDoc.agents.access_api_key_id = apiKey.id; + agentDoc['fleet-agents'].access_api_key_id = apiKey.id; await esClient.update({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', refresh: 'true', body: { doc: agentDoc, diff --git a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts index d8e9749744ea4..c934ddf8a406b 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -33,13 +33,13 @@ export default function(providerContext: FtrProviderContext) { body: { _source: enrollmentApiKeyDoc }, } = await esClient.get({ index: '.kibana', - id: 'enrollment_api_keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', + id: 'fleet-enrollment-api-keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', }); // @ts-ignore - enrollmentApiKeyDoc.enrollment_api_keys.api_key_id = apiKey.id; + enrollmentApiKeyDoc['fleet-enrollment-api-keys'].api_key_id = apiKey.id; await esClient.update({ index: '.kibana', - id: 'enrollment_api_keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', + id: 'fleet-enrollment-api-keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', refresh: 'true', body: { doc: enrollmentApiKeyDoc, diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index b484f1f5a8ed2..5b8e03269ceef 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -40,17 +40,18 @@ export default function(providerContext: FtrProviderContext) { body: { _source: agentDoc }, } = await esClient.get({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', }); // @ts-ignore - agentDoc.agents.access_api_key_id = accessAPIKeyId; - agentDoc.agents.default_api_key = Buffer.from( + agentDoc['fleet-agents'].access_api_key_id = accessAPIKeyId; + agentDoc['fleet-agents'].default_api_key_id = outputAPIKeyBody.id; + agentDoc['fleet-agents'].default_api_key = Buffer.from( `${outputAPIKeyBody.id}:${outputAPIKeyBody.api_key}` ).toString('base64'); await esClient.update({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', refresh: 'true', body: { doc: agentDoc, @@ -61,50 +62,29 @@ export default function(providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); - it('should not allow both ids and kuery in the payload', async () => { - await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({ - ids: ['agent:1'], - kuery: ['agents.id:1'], - }) - .expect(400); - }); - - it('should not allow no ids or kuery in the payload', async () => { - await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({}) - .expect(400); - }); - it('allow to unenroll using a list of ids', async () => { const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) + .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ ids: ['agent1'], }) .expect(200); - expect(body).to.have.keys('results', 'success'); + expect(body).to.have.keys('success'); expect(body.success).to.be(true); - expect(body.results).to.have.length(1); - expect(body.results[0].success).to.be(true); }); it('should invalidate related API keys', async () => { const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) + .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ ids: ['agent1'], }) .expect(200); - expect(body).to.have.keys('results', 'success'); + expect(body).to.have.keys('success'); expect(body.success).to.be(true); const { @@ -119,25 +99,5 @@ export default function(providerContext: FtrProviderContext) { expect(outputAPIKeys).length(1); expect(outputAPIKeys[0].invalidated).eql(true); }); - - it('allow to unenroll using a kibana query', async () => { - const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({ - kuery: 'agents.shared_id:agent2_filebeat OR agents.shared_id:agent3_metricbeat', - }) - .expect(200); - - expect(body).to.have.keys('results', 'success'); - expect(body.success).to.be(true); - expect(body.results).to.have.length(2); - expect(body.results[0].success).to.be(true); - - const agentsUnenrolledIds = body.results.map((r: { id: string }) => r.id); - - expect(agentsUnenrolledIds).to.contain('agent2'); - expect(agentsUnenrolledIds).to.contain('agent3'); - }); }); } diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js index 8bb3475da6cc9..28a317893f5b2 100644 --- a/x-pack/test/api_integration/apis/infra/index.js +++ b/x-pack/test/api_integration/apis/infra/index.js @@ -11,6 +11,7 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./log_entries')); loadTestFile(require.resolve('./log_entry_highlights')); loadTestFile(require.resolve('./logs_without_millis')); + loadTestFile(require.resolve('./log_sources')); loadTestFile(require.resolve('./log_summary')); loadTestFile(require.resolve('./metrics')); loadTestFile(require.resolve('./sources')); diff --git a/x-pack/test/api_integration/apis/infra/log_entries.ts b/x-pack/test/api_integration/apis/infra/log_entries.ts index 3c12f5e4dc789..991dc4a7f96cf 100644 --- a/x-pack/test/api_integration/apis/infra/log_entries.ts +++ b/x-pack/test/api_integration/apis/infra/log_entries.ts @@ -126,7 +126,7 @@ export default function({ getService }: FtrProviderContext) { expect(messageColumn.message.length).to.be.greaterThan(0); }); - it('Returns the context fields', async () => { + it('Does not build context if entry does not have all fields', async () => { const { body } = await supertest .post(LOG_ENTRIES_PATH) .set(COMMON_HEADERS) @@ -147,9 +147,7 @@ export default function({ getService }: FtrProviderContext) { const entries = logEntriesResponse.data.entries; const entry = entries[0]; - - expect(entry.context).to.have.property('host.name'); - expect(entry.context['host.name']).to.be('demo-stack-nginx-01'); + expect(entry.context).to.eql({}); }); it('Paginates correctly with `after`', async () => { diff --git a/x-pack/test/api_integration/apis/infra/log_sources.ts b/x-pack/test/api_integration/apis/infra/log_sources.ts new file mode 100644 index 0000000000000..73d59bcdcd9a4 --- /dev/null +++ b/x-pack/test/api_integration/apis/infra/log_sources.ts @@ -0,0 +1,179 @@ +/* + * 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 expect from '@kbn/expect'; +import { beforeEach } from 'mocha'; +import { + getLogSourceConfigurationSuccessResponsePayloadRT, + patchLogSourceConfigurationSuccessResponsePayloadRT, +} from '../../../../plugins/infra/common/http_api/log_sources'; +import { decodeOrThrow } from '../../../../plugins/infra/common/runtime_types'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const logSourceConfiguration = getService('infraLogSourceConfiguration'); + + describe('log sources api', () => { + before(() => esArchiver.load('infra/metrics_and_logs')); + after(() => esArchiver.unload('infra/metrics_and_logs')); + beforeEach(() => esArchiver.load('empty_kibana')); + afterEach(() => esArchiver.unload('empty_kibana')); + + describe('source configuration get method for non-existant source', () => { + it('returns the default source configuration', async () => { + const response = await logSourceConfiguration + .createGetLogSourceConfigurationAgent('default') + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(origin).to.be('fallback'); + expect(configuration.name).to.be('Default'); + expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.fields.timestamp).to.be('@timestamp'); + expect(configuration.fields.tiebreaker).to.be('_doc'); + expect(configuration.logColumns[0]).to.have.key('timestampColumn'); + expect(configuration.logColumns[1]).to.have.key('fieldColumn'); + expect(configuration.logColumns[2]).to.have.key('messageColumn'); + }); + }); + + describe('source configuration patch method for non-existant source', () => { + it('creates a source configuration', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', { + name: 'NAME', + description: 'DESCRIPTION', + logAlias: 'filebeat-**', + fields: { + tiebreaker: 'TIEBREAKER', + timestamp: 'TIMESTAMP', + }, + logColumns: [ + { + messageColumn: { + id: 'MESSAGE_COLUMN', + }, + }, + ], + }) + .expect(200); + + // check direct response + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('NAME'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-**'); + expect(configuration.fields.timestamp).to.be('TIMESTAMP'); + expect(configuration.fields.tiebreaker).to.be('TIEBREAKER'); + expect(configuration.logColumns).to.have.length(1); + expect(configuration.logColumns[0]).to.have.key('messageColumn'); + + // check for persistence + const { + data: { configuration: persistedConfiguration }, + } = await logSourceConfiguration.getLogSourceConfiguration('default'); + + expect(configuration).to.eql(persistedConfiguration); + }); + + it('creates a source configuration with default values for unspecified properties', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', {}) + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('Default'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.fields.timestamp).to.be('@timestamp'); + expect(configuration.fields.tiebreaker).to.be('_doc'); + expect(configuration.logColumns).to.have.length(3); + expect(configuration.logColumns[0]).to.have.key('timestampColumn'); + expect(configuration.logColumns[1]).to.have.key('fieldColumn'); + expect(configuration.logColumns[2]).to.have.key('messageColumn'); + + // check for persistence + const { + data: { configuration: persistedConfiguration, origin: persistedOrigin }, + } = await logSourceConfiguration.getLogSourceConfiguration('default'); + + expect(persistedOrigin).to.be('stored'); + expect(configuration).to.eql(persistedConfiguration); + }); + }); + + describe('source configuration patch method for existing source', () => { + beforeEach(async () => { + await logSourceConfiguration.updateLogSourceConfiguration('default', {}); + }); + + it('updates a source configuration', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', { + name: 'NAME', + description: 'DESCRIPTION', + logAlias: 'filebeat-**', + fields: { + tiebreaker: 'TIEBREAKER', + timestamp: 'TIMESTAMP', + }, + logColumns: [ + { + messageColumn: { + id: 'MESSAGE_COLUMN', + }, + }, + ], + }) + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('NAME'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-**'); + expect(configuration.fields.timestamp).to.be('TIMESTAMP'); + expect(configuration.fields.tiebreaker).to.be('TIEBREAKER'); + expect(configuration.logColumns).to.have.length(1); + expect(configuration.logColumns[0]).to.have.key('messageColumn'); + }); + + it('partially updates a source configuration', async () => { + const response = await logSourceConfiguration + .createUpdateLogSourceConfigurationAgent('default', { + name: 'NAME', + }) + .expect(200); + + const { + data: { configuration, origin }, + } = decodeOrThrow(patchLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + + expect(configuration.name).to.be('NAME'); + expect(origin).to.be('stored'); + expect(configuration.logAlias).to.be('filebeat-*,kibana_sample_data_logs*'); + expect(configuration.fields.timestamp).to.be('@timestamp'); + expect(configuration.fields.tiebreaker).to.be('_doc'); + expect(configuration.logColumns).to.have.length(3); + expect(configuration.logColumns[0]).to.have.key('timestampColumn'); + expect(configuration.logColumns[1]).to.have.key('fieldColumn'); + expect(configuration.logColumns[2]).to.have.key('messageColumn'); + }); + }); + }); +} 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 4f17f9db67483..5c43e8938a8c1 100644 --- a/x-pack/test/api_integration/apis/infra/metrics_alerting.ts +++ b/x-pack/test/api_integration/apis/infra/metrics_alerting.ts @@ -32,18 +32,20 @@ export default function({ getService }: FtrProviderContext) { describe('querying the entire infrastructure', () => { for (const aggType of aggs) { it(`should work with the ${aggType} aggregator`, async () => { - const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType)); + const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), '@timestamp'); 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(); }); } it('should work with a filterQuery', async () => { const searchBody = getElasticsearchMetricQuery( getSearchParams('avg'), + '@timestamp', undefined, '{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}' ); @@ -53,23 +55,45 @@ 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'), + '@timestamp', + 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', () => { for (const aggType of aggs) { it(`should work with the ${aggType} aggregator`, async () => { - const searchBody = getElasticsearchMetricQuery(getSearchParams(aggType), 'agent.id'); + const searchBody = getElasticsearchMetricQuery( + getSearchParams(aggType), + '@timestamp', + 'agent.id' + ); 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(); }); } it('should work with a filterQuery', async () => { const searchBody = getElasticsearchMetricQuery( getSearchParams('avg'), + '@timestamp', 'agent.id', '{"bool":{"should":[{"match_phrase":{"agent.hostname":"foo"}}],"minimum_should_match":1}}' ); @@ -79,6 +103,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(); }); }); }); diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts index cdbf5a3e6a1fe..2463dbe4500b5 100644 --- a/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts +++ b/x-pack/test/api_integration/apis/logstash/pipeline/delete.ts @@ -20,7 +20,6 @@ export default function({ getService }: FtrProviderContext) { .send({ id: 'fast_generator', description: 'foobar baz', - username: 'seger', pipeline: 'input { generator {} }\n\n output { stdout {} }', }) .expect(204); diff --git a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts index 2ca9fbe7d68e0..ca0cfb19b9454 100644 --- a/x-pack/test/api_integration/apis/logstash/pipeline/save.ts +++ b/x-pack/test/api_integration/apis/logstash/pipeline/save.ts @@ -28,7 +28,6 @@ export default function({ getService }: FtrProviderContext) { .send({ id: 'fast_generator', description: 'foobar baz', - username: 'seger', pipeline: 'input { generator {} }\n\n output { stdout {} }', }) .expect(204); diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js index 22f0bde50b073..b9a0bfd40a8d6 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js @@ -5,33 +5,27 @@ */ import { API_BASE_PATH } from './constants'; -import { getRandomString } from './lib'; -import { getAutoFollowIndexPayload } from './fixtures'; export const registerHelpers = supertest => { let autoFollowPatternsCreated = []; const loadAutoFollowPatterns = () => supertest.get(`${API_BASE_PATH}/auto_follow_patterns`); - const getAutoFollowPattern = name => - supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${name}`); + const getAutoFollowPattern = id => supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${id}`); - const createAutoFollowPattern = ( - name = getRandomString(), - payload = getAutoFollowIndexPayload() - ) => { - autoFollowPatternsCreated.push(name); + const createAutoFollowPattern = payload => { + autoFollowPatternsCreated.push(payload.id); return supertest .post(`${API_BASE_PATH}/auto_follow_patterns`) .set('kbn-xsrf', 'xxx') - .send({ ...payload, id: name }); + .send(payload); }; - const deleteAutoFollowPattern = name => { - autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== name); + const deleteAutoFollowPattern = id => { + autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== id); - return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${name}`).set('kbn-xsrf', 'xxx'); + return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${id}`).set('kbn-xsrf', 'xxx'); }; const deleteAllAutoFollowPatterns = () => diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js index 3efb4d6600f7f..7a95ba7fcd981 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js @@ -6,8 +6,7 @@ import expect from '@kbn/expect'; -import { getRandomString } from './lib'; -import { getAutoFollowIndexPayload } from './fixtures'; +import { REMOTE_CLUSTER_NAME } from './constants'; import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers'; import { registerHelpers as registerAutoFollowPatternHelpers } from './auto_follow_pattern.helpers'; @@ -37,44 +36,60 @@ export default function({ getService }) { describe('when remote cluster does not exist', () => { it('should throw a 404 error when cluster is unknown', async () => { - const payload = getAutoFollowIndexPayload(); - payload.remoteCluster = 'unknown-cluster'; + const { body } = await createAutoFollowPattern({ + id: 'pattern0', + remoteCluster: 'unknown-cluster', + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); - const { body } = await createAutoFollowPattern(undefined, payload).expect(404); + expect(body.statusCode).to.be(404); expect(body.attributes.cause[0]).to.contain('no such remote cluster'); }); }); describe('when remote cluster exists', () => { - before(() => addCluster()); + before(async () => addCluster()); describe('create()', () => { it('should create an auto-follow pattern when cluster is known', async () => { - const name = getRandomString(); - const { body } = await createAutoFollowPattern(name).expect(200); - console.log(body); - + const { body, statusCode } = await createAutoFollowPattern({ + id: 'pattern1', + remoteCluster: REMOTE_CLUSTER_NAME, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); + + expect(statusCode).to.be(200); expect(body.acknowledged).to.eql(true); }); }); describe('get()', () => { it('should return a 404 when the auto-follow pattern is not found', async () => { - const name = getRandomString(); - const { body } = await getAutoFollowPattern(name).expect(404); - + const { body } = await getAutoFollowPattern('missing-pattern'); + expect(body.statusCode).to.be(404); expect(body.attributes.cause).not.to.be(undefined); }); it('should return an auto-follow pattern that was created', async () => { - const name = getRandomString(); - const autoFollowPattern = getAutoFollowIndexPayload(); - - await createAutoFollowPattern(name, autoFollowPattern); - - const { body } = await getAutoFollowPattern(name).expect(200); - - expect(body).to.eql({ ...autoFollowPattern, name }); + await createAutoFollowPattern({ + id: 'pattern2', + remoteCluster: REMOTE_CLUSTER_NAME, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); + + const { body, statusCode } = await getAutoFollowPattern('pattern2'); + + expect(statusCode).to.be(200); + expect(body).to.eql({ + name: 'pattern2', + remoteCluster: REMOTE_CLUSTER_NAME, + active: true, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js index de47f5d9ea85e..6e254b27356f2 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js @@ -7,13 +7,6 @@ import { REMOTE_CLUSTER_NAME } from './constants'; import { getRandomString } from './lib'; -export const getAutoFollowIndexPayload = (remoteCluster = REMOTE_CLUSTER_NAME, active = true) => ({ - active, - remoteCluster, - leaderIndexPatterns: ['leader-*'], - followIndexPattern: '{{leader_index}}_follower', -}); - export const getFollowerIndexPayload = ( leaderIndexName = getRandomString(), remoteCluster = REMOTE_CLUSTER_NAME, diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js index eabf474120f2b..d03b1f83fb404 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../legacy/plugins/cross_cluster_replication/common/constants'; +import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants'; import { getFollowerIndexPayload } from './fixtures'; import { registerHelpers as registerElasticSearchHelpers, getRandomString } from './lib'; import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers'; @@ -57,7 +57,8 @@ export default function({ getService }) { expect(body.attributes.cause[0]).to.contain('no such index'); }); - it('should create a follower index that follows an existing remote index', async () => { + // NOTE: If this test fails locally it's probably because you have another cluster running. + it('should create a follower index that follows an existing leader index', async () => { // First let's create an index to follow const leaderIndex = await createIndex(); @@ -65,7 +66,7 @@ export default function({ getService }) { const { body } = await createFollowerIndex(undefined, payload).expect(200); // There is a race condition in which Elasticsearch can respond without acknowledging, - // i.e. `body .follow_index_shards_acked` is sometimes true and sometimes false. + // i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false. // By only asserting that `follow_index_created` is true, we eliminate this flakiness. expect(body.follow_index_created).to.eql(true); }); @@ -79,6 +80,7 @@ export default function({ getService }) { expect(body.attributes.cause[0]).to.contain('no such index'); }); + // NOTE: If this test fails locally it's probably because you have another cluster running. it('should return a follower index that was created', async () => { const leaderIndex = await createIndex(); diff --git a/x-pack/test/api_integration/apis/management/index.js b/x-pack/test/api_integration/apis/management/index.js index 352cd56d0fc9f..cef2caa918620 100644 --- a/x-pack/test/api_integration/apis/management/index.js +++ b/x-pack/test/api_integration/apis/management/index.js @@ -12,5 +12,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./rollup')); loadTestFile(require.resolve('./index_management')); loadTestFile(require.resolve('./index_lifecycle_management')); + loadTestFile(require.resolve('./ingest_pipelines')); }); } diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 7195b8680a286..9442beda3501d 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -34,7 +34,8 @@ export default function({ getService }) { clearCache, } = registerHelpers({ supertest }); - describe('indices', () => { + // FLAKY: https://github.com/elastic/kibana/issues/64473 + describe.skip('indices', () => { after(() => Promise.all([cleanUpEsResources()])); describe('clear cache', () => { @@ -193,10 +194,10 @@ export default function({ getService }) { 'size', 'isFrozen', 'aliases', - 'ilm', // data enricher - 'isRollupIndex', // data enricher // Cloud disables CCR, so wouldn't expect follower indices. 'isFollowerIndex', // data enricher + 'ilm', // data enricher + 'isRollupIndex', // data enricher ]; expect(Object.keys(body[0])).to.eql(expectedKeys); }); @@ -219,10 +220,10 @@ export default function({ getService }) { 'size', 'isFrozen', 'aliases', - 'ilm', // data enricher - 'isRollupIndex', // data enricher // Cloud disables CCR, so wouldn't expect follower indices. 'isFollowerIndex', // data enricher + 'ilm', // data enricher + 'isRollupIndex', // data enricher ]; expect(Object.keys(body[0])).to.eql(expectedKeys); expect(body.length > 1).to.be(true); // to contrast it with the next test diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts new file mode 100644 index 0000000000000..ca222ebc2c1e3 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('Ingest Node Pipelines', () => { + loadTestFile(require.resolve('./ingest_pipelines')); + }); +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts new file mode 100644 index 0000000000000..88a78d048a3b6 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -0,0 +1,330 @@ +/* + * 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 expect from '@kbn/expect'; +import { registerEsHelpers } from './lib'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const API_BASE_PATH = '/api/ingest_pipelines'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const { createPipeline, deletePipeline } = registerEsHelpers(getService); + + describe('Pipelines', function() { + describe('Create', () => { + const PIPELINE_ID = 'test_create_pipeline'; + after(() => deletePipeline(PIPELINE_ID)); + + it('should create a pipeline', async () => { + const { body } = await supertest + .post(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .send({ + name: PIPELINE_ID, + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + on_failure: [ + { + set: { + field: 'error.message', + value: '{{ failure_message }}', + }, + }, + ], + version: 1, + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow creation of an existing pipeline', async () => { + const { body } = await supertest + .post(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .send({ + name: PIPELINE_ID, + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }) + .expect(409); + + expect(body).to.eql({ + statusCode: 409, + error: 'Conflict', + message: `There is already a pipeline with name '${PIPELINE_ID}'.`, + }); + }); + }); + + describe('Update', () => { + const PIPELINE_ID = 'test_update_pipeline'; + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); + after(() => deletePipeline(PIPELINE_ID)); + + it('should allow an existing pipeline to be updated', async () => { + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...PIPELINE, + description: 'updated test pipeline description', + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + + it('should not allow a non-existing pipeline to be updated', async () => { + const uri = `${API_BASE_PATH}/pipeline_does_not_exist`; + + const { body } = await supertest + .put(uri) + .set('kbn-xsrf', 'xxx') + .send({ + ...PIPELINE, + description: 'updated test pipeline description', + }) + .expect(404); + + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + }); + }); + + describe('Get', () => { + const PIPELINE_ID = 'test_pipeline'; + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + before(() => createPipeline({ body: PIPELINE, id: PIPELINE_ID })); + after(() => deletePipeline(PIPELINE_ID)); + + describe('all pipelines', () => { + it('should return an array of pipelines', async () => { + const { body } = await supertest + .get(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(Array.isArray(body)).to.be(true); + + // There are some pipelines created OOTB with ES + // To not be dependent on these, we only confirm the pipeline we created as part of the test exists + const testPipeline = body.find(({ name }: { name: string }) => name === PIPELINE_ID); + + expect(testPipeline).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + + describe('one pipeline', () => { + it('should return a single pipeline', async () => { + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .get(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + ...PIPELINE, + name: PIPELINE_ID, + }); + }); + }); + }); + + describe('Delete', () => { + const PIPELINE = { + description: 'test pipeline description', + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + version: 1, + }; + + it('should delete a pipeline', async () => { + // Create pipeline to be deleted + const PIPELINE_ID = 'test_delete_pipeline'; + createPipeline({ body: PIPELINE, id: PIPELINE_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ID}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [PIPELINE_ID], + errors: [], + }); + }); + + it('should delete multiple pipelines', async () => { + // Create pipelines to be deleted + const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; + const PIPELINE_TWO_ID = 'test_delete_pipeline_2'; + createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); + createPipeline({ body: PIPELINE, id: PIPELINE_TWO_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_TWO_ID}`; + + const { + body: { itemsDeleted, errors }, + } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(errors).to.eql([]); + + // The itemsDeleted array order isn't guaranteed, so we assert against each pipeline name instead + [PIPELINE_ONE_ID, PIPELINE_TWO_ID].forEach(pipelineName => { + expect(itemsDeleted.includes(pipelineName)).to.be(true); + }); + }); + + it('should return an error for any pipelines not sucessfully deleted', async () => { + const PIPELINE_DOES_NOT_EXIST = 'pipeline_does_not_exist'; + + // Create pipeline to be deleted + const PIPELINE_ONE_ID = 'test_delete_pipeline_1'; + createPipeline({ body: PIPELINE, id: PIPELINE_ONE_ID }); + + const uri = `${API_BASE_PATH}/${PIPELINE_ONE_ID},${PIPELINE_DOES_NOT_EXIST}`; + + const { body } = await supertest + .delete(uri) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.eql({ + itemsDeleted: [PIPELINE_ONE_ID], + errors: [ + { + name: PIPELINE_DOES_NOT_EXIST, + error: { + msg: '[resource_not_found_exception] pipeline [pipeline_does_not_exist] is missing', + path: '/_ingest/pipeline/pipeline_does_not_exist', + query: {}, + statusCode: 404, + response: JSON.stringify({ + error: { + root_cause: [ + { + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + ], + type: 'resource_not_found_exception', + reason: 'pipeline [pipeline_does_not_exist] is missing', + }, + status: 404, + }), + }, + }, + ], + }); + }); + }); + + describe('Simulate', () => { + it('should successfully simulate a pipeline', async () => { + const { body } = await supertest + .post(`${API_BASE_PATH}/simulate`) + .set('kbn-xsrf', 'xxx') + .send({ + pipeline: { + description: 'test simulate pipeline description', + processors: [ + { + set: { + field: 'field2', + value: '_value', + }, + }, + ], + }, + documents: [ + { + _index: 'index', + _id: 'id', + _source: { + foo: 'bar', + }, + }, + { + _index: 'index', + _id: 'id', + _source: { + foo: 'rab', + }, + }, + ], + }) + .expect(200); + + // The simulate ES response is quite long and includes timestamps + // so for now, we just confirm the docs array is returned with the correct length + expect(body.docs?.length).to.eql(2); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts new file mode 100644 index 0000000000000..2f42596a66b54 --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/elasticsearch.ts @@ -0,0 +1,39 @@ +/* + * 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 { FtrProviderContext } from '../../../../ftr_provider_context'; + +interface Processor { + [key: string]: { + [key: string]: unknown; + }; +} + +interface Pipeline { + id: string; + body: { + description: string; + processors: Processor[]; + version?: number; + }; +} + +/** + * Helpers to create and delete pipelines on the Elasticsearch instance + * during our tests. + * @param {ElasticsearchClient} es The Elasticsearch client instance + */ +export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { + const es = getService('legacyEs'); + + const createPipeline = (pipeline: Pipeline) => es.ingest.putPipeline(pipeline); + + const deletePipeline = (pipelineId: string) => es.ingest.deletePipeline({ id: pipelineId }); + + return { + createPipeline, + deletePipeline, + }; +}; diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts new file mode 100644 index 0000000000000..66ea0fe40c4ce --- /dev/null +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/lib/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { registerEsHelpers } from './elasticsearch'; 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 new file mode 100644 index 0000000000000..10857caab98e2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `fq_single_${Date.now()}`; + + const testDataList = [ + { + testTitle: 'ML Poweruser creates a single metric job', + user: USER.ML_POWERUSER, + jobId: `${jobId}_1`, + requestBody: { + job_id: `${jobId}_1`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 200, + responseBody: { + job_id: `${jobId}_1`, + job_type: 'anomaly_detector', + groups: ['automated', 'farequote', 'single-metric'], + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + analysis_config: { + bucket_span: '30m', + summary_count_field_name: 'doc_count', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + analysis_limits: { model_memory_limit: '11mb', categorization_examples_limit: 4 }, + data_description: { time_field: '@timestamp', time_format: 'epoch_ms' }, + model_plot_config: { enabled: true }, + model_snapshot_retention_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + }, + }, + { + testTitle: 'ML viewer cannot create a job', + user: USER.ML_VIEWER, + jobId: `${jobId}_2`, + requestBody: { + job_id: `${jobId}_2`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }, + }, + }, + ]; + + describe('create', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .put(`/api/ml/anomaly_detectors/${testData.jobId}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + expect(body.job_id).to.eql(expectedResponse.job_id); + expect(body.groups).to.eql(expectedResponse.groups); + expect(body.analysis_config!.bucket_span).to.eql( + expectedResponse.analysis_config!.bucket_span + ); + expect(body.analysis_config.detectors).to.have.length( + expectedResponse.analysis_config!.detectors.length + ); + expect(body.analysis_config.detectors[0]).to.eql( + expectedResponse.analysis_config!.detectors[0] + ); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts new file mode 100644 index 0000000000000..fb8acaf5c3ae9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('anomaly detectors', function() { + loadTestFile(require.resolve('./create')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts new file mode 100644 index 0000000000000..dfa81b5d78c65 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const metricFieldsTestData = { + testTitle: 'returns stats for metric fields over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'JZA' }, // Only use one airline to ensure no sampling. + }, + }, + }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + interval: '1d', + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { + documentCounts: { + interval: '1d', + buckets: { + '1454803200000': 846, + '1454889600000': 846, + '1454976000000': 859, + '1455062400000': 851, + '1455148800000': 858, + }, + }, + }, + { + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + fieldName: 'responsetime', + count: 4260, + min: 963.4293212890625, + max: 1042.13525390625, + avg: 1000.0378077547315, + isTopValuesSampled: false, + topValues: [ + { key: 980.0411987304688, doc_count: 2 }, + { key: 989.278076171875, doc_count: 2 }, + { key: 989.763916015625, doc_count: 2 }, + { key: 991.290771484375, doc_count: 2 }, + { key: 992.0765991210938, doc_count: 2 }, + { key: 993.8115844726562, doc_count: 2 }, + { key: 993.8973999023438, doc_count: 2 }, + { key: 994.0230102539062, doc_count: 2 }, + { key: 994.364990234375, doc_count: 2 }, + { key: 994.916015625, doc_count: 2 }, + ], + topValuesSampleSize: 4260, + topValuesSamplerShardSize: -1, + }, + ], + }, + }; + + const nonMetricFieldsTestData = { + testTitle: 'returns stats for non-metric fields specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + fields: [ + { fieldName: '@timestamp', type: 'date', cardinality: 4751 }, + { fieldName: '@version.keyword', type: 'keyword', cardinality: 1 }, + { fieldName: 'airline', type: 'keyword', cardinality: 19 }, + { fieldName: 'type', type: 'text', cardinality: 0 }, + { fieldName: 'type.keyword', type: 'keyword', cardinality: 1 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { fieldName: '@timestamp', count: 1733, earliest: 1454889602000, latest: 1454975948000 }, + { + fieldName: '@version.keyword', + isTopValuesSampled: false, + topValues: [{ key: '1', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'airline', + isTopValuesSampled: false, + topValues: [{ key: 'AAL', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'type.keyword', + isTopValuesSampled: false, + topValues: [{ key: 'farequote', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { fieldName: 'type', examples: ['farequote'] }, + ], + }, + }; + + const errorTestData = { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exists', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + maxExamples: 10, + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + }, + }, + }; + + async function runGetFieldStatsRequest( + index: string, + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise<any> { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_field_stats/${index}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareByFieldName(a: { fieldName: string }, b: { fieldName: string }) { + if (a.fieldName < b.fieldName) { + return -1; + } + if (a.fieldName > b.fieldName) { + return 1; + } + return 0; + } + + describe('get_field_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + it(`${metricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + metricFieldsTestData.index, + metricFieldsTestData.user, + metricFieldsTestData.requestBody, + metricFieldsTestData.expected.responseCode + ); + + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + const expected = metricFieldsTestData.expected; + expect(body).to.have.length(expected.responseBody.length); + + const actualDocCounts = body[0]; + const expectedDocCounts = expected.responseBody[0]; + expect(actualDocCounts).to.eql(expectedDocCounts); + + const actualFieldData = { ...body[1] }; + delete actualFieldData.median; + delete actualFieldData.distribution; + + expect(actualFieldData).to.eql(expected.responseBody[1]); + }); + + it(`${nonMetricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + nonMetricFieldsTestData.index, + nonMetricFieldsTestData.user, + nonMetricFieldsTestData.requestBody, + nonMetricFieldsTestData.expected.responseCode + ); + + // Sort the fields in the response before validating. + const expectedRspFields = nonMetricFieldsTestData.expected.responseBody.sort( + compareByFieldName + ); + const actualRspFields = body.sort(compareByFieldName); + expect(actualRspFields).to.eql(expectedRspFields); + }); + + it(`${errorTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + errorTestData.index, + errorTestData.user, + errorTestData.requestBody, + errorTestData.expected.responseCode + ); + + expect(body.error).to.eql(errorTestData.expected.responseBody.error); + expect(body.message).to.eql(errorTestData.expected.responseBody.message); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts new file mode 100644 index 0000000000000..6490c19c64483 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts @@ -0,0 +1,154 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns stats over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 86274, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 78580 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 19 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 83346 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns stats when specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 1733, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1713 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1730 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exist', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['@version', 'type'], + samplerShardSize: 1000, + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exist], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exist" & index_uuid="_na_" & index="ft_farequote_not_exist" }', + }, + }, + }, + ]; + + describe('get_overall_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_overall_stats/${testData.index}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts new file mode 100644 index 0000000000000..ce9e44618f1af --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('data visualizer', function() { + loadTestFile(require.resolve('./get_field_stats')); + loadTestFile(require.resolve('./get_overall_stats')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts new file mode 100644 index 0000000000000..245375562b5c1 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts @@ -0,0 +1,115 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns cardinality of customer name fields over full time range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + 'customer_first_name.keyword': 46, + 'customer_last_name.keyword': 183, + }, + }, + }, + { + testTitle: 'returns cardinality of geoip fields over specified range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['geoip.city_name', 'geoip.continent_name', 'geoip.country_iso_code'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: { + 'geoip.city_name': 10, + 'geoip.continent_name': 5, + 'geoip.country_iso_code': 9, + }, + }, + }, + { + testTitle: 'returns empty response for non aggregatable field', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['manufacturer'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: {}, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('field_cardinality', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/field_cardinality') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/fields_service/index.ts b/x-pack/test/api_integration/apis/ml/fields_service/index.ts new file mode 100644 index 0000000000000..312602e589119 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('fields service', function() { + loadTestFile(require.resolve('./field_cardinality')); + loadTestFile(require.resolve('./time_field_range')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts new file mode 100644 index 0000000000000..2f0fd4fc6c5e3 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns expected time range with index and match_all query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560297859000, + string: '2019-06-12T00:04:19.000Z', + }, + end: { + epoch: 1562975136000, + string: '2019-07-12T23:45:36.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns expected time range with index and query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { + term: { + 'customer_first_name.keyword': { + value: 'Brigitte', + }, + }, + }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560298982000, + string: '2019-06-12T00:23:02.000Z', + }, + end: { + epoch: 1562973754000, + string: '2019-07-12T23:22:34.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('time_field_range', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/time_field_range') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index f012883c46ca3..58356637c63ac 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -31,11 +31,11 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./bucket_span_estimator')); - loadTestFile(require.resolve('./calculate_model_memory_limit')); - loadTestFile(require.resolve('./categorization_field_examples')); - loadTestFile(require.resolve('./get_module')); - loadTestFile(require.resolve('./recognize_module')); - loadTestFile(require.resolve('./setup_module')); + loadTestFile(require.resolve('./modules')); + loadTestFile(require.resolve('./anomaly_detectors')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./fields_service')); + loadTestFile(require.resolve('./job_validation')); + loadTestFile(require.resolve('./jobs')); }); } diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts rename to x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts index bc0dc3019d7c9..0b4aca9660be4 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts rename to x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts index 59e3dfcca00f9..f17814633ce8f 100644 --- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts new file mode 100644 index 0000000000000..6ca9dcbbe9e5b --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('job validation', function() { + loadTestFile(require.resolve('./bucket_span_estimator')); + loadTestFile(require.resolve('./calculate_model_memory_limit')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts similarity index 98% rename from x-pack/test/api_integration/apis/ml/categorization_field_examples.ts rename to x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts index df0153f965942..bcc6c4907100c 100644 --- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts new file mode 100644 index 0000000000000..70a64f198d6f4 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('jobs', function() { + loadTestFile(require.resolve('./categorization_field_examples')); + loadTestFile(require.resolve('./jobs_summary')); + }); +} 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 new file mode 100644 index 0000000000000..a5cb68d782126 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -0,0 +1,374 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; +import { Job } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const SINGLE_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_single_${Date.now()}`, + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, +}; + +const MULTI_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_multi_${Date.now()}`, + description: 'mean(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: true }, +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + const testDataListNoJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + { + testTitle: 'as ML Viewer', + user: USER.ML_VIEWER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListWithJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: { + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id], + }, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + fullJob: { + // Only tests against some of the fields in the fullJob property. + job_id: SINGLE_METRIC_JOB_CONFIG.job_id, + job_type: 'anomaly_detector', + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + }, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListNegative = [ + { + testTitle: 'as ML Unauthorized user', + user: USER.ML_UNAUTHORIZED, + requestBody: {}, + // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. + expected: { + responseCode: 404, + error: 'Not Found', + }, + }, + ]; + + async function runJobsSummaryRequest( + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise<any> { + const { body } = await supertest + .post('/api/ml/jobs/jobs_summary') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareById(a: { id: string }, b: { id: string }) { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + } + + function getGroups(jobs: Array<{ groups: string[] }>) { + const groupIds: string[] = []; + jobs.forEach(job => { + const groups = job.groups; + groups.forEach(group => { + if (groupIds.indexOf(group) === -1) { + groupIds.push(group); + } + }); + }); + return groupIds.sort(); + } + + describe('jobs_summary', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('sets up jobs', async () => { + for (const job of testSetupJobConfigs) { + await ml.api.createAnomalyDetectionJob(job); + } + }); + + for (const testData of testDataListNoJobId) { + describe('gets job summary with no job IDs supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + }); + }); + } + + for (const testData of testDataListWithJobId) { + describe('gets job summary with job ID supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + + // Validate the response for the specified job IDs contains a fullJob property. + const requestedJobIds = testData.requestBody.jobIds; + for (const job of body) { + if (requestedJobIds.includes(job.id)) { + expect(job).to.have.property('fullJob'); + } else { + expect(job).not.to.have.property('fullJob'); + } + } + + for (const expectedJob of expectedResponse) { + const expectedJobId = expectedJob.id; + const actualJob = body.find((job: { id: string }) => job.id === expectedJobId); + if (expectedJob.fullJob) { + expect(actualJob).to.have.property('fullJob'); + expect(actualJob.fullJob).to.have.property('analysis_config'); + expect(actualJob.fullJob.analysis_config).to.eql(expectedJob.fullJob.analysis_config); + } else { + expect(actualJob).not.to.have.property('fullJob'); + } + } + }); + }); + } + + for (const testData of testDataListNegative) { + describe('rejects request', function() { + it(testData.testTitle, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(body) + .to.have.property('error') + .eql(testData.expected.error); + + expect(body).to.have.property('message'); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts similarity index 92% rename from x-pack/test/api_integration/apis/ml/get_module.ts rename to x-pack/test/api_integration/apis/ml/modules/get_module.ts index a50d3c0abe430..e19d45999c88e 100644 --- a/x-pack/test/api_integration/apis/ml/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/modules/index.ts b/x-pack/test/api_integration/apis/ml/modules/index.ts new file mode 100644 index 0000000000000..4fdc404c607aa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/modules/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('modules', function() { + loadTestFile(require.resolve('./get_module')); + loadTestFile(require.resolve('./recognize_module')); + loadTestFile(require.resolve('./setup_module')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts similarity index 93% rename from x-pack/test/api_integration/apis/ml/recognize_module.ts rename to x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 8e360579c1459..948728189b8bd 100644 --- a/x-pack/test/api_integration/apis/ml/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts similarity index 94% rename from x-pack/test/api_integration/apis/ml/setup_module.ts rename to x-pack/test/api_integration/apis/ml/modules/setup_module.ts index e603782b25717..c42fc95c1bc7f 100644 --- a/x-pack/test/api_integration/apis/ml/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -6,10 +6,10 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', @@ -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', }, }, ]; diff --git a/x-pack/test/api_integration/apis/security/api_keys.ts b/x-pack/test/api_integration/apis/security/api_keys.ts new file mode 100644 index 0000000000000..276a5367a419e --- /dev/null +++ b/x-pack/test/api_integration/apis/security/api_keys.ts @@ -0,0 +1,28 @@ +/* + * 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 expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('API Keys', () => { + describe('GET /internal/security/api_key/_enabled', () => { + it('should indicate that API Keys are enabled', async () => { + await supertest + .get('/internal/security/api_key/_enabled') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200) + .then((response: Record<string, any>) => { + const payload = response.body; + expect(payload).to.eql({ apiKeysEnabled: true }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index ad1876cb717f1..7bb79a589d522 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -11,6 +11,7 @@ export default function({ loadTestFile }) { // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index dcbdb17724249..3e426f210afa8 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -13,6 +13,7 @@ export default function({ loadTestFile }: FtrProviderContext) { // Updates here should be mirrored in `./index.js` if tests // should also run under a trial/platinum license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index ef7e48388ff66..fcdf268ff27b0 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -56,7 +56,7 @@ export default function({ getService }: FtrProviderContext) { expect(body.now).to.be.a('number'); expect(body.idleTimeoutExpiration).to.be.a('number'); expect(body.lifespanExpiration).to.be(null); - expect(body.provider).to.be('basic'); + expect(body.provider).to.eql({ type: 'basic', name: 'basic' }); }); it('should not extend the session', async () => { diff --git a/x-pack/test/api_integration/apis/siem/authentications.ts b/x-pack/test/api_integration/apis/siem/authentications.ts index cf9d8d8c9a515..b89a1448d5fe6 100644 --- a/x-pack/test/api_integration/apis/siem/authentications.ts +++ b/x-pack/test/api_integration/apis/siem/authentications.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { authenticationsQuery } from '../../../../legacy/plugins/siem/public/containers/authentications/index.gql_query'; -import { GetAuthenticationsQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { authenticationsQuery } from '../../../../plugins/siem/public/containers/authentications/index.gql_query'; +import { GetAuthenticationsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/hosts.ts b/x-pack/test/api_integration/apis/siem/hosts.ts index 6e74375cedc12..0a2ee9c82bce2 100644 --- a/x-pack/test/api_integration/apis/siem/hosts.ts +++ b/x-pack/test/api_integration/apis/siem/hosts.ts @@ -12,10 +12,10 @@ import { GetHostFirstLastSeenQuery, GetHostsTableQuery, HostsFields, -} from '../../../../legacy/plugins/siem/public/graphql/types'; -import { HostOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/hosts/overview/host_overview.gql_query'; -import { HostFirstLastSeenGqlQuery } from './../../../../legacy/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query'; -import { HostsTableQuery } from './../../../../legacy/plugins/siem/public/containers/hosts/hosts_table.gql_query'; +} from '../../../../plugins/siem/public/graphql/types'; +import { HostOverviewQuery } from '../../../../plugins/siem/public/containers/hosts/overview/host_overview.gql_query'; +import { HostFirstLastSeenGqlQuery } from '../../../../plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query'; +import { HostsTableQuery } from '../../../../plugins/siem/public/containers/hosts/hosts_table.gql_query'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/ip_overview.ts b/x-pack/test/api_integration/apis/siem/ip_overview.ts index bec31a018cac8..2f1a792aff25b 100644 --- a/x-pack/test/api_integration/apis/siem/ip_overview.ts +++ b/x-pack/test/api_integration/apis/siem/ip_overview.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { ipOverviewQuery } from '../../../../legacy/plugins/siem/public/containers/ip_overview/index.gql_query'; -import { GetIpOverviewQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { ipOverviewQuery } from '../../../../plugins/siem/public/containers/ip_overview/index.gql_query'; +import { GetIpOverviewQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/kpi_host_details.ts b/x-pack/test/api_integration/apis/siem/kpi_host_details.ts index 02152c0b71ca9..30f9f6f04a242 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_host_details.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_host_details.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { kpiHostDetailsQuery } from '../../../../legacy/plugins/siem/public/containers/kpi_host_details/index.gql_query'; -import { GetKpiHostDetailsQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { kpiHostDetailsQuery } from '../../../../plugins/siem/public/containers/kpi_host_details/index.gql_query'; +import { GetKpiHostDetailsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index 9e43405b324fa..2303b9ecfb78f 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { kpiHostsQuery } from '../../../../legacy/plugins/siem/public/containers/kpi_hosts/index.gql_query'; -import { GetKpiHostsQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { kpiHostsQuery } from '../../../../plugins/siem/public/containers/kpi_hosts/index.gql_query'; +import { GetKpiHostsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/kpi_network.ts b/x-pack/test/api_integration/apis/siem/kpi_network.ts index 6b12b4e3c938a..22e133e48bbd2 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_network.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_network.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { kpiNetworkQuery } from '../../../../legacy/plugins/siem/public/containers/kpi_network/index.gql_query'; -import { GetKpiNetworkQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { kpiNetworkQuery } from '../../../../plugins/siem/public/containers/kpi_network/index.gql_query'; +import { GetKpiNetworkQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/network_dns.ts b/x-pack/test/api_integration/apis/siem/network_dns.ts index 13e98f09d072b..1eba41e238c81 100644 --- a/x-pack/test/api_integration/apis/siem/network_dns.ts +++ b/x-pack/test/api_integration/apis/siem/network_dns.ts @@ -5,12 +5,12 @@ */ import expect from '@kbn/expect'; -import { networkDnsQuery } from '../../../../legacy/plugins/siem/public/containers/network_dns/index.gql_query'; +import { networkDnsQuery } from '../../../../plugins/siem/public/containers/network_dns/index.gql_query'; import { Direction, GetNetworkDnsQuery, NetworkDnsFields, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts index ee4344bb0f1ee..6ab7945e9000d 100644 --- a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts +++ b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts @@ -5,13 +5,13 @@ */ import expect from '@kbn/expect'; -import { networkTopNFlowQuery } from '../../../../legacy/plugins/siem/public/containers/network_top_n_flow/index.gql_query'; +import { networkTopNFlowQuery } from '../../../../plugins/siem/public/containers/network_top_n_flow/index.gql_query'; import { Direction, FlowTargetSourceDest, GetNetworkTopNFlowQuery, NetworkTopTablesFields, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const EDGE_LENGTH = 10; diff --git a/x-pack/test/api_integration/apis/siem/overview_host.ts b/x-pack/test/api_integration/apis/siem/overview_host.ts index 7e5cbd7673af7..95dbb44e30c41 100644 --- a/x-pack/test/api_integration/apis/siem/overview_host.ts +++ b/x-pack/test/api_integration/apis/siem/overview_host.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { DEFAULT_INDEX_PATTERN } from '../../../../plugins/siem/common/constants'; -import { overviewHostQuery } from '../../../../legacy/plugins/siem/public/containers/overview/overview_host/index.gql_query'; -import { GetOverviewHostQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { overviewHostQuery } from '../../../../plugins/siem/public/containers/overview/overview_host/index.gql_query'; +import { GetOverviewHostQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/overview_network.ts b/x-pack/test/api_integration/apis/siem/overview_network.ts index ce2f8d4a0b5e5..ef7d82d2ea8d9 100644 --- a/x-pack/test/api_integration/apis/siem/overview_network.ts +++ b/x-pack/test/api_integration/apis/siem/overview_network.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { overviewNetworkQuery } from '../../../../legacy/plugins/siem/public/containers/overview/overview_network/index.gql_query'; -import { GetOverviewNetworkQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { overviewNetworkQuery } from '../../../../plugins/siem/public/containers/overview/overview_network/index.gql_query'; +import { GetOverviewNetworkQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts b/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts index 5aa7a10e07c2a..75670374b6f63 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import gql from 'graphql-tag'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { persistTimelineNoteMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/notes/persist.gql_query'; +import { persistTimelineNoteMutation } from '../../../../../plugins/siem/public/containers/timeline/notes/persist.gql_query'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts b/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts index f1e926b3ede42..39055e971d118 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { persistTimelinePinnedEventMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query'; +import { persistTimelinePinnedEventMutation } from '../../../../../plugins/siem/public/containers/timeline/pinned_event/persist.gql_query'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts index 6fe11bc294795..2d9f576ef37e9 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts @@ -15,10 +15,10 @@ import ApolloClient from 'apollo-client'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { deleteTimelineMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/delete/persist.gql_query'; -import { persistTimelineFavoriteMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/favorite/persist.gql_query'; -import { persistTimelineMutation } from '../../../../../legacy/plugins/siem/public/containers/timeline/persist.gql_query'; -import { TimelineResult } from '../../../../../legacy/plugins/siem/public/graphql/types'; +import { deleteTimelineMutation } from '../../../../../plugins/siem/public/containers/timeline/delete/persist.gql_query'; +import { persistTimelineFavoriteMutation } from '../../../../../plugins/siem/public/containers/timeline/favorite/persist.gql_query'; +import { persistTimelineMutation } from '../../../../../plugins/siem/public/containers/timeline/persist.gql_query'; +import { TimelineResult } from '../../../../../plugins/siem/public/graphql/types'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/sources.ts b/x-pack/test/api_integration/apis/siem/sources.ts index e0db91449a8cc..2338d4ce45c8d 100644 --- a/x-pack/test/api_integration/apis/siem/sources.ts +++ b/x-pack/test/api_integration/apis/siem/sources.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; -import { sourceQuery } from '../../../../legacy/plugins/siem/public/containers/source/index.gql_query'; -import { SourceQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { sourceQuery } from '../../../../plugins/siem/public/containers/source/index.gql_query'; +import { SourceQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/timeline.ts b/x-pack/test/api_integration/apis/siem/timeline.ts index a6ab4493448f3..de57b0c3f469f 100644 --- a/x-pack/test/api_integration/apis/siem/timeline.ts +++ b/x-pack/test/api_integration/apis/siem/timeline.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { timelineQuery } from '../../../../legacy/plugins/siem/public/containers/timeline/index.gql_query'; -import { Direction, GetTimelineQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { timelineQuery } from '../../../../plugins/siem/public/containers/timeline/index.gql_query'; +import { Direction, GetTimelineQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const LTE = new Date('3000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/timeline_details.ts b/x-pack/test/api_integration/apis/siem/timeline_details.ts index 5d1e645bcf80b..f88d5355f22c1 100644 --- a/x-pack/test/api_integration/apis/siem/timeline_details.ts +++ b/x-pack/test/api_integration/apis/siem/timeline_details.ts @@ -7,11 +7,8 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import { timelineDetailsQuery } from '../../../../legacy/plugins/siem/public/containers/timeline/details/index.gql_query'; -import { - DetailItem, - GetTimelineDetailsQuery, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +import { timelineDetailsQuery } from '../../../../plugins/siem/public/containers/timeline/details/index.gql_query'; +import { DetailItem, GetTimelineDetailsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; type DetailsData = Array< diff --git a/x-pack/test/api_integration/apis/siem/tls.ts b/x-pack/test/api_integration/apis/siem/tls.ts index 8467308d709af..e4e8b5db3d7e3 100644 --- a/x-pack/test/api_integration/apis/siem/tls.ts +++ b/x-pack/test/api_integration/apis/siem/tls.ts @@ -5,13 +5,13 @@ */ import expect from '@kbn/expect'; -import { tlsQuery } from '../../../../legacy/plugins/siem/public/containers/tls/index.gql_query'; +import { tlsQuery } from '../../../../plugins/siem/public/containers/tls/index.gql_query'; import { Direction, TlsFields, FlowTarget, GetTlsQuery, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/uncommon_processes.ts b/x-pack/test/api_integration/apis/siem/uncommon_processes.ts index b463c4db99653..c9674e740f76d 100644 --- a/x-pack/test/api_integration/apis/siem/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/siem/uncommon_processes.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { uncommonProcessesQuery } from '../../../../legacy/plugins/siem/public/containers/uncommon_processes/index.gql_query'; -import { GetUncommonProcessesQuery } from '../../../../legacy/plugins/siem/public/graphql/types'; +import { uncommonProcessesQuery } from '../../../../plugins/siem/public/containers/uncommon_processes/index.gql_query'; +import { GetUncommonProcessesQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/users.ts b/x-pack/test/api_integration/apis/siem/users.ts index f1869d40fbf1d..c8ea1be7d3f11 100644 --- a/x-pack/test/api_integration/apis/siem/users.ts +++ b/x-pack/test/api_integration/apis/siem/users.ts @@ -5,13 +5,13 @@ */ import expect from '@kbn/expect'; -import { usersQuery } from '../../../../legacy/plugins/siem/public/containers/users/index.gql_query'; +import { usersQuery } from '../../../../plugins/siem/public/containers/users/index.gql_query'; import { Direction, UsersFields, FlowTarget, GetUsersQuery, -} from '../../../../legacy/plugins/siem/public/graphql/types'; +} from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts index 6d125807e169d..6c566ec7cb23b 100644 --- a/x-pack/test/api_integration/apis/uptime/feature_controls.ts +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PINGS_DATE_RANGE_END, PINGS_DATE_RANGE_START } from './constants'; -import { API_URLS } from '../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../plugins/uptime/common/constants'; export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/certs.ts b/x-pack/test/api_integration/apis/uptime/rest/certs.ts index 7510ea3f34d28..4917917fdd6bc 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/certs.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/certs.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import moment from 'moment'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { CertType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; +import { CertType } from '../../../../../plugins/uptime/common/runtime_types'; import { makeChecksWithStatus } from './helper/make_checks'; export default function({ getService }: FtrProviderContext) { @@ -21,7 +21,7 @@ export default function({ getService }: FtrProviderContext) { describe('empty index', async () => { it('returns empty array for no data', async () => { const apiResponse = await supertest.get(API_URLS.CERTS); - expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[]}'); + expect(JSON.stringify(apiResponse.body)).to.eql('{"certs":[],"total":0}'); }); }); @@ -39,10 +39,10 @@ export default function({ getService }: FtrProviderContext) { 10000, { tls: { - certificate_not_valid_after: cnva, - certificate_not_valid_before: cnvb, server: { x509: { + not_after: cnva, + not_before: cnvb, issuer: { common_name: 'issuer-common-name', }, @@ -78,9 +78,12 @@ export default function({ getService }: FtrProviderContext) { const cert = body.certs[0]; expect(Array.isArray(cert.monitors)).to.be(true); - expect(cert.monitors[0]).to.eql({ id: monitorId }); - expect(cert.certificate_not_valid_after).to.eql(cnva); - expect(cert.certificate_not_valid_before).to.eql(cnvb); + expect(cert.monitors[0]).to.eql({ + id: monitorId, + url: 'http://localhost:5678/pattern?r=200x5,500x1', + }); + expect(cert.not_after).to.eql(cnva); + expect(cert.not_before).to.eql(cnvb); expect(cert.common_name).to.eql('subject-common-name'); expect(cert.issuer).to.eql('issuer-common-name'); }); diff --git a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts index 5258426cf193c..f343cd1da8788 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/doc_count.ts @@ -5,7 +5,7 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; import { expectFixtureEql } from './helper/expect_fixture_eql'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { describe('docCount query', () => { diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index a1b731169f0a0..95caf50d1ca7a 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -5,24 +5,26 @@ */ import expect from '@kbn/expect'; +import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { defaultDynamicSettings } from '../../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; - +import { DynamicSettingsType } from '../../../../../plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('dynamic settings', () => { it('returns the defaults when no user settings have been saved', async () => { const apiResponse = await supertest.get(`/api/uptime/dynamic_settings`); - expect(apiResponse.body).to.eql(defaultDynamicSettings as any); + expect(apiResponse.body).to.eql(DYNAMIC_SETTINGS_DEFAULTS); + expect(isRight(DynamicSettingsType.decode(apiResponse.body))).to.be.ok(); }); it('can change the settings', async () => { const newSettings = { heartbeatIndices: 'myIndex1*', - certificatesThresholds: { - errorState: 5, - warningState: 15, + certThresholds: { + expiration: 5, + age: 15, }, }; const postResponse = await supertest @@ -35,6 +37,7 @@ export default function({ getService }: FtrProviderContext) { const getResponse = await supertest.get(`/api/uptime/dynamic_settings`); expect(getResponse.body).to.eql(newSettings); + expect(isRight(DynamicSettingsType.decode(getResponse.body))).to.be.ok(); }); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json index 9a33be807670e..1baff443bd97f 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json @@ -27,5 +27,6 @@ "full": "http://localhost:5678/pattern?r=200x1" }, "docId": "h5toHm0B0I9WX_CznN_V", - "timestamp": "2019-09-11T03:40:34.371Z" -} \ No newline at end of file + "timestamp": "2019-09-11T03:40:34.371Z", + "tls": {} +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts index ae326c8b2aee0..5f62a3c55a2eb 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_checks.ts @@ -6,119 +6,36 @@ import uuid from 'uuid'; import { merge, flattenDeep } from 'lodash'; - -const INDEX_NAME = 'heartbeat-8-generated-test'; - -export const makePing = async ( - es: any, - monitorId: string, - fields: { [key: string]: any }, - mogrify: (doc: any) => any, - refresh: boolean = true -) => { - const baseDoc = { - tcp: { - rtt: { - connect: { - us: 14687, - }, - }, - }, - observer: { - geo: { - name: 'mpls', - location: '37.926868, -78.024902', - }, - hostname: 'avc-x1e', - }, - agent: { - hostname: 'avc-x1e', - id: '10730a1a-4cb7-45ce-8524-80c4820476ab', - type: 'heartbeat', - ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad', - version: '8.0.0', - }, - '@timestamp': new Date().toISOString(), - resolve: { - rtt: { - us: 350, - }, - ip: '127.0.0.1', - }, - ecs: { - version: '1.1.0', - }, - host: { - name: 'avc-x1e', - }, - http: { - rtt: { - response_header: { - us: 19349, - }, - total: { - us: 48954, - }, - write_request: { - us: 33, - }, - content: { - us: 51, - }, - validate: { - us: 19400, - }, - }, - response: { - status_code: 200, - body: { - bytes: 3, - hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf', - }, - }, - }, - monitor: { - duration: { - us: 49347, - }, - ip: '127.0.0.1', - id: monitorId, - check_group: uuid.v4(), - type: 'http', - status: 'up', - }, - event: { - dataset: 'uptime', - }, - url: { - path: '/pattern', - scheme: 'http', - port: 5678, - domain: 'localhost', - query: 'r=200x5,500x1', - full: 'http://localhost:5678/pattern?r=200x5,500x1', - }, - }; - - const doc = mogrify(merge(baseDoc, fields)); - - await es.index({ - index: INDEX_NAME, - refresh, - body: doc, - }); - - return doc; +import { makePing } from './make_ping'; +import { TlsProps } from './make_tls'; + +interface CheckProps { + es: any; + monitorId?: string; + numIps?: number; + fields?: { [key: string]: any }; + mogrify?: (doc: any) => any; + refresh?: boolean; + tls?: boolean | TlsProps; +} + +const getRandomMonitorId = () => { + return ( + 'monitor-' + + Math.random() + .toString(36) + .substring(7) + ); }; - -export const makeCheck = async ( - es: any, - monitorId: string, - numIps: number, - fields: { [key: string]: any }, - mogrify: (doc: any) => any, - refresh: boolean = true -) => { +export const makeCheck = async ({ + es, + monitorId = getRandomMonitorId(), + numIps = 1, + fields = {}, + mogrify = d => d, + refresh = true, + tls = false, +}: CheckProps): Promise<{ monitorId: string; docs: any }> => { const cgFields = { monitor: { check_group: uuid.v4(), @@ -139,7 +56,7 @@ export const makeCheck = async ( if (i === numIps - 1) { pingFields.summary = summary; } - const doc = await makePing(es, monitorId, pingFields, mogrify, false); + const doc = await makePing(es, monitorId, pingFields, mogrify, false, tls as any); docs.push(doc); // @ts-ignore summary[doc.monitor.status]++; @@ -149,15 +66,15 @@ export const makeCheck = async ( await es.indices.refresh(); } - return docs; + return { monitorId, docs }; }; export const makeChecks = async ( es: any, monitorId: string, - numChecks: number, - numIps: number, - every: number, // number of millis between checks + numChecks: number = 1, + numIps: number = 1, + every: number = 10000, // number of millis between checks fields: { [key: string]: any } = {}, mogrify: (doc: any) => any = d => d, refresh: boolean = true @@ -177,7 +94,8 @@ export const makeChecks = async ( }, }, }); - checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify, false)); + const { docs } = await makeCheck({ es, monitorId, numIps, fields, mogrify, refresh: false }); + checks.push(docs); } if (refresh) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts new file mode 100644 index 0000000000000..908c571e07e06 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts @@ -0,0 +1,118 @@ +/* + * 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 uuid from 'uuid'; +import { merge } from 'lodash'; +import { makeTls, TlsProps } from './make_tls'; + +const INDEX_NAME = 'heartbeat-8-generated-test'; + +export const makePing = async ( + es: any, + monitorId: string, + fields: { [key: string]: any }, + mogrify: (doc: any) => any, + refresh: boolean = true, + tls: boolean | TlsProps = false +) => { + const baseDoc: any = { + tcp: { + rtt: { + connect: { + us: 14687, + }, + }, + }, + observer: { + geo: { + name: 'mpls', + location: '37.926868, -78.024902', + }, + hostname: 'avc-x1e', + }, + agent: { + hostname: 'avc-x1e', + id: '10730a1a-4cb7-45ce-8524-80c4820476ab', + type: 'heartbeat', + ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad', + version: '8.0.0', + }, + '@timestamp': new Date().toISOString(), + resolve: { + rtt: { + us: 350, + }, + ip: '127.0.0.1', + }, + ecs: { + version: '1.1.0', + }, + host: { + name: 'avc-x1e', + }, + http: { + rtt: { + response_header: { + us: 19349, + }, + total: { + us: 48954, + }, + write_request: { + us: 33, + }, + content: { + us: 51, + }, + validate: { + us: 19400, + }, + }, + response: { + status_code: 200, + body: { + bytes: 3, + hash: '27badc983df1780b60c2b3fa9d3a19a00e46aac798451f0febdca52920faaddf', + }, + }, + }, + monitor: { + duration: { + us: 49347, + }, + ip: '127.0.0.1', + id: monitorId, + check_group: uuid.v4(), + type: 'http', + status: 'up', + }, + event: { + dataset: 'uptime', + }, + url: { + path: '/pattern', + scheme: 'http', + port: 5678, + domain: 'localhost', + query: 'r=200x5,500x1', + full: 'http://localhost:5678/pattern?r=200x5,500x1', + }, + }; + + if (tls) { + baseDoc.tls = makeTls(tls as any); + } + + const doc = mogrify(merge(baseDoc, fields)); + + await es.index({ + index: INDEX_NAME, + refresh, + body: doc, + }); + + return doc; +}; diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts new file mode 100644 index 0000000000000..3606462522024 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import crypto from 'crypto'; + +export interface TlsProps { + valid?: boolean; + commonName?: string; + expiry?: string; + sha256?: string; +} + +type Props = TlsProps & boolean; + +// Note This is just a mock sha256 value, this doesn't actually generate actually sha 256 val +export const getSha256 = () => { + return crypto + .randomBytes(64) + .toString('hex') + .toUpperCase(); +}; + +export const makeTls = ({ valid = true, commonName = '*.elastic.co', expiry, sha256 }: Props) => { + const expiryDate = + expiry ?? + moment() + .add(valid ? 2 : -2, 'months') + .toISOString(); + + return { + version: '1.3', + cipher: 'TLS-AES-128-GCM-SHA256', + certificate_not_valid_before: '2020-03-01T00:00:00.000Z', + certificate_not_valid_after: expiryDate, + server: { + x509: { + not_before: '2020-03-01T00:00:00.000Z', + not_after: '2020-05-30T12:00:00.000Z', + issuer: { + distinguished_name: + 'CN=DigiCert SHA2 High Assurance Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US', + common_name: 'DigiCert SHA2 High Assurance Server CA', + }, + subject: { + common_name: commonName, + distinguished_name: 'CN=*.facebook.com,O=Facebook Inc.,L=Menlo Park,ST=California,C=US', + }, + serial_number: '10043199409725537507026285099403602396', + signature_algorithm: 'SHA256-RSA', + public_key_algorithm: 'ECDSA', + public_key_curve: 'P-256', + }, + hash: { + sha256: sha256 ?? '1a48f1db13c3bd1482ba1073441e74a1bb1308dc445c88749e0dc4f1889a88a4', + sha1: '23291c758d925b9f4bb3584de3763317e94c6ce9', + }, + }, + established: true, + rtt: { + handshake: { + us: 33103, + }, + }, + version_protocol: 'tls', + }; +}; diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts index 3c17370532f91..c3d5849e028ab 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; import { makeChecksWithStatus } from './helper/make_checks'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { MonitorSummary } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummary } from '../../../../../plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index f1e37bff405fd..c5a691312f525 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; -import { MonitorSummaryResultType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { MonitorSummaryResultType } from '../../../../../plugins/uptime/common/runtime_types'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; interface ExpectedMonitorStatesPage { response: any; diff --git a/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts index a261763d5991f..3d754d89cf9be 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/ping_list.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { isLeft } from 'fp-ts/lib/Either'; import { PathReporter } from 'io-ts/lib/PathReporter'; -import { PingsResponseType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; +import { PingsResponseType } from '../../../../../plugins/uptime/common/runtime_types'; import { FtrProviderContext } from '../../../ftr_provider_context'; function decodePingsResponseData(response: any) { diff --git a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts index 017ef02afe5ea..99e09aa5ce886 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/telemetry_collectors.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { API_URLS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { API_URLS } from '../../../../../plugins/uptime/common/constants'; import { makeChecksWithStatus } from './helper/make_checks'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 0eac7c58044e6..dda8c2d888d30 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -30,7 +30,6 @@ export async function getApiIntegrationConfig({ readConfigFile }) { '--telemetry.optIn=true', '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', - '--xpack.ingestManager.fleet.enabled=true', '--xpack.endpoint.alertResultListDefaultDateRange.from=2018-01-10T00:00:00.000Z', ], }, diff --git a/x-pack/test/api_integration/config_security_basic.js b/x-pack/test/api_integration/config_security_basic.js index d21bfa4d7031a..4c4b77ee5b080 100644 --- a/x-pack/test/api_integration/config_security_basic.js +++ b/x-pack/test/api_integration/config_security_basic.js @@ -13,6 +13,7 @@ export default async function({ readConfigFile }) { config.esTestCluster.serverArgs = [ 'xpack.license.self_generated.type=basic', 'xpack.security.enabled=true', + 'xpack.security.authc.api_key.enabled=true', ]; config.testFiles = [require.resolve('./apis/security/security_basic')]; return config; diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index 84b8476bd1dd1..6dcc9bb291b02 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -21,6 +21,7 @@ import { } from './infraops_graphql_client'; import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client'; import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; +import { InfraLogSourceConfigurationProvider } from './infra_log_source_configuration'; import { MachineLearningProvider } from './ml'; import { IngestManagerProvider } from './ingest_manager'; @@ -35,6 +36,7 @@ export const services = { infraOpsGraphQLClient: InfraOpsGraphQLClientProvider, infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider, infraOpsSourceConfiguration: InfraOpsSourceConfigurationProvider, + infraLogSourceConfiguration: InfraLogSourceConfigurationProvider, siemGraphQLClient: SiemGraphQLClientProvider, siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider, supertestWithoutAuth: SupertestWithoutAuthProvider, diff --git a/x-pack/test/api_integration/services/infra_log_source_configuration.ts b/x-pack/test/api_integration/services/infra_log_source_configuration.ts new file mode 100644 index 0000000000000..851720895c620 --- /dev/null +++ b/x-pack/test/api_integration/services/infra_log_source_configuration.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 { + getLogSourceConfigurationPath, + getLogSourceConfigurationSuccessResponsePayloadRT, + PatchLogSourceConfigurationRequestBody, + patchLogSourceConfigurationRequestBodyRT, + patchLogSourceConfigurationResponsePayloadRT, +} from '../../../plugins/infra/common/http_api/log_sources'; +import { decodeOrThrow } from '../../../plugins/infra/common/runtime_types'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function InfraLogSourceConfigurationProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + + const createGetLogSourceConfigurationAgent = (sourceId: string) => + supertest + .get(getLogSourceConfigurationPath(sourceId)) + .set({ + 'kbn-xsrf': 'some-xsrf-token', + }) + .send(); + + const getLogSourceConfiguration = async (sourceId: string) => { + log.debug(`Fetching Logs UI source configuration "${sourceId}"`); + + const response = await createGetLogSourceConfigurationAgent(sourceId); + + return decodeOrThrow(getLogSourceConfigurationSuccessResponsePayloadRT)(response.body); + }; + + const createUpdateLogSourceConfigurationAgent = ( + sourceId: string, + sourceProperties: PatchLogSourceConfigurationRequestBody['data'] + ) => + supertest + .patch(getLogSourceConfigurationPath(sourceId)) + .set({ + 'kbn-xsrf': 'some-xsrf-token', + }) + .send(patchLogSourceConfigurationRequestBodyRT.encode({ data: sourceProperties })); + + const updateLogSourceConfiguration = async ( + sourceId: string, + sourceProperties: PatchLogSourceConfigurationRequestBody['data'] + ) => { + log.debug( + `Updating Logs UI source configuration "${sourceId}" with properties ${JSON.stringify( + sourceProperties + )}` + ); + + const response = await createUpdateLogSourceConfigurationAgent(sourceId, sourceProperties); + + return decodeOrThrow(patchLogSourceConfigurationResponsePayloadRT)(response.body); + }; + + return { + createGetLogSourceConfigurationAgent, + createUpdateLogSourceConfigurationAgent, + getLogSourceConfiguration, + updateLogSourceConfiguration, + }; +} diff --git a/x-pack/test/api_integration/services/siem_graphql_client.ts b/x-pack/test/api_integration/services/siem_graphql_client.ts index 7d6aa5e8a9dd5..ba60e66682955 100644 --- a/x-pack/test/api_integration/services/siem_graphql_client.ts +++ b/x-pack/test/api_integration/services/siem_graphql_client.ts @@ -11,7 +11,7 @@ import { ApolloClient } from 'apollo-client'; import { HttpLink } from 'apollo-link-http'; import { FtrProviderContext } from '../ftr_provider_context'; -import introspectionQueryResultData from '../../../legacy/plugins/siem/public/graphql/introspection.json'; +import introspectionQueryResultData from '../../../plugins/siem/public/graphql/introspection.json'; interface SiemGraphQLClientFactoryOptions { username?: string; diff --git a/x-pack/test/case_api_integration/basic/config.ts b/x-pack/test/case_api_integration/basic/config.ts new file mode 100644 index 0000000000000..f9c248ec3d56f --- /dev/null +++ b/x-pack/test/case_api_integration/basic/config.ts @@ -0,0 +1,14 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('basic', { + disabledPlugins: [], + license: 'basic', + ssl: true, +}); diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts new file mode 100644 index 0000000000000..a9fc2706a6ba2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return an empty find body correctly if no configuration is loaded', async () => { + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({}); + }); + + it('should return a configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts new file mode 100644 index 0000000000000..836c76d500034 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../plugins/case/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('get_connectors', () => { + it('should return an empty find body correctly if no connectors are loaded', async () => { + const { body } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts new file mode 100644 index 0000000000000..d66baa2a2eee2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should patch a configuration', async () => { + const res = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: res.body.version }) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); + }); + + it('should handle patch request when there is no configuration', async () => { + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'You can not patch this configuration since you did not created first with a post.', + statusCode: 409, + }); + }); + + it('should handle patch request when versions are different', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'This configuration has been updated. Please refresh before saving additional updates.', + statusCode: 409, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts new file mode 100644 index 0000000000000..c2284492e5b77 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts @@ -0,0 +1,62 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should create a configuration', async () => { + const { body } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should keep only the latest configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration('connector-2')) + .expect(200); + + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts new file mode 100644 index 0000000000000..efd5369c019d8 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('case api basic', function() { + // Fastest ciGroup for the moment. + this.tags('ciGroup2'); + + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/get_connectors')); + }); +}; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts new file mode 100644 index 0000000000000..862705ab9610b --- /dev/null +++ b/x-pack/test/case_api_integration/common/config.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import path from 'path'; + +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.server-log', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers, + services, + junit: { + reportName: 'X-Pack Case API Integration Tests', + }, + esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'), + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && + ['trial', 'basic'].includes(license)}`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.whitelistedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + ])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.alerting.enabled=true', + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts @@ -0,0 +1,11 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts new file mode 100644 index 0000000000000..6d0db69309b90 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasesConfigureRequest, CasesConfigureResponse } from '../../../../plugins/case/common/api'; + +export const getConfiguration = (connector_id: string = 'connector-1'): CasesConfigureRequest => { + return { + connector_id, + connector_name: 'Connector 1', + closure_type: 'close-by-user', + }; +}; + +export const getConfigurationOutput = (update = false): Partial<CasesConfigureResponse> => { + return { + ...getConfiguration(), + created_by: { email: null, full_name: null, username: 'elastic' }, + updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null, + }; +}; + +export const removeServerGeneratedPropertiesFromConfigure = ( + config: Partial<CasesConfigureResponse> +): Partial<CasesConfigureResponse> => { + const { created_at, updated_at, version, ...rest } = config; + return rest; +}; + +export const deleteConfiguration = async (es: any): Promise<void> => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:cases-configure', + waitForCompletion: true, + refresh: 'wait_for', + body: {}, + }); +}; + +export const getConnector = () => ({ + name: 'ServiceNow Connector', + actionTypeId: '.servicenow', + secrets: { + username: 'admin', + password: 'admin', + }, + config: { + apiUrl: 'localhost', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, +}); diff --git a/x-pack/test/case_api_integration/common/services.ts b/x-pack/test/case_api_integration/common/services.ts new file mode 100644 index 0000000000000..a927a31469bab --- /dev/null +++ b/x-pack/test/case_api_integration/common/services.ts @@ -0,0 +1,7 @@ +/* + * 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 { services } from '../../api_integration/services'; diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index e89352118990a..1e6600c7cd2c0 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -78,7 +78,6 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) 'some.non.existent.com', ])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, - '--xpack.alerting.enabled=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, diff --git a/x-pack/test/epm_api_integration/config.ts b/x-pack/test/epm_api_integration/config.ts index e95a389ef20ed..b04bc76ccb315 100644 --- a/x-pack/test/epm_api_integration/config.ts +++ b/x-pack/test/epm_api_integration/config.ts @@ -28,7 +28,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.ingestManager.epm.enabled=true', '--xpack.ingestManager.epm.registryUrl=http://localhost:6666', ], }, diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 1c83a17e78ca7..abfd90bd6ad8e 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -13,7 +13,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const security = getService('security'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await security.testUser.setRoles(['kibana_admin']); await pageObjects.common.navigateToApp('apiKeys'); diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index de3976509be1f..3bd3749ec6935 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,8 +19,7 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - // FLAKY: https://github.com/elastic/kibana/issues/62927 - describe.skip('custom elements', function() { + describe('custom elements', function() { this.tags('skipFirefox'); before(async () => { @@ -41,8 +40,11 @@ export default function canvasCustomElementTest({ // find the first workpad element (a markdown element) and click it to select it await testSubjects.click('canvasWorkpadPage > canvasWorkpadPageElementContent', 20000); + // click "Edit" menu + await testSubjects.click('canvasWorkpadEditMenuButton', 20000); + // click the "Save as new element" button - await find.clickByCssSelector('[aria-label="Save as new element"]', 20000); + await testSubjects.click('canvasWorkpadEditMenu__saveElementButton', 20000); // fill out the custom element form and submit it await PageObjects.canvas.fillOutCustomElementForm( @@ -57,15 +59,11 @@ export default function canvasCustomElementTest({ }); it('adds the custom element to the workpad when prompted', async () => { - await PageObjects.canvas.openAddElementModal(); - - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the custom element is the one expected and click it to add to the workpad - const customElement = await find.byCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper' - ); + const customElement = await find.byCssSelector('.canvasElementCard__wrapper'); const elementName = await customElement.findByCssSelector('.euiCard__title'); expect(await elementName.getVisibleText()).to.contain('My New Element'); customElement.click(); @@ -95,14 +93,11 @@ export default function canvasCustomElementTest({ }); it('saves custom element modifications', async () => { - await PageObjects.canvas.openAddElementModal(); - - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the correct amount of custom elements exist - const container = await find.byCssSelector('[aria-labelledby="customElements"]'); - const customElements = await container.findAllByCssSelector('.canvasElementCard__wrapper'); + const customElements = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElements).to.have.length(1); // hover over the custom element to bring up the edit and delete icons @@ -110,8 +105,7 @@ export default function canvasCustomElementTest({ await customElement.moveMouseTo(); // click the edit element button - const editBtn = await customElement.findByCssSelector('[aria-label="Edit element"]'); - await editBtn.click(); + await testSubjects.click('canvasElementCard__editButton', 20000); // fill out the custom element form and submit it await PageObjects.canvas.fillOutCustomElementForm( @@ -121,22 +115,21 @@ export default function canvasCustomElementTest({ // ensure the custom element in the modal shows the updated text await retry.try(async () => { - const elementName = await find.byCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper .euiCard__title' - ); + const elementName = await find.byCssSelector('.canvasElementCard__wrapper .euiCard__title'); expect(await elementName.getVisibleText()).to.contain('My Edited New Element'); }); + + // Close the modal + await PageObjects.canvas.closeSavedElementsModal(); }); it('deletes custom element when prompted', async () => { - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the correct amount of custom elements exist - const customElements = await find.allByCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper' - ); + const customElements = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElements).to.have.length(1); // hover over the custom element to bring up the edit and delete icons @@ -144,22 +137,18 @@ export default function canvasCustomElementTest({ await customElement.moveMouseTo(); // click the delete element button - const editBtn = await customElement.findByCssSelector('[aria-label="Delete element"]'); - await editBtn.click(); + await testSubjects.click('canvasElementCard__deleteButton', 20000); - await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton', 20000); // ensure the custom element was deleted await retry.try(async () => { - const containerAgain = await find.byCssSelector('[aria-labelledby="customElements"]'); - const customElementsAgain = await containerAgain.findAllByCssSelector( - '.canvasElementCard__wrapper' - ); + const customElementsAgain = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElementsAgain).to.have.length(0); }); // Close the modal - await browser.pressKeys(browser.keys.ESCAPE); + await PageObjects.canvas.closeSavedElementsModal(); }); }); } diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index a240a55b9765c..df41b725f6daf 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -15,7 +15,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common']); describe('smoke test', function() { - this.tags('smoke'); + this.tags('includeFirefox'); const workpadListSelector = 'canvasWorkpadLoaderTable > canvasWorkpadLoaderWorkpad'; const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31'; diff --git a/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts b/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts index eef47ae0341c8..8d3b98004245e 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts @@ -12,7 +12,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('crossClusterReplication'); }); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 794bd2f1f0db3..23825836caad3 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/functional/apps/dashboard/reporting/README.md b/x-pack/test/functional/apps/dashboard/reporting/README.md new file mode 100644 index 0000000000000..3a2b8f5cc783f --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/README.md @@ -0,0 +1,22 @@ +## The Dashboard Reporting Tests + +### Baseline snapshots + +The reporting tests create a few PNG reports and do a snapshot comparison against stored baselines. The baseline images are stored in `./reports/baseline`. + +### Updating the baselines + +Every now and then visual changes will be made that will require the snapshots to be updated. This is how you go about updating it. + +1. **Load the ES Archive containing the dashboard and data.** + This will load the test data into an Elasticsearch instance running via the functional test server: + ``` + node scripts/es_archiver load reporting/ecommerce --config=x-pack/test/functional/config.js + node scripts/es_archiver load reporting/ecommerce_kibana --config=x-pack/test/functional/config.js + ``` +2. **Generate the reports of the E-commerce dashboard in the Kibana UI.** + Navigate to `http://localhost:5620`, find the archived dashboard, and generate all the types of reports for which there are stored baseline images. +3. **Download the reports, and save them into the `reports/baseline` folder.** + Change the names of the PNG/PDF files to overwrite the stored baselines. + +The next time functional tests run, the generated reports will be compared to the latest image that you have saved :bowtie: \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.ts b/x-pack/test/functional/apps/dashboard/reporting/index.ts new file mode 100644 index 0000000000000..796e15b4e270f --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts @@ -0,0 +1,127 @@ +/* + * 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 expect from '@kbn/expect'; +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); +const mkdirAsync = promisify(fs.mkdir); + +const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const config = getService('config'); + const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); + + describe('Reporting', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + describe('Print PDF button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + }); + + describe('Print Layout', () => { + it('downloads a PDF file', async function() { + // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs + // function is taking about 15 seconds per comparison in jenkins. + this.timeout(300000); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + await PageObjects.reporting.checkUsePrintLayout(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/pdf'); + }); + }); + + describe('Print PNG button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.reporting.openPngReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.dashboard.saveDashboard('My PNG Dash'); + await PageObjects.reporting.openPngReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + }); + + describe('Preserve Layout', () => { + it('matches baseline report', async function() { + 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: string, reportExt: string) => { + const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); + const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); + log.debug(`getBaselineReportPath (${fullPath})`); + return fullPath; + }; + + this.timeout(300000); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPngReportingPanel(); + await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); + await PageObjects.reporting.clickGenerateReportButton(); + await PageObjects.reporting.removeForceSharedItemsContainerSize(); + + const url = await PageObjects.reporting.getReportURL(60000); + const reportData = await PageObjects.reporting.getRawPdfReportData(url); + const reportFileName = 'dashboard_preserve_layout'; + const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); + const percentSimilar = await checkIfPngsMatch( + sessionReportPath, + getBaselineReportPath(reportFileName, 'png'), + config.get('screenshots.directory'), + log + ); + + expect(percentSimilar).to.be.lessThan(0.1); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts new file mode 100644 index 0000000000000..b2eb645c8372c --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/lib/compare_pngs.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { promisify } from 'bluebird'; +import fs from 'fs'; +import path from 'path'; +import { comparePngs } from '../../../../../../../test/functional/services/lib/compare_pngs'; + +const mkdirAsync = promisify<void, fs.PathLike, { recursive: boolean }>(fs.mkdir); + +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. + const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); + const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); + + await mkdirAsync(sessionDirectoryPath, { recursive: true }); + await mkdirAsync(failureDirectoryPath, { recursive: true }); + + const actualpngFileName = path.basename(actualpngPath, '.png'); + const baselinepngFileName = path.basename(baselinepngPath, '.png'); + + const baselineCopyPath = path.resolve( + sessionDirectoryPath, + `${baselinepngFileName}_baseline.png` + ); + const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualpngFileName}_actual.png`); + + // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we + // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have + // mac and linux covered which is better than nothing for now. + try { + log.debug(`writeFileSync: ${baselineCopyPath}`); + fs.writeFileSync(baselineCopyPath, fs.readFileSync(baselinepngPath)); + } catch (error) { + log.error(`No baseline png found at ${baselinepngPath}`); + return 0; + } + log.debug(`writeFileSync: ${actualCopyPath}`); + fs.writeFileSync(actualCopyPath, fs.readFileSync(actualpngPath)); + + let diffTotal = 0; + + const diffPngPath = path.resolve(failureDirectoryPath, `${baselinepngFileName}-${1}.png`); + diffTotal += await comparePngs( + actualCopyPath, + baselineCopyPath, + diffPngPath, + sessionDirectoryPath, + log + ); + + return diffTotal; +} diff --git a/x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/reporting/reports/baseline/dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/reporting/functional/reports/baseline/dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/reporting/reports/baseline/dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index dc8c488460100..76ca613af4b55 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -28,8 +28,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY: https://github.com/elastic/kibana/issues/60535 - describe.skip('security', () => { + describe('security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index f33b8b4899d16..4bedc757f0b57 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -24,8 +24,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY: https://github.com/elastic/kibana/issues/60559 - describe.skip('spaces', () => { + describe('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 2e5bdd736337d..7688315a00516 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/functional/apps/discover/reporting.ts b/x-pack/test/functional/apps/discover/reporting.ts new file mode 100644 index 0000000000000..7a33e7f5135d4 --- /dev/null +++ b/x-pack/test/functional/apps/discover/reporting.ts @@ -0,0 +1,76 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['reporting', 'common', 'discover']); + const filterBar = getService('filterBar'); + + describe('Discover', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + }); + + describe('Generate CSV button', () => { + beforeEach(() => PageObjects.common.navigateToApp('discover')); + + it('is not available if new', async () => { + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + + it('becomes available/not available when a saved search is created, changed and saved again', async () => { + // create new search, csv export is not available + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + // save search, csv export is available + await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton 2'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + // add filter, csv export is not available + await filterBar.addFilter('currency', 'is', 'EUR'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + // save search again, csv export is available + await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton 2'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + + it('generates a report with data', async () => { + await PageObjects.discover.clickNewSearchButton(); + await PageObjects.reporting.setTimepickerInDataRange(); + await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + }); + + it('generates a report with no data', async () => { + await PageObjects.reporting.setTimepickerInNoDataRange(); + await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); + await PageObjects.reporting.openCsvReportingPanel(); + expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/graph/graph.ts b/x-pack/test/functional/apps/graph/graph.ts index 2bbc39969370b..fcf7298c5577a 100644 --- a/x-pack/test/functional/apps/graph/graph.ts +++ b/x-pack/test/functional/apps/graph/graph.ts @@ -76,7 +76,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { } it('should show correct node labels', async function() { - await PageObjects.graph.selectIndexPattern('secrepo*'); + await PageObjects.graph.selectIndexPattern('secrepo'); await buildGraph(); const { nodes } = await PageObjects.graph.getGraphObjects(); const circlesText = nodes.map(({ label }) => label); @@ -120,7 +120,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { it('should create new Graph workspace', async function() { await PageObjects.graph.newGraph(); - await PageObjects.graph.selectIndexPattern('secrepo*'); + await PageObjects.graph.selectIndexPattern('secrepo'); const { nodes, edges } = await PageObjects.graph.getGraphObjects(); expect(nodes).to.be.empty(); expect(edges).to.be.empty(); diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index 994f65a79011e..09d246a3a6e1b 100644 --- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js +++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js @@ -12,7 +12,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['grokDebugger']); describe('grok debugger app', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); // Increase window height to ensure "Simulate" button is shown above the diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index c7ad3f933ea64..b5d43be21f0c5 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -12,7 +12,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('indexLifecycleManagement'); }); diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index c6b7517fc1858..046b8ec44b9fa 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -14,7 +14,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const browser = getService('browser'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('indexManagement'); }); diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 0c1d57202b8eb..ed8bec570ab60 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -15,7 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraHome']); describe('Home page', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index a287d53d5df0b..89ed51f65b930 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { URL } from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; const ONE_HOUR = 60 * 60 * 1000; @@ -21,7 +22,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const traceId = '433b4651687e18be2c6c8e3b11f53d09'; describe('Infra link-to', function() { - this.tags('smoke'); it('redirects to the logs app and parses URL search params correctly', async () => { const location = { hash: '', @@ -29,8 +29,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { search: `time=${timestamp}&filter=trace.id:${traceId}`, state: undefined, }; - const expectedSearchString = `logFilter=(expression:'trace.id:${traceId}',kind:kuery)&logPosition=(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)&sourceId=default`; - const expectedRedirectPath = '/logs/stream?'; await pageObjects.common.navigateToUrlWithBrowserHistory( 'infraLogs', @@ -42,9 +40,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); await retry.tryForTime(5000, async () => { const currentUrl = await browser.getCurrentUrl(); - const decodedUrl = decodeURIComponent(currentUrl); - expect(decodedUrl).to.contain(expectedRedirectPath); - expect(decodedUrl).to.contain(expectedSearchString); + const parsedUrl = new URL(currentUrl); + + expect(parsedUrl.pathname).to.be('/app/logs/stream'); + expect(parsedUrl.searchParams.get('logFilter')).to.be( + `(expression:'trace.id:${traceId}',kind:kuery)` + ); + expect(parsedUrl.searchParams.get('logPosition')).to.be( + `(end:'${endDate}',position:(tiebreaker:0,time:${timestamp}),start:'${startDate}',streamLive:!f)` + ); + expect(parsedUrl.searchParams.get('sourceId')).to.be('default'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts index c703738e37228..3dd1592286a3a 100644 --- a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts +++ b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts @@ -13,7 +13,7 @@ export default ({ getService }: FtrProviderContext) => { const retry = getService('retry'); describe('Log Entry Categories Tab', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('with a trial license', () => { it('is visible', async () => { diff --git a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts index 95228a520aaa2..ee4b59d04899f 100644 --- a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts +++ b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts @@ -13,7 +13,7 @@ export default ({ getService }: FtrProviderContext) => { const retry = getService('retry'); describe('Log Entry Rate Tab', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('with a trial license', () => { it('is visible', async () => { diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index f40c908f23c80..7293405ce80ff 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -17,8 +17,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); describe('Logs Source Configuration', function() { - this.tags('smoke'); - before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts index d334fa7956be4..1c03b3ebf6681 100644 --- a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts @@ -15,7 +15,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraHome']); describe('Infrastructure Source Configuration', function() { - this.tags('smoke'); before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/ingest_pipelines/index.ts b/x-pack/test/functional/apps/ingest_pipelines/index.ts new file mode 100644 index 0000000000000..87e7e70c0b5e0 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Ingest pipelines app', function() { + this.tags('ciGroup3'); + loadTestFile(require.resolve('./ingest_pipelines')); + }); +}; diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts new file mode 100644 index 0000000000000..1b22f8f35d7ad --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'ingestPipelines']); + const log = getService('log'); + + describe('Ingest Pipelines', function() { + this.tags('smoke'); + before(async () => { + await pageObjects.common.navigateToApp('ingestPipelines'); + }); + + it('Loads the app', async () => { + await log.debug('Checking for section heading to say Ingest Node Pipelines.'); + + const headingText = await pageObjects.ingestPipelines.sectionHeadingText(); + expect(headingText).to.be('Ingest Node Pipelines'); + }); + }); +}; diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index be7a2faae6711..082008bccddd1 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -26,12 +26,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); - async function assertExpectedMetric() { + async function assertExpectedMetric(metricCount: string = '19,986') { await PageObjects.lens.assertExactText( '[data-test-subj="lns_metric_title"]', 'Maximum of bytes' ); - await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', '19,986'); + await PageObjects.lens.assertExactText('[data-test-subj="lns_metric_value"]', metricCount); } async function assertExpectedTable() { @@ -40,8 +40,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 'Maximum of bytes' ); await PageObjects.lens.assertExactText( - '[data-test-subj="lnsDataTable"] tbody .euiTableCellContent__text', - '19,986' + '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValue"]', + '19,985' + ); + await PageObjects.lens.assertExactText( + '[data-test-subj="lnsDataTable"] [data-test-subj="lnsDataTableCellValueFilterable"]', + 'IN' ); } @@ -86,7 +90,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await assertExpectedMetric(); }); - it('click on the bar in XYChart adds proper filters/timerange', async () => { + it('click on the bar in XYChart adds proper filters/timerange in dashboard', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await dashboardAddPanel.clickOpenAddPanel(); @@ -102,15 +106,22 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { expect(hasIpFilter).to.be(true); }); - it('should allow seamless transition to and from table view', async () => { + it('should allow seamless transition to and from table view and add a filter', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); await PageObjects.lens.goToTimeRange(); await assertExpectedMetric(); await PageObjects.lens.switchToVisualization('lnsChartSwitchPopover_lnsDatatable'); + await PageObjects.lens.configureDimension({ + dimension: '[data-test-subj="lnsDatatable_column"] [data-test-subj="lns-empty-dimension"]', + operation: 'terms', + field: 'geo.dest', + }); + await PageObjects.lens.save('Artistpreviouslyknownaslens'); + await find.clickByCssSelector('[data-test-subj="lensDatatableFilterOut"]'); await assertExpectedTable(); await PageObjects.lens.switchToVisualization('lnsChartSwitchPopover_lnsMetric'); - await assertExpectedMetric(); + await assertExpectedMetric('19,985'); }); it('should allow creation of lens visualizations', async () => { diff --git a/x-pack/test/functional/apps/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index ee710b00b0be8..0ca09aa317bc0 100644 --- a/x-pack/test/functional/apps/logstash/index.js +++ b/x-pack/test/functional/apps/logstash/index.js @@ -6,7 +6,7 @@ export default function({ loadTestFile }) { describe('logstash', function() { - this.tags(['ciGroup2', 'smoke']); + this.tags(['ciGroup2']); loadTestFile(require.resolve('./pipeline_list')); loadTestFile(require.resolve('./pipeline_create')); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts index a5faf325aa6cb..a9133bb380179 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts @@ -195,7 +195,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '10.0 MB', total_by_field_count: '37', total_over_field_count: '92', total_partition_field_count: '8', @@ -262,7 +261,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '100.0 MB', total_by_field_count: '994', total_over_field_count: '0', total_partition_field_count: '2', @@ -277,7 +275,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('advanced job', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); @@ -549,7 +547,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobId, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: @@ -813,7 +810,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobIdClone, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts index 8827559a5f470..ae88208fa9a11 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts @@ -57,7 +57,7 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('anomaly explorer', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts index 9b5ae171d4115..b8ac646c35875 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts @@ -64,7 +64,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '15.0 MB', total_by_field_count: '30', total_over_field_count: '0', total_partition_field_count: '2', @@ -77,7 +76,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('categorization', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/categorization'); await ml.testResources.createIndexPatternIfNeeded('ft_categorization', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts index 570deee01c684..d3934d674124e 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts @@ -153,7 +153,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '10.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -166,7 +165,7 @@ export default function({ getService }: FtrProviderContext) { ]; describe('job on data set with date_nanos time field', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/event_rate_nanos'); await ml.testResources.createIndexPatternIfNeeded( @@ -422,7 +421,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobId, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index 4739f987541d6..f6a9c96492f39 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -61,7 +61,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '59', total_over_field_count: '0', total_partition_field_count: '58', @@ -74,7 +73,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('multi metric', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index 0279c70bb73a9..547c489411b5f 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -75,7 +75,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '8.0 MB', total_by_field_count: '25', total_over_field_count: '92', total_partition_field_count: '3', @@ -88,7 +87,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('population', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index a5652d76358eb..b1fee1633641a 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -54,7 +54,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -105,7 +104,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -156,7 +154,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -208,7 +205,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -259,7 +255,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -272,7 +267,7 @@ export default function({ getService }: FtrProviderContext) { ]; describe('saved search', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); @@ -464,7 +459,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobId, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index 43053decb3924..0f8655e3c6bbc 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -60,7 +60,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '15.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -73,7 +72,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('single metric', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts index cc7c9828ce87d..9e1829998bec4 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts @@ -40,7 +40,7 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('single metric viewer', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts index 8a6741bd88daa..eb57a743f14cb 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts @@ -12,7 +12,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('classification creation', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index d98d8feaaf4fe..93f225989592e 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -14,8 +14,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('jobs cloning supported by UI form', function() { - this.tags(['smoke']); - const testDataList: Array<{ suiteTitle: string; archive: string; diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts index 8dfe058cf6885..4e8a9000598b3 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts @@ -12,7 +12,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('outlier detection creation', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/ihp_outlier'); await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts index 271f3e2018dad..c3718e421d451 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts @@ -12,7 +12,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('regression creation', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts index ae958ad7f570f..868e75228f1cc 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts @@ -34,7 +34,7 @@ export default function({ getService }: FtrProviderContext) { ]; describe('file based', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await ml.testResources.setKibanaTimeZoneToUTC(); diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts index e71b57a4562e7..e4e0c1c92d73a 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts @@ -376,7 +376,7 @@ export default function({ getService }: FtrProviderContext) { } describe('index based', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/pages.ts b/x-pack/test/functional/apps/machine_learning/pages.ts index 95930f18061fa..35536b4eeda12 100644 --- a/x-pack/test/functional/apps/machine_learning/pages.ts +++ b/x-pack/test/functional/apps/machine_learning/pages.ts @@ -10,7 +10,7 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('page navigation', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['skipFirefox', 'mlqa']); before(async () => { await ml.api.cleanMlIndices(); await ml.securityUI.loginAsMlPowerUser(); diff --git a/x-pack/test/functional/apps/remote_clusters/home_page.ts b/x-pack/test/functional/apps/remote_clusters/home_page.ts index 879a0a11ede78..394e48404b927 100644 --- a/x-pack/test/functional/apps/remote_clusters/home_page.ts +++ b/x-pack/test/functional/apps/remote_clusters/home_page.ts @@ -11,7 +11,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'remoteClusters']); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('remoteClusters'); }); diff --git a/x-pack/test/functional/apps/reporting_management/index.ts b/x-pack/test/functional/apps/reporting_management/index.ts new file mode 100644 index 0000000000000..07306d8fb2956 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('reporting management app', function() { + this.tags('ciGroup7'); + loadTestFile(require.resolve('./report_delete_pagination')); + }); +}; diff --git a/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts b/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts new file mode 100644 index 0000000000000..5b2edad9e6d95 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts @@ -0,0 +1,59 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'reporting']); + const log = getService('log'); + const retry = getService('retry'); + const security = getService('security'); + + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + + describe('Delete reports', function() { + before(async () => { + await security.testUser.setRoles(['global_discover_read', 'reporting_user']); + await esArchiver.load('empty_kibana'); + await esArchiver.load('reporting/archived_reports'); + await pageObjects.common.navigateToActualUrl('kibana', '/management/kibana/reporting'); + await testSubjects.existOrFail('reportJobListing', { timeout: 200000 }); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await esArchiver.unload('reporting/archived_reports'); + await security.testUser.restoreDefaults(); + }); + + it('Confirm single report deletion works', async () => { + log.debug('Checking for reports.'); + await retry.try(async () => { + await testSubjects.click('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + }); + const deleteButton = await testSubjects.find('deleteReportButton'); + await retry.waitFor('delete button to become enabled', async () => { + return await deleteButton.isEnabled(); + }); + await deleteButton.click(); + await testSubjects.exists('confirmModalBodyText'); + await testSubjects.click('confirmModalConfirmButton'); + await retry.try(async () => { + await testSubjects.waitForDeleted('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + }); + }); + + // functional test for report pagination: https://github.com/elastic/kibana/pull/62881 + it('Report pagination', async () => { + const previousButton = await testSubjects.find('pagination-button-previous'); + expect(await previousButton.getAttribute('disabled')).to.be('true'); + await testSubjects.click('pagination-button-1'); + expect(await previousButton.getAttribute('disabled')).to.be(null); + }); + }); +}; diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 8ab84126b2b30..3bcf504c17a6f 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -19,7 +19,8 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'header']); - describe('Management', function() { + // FLAKY: https://github.com/elastic/kibana/issues/61173 + describe.skip('Management', function() { this.tags(['skipFirefox']); before(async () => { diff --git a/x-pack/test/functional/apps/security/security.ts b/x-pack/test/functional/apps/security/security.ts index 2096a7755e01d..37516acec7c4b 100644 --- a/x-pack/test/functional/apps/security/security.ts +++ b/x-pack/test/functional/apps/security/security.ts @@ -16,7 +16,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const spaces = getService('spaces'); describe('Security', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('Login Page', () => { before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index a007c40a06b62..b4adfe1ce2b66 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -12,7 +12,6 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); describe('useremail', function() { - this.tags('smoke'); before(async () => { await esArchiver.load('security/discover'); await PageObjects.settings.navigateTo(); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index f49a74a661a63..04d59334a01c4 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -12,7 +12,6 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); describe('users', function() { - this.tags('smoke'); before(async () => { log.debug('users'); await PageObjects.settings.navigateTo(); diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index 608c7f321a08f..56ebee79d06ff 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -13,7 +13,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('legacyEs'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('snapshotRestore'); }); diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 38220c15cb266..d45b8a1ea4cdb 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -13,7 +13,7 @@ export default function enterSpaceFunctonalTests({ const PageObjects = getPageObjects(['security', 'spaceSelector']); describe('Enter Space', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('spaces/enter_space'); await PageObjects.security.forceLogout(); diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index 7b4a1e6e2b8a0..b88311597d765 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -21,7 +21,7 @@ export default function spaceSelectorFunctonalTests({ ]); describe('Spaces', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('Space Selector', () => { before(async () => { await esArchiver.load('spaces/selector'); diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index 58551aaaf4112..fa5a0c0aa0402 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -13,7 +13,7 @@ export default function statusPageFunctonalTests({ const PageObjects = getPageObjects(['security', 'statusPage', 'home']); describe('Status Page', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index e6e12f60f0bcc..4f71d81eacdf8 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -27,7 +27,6 @@ export default function({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('cloning', function() { - this.tags(['smoke']); const transformConfig = getTransformConfig(); before(async () => { diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index bea6b814ee8a3..0e61635fb70e4 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -18,7 +18,6 @@ export default function({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('creation_index_pattern', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); @@ -90,7 +89,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -144,7 +143,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -180,14 +179,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the source index preview', async () => { - await transform.wizard.assertSourceIndexPreview( - testData.expected.sourcePreview.columns, - testData.expected.sourcePreview.rows + it('shows the index preview', async () => { + await transform.wizard.assertIndexPreview( + testData.expected.indexPreview.columns, + testData.expected.indexPreview.rows ); }); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 993bd3a79abbc..26aec913e4756 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -18,7 +18,6 @@ export default function({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('creation_saved_search', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await transform.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); @@ -65,7 +64,7 @@ export default function({ getService }: FtrProviderContext) { progress: '100', }, sourceIndex: 'ft_farequote', - sourcePreview: { + indexPreview: { column: 2, values: ['ASA'], }, @@ -101,14 +100,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the filtered source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewColumnValues( - testData.expected.sourcePreview.column, - testData.expected.sourcePreview.values + it('shows the filtered index preview', async () => { + await transform.wizard.assertIndexPreviewColumnValues( + testData.expected.indexPreview.column, + testData.expected.indexPreview.values ); }); diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts index 42118f4c63b1f..d2210bee0063d 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts @@ -14,7 +14,7 @@ export default function upgradeAssistantFunctionalTests({ const PageObjects = getPageObjects(['upgradeAssistant']); describe('Upgrade Checkup', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => await esArchiver.load('empty_kibana')); after(async () => { await PageObjects.upgradeAssistant.expectTelemetryHasFinish(); diff --git a/x-pack/test/functional/apps/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts new file mode 100644 index 0000000000000..05967e0f3acaf --- /dev/null +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -0,0 +1,62 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { makeCheck } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; +import { getSha256 } from '../../../api_integration/apis/uptime/rest/helper/make_tls'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const { uptime } = getPageObjects(['uptime']); + const uptimeService = getService('uptime'); + + const es = getService('es'); + + describe('certificate page', function() { + before(async () => { + await uptime.goToRoot(true); + }); + + beforeEach(async () => { + await makeCheck({ es, tls: true }); + await uptimeService.navigation.refreshApp(); + }); + + it('can navigate to cert page', async () => { + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.hasViewCertButton(); + await uptimeService.navigation.goToCertificates(); + }); + + it('displays certificates', async () => { + await uptimeService.cert.hasCertificates(); + }); + + it('displays specific certificates', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + tls: { + sha256: certId, + }, + }); + + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.certificateExists({ certId, monitorId }); + }); + + it('performs search against monitor id', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + tls: { + sha256: certId, + }, + }); + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.searchIsWorking(monitorId); + }); + }); +}; diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index f47214dc2ad2f..6ecd39f696312 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -53,6 +53,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./locations')); loadTestFile(require.resolve('./settings')); + loadTestFile(require.resolve('./certificates')); }); describe('with real-world data', () => { before(async () => { diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index e81bbc5ae42f9..7a813a5cdfb52 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -6,10 +6,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { - defaultDynamicSettings, - DynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../plugins/uptime/common/constants'; import { makeChecks } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; export default ({ getPageObjects, getService }: FtrProviderContext) => { @@ -32,7 +30,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await settings.go(); const fields = await settings.loadFields(); - expect(fields).to.eql(defaultDynamicSettings); + expect(fields).to.eql(DYNAMIC_SETTINGS_DEFAULTS); }); it('should disable the apply button when invalid or unchanged', async () => { @@ -62,7 +60,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await settings.go(); - const newFieldValues: DynamicSettings = { heartbeatIndices: 'new*' }; + const newFieldValues: DynamicSettings = { + heartbeatIndices: 'new*', + certThresholds: { + age: 365, + expiration: 30, + }, + }; await settings.changeHeartbeatIndicesInput(newFieldValues.heartbeatIndices); await settings.apply(); @@ -91,7 +95,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields.certificatesThresholds.errorState).to.eql(newErrorThreshold); + expect(fields.certThresholds?.expiration).to.eql(newErrorThreshold); }); it('changing certificate expiration warning threshold is reflected in settings page', async () => { @@ -108,7 +112,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields.certificatesThresholds.warningState).to.eql(newWarningThreshold); + expect(fields.certThresholds?.age).to.eql(newWarningThreshold); }); }); }; diff --git a/x-pack/test/functional/apps/visualize/index.ts b/x-pack/test/functional/apps/visualize/index.ts index f30367ba3dd0b..1c23b8cde8606 100644 --- a/x-pack/test/functional/apps/visualize/index.ts +++ b/x-pack/test/functional/apps/visualize/index.ts @@ -15,5 +15,6 @@ export default function visualize({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./hybrid_visualization')); loadTestFile(require.resolve('./precalculated_histogram')); loadTestFile(require.resolve('./preserve_url')); + loadTestFile(require.resolve('./reporting')); }); } diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts new file mode 100644 index 0000000000000..5ef954e334d81 --- /dev/null +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const PageObjects = getPageObjects([ + 'reporting', + 'common', + 'dashboard', + 'visualize', + 'visEditor', + ]); + + describe('Visualize', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + describe('Print PDF button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToUrl('visualize', 'new'); + await PageObjects.visualize.clickAreaChart(); + await PageObjects.visualize.clickNewSearch('ecommerce'); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.reporting.setTimepickerInDataRange(); + await PageObjects.visEditor.clickBucket('X-axis'); + await PageObjects.visEditor.selectAggregation('Date Histogram'); + await PageObjects.visEditor.clickGo(); + await PageObjects.visualize.saveVisualization('my viz'); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + + it('downloaded PDF has OK status', async function() { + // Generating and then comparing reports can take longer than the default 60s timeout + this.timeout(180000); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/pdf'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/watcher/index.js b/x-pack/test/functional/apps/watcher/index.js index 9f0d5a5de405a..e894a890fdb50 100644 --- a/x-pack/test/functional/apps/watcher/index.js +++ b/x-pack/test/functional/apps/watcher/index.js @@ -6,7 +6,7 @@ export default function({ loadTestFile }) { describe('watcher app', function() { - this.tags(['ciGroup1', 'smoke']); + this.tags(['ciGroup1', 'includeFirefox']); //loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./watcher_test')); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index f26110513a9b3..f6b80b1b9fc67 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -53,10 +53,13 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/index_patterns'), resolve(__dirname, './apps/index_management'), resolve(__dirname, './apps/index_lifecycle_management'), + resolve(__dirname, './apps/ingest_pipelines'), resolve(__dirname, './apps/snapshot_restore'), resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), resolve(__dirname, './apps/transform'), + resolve(__dirname, './apps/reporting_management'), + // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), ], @@ -173,6 +176,10 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana', hash: '/management/elasticsearch/index_lifecycle_management', }, + ingestPipelines: { + pathname: '/app/kibana', + hash: '/management/elasticsearch/ingest_pipelines', + }, snapshotRestore: { pathname: '/app/kibana', hash: '/management/elasticsearch/snapshot_restore', @@ -196,6 +203,10 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana/', hash: '/management/elasticsearch/transform', }, + reporting: { + pathname: '/app/kibana/', + hash: '/management/kibana/reporting', + }, }, // choose where esArchiver should load archives from @@ -228,6 +239,17 @@ export default async function({ readConfigFile }) { kibana: [], }, + global_discover_read: { + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }, + //Kibana feature privilege isn't specific to advancedSetting. It can be anything. https://github.com/elastic/kibana/issues/35965 test_api_keys: { elasticsearch: { diff --git a/x-pack/test/functional/es_archives/empty_kibana/data.json.gz b/x-pack/test/functional/es_archives/empty_kibana/data.json.gz deleted file mode 100644 index 8334749a696d7..0000000000000 Binary files a/x-pack/test/functional/es_archives/empty_kibana/data.json.gz and /dev/null differ diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz index a71281c0ecfec..3d4f0e11a7cc6 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz differ 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 1ffb119ca1023..d22e3cd3fecdd 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -1,18 +1,18 @@ { "type": "doc", "value": { - "id": "agents:agent1", + "id": "fleet-agents:agent1", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-2", "active": true, "shared_id": "agent1_filebeat", "config_id": "1", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -21,17 +21,17 @@ { "type": "doc", "value": { - "id": "agents:agent2", + "id": "fleet-agents:agent2", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-2", "active": true, "shared_id": "agent2_filebeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -40,17 +40,17 @@ { "type": "doc", "value": { - "id": "agents:agent3", + "id": "fleet-agents:agent3", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-3", "active": true, "shared_id": "agent3_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -59,17 +59,17 @@ { "type": "doc", "value": { - "id": "agents:agent4", + "id": "fleet-agents:agent4", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-4", "active": true, "shared_id": "agent4_metricbeat", "type": "PERMANENT", - "local_metadata": "{}", - "user_provided_metadata": "{}" + "local_metadata": {}, + "user_provided_metadata": {} } } } @@ -78,17 +78,17 @@ { "type": "doc", "value": { - "id": "enrollment_api_keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0", + "id": "fleet-enrollment-api-keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0", "index": ".kibana", "source": { - "enrollment_api_keys" : { + "fleet-enrollment-api-keys" : { "created_at" : "2019-10-10T16:31:12.518Z", "name": "FleetEnrollmentKey:1", "api_key_id" : "key", "config_id" : "policy:1", "active" : true }, - "type" : "enrollment_api_keys", + "type" : "fleet-enrollment-api-keys", "references": [] } } @@ -97,11 +97,11 @@ { "type": "doc", "value": { - "id": "events:event1", + "id": "fleet-agent-events:event1", "index": ".kibana", "source": { - "type": "agent_events", - "agent_events": { + "type": "fleet-agent-events", + "fleet-agent-events": { "agent_id": "agent1", "type": "STATE", "subtype": "STARTED", @@ -116,11 +116,11 @@ { "type": "doc", "value": { - "id": "events:event2", + "id": "fleet-agent-events:event2", "index": ".kibana", "source": { - "type": "agent_events", - "agent_events": { + "type": "fleet-agent-events", + "fleet-agent-events": { "agent_id": "agent1", "type": "STATE", "subtype": "STOPPED", @@ -135,11 +135,11 @@ { "type": "doc", "value": { - "id": "agent_actions:37ed51ff-e80f-4f2a-a62d-f4fa975e7d85", + "id": "fleet-agent-actions:37ed51ff-e80f-4f2a-a62d-f4fa975e7d85", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "created_at": "2019-09-04T15:04:07+0000", "type": "RESUME", @@ -152,11 +152,11 @@ { "type": "doc", "value": { - "id": "agent_actions:b400439c-bbbf-43d5-83cb-cf8b7e32506f", + "id": "fleet-agent-actions:b400439c-bbbf-43d5-83cb-cf8b7e32506f", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "type": "PAUSE", "created_at": "2019-09-04T15:01:07+0000", @@ -169,11 +169,11 @@ { "type": "doc", "value": { - "id": "agent_actions:48cebde1-c906-4893-b89f-595d943b72a1", + "id": "fleet-agent-actions:48cebde1-c906-4893-b89f-595d943b72a1", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "type": "CONFIG_CHANGE", "created_at": "2020-03-15T03:47:15.129Z", @@ -186,11 +186,11 @@ { "type": "doc", "value": { - "id": "agent_actions:48cebde1-c906-4893-b89f-595d943b72a2", + "id": "fleet-agent-actions:48cebde1-c906-4893-b89f-595d943b72a2", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "type": "CONFIG_CHANGE", "created_at": "2020-03-15T03:47:15.129Z", 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 31ae161049303..409cc3c689eaf 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -9,7 +9,7 @@ "dynamic": "strict", "_meta": { "migrationMappingPropertyHashes": { - "outputs": "aee9782e0d500b867859650a36280165", + "ingest-outputs": "aee9782e0d500b867859650a36280165", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", "visualization": "52d7a13ad68a150c4525b292d23e12cc", "references": "7997cf5a56cc02bdc9c93361bde732b0", @@ -23,14 +23,14 @@ "dashboard": "d00f614b29a80360e1190193fd333bab", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "siem-detection-engine-rule-actions": "90eee2e4635260f4be0a1da8f5bc0aa0", - "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", "inventory-view": "9ecce5b58867403613d82fe496470b34", - "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", "cases-comments": "c2061fb929f585df57425102fa928b4b", "canvas-element": "7390014e1091044523666d97247392fc", @@ -50,20 +50,20 @@ "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", "map": "23d7aa4a720d4938ccde3983f87bd58d", "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", - "epm-package": "75d12cd13c867fd713d7dfb27366bc20", + "epm-packages": "75d12cd13c867fd713d7dfb27366bc20", "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", "cases": "08b8b110dbca273d37e8aef131ecab61", "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", "url": "c7f66a0df8b1b52f17c28c4adb111105", - "agents": "c3eeb7b9d97176f15f6d126370ab23c7", + "fleet-agents": "c3eeb7b9d97176f15f6d126370ab23c7", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "index-pattern": "66eccb05066c5a89924f48a9e9736499", "maps-telemetry": "268da3a48066123fc5baf35abaa55014", "namespace": "2f4316de49999235636386fe51dc06c1", "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "agent_actions": "ed270b46812f0fa1439366c428a2cf17", + "fleet-agent-actions": "ed270b46812f0fa1439366c428a2cf17", "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", "config": "ae24d22d5986d04124cc6568f771066f", @@ -107,7 +107,7 @@ } } }, - "agent_actions": { + "fleet-agent-actions": { "properties": { "agent_id": { "type": "keyword" @@ -160,7 +160,7 @@ } } }, - "agent_events": { + "fleet-agent-events": { "properties": { "action_id": { "type": "keyword" @@ -194,7 +194,7 @@ } } }, - "agents": { + "fleet-agents": { "properties": { "access_api_key_id": { "type": "keyword" @@ -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" @@ -1705,7 +1705,7 @@ } } }, - "enrollment_api_keys": { + "fleet-enrollment-api-keys": { "properties": { "active": { "type": "boolean" @@ -1736,7 +1736,7 @@ } } }, - "epm-package": { + "epm-packages": { "properties": { "installed": { "type": "nested", @@ -2211,7 +2211,7 @@ "namespace": { "type": "keyword" }, - "outputs": { + "ingest-outputs": { "properties": { "api_key": { "type": "keyword" diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz new file mode 100644 index 0000000000000..34a30bd84a592 Binary files /dev/null and b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json new file mode 100644 index 0000000000000..20f8f840ee863 --- /dev/null +++ b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json @@ -0,0 +1,108 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": ".reporting-2020.04.19", + "mappings": { + "properties": { + "attempts": { + "type": "short" + }, + "browser_type": { + "type": "keyword" + }, + "completed_at": { + "type": "date" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "jobtype": { + "type": "keyword" + }, + "kibana_id": { + "type": "keyword" + }, + "kibana_name": { + "type": "keyword" + }, + "max_attempts": { + "type": "short" + }, + "meta": { + "properties": { + "layout": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "objectType": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "output": { + "properties": { + "content": { + "enabled": false, + "type": "object" + }, + "content_type": { + "type": "keyword" + }, + "csv_contains_formulas": { + "type": "boolean" + }, + "max_size_reached": { + "type": "boolean" + }, + "size": { + "type": "long" + } + } + }, + "payload": { + "enabled": false, + "type": "object" + }, + "priority": { + "type": "byte" + }, + "process_expiration": { + "type": "date" + }, + "started_at": { + "type": "date" + }, + "status": { + "type": "keyword" + }, + "timeout": { + "type": "long" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "prefer_v2_templates": "false" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/uptime/blank/mappings.json b/x-pack/test/functional/es_archives/uptime/blank/mappings.json index fff4ef47bce0c..dd7f5cb9aa778 100644 --- a/x-pack/test/functional/es_archives/uptime/blank/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/blank/mappings.json @@ -1665,6 +1665,13 @@ }, "id": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "ip": { @@ -1672,6 +1679,13 @@ }, "name": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "status": { @@ -3079,10 +3093,21 @@ }, "x509": { "properties": { + "alternative_names": { + "type": "keyword", + "ignore_above": 1024 + }, "issuer": { "properties": { "common_name": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "distinguished_name": { @@ -3092,14 +3117,16 @@ } }, "not_after": { - "type": "keyword", - "ignore_above": 1024 + "type": "date" }, "not_before": { + "type": "date" + }, + "public_key_algorithm": { "type": "keyword", "ignore_above": 1024 }, - "public_key_algorithm": { + "public_key_curve": { "type": "keyword", "ignore_above": 1024 }, @@ -3121,6 +3148,13 @@ "properties": { "common_name": { "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, "ignore_above": 1024 }, "distinguished_name": { @@ -3128,6 +3162,10 @@ "ignore_above": 1024 } } + }, + "version_number": { + "type": "keyword", + "ignore_above": 1024 } } } diff --git a/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json b/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json index 2b6002ddb3fab..97b72510da286 100644 --- a/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json +++ b/x-pack/test/functional/es_archives/uptime/full_heartbeat/mappings.json @@ -12,79 +12,115 @@ "beat": "heartbeat", "version": "8.0.0" }, - "date_detection": false, "dynamic_templates": [ { "labels": { + "path_match": "labels.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" + } } }, { "container.labels": { + "path_match": "container.labels.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" + } } }, { "dns.answers": { + "path_match": "dns.answers.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "log.syslog": { + "path_match": "log.syslog.*", "match_mapping_type": "string", - "path_match": "dns.answers.*" + "mapping": { + "type": "keyword" + } } }, { - "fields": { + "network.inner": { + "path_match": "network.inner.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "observer.egress": { + "path_match": "observer.egress.*", "match_mapping_type": "string", - "path_match": "fields.*" + "mapping": { + "type": "keyword" + } } }, { - "docker.container.labels": { + "observer.ingress": { + "path_match": "observer.ingress.*", + "match_mapping_type": "string", "mapping": { "type": "keyword" - }, + } + } + }, + { + "fields": { + "path_match": "fields.*", + "match_mapping_type": "string", + "mapping": { + "type": "keyword" + } + } + }, + { + "docker.container.labels": { + "path_match": "docker.container.labels.*", "match_mapping_type": "string", - "path_match": "docker.container.labels.*" + "mapping": { + "type": "keyword" + } } }, { "kubernetes.labels.*": { + "path_match": "kubernetes.labels.*", "mapping": { "type": "keyword" - }, - "path_match": "kubernetes.labels.*" + } } }, { "kubernetes.annotations.*": { + "path_match": "kubernetes.annotations.*", "mapping": { "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" + } } }, { "strings_as_keyword": { + "match_mapping_type": "string", "mapping": { "ignore_above": 1024, "type": "keyword" - }, - "match_mapping_type": "string" + } } } ], + "date_detection": false, "properties": { "@timestamp": { "type": "date" @@ -92,28 +128,28 @@ "agent": { "properties": { "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -125,8 +161,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -135,8 +177,8 @@ "client": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -146,8 +188,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -157,41 +205,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -199,8 +247,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -218,43 +266,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -265,76 +337,97 @@ "account": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "availability_zone": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "image": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "instance": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "machine": { "properties": { "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "project": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "provider": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" } } }, "container": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "image": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "tag": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -342,20 +435,20 @@ "type": "object" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "runtime": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "destination": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -365,8 +458,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -376,41 +475,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -418,8 +517,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -437,43 +536,144 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 } } } @@ -484,55 +684,63 @@ "answers": { "properties": { "class": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "data": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "ttl": { "type": "long" }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "header_flags": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "op_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "question": { "properties": { "class": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "registered_domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "subdomain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -540,12 +748,12 @@ "type": "ip" }, "response_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -563,51 +771,61 @@ "ecs": { "properties": { "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "error": { "properties": { "code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "message": { - "norms": false, - "type": "text" + "type": "text", + "norms": false + }, + "stack_trace": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "event": { "properties": { "action": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "category": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "created": { "type": "date" }, "dataset": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "duration": { "type": "long" @@ -616,32 +834,39 @@ "type": "date" }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "ingested": { + "type": "date" }, "kind": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "module": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "outcome": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "provider": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 }, "risk_score": { "type": "float" @@ -659,12 +884,16 @@ "type": "date" }, "timezone": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "url": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -676,6 +905,31 @@ "accessed": { "type": "date" }, + "attributes": { + "type": "keyword", + "ignore_above": 1024 + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, "created": { "type": "date" }, @@ -683,254 +937,318 @@ "type": "date" }, "device": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "directory": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "drive_letter": { + "type": "keyword", + "ignore_above": 1 }, "extension": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "gid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "group": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "hash": { "properties": { "md5": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha1": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha256": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha512": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "inode": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "mime_type": { + "type": "keyword", + "ignore_above": 1024 }, "mode": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "mtime": { "type": "date" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "owner": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "size": { "type": "long" }, "target_path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { "properties": { "md5": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha1": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha256": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "sha512": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "host": { "properties": { "architecture": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "containerized": { "type": "boolean" }, + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "ip": { "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "os": { "properties": { "build": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "codename": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uptime": { "type": "long" @@ -938,40 +1256,56 @@ "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -987,8 +1321,14 @@ "type": "long" }, "content": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, @@ -996,12 +1336,12 @@ "type": "long" }, "method": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "referrer": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1013,18 +1353,28 @@ "type": "long" }, "content": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "bytes": { "type": "long" }, + "redirects": { + "type": "keyword", + "ignore_above": 1024 + }, "status_code": { "type": "long" } @@ -1077,8 +1427,8 @@ } }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1096,17 +1446,33 @@ } } }, + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "jolokia": { "properties": { "agent": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1116,22 +1482,22 @@ "server": { "properties": { "product": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "vendor": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "url": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1147,20 +1513,20 @@ "container": { "properties": { "image": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "deployment": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1172,42 +1538,42 @@ } }, "namespace": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "node": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "pod": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "uid": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "replicaset": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "statefulset": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } } @@ -1219,28 +1585,76 @@ "log": { "properties": { "level": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "logger": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "origin": { + "properties": { + "file": { + "properties": { + "line": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "function": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "syslog": { + "properties": { + "facility": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "priority": { + "type": "long" + }, + "severity": { + "properties": { + "code": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } } } }, "message": { - "norms": false, - "type": "text" + "type": "text", + "norms": false }, "monitor": { "properties": { "check_group": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "duration": { "properties": { @@ -1250,238 +1664,683 @@ } }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 }, "ip": { "type": "ip" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 }, "status": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "timespan": { + "type": "date_range" }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "network": { "properties": { "application": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "bytes": { "type": "long" }, "community_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "direction": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "forwarded_ip": { "type": "ip" }, "iana_number": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "inner": { + "properties": { + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "packets": { "type": "long" }, "protocol": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "transport": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } } } }, "observer": { "properties": { + "egress": { + "properties": { + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "zone": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hostname": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "ingress": { + "properties": { + "interface": { + "properties": { + "alias": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "zone": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "ip": { "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, + "product": { + "type": "keyword", + "ignore_above": 1024 + }, "serial_number": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "vendor": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "organization": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "package": { + "properties": { + "architecture": { + "type": "keyword", + "ignore_above": 1024 + }, + "build_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "checksum": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "install_scope": { + "type": "keyword", + "ignore_above": 1024 + }, + "installed": { + "type": "date" + }, + "license": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "size": { + "type": "long" + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 } } }, "process": { "properties": { "args": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, - "executable": { - "ignore_above": 1024, - "type": "keyword" + "args_count": { + "type": "long" }, - "hash": { + "code_signature": { "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" + "exists": { + "type": "boolean" }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" + "status": { + "type": "keyword", + "ignore_above": 1024 }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" + "subject_name": { + "type": "keyword", + "ignore_above": 1024 }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "executable": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 } } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "parent": { + "properties": { + "args": { + "type": "keyword", + "ignore_above": 1024 + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "entity_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "executable": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha512": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "title": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + }, + "pe": { + "properties": { + "company": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "file_version": { + "type": "keyword", + "ignore_above": 1024 + }, + "original_file_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "product": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "pgid": { "type": "long" @@ -1501,28 +2360,84 @@ "type": "long" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "title": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "uptime": { "type": "long" }, "working_directory": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "type": "keyword", + "ignore_above": 1024 + }, + "strings": { + "type": "keyword", + "ignore_above": 1024 + }, + "type": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "hive": { + "type": "keyword", + "ignore_above": 1024 + }, + "key": { + "type": "keyword", + "ignore_above": 1024 + }, + "path": { + "type": "keyword", + "ignore_above": 1024 + }, + "value": { + "type": "keyword", + "ignore_above": 1024 } } }, "related": { "properties": { + "hash": { + "type": "keyword", + "ignore_above": 1024 + }, "ip": { "type": "ip" + }, + "user": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1540,11 +2455,55 @@ } } }, + "rule": { + "properties": { + "author": { + "type": "keyword", + "ignore_above": 1024 + }, + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "license": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "ruleset": { + "type": "keyword", + "ignore_above": 1024 + }, + "uuid": { + "type": "keyword", + "ignore_above": 1024 + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, "server": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -1554,8 +2513,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1565,41 +2530,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1607,8 +2572,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -1626,43 +2591,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1671,28 +2660,36 @@ "service": { "properties": { "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "node": { + "properties": { + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } }, "state": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "type": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1714,8 +2711,8 @@ "source": { "properties": { "address": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "as": { "properties": { @@ -1725,8 +2722,14 @@ "organization": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1736,41 +2739,41 @@ "type": "long" }, "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "geo": { "properties": { "city_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "continent_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "country_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "location": { "type": "geo_point" }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "region_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1778,8 +2781,8 @@ "type": "ip" }, "mac": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "nat": { "properties": { @@ -1797,43 +2800,67 @@ "port": { "type": "long" }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 + }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } } @@ -1850,8 +2877,8 @@ } }, "tags": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "tcp": { "properties": { @@ -1875,11 +2902,57 @@ } } }, + "threat": { + "properties": { + "framework": { + "type": "keyword", + "ignore_above": 1024 + }, + "tactic": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "technique": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, "timeseries": { "properties": { "instance": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1891,6 +2964,78 @@ "certificate_not_valid_before": { "type": "date" }, + "cipher": { + "type": "keyword", + "ignore_above": 1024 + }, + "client": { + "properties": { + "certificate": { + "type": "keyword", + "ignore_above": 1024 + }, + "certificate_chain": { + "type": "keyword", + "ignore_above": 1024 + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "issuer": { + "type": "keyword", + "ignore_above": 1024 + }, + "ja3": { + "type": "keyword", + "ignore_above": 1024 + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "server_name": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject": { + "type": "keyword", + "ignore_above": 1024 + }, + "supported_ciphers": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "curve": { + "type": "keyword", + "ignore_above": 1024 + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "type": "keyword", + "ignore_above": 1024 + }, + "resumed": { + "type": "boolean" + }, "rtt": { "properties": { "handshake": { @@ -1901,6 +3046,138 @@ } } } + }, + "server": { + "properties": { + "certificate": { + "type": "keyword", + "ignore_above": 1024 + }, + "certificate_chain": { + "type": "keyword", + "ignore_above": 1024 + }, + "hash": { + "properties": { + "md5": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha1": { + "type": "keyword", + "ignore_above": 1024 + }, + "sha256": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "issuer": { + "type": "keyword", + "ignore_above": 1024 + }, + "ja3s": { + "type": "keyword", + "ignore_above": 1024 + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "subject": { + "type": "keyword", + "ignore_above": 1024 + }, + "x509": { + "properties": { + "alternative_names": { + "type": "keyword", + "ignore_above": 1024 + }, + "issuer": { + "properties": { + "common_name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 + }, + "distinguished_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "type": "keyword", + "ignore_above": 1024 + }, + "public_key_curve": { + "type": "keyword", + "ignore_above": 1024 + }, + "public_key_exponent": { + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "type": "keyword", + "ignore_above": 1024 + }, + "signature_algorithm": { + "type": "keyword", + "ignore_above": 1024 + }, + "subject": { + "properties": { + "common_name": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false, + "analyzer": "simple" + } + }, + "ignore_above": 1024 + }, + "distinguished_name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "version_number": { + "type": "keyword", + "ignore_above": 1024 + } + } + } + } + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + }, + "version_protocol": { + "type": "keyword", + "ignore_above": 1024 } } }, @@ -1909,16 +3186,16 @@ "trace": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "transaction": { "properties": { "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } } @@ -1927,83 +3204,123 @@ "url": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "extension": { + "type": "keyword", + "ignore_above": 1024 }, "fragment": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "password": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "path": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "port": { "type": "long" }, "query": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "registered_domain": { + "type": "keyword", + "ignore_above": 1024 }, "scheme": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + }, + "top_level_domain": { + "type": "keyword", + "ignore_above": 1024 }, "username": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "user": { "properties": { "domain": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "email": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full_name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "group": { "properties": { + "domain": { + "type": "keyword", + "ignore_above": 1024 + }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "hash": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "id": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 } } }, @@ -2012,50 +3329,147 @@ "device": { "properties": { "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "original": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "os": { "properties": { "family": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "full": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "kernel": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "name": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 }, "platform": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 } } }, "version": { - "ignore_above": 1024, - "type": "keyword" + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vlan": { + "properties": { + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "name": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "vulnerability": { + "properties": { + "category": { + "type": "keyword", + "ignore_above": 1024 + }, + "classification": { + "type": "keyword", + "ignore_above": 1024 + }, + "description": { + "type": "keyword", + "fields": { + "text": { + "type": "text", + "norms": false + } + }, + "ignore_above": 1024 + }, + "enumeration": { + "type": "keyword", + "ignore_above": 1024 + }, + "id": { + "type": "keyword", + "ignore_above": 1024 + }, + "reference": { + "type": "keyword", + "ignore_above": 1024 + }, + "report_id": { + "type": "keyword", + "ignore_above": 1024 + }, + "scanner": { + "properties": { + "vendor": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "score": { + "properties": { + "base": { + "type": "float" + }, + "environmental": { + "type": "float" + }, + "temporal": { + "type": "float" + }, + "version": { + "type": "keyword", + "ignore_above": 1024 + } + } + }, + "severity": { + "type": "keyword", + "ignore_above": 1024 } } } diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index de826097a5be6..ce36385a2f9df 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -51,8 +51,12 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { expect(disabledAttr).to.be('true'); }, - async openAddElementModal() { + async openSavedElementsModal() { await testSubjects.click('add-element-button'); + await testSubjects.click('saved-elements-menu-option'); + }, + async closeSavedElementsModal() { + await testSubjects.click('saved-elements-modal-close-button'); }, async expectAddElementButton() { @@ -61,14 +65,12 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { async expectNoAddElementButton() { // Ensure page is fully loaded first by waiting for the refresh button - const refreshPopoverExists = await find.existsByCssSelector('#auto-refresh-popover', 20000); + const refreshPopoverExists = await testSubjects.exists('canvas-refresh-control', { + timeout: 20000, + }); expect(refreshPopoverExists).to.be(true); - const addElementButtonExists = await find.existsByCssSelector( - 'button[data-test-subj=add-element-button]', - 10 // don't need much of a wait at all here, because we already waited for refresh button above - ); - expect(addElementButtonExists).to.be(false); + await testSubjects.missingOrFail('add-element-button'); }, }; } diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 782d57adea770..833cc452a5d31 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'; @@ -46,6 +45,7 @@ import { LensPageProvider } from './lens_page'; import { InfraMetricExplorerProvider } from './infra_metric_explorer'; import { RoleMappingsPageProvider } from './role_mappings_page'; import { SpaceSelectorPageProvider } from './space_selector_page'; +import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -79,4 +79,5 @@ export const pageObjects = { copySavedObjectsToSpace: CopySavedObjectsToSpacePageProvider, lens: LensPageProvider, roleMappings: RoleMappingsPageProvider, + ingestPipelines: IngestPipelinesPageProvider, }; diff --git a/x-pack/test/functional/page_objects/ingest_pipelines_page.ts b/x-pack/test/functional/page_objects/ingest_pipelines_page.ts new file mode 100644 index 0000000000000..abc85277a3617 --- /dev/null +++ b/x-pack/test/functional/page_objects/ingest_pipelines_page.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. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function IngestPipelinesPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async sectionHeadingText() { + return await testSubjects.getVisibleText('appTitle'); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 1bf637c50b0ba..57b2847cc2e50 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -12,6 +12,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont const testSubjects = getService('testSubjects'); const retry = getService('retry'); const find = getService('find'); + const comboBox = getService('comboBox'); const PageObjects = getPageObjects([ 'header', 'common', @@ -107,20 +108,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont * @param opts.operation - the desired operation ID for the dimension * @param opts.field - the desired field for the dimension */ - async configureDimension(opts: { dimension: string; operation?: string; field?: string }) { + async configureDimension(opts: { dimension: string; operation: string; field: string }) { await find.clickByCssSelector(opts.dimension); - if (opts.operation) { - await find.clickByCssSelector( - `[data-test-subj="lns-indexPatternDimensionIncompatible-${opts.operation}"], - [data-test-subj="lns-indexPatternDimension-${opts.operation}"]` - ); - } + await find.clickByCssSelector( + `[data-test-subj="lns-indexPatternDimensionIncompatible-${opts.operation}"], + [data-test-subj="lns-indexPatternDimension-${opts.operation}"]` + ); - if (opts.field) { - await testSubjects.click('indexPattern-dimension-field'); - await testSubjects.click(`lns-fieldOption-${opts.field}`); - } + const target = await testSubjects.find('indexPattern-dimension-field'); + await comboBox.openOptionsList(target); + await comboBox.setElement(target, opts.field); }, /** diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js deleted file mode 100644 index cdfafeec1bf46..0000000000000 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ /dev/null @@ -1,175 +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 { 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 }) { - const retry = getService('retry'); - const log = getService('log'); - const config = getService('config'); - const testSubjects = getService('testSubjects'); - const browser = getService('browser'); - const PageObjects = getPageObjects(['common', 'security', 'share', 'timePicker']); - - class ReportingPage { - async forceSharedItemsContainerSize({ width }) { - await browser.execute(` - var el = document.querySelector('[data-shared-items-container]'); - el.style.flex="none"; - el.style.width="${width}px"; - `); - } - - async getReportURL(timeout) { - log.debug('getReportURL'); - - const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout); - - log.debug(`getReportURL got url: ${url}`); - - return url; - } - - async removeForceSharedItemsContainerSize() { - await browser.execute(` - var el = document.querySelector('[data-shared-items-container]'); - el.style.flex = null; - el.style.width = null; - `); - } - - getResponse(url) { - log.debug(`getResponse for ${url}`); - const auth = config.get('servers.elasticsearch.auth'); - const headers = { - Authorization: `Basic ${Buffer.from(auth).toString('base64')}`, - }; - const parsedUrl = parse(url); - return new Promise((resolve, reject) => { - http - .get( - { - hostname: parsedUrl.hostname, - path: parsedUrl.path, - port: parsedUrl.port, - responseType: 'arraybuffer', - headers, - }, - res => { - resolve(res); - } - ) - .on('error', e => { - reject(e); - }); - }); - } - - async getRawPdfReportData(url) { - const data = []; // 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))); - }); - } - - async openCsvReportingPanel() { - log.debug('openCsvReportingPanel'); - await PageObjects.share.openShareMenuItem('CSV Reports'); - } - - async openPdfReportingPanel() { - log.debug('openPdfReportingPanel'); - await PageObjects.share.openShareMenuItem('PDF Reports'); - } - - async openPngReportingPanel() { - log.debug('openPngReportingPanel'); - await PageObjects.share.openShareMenuItem('PNG Reports'); - } - - async clearToastNotifications() { - const toasts = await testSubjects.findAll('toastCloseButton'); - await Promise.all(toasts.map(async t => await t.click())); - } - - async getQueueReportError() { - return await testSubjects.exists('queueReportError'); - } - - async getGenerateReportButton() { - return await retry.try(async () => await testSubjects.find('generateReportButton')); - } - - async isGenerateReportButtonDisabled() { - const generateReportButton = await this.getGenerateReportButton(); - return await retry.try(async () => { - const isDisabled = await generateReportButton.getAttribute('disabled'); - return isDisabled; - }); - } - - async canReportBeCreated() { - await this.clickGenerateReportButton(); - const success = await this.checkForReportingToasts(); - return success; - } - - async checkUsePrintLayout() { - // The print layout checkbox slides in as part of an animation, and tests can - // attempt to click it too quickly, leading to flaky tests. The 500ms wait allows - // the animation to complete before we attempt a click. - const menuAnimationDelay = 500; - await retry.tryForTime(menuAnimationDelay, () => testSubjects.click('usePrintLayout')); - } - - async clickGenerateReportButton() { - await testSubjects.click('generateReportButton'); - } - - async checkForReportingToasts() { - log.debug('Reporting:checkForReportingToasts'); - const isToastPresent = await testSubjects.exists('completeReportSuccess', { - allowHidden: true, - timeout: 90000, - }); - // Close toast so it doesn't obscure the UI. - if (isToastPresent) { - await testSubjects.click('completeReportSuccess > toastCloseButton'); - } - - return isToastPresent; - } - - async setTimepickerInDataRange() { - log.debug('Reporting:setTimepickerInDataRange'); - const fromTime = 'Sep 19, 2015 @ 06:31:44.000'; - const toTime = 'Sep 19, 2015 @ 18:01:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - } - - async setTimepickerInNoDataRange() { - log.debug('Reporting:setTimepickerInNoDataRange'); - const fromTime = 'Sep 19, 1999 @ 06:31:44.000'; - const toTime = 'Sep 23, 1999 @ 18:31:44.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - } - } - - return new ReportingPage(); -} diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts new file mode 100644 index 0000000000000..2c20519a8d214 --- /dev/null +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -0,0 +1,169 @@ +/* + * 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 http, { IncomingMessage } from 'http'; +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; +import { parse } from 'url'; + +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' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript + + class ReportingPage { + async forceSharedItemsContainerSize({ width }: { width: number }) { + await browser.execute(` + var el = document.querySelector('[data-shared-items-container]'); + el.style.flex="none"; + el.style.width="${width}px"; + `); + } + + async getReportURL(timeout: number) { + log.debug('getReportURL'); + + const url = await testSubjects.getAttribute('downloadCompletedReportButton', 'href', timeout); + + log.debug(`getReportURL got url: ${url}`); + + return url; + } + + async removeForceSharedItemsContainerSize() { + await browser.execute(` + var el = document.querySelector('[data-shared-items-container]'); + el.style.flex = null; + el.style.width = null; + `); + } + + getResponse(url: string): Promise<IncomingMessage> { + 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 = { + Authorization: `Basic ${Buffer.from(auth).toString('base64')}`, + }; + const parsedUrl = parse(url); + return new Promise((resolve, reject) => { + http + .get( + { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + port: parsedUrl.port, + headers, + }, + (res: IncomingMessage) => { + resolve(res); + } + ) + .on('error', (e: Error) => { + log.error(e); + reject(e); + }); + }); + } + + async getRawPdfReportData(url: string): Promise<Buffer> { + 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); + + if (response) { + response.on('data', (chunk: Buffer) => data.push(chunk)); + response.on('end', () => resolve(Buffer.concat(data))); + } + }); + } + + async openCsvReportingPanel() { + log.debug('openCsvReportingPanel'); + await PageObjects.share.openShareMenuItem('CSV Reports'); + } + + async openPdfReportingPanel() { + log.debug('openPdfReportingPanel'); + await PageObjects.share.openShareMenuItem('PDF Reports'); + } + + async openPngReportingPanel() { + log.debug('openPngReportingPanel'); + await PageObjects.share.openShareMenuItem('PNG Reports'); + } + + async clearToastNotifications() { + const toasts = await testSubjects.findAll('toastCloseButton'); + await Promise.all(toasts.map(async t => await t.click())); + } + + async getQueueReportError() { + return await testSubjects.exists('queueReportError'); + } + + async getGenerateReportButton() { + return await retry.try(async () => await testSubjects.find('generateReportButton')); + } + + async isGenerateReportButtonDisabled() { + const generateReportButton = await this.getGenerateReportButton(); + return await retry.try(async () => { + const isDisabled = await generateReportButton.getAttribute('disabled'); + return isDisabled; + }); + } + + async canReportBeCreated() { + await this.clickGenerateReportButton(); + const success = await this.checkForReportingToasts(); + return success; + } + + async checkUsePrintLayout() { + // The print layout checkbox slides in as part of an animation, and tests can + // attempt to click it too quickly, leading to flaky tests. The 500ms wait allows + // the animation to complete before we attempt a click. + const menuAnimationDelay = 500; + await retry.tryForTime(menuAnimationDelay, () => testSubjects.click('usePrintLayout')); + } + + async clickGenerateReportButton() { + await testSubjects.click('generateReportButton'); + } + + async checkForReportingToasts() { + log.debug('Reporting:checkForReportingToasts'); + const isToastPresent = await testSubjects.exists('completeReportSuccess', { + allowHidden: true, + timeout: 90000, + }); + // Close toast so it doesn't obscure the UI. + if (isToastPresent) { + await testSubjects.click('completeReportSuccess > toastCloseButton'); + } + + return isToastPresent; + } + + async setTimepickerInDataRange() { + log.debug('Reporting:setTimepickerInDataRange'); + const fromTime = 'Sep 19, 2015 @ 06:31:44.000'; + const toTime = 'Sep 19, 2015 @ 18:01:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + + async setTimepickerInNoDataRange() { + log.debug('Reporting:setTimepickerInNoDataRange'); + const fromTime = 'Sep 19, 1999 @ 06:31:44.000'; + const toTime = 'Sep 23, 1999 @ 18:31:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + } + return new ReportingPage(); +} diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index b399327012a77..84eb0cc378771 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -385,7 +385,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { // have to remove the '*' return find .clickByCssSelector( - 'div[data-test-subj="fieldInput0"] .euiBadge[title="*"] svg.euiIcon' + 'div[data-test-subj="fieldInput0"] [title="Remove * from selection in this group"] svg.euiIcon' ) .then(function() { return addGrantedField(userObj.elasticsearch.indices[0].field_security.grant); diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index 0ebcb5c87deee..53c89eadeced7 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -13,8 +13,11 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo const retry = getService('retry'); return new (class UptimePage { - public async goToRoot() { + public async goToRoot(refresh?: boolean) { await navigation.goToUptime(); + if (refresh) { + await navigation.refreshApp(); + } } public async setDateRange(start: string, end: string) { diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts index bada1d42b564a..bd7d76e34b447 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts @@ -41,7 +41,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertRegressionTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertClassificationEvaluatePanelElementsExists() { @@ -50,7 +50,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertClassificationTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertOutlierTablePanelExists() { diff --git a/x-pack/test/functional/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts index 0e638963f2367..e2451328ea941 100644 --- a/x-pack/test/functional/services/machine_learning/job_table.ts +++ b/x-pack/test/functional/services/machine_learning/job_table.ts @@ -187,44 +187,24 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte expectedCounts: object, expectedModelSizeStats: object ) { - const countDetails = await this.parseJobCounts(jobId); - const counts = countDetails.counts; - - // fields that have changing values are only validated - // to be present and then removed so they don't make - // the object validation fail - expect(counts).to.have.property('last_data_time'); - delete counts.last_data_time; - - expect(counts).to.eql(expectedCounts); - - const modelSizeStats = countDetails.modelSizeStats; - - // fields that have changing values are only validated - // to be present and then removed so they don't make - // the object validation fail - expect(modelSizeStats).to.have.property('log_time'); - delete modelSizeStats.log_time; - expect(modelSizeStats).to.have.property('model_bytes'); - delete modelSizeStats.model_bytes; - - // remove categorization fields from validation until - // the ES version is updated - delete modelSizeStats.categorization_status; - delete modelSizeStats.categorized_doc_count; - delete modelSizeStats.dead_category_count; - delete modelSizeStats.frequent_category_count; - delete modelSizeStats.rare_category_count; - delete modelSizeStats.total_category_count; - - // MML during clone has changed in #61589 - // TODO: adjust test code to reflect the new behavior - expect(modelSizeStats).to.have.property('model_bytes_memory_limit'); - delete modelSizeStats.model_bytes_memory_limit; - // @ts-ignore - delete expectedModelSizeStats.model_bytes_memory_limit; - - expect(modelSizeStats).to.eql(expectedModelSizeStats); + const { counts, modelSizeStats } = await this.parseJobCounts(jobId); + + // Only check for expected keys / values, ignore additional properties + // This way the tests stay stable when new properties are added on the ES side + for (const [key, value] of Object.entries(expectedCounts)) { + expect(counts) + .to.have.property(key) + .eql(value, `Expected counts property '${key}' to exist with value '${value}'`); + } + + for (const [key, value] of Object.entries(expectedModelSizeStats)) { + expect(modelSizeStats) + .to.have.property(key) + .eql( + value, + `Expected model size stats property '${key}' to exist with value '${value}')` + ); + } } public async clickActionsMenu(jobId: string) { diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index ba9096a372d9a..e63af493438d6 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -52,8 +52,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertDetailsSummaryExists(); }, - async assertSourceIndexPreviewExists(subSelector?: string) { - let selector = 'transformSourceIndexPreview'; + async assertIndexPreviewExists(subSelector?: string) { + let selector = 'transformIndexPreview'; if (subSelector !== undefined) { selector = `${selector} ${subSelector}`; } else { @@ -62,8 +62,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, - async assertSourceIndexPreviewLoaded() { - await this.assertSourceIndexPreviewExists('loaded'); + async assertIndexPreviewLoaded() { + await this.assertIndexPreviewExists('loaded'); }, async assertPivotPreviewExists(subSelector?: string) { @@ -124,10 +124,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreview(columns: number, rows: number) { + async assertIndexPreview(columns: number, rows: number) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rowsData = await this.parseEuiDataGrid('transformSourceIndexPreview'); + const rowsData = await this.parseEuiDataGrid('transformIndexPreview'); expect(rowsData).to.length( rows, @@ -143,8 +143,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { - await this.assertEuiDataGridColumnValues('transformSourceIndexPreview', column, values); + async assertIndexPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiDataGridColumnValues('transformIndexPreview', column, values); }, async assertPivotPreviewColumnValues(column: number, values: string[]) { diff --git a/x-pack/test/functional/services/uptime/certificates.ts b/x-pack/test/functional/services/uptime/certificates.ts new file mode 100644 index 0000000000000..fb7cb6191b0ae --- /dev/null +++ b/x-pack/test/functional/services/uptime/certificates.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function UptimeCertProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + + const changeSearchField = async (text: string) => { + const input = await testSubjects.find('uptimeCertSearch'); + await input.clearValueWithKeyboard(); + await input.type(text); + }; + + return { + async hasViewCertButton() { + return retry.tryForTime(15000, async () => { + await testSubjects.existOrFail('uptimeCertificatesLink'); + }); + }, + async certificateExists(cert: { certId: string; monitorId: string }) { + return retry.tryForTime(15000, async () => { + await testSubjects.existOrFail(cert.certId); + await testSubjects.existOrFail('monitor-page-link-' + cert.monitorId); + }); + }, + async hasCertificates(expectedTotal?: number) { + return retry.tryForTime(15000, async () => { + const totalCerts = await testSubjects.getVisibleText('uptimeCertTotal'); + if (expectedTotal) { + expect(Number(totalCerts) === expectedTotal).to.eql(true); + } else { + expect(Number(totalCerts) > 0).to.eql(true); + } + }); + }, + async searchIsWorking(monId: string) { + const self = this; + return retry.tryForTime(15000, async () => { + await changeSearchField(monId); + await self.hasCertificates(1); + }); + }, + }; +} diff --git a/x-pack/test/functional/services/uptime/navigation.ts b/x-pack/test/functional/services/uptime/navigation.ts index 36a5d7c9702f8..c17fa3a5f6339 100644 --- a/x-pack/test/functional/services/uptime/navigation.ts +++ b/x-pack/test/functional/services/uptime/navigation.ts @@ -25,9 +25,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv }); }; + const refreshApp = async () => { + await testSubjects.click('superDatePickerApplyTimeButton'); + }; + return { async refreshApp() { - await testSubjects.click('superDatePickerApplyTimeButton'); + await refreshApp(); }, async goToUptime() { @@ -60,6 +64,13 @@ export function UptimeNavigationProvider({ getService, getPageObjects }: FtrProv } }, + goToCertificates: async () => { + return retry.tryForTime(30 * 1000, async () => { + await testSubjects.click('uptimeCertificatesLink'); + await testSubjects.existOrFail('uptimeCertificatesPage'); + }); + }, + async loadDataAndGoToMonitorPage(dateStart: string, dateEnd: string, monitorId: string) { await PageObjects.timePicker.setAbsoluteRange(dateStart, dateEnd); await this.goToMonitor(monitorId); diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index 14cab368b766a..96f5e45ce2ca4 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -5,6 +5,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; +import { DynamicSettings } from '../../../../plugins/uptime/common/runtime_types'; export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); @@ -25,24 +26,24 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) { await changeInputField(text, 'heartbeat-indices-input-loaded'); }, changeErrorThresholdInput: async (text: string) => { - await changeInputField(text, 'error-state-threshold-input-loaded'); + await changeInputField(text, 'expiration-threshold-input-loaded'); }, changeWarningThresholdInput: async (text: string) => { - await changeInputField(text, 'warning-state-threshold-input-loaded'); + await changeInputField(text, 'age-threshold-input-loaded'); }, - loadFields: async () => { + loadFields: async (): Promise<DynamicSettings> => { const indInput = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - const errorInput = await testSubjects.find('error-state-threshold-input-loaded', 5000); - const warningInput = await testSubjects.find('warning-state-threshold-input-loaded', 5000); + const expirationInput = await testSubjects.find('expiration-threshold-input-loaded', 5000); + const ageInput = await testSubjects.find('age-threshold-input-loaded', 5000); const heartbeatIndices = await indInput.getAttribute('value'); - const errorThreshold = await errorInput.getAttribute('value'); - const warningThreshold = await warningInput.getAttribute('value'); + const expiration = await expirationInput.getAttribute('value'); + const age = await ageInput.getAttribute('value'); return { heartbeatIndices, - certificatesThresholds: { - errorState: errorThreshold, - warningState: warningThreshold, + certThresholds: { + age: parseInt(age, 10), + expiration: parseInt(expiration, 10), }, }; }, diff --git a/x-pack/test/functional/services/uptime/uptime.ts b/x-pack/test/functional/services/uptime/uptime.ts index 601feb6b0646e..8d36ba4bf6cfd 100644 --- a/x-pack/test/functional/services/uptime/uptime.ts +++ b/x-pack/test/functional/services/uptime/uptime.ts @@ -12,6 +12,7 @@ import { UptimeMonitorProvider } from './monitor'; import { UptimeNavigationProvider } from './navigation'; import { UptimeAlertsProvider } from './alerts'; import { UptimeMLAnomalyProvider } from './ml_anomaly'; +import { UptimeCertProvider } from './certificates'; export function UptimeProvider(context: FtrProviderContext) { const common = UptimeCommonProvider(context); @@ -20,6 +21,7 @@ export function UptimeProvider(context: FtrProviderContext) { const navigation = UptimeNavigationProvider(context); const alerts = UptimeAlertsProvider(context); const ml = UptimeMLAnomalyProvider(context); + const cert = UptimeCertProvider(context); return { common, @@ -28,5 +30,6 @@ export function UptimeProvider(context: FtrProviderContext) { navigation, alerts, ml, + cert, }; } diff --git a/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts index c543046031e9f..fdebdae9e5d0e 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -41,13 +41,13 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('welcomeTitle'); }); - it(`endpoint management shows 'Hosts'`, async () => { + it(`endpoint hosts shows hosts lists page`, async () => { await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts', undefined, { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); }); }); diff --git a/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts index c2c4068212484..b944056e00911 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts @@ -31,7 +31,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the hosts page when the Hosts tab is selected', async () => { await (await testSubjects.find('hostsEndpointTab')).click(); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); }); it('renders the alerts page when the Alerts tab is selected', async () => { @@ -46,7 +46,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the home page when Home tab is selected after selecting another tab', async () => { await (await testSubjects.find('hostsEndpointTab')).click(); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); await (await testSubjects.find('homeEndpointTab')).click(); await testSubjects.existOrFail('welcomeTitle'); diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts index 6ae78ab9d48ac..d7f1cc21828d1 100644 --- a/x-pack/test/functional_endpoint/config.ts +++ b/x-pack/test/functional_endpoint/config.ts @@ -30,8 +30,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', - '--xpack.ingestManager.epm.enabled=true', - '--xpack.ingestManager.fleet.enabled=true', ], }, }; 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 bbf8881f0c62a..9e99c60b4dcb7 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 @@ -68,7 +68,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await nameInput.click(); await testSubjects.click('.slack-ActionTypeSelectOption'); - await testSubjects.click('createActionConnectorButton'); + await testSubjects.click('addNewActionConnectorButton-.slack'); const slackConnectorName = generateUniqueKey(); await testSubjects.setValue('nameInput', slackConnectorName); await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); @@ -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'); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index 562f64656319e..1facc05bc186d 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -21,10 +21,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Connectors', function() { before(async () => { await alerting.actions.createAction({ - name: `server-log-${Date.now()}`, - actionTypeId: '.server-log', + name: `slack-${Date.now()}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }); await pageObjects.common.navigateToApp('triggersActions'); @@ -36,12 +38,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); @@ -54,7 +55,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResults).to.eql([ { name: connectorName, - actionType: 'Server log', + actionType: 'Slack', referencedByCount: '0', }, ]); @@ -66,12 +67,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); @@ -84,10 +84,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); - const nameInputToUpdate = await testSubjects.find('nameInput'); - await nameInputToUpdate.click(); - await nameInputToUpdate.clearValue(); - await nameInputToUpdate.type(updatedConnectorName); + await testSubjects.setValue('nameInput', updatedConnectorName); + + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveEditedActionButton"]:not(disabled)'); @@ -100,7 +99,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(searchResultsAfterEdit).to.eql([ { name: updatedConnectorName, - actionType: 'Server log', + actionType: 'Slack', referencedByCount: '0', }, ]); @@ -110,12 +109,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createConnector(connectorName: string) { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); await pageObjects.common.closeToast(); @@ -148,12 +146,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function createConnector(connectorName: string) { await pageObjects.triggersActionsUI.clickCreateConnectorButton(); - await testSubjects.click('.server-log-card'); + await testSubjects.click('.slack-card'); + + await testSubjects.setValue('nameInput', connectorName); - const nameInput = await testSubjects.find('nameInput'); - await nameInput.click(); - await nameInput.clearValue(); - await nameInput.type(connectorName); + await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); await pageObjects.common.closeToast(); @@ -186,7 +183,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should not be able to delete a preconfigured connector', async () => { - const preconfiguredConnectorName = 'xyz'; + const preconfiguredConnectorName = 'Serverlog'; await pageObjects.triggersActionsUI.searchConnectors(preconfiguredConnectorName); const searchResults = await pageObjects.triggersActionsUI.getConnectorsList(); @@ -197,7 +194,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should not be able to edit a preconfigured connector', async () => { - const preconfiguredConnectorName = 'xyz'; + const preconfiguredConnectorName = 'xyztest'; await pageObjects.triggersActionsUI.searchConnectors(preconfiguredConnectorName); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 2c29954528bd5..7970c9b24427e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -17,6 +17,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); const alerting = getService('alerting'); const retry = getService('retry'); + const find = getService('find'); describe('Alert Details', function() { describe('Header', function() { @@ -26,16 +27,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const actions = await Promise.all([ alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${0}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${0}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${1}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${1}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), ]); @@ -71,7 +76,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(alertType).to.be(`Always Firing`); const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); - expect(actionType).to.be(`Server log`); + expect(actionType).to.be(`Slack`); expect(actionCount).to.be(`+1`); }); @@ -148,6 +153,111 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); + describe('Edit alert button', function() { + const testRunUuid = uuid.v4(); + + it('should open edit alert flyout', async () => { + await pageObjects.common.navigateToApp('triggersActions'); + const params = { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000, 5000], + index: ['.kibana_1'], + timeField: 'alert', + }; + const alert = await alerting.alerts.createAlertWithActions( + testRunUuid, + '.index-threshold', + params, + [ + { + group: 'threshold met', + id: 'my-server-log', + params: { level: 'info', message: ' {{context.message}}' }, + }, + ] + ); + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + const editButton = await testSubjects.find('openEditAlertFlyoutButton'); + await editButton.click(); + expect(await testSubjects.exists('hasActionsDisabled')).to.eql(false); + + const updatedAlertName = `Changed Alert Name ${uuid.v4()}`; + 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}'`); + + const headingText = await pageObjects.alertDetailsUI.getHeadingText(); + expect(headingText).to.be(updatedAlertName); + }); + + it('should reset alert when canceling an edit', async () => { + await pageObjects.common.navigateToApp('triggersActions'); + const params = { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000, 5000], + index: ['.kibana_1'], + timeField: 'alert', + }; + const alert = await alerting.alerts.createAlertWithActions( + testRunUuid, + '.index-threshold', + params + ); + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + const editButton = await testSubjects.find('openEditAlertFlyoutButton'); + await editButton.click(); + + const updatedAlertName = `Changed Alert Name ${uuid.v4()}`; + await testSubjects.setValue('alertNameInput', updatedAlertName, { + clearWithKeyboard: true, + }); + + await testSubjects.click('cancelSaveEditedAlertButton'); + await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); + + await editButton.click(); + + const nameInputAfterCancel = await testSubjects.find('alertNameInput'); + const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); + expect(textAfterCancel).to.eql(alert.name); + }); + }); + describe('View In App', function() { const testRunUuid = uuid.v4(); @@ -206,16 +316,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const actions = await Promise.all([ alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${0}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${0}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${1}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${1}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), ]); @@ -418,16 +532,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const actions = await Promise.all([ alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${0}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${0}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), alerting.actions.createAction({ - name: `server-log-${testRunUuid}-${1}`, - actionTypeId: '.server-log', + name: `slack-${testRunUuid}-${1}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }), ]); @@ -453,6 +571,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } ); + // await first run to complete so we have an initial state + await retry.try(async () => { + const { alertInstances } = await alerting.alerts.getAlertState(alert.id); + expect(Object.keys(alertInstances).length).to.eql(instances.length); + }); + // refresh to see alert await browser.refresh(); @@ -463,12 +587,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // click on first alert await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); - - // await first run to complete so we have an initial state - await retry.try(async () => { - const { alertInstances } = await alerting.alerts.getAlertState(alert.id); - expect(Object.keys(alertInstances).length).to.eql(instances.length); - }); }); const PAGE_SIZE = 10; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index f049406b639c7..2edab1b164a1b 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -59,10 +59,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('navigates to an alert details page', async () => { const action = await alerting.actions.createAction({ - name: `server-log-${Date.now()}`, - actionTypeId: '.server-log', + name: `Slack-${Date.now()}`, + actionTypeId: '.slack', config: {}, - secrets: {}, + secrets: { + webhookUrl: 'https://test', + }, }); const alert = await alerting.alerts.createAlwaysFiringWithAction( diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index a620b1d953376..ef2270fb97745 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -10,6 +10,21 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; import { pageObjects } from './page_objects'; +// .server-log is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + // eslint-disable-next-line import/no-default-export export default async function({ readConfigFile }: FtrConfigProviderContext) { const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); @@ -50,17 +65,21 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--plugin-path=${join(__dirname, 'fixtures', 'plugins', 'alerts')}`, - '--xpack.actions.enabled=true', - '--xpack.alerting.enabled=true', + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.actions.preconfigured=${JSON.stringify([ { id: 'my-slack1', actionTypeId: '.slack', - name: 'Slack#xyz', + name: 'Slack#xyztest', config: { webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', }, }, + { + id: 'my-server-log', + actionTypeId: '.server-log', + name: 'Serverlog#xyz', + }, ])}`, ], }, diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts index 5b506c20e029c..2a0d28f246765 100644 --- a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -22,6 +22,44 @@ export class Alerts { }); } + public async createAlertWithActions( + name: string, + alertTypeId: string, + params?: Record<string, any>, + actions?: Array<{ + id: string; + group: string; + params: Record<string, any>; + }>, + tags?: string[], + consumer?: string, + schedule?: Record<string, any>, + throttle?: string + ) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags, + alertTypeId, + consumer: consumer ?? 'bar', + schedule: schedule ?? { interval: '1m' }, + throttle: throttle ?? '1m', + actions: actions ?? [], + params: params ?? {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + public async createNoOp(name: string) { this.log.debug(`creating alert ${name}`); diff --git a/x-pack/test/plugin_api_integration/config.ts b/x-pack/test/plugin_api_integration/config.ts index c581e0c246e13..adb31f3562a6f 100644 --- a/x-pack/test/plugin_api_integration/config.ts +++ b/x-pack/test/plugin_api_integration/config.ts @@ -22,6 +22,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('./test_suites/task_manager'), require.resolve('./test_suites/event_log'), + require.resolve('./test_suites/licensed_feature_usage'), ], services, servers: integrationConfig.get('servers'), 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 c5f3e65581df9..9622715e87e55 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/plugins/feature_usage_test/kibana.json b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/kibana.json new file mode 100644 index 0000000000000..b11b7ada24a57 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "feature_usage_test", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "feature_usage_test"], + "requiredPlugins": ["licensing"], + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/index.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/index.ts new file mode 100644 index 0000000000000..e07915ab5f46b --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/index.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. + */ + +import { PluginInitializer } from 'kibana/server'; +import { + FeatureUsageTestPlugin, + FeatureUsageTestPluginSetup, + FeatureUsageTestPluginStart, +} from './plugin'; + +export const plugin: PluginInitializer< + FeatureUsageTestPluginSetup, + FeatureUsageTestPluginStart +> = () => new FeatureUsageTestPlugin(); diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts new file mode 100644 index 0000000000000..b36d6dca077f7 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/server'; +import { + LicensingPluginSetup, + LicensingPluginStart, +} from '../../../../../plugins/licensing/server'; +import { registerRoutes } from './routes'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface FeatureUsageTestPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface FeatureUsageTestPluginStart {} + +export interface FeatureUsageTestSetupDependencies { + licensing: LicensingPluginSetup; +} +export interface FeatureUsageTestStartDependencies { + licensing: LicensingPluginStart; +} + +export class FeatureUsageTestPlugin + implements + Plugin< + FeatureUsageTestPluginSetup, + FeatureUsageTestPluginStart, + FeatureUsageTestSetupDependencies, + FeatureUsageTestStartDependencies + > { + public setup( + { + http, + getStartServices, + }: CoreSetup<FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart>, + { licensing }: FeatureUsageTestSetupDependencies + ) { + licensing.featureUsage.register('test_feature_a'); + licensing.featureUsage.register('test_feature_b'); + licensing.featureUsage.register('test_feature_c'); + + registerRoutes(http.createRouter(), getStartServices); + + return {}; + } + + public start() { + return {}; + } +} diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/hit.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/hit.ts new file mode 100644 index 0000000000000..494bcdbf5f61e --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/hit.ts @@ -0,0 +1,39 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart } from '../plugin'; + +export function registerFeatureHitRoute( + router: IRouter, + getStartServices: StartServicesAccessor< + FeatureUsageTestStartDependencies, + FeatureUsageTestPluginStart + > +) { + router.get( + { + path: '/api/feature_usage_test/hit', + validate: { + query: schema.object({ + featureName: schema.string(), + usedAt: schema.maybe(schema.number()), + }), + }, + }, + async (context, request, response) => { + const [, { licensing }] = await getStartServices(); + try { + const { featureName, usedAt } = request.query; + licensing.featureUsage.notifyUsage(featureName, usedAt); + return response.ok(); + } catch (e) { + return response.badRequest(); + } + } + ); +} diff --git a/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/index.ts b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/index.ts new file mode 100644 index 0000000000000..a8225838fd9bf --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/feature_usage_test/server/routes/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, StartServicesAccessor } from 'src/core/server'; +import { FeatureUsageTestStartDependencies, FeatureUsageTestPluginStart } from '../plugin'; + +import { registerFeatureHitRoute } from './hit'; + +export function registerRoutes( + router: IRouter, + getStartServices: StartServicesAccessor< + FeatureUsageTestStartDependencies, + FeatureUsageTestPluginStart + > +) { + registerFeatureHitRoute(router, getStartServices); +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json new file mode 100644 index 0000000000000..416ef7fa34591 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "sample_task_plugin", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["taskManager"], + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json new file mode 100644 index 0000000000000..c8d47decd94c1 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/package.json @@ -0,0 +1,18 @@ +{ + "name": "sample_task_plugin", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/sample_task_plugin", + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + }, + "license": "Apache-2.0", + "dependencies": {} +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts new file mode 100644 index 0000000000000..77233f463734a --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { SampleTaskManagerFixturePlugin } from './plugin'; + +export const plugin = () => new SampleTaskManagerFixturePlugin(); diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts new file mode 100644 index 0000000000000..1fee2decbcba9 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/init_routes.ts @@ -0,0 +1,252 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IKibanaResponse, + IRouter, + CoreSetup, +} from 'kibana/server'; +import { EventEmitter } from 'events'; +import { TaskManagerStartContract } from '../../../../../plugins/task_manager/server'; + +const scope = 'testing'; +const taskManagerQuery = { + bool: { + filter: { + bool: { + must: [ + { + term: { + 'task.scope': scope, + }, + }, + ], + }, + }, + }, +}; + +export function initRoutes( + router: IRouter, + core: CoreSetup, + taskManagerStart: Promise<TaskManagerStartContract>, + taskTestingEvents: EventEmitter +) { + async function ensureIndexIsRefreshed() { + return await core.elasticsearch.adminClient.callAsInternalUser('indices.refresh', { + index: '.kibana_task_manager', + }); + } + + router.post( + { + path: `/api/sample_tasks/schedule`, + validate: { + body: schema.object({ + task: schema.object({ + taskType: schema.string(), + schedule: schema.maybe( + schema.object({ + interval: schema.string(), + }) + ), + interval: schema.maybe(schema.string()), + params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + state: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + id: schema.maybe(schema.string()), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const taskManager = await taskManagerStart; + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskResult = await taskManager.schedule(task, { req }); + + return res.ok({ body: taskResult }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/run_now`, + validate: { + body: schema.object({ + task: schema.object({ + id: schema.string({}), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + const { + task: { id }, + } = req.body; + try { + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.runNow(id) }); + } catch (err) { + return res.ok({ body: { id, error: `${err}` } }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/ensure_scheduled`, + validate: { + body: schema.object({ + task: schema.object({ + taskType: schema.string(), + params: schema.object({}), + state: schema.maybe(schema.object({})), + id: schema.maybe(schema.string()), + }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const { task: taskFields } = req.body; + const task = { + ...taskFields, + scope: [scope], + }; + + const taskManager = await taskManagerStart; + const taskResult = await taskManager.ensureScheduled(task, { req }); + + return res.ok({ body: taskResult }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.post( + { + path: `/api/sample_tasks/event`, + validate: { + body: schema.object({ + event: schema.string(), + data: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const { event, data } = req.body; + taskTestingEvents.emit(event, data); + return res.ok({ body: event }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.get( + { + path: `/api/sample_tasks`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + const taskManager = await taskManagerStart; + return res.ok({ + body: await taskManager.fetch({ + query: taskManagerQuery, + }), + }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); + + router.get( + { + path: `/api/sample_tasks/task/{taskId}`, + validate: { + params: schema.object({ + taskId: schema.string(), + }), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + await ensureIndexIsRefreshed(); + const taskManager = await taskManagerStart; + return res.ok({ body: await taskManager.get(req.params.taskId) }); + } catch (err) { + return res.ok({ body: err }); + } + return res.ok({ body: {} }); + } + ); + + router.delete( + { + path: `/api/sample_tasks`, + validate: {}, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest<any, any, any, any>, + res: KibanaResponseFactory + ): Promise<IKibanaResponse<any>> { + try { + let tasksFound = 0; + const taskManager = await taskManagerStart; + do { + const { docs: tasks } = await taskManager.fetch({ + query: taskManagerQuery, + }); + tasksFound = tasks.length; + await Promise.all(tasks.map(task => taskManager.remove(task.id))); + } while (tasksFound > 0); + return res.ok({ body: 'OK' }); + } catch (err) { + return res.ok({ body: err }); + } + } + ); +} diff --git a/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts new file mode 100644 index 0000000000000..508d58b8f0ca9 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/sample_task_plugin/server/plugin.ts @@ -0,0 +1,164 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart } from 'kibana/server'; +import { EventEmitter } from 'events'; +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { initRoutes } from './init_routes'; +import { + TaskManagerSetupContract, + TaskManagerStartContract, + ConcreteTaskInstance, +} from '../../../../../plugins/task_manager/server'; +import { DEFAULT_MAX_WORKERS } from '../../../../../plugins/task_manager/server/config'; + +// this plugin's dependendencies +export interface SampleTaskManagerFixtureSetupDeps { + taskManager: TaskManagerSetupContract; +} +export interface SampleTaskManagerFixtureStartDeps { + taskManager: TaskManagerStartContract; +} + +export class SampleTaskManagerFixturePlugin + implements + Plugin<void, void, SampleTaskManagerFixtureSetupDeps, SampleTaskManagerFixtureStartDeps> { + taskManagerStart$: Subject<TaskManagerStartContract> = new Subject<TaskManagerStartContract>(); + taskManagerStart: Promise<TaskManagerStartContract> = this.taskManagerStart$ + .pipe(first()) + .toPromise(); + + public setup(core: CoreSetup, { taskManager }: SampleTaskManagerFixtureSetupDeps) { + const taskTestingEvents = new EventEmitter(); + taskTestingEvents.setMaxListeners(DEFAULT_MAX_WORKERS * 2); + + const defaultSampleTaskConfig = { + timeout: '1m', + // This task allows tests to specify its behavior (whether it reschedules itself, whether it errors, etc) + // taskInstance.params has the following optional fields: + // nextRunMilliseconds: number - If specified, the run method will return a runAt that is now + nextRunMilliseconds + // failWith: string - If specified, the task will throw an error with the specified message + // failOn: number - If specified, the task will only throw the `failWith` error when `count` equals to the failOn value + // waitForParams : boolean - should the task stall ands wait to receive params asynchronously before using the default params + // waitForEvent : string - if provided, the task will stall (after completing the run) and wait for an asyn event before completing + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => ({ + async run() { + const { params, state, id } = taskInstance; + const prevState = state || { count: 0 }; + + const count = (prevState.count || 0) + 1; + + const runParams = { + ...params, + // if this task requires custom params provided async - wait for them + ...(params.waitForParams ? await once(taskTestingEvents, id) : {}), + }; + + if (runParams.failWith) { + if (!runParams.failOn || (runParams.failOn && count === runParams.failOn)) { + throw new Error(runParams.failWith); + } + } + + await core.elasticsearch.adminClient.callAsInternalUser('index', { + index: '.kibana_task_manager_test_result', + body: { + type: 'task', + taskId: taskInstance.id, + params: JSON.stringify(runParams), + state: JSON.stringify(state), + ranAt: new Date(), + }, + refresh: true, + }); + + // Stall task run until a certain event is triggered + if (runParams.waitForEvent) { + await once(taskTestingEvents, runParams.waitForEvent); + } + + return { + state: { count }, + runAt: millisecondsFromNow(runParams.nextRunMilliseconds), + }; + }, + }), + }; + + taskManager.registerTaskDefinitions({ + sampleTask: { + ...defaultSampleTaskConfig, + type: 'sampleTask', + title: 'Sample Task', + description: 'A sample task for testing the task_manager.', + }, + singleAttemptSampleTask: { + ...defaultSampleTaskConfig, + type: 'singleAttemptSampleTask', + title: 'Failing Sample Task', + description: + 'A sample task for testing the task_manager that fails on the first attempt to run.', + // fail after the first failed run + maxAttempts: 1, + }, + }); + + taskManager.addMiddleware({ + async beforeSave({ taskInstance, ...opts }) { + const modifiedInstance = { + ...taskInstance, + params: { + originalParams: taskInstance.params, + superFly: 'My middleware param!', + }, + }; + + return { + ...opts, + taskInstance: modifiedInstance, + }; + }, + + async beforeRun({ taskInstance, ...opts }) { + return { + ...opts, + taskInstance: { + ...taskInstance, + params: taskInstance.params.originalParams, + }, + }; + }, + + async beforeMarkRunning(context) { + return context; + }, + }); + initRoutes(core.http.createRouter(), core, this.taskManagerStart, taskTestingEvents); + } + + public start(core: CoreStart, { taskManager }: SampleTaskManagerFixtureStartDeps) { + this.taskManagerStart$.next(taskManager); + this.taskManagerStart$.complete(); + } + public stop() {} +} + +function millisecondsFromNow(ms: number) { + if (!ms) { + return; + } + + const dt = new Date(); + dt.setTime(dt.getTime() + ms); + return dt; +} + +const once = function(emitter: EventEmitter, event: string): Promise<Record<string, unknown>> { + return new Promise(resolve => { + emitter.once(event, data => resolve(data || {})); + }); +}; diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js b/x-pack/test/plugin_api_integration/plugins/task_manager/index.js deleted file mode 100644 index e5b645367b8b7..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/index.js +++ /dev/null @@ -1,150 +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. - */ - -const { DEFAULT_MAX_WORKERS } = require('../../../../plugins/task_manager/server/config.ts'); -const { EventEmitter } = require('events'); - -import { initRoutes } from './init_routes'; - -const once = function(emitter, event) { - return new Promise(resolve => { - emitter.once(event, data => resolve(data || {})); - }); -}; - -export default function TaskTestingAPI(kibana) { - const taskTestingEvents = new EventEmitter(); - taskTestingEvents.setMaxListeners(DEFAULT_MAX_WORKERS * 2); - - return new kibana.Plugin({ - name: 'sampleTask', - require: ['elasticsearch', 'task_manager'], - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server) { - const taskManager = { - ...server.newPlatform.setup.plugins.taskManager, - ...server.newPlatform.start.plugins.taskManager, - }; - const legacyTaskManager = server.plugins.task_manager; - - const defaultSampleTaskConfig = { - timeout: '1m', - // This task allows tests to specify its behavior (whether it reschedules itself, whether it errors, etc) - // taskInstance.params has the following optional fields: - // nextRunMilliseconds: number - If specified, the run method will return a runAt that is now + nextRunMilliseconds - // failWith: string - If specified, the task will throw an error with the specified message - // failOn: number - If specified, the task will only throw the `failWith` error when `count` equals to the failOn value - // waitForParams : boolean - should the task stall ands wait to receive params asynchronously before using the default params - // waitForEvent : string - if provided, the task will stall (after completing the run) and wait for an asyn event before completing - createTaskRunner: ({ taskInstance }) => ({ - async run() { - const { params, state, id } = taskInstance; - const prevState = state || { count: 0 }; - - const count = (prevState.count || 0) + 1; - - const runParams = { - ...params, - // if this task requires custom params provided async - wait for them - ...(params.waitForParams ? await once(taskTestingEvents, id) : {}), - }; - - if (runParams.failWith) { - if (!runParams.failOn || (runParams.failOn && count === runParams.failOn)) { - throw new Error(runParams.failWith); - } - } - - const callCluster = server.plugins.elasticsearch.getCluster('admin') - .callWithInternalUser; - await callCluster('index', { - index: '.kibana_task_manager_test_result', - body: { - type: 'task', - taskId: taskInstance.id, - params: JSON.stringify(runParams), - state: JSON.stringify(state), - ranAt: new Date(), - }, - refresh: true, - }); - - // Stall task run until a certain event is triggered - if (runParams.waitForEvent) { - await once(taskTestingEvents, runParams.waitForEvent); - } - - return { - state: { count }, - runAt: millisecondsFromNow(runParams.nextRunMilliseconds), - }; - }, - }), - }; - - taskManager.registerTaskDefinitions({ - sampleTask: { - ...defaultSampleTaskConfig, - title: 'Sample Task', - description: 'A sample task for testing the task_manager.', - }, - singleAttemptSampleTask: { - ...defaultSampleTaskConfig, - title: 'Failing Sample Task', - description: - 'A sample task for testing the task_manager that fails on the first attempt to run.', - // fail after the first failed run - maxAttempts: 1, - }, - }); - - taskManager.addMiddleware({ - async beforeSave({ taskInstance, ...opts }) { - const modifiedInstance = { - ...taskInstance, - params: { - originalParams: taskInstance.params, - superFly: 'My middleware param!', - }, - }; - - return { - ...opts, - taskInstance: modifiedInstance, - }; - }, - - async beforeRun({ taskInstance, ...opts }) { - return { - ...opts, - taskInstance: { - ...taskInstance, - params: taskInstance.params.originalParams, - }, - }; - }, - }); - - initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents); - }, - }); -} - -function millisecondsFromNow(ms) { - if (!ms) { - return; - } - - const dt = new Date(); - dt.setTime(dt.getTime() + ms); - return dt; -} diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js b/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js deleted file mode 100644 index 785fbed341423..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/init_routes.js +++ /dev/null @@ -1,236 +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 Joi from 'joi'; - -const scope = 'testing'; -const taskManagerQuery = { - bool: { - filter: { - bool: { - must: [ - { - term: { - 'task.scope': scope, - }, - }, - ], - }, - }, - }, -}; - -export function initRoutes(server, taskManager, legacyTaskManager, taskTestingEvents) { - const callCluster = server.plugins.elasticsearch.getCluster('admin').callWithInternalUser; - - async function ensureIndexIsRefreshed() { - return await callCluster('indices.refresh', { - index: '.kibana_task_manager', - }); - } - - server.route({ - path: '/api/sample_tasks/schedule', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - schedule: Joi.object({ - interval: Joi.string(), - }).optional(), - interval: Joi.string().optional(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await taskManager.schedule(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - /* - Schedule using legacy Api - */ - server.route({ - path: '/api/sample_tasks/schedule_legacy', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - schedule: Joi.object({ - interval: Joi.string(), - }).optional(), - interval: Joi.string().optional(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await legacyTaskManager.schedule(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/run_now', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - const { - task: { id }, - } = request.payload; - try { - return await taskManager.runNow(id); - } catch (err) { - return { id, error: `${err}` }; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/ensure_scheduled', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - task: Joi.object({ - taskType: Joi.string().required(), - params: Joi.object().required(), - state: Joi.object().optional(), - id: Joi.string().optional(), - }), - }), - }, - }, - async handler(request) { - try { - const { task: taskFields } = request.payload; - const task = { - ...taskFields, - scope: [scope], - }; - - const taskResult = await taskManager.ensureScheduled(task, { request }); - - return taskResult; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/event', - method: 'POST', - config: { - validate: { - payload: Joi.object({ - event: Joi.string().required(), - data: Joi.object() - .optional() - .default({}), - }), - }, - }, - async handler(request) { - try { - const { event, data } = request.payload; - taskTestingEvents.emit(event, data); - return { event }; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks', - method: 'GET', - async handler() { - try { - return taskManager.fetch({ - query: taskManagerQuery, - }); - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks/task/{taskId}', - method: 'GET', - async handler(request) { - try { - await ensureIndexIsRefreshed(); - return await taskManager.get(request.params.taskId); - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/sample_tasks', - method: 'DELETE', - async handler() { - try { - let tasksFound = 0; - do { - const { docs: tasks } = await taskManager.fetch({ - query: taskManagerQuery, - }); - tasksFound = tasks.length; - await Promise.all(tasks.map(task => taskManager.remove(task.id))); - } while (tasksFound > 0); - return 'OK'; - } catch (err) { - return err; - } - }, - }); -} diff --git a/x-pack/test/plugin_api_integration/plugins/task_manager/package.json b/x-pack/test/plugin_api_integration/plugins/task_manager/package.json deleted file mode 100644 index ec63c512e9cd7..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/task_manager/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "sample_task_plugin", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0", - "dependencies": { - "joi": "^13.5.2" - } -} 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 d664357c3ba12..f3a3d58336b1d 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 @@ -7,7 +7,7 @@ import { merge, omit, times, chunk, isEmpty } from 'lodash'; import uuid from 'uuid'; import expect from '@kbn/expect/expect.js'; -import moment, { Moment } from 'moment'; +import moment from 'moment'; import { FtrProviderContext } from '../../ftr_provider_context'; import { IEvent } from '../../../../plugins/event_log/server'; import { IValidatedEvent } from '../../../../plugins/event_log/server/types'; @@ -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(); @@ -43,10 +45,8 @@ export default function({ getService }: FtrProviderContext) { it('should support pagination for events', async () => { const id = uuid.v4(); - const timestamp = moment(); - const [firstExpectedEvent, ...expectedEvents] = times(6, () => - fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))) - ); + const [firstExpectedEvent, ...expectedEvents] = times(6, () => fakeEvent(id)); + // run one first to create the SO and avoid clashes await logTestEvent(id, firstExpectedEvent); await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); @@ -82,10 +82,7 @@ export default function({ getService }: FtrProviderContext) { it('should support sorting by event end', async () => { const id = uuid.v4(); - const timestamp = moment(); - const [firstExpectedEvent, ...expectedEvents] = times(6, () => - fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))) - ); + const [firstExpectedEvent, ...expectedEvents] = times(6, () => fakeEvent(id)); // run one first to create the SO and avoid clashes await logTestEvent(id, firstExpectedEvent); await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); @@ -106,21 +103,24 @@ export default function({ getService }: FtrProviderContext) { it('should support date ranges for events', async () => { const id = uuid.v4(); - const timestamp = moment(); - - const firstEvent = fakeEvent(id, fakeEventTiming(timestamp)); + // write a document that shouldn't be found in the inclusive date range search + const firstEvent = fakeEvent(id); await logTestEvent(id, firstEvent); - await delay(100); - const start = timestamp.add(1, 's').toISOString(); + // wait a second, get the start time for the date range search + await delay(1000); + const start = new Date().toISOString(); - const expectedEvents = times(6, () => fakeEvent(id, fakeEventTiming(timestamp.add(1, 's')))); + // write the documents that we should be found in the date range searches + const expectedEvents = times(6, () => fakeEvent(id)); await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); - const end = timestamp.add(1, 's').toISOString(); + // get the end time for the date range search + const end = new Date().toISOString(); - await delay(100); - const lastEvent = fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))); + // write a document that shouldn't be found in the inclusive date range search + await delay(1000); + const lastEvent = fakeEvent(id); await logTestEvent(id, lastEvent); await retry.try(async () => { @@ -195,33 +195,17 @@ export default function({ getService }: FtrProviderContext) { .expect(200); } - function fakeEventTiming(start: Moment): Partial<IEvent> { - return { - event: { - start: start.toISOString(), - end: start - .clone() - .add(500, 'milliseconds') - .toISOString(), - }, - }; - } - function fakeEvent(id: string, overrides: Partial<IEvent> = {}): IEvent { - const start = moment().toISOString(); - const end = moment().toISOString(); return merge( { event: { provider: 'event_log_fixture', action: 'test', - start, - end, - duration: 1000000, }, 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 2de395308ce74..361d80aaedd41 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 @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import uuid from 'uuid'; import expect from '@kbn/expect/expect.js'; import { IEvent } from '../../../../plugins/event_log/server'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -97,10 +98,10 @@ export default function({ getService }: FtrProviderContext) { await registerProviderActions('provider4', ['action1', 'action2']); } - const eventId = '1'; + 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); diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts new file mode 100644 index 0000000000000..41f2cfc7983ef --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/feature_usage.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + const notifyUsage = async (featureName: string, usedAt: number) => { + await supertest.get(`/api/feature_usage_test/hit?featureName=${featureName}&usedAt=${usedAt}`); + }; + + const toISO = (time: number) => new Date(time).toISOString(); + + describe('/api/licensing/feature_usage', () => { + it('returns a map of last feature usages', async () => { + const timeA = Date.now(); + await notifyUsage('test_feature_a', timeA); + + const timeB = Date.now() - 4567; + await notifyUsage('test_feature_b', timeB); + + const response = await supertest.get('/api/licensing/feature_usage').expect(200); + + expect(response.body.test_feature_a).to.eql(toISO(timeA)); + expect(response.body.test_feature_b).to.eql(toISO(timeB)); + }); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts new file mode 100644 index 0000000000000..6cafb60bf8167 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/licensed_feature_usage/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('Licensed feature usage APIs', function() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_usage')); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js index e8f976d5ae6e3..00cefa42711c9 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/task_manager_integration.js @@ -11,7 +11,7 @@ import supertestAsPromised from 'supertest-as-promised'; const { task: { properties: taskManagerIndexMapping }, -} = require('../../../../legacy/plugins/task_manager/server/mappings.json'); +} = require('../../../../plugins/task_manager/server/saved_objects/mappings.json'); const { DEFAULT_MAX_WORKERS, @@ -90,15 +90,6 @@ export default function({ getService }) { .then(response => response.body); } - function scheduleTaskUsingLegacyApi(task) { - return supertest - .post('/api/sample_tasks/schedule_legacy') - .set('kbn-xsrf', 'xxx') - .send({ task }) - .expect(200) - .then(response => response.body); - } - function runTaskNow(task) { return supertest .post('/api/sample_tasks/run_now') @@ -587,15 +578,5 @@ export default function({ getService }) { expect(getTaskById(tasks, longRunningTask.id).state.count).to.eql(1); }); }); - - it('should retain the legacy api until v8.0.0', async () => { - const result = await scheduleTaskUsingLegacyApi({ - id: 'task-with-legacy-api', - taskType: 'sampleTask', - params: {}, - }); - - expect(result.id).to.be('task-with-legacy-api'); - }); }); } diff --git a/x-pack/test/reporting/README.md b/x-pack/test/reporting/README.md deleted file mode 100644 index fc5147ad8c454..0000000000000 --- a/x-pack/test/reporting/README.md +++ /dev/null @@ -1,153 +0,0 @@ -## The Reporting Tests - -### Overview - -Reporting tests have their own top level test folder because: - - Current API tests run with `optimize.enabled=false` flag for performance reasons, but reporting actually requires UI assets. - - Reporting tests take a lot longer than other test types. This separation allows developers to run them in isolation, or to run other functional or API tests without them. - - ### Running the tests - - There is more information on running x-pack tests here: https://github.com/elastic/kibana/blob/master/x-pack/README.md#running-functional-tests. Similar to running the API tests, you need to specify a reporting configuration file. Reporting currently has two configuration files you can point to: - - test/reporting/configs/chromium_api.js - - test/reporting/configs/chromium_functional.js - - The `api` versions hit the reporting api and ensure report generation completes successfully, but does not verify the output of the reports. This is done in the `functional` test versions, which does a snapshot comparison of the generated URL against a baseline to determine success. - - To run the tests in a single command. : -1. cd into x-pack directory. -2. run: - ``` -node scripts/functional_tests --config test/reporting/configs/[config_file_name_here].js - ``` - - You can also run the test server seperately from the runner. This is beneficial when debugging as Kibana and Elasticsearch will remain up and running throughout multiple test runs. To do this: - -1. cd into x-pack directory. -2. In one terminal window, run: - ``` -node scripts/functional_tests_server.js --config test/reporting/configs/[test_config_name_here].js - ``` -3. In another terminal window, cd into x-pack dir and run: - ``` -node ../scripts/functional_test_runner.js --config test/reporting/configs/[test_config_name_here].js - ``` - -```sh -//OSX -brew install imagemagick ghostscript poppler - -//Ubutnu -sudo apt-get install imagemagick ghostscript poppler-utils -``` - -For windows: - -ImageMagick-6.9.9-37-Q16-HDRI-x64-dll.exe -from -https://sourceforge.net/projects/imagemagick/postdownload -Install with all default options - -gs925w64.exe -from -https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs925/gs925w64.exe -Install with all default options - - -**Note:** Configurations from `kibana.dev.yml` are picked up when running the tests. Ensure that `kibana.dev.yml` does not contain any `xpack.reporting` configurations. - -### Reporting baseline snapshots - -The functional version of the reporting tests create a few pdf reports and do a snapshot comparison against a couple baselines. The baseline images are stored in `./functional/reports/baseline`. - -#### Updating the baselines - -Every now and then visual changes will be made that will require the snapshots to be updated. This is how you go about updating it. I will discuss generating snapshots from chromium since that is the way of the future. - -1. Run the test server for chromium. - ``` -node scripts/functional_tests_server.js --config test/reporting/configs/chromium_functional.js - ``` - 2. Run the test runner - ``` - node ../scripts/functional_test_runner.js --config test/reporting/configs/chromium_functional.js - ``` - 3. This will create new report snapshots in `./functional/reports/session/`. - 4. After manually verifying the new reports in the `session` folder, copy them into [./functional/reports/baseline/](https://github.com/elastic/kibana/blob/master/x-pack/test/reporting/functional/reports/baseline) - 5. Create a new PR with the new snapshots in the baseline folder. - -**Note:** Dashboard has some snapshot testing too, in `_dashboard_snapshots.js`. This test watches for a command line flag `--updateBaselines` which automates updating the baselines. Probably worthwhile to do some similar here in the long run. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana -``` -^^ That loads the .kibana index. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/data -``` -^^ That loads the data indices. - -**Note:** Depending on your kibana.yml configuration, you may need to adjust the username, pw, and port in the urls above. - -5. Navigate to Kibana in the browser (`http://localhost:5601`) -6. Log in, pick any index to be the default to get page the management screen (doesn’t matter) -7. Generate some reporting URLs - - Use a mixture of Visualize, Discover (CSV), Dashboard - - Can view the current test coverage by checkout out [api/generation_urls.js](https://github.com/elastic/kibana/blob/master/x-pack/test/reporting/api/generation_urls.js). You can use different ones for better test coverage (e.g. different dashboards, different visualizations). - - Don’t generate urls from huge dashboards since this is time consuming. - - Use dashboards that have time saved with them if you wish to have data included. -8. Save these reporting urls. -9. Navigate back to the main branch via `git checkout master`. Then create, or work off your branch as usual to add the extra test coverage. -10. Copy the urls into `api/generation_urls.js`, stripping out the domain, port and base path (if they have it). -11. Write your new tests in [api/bwc_generation_urls.js](https://github.com/elastic/kibana/blob/master/x-pack/test/reporting/api/bwc_generation_urls.js) -12. Run tests via the mechanism above. - -**Note:** We may at some point wish to push most of these tests into an integration suite, as testing BWC of urls generated in every single minor, especially if there were not notable changes, may be overkill, especially given the time they add to the ci. - -### Expanding test coverage by including more data - -As Kibana development progresses, our existing data indices often fail to cover new situations, such as: - - Reports with new visualization types - - Canvas reports - - Reports with visualizations referencing new index types (e.g. visualizations referencing rolled up indices) - - etc - - Every now and then we should expand our test coverage to include new features. This is how you go about doing that in the context of reporting: - - 1. Checkout the `[version].x` branch, where `version` is the currently working minor version (e.g. 6.x, not `master`). This is because we don't want to run tests from data generated from a master version. The opposite works fine however - tests on master can run against data generated from the last minor. At least generally, though major version upgrades may require updating archived data (or be run through the appropriate migration scripts). - - 2. Load the current archives via: - ``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana -``` -^^ That loads the .kibana index. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 load ../../../../test/functional/fixtures/es_archiver/dashboard/current/data -``` -^^ That loads the data indices. - -**Note:** Your es-url parameter might be different, but those are the default ports if running via `yarn start` and `yarn es snapshot --license trial`. - -3. Now generate the new data, create new index patterns, create new visualizations, and create new dashboards using those visualizations. All the fun stuff that you may want to use in your tests. - -**Note:** This data is used in open source dashboard testing. All visualizations and saved searches that have `Rendering Test` in their name are dynamically added to a new dashboard and their rendering is confirmed in https://github.com/elastic/kibana/tree/master/test/functional/apps/dashboard/_embedddable_rendering.js. You may need to adjust the expectations if you add new tests (which will be a good thing anyway, help extend the basic rendering tests - this way issues are caught before it gets to reporting tests!). Similarly all visualizations and saved searches that have `Filter Bytes Test` in their name are tested in https://github.com/elastic/kibana/tree/master/test/functional/apps/dashboard/_dashboard_filtering.js - -**Note:** The current reporting tests add visualizations from what is in `PageObjects.dashboard.getTestVisualizationNames`. We should probably instead use a saved dashboard we generate this report from. Then you can add any new visualizations, re-save the dashboard, and re-generate the snapshot above. - -4. After adding more visualizations to a test dashboard, update tests if necessary, update snapshots, then **save the new archives**! - ``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 save ../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana -``` -^^ That saves the .kibana index. - -``` -node ../scripts/es_archiver.js --es-url http://elastic:changeme@localhost:9200 save ../../../../test/functional/fixtures/es_archiver/dashboard/current/data log* animal* dog* [ANY OTHER NEW INDEX NAME YOU ADDED]* -``` -^^ That saves the data indices. The last parameter is a list of indices you want archived. You don't want to include the `.kibana` one in there (this way you can use a custom `.kibana`, but can reuse the data archive, for tests, and `.kibana` archive is small, but the data archives are larger and should be reused). - -5. Create your PR with test updates, and the larger archive. - - - diff --git a/x-pack/test/reporting/configs/chromium_functional.js b/x-pack/test/reporting/configs/chromium_functional.js deleted file mode 100644 index 753d2b2a20ab9..0000000000000 --- a/x-pack/test/reporting/configs/chromium_functional.js +++ /dev/null @@ -1,39 +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. - */ - -export default async function({ readConfigFile }) { - // TODO move reporting tests to x-pack/test/functional/apps/<integration_app>/reporting - const functionalConfig = await readConfigFile(require.resolve('../../functional/config.js')); - - return { - services: functionalConfig.get('services'), - pageObjects: functionalConfig.get('pageObjects'), - servers: functionalConfig.get('servers'), - apps: functionalConfig.get('apps'), - screenshots: functionalConfig.get('screenshots'), - junit: { - reportName: 'X-Pack Chromium Functional Reporting Tests', - }, - testFiles: [require.resolve('../functional')], - kbnTestServer: { - ...functionalConfig.get('kbnTestServer'), - serverArgs: [ - ...functionalConfig.get('kbnTestServer.serverArgs'), - '--logging.events.log', - '["info","warning","error","fatal","optimize","reporting"]', - '--xpack.endpoint.enabled=true', - '--xpack.reporting.csv.enablePanelActionDownload=true', - '--xpack.reporting.csv.checkForFormulas=false', - '--xpack.reporting.csv.maxSizeBytes=25000000', - '--xpack.security.session.idleTimeout=3600000', - '--xpack.spaces.enabled=false', - ], - }, - esArchiver: functionalConfig.get('esArchiver'), - esTestCluster: functionalConfig.get('esTestCluster'), - security: { disableTestUser: true }, - }; -} diff --git a/x-pack/test/reporting/functional/index.js b/x-pack/test/reporting/functional/index.js deleted file mode 100644 index be3ac09bdec10..0000000000000 --- a/x-pack/test/reporting/functional/index.js +++ /dev/null @@ -1,12 +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. - */ - -export default function({ loadTestFile }) { - describe('reporting app', function() { - this.tags('ciGroup6'); - loadTestFile(require.resolve('./reporting')); - }); -} diff --git a/x-pack/test/reporting/functional/lib/compare_pngs.js b/x-pack/test/reporting/functional/lib/compare_pngs.js deleted file mode 100644 index 16b1008c645a0..0000000000000 --- a/x-pack/test/reporting/functional/lib/compare_pngs.js +++ /dev/null @@ -1,58 +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 path from 'path'; -import fs from 'fs'; -import { promisify } from 'bluebird'; -import { comparePngs } from '../../../../../test/functional/services/lib/compare_pngs'; - -const mkdirAsync = promisify(fs.mkdir); - -export async function checkIfPngsMatch(actualpngPath, baselinepngPath, screenshotsDirectory, log) { - 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. - const sessionDirectoryPath = path.resolve(screenshotsDirectory, 'session'); - const failureDirectoryPath = path.resolve(screenshotsDirectory, 'failure'); - - await mkdirAsync(sessionDirectoryPath, { recursive: true }); - await mkdirAsync(failureDirectoryPath, { recursive: true }); - - const actualpngFileName = path.basename(actualpngPath, '.png'); - const baselinepngFileName = path.basename(baselinepngPath, '.png'); - - const baselineCopyPath = path.resolve( - sessionDirectoryPath, - `${baselinepngFileName}_baseline.png` - ); - const actualCopyPath = path.resolve(sessionDirectoryPath, `${actualpngFileName}_actual.png`); - - // Don't cause a test failure if the baseline snapshot doesn't exist - we don't have all OS's covered and we - // don't want to start causing failures for other devs working on OS's which are lacking snapshots. We have - // mac and linux covered which is better than nothing for now. - try { - log.debug(`writeFileSync: ${baselineCopyPath}`); - fs.writeFileSync(baselineCopyPath, fs.readFileSync(baselinepngPath)); - } catch (error) { - log.error(`No baseline png found at ${baselinepngPath}`); - return 0; - } - log.debug(`writeFileSync: ${actualCopyPath}`); - fs.writeFileSync(actualCopyPath, fs.readFileSync(actualpngPath)); - - let diffTotal = 0; - - const diffPngPath = path.resolve(failureDirectoryPath, `${baselinepngFileName}-${1}.png`); - diffTotal += await comparePngs( - actualCopyPath, - baselineCopyPath, - diffPngPath, - sessionDirectoryPath, - log - ); - - return diffTotal; -} diff --git a/x-pack/test/reporting/functional/lib/index.js b/x-pack/test/reporting/functional/lib/index.js deleted file mode 100644 index e7a08753b591f..0000000000000 --- a/x-pack/test/reporting/functional/lib/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { checkIfPngsMatch } from './compare_pngs'; diff --git a/x-pack/test/reporting/functional/reporting.js b/x-pack/test/reporting/functional/reporting.js deleted file mode 100644 index 6107363986a40..0000000000000 --- a/x-pack/test/reporting/functional/reporting.js +++ /dev/null @@ -1,230 +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 expect from '@kbn/expect'; -import path from 'path'; -import fs from 'fs'; -import { promisify } from 'util'; -import { checkIfPngsMatch } from './lib'; - -const writeFileAsync = promisify(fs.writeFile); -const mkdirAsync = promisify(fs.mkdir); - -const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); - -/* - * TODO Remove this file and spread the tests to various apps - */ - -export default function({ getService, getPageObjects }) { - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - const log = getService('log'); - const config = getService('config'); - const PageObjects = getPageObjects([ - 'reporting', - 'common', - 'dashboard', - 'header', - 'discover', - 'visualize', - 'visEditor', - ]); - - describe('Reporting', () => { - describe('Dashboard', () => { - before('initialize tests', async () => { - log.debug('ReportingPage:initTests'); - await esArchiver.loadIfNeeded('reporting/ecommerce'); - await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); - await browser.setWindowSize(1600, 850); - }); - after('clean up archives', async () => { - await esArchiver.unload('reporting/ecommerce'); - await esArchiver.unload('reporting/ecommerce_kibana'); - }); - - describe('Print PDF button', () => { - it('is not available if new', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); - }); - - it('becomes available when saved', async () => { - await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); - await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); - }); - }); - - describe('Print Layout', () => { - it('downloads a PDF file', async function() { - // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs - // function is taking about 15 seconds per comparison in jenkins. - this.timeout(300000); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); - await PageObjects.reporting.openPdfReportingPanel(); - await PageObjects.reporting.checkUsePrintLayout(); - await PageObjects.reporting.clickGenerateReportButton(); - - const url = await PageObjects.reporting.getReportURL(60000); - const res = await PageObjects.reporting.getResponse(url); - - expect(res.statusCode).to.equal(200); - expect(res.headers['content-type']).to.equal('application/pdf'); - }); - }); - - describe('Print PNG button', () => { - it('is not available if new', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.reporting.openPngReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); - }); - - it('becomes available when saved', async () => { - await PageObjects.dashboard.saveDashboard('My PNG Dash'); - await PageObjects.reporting.openPngReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); - }); - }); - - describe('Preserve Layout', () => { - it('matches baseline report', async function() { - const writeSessionReport = async (name, rawPdf, reportExt) => { - 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 baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); - const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); - log.debug(`getBaselineReportPath (${fullPath})`); - return fullPath; - }; - - this.timeout(300000); - - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); - await PageObjects.reporting.openPngReportingPanel(); - await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); - await PageObjects.reporting.clickGenerateReportButton(); - await PageObjects.reporting.removeForceSharedItemsContainerSize(); - - const url = await PageObjects.reporting.getReportURL(60000); - const reportData = await PageObjects.reporting.getRawPdfReportData(url); - const reportFileName = 'dashboard_preserve_layout'; - const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); - const percentSimilar = await checkIfPngsMatch( - sessionReportPath, - getBaselineReportPath(reportFileName, 'png'), - config.get('screenshots.directory'), - log - ); - - expect(percentSimilar).to.be.lessThan(0.1); - }); - }); - }); - - describe('Discover', () => { - before('initialize tests', async () => { - log.debug('ReportingPage:initTests'); - await esArchiver.loadIfNeeded('reporting/ecommerce'); - await browser.setWindowSize(1600, 850); - }); - after('clean up archives', async () => { - await esArchiver.unload('reporting/ecommerce'); - }); - - describe('Generate CSV button', () => { - beforeEach(() => PageObjects.common.navigateToApp('discover')); - - it('is not available if new', async () => { - await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); - }); - - it('becomes available when saved', async () => { - await PageObjects.discover.saveSearch('my search - expectEnabledGenerateReportButton'); - await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); - }); - - it('generates a report with data', async () => { - await PageObjects.reporting.setTimepickerInDataRange(); - await PageObjects.discover.saveSearch('my search - with data - expectReportCanBeCreated'); - await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); - }); - - it('generates a report with no data', async () => { - await PageObjects.reporting.setTimepickerInNoDataRange(); - await PageObjects.discover.saveSearch('my search - no data - expectReportCanBeCreated'); - await PageObjects.reporting.openCsvReportingPanel(); - expect(await PageObjects.reporting.canReportBeCreated()).to.be(true); - }); - }); - }); - - describe('Visualize', () => { - before('initialize tests', async () => { - log.debug('ReportingPage:initTests'); - await esArchiver.loadIfNeeded('reporting/ecommerce'); - await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); - await browser.setWindowSize(1600, 850); - }); - after('clean up archives', async () => { - await esArchiver.unload('reporting/ecommerce'); - await esArchiver.unload('reporting/ecommerce_kibana'); - }); - - describe('Print PDF button', () => { - it('is not available if new', async () => { - await PageObjects.common.navigateToUrl('visualize', 'new'); - await PageObjects.visualize.clickAreaChart(); - await PageObjects.visualize.clickNewSearch('ecommerce'); - await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); - }); - - it('becomes available when saved', async () => { - await PageObjects.reporting.setTimepickerInDataRange(); - await PageObjects.visEditor.clickBucket('X-axis'); - await PageObjects.visEditor.selectAggregation('Date Histogram'); - await PageObjects.visEditor.clickGo(); - await PageObjects.visualize.saveVisualization('my viz'); - await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); - }); - - it('downloaded PDF has OK status', async function() { - // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs - // function is taking about 15 seconds per comparison in jenkins. - this.timeout(180000); - - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); - await PageObjects.reporting.openPdfReportingPanel(); - await PageObjects.reporting.clickGenerateReportButton(); - - const url = await PageObjects.reporting.getReportURL(60000); - const res = await PageObjects.reporting.getResponse(url); - - expect(res.statusCode).to.equal(200); - expect(res.headers['content-type']).to.equal('application/pdf'); - }); - }); - }); - }); -} diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 3b11ef61a1ab2..0b127288e7958 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -35,7 +35,7 @@ export default function({ getService }: FtrProviderContext) { }); } - async function checkSessionCookie(sessionCookie: Cookie) { + async function checkSessionCookie(sessionCookie: Cookie, username = 'a@b.c') { expect(sessionCookie.key).to.be('sid'); expect(sessionCookie.value).to.not.be.empty(); expect(sessionCookie.path).to.be('/'); @@ -59,7 +59,7 @@ export default function({ getService }: FtrProviderContext) { 'authentication_provider', ]); - expect(apiResponse.body.username).to.be('a@b.c'); + expect(apiResponse.body.username).to.be(username); } describe('SAML authentication', () => { @@ -668,6 +668,29 @@ export default function({ getService }: FtrProviderContext) { const existingUsername = 'a@b.c'; let existingSessionCookie: Cookie; + const testScenarios: Array<[string, () => Promise<void>]> = [ + // Default scenario when active cookie has an active access token. + ['when access token is valid', async () => {}], + // Scenario when active cookie has an expired access token. Access token expiration is set + // to 15s for API integration tests so we need to wait for 20s to make sure token expires. + ['when access token is expired', async () => await delay(20000)], + // Scenario when active cookie references to access/refresh token pair that were already + // removed from Elasticsearch (to simulate 24h when expired tokens are removed). + [ + 'when access token document is missing', + async () => { + const esResponse = await getService('legacyEs').deleteByQuery({ + index: '.security-tokens', + q: 'doc_type:token', + refresh: true, + }); + expect(esResponse) + .to.have.property('deleted') + .greaterThan(0); + }, + ], + ]; + beforeEach(async () => { const captureURLResponse = await supertest .get('/abc/xyz/handshake?one=two three') @@ -701,76 +724,76 @@ export default function({ getService }: FtrProviderContext) { )!; }); - it('should renew session and redirect to the home page if login is for the same user', async () => { - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) - .expect('location', '/') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); - - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', existingUsername); - }); - - it('should create a new session and redirect to the `overwritten_session` if login is for another user', async () => { - const newUsername = 'c@d.e'; - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) - .expect('location', '/security/overwritten_session') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); + for (const [description, setup] of testScenarios) { + it(`should renew session and redirect to the home page if login is for the same user ${description}`, async () => { + await setup(); + + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be('/'); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie); + }); - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', newUsername); - }); + it(`should create a new session and redirect to the \`overwritten_session\` if login is for another user ${description}`, async () => { + await setup(); + + const newUsername = 'c@d.e'; + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be( + '/security/overwritten_session' + ); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie, newUsername); + }); + } }); describe('handshake with very long URL path or fragment', () => { diff --git a/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz b/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz new file mode 100644 index 0000000000000..c7acb36992af3 Binary files /dev/null and b/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz differ diff --git a/x-pack/test/siem_cypress/es_archives/timeline/mappings.json b/x-pack/test/siem_cypress/es_archives/timeline/mappings.json new file mode 100644 index 0000000000000..d3412f9d43b57 --- /dev/null +++ b/x-pack/test/siem_cypress/es_archives/timeline/mappings.json @@ -0,0 +1,2976 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "agent_actions": "ed270b46812f0fa1439366c428a2cf17", + "agent_configs": "38abaf89513877745c359e7700c0c66a", + "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "agents": "c3eeb7b9d97176f15f6d126370ab23c7", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "08b8b110dbca273d37e8aef131ecab61", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", + "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "epm-package": "0be91c6758421dd5d0f1a58e9e5bc7c3", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "9ecce5b58867403613d82fe496470b34", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "outputs": "aee9782e0d500b867859650a36280165", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "siem-detection-engine-rule-actions": "90eee2e4635260f4be0a1da8f5bc0aa0", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "dynamic": "true", + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index a540c7e3c9786..81b29732377da 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -12,7 +12,7 @@ "exclude": [ "test/**/*", "plugins/siem/cypress/**/*", - "legacy/plugins/apm/e2e/cypress/**/*", + "plugins/apm/e2e/cypress/**/*", "**/typespec_tests.ts" ], "compilerOptions": { diff --git a/yarn.lock b/yarn.lock index 45540cd2675b7..94e6a0a11aa99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1088,7 +1088,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.4.2", "@babel/runtime-corejs2@^7.6.3": +"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.6.3": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.9.2.tgz#f11d074ff99b9b4319b5ecf0501f12202bf2bf4d" integrity sha512-ayjSOxuK2GaSDJFCtLgHnYjuMyIpViNujWrZo8GUpN60/n7juzJKK5yOo6RFVb0zdU9ACJFK+MsZrUnj3OmXMw== @@ -1096,14 +1096,6 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/runtime@7.0.0-beta.54": - version "7.0.0-beta.54" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf" - integrity sha1-Oeu0JyP+fKSz4bAOln6AE41Hyt8= - dependencies: - core-js "^2.5.7" - regenerator-runtime "^0.12.0" - "@babel/runtime@7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" @@ -1173,7 +1165,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.9.5": +"@babel/types@^7.4", "@babel/types@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== @@ -1200,6 +1192,32 @@ date-fns "^1.27.2" figures "^1.7.0" +"@cypress/request@2.88.5": + version "2.88.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + "@cypress/webpack-preprocessor@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-4.1.0.tgz#8c4debc0b1abf045b62524d1996dd9aeaf7e86a8" @@ -1244,10 +1262,10 @@ dependencies: "@elastic/apm-rum-core" "^5.2.0" -"@elastic/charts@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.3.0.tgz#cbdeec1860af274edc7a5f5b9dd26ec48c64bb64" - integrity sha512-4kSlSwdDRsVKVX8vRUkwxOu1IT6WIepgLnP0OZT7cFjgrC1SV/16c3YLw2NZDaVe0M/H4rpeNWW30VyrzZVhyw== +"@elastic/charts@18.4.2": + version "18.4.2" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.4.2.tgz#7d3c40dca8a7a701fb7227382191b84d36d8b32a" + integrity sha512-fmEDRUeFEtVWGurafhp/5bHBypOjdXiRXY074tCqnez43hA2iA4v1KrZL8tPFlMePgc/kpZf9wb8aEwxlfWumw== dependencies: classnames "^2.2.6" d3-array "^1.2.4" @@ -1314,16 +1332,16 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@21.0.1": - version "21.0.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-21.0.1.tgz#7cf6846ed88032aebd72f75255298df2fbe26554" - integrity sha512-Hf8ZGRI265qpOKwnnqhZkaMQvali+Xg6FAaNZSskkpXvdLhwGtUGC4YU7HW2vb7svq6IpNUuz+5XWrMLLzVY9w== +"@elastic/eui@22.3.0": + version "22.3.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-22.3.0.tgz#31a7a0aaf69b329acff2791fca677a824b63539d" + integrity sha512-LoQd11RoD6cbDuVQhwTr3lR4Jga8D5cBGaKFPzaS8MoxW5vCu0gcosjI6O5SqmCwifyfU4JP5zeOjy8TawJFxw== dependencies: - "@types/chroma-js" "^1.4.3" + "@types/chroma-js" "^2.0.0" "@types/enzyme" "^3.1.13" "@types/lodash" "^4.14.116" "@types/numeral" "^0.0.25" - "@types/react-beautiful-dnd" "^10.1.0" + "@types/react-beautiful-dnd" "^12.1.2" "@types/react-input-autosize" "^2.0.2" "@types/react-virtualized" "^9.18.7" chroma-js "^2.0.4" @@ -1335,7 +1353,7 @@ numeral "^2.0.6" prop-types "^15.6.0" react-ace "^7.0.5" - react-beautiful-dnd "^10.1.0" + react-beautiful-dnd "^13.0.0" react-focus-lock "^1.17.7" react-input-autosize "^2.2.2" react-is "~16.3.0" @@ -1385,10 +1403,10 @@ through2 "^2.0.0" update-notifier "^0.5.0" -"@elastic/maki@6.2.0": - version "6.2.0" - resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.2.0.tgz#d0a85aa248bdc14dca44e1f9430c0b670f65e489" - integrity sha512-QkmRNpEY4Dy6eqwDimR5X9leMgdPFjdANmpEIwEW1XVUG2U4YtB2BXhDxsnMmNTUrJUjtnjnwgwBUyg0pU0FTg== +"@elastic/maki@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.3.0.tgz#09780650f1510554bef9121b9db86ce297f021f1" + integrity sha512-a2U2DaemIJaW+3nL/sN/+JScdrkoggoGHLDtRPurk2Axnpa9O9QHekmMXLO7eLK1brDpYcplqGE6hwFaMvRRUg== "@elastic/node-crypto@1.1.1": version "1.1.1" @@ -1546,11 +1564,11 @@ through2 "^2.0.3" "@hapi/boom@7.x.x": - version "7.4.2" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-7.4.2.tgz#c16957cd09796f6c1bfb4031bdc39d66d6d750c3" - integrity sha512-T2CYcTI0AqSvC6YC7keu/fh9LVSMzfoMLharBnPbOwmc+Cexj9joIc5yNDKunaxYq9LPuOwMS0f2B3S1tFQUNw== + version "7.4.11" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-7.4.11.tgz#37af8417eb9416aef3367aa60fa04a1a9f1fc262" + integrity sha512-VSU/Cnj1DXouukYxxkes4nNJonCnlogHvIff1v1RVoN4xzkKhMXX+GRmb3NyH1iar10I9WFPDv2JPwfH3GaV0A== dependencies: - "@hapi/hoek" "6.x.x" + "@hapi/hoek" "8.x.x" "@hapi/bourne@1.x.x": version "1.3.2" @@ -1565,11 +1583,6 @@ "@hapi/hoek" "8.x.x" fast-safe-stringify "2.x.x" -"@hapi/hoek@6.x.x": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-6.2.1.tgz#d3a66329159af879bfdf0b0cff2229c43c5a3451" - integrity sha512-+ryw4GU9pjr1uT6lBuErHJg3NYqzwJTvZ75nKuJijEzpd00Uqi6oiawTGDDf5Hl0zWmI7qHfOtaqB0kpQZJQzA== - "@hapi/hoek@8.x.x": version "8.5.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" @@ -3712,15 +3725,20 @@ resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.2.5.tgz#582b2476169a6cba460a214d476c744441d873d5" integrity sha1-WCskdhaabLpGCiFNR2x0REHYc9U= -"@types/bluebird@*": - version "3.5.28" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.28.tgz#04c1a520ff076649236bc8ca21198542ce2bdb09" - integrity sha512-0Vk/kqkukxPKSzP9c8WJgisgGDx5oZDbsLLWIP5t70yThO/YleE+GEm2S1GlRALTaack3O7U5OS5qEm7q2kciA== +"@types/blob-util@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" + integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== + +"@types/bluebird@*", "@types/bluebird@^3.1.1": + version "3.5.30" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.30.tgz#ee034a0eeea8b84ed868b1aa60d690b08a6cfbc5" + integrity sha512-8LhzvcjIoqoi1TghEkRMkbbmM+jhHnBokPGkJWjclMK+Ks0MxEBow3/p2/iFTZ+OIbJHQDSfpgdZEb+af3gfVw== -"@types/bluebird@^3.1.1": - version "3.5.20" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.20.tgz#f6363172add6f4eabb8cada53ca9af2781e8d6a1" - integrity sha512-Wk41MVdF+cHBfVXj/ufUHJeO3BlIQr1McbHZANErMykaCWeDSZbH5erGjNBw2/3UlRdSxZbLfSuQTzFmPOYFsA== +"@types/bluebird@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" + integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== "@types/boom@*", "@types/boom@^7.2.0": version "7.2.0" @@ -3752,6 +3770,19 @@ resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-10.0.1.tgz#266679017749041fe9873fee1131dd2aaa04a07e" integrity sha512-ECuJ+f5gGHiLeiE4RlE/xdqv/0JVDToegPV1aTb10tQStYa0Ycq2OJfQukDv3IFaw3B+CMV46jHc5bXe6QXEQg== +"@types/chai-jquery@1.1.40": + version "1.1.40" + resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" + integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== + dependencies: + "@types/chai" "*" + "@types/jquery" "*" + +"@types/chai@*", "@types/chai@4.2.7", "@types/chai@^4.2.11": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + "@types/chance@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df" @@ -3767,10 +3798,10 @@ resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.2.tgz#3152c8dedfa8621f1ccaaabb40722a8aca808bcf" integrity sha512-Ni8yCN1vF0yfnfKf5bNrBm+92EdZIX2sUk+A4t4QvO1x/9G04rGyC0nik4i5UcNfx8Q7MhX4XUDcy2nrkKQLFg== -"@types/chroma-js@^1.4.3": - version "1.4.3" - resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-1.4.3.tgz#4456e5cb46885a4952324e55a4b6d4064904790c" - integrity sha512-m33zg9cRLtuaUSzlbMrr7iLIKNzrD4+M6Unt5+9mCu4BhR5NwnRjVKblINCwzcBXooukIgld8DtEncP8qpvbNg== +"@types/chroma-js@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.0.0.tgz#b0fc98c8625d963f14e8138e0a7961103303ab22" + integrity sha512-iomunXsXjDxhm2y1OeJt8NwmgC7RyNkPAOddlYVGsbGoX8+1jYt84SG4/tf6RWcwzROLx1kPXPE95by1s+ebIg== "@types/chromedriver@^2.38.0": version "2.38.0" @@ -4189,7 +4220,7 @@ resolved "https://registry.yarnpkg.com/@types/joi/-/joi-13.6.1.tgz#325486a397504f8e22c8c551dc8b0e1d41d5d5ae" integrity sha512-JxZ0NP8NuB0BJOXi1KvAA6rySLTPmhOy4n2gzSFq/IFM3LNFm0h+2Vn/bPPgEYlWqzS2NPeLgKqfm75baX+Hog== -"@types/jquery@*", "@types/jquery@^3.3.31": +"@types/jquery@*", "@types/jquery@3.3.31", "@types/jquery@^3.3.31": version "3.3.31" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== @@ -4206,7 +4237,7 @@ resolved "https://registry.yarnpkg.com/@types/js-search/-/js-search-1.4.0.tgz#f2d4afa176a4fc7b17fb46a1593847887fa1fb7b" integrity sha1-8tSvoXak/HsX+0ahWThHiH+h+3s= -"@types/js-yaml@^3.11.1", "@types/js-yaml@^3.12.1": +"@types/js-yaml@^3.11.1": version "3.12.1" resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.12.1.tgz#5c6f4a1eabca84792fbd916f0cb40847f123c656" integrity sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA== @@ -4295,9 +4326,14 @@ "@types/lodash" "*" "@types/lodash@*", "@types/lodash@^4.14.110", "@types/lodash@^4.14.116": - version "4.14.137" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.137.tgz#8a4804937dc6462274ffcc088df8f14fc1b368e2" - integrity sha512-g4rNK5SRKloO+sUGbuO7aPtwbwzMgjK+bm9BBhLD7jGUiGR7zhwYEhSln/ihgYQBeIJ5j7xjyaYzrWTcu3UotQ== + version "4.14.150" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.150.tgz#649fe44684c3f1fcb6164d943c5a61977e8cf0bd" + integrity sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w== + +"@types/lodash@4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== "@types/lodash@^3.10.1": version "3.10.2" @@ -4355,7 +4391,7 @@ dependencies: "@types/mime-db" "*" -"@types/minimatch@*", "@types/minimatch@^3.0.3": +"@types/minimatch@*", "@types/minimatch@3.0.3", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -4372,6 +4408,11 @@ dependencies: "@types/node" "*" +"@types/mocha@5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + "@types/mocha@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" @@ -4477,10 +4518,10 @@ dependencies: "@types/node" "*" -"@types/papaparse@^4.5.11": - version "4.5.11" - resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-4.5.11.tgz#dcd4f64da55f768c2e2cf92ccac1973c67a73890" - integrity sha512-zOw6K7YyA/NuZ2yZ8lzZFe2U3fn+vFfcRfiQp4ZJHG6y8WYWy2SYFbq6mp4yUgpIruJHBjKZtgyE0vvCoWEq+A== +"@types/papaparse@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.0.3.tgz#7cedc1ebc9484819af8306a8b42f9f08ca9bdb44" + integrity sha512-SgWGWnBGxl6XgjKDM2eoDg163ZFQtH6m6C2aOuaAf1T2gUB3rjaiPDDARbY9WlacRgZqieRG9imAfJaJ+5ouDA== dependencies: "@types/node" "*" @@ -4546,13 +4587,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-beautiful-dnd@^10.1.0": - version "10.1.1" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#7afae39a4247f30c13b8bbb726ccd1b8cda9d4a5" - integrity sha512-75XELhEIWKTkyd1GdVZFvS1MtJwDs9tM37BbIat8mevcw+uH5dcJzZiwESHIWAzySHawS48nkKCQk/bEDp13Mw== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^12.1.1": version "12.1.1" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#149e638c0f912eee6b74ea419b26bb43d0b1da60" @@ -4560,6 +4594,13 @@ dependencies: "@types/react" "*" +"@types/react-beautiful-dnd@^12.1.2": + version "12.1.2" + resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.2.tgz#dfd1bdb072e92c1363e5f7a4c1842eaf95f77b21" + integrity sha512-h+0mA4cHmzL4BhyCniB6ZSSZhfO9LpXXbnhdAfa2k7klS03woiOT+Dh5AchY6eoQXk3vQVtqn40YY3u+MwFs8A== + dependencies: + "@types/react" "*" + "@types/react-color@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0" @@ -4757,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" @@ -4764,11 +4810,36 @@ dependencies: "@types/node" "*" +"@types/sinon-chai@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" + integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + +"@types/sinon@*": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.0.tgz#5b70a360f55645dd64f205defd2a31b749a59799" + integrity sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinon@7.5.1": + version "7.5.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" + integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== + "@types/sinon@^7.0.13": version "7.0.13" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + "@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" @@ -5514,11 +5585,6 @@ adm-zip@0.4.11: resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a" integrity sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA== -adm-zip@^0.4.13: - version "0.4.13" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a" - integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw== - affine-hull@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/affine-hull/-/affine-hull-1.0.0.tgz#763ff1d38d063ceb7e272f17ee4d7bbcaf905c5d" @@ -6425,11 +6491,6 @@ array-reduce@~0.0.0: resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= - array-slice@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" @@ -6461,11 +6522,6 @@ array-uniq@^1.0.0, array-uniq@^1.0.1: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -6692,7 +6748,7 @@ async@^2.6.0, async@^2.6.1: dependencies: lodash "^4.17.10" -async@^2.6.3: +async@^2.6.2, async@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -6787,15 +6843,7 @@ axios@^0.18.0: follow-redirects "1.5.10" is-buffer "^2.0.2" -axios@^0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" - integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== - dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" - -axios@^0.19.2: +axios@^0.19.0, axios@^0.19.2: version "0.19.2" resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== @@ -7133,6 +7181,14 @@ babel-plugin-transform-define@^1.3.1: lodash "^4.17.11" traverse "0.6.6" +babel-plugin-transform-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-imports/-/babel-plugin-transform-imports-2.0.0.tgz#9e5f49f751a9d34ba8f4bb988c7e48ed2419c6b6" + integrity sha512-65ewumYJ85QiXdcB/jmiU0y0jg6eL6CdnDqQAqQ8JMOKh1E52VPG3NJzbVKWcgovUR5GBH8IWpCXQ7I8Q3wjgw== + dependencies: + "@babel/types" "^7.4" + is-valid-path "^0.1.1" + babel-plugin-transform-inline-consecutive-adds@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1" @@ -7578,7 +7634,7 @@ bluebird@3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bluebird@^3.3.0, bluebird@^3.3.1: +bluebird@^3.3.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== @@ -7773,13 +7829,6 @@ brace@0.11.1, brace@^0.11.0, brace@^0.11.1: resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= -braces@^0.1.2: - version "0.1.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" - integrity sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY= - dependencies: - expand-range "^0.1.0" - braces@^2.3.0, braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -7796,7 +7845,7 @@ braces@^2.3.0, braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -8701,7 +8750,7 @@ cheerio@^1.0.0-rc.3: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@2.1.2, chokidar@^2.0.2, chokidar@^2.0.3, chokidar@^2.0.4: +chokidar@2.1.2, chokidar@^2.0.2, chokidar@^2.0.4: version "2.1.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg== @@ -8769,7 +8818,7 @@ chokidar@^2.0.0, chokidar@^2.1.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.2.2: +chokidar@^3.0.0, chokidar@^3.2.2: version "3.3.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== @@ -8848,11 +8897,6 @@ circular-json@^0.3.1: resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== -circular-json@^0.5.5: - version "0.5.9" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" - integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ== - class-extend@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/class-extend/-/class-extend-0.1.2.tgz#8057a82b00f53f82a5d62c50ef8cffdec6fabc34" @@ -9317,13 +9361,6 @@ colorspace@1.1.x: color "3.0.x" text-hex "1.0.x" -combine-lists@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" - integrity sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y= - dependencies: - lodash "^4.5.0" - combined-stream@^1.0.5: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -9479,15 +9516,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.0, concat-stream@^1.4.7, concat-stream@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" - integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc= - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" @@ -9498,6 +9526,15 @@ concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^ readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^1.4.7, concat-stream@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc= + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-stream@~1.5.0: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" @@ -9873,7 +9910,7 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.5.7, core-js@^2.6.5, core-js@^2.6.9: +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -10206,18 +10243,6 @@ cson@5.1.0: requirefresh "^2.1.0" safefs "^4.1.0" -css-box-model@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.0.0.tgz#60142814f2b25be00c4aac65ea1a55a531b18922" - integrity sha512-MGipbCM6/HGmsOwN6Enq1OvNKy8H5Q1XKoyBszxwv2efly7ZVg+HcFILX8O6S0xfj27l1+6P7FyCjcQ90m5HBQ== - -css-box-model@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.1.1.tgz#c9fd8e7a8b1d59d41d6812fd1765433f671b2ee0" - integrity sha512-ZxbuLFeAPEDb0wPbGfT7783Vb00MVAkvOlMKwr0kA2PD5EGxk6P3MAhedvVuyVJCWb54bb+6HQ7pdPYENf8AZw== - dependencies: - tiny-invariant "^1.0.3" - css-box-model@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0" @@ -10421,13 +10446,24 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" -cypress@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.2.0.tgz#45673fb648b1a77b9a78d73e58b89ed05212d243" - integrity sha512-8LdreL91S/QiTCLYLNbIjLL8Ht4fJmu/4HGLxUI20Tc7JSfqEfCmXELrRfuPT0kjosJwJJZacdSji9XSRkPKUw== +cypress@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.4.1.tgz#f5aa1aa5f328f1299bff328103f7cbad89e80f29" + integrity sha512-LcskZ/PXRG9XTlEeeenKqz/KddT1x+7O7dqXsdKWPII01LxLNmNHIvHnlUqApchVbinJ5vir6J255CkELSeL0A== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" + "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" + "@types/blob-util" "1.3.3" + "@types/bluebird" "3.5.29" + "@types/chai" "4.2.7" + "@types/chai-jquery" "1.1.40" + "@types/jquery" "3.3.31" + "@types/lodash" "4.14.149" + "@types/minimatch" "3.0.3" + "@types/mocha" "5.2.7" + "@types/sinon" "7.5.1" + "@types/sinon-chai" "3.2.3" "@types/sizzle" "2.3.2" arch "2.1.1" bluebird "3.7.2" @@ -10441,7 +10477,7 @@ cypress@^4.2.0: eventemitter2 "4.1.2" execa "1.0.0" executable "4.1.1" - extract-zip "1.6.7" + extract-zip "1.7.0" fs-extra "8.1.0" getos "3.1.4" is-ci "2.0.0" @@ -10450,12 +10486,11 @@ cypress@^4.2.0: listr "0.14.3" lodash "4.17.15" log-symbols "3.0.0" - minimist "1.2.2" + minimist "1.2.5" moment "2.24.0" ospath "1.2.2" pretty-bytes "5.3.0" ramda "0.26.1" - request cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16 request-progress "3.0.0" supports-color "7.1.0" tmp "0.1.0" @@ -10764,10 +10799,10 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== -date-format@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" - integrity sha1-YV6CjiM90aubua4JUODOzPpuytg= +date-format@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" + integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== date-now@^0.1.4: version "0.1.4" @@ -13012,15 +13047,6 @@ exit@^0.1.2, exit@~0.1.1: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= -expand-braces@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" - integrity sha1-SIsdHSRRyz06axks/AMPRMWFX+o= - dependencies: - array-slice "^0.2.3" - array-unique "^0.2.1" - braces "^0.1.2" - expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -13034,14 +13060,6 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expand-range@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" - integrity sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ= - dependencies: - is-number "^0.1.1" - repeat-string "^0.2.2" - expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -13255,27 +13273,7 @@ extract-stack@^1.0.0: resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-1.0.0.tgz#b97acaf9441eea2332529624b732fc5a1c8165fa" integrity sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo= -extract-zip@1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" - integrity sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw= - dependencies: - concat-stream "1.6.0" - debug "2.6.9" - mkdirp "0.5.0" - yauzl "2.4.1" - -extract-zip@1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= - dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" - -extract-zip@^1.6.6, extract-zip@^1.7.0: +extract-zip@1.7.0, extract-zip@^1.6.6, extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== @@ -14959,11 +14957,11 @@ gonzales-pe-sl@^4.2.3: minimist "1.1.x" gonzales-pe@^4.2.3: - version "4.2.4" - resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.4.tgz#356ae36a312c46fe0f1026dd6cb539039f8500d2" - integrity sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" + integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== dependencies: - minimist "1.1.x" + minimist "^1.2.5" good-listener@^1.2.2: version "1.2.2" @@ -15623,14 +15621,15 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.5.3, handlebars@^4.0.1, handlebars@^4.1.2: - version "4.5.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" - integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== +handlebars@4.7.6, handlebars@^4.0.1, handlebars@^4.1.2: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== dependencies: + minimist "^1.2.5" neo-async "^2.6.0" - optimist "^0.6.1" source-map "^0.6.1" + wordwrap "^1.0.0" optionalDependencies: uglify-js "^3.1.4" @@ -16388,16 +16387,14 @@ idx@^2.5.6: resolved "https://registry.yarnpkg.com/idx/-/idx-2.5.6.tgz#1f824595070100ae9ad585c86db08dc74f83a59d" integrity sha512-WFXLF7JgPytbMgelpRY46nHz5tyDcedJ76pLV+RJWdb8h33bxFq4bdZau38DhNSzk5eVniBf1K3jwfK+Lb5nYA== -iedriver@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/iedriver/-/iedriver-3.14.1.tgz#447c49be83c62d3f2f158283d58ccf7b35002be8" - integrity sha512-YyCi703BGK7R37A8QlSe2B87xgwDGGoPqBrlXe4Q68o/MNLJrR53/IpTs6J1+KKk51MLiTbWa57N7P3KZ11tow== +iedriver@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/iedriver/-/iedriver-3.14.2.tgz#a19391ff123e21823ce0afe300e38b58a7dc79c4" + integrity sha512-vvFwfpOOZXmpXT/3Oa9SOFrr4uZNNUtBKPLRz7z8oZigvvIOokDiBlbImrd80q+rgjkmqUGi6a2NnpyCOAXnOw== dependencies: - adm-zip "^0.4.13" - extract-zip "1.6.6" + extract-zip "1.7.0" kew "~0.1.7" - md5-file "^1.1.4" - mkdirp "0.3.5" + mkdirp "0.5.4" npmconf "^2.1.3" request "^2.88.0" rimraf "~2.0.2" @@ -17330,11 +17327,6 @@ is-number-object@^1.0.4: resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== -is-number@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" - integrity sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY= - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -17599,7 +17591,7 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= -is-valid-path@0.1.1: +is-valid-path@0.1.1, is-valid-path@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df" integrity sha1-EQ+f90w39mPh7HkV60UfLbk6yd8= @@ -17660,10 +17652,10 @@ isbinaryfile@4.0.2: resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.2.tgz#bfc45642da645681c610cca831022e30af426488" integrity sha512-C3FSxJdNrEr2F4z6uFtNzECDM5hXk+46fxaa+cwBe5/XrWSmzdG8DDgyjfX6/NRdBB21q2JXuRAzPCUs+fclnQ== -isbinaryfile@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" - integrity sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE= +isbinaryfile@^4.0.2: + version "4.0.6" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" + integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== isemail@3.x.x: version "3.1.4" @@ -18861,51 +18853,47 @@ karma-junit-reporter@1.2.0: path-is-absolute "^1.0.0" xmlbuilder "8.2.2" -karma-mocha@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf" - integrity sha1-7qrH/8DiAetjxGdEDStpx883eL8= +karma-mocha@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.0.tgz#ad6b56b6a72e9b191e4c432dd30f4a44fc2435bc" + integrity sha512-qiZkZDJnn2kb9t2m4LoM4cYJHJVPoxvAYYe0B+go5s+A/3vc/3psUT05zW4yFz4vT0xHf+XzTTery8zdr8GWgA== dependencies: - minimist "1.2.0" + minimist "^1.2.3" karma-safari-launcher@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz#96982a2cc47d066aae71c553babb28319115a2ce" integrity sha1-lpgqLMR9BmquccVTursoMZEVos4= -karma@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/karma/-/karma-3.1.4.tgz#3890ca9722b10d1d14b726e1335931455788499e" - integrity sha512-31Vo8Qr5glN+dZEVIpnPCxEGleqE0EY6CtC2X9TagRV3rRQ3SNrvfhddICkJgUK3AgqpeKSZau03QumTGhGoSw== +karma@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/karma/-/karma-5.0.2.tgz#e404373dac6e3fa08409ae4d9eda7d83adb43ee5" + integrity sha512-RpUuCuGJfN3WnjYPGIH+VBF8023Lfm3TQH6D1kcNL+FxtEPc2UUz/nVjbVAGXH4Pm+Q7FVOAQjdAeFUpXpQ3IA== dependencies: - bluebird "^3.3.0" body-parser "^1.16.1" - chokidar "^2.0.3" + braces "^3.0.2" + chokidar "^3.0.0" colors "^1.1.0" - combine-lists "^1.0.0" connect "^3.6.0" - core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" - expand-braces "^0.1.1" flatted "^2.0.0" glob "^7.1.1" graceful-fs "^4.1.2" http-proxy "^1.13.0" - isbinaryfile "^3.0.0" - lodash "^4.17.5" - log4js "^3.0.0" + isbinaryfile "^4.0.2" + lodash "^4.17.14" + log4js "^4.0.0" mime "^2.3.1" minimatch "^3.0.2" - optimist "^0.6.1" qjobs "^1.1.4" range-parser "^1.2.0" rimraf "^2.6.0" - safe-buffer "^5.0.1" socket.io "2.1.1" source-map "^0.6.1" tmp "0.0.33" - useragent "2.3.0" + ua-parser-js "0.7.21" + yargs "^15.3.1" kdbush@^3.0.0: version "3.0.0" @@ -19770,7 +19758,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -19831,16 +19819,16 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" -log4js@^3.0.0: - version "3.0.6" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.6.tgz#e6caced94967eeeb9ce399f9f8682a4b2b28c8ff" - integrity sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ== +log4js@^4.0.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5" + integrity sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw== dependencies: - circular-json "^0.5.5" - date-format "^1.2.0" - debug "^3.1.0" - rfdc "^1.1.2" - streamroller "0.7.0" + date-format "^2.0.0" + debug "^4.1.1" + flatted "^2.0.0" + rfdc "^1.1.4" + streamroller "^1.0.6" logform@^2.1.1: version "2.1.2" @@ -20197,11 +20185,6 @@ material-colors@^1.2.1: resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.5.tgz#5292593e6754cb1bcc2b98030e4e0d6a3afc9ea1" integrity sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE= -md5-file@^1.1.4: - version "1.1.10" - resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-1.1.10.tgz#d8f4fce76c92cb20b7d143a59f58ca49b4cf3174" - integrity sha1-2PT852ySyyC30UOln1jKSbTPMXQ= - md5.js@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" @@ -20305,21 +20288,11 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^1.1.0" -memoize-one@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.2.tgz#3fb8db695aa14ab9c0f1644e1585a8806adc1aee" - integrity sha512-ucx2DmXTeZTsS4GPPUZCbULAN7kdPT1G+H49Y34JjbQ5ESc6OGhVxKvb1iKhr9v19ZB9OtnHwNnhUnNR/7Wteg== - memoize-one@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.1.tgz#35a709ffb6e5f0cb79f9679a96f09ec3a35addfa" integrity sha512-S3plzyksLOSF4pkf1Xlb7mA8ZRKZlgp3ebg7rULbfwPT8Ww7uZz5CbLgRKaR92GeXpsNiFbfCRWf/uOrCYIbRg== -memoize-one@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.0.2.tgz#6aba5276856d72fb44ead3efab86432f94ba203d" - integrity sha512-o7lldN4fs/axqctc03NF+PMhd2veRrWeJ2n2GjEzUPBD4F9rmNg4A+bQCACIzwjHJEXuYv4aFFMaH35KZfHUrw== - memoize-one@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" @@ -20682,21 +20655,11 @@ minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.2.tgz#b00a00230a1108c48c169e69a291aafda3aacd63" - integrity sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA== - -minimist@^1.2.5: +minimist@1.2.5, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - minimost@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/minimost/-/minimost-1.0.0.tgz#1d07954aa0268873408b95552fbffc5977dfc78b" @@ -20796,18 +20759,6 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.3.5, mkdirp@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" - integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= - -mkdirp@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" - integrity sha1-HXMHam35hs2TROFecfzAWkyavxI= - dependencies: - minimist "0.0.8" - mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -20822,13 +20773,18 @@ mkdirp@0.5.3: dependencies: minimist "^1.2.5" -mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.4, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== dependencies: minimist "^1.2.5" +mkdirp@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" + integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -22161,14 +22117,6 @@ optimism@^0.9.0: dependencies: "@wry/context" "^0.4.0" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - optional-js@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.1.1.tgz#c2dc519ad119648510b4d241dbb60b1167c36a46" @@ -22585,10 +22533,10 @@ pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== -papaparse@^4.6.3: - version "4.6.3" - resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.6.3.tgz#742e5eaaa97fa6c7e1358d2934d8f18f44aee781" - integrity sha512-LRq7BrHC2kHPBYSD50aKuw/B/dGcg29omyJbKWY3KsYUZU69RKwaBHu13jGmCYBtOc4odsLCrFyk6imfyNubJQ== +papaparse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.2.0.tgz#97976a1b135c46612773029153dc64995caa3b7b" + integrity sha512-ylq1wgUSnagU+MKQtNeVqrPhZuMYBvOSL00DHycFTCxownF95gpLAk1HiHdUW77N8yxRq1qHXLdlIPyBSG9NSA== parallel-transform@^1.1.0: version "1.1.0" @@ -23618,15 +23566,6 @@ prop-types@15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@15.6.1: - version "15.6.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" - integrity sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ== - dependencies: - fbjs "^0.8.16" - loose-envify "^1.3.1" - object-assign "^4.1.1" - prop-types@15.7.2, prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" @@ -24251,20 +24190,6 @@ react-apollo@^2.1.4: lodash "^4.17.10" prop-types "^15.6.0" -react-beautiful-dnd@^10.1.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-10.1.1.tgz#d753088d77d7632e77cf8a8935fafcffa38f574b" - integrity sha512-TdE06Shfp56wm28EzjgC56EEMgGI5PDHejJ2bxuAZvZr8CVsbksklsJC06Hxf0MSL7FHbflL/RpkJck9isuxHg== - dependencies: - "@babel/runtime-corejs2" "^7.4.2" - css-box-model "^1.1.1" - memoize-one "^5.0.1" - prop-types "^15.6.1" - raf-schd "^4.0.0" - react-redux "^5.0.7" - redux "^4.0.1" - tiny-invariant "^1.0.4" - react-beautiful-dnd@^12.2.0: version "12.2.0" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.2.0.tgz#e5f6222f9e7934c6ed4ee09024547f9e353ae423" @@ -24278,20 +24203,18 @@ react-beautiful-dnd@^12.2.0: redux "^4.0.4" use-memo-one "^1.1.1" -react-beautiful-dnd@^8.0.7: - version "8.0.7" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-8.0.7.tgz#2cc7ba62bffe08d3dad862fd8f48204440901b43" - integrity sha512-j2cClhKuACXp/KcG+YXSrVxZ7AQl13dG9X+ojstR6H2G0yoA+1GZn/O147PWVVScmfk/mSt60GNseH7vjae7vQ== +react-beautiful-dnd@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" + integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg== dependencies: - "@babel/runtime" "7.0.0-beta.54" - css-box-model "^1.0.0" - memoize-one "^4.0.0" - prop-types "15.6.1" - raf-schd "^4.0.0" - react-motion "^0.5.2" - react-redux "^5.0.7" - redux "^4.0.0" - tiny-invariant "^0.0.3" + "@babel/runtime" "^7.8.4" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.1.1" + redux "^4.0.4" + use-memo-one "^1.1.1" react-clientside-effect@^1.2.0: version "1.2.0" @@ -24658,15 +24581,6 @@ react-motion@^0.4.8: prop-types "^15.5.8" raf "^3.1.0" -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" - react-onclickoutside@^6.5.0: version "6.7.1" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz#6a5b5b8b4eae6b776259712c89c8a2b36b17be93" @@ -24733,7 +24647,7 @@ react-portal@^3.2.0: dependencies: prop-types "^15.5.8" -react-redux@^5.0.7, react-redux@^5.1.2: +react-redux@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57" integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q== @@ -25194,7 +25108,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0": string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -25456,14 +25370,6 @@ redux@^4.0.0: loose-envify "^1.1.0" symbol-observable "^1.2.0" -redux@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" - integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg== - dependencies: - loose-envify "^1.4.0" - symbol-observable "^1.2.0" - redux@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796" @@ -25756,11 +25662,6 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" integrity sha1-7wiaF40Ug7quTZPrmLT55OEdmQo= -repeat-string@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" - integrity sha1-x6jTI2BoNiBZp+RlH8aITosftK4= - repeat-string@^1.5.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -25963,31 +25864,6 @@ request@^2.87.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16: - version "2.88.1" - resolved "https://codeload.github.com/cypress-io/request/tar.gz/b5af0d1fa47eec97ba980cde90a13e69a2afcd16" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-ancestors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/require-ancestors/-/require-ancestors-1.0.0.tgz#807831f8f8081fb12863da81ddb15c8f2a73a004" @@ -26321,10 +26197,10 @@ rework@1.0.1: convert-source-map "^0.3.3" css "^2.0.0" -rfdc@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.2.tgz#e6e72d74f5dc39de8f538f65e00c36c18018e349" - integrity sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA== +rfdc@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" + integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== right-align@^0.1.1: version "0.1.3" @@ -27039,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" @@ -27991,15 +27874,16 @@ stream-spigot@~2.1.2: dependencies: readable-stream "~1.1.0" -streamroller@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" - integrity sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ== +streamroller@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9" + integrity sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg== dependencies: - date-format "^1.2.0" - debug "^3.1.0" - mkdirp "^0.5.1" - readable-stream "^2.3.0" + async "^2.6.2" + date-format "^2.0.0" + debug "^3.2.6" + fs-extra "^7.0.1" + lodash "^4.17.14" strict-uri-encode@^1.0.0: version "1.1.0" @@ -29092,12 +28976,7 @@ tiny-inflate@^1.0.0, tiny-inflate@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.2.tgz#93d9decffc8805bd57eae4310f0b745e9b6fb3a7" integrity sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c= -tiny-invariant@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-0.0.3.tgz#4c7283c950e290889e9e94f64d3586ec9156cf44" - integrity sha512-SA2YwvDrCITM9fTvHTHRpq9W6L2fBsClbqm3maT5PZux4Z73SPPDYwJMtnoWh6WMgmCkJij/LaOlWiqJqFMK8g== - -tiny-invariant@^1.0.2, tiny-invariant@^1.0.3, tiny-invariant@^1.0.4: +tiny-invariant@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" integrity sha512-lMhRd/djQJ3MoaHEBrw8e2/uM4rs9YMNk0iOr8rHQ0QdbM7D4l0gFl3szKdeixrlyfm9Zqi4dxHCM2qVG8ND5g== @@ -30161,10 +30040,10 @@ typings-tester@^0.3.2: dependencies: commander "^2.12.2" -ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: - version "0.7.19" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" - integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== +ua-parser-js@0.7.21, ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: + version "0.7.21" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" + integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" @@ -30733,7 +30612,7 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@2.3.0, useragent@^2.3.0: +useragent@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== @@ -32011,11 +31890,6 @@ wordwrap@^1.0.0, wordwrap@~1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" @@ -32635,13 +32509,6 @@ yauzl@2.10.0, yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= - dependencies: - fd-slicer "~1.0.1" - yauzl@^2.4.2: version "2.9.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.9.1.tgz#a81981ea70a57946133883f029c5821a89359a7f"